speaker-calibration 2.2.117 → 2.2.119

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/.eslintignore +71 -71
  2. package/.eslintrc.json +40 -40
  3. package/.prettierignore +69 -69
  4. package/.prettierrc +14 -14
  5. package/LICENSE +20 -20
  6. package/README.md +133 -133
  7. package/__mocks__/fileMock.js +1 -1
  8. package/__mocks__/styleMock.js +1 -1
  9. package/babel.config.js +3 -3
  10. package/coverage/clover.xml +71 -71
  11. package/coverage/coverage-final.json +224 -224
  12. package/coverage/lcov-report/PythonServerInterface.js.html +265 -265
  13. package/coverage/lcov-report/base.css +354 -354
  14. package/coverage/lcov-report/block-navigation.js +82 -82
  15. package/coverage/lcov-report/index.html +123 -123
  16. package/coverage/lcov-report/prettify.css +101 -101
  17. package/coverage/lcov-report/prettify.js +937 -937
  18. package/coverage/lcov-report/sorter.js +189 -189
  19. package/coverage/lcov-report/src/index.html +121 -121
  20. package/coverage/lcov-report/src/server/PythonServerInterface.js.html +268 -268
  21. package/coverage/lcov-report/src/server/index.html +123 -123
  22. package/coverage/lcov-report/src/tasks/audioCalibrator.js.html +499 -499
  23. package/coverage/lcov-report/src/tasks/audioRecorder.js.html +412 -412
  24. package/coverage/lcov-report/src/tasks/index.html +143 -143
  25. package/coverage/lcov-report/src/tasks/volume/index.html +123 -123
  26. package/coverage/lcov-report/src/tasks/volume/volume.js.html +409 -409
  27. package/coverage/lcov-report/src/utils.js.html +172 -172
  28. package/coverage/lcov.info +91 -91
  29. package/dist/example/fetch-languages-sheets.js +77 -77
  30. package/dist/example/i18n.js +35846 -35846
  31. package/dist/example/index.html +47 -47
  32. package/dist/example/listener.html +62 -62
  33. package/dist/example/listener.js +129 -129
  34. package/dist/example/server.js +51 -51
  35. package/dist/example/speaker.html +145 -145
  36. package/dist/example/speakerUI.js +273 -273
  37. package/dist/example/styles.css +92 -92
  38. package/dist/main.js +17 -17
  39. package/dist/mlsGen.js +6814 -6814
  40. package/dist/mlsGen.wasm +0 -0
  41. package/dist/package-lock.json +1018 -1018
  42. package/dist/package.json +18 -18
  43. package/doc/AudioCalibrator.html +417 -417
  44. package/doc/AudioPeer.html +251 -251
  45. package/doc/AudioRecorder.html +195 -195
  46. package/doc/ImpulseResponse.html +215 -215
  47. package/doc/Listener.html +308 -308
  48. package/doc/MlsGenInterface.html +226 -226
  49. package/doc/MyEventEmitter.html +274 -274
  50. package/doc/PythonServerAPI.html +109 -109
  51. package/doc/Speaker.html +276 -276
  52. package/doc/Takes%20a%20target%20element%20where%20html%20elements%20will%20be%20appended..html +128 -128
  53. package/doc/Takes%20the%20url%20of%20the%20current%20site%0Aand%20a%20target%20element%20where%20html%20elements%20will%20be%20appended..html +138 -138
  54. package/doc/Takes%20the%20url%20of%20the%20current%20site%20and%20a%20target%20element%20where%20html%20elements%20will%20be%20appended..html +137 -137
  55. package/doc/Volume.html +88 -88
  56. package/doc/audioCalibrator.js.html +179 -179
  57. package/doc/audioPeer.js.html +175 -175
  58. package/doc/audioRecorder.js.html +163 -163
  59. package/doc/creates%20a%20new%20AudioRecorder%20instance.%20%0ASets%20up%20the%20audio%20context%20and%20file%20reader..html +114 -114
  60. package/doc/fonts/OpenSans-Bold-webfont.svg +1829 -1829
  61. package/doc/fonts/OpenSans-BoldItalic-webfont.svg +1829 -1829
  62. package/doc/fonts/OpenSans-Italic-webfont.svg +1829 -1829
  63. package/doc/fonts/OpenSans-Light-webfont.svg +1830 -1830
  64. package/doc/fonts/OpenSans-LightItalic-webfont.svg +1834 -1834
  65. package/doc/fonts/OpenSans-Regular-webfont.svg +1830 -1830
  66. package/doc/global.html +308 -308
  67. package/doc/index.html +58 -58
  68. package/doc/listener.js.html +170 -170
  69. package/doc/mlsGen_mlsGenInterface.js.html +117 -117
  70. package/doc/myEventEmitter.js.html +124 -124
  71. package/doc/peer-connection_audioPeer.js.html +188 -188
  72. package/doc/peer-connection_listener.js.html +311 -311
  73. package/doc/peer-connection_speaker.js.html +381 -381
  74. package/doc/scripts/linenumber.js +25 -25
  75. package/doc/scripts/prettify/Apache-License-2.0.txt +202 -202
  76. package/doc/scripts/prettify/lang-css.js +24 -24
  77. package/doc/scripts/prettify/prettify.js +640 -640
  78. package/doc/server_PythonServerAPI.js.html +160 -160
  79. package/doc/speaker.js.html +248 -248
  80. package/doc/styles/jsdoc-default.css +371 -371
  81. package/doc/styles/prettify-jsdoc.css +111 -111
  82. package/doc/styles/prettify-tomorrow.css +163 -163
  83. package/doc/tasks_audioCalibrator.js.html +207 -207
  84. package/doc/tasks_audioRecorder.js.html +190 -190
  85. package/doc/tasks_impulse-response_impulseResponse.js.html +442 -442
  86. package/doc/tasks_impulse-response_mlsGen_mlsGenInterface.js.html +175 -175
  87. package/doc/tasks_volume_volume.js.html +185 -185
  88. package/doc/utils.js.html +105 -105
  89. package/jest.config.js +173 -173
  90. package/makefile +74 -0
  91. package/netlify.toml +26 -26
  92. package/package.json +69 -69
  93. package/src/config/firebase.js +26 -26
  94. package/src/index.html +21 -21
  95. package/src/main.js +23 -23
  96. package/src/myEventEmitter.js +83 -83
  97. package/src/peer-connection/audioPeer.js +151 -151
  98. package/src/peer-connection/listener.js +327 -327
  99. package/src/peer-connection/peerErrors.js +25 -25
  100. package/src/peer-connection/speaker.js +486 -485
  101. package/src/server/PythonServerAPI.js +673 -673
  102. package/src/tasks/audioCalibrator.js +310 -310
  103. package/src/tasks/audioRecorder.js +301 -301
  104. package/src/tasks/combination/combination.js +2570 -2508
  105. package/src/tasks/combination/mlsGen/mlsGen.cpp +98 -98
  106. package/src/tasks/combination/mlsGen/mlsGen.hpp +303 -303
  107. package/src/tasks/combination/mlsGen/mlsGenInterface.js +131 -131
  108. package/src/tasks/combination/mlsGen/mlsGenTest.cpp +180 -180
  109. package/src/tasks/impulse-response/impulseResponse.js +610 -610
  110. package/src/tasks/impulse-response/mlsGen/mlsGen.cpp +98 -98
  111. package/src/tasks/impulse-response/mlsGen/mlsGen.hpp +303 -303
  112. package/src/tasks/impulse-response/mlsGen/mlsGenInterface.js +131 -131
  113. package/src/tasks/impulse-response/mlsGen/mlsGenTest.cpp +180 -180
  114. package/src/tasks/volume/volume.cpp +2 -2
  115. package/src/tasks/volume/volume.hpp +22 -22
  116. package/src/tasks/volume/volume.js +279 -279
  117. package/src/utils.js +135 -135
  118. package/webpack.config.js +37 -37
  119. package/.gitignore +0 -81
@@ -1,485 +1,486 @@
1
- import QRCode from 'qrcode';
2
- import AudioPeer from './audioPeer';
3
- import {sleep} from '../utils';
4
- import {
5
- UnsupportedDeviceError,
6
- MissingSpeakerIdError,
7
- CalibrationTimedOutError,
8
- } from './peerErrors';
9
-
10
- import {phrases} from '../../dist/example/i18n';
11
-
12
- /**
13
- * @class Handles the speaker's side of the connection. Responsible for initiating the connection,
14
- * rendering the QRCode, and answering the call.
15
- * @augments AudioPeer
16
- */
17
- class Speaker extends AudioPeer {
18
- /**
19
- * Takes the url of the current site and a target element where html elements will be appended.
20
- *
21
- * @param params - See type definition for initParameters.
22
- * @param Calibrator - An instance of the AudioCalibrator class, should not use AudioCalibrator directly, instead use an extended class available in /tasks/.
23
- * @param CalibratorInstance
24
- * @example
25
- */
26
- constructor(params, CalibratorInstance) {
27
- super(params);
28
- this.language = params?.language ?? 'en-US';
29
- this.siteUrl += '/listener?';
30
- this.ac = CalibratorInstance;
31
- this.result = null;
32
- this.debug = params?.debug ?? false;
33
- this.isSmartPhone = params?.isSmartPhone ?? false;
34
- this.calibrateSoundHz = params?.calibrateSoundHz ?? 48000;
35
- this.calibrateSoundSamplingDesiredBits = params?.calibrateSoundSamplingDesiredBits ?? 24;
36
- this.instructionDisplayId = params?.instructionDisplayId ?? '';
37
- this.soundSubtitleId = params?.soundSubtitleId ?? '';
38
- this.timeToCalibrateDisplay = params?.timeToCalibrateId ?? '';
39
- this.soundMessageId = params?.soundMessageId ?? '';
40
- this.titleDisplayId = params?.titleDisplayId ?? '';
41
- this.timeToCalibrate = params?.timeToCalibrate ?? 10;
42
-
43
- /* Set up callbacks that handle any events related to our peer object. */
44
- this.peer.on('open', this.#onPeerOpen);
45
- this.peer.on('connection', this.#onPeerConnection);
46
- this.peer.on('close', this.onPeerClose);
47
- this.peer.on('disconnected', this.#onPeerDisconnected);
48
- this.peer.on('error', this.#onPeerError);
49
- }
50
- /**
51
- * Async factory method that creates the Speaker object, and returns a promise that resolves to the result of the calibration.
52
- *
53
- * @param params - The parameters to be passed to the peer object.
54
- * @param Calibrator - The class that defines the calibration process.
55
- * @param CalibratorInstance
56
- * @param timeOut - The amount of time to wait before timing out the connection (in milliseconds).
57
- * @public
58
- * @example
59
- */
60
- static startCalibration = async (params, CalibratorInstance, timeOut = 180000) => {
61
- window.speaker = new Speaker(params, CalibratorInstance);
62
- const {speaker} = window;
63
-
64
- // wrap the calibration process in a promise so we can await it
65
- return new Promise((resolve, reject) => {
66
- // when a call is received
67
- speaker.peer.on('call', async call => {
68
- // Answer the call (one way)
69
-
70
- call.answer();
71
- speaker.#removeUIElems();
72
- speaker.#showSpinner();
73
- speaker.ac.createLocalAudio(document.getElementById(speaker.targetElement));
74
- // when we start receiving audio
75
- call.on('stream', async stream => {
76
- window.localStream = stream;
77
- window.localAudio.srcObject = stream;
78
- window.localAudio.autoplay = false;
79
-
80
- // if the sinkSamplingRate is not set sleep
81
- while (!speaker.ac.sampleRatesSet()) {
82
- console.log('SinkSamplingRate is undefined, sleeping');
83
- await sleep(1);
84
- }
85
-
86
- if (params.displayUpdate) {
87
- params.displayUpdate.style.display = '';
88
- }
89
-
90
- // resolve when we have a result
91
- speaker.result = await speaker.ac.startCalibration(
92
- stream,
93
- params.gainValues,
94
- params.ICalib,
95
- params.knownIR,
96
- params.microphoneName,
97
- params.calibrateSoundCheck,
98
- params.isSmartPhone,
99
- params.calibrateSoundBurstDb,
100
- params.calibrateSoundBurstRepeats,
101
- params.calibrateSoundBurstSec,
102
- params.calibrateSoundBurstsWarmup,
103
- params.calibrateSoundHz,
104
- params.calibrateSoundIRSec,
105
- params.calibrateSoundIIRSec,
106
- params.calibrateSound1000HzPreSec,
107
- params.calibrateSound1000HzSec,
108
- params.calibrateSound1000HzPostSec,
109
- params.calibrateSoundBackgroundSecs,
110
- params.calibrateSoundSmoothOctaves,
111
- params.calibrateSoundPowerBinDesiredSec,
112
- params.calibrateSoundPowerDbSDToleratedDb,
113
- params.micManufacturer,
114
- params.micSerialNumber,
115
- params.micModelNumber,
116
- params.micModelName,
117
- params.calibrateMicrophonesBool,
118
- params.authorEmails,
119
- params.webAudioDeviceNames,
120
- params.IDsToSaveInSoundProfileLibrary,
121
- params.restartButton
122
- );
123
- speaker.#removeUIElems();
124
- resolve(speaker.result);
125
- });
126
- // if we do not receive a result within the timeout, reject
127
- setTimeout(() => {
128
- reject(
129
- new CalibrationTimedOutError(
130
- `Calibration failed to produce a result after ${
131
- timeOut / 1000
132
- } seconds. Please try again.`
133
- )
134
- );
135
- }, timeOut);
136
- });
137
- });
138
- };
139
-
140
- static testIIR = async (params, CalibratorInstance, IIR, timeOut = 180000) => {
141
- window.speaker = new Speaker(params, CalibratorInstance);
142
- const {speaker} = window;
143
-
144
- // wrap the calibration process in a promise so we can await it
145
- return new Promise((resolve, reject) => {
146
- // when a call is received
147
- speaker.peer.on('call', async call => {
148
- // Answer the call (one way)
149
- call.answer();
150
- speaker.#removeUIElems();
151
- speaker.#showSpinner();
152
- speaker.ac.createLocalAudio(document.getElementById(speaker.targetElement));
153
- // when we start receiving audio
154
- call.on('stream', async stream => {
155
- window.localStream = stream;
156
- window.localAudio.srcObject = stream;
157
- window.localAudio.autoplay = false;
158
-
159
- // if the sinkSamplingRate is not set sleep
160
- while (!speaker.ac.sampleRatesSet()) {
161
- console.log('SinkSamplingRate is undefined, sleeping');
162
- await sleep(1);
163
- }
164
- // resolve when we have a result
165
- speaker.result = await speaker.ac.playMLSwithIIR(stream, IIR);
166
- speaker.#removeUIElems();
167
- resolve(speaker.result);
168
- });
169
- // if we do not receive a result within the timeout, reject
170
- setTimeout(() => {
171
- reject(
172
- new CalibrationTimedOutError(
173
- `Calibration failed to produce a result after ${
174
- timeOut / 1000
175
- } seconds. Please try again.`
176
- )
177
- );
178
- }, timeOut);
179
- });
180
- });
181
- };
182
-
183
- /**
184
- * Called after the peer conncection has been opened.
185
- * Generates a QR code for the connection and displays it.
186
- *
187
- * @private
188
- * @example
189
- */
190
- #showQRCode = () => {
191
- // Get query string, the URL parameters to specify a Listener
192
- const queryStringParameters = {
193
- speakerPeerId: this.peer.id,
194
- isSmartPhone: this.isSmartPhone,
195
- calibrateSoundHz: this.calibrateSoundHz,
196
- calibrateSoundSamplingDesiredBits: this.calibrateSoundSamplingDesiredBits,
197
- lang: this.language,
198
- };
199
- const queryString = this.queryStringFromObject(queryStringParameters);
200
- const uri = this.siteUrl + queryString;
201
- if (this.isSmartPhone) {
202
- // Display QR code for the participant to scan
203
- const qrCanvas = document.createElement('canvas');
204
- qrCanvas.setAttribute('id', 'qrCanvas');
205
- console.log(uri);
206
- QRCode.toCanvas(qrCanvas, uri, error => {
207
- if (error) console.error(error);
208
- });
209
- const qrImage = new Image(400, 400);
210
- qrImage.setAttribute('id', 'compatibilityCheckQRImage');
211
- qrImage.style.zIndex = Infinity;
212
- qrImage.style.width = 400;
213
- qrImage.style.height = 400;
214
- qrImage.style.aspectRatio = 1;
215
- qrImage.src = qrCanvas.toDataURL();
216
- qrImage.style.maxHeight = '150px';
217
- qrImage.style.maxWidth = '150px';
218
- document.getElementById(this.targetElement).appendChild(qrImage);
219
- } else {
220
- // show the link to the user
221
- // If specified HTML Id is available, show QR code there
222
- if (document.getElementById(this.targetElement)) {
223
- // const linkTag = document.createElement('a');
224
- // linkTag.setAttribute('href', uri);
225
- // linkTag.innerHTML = 'Click here to start the calibration';
226
- // linkTag.target = '_blank';
227
- // document.getElementById(this.targetElement).appendChild(linkTag);
228
- // document.getElementById(this.targetElement).appendChild(qrCanvas);
229
-
230
- const proceedButton = document.createElement('button');
231
- proceedButton.setAttribute('id', 'calibrationProceedButton');
232
- proceedButton.setAttribute('class', 'btn btn-success');
233
- proceedButton.innerHTML = 'Proceed';
234
- proceedButton.onclick = () => {
235
- // open the link in a new tab
236
- window.open(uri, '_blank');
237
- // remove the button
238
- document.getElementById('calibrationProceedButton').remove();
239
- };
240
- document.getElementById(this.targetElement).appendChild(proceedButton);
241
- }
242
- }
243
- // or just print it to console
244
- console.log('TEST: Peer reachable at: ', uri);
245
- };
246
-
247
- #showSpinner = () => {
248
- const spinner = document.createElement('div');
249
- spinner.className = 'spinner-border ml-auto';
250
- spinner.role = 'status';
251
- spinner.ariaHidden = 'true';
252
- document.getElementById(this.targetElement).appendChild(spinner);
253
-
254
- // clear instructionDisplay
255
- const soundMessage = document.getElementById(this.soundMessageId);
256
- soundMessage.innerHTML = '';
257
- soundMessage.style.display = 'none';
258
- const instructionDisplay = document.getElementById(this.instructionDisplayId);
259
- const background = document.getElementById('background'); // todo: get background id from params
260
- const subtitle = document.getElementById(this.soundSubtitleId);
261
- if (subtitle) {
262
- subtitle.innerHTML = '';
263
- }
264
- if (instructionDisplay) {
265
- instructionDisplay.innerHTML = '';
266
- instructionDisplay.style.whiteSpace = 'nowrap';
267
- instructionDisplay.style.fontWeight = 'bold';
268
- instructionDisplay.style.width = 'fit-content';
269
- instructionDisplay.innerHTML = phrases.RC_soundRecording[this.language];
270
- let fontSize = 100;
271
- instructionDisplay.style.fontSize = fontSize + 'px';
272
- while (instructionDisplay.scrollWidth > background.scrollWidth * 0.9 && fontSize > 10) {
273
- fontSize--;
274
- instructionDisplay.style.fontSize = fontSize + 'px';
275
- }
276
- // const p = document.createElement('p');
277
- // // font size
278
- // p.style.fontSize = '1.1rem';
279
- // p.style.fontWeight = 'normal';
280
- // p.style.paddingTop = '20px';
281
- // const timeToCalibrateText = phrases.RC_howLongToCalibrate['en-US'];
282
- // p.innerHTML = timeToCalibrateText.replace('111', this.timeToCalibrate);
283
- // instructionDisplay.appendChild(p);
284
- }
285
-
286
- const timeToCalibrateDisplay = document.getElementById(this.timeToCalibrateDisplay);
287
- if (timeToCalibrateDisplay) {
288
- const timeToCalibrateText = phrases.RC_howLongToCalibrate[this.language];
289
- timeToCalibrateDisplay.innerHTML = timeToCalibrateText.replace('111', this.timeToCalibrate);
290
- timeToCalibrateDisplay.style.fontWeight = 'normal';
291
- timeToCalibrateDisplay.style.fontSize = '1rem';
292
- // timeToCalibrateDisplay.style.paddingTop = '20px';
293
- }
294
-
295
- // Update title - titleDisplayId
296
- const titleDisplay = document.getElementById(this.titleDisplayId);
297
- if (titleDisplay) {
298
- // replace 5 with 6
299
- titleDisplay.innerHTML = this.isSmartPhone
300
- ? titleDisplay.innerHTML.replace('2', '3')
301
- : titleDisplay.innerHTML.replace('4', '5');
302
- }
303
- };
304
-
305
- #removeUIElems = () => {
306
- const parent = document.getElementById(this.targetElement);
307
- while (parent.firstChild) {
308
- parent.firstChild.remove();
309
- }
310
- };
311
-
312
- /**
313
- * Called when the peer connection is opened.
314
- * Saves the peer id and calls the QR code generator.
315
- *
316
- * @param peerId - The peer id of the peer connection.
317
- * @param id
318
- * @private
319
- * @example
320
- */
321
- #onPeerOpen = id => {
322
- // Workaround for peer.reconnect deleting previous id
323
- if (id === null) {
324
- console.error('Received null id from peer open');
325
- this.peer.id = this.lastPeerId;
326
- } else {
327
- this.lastPeerId = this.peer.id;
328
- }
329
-
330
- if (id !== this.peer.id) {
331
- console.warn('DEBUG Check you assumption that id === this.peer.id');
332
- }
333
-
334
- this.#showQRCode();
335
- };
336
-
337
- /**
338
- * Called when the peer connection is established.
339
- * Enforces a single connection.
340
- *
341
- * @param connection - The connection object.
342
- * @private
343
- * @example
344
- */
345
- #onPeerConnection = connection => {
346
- // Allow only a single connection
347
- if (this.conn && this.conn.open) {
348
- connection.on('open', () => {
349
- connection.send('Already connected to another client');
350
- setTimeout(() => {
351
- connection.close();
352
- }, 500);
353
- });
354
- return;
355
- }
356
-
357
- this.conn = connection;
358
- console.log('Connected to: ', this.conn.peer);
359
- this.#ready();
360
- };
361
-
362
- /**
363
- * Called when the peer connection is closed.
364
- *
365
- * @private
366
- * @example
367
- */
368
- onPeerClose = () => {
369
- this.conn = null;
370
- console.log('Connection destroyed');
371
- };
372
-
373
- static closeConnection = () => {
374
- this.conn = null;
375
- console.log('Connection destroyed');
376
- };
377
-
378
- /**
379
- * Called when the peer connection is disconnected.
380
- * Attempts to reconnect.
381
- *
382
- * @private
383
- * @example
384
- */
385
- #onPeerDisconnected = () => {
386
- console.log('Connection lost. Please reconnect');
387
-
388
- // Workaround for peer.reconnect deleting previous id
389
- this.peer.id = this.lastPeerId;
390
- // eslint-disable-next-line no-underscore-dangle
391
- this.peer._lastServerId = this.lastPeerId;
392
- this.peer.reconnect();
393
- };
394
-
395
- /**
396
- * Called when the peer connection encounters an error.
397
- *
398
- * @param error
399
- * @private
400
- * @example
401
- */
402
- #onPeerError = error => {
403
- // TODO: check if this function is needed or not
404
- console.error(error);
405
- };
406
-
407
- /**
408
- * Called when data is received from the peer connection.
409
- *
410
- * @param data
411
- * @private
412
- * @example
413
- */
414
- #onIncomingData = data => {
415
- // enforce object type
416
- if (
417
- !Object.prototype.hasOwnProperty.call(data, 'name') ||
418
- !Object.prototype.hasOwnProperty.call(data, 'payload')
419
- ) {
420
- console.error('Received malformed data: ', data);
421
- return;
422
- }
423
-
424
- switch (data.name) {
425
- case 'samplingRate':
426
- this.ac.setSamplingRates(data.payload);
427
- break;
428
- case 'sampleSize':
429
- this.ac.setSampleSize(data.payload);
430
- break;
431
- case 'deviceType':
432
- this.ac.setDeviceType(data.payload);
433
- break;
434
- case 'deviceName':
435
- this.ac.setDeviceName(data.payload);
436
- break;
437
- case 'deviceInfo':
438
- this.ac.setDeviceInfo(data.payload);
439
- console.log('Received device info from listener: ', data.payload);
440
- break;
441
- case UnsupportedDeviceError.name:
442
- case MissingSpeakerIdError.name:
443
- throw data.payload;
444
- break;
445
- default:
446
- break;
447
- }
448
- };
449
-
450
- /**
451
- * Called when the peer connection is #ready.
452
- *
453
- * @private
454
- * @example
455
- */
456
- #ready = () => {
457
- // Perform callback with data
458
- this.conn.on('data', this.#onIncomingData);
459
- this.conn.on('close', () => {
460
- console.log('Connection reset<br>Awaiting connection...');
461
- this.conn = null;
462
- });
463
- };
464
-
465
- /** .
466
- * .
467
- * .
468
- * Debug method for downloading the recorded audio
469
- *
470
- * @public
471
- * @example
472
- */
473
- downloadData = () => {
474
- this.ac.downloadData();
475
- };
476
- }
477
-
478
- /*
479
- Referenced links:
480
- https://stackoverflow.com/questions/28016664/when-you-pass-this-as-an-argument/28016676#28016676
481
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
482
- https://stackoverflow.com/questions/879152/how-do-i-make-javascript-beep [3]
483
- */
484
-
485
- export default Speaker;
1
+ import QRCode from 'qrcode';
2
+ import AudioPeer from './audioPeer';
3
+ import {sleep} from '../utils';
4
+ import {
5
+ UnsupportedDeviceError,
6
+ MissingSpeakerIdError,
7
+ CalibrationTimedOutError,
8
+ } from './peerErrors';
9
+
10
+ import {phrases} from '../../dist/example/i18n';
11
+
12
+ /**
13
+ * @class Handles the speaker's side of the connection. Responsible for initiating the connection,
14
+ * rendering the QRCode, and answering the call.
15
+ * @augments AudioPeer
16
+ */
17
+ class Speaker extends AudioPeer {
18
+ /**
19
+ * Takes the url of the current site and a target element where html elements will be appended.
20
+ *
21
+ * @param params - See type definition for initParameters.
22
+ * @param Calibrator - An instance of the AudioCalibrator class, should not use AudioCalibrator directly, instead use an extended class available in /tasks/.
23
+ * @param CalibratorInstance
24
+ * @example
25
+ */
26
+ constructor(params, CalibratorInstance) {
27
+ super(params);
28
+ this.language = params?.language ?? 'en-US';
29
+ this.siteUrl += '/listener?';
30
+ this.ac = CalibratorInstance;
31
+ this.result = null;
32
+ this.debug = params?.debug ?? false;
33
+ this.isSmartPhone = params?.isSmartPhone ?? false;
34
+ this.calibrateSoundHz = params?.calibrateSoundHz ?? 48000;
35
+ this.calibrateSoundSamplingDesiredBits = params?.calibrateSoundSamplingDesiredBits ?? 24;
36
+ this.instructionDisplayId = params?.instructionDisplayId ?? '';
37
+ this.soundSubtitleId = params?.soundSubtitleId ?? '';
38
+ this.timeToCalibrateDisplay = params?.timeToCalibrateId ?? '';
39
+ this.soundMessageId = params?.soundMessageId ?? '';
40
+ this.titleDisplayId = params?.titleDisplayId ?? '';
41
+ this.timeToCalibrate = params?.timeToCalibrate ?? 10;
42
+
43
+ /* Set up callbacks that handle any events related to our peer object. */
44
+ this.peer.on('open', this.#onPeerOpen);
45
+ this.peer.on('connection', this.#onPeerConnection);
46
+ this.peer.on('close', this.onPeerClose);
47
+ this.peer.on('disconnected', this.#onPeerDisconnected);
48
+ this.peer.on('error', this.#onPeerError);
49
+ }
50
+ /**
51
+ * Async factory method that creates the Speaker object, and returns a promise that resolves to the result of the calibration.
52
+ *
53
+ * @param params - The parameters to be passed to the peer object.
54
+ * @param Calibrator - The class that defines the calibration process.
55
+ * @param CalibratorInstance
56
+ * @param timeOut - The amount of time to wait before timing out the connection (in milliseconds).
57
+ * @public
58
+ * @example
59
+ */
60
+ static startCalibration = async (params, CalibratorInstance, timeOut = 180000) => {
61
+ window.speaker = new Speaker(params, CalibratorInstance);
62
+ const {speaker} = window;
63
+
64
+ // wrap the calibration process in a promise so we can await it
65
+ return new Promise((resolve, reject) => {
66
+ // when a call is received
67
+ speaker.peer.on('call', async call => {
68
+ // Answer the call (one way)
69
+
70
+ call.answer();
71
+ speaker.#removeUIElems();
72
+ speaker.#showSpinner();
73
+ speaker.ac.createLocalAudio(document.getElementById(speaker.targetElement));
74
+ // when we start receiving audio
75
+ call.on('stream', async stream => {
76
+ window.localStream = stream;
77
+ window.localAudio.srcObject = stream;
78
+ window.localAudio.autoplay = false;
79
+
80
+ // if the sinkSamplingRate is not set sleep
81
+ while (!speaker.ac.sampleRatesSet()) {
82
+ console.log('SinkSamplingRate is undefined, sleeping');
83
+ await sleep(1);
84
+ }
85
+
86
+ if (params.displayUpdate) {
87
+ params.displayUpdate.style.display = '';
88
+ }
89
+
90
+ // resolve when we have a result
91
+ speaker.result = await speaker.ac.startCalibration(
92
+ stream,
93
+ params.gainValues,
94
+ params.ICalib,
95
+ params.knownIR,
96
+ params.microphoneName,
97
+ params.calibrateSoundCheck,
98
+ params.isSmartPhone,
99
+ params.calibrateSoundBurstDb,
100
+ params.calibrateSoundBurstRepeats,
101
+ params.calibrateSoundBurstSec,
102
+ params.calibrateSoundBurstsWarmup,
103
+ params.calibrateSoundHz,
104
+ params.calibrateSoundIRSec,
105
+ params.calibrateSoundIIRSec,
106
+ params.calibrateSound1000HzPreSec,
107
+ params.calibrateSound1000HzSec,
108
+ params.calibrateSound1000HzPostSec,
109
+ params.calibrateSoundBackgroundSecs,
110
+ params.calibrateSoundSmoothOctaves,
111
+ params.calibrateSoundPowerBinDesiredSec,
112
+ params.calibrateSoundPowerDbSDToleratedDb,
113
+ params.micManufacturer,
114
+ params.micSerialNumber,
115
+ params.micModelNumber,
116
+ params.micModelName,
117
+ params.calibrateMicrophonesBool,
118
+ params.authorEmails,
119
+ params.webAudioDeviceNames,
120
+ params.IDsToSaveInSoundProfileLibrary,
121
+ params.restartButton,
122
+ params.calibrateSoundLimit
123
+ );
124
+ speaker.#removeUIElems();
125
+ resolve(speaker.result);
126
+ });
127
+ // if we do not receive a result within the timeout, reject
128
+ setTimeout(() => {
129
+ reject(
130
+ new CalibrationTimedOutError(
131
+ `Calibration failed to produce a result after ${
132
+ timeOut / 1000
133
+ } seconds. Please try again.`
134
+ )
135
+ );
136
+ }, timeOut);
137
+ });
138
+ });
139
+ };
140
+
141
+ static testIIR = async (params, CalibratorInstance, IIR, timeOut = 180000) => {
142
+ window.speaker = new Speaker(params, CalibratorInstance);
143
+ const {speaker} = window;
144
+
145
+ // wrap the calibration process in a promise so we can await it
146
+ return new Promise((resolve, reject) => {
147
+ // when a call is received
148
+ speaker.peer.on('call', async call => {
149
+ // Answer the call (one way)
150
+ call.answer();
151
+ speaker.#removeUIElems();
152
+ speaker.#showSpinner();
153
+ speaker.ac.createLocalAudio(document.getElementById(speaker.targetElement));
154
+ // when we start receiving audio
155
+ call.on('stream', async stream => {
156
+ window.localStream = stream;
157
+ window.localAudio.srcObject = stream;
158
+ window.localAudio.autoplay = false;
159
+
160
+ // if the sinkSamplingRate is not set sleep
161
+ while (!speaker.ac.sampleRatesSet()) {
162
+ console.log('SinkSamplingRate is undefined, sleeping');
163
+ await sleep(1);
164
+ }
165
+ // resolve when we have a result
166
+ speaker.result = await speaker.ac.playMLSwithIIR(stream, IIR);
167
+ speaker.#removeUIElems();
168
+ resolve(speaker.result);
169
+ });
170
+ // if we do not receive a result within the timeout, reject
171
+ setTimeout(() => {
172
+ reject(
173
+ new CalibrationTimedOutError(
174
+ `Calibration failed to produce a result after ${
175
+ timeOut / 1000
176
+ } seconds. Please try again.`
177
+ )
178
+ );
179
+ }, timeOut);
180
+ });
181
+ });
182
+ };
183
+
184
+ /**
185
+ * Called after the peer conncection has been opened.
186
+ * Generates a QR code for the connection and displays it.
187
+ *
188
+ * @private
189
+ * @example
190
+ */
191
+ #showQRCode = () => {
192
+ // Get query string, the URL parameters to specify a Listener
193
+ const queryStringParameters = {
194
+ speakerPeerId: this.peer.id,
195
+ isSmartPhone: this.isSmartPhone,
196
+ calibrateSoundHz: this.calibrateSoundHz,
197
+ calibrateSoundSamplingDesiredBits: this.calibrateSoundSamplingDesiredBits,
198
+ lang: this.language,
199
+ };
200
+ const queryString = this.queryStringFromObject(queryStringParameters);
201
+ const uri = this.siteUrl + queryString;
202
+ if (this.isSmartPhone) {
203
+ // Display QR code for the participant to scan
204
+ const qrCanvas = document.createElement('canvas');
205
+ qrCanvas.setAttribute('id', 'qrCanvas');
206
+ console.log(uri);
207
+ QRCode.toCanvas(qrCanvas, uri, error => {
208
+ if (error) console.error(error);
209
+ });
210
+ const qrImage = new Image(400, 400);
211
+ qrImage.setAttribute('id', 'compatibilityCheckQRImage');
212
+ qrImage.style.zIndex = Infinity;
213
+ qrImage.style.width = 400;
214
+ qrImage.style.height = 400;
215
+ qrImage.style.aspectRatio = 1;
216
+ qrImage.src = qrCanvas.toDataURL();
217
+ qrImage.style.maxHeight = '150px';
218
+ qrImage.style.maxWidth = '150px';
219
+ document.getElementById(this.targetElement).appendChild(qrImage);
220
+ } else {
221
+ // show the link to the user
222
+ // If specified HTML Id is available, show QR code there
223
+ if (document.getElementById(this.targetElement)) {
224
+ // const linkTag = document.createElement('a');
225
+ // linkTag.setAttribute('href', uri);
226
+ // linkTag.innerHTML = 'Click here to start the calibration';
227
+ // linkTag.target = '_blank';
228
+ // document.getElementById(this.targetElement).appendChild(linkTag);
229
+ // document.getElementById(this.targetElement).appendChild(qrCanvas);
230
+
231
+ const proceedButton = document.createElement('button');
232
+ proceedButton.setAttribute('id', 'calibrationProceedButton');
233
+ proceedButton.setAttribute('class', 'btn btn-success');
234
+ proceedButton.innerHTML = 'Proceed';
235
+ proceedButton.onclick = () => {
236
+ // open the link in a new tab
237
+ window.open(uri, '_blank');
238
+ // remove the button
239
+ document.getElementById('calibrationProceedButton').remove();
240
+ };
241
+ document.getElementById(this.targetElement).appendChild(proceedButton);
242
+ }
243
+ }
244
+ // or just print it to console
245
+ console.log('TEST: Peer reachable at: ', uri);
246
+ };
247
+
248
+ #showSpinner = () => {
249
+ const spinner = document.createElement('div');
250
+ spinner.className = 'spinner-border ml-auto';
251
+ spinner.role = 'status';
252
+ spinner.ariaHidden = 'true';
253
+ document.getElementById(this.targetElement).appendChild(spinner);
254
+
255
+ // clear instructionDisplay
256
+ const soundMessage = document.getElementById(this.soundMessageId);
257
+ soundMessage.innerHTML = '';
258
+ soundMessage.style.display = 'none';
259
+ const instructionDisplay = document.getElementById(this.instructionDisplayId);
260
+ const background = document.getElementById('background'); // todo: get background id from params
261
+ const subtitle = document.getElementById(this.soundSubtitleId);
262
+ if (subtitle) {
263
+ subtitle.innerHTML = '';
264
+ }
265
+ if (instructionDisplay) {
266
+ instructionDisplay.innerHTML = '';
267
+ instructionDisplay.style.whiteSpace = 'nowrap';
268
+ instructionDisplay.style.fontWeight = 'bold';
269
+ instructionDisplay.style.width = 'fit-content';
270
+ instructionDisplay.innerHTML = phrases.RC_soundRecording[this.language];
271
+ let fontSize = 100;
272
+ instructionDisplay.style.fontSize = fontSize + 'px';
273
+ while (instructionDisplay.scrollWidth > background.scrollWidth * 0.9 && fontSize > 10) {
274
+ fontSize--;
275
+ instructionDisplay.style.fontSize = fontSize + 'px';
276
+ }
277
+ // const p = document.createElement('p');
278
+ // // font size
279
+ // p.style.fontSize = '1.1rem';
280
+ // p.style.fontWeight = 'normal';
281
+ // p.style.paddingTop = '20px';
282
+ // const timeToCalibrateText = phrases.RC_howLongToCalibrate['en-US'];
283
+ // p.innerHTML = timeToCalibrateText.replace('111', this.timeToCalibrate);
284
+ // instructionDisplay.appendChild(p);
285
+ }
286
+
287
+ const timeToCalibrateDisplay = document.getElementById(this.timeToCalibrateDisplay);
288
+ if (timeToCalibrateDisplay) {
289
+ const timeToCalibrateText = phrases.RC_howLongToCalibrate[this.language];
290
+ timeToCalibrateDisplay.innerHTML = timeToCalibrateText.replace('111', this.timeToCalibrate);
291
+ timeToCalibrateDisplay.style.fontWeight = 'normal';
292
+ timeToCalibrateDisplay.style.fontSize = '1rem';
293
+ // timeToCalibrateDisplay.style.paddingTop = '20px';
294
+ }
295
+
296
+ // Update title - titleDisplayId
297
+ const titleDisplay = document.getElementById(this.titleDisplayId);
298
+ if (titleDisplay) {
299
+ // replace 5 with 6
300
+ titleDisplay.innerHTML = this.isSmartPhone
301
+ ? titleDisplay.innerHTML.replace('2', '3')
302
+ : titleDisplay.innerHTML.replace('4', '5');
303
+ }
304
+ };
305
+
306
+ #removeUIElems = () => {
307
+ const parent = document.getElementById(this.targetElement);
308
+ while (parent.firstChild) {
309
+ parent.firstChild.remove();
310
+ }
311
+ };
312
+
313
+ /**
314
+ * Called when the peer connection is opened.
315
+ * Saves the peer id and calls the QR code generator.
316
+ *
317
+ * @param peerId - The peer id of the peer connection.
318
+ * @param id
319
+ * @private
320
+ * @example
321
+ */
322
+ #onPeerOpen = id => {
323
+ // Workaround for peer.reconnect deleting previous id
324
+ if (id === null) {
325
+ console.error('Received null id from peer open');
326
+ this.peer.id = this.lastPeerId;
327
+ } else {
328
+ this.lastPeerId = this.peer.id;
329
+ }
330
+
331
+ if (id !== this.peer.id) {
332
+ console.warn('DEBUG Check you assumption that id === this.peer.id');
333
+ }
334
+
335
+ this.#showQRCode();
336
+ };
337
+
338
+ /**
339
+ * Called when the peer connection is established.
340
+ * Enforces a single connection.
341
+ *
342
+ * @param connection - The connection object.
343
+ * @private
344
+ * @example
345
+ */
346
+ #onPeerConnection = connection => {
347
+ // Allow only a single connection
348
+ if (this.conn && this.conn.open) {
349
+ connection.on('open', () => {
350
+ connection.send('Already connected to another client');
351
+ setTimeout(() => {
352
+ connection.close();
353
+ }, 500);
354
+ });
355
+ return;
356
+ }
357
+
358
+ this.conn = connection;
359
+ console.log('Connected to: ', this.conn.peer);
360
+ this.#ready();
361
+ };
362
+
363
+ /**
364
+ * Called when the peer connection is closed.
365
+ *
366
+ * @private
367
+ * @example
368
+ */
369
+ onPeerClose = () => {
370
+ this.conn = null;
371
+ console.log('Connection destroyed');
372
+ };
373
+
374
+ static closeConnection = () => {
375
+ this.conn = null;
376
+ console.log('Connection destroyed');
377
+ };
378
+
379
+ /**
380
+ * Called when the peer connection is disconnected.
381
+ * Attempts to reconnect.
382
+ *
383
+ * @private
384
+ * @example
385
+ */
386
+ #onPeerDisconnected = () => {
387
+ console.log('Connection lost. Please reconnect');
388
+
389
+ // Workaround for peer.reconnect deleting previous id
390
+ this.peer.id = this.lastPeerId;
391
+ // eslint-disable-next-line no-underscore-dangle
392
+ this.peer._lastServerId = this.lastPeerId;
393
+ this.peer.reconnect();
394
+ };
395
+
396
+ /**
397
+ * Called when the peer connection encounters an error.
398
+ *
399
+ * @param error
400
+ * @private
401
+ * @example
402
+ */
403
+ #onPeerError = error => {
404
+ // TODO: check if this function is needed or not
405
+ console.error(error);
406
+ };
407
+
408
+ /**
409
+ * Called when data is received from the peer connection.
410
+ *
411
+ * @param data
412
+ * @private
413
+ * @example
414
+ */
415
+ #onIncomingData = data => {
416
+ // enforce object type
417
+ if (
418
+ !Object.prototype.hasOwnProperty.call(data, 'name') ||
419
+ !Object.prototype.hasOwnProperty.call(data, 'payload')
420
+ ) {
421
+ console.error('Received malformed data: ', data);
422
+ return;
423
+ }
424
+
425
+ switch (data.name) {
426
+ case 'samplingRate':
427
+ this.ac.setSamplingRates(data.payload);
428
+ break;
429
+ case 'sampleSize':
430
+ this.ac.setSampleSize(data.payload);
431
+ break;
432
+ case 'deviceType':
433
+ this.ac.setDeviceType(data.payload);
434
+ break;
435
+ case 'deviceName':
436
+ this.ac.setDeviceName(data.payload);
437
+ break;
438
+ case 'deviceInfo':
439
+ this.ac.setDeviceInfo(data.payload);
440
+ console.log('Received device info from listener: ', data.payload);
441
+ break;
442
+ case UnsupportedDeviceError.name:
443
+ case MissingSpeakerIdError.name:
444
+ throw data.payload;
445
+ break;
446
+ default:
447
+ break;
448
+ }
449
+ };
450
+
451
+ /**
452
+ * Called when the peer connection is #ready.
453
+ *
454
+ * @private
455
+ * @example
456
+ */
457
+ #ready = () => {
458
+ // Perform callback with data
459
+ this.conn.on('data', this.#onIncomingData);
460
+ this.conn.on('close', () => {
461
+ console.log('Connection reset<br>Awaiting connection...');
462
+ this.conn = null;
463
+ });
464
+ };
465
+
466
+ /** .
467
+ * .
468
+ * .
469
+ * Debug method for downloading the recorded audio
470
+ *
471
+ * @public
472
+ * @example
473
+ */
474
+ downloadData = () => {
475
+ this.ac.downloadData();
476
+ };
477
+ }
478
+
479
+ /*
480
+ Referenced links:
481
+ https://stackoverflow.com/questions/28016664/when-you-pass-this-as-an-argument/28016676#28016676
482
+ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
483
+ https://stackoverflow.com/questions/879152/how-do-i-make-javascript-beep [3]
484
+ */
485
+
486
+ export default Speaker;