音频采集(VUE3+JAVA)

vue部分代码

xx.vue

javascript 复制代码
import Recorder from './Recorder.js';
export default {
  data() {
        return {
            mediaStream: null,
            recorder: null,
            isRecording: false,
            audioChunks: [],
            vadInterval: null // 新增:用于存储声音活动检测的间隔 ID
        };
    },
    async mounted() {
        this.mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
        this.startVAD();
    },
    beforeDestroy() {
        // 新增:组件销毁前清理声音活动检测的间隔
        if (this.vadInterval) {
            cancelAnimationFrame(this.vadInterval);
        }
    },
    created() {
        this.defaultLogin();
    },
    methods: {
        startVAD() {
            const audioContext = new (window.AudioContext || window.webkitAudioContext)();
            const source = audioContext.createMediaStreamSource(this.mediaStream);
            const analyser = audioContext.createAnalyser();
            source.connect(analyser);
            analyser.fftSize = 2048;
            const bufferLength = analyser.frequencyBinCount;
            const dataArray = new Uint8Array(bufferLength);
            const checkVoiceActivity = () => {
                analyser.getByteFrequencyData(dataArray);
                let sum = 0;
                for (let i = 0; i < bufferLength; i++) {
                    sum += dataArray[i];
                }
                const average = sum / bufferLength;
                if (average > 30 && !this.isRecording) {
                    this.startRecording();
                } else if (average < 10 && this.isRecording) {
                    setTimeout(() => {
                        analyser.getByteFrequencyData(dataArray);
                        let newSum = 0;
                        for (let i = 0; i < bufferLength; i++) {
                            newSum += dataArray[i];
                        }
                        const newAverage = newSum / bufferLength;
                        if (newAverage < 10) {
                            this.stopRecording();
                        }
                    }, 500);
                }
                this.vadInterval = requestAnimationFrame(checkVoiceActivity); // 存储间隔 ID
            };
            requestAnimationFrame(checkVoiceActivity);
        },
        startRecording() {
            this.recorder = new Recorder(this.mediaStream);
            this.recorder.record();
            this.isRecording = true;
            console.log('开始录制');
        },
        stopRecording() {
            if (this.recorder && this.isRecording) {
                this.recorder.stopAndExport((blob) => {
                    const formData = new FormData();
                    formData.append('audioFile', blob, 'recorded-audio.opus');
                });
                this.isRecording = false;
                console.log('停止录制');
            }
        }
    }
};

Recorder.js

javascript 复制代码
class Recorder {
    constructor(stream) {
        const AudioContext = window.AudioContext || window.webkitAudioContext;

        try {
            this.audioContext = new AudioContext();
        } catch (error) {
            console.error('创建 AudioContext 失败:', error);
            throw new Error('无法创建音频上下文,录音功能无法使用');
        }

        this.stream = stream;
        this.mediaRecorder = new MediaRecorder(stream);
        this.audioChunks = [];

        this.mediaRecorder.addEventListener('dataavailable', (event) => {
            if (event.data.size > 0) {
                this.audioChunks.push(event.data);
            }
        });

        this.mediaRecorder.addEventListener('stop', () => {
            console.log('录音停止,开始导出音频');
        });
    }

    record() {
        try {
            this.mediaRecorder.start();
        } catch (error) {
            console.error('开始录音失败:', error);
            throw new Error('无法开始录音');
        }
    }

    stop() {
        try {
            this.mediaRecorder.stop();
        } catch (error) {
            console.error('停止录音失败:', error);
            throw new Error('无法停止录音');
        }
    }

    exportWAV(callback) {
        try {
            const blob = new Blob(this.audioChunks, { type: 'audio/wav' });
            console.log('生成的 Blob 的 MIME 类型:', blob.type);
            const reader = new FileReader();
            reader.readAsArrayBuffer(blob);
            reader.onloadend = () => {
                const arrayBuffer = reader.result;
            };
            callback(blob);
            this.audioChunks = [];
        } catch (error) {
            console.error('导出 WAV 格式失败:', error);
            throw new Error('无法导出 WAV 格式的音频');
        }
    }

    stopAndExport(callback) {
        this.mediaRecorder.addEventListener('stop', () => {
            this.exportWAV(callback);
        });
        this.stop();
    }
}

export default Recorder;

JAVA部分

VoiceInputServiceImpl.java

java 复制代码
package com.medical.asr.service.impl;

import com.medical.asr.service.VoiceInputService;
import com.medical.common.props.FileProps;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tool.utils.StringPool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

@Service
@Transactional(rollbackFor = Exception.class)
@Slf4j
public class VoiceInputServiceImpl implements VoiceInputService {

    @Autowired
    private FileProps fileProps;

    /**
     * 接收音频文件,并保存在目录下
     * @param audioFile 音频文件
     * @return 文件路径
     */
    private String receiveAudio(MultipartFile audioFile) {
        if (audioFile == null || audioFile.isEmpty()) {
            log.info("未收到音频文件");
            return StringPool.EMPTY;
        }
        try {
            //文件存放的地址
            String uploadDir = fileProps.getUploadPath();
            System.out.println(uploadDir);
            File dir = new File(uploadDir);
            if (!dir.exists()) {
                dir.mkdirs();
            }
            String fileName = System.currentTimeMillis() + "-" + audioFile.getOriginalFilename();
            Path filePath = Paths.get(uploadDir, fileName);
            Files.write(filePath, audioFile.getBytes());
            log.info("Received audio file: " + fileName);
            log.info("音频接收成功");
            return filePath.toString();
        } catch (IOException e) {
            log.error("保存音频文件时出错: " + e.getMessage());
            return StringPool.EMPTY;
        }
    }

}

但是出了一个问题,就是这样生成的音频文件,通过ffmpeg查看发现是存在问题的,用来听没问题,但是要做加工,就不是合适的WAV文件。

人家需要满足这样的条件:

而我们这边出来的音频文件是这样的:

sample_rate(采样率), bits_per_sample(每个采样点所使用的位数 / 位深度), codec_name(编解码器名称), codec_long_name(编解码器完整名称 / 详细描述)这几项都不满足 。于是查找opus转pcm的方案,修改之前的代码,新代码为:

java 复制代码
private String receiveAudio(MultipartFile audioFile) {
        if (audioFile == null || audioFile.isEmpty()) {
            log.info("未收到音频文件");
            return StringPool.EMPTY;
        }
        try {
            String uploadDir = fileProps.getUploadPath();
            System.out.println(uploadDir);
            File dir = new File(uploadDir);
            if (!dir.exists()) {
                dir.mkdirs();
            }
            String fileName = System.currentTimeMillis() + "-" + audioFile.getOriginalFilename();
            Path filePath = Paths.get(uploadDir, fileName);
            Files.write(filePath, audioFile.getBytes());
            log.info("Received audio file: " + fileName);
            log.info("音频接收成功");
            // 转换音频格式和采样率
            int dotIndex = fileName.lastIndexOf('.');
            if (dotIndex!= -1) {
                fileName = fileName.substring(0, dotIndex);
            }
            String outputPath = Paths.get(uploadDir, "converted_" + fileName + ".wav").toString();
            //新增的部分
            convertAudio(filePath.toString(), outputPath);
            return outputPath;
        } catch (IOException e) {
            log.error("保存音频文件时出错: " + e.getMessage());
            return StringPool.EMPTY;
        }
    }

    public static void convertAudio(String inputPath, String outputPath) {
        String ffmpegCommand = "ffmpeg -i " + inputPath + " -ar 16000 -ac 1 -acodec pcm_s16le " + outputPath;
        try {
            Process process = Runtime.getRuntime().exec(ffmpegCommand);

            // 读取进程的输出和错误流,以便及时发现问题
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = reader.readLine())!= null) {
                System.out.println(line);
            }

            reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            while ((line = reader.readLine())!= null) {
                System.out.println(line);
            }

            process.waitFor();
        } catch (IOException | InterruptedException e) {
            log.error("转换音频时出错: " + e.getMessage());
            e.printStackTrace();
        }
    }

这里面使用了ffmpeg的命令,如果当前环境没有ffmpeg,要记得先去下载安装ffmpeg,然后配置环境变量后再使用。

相关推荐
怕浪猫几秒前
React从入门到出门 第五章 React Router 配置与原理初探
前端·javascript·react.js
鱼跃鹰飞3 分钟前
经典面试题:K8S的自动缩扩容和崩溃恢复
java·容器·kubernetes
鹏多多7 分钟前
前端2025年终总结:借着AI做大做强再创辉煌
前端·javascript
Coder_Boy_8 分钟前
Spring Boot 事务回滚异常 UnexpectedRollbackException 详解(常见问题集合)
java·spring boot·后端
青云交9 分钟前
Java 大视界 -- 基于 Java+Redis Cluster 构建分布式缓存系统:实战与一致性保障(444)
java·redis·缓存·缓存穿透·分布式缓存·一致性保障·java+redis clus
哈__10 分钟前
React Native 鸿蒙跨平台开发:Vibration 实现鸿蒙端设备的震动反馈
javascript·react native·react.js
不知疲倦的仄仄10 分钟前
第五天:深度解密 Netty ByteBuf:高性能 IO 的基石
java·开源·github
WebGISer_白茶乌龙桃12 分钟前
Cesium实现“悬浮岛”式,三维立体的行政区划
javascript·vue.js·3d·web3·html5·webgl
xiaobaishuoAI13 分钟前
后端工程化实战指南:从规范到自动化,打造高效协作体系
java·大数据·运维·人工智能·maven·devops·geo
水中加点糖15 分钟前
RagFlow实现多模态搜索(文、图、视频)与(关键字/相似度)搜索原理(二)
python·ai·音视频·knn·ragflow·多模态搜索·相似度搜索