/** * Audio export utilities * Supports WAV export with various bit depths */ export interface ExportOptions { format: 'wav'; bitDepth: 16 | 24 | 32; sampleRate?: number; // If different from source, will resample normalize?: boolean; // Normalize to prevent clipping } /** * Convert an AudioBuffer to WAV file */ export function audioBufferToWav( audioBuffer: AudioBuffer, options: ExportOptions = { format: 'wav', bitDepth: 16 } ): ArrayBuffer { const { bitDepth, normalize } = options; const numberOfChannels = audioBuffer.numberOfChannels; const sampleRate = audioBuffer.sampleRate; const length = audioBuffer.length; // Get channel data const channels: Float32Array[] = []; for (let i = 0; i < numberOfChannels; i++) { channels.push(audioBuffer.getChannelData(i)); } // Find peak if normalizing let peak = 1.0; if (normalize) { peak = 0; for (const channel of channels) { for (let i = 0; i < channel.length; i++) { const abs = Math.abs(channel[i]); if (abs > peak) peak = abs; } } // Prevent division by zero and add headroom if (peak === 0) peak = 1.0; else peak = peak * 1.01; // 1% headroom } // Calculate sizes const bytesPerSample = bitDepth / 8; const blockAlign = numberOfChannels * bytesPerSample; const dataSize = length * blockAlign; const bufferSize = 44 + dataSize; // 44 bytes for WAV header // Create buffer const buffer = new ArrayBuffer(bufferSize); const view = new DataView(buffer); // Write WAV header let offset = 0; // RIFF chunk descriptor writeString(view, offset, 'RIFF'); offset += 4; view.setUint32(offset, bufferSize - 8, true); offset += 4; // File size - 8 writeString(view, offset, 'WAVE'); offset += 4; // fmt sub-chunk writeString(view, offset, 'fmt '); offset += 4; view.setUint32(offset, 16, true); offset += 4; // Subchunk size (16 for PCM) view.setUint16(offset, bitDepth === 32 ? 3 : 1, true); offset += 2; // Audio format (1 = PCM, 3 = IEEE float) view.setUint16(offset, numberOfChannels, true); offset += 2; view.setUint32(offset, sampleRate, true); offset += 4; view.setUint32(offset, sampleRate * blockAlign, true); offset += 4; // Byte rate view.setUint16(offset, blockAlign, true); offset += 2; view.setUint16(offset, bitDepth, true); offset += 2; // data sub-chunk writeString(view, offset, 'data'); offset += 4; view.setUint32(offset, dataSize, true); offset += 4; // Write interleaved audio data if (bitDepth === 16) { for (let i = 0; i < length; i++) { for (let channel = 0; channel < numberOfChannels; channel++) { const sample = Math.max(-1, Math.min(1, channels[channel][i] / peak)); view.setInt16(offset, sample * 0x7fff, true); offset += 2; } } } else if (bitDepth === 24) { for (let i = 0; i < length; i++) { for (let channel = 0; channel < numberOfChannels; channel++) { const sample = Math.max(-1, Math.min(1, channels[channel][i] / peak)); const int24 = Math.round(sample * 0x7fffff); view.setUint8(offset, int24 & 0xff); offset++; view.setUint8(offset, (int24 >> 8) & 0xff); offset++; view.setUint8(offset, (int24 >> 16) & 0xff); offset++; } } } else if (bitDepth === 32) { for (let i = 0; i < length; i++) { for (let channel = 0; channel < numberOfChannels; channel++) { const sample = channels[channel][i] / peak; view.setFloat32(offset, sample, true); offset += 4; } } } return buffer; } /** * Download an ArrayBuffer as a file */ export function downloadArrayBuffer( arrayBuffer: ArrayBuffer, filename: string, mimeType: string = 'audio/wav' ): void { const blob = new Blob([arrayBuffer], { type: mimeType }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } // Helper to write string to DataView function writeString(view: DataView, offset: number, string: string): void { for (let i = 0; i < string.length; i++) { view.setUint8(offset + i, string.charCodeAt(i)); } }