Skip to content

2.5 (b): worker mode in the Max object — @mode, @latency, @latencysamples - #30

Merged
tap merged 2 commits into
development/v2from
v2/worker-max
Oct 1, 2026
Merged

tap merged 2 commits into
development/v2from
v2/worker-max

Conversation

@tap

@tap tap commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Step (b) of plan 2.5, the last part of worker mode. It's based on development/v2 now that #29 has merged.

What changes

  • @mode worker runs process() on the core's worker thread. The audio thread only copies vectors to and from it and never takes the GIL. @mode direct, the default, behaves as before.
  • @latency is the whole delay in milliseconds, rounded up to whole signal vectors. The default is 30, and the value must exceed Max's I/O vector duration (see below).
  • @latencysamples is read-only and gives the delay in samples, so a patch can line up other signal paths (with a delay~, say). It's 0 in direct mode.
  • When settings apply. Setting @mode or @latency takes effect at once: it rebuilds the signal chain. dspsetup starts the worker for each chain with the object's inlet and outlet counts.
  • Reserved names. The object reserves mode, latency and latencysamples. Fields are now checked against reserved names as methods already were, with a core test.
  • Reports. The report queue flushes the worker's reports along with the processor's. Destroying the object stops the worker before the processor goes away.

What measuring in Max changed (this machine: 96 kHz, I/O vector 512, signal vector 64)

  • The worker thread needs audio-thread scheduling. As an ordinary thread on a busy machine, it was late in 2 of 5 quiet seconds even with 21 ms of latency. The fix:

    • The core calls a new thread_setup hook on the worker thread as it starts.
    • The wrapper sets Mach's time-constraint policy on macOS (period one vector, half of it computation) and THREAD_PRIORITY_TIME_CRITICAL on Windows. Both are documented OS calls.
    • The core also makes the thread's Python thread state before the audio thread can hand it work.
  • The latency must exceed the I/O vector. Max computes an I/O vector's worth of signal vectors back to back (8 at a time here), so the worker only has the latency beyond that burst. Seconds with a late vector:

    latency late seconds
    2 vectors (1.3 ms) 10 of 10
    4 vectors (2.7 ms) 10 of 10
    8 vectors (5.3 ms) 3 of 10
    10 ms 2 of 11
    30 ms 0 of 20, in two runs

    So @latency is in milliseconds, and the default is 30. That leaves about 18 ms beyond a 512-sample I/O vector at 44.1 kHz. The ReadMe tells users to raise it if they use a larger I/O vector. The plan records both revisions of the default you approved.

  • @latencysamples always read 0 in Max. Max refuses to set a read-only attribute through its own setter, so it's now set directly. The mock kernel doesn't model that refusal, which is why the mock test passed anyway.

Tests

  • Core: milliseconds-to-vectors rounding, including an exact 4 ms at 48 kHz, which must come out as 3 vectors. The fields check against reserved names.
  • Mock test: worker mode delays by @latency and reports @latencysamples. It also checks returning to direct mode, an invalid mode becoming direct, and a negative latency being held at 0 (one vector).
  • Runtime test worker, in Max, at the default:
    • The output matches delay~ by @latencysamples, sample for sample.
    • A 0.2 s stall is late, reported once, and comes back at the same latency.
    • The output still matches after a reload.
    • mode direct takes the delay away.
  • Results:
    • Full Max suite: 12 patchers, 103 assertions.
    • Core 85/85 locally; ThreadSanitizer clean on the worker, threading and reload scenarios.
    • clang-format and clang-tidy-18 pass.
  • The reference page is regenerated by Max (thanks to 6.8: the reference page from Max — date the .mxo on each build #26) and committed. It gains the three attributes and the new description.
  • The runner's stale-build check now ignores *_test.cpp, which isn't part of the external.

This completes 2.5. The plan ticks it, and the help patcher gains a note on @mode worker and @latency, which I checked open in Max (its window is 50 px taller to fit).

🤖 Generated with Claude Code

https://claude.ai/code/session_019rVV9SkjkWmiBXFMT4whkz

tap and others added 2 commits October 1, 2026 06:41
…ples

@mode worker runs process() on the core's worker, @Latency milliseconds behind the audio (30 by
default, rounded up to whole signal vectors); the read-only @latencysamples gives the delay in
samples, 0 in direct mode. Both settings take effect at once, by rebuilding the signal chain, and
the worker is started for each chain in dspsetup with the object's inlets and outlets.

What measuring in Max changed:
- The worker thread gets the scheduling of an audio thread — Mach's time-constraint policy on
  macOS, THREAD_PRIORITY_TIME_CRITICAL on Windows — through a hook the core calls on the thread as
  it starts: on a busy machine an ordinary thread was late even with 21 ms of latency. The core
  also makes the thread's Python thread state before the audio thread can give it work.
- Max computes an I/O vector's worth of signal vectors back to back (here 512 samples: 8 at once),
  so the worker has only the latency beyond that burst: 2 vectors (the first default) left a late
  vector in every second, 10 ms in 2 of 11. @Latency is therefore milliseconds, the whole delay,
  default 30, documented as having to exceed the I/O vector.

Fields are now checked against the host's reserved names as methods are, and the object reserves
its new attributes. @latencysamples is set directly (Max refuses to set a readonly attribute
through its own setter, which the mock kernel does not model). The runner's stale-build check
ignores the mock test's source.

Mock test: worker mode delays by @Latency, reports @latencysamples, and returns to direct mode.
Runtime test in Max (worker, at the default): the output matches delay~ by @latencysamples sample
for sample, through a 0.2 s stall (reported once) and a reload; @mode direct takes the delay away.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rVV9SkjkWmiBXFMT4whkz
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rVV9SkjkWmiBXFMT4whkz
@tap
tap merged commit efbf884 into development/v2 Oct 1, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant