Lock-to-clock always on (remove option); service audio hardcoded to Opus live config
Ed, 2026-07-17. The bigger half of the service crackle fix: raw PCM sounded hideous; Opus at the live-jamming frame with Small packets and lock-to-clock sounds right. Lock to audio clock (TightLatencyMode) is now ALWAYS ON and no longer a user option: - Removed the checkbox from the main window and the service dialog. Nobody ever runs with it off -- off just adds delay. - Main app forces the sender tight at startup; Profile.TightLatencyMode defaults true and is kept only for file compatibility. Load/SaveTightLatencyMode accessors removed. Service audio profile is now fixed and unconfigurable: - Removed the "Audio profile" tab from the service config dialog entirely (two tabs left: Connectivity, Audio send). - The service FORCES Opus + 2.5 ms frame (120 samples) + Small packets + lock-to-clock at runtime in ServiceSendHost.ApplyProfile, ignoring whatever the profile carries, so a stale/hand-edited profile can never put it back on a bad codec. SaveToProfile writes the same fixed values. Robustness: ApplyProfile now swallows a capture Start() failure (with lock-to-clock always on the WASAPI lane is push-mode, which opens the device synchronously and throws if it is invalid/gone) -- presence stays up and the device-watcher/self-heal re-open when a device is available, instead of the exception crashing the service loop. Self-test "Service sender parity" rewritten to prove the service overrides a PCM profile to Opus/120. Gate: 39/39. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ec149ed09e
commit
a450cae66a
@@ -311,19 +311,30 @@ public sealed class ServiceSendHost : IDisposable
|
||||
var plainPassword = string.IsNullOrEmpty(profile.Password) ? "" : RemSoundCrypto.Deobfuscate(profile.Password);
|
||||
sender.AudioKey = string.IsNullOrEmpty(plainPassword) ? null : RemSoundCrypto.DeriveKey(plainPassword);
|
||||
sender.AudioFingerprint = string.IsNullOrEmpty(plainPassword) ? null : RemSoundCrypto.Fingerprint(plainPassword);
|
||||
// Opus frame size follows the send rate the same way the main app does (the "Small" rate
|
||||
// halves the Opus frame) — otherwise the service would encode at a different frame than the
|
||||
// main app would for the identical profile.
|
||||
sender.ConfigureCodec(profile.Codec, MainForm.EffectiveOpusFrameSamples(profile.Codec, profile.OpusFrameSamplesPerChannel, profile.SendRate));
|
||||
sender.SetSendRate(profile.SendRate);
|
||||
sender.SetTightLatency(profile.TightLatencyMode);
|
||||
// The service's audio transport is FIXED to the known-good live-jamming config, regardless of
|
||||
// what the profile carries (the config dialog no longer exposes these — Ed, 2026-07-17). Raw
|
||||
// PCM sounded hideous over the service; Opus at the 2.5 ms live frame with Small packets and
|
||||
// lock-to-clock is what sounds right. Forcing it here means a stale or hand-edited profile can
|
||||
// never put the service back on a bad codec.
|
||||
const AudioTransportCodec serviceCodec = AudioTransportCodec.Opus;
|
||||
const int serviceOpusFrameSamples = 120; // 2.5 ms at 48 kHz
|
||||
const SendRate serviceSendRate = SendRate.Tight; // "Small" packets
|
||||
sender.ConfigureCodec(serviceCodec, MainForm.EffectiveOpusFrameSamples(serviceCodec, serviceOpusFrameSamples, serviceSendRate));
|
||||
sender.SetSendRate(serviceSendRate);
|
||||
sender.SetTightLatency(true); // lock to audio clock — always on
|
||||
// Arm the full set to begin with (nothing is known-dead yet); RefreshSendArming then prunes any
|
||||
// peer the heartbeat can't reach and re-arms it when it recovers.
|
||||
allEndpoints = endpoints.ToArray();
|
||||
armedSignature = null;
|
||||
sender.SetReceivers(endpoints);
|
||||
sender.Configure(specs);
|
||||
sender.Start();
|
||||
// A device-open failure at Start must NOT throw out of the service loop. With lock-to-clock
|
||||
// always on, the WASAPI lane uses push-mode, which opens the endpoint synchronously and throws
|
||||
// (e.g. ArgumentException) if the device id is invalid or the device has vanished. Swallow it:
|
||||
// presence still comes up, and the device-change watcher / self-heal re-open the capture when a
|
||||
// good device appears. Without this guard a disappeared device would crash the whole service.
|
||||
try { sender.Start(); }
|
||||
catch (Exception ex) { log?.Invoke($"service: capture start failed ({ex.GetType().Name}: {ex.Message}) — presence up; will re-open when a device is available"); }
|
||||
SwapMeterDevices(specs); // endpoint-meter readers for the deaf-capture detector
|
||||
// Instant self-heal trigger: session-created notification on the default render device.
|
||||
// Recreated per apply so it re-points at the current default after a device change.
|
||||
|
||||
Reference in New Issue
Block a user