背景
我们做的是一个类似朗读的小程序,有多个词语需要录音,在最后提交的时候需要生成一个完整的音频(每个词语拼合),传到oss上作为 一个单一的路径传给后端,那么该怎么做呢?
思路
小程序可以和node一样调用fs.readFile, unit8Array等方法,那unit8Array是什么呢,它可以用来存放二进制的数据,而我们的音频就是二进制数据类型的
graph TD
读取所有音频文件 --> 计算所有buffer的长度 --> 合并缓冲区 --> 生成输出文件路径 --> 写入合并后的文件
buffer和unit8Array的区别
我们用fs.readFile读取出的音频文件是buffer类型的,buffer没有set方法,不能动态设置音频合成的偏移量,这样导致我们无法合成音频,而unit8Array则提供了这个能力,下面我将通过这些方法进行音频的合成。
样例代码
第一步
js
/**
* 读取所有音频文件
* @param {Array} filePaths - 文件路径数组
* @param {Function} onProgress - 进度回调
* @returns {Promise<Array>} 文件缓冲区数组
*/
async readAudioFiles(filePaths, onProgress) { // filePaths是当前所有录音的路径
const fileBuffers = [];
const totalFiles = filePaths.length;
console.log(`开始读取 ${totalFiles} 个音频文件...`);
for (let i = 0; i < filePaths.length; i++) {
const filePath = filePaths[i];
try {
console.log(`读取文件 ${i + 1}/${totalFiles}: ${path.basename(filePath)}`);
const fileData = await fs.readFile(filePath);
const buffer = new Uint8Array(fileData);
fileBuffers.push({
buffer: buffer,
path: filePath,
size: buffer.length
}); // 储存每一个文件的buffer数据
// 更新进度
if (onProgress) {
const progress = Math.round(((i + 1) / totalFiles) * 50); // 读取占50%进度
onProgress(progress, `已读取 ${i + 1}/${totalFiles} 个文件`);
}
} catch (error) {
throw new Error(`读取文件失败: ${filePath}, 错误: ${error.message}`);
}
}
return fileBuffers;
}
第二步
js
/**
* 计算总长度
* @param {Array} fileBuffers - 文件缓冲区数组
* @returns {number} 总长度
*/
calculateTotalLength(fileBuffers) {
let totalLength = 0;
for (const fileBuffer of fileBuffers) {
totalLength += fileBuffer.buffer.length;
}
return totalLength;
}
第三步
js
/**
* 合并缓冲区
* @param {Array} fileBuffers - 文件缓冲区数组
* @param {Uint8Array} mergedBuffer - 合并缓冲区
* @param {Function} onProgress - 进度回调
*/
async mergeBuffers(fileBuffers, mergedBuffer, onProgress) {
console.log('开始合并音频数据...');
let offset = 0;
const totalFiles = fileBuffers.length;
for (let i = 0; i < fileBuffers.length; i++) {
const fileBuffer = fileBuffers[i];
console.log(`合并文件 ${i + 1}/${totalFiles}: ${path.basename(fileBuffer.path)}`);
// 将当前文件的缓冲区数据复制到合并缓冲区
mergedBuffer.set(fileBuffer.buffer, offset);
offset += fileBuffer.buffer.length;
// 更新进度 (合并占50%进度)
if (onProgress) {
const progress = 50 + Math.round(((i + 1) / totalFiles) * 50);
onProgress(progress, `已合并 ${i + 1}/${totalFiles} 个文件`);
}
}
console.log('音频数据合并完成');
}
第四步
js
/**
* 生成输出文件路径
* @param {Array} filePaths - 输入文件路径数组
* @param {string} outputFormat - 输出格式
* @param {string} outputName - 输出文件名
* @returns {Promise<string>} 输出文件路径
*/
async generateOutputPath(filePaths, outputFormat, outputName) {
let fileExtension;
if (outputFormat === 'auto') {
// 使用第一个文件的扩展名
const firstFilePath = filePaths[0];
fileExtension = path.extname(firstFilePath);
} else {
fileExtension = outputFormat.startsWith('.') ? outputFormat : `.${outputFormat}`;
}
const fileName = outputName || `merged_audio_${Date.now()}${fileExtension}`;
return path.join(this.outputDir, fileName);
}
第五步
js
/**
* 写入合并后的文件
* @param {Uint8Array} mergedBuffer - 合并后的缓冲区
* @param {string} outputPath - 输出路径
*/
async writeMergedFile(mergedBuffer, outputPath) {
console.log(`写入合并文件: ${outputPath}`);
try {
await fs.writeFile(outputPath, mergedBuffer.buffer);
console.log('文件写入成功');
} catch (error) {
throw new Error(`写入文件失败: ${error.message}`);
}
}
第六步
js
/**
* 合并多个音频文件
* @param {Array} filePaths - 音频文件路径数组
* @param {Object} options - 合并选项
* @returns {Promise<Object>} 合并结果
*/
async mergeAudioFiles(filePaths, options = {}) {
const {
outputFormat = 'auto',
outputName = null,
onProgress = null
} = options;
try {
console.log('开始音频合并过程...');
// 1. 验证输入文件
await this.validateInputFiles(filePaths);
// 2. 读取所有音频文件
const fileBuffers = await this.readAudioFiles(filePaths, onProgress);
// 3. 计算合并后的总长度
const totalLength = this.calculateTotalLength(fileBuffers);
console.log(`总文件大小: ${(totalLength / 1024 / 1024).toFixed(2)} MB`);
// 4. 创建合并缓冲区
const mergedBuffer = new Uint8Array(totalLength);
// 5. 合并音频数据
await this.mergeBuffers(fileBuffers, mergedBuffer, onProgress);
// 6. 确定输出文件格式和路径
const outputPath = await this.generateOutputPath(filePaths, outputFormat, outputName);
// 7. 写入合并后的文件
await this.writeMergedFile(mergedBuffer, outputPath);
console.log('音频合并完成:', outputPath);
return {
success: true,
outputPath: outputPath,
fileSize: totalLength,
fileCount: filePaths.length,
message: '音频合并成功'
};
} catch (error) {
console.error('音频合并失败:', error);
return {
success: false,
error: error.message,
message: '音频合并失败'
};
}
}