import { AudioSegment, TextChunk, VoiceSettings, PlaybackStatus } from '../types/audio.js'; import { ElevenLabsClient } from './elevenlabs-client.js'; import { v4 as uuidv4 } from 'uuid'; export class AudioBufferManager { private segments: AudioSegment[] = []; private currentIndex: number = 0; private elevenLabsClient: ElevenLabsClient; private preloadCount: number = 3; // How many segments to preload private isPlaying: boolean = false; private isPaused: boolean = false; constructor(elevenLabsClient: ElevenLabsClient) { this.elevenLabsClient = elevenLabsClient; } async loadChunks(chunks: TextChunk[], voiceSettings: VoiceSettings): Promise { console.log(`🎡 Loading ${chunks.length} chunks into buffer manager`); // Initialize segments this.segments = chunks.map(chunk => ({ id: uuidv4(), chunk, status: 'pending' as const })); this.currentIndex = 0; this.isPlaying = false; this.isPaused = false; // Pre-load first few segments await this.preloadSegments(0, Math.min(this.preloadCount, chunks.length), voiceSettings); } private async preloadSegments( startIndex: number, count: number, voiceSettings: VoiceSettings ): Promise { const endIndex = Math.min(startIndex + count, this.segments.length); const toLoad = this.segments.slice(startIndex, endIndex) .filter(segment => segment.status === 'pending'); if (toLoad.length === 0) return; console.log(`⏳ Preloading segments ${startIndex} to ${endIndex - 1}`); // Load segments in parallel for faster processing const loadPromises = toLoad.map(async (segment) => { try { segment.status = 'loading'; const result = await this.elevenLabsClient.synthesizeText( segment.chunk.text, voiceSettings.voice, voiceSettings ); segment.audioBuffer = result.audioBuffer; if (result.duration !== undefined) { segment.duration = result.duration; } segment.status = 'ready'; segment.synthesizedAt = new Date(); console.log(`βœ… Segment ${segment.chunk.order} ready (${segment.duration?.toFixed(1)}s)`); } catch (error) { console.error(`❌ Failed to load segment ${segment.chunk.order}:`, error); segment.status = 'error'; } }); await Promise.allSettled(loadPromises); } async getNextSegment(voiceSettings: VoiceSettings): Promise { if (this.currentIndex >= this.segments.length) { console.log('🏁 Reached end of segments'); return null; } const segment = this.segments[this.currentIndex]; // Ensure current segment is ready if (segment && segment.status !== 'ready') { console.log(`⏳ Waiting for segment ${this.currentIndex} to be ready...`); await this.preloadSegments(this.currentIndex, 1, voiceSettings); } // Pre-load upcoming segments in background const upcomingIndex = this.currentIndex + 1; if (upcomingIndex < this.segments.length) { // Don't await - load in background this.preloadSegments(upcomingIndex, this.preloadCount, voiceSettings) .catch(error => console.error('Background preload error:', error)); } if (segment && segment.status === 'ready') { segment.status = 'playing'; this.isPlaying = true; return segment; } console.error(`❌ Segment ${this.currentIndex} is not ready:`, segment?.status); return null; } markSegmentPlayed(): void { if (this.currentIndex < this.segments.length) { const segment = this.segments[this.currentIndex]; if (segment) { segment.status = 'played'; } this.currentIndex++; } } pause(): void { this.isPaused = true; this.isPlaying = false; console.log('⏸️ Buffer manager paused'); } resume(): void { this.isPaused = false; console.log('▢️ Buffer manager resumed'); } stop(): void { this.isPlaying = false; this.isPaused = false; this.currentIndex = 0; // Reset all segments to ready (if they were loaded) this.segments.forEach(segment => { if (segment.status === 'playing' || segment.status === 'played') { segment.status = 'ready'; } }); console.log('⏹️ Buffer manager stopped and reset'); } goBack(): AudioSegment | null { if (this.currentIndex > 0) { this.currentIndex--; const segment = this.segments[this.currentIndex]; if (segment) { segment.status = 'ready'; // Reset to ready for replay console.log(`βͺ Rewound to segment ${this.currentIndex}`); return segment; } } return null; } skipForward(): AudioSegment | null { if (this.currentIndex < this.segments.length - 1) { // Mark current as played this.markSegmentPlayed(); const segment = this.segments[this.currentIndex]; if (segment) { console.log(`⏩ Skipped to segment ${this.currentIndex}`); return segment; } } return null; } seekToChunk(chunkIndex: number): AudioSegment | null { if (chunkIndex >= 0 && chunkIndex < this.segments.length) { this.currentIndex = chunkIndex; const segment = this.segments[this.currentIndex]; if (segment) { segment.status = 'ready'; // Reset for replay console.log(`🎯 Seeked to segment ${chunkIndex}`); return segment; } } return null; } replayCurrentChunk(): AudioSegment | null { const segment = this.segments[this.currentIndex]; if (segment) { segment.status = 'ready'; // Reset for replay console.log(`πŸ”„ Replaying current segment ${this.currentIndex}`); return segment; } return null; } getPlaybackProgress(): { currentChunk: number; totalChunks: number; completionPercentage: number; readySegments: number; isPlaying: boolean; isPaused: boolean; } { const readyCount = this.segments.filter(s => s.status === 'ready' || s.status === 'played').length; return { currentChunk: this.currentIndex, totalChunks: this.segments.length, completionPercentage: this.segments.length > 0 ? (this.currentIndex / this.segments.length) * 100 : 0, readySegments: readyCount, isPlaying: this.isPlaying, isPaused: this.isPaused }; } getSegmentsSummary(): Array<{ id: string; order: number; text: string; status: string; duration?: number; isCurrent: boolean; }> { return this.segments.map(segment => { const summary: { id: string; order: number; text: string; status: string; duration?: number; isCurrent: boolean; } = { id: segment.id, order: segment.chunk.order, text: segment.chunk.text.substring(0, 100) + (segment.chunk.text.length > 100 ? '...' : ''), status: segment.status, isCurrent: segment.chunk.order === this.currentIndex }; if (segment.duration !== undefined) { summary.duration = segment.duration; } return summary; }); } getCurrentSegment(): AudioSegment | null { return this.segments[this.currentIndex] || null; } hasMoreSegments(): boolean { return this.currentIndex < this.segments.length; } getTotalDuration(): number { return this.segments.reduce((total, segment) => total + (segment.duration || 0), 0); } getEstimatedTimeRemaining(): number { const remainingSegments = this.segments.slice(this.currentIndex); return remainingSegments.reduce((total, segment) => total + (segment.duration || 0), 0); } // Cleanup method to free memory cleanup(): void { this.segments.forEach(segment => { delete segment.audioBuffer; }); this.segments = []; this.currentIndex = 0; this.isPlaying = false; this.isPaused = false; console.log('🧹 Buffer manager cleaned up'); } }