Files
RemSound/src/RemSound.App/CuePlayer.cs
T
EdnunpandClaude Opus 4.8 a408d2b56e v4.0: full audio-cue system, cause-aware auto-tune, four-tab Preferences, install-side default sounds
Audio cues
- Cues for send/receive on-off, minimise/restore, checkbox tick/untick, and tab switch
- Soft keyboard clicks while typing, with a distinct passkey sound on password fields
- Per-cue "Choose sound" variant picker; "(none)" silences a cue; front-most missing-sound warning
- Send/receive cues take priority over the generic checkbox sound; programmatic ticks stay silent

Preferences
- Redesigned into four tabs (General, Audio cues, Startup behaviour, Update settings)
- Startup behaviour moved in from the Options menu
- NVDA now announces the dialog on open (focus a real named control, not the quiet tab control)

Auto-tune
- Cause-aware: tells device render-callback stalls (more buffer can't fix) apart from genuine
  network/buffer starvation, so it no longer pins latency high on chunky onboard cards
- Lowering the target eases the buffer down (glide) instead of trimming it, so no clicks while tuning

Sounds layout
- Shipped defaults moved out of the per-user folder into an install-side "default sounds" folder,
  so updates can refresh them; user customs are Browse-picked file paths and are left untouched
- Startup migration removes both legacy sound folders; verified from oldest (v1.0-v3.3) and v3.4 layouts

Quiet automated launches
- New --silent launch flag mutes all cue sounds and suppresses the startup dialogs (migration notice,
  update check, Realtek/mic/missing-sound warnings) so test launches never disturb the user
- run-tests / build-release / SelfTest repointed to the new "default sounds" layout

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 11:57:19 +01:00

77 lines
3.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using NAudio.Wave;
using NAudio.Wave.SampleProviders;
namespace RemSound.App;
/// <summary>
/// Plays a short cue WAV reliably, regardless of its format. Replaces System.Media.SoundPlayer,
/// which only handles bog-standard 16-bit / 44.148 kHz PCM and silently fails (plays nothing,
/// intermittently) on anything else — which is exactly why RemSound's own 96 kHz / 24-bit cue
/// files, and any custom WAV a user browses for, played unpredictably (2026-06-02 investigation
/// of Ed's "didn't hear the disconnect cue" report).
///
/// Each Play() reads the file with NAudio (which copes with 16/24/32-bit and any sample rate),
/// resamples to a universally-safe 48 kHz / 16-bit with a pure-managed resampler (no Media
/// Foundation, so it works on Windows 7 too), and renders it through WaveOut to the default
/// output device. Setup happens on a background thread so a cue never hitches the UI, and the
/// output + reader are disposed when playback finishes. Cues are occasional and short, so a
/// fresh device-open per play is fine and keeps the class stateless and overlap-safe (two cues
/// close together simply mix). 2026-06-02.
/// </summary>
internal sealed class CuePlayer : IDisposable
{
/// <summary>The "this is a silent / automated launch" flag, set once at startup by the
/// <c>--silent</c> flag. Primarily an app-wide mute for every cue sound (startup, connect,
/// checkbox, tab-switch, send/receive, previews — everything that plays through a CuePlayer).
/// It ALSO doubles as the signal to suppress the unattended startup pop-ups that would otherwise
/// ding at and bother whoever's at the screen during a throwaway test launch — the missing-sound
/// warning and the Realtek-ASIO-detected warning both check it. So a <c>--silent</c> launch is
/// completely quiet: no cue audio, no warning dings, no dialogs. Off in normal use.</summary>
public static bool GloballyMuted { get; set; }
private readonly string filePath;
public CuePlayer(string filePath) => this.filePath = filePath;
public void Play()
{
if (GloballyMuted) return;
var path = filePath;
Task.Run(() =>
{
AudioFileReader? reader = null;
WaveOutEvent? output = null;
try
{
reader = new AudioFileReader(path);
ISampleProvider source = reader;
if (reader.WaveFormat.SampleRate != 48000)
{
source = new WdlResamplingSampleProvider(source, 48000);
}
output = new WaveOutEvent();
var capturedReader = reader;
var capturedOutput = output;
output.PlaybackStopped += (_, _) =>
{
try { capturedOutput.Dispose(); } catch { /* best-effort */ }
try { capturedReader.Dispose(); } catch { /* best-effort */ }
};
output.Init(new SampleToWaveProvider16(source));
output.Play();
}
catch
{
// A cue that won't load or play must never disturb anything. Clean up and move on.
try { output?.Dispose(); } catch { /* ignore */ }
try { reader?.Dispose(); } catch { /* ignore */ }
}
});
}
public void Dispose()
{
// Nothing persistent to release — each Play owns and disposes its own reader + output.
}
}