feat(clients): event sound effects + optional text-to-speech

Add audible cues and optional spoken announcements for session events
(join/leave, channel + PM sent/recv, login, logout/connection-lost,
mic on/off, voice-activity, PTT) across all three clients, driven off
the shared C ABI vc_event stream so the mapping stays consistent.

TTS is off by default; when enabled it announces events and reads
message/PM bodies aloud. Master toggles + a sound-volume slider; the
per-utterance voice-activity and PTT cues default off. WAVs ship from
assets/sounds/.

Windows (built + verified): new VoiceCat.App/Notifications/ layer
(FeedbackSettings -> %AppData%\VoiceCat\feedback.json, SoundPlayerPool
via System.Media.SoundPlayer, SpeechAnnouncer via Prismatoid 0.3.0,
EventFeedback dispatcher); MainForm hooks; NotificationSettingsForm
under Settings > Notifications; csproj adds the Prismatoid PackageRef
and copies the WAVs into sounds\.

macOS + iOS (written, not yet built -- needs a Mac): shared
VoiceCatCore/Feedback/ (SoundEvent, EventFeedback = AVAudioPlayer pool
+ native AVSpeechSynthesizer, FeedbackSettings over UserDefaults); WAVs
bundled via Package.swift resources (.process). Hooks in SessionState/
AppState (iOS) and MainWindowController (macOS); settings UI in
SettingsView (iOS) and SettingsWindowController (macOS).

No core/server code touched; ctest --preset dev unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 15:20:11 +02:00
parent 725bd8e925
commit 50416c33a2
45 changed files with 812 additions and 19 deletions

View File

@@ -0,0 +1,128 @@
using VoiceCat.App.Notifications;
namespace VoiceCat.App.Forms;
/// <summary>
/// Edits and persists <see cref="FeedbackSettings"/> (event sounds + spoken announcements).
/// On OK the supplied <see cref="EventFeedback"/> is updated live and the settings saved.
/// </summary>
public sealed class NotificationSettingsForm : Form
{
private readonly EventFeedback _feedback;
private readonly CheckBox _chkSounds;
private readonly CheckBox _chkSpeech;
private readonly TrackBar _trkVolume;
private readonly CheckBox _chkSelfTalk;
private readonly CheckBox _chkPtt;
public NotificationSettingsForm(EventFeedback feedback)
{
_feedback = feedback;
var s = feedback.Settings;
Text = "Notification settings";
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
StartPosition = FormStartPosition.CenterParent;
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(360, 300);
_chkSounds = new CheckBox
{
Text = "Play event &sounds",
Location = new Point(12, 12),
AutoSize = true,
Checked = s.Sounds,
};
var lblVolume = new Label
{
Text = "Sound &volume:",
Location = new Point(12, 42),
AutoSize = true,
};
_trkVolume = new TrackBar
{
Location = new Point(12, 64),
Size = new Size(330, 45),
Minimum = 0,
Maximum = 100,
TickFrequency = 10,
Value = (int)Math.Round(Math.Clamp(s.Volume, 0f, 1f) * 100),
};
_trkVolume.AccessibleName = "Sound volume";
_chkSpeech = new CheckBox
{
Text = "&Speak events (text-to-speech)",
Location = new Point(12, 118),
AutoSize = true,
Checked = s.Speech,
};
var lblSpeechHint = new Label
{
Text = feedback.SpeechAvailable
? "Announces joins/leaves and reads message text aloud."
: "No speech engine available on this machine.",
Location = new Point(30, 142),
AutoSize = true,
ForeColor = SystemColors.GrayText,
};
var lblOptional = new Label
{
Text = "Optional sounds:",
Location = new Point(12, 174),
AutoSize = true,
};
_chkSelfTalk = new CheckBox
{
Text = "Your own voice-&activity start/stop",
Location = new Point(12, 198),
AutoSize = true,
Checked = s.SelfTalkSounds,
};
_chkPtt = new CheckBox
{
Text = "&Push-to-talk cue",
Location = new Point(12, 224),
AutoSize = true,
Checked = s.PttSound,
};
var btnOk = new Button
{
Text = "&OK",
DialogResult = DialogResult.OK,
Location = new Point(192, 262),
Size = new Size(75, 27),
};
var btnCancel = new Button
{
Text = "&Cancel",
DialogResult = DialogResult.Cancel,
Location = new Point(273, 262),
Size = new Size(75, 27),
};
AcceptButton = btnOk;
CancelButton = btnCancel;
Controls.AddRange([_chkSounds, lblVolume, _trkVolume, _chkSpeech, lblSpeechHint,
lblOptional, _chkSelfTalk, _chkPtt, btnOk, btnCancel]);
btnOk.Click += (_, _) => Apply();
}
private void Apply()
{
var s = _feedback.Settings;
s.Sounds = _chkSounds.Checked;
s.Speech = _chkSpeech.Checked;
s.Volume = _trkVolume.Value / 100f;
s.SelfTalkSounds = _chkSelfTalk.Checked;
s.PttSound = _chkPtt.Checked;
s.Save();
}
}