纯JS实现音频的合并或拼接实例页面
回到相关文章 »效果:
原始音频
段落1:
段落2:
背景音:
操作与预览
//zxx: 先拼接再合并
代码:
HTML代码:
<h4>原始音频</h4> <p>段落1:<audio src="./assets/1.wav" controls></audio></p> <p>段落2:<audio src="./assets/2.wav" controls></audio></p> <p>背景音:<audio src="./assets/bgmusic.wav" controls></audio></p> <h4>操作与预览</h4> <button id="audioConcat">拼接两个段落音频</button> <div id="outputConcat" class="output"></div> <button id="audioMerge" disabled>合并音频</button> <div id="outputMerge" class="output"></div>
JS代码:
// 音频地址 const audioSrc = [ './assets/1.wav', './assets/2.wav' ]; const bgAudioSrc = './assets/bgmusic.wav'; // AudioContext const audioContext = new AudioContext(); // 基于src地址获得 AudioBuffer 的方法 const getAudioBuffer = (src) => { return new Promise((resolve, reject) => { fetch(src).then(response => response.arrayBuffer()).then(arrayBuffer => { audioContext.decodeAudioData(arrayBuffer).then(buffer => { resolve(buffer); }); }) }) } // 拼接音频的方法 const concatAudio = (arrBufferList) => { // 获得 AudioBuffer const audioBufferList = arrBufferList; // 最大通道数 const maxChannelNumber = Math.max(...audioBufferList.map(audioBuffer => audioBuffer.numberOfChannels)); // 总长度 const totalLength = audioBufferList.map((buffer) => buffer.length).reduce((lenA, lenB) => lenA + lenB, 0); // 创建一个新的 AudioBuffer const newAudioBuffer = audioContext.createBuffer(maxChannelNumber, totalLength, audioBufferList[0].sampleRate); // 将所有的 AudioBuffer 的数据拷贝到新的 AudioBuffer 中 let offset = 0; audioBufferList.forEach((audioBuffer, index) => { for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) { newAudioBuffer.getChannelData(channel).set(audioBuffer.getChannelData(channel), offset); } offset += audioBuffer.length; }); return newAudioBuffer; } // 合并音频的方法 const mergeAudio = (arrBufferList) => { // 获得 AudioBuffer const audioBufferList = arrBufferList; // 最大播放时长 const maxDuration = Math.max(...audioBufferList.map(audioBuffer => audioBuffer.duration)); // 最大通道数 const maxChannelNumber = Math.max(...audioBufferList.map(audioBuffer => audioBuffer.numberOfChannels)); // 创建一个新的 AudioBuffer const newAudioBuffer = audioContext.createBuffer(maxChannelNumber, audioBufferList[0].sampleRate * maxDuration, audioBufferList[0].sampleRate); // 将所有的 AudioBuffer 的数据合并到新的 AudioBuffer 中 audioBufferList.forEach((audioBuffer, index) => { for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) { const outputData = newAudioBuffer.getChannelData(channel); const bufferData = audioBuffer.getChannelData(channel); for (let i = audioBuffer.getChannelData(channel).length - 1; i >= 0; i--) { outputData[i] += bufferData[i]; } newAudioBuffer.getChannelData(channel).set(outputData); } }); return newAudioBuffer; } // 这个背景音乐合并的时候需要,所以放外面了 let concatAudioBuffer = null; // 点击按钮后 audioConcat.onclick = async function () { if (this.classList.contains('loading')) { return; } this.classList.add('loading'); // 拼接音频 const arrBufferList = await Promise.all(audioSrc.map(src => getAudioBuffer(src))); concatAudioBuffer = concatAudio(arrBufferList); const newAudioSrc = URL.createObjectURL(bufferToWave(concatAudioBuffer, concatAudioBuffer.length)); outputConcat.innerHTML = `<audio src="${newAudioSrc}" controls></audio>`; // 移除 loading 样式 this.classList.remove('loading'); // 移除合并按钮的禁用态 audioMerge.disabled = false; } // 合并按钮点击后 audioMerge.onclick = async function () { if (!concatAudioBuffer || this.classList.contains('loading')) { return; } this.classList.add('loading'); // 请求背景音乐的 buffer 数据 const bgAudioBuffer = await getAudioBuffer(bgAudioSrc); // 合并音频 const newAudioBuffer = mergeAudio([concatAudioBuffer, bgAudioBuffer]); // 合并音频 const newAudioSrc = URL.createObjectURL(bufferToWave(newAudioBuffer, newAudioBuffer.length)); // 预览合并后的音频 outputMerge.innerHTML = `<audio src="${newAudioSrc}" controls></audio>`; // 移除 loading 样式 this.classList.remove('loading'); } // AudioBuffer 转 blob function bufferToWave(abuffer, len) { var numOfChan = abuffer.numberOfChannels, length = len * numOfChan * 2 + 44, buffer = new ArrayBuffer(length), view = new DataView(buffer), channels = [], i, sample, offset = 0, pos = 0; // write WAVE header // "RIFF" setUint32(0x46464952); // file length - 8 setUint32(length - 8); // "WAVE" setUint32(0x45564157); // "fmt " chunk setUint32(0x20746d66); // length = 16 setUint32(16); // PCM (uncompressed) setUint16(1); setUint16(numOfChan); setUint32(abuffer.sampleRate); // avg. bytes/sec setUint32(abuffer.sampleRate * 2 * numOfChan); // block-align setUint16(numOfChan * 2); // 16-bit (hardcoded in this demo) setUint16(16); // "data" - chunk setUint32(0x61746164); // chunk length setUint32(length - pos - 4); // write interleaved data for(i = 0; i < abuffer.numberOfChannels; i++) channels.push(abuffer.getChannelData(i)); while(pos < length) { // interleave channels for(i = 0; i < numOfChan; i++) { // clamp sample = Math.max(-1, Math.min(1, channels[i][offset])); // scale to 16-bit signed int sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767)|0; // write 16-bit sample view.setInt16(pos, sample, true); pos += 2; } // next source sample offset++ } // create Blob return new Blob([buffer], {type: "audio/wav"}); function setUint16(data) { view.setUint16(pos, data, true); pos += 2; } function setUint32(data) { view.setUint32(pos, data, true); pos += 4; } }