How to Convert Audio from wav to mp3 in JavaScript Using ffmpeg.wasm

Check out this basic demo at https://convert.devtails.xyz/and keep an eye out for client side audio conversions in https://kaizen.place/ .

I just posted the following article the other day about using fluent-ffmpeg to convert wavs to mp3s in node.

https://devtails.medium.com/how-to-convert-audio-from-wav-to-mp3-in-node-js-using-ffmpeg-e5cb4af2da6

I had initially pondered doing the conversion on the client side, but wasn’t immediately able to find something. An astute Reddit commentor pointed me to ffmpeg.wasm and here we are with a followup for how to make the same conversion, but this time inside the browser.

The ffmpeg.wasm project has a Getting Started page, that was mostly helpful, but it skips a couple critical steps that had me tripped up for a bit.

Create Project With JavaScript Bundler

If you don’t already have a project setup with a bundler, you can quickly spin up your own.

npm initORyarn init

This will make a package.json file. Feel free to just press enter through all the configuration options as the defaults will be just fine.

Add esbuild

yarn add --dev esbuild

I’ve written about esbuild in the past, and it will likely be a recurring theme as I try to figure out if it’s safe for production use. The benefit of it here is that there’s no ugly bundling configuration file to explain.

Add Build Script

Update package.json to add the following build script

"scripts": {
"build": "esbuild src/app.js --bundle --outfile=public/app.js --minify"
},

I experienced some interesting behaviour with the ffmpeg library. Without the --minify above, it attempts to load the ffmpeg-core files from http://localhost:4242/node_modules/@ffmpeg/core/dist/ffmpeg-core.wasm . In an earlier experiment I faced the same issue with webpack, I’m not sure what the issue is, but for these purposes a minified build works just fine.

Add public/index.html

We’re just going for something simple to show that this works. A simple input to accept the file to convert and then an audio element to preview the converted mp3 file (and allow for download).

<html lang="en"><head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ffmpeg.wasm</title>
<script src="/app.js" async defer></script>
</head>
<body>
<input type="file" id="uploader">
<div>
<audio id="track" controls></audio>
</div>
</body>
</html>

Install ffmpeg

# Use npm
$ npm install @ffmpeg/ffmpeg
# Use yarn
$ yarn add @ffmpeg/ffmpeg

Add src/app.js

Whenever a file is selected in the file input, we want to convert it to an mp3 and show the new mp3 on the existing audio element.

const { createFFmpeg } = require('@ffmpeg/ffmpeg');// The log true is optional, shows ffmpeg logs in the console
const ffmpeg = createFFmpeg({ log: true });
const transcode = async ({ target: { files } }) => {
const { name } = files[0];
// This loads up the ffmpeg.wasm files from a CDN
await ffmpeg.load();
const arrayBuffer = await files[0].arrayBuffer()
const uint8Array = new Uint8Array(arrayBuffer);
ffmpeg.FS("writeFile", name, uint8Array);
await ffmpeg.run('-i', name, 'test.mp3');
const data = ffmpeg.FS('readFile', 'test.mp3');
const track = document.getElementById("track");
track.src = URL.createObjectURL(new Blob([data.buffer], { type: 'audio/mpeg' }));
}
const uploader = document.getElementById('uploader')
uploader.addEventListener('change', transcode);

Build public/app.js Bundle

yarn build

This will output public/app.js with your code from src/app.js and the required code from the imported ffmpeg library.

Attempt (and fail) to Open This With a Simple Http Server

You can try hosting the public folder with something like http-server. You will see the following warning:

[Deprecation] SharedArrayBuffer will require cross-origin isolation as of M92, around July 2021. See https://developer.chrome.com/blog/enabling-shared-array-buffer/ for more details.

Promptly followed by this error:

Uncaught (in promise) ReferenceError: SharedArrayBuffer is not defined

Adding a Simple Express Server

In order for this to all work you will need a custom server in order to achieve “cross-origin isolation”.

Install express

yarn add express

Create server.js

The Cross-Origin-Embedder-Policy must be set to require-corp and the Cross-Origin-Opener-Policy must be set to same-origin .

const express = require("express");const app = express();app.use((req, res, next) => {
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
next();
});
app.use(express.static("public"))app.listen(4242)

Start the Server

node server.js

You should now be able to access the page on http://localhost:4242. Find yourself a wav file to select in the input and open the console to see all kinds of debug info from ffmpeg.

Conclusion

This specifically focused on converting wav files to mp3s. But it should be pretty easy to see how you can use this to convert basically any media format to any other media format. The ffmpeg-core.wasm file clocks in at 8.5 MB, which is pretty steep. However, it won’t be loaded until you call ffmpeg.load() , which means you can avoid loading it until a user is actually attempting to convert some kind of file. Once loaded it remains cached via the CDN URL, which means the next time someone goes to convert some audio it won’t need to refetch it.

Also worth mentioning that SharedArrayBuffer is not available everywhere. Notably it is missing from current versions of Safari. Have a look at caniuse to get a sense of where it is and isn’t available.

VPE @ https://dubsado.com/. Writing about rust, web development, engineering management, and everything in between. https://devtails.xyz/