Skip to content
56 changes: 55 additions & 1 deletion src/sound/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,58 @@ 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
Comment thread
pljones marked this conversation as resolved.
Outdated
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. `CSoundBase` inherits `QThread`, but
nothing here overrides `run()` or calls `start()`, so no such thread exists. Backends keep the
Comment thread
pljones marked this conversation as resolved.
Outdated
callback away from a device that is being re-initialised in two ways, and the ASIO backend is the
exception to both:
Comment thread
pljones marked this conversation as resolved.
Outdated

| backend | audio callback | ignores the callback while stopped | takes `MutexAudioProcessCallback` |
Comment thread
pljones marked this conversation as resolved.
Outdated
|---|---|---|---|
| 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
Comment thread
pljones marked this conversation as resolved.
Outdated
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

- 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