If you have ever needed to combine multiple audio tracks—like stitching together podcast segments, merging voice memos, or blending sound effects—you have probably relied on desktop software like Audacity or heavy server-side scripts powered by FFmpeg.
Traditionally, implementing an audio merging feature in a web application requires users to upload multiple files to a cloud server, queue them in a backend worker, execute complex concatenation commands, and download the combined result. For a simple utility, this creates unnecessary network latency, high storage overhead, and severe privacy friction.
In this article, we will look at how we built ToolMole Audio Joiner (live at toolmole.com/audio-joiner), a fully client-side browser utility that decodes, sequences, and merges multiple audio files directly inside the user's browser sandbox.
The Architecture: Why Client-Side Audio Merging?
By shifting audio manipulation entirely to the client device using modern browser APIs, we achieve three major engineering advantages:
Zero Server Storage & Bandwidth Costs: Files are processed locally, dropping server egress and multi-part upload bandwidth bills to $0.
Instant Local Processing: No waiting in upload queues. Concatenation happens instantly using native browser memory.
Guaranteed Privacy: Users handling private voice notes, custom samples, or unreleased tracks never expose their raw data to third-party servers.
Core Technical Workflow
To concatenate multiple audio buffers sequentially in JavaScript, you need to align their sample rates, match their channel configurations, and merge their PCM data arrays.
- Decoding Multiple Input Files Using the HTML5 File API and the Web Audio API's AudioContext.decodeAudioData(), you can read and decode each uploaded file into separate AudioBuffer instances:
JavaScript
// Example: Decoding multiple files locally
async function decodeAudioFiles(files) {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const decodedBuffers = [];
for (const file of files) {
const arrayBuffer = await file.arrayBuffer();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
decodedBuffers.push(audioBuffer);
}
return { audioContext, decodedBuffers };
}
- Concatenating Audio Buffers Sequentially Once decoded, you calculate the total frame length across all tracks, create a new destination AudioBuffer, and copy the PCM sample data channel-by-channel in chronological order:
JavaScript
function concatenateAudioBuffers(audioContext, buffers) {
const numberOfChannels = buffers[0].numberOfChannels;
const sampleRate = buffers[0].sampleRate;
// Calculate total frame count across all input tracks
const totalLength = buffers.reduce((acc, buf) => acc + buf.length, 0);
// Create a master destination buffer
const combinedBuffer = audioContext.createBuffer(numberOfChannels, totalLength, sampleRate);
let offset = 0;
for (const buffer of buffers) {
for (let channel = 0; channel < numberOfChannels; channel++) {
const channelData = buffer.getChannelData(channel);
combinedBuffer.getChannelData(channel).set(channelData, offset);
}
offset += buffer.length;
}
return combinedBuffer;
}
Top comments (0)