Files

245 lines
8.0 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Runtime.InteropServices;
using Robovoice.Core;
namespace Robovoice.Tts.LibPiper;
public sealed class LibPiperTtsEngine : ITtsEngine
{
private readonly string _modelPath;
private readonly string _espeakDataPath;
private readonly float? _noiseScaleOverride;
private readonly float? _lengthScaleOverride;
private readonly float? _noiseWScaleOverride;
private float _noiseScale;
private float _lengthScale;
private float _noiseWScale;
private IntPtr _synth;
private int _sampleRate;
private bool _initialized;
private bool _disposed;
public string Name => "libpiper";
public int SampleRate => _sampleRate;
public LibPiperTtsEngine(
string modelPath,
string espeakDataPath,
float? noiseScale = null,
float? lengthScale = null,
float? noiseWScale = null)
{
_modelPath = modelPath;
_espeakDataPath = espeakDataPath;
_noiseScaleOverride = noiseScale;
_lengthScaleOverride = lengthScale;
_noiseWScaleOverride = noiseWScale;
_noiseScale = noiseScale ?? 0.667f;
_lengthScale = lengthScale ?? 1.0f;
_noiseWScale = noiseWScale ?? 0.8f;
}
public Task InitializeAsync(CancellationToken ct = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_initialized)
return Task.CompletedTask;
NativeDependencyLoader.EnsureLoaded();
_synth = PiperCreateUtf8(_modelPath, _modelPath + ".json", _espeakDataPath);
if (_synth == IntPtr.Zero)
throw new InvalidOperationException(
$"piper_create failed for model: {_modelPath}");
var defaults = PiperNative.piper_default_synthesize_options(_synth);
_noiseScale = _noiseScaleOverride ?? defaults.NoiseScale;
_lengthScale = _lengthScaleOverride ?? defaults.LengthScale;
_noiseWScale = _noiseWScaleOverride ?? defaults.NoiseWScale;
_sampleRate = 22050;
_initialized = true;
return Task.CompletedTask;
}
public async IAsyncEnumerable<AudioChunk> SynthesizeAsync(
string text,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default)
{
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)
{
ct.ThrowIfCancellationRequested();
PiperAudioChunk chunk = default;
int result = await Task.Run(() => PiperNative.piper_synthesize_next(_synth, out chunk), ct);
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 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();
if (trimmed.Length == 0)
return text;
char last = trimmed[^1];
if (last is '.' or '!' or '?' or ',' or ';' or ':' or ')' or ']' or '}' or '"' or '\'' or '。' or '' or '')
return text;
return trimmed + ".";
}
private static IntPtr PiperCreateUtf8(string modelPath, string? configPath, string espeakDataPath)
{
byte[] modelBytes = System.Text.Encoding.UTF8.GetBytes(modelPath + "\0");
byte[] espeakBytes = System.Text.Encoding.UTF8.GetBytes(espeakDataPath + "\0");
GCHandle modelPin = GCHandle.Alloc(modelBytes, GCHandleType.Pinned);
GCHandle espeakPin = GCHandle.Alloc(espeakBytes, GCHandleType.Pinned);
GCHandle? configPin = null;
byte[]? configBytes = null;
if (configPath is not null)
{
configBytes = System.Text.Encoding.UTF8.GetBytes(configPath + "\0");
configPin = GCHandle.Alloc(configBytes, GCHandleType.Pinned);
}
try
{
return PiperNative.piper_create(
modelPin.AddrOfPinnedObject(),
configPin?.AddrOfPinnedObject() ?? IntPtr.Zero,
espeakPin.AddrOfPinnedObject());
}
finally
{
modelPin.Free();
espeakPin.Free();
configPin?.Free();
}
}
public ValueTask DisposeAsync()
{
if (_disposed)
return ValueTask.CompletedTask;
if (_synth != IntPtr.Zero)
{
PiperNative.piper_free(_synth);
_synth = IntPtr.Zero;
}
_disposed = true;
return ValueTask.CompletedTask;
}
}