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:
@@ -0,0 +1,58 @@
|
||||
using System.Media;
|
||||
|
||||
namespace VoiceCat.App.Notifications;
|
||||
|
||||
/// <summary>
|
||||
/// Plays short event cues from the app's <c>sounds\</c> folder. Each WAV is loaded once into a
|
||||
/// cached <see cref="SoundPlayer"/> (built into the Windows Desktop framework — no extra
|
||||
/// dependency). <see cref="Play"/> is non-blocking (<see cref="SoundPlayer.Play"/> renders on a
|
||||
/// background thread).
|
||||
///
|
||||
/// Note: <see cref="SoundPlayer"/> exposes no gain control, so volume is honoured only as a mute
|
||||
/// gate (volume == 0 → silent). If finer control or reliable overlapping playback is needed
|
||||
/// later, swap this for NAudio.
|
||||
/// </summary>
|
||||
public sealed class SoundPlayerPool : IDisposable
|
||||
{
|
||||
private static readonly string SoundsDir =
|
||||
Path.Combine(AppContext.BaseDirectory, "sounds");
|
||||
|
||||
private readonly Dictionary<SoundEvent, SoundPlayer?> _players = [];
|
||||
|
||||
/// <summary>Resolve, load and cache the player for an event. Returns null if the file is
|
||||
/// missing or fails to load — playback then silently no-ops.</summary>
|
||||
private SoundPlayer? Get(SoundEvent ev)
|
||||
{
|
||||
if (_players.TryGetValue(ev, out var cached)) return cached;
|
||||
|
||||
SoundPlayer? player = null;
|
||||
try
|
||||
{
|
||||
string path = Path.Combine(SoundsDir, ev.FileName());
|
||||
if (File.Exists(path))
|
||||
{
|
||||
player = new SoundPlayer(path);
|
||||
player.Load();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
player = null; // unreadable/invalid WAV — degrade to silence
|
||||
}
|
||||
|
||||
_players[ev] = player;
|
||||
return player;
|
||||
}
|
||||
|
||||
public void Play(SoundEvent ev)
|
||||
{
|
||||
try { Get(ev)?.Play(); }
|
||||
catch { /* never let a notification sound surface as an error */ }
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var p in _players.Values) p?.Dispose();
|
||||
_players.Clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user