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.
@@ -0,0 +1,772 @@
1
+ import AudioCalibrator from '../audioCalibrator';
2
+ import MlsGenInterface from './mlsGen/mlsGenInterface';
3
+
4
+ import {sleep, csvToArray, saveToCSV} from '../../utils';
5
+
6
+ /**
7
+ *
8
+ */
9
+ class Combination 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 = 4, lowHz = 20, highHz = 10000}) {
20
+ super(numCaptures, numMLSPerCapture);
21
+ this.#mlsOrder = parseInt(mlsOrder, 10);
22
+ this.#P = 2 ** mlsOrder - 1;
23
+ this.#download = download;
24
+ this.#mls = [];
25
+ this.#lowHz = lowHz;
26
+ this.#highHz = highHz;
27
+ }
28
+
29
+ /** @private */
30
+ stepNum = 0;
31
+
32
+ /** @private */
33
+ totalSteps = 25;
34
+
35
+ /** @private */
36
+ #download;
37
+
38
+ /** @private */
39
+ #mlsGenInterface;
40
+
41
+ /** @private */
42
+ #mlsBufferView;
43
+
44
+ /** @private */
45
+ invertedImpulseResponse = null;
46
+
47
+ /** @private */
48
+ impulseResponses = [];
49
+
50
+ /** @private */
51
+ #mlsOrder;
52
+
53
+ /** @private */
54
+ #lowHz;
55
+
56
+ /** @private */
57
+ #highHz;
58
+
59
+ /** @private */
60
+ #mls;
61
+
62
+ /** @private */
63
+ #P;
64
+
65
+ /** @private */
66
+ #audioContext;
67
+
68
+ /** @private */
69
+ TAPER_SECS = 5;
70
+
71
+ /** @private */
72
+ offsetGainNode;
73
+
74
+ /** @private */
75
+ convolution;
76
+ ////////////////////////volume
77
+ /** @private */
78
+ #CALIBRATION_TONE_FREQUENCY = 1000; // Hz
79
+
80
+ /** @private */
81
+ #CALIBRATION_TONE_TYPE = 'sine';
82
+
83
+ /** @private */
84
+ #CALIBRATION_TONE_DURATION = 5; // seconds
85
+
86
+ /** @private */
87
+ outDBSPL = null;
88
+ THD = null;
89
+ outDBSPL1000 = null;
90
+
91
+ /** @private */
92
+ TAPER_SECS = 0.010; // seconds
93
+
94
+ /** .
95
+ * .
96
+ * .
97
+ * Sends all the computed impulse responses to the backend server for processing
98
+ *
99
+ * @returns sets the resulting inverted impulse response to the class property
100
+ * @example
101
+ */
102
+ sendImpulseResponsesToServerForProcessing = async () => {
103
+ const computedIRs = await Promise.all(this.impulseResponses);
104
+ const filteredComputedIRs = computedIRs.filter(element => {
105
+ return element != undefined;
106
+ });
107
+ const mls = this.#mls;
108
+ const lowHz = this.#lowHz;
109
+ const highHz = this.#highHz;
110
+ this.stepNum += 1;
111
+ this.emit('update', {message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: computing the IIR...`});
112
+ return this.pyServerAPI
113
+ .getInverseImpulseResponse({
114
+ payload: filteredComputedIRs.slice(0, this.numCaptures),
115
+ mls,
116
+ lowHz,
117
+ highHz
118
+ })
119
+ .then(res => {
120
+ console.log(res);
121
+ this.stepNum += 1;
122
+ this.emit('update', {message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: done computing the IIR...`});
123
+ this.invertedImpulseResponse = res["iir"];
124
+ this.convolution = res["convolution"];
125
+ })
126
+ .catch(err => {
127
+ // this.emit('InvertedImpulseResponse', {res: false});
128
+ console.error(err);
129
+ });
130
+ };
131
+
132
+ /** .
133
+ * .
134
+ * .
135
+ * Sends the recorded signal, or a given csv string of a signal, to the back end server for processing
136
+ *
137
+ * @param {<array>String} signalCsv - Optional csv string of a previously recorded signal, if given, this signal will be processed
138
+ * @example
139
+ */
140
+ sendRecordingToServerForProcessing = signalCsv => {
141
+ const allSignals = this.getAllRecordedSignals();
142
+ const numSignals = allSignals.length;
143
+ const mls = this.#mls;
144
+ const payload =
145
+ signalCsv && signalCsv.length > 0 ? csvToArray(signalCsv) : allSignals[numSignals - 1];
146
+ console.log('sending rec');
147
+ this.stepNum += 1;
148
+ this.emit('update', {message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: computing the IR of the last recording...`});
149
+ this.impulseResponses.push(
150
+ this.pyServerAPI
151
+ .getImpulseResponse({
152
+ sampleRate: this.sourceSamplingRate || 96000,
153
+ payload,
154
+ mls,
155
+ P: this.#P,
156
+ })
157
+ .then(res => {
158
+ if (this.numSuccessfulCaptured < this.numCaptures) {
159
+ this.numSuccessfulCaptured += 1;
160
+ console.log("num succ capt: " + this.numSuccessfulCaptured);
161
+ this.stepNum += 1;
162
+ this.emit('update', {
163
+ message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: ${this.numSuccessfulCaptured}/${this.numCaptures} IRs computed...`,
164
+ });
165
+ }
166
+ return res;
167
+ })
168
+ .catch(err => {
169
+ console.error(err);
170
+ })
171
+ );
172
+ };
173
+
174
+ /**
175
+ * Passed to the calibration steps function, awaits the desired amount of seconds to capture the desired number
176
+ * of MLS periods defined in the constructor.
177
+ *
178
+ * @example
179
+ */
180
+ #awaitDesiredMLSLength = async () => {
181
+ // seconds per MLS = P / SR
182
+ // await N * P / SR
183
+ this.stepNum += 1;
184
+ this.emit('update', {
185
+ message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: sampling the calibration signal...`,
186
+ });
187
+ await sleep((this.#P / this.sourceSamplingRate) * this.numMLSPerCapture);
188
+ };
189
+
190
+ /** .
191
+ * .
192
+ * .
193
+ * Passed to the calibration steps function, awaits the onset of the signal to ensure a steady state
194
+ *
195
+ * @example
196
+ */
197
+ #awaitSignalOnset = async () => {
198
+ this.stepNum += 1;
199
+ this.emit('update', {
200
+ message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: waiting for the signal to stabilize...`,
201
+ });
202
+ await sleep(this.TAPER_SECS);
203
+ };
204
+
205
+ /**
206
+ * Called immediately after a recording is captured. Used to process the resulting signal
207
+ * whether by sending the result to a server or by computing a result locally.
208
+ *
209
+ * @example
210
+ */
211
+ #afterMLSRecord = () => {
212
+ console.log('after record');
213
+ this.sendRecordingToServerForProcessing();
214
+ };
215
+
216
+ #afterMLSwIIRRecord = () => {
217
+ if (this.numSuccessfulCaptured < this.numCaptures) {
218
+ this.numSuccessfulCaptured += 1;
219
+ this.stepNum += 1;
220
+ this.emit('update', {
221
+ message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: ${this.numSuccessfulCaptured} recordings of convolved MLS captured`,
222
+ });
223
+ }
224
+ };
225
+
226
+ /** .
227
+ * .
228
+ * .
229
+ * Created an S Curver Buffer to taper the signal onset
230
+ *
231
+ * @param {*} length
232
+ * @param {*} phase
233
+ * @returns
234
+ * @example
235
+ */
236
+ static createSCurveBuffer = (length, phase) => {
237
+ const curve = new Float32Array(length);
238
+ let i;
239
+ for (i = 0; i < length; i += 1) {
240
+ // scale the curve to be between 0-1
241
+ curve[i] = Math.sin((Math.PI * i) / length - phase) / 2 + 0.5;
242
+ }
243
+ return curve;
244
+ };
245
+
246
+ static createInverseSCurveBuffer = (length, phase) => {
247
+ const curve = new Float32Array(length);
248
+ let i;
249
+ let j = length - 1;
250
+ for (i = 0; i < length; i += 1) {
251
+ // scale the curve to be between 0-1
252
+ curve[i] = Math.sin((Math.PI * j) / length - phase) / 2 + 0.5;
253
+ j -= 1;
254
+ }
255
+ return curve;
256
+ };
257
+
258
+ /**
259
+ * Construct a Calibration Node with the calibration parameters.
260
+ *
261
+ * @param CALIBRATION_TONE_FREQUENCY
262
+ * @private
263
+ * @example
264
+ */
265
+ #createPureTonenNode = CALIBRATION_TONE_FREQUENCY => {
266
+ const audioContext = this.makeNewSourceAudioContext();
267
+ const oscilator = audioContext.createOscillator();
268
+ const gainNode = audioContext.createGain();
269
+
270
+ oscilator.frequency.value = CALIBRATION_TONE_FREQUENCY;
271
+ oscilator.type = 'sine';
272
+ gainNode.gain.value = 0.04;
273
+
274
+ oscilator.connect(gainNode);
275
+ gainNode.connect(audioContext.destination);
276
+
277
+ this.addCalibrationNode(oscilator);
278
+ };
279
+
280
+ /**
281
+ * Construct a Calibration Node with the calibration parameters.
282
+ *
283
+ * @param dataBuffer
284
+ * @private
285
+ * @example
286
+ */
287
+ #createCalibrationNodeFromBuffer = dataBuffer => {
288
+ const audioContext = this.makeNewSourceAudioContext();
289
+ const buffer = audioContext.createBuffer(
290
+ 1, // number of channels
291
+ dataBuffer.length,
292
+ audioContext.sampleRate // sample rate
293
+ );
294
+
295
+ const data = buffer.getChannelData(0); // get data
296
+ // fill the buffer with our data
297
+ try {
298
+ for (let i = 0; i < dataBuffer.length; i += 1) {
299
+ data[i] = dataBuffer[i]*.1;
300
+ }
301
+ } catch (error) {
302
+ console.error(error);
303
+ }
304
+ console.log("mls second, same?");
305
+ console.log(data);
306
+ const onsetGainNode = audioContext.createGain();
307
+ this.offsetGainNode = audioContext.createGain();
308
+ const source = audioContext.createBufferSource();
309
+
310
+ source.buffer = buffer;
311
+ source.loop = true;
312
+ source.connect(onsetGainNode);
313
+ onsetGainNode.connect(this.offsetGainNode);
314
+ this.offsetGainNode.connect(audioContext.destination);
315
+
316
+ const onsetCurve = Combination.createSCurveBuffer(this.sourceSamplingRate, Math.PI / 2);
317
+ onsetGainNode.gain.setValueCurveAtTime(onsetCurve, 0, this.TAPER_SECS);
318
+ this.addCalibrationNode(source);
319
+ };
320
+
321
+ /**
322
+ * Given a data buffer, creates the required calibration node
323
+ *
324
+ * @param {*} dataBufferArray
325
+ * @example
326
+ */
327
+ #setCalibrationNodesFromBuffer = (dataBufferArray = [this.#mlsBufferView]) => {
328
+ if (dataBufferArray.length === 1) {
329
+ console.log('data buffer aray');
330
+ console.log(dataBufferArray);
331
+ this.#createCalibrationNodeFromBuffer(dataBufferArray[0]);
332
+ } else {
333
+ throw new Error('The length of the data buffer array must be 1');
334
+ }
335
+
336
+
337
+ };
338
+
339
+ /**
340
+ * function to put MLS filtered IIR data obtained from
341
+ * python server into our audio buffer to be played aloud
342
+ */
343
+ #putInPythonConv = () => {
344
+ const audioCtx = this.makeNewSourceAudioContextConvolved();
345
+ const buffer = audioCtx.createBuffer(
346
+ 1, // number of channels
347
+ this.convolution.length,
348
+ audioCtx.sampleRate // sample rate
349
+ );
350
+
351
+ const data = buffer.getChannelData(0); // get data
352
+ // fill the buffer with our data
353
+ try {
354
+ for (let i = 0; i < this.convolution.length; i += 1) {
355
+ data[i] = this.convolution[i];
356
+ }
357
+ } catch (error) {
358
+ console.error(error);
359
+ }
360
+
361
+ const source = audioCtx.createBufferSource();
362
+
363
+ source.buffer = buffer;
364
+ source.loop = true;
365
+ source.connect(audioCtx.destination);
366
+
367
+ this.addCalibrationNodeConvolved(source);
368
+ }
369
+
370
+ /**
371
+ * Creates an audio context and plays it for a few seconds.
372
+ *
373
+ * @private
374
+ * @returns - Resolves when the audio is done playing.
375
+ * @example
376
+ */
377
+ #playCalibrationAudio = () => {
378
+ this.calibrationNodes[0].start(0);
379
+ this.#mls = this.calibrationNodes[0].buffer.getChannelData(0);
380
+ this.stepNum += 1;
381
+ this.emit('update', {message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: playing the calibration tone...`});
382
+ };
383
+
384
+
385
+ #playCalibrationAudioConvolved = () => {
386
+ this.calibrationNodesConvolved[0].start(0);
387
+ this.stepNum += 1;
388
+ this.emit('update',{message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: playing the convolved calibration tone...`})
389
+ }
390
+
391
+ /** .
392
+ * .
393
+ * .
394
+ * Stops the audio with tapered offset
395
+ *
396
+ * @example
397
+ */
398
+ #stopCalibrationAudio = () => {
399
+ this.offsetGainNode.gain.setValueAtTime(
400
+ this.offsetGainNode.gain.value,
401
+ this.sourceAudioContext.currentTime
402
+ );
403
+
404
+ this.offsetGainNode.gain.setTargetAtTime(0, this.sourceAudioContext.currentTime, 0.5);
405
+ this.calibrationNodes[0].stop(0);
406
+ this.sourceAudioContext.close();
407
+ this.stepNum += 1;
408
+ this.emit('update', {message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: stopping the calibration tone...`});
409
+ };
410
+
411
+ #stopCalibrationAudioConvolved = () => {
412
+ this.offsetGainNode.gain.setValueAtTime(
413
+ this.offsetGainNode.gain.value,
414
+ this.sourceAudioContextConvolved.currentTime
415
+ );
416
+
417
+ this.offsetGainNode.gain.setTargetAtTime(0, this.sourceAudioContextConvolved.currentTime, 0.5);
418
+ //this.calibrationNodesConvolved[0].stop(0);
419
+ console.log("right before closing volved audio context");
420
+ this.sourceAudioContextConvolved.close();
421
+ this.stepNum += 1;
422
+ this.emit('update', {message: `All Hz Calibration Step ${this.stepNum}/${this.totalSteps}: stopping the convolved calibration tone...`});
423
+
424
+ }
425
+
426
+ playMLSwithIIR = async (stream, iir) => {
427
+ console.log('play mls with iir');
428
+ this.invertedImpulseResponse = iir;
429
+ // initialize the MLSGenInterface object with it's factory method
430
+
431
+ await MlsGenInterface.factory(
432
+ this.#mlsOrder,
433
+ this.sinkSamplingRate,
434
+ this.sourceSamplingRate
435
+ ).then(mlsGenInterface => {
436
+ this.#mlsGenInterface = mlsGenInterface;
437
+ this.#mlsBufferView = this.#mlsGenInterface.getMLS();
438
+ });
439
+
440
+ console.log('after mls factory'); //works up to here.
441
+ console.log(this.#mls);
442
+ // after intializating, start the calibration steps with garbage collection
443
+ await this.#mlsGenInterface.withGarbageCollection([
444
+ () =>
445
+ this.calibrationSteps(
446
+ stream,
447
+ this.#playCalibrationAudioConvolved, // play audio func (required)
448
+ this.#putInPythonConv, // before play func
449
+ this.#awaitSignalOnset, // before record
450
+ () => this.numSuccessfulCaptured < this.numCaptures,
451
+ this.#awaitDesiredMLSLength, // during record
452
+ this.#afterMLSwIIRRecord, // after record
453
+ 'filtered'
454
+ ),
455
+ ]);
456
+ };
457
+
458
+ /**
459
+ * Public method to start the calibration process. Objects intialized from webassembly allocate new memory
460
+ * and must be manually freed. This function is responsible for intializing the MlsGenInterface,
461
+ * and wrapping the calibration steps with a garbage collection safe gaurd.
462
+ *
463
+ * @public
464
+ * @param stream - The stream of audio from the Listener.
465
+ * @example
466
+ */
467
+ startCalibrationImpulseResponse = async stream => {
468
+ // initialize the MLSGenInterface object with it's factory method
469
+ await MlsGenInterface.factory(
470
+ this.#mlsOrder,
471
+ this.sinkSamplingRate,
472
+ this.sourceSamplingRate
473
+ ).then(mlsGenInterface => {
474
+ this.#mlsGenInterface = mlsGenInterface;
475
+ this.#mlsBufferView = this.#mlsGenInterface.getMLS();
476
+ });
477
+
478
+ // after intializating, start the calibration steps with garbage collection
479
+ await this.#mlsGenInterface.withGarbageCollection([
480
+ () =>
481
+ this.calibrationSteps(
482
+ stream,
483
+ this.#playCalibrationAudio, // play audio func (required)
484
+ this.#setCalibrationNodesFromBuffer, // before play func
485
+ this.#awaitSignalOnset, // before record
486
+ () => this.numSuccessfulCaptured < this.numCaptures, // loop while true
487
+ this.#awaitDesiredMLSLength, // during record
488
+ this.#afterMLSRecord, // after record
489
+ 'unfiltered'
490
+ ),
491
+ ]);
492
+
493
+ this.#stopCalibrationAudio();
494
+
495
+ // at this stage we've captured all the required signals,
496
+ // and have received IRs for each one
497
+ // so let's send all the IRs to the server to be converted to a single IIR
498
+ await this.sendImpulseResponsesToServerForProcessing();
499
+
500
+ this.numSuccessfulCaptured = 0;
501
+ // debugging function, use to test the result of the IIR
502
+ await this.playMLSwithIIR(stream, this.invertedImpulseResponse);
503
+ this.#stopCalibrationAudioConvolved();
504
+
505
+ let recs = this.getAllRecordedSignals();
506
+ let conv_recs = this.getAllFilteredRecordedSignals();
507
+ let unconv_rec = recs[0];
508
+ let conv_rec = conv_recs[0];
509
+
510
+ let results = await this.pyServerAPI
511
+ .getPSD({
512
+ unconv_rec,
513
+ conv_rec,
514
+ })
515
+ .then(res => {
516
+ return res;
517
+ })
518
+ .catch(err => {
519
+ console.error(err);
520
+ })
521
+
522
+ let iir_and_plots = {
523
+ "iir": this.invertedImpulseResponse,
524
+ "x_unconv": results["x_unconv"],
525
+ "y_unconv": results["y_unconv"],
526
+ "x_conv": results["x_conv"],
527
+ "y_conv": results["y_conv"]
528
+ }
529
+ if (this.#download) {
530
+ this.downloadSingleUnfilteredRecording();
531
+ this.downloadSingleFilteredRecording();
532
+ saveToCSV(this.#mls,"MLS.csv");
533
+ saveToCSV(this.convolution,'python_convolution_mls_iir.csv');
534
+ saveToCSV(this.invertedImpulseResponse,'IIR.csv');
535
+ const computedIRagain = await Promise.all(this.impulseResponses)
536
+ .then(res => {
537
+ for (let i = 0; i < res.length; i++){
538
+ if (res[i] != undefined){
539
+ saveToCSV(res[i], `IR_${i}`);
540
+ }
541
+ }
542
+ })
543
+ }
544
+
545
+ return iir_and_plots;
546
+ };
547
+
548
+ //////////////////////volume
549
+
550
+ handleIncomingData = data => {
551
+ console.log('Received data: ', data);
552
+ if (data.type === 'soundGainDBSPL') {
553
+ this.soundGainDBSPL = data.value;
554
+ } else {
555
+ throw new Error(`Unknown data type: ${data.type}`);
556
+ }
557
+ };
558
+
559
+ createSCurveBuffer = (onSetBool=true) => {
560
+
561
+ const curve = new Float32Array(this.TAPER_SECS*this.sourceSamplingRate+1);
562
+ const frequency = 1 / (4 * this.TAPER_SECS);
563
+ let j = 0;
564
+ for (let i = 0; i < this.TAPER_SECS*this.sourceSamplingRate+1; i += 1) {
565
+ const phase = 2 * Math.PI * frequency * j;
566
+ const onsetTaper = Math.pow(Math.sin(phase) , 2);
567
+ const offsetTaper = Math.pow(Math.cos(phase) , 2);
568
+ curve[i] = onSetBool? onsetTaper : offsetTaper;
569
+ j += (1 / this.sourceSamplingRate);
570
+ }
571
+ return curve;
572
+ };
573
+
574
+ #getTruncatedSignal = (left = 3.5, right = 4.5) => {
575
+ const start = Math.floor(left * this.sourceSamplingRate);
576
+ const end = Math.floor(right * this.sourceSamplingRate);
577
+ const result = Array.from(this.getLastRecordedSignal().slice(start, end));
578
+
579
+ /**
580
+ * function to check that capture was properly made
581
+ * @param {*} list
582
+ */
583
+ const checkResult = list => {
584
+ const setItem = new Set(list);
585
+ if (setItem.size === 1 && setItem.has(0)) {
586
+ console.warn(
587
+ 'The last capture failed, all recorded signal is zero',
588
+ this.getAllRecordedSignals()
589
+ );
590
+ }
591
+ if (setItem.size === 0) {
592
+ console.warn('The last capture failed, no recorded signal');
593
+ }
594
+ };
595
+ checkResult(result);
596
+ return result;
597
+ };
598
+
599
+ /**
600
+ *
601
+ *
602
+ Construct a calibration Node with the calibration parameters and given gain value
603
+ * @param {*} gainValue
604
+ * */
605
+ #createCalibrationToneWithGainValue = gainValue => {
606
+ const audioContext = this.makeNewSourceAudioContext();
607
+ const oscilator = audioContext.createOscillator();
608
+ const gainNode = audioContext.createGain();
609
+ const taperGainNode = audioContext.createGain();
610
+ const offsetGainNode = audioContext.createGain();
611
+ const totalDuration = this.#CALIBRATION_TONE_DURATION * 1.2;
612
+
613
+ oscilator.frequency.value = this.#CALIBRATION_TONE_FREQUENCY;
614
+ oscilator.type = this.#CALIBRATION_TONE_TYPE;
615
+ gainNode.gain.value = gainValue;
616
+
617
+ oscilator.connect(gainNode);
618
+ gainNode.connect(taperGainNode);
619
+ const onsetCurve = this.createSCurveBuffer();
620
+ taperGainNode.gain.setValueCurveAtTime(onsetCurve, 0, this.TAPER_SECS);
621
+ taperGainNode.connect(offsetGainNode);
622
+ const offsetCurve = this.createSCurveBuffer(false);
623
+ offsetGainNode.gain.setValueCurveAtTime(offsetCurve, (totalDuration-this.TAPER_SECS), this.TAPER_SECS);
624
+ offsetGainNode.connect(audioContext.destination);
625
+
626
+ this.addCalibrationNode(oscilator);
627
+ };
628
+
629
+ /**
630
+ * Construct a Calibration Node with the calibration parameters.
631
+ *
632
+ * @private
633
+ * @example
634
+ */
635
+ #createCalibrationNode = () => {
636
+ const audioContext = this.makeNewSourceAudioContext();
637
+ const oscilator = audioContext.createOscillator();
638
+ const gainNode = audioContext.createGain();
639
+
640
+ oscilator.frequency.value = this.#CALIBRATION_TONE_FREQUENCY;
641
+ oscilator.type = this.#CALIBRATION_TONE_TYPE;
642
+ gainNode.gain.value = 0.04;
643
+
644
+ oscilator.connect(gainNode);
645
+ gainNode.connect(audioContext.destination);
646
+
647
+ this.addCalibrationNode(oscilator);
648
+ };
649
+
650
+ #playCalibrationAudioVolume = async () => {
651
+ const totalDuration = this.#CALIBRATION_TONE_DURATION * 1.2;
652
+
653
+ this.calibrationNodes[0].start(0);
654
+ this.calibrationNodes[0].stop(totalDuration);
655
+ console.log(`Playing a buffer of ${this.#CALIBRATION_TONE_DURATION} seconds of audio`);
656
+ console.log(`Waiting a total of ${totalDuration} seconds`);
657
+ await sleep(totalDuration);
658
+ };
659
+
660
+ #sendToServerForProcessing = (lCalib = 104.92978421490648) => {
661
+ console.log('Sending data to server');
662
+ this.pyServerAPI
663
+ .getVolumeCalibration({
664
+ sampleRate: this.sourceSamplingRate,
665
+ payload: this.#getTruncatedSignal(),
666
+ lCalib: lCalib,
667
+ })
668
+ .then(res => {
669
+ if (this.outDBSPL === null) {
670
+ this.outDBSPL = res['outDbSPL'];
671
+ this.outDBSPL1000 = res['outDbSPL1000'];
672
+ this.THD = res['thd'];
673
+ }
674
+ })
675
+ .catch(err => {
676
+ console.warn(err);
677
+ });
678
+ };
679
+
680
+ startCalibrationVolume = async (stream, gainValues, lCalib = 104.92978421490648) => {
681
+ const trialIterations = gainValues.length;
682
+ const thdValues = [];
683
+ const inDBValues = [];
684
+ let inDB = 0;
685
+ const outDBSPLValues = [];
686
+ const outDBSPL1000Values = [];
687
+
688
+ // do one calibration that will be discarded
689
+ const soundLevelToDiscard = -60;
690
+ const gainToDiscard = Math.pow(10, soundLevelToDiscard / 20);
691
+ this.emit('update', {message: `1000 Hz Calibration: Sound Level ${soundLevelToDiscard} dB`});
692
+ do {
693
+ // eslint-disable-next-line no-await-in-loop
694
+ await this.volumeCalibrationSteps(
695
+ stream,
696
+ this.#playCalibrationAudioVolume,
697
+ this.#createCalibrationToneWithGainValue,
698
+ this.#sendToServerForProcessing,
699
+ gainToDiscard,
700
+ lCalib //todo make this a class parameter
701
+ );
702
+ } while (this.outDBSPL === null);
703
+ //reset the values
704
+ this.outDBSPL = null;
705
+ this.outDBSPL = null;
706
+ this.outDBSPL1000 = null;
707
+ this.THD = null;
708
+
709
+ // run the calibration at different gain values provided by the user
710
+ for (let i = 0; i < trialIterations; i++) {
711
+ //convert gain to DB and add to inDB
712
+ inDB = Math.log10(gainValues[i]) * 20;
713
+ // precision to 1 decimal place
714
+ inDB = Math.round(inDB * 10) / 10;
715
+ inDBValues.push(inDB);
716
+ this.emit('update', {message: `1000 Hz Calibration: Sound Level ${inDB} dB`});
717
+ do {
718
+ // eslint-disable-next-line no-await-in-loop
719
+ await this.volumeCalibrationSteps(
720
+ stream,
721
+ this.#playCalibrationAudioVolume,
722
+ this.#createCalibrationToneWithGainValue,
723
+ this.#sendToServerForProcessing,
724
+ gainValues[i],
725
+ lCalib //todo make this a class parameter
726
+ );
727
+ } while (this.outDBSPL === null);
728
+ outDBSPL1000Values.push(this.outDBSPL1000);
729
+ thdValues.push(this.THD);
730
+ outDBSPLValues.push(this.outDBSPL);
731
+
732
+ this.outDBSPL = null;
733
+ this.outDBSPL1000 = null;
734
+ this.THD = null;
735
+ }
736
+
737
+ // get the volume calibration parameters from the server
738
+ const parameters = await this.pyServerAPI
739
+ .getVolumeCalibrationParameters({
740
+ inDBValues: inDBValues,
741
+ outDBSPLValues: outDBSPL1000Values,
742
+ lCalib: lCalib,
743
+ })
744
+ .then(res => {
745
+ return res;
746
+ });
747
+ const result = {
748
+ parameters: parameters,
749
+ inDBValues: inDBValues,
750
+ outDBSPLValues: outDBSPLValues,
751
+ outDBSPL1000Values: outDBSPL1000Values,
752
+ thdValues: thdValues,
753
+ };
754
+
755
+ return result;
756
+ };
757
+
758
+ startCalibration = async (stream, gainValues, lCalib = 104.92978421490648) => {
759
+ let volumeResults = await this.startCalibrationVolume(stream, gainValues, lCalib = 104.92978421490648);
760
+ let impulseResponseResults = await this.startCalibrationImpulseResponse(stream);
761
+ console.log(volumeResults);
762
+ console.log(impulseResponseResults);
763
+ const total_results = {...volumeResults, ...impulseResponseResults};
764
+ console.log('total');
765
+ console.log(total_results);
766
+ return total_results;
767
+ }
768
+
769
+
770
+ }
771
+
772
+ export default Combination;