flat: update
This commit is contained in:
348
hook/useRealtimeRecorder2.js
Normal file
348
hook/useRealtimeRecorder2.js
Normal file
@@ -0,0 +1,348 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user