speaker-calibration 2.2.218 → 2.2.219

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "speaker-calibration",
3
- "version": "2.2.218",
3
+ "version": "2.2.219",
4
4
  "description": "Speaker calibration library for auditory testing",
5
5
  "main": "dist/main.js",
6
6
  "directories": {
@@ -1,6 +1,6 @@
1
1
  // get element with id message
2
2
  import {phrases} from '../../dist/example/i18n.js';
3
- import {Listener} from '../main.js';
3
+ import Listener from '../peer-connection/listener.js';
4
4
  // get url query parameters
5
5
  const urlParams = new URLSearchParams(window.location.search);
6
6
 
@@ -16,117 +16,315 @@ const listenerParameters = {
16
16
  const container = document.getElementById('listenerContainer');
17
17
  const recordingInProgress = phrases.RC_soundRecording['en-US'];
18
18
  const backToExperimentWindow = phrases.RC_backToExperimentWindow['en-US'];
19
- const allowMicrophone = phrases.RC_allowMicrophoneUse['en-US'].replace(/\n/g, '<br>');
20
- const placeSmartphoneMicrophone = phrases.RC_placeSmartphoneMicrophone['en-US'].replace(
21
- /\n/g,
22
- '<br>'
23
- );
24
- const turnMeToReadBelow = phrases.RC_turnMeToReadBelow['en-US'].replace(/\n/g, '<br>');
19
+ const allowMicrophone = phrases.RC_allowMicrophoneUse['en-US'];
20
+ const placeSmartphoneMicrophone = phrases.RC_placeSmartphoneMicrophone['en-US'];
21
+ const turnMeToReadBelow = phrases.RC_turnMeToReadBelow['en-US'];
25
22
  const recordingInProgressElement = document.getElementById('recordingInProgress');
26
23
  const allowMicrophoneElement = document.getElementById('allowMicrophone');
27
24
  const turnMessageElement = document.getElementById('turnMeToReadBelow');
28
25
 
29
26
  switch (isSmartPhone) {
30
27
  case 'true':
31
- allowMicrophoneElement.innerHTML = placeSmartphoneMicrophone;
32
- allowMicrophoneElement.style.lineHeight = '1.2rem';
33
- allowMicrophoneElement.style.fontSize = '14px';
34
- turnMessageElement.innerHTML = turnMeToReadBelow;
35
- turnMessageElement.style.lineHeight = '1.2rem';
36
- turnMessageElement.style.fontSize = '14px';
37
- // show the html upsidedown
38
- const phrasesContainer = document.getElementById('phrases');
39
- // add class
40
- phrasesContainer.classList.add('phrases');
41
- const html = document.querySelector('html');
42
- html.style.overflow = 'hidden';
43
- const display = document.getElementById('updateDisplay');
44
- display.classList.add('updateDisplay');
45
- container.style.display = 'block';
46
- // event listener for id calibrationBeginButton
47
- const calibrationBeginButton = document.getElementById('calibrationBeginButton');
48
- console.log('Waiting for proceed button click');
28
+ //hide target element
29
+ const targetElement = document.getElementById('display');
30
+ targetElement.style.display = 'none';
31
+ // Initialize Listener early
32
+ const initializeListener = async () => {
33
+ window.listener = new Listener(listenerParameters);
34
+ };
49
35
 
50
- calibrationBeginButton.addEventListener('click', async () => {
51
- console.log('Proceed button clicked');
36
+ // Check microphone permission first
37
+ async function checkAndRequestMicrophonePermission() {
38
+ try {
39
+ const permissionStatus = await navigator.permissions.query({name: 'microphone'});
52
40
 
53
- // remove the button
54
- calibrationBeginButton.remove();
55
- // remove turn message
56
- turnMessageElement.remove();
57
- // set the text of the html elements
58
- recordingInProgressElement.innerHTML = recordingInProgress;
59
- allowMicrophoneElement.innerHTML = allowMicrophone;
41
+ if (permissionStatus.state === 'granted') {
42
+ // Permission already granted, proceed to normal flow
43
+ initializeSmartPhoneDisplay();
44
+ return;
45
+ }
60
46
 
61
- recordingInProgressElement.style.whiteSpace = 'nowrap';
62
- recordingInProgressElement.style.fontWeight = 'bold';
63
- // fit content
64
- recordingInProgressElement.style.width = 'fit-content';
65
- let fontSize = 100;
66
- recordingInProgressElement.style.fontSize = fontSize + 'px';
47
+ // Show permission request message
48
+ allowMicrophoneElement.innerText = phrases.RC_microphonePermission['en-US'];
49
+ container.style.display = 'block';
67
50
 
68
- console.log('Adjusting font size for recording in progress text');
69
- while (recordingInProgressElement.scrollWidth > window.innerWidth * 0.9 && fontSize > 10) {
70
- fontSize--;
71
- recordingInProgressElement.style.fontSize = fontSize + 'px';
72
- }
73
- console.log('Done adjusting font size for recording in progress text');
74
- const webAudioDeviceNames = {microphone: '', deviceID: ''};
75
- const externalMicList = ['UMIK', 'Airpods', 'Bluetooth'];
76
- try {
77
- console.log('Getting user media...Should ask for microphone permission');
78
- const stream = await navigator.mediaDevices.getUserMedia({audio: true});
79
- console.log('Got user media');
80
- if (stream) {
81
- console.log('Getting devices');
82
- const devices = await navigator.mediaDevices.enumerateDevices();
83
- console.log(devices);
84
- const mics = devices.filter(device => device.kind === 'audioinput');
85
- mics.forEach(mic => {
86
- if (externalMicList.some(externalMic => mic.label.includes(externalMic))) {
87
- webAudioDeviceNames.microphone = mic.label;
88
- webAudioDeviceNames.deviceID = mic.deviceId;
51
+ // Function to request microphone access
52
+ async function requestMicAccess(attempt = 1) {
53
+ try {
54
+ await navigator.mediaDevices.getUserMedia({audio: true});
55
+ // Permission granted, proceed to normal flow
56
+ initializeSmartPhoneDisplay();
57
+ } catch (err) {
58
+ if (err.name === 'NotAllowedError') {
59
+ console.log('Permission explicitly denied');
60
+ // Permission explicitly denied
61
+ allowMicrophoneElement.innerText = phrases.RC_microphonePermissionDenied['en-US'];
62
+ // Send denied status and end study
63
+ let error = JSON.stringify(err);
64
+ await window.listener.sendPermissionStatus({type: 'denied', error: error});
65
+ return;
66
+ }
67
+
68
+ // If 10 seconds passed, try again
69
+ if (attempt < 3) {
70
+ console.log('Retrying microphone access');
71
+ // Limit retries
72
+ setTimeout(() => requestMicAccess(attempt + 1), 10000);
73
+ } else {
74
+ console.log('All retries failed, treating as denied');
75
+ // After all retries failed, treat as denied
76
+ allowMicrophoneElement.innerText = phrases.RC_microphonePermissionDenied['en-US'];
77
+ let error = JSON.stringify(err);
78
+ await window.listener.sendPermissionStatus({type: 'error', error: error});
89
79
  }
90
- });
91
- if (webAudioDeviceNames.microphone === '') {
92
- webAudioDeviceNames.microphone = mics[0].label;
93
- webAudioDeviceNames.deviceID = mics[0].deviceId;
94
80
  }
95
81
  }
82
+
83
+ requestMicAccess();
96
84
  } catch (err) {
97
- console.log(err);
85
+ console.error('Error checking microphone permission:', err);
86
+ allowMicrophoneElement.innerText = phrases.RC_microphonePermissionDenied['en-US'];
87
+ let error = JSON.stringify(err);
88
+ await window.listener.sendPermissionStatus({type: 'error', error: error});
98
89
  }
99
- listenerParameters.microphoneFromAPI = webAudioDeviceNames.microphone;
100
- listenerParameters.microphoneDeviceId = webAudioDeviceNames.microphone;
101
- let lock = null;
102
- try {
103
- if ('wakeLock' in navigator) {
104
- lock = await navigator.wakeLock.request('screen');
90
+ }
91
+
92
+ function initializeSmartPhoneDisplay() {
93
+ allowMicrophoneElement.innerText = placeSmartphoneMicrophone;
94
+ allowMicrophoneElement.style.lineHeight = '1.2rem';
95
+ allowMicrophoneElement.style.fontSize = '14px';
96
+ turnMessageElement.innerText = turnMeToReadBelow;
97
+ turnMessageElement.style.lineHeight = '1.2rem';
98
+ turnMessageElement.style.fontSize = '14px';
99
+
100
+ // Show the html upsidedown and adjust layout
101
+ const phrasesContainer = document.getElementById('phrases');
102
+ phrasesContainer.classList.add('phrases');
103
+
104
+ // Hide all elements except what's needed for calibration
105
+ const html = document.querySelector('html');
106
+ html.style.overflow = 'hidden';
107
+
108
+ // Adjust the display container
109
+ const display = document.getElementById('updateDisplay');
110
+ display.classList.add('updateDisplay');
111
+ display.style.position = 'absolute';
112
+ display.style.top = '50%';
113
+ display.style.left = '50%';
114
+ display.style.transform = 'translate(-50%, -50%) rotate(180deg)';
115
+ display.style.width = '100%';
116
+ display.style.textAlign = 'center';
117
+
118
+ container.style.display = 'block';
119
+
120
+ // event listener for id calibrationBeginButton
121
+ const calibrationBeginButton = document.getElementById('calibrationBeginButton');
122
+ console.log('Waiting for proceed button click');
123
+
124
+ calibrationBeginButton.addEventListener('click', async () => {
125
+ console.log('Proceed button clicked');
126
+
127
+ // Clear unnecessary elements
128
+ calibrationBeginButton.remove();
129
+ turnMessageElement.remove();
130
+
131
+ // Create a header container for fixed elements
132
+ const headerContainer = document.createElement('div');
133
+ headerContainer.id = 'headerContainer';
134
+ headerContainer.style.position = 'fixed';
135
+ headerContainer.style.bottom = '0';
136
+ headerContainer.style.left = '0';
137
+ headerContainer.style.width = '100%';
138
+ headerContainer.style.background = 'white';
139
+ headerContainer.style.padding = '10px';
140
+ headerContainer.style.zIndex = '1000';
141
+ headerContainer.style.transform = 'rotate(180deg)';
142
+ container.appendChild(headerContainer);
143
+
144
+ // Set title based on screen width
145
+ const title = document.createElement('h1');
146
+ const titleText =
147
+ window.innerWidth >= 1366
148
+ ? phrases.RC_soundRecording['en-US']
149
+ : phrases.RC_soundRecordingSmallScreen['en-US'];
150
+
151
+ // Split small screen title into lines if needed
152
+ if (window.innerWidth < 1366 && titleText.includes('\n')) {
153
+ const lines = titleText.split('\n');
154
+
155
+ // Create container for title lines
156
+ const titleContainer = document.createElement('div');
157
+ titleContainer.style.display = 'flex';
158
+ titleContainer.style.flexDirection = 'column';
159
+ titleContainer.style.alignItems = 'left';
160
+ titleContainer.style.lineHeight = '1.2';
161
+
162
+ // Add each line
163
+ lines.forEach(line => {
164
+ const lineDiv = document.createElement('p');
165
+ lineDiv.textContent = line;
166
+ lineDiv.style.width = 'fit-content';
167
+ titleContainer.appendChild(lineDiv);
168
+ });
169
+
170
+ title.appendChild(titleContainer);
171
+ } else {
172
+ title.textContent = titleText;
173
+ title.style.lineHeight = '1.2';
105
174
  }
106
- } catch (err) {
107
- console.log(err);
108
- }
109
- console.log(lock);
110
- console.log('Starting Calibration');
111
- console.log('Device id in example listenr:', listenerParameters.microphoneDeviceId);
112
- window.listener = new Listener(listenerParameters);
113
- console.log(window.listener);
114
- if (lock) {
115
- lock.release();
175
+
176
+ title.style.margin = '0';
177
+ title.style.whiteSpace = 'pre-line'; // Preserve line breaks
178
+ headerContainer.appendChild(title);
179
+
180
+ // Function to adjust font size to fill width
181
+ const adjustFontSize = (element, maxWidth) => {
182
+ let fontSize = 20; // Start with a reasonable minimum size
183
+ element.style.fontSize = fontSize + 'px';
184
+ // Increase font size until text fills width (minus margins)
185
+ while (element.scrollWidth < maxWidth - 40 && fontSize < 200) {
186
+ fontSize++;
187
+ element.style.fontSize = fontSize + 'px';
188
+ }
189
+
190
+ // Step back one to ensure we don't overflow
191
+ fontSize--;
192
+ element.style.fontSize = fontSize + 'px';
193
+ return fontSize;
194
+ };
195
+
196
+ // For small screen, ensure all lines use same font size
197
+ if (window.innerWidth < 1366 && titleText.includes('\n')) {
198
+ const lines = title.querySelectorAll('p');
199
+ let minFontSize = Infinity;
200
+
201
+ // First pass: find the smallest font size that fits for any line
202
+ lines.forEach(line => {
203
+ const fontSize = adjustFontSize(line, window.innerWidth);
204
+ minFontSize = Math.min(minFontSize, fontSize);
205
+ });
206
+
207
+ // Second pass: apply the smallest font size to all lines
208
+ lines.forEach(line => {
209
+ line.style.fontSize = minFontSize + 'px';
210
+ });
211
+ } else {
212
+ // For single line title, just adjust to fill width
213
+ adjustFontSize(title, window.innerWidth);
214
+ }
215
+
216
+ // Get the header height after text is added and sized
217
+ const headerHeight = headerContainer.getBoundingClientRect().height;
218
+
219
+ // Adjust the display container to start after header
220
+ const display = document.getElementById('updateDisplay');
221
+ display.classList.add('updateDisplay');
222
+ display.style.position = 'fixed';
223
+ display.style.bottom = `${headerHeight}px`; // Start after header
224
+ display.style.left = '0';
225
+ display.style.right = '0';
226
+ display.style.top = '0';
227
+ display.style.transform = 'rotate(180deg)';
228
+ display.style.overflowY = 'auto';
229
+ display.style.padding = '20px';
230
+ display.style.background = 'white';
231
+
232
+ // Position microphone instruction at the top (appears at bottom due to rotation)
233
+ allowMicrophoneElement.innerText = '';
234
+ allowMicrophoneElement.style.position = 'fixed';
235
+ allowMicrophoneElement.style.top = '20px';
236
+ allowMicrophoneElement.style.left = '50%';
237
+ allowMicrophoneElement.style.transform = 'translateX(-50%) rotate(180deg)';
238
+ allowMicrophoneElement.style.width = '90%';
239
+ allowMicrophoneElement.style.textAlign = 'center';
240
+ allowMicrophoneElement.style.zIndex = '1000';
241
+
242
+ let lock = null;
243
+ try {
244
+ if ('wakeLock' in navigator) {
245
+ lock = await navigator.wakeLock.request('screen');
246
+ }
247
+ } catch (err) {
248
+ console.log(err);
249
+ }
250
+
251
+ const webAudioDeviceNames = {microphone: '', deviceID: ''};
252
+ const externalMicList = ['UMIK', 'Airpods', 'Bluetooth'];
253
+ try {
254
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
255
+ if (stream) {
256
+ const devices = await navigator.mediaDevices.enumerateDevices();
257
+ const mics = devices.filter(device => device.kind === 'audioinput');
258
+ mics.forEach(mic => {
259
+ if (externalMicList.some(externalMic => mic.label.includes(externalMic))) {
260
+ webAudioDeviceNames.microphone = mic.label;
261
+ webAudioDeviceNames.deviceID = mic.deviceId;
262
+ }
263
+ });
264
+ if (webAudioDeviceNames.microphone === '') {
265
+ webAudioDeviceNames.microphone = mics[0].label;
266
+ webAudioDeviceNames.deviceID = mics[0].deviceId;
267
+ }
268
+ }
269
+ } catch (err) {
270
+ console.log(err);
271
+ }
272
+ window.listener.setMicrophoneFromAPI(webAudioDeviceNames.microphone);
273
+ window.listener.setMicrophoneDeviceId(webAudioDeviceNames.microphone);
274
+ // show target element
275
+ targetElement.style.display = 'block';
276
+ await window.listener.startCalibration();
277
+ if (lock) {
278
+ lock.release();
279
+ }
280
+ });
281
+ }
282
+
283
+ // Wrap the initialization in an IIFE
284
+ (async function initializeSmartPhoneMode() {
285
+ await initializeListener();
286
+
287
+ const timeout = 30000; // 30 seconds timeout
288
+ const startTime = Date.now();
289
+
290
+ // Wait for peer connection setup with timeout
291
+ while (Date.now() - startTime < timeout) {
292
+ if (
293
+ window.listener.peer.id !== null &&
294
+ window.listener.conn !== null &&
295
+ window.listener.connOpen
296
+ ) {
297
+ console.log('Connection established successfully');
298
+ await checkAndRequestMicrophonePermission();
299
+ return;
300
+ }
301
+ console.log('Waiting for connection setup...');
302
+ await new Promise(resolve => setTimeout(resolve, 100));
116
303
  }
117
- });
304
+
305
+ // If we get here, we've timed out
306
+ console.error('Connection setup timed out after 30 seconds');
307
+ allowMicrophoneElement.innerText = phrases.RC_microphonePermissionDenied['en-US'];
308
+ await window.listener.sendPermissionStatus({
309
+ type: 'error',
310
+ error: 'Connection setup timed out after 30 seconds',
311
+ });
312
+ })().catch(console.error);
118
313
  break;
119
314
  case 'false':
315
+ // Initialize listener immediately
316
+ listenerParameters.microphoneDeviceId = urlParams.get('deviceId');
317
+ window.listener = new Listener(listenerParameters);
318
+
120
319
  // remove the button
121
320
  const calibrationBeginButton2 = document.getElementById('calibrationBeginButton');
122
321
  calibrationBeginButton2.remove();
123
322
  container.style.display = 'block';
124
- // event listener for when the page is loaded
125
323
 
126
- window.addEventListener('load', () => {
324
+ window.addEventListener('load', async () => {
127
325
  // set the text of the html elements
128
- recordingInProgressElement.innerHTML = recordingInProgress;
129
- allowMicrophoneElement.innerHTML = allowMicrophone;
326
+ recordingInProgressElement.innerText = recordingInProgress;
327
+ allowMicrophoneElement.innerText = allowMicrophone;
130
328
 
131
329
  recordingInProgressElement.style.whiteSpace = 'nowrap';
132
330
  recordingInProgressElement.style.fontWeight = 'bold';
@@ -143,10 +341,10 @@ switch (isSmartPhone) {
143
341
  const message = document.getElementById('message');
144
342
  message.style.lineHeight = '2.5rem';
145
343
  const p = document.createElement('p');
146
- p.innerHTML = backToExperimentWindow;
344
+ p.innerText = backToExperimentWindow;
147
345
  message.appendChild(p);
148
- listenerParameters.microphoneDeviceId = urlParams.get('deviceId');
149
- window.listener = new Listener(listenerParameters);
346
+
347
+ await window.listener.startCalibration();
150
348
  console.log(window.listener);
151
349
  });
152
350
  break;
package/src/main.js CHANGED
@@ -1,4 +1,4 @@
1
- import Listener from './peer-connection/listener';
1
+ // import Listener from './peer-connection/listener';
2
2
  import Speaker from './peer-connection/speaker';
3
3
 
4
4
  import VolumeCalibration from './tasks/volume/volume';
@@ -12,7 +12,6 @@ import {
12
12
  } from './peer-connection/peerErrors';
13
13
 
14
14
  export {
15
- Listener,
16
15
  Speaker,
17
16
  VolumeCalibration,
18
17
  ImpulseResponseCalibration,
@@ -32,6 +32,7 @@ class Listener extends AudioPeer {
32
32
  // previous calibrateSoundSamplingDesiredBits
33
33
  urlParameters.bits !== null && urlParameters.bits !== undefined ? urlParameters.bits : 24;
34
34
  this.speakerPeerId = urlParameters.speakerPeerId;
35
+ this.connOpen = false;
35
36
 
36
37
  this.peer.on('open', this.onPeerOpen);
37
38
  this.peer.on('connection', this.onPeerConnection);
@@ -71,22 +72,22 @@ class Listener extends AudioPeer {
71
72
  onConnData = data => {
72
73
  this.displayUpdate('Listener - onConnData');
73
74
  const hasSpeakerID = Object.prototype.hasOwnProperty.call(data, 'speakerPeerId');
74
- if (!hasSpeakerID) {
75
- this.displayUpdate('Error in parsing data received! Must set "speakerPeerId" property');
76
- throw new MissingSpeakerIdError('Must set "speakerPeerId" property');
77
- } else {
78
- // this.conn.close();
79
- this.displayUpdate(this.speakerPeerId);
80
- this.speakerPeerId = data.speakerPeerId;
81
- const newParams = {
82
- speakerPeerId: this.speakerPeerId,
83
- };
84
- /*
85
- FUTURE does this limit usable environments?
86
- ie does this work if internet is lost after initial page load?
87
- */
88
- window.location.search = this.queryStringFromObject(newParams); // Redirect to correctly constructed keypad page
89
- }
75
+ // if (!hasSpeakerID) {
76
+ // this.displayUpdate('Error in parsing data received! Must set "speakerPeerId" property');
77
+ // throw new MissingSpeakerIdError('Must set "speakerPeerId" property');
78
+ // } else {
79
+ // // this.conn.close();
80
+ // this.displayUpdate(this.speakerPeerId);
81
+ // this.speakerPeerId = data.speakerPeerId;
82
+ // const newParams = {
83
+ // speakerPeerId: this.speakerPeerId,
84
+ // };
85
+ // /*
86
+ // FUTURE does this limit usable environments?
87
+ // ie does this work if internet is lost after initial page load?
88
+ // */
89
+ // window.location.search = this.queryStringFromObject(newParams); // Redirect to correctly constructed keypad page
90
+ // }
90
91
  };
91
92
 
92
93
  join = async () => {
@@ -112,18 +113,23 @@ class Listener extends AudioPeer {
112
113
  this.displayUpdate('Created connection');
113
114
  this.conn.on('open', async () => {
114
115
  this.displayUpdate('Listener - conn open');
116
+ this.connOpen = true;
115
117
  await this.getDeviceInfo();
116
118
  // this.sendSamplingRate();
117
- await this.openAudioStream();
118
119
  });
119
120
 
120
121
  // Handle incoming data (messages only since this is the signal sender)
121
122
  this.conn.on('data', this.onConnData);
122
123
  this.conn.on('close', () => {
123
124
  console.log('Connection closed');
125
+ this.connOpen = false;
124
126
  });
125
127
  };
126
128
 
129
+ startCalibration = async () => {
130
+ await this.openAudioStream();
131
+ };
132
+
127
133
  getMobileOS = () => {
128
134
  const ua = navigator.userAgent;
129
135
  if (/android/i.test(ua)) {
@@ -163,6 +169,14 @@ class Listener extends AudioPeer {
163
169
  });
164
170
  };
165
171
 
172
+ sendPermissionStatus = status => {
173
+ // this.displayUpdate('Listener - sendPermissionStatus');
174
+ this.conn.send({
175
+ name: 'permissionStatus',
176
+ payload: status,
177
+ });
178
+ };
179
+
166
180
  getDeviceInfo = async () => {
167
181
  const deviceInfo = {};
168
182
  try {
@@ -271,6 +285,12 @@ class Listener extends AudioPeer {
271
285
 
272
286
  return contraints;
273
287
  };
288
+ setMicrophoneFromAPI = microphoneFromAPI => {
289
+ this.microphoneFromAPI = microphoneFromAPI;
290
+ };
291
+ setMicrophoneDeviceId = microphoneDeviceId => {
292
+ this.microphoneDeviceId = microphoneDeviceId;
293
+ };
274
294
  getDeviceIdByLabel = async targetLabel => {
275
295
  try {
276
296
  //get permission to use audio first. (Returns empty labels on some computers if not done first)