Codificar áudio em MP3 no navegador com WebAssembly
Transforme um arquivo de áudio curto em MP3 sem fazer upload dele. Este tutorial cria uma pequena aplicação de navegador com FFmpeg.wasm, um servidor estático local, um botão de cancelamento e um link para download.
A codificação no navegador mantém o áudio selecionado no dispositivo do usuário. O navegador baixa os recursos do codificador, lê o arquivo selecionado para a memória e executa o FFmpeg em um Web Worker. A codificação ainda usa CPU e memória, por isso este exemplo limita as entradas a 25 MiB e processa um arquivo por vez.
Escolher o conjunto de pacotes do FFmpeg
O FFmpeg.wasm empacota o FFmpeg como WebAssembly. Usamos
@ffmpeg/core@0.12.10, de thread única, com @ffmpeg/ffmpeg@0.12.15. O wrapper e o núcleo têm números de versão
independentes; instalar o mesmo número para ambos não define um conjunto válido de pacotes.
- Processamento local: a aplicação não tem endpoint de upload de áudio.
- Responsividade: um worker executa o codificador fora da thread principal da interface.
- Suporte a formatos: o FFmpeg fornece os decodificadores e o codificador MP3 usados aqui.
O WebAssembly não garante velocidade de codificação nativa. As perguntas frequentes do FFmpeg.wasm explicam suas limitações de desempenho e memória; teste arquivos representativos nos dispositivos aos quais você oferece suporte.
Configurar o projeto local
Pré-requisitos
Use Node.js 24.15 ou posterior da linha 24.x, ou 26.5 ou posterior da linha 26.x, Bash e um navegador
com WebAssembly e workers de módulo. Este passo a passo foi testado no Linux com Node.js 24.15.0,
26.5.0 e 26.8.1, Yarn 4.12.0 e Chromium 145. Esses são os ambientes de execução de servidor testados,
não requisitos do codificador no navegador. O Node executa server.ts diretamente com
suporte nativo a TypeScript;
não é necessário um compilador nem um empacotador.
Verifique se node e corepack estão no seu PATH antes de criar os arquivos. Instale o
Corepack separadamente se sua distribuição do Node
não o incluir. A configuração abaixo solicita explicitamente o Yarn 4.12.0 ao Corepack.
Comece com um WAV curto e íntegro, mono ou estéreo, a 44,1 kHz ou 48 kHz. Outros contêineres de áudio
funcionam apenas quando seu decodificador está incluído no núcleo FFmpeg de versão fixada; taxas de
amostragem ou disposições de canais não compatíveis podem passar por reamostragem ou redução de
canais para MP3. O atributo accept do seletor de arquivos é uma conveniência, não uma
validação. Outros navegadores e dispositivos móveis precisam de testes próprios.
Configuração do projeto
Cole este bloco no Bash a partir do diretório em que você quer criar o projeto. Ele se recusa a usar
um diretório webassembly-audio-encoder existente e mantém seu shell no diretório original,
inclusive se a instalação falhar. O arquivo de lock no diretório filho cria um projeto Yarn separado,
e o nome personalizado do arquivo de configuração evita herdar as configurações usuais de
.yarnrc.yml de um projeto pai.
(
command -v node >/dev/null &&
command -v corepack >/dev/null &&
mkdir webassembly-audio-encoder &&
cd webassembly-audio-encoder &&
printf '%s\n' \
'{"private":true,"type":"module","packageManager":"yarn@4.12.0",' \
'"dependencies":{"@ffmpeg/ffmpeg":"0.12.15","@ffmpeg/core":"0.12.10","express":"5.1.0"}}' \
> package.json &&
printf '\n' > yarn.lock &&
printf '%s\n' 'nodeLinker: node-modules' 'enableGlobalCache: false' \
> .audio-yarnrc.yml &&
YARN_RC_FILENAME=.audio-yarnrc.yml corepack yarn@4.12.0 install &&
mkdir -p public/vendor/ffmpeg public/vendor/core &&
cp -R node_modules/@ffmpeg/ffmpeg/dist/esm/. public/vendor/ffmpeg/ &&
cp -R node_modules/@ffmpeg/core/dist/esm/. public/vendor/core/
)
Mantenha todo o diretório ffmpeg: seus módulos JavaScript incluem o worker do wrapper e suas
importações relativas. Ambos os diretórios de dependências locais devem vir desse conjunto de
pacotes instalado. Não há dependência de CDN em tempo de execução, e essas importações relativas
no navegador não precisam de um empacotador.
Se a instalação ou a cópia falhar depois que os arquivos de configuração tiverem sido gravados, mantenha o diretório, resolva o erro informado e repita as etapas restantes a partir do mesmo diretório pai:
(
cd webassembly-audio-encoder &&
YARN_RC_FILENAME=.audio-yarnrc.yml corepack yarn@4.12.0 install &&
mkdir -p public/vendor/ffmpeg public/vendor/core &&
cp -R node_modules/@ffmpeg/ffmpeg/dist/esm/. public/vendor/ffmpeg/ &&
cp -R node_modules/@ffmpeg/core/dist/esm/. public/vendor/core/
)
A nova tentativa substitui os arquivos dos pacotes copiados localmente e preserva os arquivos da
sua aplicação. Use o mesmo seletor YARN_RC_FILENAME em qualquer comando Yarn posterior neste projeto.
Configuração do servidor de desenvolvimento
Salve este conteúdo como webassembly-audio-encoder/server.ts. Ele serve apenas public/, portanto os arquivos do projeto
não ficam expostos. A porta 3000 deve estar livre. O Express 5
passa falhas de vinculação para o callback de listen;
o código imprime um erro e encerra com um status diferente de zero:
import { fileURLToPath } from 'node:url'
import express from 'express'
const app = express()
app.use(express.static(fileURLToPath(new URL('./public/', import.meta.url))))
app.listen(3000, '127.0.0.1', (error) => {
if (error) {
console.error(`Cannot start the audio server: ${error.message}`)
process.exitCode = 1
return
}
console.log('Open http://127.0.0.1:3000')
})
Depois de criar os arquivos abaixo, inicie o servidor a partir do diretório pai:
(cd webassembly-audio-encoder && node server.ts)
Abra http://127.0.0.1:3000. Pare o servidor com Ctrl+C ao terminar; seu shell permanece no
diretório pai. Este servidor de loopback é destinado ao passo a passo local.
Adicionar os controles do codificador
Salve este conteúdo como webassembly-audio-encoder/public/index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Browser audio encoder</title>
</head>
<body>
<h1>Encode audio to MP3</h1>
<label for="uploader">Audio file, up to 25 MiB</label>
<input type="file" id="uploader" accept="audio/*" />
<button id="encodeButton" type="button">Encode audio</button>
<button id="cancelButton" type="button" disabled>Cancel</button>
<p id="status" role="status">Choose an audio file.</p>
<a id="download" download="output.mp3" hidden>Download MP3</a>
<script type="module" src="./index.js"></script>
</body>
</html>
Salve este conteúdo como webassembly-audio-encoder/public/index.js. Cada tentativa recebe seu próprio worker e sistema de
arquivos virtual. Encerrar esse worker descarta seus arquivos virtuais e o estado do codificador,
inclusive após uma conversão malsucedida. O Blob do download concluído permanece disponível até que
sua URL seja revogada.
import { FFmpeg } from './vendor/ffmpeg/index.js'
const uploader = document.getElementById('uploader')
const encodeButton = document.getElementById('encodeButton')
const cancelButton = document.getElementById('cancelButton')
const status = document.getElementById('status')
const download = document.getElementById('download')
let active = null
let downloadURL = null
function clearDownload() {
download.hidden = true
download.removeAttribute('href')
if (downloadURL !== null) URL.revokeObjectURL(downloadURL)
downloadURL = null
}
function cancelEncoding() {
if (active === null) return
active.canceled = true
active.ffmpeg.terminate()
}
async function encodeFile() {
if (active !== null) return
clearDownload()
const file = uploader.files?.[0]
if (!file || file.size === 0 || file.size > 25 * 1024 * 1024) {
status.textContent = 'Choose a nonempty audio file of at most 25 MiB.'
return
}
const job = { ffmpeg: new FFmpeg(), canceled: false }
active = job
uploader.disabled = true
encodeButton.disabled = true
cancelButton.disabled = false
status.textContent = 'Loading the encoder…'
// Also bound loading and worker failures that may never reply to the wrapper.
const deadline = setTimeout(() => {
job.ffmpeg.terminate()
}, 120_000)
try {
await job.ffmpeg.load({
coreURL: new URL('./vendor/core/ffmpeg-core.js', location.href).href,
wasmURL: new URL('./vendor/core/ffmpeg-core.wasm', location.href).href,
})
const input = new Uint8Array(await file.arrayBuffer())
if (job.canceled) return
await job.ffmpeg.writeFile('input.audio', input)
status.textContent = 'Encoding…'
const exitCode = await job.ffmpeg.exec(
['-i', 'input.audio', '-map', '0:a:0', '-vn', '-c:a', 'libmp3lame', '-b:a', '192k', 'output.mp3'],
60_000,
)
if (exitCode !== 0) throw new Error('Encoder failed or timed out')
const output = await job.ffmpeg.readFile('output.mp3')
if (!(output instanceof Uint8Array) || output.byteLength === 0) {
throw new Error('Encoder returned no audio')
}
downloadURL = URL.createObjectURL(new Blob([output], { type: 'audio/mpeg' }))
download.href = downloadURL
download.hidden = false
status.textContent = 'Done. Your MP3 is ready to download.'
} catch {
status.textContent = job.canceled
? 'Encoding canceled.'
: 'Encoding failed. Try a shorter supported audio file and check the encoder assets.'
} finally {
clearTimeout(deadline)
job.ffmpeg.terminate()
active = null
uploader.disabled = false
encodeButton.disabled = false
cancelButton.disabled = true
if (job.canceled) status.textContent = 'Encoding canceled.'
}
}
encodeButton.addEventListener('click', encodeFile)
cancelButton.addEventListener('click', cancelEncoding)
window.addEventListener('pagehide', () => {
cancelEncoding()
clearDownload()
})
window.addEventListener('pageshow', (event) => {
if (event.persisted && active === null) {
status.textContent = 'Choose an audio file to encode again.'
}
})
Os nomes fixos dos arquivos virtuais evitam que o nome de um arquivo selecionado seja tratado como
uma opção ou um caminho do FFmpeg. Um mapeamento explícito de áudio seleciona o primeiro fluxo de
áudio, e um código de saída diferente de zero impede que uma saída parcial seja oferecida como um
download bem-sucedido. O link do MP3 permanece válido até a próxima tentativa de codificação ou até
que a página seja ocultada. Neste comando, -b:a 192k seleciona uma taxa de bits constante para MP3.
As opções de libmp3lame distinguem a taxa de bits das
configurações de qualidade com taxa de bits variável.
Carregar recursos locais compatíveis
O wrapper do FFmpeg inicia um
worker de módulo a partir de vendor/ffmpeg/worker.js, copiado anteriormente. Esse worker importa o núcleo ESM da
mesma origem e carrega seu arquivo Wasm. Esse núcleo de thread única não precisa de um
ffmpeg-core.worker.js separado nem de SharedArrayBuffer. Não o substitua por @ffmpeg/core-mt sem também
implementar seus requisitos adicionais de worker e isolamento entre origens.
Limpar os recursos de cada conversão
A camada JavaScript gerencia a seleção de arquivos e as URLs de download; o FFmpeg cuida da decodificação e da codificação. Um AudioContext ou AudioWorklet é desnecessário para essa conversão de arquivos.
A referência da API do FFmpeg documenta
as operações de arquivos baseadas em promessas, o tempo limite de execução e
terminate(). Criar um worker por tarefa custa tempo de inicialização, mas simplifica o
cancelamento e a limpeza dos arquivos virtuais.
Converter e verificar uma gravação
Selecione um WAV curto com som reconhecível tanto no início quanto no fim e pressione
Encode audio. Quando a codificação terminar, o status exibirá
Done. Your MP3 is ready to download.
Acesse Download MP3. Abra o arquivo salvo output.mp3 em um reprodutor de áudio e
ouça até o fim. A duração, os canais e o conteúdo devem corresponder à gravação selecionada.
Em seguida, teste outra gravação e depois um arquivo vazio ou não compatível após uma conversão bem-sucedida. O link de download anterior deve desaparecer quando você pressionar Encode audio. Pressione Cancel durante o carregamento e durante a codificação; depois disso, Encode audio deve ficar disponível novamente. Saia da página e volte: a página deve permitir outra conversão, mantendo oculto o link de download antigo.
O encerramento bem-sucedido do codificador não comprova que uma entrada danificada estava íntegra. O FFmpeg pode recuperar áudio de alguns arquivos truncados e ainda retornar zero. Use entradas íntegras e verifique toda a gravação baixada antes de confiar nela.
O limite de entrada de 25 MiB é uma política desta demonstração, não uma garantia de uso máximo de memória. O áudio decodificado e o codificador podem ocupar muito mais memória do que a entrada comprimida. O comando tem um tempo limite de codificação de 60 segundos, e o prazo externo de 120 segundos encerra um carregamento ou uma chamada ao worker que tenha travado. Para arquivos maiores, considere o processamento no servidor em vez de aumentar os limites sem medições.
Mantenha os arquivos de dependências locais junto com o código da aplicação e repita as verificações de conversão ao atualizar os pacotes de versões fixadas. Este exemplo não fornece endpoint de upload nem de armazenamento; os downloads são salvos pelo seu navegador.
Para fluxos de trabalho que precisam de uploads e processamento no servidor, explore o serviço de codificação de áudio da Transloadit.
