Files
memind/src/voice/audioWav.ts
John 6ee6fd64dd Add MindSpace page live edit, chat skills, and H5 deploy tooling.
Introduce page edit sessions with draft preview and patch API, chat skill picker, user memory profile, h5ApiBase resolution, voice WAV transport, and scripts for 105/g2 deployment and Plaza local dev.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 22:09:38 -07:00

91 lines
2.7 KiB
TypeScript

const TARGET_SAMPLE_RATE = 16_000;
function mixToMono(buffer: AudioBuffer): AudioBuffer {
if (buffer.numberOfChannels === 1) return buffer;
const mono = new AudioBuffer({
length: buffer.length,
numberOfChannels: 1,
sampleRate: buffer.sampleRate,
});
const out = mono.getChannelData(0);
const channels = buffer.numberOfChannels;
for (let ch = 0; ch < channels; ch += 1) {
const data = buffer.getChannelData(ch);
for (let i = 0; i < buffer.length; i += 1) {
out[i] += data[i] / channels;
}
}
return mono;
}
function encodeWav(audioBuffer: AudioBuffer): Blob {
const channel = audioBuffer.getChannelData(0);
const dataSize = channel.length * 2;
const buffer = new ArrayBuffer(44 + dataSize);
const view = new DataView(buffer);
const writeString = (offset: number, value: string) => {
for (let i = 0; i < value.length; i += 1) {
view.setUint8(offset + i, value.charCodeAt(i));
}
};
writeString(0, 'RIFF');
view.setUint32(4, 36 + dataSize, true);
writeString(8, 'WAVE');
writeString(12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, 1, true);
view.setUint32(24, audioBuffer.sampleRate, true);
view.setUint32(28, audioBuffer.sampleRate * 2, true);
view.setUint16(32, 2, true);
view.setUint16(34, 16, true);
writeString(36, 'data');
view.setUint32(40, dataSize, true);
let offset = 44;
for (let i = 0; i < channel.length; i += 1) {
const sample = Math.max(-1, Math.min(1, channel[i] ?? 0));
view.setInt16(offset, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true);
offset += 2;
}
return new Blob([buffer], { type: 'audio/wav' });
}
/** 将浏览器录音转为 16kHz 单声道 WAV,提升 ASR 解码成功率。失败时返回原 blob。 */
export async function blobToWav16kMono(blob: Blob): Promise<Blob> {
if (!blob.size) return blob;
const audioContext = new AudioContext();
try {
const arrayBuffer = await blob.arrayBuffer();
let decoded: AudioBuffer;
try {
decoded = await audioContext.decodeAudioData(arrayBuffer.slice(0));
} catch {
return blob;
}
const mono = mixToMono(decoded);
const duration = mono.duration;
if (!Number.isFinite(duration) || duration <= 0) return blob;
const offline = new OfflineAudioContext(
1,
Math.max(1, Math.ceil(duration * TARGET_SAMPLE_RATE)),
TARGET_SAMPLE_RATE,
);
const source = offline.createBufferSource();
source.buffer = mono;
source.connect(offline.destination);
source.start(0);
const rendered = await offline.startRendering();
if (!rendered.length) return blob;
return encodeWav(rendered);
} finally {
await audioContext.close().catch(() => undefined);
}
}