Files
ks-app-employment-service/hook/useRecorder.js

129 lines
3.8 KiB
JavaScript
Raw Normal View History

2025-03-28 15:19:42 +08:00
// composables/useRealtimeRecorder.js
import {
ref
} from 'vue'
export function useRealtimeRecorder(wsUrl) {
const isRecording = ref(false)
const recognizedText = ref('')
let audioContext = null
let audioWorkletNode = null
let sourceNode = null
let socket = null
const startRecording = async () => {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true
})
audioContext = new(window.AudioContext || window.webkitAudioContext)()
const processorCode = `
class RecorderProcessor extends AudioWorkletProcessor {
constructor() {
super()
this.buffer = []
this.inputSampleRate = sampleRate
this.targetSampleRate = 16000
}
process(inputs) {
const input = inputs[0][0]
if (!input) return true
this.buffer.push(...input)
const requiredSamples = this.inputSampleRate / 10 // 100ms
if (this.buffer.length >= requiredSamples) {
const resampled = this.downsample(this.buffer, this.inputSampleRate, this.targetSampleRate)
const int16Buffer = this.floatTo16BitPCM(resampled)
this.port.postMessage(int16Buffer)
this.buffer = []
}
return true
}
downsample(buffer, inRate, outRate) {
if (outRate === inRate) return buffer
const ratio = inRate / outRate
const len = Math.floor(buffer.length / ratio)
const result = new Float32Array(len)
for (let i = 0; i < len; i++) {
const start = Math.floor(i * ratio)
const end = Math.floor((i + 1) * ratio)
let sum = 0
for (let j = start; j < end && j < buffer.length; j++) sum += buffer[j]
result[i] = sum / (end - start)
}
return result
}
floatTo16BitPCM(input) {
const output = new Int16Array(input.length)
for (let i = 0; i < input.length; i++) {
const s = Math.max(-1, Math.min(1, input[i]))
output[i] = s < 0 ? s * 0x8000 : s * 0x7FFF
}
return output.buffer
}
}
registerProcessor('recorder-processor', RecorderProcessor)
`
const blob = new Blob([processorCode], {
type: 'application/javascript'
})
const blobUrl = URL.createObjectURL(blob)
await audioContext.audioWorklet.addModule(blobUrl)
socket = new WebSocket(wsUrl)
socket.onmessage = (e) => {
recognizedText.value = e.data
}
sourceNode = audioContext.createMediaStreamSource(stream)
audioWorkletNode = new AudioWorkletNode(audioContext, 'recorder-processor')
audioWorkletNode.port.onmessage = (e) => {
const audioData = e.data
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(audioData)
}
}
sourceNode.connect(audioWorkletNode)
audioWorkletNode.connect(audioContext.destination)
isRecording.value = true
}
const stopRecording = () => {
sourceNode?.disconnect()
audioWorkletNode?.disconnect()
audioContext?.close()
if (socket?.readyState === WebSocket.OPEN) {
socket.send('[end]')
socket.close()
}
audioContext = null
sourceNode = null
audioWorkletNode = null
socket = null
isRecording.value = false
}
const cancelRecording = () => {
stopRecording()
recognizedText.value = ''
}
return {
isRecording,
recognizedText,
startRecording,
stopRecording,
cancelRecording
}
}