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 {@Autowiredprivate 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,然后配置环境变量后再使用。