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,56 @@
|
||||
using Prismatoid;
|
||||
|
||||
namespace VoiceCat.App.Notifications;
|
||||
|
||||
/// <summary>
|
||||
/// Spoken event announcements via Prismatoid (bindings for the Prism speech library —
|
||||
/// integrates with the active screen reader / system speech, no extra runtime deps).
|
||||
///
|
||||
/// All construction and calls are wrapped so a machine with no available speech backend simply
|
||||
/// degrades to silence rather than crashing the app.
|
||||
/// </summary>
|
||||
public sealed class SpeechAnnouncer : IDisposable
|
||||
{
|
||||
private readonly PrismContext? _context;
|
||||
private readonly object? _backend; // SpeechBackend; held as object to keep this resilient
|
||||
|
||||
public SpeechAnnouncer()
|
||||
{
|
||||
try
|
||||
{
|
||||
_context = new PrismContext();
|
||||
_backend = _context.AcquireBestBackend();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_context?.Dispose();
|
||||
_context = null;
|
||||
_backend = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>True when a speech backend is available on this machine.</summary>
|
||||
public bool Available => _backend is not null;
|
||||
|
||||
/// <summary>Speak <paramref name="text"/>. Queued (does not interrupt prior speech) so a
|
||||
/// burst of events is read in order.</summary>
|
||||
public void Speak(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text)) return;
|
||||
try
|
||||
{
|
||||
if (_backend is { } b)
|
||||
((dynamic)b).Speak(text, interrupt: false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* never surface a speech failure */
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { (_backend as IDisposable)?.Dispose(); } catch { /* ignore */ }
|
||||
_context?.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user