Skip to content

Fix SSTV capture recovery after USB soundcard stall - #188

Merged
magicbug merged 2 commits into
mainfrom
cursor/sstv-portaudio-recovery-3d11
Oct 27, 2026
Merged

magicbug merged 2 commits into
mainfrom
cursor/sstv-portaudio-recovery-3d11

Conversation

@magicbug

Copy link
Copy Markdown
Owner

Root cause

When a macOS USB audio device stops delivering samples (without the app sleeping), PortAudio often keeps the old stream handle closed but leaves the host in a state where opening a new stream on the same device index fails (PortAudioException: Error opening PortAudio Stream). The device name still appears in enumeration, so OscarWatch retried StartCapture every 3 seconds forever.

PortAudio is initialised once per process. A full Pa_Terminate / Pa_Initialize cycle clears that stale state, but only when no other PortAudio stream is open. Several services (SSTV, FT4, Terminal modem, pass recording, alert tones) each called PortAudio.Initialize independently, and pass recording called PortAudio.Terminate on dispose, which made a naive global refresh unsafe.

Fix

  1. PortAudioRuntime – single probe/init path, reference count of open streams, and TryRefreshHost() (Terminate/Initialize) when the count is zero.
  2. Capture reopen – SSTV (and FT4/recording/terminal) stop the local stream, retry open, and on failure refresh the PortAudio host when safe, then retry again.
  3. CaptureStallWatch – exponential backoff on failed reopens (3 s → 60 s cap) instead of a fixed 3 s loop.
  4. SSTV UI – new Sstv.Status.CaptureRetrying status while the soundcard is stalled/reconnecting; clears when audio returns.
  5. ThrottledLogger – rotator connect, rig connect, CI-V port open, and SSTV/FT4 reopen failures log the full stack trace once, then suppress repeats for five minutes (with a summary line).

Tests

  • CaptureStallWatchTests (backoff)
  • ThrottledLoggerTests
  • PortAudioRuntimeTests (refresh skipped while a stream is registered)
  • Full OscarWatch.Tests suite passes on Linux CI configuration.

Manual test (macOS)

  1. Start SSTV live decode on a USB soundcard (e.g. IC-9700 USB codec).
  2. Confirm pictures decode and the waterfall moves.
  3. Unplug the USB cable (or use Audio MIDI Setup to simulate a device reset), wait until capture stalls (~3 s).
  4. Confirm the SSTV status shows “No audio from the soundcard. Reconnecting (attempt N)…” instead of “Listening”.
  5. Replug the device (or restore the audio path) without quitting OscarWatch.
  6. Confirm capture recovers, the waterfall resumes, and decoding works within the same session.
  7. Optional: check logs for a single full reopen-failure stack trace followed by throttled summaries, not thousands of identical traces.
Open in Web Open in Cursor 

When a USB input stops delivering audio, PortAudio can keep stale host state
so reopening the stream fails until Terminate/Initialize. Add a shared
PortAudioRuntime that tracks open streams and refreshes the host when safe.

SSTV shows a reconnecting status, backs off failed reopen attempts, and the
same recovery path is wired for FT4, pass recording, and terminal audio.
Rotator, rig, and CI-V connect failures log the full stack trace once then
summarise repeats.

Co-authored-by: Peter Goodhall <magicbug@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Stream-lifetime races and unbalanced accounting can invalidate active audio handles, while recovery paths retain stale state or block host refresh.

8 open issues
What changed in this PR

This PR aims to restore SSTV capture after USB soundcard stalls without restarting OscarWatch, while centralizing PortAudio lifecycle management across audio services.

Changes:

  • Introduces shared PortAudio initialization, stream accounting, and host-refresh recovery.
  • Adds capture retry backoff and localized SSTV reconnection status.
  • Throttles repeated hardware connection failures and adds focused tests.
File Description
OscarWatch/​ViewModels/​SstvViewModel.cs Displays capture reconnection status.
OscarWatch/​Terminal/​PacketAudioService.cs Adopts shared initialization and stream accounting.
OscarWatch/​Sstv/​SstvReceiverService.cs Tracks capture health and reopen results.
OscarWatch/​Sstv/​SstvAudioService.cs Adds host-refresh capture recovery.
OscarWatch/​Services/​PlatformAlertSoundService.cs Integrates alert playback with shared runtime.
OscarWatch/​Rotator/​RotatorController.cs Throttles connection failure logs.
OscarWatch/​Rig/​RigController.cs Throttles rig connection failure logs.
OscarWatch/​Rig/​IcomCivDriverBase.cs Throttles CI-V transport failure logs.
OscarWatch/​Resources/​Strings.zh-CN.resx Adds Chinese reconnection text.
OscarWatch/​Resources/​Strings.th.resx Adds Thai reconnection text.
OscarWatch/​Resources/​Strings.ru.resx Adds Russian reconnection text.
OscarWatch/​Resources/​Strings.resx Adds default English reconnection text.
OscarWatch/​Resources/​Strings.pt-BR.resx Adds Brazilian Portuguese reconnection text.
OscarWatch/​Resources/​Strings.ja.resx Adds Japanese reconnection text.
OscarWatch/​Resources/​Strings.it.resx Adds Italian reconnection text.
OscarWatch/​Resources/​Strings.id.resx Adds Indonesian reconnection text.
OscarWatch/​Resources/​Strings.es.resx Adds Spanish reconnection text.
OscarWatch/​Resources/​Strings.de.resx Adds German reconnection text.
OscarWatch/​Recording/​PortAudioRuntime.cs Centralizes initialization, accounting, and host refresh.
OscarWatch/​Recording/​PortAudioRecordingService.cs Uses shared runtime and retries capture opening.
OscarWatch/​Ft4/​Ft4ModemService.cs Reports reopen results and throttles failures.
OscarWatch/​Ft4/​Ft4AudioService.cs Adds shared accounting and capture recovery.
OscarWatch/​Diagnostics/​ThrottledLogger.cs Adds warning suppression and periodic summaries.
OscarWatch.Tests/​ThrottledLoggerTests.cs Tests warning suppression and reset.
OscarWatch.Tests/​PortAudioRuntimeTests.cs Tests refresh rejection with a registered stream.
OscarWatch.Tests/​CaptureStallWatchTests.cs Tests retry delay growth and reset.
OscarWatch.Core/​Recording/​CaptureStallWatch.cs Adds capped exponential retry backoff.

Review effort: Balanced

Comment on lines +217 to +219
_stream = OpenCaptureStream(
deviceIndex,
deviceInfo,
Comment on lines +227 to +232
_wavWriter = new WavWriter(outputPath, actualSampleRate, (short)channels);
StartWriterThread();

_captureActive = true;
_stream.Start();
PortAudioRuntime.NotifyStreamOpened();
if (!_initialized && !EnsureInitialized())
return false;

if (Volatile.Read(ref _activeStreams) > 0)
finally
{
Log.Debug(ex, "PortAudio alert stream stop failed");
Recording.PortAudioRuntime.NotifyStreamClosed();
Comment on lines +242 to +243
_input.Start();
PortAudioRuntime.NotifyStreamOpened();
Comment on lines +146 to +148
StopCaptureUnlocked();
if (!PortAudioRuntime.TryRefreshHost())
throw firstError;
Comment on lines +84 to +86
_initialized = PortAudioRuntime.EnsureInitialized();
if (!_initialized)
_initError = "PortAudio is not available.";
_portAudioReady = true;
}

return PlayPcmViaPortAudio(AlertToneWav.CreatePcm16());
A failed recording or terminal start could decrement another service's stream count, and pass recording retried a device index that a host refresh may have given to a different soundcard. Dispose the alert stream before releasing it, and check the shared runtime on each recording and alert attempt.
@magicbug
magicbug merged commit 10f326a into main Oct 27, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants