import AudioCalibrator from '../audioCalibrator';
import MlsGenInterface from './mlsGen/mlsGenInterface';
import {sleep, csvToArray} from '../../utils';
/**
*
*/
class ImpulseResponse extends AudioCalibrator {
/**
* Default constructor. Creates an instance with any number of paramters passed or the default parameters defined here.
* @param {*} param0
*/
constructor({download = false, mlsOrder = 18, numCaptures = 5, numMLSPerCapture = 4}) {
super(numCaptures, numMLSPerCapture);
this.#mlsOrder = parseInt(mlsOrder, 10);
this.#P = 2 ** mlsOrder - 1;
this.#download = download;
}
/** @private */
#download;
/** @private */
#mlsGenInterface;
/** @private */
#mlsBufferView;
/** @private */
invertedImpulseResponse = null;
/** @private */
impulseResponses = [];
/** @private */
#mlsOrder;
#P;
/** @private */
TAPER_SECS = 5;
/** @private */
offsetGainNode;
/**
* Sends all the computed impulse responses to the backend server for processing
* @returns sets the resulting inverted impulse response to the class property
*/
sendImpulseResponsesToServerForProcessing = async () => {
const computedIRs = await Promise.all(this.impulseResponses);
this.emit('update', {message: `computing the IIR...`});
return this.pyServerAPI
.getInverseImpulseResponse({
payload: computedIRs,
})
.then(res => {
this.emit('update', {message: `done computing the IIR...`});
this.invertedImpulseResponse = res;
})
.catch(err => {
// this.emit('InvertedImpulseResponse', {res: false});
console.error(err);
});
};
/**
* Sends the recorded signal, or a given csv string of a signal, to the back end server for processing
* @param {<array>String} signalCsv - Optional csv string of a previously recorded signal, if given, this signal will be processed
*/
sendRecordingToServerForProcessing = signalCsv => {
const allSignals = this.getAllRecordedSignals();
const numSignals = allSignals.length;
const payload =
signalCsv && signalCsv.length > 0 ? csvToArray(signalCsv) : allSignals[numSignals - 1];
this.emit('update', {message: `computing the IR of the last recording...`});
this.impulseResponses.push(
this.pyServerAPI
.getImpulseResponse({
sampleRate: this.sourceSamplingRate || 96000,
payload,
P: this.#P,
})
.then(res => {
if (this.numSuccessfulCaptured < this.numCaptures) {
this.numSuccessfulCaptured += 1;
this.emit('update', {
message: `${this.numSuccessfulCaptured}/${this.numCaptures} IRs computed...`,
});
}
return res;
})
.catch(err => {
console.error(err);
})
);
};
/**
* Passed to the calibration steps function, awaits the desired amount of seconds to capture the desired number
* of MLS periods defined in the constructor
*/
#awaitDesiredMLSLength = async () => {
// seconds per MLS = P / SR
// await N * P / SR
this.emit('update', {
message: `sampling the calibration signal...`,
});
await sleep((this.#P / this.sourceSamplingRate) * this.numMLSPerCapture);
};
/**
* Passed to the calibration steps function, awaits the onset of the signal to ensure a steady state
*/
#awaitSignalOnset = async () => {
this.emit('update', {
message: `waiting for the signal to stabalize...`,
});
await sleep(this.TAPER_SECS);
};
/**
* Called immediately after a recording is captured. Used to process the resulting signal
* whether by sending the result to a server or by computing a result locally
*/
#afterMLSRecord = () => {
if (this.#download) {
this.downloadData();
}
this.#stopCalibrationAudio();
this.sendRecordingToServerForProcessing();
};
#afterMLSwIIRRecord = () => {
if (this.#download) {
this.downloadData();
}
this.#stopCalibrationAudio();
};
/**
* Created an S Curver Buffer to taper the signal onset
* @param {*} length
* @param {*} phase
* @returns
*/
static createSCurveBuffer = (length, phase) => {
const curve = new Float32Array(length);
let i;
for (i = 0; i < length; i += 1) {
// scale the curve to be between 0-1
curve[i] = Math.sin((Math.PI * i) / length - phase) / 2 + 0.5;
}
return curve;
};
static createInverseSCurveBuffer = (length, phase) => {
const curve = new Float32Array(length);
let i;
let j = length - 1;
for (i = 0; i < length; i += 1) {
// scale the curve to be between 0-1
curve[i] = Math.sin((Math.PI * j) / length - phase) / 2 + 0.5;
j -= 1;
}
return curve;
};
/**
* Construct a Calibration Node with the calibration parameters.
* @private
*/
#createPureTonenNode = CALIBRATION_TONE_FREQUENCY => {
const audioContext = this.makeNewSourceAudioContext();
const oscilator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscilator.frequency.value = CALIBRATION_TONE_FREQUENCY;
oscilator.type = 'sine';
gainNode.gain.value = 0.04;
oscilator.connect(gainNode);
gainNode.connect(audioContext.destination);
this.addCalibrationNode(oscilator);
};
/**
* Construct a Calibration Node with the calibration parameters.
* @private
*/
#createCalibrationNodeFromBuffer = dataBuffer => {
const audioContext = this.makeNewSourceAudioContext();
const buffer = audioContext.createBuffer(
1, // number of channels
dataBuffer.length,
audioContext.sampleRate // sample rate
);
const data = buffer.getChannelData(0); // get data
// fill the buffer with our data
try {
for (let i = 0; i < dataBuffer.length; i += 1) {
data[i] = dataBuffer[i];
}
} catch (error) {
console.error(error);
}
const source = audioContext.createBufferSource();
source.buffer = buffer;
source.loop = true;
source.connect(audioContext.destination);
this.addCalibrationNode(source);
};
/**
* Given a data buffer, creates the required calibration node
* @param {*} dataBufferArray
*/
#setCalibrationNodesFromBuffer = (dataBufferArray = [this.#mlsBufferView]) => {
if (dataBufferArray.length === 1) {
this.#createCalibrationNodeFromBuffer(dataBufferArray[0]);
} else {
throw new Error('The length of the data buffer array must be 1');
}
};
#createImpulseResponseFilterGraph = (calibrationSignal, iir) => {
const audioCtx = this.makeNewSourceAudioContext();
// -------------------------------------------------------- IIR
const iirBuffer = audioCtx.createBuffer(
1,
// TODO: quality check this
iir.length - 1,
audioCtx.sampleRate
);
// Fill the buffer with the inverted impulse response
const iirChannelZeroBuffer = iirBuffer.getChannelData(0);
for (let i = 0; i < iirBuffer.length; i++) {
// audio needs to be in [-1.0; 1.0]
iirChannelZeroBuffer[i] = iir[i];
}
const convolverNode = audioCtx.createConvolver();
convolverNode.normalize = false;
convolverNode.channelCount = 1;
convolverNode.buffer = iirBuffer;
// ------------------------------------------------------ MLS
const calibrationSignalBuffer = audioCtx.createBuffer(
1, // number of channels
calibrationSignal.length,
audioCtx.sampleRate // sample rate
);
const mlsChannelZeroBuffer = calibrationSignalBuffer.getChannelData(0); // get data
// fill the buffer with our data
try {
for (let i = 0; i < calibrationSignal.length; i += 1) {
mlsChannelZeroBuffer[i] = calibrationSignal[i];
}
} catch (error) {
console.error(error);
}
const sourceNode = audioCtx.createBufferSource();
sourceNode.buffer = calibrationSignalBuffer;
sourceNode.loop = true;
sourceNode.connect(convolverNode);
convolverNode.connect(audioCtx.destination);
console.log({convolverNode, sourceNode});
this.addCalibrationNode(sourceNode);
};
#createIIRwMLSGraph = () => {
this.#createImpulseResponseFilterGraph(this.impulseResponses, [this.#mlsBufferView][0]);
};
/**
* Creates an audio context and plays it for a few seconds.
* @private
* @returns {Promise} - Resolves when the audio is done playing.
*/
#playCalibrationAudio = () => {
this.calibrationNodes[0].start(0);
this.emit('update', {message: 'playing the calibration tone...'});
};
/**
* Stops the audio with tapered offset
*/
#stopCalibrationAudio = () => {
this.calibrationNodes[0].stop();
this.emit('update', {message: 'stopping the calibration tone...'});
};
playMLSwithIIR = async (stream, iir) => {
this.invertedImpulseResponse = iir;
// initialize the MLSGenInterface object with it's factory method
await MlsGenInterface.factory(
this.#mlsOrder,
this.sinkSamplingRate,
this.sourceSamplingRate
).then(mlsGenInterface => {
this.#mlsGenInterface = mlsGenInterface;
this.#mlsBufferView = this.#mlsGenInterface.getMLS();
});
// after intializating, start the calibration steps with garbage collection
await this.#mlsGenInterface.withGarbageCollection([
[
this.calibrationSteps,
[
stream,
this.#playCalibrationAudio, // play audio func (required)
this.#createImpulseResponseFilterGraph, // before play func
null, // before record
this.#awaitDesiredMLSLength, // during record
this.#afterMLSwIIRRecord, // after record
],
],
]);
};
/**
* Public method to start the calibration process. Objects intialized from webassembly allocate new memory
* and must be manually freed. This function is responsible for intializing the MlsGenInterface,
* and wrapping the calibration steps with a garbage collection safe gaurd.
* @public
* @param {MediaStream} stream - The stream of audio from the Listener.
*/
startCalibration = async stream => {
// initialize the MLSGenInterface object with it's factory method
await MlsGenInterface.factory(
this.#mlsOrder,
this.sinkSamplingRate,
this.sourceSamplingRate
).then(mlsGenInterface => {
this.#mlsGenInterface = mlsGenInterface;
this.#mlsBufferView = this.#mlsGenInterface.getMLS();
});
// after intializating, start the calibration steps with garbage collection
await this.#mlsGenInterface.withGarbageCollection([
[
this.calibrationSteps,
[
stream,
this.#playCalibrationAudio, // play audio func (required)
this.#setCalibrationNodesFromBuffer, // before play func
null, // before record
this.#awaitDesiredMLSLength, // during record
this.#afterMLSRecord, // after record
],
],
]);
// await the server response
await this.sendImpulseResponsesToServerForProcessing();
await this.playMLSwithIIR(stream, this.invertedImpulseResponse);
return this.invertedImpulseResponse;
};
}
export default ImpulseResponse;