speaker-calibration 2.1.16 → 2.1.18

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