129 lines
3.8 KiB
JavaScript
129 lines
3.8 KiB
JavaScript
// 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
|
|
}
|
|
} |