speaker-calibration 2.0.0 → 2.1.0

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.
Files changed (126) hide show
  1. package/.eslintignore +72 -0
  2. package/.eslintrc.json +40 -0
  3. package/.gitignore +78 -0
  4. package/.prettierignore +70 -0
  5. package/.prettierrc +15 -0
  6. package/LICENSE +20 -20
  7. package/README.md +133 -133
  8. package/__mocks__/fileMock.js +1 -0
  9. package/__mocks__/styleMock.js +1 -0
  10. package/babel.config.js +3 -0
  11. package/coverage/clover.xml +71 -0
  12. package/coverage/coverage-final.json +224 -0
  13. package/coverage/lcov-report/PythonServerInterface.js.html +265 -0
  14. package/coverage/lcov-report/base.css +354 -0
  15. package/coverage/lcov-report/block-navigation.js +82 -0
  16. package/coverage/lcov-report/favicon.png +0 -0
  17. package/coverage/lcov-report/index.html +123 -0
  18. package/coverage/lcov-report/prettify.css +101 -0
  19. package/coverage/lcov-report/prettify.js +937 -0
  20. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  21. package/coverage/lcov-report/sorter.js +189 -0
  22. package/coverage/lcov-report/src/index.html +121 -0
  23. package/coverage/lcov-report/src/server/PythonServerInterface.js.html +268 -0
  24. package/coverage/lcov-report/src/server/index.html +123 -0
  25. package/coverage/lcov-report/src/tasks/audioCalibrator.js.html +499 -0
  26. package/coverage/lcov-report/src/tasks/audioRecorder.js.html +412 -0
  27. package/coverage/lcov-report/src/tasks/index.html +143 -0
  28. package/coverage/lcov-report/src/tasks/volume/index.html +123 -0
  29. package/coverage/lcov-report/src/tasks/volume/volume.js.html +409 -0
  30. package/coverage/lcov-report/src/utils.js.html +172 -0
  31. package/coverage/lcov.info +91 -0
  32. package/dist/example/Queen-Bohemian_Rhapsody.wav +0 -0
  33. package/dist/example/Queen-Bohemian_Rhapsody_g_filtered.wav +0 -0
  34. package/dist/example/index.html +47 -0
  35. package/dist/example/listener.html +89 -0
  36. package/dist/example/server.js +49 -0
  37. package/dist/example/speaker.html +126 -0
  38. package/dist/example/speakerUI.js +217 -0
  39. package/dist/example/styles.css +40 -0
  40. package/dist/main.js +1 -1
  41. package/dist/mlsGen.js +6814 -6814
  42. package/dist/mlsGen.wasm +0 -0
  43. package/doc/AudioCalibrator.html +417 -0
  44. package/doc/AudioPeer.html +251 -0
  45. package/doc/AudioRecorder.html +195 -0
  46. package/doc/ImpulseResponse.html +215 -0
  47. package/doc/Listener.html +308 -0
  48. package/doc/MlsGenInterface.html +226 -0
  49. package/doc/MyEventEmitter.html +274 -0
  50. package/doc/PythonServerAPI.html +109 -0
  51. package/doc/Speaker-Calibration-UML-Diagram.png +0 -0
  52. package/doc/Speaker.html +276 -0
  53. package/doc/Takes%20a%20target%20element%20where%20html%20elements%20will%20be%20appended..html +128 -0
  54. package/doc/Takes%20the%20url%20of%20the%20current%20site%0Aand%20a%20target%20element%20where%20html%20elements%20will%20be%20appended..html +138 -0
  55. package/doc/Takes%20the%20url%20of%20the%20current%20site%20and%20a%20target%20element%20where%20html%20elements%20will%20be%20appended..html +137 -0
  56. package/doc/Volume.html +88 -0
  57. package/doc/audioCalibrator.js.html +179 -0
  58. package/doc/audioPeer.js.html +175 -0
  59. package/doc/audioRecorder.js.html +163 -0
  60. package/doc/creates%20a%20new%20AudioRecorder%20instance.%20%0ASets%20up%20the%20audio%20context%20and%20file%20reader..html +114 -0
  61. package/doc/fonts/OpenSans-Bold-webfont.eot +0 -0
  62. package/doc/fonts/OpenSans-Bold-webfont.svg +1830 -0
  63. package/doc/fonts/OpenSans-Bold-webfont.woff +0 -0
  64. package/doc/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  65. package/doc/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
  66. package/doc/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  67. package/doc/fonts/OpenSans-Italic-webfont.eot +0 -0
  68. package/doc/fonts/OpenSans-Italic-webfont.svg +1830 -0
  69. package/doc/fonts/OpenSans-Italic-webfont.woff +0 -0
  70. package/doc/fonts/OpenSans-Light-webfont.eot +0 -0
  71. package/doc/fonts/OpenSans-Light-webfont.svg +1831 -0
  72. package/doc/fonts/OpenSans-Light-webfont.woff +0 -0
  73. package/doc/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  74. package/doc/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
  75. package/doc/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  76. package/doc/fonts/OpenSans-Regular-webfont.eot +0 -0
  77. package/doc/fonts/OpenSans-Regular-webfont.svg +1831 -0
  78. package/doc/fonts/OpenSans-Regular-webfont.woff +0 -0
  79. package/doc/global.html +308 -0
  80. package/doc/index.html +58 -0
  81. package/doc/listener.js.html +170 -0
  82. package/doc/mlsGen_mlsGenInterface.js.html +117 -0
  83. package/doc/myEventEmitter.js.html +124 -0
  84. package/doc/peer-connection_audioPeer.js.html +188 -0
  85. package/doc/peer-connection_listener.js.html +311 -0
  86. package/doc/peer-connection_speaker.js.html +381 -0
  87. package/doc/sc-activity-diagram.png +0 -0
  88. package/doc/scripts/linenumber.js +25 -0
  89. package/doc/scripts/prettify/Apache-License-2.0.txt +202 -0
  90. package/doc/scripts/prettify/lang-css.js +24 -0
  91. package/doc/scripts/prettify/prettify.js +640 -0
  92. package/doc/server_PythonServerAPI.js.html +160 -0
  93. package/doc/speaker.js.html +248 -0
  94. package/doc/styles/jsdoc-default.css +371 -0
  95. package/doc/styles/prettify-jsdoc.css +111 -0
  96. package/doc/styles/prettify-tomorrow.css +163 -0
  97. package/doc/tasks_audioCalibrator.js.html +207 -0
  98. package/doc/tasks_audioRecorder.js.html +190 -0
  99. package/doc/tasks_impulse-response_impulseResponse.js.html +442 -0
  100. package/doc/tasks_impulse-response_mlsGen_mlsGenInterface.js.html +175 -0
  101. package/doc/tasks_volume_volume.js.html +185 -0
  102. package/doc/utils.js.html +105 -0
  103. package/jest.config.js +173 -0
  104. package/netlify.toml +27 -0
  105. package/package.json +67 -66
  106. package/src/index.html +21 -0
  107. package/src/main.js +21 -0
  108. package/src/myEventEmitter.js +83 -0
  109. package/src/peer-connection/audioPeer.js +151 -0
  110. package/src/peer-connection/listener.js +251 -0
  111. package/src/peer-connection/peerErrors.js +25 -0
  112. package/src/peer-connection/speaker.js +346 -0
  113. package/src/server/PythonServerAPI.js +117 -0
  114. package/src/tasks/audioCalibrator.js +218 -0
  115. package/src/tasks/audioRecorder.js +148 -0
  116. package/src/tasks/impulse-response/impulseResponse.js +436 -0
  117. package/src/tasks/impulse-response/mlsGen/mlsGen.cpp +99 -0
  118. package/src/tasks/impulse-response/mlsGen/mlsGen.hpp +304 -0
  119. package/src/tasks/impulse-response/mlsGen/mlsGenInterface.js +131 -0
  120. package/src/tasks/impulse-response/mlsGen/mlsGenTest.cpp +181 -0
  121. package/src/tasks/volume/volume.cpp +3 -0
  122. package/src/tasks/volume/volume.hpp +23 -0
  123. package/src/tasks/volume/volume.js +157 -0
  124. package/src/utils.js +55 -0
  125. package/webpack.config.js +37 -0
  126. package/README +0 -3
@@ -0,0 +1,148 @@
1
+ import MyEventEmitter from '../myEventEmitter';
2
+
3
+ /**
4
+ * @class provides a simple interface for recording audio from a microphone
5
+ * using the Media Recorder API.
6
+ */
7
+ class AudioRecorder extends MyEventEmitter {
8
+ /** @private */
9
+ #mediaRecorder;
10
+
11
+ /** @private */
12
+ #recordedChunks = [];
13
+
14
+ /** @private */
15
+ #audioBlob;
16
+
17
+ /** @private */
18
+ #audioContext;
19
+
20
+ /** @private */
21
+ #recordedSignals = [];
22
+
23
+ /** @private */
24
+ sinkSamplingRate;
25
+
26
+ /**
27
+ * Decode the audio data from the recorded audio blob.
28
+ *
29
+ * @private
30
+ * @example
31
+ */
32
+ #saveRecording = async () => {
33
+ const arrayBuffer = await this.#audioBlob.arrayBuffer();
34
+ const audioBuffer = await this.#audioContext.decodeAudioData(arrayBuffer);
35
+ const data = audioBuffer.getChannelData(0);
36
+
37
+ console.log(`Decoded audio buffer with ${data.length} samples`);
38
+ this.#recordedSignals.push(Array.from(data));
39
+ };
40
+
41
+ /**
42
+ * Event listener triggered when data is available in the media recorder.
43
+ *
44
+ * @private
45
+ * @param e - The event object.
46
+ * @example
47
+ */
48
+ #onRecorderDataAvailable = e => {
49
+ if (e.data && e.data.size > 0) this.#recordedChunks.push(e.data);
50
+ };
51
+
52
+ /**
53
+ * Method to create a media recorder object and set up event listeners.
54
+ *
55
+ * @private
56
+ * @param stream - The stream of audio from the Listener.
57
+ * @example
58
+ */
59
+ #setMediaRecorder = stream => {
60
+ // Create a new MediaRecorder object
61
+ this.#mediaRecorder = new MediaRecorder(stream);
62
+
63
+ // Add event listeners
64
+ this.#mediaRecorder.ondataavailable = e => this.#onRecorderDataAvailable(e);
65
+ };
66
+
67
+ #setAudioContext = () => {
68
+ this.#audioContext = new (window.AudioContext ||
69
+ window.webkitAudioContext ||
70
+ window.audioContext)({
71
+ sampleRate: this.sinkSamplingRate,
72
+ });
73
+ };
74
+
75
+ /**
76
+ * Public method to start the recording process.
77
+ *
78
+ * @param stream - The stream of audio from the Listener.
79
+ * @example
80
+ */
81
+ startRecording = async stream => {
82
+ // Create a fresh audio context
83
+ this.#setAudioContext();
84
+ // Set up media recorder if needed
85
+ if (!this.#mediaRecorder) this.#setMediaRecorder(stream);
86
+ // clear recorded chunks
87
+ this.#recordedChunks = [];
88
+ // start recording
89
+ this.#mediaRecorder.start();
90
+ };
91
+
92
+ /**
93
+ * Method to stop the recording process.
94
+ *
95
+ * @public
96
+ * @example
97
+ */
98
+ stopRecording = async () => {
99
+ // Stop the media recorder, and wait for the data to be available
100
+ await new Promise(resolve => {
101
+ this.#mediaRecorder.onstop = () => {
102
+ // when the stop event is triggered, resolve the promise
103
+ this.#audioBlob = new Blob(this.#recordedChunks, {
104
+ type: 'audio/wav; codecs=opus',
105
+ });
106
+ resolve(this.#audioBlob);
107
+ };
108
+ // call stop
109
+ this.#mediaRecorder.stop();
110
+ });
111
+ // Now that we have data, save it
112
+ await this.#saveRecording();
113
+ };
114
+
115
+ /** .
116
+ * .
117
+ * .
118
+ * Public method to get the last recorded audio signal
119
+ *
120
+ * @returns
121
+ * @example
122
+ */
123
+ getLastRecordedSignal = () => this.#recordedSignals[this.#recordedSignals.length - 1];
124
+
125
+ /** .
126
+ * .
127
+ * .
128
+ * Public method to get all the recorded audio signals
129
+ *
130
+ * @returns
131
+ * @example
132
+ */
133
+ getAllRecordedSignals = () => this.#recordedSignals;
134
+
135
+ /** .
136
+ * .
137
+ * .
138
+ * Public method to set the sampling rate used by the capture device
139
+ *
140
+ * @param {Number} sinkSamplingRate - The sampling rate of the capture device
141
+ * @example
142
+ */
143
+ setSinkSamplingRate = sinkSamplingRate => {
144
+ this.sinkSamplingRate = sinkSamplingRate;
145
+ };
146
+ }
147
+
148
+ export default AudioRecorder;
@@ -0,0 +1,436 @@
1
+ import AudioCalibrator from '../audioCalibrator';
2
+ import MlsGenInterface from './mlsGen/mlsGenInterface';
3
+
4
+ import {sleep, csvToArray} from '../../utils';
5
+
6
+ /**
7
+ *
8
+ */
9
+ class ImpulseResponse extends AudioCalibrator {
10
+ /**
11
+ * Default constructor. Creates an instance with any number of paramters passed or the default parameters defined here.
12
+ *
13
+ * @param {Object<boolean, number, number, number>} calibratorParams - paramter object
14
+ * @param {boolean} [calibratorParams.download = false] - boolean flag to download captures
15
+ * @param {number} [calibratorParams.mlsOrder = 18] - order of the MLS to be generated
16
+ * @param {number} [calibratorParams.numCaptures = 5] - number of captures to perform
17
+ * @param {number} [calibratorParams.numMLSPerCapture = 4] - number of bursts of MLS per capture
18
+ */
19
+ constructor({download = false, mlsOrder = 18, numCaptures = 3, numMLSPerCapture = 3}) {
20
+ super(numCaptures, numMLSPerCapture);
21
+ this.#mlsOrder = parseInt(mlsOrder, 10);
22
+ this.#P = 2 ** mlsOrder - 1;
23
+ this.#download = download;
24
+ }
25
+
26
+ /** @private */
27
+ #download;
28
+
29
+ /** @private */
30
+ #mlsGenInterface;
31
+
32
+ /** @private */
33
+ #mlsBufferView;
34
+
35
+ /** @private */
36
+ invertedImpulseResponse = null;
37
+
38
+ /** @private */
39
+ impulseResponses = [];
40
+
41
+ /** @private */
42
+ #mlsOrder;
43
+
44
+ /** @private */
45
+ #P;
46
+
47
+ /** @private */
48
+ TAPER_SECS = 5;
49
+
50
+ /** @private */
51
+ offsetGainNode;
52
+
53
+ /** .
54
+ * .
55
+ * .
56
+ * Sends all the computed impulse responses to the backend server for processing
57
+ *
58
+ * @returns sets the resulting inverted impulse response to the class property
59
+ * @example
60
+ */
61
+ sendImpulseResponsesToServerForProcessing = async () => {
62
+ const computedIRs = await Promise.all(this.impulseResponses);
63
+ this.emit('update', {message: `computing the IIR...`});
64
+ return this.pyServerAPI
65
+ .getInverseImpulseResponse({
66
+ payload: computedIRs.slice(0, this.numCaptures),
67
+ })
68
+ .then(res => {
69
+ this.emit('update', {message: `done computing the IIR...`});
70
+ this.invertedImpulseResponse = res;
71
+ })
72
+ .catch(err => {
73
+ // this.emit('InvertedImpulseResponse', {res: false});
74
+ console.error(err);
75
+ });
76
+ };
77
+
78
+ /** .
79
+ * .
80
+ * .
81
+ * Sends the recorded signal, or a given csv string of a signal, to the back end server for processing
82
+ *
83
+ * @param {<array>String} signalCsv - Optional csv string of a previously recorded signal, if given, this signal will be processed
84
+ * @example
85
+ */
86
+ sendRecordingToServerForProcessing = signalCsv => {
87
+ const allSignals = this.getAllRecordedSignals();
88
+ const numSignals = allSignals.length;
89
+ const payload =
90
+ signalCsv && signalCsv.length > 0 ? csvToArray(signalCsv) : allSignals[numSignals - 1];
91
+
92
+ this.emit('update', {message: `computing the IR of the last recording...`});
93
+ this.impulseResponses.push(
94
+ this.pyServerAPI
95
+ .getImpulseResponse({
96
+ sampleRate: this.sourceSamplingRate || 96000,
97
+ payload,
98
+ P: this.#P,
99
+ })
100
+ .then(res => {
101
+ if (this.numSuccessfulCaptured < this.numCaptures) {
102
+ this.numSuccessfulCaptured += 1;
103
+ this.emit('update', {
104
+ message: `${this.numSuccessfulCaptured}/${this.numCaptures} IRs computed...`,
105
+ });
106
+ }
107
+ return res;
108
+ })
109
+ .catch(err => {
110
+ console.error(err);
111
+ })
112
+ );
113
+ };
114
+
115
+ /**
116
+ * Passed to the calibration steps function, awaits the desired amount of seconds to capture the desired number
117
+ * of MLS periods defined in the constructor.
118
+ *
119
+ * @example
120
+ */
121
+ #awaitDesiredMLSLength = async () => {
122
+ // seconds per MLS = P / SR
123
+ // await N * P / SR
124
+ this.emit('update', {
125
+ message: `sampling the calibration signal...`,
126
+ });
127
+ await sleep((this.#P / this.sourceSamplingRate) * this.numMLSPerCapture);
128
+ };
129
+
130
+ /** .
131
+ * .
132
+ * .
133
+ * Passed to the calibration steps function, awaits the onset of the signal to ensure a steady state
134
+ *
135
+ * @example
136
+ */
137
+ #awaitSignalOnset = async () => {
138
+ this.emit('update', {
139
+ message: `waiting for the signal to stabalize...`,
140
+ });
141
+ await sleep(this.TAPER_SECS);
142
+ };
143
+
144
+ /**
145
+ * Called immediately after a recording is captured. Used to process the resulting signal
146
+ * whether by sending the result to a server or by computing a result locally.
147
+ *
148
+ * @example
149
+ */
150
+ #afterMLSRecord = () => {
151
+ if (this.#download) {
152
+ this.downloadData();
153
+ }
154
+ this.sendRecordingToServerForProcessing();
155
+ };
156
+
157
+ #afterMLSwIIRRecord = () => {
158
+ if (this.#download) {
159
+ this.downloadData();
160
+ }
161
+ this.#stopCalibrationAudio();
162
+ };
163
+
164
+ /** .
165
+ * .
166
+ * .
167
+ * Created an S Curver Buffer to taper the signal onset
168
+ *
169
+ * @param {*} length
170
+ * @param {*} phase
171
+ * @returns
172
+ * @example
173
+ */
174
+ static createSCurveBuffer = (length, phase) => {
175
+ const curve = new Float32Array(length);
176
+ let i;
177
+ for (i = 0; i < length; i += 1) {
178
+ // scale the curve to be between 0-1
179
+ curve[i] = Math.sin((Math.PI * i) / length - phase) / 2 + 0.5;
180
+ }
181
+ return curve;
182
+ };
183
+
184
+ static createInverseSCurveBuffer = (length, phase) => {
185
+ const curve = new Float32Array(length);
186
+ let i;
187
+ let j = length - 1;
188
+ for (i = 0; i < length; i += 1) {
189
+ // scale the curve to be between 0-1
190
+ curve[i] = Math.sin((Math.PI * j) / length - phase) / 2 + 0.5;
191
+ j -= 1;
192
+ }
193
+ return curve;
194
+ };
195
+
196
+ /**
197
+ * Construct a Calibration Node with the calibration parameters.
198
+ *
199
+ * @param CALIBRATION_TONE_FREQUENCY
200
+ * @private
201
+ * @example
202
+ */
203
+ #createPureTonenNode = CALIBRATION_TONE_FREQUENCY => {
204
+ const audioContext = this.makeNewSourceAudioContext();
205
+ const oscilator = audioContext.createOscillator();
206
+ const gainNode = audioContext.createGain();
207
+
208
+ oscilator.frequency.value = CALIBRATION_TONE_FREQUENCY;
209
+ oscilator.type = 'sine';
210
+ gainNode.gain.value = 0.04;
211
+
212
+ oscilator.connect(gainNode);
213
+ gainNode.connect(audioContext.destination);
214
+
215
+ this.addCalibrationNode(oscilator);
216
+ };
217
+
218
+ /**
219
+ * Construct a Calibration Node with the calibration parameters.
220
+ *
221
+ * @param dataBuffer
222
+ * @private
223
+ * @example
224
+ */
225
+ #createCalibrationNodeFromBuffer = dataBuffer => {
226
+ const audioContext = this.makeNewSourceAudioContext();
227
+ const buffer = audioContext.createBuffer(
228
+ 1, // number of channels
229
+ dataBuffer.length,
230
+ audioContext.sampleRate // sample rate
231
+ );
232
+
233
+ const data = buffer.getChannelData(0); // get data
234
+ // fill the buffer with our data
235
+ try {
236
+ for (let i = 0; i < dataBuffer.length; i += 1) {
237
+ data[i] = dataBuffer[i];
238
+ }
239
+ } catch (error) {
240
+ console.error(error);
241
+ }
242
+
243
+ const onsetGainNode = audioContext.createGain();
244
+ this.offsetGainNode = audioContext.createGain();
245
+ const source = audioContext.createBufferSource();
246
+
247
+ source.buffer = buffer;
248
+ source.loop = true;
249
+ source.connect(onsetGainNode);
250
+ onsetGainNode.connect(this.offsetGainNode);
251
+ this.offsetGainNode.connect(audioContext.destination);
252
+
253
+ const onsetCurve = ImpulseResponse.createSCurveBuffer(this.sourceSamplingRate, Math.PI / 2);
254
+ onsetGainNode.gain.setValueCurveAtTime(onsetCurve, 0, this.TAPER_SECS);
255
+
256
+ this.addCalibrationNode(source);
257
+ };
258
+
259
+ /**
260
+ * Given a data buffer, creates the required calibration node
261
+ *
262
+ * @param {*} dataBufferArray
263
+ * @example
264
+ */
265
+ #setCalibrationNodesFromBuffer = (dataBufferArray = [this.#mlsBufferView]) => {
266
+ if (dataBufferArray.length === 1) {
267
+ this.#createCalibrationNodeFromBuffer(dataBufferArray[0]);
268
+ } else {
269
+ throw new Error('The length of the data buffer array must be 1');
270
+ }
271
+ };
272
+
273
+ #createImpulseResponseFilterGraph = (calibrationSignal, iir) => {
274
+ const audioCtx = this.makeNewSourceAudioContext();
275
+
276
+ // -------------------------------------------------------- IIR
277
+ const iirBuffer = audioCtx.createBuffer(
278
+ 1,
279
+ // TODO: quality check this
280
+ iir.length - 1,
281
+ audioCtx.sampleRate
282
+ );
283
+
284
+ // Fill the buffer with the inverted impulse response
285
+ const iirChannelZeroBuffer = iirBuffer.getChannelData(0);
286
+ for (let i = 0; i < iirBuffer.length; i += 1) {
287
+ // audio needs to be in [-1.0; 1.0]
288
+ iirChannelZeroBuffer[i] = iir[i];
289
+ }
290
+
291
+ const convolverNode = audioCtx.createConvolver();
292
+
293
+ convolverNode.normalize = false;
294
+ convolverNode.channelCount = 1;
295
+ convolverNode.buffer = iirBuffer;
296
+
297
+ // ------------------------------------------------------ MLS
298
+ const calibrationSignalBuffer = audioCtx.createBuffer(
299
+ 1, // number of channels
300
+ calibrationSignal.length,
301
+ audioCtx.sampleRate // sample rate
302
+ );
303
+
304
+ const mlsChannelZeroBuffer = calibrationSignalBuffer.getChannelData(0); // get data
305
+ // fill the buffer with our data
306
+ try {
307
+ for (let i = 0; i < calibrationSignal.length; i += 1) {
308
+ mlsChannelZeroBuffer[i] = calibrationSignal[i];
309
+ }
310
+ } catch (error) {
311
+ console.error(error);
312
+ }
313
+
314
+ const sourceNode = audioCtx.createBufferSource();
315
+
316
+ sourceNode.buffer = calibrationSignalBuffer;
317
+ sourceNode.loop = true;
318
+ sourceNode.connect(convolverNode);
319
+
320
+ convolverNode.connect(audioCtx.destination);
321
+
322
+ console.log({convolverNode, sourceNode});
323
+
324
+ this.addCalibrationNode(sourceNode);
325
+ };
326
+
327
+ #createIIRwMLSGraph = () => {
328
+ this.#createImpulseResponseFilterGraph(this.impulseResponses, [this.#mlsBufferView][0]);
329
+ };
330
+
331
+ /**
332
+ * Creates an audio context and plays it for a few seconds.
333
+ *
334
+ * @private
335
+ * @returns - Resolves when the audio is done playing.
336
+ * @example
337
+ */
338
+ #playCalibrationAudio = () => {
339
+ this.calibrationNodes[0].start(0);
340
+ this.emit('update', {message: 'playing the calibration tone...'});
341
+ };
342
+
343
+ /** .
344
+ * .
345
+ * .
346
+ * Stops the audio with tapered offset
347
+ *
348
+ * @example
349
+ */
350
+ #stopCalibrationAudio = () => {
351
+ this.offsetGainNode.gain.setValueAtTime(
352
+ this.offsetGainNode.gain.value,
353
+ this.sourceAudioContext.currentTime
354
+ );
355
+
356
+ this.offsetGainNode.gain.setTargetAtTime(0, this.sourceAudioContext.currentTime, 0.5);
357
+ this.emit('update', {message: 'stopping the calibration tone...'});
358
+ };
359
+
360
+ playMLSwithIIR = async (stream, iir) => {
361
+ this.invertedImpulseResponse = iir;
362
+ // initialize the MLSGenInterface object with it's factory method
363
+ await MlsGenInterface.factory(
364
+ this.#mlsOrder,
365
+ this.sinkSamplingRate,
366
+ this.sourceSamplingRate
367
+ ).then(mlsGenInterface => {
368
+ this.#mlsGenInterface = mlsGenInterface;
369
+ this.#mlsBufferView = this.#mlsGenInterface.getMLS();
370
+ });
371
+
372
+ // after intializating, start the calibration steps with garbage collection
373
+ await this.#mlsGenInterface.withGarbageCollection([
374
+ [
375
+ this.calibrationSteps,
376
+ [
377
+ stream,
378
+ this.#playCalibrationAudio, // play audio func (required)
379
+ this.#createImpulseResponseFilterGraph, // before play func
380
+ null, // before record
381
+ this.#awaitDesiredMLSLength, // during record
382
+ this.#afterMLSwIIRRecord, // after record
383
+ ],
384
+ ],
385
+ ]);
386
+ };
387
+
388
+ /**
389
+ * Public method to start the calibration process. Objects intialized from webassembly allocate new memory
390
+ * and must be manually freed. This function is responsible for intializing the MlsGenInterface,
391
+ * and wrapping the calibration steps with a garbage collection safe gaurd.
392
+ *
393
+ * @public
394
+ * @param stream - The stream of audio from the Listener.
395
+ * @example
396
+ */
397
+ startCalibration = async stream => {
398
+ // initialize the MLSGenInterface object with it's factory method
399
+ await MlsGenInterface.factory(
400
+ this.#mlsOrder,
401
+ this.sinkSamplingRate,
402
+ this.sourceSamplingRate
403
+ ).then(mlsGenInterface => {
404
+ this.#mlsGenInterface = mlsGenInterface;
405
+ this.#mlsBufferView = this.#mlsGenInterface.getMLS();
406
+ });
407
+
408
+ // after intializating, start the calibration steps with garbage collection
409
+ await this.#mlsGenInterface.withGarbageCollection([
410
+ () =>
411
+ this.calibrationSteps(
412
+ stream,
413
+ this.#playCalibrationAudio, // play audio func (required)
414
+ this.#setCalibrationNodesFromBuffer, // before play func
415
+ this.#awaitSignalOnset, // before record
416
+ () => this.numSuccessfulCaptured < this.numCaptures, // loop while true
417
+ this.#awaitDesiredMLSLength, // during record
418
+ this.#afterMLSRecord // after record
419
+ ),
420
+ ]);
421
+
422
+ this.#stopCalibrationAudio();
423
+
424
+ // at this stage we've captured all the required signals,
425
+ // and have received IRs for each one
426
+ // so let's send all the IRs to the server to be converted to a single IIR
427
+ await this.sendImpulseResponsesToServerForProcessing();
428
+
429
+ // debugging function, use to test the result of the IIR
430
+ // await this.playMLSwithIIR(stream, this.invertedImpulseResponse);
431
+
432
+ return this.invertedImpulseResponse;
433
+ };
434
+ }
435
+
436
+ export default ImpulseResponse;
@@ -0,0 +1,99 @@
1
+ #include "mlsGen.hpp"
2
+ #include <sanitizer/lsan_interface.h>
3
+
4
+ #ifdef __cplusplus
5
+ extern "C" {
6
+ #endif
7
+
8
+ MLSGen::MLSGen(long N, long srcSR, long sinkSR) {
9
+ MLSGen::N = N;
10
+ MLSGen::srcSR = srcSR;
11
+ MLSGen::sinkSR = sinkSR;
12
+ P = (1 << N) - 1;
13
+ mls = new bool[P];
14
+ tagL = new long[P];
15
+ tagS = new long[P];
16
+ generatedSignal = new float[P];
17
+ recordedSignal = new float[P];
18
+ perm = new float[P + 1];
19
+ resp = new float[P + 1];
20
+ }
21
+
22
+ #ifndef __EMSCRIPTEN__
23
+ MLSGen::~MLSGen() {
24
+ delete[] mls;
25
+ delete[] tagL;
26
+ delete[] tagS;
27
+ delete[] generatedSignal;
28
+ delete[] recordedSignal;
29
+ delete[] perm;
30
+ delete[] resp;
31
+ }
32
+ #endif
33
+
34
+ #ifdef __EMSCRIPTEN__
35
+
36
+ using namespace emscripten;
37
+
38
+ void MLSGen::Destruct() {
39
+ delete[] mls;
40
+ delete[] tagL;
41
+ delete[] tagS;
42
+ delete[] generatedSignal;
43
+ delete[] recordedSignal;
44
+ delete[] perm;
45
+ delete[] resp;
46
+ }
47
+
48
+ emscripten::val MLSGen::getMLS() {
49
+ // if mls is not generated, generate it
50
+ if (*(&mls + 1) - mls != P) {
51
+ generateMls();
52
+ }
53
+ for (int i = 0; i < P; i++) {
54
+ generatedSignal[i] = -2 * mls[i] + 1;
55
+ }
56
+ return emscripten::val(typed_memory_view(P, generatedSignal));
57
+ }
58
+
59
+ emscripten::val MLSGen::setRecordedSignalsMemoryView(long sizeRecordedSignals) {
60
+ C = sizeRecordedSignals;
61
+ recordedSignals = new float[C];
62
+ return emscripten::val(typed_memory_view(C, recordedSignals));
63
+ }
64
+
65
+ emscripten::val MLSGen::getRecordedSignalsMemoryView() {
66
+ return emscripten::val(typed_memory_view(C, recordedSignals));
67
+ }
68
+
69
+ emscripten::val MLSGen::getImpulseResponse() {
70
+ generateTagL(); // Generate tagL for the L matrix
71
+ generateTagS(); // Generate tagS for the S matrix
72
+ GenerateSignal(); // Generate the signal TEST PURPOSES
73
+ permuteSignal(); // Permute the signal according to tagS
74
+ fastHadamard(); // Do a Hadamard transform in place
75
+ permuteResponse(); // Permute the impulseresponse according to tagL
76
+ return emscripten::val(typed_memory_view(P + 1, resp));
77
+ }
78
+
79
+ // Binding code
80
+ EMSCRIPTEN_BINDINGS(mls_gen_module) {
81
+ class_<MLSGen>("MLSGen")
82
+ .constructor<long, long, long>()
83
+ .function("Destruct", &MLSGen::Destruct)
84
+ .function("getMLS", &MLSGen::getMLS)
85
+ .function("getRecordedSignalsMemoryView",
86
+ &MLSGen::getRecordedSignalsMemoryView)
87
+ .function("setRecordedSignalsMemoryView",
88
+ &MLSGen::setRecordedSignalsMemoryView)
89
+ .function("getImpulseResponse", &MLSGen::getImpulseResponse);
90
+ function("doLeakCheck", &__lsan_do_recoverable_leak_check);
91
+ };
92
+ #endif
93
+
94
+ #ifdef __cplusplus
95
+ }
96
+ #endif
97
+
98
+ // https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html
99
+ // https://web.dev/webassembly-memory-debugging/