Repository navigation
Fix SSTV capture recovery after USB soundcard stall - #188
Merged
Merged
Conversation
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>
There was a problem hiding this comment.
🟡 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
Re-resolve device identity after host refresh · New Balance stream registration before fallible startup · New Reserve streams before native device setup · New Dispose alert stream before unregistering · New Register input before starting it · New Refresh both FT4 capture and output streams · New Recheck PortAudio initialization on every operation · New Recheck PortAudio initialization before alert playback · New
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


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 retriedStartCaptureevery 3 seconds forever.PortAudio is initialised once per process. A full
Pa_Terminate/Pa_Initializecycle clears that stale state, but only when no other PortAudio stream is open. Several services (SSTV, FT4, Terminal modem, pass recording, alert tones) each calledPortAudio.Initializeindependently, and pass recording calledPortAudio.Terminateon dispose, which made a naive global refresh unsafe.Fix
PortAudioRuntime– single probe/init path, reference count of open streams, andTryRefreshHost()(Terminate/Initialize) when the count is zero.CaptureStallWatch– exponential backoff on failed reopens (3 s → 60 s cap) instead of a fixed 3 s loop.Sstv.Status.CaptureRetryingstatus while the soundcard is stalled/reconnecting; clears when audio returns.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)ThrottledLoggerTestsPortAudioRuntimeTests(refresh skipped while a stream is registered)OscarWatch.Testssuite passes on Linux CI configuration.Manual test (macOS)