node,小程序合成音频的方式

背景

我们做的是一个类似朗读的小程序,有多个词语需要录音,在最后提交的时候需要生成一个完整的音频(每个词语拼合),传到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: '音频合并失败'
            };
        }
    }

gitHub地址

github.com/abcdftg/aud...

相关推荐
爱吃无爪鱼几秒前
02-前端开发核心概念完全指南
css·vue.js·前端框架·npm·node.js·sass
拾忆,想起2 分钟前
Dubbo本地存根与本地伪装实战指南:提升微服务容错能力的利器
前端·微服务·云原生·架构·dubbo·safari
本妖精不是妖精3 分钟前
在 CentOS 7 上部署 Node.js 18 + Claude Code
linux·python·centos·node.js·claudecode
wuli_滔滔6 分钟前
DevUI弹窗体系重构:微前端场景下的模态管理策略
前端·重构·架构
fruge9 分钟前
Angular 17 新特性深度解析:独立组件 + 信号系统实战
前端·javascript·vue.js
卓码软件测评9 分钟前
第三方软件质量检测机构:【Apifox多格式支持处理JSON、XML、GraphQL等响应类型】
前端·测试工具·正则表达式·测试用例·压力测试
心随雨下12 分钟前
Flutter加载自定义CSS样式文件方法
前端·css·flutter
X***C86215 分钟前
SpringMVC 请求参数接收
前端·javascript·算法
GDAL18 分钟前
css实现元素居中的18种方法
前端·css·面试·html·css3·css居中