我最近接手的这个electron项目,需要对音频做处理,做一些简单编辑操作,复制粘贴剪切撤销之类的,编辑完成后需保存新的音频文件到电脑,我使用的音频波形可视化库是wavesurfer.js,加载的音频采样率默认是48000(音频实际是8000),这样会导致新生成的文件大小翻数倍
但是我用如下方式直接在复制数据的时候指定采样率会导致音频失真
// 假设我们有个 audioBuffer 类型源数据
let audioCtx = new AudioContext(); // 创建音频上下文
let channels = audioBuffer.numberOfChannels; // 源数据的声道数
let rate = 8000; // 采样率
let frameCount = 10 * rate; // 音频帧数 = 时长*采样率
// 创建指定采用率、同样声道数量,同样长度的空的AudioBuffer
let newAudioBuffer = audioCtx.createBuffer(channels, frameCount, rate);
// 遍历声道
for (let i = 0; i < channels; i++) {
// 截取 audioBuffer 数据 赋值给 newAudioBuffer
newAudioBuffer.getChannelData(i).set(audioBuffer.getChannelData(i));
}
以上方法并不可行,不能直接修改采样率,接下来的方法亲测可用
参考文章:
直接上代码,使用的时候只需要使用audioBufferToWav方法就行了
使用
electron可以同时使用web api 和node api
import audioBufferToWav from './toWav.js';
let wav = audioBufferToWav(this.newAudioBuffer); // 直接转成wav
let buffer = Buffer.from(wav); // 再转成buffer
fs.writeFileSync(filePath, wavBuffer); // 使用node自带读写模块fs写入文件到电脑
js部分
// toWav.js
/** audioBuffer 转wav
* @param {audioBuffer} samples 音频样本
* @param {Object} opt={} 配置项 outputRate采样率 bitDepth位深 numChannels声道数
*/
export default function audioBufferToWav(samples, opt = {}) {
var outputRate = opt.outputRate || 8000; // 指定输出采样率
var bitDepth = opt.bitDepth || 16; // 指定位数
var numChannels = opt.numChannels || samples.numberOfChannels; // 指定声道数
var times = (samples.sampleRate / outputRate) >> 0; // 计算压缩率
// 采样率压缩核心实现在这一步
var interleaved =
numChannels === 2? interleave(times,samples.getChannelData(0),samples.getChannelData(1))
: interleave(times, samples.getChannelData(0));
return encodeWAV(interleaved, outputRate, bitDepth, numChannels);
}
/** 压缩采样率
* @params {Number} times 压缩率
* @params {Float32Array} inputL 左声道
* @params {Float32Array} inputR 右声道
*/
function interleave(times, inputL, inputR) {
var length;
if (inputR) length = ((inputL.length + inputR.length) / times) >> 0; // 计算压缩后的数据长度
else length = (inputL.length / times) >> 0;
var result = new Float32Array(length);
var index = 0,inputIndex = 0;
while (index < length) {
result[index++] = inputL[inputIndex];
if (inputR) result[index++] = inputR[inputIndex];
inputIndex += times; // 每次跳过压缩率的倍数
}
return result;
}
/** buffer转wav
* @params {audioBuffer} samples 样本
* @params {Number} outputRate 采样率
* @params {Number} bitDepth 位深 8 16 32
* @params {Number} numChannels 声道数
*/
function encodeWAV(samples, outputRate, bitDepth, numChannels) {
let bytesPerSample = bitDepth / 8; // 字节
var blockAlign = numChannels * bytesPerSample; // 采样一次占用字节数
var len = samples.length * bytesPerSample; // 样本长度
var buffer = new ArrayBuffer(44 + len); // 有44字节是头部文件
var view = new DataView(buffer);
/* 资源交换文件标识符 */
writeString(view, 0, 'RIFF');
/* 下个地址开始到文件尾总字节数,即文件大小-8 */
view.setUint32(4, /*32*/ 36 + len, true);
/* WAV文件标志 */
writeString(view, 8, 'WAVE');
/* 波形格式标志 */
writeString(view, 12, 'fmt ');
/* 过滤字节,一般为 0x10 = 16 */
view.setUint32(16, 16, true);
/* 格式类别 (PCM形式采样数据) */
view.setUint16(20, 1, true);
/* 通道数 */
view.setUint16(22, numChannels, true);
/* 采样率,每秒样本数,表示每个通道的播放速度 */
view.setUint32(24, outputRate, true);
/* 波形数据传输率 (每秒平均字节数) 通道数×每秒数据位数×每样本数据位/8 */
view.setUint32(28, outputRate * blockAlign, true);
/* 快数据调整数 采样一次占用字节数 通道数×每样本的数据位数/8 */
view.setUint16(32, blockAlign, true);
/* 每样本数据位数 */
view.setUint16(34, bitDepth, true);
/* 数据标识符 */
writeString(view, 36, 'data');
/* 采样数据总数,即数据总大小-44 */
view.setUint32(40, len, true);
/* 采样数据 */
if (bitDepth === 8) floatTo8BitPCM(view, 44, samples); // 8位
else if (bitDepth === 16) floatTo16BitPCM(view, 44, samples); // 16位
else writeFloat32(view, 44, samples); // 32位
return buffer;
}
function writeString(view, offset, string) {
for (var i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
function floatTo8BitPCM(output, offset, input) {
for (var i = 0; i < input.length; i++, offset++) {
//这里只能加1了
var s = Math.max(-1, Math.min(1, input[i]));
var val = s < 0 ? s * 0x8000 : s * 0x7fff;
val = parseInt(255 / (65535 / (val + 32768))); // 有人声的时候会有杂音
// var val = ((s < 0 ? s * 0x8000 : s * 0x7fff) >> 8) + 128; // 有人声无人声都会有杂音
output.setInt8(offset, val, true);
}
}
function floatTo16BitPCM(output, offset, input) {
for (var i = 0; i < input.length; i++, offset += 2) {
//因为是int16所以占2个字节,所以偏移量是+2
var s = Math.max(-1, Math.min(1, input[i]));
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
}
}
function writeFloat32(output, offset, input) {
for (let i = 0; i < input.length; i++, offset += 4) {
output.setFloat32(offset, input[i], true);
}
}