Files
Robovoice/Robovoice.Stt.Tcp/TcpSttSource.cs
T

292 lines
7.5 KiB
C#
Raw Normal View History

using System.Net;
using System.Net.Sockets;
using System.Text;
using Robovoice.Core;
namespace Robovoice.Stt.Tcp;
public sealed class TcpSttSource : ISttSource
{
private TcpClient? _tcp;
private NetworkStream? _stream;
private StreamWriter? _writer;
private Thread? _connectThread;
private volatile bool _running;
private readonly object _sendLock = new();
private uint _session;
private bool _disposed;
public string Endpoint { get; set; } = "127.0.0.1:6996";
public Action<string>? Log { get; set; }
public event TranscriptEventHandler? TranscriptReceived;
public Task StartAsync(CancellationToken ct = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_running)
return Task.CompletedTask;
_running = true;
_connectThread = new Thread(ConnectLoop) { IsBackground = true, Name = "TcpSttSource-Connect" };
_connectThread.Start();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken ct = default)
{
_running = false;
lock (_sendLock)
{
_writer?.Dispose();
_stream?.Dispose();
_tcp?.Close();
_writer = null;
_stream = null;
_tcp = null;
}
// Threads are background — they'll die when the process exits.
// Closing the socket unblocks any pending Read.
_connectThread?.Join(1000);
return Task.CompletedTask;
}
private void ConnectLoop()
{
while (_running)
{
IPEndPoint? endpoint = ParseEndpoint(Endpoint);
if (endpoint is null)
{
Log?.Invoke($"STT: invalid endpoint '{Endpoint}'");
SleepInterruptible(3000);
continue;
}
try
{
var tcp = new TcpClient();
tcp.Connect(endpoint.Address, endpoint.Port);
tcp.NoDelay = true;
lock (_sendLock)
{
_tcp = tcp;
_stream = tcp.GetStream();
_writer = new StreamWriter(_stream, new UTF8Encoding(false)) { AutoFlush = true };
}
Log?.Invoke($"STT: connected to {Endpoint}");
// Blocking receive loop — runs until disconnected or stopped.
ReceiveLoop();
Log?.Invoke("STT: disconnected");
}
catch (Exception ex)
{
if (_running)
Log?.Invoke($"STT: connection failed ({ex.Message}), retrying...");
}
finally
{
lock (_sendLock)
{
_writer?.Dispose();
_stream?.Dispose();
_tcp?.Close();
_writer = null;
_stream = null;
_tcp = null;
}
}
if (_running)
SleepInterruptible(3000);
}
}
private void ReceiveLoop()
{
byte[] buffer = new byte[4096];
StringBuilder lineBuf = new();
while (_running)
{
NetworkStream? stream;
lock (_sendLock)
{
stream = _stream;
}
if (stream is null)
break;
int bytesRead;
try
{
bytesRead = stream.Read(buffer, 0, buffer.Length);
}
catch
{
break;
}
if (bytesRead == 0)
break;
for (int i = 0; i < bytesRead; i++)
{
byte b = buffer[i];
if (b == '\n')
{
string line = lineBuf.ToString().TrimEnd('\r');
lineBuf.Clear();
TranscriptMessage? message = ParseReply(line);
if (message is not null)
{
TranscriptReceived?.Invoke(this, new TranscriptEventArgs
{
Message = message,
});
}
}
else
{
lineBuf.Append((char)b);
}
}
}
}
private void SleepInterruptible(int ms)
{
int slice = 100;
int waited = 0;
while (_running && waited < ms)
{
int chunk = Math.Min(slice, ms - waited);
Thread.Sleep(chunk);
waited += chunk;
}
}
public void SendOn()
{
_session++;
Send($"ON {_session}");
}
public void SendOff()
{
Send($"OFF {_session}");
}
private void Send(string message)
{
lock (_sendLock)
{
if (_writer is null)
return;
try
{
_writer.WriteLine(message);
}
catch
{
Log?.Invoke($"STT: failed to send '{message}' (not connected?)");
}
}
}
private static IPEndPoint? ParseEndpoint(string endpoint)
{
int colon = endpoint.LastIndexOf(':');
if (colon <= 0)
return null;
string host = endpoint[..colon];
if (!int.TryParse(endpoint[(colon + 1)..], out int port))
return null;
if (IPAddress.TryParse(host, out var addr))
return new IPEndPoint(addr, port);
try
{
var addresses = Dns.GetHostAddresses(host);
addr = addresses.FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetwork);
if (addr is null)
return null;
return new IPEndPoint(addr, port);
}
catch
{
return null;
}
}
private TranscriptMessage? ParseReply(string line)
{
if (line.StartsWith("P ", StringComparison.Ordinal))
{
string rest = line["P ".Length..];
int space = rest.IndexOf(' ');
if (space < 0)
return null;
if (!uint.TryParse(rest[..space], out uint session))
return null;
if (session != _session)
{
Log?.Invoke($"STT: dropping stale reply (session {session} != current {_session})");
return null;
}
return new TranscriptMessage(TranscriptType.Partial, rest[(space + 1)..]);
}
if (line.StartsWith("F ", StringComparison.Ordinal))
{
string rest = line["F ".Length..];
int space = rest.IndexOf(' ');
if (space < 0)
{
if (uint.TryParse(rest, out uint session) && session == _session)
return new TranscriptMessage(TranscriptType.Final, string.Empty);
return null;
}
if (!uint.TryParse(rest[..space], out uint ses))
return null;
if (ses != _session)
{
Log?.Invoke($"STT: dropping stale reply (session {ses} != current {_session})");
return null;
}
return new TranscriptMessage(TranscriptType.Final, rest[(space + 1)..]);
}
return null;
}
public ValueTask DisposeAsync()
{
if (_disposed) return ValueTask.CompletedTask;
_disposed = true;
StopAsync();
return ValueTask.CompletedTask;
}
}