From 4c7b56f6f205895b171039ad9a17c5d454da1696 Mon Sep 17 00:00:00 2001 From: mcfnord Date: Sat, 8 Aug 2026 17:52:51 +0000 Subject: [PATCH 1/7] src/sound/README.md: start documenting the sound design Replaces the "Fixme: The sound design is not yet documented" placeholder with the parts that are load bearing for anyone touching a backend: how Init()'s return value negotiates the buffer size, how many times it is called and by whom, how a driver-initiated buffer size change re-enters the client, and how each backend keeps its audio callback off a device that is being re-initialised. The callback table is the part worth having written down. ASIO is the only backend that neither ignores its callback while stopped nor takes MutexAudioProcessCallback, so CSoundBase::Stop()'s wait for a callback in flight does nothing there and asio/CSound::Stop() waits on ASIOMutex instead. This is a start, not the whole design, so the blanket Fixme is replaced by a list of the areas still missing rather than dropped: device enumeration and SetDev() failure handling, channel selection and mixing, MIDI, latency reporting and the sound card conversion buffer. --- src/sound/README.md | 52 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/sound/README.md b/src/sound/README.md index 8143aea1ab..8c25a8f9ce 100644 --- a/src/sound/README.md +++ b/src/sound/README.md @@ -48,4 +48,54 @@ This folder contains the related files for all sound APIs. ## Documentation of sound design -**Fixme:** The sound design is not yet documented. +This describes how the code behaves today. It covers the device lifecycle and the threading rules +around the audio callback; the areas still missing are listed at the end. + +Each platform provides one `CSound` class deriving from `CSoundBase`, and exactly one of the +subdirectories is compiled in: `asio/` (Windows), `coreaudio-mac/`, `coreaudio-ios/`, `oboe/` +(Android) and `jack/` (where JACK is enabled). + +### Buffer size negotiation + +`Init ( iNewPrefMonoBufferSize )` returns the mono buffer size the device actually accepted, +which may differ from the one requested. `CClient::Init()` uses that return value to find out +which sizes a device supports, so it calls `Init()` four times per invocation: once for each of +`FRAME_SIZE_FACTOR_PREFERRED`, `FRAME_SIZE_FACTOR_DEFAULT` and `FRAME_SIZE_FACTOR_SAFE` to fill +`bFraSiFactPrefSupported`, `bFraSiFactDefSupported` and `bFraSiFactSafeSupported`, then once with +the size selected in the settings. Those three flags drive the enabled state of the buffer delay +radio buttons, and the settings dialog polls them once a second; no signal runs from the sound +device to that dialog. + +A driver may also change the buffer size on its own. `kAsioBufferSizeChange` in the ASIO backend +and JACK's buffer size callback both report that by calling +`EmitReinitRequestSignal ( RS_ONLY_RESTART_AND_INIT )`, which reaches +`CClient::OnSndCrdReinitRequest` and repeats the negotiation above. + +### Start, stop and the audio callback + +`Init()` is only ever entered with the device stopped. Callers that may be running stop it first +and restart it afterwards, which is the `bWasRunning` pattern throughout `client.cpp`. + +The audio callback runs on a thread owned by the driver. Backends keep it away from a device that +is being re-initialised in two ways, and the ASIO backend is the exception to both: + +| backend | audio callback | ignores the callback while stopped | takes `MutexAudioProcessCallback` | +|---|---|---|---| +| JACK | `process()` | yes, `IsRunning()` | yes | +| CoreAudio (macOS) | `callbackIO()` | yes, `bRun` | yes | +| CoreAudio (iOS) | `processBufferList()` | no | yes | +| Oboe | `onAudioReady()` | yes, `!bRun` | yes | +| ASIO | `bufferSwitch()` | no | no, it uses its own `ASIOMutex` | + +`CSoundBase::Stop()` clears `bRun` and then takes `MutexAudioProcessCallback` to wait for a +callback that is already in flight. The ASIO backend never takes that mutex, so on Windows that +wait returns immediately and `CSound::Stop()` waits on `ASIOMutex` instead. + +### Not yet documented + +- device enumeration, and what `SetDev()` does when a device cannot be used +- input and output channel selection, and the input channel mixing in the callbacks +- MIDI: device selection, controller mapping and `ParseMIDIMessage()` +- latency reporting via `GetInOutLatencyMs()` +- the sound card conversion buffer used when a device's buffer size is not a multiple of the + system frame size From 4ea450601f951c3305eaa89cc5ae2b3eccf2176c Mon Sep 17 00:00:00 2001 From: jrd Date: Sat, 8 Aug 2026 22:51:09 +0000 Subject: [PATCH 2/7] src/sound/README.md: note that CSoundBase's QThread is never started CSoundBase derives from QThread, so a reader can reasonably expect a sound thread. There is none: no override of run() and no call to start() exists in the sound layer -- the only two run() overrides in src/ are CHighPrecisionTimer (util.h) and CSocketThread (socket.h). Audio callbacks always arrive on driver-owned threads. Moved here from the src/README.md draft (#3875), where it sat under the thread table; this is the file that introduces CSoundBase. --- src/sound/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/sound/README.md b/src/sound/README.md index 8c25a8f9ce..673e830d75 100644 --- a/src/sound/README.md +++ b/src/sound/README.md @@ -76,8 +76,10 @@ and JACK's buffer size callback both report that by calling `Init()` is only ever entered with the device stopped. Callers that may be running stop it first and restart it afterwards, which is the `bWasRunning` pattern throughout `client.cpp`. -The audio callback runs on a thread owned by the driver. Backends keep it away from a device that -is being re-initialised in two ways, and the ASIO backend is the exception to both: +The audio callback runs on a thread owned by the driver. `CSoundBase` inherits `QThread`, but +nothing here overrides `run()` or calls `start()`, so no such thread exists. Backends keep the +callback away from a device that is being re-initialised in two ways, and the ASIO backend is the +exception to both: | backend | audio callback | ignores the callback while stopped | takes `MutexAudioProcessCallback` | |---|---|---|---| From 9b2bee85b4112bf49a5d7841b5e8d03640548f57 Mon Sep 17 00:00:00 2001 From: jrd Date: Tue, 11 Aug 2026 17:40:48 +0000 Subject: [PATCH 3/7] =?UTF-8?q?docs(src/sound/README.md):=20clarify=20ASIO?= =?UTF-8?q?Mutex=20ownership=20=E2=80=94=20ASIO=20backend=20code,=20not=20?= =?UTF-8?q?Jamulus=20core?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The text now explicitly states that ASIOMutex is defined and owned by the ASIO backend (in asio/sound.h), addressing the review feedback to make clear whether the mutex is owned by ASIO code or Jamulus code. Co-Authored-By: Claude Haiku 4.5 --- src/sound/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/sound/README.md b/src/sound/README.md index 673e830d75..fa19bab16d 100644 --- a/src/sound/README.md +++ b/src/sound/README.md @@ -90,8 +90,10 @@ exception to both: | ASIO | `bufferSwitch()` | no | no, it uses its own `ASIOMutex` | `CSoundBase::Stop()` clears `bRun` and then takes `MutexAudioProcessCallback` to wait for a -callback that is already in flight. The ASIO backend never takes that mutex, so on Windows that -wait returns immediately and `CSound::Stop()` waits on `ASIOMutex` instead. +callback that is already in flight. The ASIO backend is the exception: it defines and owns its +own `ASIOMutex` (in `asio/sound.h`) instead of using the shared `MutexAudioProcessCallback`. +So on Windows, `CSoundBase::Stop()`'s wait returns immediately, and the ASIO-specific +`CSound::Stop()` waits on `ASIOMutex` instead. ### Not yet documented From 9fe067e82f413c4263d04c2c1d27fdd15f434312 Mon Sep 17 00:00:00 2001 From: jrd Date: Tue, 11 Aug 2026 20:24:32 +0000 Subject: [PATCH 4/7] docs(src/sound/README.md): buffer-size notification, Stop() ordering, callback guarding Addresses the remaining review comments. Buffer-size negotiation now states what can be done and by whom: only ASIO (kAsioBufferSizeChange) and JACK have a native change-notification callback. The two CoreAudio backends watch device-identity and route events only, so a size change on its own is invisible to them; Oboe detects the mismatch but logs it rather than renegotiating. Callback guarding is split into two tables: what each backend's own Stop() calls before touching bRun, and where each callback reads the running flag relative to the mutex. IsRunning(), bRun and !bRun are three spellings of one check, but only Oboe's sits before any lock -- JACK's and CoreAudio (macOS)'s run after it, so they skip the processing without saving the mutex contention. CoreAudio (iOS) and ASIO have no flag check at all and depend entirely on their driver-level stop call. Also corrects the subdirectory count in the intro: src/sound/midi-win/ holds CMidi rather than a CSound backend, and the default Windows build compiles it alongside asio/. Co-Authored-By: Claude Opus 5 --- src/sound/README.md | 89 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 67 insertions(+), 22 deletions(-) diff --git a/src/sound/README.md b/src/sound/README.md index fa19bab16d..c4ebfc5427 100644 --- a/src/sound/README.md +++ b/src/sound/README.md @@ -51,9 +51,11 @@ This folder contains the related files for all sound APIs. This describes how the code behaves today. It covers the device lifecycle and the threading rules around the audio callback; the areas still missing are listed at the end. -Each platform provides one `CSound` class deriving from `CSoundBase`, and exactly one of the -subdirectories is compiled in: `asio/` (Windows), `coreaudio-mac/`, `coreaudio-ios/`, `oboe/` -(Android) and `jack/` (where JACK is enabled). +Each platform provides one `CSound` class deriving from `CSoundBase`, and exactly one of the five +backend subdirectories is compiled in: `asio/` (Windows), `coreaudio-mac/`, `coreaudio-ios/`, +`oboe/` (Android) and `jack/` (where JACK is enabled). A sixth subdirectory, `midi-win/`, holds the +Windows MIDI implementation (`CMidi`) rather than a backend; the default Windows build compiles it +alongside `asio/`, while a `CONFIG+=jackonwindows` build takes `jack/` and neither of the other two. ### Buffer size negotiation @@ -66,10 +68,18 @@ the size selected in the settings. Those three flags drive the enabled state of radio buttons, and the settings dialog polls them once a second; no signal runs from the sound device to that dialog. -A driver may also change the buffer size on its own. `kAsioBufferSizeChange` in the ASIO backend -and JACK's buffer size callback both report that by calling +A driver can also change its buffer size on its own, outside any call from Jamulus. Reporting +that back needs a native change-notification callback from the driver API, and only two of the +five backends have one: ASIO's `kAsioBufferSizeChange` (`asioMessages()`, `asio/sound.cpp`) and +JACK's buffer-size callback (`jack/sound.cpp`) both call `EmitReinitRequestSignal ( RS_ONLY_RESTART_AND_INIT )`, which reaches -`CClient::OnSndCrdReinitRequest` and repeats the negotiation above. +`CClient::OnSndCrdReinitRequest` and repeats the negotiation above. The other three backends +can't do this the same way: the two CoreAudio backends only watch device-identity/route events +(`kAudioDevicePropertyDeviceHasChanged`/`IsAlive`/default-device switches on macOS, +`AVAudioSessionRouteChangeNotification` on iOS) and never register a buffer-size-specific +listener, so a size change on its own is invisible to them. Oboe does detect it — +`onAudioInput()` compares the delivered frame count against the requested size on every callback +— but the mismatch is only logged (`qDebug()`), never renegotiated. ### Start, stop and the audio callback @@ -77,23 +87,58 @@ and JACK's buffer size callback both report that by calling and restart it afterwards, which is the `bWasRunning` pattern throughout `client.cpp`. The audio callback runs on a thread owned by the driver. `CSoundBase` inherits `QThread`, but -nothing here overrides `run()` or calls `start()`, so no such thread exists. Backends keep the -callback away from a device that is being re-initialised in two ways, and the ASIO backend is the -exception to both: - -| backend | audio callback | ignores the callback while stopped | takes `MutexAudioProcessCallback` | +nothing here overrides `run()` or calls `start()`, so no such thread exists — nor has it ever: +neither appears anywhere in this file's tracked history, and no other `QThread`-specific method +is used in the sound layer either. The inheritance predates the current driver-callback +architecture and contributes nothing; changing it to `QObject` looks safe from this file alone, +but that wasn't verified further here. + +Every backend but ASIO also overrides `Stop()`, and every override but JACK's calls a +driver-level stop function *before* touching `bRun` at all: + +| backend | own `Stop()` calls first | +|---|---| +| ASIO | (no override — `ASIOStop()` happens inside `CSound::Stop()` itself, see below) | +| CoreAudio (macOS) | `AudioDeviceStop()` + `AudioDeviceDestroyIOProcID()` | +| CoreAudio (iOS) | `AudioOutputUnitStop()` | +| Oboe | `closeStreams()` | +| JACK | nothing — goes straight to `CSoundBase::Stop()` | + +Inside the callback itself, backends differ again, and not just in which flag they read but in +*when* they read it relative to the mutex: + +| backend | audio callback | flag check | mutex behavior | |---|---|---|---| -| JACK | `process()` | yes, `IsRunning()` | yes | -| CoreAudio (macOS) | `callbackIO()` | yes, `bRun` | yes | -| CoreAudio (iOS) | `processBufferList()` | no | yes | -| Oboe | `onAudioReady()` | yes, `!bRun` | yes | -| ASIO | `bufferSwitch()` | no | no, it uses its own `ASIOMutex` | - -`CSoundBase::Stop()` clears `bRun` and then takes `MutexAudioProcessCallback` to wait for a -callback that is already in flight. The ASIO backend is the exception: it defines and owns its -own `ASIOMutex` (in `asio/sound.h`) instead of using the shared `MutexAudioProcessCallback`. -So on Windows, `CSoundBase::Stop()`'s wait returns immediately, and the ASIO-specific -`CSound::Stop()` waits on `ASIOMutex` instead. +| Oboe | `onAudioReady()` | `!bRun`, first line, before any lock | skipped entirely once stopped | +| JACK | `process()` | `IsRunning()`, after the lock | always taken; only the processing is skipped | +| CoreAudio (macOS) | `callbackIO()` | `bRun`, after the lock | always taken; only the processing is skipped | +| CoreAudio (iOS) | `processBufferList()` | none | always taken; always processes | +| ASIO | `bufferSwitch()` | none | always taken (its own `ASIOMutex`); always processes | + +`IsRunning()`, `bRun` and `!bRun` all read the same flag (`IsRunning()` is `return bRun;`) — +three spellings of one check. But only Oboe's placement actually avoids the mutex; JACK's and +CoreAudio (macOS)'s checks run *after* the lock is already held, so they cost the same +mutex-contention as not checking at all and only save the processing work itself. + +`CSoundBase::Stop()` clears `bRun` and then briefly takes `MutexAudioProcessCallback`, releasing +it as soon as `Stop()` returns: +```cpp +void CSoundBase::Stop() { + bRun = false; + QMutexLocker locker ( &MutexAudioProcessCallback ); +} +``` +CoreAudio (iOS) and ASIO have no flag check anywhere in their callback, so they depend entirely +on their own driver-level stop call above (or, for ASIO, on `ASIOStop()` inside its own +`CSound::Stop()`) actually preventing further callbacks — if the driver fires one more callback +after that call returns, it runs to completion regardless of `bRun`. Whether `AudioOutputUnitStop` +/ `ASIOStop` are synchronous enough to rule that out wasn't tested here; it would need real +hardware. + +ASIO defines and owns its own `ASIOMutex` (in `asio/sound.h`) instead of using the shared +`MutexAudioProcessCallback`. Its own `CSound::Stop()` calls `ASIOStop()` first, then +`CSoundBase::Stop()` (whose wait on the unused `MutexAudioProcessCallback` returns immediately +for this backend), then waits on `ASIOMutex` directly to confirm the callback thread is done. ### Not yet documented From 484fa0ab53332256c65d12c7d3e7bee4646cb623 Mon Sep 17 00:00:00 2001 From: jrd Date: Tue, 11 Aug 2026 20:29:30 +0000 Subject: [PATCH 5/7] docs(src/sound/README.md): why ASIO owns a separate mutex The file said what ASIO does differently but not why, so the difference read as an unexplained inconsistency. It is chronological: ASIOMutex was added with the ASIO backend itself in 5eb86941 (2008-07-12), when ASIO was the only backend and CSoundBase did not yet exist (3fb2d9ca, 2009-02-22); its drain-on-stop wait followed in 73f408e4 (2011-12-27). The shared MutexAudioProcessCallback arrived in ecff80fc (2020-08-26) to fix a crash on quick JACK reconfiguration, touching linux/sound.cpp and soundbase.{h,cpp} only, and was never extended to ASIO. The two are also not interchangeable today: ASIOMutex is held across the whole of CSound::Init(), which no other backend does, and ASIO's Stop() uses tryLock ( 5000 ) where CSoundBase::Stop() blocks unconditionally. Since ASIO's stop can return with a callback still in flight, the Init() lock is what keeps ASIOCreateBuffers() off a live bufferSwitch(). The intro said the file describes how the code behaves today, while the QThread paragraph already explained how that inheritance arose; it now says origins are given where they are needed to read the behaviour correctly. Co-Authored-By: Claude Opus 5 --- src/sound/README.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/sound/README.md b/src/sound/README.md index c4ebfc5427..fa007aee8d 100644 --- a/src/sound/README.md +++ b/src/sound/README.md @@ -49,7 +49,9 @@ This folder contains the related files for all sound APIs. ## Documentation of sound design This describes how the code behaves today. It covers the device lifecycle and the threading rules -around the audio callback; the areas still missing are listed at the end. +around the audio callback; the areas still missing are listed at the end. Where today's behaviour +only makes sense in light of how it arose, the origin is given as well, so that an accident of +history is not read as a deliberate design. Each platform provides one `CSound` class deriving from `CSoundBase`, and exactly one of the five backend subdirectories is compiled in: `asio/` (Windows), `coreaudio-mac/`, `coreaudio-ios/`, @@ -140,6 +142,25 @@ ASIO defines and owns its own `ASIOMutex` (in `asio/sound.h`) instead of using t `CSoundBase::Stop()` (whose wait on the unused `MutexAudioProcessCallback` returns immediately for this backend), then waits on `ASIOMutex` directly to confirm the callback thread is done. +The two are not interchangeable, because `ASIOMutex` covers more ground. It is also held across +the whole of `CSound::Init()` — spanning `ASIODisposeBuffers()`, `ASIOCreateBuffers()` and the +`vecsMultChanAudioSndCrd` reallocation — whereas no other backend locks anything in its `Init()`. +The waits differ too: `CSoundBase::Stop()` blocks unconditionally on its `QMutexLocker`, while +ASIO's `Stop()` uses `tryLock ( 5000 )` and carries on regardless if the callback has not finished +within five seconds. The two differences compound: because ASIO's stop can return while a callback +is still in flight, the lock held across `Init()` is what actually keeps `ASIOCreateBuffers()` off +a live `bufferSwitch()`. + +That there are two mutexes at all is chronological rather than a design decision. `ASIOMutex` was +added with the ASIO backend itself in `5eb86941` (2008-07-12), when ASIO was the only backend and +`CSoundBase` did not yet exist (`3fb2d9ca`, 2009-02-22); its drain-on-stop wait followed in +`73f408e4` (2011-12-27). The shared `MutexAudioProcessCallback` arrived nine years after that, in +`ecff80fc` (2020-08-26), to fix a crash when the JACK backend was reconfigured quickly; that commit +touches `linux/sound.cpp` and `soundbase.{h,cpp}` and nothing else. It is a later, independent +re-implementation of a guard ASIO already had, and it was never extended to ASIO. Collapsing the +two into one would therefore not be a rename: it would have to preserve the coverage across +`Init()` and settle which of the two wait policies applies. + ### Not yet documented - device enumeration, and what `SetDev()` does when a device cannot be used From ca0f56749ca0cc0e15b6d796750d36be92df0740 Mon Sep 17 00:00:00 2001 From: jrd Date: Wed, 12 Aug 2026 23:42:40 +0000 Subject: [PATCH 6/7] src/sound/README.md: order the mutex-behavior table to match the Stop() table pljones on #3873: keep a consistent order between the backend-based tables. The Stop() table orders ASIO, CoreAudio (macOS), CoreAudio (iOS), Oboe, JACK; the callback/mutex table below it didn't match. Reordered rows only, no content changed. Co-Authored-By: Claude Sonnet 5 --- src/sound/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sound/README.md b/src/sound/README.md index fa007aee8d..a13223be03 100644 --- a/src/sound/README.md +++ b/src/sound/README.md @@ -111,11 +111,11 @@ Inside the callback itself, backends differ again, and not just in which flag th | backend | audio callback | flag check | mutex behavior | |---|---|---|---| -| Oboe | `onAudioReady()` | `!bRun`, first line, before any lock | skipped entirely once stopped | -| JACK | `process()` | `IsRunning()`, after the lock | always taken; only the processing is skipped | +| ASIO | `bufferSwitch()` | none | always taken (its own `ASIOMutex`); always processes | | CoreAudio (macOS) | `callbackIO()` | `bRun`, after the lock | always taken; only the processing is skipped | | CoreAudio (iOS) | `processBufferList()` | none | always taken; always processes | -| ASIO | `bufferSwitch()` | none | always taken (its own `ASIOMutex`); always processes | +| Oboe | `onAudioReady()` | `!bRun`, first line, before any lock | skipped entirely once stopped | +| JACK | `process()` | `IsRunning()`, after the lock | always taken; only the processing is skipped | `IsRunning()`, `bRun` and `!bRun` all read the same flag (`IsRunning()` is `return bRun;`) — three spellings of one check. But only Oboe's placement actually avoids the mutex; JACK's and From 0e4ae3c58acaf4b82bbd5ecf8510ea16cfc941d8 Mon Sep 17 00:00:00 2001 From: jrd Date: Thu, 13 Aug 2026 01:22:59 +0000 Subject: [PATCH 7/7] src/sound/README.md: correct the Stop() claim, name the three guard mechanisms Three review points from pljones on #3873, plus a defect in our own pushed text found while checking them. The correctness fix: "Every backend but ASIO also overrides Stop()" was false, and the ASIO table row said "no override". All five backends override Stop() (asio/sound.cpp:524, coreaudio-mac 725, jack 197, oboe 193, coreaudio-ios/sound.mm), and ASIO's is one of the four that calls a driver-level stop first: ASIOStop(), then CSoundBase::Stop(), then a tryLock on ASIOMutex. JACK's is the only override that goes straight to the base class. "I make that three ways, then. It should be written plainly." -- the three mechanisms are now named up front (driver-level stop call, bRun flag check, shared mutex) before the tables that detail them. No two backends combine the three the same way. "Probably best recorded in an issue rather than this file." -- the QThread paragraph loses its history and its QObject recommendation, keeping only the fact that no thread is ever created. Also corrects the iOS row of the callback table: the driver-invoked callback is recordingCallback(), not processBufferList(), and the mutex is taken inside the latter only -- the output copy back into ioData runs unlocked. Co-Authored-By: Claude Opus 5 --- src/sound/README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/sound/README.md b/src/sound/README.md index a13223be03..078d3664d1 100644 --- a/src/sound/README.md +++ b/src/sound/README.md @@ -89,18 +89,19 @@ listener, so a size change on its own is invisible to them. Oboe does detect it and restart it afterwards, which is the `bWasRunning` pattern throughout `client.cpp`. The audio callback runs on a thread owned by the driver. `CSoundBase` inherits `QThread`, but -nothing here overrides `run()` or calls `start()`, so no such thread exists — nor has it ever: -neither appears anywhere in this file's tracked history, and no other `QThread`-specific method -is used in the sound layer either. The inheritance predates the current driver-callback -architecture and contributes nothing; changing it to `QObject` looks safe from this file alone, -but that wasn't verified further here. +nothing overrides `run()` or calls `start()`, so no such thread is ever created. -Every backend but ASIO also overrides `Stop()`, and every override but JACK's calls a -driver-level stop function *before* touching `bRun` at all: +Three separate mechanisms keep that callback off a device that is stopping or being +re-initialised: the driver-level stop call in a backend's own `Stop()`, the `bRun` flag the +callback may test, and a mutex the callback and `Stop()` share. All three are in use, but no two +backends combine them the same way. + +All five backends override `Stop()`, and all but JACK's call a driver-level stop function before +`CSoundBase::Stop()` clears `bRun`: | backend | own `Stop()` calls first | |---|---| -| ASIO | (no override — `ASIOStop()` happens inside `CSound::Stop()` itself, see below) | +| ASIO | `ASIOStop()` — then, after the base call, waits on `ASIOMutex`, see below | | CoreAudio (macOS) | `AudioDeviceStop()` + `AudioDeviceDestroyIOProcID()` | | CoreAudio (iOS) | `AudioOutputUnitStop()` | | Oboe | `closeStreams()` | @@ -113,8 +114,8 @@ Inside the callback itself, backends differ again, and not just in which flag th |---|---|---|---| | ASIO | `bufferSwitch()` | none | always taken (its own `ASIOMutex`); always processes | | CoreAudio (macOS) | `callbackIO()` | `bRun`, after the lock | always taken; only the processing is skipped | -| CoreAudio (iOS) | `processBufferList()` | none | always taken; always processes | -| Oboe | `onAudioReady()` | `!bRun`, first line, before any lock | skipped entirely once stopped | +| CoreAudio (iOS) | `recordingCallback()` | none | taken in `processBufferList()` only; output copy unlocked | +| Oboe | `onAudioReady()` | `!bRun`, before any lock | skipped once stopped; only `onAudioOutput()` locks | | JACK | `process()` | `IsRunning()`, after the lock | always taken; only the processing is skipped | `IsRunning()`, `bRun` and `!bRun` all read the same flag (`IsRunning()` is `return bRun;`) — @@ -130,12 +131,11 @@ void CSoundBase::Stop() { QMutexLocker locker ( &MutexAudioProcessCallback ); } ``` -CoreAudio (iOS) and ASIO have no flag check anywhere in their callback, so they depend entirely -on their own driver-level stop call above (or, for ASIO, on `ASIOStop()` inside its own -`CSound::Stop()`) actually preventing further callbacks — if the driver fires one more callback -after that call returns, it runs to completion regardless of `bRun`. Whether `AudioOutputUnitStop` -/ `ASIOStop` are synchronous enough to rule that out wasn't tested here; it would need real -hardware. +CoreAudio (iOS) and ASIO have no flag check anywhere in their callback, so they depend entirely on +their own driver-level stop call — `AudioOutputUnitStop()` and `ASIOStop()` — actually preventing +further callbacks: if the driver fires one more after that call returns, it runs to completion +regardless of `bRun`. Whether either is synchronous enough to rule that out wasn't tested here; it +would need real hardware. ASIO defines and owns its own `ASIOMutex` (in `asio/sound.h`) instead of using the shared `MutexAudioProcessCallback`. Its own `CSound::Stop()` calls `ASIOStop()` first, then