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...

相关推荐
β添砖java13 小时前
vivo响应式官网
前端·css·html·1024程序员节
web打印社区18 小时前
使用React如何静默打印页面:完整的前端打印解决方案
前端·javascript·vue.js·react.js·pdf·1024程序员节
喜欢踢足球的老罗18 小时前
[特殊字符] PM2 入门实战:从 0 到线上托管 React SPA
前端·react.js·前端框架
小光学长18 小时前
基于Vue的课程达成度分析系统t84pzgwk(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
前端·数据库·vue.js
Baklib梅梅19 小时前
探码科技再获“专精特新”认定:Baklib引领AI内容管理新方向
前端·ruby on rails·前端框架·ruby
南方以南_19 小时前
Chrome开发者工具
前端·chrome
YiHanXii20 小时前
this 输出题
前端·javascript·1024程序员节
楊无好20 小时前
React中ref
前端·react.js
程琬清君20 小时前
vue3 confirm倒计时
前端·1024程序员节
歪歪10020 小时前
在C#中详细介绍一下Visual Studio中如何使用数据可视化工具
开发语言·前端·c#·visual studio code·visual studio·1024程序员节