Fading Coder

One Final Commit for the Last Sprint

Home > Tools > Content

Building a Digital Queue Announcer with C# WinForms and Text-to-Speech

Tools May 15 1

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

Related Articles

Efficient Usage of HTTP Client in IntelliJ IDEA

IntelliJ IDEA incorporates a versatile HTTP client tool, enabling developres to interact with RESTful services and APIs effectively with in the editor. This functionality streamlines workflows, replac...

Installing CocoaPods on macOS Catalina (10.15) Using a User-Managed Ruby

System Ruby on macOS 10.15 frequently fails to build native gems required by CocoaPods (for example, ffi), leading to errors like: ERROR: Failed to build gem native extension checking for ffi.h... no...

Resolve PhpStorm "Interpreter is not specified or invalid" on WAMP (Windows)

Symptom PhpStorm displays: "Interpreter is not specified or invalid. Press ‘Fix’ to edit your project configuration." This occurs when the IDE cannot locate a valid PHP CLI executable or when the debu...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.