Spaces:
Running
Running
// Shim for Safari. | |
window.AudioContext = window.AudioContext || window.webkitAudioContext | |
function audioBufferToWav(buffer, opt) { | |
opt = opt || {} | |
var numChannels = buffer.numberOfChannels | |
var sampleRate = buffer.sampleRate | |
var format = opt.float32 ? 3 : 1 | |
var bitDepth = format === 3 ? 32 : 16 | |
var result | |
if (numChannels === 2) { | |
result = interleave(buffer.getChannelData(0), buffer.getChannelData(1)) | |
} else { | |
result = buffer.getChannelData(0) | |
} | |
return encodeWAV(result, format, sampleRate, numChannels, bitDepth) | |
} | |
function encodeWAV(samples, format, sampleRate, numChannels, bitDepth) { | |
var bytesPerSample = bitDepth / 8 | |
var blockAlign = numChannels * bytesPerSample | |
var buffer = new ArrayBuffer(44 + samples.length * bytesPerSample) | |
var view = new DataView(buffer) | |
/* RIFF identifier */ | |
writeString(view, 0, 'RIFF') | |
/* RIFF chunk length */ | |
view.setUint32(4, 36 + samples.length * bytesPerSample, true) | |
/* RIFF type */ | |
writeString(view, 8, 'WAVE') | |
/* format chunk identifier */ | |
writeString(view, 12, 'fmt ') | |
/* format chunk length */ | |
view.setUint32(16, 16, true) | |
/* sample format (raw) */ | |
view.setUint16(20, format, true) | |
/* channel count */ | |
view.setUint16(22, numChannels, true) | |
/* sample rate */ | |
view.setUint32(24, sampleRate, true) | |
/* byte rate (sample rate * block align) */ | |
view.setUint32(28, sampleRate * blockAlign, true) | |
/* block align (channel count * bytes per sample) */ | |
view.setUint16(32, blockAlign, true) | |
/* bits per sample */ | |
view.setUint16(34, bitDepth, true) | |
/* data chunk identifier */ | |
writeString(view, 36, 'data') | |
/* data chunk length */ | |
view.setUint32(40, samples.length * bytesPerSample, true) | |
if (format === 1) { // Raw PCM | |
floatTo16BitPCM(view, 44, samples) | |
} else { | |
writeFloat32(view, 44, samples) | |
} | |
return buffer | |
} | |
function interleave(inputL, inputR) { | |
var length = inputL.length + inputR.length | |
var result = new Float32Array(length) | |
var index = 0 | |
var inputIndex = 0 | |
while (index < length) { | |
result[index++] = inputL[inputIndex] | |
result[index++] = inputR[inputIndex] | |
inputIndex++ | |
} | |
return result | |
} | |
function writeFloat32(output, offset, input) { | |
for (var i = 0; i < input.length; i++, offset += 4) { | |
output.setFloat32(offset, input[i], true) | |
} | |
} | |
function floatTo16BitPCM(output, offset, input) { | |
for (var i = 0; i < input.length; i++, offset += 2) { | |
var s = Math.max(-1, Math.min(1, input[i])) | |
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true) | |
} | |
} | |
function writeString(view, offset, string) { | |
for (var i = 0; i < string.length; i++) { | |
view.setUint8(offset + i, string.charCodeAt(i)) | |
} | |
} | |
// Safari does not support promise-based decodeAudioData, need to use callback instead. | |
const decodeAudioData = buffer => new Promise((res, rej) => { | |
new AudioContext().decodeAudioData(buffer, res, rej) | |
}) | |
const startRecording = async () => { | |
const data = [] | |
// Ask for mic permissions. | |
const stream = await navigator.mediaDevices.getUserMedia({ video: false, audio: true }) | |
window.stream = stream | |
// Use polyfill for older browsers. | |
if (!window.MediaRecorder) { | |
window.MediaRecorder = OpusMediaRecorder | |
window.recorder = new MediaRecorder(stream, {}, { | |
OggOpusEncoderWasmPath: 'https://cdn.jsdelivr.net/npm/opus-media-recorder@latest/OggOpusEncoder.wasm', | |
WebMOpusEncoderWasmPath: 'https://cdn.jsdelivr.net/npm/opus-media-recorder@latest/WebMOpusEncoder.wasm' | |
}) | |
} | |
else window.recorder = new MediaRecorder(stream) | |
// Handle incoming data. | |
window.recorder.ondataavailable = e => data.push(e.data) | |
window.recorder.start() | |
window.recorder.onerror = e => { throw e.error || new Error(e.name) } | |
window.recorder.onstop = async (e) => { | |
const blob = new Blob(data) | |
const fetchedBlob = await fetch(URL.createObjectURL(blob)) | |
const arrayBuffer = await fetchedBlob.arrayBuffer() | |
// Convert to wav format. | |
const wav = audioBufferToWav(await decodeAudioData(arrayBuffer)) | |
const formData = new FormData() | |
formData.append('files', new Blob([wav], { type: 'audio/wave' }), 'sound.wav') | |
// Send the audio file to Wave server. | |
const res = await fetch(wave.uploadURL, { method: 'POST', body: formData }) | |
const { files } = await res.json() | |
// Emit event (q.events.audio.captured) with a URL of the audio file at Wave server. | |
window.wave.emit('audio', 'captured', files[0]) | |
} | |
} | |
const stopRecording = () => { | |
window.recorder.stop() | |
window.stream.getTracks().forEach(track => track.stop()) | |
} | |