import { Article, AudioSegment } from '../types'; import { SAMPLE_RATE } from '../constants'; /** * Combines multiple audio segments into a single downloadable file. * Since browser-based MP3 encoding is complex, we export as WAV format * which is widely supported and maintains quality. */ export const exportArticleAudio = async ( article: Article, onProgress?: (progress: number) => void ): Promise => { const audioSegments = article.segments.filter(seg => seg.audioUrl); if (audioSegments.length === 0) { throw new Error('No audio segments available for export'); } // Fetch all audio data const audioBuffers: ArrayBuffer[] = []; let totalLength = 0; for (let i = 0; i < audioSegments.length; i++) { const segment = audioSegments[i]; if (!segment.audioUrl) continue; try { const response = await fetch(segment.audioUrl); const arrayBuffer = await response.arrayBuffer(); audioBuffers.push(arrayBuffer); // Parse WAV to get PCM data length (skip 44-byte header) totalLength += arrayBuffer.byteLength - 44; if (onProgress) { onProgress(Math.round(((i + 1) / audioSegments.length) * 100)); } } catch (e) { console.warn(`Failed to fetch segment ${i}:`, e); } } if (audioBuffers.length === 0) { throw new Error('Failed to fetch any audio segments'); } // Combine PCM data from all WAV files const combinedPcm = new Uint8Array(totalLength); let offset = 0; for (const buffer of audioBuffers) { // Skip WAV header (44 bytes) and copy PCM data const pcmData = new Uint8Array(buffer, 44); combinedPcm.set(pcmData, offset); offset += pcmData.length; } // Create new WAV file with combined data return createWavFile(combinedPcm, SAMPLE_RATE); }; /** * Creates a WAV file from PCM data */ const createWavFile = (pcmData: Uint8Array, sampleRate: number): Blob => { const numChannels = 1; const bitsPerSample = 16; const byteRate = sampleRate * numChannels * (bitsPerSample / 8); const blockAlign = numChannels * (bitsPerSample / 8); const dataSize = pcmData.length; const fileSize = 44 + dataSize; const buffer = new ArrayBuffer(fileSize); const view = new DataView(buffer); // RIFF header writeString(view, 0, 'RIFF'); view.setUint32(4, fileSize - 8, true); writeString(view, 8, 'WAVE'); // fmt chunk writeString(view, 12, 'fmt '); view.setUint32(16, 16, true); // chunk size view.setUint16(20, 1, true); // PCM format view.setUint16(22, numChannels, true); view.setUint32(24, sampleRate, true); view.setUint32(28, byteRate, true); view.setUint16(32, blockAlign, true); view.setUint16(34, bitsPerSample, true); // data chunk writeString(view, 36, 'data'); view.setUint32(40, dataSize, true); // PCM data const outputArray = new Uint8Array(buffer); outputArray.set(pcmData, 44); return new Blob([buffer], { type: 'audio/wav' }); }; const writeString = (view: DataView, offset: number, string: string) => { for (let i = 0; i < string.length; i++) { view.setUint8(offset + i, string.charCodeAt(i)); } }; /** * Triggers download of the audio file */ export const downloadAudio = (blob: Blob, filename: string): void => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename.endsWith('.wav') ? filename : `${filename}.wav`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; /** * Export and download article audio with progress tracking */ export const exportAndDownloadArticle = async ( article: Article, onProgress?: (progress: number) => void ): Promise => { const blob = await exportArticleAudio(article, onProgress); const safeTitle = article.title .replace(/[^a-zA-Z0-9\s]/g, '') .replace(/\s+/g, '_') .substring(0, 50); downloadAudio(blob, `${safeTitle}_newscaster.wav`); };