84 lines
3.4 KiB
JavaScript
84 lines
3.4 KiB
JavaScript
const button = document.querySelector('#record');
|
|
const status = document.querySelector('#status');
|
|
const download = document.querySelector('#download');
|
|
let objectUrl;
|
|
|
|
function wav(blocks, rate) {
|
|
const n = blocks.reduce((count, block) => count + block.length, 0);
|
|
const data = new DataView(new ArrayBuffer(44 + n * 2));
|
|
const ascii = (offset, value) => [...value].forEach((letter, i) => data.setUint8(offset + i, letter.charCodeAt(0)));
|
|
ascii(0, 'RIFF');
|
|
data.setUint32(4, 36 + n * 2, true);
|
|
ascii(8, 'WAVEfmt ');
|
|
data.setUint32(16, 16, true);
|
|
data.setUint16(20, 1, true);
|
|
data.setUint16(22, 1, true);
|
|
data.setUint32(24, rate, true);
|
|
data.setUint32(28, rate * 2, true);
|
|
data.setUint16(32, 2, true);
|
|
data.setUint16(34, 16, true);
|
|
ascii(36, 'data');
|
|
data.setUint32(40, n * 2, true);
|
|
let offset = 44;
|
|
for (const block of blocks) {
|
|
for (const value of block) {
|
|
data.setInt16(offset, Math.round(Math.max(-1, Math.min(1, value)) * 32767), true);
|
|
offset += 2;
|
|
}
|
|
}
|
|
return new Blob([data], { type: 'audio/wav' });
|
|
}
|
|
|
|
button.onclick = async () => {
|
|
button.disabled = true;
|
|
let audio, stream, deadline, node;
|
|
const close = () => {
|
|
clearTimeout(deadline);
|
|
node?.disconnect();
|
|
stream?.getTracks().forEach(track => track.stop());
|
|
if (audio && audio.state !== 'closed') void audio.close();
|
|
button.disabled = false;
|
|
};
|
|
try {
|
|
audio = new AudioContext();
|
|
await audio.resume();
|
|
stream = await navigator.mediaDevices.getUserMedia({
|
|
video: false,
|
|
audio: { channelCount: 1, echoCancellation: false, noiseSuppression: false, autoGainControl: false },
|
|
});
|
|
const settings = stream.getAudioTracks()[0].getSettings();
|
|
if ([settings.echoCancellation, settings.noiseSuppression, settings.autoGainControl].includes(true)) {
|
|
throw new Error('Браузер оставил включённой обработку микрофона. Для точного замера используй другой браузер или Mac.');
|
|
}
|
|
await audio.audioWorklet.addModule('/room-mic-worklet.js');
|
|
node = new AudioWorkletNode(audio, 'room-mic');
|
|
const blocks = [];
|
|
node.port.onmessage = ({ data }) => {
|
|
if (data !== 'done') {
|
|
blocks.push(data);
|
|
return;
|
|
}
|
|
const blob = wav(blocks, audio.sampleRate);
|
|
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
objectUrl = URL.createObjectURL(blob);
|
|
download.href = objectUrl;
|
|
download.download = `room-${new Date().toISOString().replaceAll(':', '-')}.wav`;
|
|
download.hidden = false;
|
|
status.textContent = 'Запись готова. Файл сохранится на этом устройстве.';
|
|
close();
|
|
};
|
|
audio.createMediaStreamSource(stream).connect(node);
|
|
node.connect(audio.destination);
|
|
const unreported = [settings.echoCancellation, settings.noiseSuppression, settings.autoGainControl].some(value => value === undefined);
|
|
status.textContent = 'Запись идёт. Запусти тестовый сигнал.' +
|
|
(unreported ? ' Браузер не сообщил все настройки обработки.' : '');
|
|
deadline = setTimeout(() => {
|
|
close();
|
|
status.textContent = 'Запись прервалась. Оставь вкладку открытой и попробуй снова.';
|
|
}, 19000);
|
|
} catch (error) {
|
|
close();
|
|
status.textContent = error.message;
|
|
}
|
|
};
|