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()) | |
| } | |