using Robovoice.Core; using Robovoice.Stt.File; using Robovoice.Tts.LibPiper; namespace Robovoice.App; internal sealed class Orchestrator : IAsyncDisposable { private readonly LibPiperTtsEngine _tts; private readonly AudioOutput _audioOutput; private readonly FileSttSource _sttSource; private readonly Action _log; private CancellationTokenSource? _currentCts; private bool _disposed; public string OutputDeviceName { get; set; } = string.Empty; public Orchestrator( LibPiperTtsEngine tts, AudioOutput audioOutput, FileSttSource sttSource, Action log) { _tts = tts; _audioOutput = audioOutput; _audioOutput.Log = log; _sttSource = sttSource; _log = log; _sttSource.TranscriptReceived += OnTranscript; } private void OnTranscript(object? sender, TranscriptEventArgs e) { if (e.Message.Type == TranscriptType.Final) { _ = SynthesizeAsync(e.Message.Text); } } public async Task InitializeTtsAsync() { _log("Initializing TTS engine..."); await _tts.InitializeAsync(); _log($"TTS ready (sample rate: {_tts.SampleRate} Hz)"); } public async Task SynthesizeAsync(string text) { _currentCts?.Cancel(); _currentCts = new CancellationTokenSource(); var ct = _currentCts.Token; var sw = System.Diagnostics.Stopwatch.StartNew(); _log($"FINAL: \"{text}\" ({text.Length} chars)"); try { bool started = false; int chunkCount = 0; int totalSamples = 0; await foreach (var chunk in _tts.SynthesizeAsync(text, ct)) { if (!started) { started = true; _audioOutput.Start(chunk.SampleRate, OutputDeviceName); _log($"TTS: first chunk ({sw.ElapsedMilliseconds}ms)"); } _audioOutput.WriteSamples(chunk.Samples); chunkCount++; totalSamples += chunk.Samples.Length; } if (!started) { _log("TTS: no audio produced"); } else { _audioOutput.Flush(); double durationSec = (double)totalSamples / _tts.SampleRate; _log($"TTS: done ({chunkCount} chunks, {durationSec:F2}s audio, {sw.ElapsedMilliseconds}ms)"); } } catch (OperationCanceledException) { _log("TTS: cancelled"); _audioOutput.Stop(); } catch (Exception ex) { _log($"TTS error: {ex.Message}"); } sw.Stop(); } public void TriggerNextLine() { _sttSource.EmitNext(); } public string? GetPendingLine() => _sttSource.GetPendingLine(); public int GetCurrentLineIndex() => _sttSource.CurrentIndex; public int GetLineCount() => _sttSource.LineCount; public async ValueTask DisposeAsync() { if (_disposed) return; _currentCts?.Cancel(); _currentCts?.Dispose(); _sttSource.TranscriptReceived -= OnTranscript; await _sttSource.DisposeAsync(); await _tts.DisposeAsync(); _audioOutput.Dispose(); _disposed = true; } }