Building a Digital Queue Announcer with C# WinForms and Text-to-Speech
Systemm Architecture
The solution employs a producer-consumer architecture to decouple the graphical interface from background audio processing. User inputs populate a thread-safe buffer, which is continuous monitored by a dedicated worker task. Once a ticket number is retrieved from the buffer, the System.Speech.Synthesis API converts it to audio, which is routed to the default system output. This separation prevents UI freezing during lengthy voice generation or network operations.
- Input Handler: Captures user entries and validates formats before pushing to the buffer.
- Queue Manager: Maintains a thread-safe collection of pending announcements.
- Audio Dispatcher: Consumes queue items, manages TTS engine state, and controls playback timing.
- Presentation Layer: WinForms controls for manual entry, operational toggles, and real-time status indicators.
Core Implementation
1. Background Announcer Service
using System.Collections.Concurrent;
using System.Speech.Synthesis;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
public class QueueAnnouncer
{
private readonly SpeechSynthesizer _synth;
private readonly ConcurrentQueue<string> _buffer = new();
private CancellationTokenSource _cancelToken;
private Task _worker;
public QueueAnnouncer()
{
_synth = new SpeechSynthesizer();
_synth.SetOutputToDefaultAudioDevice();
SelectPreferredVoice();
}
private void SelectPreferredVoice()
{
var voices = _synth.GetInstalledVoices();
var target = voices.FirstOrDefault(v => v.VoiceInfo.Culture.Name.StartsWith("zh"))
?? voices.FirstOrDefault();
if (target != null) _synth.SelectVoice(target.VoiceInfo.Name);
}
public void AdjustParameters(int speed = 0, int vol = 100)
{
_synth.Rate = System.Math.Max(-10, System.Math.Min(10, speed));
_synth.Volume = System.Math.Max(0, System.Math.Min(100, vol));
}
public void AddEntry(string ticket)
{
if (!string.IsNullOrWhiteSpace(ticket))
_buffer.Enqueue(ticket.Trim());
}
public void BeginProcessing()
{
if (_worker?.IsCompleted == false) return;
_cancelToken = new CancellationTokenSource();
_worker = Task.Run(() => RunLoopAsync(_cancelToken.Token));
}
public void HaltProcessing()
{
_cancelToken?.Cancel();
_worker?.Wait();
_synth.SpeakAsyncCancelAll();
}
private async Task RunLoopAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
if (_buffer.TryDequeue(out string nextTicket))
{
await Task.Run(() => _synth.Speak(nextTicket), ct);
await Task.Delay(2000, ct);
}
else
{
await Task.Delay(500, ct);
}
}
}
public int RemainingCount => _buffer.Count;
}
2. WinForms Controller
using System;
using System.Windows.Forms;
public partial class ControlPanel : Form
{
private readonly QueueAnnouncer _service = new();
private bool _isActive;
public ControlPanel()
{
InitializeComponent();
btnPower.Click += OnPowerToggle;
btnSubmit.Click += OnSubmitTicket;
}
private void OnPowerToggle(object sender, EventArgs e)
{
_isActive = !_isActive;
if (_isActive)
{
_service.AdjustParameters(2, 90);
_service.BeginProcessing();
btnPower.Text = "暂停服务";
}
else
{
_service.HaltProcessing();
btnPower.Text = "启动服务";
}
}
private void OnSubmitTicket(object sender, EventArgs e)
{
var input = txtInputBox.Text.Trim();
if (string.IsNullOrEmpty(input)) return;
_service.AddEntry(input);
txtInputBox.Clear();
RefreshStatus();
}
private void RefreshStatus()
{
if (InvokeRequired)
{
BeginInvoke(new Action(RefreshStatus));
return;
}
lblQueueInfo.Text = $"待播报队列长度:{_service.RemainingCount}";
}
}
Extended Capabilities
Persistent Queue Storage
To survive application restarts, the pending buffer can be serialized to disk. The following methods handle plain-text serialization and deserialization:
using System.IO;
using System.Linq;
public void PersistQueue(string filePath)
{
File.WriteAllLines(filePath, _buffer.ToArray());
}
public void RestoreQueue(string filePath)
{
if (!File.Exists(filePath)) return;
foreach (var line in File.ReadLines(filePath))
{
AddEntry(line);
}
}
Network Ticket Integration
A lightweight TCP endpoint allows remote terminals or kiosks to push numbers into the local queue asynchronously:
using System.Net;
using System.Net.Sockets;
using System.IO;
public void OpenNetworkPort(int port = 8888)
{
var listener = new TcpListener(IPAddress.Any, port);
listener.Start();
Task.Run(async () =>
{
while (true)
{
var client = await listener.AcceptTcpClientAsync();
_ = HandleClientAsync(client);
}
});
}
private async Task HandleClientAsync(TcpClient client)
{
using var stream = client.GetStream();
using var reader = new StreamReader(stream);
var payload = await reader.ReadLineAsync();
if (!string.IsNullOrWhiteSpace(payload))
AddEntry(payload);
client.Close();
}
Deployment Guidelines & Performance Tuning
Voice Engine Requirements: The underlying OS must contain the target language's TTS packs. Verify installation via Control Panel > Speech Recognition > Text-to-Speech. Wrap synthesis calls in try-catch blocks to gracefully handle missing voices or audio device conflicts.
Optimization Metrics:
Project Layout
AnnouncerSuite/
├── AnnouncerSuite.sln
├── CoreEngine/
│ ├── QueueAnnouncer.cs
│ └── NetworkGateway.cs
├── DesktopUI/
│ ├── ControlPanel.cs
│ └── ControlPanel.Designer.cs
├── Configuration/
│ └── speech-settings.json
└── Tests/
└── AnnouncerUnitTests.cs