Skip to content

src/sound/README.md: start documenting the sound design - #3873

Open
mcfnord wants to merge 7 commits into
jamulussoftware:mainfrom
mcfnord:docs-sound-design
Open

src/sound/README.md: start documenting the sound design#3873
mcfnord wants to merge 7 commits into
jamulussoftware:mainfrom
mcfnord:docs-sound-design

Conversation

@mcfnord

@mcfnord mcfnord commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

MY LLM WROTE:

Short description of changes

src/sound/README.md ends with "Fixme: The sound design is not yet documented." This makes a start on that section.

It describes how the code behaves today, not why it was designed that way — intent is not something a reader of the source can verify, so none is asserted. It is also not the whole design, so the blanket Fixme is replaced by a list of the areas still missing rather than dropped.

What the new section covers:

  • Buffer size negotiationInit() returns the size the device actually accepted, which is how CClient::Init() discovers which sizes are supported, and why it calls Init() four times per invocation. Also that the three bFraSiFact*Supported flags drive the buffer delay radio buttons, and that the settings dialog polls them once a second rather than being signalled.
  • Driver-initiated size changeskAsioBufferSizeChange and JACK's buffer size callback both arrive through EmitReinitRequestSignal ( RS_ONLY_RESTART_AND_INIT ).
  • Start, stop and the audio callback — that Init() is only ever entered with the device stopped, and a table of which backend ignores its audio callback while stopped and which takes MutexAudioProcessCallback. ASIO is the exception to both, which is why CSoundBase::Stop()'s wait for a callback in flight does nothing on Windows and asio/CSound::Stop() waits on ASIOMutex instead.
  • Not yet documented — device enumeration and SetDev() failure handling, channel selection and mixing, MIDI, GetInOutLatencyMs(), and the sound card conversion buffer.

CHANGELOG: SKIP

Context: Fixes an issue?

No issue. The material comes out of #3869, where the buffer size display question (issuecomment-5227331894) turned into the lifecycle and callback rules written down here.

Does this change need documentation? What needs to be documented and how?

This is the documentation. Nothing is needed on the website: it is developer-facing detail about one source folder, which is what src/sound/README.md already exists to hold.

Status of this Pull Request

Working implementation. Every statement in it is checkable against the tree at the commit it was written on, and the table was read off all five backends rather than assumed.

What is missing until this pull request can be merged?

Review, and a decision on scope: whether a partial section plus an explicit "not yet documented" list is the right shape for that Fixme, or whether it should stay a placeholder until the whole design is covered.

Checklist

  • I've verified that this Pull Request follows the general code principles
  • I tested my code and it does what I want
  • My code follows the style guide
  • I waited some time after this Pull Request was opened and all GitHub checks completed without errors.
  • I've filled all the content above

No checks run on this one: autobuild.yml carries paths-ignore: '**README.md' and coding-style-check.yml only triggers on **.cpp/**.h, so the fourth box stays unticked rather than claiming a green run that never happened.

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.
@ann0see

ann0see commented Aug 8, 2026

Copy link
Copy Markdown
Member

I assume that's mostly human written or not?

@mcfnord

mcfnord commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

I assume that's mostly human written or not?

No. The LLM wrote that. What makes you think a human did? The lack of "MY LLM WROTE:" prefix? Or something else?

@ann0see

ann0see commented Aug 8, 2026

Copy link
Copy Markdown
Member

No. This is fine. It sounds a bit more natural than the other LLM responses though.

@mcfnord

mcfnord commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

No. This is fine. It sounds a bit more natural than the other LLM responses though.

Interesting feedback. Very frequently I ask my LLM to speak in comments "with extreme concision" partly because I am sensitive to the "wall of text" criticism. So I'm often tilting it toward saying the bare minimum, as a way to avoid overwhelming the humans. Those humans who want to know will parse the dense prose.

Perhaps extreme concision isn't helpful. For this in-repo content, people kind of are hoping for a "wall of text" that guides them through a basic understanding. And I bet this kind of content will super-charge new AIs that join us.

My observation here is that I kind of wish the content routinely included hyperlinks, but I'm not sure that would work out. Maybe with relative paths?

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 (jamulussoftware#3875), where it sat under the
thread table; this is the file that introduces CSoundBase.
mcfnord pushed a commit to mcfnord/jamulus that referenced this pull request Aug 8, 2026
Applies @ann0see's review on jamulussoftware#3875:

- Intro cut to two sentences; the paragraph about what the file does and
  does not assert is gone.
- File list back to one line each: the SendMessQueue detail, the SockBuf
  and CProtocol members and the vecChannels name are all readable in the
  file itself. Kept "the client has one; the server an array of
  MAX_NUM_CHANNELS", which is in server.h, not channel.cpp.
- The three-bullet block after the thread table is one paragraph. The
  CSoundBase QThread note moves to src/sound/README.md (jamulussoftware#3873), where a
  reader meets the class; the send/receive clocking bullet is dropped, as
  the table above already carries it. The parenthetical about how the
  thread identities were checked is dropped too: it describes the method,
  not the code, and the util.cpp TODO makes the point on its own.

122 lines to 105. No claim changed.
@dingodoppelt dingodoppelt added the AI AI generated or potentially AI generated label Aug 9, 2026
Comment thread src/sound/README.md Outdated
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd turn this around to state what can be done, how it can be done and then state which drivers currently do it (ideally, "and why" as well as "and why none of the others").

Comment thread src/sound/README.md Outdated
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is worth investigating. If the architectural framework was put in place to put this on a thread, it's probably meant to be on a thread or the code should be cleaned up.

The architecture is meant to have one of the following:

  • GUI:
    • Qt main thread at normal priority
    • CClient with CSound (to/from the audio hardware) and CChannel (to/from the network) at raised "real time" priority
  • Headless:
    • CClient with CSound (to/from the audio hardware) and CChannel (to/from the network) at raised "real time" priority

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 AI: On the QThread question: run() is never overridden and start() is never called anywhere in this class's tracked history, back to the commit that introduced it, and no other QThread-specific method (setPriority, msleep, currentThread) appears anywhere in the sound layer. There is no thread to put anything on. Changing the base to QObject requires nothing from this file; whether anything downstream relies on the class actually being a QThread (a qobject_cast<QThread*>, moveToThread) was not checked, so that change stays out of this PR rather than being asserted risk-free.

On ASIO doing it differently: the history inverts the guess. ASIOMutex arrives on 2008-07-12 (5eb86941) with the ASIO backend itself, when ASIO was the only backend — seven months before CSoundBase exists at all (3fb2d9ca, 2009-02-22). Its drain-on-stop wait was added on 2011-12-27 (73f408e4, "make sure that sound working thread is actually killed on stopping the audio interface"). The shared MutexAudioProcessCallback appears nine years after that, on 2020-08-26 (ecff80fc), as a fix for a JACK crash on quick reconfiguration; that commit touches linux/sound.cpp and soundbase.{h,cpp} and nothing else. So ASIO did not skip an existing architecture — the shared mutex is a later, independent re-implementation of a guard ASIO already had, scoped to the JACK bug that prompted it and never extended outward.

On whether the same mutex is worth using: not as a substitution, because the two are not doing the same job today. ASIOMutex is also held across the whole of CSound::Init()ASIODisposeBuffers(), ASIOCreateBuffers() and the vecsMultChanAudioSndCrd reallocation — and no other backend locks anything in Init(). The waits differ too: CSoundBase::Stop() blocks unconditionally on a QMutexLocker, while ASIO's uses tryLock ( 5000 ) and returns anyway on timeout. Since ASIO's stop can return with a callback still in flight, that Init() lock is what keeps ASIOCreateBuffers() off a live bufferSwitch(). Narrowing exactly that scope is what #3869 proposes, so the merge question sits better with it than here: unifying the two would have to preserve the coverage across Init() and settle which of the two wait policies applies.

Two unrelated items surfaced in the same pass, both out of scope for a docs PR: CSound::onAudioInput() in the Oboe backend memcpys using the driver-delivered frame count into a buffer sized for the requested count, and the mismatch check runs after the copy rather than before — a device delivering more frames than requested overflows the buffer before anything notices. And the callback-guarding column in the revised table takes AudioOutputUnitStop() and ASIOStop() to synchronously stop the driver calling back again; that is untested on hardware.

Comment thread src/sound/README.md Outdated
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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I make that three ways, then. It should be written plainly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 AI: Now the three are named ahead of the tables that detail them: the driver-level stop call in a backend's own Stop(), the bRun flag the callback may test, and the mutex Stop() shares with the callback. No two of the five backends combine them the same way.

Checking that turned up a false sentence in the pushed text — "Every backend but ASIO also overrides Stop()", sitting above a table row reading "no override". ASIO's override does exist, and is one of the four that calls a driver-level stop first; JACK's is the only one going straight to the base class. Both corrected, along with the iOS row of the callback table: the driver-invoked callback is recordingCallback(), and the mutex is taken inside processBufferList() only, so the output copy back into ioData runs unlocked.

Comment thread src/sound/README.md Outdated
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` |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It might be worth seeing why this variation exists, anyway.

Comment thread src/sound/README.md Outdated
… code, not Jamulus core

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 <noreply@anthropic.com>
Comment thread src/sound/README.md Outdated
`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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

OK, I guess this leaves open the question: is there a good reason for having a different way of doing it for ASIO? (My guess: no, it was just the second backend and done completely separately without forward thinking; only when the third backend came along did the idea of an architecture appear and it's not particularly apparent even now...) And the next question: is it worth using the same mutex?

jrd and others added 2 commits August 11, 2026 20:24
… 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 <noreply@anthropic.com>
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 5eb8694 (2008-07-12), when ASIO was the only backend and
CSoundBase did not yet exist (3fb2d9c, 2009-02-22); its drain-on-stop wait
followed in 73f408e (2011-12-27). The shared MutexAudioProcessCallback arrived
in ecff80f (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 <noreply@anthropic.com>
Comment thread src/sound/README.md
`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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ah the joy of code reviews... I guess we'll need a few backend-specific issues to clean up those inconsistencies.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 AI: The candidates this pass surfaced, each self-contained:

  • A buffer-size change on the device's own initiative goes unnoticed on three of the five. Oboe compares and only logs it; neither CoreAudio backend registers a size listener at all.
  • The Oboe input copy uses the driver-delivered frame count into a buffer sized for the requested count, and the mismatch check runs after the copy rather than before it.
  • On iOS the mutex covers the input half only, so the output copy back into ioData is unlocked.
  • The running-flag check sits after the lock in JACK and CoreAudio (macOS), before it in Oboe, and is absent in ASIO and CoreAudio (iOS) — so two backends skip the processing at full mutex cost, and two never check.
  • The two mutexes: ASIOMutex is also held across the whole of CSound::Init(), which the shared one never is anywhere, so collapsing them is a behaviour change rather than a rename.

Comment thread src/sound/README.md Outdated
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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Probably best recorded in an issue rather than this file (with "but see issue #xxx" if you like).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 AI: Trimmed to the fact — no such thread is ever created — with the history and the QObject suggestion out of the file, for an issue instead.

On the architecture point: the raised-priority half of it does exist, on the network side. CHighPrioSocket::Start() runs its CSocketThread at QThread::TimeCriticalPriority. Sound has no equivalent: CSoundBase : public QThread is never started, and src/sound/ holds no start() call, no run() override and no other QThread method at all — so the audio callback arrives on a thread the driver owns, at whatever priority that driver gives it, a value Jamulus neither sets nor reads.

Comment thread src/sound/README.md
jrd and others added 2 commits August 12, 2026 23:42
…() table

pljones on jamulussoftware#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 <noreply@anthropic.com>
…echanisms

Three review points from pljones on jamulussoftware#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 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI AI generated or potentially AI generated

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants