348 lines
9.7 KiB
JavaScript
348 lines
9.7 KiB
JavaScript
import {
|
|
ref,
|
|
onUnmounted
|
|
} from 'vue'
|
|
import {
|
|
$api
|
|
} from '../common/globalFunction'; // 你的请求封装
|
|
import config from '@/config'
|
|
|
|
// 开源
|
|
export function useAudioRecorder() {
|
|
// --- 状态定义 ---
|
|
const isRecording = ref(false)
|
|
const isSocketConnected = ref(false)
|
|
const recordingDuration = ref(0)
|
|
const volumeLevel = ref(0) // 0-100
|
|
const recognizedText = ref('')
|
|
|
|
// --- 内部变量 ---
|
|
let socketTask = null
|
|
let durationTimer = null
|
|
|
|
// --- APP/小程序 变量 ---
|
|
let recorderManager = null;
|
|
|
|
// --- H5 变量 ---
|
|
let audioContext = null;
|
|
let scriptProcessor = null;
|
|
let mediaStreamSource = null;
|
|
let h5Stream = null;
|
|
|
|
// --- 配置项 ---
|
|
const RECORD_CONFIG = {
|
|
duration: 600000,
|
|
sampleRate: 16000,
|
|
numberOfChannels: 1,
|
|
format: 'pcm',
|
|
frameSize: 4096
|
|
}
|
|
|
|
/**
|
|
* 获取 WebSocket 地址 (含 Token)
|
|
*/
|
|
const getWsUrl = async () => {
|
|
let wsUrl = config.vioceBaseURl
|
|
|
|
// 拼接 Token
|
|
const token = uni.getStorageSync('token') || '';
|
|
if (token) {
|
|
const separator = wsUrl.includes('?') ? '&' : '?';
|
|
wsUrl = `${wsUrl}${separator}token=${encodeURIComponent(token)}`;
|
|
}
|
|
return wsUrl;
|
|
}
|
|
|
|
/**
|
|
* 开始录音 (入口)
|
|
*/
|
|
const startRecording = async () => {
|
|
if (isRecording.value) return
|
|
|
|
try {
|
|
recognizedText.value = ''
|
|
volumeLevel.value = 0
|
|
|
|
// #ifdef H5
|
|
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
|
|
uni.showToast({
|
|
title: 'H5录音需要HTTPS环境',
|
|
icon: 'none'
|
|
});
|
|
return;
|
|
}
|
|
// #endif
|
|
|
|
const url = await getWsUrl()
|
|
console.log('正在连接 ASR:', url)
|
|
|
|
await connectSocket(url);
|
|
|
|
} catch (err) {
|
|
console.error('启动失败:', err);
|
|
uni.showToast({
|
|
title: '启动失败: ' + (err.message || ''),
|
|
icon: 'none'
|
|
});
|
|
cleanup();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 连接 WebSocket
|
|
*/
|
|
const connectSocket = (url) => {
|
|
return new Promise((resolve, reject) => {
|
|
socketTask = uni.connectSocket({
|
|
url: url,
|
|
success: () => console.log('Socket 连接请求发送'),
|
|
fail: (err) => reject(err)
|
|
});
|
|
|
|
socketTask.onOpen((res) => {
|
|
console.log('WebSocket 已连接');
|
|
isSocketConnected.value = true;
|
|
|
|
// #ifdef H5
|
|
startH5Recording().then(() => resolve()).catch(err => {
|
|
socketTask.close();
|
|
reject(err);
|
|
});
|
|
// #endif
|
|
|
|
// #ifndef H5
|
|
startAppRecording();
|
|
resolve();
|
|
// #endif
|
|
});
|
|
|
|
socketTask.onMessage((res) => {
|
|
// 接收文本结果
|
|
if (res.data) {
|
|
recognizedText.value = res.data;
|
|
}
|
|
});
|
|
|
|
socketTask.onError((err) => {
|
|
console.error('Socket 错误:', err);
|
|
isSocketConnected.value = false;
|
|
stopRecording();
|
|
});
|
|
|
|
socketTask.onClose(() => {
|
|
isSocketConnected.value = false;
|
|
console.log('Socket 已关闭');
|
|
});
|
|
})
|
|
}
|
|
|
|
const startH5Recording = async () => {
|
|
try {
|
|
// 1. 获取麦克风流
|
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
audio: true
|
|
});
|
|
h5Stream = stream;
|
|
|
|
// 2. 创建 AudioContext
|
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
audioContext = new AudioContext({
|
|
sampleRate: 16000
|
|
});
|
|
|
|
mediaStreamSource = audioContext.createMediaStreamSource(stream);
|
|
scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
|
|
|
|
scriptProcessor.onaudioprocess = (event) => {
|
|
if (!isSocketConnected.value || !socketTask) return;
|
|
|
|
const inputData = event.inputBuffer.getChannelData(0);
|
|
|
|
calculateVolume(inputData, true);
|
|
|
|
const buffer = new ArrayBuffer(inputData.length * 2);
|
|
const view = new DataView(buffer);
|
|
for (let i = 0; i < inputData.length; i++) {
|
|
let s = Math.max(-1, Math.min(1, inputData[i]));
|
|
view.setInt16(i * 2, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
|
}
|
|
|
|
socketTask.send({
|
|
data: buffer,
|
|
fail: (e) => console.error('发送音频失败', e)
|
|
});
|
|
};
|
|
|
|
mediaStreamSource.connect(scriptProcessor);
|
|
scriptProcessor.connect(audioContext.destination);
|
|
|
|
isRecording.value = true;
|
|
recordingDuration.value = 0;
|
|
durationTimer = setInterval(() => recordingDuration.value++, 1000);
|
|
|
|
console.log('H5 录音已启动');
|
|
|
|
} catch (err) {
|
|
console.error('H5 录音启动失败:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
const stopH5Resources = () => {
|
|
if (scriptProcessor) scriptProcessor.disconnect();
|
|
if (mediaStreamSource) mediaStreamSource.disconnect();
|
|
if (audioContext) audioContext.close();
|
|
if (h5Stream) h5Stream.getTracks().forEach(track => track.stop());
|
|
|
|
scriptProcessor = null;
|
|
mediaStreamSource = null;
|
|
audioContext = null;
|
|
h5Stream = null;
|
|
}
|
|
|
|
const startAppRecording = () => {
|
|
recorderManager = uni.getRecorderManager();
|
|
|
|
recorderManager.onFrameRecorded((res) => {
|
|
const {
|
|
frameBuffer
|
|
} = res;
|
|
|
|
calculateVolume(frameBuffer, false);
|
|
|
|
if (isSocketConnected.value && socketTask) {
|
|
socketTask.send({
|
|
data: frameBuffer
|
|
});
|
|
}
|
|
});
|
|
|
|
recorderManager.onStart(() => {
|
|
console.log('APP 录音已开始');
|
|
isRecording.value = true;
|
|
recordingDuration.value = 0;
|
|
durationTimer = setInterval(() => recordingDuration.value++, 1000);
|
|
});
|
|
|
|
recorderManager.onError((err) => {
|
|
console.error('APP 录音报错:', err);
|
|
cleanup();
|
|
});
|
|
|
|
recorderManager.start(RECORD_CONFIG);
|
|
}
|
|
const stopHardwareResource = () => {
|
|
// APP/小程序停止
|
|
if (recorderManager) {
|
|
recorderManager.stop();
|
|
}
|
|
|
|
// H5停止
|
|
// #ifdef H5
|
|
if (scriptProcessor) scriptProcessor.disconnect();
|
|
if (mediaStreamSource) mediaStreamSource.disconnect();
|
|
if (audioContext) audioContext.close();
|
|
if (h5Stream) h5Stream.getTracks().forEach(track => track.stop());
|
|
|
|
scriptProcessor = null;
|
|
mediaStreamSource = null;
|
|
audioContext = null;
|
|
h5Stream = null;
|
|
// #endif
|
|
}
|
|
|
|
/**
|
|
* 停止录音 (通用)
|
|
*/
|
|
const stopRecording = () => {
|
|
// 停止 APP 录音
|
|
if (recorderManager) {
|
|
recorderManager.stop();
|
|
}
|
|
|
|
// 停止 H5 录音资源
|
|
// #ifdef H5
|
|
stopH5Resources();
|
|
// #endif
|
|
|
|
// 关闭 Socket
|
|
if (socketTask) {
|
|
socketTask.close();
|
|
}
|
|
|
|
cleanup();
|
|
}
|
|
|
|
const cancelRecording = () => {
|
|
if (!isRecording.value) return;
|
|
|
|
console.log('取消录音 - 丢弃结果');
|
|
|
|
// 1. 停止硬件录音
|
|
stopHardwareResource();
|
|
|
|
// 2. 强制关闭 Socket
|
|
if (socketTask) {
|
|
socketTask.close();
|
|
}
|
|
|
|
// 3. 关键:清空已识别的文本
|
|
recognizedText.value = '';
|
|
|
|
// 4. 清理资源
|
|
cleanup();
|
|
}
|
|
|
|
/**
|
|
* 清理状态
|
|
*/
|
|
const cleanup = () => {
|
|
clearInterval(durationTimer);
|
|
isRecording.value = false;
|
|
isSocketConnected.value = false;
|
|
socketTask = null;
|
|
recorderManager = null;
|
|
volumeLevel.value = 0;
|
|
}
|
|
|
|
/**
|
|
* 计算音量 (兼容 Float32 和 Int16/ArrayBuffer)
|
|
*/
|
|
const calculateVolume = (data, isFloat32) => {
|
|
let sum = 0;
|
|
let length = 0;
|
|
|
|
if (isFloat32) {
|
|
length = data.length;
|
|
for (let i = 0; i < length; i += 10) {
|
|
sum += Math.abs(data[i]);
|
|
}
|
|
volumeLevel.value = Math.min(100, Math.floor((sum / (length / 10)) * 100 * 3));
|
|
} else {
|
|
const int16Data = new Int16Array(data);
|
|
length = int16Data.length;
|
|
for (let i = 0; i < length; i += 10) {
|
|
sum += Math.abs(int16Data[i]);
|
|
}
|
|
const avg = sum / (length / 10);
|
|
volumeLevel.value = Math.min(100, Math.floor((avg / 10000) * 100));
|
|
}
|
|
}
|
|
|
|
onUnmounted(() => {
|
|
if (isRecording.value) {
|
|
stopRecording();
|
|
}
|
|
})
|
|
|
|
return {
|
|
isRecording,
|
|
isSocketConnected,
|
|
recordingDuration,
|
|
volumeLevel,
|
|
recognizedText,
|
|
startRecording,
|
|
stopRecording,
|
|
cancelRecording
|
|
}
|
|
} |