Files
Robovoice/Robovoice.App/Orchestrator.cs
T
mute 8585972a1e v0.1: Robovoice — PTT re-voicing app
WinForms tray app that captures text (from file or direct input) and
synthesizes speech via libpiper (Piper TTS), outputting to any audio
device (VB-CABLE for virtual mic routing).

Features:
- Global PTT hotkey (configurable F1-F12) via WH_KEYBOARD_LL
- Text file sequential reader (line-by-line on each PTT cycle)
- Direct text input with Speak button
- Voice manager: browse 147-voice Piper catalogue, download, remove
- libpiper P/Invoke wrapper with UTF-8 marshaling, streaming chunks
- BufferedWaveProvider streaming playback with trailing silence flush
- Sentence terminator auto-append (fixes espeak-ng final-word drop)
- Tray icon with minimize-to-tray

Architecture:
- Robovoice.Core: ITtsEngine, ISttSource interfaces, voice catalogue
- Robovoice.Tts.LibPiper: P/Invoke wrapper for libpiper.dll
- Robovoice.Stt.File: text file STT source (testing without mic server)
- Robovoice.Stt.Udp: UDP client stub (for future Linux mic server)
- Robovoice.App: WinForms UI, orchestrator, PTT hotkey, audio output
2026-08-10 11:27:28 +00:00

121 lines
3.3 KiB
C#

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<string> _log;
private CancellationTokenSource? _currentCts;
private bool _disposed;
public string OutputDeviceName { get; set; } = string.Empty;
public Orchestrator(
LibPiperTtsEngine tts,
AudioOutput audioOutput,
FileSttSource sttSource,
Action<string> 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;
}
}