音频采集(VUE3+JAVA)

news/2025/2/21 13:17:25/

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


http://www.ppmy.cn/news/1573461.html

相关文章

Spring Security,servlet filter,和白名单之间的关系

首先&#xff0c;Servlet Filter是Java Web应用中的基础组件&#xff0c;用于拦截请求和响应&#xff0c;进行预处理和后处理。它们在处理HTTP请求时处于最外层&#xff0c;可以执行日志记录、身份验证、授权等操作。白名单机制通常指允许特定IP、用户或请求通过的安全策略&…

【ISO 14229-1:2023 UDS诊断(会话控制0x10服务)测试用例CAPL代码全解析②】

ISO 14229-1:2023 UDS诊断【会话控制0x10服务】_TestCase02 作者&#xff1a;车端域控测试工程师 更新日期&#xff1a;2025年02月15日 关键词&#xff1a;UDS诊断、0x10服务、诊断会话控制、ECU测试、ISO 14229-1:2023 TC10-002测试用例 用例ID测试场景验证要点参考条款预期…

推荐几款较好的开源成熟框架

一. 若依&#xff1a; 1. 官方网站&#xff1a;https://doc.ruoyi.vip/ruoyi/ 2. 若依SpringBootVueElement 的后台管理系统&#xff1a;https://gitee.com/y_project/RuoYi-Vue 3. 若依SpringBootVueElement 的后台管理系统&#xff1a;https://gitee.com/y_project/RuoYi-Cl…

动手学深度学习11.7. AdaGrad算法-笔记练习(PyTorch)

以下内容为结合李沐老师的课程和教材补充的学习笔记&#xff0c;以及对课后练习的一些思考&#xff0c;自留回顾&#xff0c;也供同学之人交流参考。 本节课程地址&#xff1a;72 优化算法【动手学深度学习v2】_哔哩哔哩_bilibili 本节教材地址&#xff1a;11.7. AdaGrad算法…

kafka为什么这么快?

前言 Kafka的高效有几个关键点&#xff0c;首先是顺序读写。磁盘的顺序访问速度其实很快&#xff0c;甚至比内存的随机访问还要快。Kafka在设计上利用了这一点&#xff0c;将消息顺序写入日志文件&#xff0c;这样减少了磁盘寻道的时间&#xff0c;提高了吞吐量。与传统数据库的…

Python图形界面(GUI)Tkinter笔记(二十四):Frame框架功能控件

Frame控件,也就是我们通常所说的框架,它是一个非常重要的容器控件。它本身并不直接参与与用户的交互过程,而是扮演着一个组织者和管理者的角色,主要负责组织和管理其他控件的布局。简单来说,它的工作原理就像是把原本的“根窗口”或者“父窗口”划分成几个独立的“子窗口”…

【HarmonyOS Next】图片选择方案

背景 封装一个选择图片和调用拍照相机的按钮&#xff0c;展示api13下选择图片和调用相机&#xff0c;可以使用不申请用户权限的方式&#xff0c;进行图片的选择和修改。但是&#xff0c;目前方案并未包含上传图片保存的功能&#xff0c;仅提供图片选择或者拍照后&#xff0c;图…

PostgreSQL认证指南

PostgreSQL 作为一款强大的开源关系型数据库&#xff0c;深受开发者和企业的青睐。获得 PostgreSQL 专家认证&#xff0c;不仅能提升个人在数据库领域的专业能力&#xff0c;还能为职业发展增添有力筹码。下面为大家详细介绍 PostgreSQL 专家认证的学习路径。 一、深入理解基础…