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

相关推荐
子兮曰2 小时前
🔥深度解析:Nginx目录浏览美化与功能增强实战指南
前端·javascript·nginx
我是日安2 小时前
从零到一打造 Vue3 响应式系统 Day 4 - 核心概念:收集依赖、触发更新
前端·vue.js
跟橙姐学代码2 小时前
不要再用 print() 了!Python logging 库才是调试的终极武器
前端·python
ze_juejin2 小时前
JavaScript 中预防 XSS(跨站脚本攻击)
前端
我是天龙_绍2 小时前
🐴 记住了,节流(throttle)与防抖(debounce)
前端
凡二人2 小时前
Flip-js 优雅的处理元素结构变化的动画(解读)
前端·typescript
争当第一摸鱼前端2 小时前
Electron中的下载操作
前端
sjin2 小时前
React 源码 - Commit Phase 的工作细节
前端
FisherYu2 小时前
AI环境搭建pytorch+yolo8搭建
前端·计算机视觉