-
-
Save meziantou/edb7217fddfbb70e899e to your computer and use it in GitHub Desktop.
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<title></title> | |
</head> | |
<body> | |
<h1>Audio</h1> | |
<button id="startRecordingButton">Start recording</button> | |
<button id="stopRecordingButton">Stop recording</button> | |
<button id="playButton">Play</button> | |
<button id="downloadButton">Download</button> | |
<script> | |
var startRecordingButton = document.getElementById("startRecordingButton"); | |
var stopRecordingButton = document.getElementById("stopRecordingButton"); | |
var playButton = document.getElementById("playButton"); | |
var downloadButton = document.getElementById("downloadButton"); | |
var leftchannel = []; | |
var rightchannel = []; | |
var recorder = null; | |
var recordingLength = 0; | |
var volume = null; | |
var mediaStream = null; | |
var sampleRate = 44100; | |
var context = null; | |
var blob = null; | |
startRecordingButton.addEventListener("click", function () { | |
// Initialize recorder | |
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; | |
navigator.getUserMedia( | |
{ | |
audio: true | |
}, | |
function (e) { | |
console.log("user consent"); | |
// creates the audio context | |
window.AudioContext = window.AudioContext || window.webkitAudioContext; | |
context = new AudioContext(); | |
// creates an audio node from the microphone incoming stream | |
mediaStream = context.createMediaStreamSource(e); | |
// https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/createScriptProcessor | |
// bufferSize: the onaudioprocess event is called when the buffer is full | |
var bufferSize = 2048; | |
var numberOfInputChannels = 2; | |
var numberOfOutputChannels = 2; | |
if (context.createScriptProcessor) { | |
recorder = context.createScriptProcessor(bufferSize, numberOfInputChannels, numberOfOutputChannels); | |
} else { | |
recorder = context.createJavaScriptNode(bufferSize, numberOfInputChannels, numberOfOutputChannels); | |
} | |
recorder.onaudioprocess = function (e) { | |
leftchannel.push(new Float32Array(e.inputBuffer.getChannelData(0))); | |
rightchannel.push(new Float32Array(e.inputBuffer.getChannelData(1))); | |
recordingLength += bufferSize; | |
} | |
// we connect the recorder | |
mediaStream.connect(recorder); | |
recorder.connect(context.destination); | |
}, | |
function (e) { | |
console.error(e); | |
}); | |
}); | |
stopRecordingButton.addEventListener("click", function () { | |
// stop recording | |
recorder.disconnect(context.destination); | |
mediaStream.disconnect(recorder); | |
// we flat the left and right channels down | |
// Float32Array[] => Float32Array | |
var leftBuffer = flattenArray(leftchannel, recordingLength); | |
var rightBuffer = flattenArray(rightchannel, recordingLength); | |
// we interleave both channels together | |
// [left[0],right[0],left[1],right[1],...] | |
var interleaved = interleave(leftBuffer, rightBuffer); | |
// we create our wav file | |
var buffer = new ArrayBuffer(44 + interleaved.length * 2); | |
var view = new DataView(buffer); | |
// RIFF chunk descriptor | |
writeUTFBytes(view, 0, 'RIFF'); | |
view.setUint32(4, 44 + interleaved.length * 2, true); | |
writeUTFBytes(view, 8, 'WAVE'); | |
// FMT sub-chunk | |
writeUTFBytes(view, 12, 'fmt '); | |
view.setUint32(16, 16, true); // chunkSize | |
view.setUint16(20, 1, true); // wFormatTag | |
view.setUint16(22, 2, true); // wChannels: stereo (2 channels) | |
view.setUint32(24, sampleRate, true); // dwSamplesPerSec | |
view.setUint32(28, sampleRate * 4, true); // dwAvgBytesPerSec | |
view.setUint16(32, 4, true); // wBlockAlign | |
view.setUint16(34, 16, true); // wBitsPerSample | |
// data sub-chunk | |
writeUTFBytes(view, 36, 'data'); | |
view.setUint32(40, interleaved.length * 2, true); | |
// write the PCM samples | |
var index = 44; | |
var volume = 1; | |
for (var i = 0; i < interleaved.length; i++) { | |
view.setInt16(index, interleaved[i] * (0x7FFF * volume), true); | |
index += 2; | |
} | |
// our final blob | |
blob = new Blob([view], { type: 'audio/wav' }); | |
}); | |
playButton.addEventListener("click", function () { | |
if (blob == null) { | |
return; | |
} | |
var url = window.URL.createObjectURL(blob); | |
var audio = new Audio(url); | |
audio.play(); | |
}); | |
downloadButton.addEventListener("click", function () { | |
if (blob == null) { | |
return; | |
} | |
var url = URL.createObjectURL(blob); | |
var a = document.createElement("a"); | |
document.body.appendChild(a); | |
a.style = "display: none"; | |
a.href = url; | |
a.download = "sample.wav"; | |
a.click(); | |
window.URL.revokeObjectURL(url); | |
}); | |
function flattenArray(channelBuffer, recordingLength) { | |
var result = new Float32Array(recordingLength); | |
var offset = 0; | |
for (var i = 0; i < channelBuffer.length; i++) { | |
var buffer = channelBuffer[i]; | |
result.set(buffer, offset); | |
offset += buffer.length; | |
} | |
return result; | |
} | |
function interleave(leftChannel, rightChannel) { | |
var length = leftChannel.length + rightChannel.length; | |
var result = new Float32Array(length); | |
var inputIndex = 0; | |
for (var index = 0; index < length;) { | |
result[index++] = leftChannel[inputIndex]; | |
result[index++] = rightChannel[inputIndex]; | |
inputIndex++; | |
} | |
return result; | |
} | |
function writeUTFBytes(view, offset, string) { | |
for (var i = 0; i < string.length; i++) { | |
view.setUint8(offset + i, string.charCodeAt(i)); | |
} | |
} | |
</script> | |
</body> | |
</html> |
Hey, great work. Can I ask why are you multiplying by 0x7FFF when writing PCM data ? Media stream returns 32Float in range from -1 to 1 if Im correct. To convert to 16 bit PCM you should use this formula if Im correct sample < 0 ? sample * 0x8000 : sample * 0x7FFF.
If Im wrong please feel free to correct me. Thanks !
How can i have the final blob of wav,mono,16bit,16Khz,PCM audio?
How to reverse the procedure to get the samples as Float32Array
from wav
file?
Hi,
Can you pls give sample for record Mono channel and 16000Hz.
I've done this part
view.setUint16(22, 1, true); // wChannels: mono (1channel) / stereo (2 channels)
view.setUint32(24, sampleRate, true); // dwSamplesPerSec
view.setUint32(28, sampleRate * 2, true); // dwAvgBytesPerSec sampleRate *2 for 16 bit mono / sampleRate *4 for 16 bit stereo
but after the changes its not working.
Hello, this has worked great for me in my current project, however I've encountered issues where the recorded audio gives the voice being recorded a deeper tone. Has anyone else run into this?
@miketheappdev
Set sampleRate to 48000 and you'll get the normal pitch.
I can get this to work on Chrome in my PC, but not on Safari on iPadOS 14.01. Any advice?
@Anders-Lunde Which portion of the code does not work? Does Safari support WAV file playback?
I can get this to work on Chrome in my PC, but not on Safari on iPadOS 14.01. Any advice?
@Anders-Lunde Which portion of the code does not work? Does Safari support WAV file playback?
I figured it out. It was related to AudioContext, in in my typescript project I had to do this:
const AudioContext = window.AudioContext || // Default
(window as any).webkitAudioContext; // Safari and old versions of Chrome
const context = new AudioContext();
Hi Guys,
Can anyone help me to record the Wav audio in a 24-bit format? I achieved it in 16 and 32 bit.
Thanks in advance
Thanks,
PJ
Thanks for sharing the cool snippet and can we check silence and automatically stop the recording and when a person starts to speak then restart the recording,
How to reverse the procedure to get the samples as
Float32Array
from wav file?
wav.slice(44)
, https://stackoverflow.com/a/35248852
Hi guys, can any one tell me how to send the blob or the arrayBuffer to the blazor ?
Hi , I tried same approach as mentioned above with multichannel
var buffer = [];
for (var channel = 0; channel < _2 ; channel++) {
buffer.push(e.inputBuffer.getChannelData(channel));
}
I am using single audio device and expecting byte data for each channels - buffer[0] = [0,1,2,3,2] , buffer[1] = [0,0,0,0,0]
but receive similar data from both channels - eg (channel 1) buffer[0]= [0,1,2,3,2] , (channel 2) buffer[1] = [0,1,2,3,2]
please help me , how to get the buffer based on channels.
Thanks
Pradeep
hello, this has been very helpful.
One feedback: while using it, I noticed that the microphone is not closed unless you add on top of what you wrote:
// stop recording
recorder.disconnect(context.destination);
mediaStream.disconnect(recorder);
also:
mediaStream.getAudioTracks().forEach(track => {
track.stop();
})
context.close();
I hope this could be my small contribution back :)
@iamzhanghao: If my research is correct, you cannot influence the sampling rate but have to work with the sampling rate the browser offers you. [Update]: You can resample an AudioBuffer using OfflineAudioContext. Example see here: https://stackoverflow.com/questions/27598270/resample-audio-buffer-from-44100-to-16000 [/Update]
Regarding mono vs. stereo: Assuming that you receive similar data from both, left and right channel (or know on which channel you want to record) you just have to make some changes to the riff header. If I'm not mistaken it should be enough to change wChannels and dwAvgBytesPerSec:
then remove the interleaving. It's the line :
var interleaved = interleave(leftBuffer, rightBuffer);
and replace all remaining occurrences of interleaved with leftBuffer or rightBuffer to set the right buffer size and write the left or right buffer.