Issue #23's log proved the service can be perfectly healthy on the network
(discoverable, heartbeats, peers armed) while its WASAPI loopback capture is
open but STARVED — zero buffers ever delivered on the boot lock screen, so
peers connect and hear nothing. And the log was blind below 'streaming N
sources' because the service never wired the audio engine's diagnostics in.
- sender.Diagnostic now feeds the service log: capture opens/failures, backend
switches, the silence keepalive result, composite mode. The keepalive already
runs on every loopback device (SilentRenderKeepAlive via MixingEngine); on the
next boot repro the log will show whether it started or was refused in
session 0 pre-login — the deciding fact.
- New capture-health watch on the existing 1s tick: logs 'first capture
callback received' once audio genuinely flows, and an explicit 'ZERO audio
callbacks after 10s' line naming the starved-loopback fault instead of the
log just going quiet.
Gate 37/37.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Checked the service's connection handling against the networking issues that
shaped the main app. The service reused the low-level protocol components
(discovery, heartbeat, listener, sender), so it inherits their behaviour — but
it was MISSING the app's higher-level connection management from
MainForm.RefreshAudioReceivers: it called SetReceivers once with ALL configured
peers and blind-sent forever, even into dead addresses. That's exactly issue #8
(streaming into a peer that's gone) and it ignored issue #15 (retry/recover).
Fix: the service now streams ONLY to peers the heartbeat can reach, drops any
that stay unreachable past a 30s grace window, and re-arms them the moment they
recover — the same logic (and 30s threshold) as the app. Runs on the service's
existing 1s poll tick (no new timer, no background pile-up). Send-only, so no
"actively receiving" carve-out.
- ServiceNetworkPresence.PeerHealthSnapshot() exposes the heartbeat health.
- ServiceSendHost.ComputeArmedEndpoints (pure) + RefreshSendArming, wired into
RunLoopCore.
- Self-test "Service reachability-gated sending": reachable armed, long-
unreachable dropped, grace-window kept, no-data arms all. Gate 35/35.
Coverage of the other networking issues: multi-homed LAN+VPN (#18) is a
receiver-side allow-list fix — N/A to a send-only service, and its sender-side
support (announcing on all interfaces) is inherited from PeerDiscoveryService.
Forced/locked IPs (#17/#7): the service resolves peers literally and never
follows names, so it's inherently "locked" (what #17 asked for). Device
recovery (#5): already built. NOT built: discovery-based name-following (the
app can chase a peer whose IP changes); the service stays on its configured
addresses by design — flagged for Ed to decide if the service needs it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The send-only service could never be found or connected to — it only pushed
audio blindly to fixed peer addresses, with no beacon and nothing listening.
So a phone could neither discover it nor dial it. This gives the service a real
network presence built from the SAME components the interactive app uses, wired
the same way, so its discovery / heartbeat / NAT-pinhole / relay behaviour is
identical to the app's — which is what makes it "one identity" (both announce
under the machine name and pair through the relay the same way). Works LAN and,
inheriting the app's relay path, across the internet.
New ServiceNetworkPresence (reuses PeerDiscoveryService + AudioReceiver listener
+ HeartbeatService, wired to the host's AudioSender):
- Discoverable: announces send-only under the machine name (LAN broadcast +
unicast to the configured peers for across-the-internet).
- Reachable: binds the well-known audio-port listener; PLAYBACK stays OFF
(send-only never plays received audio — the listener only carries
heartbeat/pairing).
- Pairable: heartbeat pings the peers (opens the NAT pinhole, drives relay
pairing); replies route back on the listener (LAN) or the sender socket
(relay).
Integrated into ServiceSendHost: comes up alongside the sender while streaming,
and — critically — tears ALL the way down to a shell on Suspend (stop
announcing, unbind the port, stop the heartbeat) so the service and the
interactive app never both hold the network. A brief dropout on that handover
is accepted (Ed's call); only one owns the network at a time.
Tests: "Service network presence" (Start binds the listener + comes up; Stop
unbinds to a shell; re-startable). "Service send host" now also asserts the
presence comes up with streaming and drops to a shell on Suspend. Gate 33/33.
NOTE: the live discover/connect/relay path can only be proven by the tester's
phone — the headless tests prove the lifecycle and teardown, not the internet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local checkpoint - NOT for public release. A "what else does a service need" pass.
- AUTO-RESTART ON CRASH: DoInstall now sets sc failure actions (restart 5s/10s/then 60s,
reset daily). Without this a crashed service stays dead until reboot - fatal for an
always-on streamer.
- FINDABLE LOGS: --run-service redirects the service's data dir to the machine-wide
ProgramData\RemSound\service location, so its log sits next to its profile instead of
buried in the SYSTEM account's AppData.
- POWER RESUME: the service handles OnPowerEvent and re-opens capture on wake (audio
devices re-initialise after sleep; the device-change watcher usually catches it, but a
resume doesn't always fire an endpoint change, so we re-open explicitly).
- Start/stop already auto-log to the Windows Event Log via ServiceBase.
Test: "Service registration args" now also checks the audio-service dependency and the
auto-restart failure args. Gate 27/27.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local checkpoint - NOT for public release. Ed: the service profile must never be
reachable except through the Service menu.
Also fixes a real bug: the service runs as SYSTEM, whose per-user data folder is NOT the
interactive user's - so a profile saved in the user's profiles folder (or AppConfig, both
per-user) was invisible to the service. It would have idled, never streaming.
- New RemSound.Core.ServiceStore: the service profile + its settings (logging) live in a
MACHINE-WIDE ProgramData\RemSound\service location - same absolute path for the user
(config dialog) and SYSTEM (service). Moved ServiceProfileName/ServiceLoggingEnabled off
AppConfig (per-user) onto this store.
- ServiceSendHost.FromConfig + RemSoundService now read ServiceStore; ConfigureServiceProfile
saves there (and migrates + deletes any profile left in the old user-folder location).
- Because it's no longer in the user's profiles folder, it can't appear in the startup
picker, File->Open, Recent profiles, or the password manager (all of which read the user
ProfileStore); the reserved-title filter in ListProfileTitles stays as belt-and-braces.
- Password button renamed "Set service profile password".
- New self-test "Service profile isolation": store is under ProgramData, the reserved title
is filtered from the listing, and it round-trips through the machine-wide store.
Gate 27/27.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local checkpoint - NOT for public release. Audited ServiceSendHost against MainForm's
send path (Ed: make the service reuse the same code, be just as stable). Three real
divergences found and fixed:
1. ENCRYPTION FINGERPRINT (critical): the host set sender.AudioKey but NOT
sender.AudioFingerprint. The main app (RecomputeAudioCrypto) sets both, and the peer
verifies the fingerprint before accepting a stream - so the service's encrypted audio
would have been REJECTED at the far end. Now derives and sets both from the password.
2. OPUS FRAME: the main app applies EffectiveOpusFrameSamples (the "Small" send rate
halves the Opus frame); the host passed the raw frame, so it would encode differently
than the main app for the same profile. Now reuses MainForm.EffectiveOpusFrameSamples
(made internal - same code, not a copy).
3. PEER PORT: send target fell back to the profile's LOCAL AudioPort; the correct default
is RemPacket.DefaultPeerDialPort (what the main app's manual-peer path uses). Same value
today but the right constant.
Reviewed and OK: sender defaults to WasapiOnly (no SetAudioMode needed); BuildSendSpecs
matches ApplySendSources for explicit-device profiles; default-device changes are covered
by the device-change watcher; direct-send (no relay/StartReceiving) is the intended v1 scope.
New self-test "Service sender parity" asserts key + fingerprint + effective Opus frame
match the main app. Gate 26/26.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local checkpoint - NOT for public release. Ed: don't add continuous background checks
(they've piled up before) - reuse the disconnect/reconnect mechanism the main app uses.
Replaced the 5s packet-flow health poll with AudioDeviceChangeNotifier - the SAME
event-driven watcher the main window uses. It fires only when a device is added/removed/
changes state or the default changes (nothing polls, nothing accumulates - it's a single
registered COM callback, disposed with the host). While the service intends to send, that
event re-opens capture, covering: the audio stack finishing coming up at boot, a device
plugged/unplugged, and the audio service restarting. Debounced (a hot-plug fires a burst).
Also dropped the per-tick Resume retry: now one start attempt per app-absence, then the
device watcher drives any re-open. The only remaining periodic thing is the tiny 1s
presence-token check for the app-yield (a mutex probe - allocates nothing, accumulates
nothing). Gate 25/25.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local checkpoint - NOT for public release. Answers "what if it looks for sound
devices before their services are up?"
Two layers now:
- depend= Audiosrv/AudioEndpointBuilder (prior commit) makes Windows start the service
only once the audio services are running.
- Self-heal in the run loop: while it should be sending, if no packets have flowed for
~5s then no capture is actually running (endpoints not fully ready at boot, a device
dropped, or the audio service restarted) - it re-opens the capture. And while NOT
sending it already re-tries every second, so a slow-to-appear audio stack or a device
that returns later is picked up automatically.
So even if the service races ahead of the endpoints being fully enumerated, it keeps
retrying/re-opening until audio actually flows, rather than sitting silent. Gate 25/25.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local checkpoint - NOT for public release. First increment of the send-only
Windows service feature (design in memory/project_remsound_service).
THE SPIKE PASSED: the send engine runs fully headless (no window, no message
pump) and streams, proven by a real self-test — the one genuine unknown that
gated the whole feature. Also proves the app-yield model end to end.
What's in this increment (all headless, all tested, 19/19 gate):
- AppConfig: ServiceProfileName + ServiceLoggingEnabled (machine-wide).
- InteractivePresence (Core): the cross-session app-yield token. App holds a
Global\ mutex for its lifetime; the service checks it and yields while an
interactive app is present, resuming when it closes OR crashes (OS frees the
mutex). Name-parameterised internal seams for isolated testing.
- ServiceSendHost (App): loads a send-only profile and streams it to its peers,
WASAPI-only, no ASIO/receive. ApplyProfile/Suspend/Resume + a RunLoop that
drives them from the presence token with a settle delay. v1 sends to direct
peer addresses (LAN/port-forwarded); NAT/relay discovery stays the app's job.
- Program.cs: the interactive app now acquires the presence token at startup so
a future service yields to it.
- Tests: "Service app-yield token" (held=present, released=absent) and "Service
send host (headless stream + yield)" — streams a captured device to a local
receiver over loopback, verifies start/suspend/resume, then drives the full
RunLoop against the token (held=suspended, released=resumes-and-flows).
Still to come (later increments): the --run-service entry + Windows-service
registration, the Service menu, the 3-tab config dialog, updater integration,
docs. None user-facing yet, so nothing deployed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>