v0.9: VoicePipeline refactor — queue-based pipeline, lock/release model, thread-based TCP

This commit is contained in:
2026-08-13 12:38:27 +00:00
parent 41a53d35b0
commit caad0ed50e
6 changed files with 556 additions and 554 deletions
@@ -120,6 +120,71 @@ public sealed class LibPiperTtsEngine : ITtsEngine
}
}
public IEnumerable<AudioChunk> SynthesizeSync(string text)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (!_initialized)
throw new InvalidOperationException("Engine not initialized.");
var options = new PiperSynthesizeOptions
{
SpeakerId = 0,
LengthScale = _lengthScale,
NoiseScale = _noiseScale,
NoiseWScale = _noiseWScale,
};
byte[] textBytes = System.Text.Encoding.UTF8.GetBytes(EnsureTerminator(text) + "\0");
GCHandle textPin = GCHandle.Alloc(textBytes, GCHandleType.Pinned);
try
{
int startResult = PiperNative.piper_synthesize_start(
_synth, textPin.AddrOfPinnedObject(), in options);
if (startResult != PiperNative.PiperOk)
throw new InvalidOperationException($"piper_synthesize_start failed: {startResult}");
while (true)
{
PiperAudioChunk chunk = default;
int result = PiperNative.piper_synthesize_next(_synth, out chunk);
if (chunk.NumSamples > 0 && chunk.Samples != IntPtr.Zero)
{
int numSamples = (int)chunk.NumSamples;
float[] samples = new float[numSamples];
Marshal.Copy(chunk.Samples, samples, 0, numSamples);
if (chunk.SampleRate > 0)
_sampleRate = chunk.SampleRate;
yield return new AudioChunk(samples, _sampleRate);
}
if (result == PiperNative.PiperDone || chunk.IsLast)
break;
if (result < 0)
throw new InvalidOperationException($"piper_synthesize_next failed: {result}");
}
}
finally
{
textPin.Free();
}
}
public void SynthesizeDrain()
{
if (!_initialized || _synth == IntPtr.Zero)
return;
PiperAudioChunk chunk;
while (PiperNative.piper_synthesize_next(_synth, out chunk) != PiperNative.PiperDone)
{
if (chunk.IsLast) break;
}
}
private static string EnsureTerminator(string text)
{
string trimmed = text.TrimEnd();