Release v3.3: end-to-end encrypted audio, plus cue and reliability fixes
Headline: all audio is now encrypted (AES-256-GCM), keyed by a per-profile password. Mandatory — v3.3 only interoperates with v3.3+. Encryption - RemSoundCrypto (Core): PBKDF2 key derivation, AES-GCM encrypt/decrypt (low-alloc, into-span), password fingerprint, light on-disk obfuscation. - Wire: SenderLane encrypts the audio payload (PCM split across parts when the +28 overhead crosses MTU); AudioReceiver/StreamSession decrypt via a shared single-thread AudioDecryptor. Fingerprint piggybacks on the Format packet (offset 36, backward-compatible) so a peer can detect a password mismatch. - Profile.Password (scrambled), carried through BuildCurrentProfile; MainForm derives + pushes the key/fingerprint to sender + receiver (RecomputeAudioCrypto). - UX: ask-for-password on profile create; File -> Change this profile's password (ProfilePasswordDialog); Options -> Profile passwords (manager); a gate that prompts before streaming without a password; and a clear "passwords don't match" / "peer needs to update" message driven by the fingerprint. Cue fixes - CuePlayer (NAudio) replaces System.Media.SoundPlayer, which silently failed on the 96 kHz/24-bit cue WAVs (and any custom file) — cues now play reliably, resampled to 48 kHz/16-bit. Also fixes the Preferences preview button. - Connect/disconnect cues now audio-gated with hysteresis: connected when audio flows OR heartbeat healthy; lost only when audio stops AND heartbeat unreachable. Kills false disconnects and the receive-only "no cues" case. - Honest cue logging (played / muted / not loaded). Smaller - Endpoint stickiness: keep the audio target pinned to the heartbeat-proven address instead of chasing a multi-homed peer's other (unreachable) address. - "Online/offline" label now audio+heartbeat aware, not discovery-only. - "Show what's new after each update" preference (on by default). Docs: About v3.3 block, RELEASE_NOTES, README (encryption as a headline), manual section 12 "Passwords and encryption" (+ renumber), MANUAL.md regenerated. Version 3.2.0 -> 3.3.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
cdcac859c4
commit
959720f54d
@@ -0,0 +1,66 @@
|
||||
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.1–48 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
|
||||
{
|
||||
private readonly string filePath;
|
||||
|
||||
public CuePlayer(string filePath) => this.filePath = filePath;
|
||||
|
||||
public void Play()
|
||||
{
|
||||
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.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user