Files
makelore/src/lib/voice-recording.ts

91 lines
3.2 KiB
TypeScript

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<string> {
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<Blob> {
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());
}