fix: 前端录音改用WAV格式(PCM 16kHz),兼容模型端
This commit is contained in:
@@ -304,6 +304,9 @@
|
||||
let audioChunks = [];
|
||||
let conversationId = null;
|
||||
let audioContext = null;
|
||||
let audioStream = null;
|
||||
let scriptProcessor = null;
|
||||
let recordedBuffers = [];
|
||||
|
||||
// 元素
|
||||
const recordBtn = document.getElementById('recordBtn');
|
||||
@@ -333,10 +336,58 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 创建 WAV 文件
|
||||
function createWavFile(audioBuffer, sampleRate = 16000) {
|
||||
const numChannels = 1;
|
||||
const bitsPerSample = 16;
|
||||
const bytesPerSample = bitsPerSample / 8;
|
||||
const blockAlign = numChannels * bytesPerSample;
|
||||
const byteRate = sampleRate * blockAlign;
|
||||
const dataSize = audioBuffer.length * bytesPerSample;
|
||||
const headerSize = 44;
|
||||
const totalSize = headerSize + dataSize;
|
||||
|
||||
const buffer = new ArrayBuffer(totalSize);
|
||||
const view = new DataView(buffer);
|
||||
|
||||
// WAV header
|
||||
writeString(view, 0, 'RIFF');
|
||||
view.setUint32(4, totalSize - 8, true);
|
||||
writeString(view, 8, 'WAVE');
|
||||
writeString(view, 12, 'fmt ');
|
||||
view.setUint32(16, 16, true); // fmt chunk size
|
||||
view.setUint16(20, 1, true); // audio format (PCM)
|
||||
view.setUint16(22, numChannels, true);
|
||||
view.setUint32(24, sampleRate, true);
|
||||
view.setUint32(28, byteRate, true);
|
||||
view.setUint16(32, blockAlign, true);
|
||||
view.setUint16(34, bitsPerSample, true);
|
||||
writeString(view, 36, 'data');
|
||||
view.setUint32(40, dataSize, true);
|
||||
|
||||
// 写入音频数据
|
||||
floatTo16BitPCM(view, 44, audioBuffer);
|
||||
|
||||
return new Blob([buffer], { type: 'audio/wav' });
|
||||
}
|
||||
|
||||
function writeString(view, offset, string) {
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
view.setUint8(offset + i, string.charCodeAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
function floatTo16BitPCM(view, offset, input) {
|
||||
for (let i = 0; i < input.length; i++, offset += 2) {
|
||||
const s = Math.max(-1, Math.min(1, input[i]));
|
||||
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化录音
|
||||
async function initAudio() {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audioStream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
@@ -344,22 +395,21 @@
|
||||
}
|
||||
});
|
||||
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
|
||||
// 创建 MediaRecorder
|
||||
mediaRecorder = new MediaRecorder(stream, {
|
||||
mimeType: 'audio/webm'
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
||||
sampleRate: 16000
|
||||
});
|
||||
|
||||
mediaRecorder.ondataavailable = (e) => {
|
||||
audioChunks.push(e.data);
|
||||
const source = audioContext.createMediaStreamSource(audioStream);
|
||||
scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
|
||||
|
||||
scriptProcessor.onaudioprocess = (e) => {
|
||||
if (isRecording) {
|
||||
recordedBuffers.push(e.inputBuffer.getChannelData(0).slice());
|
||||
}
|
||||
};
|
||||
|
||||
mediaRecorder.onstop = async () => {
|
||||
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
||||
audioChunks = [];
|
||||
await sendAudio(audioBlob);
|
||||
};
|
||||
source.connect(scriptProcessor);
|
||||
scriptProcessor.connect(audioContext.destination);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -371,13 +421,12 @@
|
||||
|
||||
// 开始录音
|
||||
async function startRecording() {
|
||||
if (!mediaRecorder) {
|
||||
if (!audioContext) {
|
||||
const success = await initAudio();
|
||||
if (!success) return;
|
||||
}
|
||||
|
||||
audioChunks = [];
|
||||
mediaRecorder.start();
|
||||
recordedBuffers = [];
|
||||
isRecording = true;
|
||||
|
||||
recordBtn.classList.add('recording');
|
||||
@@ -390,16 +439,29 @@
|
||||
|
||||
// 停止录音
|
||||
function stopRecording() {
|
||||
if (mediaRecorder && isRecording) {
|
||||
mediaRecorder.stop();
|
||||
if (isRecording) {
|
||||
isRecording = false;
|
||||
|
||||
// 合并所有缓冲区
|
||||
const totalLength = recordedBuffers.reduce((acc, buf) => acc + buf.length, 0);
|
||||
const mergedBuffer = new Float32Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const buf of recordedBuffers) {
|
||||
mergedBuffer.set(buf, offset);
|
||||
offset += buf.length;
|
||||
}
|
||||
|
||||
// 创建 WAV 文件
|
||||
const wavBlob = createWavFile(mergedBuffer, 16000);
|
||||
|
||||
recordBtn.classList.remove('recording');
|
||||
recordBtn.querySelector('.icon').textContent = '🎤';
|
||||
recordBtn.querySelector('.text').textContent = '点击录音';
|
||||
recordStatus.textContent = '处理中...';
|
||||
recordStatus.classList.remove('recording');
|
||||
waveform.style.display = 'none';
|
||||
|
||||
sendAudio(wavBlob);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,7 +471,7 @@
|
||||
showLoading();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('audio', audioBlob, 'recording.webm');
|
||||
formData.append('audio', audioBlob, 'recording.wav');
|
||||
if (conversationId) {
|
||||
formData.append('conversation_id', conversationId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user