v0.2: streaming playback, model params, text input

- Stream audio directly to device via BufferedWaveProvider (no temp WAVs)
- Add Noise/Speed/NoiseW sliders with live value labels
- Sliders reinit engine on MouseUp (not Scroll) to avoid locking
- Add text input field with Speak button for direct synthesis
- Test button uses text input if non-empty
- Fix sentence terminator auto-append (espeak-ng final-word drop)
- Fix UTF-8 text marshaling (piper_synthesize_start keeps text ref)
- Fix voice catalogue language column (JSON snake_case mapping)
- Use piper_default_synthesize_options for model-specific defaults
- Fix nullable warnings in designer files
- Fix unused _playing field in AudioOutput
This commit is contained in:
2026-08-10 11:50:48 +00:00
parent 8585972a1e
commit f7ea07b78e
4 changed files with 128 additions and 13 deletions
-4
View File
@@ -8,7 +8,6 @@ internal sealed class AudioOutput : IDisposable
private BufferedWaveProvider? _bufferProvider; private BufferedWaveProvider? _bufferProvider;
private int _sampleRate; private int _sampleRate;
private string _deviceName = string.Empty; private string _deviceName = string.Empty;
private bool _playing;
private bool _disposed; private bool _disposed;
public Action<string>? Log { get; set; } public Action<string>? Log { get; set; }
@@ -35,7 +34,6 @@ internal sealed class AudioOutput : IDisposable
_waveOut.Init(_bufferProvider); _waveOut.Init(_bufferProvider);
_waveOut.PlaybackStopped += OnPlaybackStopped; _waveOut.PlaybackStopped += OnPlaybackStopped;
_playing = true;
_waveOut.Play(); _waveOut.Play();
} }
@@ -60,7 +58,6 @@ internal sealed class AudioOutput : IDisposable
private void OnPlaybackStopped(object? sender, StoppedEventArgs e) private void OnPlaybackStopped(object? sender, StoppedEventArgs e)
{ {
_playing = false;
} }
public void Stop() public void Stop()
@@ -74,7 +71,6 @@ internal sealed class AudioOutput : IDisposable
} }
_bufferProvider?.ClearBuffer(); _bufferProvider?.ClearBuffer();
_bufferProvider = null; _bufferProvider = null;
_playing = false;
} }
private static int FindDevice(string? deviceName) private static int FindDevice(string? deviceName)
+99 -8
View File
@@ -1,3 +1,4 @@
#nullable enable
namespace Robovoice.App; namespace Robovoice.App;
partial class MainForm partial class MainForm
@@ -22,6 +23,15 @@ partial class MainForm
private Label lblTextInput = null!; private Label lblTextInput = null!;
private TextBox txtTextInput = null!; private TextBox txtTextInput = null!;
private Button btnSpeakInput = null!; private Button btnSpeakInput = null!;
private Label lblNoise = null!;
private TrackBar trkNoise = null!;
private Label lblNoiseVal = null!;
private Label lblSpeed = null!;
private TrackBar trkSpeed = null!;
private Label lblSpeedVal = null!;
private Label lblNoiseW = null!;
private TrackBar trkNoiseW = null!;
private Label lblNoiseWVal = null!;
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
@@ -52,6 +62,15 @@ partial class MainForm
lblTextInput = new Label(); lblTextInput = new Label();
txtTextInput = new TextBox(); txtTextInput = new TextBox();
btnSpeakInput = new Button(); btnSpeakInput = new Button();
lblNoise = new Label();
trkNoise = new TrackBar();
lblNoiseVal = new Label();
lblSpeed = new Label();
trkSpeed = new TrackBar();
lblSpeedVal = new Label();
lblNoiseW = new Label();
trkNoiseW = new TrackBar();
lblNoiseWVal = new Label();
SuspendLayout(); SuspendLayout();
@@ -126,23 +145,86 @@ partial class MainForm
lblLineStatus.TextAlign = ContentAlignment.MiddleRight; lblLineStatus.TextAlign = ContentAlignment.MiddleRight;
lblLineStatus.ForeColor = Color.DarkBlue; lblLineStatus.ForeColor = Color.DarkBlue;
// lblNoise
lblNoise.Text = "Noise:";
lblNoise.Location = new Point(12, 82);
lblNoise.Size = new Size(40, 23);
lblNoise.TextAlign = ContentAlignment.MiddleLeft;
// trkNoise (0-1000 → 0.0-1.0, default 667)
trkNoise.Location = new Point(52, 78);
trkNoise.Size = new Size(120, 45);
trkNoise.Minimum = 0;
trkNoise.Maximum = 1000;
trkNoise.Value = 667;
trkNoise.TickFrequency = 200;
trkNoise.Orientation = Orientation.Horizontal;
// lblNoiseVal
lblNoiseVal.Text = "0.667";
lblNoiseVal.Location = new Point(175, 82);
lblNoiseVal.Size = new Size(35, 23);
lblNoiseVal.TextAlign = ContentAlignment.MiddleLeft;
// lblSpeed
lblSpeed.Text = "Speed:";
lblSpeed.Location = new Point(220, 82);
lblSpeed.Size = new Size(40, 23);
lblSpeed.TextAlign = ContentAlignment.MiddleLeft;
// trkSpeed (50-300 → 0.5-3.0, default 100)
trkSpeed.Location = new Point(260, 78);
trkSpeed.Size = new Size(120, 45);
trkSpeed.Minimum = 50;
trkSpeed.Maximum = 300;
trkSpeed.Value = 100;
trkSpeed.TickFrequency = 50;
trkSpeed.Orientation = Orientation.Horizontal;
// lblSpeedVal
lblSpeedVal.Text = "1.00";
lblSpeedVal.Location = new Point(383, 82);
lblSpeedVal.Size = new Size(35, 23);
lblSpeedVal.TextAlign = ContentAlignment.MiddleLeft;
// lblNoiseW
lblNoiseW.Text = "NoiseW:";
lblNoiseW.Location = new Point(428, 82);
lblNoiseW.Size = new Size(45, 23);
lblNoiseW.TextAlign = ContentAlignment.MiddleLeft;
// trkNoiseW (0-1000 → 0.0-1.0, default 800)
trkNoiseW.Location = new Point(475, 78);
trkNoiseW.Size = new Size(120, 45);
trkNoiseW.Minimum = 0;
trkNoiseW.Maximum = 1000;
trkNoiseW.Value = 800;
trkNoiseW.TickFrequency = 200;
trkNoiseW.Orientation = Orientation.Horizontal;
// lblNoiseWVal
lblNoiseWVal.Text = "0.800";
lblNoiseWVal.Location = new Point(598, 82);
lblNoiseWVal.Size = new Size(45, 23);
lblNoiseWVal.TextAlign = ContentAlignment.MiddleLeft;
// txtLog // txtLog
txtLog.Location = new Point(12, 112); txtLog.Location = new Point(12, 149);
txtLog.Size = new Size(800, 310); txtLog.Size = new Size(800, 310);
// lblTextInput // lblTextInput
lblTextInput.Text = "Text:"; lblTextInput.Text = "Text:";
lblTextInput.Location = new Point(12, 84); lblTextInput.Location = new Point(12, 121);
lblTextInput.Size = new Size(35, 23); lblTextInput.Size = new Size(35, 23);
lblTextInput.TextAlign = ContentAlignment.MiddleLeft; lblTextInput.TextAlign = ContentAlignment.MiddleLeft;
// txtTextInput // txtTextInput
txtTextInput.Location = new Point(50, 81); txtTextInput.Location = new Point(50, 118);
txtTextInput.Size = new Size(680, 23); txtTextInput.Size = new Size(680, 23);
// btnSpeakInput // btnSpeakInput
btnSpeakInput.Text = "Speak"; btnSpeakInput.Text = "Speak";
btnSpeakInput.Location = new Point(737, 80); btnSpeakInput.Location = new Point(737, 117);
btnSpeakInput.Size = new Size(75, 25); btnSpeakInput.Size = new Size(75, 25);
btnSpeakInput.UseVisualStyleBackColor = true; btnSpeakInput.UseVisualStyleBackColor = true;
txtLog.ReadOnly = true; txtLog.ReadOnly = true;
@@ -152,20 +234,20 @@ partial class MainForm
// chkMinimizeToTray // chkMinimizeToTray
chkMinimizeToTray.Text = "Minimize to tray on close"; chkMinimizeToTray.Text = "Minimize to tray on close";
chkMinimizeToTray.Location = new Point(12, 440); chkMinimizeToTray.Location = new Point(12, 477);
chkMinimizeToTray.Size = new Size(180, 24); chkMinimizeToTray.Size = new Size(180, 24);
chkMinimizeToTray.UseVisualStyleBackColor = true; chkMinimizeToTray.UseVisualStyleBackColor = true;
// btnClearLog // btnClearLog
btnClearLog.Text = "Clear Log"; btnClearLog.Text = "Clear Log";
btnClearLog.Location = new Point(737, 438); btnClearLog.Location = new Point(737, 475);
btnClearLog.Size = new Size(75, 25); btnClearLog.Size = new Size(75, 25);
btnClearLog.UseVisualStyleBackColor = true; btnClearLog.UseVisualStyleBackColor = true;
// MainForm // MainForm
AutoScaleDimensions = new SizeF(7F, 15F); AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font; AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(824, 472); ClientSize = new Size(824, 509);
Controls.Add(lblPttKey); Controls.Add(lblPttKey);
Controls.Add(cmbPttKey); Controls.Add(cmbPttKey);
Controls.Add(lblVoice); Controls.Add(lblVoice);
@@ -178,13 +260,22 @@ partial class MainForm
Controls.Add(btnBrowseFile); Controls.Add(btnBrowseFile);
Controls.Add(lblFileName); Controls.Add(lblFileName);
Controls.Add(lblLineStatus); Controls.Add(lblLineStatus);
Controls.Add(lblNoise);
Controls.Add(trkNoise);
Controls.Add(lblNoiseVal);
Controls.Add(lblSpeed);
Controls.Add(trkSpeed);
Controls.Add(lblSpeedVal);
Controls.Add(lblNoiseW);
Controls.Add(trkNoiseW);
Controls.Add(lblNoiseWVal);
Controls.Add(lblTextInput); Controls.Add(lblTextInput);
Controls.Add(txtTextInput); Controls.Add(txtTextInput);
Controls.Add(btnSpeakInput); Controls.Add(btnSpeakInput);
Controls.Add(txtLog); Controls.Add(txtLog);
Controls.Add(chkMinimizeToTray); Controls.Add(chkMinimizeToTray);
Controls.Add(btnClearLog); Controls.Add(btnClearLog);
MinimumSize = new Size(840, 510); MinimumSize = new Size(840, 547);
Text = "Robovoice"; Text = "Robovoice";
FormBorderStyle = FormBorderStyle.FixedSingle; FormBorderStyle = FormBorderStyle.FixedSingle;
MaximizeBox = false; MaximizeBox = false;
+28 -1
View File
@@ -49,6 +49,13 @@ internal sealed partial class MainForm : Form
cmbOutput.SelectedIndexChanged += OnOutputChanged; cmbOutput.SelectedIndexChanged += OnOutputChanged;
cmbVoice.SelectedIndexChanged += OnVoiceChanged; cmbVoice.SelectedIndexChanged += OnVoiceChanged;
trkNoise.Scroll += OnSliderScroll;
trkSpeed.Scroll += OnSliderScroll;
trkNoiseW.Scroll += OnSliderScroll;
trkNoise.MouseUp += OnSliderReleased;
trkSpeed.MouseUp += OnSliderReleased;
trkNoiseW.MouseUp += OnSliderReleased;
SetupTray(); SetupTray();
await InitializeEngineAsync(); await InitializeEngineAsync();
@@ -124,7 +131,12 @@ internal sealed partial class MainForm : Form
_audioOutput?.Dispose(); _audioOutput?.Dispose();
_tts?.DisposeAsync().AsTask().Wait(); _tts?.DisposeAsync().AsTask().Wait();
_tts = new LibPiperTtsEngine(modelPath, _espeakDataPath); _tts = new LibPiperTtsEngine(
modelPath,
_espeakDataPath,
noiseScale: trkNoise.Value / 1000.0f,
lengthScale: trkSpeed.Value / 100.0f,
noiseWScale: trkNoiseW.Value / 1000.0f);
_audioOutput = new AudioOutput(); _audioOutput = new AudioOutput();
_sttSource = new FileSttSource(); _sttSource = new FileSttSource();
_orchestrator?.DisposeAsync().AsTask().Wait(); _orchestrator?.DisposeAsync().AsTask().Wait();
@@ -272,6 +284,21 @@ internal sealed partial class MainForm : Form
await InitializeEngineAsync(); await InitializeEngineAsync();
} }
private void OnSliderScroll(object? sender, EventArgs e)
{
lblNoiseVal.Text = $"{trkNoise.Value / 1000.0:F3}";
lblSpeedVal.Text = $"{trkSpeed.Value / 100.0:F2}";
lblNoiseWVal.Text = $"{trkNoiseW.Value / 1000.0:F3}";
}
private void OnSliderReleased(object? sender, MouseEventArgs e)
{
if (cmbVoice.SelectedItem is string name && VoiceCatalogue.IsVoiceInstalled(_voicesDir, name))
{
_ = InitializeEngineAsync();
}
}
private void OnPttKeyChanged(object? sender, EventArgs e) private void OnPttKeyChanged(object? sender, EventArgs e)
{ {
SetupHotkey(); SetupHotkey();
+1
View File
@@ -1,3 +1,4 @@
#nullable enable
namespace Robovoice.App; namespace Robovoice.App;
partial class VoiceManagerForm partial class VoiceManagerForm