speaker-calibration 2.2.220 → 2.2.221
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.
- package/.eslintignore +71 -71
- package/.eslintrc.json +40 -40
- package/.gitignore +81 -0
- package/.prettierignore +69 -69
- package/.prettierrc +14 -14
- package/LICENSE +20 -20
- package/README.md +133 -133
- package/__mocks__/fileMock.js +1 -1
- package/__mocks__/styleMock.js +1 -1
- package/babel.config.js +3 -3
- package/coverage/clover.xml +71 -71
- package/coverage/coverage-final.json +224 -224
- package/coverage/lcov-report/PythonServerInterface.js.html +265 -265
- package/coverage/lcov-report/base.css +354 -354
- package/coverage/lcov-report/block-navigation.js +82 -82
- package/coverage/lcov-report/index.html +123 -123
- package/coverage/lcov-report/prettify.css +101 -101
- package/coverage/lcov-report/prettify.js +937 -937
- package/coverage/lcov-report/sorter.js +189 -189
- package/coverage/lcov-report/src/index.html +121 -121
- package/coverage/lcov-report/src/server/PythonServerInterface.js.html +268 -268
- package/coverage/lcov-report/src/server/index.html +123 -123
- package/coverage/lcov-report/src/tasks/audioCalibrator.js.html +499 -499
- package/coverage/lcov-report/src/tasks/audioRecorder.js.html +412 -412
- package/coverage/lcov-report/src/tasks/index.html +143 -143
- package/coverage/lcov-report/src/tasks/volume/index.html +123 -123
- package/coverage/lcov-report/src/tasks/volume/volume.js.html +409 -409
- package/coverage/lcov-report/src/utils.js.html +172 -172
- package/coverage/lcov.info +91 -91
- package/dist/example/NoSleep.min.js +1 -1
- package/dist/example/fetch-languages-sheets.js +77 -77
- package/dist/example/i18n.js +29082 -28914
- package/dist/example/index.html +47 -47
- package/dist/example/listener.html +81 -79
- package/dist/example/server.js +51 -51
- package/dist/example/speaker.html +145 -145
- package/dist/example/speakerUI.js +273 -273
- package/dist/example/styles.css +152 -152
- package/dist/listener.js +4 -4
- package/dist/main.js +11 -11
- package/dist/mlsGen.js +6814 -6814
- package/dist/mlsGen.wasm +0 -0
- package/dist/package-lock.json +1018 -1018
- package/dist/package.json +18 -18
- package/doc/AudioCalibrator.html +417 -417
- package/doc/AudioPeer.html +251 -251
- package/doc/AudioRecorder.html +195 -195
- package/doc/ImpulseResponse.html +215 -215
- package/doc/Listener.html +308 -308
- package/doc/MlsGenInterface.html +226 -226
- package/doc/MyEventEmitter.html +274 -274
- package/doc/PythonServerAPI.html +109 -109
- package/doc/Speaker.html +276 -276
- package/doc/Takes%20a%20target%20element%20where%20html%20elements%20will%20be%20appended..html +128 -128
- package/doc/Takes%20the%20url%20of%20the%20current%20site%0Aand%20a%20target%20element%20where%20html%20elements%20will%20be%20appended..html +138 -138
- package/doc/Takes%20the%20url%20of%20the%20current%20site%20and%20a%20target%20element%20where%20html%20elements%20will%20be%20appended..html +137 -137
- package/doc/Volume.html +88 -88
- package/doc/audioCalibrator.js.html +179 -179
- package/doc/audioPeer.js.html +175 -175
- package/doc/audioRecorder.js.html +163 -163
- package/doc/creates%20a%20new%20AudioRecorder%20instance.%20%0ASets%20up%20the%20audio%20context%20and%20file%20reader..html +114 -114
- package/doc/fonts/OpenSans-Bold-webfont.svg +1829 -1829
- package/doc/fonts/OpenSans-BoldItalic-webfont.svg +1829 -1829
- package/doc/fonts/OpenSans-Italic-webfont.svg +1829 -1829
- package/doc/fonts/OpenSans-Light-webfont.svg +1830 -1830
- package/doc/fonts/OpenSans-LightItalic-webfont.svg +1834 -1834
- package/doc/fonts/OpenSans-Regular-webfont.svg +1830 -1830
- package/doc/global.html +308 -308
- package/doc/index.html +58 -58
- package/doc/listener.js.html +170 -170
- package/doc/mlsGen_mlsGenInterface.js.html +117 -117
- package/doc/myEventEmitter.js.html +124 -124
- package/doc/peer-connection_audioPeer.js.html +188 -188
- package/doc/peer-connection_listener.js.html +311 -311
- package/doc/peer-connection_speaker.js.html +381 -381
- package/doc/scripts/linenumber.js +25 -25
- package/doc/scripts/prettify/Apache-License-2.0.txt +202 -202
- package/doc/scripts/prettify/lang-css.js +24 -24
- package/doc/scripts/prettify/prettify.js +640 -640
- package/doc/server_PythonServerAPI.js.html +160 -160
- package/doc/speaker.js.html +248 -248
- package/doc/styles/jsdoc-default.css +371 -371
- package/doc/styles/prettify-jsdoc.css +111 -111
- package/doc/styles/prettify-tomorrow.css +163 -163
- package/doc/tasks_audioCalibrator.js.html +207 -207
- package/doc/tasks_audioRecorder.js.html +190 -190
- package/doc/tasks_impulse-response_impulseResponse.js.html +442 -442
- package/doc/tasks_impulse-response_mlsGen_mlsGenInterface.js.html +175 -175
- package/doc/tasks_volume_volume.js.html +185 -185
- package/doc/utils.js.html +105 -105
- package/jest.config.js +173 -173
- package/netlify.toml +26 -26
- package/package.json +78 -78
- package/src/config/firebase.js +26 -26
- package/src/index.html +21 -21
- package/src/listener-app/listener.js +377 -344
- package/src/main.js +22 -22
- package/src/myEventEmitter.js +83 -83
- package/src/peer-connection/audioPeer.js +148 -148
- package/src/peer-connection/listener.js +467 -467
- package/src/peer-connection/peerErrors.js +25 -25
- package/src/peer-connection/speaker.js +812 -810
- package/src/powerCheck.js +98 -98
- package/src/server/PythonServerAPI.js +869 -869
- package/src/tasks/audioCalibrator.js +360 -360
- package/src/tasks/audioRecorder.js +315 -315
- package/src/tasks/combination/combination.js +3171 -3167
- package/src/tasks/combination/mlsGen/mlsGen.cpp +98 -98
- package/src/tasks/combination/mlsGen/mlsGen.hpp +303 -303
- package/src/tasks/combination/mlsGen/mlsGenInterface.js +131 -131
- package/src/tasks/combination/mlsGen/mlsGenTest.cpp +180 -180
- package/src/tasks/impulse-response/impulseResponse.js +610 -610
- package/src/tasks/impulse-response/mlsGen/mlsGen.cpp +98 -98
- package/src/tasks/impulse-response/mlsGen/mlsGen.hpp +303 -303
- package/src/tasks/impulse-response/mlsGen/mlsGenInterface.js +131 -131
- package/src/tasks/impulse-response/mlsGen/mlsGenTest.cpp +180 -180
- package/src/tasks/volume/volume.cpp +2 -2
- package/src/tasks/volume/volume.hpp +22 -22
- package/src/tasks/volume/volume.js +279 -279
- package/src/utils.js +205 -205
- package/webpack.config.js +64 -64
- package/.github/workflows/update-phrases.yml +0 -37
- package/makefile +0 -74
|
@@ -1,610 +1,610 @@
|
|
|
1
|
-
import AudioCalibrator from '../audioCalibrator';
|
|
2
|
-
import MlsGenInterface from './mlsGen/mlsGenInterface';
|
|
3
|
-
|
|
4
|
-
import {sleep, csvToArray, saveToCSV} from '../../utils';
|
|
5
|
-
import database from '../../config/firebase';
|
|
6
|
-
import {ref, set, get, child} from 'firebase/database';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
*
|
|
10
|
-
*/
|
|
11
|
-
class ImpulseResponse extends AudioCalibrator {
|
|
12
|
-
/**
|
|
13
|
-
* Default constructor. Creates an instance with any number of paramters passed or the default parameters defined here.
|
|
14
|
-
*
|
|
15
|
-
* @param {Object<boolean, number, number, number>} calibratorParams - paramter object
|
|
16
|
-
* @param {boolean} [calibratorParams.download = false] - boolean flag to download captures
|
|
17
|
-
* @param {number} [calibratorParams.mlsOrder = 18] - order of the MLS to be generated
|
|
18
|
-
* @param {number} [calibratorParams.numCaptures = 5] - number of captures to perform
|
|
19
|
-
* @param {number} [calibratorParams.numMLSPerCapture = 4] - number of bursts of MLS per capture
|
|
20
|
-
*/
|
|
21
|
-
constructor({
|
|
22
|
-
download = false,
|
|
23
|
-
mlsOrder = 18,
|
|
24
|
-
numCaptures = 3,
|
|
25
|
-
numMLSPerCapture = 4,
|
|
26
|
-
lowHz = 20,
|
|
27
|
-
highHz = 10000,
|
|
28
|
-
loudSpeakerIr = null,
|
|
29
|
-
}) {
|
|
30
|
-
super(numCaptures, numMLSPerCapture);
|
|
31
|
-
this.#mlsOrder = parseInt(mlsOrder, 10);
|
|
32
|
-
this.#P = 2 ** mlsOrder - 1;
|
|
33
|
-
this.#download = download;
|
|
34
|
-
this.#mls = [];
|
|
35
|
-
this.#lowHz = lowHz;
|
|
36
|
-
this.#highHz = highHz;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** @private */
|
|
40
|
-
stepNum = 0;
|
|
41
|
-
|
|
42
|
-
/** @private */
|
|
43
|
-
totalSteps = 25;
|
|
44
|
-
|
|
45
|
-
/** @private */
|
|
46
|
-
#download;
|
|
47
|
-
|
|
48
|
-
/** @private */
|
|
49
|
-
#mlsGenInterface;
|
|
50
|
-
|
|
51
|
-
/** @private */
|
|
52
|
-
#mlsBufferView;
|
|
53
|
-
|
|
54
|
-
/** @private */
|
|
55
|
-
invertedImpulseResponse = null;
|
|
56
|
-
|
|
57
|
-
/** @private */
|
|
58
|
-
impulseResponses = [];
|
|
59
|
-
|
|
60
|
-
/** @private */
|
|
61
|
-
#mlsOrder;
|
|
62
|
-
|
|
63
|
-
/** @private */
|
|
64
|
-
#lowHz;
|
|
65
|
-
|
|
66
|
-
/** @private */
|
|
67
|
-
#highHz;
|
|
68
|
-
|
|
69
|
-
/** @private */
|
|
70
|
-
#mls;
|
|
71
|
-
|
|
72
|
-
/** @private */
|
|
73
|
-
#P;
|
|
74
|
-
|
|
75
|
-
/** @private */
|
|
76
|
-
#audioContext;
|
|
77
|
-
|
|
78
|
-
/** @private */
|
|
79
|
-
TAPER_SECS = 5;
|
|
80
|
-
|
|
81
|
-
/** @private */
|
|
82
|
-
offsetGainNode;
|
|
83
|
-
|
|
84
|
-
/** @private */
|
|
85
|
-
convolution;
|
|
86
|
-
|
|
87
|
-
/** @private */
|
|
88
|
-
status_denominator = 6;
|
|
89
|
-
|
|
90
|
-
/** @private */
|
|
91
|
-
status_numerator = 0;
|
|
92
|
-
|
|
93
|
-
/** @private */
|
|
94
|
-
percent_complete = 0;
|
|
95
|
-
|
|
96
|
-
/** @private */
|
|
97
|
-
status = ``;
|
|
98
|
-
|
|
99
|
-
/**generate string template that gets reevaluated as variable increases */
|
|
100
|
-
generateTemplate = () => {
|
|
101
|
-
if (this.percent_complete > 100) {
|
|
102
|
-
this.percent_complete = 100;
|
|
103
|
-
}
|
|
104
|
-
const template = `<div style="display: flex; justify-content: center;"><div style="width: 200px; height: 20px; border: 2px solid #000; border-radius: 10px;"><div style="width: ${this.percent_complete}%; height: 100%; background-color: #00aaff; border-radius: 8px;"></div></div></div>`;
|
|
105
|
-
return template;
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
/** increment numerator and percent for status bar */
|
|
109
|
-
incrementStatusBar = () => {
|
|
110
|
-
this.status_numerator += 1;
|
|
111
|
-
this.percent_complete = (this.status_numerator / this.status_denominator) * 100;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
/** .
|
|
115
|
-
* .
|
|
116
|
-
* .
|
|
117
|
-
* Sends all the computed impulse responses to the backend server for processing
|
|
118
|
-
*
|
|
119
|
-
* @returns sets the resulting inverted impulse response to the class property
|
|
120
|
-
* @example
|
|
121
|
-
*/
|
|
122
|
-
sendImpulseResponsesToServerForProcessing = async () => {
|
|
123
|
-
const computedIRs = await Promise.all(this.impulseResponses);
|
|
124
|
-
const filteredComputedIRs = computedIRs.filter(element => {
|
|
125
|
-
return element != undefined;
|
|
126
|
-
});
|
|
127
|
-
const mls = this.#mls;
|
|
128
|
-
const lowHz = this.#lowHz;
|
|
129
|
-
const highHz = this.#highHz;
|
|
130
|
-
this.stepNum += 1;
|
|
131
|
-
this.status = `computing the IIR...`.toString() + this.generateTemplate().toString();
|
|
132
|
-
this.emit('update', {message: this.status});
|
|
133
|
-
return this.pyServerAPI
|
|
134
|
-
.getInverseImpulseResponseWithRetry({
|
|
135
|
-
payload: filteredComputedIRs.slice(0, this.numCaptures),
|
|
136
|
-
mls,
|
|
137
|
-
lowHz,
|
|
138
|
-
highHz,
|
|
139
|
-
})
|
|
140
|
-
.then(res => {
|
|
141
|
-
console.log(res);
|
|
142
|
-
this.stepNum += 1;
|
|
143
|
-
this.incrementStatusBar();
|
|
144
|
-
this.status = `done computing the IIR...`.toString() + this.generateTemplate().toString();
|
|
145
|
-
this.emit('update', {message: this.status});
|
|
146
|
-
this.invertedImpulseResponse = res['iir'];
|
|
147
|
-
this.convolution = res['convolution'];
|
|
148
|
-
})
|
|
149
|
-
.catch(err => {
|
|
150
|
-
// this.emit('InvertedImpulseResponse', {res: false});
|
|
151
|
-
console.error(err);
|
|
152
|
-
});
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
/** .
|
|
156
|
-
* .
|
|
157
|
-
* .
|
|
158
|
-
* Sends the recorded signal, or a given csv string of a signal, to the back end server for processing
|
|
159
|
-
*
|
|
160
|
-
* @param {<array>String} signalCsv - Optional csv string of a previously recorded signal, if given, this signal will be processed
|
|
161
|
-
* @example
|
|
162
|
-
*/
|
|
163
|
-
sendRecordingToServerForProcessing = signalCsv => {
|
|
164
|
-
const allSignals = this.getAllRecordedSignals();
|
|
165
|
-
const numSignals = allSignals.length;
|
|
166
|
-
const mls = this.#mls;
|
|
167
|
-
const payload =
|
|
168
|
-
signalCsv && signalCsv.length > 0 ? csvToArray(signalCsv) : allSignals[numSignals - 1];
|
|
169
|
-
console.log('sending rec');
|
|
170
|
-
this.stepNum += 1;
|
|
171
|
-
this.status =
|
|
172
|
-
`computing the IR of the last recording...`.toString() + this.generateTemplate().toString();
|
|
173
|
-
this.emit('update', {message: this.status});
|
|
174
|
-
this.impulseResponses.push(
|
|
175
|
-
this.pyServerAPI
|
|
176
|
-
.getImpulseResponse({
|
|
177
|
-
sampleRate: this.sourceSamplingRate || 96000,
|
|
178
|
-
payload,
|
|
179
|
-
mls,
|
|
180
|
-
P: this.#P,
|
|
181
|
-
})
|
|
182
|
-
.then(res => {
|
|
183
|
-
if (this.numSuccessfulCaptured < this.numCaptures) {
|
|
184
|
-
this.numSuccessfulCaptured += 1;
|
|
185
|
-
console.log('num succ capt: ' + this.numSuccessfulCaptured);
|
|
186
|
-
this.stepNum += 1;
|
|
187
|
-
this.incrementStatusBar();
|
|
188
|
-
this.status =
|
|
189
|
-
`${this.numSuccessfulCaptured}/${this.numCaptures} IRs computed...`.toString() +
|
|
190
|
-
this.generateTemplate().toString();
|
|
191
|
-
this.emit('update', {message: this.status});
|
|
192
|
-
return res;
|
|
193
|
-
}
|
|
194
|
-
})
|
|
195
|
-
.catch(err => {
|
|
196
|
-
console.error(err);
|
|
197
|
-
})
|
|
198
|
-
);
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Passed to the calibration steps function, awaits the desired amount of seconds to capture the desired number
|
|
203
|
-
* of MLS periods defined in the constructor.
|
|
204
|
-
*
|
|
205
|
-
* @example
|
|
206
|
-
*/
|
|
207
|
-
#awaitDesiredMLSLength = async () => {
|
|
208
|
-
// seconds per MLS = P / SR
|
|
209
|
-
// await N * P / SR
|
|
210
|
-
this.stepNum += 1;
|
|
211
|
-
this.status =
|
|
212
|
-
`sampling the calibration signal...`.toString() + this.generateTemplate().toString();
|
|
213
|
-
this.emit('update', {message: this.status});
|
|
214
|
-
await sleep((this.#P / this.sourceSamplingRate) * this.numMLSPerCapture);
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
/** .
|
|
218
|
-
* .
|
|
219
|
-
* .
|
|
220
|
-
* Passed to the calibration steps function, awaits the onset of the signal to ensure a steady state
|
|
221
|
-
*
|
|
222
|
-
* @example
|
|
223
|
-
*/
|
|
224
|
-
#awaitSignalOnset = async () => {
|
|
225
|
-
this.stepNum += 1;
|
|
226
|
-
this.status =
|
|
227
|
-
`waiting for the signal to stabilize...`.toString() + this.generateTemplate().toString();
|
|
228
|
-
this.emit('update', {message: this.status});
|
|
229
|
-
await sleep(this.TAPER_SECS);
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Called immediately after a recording is captured. Used to process the resulting signal
|
|
234
|
-
* whether by sending the result to a server or by computing a result locally.
|
|
235
|
-
*
|
|
236
|
-
* @example
|
|
237
|
-
*/
|
|
238
|
-
#afterMLSRecord = () => {
|
|
239
|
-
console.log('after record');
|
|
240
|
-
this.sendRecordingToServerForProcessing();
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
#afterMLSwIIRRecord = () => {
|
|
244
|
-
if (this.numSuccessfulCaptured < 1) {
|
|
245
|
-
this.numSuccessfulCaptured += 1;
|
|
246
|
-
this.stepNum += 1;
|
|
247
|
-
this.incrementStatusBar();
|
|
248
|
-
this.status =
|
|
249
|
-
`${this.numSuccessfulCaptured} recording of convolved MLS captured`.toString() +
|
|
250
|
-
this.generateTemplate().toString();
|
|
251
|
-
this.emit('update', {message: this.status});
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
/** .
|
|
256
|
-
* .
|
|
257
|
-
* .
|
|
258
|
-
* Created an S Curver Buffer to taper the signal onset
|
|
259
|
-
*
|
|
260
|
-
* @param {*} length
|
|
261
|
-
* @param {*} phase
|
|
262
|
-
* @returns
|
|
263
|
-
* @example
|
|
264
|
-
*/
|
|
265
|
-
static createSCurveBuffer = (length, phase) => {
|
|
266
|
-
const curve = new Float32Array(length);
|
|
267
|
-
let i;
|
|
268
|
-
for (i = 0; i < length; i += 1) {
|
|
269
|
-
// scale the curve to be between 0-1
|
|
270
|
-
curve[i] = Math.sin((Math.PI * i) / length - phase) / 2 + 0.5;
|
|
271
|
-
}
|
|
272
|
-
return curve;
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
static createInverseSCurveBuffer = (length, phase) => {
|
|
276
|
-
const curve = new Float32Array(length);
|
|
277
|
-
let i;
|
|
278
|
-
let j = length - 1;
|
|
279
|
-
for (i = 0; i < length; i += 1) {
|
|
280
|
-
// scale the curve to be between 0-1
|
|
281
|
-
curve[i] = Math.sin((Math.PI * j) / length - phase) / 2 + 0.5;
|
|
282
|
-
j -= 1;
|
|
283
|
-
}
|
|
284
|
-
return curve;
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Construct a Calibration Node with the calibration parameters.
|
|
289
|
-
*
|
|
290
|
-
* @param CALIBRATION_TONE_FREQUENCY
|
|
291
|
-
* @private
|
|
292
|
-
* @example
|
|
293
|
-
*/
|
|
294
|
-
#createPureTonenNode = CALIBRATION_TONE_FREQUENCY => {
|
|
295
|
-
const audioContext = this.makeNewSourceAudioContext();
|
|
296
|
-
const oscilator = audioContext.createOscillator();
|
|
297
|
-
const gainNode = audioContext.createGain();
|
|
298
|
-
|
|
299
|
-
oscilator.frequency.value = CALIBRATION_TONE_FREQUENCY;
|
|
300
|
-
oscilator.type = 'sine';
|
|
301
|
-
gainNode.gain.value = 0.04;
|
|
302
|
-
|
|
303
|
-
oscilator.connect(gainNode);
|
|
304
|
-
gainNode.connect(audioContext.destination);
|
|
305
|
-
|
|
306
|
-
this.addCalibrationNode(oscilator);
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Construct a Calibration Node with the calibration parameters.
|
|
311
|
-
*
|
|
312
|
-
* @param dataBuffer
|
|
313
|
-
* @private
|
|
314
|
-
* @example
|
|
315
|
-
*/
|
|
316
|
-
#createCalibrationNodeFromBuffer = dataBuffer => {
|
|
317
|
-
const audioContext = this.makeNewSourceAudioContext();
|
|
318
|
-
const buffer = audioContext.createBuffer(
|
|
319
|
-
1, // number of channels
|
|
320
|
-
dataBuffer.length,
|
|
321
|
-
audioContext.sampleRate // sample rate
|
|
322
|
-
);
|
|
323
|
-
|
|
324
|
-
const data = buffer.getChannelData(0); // get data
|
|
325
|
-
// fill the buffer with our data
|
|
326
|
-
try {
|
|
327
|
-
for (let i = 0; i < dataBuffer.length; i += 1) {
|
|
328
|
-
data[i] = dataBuffer[i] * 0.1;
|
|
329
|
-
}
|
|
330
|
-
} catch (error) {
|
|
331
|
-
console.error(error);
|
|
332
|
-
}
|
|
333
|
-
console.log('mls second, same?');
|
|
334
|
-
console.log(data);
|
|
335
|
-
const onsetGainNode = audioContext.createGain();
|
|
336
|
-
this.offsetGainNode = audioContext.createGain();
|
|
337
|
-
const source = audioContext.createBufferSource();
|
|
338
|
-
|
|
339
|
-
source.buffer = buffer;
|
|
340
|
-
source.loop = true;
|
|
341
|
-
source.connect(onsetGainNode);
|
|
342
|
-
onsetGainNode.connect(this.offsetGainNode);
|
|
343
|
-
this.offsetGainNode.connect(audioContext.destination);
|
|
344
|
-
|
|
345
|
-
const onsetCurve = ImpulseResponse.createSCurveBuffer(this.sourceSamplingRate, Math.PI / 2);
|
|
346
|
-
onsetGainNode.gain.setValueCurveAtTime(onsetCurve, 0, this.TAPER_SECS);
|
|
347
|
-
this.addCalibrationNode(source);
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Given a data buffer, creates the required calibration node
|
|
352
|
-
*
|
|
353
|
-
* @param {*} dataBufferArray
|
|
354
|
-
* @example
|
|
355
|
-
*/
|
|
356
|
-
#setCalibrationNodesFromBuffer = (dataBufferArray = [this.#mlsBufferView]) => {
|
|
357
|
-
if (dataBufferArray.length === 1) {
|
|
358
|
-
this.#createCalibrationNodeFromBuffer(dataBufferArray[0]);
|
|
359
|
-
} else {
|
|
360
|
-
throw new Error('The length of the data buffer array must be 1');
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* function to put MLS filtered IIR data obtained from
|
|
366
|
-
* python server into our audio buffer to be played aloud
|
|
367
|
-
*/
|
|
368
|
-
#putInPythonConv = () => {
|
|
369
|
-
const audioCtx = this.makeNewSourceAudioContextConvolved();
|
|
370
|
-
const buffer = audioCtx.createBuffer(
|
|
371
|
-
1, // number of channels
|
|
372
|
-
this.convolution.length,
|
|
373
|
-
audioCtx.sampleRate // sample rate
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
const data = buffer.getChannelData(0); // get data
|
|
377
|
-
// fill the buffer with our data
|
|
378
|
-
try {
|
|
379
|
-
for (let i = 0; i < this.convolution.length; i += 1) {
|
|
380
|
-
data[i] = this.convolution[i];
|
|
381
|
-
}
|
|
382
|
-
} catch (error) {
|
|
383
|
-
console.error(error);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const source = audioCtx.createBufferSource();
|
|
387
|
-
|
|
388
|
-
source.buffer = buffer;
|
|
389
|
-
source.loop = true;
|
|
390
|
-
source.connect(audioCtx.destination);
|
|
391
|
-
|
|
392
|
-
this.addCalibrationNodeConvolved(source);
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Creates an audio context and plays it for a few seconds.
|
|
397
|
-
*
|
|
398
|
-
* @private
|
|
399
|
-
* @returns - Resolves when the audio is done playing.
|
|
400
|
-
* @example
|
|
401
|
-
*/
|
|
402
|
-
#playCalibrationAudio = () => {
|
|
403
|
-
this.calibrationNodes[0].start(0);
|
|
404
|
-
this.#mls = this.calibrationNodes[0].buffer.getChannelData(0);
|
|
405
|
-
this.stepNum += 1;
|
|
406
|
-
this.status = `playing the calibration tone...`.toString() + this.generateTemplate().toString();
|
|
407
|
-
this.emit('update', {message: this.status});
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
#playCalibrationAudioConvolved = () => {
|
|
411
|
-
this.calibrationNodesConvolved[0].start(0);
|
|
412
|
-
this.stepNum += 1;
|
|
413
|
-
this.status =
|
|
414
|
-
`playing the convolved calibration tone...`.toString() + this.generateTemplate().toString();
|
|
415
|
-
this.emit('update', {message: this.status});
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
/** .
|
|
419
|
-
* .
|
|
420
|
-
* .
|
|
421
|
-
* Stops the audio with tapered offset
|
|
422
|
-
*
|
|
423
|
-
* @example
|
|
424
|
-
*/
|
|
425
|
-
#stopCalibrationAudio = () => {
|
|
426
|
-
this.offsetGainNode.gain.setValueAtTime(
|
|
427
|
-
this.offsetGainNode.gain.value,
|
|
428
|
-
this.sourceAudioContext.currentTime
|
|
429
|
-
);
|
|
430
|
-
|
|
431
|
-
this.offsetGainNode.gain.setTargetAtTime(0, this.sourceAudioContext.currentTime, 0.5);
|
|
432
|
-
this.calibrationNodes[0].stop(0);
|
|
433
|
-
this.sourceAudioContext.close();
|
|
434
|
-
this.stepNum += 1;
|
|
435
|
-
this.status =
|
|
436
|
-
`stopping the calibration tone...`.toString() + this.generateTemplate().toString();
|
|
437
|
-
this.emit('update', {message: this.status});
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
#stopCalibrationAudioConvolved = () => {
|
|
441
|
-
this.offsetGainNode.gain.setValueAtTime(
|
|
442
|
-
this.offsetGainNode.gain.value,
|
|
443
|
-
this.sourceAudioContextConvolved.currentTime
|
|
444
|
-
);
|
|
445
|
-
|
|
446
|
-
this.offsetGainNode.gain.setTargetAtTime(0, this.sourceAudioContextConvolved.currentTime, 0.5);
|
|
447
|
-
this.sourceAudioContextConvolved.close();
|
|
448
|
-
this.stepNum += 1;
|
|
449
|
-
this.status =
|
|
450
|
-
`stopping the convolved calibration tone...`.toString() + this.generateTemplate().toString();
|
|
451
|
-
this.emit('update', {message: this.status});
|
|
452
|
-
};
|
|
453
|
-
|
|
454
|
-
playMLSwithIIR = async (stream, iir) => {
|
|
455
|
-
this.invertedImpulseResponse = iir;
|
|
456
|
-
// initialize the MLSGenInterface object with it's factory method
|
|
457
|
-
|
|
458
|
-
await MlsGenInterface.factory(
|
|
459
|
-
this.#mlsOrder,
|
|
460
|
-
this.sinkSamplingRate,
|
|
461
|
-
this.sourceSamplingRate
|
|
462
|
-
).then(mlsGenInterface => {
|
|
463
|
-
this.#mlsGenInterface = mlsGenInterface;
|
|
464
|
-
this.#mlsBufferView = this.#mlsGenInterface.getMLS();
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
// after intializating, start the calibration steps with garbage collection
|
|
468
|
-
await this.#mlsGenInterface.withGarbageCollection([
|
|
469
|
-
() =>
|
|
470
|
-
this.calibrationSteps(
|
|
471
|
-
stream,
|
|
472
|
-
this.#playCalibrationAudioConvolved, // play audio func (required)
|
|
473
|
-
this.#putInPythonConv, // before play func
|
|
474
|
-
this.#awaitSignalOnset, // before record
|
|
475
|
-
() => this.numSuccessfulCaptured < 1, // < this.numCaptures
|
|
476
|
-
this.#awaitDesiredMLSLength, // during record
|
|
477
|
-
this.#afterMLSwIIRRecord, // after record
|
|
478
|
-
'filtered'
|
|
479
|
-
),
|
|
480
|
-
]);
|
|
481
|
-
};
|
|
482
|
-
|
|
483
|
-
// function to write frq and gain to firebase database given speakerID
|
|
484
|
-
writeFrqGain = async (speakerID, frq, gain) => {
|
|
485
|
-
const data = {
|
|
486
|
-
frq,
|
|
487
|
-
gain,
|
|
488
|
-
};
|
|
489
|
-
await set(ref(database, `${speakerID}/linear`), data);
|
|
490
|
-
};
|
|
491
|
-
|
|
492
|
-
// Function to Read frq and gain from firebase database given speakerID
|
|
493
|
-
// returns an array of frq and gain if speakerID exists, returns null otherwise
|
|
494
|
-
|
|
495
|
-
readFrqGain = async speakerID => {
|
|
496
|
-
const dbRef = ref(database);
|
|
497
|
-
const snapshot = await get(child(dbRef, `${speakerID}/linear`));
|
|
498
|
-
if (snapshot.exists()) {
|
|
499
|
-
return snapshot.val();
|
|
500
|
-
}
|
|
501
|
-
return null;
|
|
502
|
-
};
|
|
503
|
-
|
|
504
|
-
// Example of how to use the writeFrqGain and readFrqGain functions
|
|
505
|
-
// writeFrqGain('speaker1', [1, 2, 3], [4, 5, 6]);
|
|
506
|
-
// Speaker1 is the speakerID you want to write to in the database
|
|
507
|
-
// readFrqGain('MiniDSPUMIK_1').then(data => console.log(data));
|
|
508
|
-
// MiniDSPUMIK_1 is the speakerID with some Data in the database
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Public method to start the calibration process. Objects intialized from webassembly allocate new memory
|
|
512
|
-
* and must be manually freed. This function is responsible for intializing the MlsGenInterface,
|
|
513
|
-
* and wrapping the calibration steps with a garbage collection safe gaurd.
|
|
514
|
-
*
|
|
515
|
-
* @public
|
|
516
|
-
* @param stream - The stream of audio from the Listener.
|
|
517
|
-
* @example
|
|
518
|
-
*/
|
|
519
|
-
|
|
520
|
-
startCalibration = async stream => {
|
|
521
|
-
// initialize the MLSGenInterface object with it's factory method
|
|
522
|
-
await MlsGenInterface.factory(
|
|
523
|
-
this.#mlsOrder,
|
|
524
|
-
this.sinkSamplingRate,
|
|
525
|
-
this.sourceSamplingRate
|
|
526
|
-
).then(mlsGenInterface => {
|
|
527
|
-
this.#mlsGenInterface = mlsGenInterface;
|
|
528
|
-
this.#mlsBufferView = this.#mlsGenInterface.getMLS();
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
// after intializating, start the calibration steps with garbage collection
|
|
532
|
-
await this.#mlsGenInterface.withGarbageCollection([
|
|
533
|
-
() =>
|
|
534
|
-
this.calibrationSteps(
|
|
535
|
-
stream,
|
|
536
|
-
this.#playCalibrationAudio, // play audio func (required)
|
|
537
|
-
this.#setCalibrationNodesFromBuffer, // before play func
|
|
538
|
-
this.#awaitSignalOnset, // before record
|
|
539
|
-
() => this.numSuccessfulCaptured < this.numCaptures, // loop while true
|
|
540
|
-
this.#awaitDesiredMLSLength, // during record
|
|
541
|
-
this.#afterMLSRecord, // after record
|
|
542
|
-
'unfiltered'
|
|
543
|
-
),
|
|
544
|
-
]);
|
|
545
|
-
|
|
546
|
-
this.#stopCalibrationAudio();
|
|
547
|
-
|
|
548
|
-
// at this stage we've captured all the required signals,
|
|
549
|
-
// and have received IRs for each one
|
|
550
|
-
// so let's send all the IRs to the server to be converted to a single IIR
|
|
551
|
-
|
|
552
|
-
await this.sendImpulseResponsesToServerForProcessing();
|
|
553
|
-
|
|
554
|
-
this.numSuccessfulCaptured = 0;
|
|
555
|
-
// debugging function, use to test the result of the IIR
|
|
556
|
-
await this.playMLSwithIIR(stream, this.invertedImpulseResponse);
|
|
557
|
-
this.#stopCalibrationAudioConvolved();
|
|
558
|
-
|
|
559
|
-
let recs = this.getAllRecordedSignals();
|
|
560
|
-
let conv_recs = this.getAllFilteredRecordedSignals();
|
|
561
|
-
let unconv_rec = recs[0];
|
|
562
|
-
let conv_rec = conv_recs[0];
|
|
563
|
-
|
|
564
|
-
this.status = `computing the PSD graphs...`.toString() + this.generateTemplate().toString();
|
|
565
|
-
this.emit('update', {message: this.status});
|
|
566
|
-
|
|
567
|
-
let results = await this.pyServerAPI
|
|
568
|
-
.getPSDWithRetry({
|
|
569
|
-
unconv_rec,
|
|
570
|
-
conv_rec,
|
|
571
|
-
})
|
|
572
|
-
.then(res => {
|
|
573
|
-
this.incrementStatusBar();
|
|
574
|
-
this.status =
|
|
575
|
-
`done computing the PSD graphs`.toString() + this.generateTemplate().toString();
|
|
576
|
-
this.emit('update', {message: this.status});
|
|
577
|
-
return res;
|
|
578
|
-
})
|
|
579
|
-
.catch(err => {
|
|
580
|
-
console.error(err);
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
let iir_and_plots = {
|
|
584
|
-
iir: this.invertedImpulseResponse,
|
|
585
|
-
x_unconv: results['x_unconv'],
|
|
586
|
-
y_unconv: results['y_unconv'],
|
|
587
|
-
x_conv: results['x_conv'],
|
|
588
|
-
y_conv: results['y_conv'],
|
|
589
|
-
};
|
|
590
|
-
|
|
591
|
-
if (this.#download) {
|
|
592
|
-
this.downloadSingleUnfilteredRecording();
|
|
593
|
-
this.downloadSingleFilteredRecording();
|
|
594
|
-
saveToCSV(this.#mls, 'MLS.csv');
|
|
595
|
-
saveToCSV(this.convolution, 'python_convolution_mls_iir.csv');
|
|
596
|
-
saveToCSV(this.invertedImpulseResponse, 'IIR.csv');
|
|
597
|
-
const computedIRagain = await Promise.all(this.impulseResponses).then(res => {
|
|
598
|
-
for (let i = 0; i < res.length; i++) {
|
|
599
|
-
if (res[i] != undefined) {
|
|
600
|
-
saveToCSV(res[i], `IR_${i}`);
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
return iir_and_plots;
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
export default ImpulseResponse;
|
|
1
|
+
import AudioCalibrator from '../audioCalibrator';
|
|
2
|
+
import MlsGenInterface from './mlsGen/mlsGenInterface';
|
|
3
|
+
|
|
4
|
+
import {sleep, csvToArray, saveToCSV} from '../../utils';
|
|
5
|
+
import database from '../../config/firebase';
|
|
6
|
+
import {ref, set, get, child} from 'firebase/database';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
*
|
|
10
|
+
*/
|
|
11
|
+
class ImpulseResponse extends AudioCalibrator {
|
|
12
|
+
/**
|
|
13
|
+
* Default constructor. Creates an instance with any number of paramters passed or the default parameters defined here.
|
|
14
|
+
*
|
|
15
|
+
* @param {Object<boolean, number, number, number>} calibratorParams - paramter object
|
|
16
|
+
* @param {boolean} [calibratorParams.download = false] - boolean flag to download captures
|
|
17
|
+
* @param {number} [calibratorParams.mlsOrder = 18] - order of the MLS to be generated
|
|
18
|
+
* @param {number} [calibratorParams.numCaptures = 5] - number of captures to perform
|
|
19
|
+
* @param {number} [calibratorParams.numMLSPerCapture = 4] - number of bursts of MLS per capture
|
|
20
|
+
*/
|
|
21
|
+
constructor({
|
|
22
|
+
download = false,
|
|
23
|
+
mlsOrder = 18,
|
|
24
|
+
numCaptures = 3,
|
|
25
|
+
numMLSPerCapture = 4,
|
|
26
|
+
lowHz = 20,
|
|
27
|
+
highHz = 10000,
|
|
28
|
+
loudSpeakerIr = null,
|
|
29
|
+
}) {
|
|
30
|
+
super(numCaptures, numMLSPerCapture);
|
|
31
|
+
this.#mlsOrder = parseInt(mlsOrder, 10);
|
|
32
|
+
this.#P = 2 ** mlsOrder - 1;
|
|
33
|
+
this.#download = download;
|
|
34
|
+
this.#mls = [];
|
|
35
|
+
this.#lowHz = lowHz;
|
|
36
|
+
this.#highHz = highHz;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @private */
|
|
40
|
+
stepNum = 0;
|
|
41
|
+
|
|
42
|
+
/** @private */
|
|
43
|
+
totalSteps = 25;
|
|
44
|
+
|
|
45
|
+
/** @private */
|
|
46
|
+
#download;
|
|
47
|
+
|
|
48
|
+
/** @private */
|
|
49
|
+
#mlsGenInterface;
|
|
50
|
+
|
|
51
|
+
/** @private */
|
|
52
|
+
#mlsBufferView;
|
|
53
|
+
|
|
54
|
+
/** @private */
|
|
55
|
+
invertedImpulseResponse = null;
|
|
56
|
+
|
|
57
|
+
/** @private */
|
|
58
|
+
impulseResponses = [];
|
|
59
|
+
|
|
60
|
+
/** @private */
|
|
61
|
+
#mlsOrder;
|
|
62
|
+
|
|
63
|
+
/** @private */
|
|
64
|
+
#lowHz;
|
|
65
|
+
|
|
66
|
+
/** @private */
|
|
67
|
+
#highHz;
|
|
68
|
+
|
|
69
|
+
/** @private */
|
|
70
|
+
#mls;
|
|
71
|
+
|
|
72
|
+
/** @private */
|
|
73
|
+
#P;
|
|
74
|
+
|
|
75
|
+
/** @private */
|
|
76
|
+
#audioContext;
|
|
77
|
+
|
|
78
|
+
/** @private */
|
|
79
|
+
TAPER_SECS = 5;
|
|
80
|
+
|
|
81
|
+
/** @private */
|
|
82
|
+
offsetGainNode;
|
|
83
|
+
|
|
84
|
+
/** @private */
|
|
85
|
+
convolution;
|
|
86
|
+
|
|
87
|
+
/** @private */
|
|
88
|
+
status_denominator = 6;
|
|
89
|
+
|
|
90
|
+
/** @private */
|
|
91
|
+
status_numerator = 0;
|
|
92
|
+
|
|
93
|
+
/** @private */
|
|
94
|
+
percent_complete = 0;
|
|
95
|
+
|
|
96
|
+
/** @private */
|
|
97
|
+
status = ``;
|
|
98
|
+
|
|
99
|
+
/**generate string template that gets reevaluated as variable increases */
|
|
100
|
+
generateTemplate = () => {
|
|
101
|
+
if (this.percent_complete > 100) {
|
|
102
|
+
this.percent_complete = 100;
|
|
103
|
+
}
|
|
104
|
+
const template = `<div style="display: flex; justify-content: center;"><div style="width: 200px; height: 20px; border: 2px solid #000; border-radius: 10px;"><div style="width: ${this.percent_complete}%; height: 100%; background-color: #00aaff; border-radius: 8px;"></div></div></div>`;
|
|
105
|
+
return template;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** increment numerator and percent for status bar */
|
|
109
|
+
incrementStatusBar = () => {
|
|
110
|
+
this.status_numerator += 1;
|
|
111
|
+
this.percent_complete = (this.status_numerator / this.status_denominator) * 100;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** .
|
|
115
|
+
* .
|
|
116
|
+
* .
|
|
117
|
+
* Sends all the computed impulse responses to the backend server for processing
|
|
118
|
+
*
|
|
119
|
+
* @returns sets the resulting inverted impulse response to the class property
|
|
120
|
+
* @example
|
|
121
|
+
*/
|
|
122
|
+
sendImpulseResponsesToServerForProcessing = async () => {
|
|
123
|
+
const computedIRs = await Promise.all(this.impulseResponses);
|
|
124
|
+
const filteredComputedIRs = computedIRs.filter(element => {
|
|
125
|
+
return element != undefined;
|
|
126
|
+
});
|
|
127
|
+
const mls = this.#mls;
|
|
128
|
+
const lowHz = this.#lowHz;
|
|
129
|
+
const highHz = this.#highHz;
|
|
130
|
+
this.stepNum += 1;
|
|
131
|
+
this.status = `computing the IIR...`.toString() + this.generateTemplate().toString();
|
|
132
|
+
this.emit('update', {message: this.status});
|
|
133
|
+
return this.pyServerAPI
|
|
134
|
+
.getInverseImpulseResponseWithRetry({
|
|
135
|
+
payload: filteredComputedIRs.slice(0, this.numCaptures),
|
|
136
|
+
mls,
|
|
137
|
+
lowHz,
|
|
138
|
+
highHz,
|
|
139
|
+
})
|
|
140
|
+
.then(res => {
|
|
141
|
+
console.log(res);
|
|
142
|
+
this.stepNum += 1;
|
|
143
|
+
this.incrementStatusBar();
|
|
144
|
+
this.status = `done computing the IIR...`.toString() + this.generateTemplate().toString();
|
|
145
|
+
this.emit('update', {message: this.status});
|
|
146
|
+
this.invertedImpulseResponse = res['iir'];
|
|
147
|
+
this.convolution = res['convolution'];
|
|
148
|
+
})
|
|
149
|
+
.catch(err => {
|
|
150
|
+
// this.emit('InvertedImpulseResponse', {res: false});
|
|
151
|
+
console.error(err);
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/** .
|
|
156
|
+
* .
|
|
157
|
+
* .
|
|
158
|
+
* Sends the recorded signal, or a given csv string of a signal, to the back end server for processing
|
|
159
|
+
*
|
|
160
|
+
* @param {<array>String} signalCsv - Optional csv string of a previously recorded signal, if given, this signal will be processed
|
|
161
|
+
* @example
|
|
162
|
+
*/
|
|
163
|
+
sendRecordingToServerForProcessing = signalCsv => {
|
|
164
|
+
const allSignals = this.getAllRecordedSignals();
|
|
165
|
+
const numSignals = allSignals.length;
|
|
166
|
+
const mls = this.#mls;
|
|
167
|
+
const payload =
|
|
168
|
+
signalCsv && signalCsv.length > 0 ? csvToArray(signalCsv) : allSignals[numSignals - 1];
|
|
169
|
+
console.log('sending rec');
|
|
170
|
+
this.stepNum += 1;
|
|
171
|
+
this.status =
|
|
172
|
+
`computing the IR of the last recording...`.toString() + this.generateTemplate().toString();
|
|
173
|
+
this.emit('update', {message: this.status});
|
|
174
|
+
this.impulseResponses.push(
|
|
175
|
+
this.pyServerAPI
|
|
176
|
+
.getImpulseResponse({
|
|
177
|
+
sampleRate: this.sourceSamplingRate || 96000,
|
|
178
|
+
payload,
|
|
179
|
+
mls,
|
|
180
|
+
P: this.#P,
|
|
181
|
+
})
|
|
182
|
+
.then(res => {
|
|
183
|
+
if (this.numSuccessfulCaptured < this.numCaptures) {
|
|
184
|
+
this.numSuccessfulCaptured += 1;
|
|
185
|
+
console.log('num succ capt: ' + this.numSuccessfulCaptured);
|
|
186
|
+
this.stepNum += 1;
|
|
187
|
+
this.incrementStatusBar();
|
|
188
|
+
this.status =
|
|
189
|
+
`${this.numSuccessfulCaptured}/${this.numCaptures} IRs computed...`.toString() +
|
|
190
|
+
this.generateTemplate().toString();
|
|
191
|
+
this.emit('update', {message: this.status});
|
|
192
|
+
return res;
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
.catch(err => {
|
|
196
|
+
console.error(err);
|
|
197
|
+
})
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Passed to the calibration steps function, awaits the desired amount of seconds to capture the desired number
|
|
203
|
+
* of MLS periods defined in the constructor.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
*/
|
|
207
|
+
#awaitDesiredMLSLength = async () => {
|
|
208
|
+
// seconds per MLS = P / SR
|
|
209
|
+
// await N * P / SR
|
|
210
|
+
this.stepNum += 1;
|
|
211
|
+
this.status =
|
|
212
|
+
`sampling the calibration signal...`.toString() + this.generateTemplate().toString();
|
|
213
|
+
this.emit('update', {message: this.status});
|
|
214
|
+
await sleep((this.#P / this.sourceSamplingRate) * this.numMLSPerCapture);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/** .
|
|
218
|
+
* .
|
|
219
|
+
* .
|
|
220
|
+
* Passed to the calibration steps function, awaits the onset of the signal to ensure a steady state
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
*/
|
|
224
|
+
#awaitSignalOnset = async () => {
|
|
225
|
+
this.stepNum += 1;
|
|
226
|
+
this.status =
|
|
227
|
+
`waiting for the signal to stabilize...`.toString() + this.generateTemplate().toString();
|
|
228
|
+
this.emit('update', {message: this.status});
|
|
229
|
+
await sleep(this.TAPER_SECS);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Called immediately after a recording is captured. Used to process the resulting signal
|
|
234
|
+
* whether by sending the result to a server or by computing a result locally.
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
*/
|
|
238
|
+
#afterMLSRecord = () => {
|
|
239
|
+
console.log('after record');
|
|
240
|
+
this.sendRecordingToServerForProcessing();
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
#afterMLSwIIRRecord = () => {
|
|
244
|
+
if (this.numSuccessfulCaptured < 1) {
|
|
245
|
+
this.numSuccessfulCaptured += 1;
|
|
246
|
+
this.stepNum += 1;
|
|
247
|
+
this.incrementStatusBar();
|
|
248
|
+
this.status =
|
|
249
|
+
`${this.numSuccessfulCaptured} recording of convolved MLS captured`.toString() +
|
|
250
|
+
this.generateTemplate().toString();
|
|
251
|
+
this.emit('update', {message: this.status});
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/** .
|
|
256
|
+
* .
|
|
257
|
+
* .
|
|
258
|
+
* Created an S Curver Buffer to taper the signal onset
|
|
259
|
+
*
|
|
260
|
+
* @param {*} length
|
|
261
|
+
* @param {*} phase
|
|
262
|
+
* @returns
|
|
263
|
+
* @example
|
|
264
|
+
*/
|
|
265
|
+
static createSCurveBuffer = (length, phase) => {
|
|
266
|
+
const curve = new Float32Array(length);
|
|
267
|
+
let i;
|
|
268
|
+
for (i = 0; i < length; i += 1) {
|
|
269
|
+
// scale the curve to be between 0-1
|
|
270
|
+
curve[i] = Math.sin((Math.PI * i) / length - phase) / 2 + 0.5;
|
|
271
|
+
}
|
|
272
|
+
return curve;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
static createInverseSCurveBuffer = (length, phase) => {
|
|
276
|
+
const curve = new Float32Array(length);
|
|
277
|
+
let i;
|
|
278
|
+
let j = length - 1;
|
|
279
|
+
for (i = 0; i < length; i += 1) {
|
|
280
|
+
// scale the curve to be between 0-1
|
|
281
|
+
curve[i] = Math.sin((Math.PI * j) / length - phase) / 2 + 0.5;
|
|
282
|
+
j -= 1;
|
|
283
|
+
}
|
|
284
|
+
return curve;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Construct a Calibration Node with the calibration parameters.
|
|
289
|
+
*
|
|
290
|
+
* @param CALIBRATION_TONE_FREQUENCY
|
|
291
|
+
* @private
|
|
292
|
+
* @example
|
|
293
|
+
*/
|
|
294
|
+
#createPureTonenNode = CALIBRATION_TONE_FREQUENCY => {
|
|
295
|
+
const audioContext = this.makeNewSourceAudioContext();
|
|
296
|
+
const oscilator = audioContext.createOscillator();
|
|
297
|
+
const gainNode = audioContext.createGain();
|
|
298
|
+
|
|
299
|
+
oscilator.frequency.value = CALIBRATION_TONE_FREQUENCY;
|
|
300
|
+
oscilator.type = 'sine';
|
|
301
|
+
gainNode.gain.value = 0.04;
|
|
302
|
+
|
|
303
|
+
oscilator.connect(gainNode);
|
|
304
|
+
gainNode.connect(audioContext.destination);
|
|
305
|
+
|
|
306
|
+
this.addCalibrationNode(oscilator);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Construct a Calibration Node with the calibration parameters.
|
|
311
|
+
*
|
|
312
|
+
* @param dataBuffer
|
|
313
|
+
* @private
|
|
314
|
+
* @example
|
|
315
|
+
*/
|
|
316
|
+
#createCalibrationNodeFromBuffer = dataBuffer => {
|
|
317
|
+
const audioContext = this.makeNewSourceAudioContext();
|
|
318
|
+
const buffer = audioContext.createBuffer(
|
|
319
|
+
1, // number of channels
|
|
320
|
+
dataBuffer.length,
|
|
321
|
+
audioContext.sampleRate // sample rate
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const data = buffer.getChannelData(0); // get data
|
|
325
|
+
// fill the buffer with our data
|
|
326
|
+
try {
|
|
327
|
+
for (let i = 0; i < dataBuffer.length; i += 1) {
|
|
328
|
+
data[i] = dataBuffer[i] * 0.1;
|
|
329
|
+
}
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error(error);
|
|
332
|
+
}
|
|
333
|
+
console.log('mls second, same?');
|
|
334
|
+
console.log(data);
|
|
335
|
+
const onsetGainNode = audioContext.createGain();
|
|
336
|
+
this.offsetGainNode = audioContext.createGain();
|
|
337
|
+
const source = audioContext.createBufferSource();
|
|
338
|
+
|
|
339
|
+
source.buffer = buffer;
|
|
340
|
+
source.loop = true;
|
|
341
|
+
source.connect(onsetGainNode);
|
|
342
|
+
onsetGainNode.connect(this.offsetGainNode);
|
|
343
|
+
this.offsetGainNode.connect(audioContext.destination);
|
|
344
|
+
|
|
345
|
+
const onsetCurve = ImpulseResponse.createSCurveBuffer(this.sourceSamplingRate, Math.PI / 2);
|
|
346
|
+
onsetGainNode.gain.setValueCurveAtTime(onsetCurve, 0, this.TAPER_SECS);
|
|
347
|
+
this.addCalibrationNode(source);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Given a data buffer, creates the required calibration node
|
|
352
|
+
*
|
|
353
|
+
* @param {*} dataBufferArray
|
|
354
|
+
* @example
|
|
355
|
+
*/
|
|
356
|
+
#setCalibrationNodesFromBuffer = (dataBufferArray = [this.#mlsBufferView]) => {
|
|
357
|
+
if (dataBufferArray.length === 1) {
|
|
358
|
+
this.#createCalibrationNodeFromBuffer(dataBufferArray[0]);
|
|
359
|
+
} else {
|
|
360
|
+
throw new Error('The length of the data buffer array must be 1');
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* function to put MLS filtered IIR data obtained from
|
|
366
|
+
* python server into our audio buffer to be played aloud
|
|
367
|
+
*/
|
|
368
|
+
#putInPythonConv = () => {
|
|
369
|
+
const audioCtx = this.makeNewSourceAudioContextConvolved();
|
|
370
|
+
const buffer = audioCtx.createBuffer(
|
|
371
|
+
1, // number of channels
|
|
372
|
+
this.convolution.length,
|
|
373
|
+
audioCtx.sampleRate // sample rate
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
const data = buffer.getChannelData(0); // get data
|
|
377
|
+
// fill the buffer with our data
|
|
378
|
+
try {
|
|
379
|
+
for (let i = 0; i < this.convolution.length; i += 1) {
|
|
380
|
+
data[i] = this.convolution[i];
|
|
381
|
+
}
|
|
382
|
+
} catch (error) {
|
|
383
|
+
console.error(error);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const source = audioCtx.createBufferSource();
|
|
387
|
+
|
|
388
|
+
source.buffer = buffer;
|
|
389
|
+
source.loop = true;
|
|
390
|
+
source.connect(audioCtx.destination);
|
|
391
|
+
|
|
392
|
+
this.addCalibrationNodeConvolved(source);
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Creates an audio context and plays it for a few seconds.
|
|
397
|
+
*
|
|
398
|
+
* @private
|
|
399
|
+
* @returns - Resolves when the audio is done playing.
|
|
400
|
+
* @example
|
|
401
|
+
*/
|
|
402
|
+
#playCalibrationAudio = () => {
|
|
403
|
+
this.calibrationNodes[0].start(0);
|
|
404
|
+
this.#mls = this.calibrationNodes[0].buffer.getChannelData(0);
|
|
405
|
+
this.stepNum += 1;
|
|
406
|
+
this.status = `playing the calibration tone...`.toString() + this.generateTemplate().toString();
|
|
407
|
+
this.emit('update', {message: this.status});
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
#playCalibrationAudioConvolved = () => {
|
|
411
|
+
this.calibrationNodesConvolved[0].start(0);
|
|
412
|
+
this.stepNum += 1;
|
|
413
|
+
this.status =
|
|
414
|
+
`playing the convolved calibration tone...`.toString() + this.generateTemplate().toString();
|
|
415
|
+
this.emit('update', {message: this.status});
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
/** .
|
|
419
|
+
* .
|
|
420
|
+
* .
|
|
421
|
+
* Stops the audio with tapered offset
|
|
422
|
+
*
|
|
423
|
+
* @example
|
|
424
|
+
*/
|
|
425
|
+
#stopCalibrationAudio = () => {
|
|
426
|
+
this.offsetGainNode.gain.setValueAtTime(
|
|
427
|
+
this.offsetGainNode.gain.value,
|
|
428
|
+
this.sourceAudioContext.currentTime
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
this.offsetGainNode.gain.setTargetAtTime(0, this.sourceAudioContext.currentTime, 0.5);
|
|
432
|
+
this.calibrationNodes[0].stop(0);
|
|
433
|
+
this.sourceAudioContext.close();
|
|
434
|
+
this.stepNum += 1;
|
|
435
|
+
this.status =
|
|
436
|
+
`stopping the calibration tone...`.toString() + this.generateTemplate().toString();
|
|
437
|
+
this.emit('update', {message: this.status});
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
#stopCalibrationAudioConvolved = () => {
|
|
441
|
+
this.offsetGainNode.gain.setValueAtTime(
|
|
442
|
+
this.offsetGainNode.gain.value,
|
|
443
|
+
this.sourceAudioContextConvolved.currentTime
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
this.offsetGainNode.gain.setTargetAtTime(0, this.sourceAudioContextConvolved.currentTime, 0.5);
|
|
447
|
+
this.sourceAudioContextConvolved.close();
|
|
448
|
+
this.stepNum += 1;
|
|
449
|
+
this.status =
|
|
450
|
+
`stopping the convolved calibration tone...`.toString() + this.generateTemplate().toString();
|
|
451
|
+
this.emit('update', {message: this.status});
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
playMLSwithIIR = async (stream, iir) => {
|
|
455
|
+
this.invertedImpulseResponse = iir;
|
|
456
|
+
// initialize the MLSGenInterface object with it's factory method
|
|
457
|
+
|
|
458
|
+
await MlsGenInterface.factory(
|
|
459
|
+
this.#mlsOrder,
|
|
460
|
+
this.sinkSamplingRate,
|
|
461
|
+
this.sourceSamplingRate
|
|
462
|
+
).then(mlsGenInterface => {
|
|
463
|
+
this.#mlsGenInterface = mlsGenInterface;
|
|
464
|
+
this.#mlsBufferView = this.#mlsGenInterface.getMLS();
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// after intializating, start the calibration steps with garbage collection
|
|
468
|
+
await this.#mlsGenInterface.withGarbageCollection([
|
|
469
|
+
() =>
|
|
470
|
+
this.calibrationSteps(
|
|
471
|
+
stream,
|
|
472
|
+
this.#playCalibrationAudioConvolved, // play audio func (required)
|
|
473
|
+
this.#putInPythonConv, // before play func
|
|
474
|
+
this.#awaitSignalOnset, // before record
|
|
475
|
+
() => this.numSuccessfulCaptured < 1, // < this.numCaptures
|
|
476
|
+
this.#awaitDesiredMLSLength, // during record
|
|
477
|
+
this.#afterMLSwIIRRecord, // after record
|
|
478
|
+
'filtered'
|
|
479
|
+
),
|
|
480
|
+
]);
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
// function to write frq and gain to firebase database given speakerID
|
|
484
|
+
writeFrqGain = async (speakerID, frq, gain) => {
|
|
485
|
+
const data = {
|
|
486
|
+
frq,
|
|
487
|
+
gain,
|
|
488
|
+
};
|
|
489
|
+
await set(ref(database, `${speakerID}/linear`), data);
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// Function to Read frq and gain from firebase database given speakerID
|
|
493
|
+
// returns an array of frq and gain if speakerID exists, returns null otherwise
|
|
494
|
+
|
|
495
|
+
readFrqGain = async speakerID => {
|
|
496
|
+
const dbRef = ref(database);
|
|
497
|
+
const snapshot = await get(child(dbRef, `${speakerID}/linear`));
|
|
498
|
+
if (snapshot.exists()) {
|
|
499
|
+
return snapshot.val();
|
|
500
|
+
}
|
|
501
|
+
return null;
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
// Example of how to use the writeFrqGain and readFrqGain functions
|
|
505
|
+
// writeFrqGain('speaker1', [1, 2, 3], [4, 5, 6]);
|
|
506
|
+
// Speaker1 is the speakerID you want to write to in the database
|
|
507
|
+
// readFrqGain('MiniDSPUMIK_1').then(data => console.log(data));
|
|
508
|
+
// MiniDSPUMIK_1 is the speakerID with some Data in the database
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Public method to start the calibration process. Objects intialized from webassembly allocate new memory
|
|
512
|
+
* and must be manually freed. This function is responsible for intializing the MlsGenInterface,
|
|
513
|
+
* and wrapping the calibration steps with a garbage collection safe gaurd.
|
|
514
|
+
*
|
|
515
|
+
* @public
|
|
516
|
+
* @param stream - The stream of audio from the Listener.
|
|
517
|
+
* @example
|
|
518
|
+
*/
|
|
519
|
+
|
|
520
|
+
startCalibration = async stream => {
|
|
521
|
+
// initialize the MLSGenInterface object with it's factory method
|
|
522
|
+
await MlsGenInterface.factory(
|
|
523
|
+
this.#mlsOrder,
|
|
524
|
+
this.sinkSamplingRate,
|
|
525
|
+
this.sourceSamplingRate
|
|
526
|
+
).then(mlsGenInterface => {
|
|
527
|
+
this.#mlsGenInterface = mlsGenInterface;
|
|
528
|
+
this.#mlsBufferView = this.#mlsGenInterface.getMLS();
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// after intializating, start the calibration steps with garbage collection
|
|
532
|
+
await this.#mlsGenInterface.withGarbageCollection([
|
|
533
|
+
() =>
|
|
534
|
+
this.calibrationSteps(
|
|
535
|
+
stream,
|
|
536
|
+
this.#playCalibrationAudio, // play audio func (required)
|
|
537
|
+
this.#setCalibrationNodesFromBuffer, // before play func
|
|
538
|
+
this.#awaitSignalOnset, // before record
|
|
539
|
+
() => this.numSuccessfulCaptured < this.numCaptures, // loop while true
|
|
540
|
+
this.#awaitDesiredMLSLength, // during record
|
|
541
|
+
this.#afterMLSRecord, // after record
|
|
542
|
+
'unfiltered'
|
|
543
|
+
),
|
|
544
|
+
]);
|
|
545
|
+
|
|
546
|
+
this.#stopCalibrationAudio();
|
|
547
|
+
|
|
548
|
+
// at this stage we've captured all the required signals,
|
|
549
|
+
// and have received IRs for each one
|
|
550
|
+
// so let's send all the IRs to the server to be converted to a single IIR
|
|
551
|
+
|
|
552
|
+
await this.sendImpulseResponsesToServerForProcessing();
|
|
553
|
+
|
|
554
|
+
this.numSuccessfulCaptured = 0;
|
|
555
|
+
// debugging function, use to test the result of the IIR
|
|
556
|
+
await this.playMLSwithIIR(stream, this.invertedImpulseResponse);
|
|
557
|
+
this.#stopCalibrationAudioConvolved();
|
|
558
|
+
|
|
559
|
+
let recs = this.getAllRecordedSignals();
|
|
560
|
+
let conv_recs = this.getAllFilteredRecordedSignals();
|
|
561
|
+
let unconv_rec = recs[0];
|
|
562
|
+
let conv_rec = conv_recs[0];
|
|
563
|
+
|
|
564
|
+
this.status = `computing the PSD graphs...`.toString() + this.generateTemplate().toString();
|
|
565
|
+
this.emit('update', {message: this.status});
|
|
566
|
+
|
|
567
|
+
let results = await this.pyServerAPI
|
|
568
|
+
.getPSDWithRetry({
|
|
569
|
+
unconv_rec,
|
|
570
|
+
conv_rec,
|
|
571
|
+
})
|
|
572
|
+
.then(res => {
|
|
573
|
+
this.incrementStatusBar();
|
|
574
|
+
this.status =
|
|
575
|
+
`done computing the PSD graphs`.toString() + this.generateTemplate().toString();
|
|
576
|
+
this.emit('update', {message: this.status});
|
|
577
|
+
return res;
|
|
578
|
+
})
|
|
579
|
+
.catch(err => {
|
|
580
|
+
console.error(err);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
let iir_and_plots = {
|
|
584
|
+
iir: this.invertedImpulseResponse,
|
|
585
|
+
x_unconv: results['x_unconv'],
|
|
586
|
+
y_unconv: results['y_unconv'],
|
|
587
|
+
x_conv: results['x_conv'],
|
|
588
|
+
y_conv: results['y_conv'],
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
if (this.#download) {
|
|
592
|
+
this.downloadSingleUnfilteredRecording();
|
|
593
|
+
this.downloadSingleFilteredRecording();
|
|
594
|
+
saveToCSV(this.#mls, 'MLS.csv');
|
|
595
|
+
saveToCSV(this.convolution, 'python_convolution_mls_iir.csv');
|
|
596
|
+
saveToCSV(this.invertedImpulseResponse, 'IIR.csv');
|
|
597
|
+
const computedIRagain = await Promise.all(this.impulseResponses).then(res => {
|
|
598
|
+
for (let i = 0; i < res.length; i++) {
|
|
599
|
+
if (res[i] != undefined) {
|
|
600
|
+
saveToCSV(res[i], `IR_${i}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return iir_and_plots;
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export default ImpulseResponse;
|