import { ALL_FORMATS, BlobSource, BufferTarget, canEncodeAudio, Conversion, Input, Mp3OutputFormat, Output, Quality, } from "mediabunny"; import { registerMp3Encoder } from "@mediabunny/mp3-encoder"; type DecodedAudio = { sampleRate: number; channelCount: number; channelData: Float32Array[]; }; export async function decodeAudio(file: File): Promise { const AudioContextCtor = window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext; if (!AudioContextCtor) throw new Error("This browser does not support local audio decoding."); const context = new AudioContextCtor(); try { const data = await file.arrayBuffer(); return await context.decodeAudioData(data.slice(0)); } finally { await context.close(); } } function clamp16(value: number) { const sample = Math.max(-1, Math.min(1, value)); return sample < 0 ? Math.round(sample * 0x8000) : Math.round(sample * 0x7fff); } function writeAscii(view: DataView, offset: number, value: string) { for (let index = 0; index < value.length; index += 1) { view.setUint8(offset + index, value.charCodeAt(index)); } } function makeWavBlob(audio: DecodedAudio) { const channels = Math.min(audio.channelCount, 2); const frameCount = audio.channelData[0]?.length || 0; const bytesPerSample = 2; const blockAlign = channels * bytesPerSample; const dataSize = frameCount * blockAlign; const buffer = new ArrayBuffer(44 + dataSize); const view = new DataView(buffer); 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, channels, true); view.setUint32(24, audio.sampleRate, true); view.setUint32(28, audio.sampleRate * blockAlign, true); view.setUint16(32, blockAlign, true); view.setUint16(34, 16, true); writeAscii(view, 36, "data"); view.setUint32(40, dataSize, true); let offset = 44; for (let frame = 0; frame < frameCount; frame += 1) { for (let channel = 0; channel < channels; channel += 1) { view.setInt16(offset, clamp16(audio.channelData[channel][frame] || 0), true); offset += 2; } } return new Blob([buffer], { type: "audio/wav" }); } export async function createRecognitionSnippet(file: File) { const decoded = await decodeAudio(file); const duration = decoded.duration; const snippetSeconds = Math.min(12, Math.max(4, duration)); const preferredStart = duration > 35 ? 20 : duration > 20 ? 8 : 0; const startSeconds = Math.min(preferredStart, Math.max(0, duration - snippetSeconds)); const startFrame = Math.floor(startSeconds * decoded.sampleRate); const snippetFrames = Math.min( Math.floor(snippetSeconds * decoded.sampleRate), Math.max(0, decoded.length - startFrame), ); const channels = Math.min(decoded.numberOfChannels, 2); const channelData: Float32Array[] = []; for (let channel = 0; channel < channels; channel += 1) { const source = decoded.getChannelData(channel); channelData.push(source.slice(startFrame, startFrame + snippetFrames)); } const blob = makeWavBlob({ sampleRate: decoded.sampleRate, channelCount: channels, channelData, }); return new File([blob], "recognition-snippet.wav", { type: "audio/wav", lastModified: Date.now(), }); } export async function convertAudioFileToMp3(file: File, bitrateKbps = 192) { if (file.type === "audio/mpeg" || file.name.toLowerCase().endsWith(".mp3")) { return new Blob([await file.arrayBuffer()], { type: "audio/mpeg" }); } if (!(await canEncodeAudio("mp3"))) { registerMp3Encoder(); } const input = new Input({ source: new BlobSource(file), formats: ALL_FORMATS, }); const target = new BufferTarget(); const output = new Output({ format: new Mp3OutputFormat(), target, }); const conversion = await Conversion.init({ input, output, tracks: "primary", video: { discard: true }, audio: { codec: "mp3", quality: new Quality({ bitrate: bitrateKbps * 1000 }), forceTranscode: true, }, }); if (!conversion.isValid) { const reasons = conversion.discardedTracks.map((entry) => entry.reason).join(", "); throw new Error( reasons ? `This audio file cannot be converted in this browser (${reasons}).` : "This audio file cannot be converted in this browser.", ); } await conversion.execute(); if (!target.buffer) throw new Error("MP3 conversion produced no output."); return new Blob([target.buffer], { type: "audio/mpeg" }); }