91 lines
3.2 KiB
TypeScript
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());
|
|
}
|