chore(integration): snapshot local main worktree
This commit is contained in:
1 parent
48a9189939
commit
33fb31fb28
116 files changed
+8108
-3026
No files matched your search
@@ -0,0 +1,90 @@
|
||||
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());
|
||||
}
|
||||
Reference in new issue
Block a user