speaker-calibration 2.1.16 → 2.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,10 +48,16 @@
48
48
  </div>
49
49
  <div class="row">
50
50
  <div class="form-check form-switch">
51
- <input class="form-check-input" type="checkbox" id="flexSwitchCheckIR" checked />
51
+ <input class="form-check-input" type="checkbox" id="flexSwitchCheckIR" />
52
52
  <label class="form-check-label" for="flexSwitchCheckIR">Impulse Response</label>
53
53
  </div>
54
54
  </div>
55
+ <div class="row">
56
+ <div class="form-check form-switch">
57
+ <input class="form-check-input" type="checkbox" id="flexSwitchCheckCombo" checked />
58
+ <label class="form-check-label" for="flexSwitchCheckCombo">Combination</label>
59
+ </div>
60
+ </div>
55
61
  <div class="row">
56
62
  <div class="form-check form-switch">
57
63
  <input class="form-check-input" type="checkbox" id="flexSwitchCheckDownload" />
@@ -1,13 +1,14 @@
1
1
  window.onload = () => {
2
2
  const flexSwitchCheckIR = document.getElementById('flexSwitchCheckIR');
3
3
  const flexSwitchCheckVolume = document.getElementById('flexSwitchCheckVolume');
4
+ const flexSwitchCheckCombo = document.getElementById('flexSwitchCheckCombo');
4
5
  const previousCaptureCSV = document.getElementById('previous-capture-csv');
5
6
  const iirCSV = document.getElementById('iir-csv');
6
7
  const playAndRecord = document.getElementById('flexSwitchPlayAndRecord');
7
8
  const sendToServerButton = document.getElementById('sendToServerButton');
8
9
  const wavFile = document.getElementById('wav-file');
9
10
 
10
- const {Speaker, VolumeCalibration, ImpulseResponseCalibration} = speakerCalibrator;
11
+ const {Speaker, VolumeCalibration, ImpulseResponseCalibration, CombinationCalibration} = speakerCalibrator;
11
12
 
12
13
  const normalize = (min, max) => {
13
14
  var delta = max - min;
@@ -147,10 +148,17 @@ window.onload = () => {
147
148
 
148
149
  flexSwitchCheckIR.onchange = () => {
149
150
  flexSwitchCheckVolume.checked = !flexSwitchCheckIR.checked;
151
+ flexSwitchCheckCombo.checked = !flexSwitchCheckIR.checked
150
152
  };
151
153
 
152
154
  flexSwitchCheckVolume.onchange = () => {
153
155
  flexSwitchCheckIR.checked = !flexSwitchCheckVolume.checked;
156
+ flexSwitchCheckCombo.checked = !flexSwitchCheckVolume.checked;
157
+ };
158
+
159
+ flexSwitchCheckCombo.onchange = () => {
160
+ flexSwitchCheckIR.checked = !flexSwitchCheckCombo.checked;
161
+ flexSwitchCheckVolume.checked = !flexSwitchCheckCombo.checked;
154
162
  };
155
163
 
156
164
  const useSpeakerCalibrator = async (calibrationLevel = 0, iir = null) => {
@@ -202,6 +210,33 @@ window.onload = () => {
202
210
  }
203
211
  };
204
212
 
213
+ const runCombinationCalibration = async calibrationLevel => {
214
+ const calibratorParams = {
215
+ numCaptures: document.getElementById('numCapturesInput').value,
216
+ numMLSPerCapture: document.getElementById('numMLSPerCaptureInput').value,
217
+ mlsOrder: document.getElementById('mlsOrder').value,
218
+ download: document.getElementById('flexSwitchCheckDownload').checked,
219
+ };
220
+
221
+ const calibrator = new CombinationCalibration(calibratorParams);
222
+
223
+ calibrator.on('update', ({message, ...rest}) => {
224
+ updateTarget.innerHTML = message;
225
+ });
226
+
227
+ try {
228
+ if (calibrationLevel == 0) {
229
+ invertedIR = await Speaker.startCalibration(speakerParameters, calibrator);
230
+ console.log({invertedIR});
231
+ await useIRResult(invertedIR);
232
+ } else {
233
+ await Speaker.testIIR(speakerParameters, calibrator, iir);
234
+ }
235
+ } catch (err) {
236
+ calibrationResult.innerText = `${err.name}: ${err.message}`;
237
+ }
238
+ };
239
+
205
240
  const runVolumeCalibration = async () => {
206
241
  const calibrator = new VolumeCalibration({});
207
242
  calibrator.on('update', ({message, ...rest}) => {
@@ -218,8 +253,10 @@ window.onload = () => {
218
253
 
219
254
  if (flexSwitchCheckIR.checked) {
220
255
  runImpulseResponseCalibration(calibrationLevel);
221
- } else {
256
+ } else if (flexSwitchCheckVolume.checked){
222
257
  runVolumeCalibration();
258
+ } else {
259
+ runCombinationCalibration(calibrationLevel);
223
260
  }
224
261
  };
225
262
 
package/dist/main.js CHANGED
@@ -697,7 +697,7 @@ eval("function hex2rgba (hex) {\n if (typeof hex === 'number') {\n hex = hex
697
697
  /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
698
698
 
699
699
  "use strict";
700
- eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"Listener\": function() { return /* reexport safe */ _peer_connection_listener__WEBPACK_IMPORTED_MODULE_0__[\"default\"]; },\n/* harmony export */ \"Speaker\": function() { return /* reexport safe */ _peer_connection_speaker__WEBPACK_IMPORTED_MODULE_1__[\"default\"]; },\n/* harmony export */ \"VolumeCalibration\": function() { return /* reexport safe */ _tasks_volume_volume__WEBPACK_IMPORTED_MODULE_2__[\"default\"]; },\n/* harmony export */ \"ImpulseResponseCalibration\": function() { return /* reexport safe */ _tasks_impulse_response_impulseResponse__WEBPACK_IMPORTED_MODULE_3__[\"default\"]; },\n/* harmony export */ \"UnsupportedDeviceError\": function() { return /* reexport safe */ _peer_connection_peerErrors__WEBPACK_IMPORTED_MODULE_4__.UnsupportedDeviceError; },\n/* harmony export */ \"MissingSpeakerIdError\": function() { return /* reexport safe */ _peer_connection_peerErrors__WEBPACK_IMPORTED_MODULE_4__.MissingSpeakerIdError; },\n/* harmony export */ \"CalibrationTimedOutError\": function() { return /* reexport safe */ _peer_connection_peerErrors__WEBPACK_IMPORTED_MODULE_4__.CalibrationTimedOutError; }\n/* harmony export */ });\n/* harmony import */ var _peer_connection_listener__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./peer-connection/listener */ \"./src/peer-connection/listener.js\");\n/* harmony import */ var _peer_connection_speaker__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./peer-connection/speaker */ \"./src/peer-connection/speaker.js\");\n/* harmony import */ var _tasks_volume_volume__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./tasks/volume/volume */ \"./src/tasks/volume/volume.js\");\n/* harmony import */ var _tasks_impulse_response_impulseResponse__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./tasks/impulse-response/impulseResponse */ \"./src/tasks/impulse-response/impulseResponse.js\");\n/* harmony import */ var _peer_connection_peerErrors__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./peer-connection/peerErrors */ \"./src/peer-connection/peerErrors.js\");\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\n\n//# sourceURL=webpack://speakerCalibrator/./src/main.js?");
700
+ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"Listener\": function() { return /* reexport safe */ _peer_connection_listener__WEBPACK_IMPORTED_MODULE_0__[\"default\"]; },\n/* harmony export */ \"Speaker\": function() { return /* reexport safe */ _peer_connection_speaker__WEBPACK_IMPORTED_MODULE_1__[\"default\"]; },\n/* harmony export */ \"VolumeCalibration\": function() { return /* reexport safe */ _tasks_volume_volume__WEBPACK_IMPORTED_MODULE_2__[\"default\"]; },\n/* harmony export */ \"ImpulseResponseCalibration\": function() { return /* reexport safe */ _tasks_impulse_response_impulseResponse__WEBPACK_IMPORTED_MODULE_3__[\"default\"]; },\n/* harmony export */ \"UnsupportedDeviceError\": function() { return /* reexport safe */ _peer_connection_peerErrors__WEBPACK_IMPORTED_MODULE_5__.UnsupportedDeviceError; },\n/* harmony export */ \"MissingSpeakerIdError\": function() { return /* reexport safe */ _peer_connection_peerErrors__WEBPACK_IMPORTED_MODULE_5__.MissingSpeakerIdError; },\n/* harmony export */ \"CalibrationTimedOutError\": function() { return /* reexport safe */ _peer_connection_peerErrors__WEBPACK_IMPORTED_MODULE_5__.CalibrationTimedOutError; },\n/* harmony export */ \"CombinationCalibration\": function() { return /* reexport safe */ _tasks_combination_combination__WEBPACK_IMPORTED_MODULE_4__[\"default\"]; }\n/* harmony export */ });\n/* harmony import */ var _peer_connection_listener__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./peer-connection/listener */ \"./src/peer-connection/listener.js\");\n/* harmony import */ var _peer_connection_speaker__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./peer-connection/speaker */ \"./src/peer-connection/speaker.js\");\n/* harmony import */ var _tasks_volume_volume__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./tasks/volume/volume */ \"./src/tasks/volume/volume.js\");\n/* harmony import */ var _tasks_impulse_response_impulseResponse__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./tasks/impulse-response/impulseResponse */ \"./src/tasks/impulse-response/impulseResponse.js\");\n/* harmony import */ var _tasks_combination_combination__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./tasks/combination/combination */ \"./src/tasks/combination/combination.js\");\n/* harmony import */ var _peer_connection_peerErrors__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./peer-connection/peerErrors */ \"./src/peer-connection/peerErrors.js\");\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\n\n//# sourceURL=webpack://speakerCalibrator/./src/main.js?");
701
701
 
702
702
  /***/ }),
703
703
 
@@ -789,6 +789,28 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _myE
789
789
 
790
790
  /***/ }),
791
791
 
792
+ /***/ "./src/tasks/combination/combination.js":
793
+ /*!**********************************************!*\
794
+ !*** ./src/tasks/combination/combination.js ***!
795
+ \**********************************************/
796
+ /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
797
+
798
+ "use strict";
799
+ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _audioCalibrator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../audioCalibrator */ \"./src/tasks/audioCalibrator.js\");\n/* harmony import */ var _mlsGen_mlsGenInterface__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./mlsGen/mlsGenInterface */ \"./src/tasks/combination/mlsGen/mlsGenInterface.js\");\n/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../utils */ \"./src/utils.js\");\n\r\n\r\n\r\n\r\n\r\n/**\r\n *\r\n */\r\nclass Combination extends _audioCalibrator__WEBPACK_IMPORTED_MODULE_0__[\"default\"] {\r\n /**\r\n * Default constructor. Creates an instance with any number of paramters passed or the default parameters defined here.\r\n *\r\n * @param {Object<boolean, number, number, number>} calibratorParams - paramter object\r\n * @param {boolean} [calibratorParams.download = false] - boolean flag to download captures\r\n * @param {number} [calibratorParams.mlsOrder = 18] - order of the MLS to be generated\r\n * @param {number} [calibratorParams.numCaptures = 5] - number of captures to perform\r\n * @param {number} [calibratorParams.numMLSPerCapture = 4] - number of bursts of MLS per capture\r\n */\r\n constructor({download = false, mlsOrder = 18, numCaptures = 3, numMLSPerCapture = 4, lowHz = 20, highHz = 10000}) {\r\n super(numCaptures, numMLSPerCapture);\r\n this.#mlsOrder = parseInt(mlsOrder, 10);\r\n this.#P = 2 ** mlsOrder - 1;\r\n this.#download = download;\r\n this.#mls = [];\r\n this.#lowHz = lowHz;\r\n this.#highHz = highHz;\r\n }\r\n\r\n /** @private */\r\n stepNum = 0;\r\n\r\n /** @private */\r\n totalSteps = 25;\r\n\r\n /** @private */\r\n #download;\r\n\r\n /** @private */\r\n #mlsGenInterface;\r\n\r\n /** @private */\r\n #mlsBufferView;\r\n\r\n /** @private */\r\n invertedImpulseResponse = null;\r\n\r\n /** @private */\r\n impulseResponses = [];\r\n\r\n /** @private */\r\n #mlsOrder;\r\n\r\n /** @private */\r\n #lowHz;\r\n\r\n /** @private */\r\n #highHz;\r\n\r\n /** @private */\r\n #mls;\r\n\r\n /** @private */\r\n #P;\r\n\r\n /** @private */\r\n #audioContext;\r\n\r\n /** @private */\r\n TAPER_SECS = 5;\r\n\r\n /** @private */\r\n offsetGainNode;\r\n\r\n /** @private */\r\n convolution;\r\n ////////////////////////volume\r\n /** @private */\r\n #CALIBRATION_TONE_FREQUENCY = 1000; // Hz\r\n\r\n /** @private */\r\n #CALIBRATION_TONE_TYPE = 'sine';\r\n\r\n /** @private */\r\n #CALIBRATION_TONE_DURATION = 5; // seconds\r\n\r\n /** @private */\r\n outDBSPL = null;\r\n THD = null;\r\n outDBSPL1000 = null;\r\n\r\n /** @private */\r\n TAPER_SECS = 0.010; // seconds\r\n\r\n /** .\r\n * .\r\n * .\r\n * Sends all the computed impulse responses to the backend server for processing\r\n *\r\n * @returns sets the resulting inverted impulse response to the class property\r\n * @example\r\n */\r\n sendImpulseResponsesToServerForProcessing = async () => {\r\n const computedIRs = await Promise.all(this.impulseResponses);\r\n const filteredComputedIRs = computedIRs.filter(element => {\r\n return element != undefined;\r\n });\r\n const mls = this.#mls;\r\n const lowHz = this.#lowHz;\r\n const highHz = this.#highHz;\r\n this.stepNum += 1;\r\n this.emit('update', {message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: computing the IIR...`});\r\n return this.pyServerAPI\r\n .getInverseImpulseResponse({\r\n payload: filteredComputedIRs.slice(0, this.numCaptures),\r\n mls,\r\n lowHz,\r\n highHz\r\n })\r\n .then(res => {\r\n console.log(res);\r\n this.stepNum += 1;\r\n this.emit('update', {message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: done computing the IIR...`});\r\n this.invertedImpulseResponse = res[\"iir\"];\r\n this.convolution = res[\"convolution\"];\r\n })\r\n .catch(err => {\r\n // this.emit('InvertedImpulseResponse', {res: false});\r\n console.error(err);\r\n });\r\n };\r\n\r\n /** .\r\n * .\r\n * .\r\n * Sends the recorded signal, or a given csv string of a signal, to the back end server for processing\r\n *\r\n * @param {<array>String} signalCsv - Optional csv string of a previously recorded signal, if given, this signal will be processed\r\n * @example\r\n */\r\n sendRecordingToServerForProcessing = signalCsv => {\r\n const allSignals = this.getAllRecordedSignals();\r\n const numSignals = allSignals.length;\r\n const mls = this.#mls;\r\n const payload =\r\n signalCsv && signalCsv.length > 0 ? (0,_utils__WEBPACK_IMPORTED_MODULE_2__.csvToArray)(signalCsv) : allSignals[numSignals - 1];\r\n console.log('sending rec');\r\n this.stepNum += 1;\r\n this.emit('update', {message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: computing the IR of the last recording...`});\r\n this.impulseResponses.push(\r\n this.pyServerAPI\r\n .getImpulseResponse({\r\n sampleRate: this.sourceSamplingRate || 96000,\r\n payload,\r\n mls,\r\n P: this.#P,\r\n })\r\n .then(res => {\r\n if (this.numSuccessfulCaptured < this.numCaptures) {\r\n this.numSuccessfulCaptured += 1;\r\n console.log(\"num succ capt: \" + this.numSuccessfulCaptured);\r\n this.stepNum += 1;\r\n this.emit('update', {\r\n message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: ${this.numSuccessfulCaptured}/${this.numCaptures} IRs computed...`,\r\n });\r\n }\r\n return res;\r\n })\r\n .catch(err => {\r\n console.error(err);\r\n })\r\n );\r\n };\r\n\r\n /**\r\n * Passed to the calibration steps function, awaits the desired amount of seconds to capture the desired number\r\n * of MLS periods defined in the constructor.\r\n *\r\n * @example\r\n */\r\n #awaitDesiredMLSLength = async () => {\r\n // seconds per MLS = P / SR\r\n // await N * P / SR\r\n this.stepNum += 1;\r\n this.emit('update', {\r\n message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: sampling the calibration signal...`,\r\n });\r\n await (0,_utils__WEBPACK_IMPORTED_MODULE_2__.sleep)((this.#P / this.sourceSamplingRate) * this.numMLSPerCapture);\r\n };\r\n\r\n /** .\r\n * .\r\n * .\r\n * Passed to the calibration steps function, awaits the onset of the signal to ensure a steady state\r\n *\r\n * @example\r\n */\r\n #awaitSignalOnset = async () => {\r\n this.stepNum += 1;\r\n this.emit('update', {\r\n message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: waiting for the signal to stabilize...`,\r\n });\r\n await (0,_utils__WEBPACK_IMPORTED_MODULE_2__.sleep)(this.TAPER_SECS);\r\n };\r\n\r\n /**\r\n * Called immediately after a recording is captured. Used to process the resulting signal\r\n * whether by sending the result to a server or by computing a result locally.\r\n *\r\n * @example\r\n */\r\n #afterMLSRecord = () => {\r\n console.log('after record');\r\n this.sendRecordingToServerForProcessing();\r\n };\r\n\r\n #afterMLSwIIRRecord = () => {\r\n if (this.numSuccessfulCaptured < this.numCaptures) {\r\n this.numSuccessfulCaptured += 1;\r\n this.stepNum += 1;\r\n this.emit('update', {\r\n message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: ${this.numSuccessfulCaptured} recordings of convolved MLS captured`,\r\n });\r\n }\r\n };\r\n\r\n /** .\r\n * .\r\n * .\r\n * Created an S Curver Buffer to taper the signal onset\r\n *\r\n * @param {*} length\r\n * @param {*} phase\r\n * @returns\r\n * @example\r\n */\r\n static createSCurveBuffer = (length, phase) => {\r\n const curve = new Float32Array(length);\r\n let i;\r\n for (i = 0; i < length; i += 1) {\r\n // scale the curve to be between 0-1\r\n curve[i] = Math.sin((Math.PI * i) / length - phase) / 2 + 0.5;\r\n }\r\n return curve;\r\n };\r\n\r\n static createInverseSCurveBuffer = (length, phase) => {\r\n const curve = new Float32Array(length);\r\n let i;\r\n let j = length - 1;\r\n for (i = 0; i < length; i += 1) {\r\n // scale the curve to be between 0-1\r\n curve[i] = Math.sin((Math.PI * j) / length - phase) / 2 + 0.5;\r\n j -= 1;\r\n }\r\n return curve;\r\n };\r\n\r\n /**\r\n * Construct a Calibration Node with the calibration parameters.\r\n *\r\n * @param CALIBRATION_TONE_FREQUENCY\r\n * @private\r\n * @example\r\n */\r\n #createPureTonenNode = CALIBRATION_TONE_FREQUENCY => {\r\n const audioContext = this.makeNewSourceAudioContext();\r\n const oscilator = audioContext.createOscillator();\r\n const gainNode = audioContext.createGain();\r\n\r\n oscilator.frequency.value = CALIBRATION_TONE_FREQUENCY;\r\n oscilator.type = 'sine';\r\n gainNode.gain.value = 0.04;\r\n\r\n oscilator.connect(gainNode);\r\n gainNode.connect(audioContext.destination);\r\n\r\n this.addCalibrationNode(oscilator);\r\n };\r\n\r\n /**\r\n * Construct a Calibration Node with the calibration parameters.\r\n *\r\n * @param dataBuffer\r\n * @private\r\n * @example\r\n */\r\n #createCalibrationNodeFromBuffer = dataBuffer => {\r\n const audioContext = this.makeNewSourceAudioContext();\r\n const buffer = audioContext.createBuffer(\r\n 1, // number of channels\r\n dataBuffer.length,\r\n audioContext.sampleRate // sample rate\r\n );\r\n\r\n const data = buffer.getChannelData(0); // get data\r\n // fill the buffer with our data\r\n try {\r\n for (let i = 0; i < dataBuffer.length; i += 1) {\r\n data[i] = dataBuffer[i]*.1;\r\n }\r\n } catch (error) {\r\n console.error(error);\r\n }\r\n console.log(\"mls second, same?\");\r\n console.log(data);\r\n const onsetGainNode = audioContext.createGain();\r\n this.offsetGainNode = audioContext.createGain();\r\n const source = audioContext.createBufferSource();\r\n\r\n source.buffer = buffer;\r\n source.loop = true;\r\n source.connect(onsetGainNode);\r\n onsetGainNode.connect(this.offsetGainNode);\r\n this.offsetGainNode.connect(audioContext.destination);\r\n\r\n const onsetCurve = Combination.createSCurveBuffer(this.sourceSamplingRate, Math.PI / 2);\r\n onsetGainNode.gain.setValueCurveAtTime(onsetCurve, 0, this.TAPER_SECS);\r\n this.addCalibrationNode(source);\r\n };\r\n\r\n /**\r\n * Given a data buffer, creates the required calibration node\r\n *\r\n * @param {*} dataBufferArray\r\n * @example\r\n */\r\n #setCalibrationNodesFromBuffer = (dataBufferArray = [this.#mlsBufferView]) => {\r\n if (dataBufferArray.length === 1) {\r\n console.log('data buffer aray');\r\n console.log(dataBufferArray);\r\n this.#createCalibrationNodeFromBuffer(dataBufferArray[0]);\r\n } else {\r\n throw new Error('The length of the data buffer array must be 1');\r\n }\r\n\r\n \r\n };\r\n\r\n /**\r\n * function to put MLS filtered IIR data obtained from\r\n * python server into our audio buffer to be played aloud\r\n */\r\n #putInPythonConv = () => {\r\n const audioCtx = this.makeNewSourceAudioContextConvolved();\r\n const buffer = audioCtx.createBuffer(\r\n 1, // number of channels\r\n this.convolution.length,\r\n audioCtx.sampleRate // sample rate\r\n );\r\n\r\n const data = buffer.getChannelData(0); // get data\r\n // fill the buffer with our data\r\n try {\r\n for (let i = 0; i < this.convolution.length; i += 1) {\r\n data[i] = this.convolution[i];\r\n }\r\n } catch (error) {\r\n console.error(error);\r\n }\r\n\r\n const source = audioCtx.createBufferSource();\r\n\r\n source.buffer = buffer;\r\n source.loop = true;\r\n source.connect(audioCtx.destination);\r\n\r\n this.addCalibrationNodeConvolved(source);\r\n }\r\n \r\n /**\r\n * Creates an audio context and plays it for a few seconds.\r\n *\r\n * @private\r\n * @returns - Resolves when the audio is done playing.\r\n * @example\r\n */\r\n #playCalibrationAudio = () => {\r\n this.calibrationNodes[0].start(0);\r\n this.#mls = this.calibrationNodes[0].buffer.getChannelData(0);\r\n this.stepNum += 1;\r\n this.emit('update', {message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: playing the calibration tone...`});\r\n }; \r\n\r\n\r\n #playCalibrationAudioConvolved = () => {\r\n this.calibrationNodesConvolved[0].start(0);\r\n this.stepNum += 1;\r\n this.emit('update',{message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: playing the convolved calibration tone...`})\r\n }\r\n\r\n /** .\r\n * .\r\n * .\r\n * Stops the audio with tapered offset\r\n *\r\n * @example\r\n */\r\n #stopCalibrationAudio = () => {\r\n this.offsetGainNode.gain.setValueAtTime(\r\n this.offsetGainNode.gain.value,\r\n this.sourceAudioContext.currentTime\r\n );\r\n\r\n this.offsetGainNode.gain.setTargetAtTime(0, this.sourceAudioContext.currentTime, 0.5);\r\n this.calibrationNodes[0].stop(0);\r\n this.sourceAudioContext.close();\r\n this.stepNum += 1;\r\n this.emit('update', {message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: stopping the calibration tone...`});\r\n };\r\n\r\n #stopCalibrationAudioConvolved = () => {\r\n this.offsetGainNode.gain.setValueAtTime(\r\n this.offsetGainNode.gain.value,\r\n this.sourceAudioContextConvolved.currentTime\r\n );\r\n\r\n this.offsetGainNode.gain.setTargetAtTime(0, this.sourceAudioContextConvolved.currentTime, 0.5);\r\n //this.calibrationNodesConvolved[0].stop(0);\r\n console.log(\"right before closing volved audio context\");\r\n this.sourceAudioContextConvolved.close();\r\n this.stepNum += 1;\r\n this.emit('update', {message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: stopping the convolved calibration tone...`});\r\n\r\n }\r\n\r\n playMLSwithIIR = async (stream, iir) => {\r\n console.log('play mls with iir');\r\n this.invertedImpulseResponse = iir;\r\n // initialize the MLSGenInterface object with it's factory method\r\n \r\n await _mlsGen_mlsGenInterface__WEBPACK_IMPORTED_MODULE_1__[\"default\"].factory(\r\n this.#mlsOrder,\r\n this.sinkSamplingRate,\r\n this.sourceSamplingRate\r\n ).then(mlsGenInterface => {\r\n this.#mlsGenInterface = mlsGenInterface;\r\n this.#mlsBufferView = this.#mlsGenInterface.getMLS();\r\n });\r\n\r\n console.log('after mls factory'); //works up to here.\r\n console.log(this.#mls);\r\n // after intializating, start the calibration steps with garbage collection\r\n await this.#mlsGenInterface.withGarbageCollection([\r\n () =>\r\n this.calibrationSteps(\r\n stream,\r\n this.#playCalibrationAudioConvolved, // play audio func (required)\r\n this.#putInPythonConv, // before play func\r\n this.#awaitSignalOnset, // before record\r\n () => this.numSuccessfulCaptured < this.numCaptures,\r\n this.#awaitDesiredMLSLength, // during record\r\n this.#afterMLSwIIRRecord, // after record\r\n 'filtered'\r\n ),\r\n ]);\r\n };\r\n\r\n /**\r\n * Public method to start the calibration process. Objects intialized from webassembly allocate new memory\r\n * and must be manually freed. This function is responsible for intializing the MlsGenInterface,\r\n * and wrapping the calibration steps with a garbage collection safe gaurd.\r\n *\r\n * @public\r\n * @param stream - The stream of audio from the Listener.\r\n * @example\r\n */\r\n startCalibrationImpulseResponse = async stream => {\r\n // initialize the MLSGenInterface object with it's factory method\r\n await _mlsGen_mlsGenInterface__WEBPACK_IMPORTED_MODULE_1__[\"default\"].factory(\r\n this.#mlsOrder,\r\n this.sinkSamplingRate,\r\n this.sourceSamplingRate\r\n ).then(mlsGenInterface => {\r\n this.#mlsGenInterface = mlsGenInterface;\r\n this.#mlsBufferView = this.#mlsGenInterface.getMLS();\r\n });\r\n\r\n // after intializating, start the calibration steps with garbage collection\r\n await this.#mlsGenInterface.withGarbageCollection([\r\n () =>\r\n this.calibrationSteps(\r\n stream,\r\n this.#playCalibrationAudio, // play audio func (required)\r\n this.#setCalibrationNodesFromBuffer, // before play func\r\n this.#awaitSignalOnset, // before record\r\n () => this.numSuccessfulCaptured < this.numCaptures, // loop while true\r\n this.#awaitDesiredMLSLength, // during record\r\n this.#afterMLSRecord, // after record\r\n 'unfiltered'\r\n ),\r\n ]);\r\n\r\n this.#stopCalibrationAudio();\r\n\r\n // at this stage we've captured all the required signals,\r\n // and have received IRs for each one\r\n // so let's send all the IRs to the server to be converted to a single IIR\r\n await this.sendImpulseResponsesToServerForProcessing();\r\n\r\n this.numSuccessfulCaptured = 0;\r\n // debugging function, use to test the result of the IIR\r\n await this.playMLSwithIIR(stream, this.invertedImpulseResponse);\r\n this.#stopCalibrationAudioConvolved();\r\n\r\n let recs = this.getAllRecordedSignals();\r\n let conv_recs = this.getAllFilteredRecordedSignals();\r\n let unconv_rec = recs[0];\r\n let conv_rec = conv_recs[0];\r\n\r\n let results = await this.pyServerAPI\r\n .getPSD({\r\n unconv_rec,\r\n conv_rec,\r\n })\r\n .then(res => {\r\n return res;\r\n })\r\n .catch(err => {\r\n console.error(err);\r\n })\r\n\r\n let iir_and_plots = {\r\n \"iir\": this.invertedImpulseResponse,\r\n \"x_unconv\": results[\"x_unconv\"],\r\n \"y_unconv\": results[\"y_unconv\"],\r\n \"x_conv\": results[\"x_conv\"],\r\n \"y_conv\": results[\"y_conv\"]\r\n }\r\n if (this.#download) {\r\n this.downloadSingleUnfilteredRecording();\r\n this.downloadSingleFilteredRecording();\r\n (0,_utils__WEBPACK_IMPORTED_MODULE_2__.saveToCSV)(this.#mls,\"MLS.csv\");\r\n (0,_utils__WEBPACK_IMPORTED_MODULE_2__.saveToCSV)(this.convolution,'python_convolution_mls_iir.csv');\r\n (0,_utils__WEBPACK_IMPORTED_MODULE_2__.saveToCSV)(this.invertedImpulseResponse,'IIR.csv');\r\n const computedIRagain = await Promise.all(this.impulseResponses)\r\n .then(res => {\r\n for (let i = 0; i < res.length; i++){\r\n if (res[i] != undefined){\r\n (0,_utils__WEBPACK_IMPORTED_MODULE_2__.saveToCSV)(res[i], `IR_${i}`);\r\n }\r\n }\r\n })\r\n }\r\n\r\n return iir_and_plots;\r\n };\r\n\r\n //////////////////////volume\r\n\r\n handleIncomingData = data => {\r\n console.log('Received data: ', data);\r\n if (data.type === 'soundGainDBSPL') {\r\n this.soundGainDBSPL = data.value;\r\n } else {\r\n throw new Error(`Unknown data type: ${data.type}`);\r\n }\r\n };\r\n\r\n createSCurveBuffer = (onSetBool=true) => {\r\n\r\n const curve = new Float32Array(this.TAPER_SECS*this.sourceSamplingRate+1);\r\n const frequency = 1 / (4 * this.TAPER_SECS);\r\n let j = 0;\r\n for (let i = 0; i < this.TAPER_SECS*this.sourceSamplingRate+1; i += 1) {\r\n const phase = 2 * Math.PI * frequency * j;\r\n const onsetTaper = Math.pow(Math.sin(phase) , 2);\r\n const offsetTaper = Math.pow(Math.cos(phase) , 2);\r\n curve[i] = onSetBool? onsetTaper : offsetTaper;\r\n j += (1 / this.sourceSamplingRate);\r\n } \r\n return curve;\r\n };\r\n\r\n #getTruncatedSignal = (left = 3.5, right = 4.5) => {\r\n const start = Math.floor(left * this.sourceSamplingRate);\r\n const end = Math.floor(right * this.sourceSamplingRate);\r\n const result = Array.from(this.getLastRecordedSignal().slice(start, end));\r\n\r\n /**\r\n * function to check that capture was properly made\r\n * @param {*} list\r\n */\r\n const checkResult = list => {\r\n const setItem = new Set(list);\r\n if (setItem.size === 1 && setItem.has(0)) {\r\n console.warn(\r\n 'The last capture failed, all recorded signal is zero',\r\n this.getAllRecordedSignals()\r\n );\r\n }\r\n if (setItem.size === 0) {\r\n console.warn('The last capture failed, no recorded signal');\r\n }\r\n };\r\n checkResult(result);\r\n return result;\r\n };\r\n\r\n /** \r\n * \r\n * \r\n Construct a calibration Node with the calibration parameters and given gain value\r\n * @param {*} gainValue\r\n * */\r\n #createCalibrationToneWithGainValue = gainValue => {\r\n const audioContext = this.makeNewSourceAudioContext();\r\n const oscilator = audioContext.createOscillator();\r\n const gainNode = audioContext.createGain();\r\n const taperGainNode = audioContext.createGain();\r\n const offsetGainNode = audioContext.createGain();\r\n const totalDuration = this.#CALIBRATION_TONE_DURATION * 1.2;\r\n\r\n oscilator.frequency.value = this.#CALIBRATION_TONE_FREQUENCY;\r\n oscilator.type = this.#CALIBRATION_TONE_TYPE;\r\n gainNode.gain.value = gainValue;\r\n\r\n oscilator.connect(gainNode);\r\n gainNode.connect(taperGainNode);\r\n const onsetCurve = this.createSCurveBuffer();\r\n taperGainNode.gain.setValueCurveAtTime(onsetCurve, 0, this.TAPER_SECS);\r\n taperGainNode.connect(offsetGainNode);\r\n const offsetCurve = this.createSCurveBuffer(false);\r\n offsetGainNode.gain.setValueCurveAtTime(offsetCurve, (totalDuration-this.TAPER_SECS), this.TAPER_SECS);\r\n offsetGainNode.connect(audioContext.destination);\r\n\r\n this.addCalibrationNode(oscilator);\r\n };\r\n\r\n /**\r\n * Construct a Calibration Node with the calibration parameters.\r\n *\r\n * @private\r\n * @example\r\n */\r\n #createCalibrationNode = () => {\r\n const audioContext = this.makeNewSourceAudioContext();\r\n const oscilator = audioContext.createOscillator();\r\n const gainNode = audioContext.createGain();\r\n\r\n oscilator.frequency.value = this.#CALIBRATION_TONE_FREQUENCY;\r\n oscilator.type = this.#CALIBRATION_TONE_TYPE;\r\n gainNode.gain.value = 0.04;\r\n\r\n oscilator.connect(gainNode);\r\n gainNode.connect(audioContext.destination);\r\n\r\n this.addCalibrationNode(oscilator);\r\n };\r\n\r\n #playCalibrationAudioVolume = async () => {\r\n const totalDuration = this.#CALIBRATION_TONE_DURATION * 1.2;\r\n\r\n this.calibrationNodes[0].start(0);\r\n this.calibrationNodes[0].stop(totalDuration);\r\n console.log(`Playing a buffer of ${this.#CALIBRATION_TONE_DURATION} seconds of audio`);\r\n console.log(`Waiting a total of ${totalDuration} seconds`);\r\n await (0,_utils__WEBPACK_IMPORTED_MODULE_2__.sleep)(totalDuration);\r\n };\r\n\r\n #sendToServerForProcessing = (lCalib = 104.92978421490648) => {\r\n console.log('Sending data to server');\r\n this.pyServerAPI\r\n .getVolumeCalibration({\r\n sampleRate: this.sourceSamplingRate,\r\n payload: this.#getTruncatedSignal(),\r\n lCalib: lCalib,\r\n })\r\n .then(res => {\r\n if (this.outDBSPL === null) {\r\n this.outDBSPL = res['outDbSPL'];\r\n this.outDBSPL1000 = res['outDbSPL1000'];\r\n this.THD = res['thd'];\r\n }\r\n })\r\n .catch(err => {\r\n console.warn(err);\r\n });\r\n };\r\n\r\n startCalibrationVolume = async (stream, gainValues, lCalib = 104.92978421490648) => {\r\n const trialIterations = gainValues.length;\r\n const thdValues = [];\r\n const inDBValues = [];\r\n let inDB = 0;\r\n const outDBSPLValues = [];\r\n const outDBSPL1000Values = [];\r\n\r\n // do one calibration that will be discarded\r\n const soundLevelToDiscard = -60;\r\n const gainToDiscard = Math.pow(10, soundLevelToDiscard / 20);\r\n this.emit('update', {message: `1000 Hz Calibration: Sound Level ${soundLevelToDiscard} dB`});\r\n do {\r\n // eslint-disable-next-line no-await-in-loop\r\n await this.volumeCalibrationSteps(\r\n stream,\r\n this.#playCalibrationAudioVolume,\r\n this.#createCalibrationToneWithGainValue,\r\n this.#sendToServerForProcessing,\r\n gainToDiscard,\r\n lCalib //todo make this a class parameter\r\n );\r\n } while (this.outDBSPL === null);\r\n //reset the values\r\n this.outDBSPL = null;\r\n this.outDBSPL = null;\r\n this.outDBSPL1000 = null;\r\n this.THD = null;\r\n\r\n // run the calibration at different gain values provided by the user\r\n for (let i = 0; i < trialIterations; i++) {\r\n //convert gain to DB and add to inDB\r\n inDB = Math.log10(gainValues[i]) * 20;\r\n // precision to 1 decimal place\r\n inDB = Math.round(inDB * 10) / 10;\r\n inDBValues.push(inDB);\r\n this.emit('update', {message: `1000 Hz Calibration: Sound Level ${inDB} dB`});\r\n do {\r\n // eslint-disable-next-line no-await-in-loop\r\n await this.volumeCalibrationSteps(\r\n stream,\r\n this.#playCalibrationAudioVolume,\r\n this.#createCalibrationToneWithGainValue,\r\n this.#sendToServerForProcessing,\r\n gainValues[i],\r\n lCalib //todo make this a class parameter\r\n );\r\n } while (this.outDBSPL === null);\r\n outDBSPL1000Values.push(this.outDBSPL1000);\r\n thdValues.push(this.THD);\r\n outDBSPLValues.push(this.outDBSPL);\r\n\r\n this.outDBSPL = null;\r\n this.outDBSPL1000 = null;\r\n this.THD = null;\r\n }\r\n\r\n // get the volume calibration parameters from the server\r\n const parameters = await this.pyServerAPI\r\n .getVolumeCalibrationParameters({\r\n inDBValues: inDBValues,\r\n outDBSPLValues: outDBSPL1000Values,\r\n lCalib: lCalib,\r\n })\r\n .then(res => {\r\n return res;\r\n });\r\n const result = {\r\n parameters: parameters,\r\n inDBValues: inDBValues,\r\n outDBSPLValues: outDBSPLValues,\r\n outDBSPL1000Values: outDBSPL1000Values,\r\n thdValues: thdValues,\r\n };\r\n\r\n return result;\r\n };\r\n\r\n startCalibration = async (stream, gainValues, lCalib = 104.92978421490648) => {\r\n let volumeResults = await this.startCalibrationVolume(stream, gainValues, lCalib = 104.92978421490648);\r\n let impulseResponseResults = await this.startCalibrationImpulseResponse(stream);\r\n console.log(volumeResults);\r\n console.log(impulseResponseResults);\r\n const total_results = {...volumeResults, ...impulseResponseResults};\r\n console.log('total');\r\n console.log(total_results);\r\n return total_results;\r\n }\r\n\r\n\r\n}\r\n\r\n/* harmony default export */ __webpack_exports__[\"default\"] = (Combination);\n\n//# sourceURL=webpack://speakerCalibrator/./src/tasks/combination/combination.js?");
800
+
801
+ /***/ }),
802
+
803
+ /***/ "./src/tasks/combination/mlsGen/mlsGenInterface.js":
804
+ /*!*********************************************************!*\
805
+ !*** ./src/tasks/combination/mlsGen/mlsGenInterface.js ***!
806
+ \*********************************************************/
807
+ /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
808
+
809
+ "use strict";
810
+ eval("__webpack_require__.r(__webpack_exports__);\n/* eslint-disable prefer-destructuring */\r\n/* eslint-disable dot-notation */\r\n// eslint-disable-next-line import/extensions\r\nconst createMLSGenModule = __webpack_require__(/*! ../../../../dist/mlsGen.js */ \"./dist/mlsGen.js\");\r\n\r\n/**\r\n * MLSGenInterface provides a class for interfacing with the MLSGen WASM module.\r\n */\r\nclass MlsGenInterface {\r\n /** @private */\r\n #mlsOrder;\r\n\r\n /** @private */\r\n #WASMInstance; // the WASM module instance\r\n\r\n /** @private */\r\n #MLSGenInstance; // the MLSGen object instance\r\n\r\n /**\r\n * Creates an instance of MlsGenInterface.\r\n * Makes a call to the WASM glue code to load the WASM module.\r\n *\r\n * @param WASMInstance\r\n * @param mlsOrder\r\n * @param sourceSamplingRate\r\n * @param sinkSamplingRate\r\n * @example\r\n */\r\n constructor(WASMInstance, mlsOrder, sourceSamplingRate, sinkSamplingRate) {\r\n this.#mlsOrder = mlsOrder;\r\n this.#WASMInstance = WASMInstance;\r\n\r\n console.warn('initializing MLSGen, need to manually garbage collect');\r\n this.#MLSGenInstance = new this.#WASMInstance['MLSGen'](\r\n mlsOrder,\r\n sourceSamplingRate,\r\n sinkSamplingRate\r\n );\r\n }\r\n\r\n /**\r\n * Factory function that provide an asynchronous function that fetches the WASM module\r\n * and returns a promise that resolves when the module is loaded.\r\n *\r\n * @param mlsOrder\r\n * @param sourceSamplingRate - The sampling rate of the source audio.\r\n * @param sinkSamplingRate - The sampling rate of the sink audio.\r\n * @returns MlsGenInterface.\r\n * @example\r\n */\r\n static factory = async (mlsOrder, sourceSamplingRate, sinkSamplingRate) => {\r\n if (sourceSamplingRate === undefined || sinkSamplingRate === undefined) {\r\n throw new Error('sourceSamplingRate and sinkSamplingRate must be defined');\r\n }\r\n return new MlsGenInterface(\r\n await createMLSGenModule().then(instance => instance),\r\n mlsOrder,\r\n sourceSamplingRate,\r\n sinkSamplingRate\r\n );\r\n };\r\n\r\n /**\r\n * A Higher-Order function that takes an async callback function that access the MLSGen object,\r\n * providing safe garbage collection.\r\n *\r\n * @param func\r\n * @param args\r\n * @param funcsWithParams\r\n * @example\r\n */\r\n withGarbageCollection = async funcsWithParams => {\r\n try {\r\n for (let i = 0; i < funcsWithParams.length; i += 1) {\r\n const funcWithParams = funcsWithParams[i];\r\n // eslint-disable-next-line no-await-in-loop\r\n await funcWithParams();\r\n }\r\n } catch (error) {\r\n console.error(error);\r\n } finally {\r\n // garbage collect\r\n if (\r\n this !== undefined &&\r\n this !== null &&\r\n this.#MLSGenInstance !== undefined &&\r\n this.#MLSGenInstance !== null\r\n ) {\r\n this.#MLSGenInstance['Destruct'](); // Call the destructor\r\n this.#MLSGenInstance['delete'](); // Delete the object\r\n console.warn(`GARBAGE COLLECTION: deleted MLSGen`);\r\n this.#WASMInstance['doLeakCheck'](); // Check for memory leaks\r\n }\r\n }\r\n };\r\n\r\n /**\r\n * Calculate and return the Impulse Response of the recorded signal.\r\n *\r\n * @returns\r\n * @example\r\n */\r\n getImpulseResponse = () => this.#MLSGenInstance['getImpulseResponse']();\r\n\r\n /**\r\n * Given a recorded MLS signal, this function sets the recordedSignal property of the MLSGen object.\r\n *\r\n * @param signals\r\n * @example\r\n */\r\n setRecordedSignals = signals => {\r\n // get memory view\r\n const averagedSignals = this.average(signals);\r\n const recordedSignalsMemoryView = this.#MLSGenInstance['setRecordedSignalsMemoryView'](\r\n averagedSignals.byteLength\r\n );\r\n for (let i = 0; i < averagedSignals.length; i += 1) {\r\n recordedSignalsMemoryView[i] = averagedSignals[i];\r\n }\r\n };\r\n\r\n /**\r\n * Calculate the Maximum Length Sequence (MLS) with period P = 2^N - 1\r\n * using the MLSGen WASM module.\r\n *\r\n * @example\r\n */\r\n getMLS = () => this.#MLSGenInstance['getMLS']();\r\n}\r\n\r\n/* harmony default export */ __webpack_exports__[\"default\"] = (MlsGenInterface);\r\n\n\n//# sourceURL=webpack://speakerCalibrator/./src/tasks/combination/mlsGen/mlsGenInterface.js?");
811
+
812
+ /***/ }),
813
+
792
814
  /***/ "./src/tasks/impulse-response/impulseResponse.js":
793
815
  /*!*******************************************************!*\
794
816
  !*** ./src/tasks/impulse-response/impulseResponse.js ***!
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "speaker-calibration",
3
- "version": "2.1.16",
3
+ "version": "2.1.17",
4
4
  "description": "Speaker calibration library for auditory testing",
5
5
  "main": "dist/main.js",
6
6
  "directories": {
package/src/main.js CHANGED
@@ -3,6 +3,7 @@ import Speaker from './peer-connection/speaker';
3
3
 
4
4
  import VolumeCalibration from './tasks/volume/volume';
5
5
  import ImpulseResponseCalibration from './tasks/impulse-response/impulseResponse';
6
+ import CombinationCalibration from './tasks/combination/combination';
6
7
 
7
8
  import {
8
9
  UnsupportedDeviceError,
@@ -18,4 +19,5 @@ export {
18
19
  UnsupportedDeviceError,
19
20
  MissingSpeakerIdError,
20
21
  CalibrationTimedOutError,
22
+ CombinationCalibration,
21
23
  };