speaker-calibration 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +72 -0
- package/.eslintrc.json +40 -0
- package/.gitignore +78 -0
- package/.prettierignore +70 -0
- package/.prettierrc +15 -0
- package/LICENSE +20 -20
- package/README.md +133 -133
- package/__mocks__/fileMock.js +1 -0
- package/__mocks__/styleMock.js +1 -0
- package/babel.config.js +3 -0
- package/coverage/clover.xml +71 -0
- package/coverage/coverage-final.json +224 -0
- package/coverage/lcov-report/PythonServerInterface.js.html +265 -0
- package/coverage/lcov-report/base.css +354 -0
- package/coverage/lcov-report/block-navigation.js +82 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +123 -0
- package/coverage/lcov-report/prettify.css +101 -0
- package/coverage/lcov-report/prettify.js +937 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +189 -0
- package/coverage/lcov-report/src/index.html +121 -0
- package/coverage/lcov-report/src/server/PythonServerInterface.js.html +268 -0
- package/coverage/lcov-report/src/server/index.html +123 -0
- package/coverage/lcov-report/src/tasks/audioCalibrator.js.html +499 -0
- package/coverage/lcov-report/src/tasks/audioRecorder.js.html +412 -0
- package/coverage/lcov-report/src/tasks/index.html +143 -0
- package/coverage/lcov-report/src/tasks/volume/index.html +123 -0
- package/coverage/lcov-report/src/tasks/volume/volume.js.html +409 -0
- package/coverage/lcov-report/src/utils.js.html +172 -0
- package/coverage/lcov.info +91 -0
- package/dist/example/Queen-Bohemian_Rhapsody.wav +0 -0
- package/dist/example/Queen-Bohemian_Rhapsody_g_filtered.wav +0 -0
- package/dist/example/index.html +47 -0
- package/dist/example/listener.html +89 -0
- package/dist/example/server.js +49 -0
- package/dist/example/speaker.html +126 -0
- package/dist/example/speakerUI.js +217 -0
- package/dist/example/styles.css +40 -0
- package/dist/main.js +1 -1
- package/dist/mlsGen.js +6814 -6814
- package/dist/mlsGen.wasm +0 -0
- package/doc/AudioCalibrator.html +417 -0
- package/doc/AudioPeer.html +251 -0
- package/doc/AudioRecorder.html +195 -0
- package/doc/ImpulseResponse.html +215 -0
- package/doc/Listener.html +308 -0
- package/doc/MlsGenInterface.html +226 -0
- package/doc/MyEventEmitter.html +274 -0
- package/doc/PythonServerAPI.html +109 -0
- package/doc/Speaker-Calibration-UML-Diagram.png +0 -0
- package/doc/Speaker.html +276 -0
- package/doc/Takes%20a%20target%20element%20where%20html%20elements%20will%20be%20appended..html +128 -0
- package/doc/Takes%20the%20url%20of%20the%20current%20site%0Aand%20a%20target%20element%20where%20html%20elements%20will%20be%20appended..html +138 -0
- package/doc/Takes%20the%20url%20of%20the%20current%20site%20and%20a%20target%20element%20where%20html%20elements%20will%20be%20appended..html +137 -0
- package/doc/Volume.html +88 -0
- package/doc/audioCalibrator.js.html +179 -0
- package/doc/audioPeer.js.html +175 -0
- package/doc/audioRecorder.js.html +163 -0
- package/doc/creates%20a%20new%20AudioRecorder%20instance.%20%0ASets%20up%20the%20audio%20context%20and%20file%20reader..html +114 -0
- package/doc/fonts/OpenSans-Bold-webfont.eot +0 -0
- package/doc/fonts/OpenSans-Bold-webfont.svg +1830 -0
- package/doc/fonts/OpenSans-Bold-webfont.woff +0 -0
- package/doc/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
- package/doc/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
- package/doc/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
- package/doc/fonts/OpenSans-Italic-webfont.eot +0 -0
- package/doc/fonts/OpenSans-Italic-webfont.svg +1830 -0
- package/doc/fonts/OpenSans-Italic-webfont.woff +0 -0
- package/doc/fonts/OpenSans-Light-webfont.eot +0 -0
- package/doc/fonts/OpenSans-Light-webfont.svg +1831 -0
- package/doc/fonts/OpenSans-Light-webfont.woff +0 -0
- package/doc/fonts/OpenSans-LightItalic-webfont.eot +0 -0
- package/doc/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
- package/doc/fonts/OpenSans-LightItalic-webfont.woff +0 -0
- package/doc/fonts/OpenSans-Regular-webfont.eot +0 -0
- package/doc/fonts/OpenSans-Regular-webfont.svg +1831 -0
- package/doc/fonts/OpenSans-Regular-webfont.woff +0 -0
- package/doc/global.html +308 -0
- package/doc/index.html +58 -0
- package/doc/listener.js.html +170 -0
- package/doc/mlsGen_mlsGenInterface.js.html +117 -0
- package/doc/myEventEmitter.js.html +124 -0
- package/doc/peer-connection_audioPeer.js.html +188 -0
- package/doc/peer-connection_listener.js.html +311 -0
- package/doc/peer-connection_speaker.js.html +381 -0
- package/doc/sc-activity-diagram.png +0 -0
- package/doc/scripts/linenumber.js +25 -0
- package/doc/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/doc/scripts/prettify/lang-css.js +24 -0
- package/doc/scripts/prettify/prettify.js +640 -0
- package/doc/server_PythonServerAPI.js.html +160 -0
- package/doc/speaker.js.html +248 -0
- package/doc/styles/jsdoc-default.css +371 -0
- package/doc/styles/prettify-jsdoc.css +111 -0
- package/doc/styles/prettify-tomorrow.css +163 -0
- package/doc/tasks_audioCalibrator.js.html +207 -0
- package/doc/tasks_audioRecorder.js.html +190 -0
- package/doc/tasks_impulse-response_impulseResponse.js.html +442 -0
- package/doc/tasks_impulse-response_mlsGen_mlsGenInterface.js.html +175 -0
- package/doc/tasks_volume_volume.js.html +185 -0
- package/doc/utils.js.html +105 -0
- package/jest.config.js +173 -0
- package/netlify.toml +27 -0
- package/package.json +67 -66
- package/src/index.html +21 -0
- package/src/main.js +21 -0
- package/src/myEventEmitter.js +83 -0
- package/src/peer-connection/audioPeer.js +151 -0
- package/src/peer-connection/listener.js +251 -0
- package/src/peer-connection/peerErrors.js +25 -0
- package/src/peer-connection/speaker.js +346 -0
- package/src/server/PythonServerAPI.js +117 -0
- package/src/tasks/audioCalibrator.js +218 -0
- package/src/tasks/audioRecorder.js +148 -0
- package/src/tasks/impulse-response/impulseResponse.js +436 -0
- package/src/tasks/impulse-response/mlsGen/mlsGen.cpp +99 -0
- package/src/tasks/impulse-response/mlsGen/mlsGen.hpp +304 -0
- package/src/tasks/impulse-response/mlsGen/mlsGenInterface.js +131 -0
- package/src/tasks/impulse-response/mlsGen/mlsGenTest.cpp +181 -0
- package/src/tasks/volume/volume.cpp +3 -0
- package/src/tasks/volume/volume.hpp +23 -0
- package/src/tasks/volume/volume.js +157 -0
- package/src/utils.js +55 -0
- package/webpack.config.js +37 -0
- package/README +0 -3
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import MyEventEmitter from '../myEventEmitter';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @class provides a simple interface for recording audio from a microphone
|
|
5
|
+
* using the Media Recorder API.
|
|
6
|
+
*/
|
|
7
|
+
class AudioRecorder extends MyEventEmitter {
|
|
8
|
+
/** @private */
|
|
9
|
+
#mediaRecorder;
|
|
10
|
+
|
|
11
|
+
/** @private */
|
|
12
|
+
#recordedChunks = [];
|
|
13
|
+
|
|
14
|
+
/** @private */
|
|
15
|
+
#audioBlob;
|
|
16
|
+
|
|
17
|
+
/** @private */
|
|
18
|
+
#audioContext;
|
|
19
|
+
|
|
20
|
+
/** @private */
|
|
21
|
+
#recordedSignals = [];
|
|
22
|
+
|
|
23
|
+
/** @private */
|
|
24
|
+
sinkSamplingRate;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Decode the audio data from the recorded audio blob.
|
|
28
|
+
*
|
|
29
|
+
* @private
|
|
30
|
+
* @example
|
|
31
|
+
*/
|
|
32
|
+
#saveRecording = async () => {
|
|
33
|
+
const arrayBuffer = await this.#audioBlob.arrayBuffer();
|
|
34
|
+
const audioBuffer = await this.#audioContext.decodeAudioData(arrayBuffer);
|
|
35
|
+
const data = audioBuffer.getChannelData(0);
|
|
36
|
+
|
|
37
|
+
console.log(`Decoded audio buffer with ${data.length} samples`);
|
|
38
|
+
this.#recordedSignals.push(Array.from(data));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Event listener triggered when data is available in the media recorder.
|
|
43
|
+
*
|
|
44
|
+
* @private
|
|
45
|
+
* @param e - The event object.
|
|
46
|
+
* @example
|
|
47
|
+
*/
|
|
48
|
+
#onRecorderDataAvailable = e => {
|
|
49
|
+
if (e.data && e.data.size > 0) this.#recordedChunks.push(e.data);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Method to create a media recorder object and set up event listeners.
|
|
54
|
+
*
|
|
55
|
+
* @private
|
|
56
|
+
* @param stream - The stream of audio from the Listener.
|
|
57
|
+
* @example
|
|
58
|
+
*/
|
|
59
|
+
#setMediaRecorder = stream => {
|
|
60
|
+
// Create a new MediaRecorder object
|
|
61
|
+
this.#mediaRecorder = new MediaRecorder(stream);
|
|
62
|
+
|
|
63
|
+
// Add event listeners
|
|
64
|
+
this.#mediaRecorder.ondataavailable = e => this.#onRecorderDataAvailable(e);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
#setAudioContext = () => {
|
|
68
|
+
this.#audioContext = new (window.AudioContext ||
|
|
69
|
+
window.webkitAudioContext ||
|
|
70
|
+
window.audioContext)({
|
|
71
|
+
sampleRate: this.sinkSamplingRate,
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Public method to start the recording process.
|
|
77
|
+
*
|
|
78
|
+
* @param stream - The stream of audio from the Listener.
|
|
79
|
+
* @example
|
|
80
|
+
*/
|
|
81
|
+
startRecording = async stream => {
|
|
82
|
+
// Create a fresh audio context
|
|
83
|
+
this.#setAudioContext();
|
|
84
|
+
// Set up media recorder if needed
|
|
85
|
+
if (!this.#mediaRecorder) this.#setMediaRecorder(stream);
|
|
86
|
+
// clear recorded chunks
|
|
87
|
+
this.#recordedChunks = [];
|
|
88
|
+
// start recording
|
|
89
|
+
this.#mediaRecorder.start();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Method to stop the recording process.
|
|
94
|
+
*
|
|
95
|
+
* @public
|
|
96
|
+
* @example
|
|
97
|
+
*/
|
|
98
|
+
stopRecording = async () => {
|
|
99
|
+
// Stop the media recorder, and wait for the data to be available
|
|
100
|
+
await new Promise(resolve => {
|
|
101
|
+
this.#mediaRecorder.onstop = () => {
|
|
102
|
+
// when the stop event is triggered, resolve the promise
|
|
103
|
+
this.#audioBlob = new Blob(this.#recordedChunks, {
|
|
104
|
+
type: 'audio/wav; codecs=opus',
|
|
105
|
+
});
|
|
106
|
+
resolve(this.#audioBlob);
|
|
107
|
+
};
|
|
108
|
+
// call stop
|
|
109
|
+
this.#mediaRecorder.stop();
|
|
110
|
+
});
|
|
111
|
+
// Now that we have data, save it
|
|
112
|
+
await this.#saveRecording();
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** .
|
|
116
|
+
* .
|
|
117
|
+
* .
|
|
118
|
+
* Public method to get the last recorded audio signal
|
|
119
|
+
*
|
|
120
|
+
* @returns
|
|
121
|
+
* @example
|
|
122
|
+
*/
|
|
123
|
+
getLastRecordedSignal = () => this.#recordedSignals[this.#recordedSignals.length - 1];
|
|
124
|
+
|
|
125
|
+
/** .
|
|
126
|
+
* .
|
|
127
|
+
* .
|
|
128
|
+
* Public method to get all the recorded audio signals
|
|
129
|
+
*
|
|
130
|
+
* @returns
|
|
131
|
+
* @example
|
|
132
|
+
*/
|
|
133
|
+
getAllRecordedSignals = () => this.#recordedSignals;
|
|
134
|
+
|
|
135
|
+
/** .
|
|
136
|
+
* .
|
|
137
|
+
* .
|
|
138
|
+
* Public method to set the sampling rate used by the capture device
|
|
139
|
+
*
|
|
140
|
+
* @param {Number} sinkSamplingRate - The sampling rate of the capture device
|
|
141
|
+
* @example
|
|
142
|
+
*/
|
|
143
|
+
setSinkSamplingRate = sinkSamplingRate => {
|
|
144
|
+
this.sinkSamplingRate = sinkSamplingRate;
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export default AudioRecorder;
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import AudioCalibrator from '../audioCalibrator';
|
|
2
|
+
import MlsGenInterface from './mlsGen/mlsGenInterface';
|
|
3
|
+
|
|
4
|
+
import {sleep, csvToArray} from '../../utils';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
*/
|
|
9
|
+
class ImpulseResponse 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 = 3}) {
|
|
20
|
+
super(numCaptures, numMLSPerCapture);
|
|
21
|
+
this.#mlsOrder = parseInt(mlsOrder, 10);
|
|
22
|
+
this.#P = 2 ** mlsOrder - 1;
|
|
23
|
+
this.#download = download;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @private */
|
|
27
|
+
#download;
|
|
28
|
+
|
|
29
|
+
/** @private */
|
|
30
|
+
#mlsGenInterface;
|
|
31
|
+
|
|
32
|
+
/** @private */
|
|
33
|
+
#mlsBufferView;
|
|
34
|
+
|
|
35
|
+
/** @private */
|
|
36
|
+
invertedImpulseResponse = null;
|
|
37
|
+
|
|
38
|
+
/** @private */
|
|
39
|
+
impulseResponses = [];
|
|
40
|
+
|
|
41
|
+
/** @private */
|
|
42
|
+
#mlsOrder;
|
|
43
|
+
|
|
44
|
+
/** @private */
|
|
45
|
+
#P;
|
|
46
|
+
|
|
47
|
+
/** @private */
|
|
48
|
+
TAPER_SECS = 5;
|
|
49
|
+
|
|
50
|
+
/** @private */
|
|
51
|
+
offsetGainNode;
|
|
52
|
+
|
|
53
|
+
/** .
|
|
54
|
+
* .
|
|
55
|
+
* .
|
|
56
|
+
* Sends all the computed impulse responses to the backend server for processing
|
|
57
|
+
*
|
|
58
|
+
* @returns sets the resulting inverted impulse response to the class property
|
|
59
|
+
* @example
|
|
60
|
+
*/
|
|
61
|
+
sendImpulseResponsesToServerForProcessing = async () => {
|
|
62
|
+
const computedIRs = await Promise.all(this.impulseResponses);
|
|
63
|
+
this.emit('update', {message: `computing the IIR...`});
|
|
64
|
+
return this.pyServerAPI
|
|
65
|
+
.getInverseImpulseResponse({
|
|
66
|
+
payload: computedIRs.slice(0, this.numCaptures),
|
|
67
|
+
})
|
|
68
|
+
.then(res => {
|
|
69
|
+
this.emit('update', {message: `done computing the IIR...`});
|
|
70
|
+
this.invertedImpulseResponse = res;
|
|
71
|
+
})
|
|
72
|
+
.catch(err => {
|
|
73
|
+
// this.emit('InvertedImpulseResponse', {res: false});
|
|
74
|
+
console.error(err);
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** .
|
|
79
|
+
* .
|
|
80
|
+
* .
|
|
81
|
+
* Sends the recorded signal, or a given csv string of a signal, to the back end server for processing
|
|
82
|
+
*
|
|
83
|
+
* @param {<array>String} signalCsv - Optional csv string of a previously recorded signal, if given, this signal will be processed
|
|
84
|
+
* @example
|
|
85
|
+
*/
|
|
86
|
+
sendRecordingToServerForProcessing = signalCsv => {
|
|
87
|
+
const allSignals = this.getAllRecordedSignals();
|
|
88
|
+
const numSignals = allSignals.length;
|
|
89
|
+
const payload =
|
|
90
|
+
signalCsv && signalCsv.length > 0 ? csvToArray(signalCsv) : allSignals[numSignals - 1];
|
|
91
|
+
|
|
92
|
+
this.emit('update', {message: `computing the IR of the last recording...`});
|
|
93
|
+
this.impulseResponses.push(
|
|
94
|
+
this.pyServerAPI
|
|
95
|
+
.getImpulseResponse({
|
|
96
|
+
sampleRate: this.sourceSamplingRate || 96000,
|
|
97
|
+
payload,
|
|
98
|
+
P: this.#P,
|
|
99
|
+
})
|
|
100
|
+
.then(res => {
|
|
101
|
+
if (this.numSuccessfulCaptured < this.numCaptures) {
|
|
102
|
+
this.numSuccessfulCaptured += 1;
|
|
103
|
+
this.emit('update', {
|
|
104
|
+
message: `${this.numSuccessfulCaptured}/${this.numCaptures} IRs computed...`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return res;
|
|
108
|
+
})
|
|
109
|
+
.catch(err => {
|
|
110
|
+
console.error(err);
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Passed to the calibration steps function, awaits the desired amount of seconds to capture the desired number
|
|
117
|
+
* of MLS periods defined in the constructor.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
*/
|
|
121
|
+
#awaitDesiredMLSLength = async () => {
|
|
122
|
+
// seconds per MLS = P / SR
|
|
123
|
+
// await N * P / SR
|
|
124
|
+
this.emit('update', {
|
|
125
|
+
message: `sampling the calibration signal...`,
|
|
126
|
+
});
|
|
127
|
+
await sleep((this.#P / this.sourceSamplingRate) * this.numMLSPerCapture);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** .
|
|
131
|
+
* .
|
|
132
|
+
* .
|
|
133
|
+
* Passed to the calibration steps function, awaits the onset of the signal to ensure a steady state
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
*/
|
|
137
|
+
#awaitSignalOnset = async () => {
|
|
138
|
+
this.emit('update', {
|
|
139
|
+
message: `waiting for the signal to stabalize...`,
|
|
140
|
+
});
|
|
141
|
+
await sleep(this.TAPER_SECS);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Called immediately after a recording is captured. Used to process the resulting signal
|
|
146
|
+
* whether by sending the result to a server or by computing a result locally.
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
*/
|
|
150
|
+
#afterMLSRecord = () => {
|
|
151
|
+
if (this.#download) {
|
|
152
|
+
this.downloadData();
|
|
153
|
+
}
|
|
154
|
+
this.sendRecordingToServerForProcessing();
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
#afterMLSwIIRRecord = () => {
|
|
158
|
+
if (this.#download) {
|
|
159
|
+
this.downloadData();
|
|
160
|
+
}
|
|
161
|
+
this.#stopCalibrationAudio();
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/** .
|
|
165
|
+
* .
|
|
166
|
+
* .
|
|
167
|
+
* Created an S Curver Buffer to taper the signal onset
|
|
168
|
+
*
|
|
169
|
+
* @param {*} length
|
|
170
|
+
* @param {*} phase
|
|
171
|
+
* @returns
|
|
172
|
+
* @example
|
|
173
|
+
*/
|
|
174
|
+
static createSCurveBuffer = (length, phase) => {
|
|
175
|
+
const curve = new Float32Array(length);
|
|
176
|
+
let i;
|
|
177
|
+
for (i = 0; i < length; i += 1) {
|
|
178
|
+
// scale the curve to be between 0-1
|
|
179
|
+
curve[i] = Math.sin((Math.PI * i) / length - phase) / 2 + 0.5;
|
|
180
|
+
}
|
|
181
|
+
return curve;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
static createInverseSCurveBuffer = (length, phase) => {
|
|
185
|
+
const curve = new Float32Array(length);
|
|
186
|
+
let i;
|
|
187
|
+
let j = length - 1;
|
|
188
|
+
for (i = 0; i < length; i += 1) {
|
|
189
|
+
// scale the curve to be between 0-1
|
|
190
|
+
curve[i] = Math.sin((Math.PI * j) / length - phase) / 2 + 0.5;
|
|
191
|
+
j -= 1;
|
|
192
|
+
}
|
|
193
|
+
return curve;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Construct a Calibration Node with the calibration parameters.
|
|
198
|
+
*
|
|
199
|
+
* @param CALIBRATION_TONE_FREQUENCY
|
|
200
|
+
* @private
|
|
201
|
+
* @example
|
|
202
|
+
*/
|
|
203
|
+
#createPureTonenNode = CALIBRATION_TONE_FREQUENCY => {
|
|
204
|
+
const audioContext = this.makeNewSourceAudioContext();
|
|
205
|
+
const oscilator = audioContext.createOscillator();
|
|
206
|
+
const gainNode = audioContext.createGain();
|
|
207
|
+
|
|
208
|
+
oscilator.frequency.value = CALIBRATION_TONE_FREQUENCY;
|
|
209
|
+
oscilator.type = 'sine';
|
|
210
|
+
gainNode.gain.value = 0.04;
|
|
211
|
+
|
|
212
|
+
oscilator.connect(gainNode);
|
|
213
|
+
gainNode.connect(audioContext.destination);
|
|
214
|
+
|
|
215
|
+
this.addCalibrationNode(oscilator);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Construct a Calibration Node with the calibration parameters.
|
|
220
|
+
*
|
|
221
|
+
* @param dataBuffer
|
|
222
|
+
* @private
|
|
223
|
+
* @example
|
|
224
|
+
*/
|
|
225
|
+
#createCalibrationNodeFromBuffer = dataBuffer => {
|
|
226
|
+
const audioContext = this.makeNewSourceAudioContext();
|
|
227
|
+
const buffer = audioContext.createBuffer(
|
|
228
|
+
1, // number of channels
|
|
229
|
+
dataBuffer.length,
|
|
230
|
+
audioContext.sampleRate // sample rate
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const data = buffer.getChannelData(0); // get data
|
|
234
|
+
// fill the buffer with our data
|
|
235
|
+
try {
|
|
236
|
+
for (let i = 0; i < dataBuffer.length; i += 1) {
|
|
237
|
+
data[i] = dataBuffer[i];
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error(error);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const onsetGainNode = audioContext.createGain();
|
|
244
|
+
this.offsetGainNode = audioContext.createGain();
|
|
245
|
+
const source = audioContext.createBufferSource();
|
|
246
|
+
|
|
247
|
+
source.buffer = buffer;
|
|
248
|
+
source.loop = true;
|
|
249
|
+
source.connect(onsetGainNode);
|
|
250
|
+
onsetGainNode.connect(this.offsetGainNode);
|
|
251
|
+
this.offsetGainNode.connect(audioContext.destination);
|
|
252
|
+
|
|
253
|
+
const onsetCurve = ImpulseResponse.createSCurveBuffer(this.sourceSamplingRate, Math.PI / 2);
|
|
254
|
+
onsetGainNode.gain.setValueCurveAtTime(onsetCurve, 0, this.TAPER_SECS);
|
|
255
|
+
|
|
256
|
+
this.addCalibrationNode(source);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Given a data buffer, creates the required calibration node
|
|
261
|
+
*
|
|
262
|
+
* @param {*} dataBufferArray
|
|
263
|
+
* @example
|
|
264
|
+
*/
|
|
265
|
+
#setCalibrationNodesFromBuffer = (dataBufferArray = [this.#mlsBufferView]) => {
|
|
266
|
+
if (dataBufferArray.length === 1) {
|
|
267
|
+
this.#createCalibrationNodeFromBuffer(dataBufferArray[0]);
|
|
268
|
+
} else {
|
|
269
|
+
throw new Error('The length of the data buffer array must be 1');
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
#createImpulseResponseFilterGraph = (calibrationSignal, iir) => {
|
|
274
|
+
const audioCtx = this.makeNewSourceAudioContext();
|
|
275
|
+
|
|
276
|
+
// -------------------------------------------------------- IIR
|
|
277
|
+
const iirBuffer = audioCtx.createBuffer(
|
|
278
|
+
1,
|
|
279
|
+
// TODO: quality check this
|
|
280
|
+
iir.length - 1,
|
|
281
|
+
audioCtx.sampleRate
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// Fill the buffer with the inverted impulse response
|
|
285
|
+
const iirChannelZeroBuffer = iirBuffer.getChannelData(0);
|
|
286
|
+
for (let i = 0; i < iirBuffer.length; i += 1) {
|
|
287
|
+
// audio needs to be in [-1.0; 1.0]
|
|
288
|
+
iirChannelZeroBuffer[i] = iir[i];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const convolverNode = audioCtx.createConvolver();
|
|
292
|
+
|
|
293
|
+
convolverNode.normalize = false;
|
|
294
|
+
convolverNode.channelCount = 1;
|
|
295
|
+
convolverNode.buffer = iirBuffer;
|
|
296
|
+
|
|
297
|
+
// ------------------------------------------------------ MLS
|
|
298
|
+
const calibrationSignalBuffer = audioCtx.createBuffer(
|
|
299
|
+
1, // number of channels
|
|
300
|
+
calibrationSignal.length,
|
|
301
|
+
audioCtx.sampleRate // sample rate
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const mlsChannelZeroBuffer = calibrationSignalBuffer.getChannelData(0); // get data
|
|
305
|
+
// fill the buffer with our data
|
|
306
|
+
try {
|
|
307
|
+
for (let i = 0; i < calibrationSignal.length; i += 1) {
|
|
308
|
+
mlsChannelZeroBuffer[i] = calibrationSignal[i];
|
|
309
|
+
}
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.error(error);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const sourceNode = audioCtx.createBufferSource();
|
|
315
|
+
|
|
316
|
+
sourceNode.buffer = calibrationSignalBuffer;
|
|
317
|
+
sourceNode.loop = true;
|
|
318
|
+
sourceNode.connect(convolverNode);
|
|
319
|
+
|
|
320
|
+
convolverNode.connect(audioCtx.destination);
|
|
321
|
+
|
|
322
|
+
console.log({convolverNode, sourceNode});
|
|
323
|
+
|
|
324
|
+
this.addCalibrationNode(sourceNode);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
#createIIRwMLSGraph = () => {
|
|
328
|
+
this.#createImpulseResponseFilterGraph(this.impulseResponses, [this.#mlsBufferView][0]);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Creates an audio context and plays it for a few seconds.
|
|
333
|
+
*
|
|
334
|
+
* @private
|
|
335
|
+
* @returns - Resolves when the audio is done playing.
|
|
336
|
+
* @example
|
|
337
|
+
*/
|
|
338
|
+
#playCalibrationAudio = () => {
|
|
339
|
+
this.calibrationNodes[0].start(0);
|
|
340
|
+
this.emit('update', {message: 'playing the calibration tone...'});
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
/** .
|
|
344
|
+
* .
|
|
345
|
+
* .
|
|
346
|
+
* Stops the audio with tapered offset
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
*/
|
|
350
|
+
#stopCalibrationAudio = () => {
|
|
351
|
+
this.offsetGainNode.gain.setValueAtTime(
|
|
352
|
+
this.offsetGainNode.gain.value,
|
|
353
|
+
this.sourceAudioContext.currentTime
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
this.offsetGainNode.gain.setTargetAtTime(0, this.sourceAudioContext.currentTime, 0.5);
|
|
357
|
+
this.emit('update', {message: 'stopping the calibration tone...'});
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
playMLSwithIIR = async (stream, iir) => {
|
|
361
|
+
this.invertedImpulseResponse = iir;
|
|
362
|
+
// initialize the MLSGenInterface object with it's factory method
|
|
363
|
+
await MlsGenInterface.factory(
|
|
364
|
+
this.#mlsOrder,
|
|
365
|
+
this.sinkSamplingRate,
|
|
366
|
+
this.sourceSamplingRate
|
|
367
|
+
).then(mlsGenInterface => {
|
|
368
|
+
this.#mlsGenInterface = mlsGenInterface;
|
|
369
|
+
this.#mlsBufferView = this.#mlsGenInterface.getMLS();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// after intializating, start the calibration steps with garbage collection
|
|
373
|
+
await this.#mlsGenInterface.withGarbageCollection([
|
|
374
|
+
[
|
|
375
|
+
this.calibrationSteps,
|
|
376
|
+
[
|
|
377
|
+
stream,
|
|
378
|
+
this.#playCalibrationAudio, // play audio func (required)
|
|
379
|
+
this.#createImpulseResponseFilterGraph, // before play func
|
|
380
|
+
null, // before record
|
|
381
|
+
this.#awaitDesiredMLSLength, // during record
|
|
382
|
+
this.#afterMLSwIIRRecord, // after record
|
|
383
|
+
],
|
|
384
|
+
],
|
|
385
|
+
]);
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Public method to start the calibration process. Objects intialized from webassembly allocate new memory
|
|
390
|
+
* and must be manually freed. This function is responsible for intializing the MlsGenInterface,
|
|
391
|
+
* and wrapping the calibration steps with a garbage collection safe gaurd.
|
|
392
|
+
*
|
|
393
|
+
* @public
|
|
394
|
+
* @param stream - The stream of audio from the Listener.
|
|
395
|
+
* @example
|
|
396
|
+
*/
|
|
397
|
+
startCalibration = async stream => {
|
|
398
|
+
// initialize the MLSGenInterface object with it's factory method
|
|
399
|
+
await MlsGenInterface.factory(
|
|
400
|
+
this.#mlsOrder,
|
|
401
|
+
this.sinkSamplingRate,
|
|
402
|
+
this.sourceSamplingRate
|
|
403
|
+
).then(mlsGenInterface => {
|
|
404
|
+
this.#mlsGenInterface = mlsGenInterface;
|
|
405
|
+
this.#mlsBufferView = this.#mlsGenInterface.getMLS();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// after intializating, start the calibration steps with garbage collection
|
|
409
|
+
await this.#mlsGenInterface.withGarbageCollection([
|
|
410
|
+
() =>
|
|
411
|
+
this.calibrationSteps(
|
|
412
|
+
stream,
|
|
413
|
+
this.#playCalibrationAudio, // play audio func (required)
|
|
414
|
+
this.#setCalibrationNodesFromBuffer, // before play func
|
|
415
|
+
this.#awaitSignalOnset, // before record
|
|
416
|
+
() => this.numSuccessfulCaptured < this.numCaptures, // loop while true
|
|
417
|
+
this.#awaitDesiredMLSLength, // during record
|
|
418
|
+
this.#afterMLSRecord // after record
|
|
419
|
+
),
|
|
420
|
+
]);
|
|
421
|
+
|
|
422
|
+
this.#stopCalibrationAudio();
|
|
423
|
+
|
|
424
|
+
// at this stage we've captured all the required signals,
|
|
425
|
+
// and have received IRs for each one
|
|
426
|
+
// so let's send all the IRs to the server to be converted to a single IIR
|
|
427
|
+
await this.sendImpulseResponsesToServerForProcessing();
|
|
428
|
+
|
|
429
|
+
// debugging function, use to test the result of the IIR
|
|
430
|
+
// await this.playMLSwithIIR(stream, this.invertedImpulseResponse);
|
|
431
|
+
|
|
432
|
+
return this.invertedImpulseResponse;
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export default ImpulseResponse;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#include "mlsGen.hpp"
|
|
2
|
+
#include <sanitizer/lsan_interface.h>
|
|
3
|
+
|
|
4
|
+
#ifdef __cplusplus
|
|
5
|
+
extern "C" {
|
|
6
|
+
#endif
|
|
7
|
+
|
|
8
|
+
MLSGen::MLSGen(long N, long srcSR, long sinkSR) {
|
|
9
|
+
MLSGen::N = N;
|
|
10
|
+
MLSGen::srcSR = srcSR;
|
|
11
|
+
MLSGen::sinkSR = sinkSR;
|
|
12
|
+
P = (1 << N) - 1;
|
|
13
|
+
mls = new bool[P];
|
|
14
|
+
tagL = new long[P];
|
|
15
|
+
tagS = new long[P];
|
|
16
|
+
generatedSignal = new float[P];
|
|
17
|
+
recordedSignal = new float[P];
|
|
18
|
+
perm = new float[P + 1];
|
|
19
|
+
resp = new float[P + 1];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#ifndef __EMSCRIPTEN__
|
|
23
|
+
MLSGen::~MLSGen() {
|
|
24
|
+
delete[] mls;
|
|
25
|
+
delete[] tagL;
|
|
26
|
+
delete[] tagS;
|
|
27
|
+
delete[] generatedSignal;
|
|
28
|
+
delete[] recordedSignal;
|
|
29
|
+
delete[] perm;
|
|
30
|
+
delete[] resp;
|
|
31
|
+
}
|
|
32
|
+
#endif
|
|
33
|
+
|
|
34
|
+
#ifdef __EMSCRIPTEN__
|
|
35
|
+
|
|
36
|
+
using namespace emscripten;
|
|
37
|
+
|
|
38
|
+
void MLSGen::Destruct() {
|
|
39
|
+
delete[] mls;
|
|
40
|
+
delete[] tagL;
|
|
41
|
+
delete[] tagS;
|
|
42
|
+
delete[] generatedSignal;
|
|
43
|
+
delete[] recordedSignal;
|
|
44
|
+
delete[] perm;
|
|
45
|
+
delete[] resp;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
emscripten::val MLSGen::getMLS() {
|
|
49
|
+
// if mls is not generated, generate it
|
|
50
|
+
if (*(&mls + 1) - mls != P) {
|
|
51
|
+
generateMls();
|
|
52
|
+
}
|
|
53
|
+
for (int i = 0; i < P; i++) {
|
|
54
|
+
generatedSignal[i] = -2 * mls[i] + 1;
|
|
55
|
+
}
|
|
56
|
+
return emscripten::val(typed_memory_view(P, generatedSignal));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
emscripten::val MLSGen::setRecordedSignalsMemoryView(long sizeRecordedSignals) {
|
|
60
|
+
C = sizeRecordedSignals;
|
|
61
|
+
recordedSignals = new float[C];
|
|
62
|
+
return emscripten::val(typed_memory_view(C, recordedSignals));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
emscripten::val MLSGen::getRecordedSignalsMemoryView() {
|
|
66
|
+
return emscripten::val(typed_memory_view(C, recordedSignals));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
emscripten::val MLSGen::getImpulseResponse() {
|
|
70
|
+
generateTagL(); // Generate tagL for the L matrix
|
|
71
|
+
generateTagS(); // Generate tagS for the S matrix
|
|
72
|
+
GenerateSignal(); // Generate the signal TEST PURPOSES
|
|
73
|
+
permuteSignal(); // Permute the signal according to tagS
|
|
74
|
+
fastHadamard(); // Do a Hadamard transform in place
|
|
75
|
+
permuteResponse(); // Permute the impulseresponse according to tagL
|
|
76
|
+
return emscripten::val(typed_memory_view(P + 1, resp));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Binding code
|
|
80
|
+
EMSCRIPTEN_BINDINGS(mls_gen_module) {
|
|
81
|
+
class_<MLSGen>("MLSGen")
|
|
82
|
+
.constructor<long, long, long>()
|
|
83
|
+
.function("Destruct", &MLSGen::Destruct)
|
|
84
|
+
.function("getMLS", &MLSGen::getMLS)
|
|
85
|
+
.function("getRecordedSignalsMemoryView",
|
|
86
|
+
&MLSGen::getRecordedSignalsMemoryView)
|
|
87
|
+
.function("setRecordedSignalsMemoryView",
|
|
88
|
+
&MLSGen::setRecordedSignalsMemoryView)
|
|
89
|
+
.function("getImpulseResponse", &MLSGen::getImpulseResponse);
|
|
90
|
+
function("doLeakCheck", &__lsan_do_recoverable_leak_check);
|
|
91
|
+
};
|
|
92
|
+
#endif
|
|
93
|
+
|
|
94
|
+
#ifdef __cplusplus
|
|
95
|
+
}
|
|
96
|
+
#endif
|
|
97
|
+
|
|
98
|
+
// https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html
|
|
99
|
+
// https://web.dev/webassembly-memory-debugging/
|