const VOICE_WAV_SAMPLE_RATE = 16_000; const VOICE_WAV_CHANNELS = 1; const VOICE_WAV_BITS_PER_SAMPLE = 16; type OfflineAudioContextConstructor = new ( numberOfChannels: number, length: number, sampleRate: number, ) => OfflineAudioContext; type AudioContextWindow = Window & { webkitAudioContext?: typeof AudioContext; webkitOfflineAudioContext?: OfflineAudioContextConstructor; }; export async function blobToBase64(blob: Blob): Promise { const bytes = new Uint8Array(await blob.arrayBuffer()); let binary = ''; const chunkSize = 0x8000; for (let index = 0; index < bytes.length; index += chunkSize) { binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize)); } return btoa(binary); } function writeAscii(view: DataView, offset: number, value: string): void { for (let index = 0; index < value.length; index += 1) { view.setUint8(offset + index, value.charCodeAt(index)); } } function audioBufferTo16BitPcmWav(audioBuffer: AudioBuffer): Blob { const samples = audioBuffer.getChannelData(0); const bytesPerSample = VOICE_WAV_BITS_PER_SAMPLE / 8; const blockAlign = VOICE_WAV_CHANNELS * bytesPerSample; const dataSize = samples.length * blockAlign; const wavBuffer = new ArrayBuffer(44 + dataSize); const view = new DataView(wavBuffer); writeAscii(view, 0, 'RIFF'); view.setUint32(4, 36 + dataSize, true); writeAscii(view, 8, 'WAVE'); writeAscii(view, 12, 'fmt '); view.setUint32(16, 16, true); view.setUint16(20, 1, true); view.setUint16(22, VOICE_WAV_CHANNELS, true); view.setUint32(24, VOICE_WAV_SAMPLE_RATE, true); view.setUint32(28, VOICE_WAV_SAMPLE_RATE * blockAlign, true); view.setUint16(32, blockAlign, true); view.setUint16(34, VOICE_WAV_BITS_PER_SAMPLE, true); writeAscii(view, 36, 'data'); view.setUint32(40, dataSize, true); let offset = 44; for (const sample of samples) { const clamped = Math.max(-1, Math.min(1, sample)); view.setInt16(offset, clamped < 0 ? clamped * 0x8000 : clamped * 0x7fff, true); offset += bytesPerSample; } return new Blob([new Uint8Array(wavBuffer)], { type: 'audio/wav' }); } export async function convertAudioBlobTo16kMonoWav(audioBlob: Blob): Promise { const audioWindow = window as AudioContextWindow; const AudioContextCtor = window.AudioContext ?? audioWindow.webkitAudioContext; const OfflineAudioContextCtor = window.OfflineAudioContext ?? audioWindow.webkitOfflineAudioContext; if (!AudioContextCtor || !OfflineAudioContextCtor) { throw new Error('当前环境无法转换录音'); } const audioContext = new AudioContextCtor(); let decodedBuffer: AudioBuffer; try { decodedBuffer = await audioContext.decodeAudioData(await audioBlob.arrayBuffer()); } finally { await audioContext.close().catch(() => undefined); } const frameCount = Math.max(1, Math.ceil(decodedBuffer.duration * VOICE_WAV_SAMPLE_RATE)); const offlineContext = new OfflineAudioContextCtor( VOICE_WAV_CHANNELS, frameCount, VOICE_WAV_SAMPLE_RATE, ); const source = offlineContext.createBufferSource(); source.buffer = decodedBuffer; source.connect(offlineContext.destination); source.start(0); return audioBufferTo16BitPcmWav(await offlineContext.startRendering()); }