Skip to content

Instantly share code, notes, and snippets.

@tatsuyasusukida
Last active October 24, 2024 21:35
Show Gist options
  • Save tatsuyasusukida/b6daa0cd09bba2fbbf6289c58777eeca to your computer and use it in GitHub Desktop.
Save tatsuyasusukida/b6daa0cd09bba2fbbf6289c58777eeca to your computer and use it in GitHub Desktop.
🎡 How to record audio using the Web Audio API in JavaScript

🎡 How to record audio using the Web Audio API in JavaScript

Demo video: How to record audio using the Web Audio API in JavaScript

About this article

This article describes how to record audio using the Web Audio API in JavaScript. The related resources are shown below.

Workflow

The workflow is shown below.

  1. Coding preparation
  2. Coding
  3. Operation check

Coding preparation

Run the following commands to parepare for coding.

mkdir javascript-audio-recorder
cd javascript-audio-recorder
npm init -y
npm install --save-dev http-server
touch audio-recorder.js encode-audio.js index.html main.js

Coding

index.html

Open index.html in you editor and enter the following content.

Click to go to index.html

audio-recorder.js

Open index.html in you editor and enter the following content.

Click to go to index.html

The points are shown below.

  1. Define the isRecording parameter.
  2. Store the sample into the buffer when the isRecording parameter is 1.
  3. Send a message when the number of elements in the buffer is 1 or more.
  4. Register the Audio Recorder as an Audio Worklet.

encode-audio.js

Open encode-audio.js in you editor and enter the following content.

Click to go to encode-audio.js

main.js

Open main.js in you editor and enter the following content.

Click to go to main.js

The points are shown below.

  1. Get the media stream to capture the audio from the microphone.
  2. Get the settings such as resolution and sampling frequency.
  3. Load the module from audio-recorder.js.
  4. Create a node to handle the media stream.
  5. Create a node to use the worklet.
  6. Set the event handler when a message is received from the worklet.
  7. Start receiving messages from the worklet.
  8. Connect media streams, worklets and speakers. Since the worklet does not output anything, no sound is played from the speaker.
  9. Set the worklet's isRecording parameter to 1 to start recording.
  10. Set the worklet's isRecording parameter to 0 to stop recording.
  11. Call the encodeAudio function to convert the recorded audio to WAVE format.

Operation check

Just opening index.html in your web browser will fail to call audioContext.audioWorklet.addModule, so run the following command to start the web server.

npx http-server -c-1

Go to http://localhost:8080 in your web browser.

You will be asked to allow access to the microphone, so click the "Allow" button.

Click the Start button to start recording.

Click the "Stop" button to stop recording.

Click the play button to play the audio.

Referenced web pages

The web pages that I referred to when writing the article are shown below.

Conclusion

There is also a way to record audio using the MediaStream Recording API, which is easier. For details on how to use the MediaStream Recording API, see How to record audio using the MediaStream Recording API with JavaScript. I would appreciate it if you could take a look. Thank you for reading!

License

MIT

/node_modules/
/package-lock.json
# Do not ignore package-lock.json other than gist.
class AudioRecorder extends AudioWorkletProcessor {
static get parameterDescriptors () { // <1>
return [
{
name: 'isRecording',
defaultValue: 0,
minValue: 0,
maxValue: 1,
},
]
}
process (inputs, outputs, parameters) {
const buffer = []
const channel = 0
for (let t = 0; t < inputs[0][channel].length; t += 1) {
if (parameters.isRecording[0] === 1) { // <2>
buffer.push(inputs[0][channel][t])
}
}
if (buffer.length >= 1) {
this.port.postMessage({buffer}) // <3>
}
return true
}
}
registerProcessor('audio-recorder', AudioRecorder) // <4>
function encodeAudio (buffers, settings) {
const sampleCount = buffers.reduce((memo, buffer) => {
return memo + buffer.length
}, 0)
const bytesPerSample = settings.sampleSize / 8
const bitsPerByte = 8
const dataLength = sampleCount * bytesPerSample
const sampleRate = settings.sampleRate
const arrayBuffer = new ArrayBuffer(44 + dataLength)
const dataView = new DataView(arrayBuffer)
dataView.setUint8(0, 'R'.charCodeAt(0)) // <10>
dataView.setUint8(1, 'I'.charCodeAt(0))
dataView.setUint8(2, 'F'.charCodeAt(0))
dataView.setUint8(3, 'F'.charCodeAt(0))
dataView.setUint32(4, 36 + dataLength, true)
dataView.setUint8(8, 'W'.charCodeAt(0))
dataView.setUint8(9, 'A'.charCodeAt(0))
dataView.setUint8(10, 'V'.charCodeAt(0))
dataView.setUint8(11, 'E'.charCodeAt(0))
dataView.setUint8(12, 'f'.charCodeAt(0))
dataView.setUint8(13, 'm'.charCodeAt(0))
dataView.setUint8(14, 't'.charCodeAt(0))
dataView.setUint8(15, ' '.charCodeAt(0))
dataView.setUint32(16, 16, true)
dataView.setUint16(20, 1, true)
dataView.setUint16(22, 1, true)
dataView.setUint32(24, sampleRate, true)
dataView.setUint32(28, sampleRate * 2, true)
dataView.setUint16(32, bytesPerSample, true)
dataView.setUint16(34, bitsPerByte * bytesPerSample, true)
dataView.setUint8(36, 'd'.charCodeAt(0))
dataView.setUint8(37, 'a'.charCodeAt(0))
dataView.setUint8(38, 't'.charCodeAt(0))
dataView.setUint8(39, 'a'.charCodeAt(0))
dataView.setUint32(40, dataLength, true)
let index = 44
for (const buffer of buffers) {
for (const value of buffer) {
dataView.setInt16(index, value * 0x7fff, true)
index += 2
}
}
return new Blob([dataView], {type: 'audio/wav'})
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>How to record audio using Web Audio API in JavaScript</title>
</head>
<body>
<h1>How to record audio using Web Audio API in JavaScript</h1>
<div>
<button type="button" id="buttonStart">Start</button>
<button type="button" id="buttonStop" disabled>Stop</button>
</div>
<div>
<audio controls id="audio"></audio>
</div>
<script src="encode-audio.js"></script>
<script src="main.js"></script>
</body>
</html>
MIT License
Copyright (c) 2024 Tatsuya Sususkida
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
async function main () {
try {
const buttonStart = document.querySelector('#buttonStart')
const buttonStop = document.querySelector('#buttonStop')
const audio = document.querySelector('#audio')
const stream = await navigator.mediaDevices.getUserMedia({ // <1>
video: false,
audio: true,
})
const [track] = stream.getAudioTracks()
const settings = track.getSettings() // <2>
const audioContext = new AudioContext()
await audioContext.audioWorklet.addModule('audio-recorder.js') // <3>
const mediaStreamSource = audioContext.createMediaStreamSource(stream) // <4>
const audioRecorder = new AudioWorkletNode(audioContext, 'audio-recorder') // <5>
const buffers = []
audioRecorder.port.addEventListener('message', event => { // <6>
buffers.push(event.data.buffer)
})
audioRecorder.port.start() // <7>
mediaStreamSource.connect(audioRecorder) // <8>
audioRecorder.connect(audioContext.destination)
buttonStart.addEventListener('click', event => {
buttonStart.setAttribute('disabled', 'disabled')
buttonStop.removeAttribute('disabled')
const parameter = audioRecorder.parameters.get('isRecording')
parameter.setValueAtTime(1, audioContext.currentTime) // <9>
buffers.splice(0, buffers.length)
})
buttonStop.addEventListener('click', event => {
buttonStop.setAttribute('disabled', 'disabled')
buttonStart.removeAttribute('disabled')
const parameter = audioRecorder.parameters.get('isRecording')
parameter.setValueAtTime(0, audioContext.currentTime) // <10>
const blob = encodeAudio(buffers, settings) // <11>
const url = URL.createObjectURL(blob)
audio.src = url
})
} catch (err) {
console.error(err)
}
}
main()
{
"name": "javascript-media-audio",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"dev": "http-server -c-1"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"http-server": "^14.1.0"
}
}
@huynhquoctruongit
Copy link

huynhquoctruongit commented Mar 9, 2024

Hello @tatsuyasusukida,
I don't understand why when I record with my airpod it increases the playback speed, but when I record with my laptop mic it's fine.

I see when i change dataView.setUint32(24, sampleRate, true) => dataView.setUint32(24, sampleRate / 2 , true), it ok on my mic airpod, and it don't ok on my laptop

@tatsuyasusukida
Copy link
Author

@huynhquoctruongit

Thank you for your comment!
I don't have AirPods, but I'll try it once I get AirPods.

Thank you for letting me know how to solve the problem.
Very helpful πŸ˜„

@JRDove
Copy link

JRDove commented Apr 16, 2024

Many thanks for your efforts! If I wanted to distribute an app using this code, what license should I use and whose name should I put on the copyrigt notice, please?

@tatsuyasusukida
Copy link
Author

@JRDove

Thank you for your comment!
I'm honored to have you use my code in your app.

The code posted on this page is available under the MIT license.
Please include my name (Tatsuya Susukida) for copyright notice.

I just added the LICENSE file to this page, so please check it out.

@JRDove
Copy link

JRDove commented Apr 16, 2024

Thank you very much. Here is (a picture of) the license file I shall include, if that's OK. I''ll send you link to my app if and when I finish it. :-)
license
UPDATE Here's a link to what I've created https://www.mediacours.com/programs/recorder/

@bonadio
Copy link

bonadio commented Jul 5, 2024

Hi @tatsuyasusukida
Thanks for your code, I learned a lot with it. I notice that the resulted recorded sound is distorted, my voice gets higher-pitched specially at the end of the recording. The beginning seems fine but at the end my voice is very different. Do you know why is that?

@TechMainul-dev
Copy link

Help
From computer web browser recorder working fine but when i visit from Phone browser, its increase playback speed. and recording file length increase also, like when i reocord 10 second it's show me 30 seconds.
how to solve this issue ?

@alejandrocoding
Copy link

If I need to record an audio even when the phone is locked, how do you suggest doing this? Web workers works with serializable data so I guess it is not an option

@regmanua
Copy link

regmanua commented Oct 4, 2024

Having issues on FireFox and Safari: RangeError: Out of bound access (Safari) and ERROR RangeError: offset is outside the bounds of the DataView (Firefox) in encodeAudio function call

@tatsuyasusukida
Copy link
Author

@TechMainul-dev @alejandrocoding @regmanua

Thank you for your comment!
I sincerely apologize for the delayed response.
Although I am unable to address the issues immediately, I will do my best to resolve the issues you reported.
I truly appreciate your valuable feedback πŸ˜„

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment