From 4c7b56f6f205895b171039ad9a17c5d454da1696 Mon Sep 17 00:00:00 2001 From: mcfnord Date: Sat, 8 Aug 2026 17:52:51 +0000 Subject: [PATCH 1/2] 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/2] 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` | |---|---|---|---|