Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
dd6f60e
Fix: Octatrack base, a FLEX <-> module machine switch reloads the tra…
npp1993 Oct 27, 2026
befa9db
Fix: Octatrack base switch fix: regenerate the beta base (5f143b36) a…
npp1993 Oct 27, 2026
a1d3435
Fix: the Octatrack USB bridge opens the unit, not another Elektron de…
npp1993 Oct 27, 2026
899eabc
Fix: Octatrack base, module machines play right after a boot
claude Oct 27, 2026
595771d
Docs: leave the after-boot engine note to its own fix
npp1993 Oct 27, 2026
7d1ba7c
Octatrack beta base: regenerated for module machines playing after a …
npp1993 Oct 27, 2026
bd86377
Docs: keep the engine guides from growing past the size rule
npp1993 Oct 27, 2026
24cbebf
Fix: Octatrack base, review fixes for module machines after a boot
claude Oct 28, 2026
d7117e7
Fix: Octatrack base, a module machine's engine loads when the bank's …
claude Oct 28, 2026
d7485fc
Fix: Octatrack base, a restored module machine shows its name at SRC …
npp1993 Oct 28, 2026
90f6008
Octatrack beta base: regenerated for the SRC label after a boot
npp1993 Oct 28, 2026
568d4e3
Octatrack beta base: regenerated for the review fixes and Part-signed…
npp1993 Oct 28, 2026
3747310
Fix: Octatrack base, no DSP STOPPED while stock holds the frames afte…
claude Oct 28, 2026
b9a3169
Octatrack base: the resident patch and source pin follow the stall wa…
npp1993 Oct 28, 2026
a069405
Octatrack beta base: regenerated for the stall watchdog fix
npp1993 Oct 28, 2026
e0caca7
Docs: an edited elekloader file the resident patch touches needs its …
claude Oct 28, 2026
b0d3e49
Revert the stall watchdog change: a pending answer must still recover…
claude Oct 28, 2026
dd17d66
Merge the FLEX switch fix and the SRC label fix into the after-boot fix
claude Oct 28, 2026
9c9ff6b
Merge upstream main (d2cb12d6) into the after-boot fix
claude Oct 28, 2026
8351a88
Docs: the after-boot record carries the flashed-base results and the …
npp1993 Oct 28, 2026
da0caf5
Octatrack beta base: regenerated for the combined after-boot, FLEX sw…
npp1993 Oct 28, 2026
18bba50
Merge remote-tracking branch 'origin/main' into claude/project-thread…
npp1993 Oct 28, 2026
9360e11
Docs: the after-boot record adds the flashed ccc53692 boots of 28 Oct…
npp1993 Oct 28, 2026
6515ac6
Merge remote-tracking branch 'origin/main' into claude/project-thread…
npp1993 Oct 28, 2026
5fcda80
Octatrack beta base: regenerated after merging main (restore retries,…
npp1993 Oct 28, 2026
9909283
Octatrack base: the owner's review of #1024 (bank scan only on a chan…
npp1993 Oct 29, 2026
ae54f94
Merge branch 'main' of github.com:repeat98/modwerk into claude/projec…
npp1993 Oct 29, 2026
241d416
Merge remote-tracking branch 'origin/main' into claude/project-thread…
npp1993 Oct 29, 2026
278b508
Octatrack beta base: regenerated on current main with Analog BD 0.1.6…
npp1993 Oct 29, 2026
4fbcc2f
Merge remote-tracking branch 'origin/main' into claude/project-thread…
npp1993 Oct 29, 2026
0cb094f
Octatrack base: the bank scan also reruns when a packaged feature bec…
npp1993 Oct 29, 2026
fc0ea32
Octatrack beta base: regenerated for the bank-scan change (Analog BD …
npp1993 Oct 29, 2026
57883aa
Merge remote-tracking branch 'origin/main' into claude/project-thread…
npp1993 Oct 29, 2026
c11913e
Octatrack beta base: regenerated on current main
npp1993 Oct 29, 2026
1f530bb
Octatrack base: the assignment counter has one plain definition (MinG…
npp1993 Oct 29, 2026
1bcfa73
Octatrack beta base: regenerated on current main
npp1993 Oct 29, 2026
ba694a1
Merge remote-tracking branch 'origin/main' into claude/project-thread…
npp1993 Oct 29, 2026
bd102e4
Octatrack beta base: regenerated on current main
npp1993 Oct 29, 2026
d8c1edd
Merge branch 'main' of github.com:repeat98/modwerk into claude/projec…
npp1993 Oct 29, 2026
6c51437
Octatrack beta base: regenerated on current main
npp1993 Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/2026-10-27-engines-play-after-boot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Octatrack (USB, beta): a module machine such as Analog BD now plays right after a power cycle. Its sound engine loads while the sequencer runs when it fits in free DSP memory, instead of waiting for you to stop, and it loads several times faster. MODULES LOADED now appears once those engines are running, so pressing PLAY on it sounds every track. If your effects fill the DSP so that one of them has to move first, the engine still waits until the Octatrack has been stopped for two seconds. A halt of the DSPs that can happen in the first minute after a cold boot (DSP STOPPED, then no module engine loads until the next power cycle) is older than this change and not fixed by it.
1 change: 1 addition & 0 deletions changelog/2026-10-27-flex-machine-switch-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Octatrack base (beta): switching a track from FLEX to Analog BD or FM Synth, or from any module machine back to FLEX, now reloads the track's live parameters from its Part, as stock does when a switch changes the machine type. Before, the switch left them on the old machine's values until SOURCE CLEAR: a machine chosen from FLEX could stay silent, and FLEX chosen back played the machine's settings. STATIC and the other stock machines are unchanged. Checked in the host test and on a MKII for Analog BD (development base: the live parameters before and after each switch, and by ear); FM Synth, POLY8 and VECTOR not yet tried on a unit.
1 change: 1 addition & 0 deletions changelog/2026-10-27-usb-bridge-elektron-device.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Octatrack USB bridge (`scripts/octatrack-usb-bridge.py`): with another Elektron device on the computer's USB, such as an Analog Rytm on Overbridge, the bridge took the first Elektron device it found and every request to the Octatrack failed. It now opens the device that offers the Modwerk interface. Checked in `sdk/tests/test_usb_bridge.py` and on a MKII with a Rytm MKII attached. The Octatrack driving guide adds the trap.
1 change: 1 addition & 0 deletions changelog/2026-10-28-boot-src-label.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Octatrack base (beta): after a boot, a track on a module machine (Analog BD, FM Synth and the others) now shows its machine's name at SRC straight away. Before, the screen kept "SRC > FLEX" until the track was tapped, because the saved modules come back a few seconds after the screen is first drawn and the base redrew the track screen but not its header. Checked in the host test and on a MKII: the name shows on every boot.
2 changes: 1 addition & 1 deletion docs/OCTATRACK_BETA_TEST_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ image RAM-booted for the S cases, then the owner flashes it once. Part 1 end to

Pick up here. In order; **O** needs the owner at the unit.

1. **Finish the engines-on-demand pull request** (branch `claude/octatrack-engines-integration`; steps in `docs/OCTATRACK_RESIDENT_ENGINES_ON_DEMAND.md`, "Where this stands"): changelog notes for the five rebuilt base-bound packages, docs, every CI check run serially on the WSL test PC, a development twin of the committed beta base `acf91e0a…` (on demand, merged with main after #619) checked on the unit, then the pull request.
1. **Finish the engines-on-demand pull request** (branch `claude/octatrack-engines-integration`; steps in `verification/2026-10-15-resident-engines-integration-build.md`): changelog notes for the five rebuilt base-bound packages, docs, every CI check run serially on the WSL test PC, a development twin of the committed beta base `acf91e0a…` (on demand, merged with main after #619) checked on the unit, then the pull request.
2. **Flash the release base** (O) and check that a kept set survives a power cycle (case 1.18); until now the new bases ran from RAM only.
3. **FUNC held at boot skips the module restore** (O, edge case 15): the testers' escape hatch if a module breaks a unit at startup. Built, never run. Also run absent-module case (c) of finding 24 with it.
4. **Finding 6, a base update versus the testers' saved sets:** every beta3 tester's card holds a set for another base and the site refuses Keep there. This release changes the base again, so every tester hits it: give it a migration path.
Expand Down
2 changes: 2 additions & 0 deletions docs/OCTATRACK_DRIVING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ before reporting success.

## Start the link

**One Elektron device on the Mac's USB.** The bridge opens the first device with Elektron's vendor id that offers the Modwerk interface; before 27 October 2026 it opened the first Elektron device of any kind, so an Analog Rytm on Overbridge plugged in beside the Octatrack made every request fail ("unit went away", "no unit running the test base"). Unplug other Elektron devices before a RAM boot or a flash all the same: on that day a RAM boot staged with a Rytm attached failed part-way and hung at the logo, and the retry with the Rytm unplugged came back.

Keep emulator socket paths short, including the host path behind a container bind mount. A long host output path can fail with `usb: cannot listen` even when its container alias is short.

```sh
Expand Down
52 changes: 25 additions & 27 deletions docs/OCTATRACK_RESIDENT_ENGINES_ON_DEMAND.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/module-guides/machines.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ A new machine must be a first-class entry wherever the instrument lists machines
- [ ] **LFO destinations.** Every continuous machine parameter works as an LFO destination without a crash or a step. **Verify** with `stress_project.py` (three LFOs per track) and record which parameters you modulated.
- [ ] **Save and reload.** A project saved with the machine reloads it, with its parameters and model. Say what a build without the module shows for that track. **Verify.**
- [ ] **The stock machines still work,** on the same track and on others.
- [ ] **Switching sounds right at once.** A machine that runs on a FLEX track keeps the stored type FLEX, so stock never reloads the track's live parameters on a FLEX ↔ machine switch; the base does (`mw_assign`, through stock's own loader). **Verify** both directions while playing, with no SOURCE CLEAR: the machine sounds, and FLEX plays its sample, not the machine's settings.
- [ ] **A bounded render.** A machine that renders on the ColdFire (POLY8, FM Synth, VECTOR) runs in the frame interrupt, and USB, the UI and every task wait while it runs. Time your render with DTCN3 and bound it, as POLY8 0.3.1's voice governor does. On USB bases the base's frame guard gives the frame interrupt 80 % of each frame and renders stock's silent source for a machine track whose render would overdraw that, so an unbounded machine drops out in a busy project instead of freezing the unit; card builds' core logger does the same (`sdk/runtime/logging/frame_guard.c`) and logs the dropouts as `GRD`. **Verify** with `npm run device -- guard` on a development base: in the busiest pattern you support, it reports no skips ([traps](../OCTATRACK_HARDWARE_TRAPS.md#dsp-loader-dynamic-dsp-code)).
- [ ] **Muted trigs.** A mute module drops a muted track's trig where stock binds the track's voice (`ev_voice_bind`). A machine that starts its own note from the trig, in its renderer, gets there first: ask the base's raised hook `ev_trig_muted` before the note starts (`raise` in `runtime.json`) and start no note on a track whose bit is set; 0 means play every trig, as stock does. Card builds have no base hooks ([HOOKS.md](../../sdk/machines/octatrack/HOOKS.md), #777).
- [ ] **Its own render.** A machine that makes its sound on the ColdFire contributes `render(track, ping, start, end)`. The base calls it twice a frame for each of its tracks, before and after the event's sample offset (the second call has `end` 16). At the packer's cursor (`*(uint32_t *)0x80001c80`) it writes four longs (source count `end - start`, ring write position `start`, rate `0x04000000` (1.0, Q26), read position `start << 26`), then one L and one R long a sample, the 16-bit sample in the high half, and moves the cursor past them; the track's filter, AMP, LFOs and effects follow as for a sample (Analog BD's `control.c`, FM Synth's `poly.s`, SYNTIZ's draft). The played pitch is the parameter record's PTCH halfword (locks, LFOs and scenes applied) on stock's curve.
Expand Down
23 changes: 23 additions & 0 deletions docs/verification/2026-10-15-resident-engines-integration-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Resident engines on demand: the integration build on the unit — 15 October 2026

Moved from `docs/OCTATRACK_RESIDENT_ENGINES_ON_DEMAND.md` on 27 October 2026; history, not current instructions.

## The integration build on the unit

Twin built from this branch before the base regeneration (on demand, `--dev`, configuration `76e15b41…`), on the owner's MKII:

- **All three engines:** placed at boot restore and by install, arena 2,224 / 3,125 (Sidechain 423 words). Eight switches between identical Parts while playing: T3 and MAIN bit-exact, no loader packet, no engine job.
- **Sidechain key phase:** T5 (COMPRESSOR, KEY T3, MON ON) lags T3 by 32 samples on 5 of 5 enables (two blocks); the earlier base gave 89 or 121 depending on the enable. On the key-phase twin, 9 of 9 equal. MON: swapping T5's COMPRESSOR for FILTER with MON on gives exact silence.
- **Evacuation:** SPRING REV at the top of core 1 (under Analog BD), sustained as a drone (all six knobs up, HP 0); installing Sidechain moved it and placed the engine. Under the drone (T3 0.34-0.36 RMS): no click (no 20 ms block above the drone's own second-difference texture), no dropout. Counters: moves and evacuations up by one, no failure.
- **Picks during an engine job:** three PLATE REV picks on T5 while Output Matrix installed: all taken, no selection refused or rolled back. Before the fix the same timing gave DSP LOAD FAILED once on the unit.
- **Analog BD on a restored Part (finding 24):** T3 left as ANALOG BD survives a RAM boot with the module in the saved set: FLEX type and marker kept, the drum plays. Before the fix the bank restore was refused: a default bank without trigs, which the unit's autosave then kept ("total silence", first blamed on Sidechain).
- **Development trap:** a RAM boot after a session of resident engine installs and removals sometimes drops the unit off USB until a power cycle (seen twice without a stray session).

## Where it stood (end of 15 October 2026)

The beta base is regenerated on demand on top of main after #619 (`acf91e0a…`, which adds the startup without the wave's outline and the two-section MODWERK STATUS page; Analog BD's recipe recaptured again, only its image hash changed). Steps 1 and 2 are done (merged with main, 15 October evening). Next:

1. Changelogs: a 15 October note on the current version of the five base-bound modules (four rebuilt, CC Map bytes unchanged); the Sidechain key-phase note now ships in the beta base.
2. This note's Status and Open → Beta base, and one line in the beta test plan naming tester base `acf91e0a…` and the finding 17 recapture.
3. The checks `npm run pr:ready` runs, serially and memory-capped (the parallel run OOMs the WSL test PC), including `import-module-build --check-only --verify-existing` for the rebuilt packages.
4. A development twin of exactly this configuration plus `--dev`, then on the unit: boot, the three engines, a Part sweep, an Analog BD reboot. Then the pull request.
83 changes: 83 additions & 0 deletions docs/verification/2026-10-27-engines-play-after-boot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Octatrack base: module machines play right after a boot — 27 October 2026

Owner review: this relaxes the stopped-only placement rule in `docs/OCTATRACK_RESIDENT_ENGINES_ON_DEMAND.md`.

**What was wrong.** On a flashed base (`5afa623e`, Analog BD 0.1.6, 14 modules), PLAY pressed on MODULES LOADED left
Analog BD silent until the next stop. Three causes, each in the base:
- An on-demand engine was placed only while the sequencer was stopped and settled.
- A controller RESET went one 24-word packet a 60 Hz board tick: Analog BD's 3,776 words took 158 ticks, about 2.6 s
a core.
- The restore said MODULES LOADED before any engine ran.

**What changes.**
- **Placed while playing** when the engine fits in free words (`dl_engine_claim` commit 1). Trims, evacuations and
unplacing still wait for a stopped, settled unit; nothing is placed while a pattern change is queued. A play-time claim
told its code must move waits for the stop without planning again each tick (`resident_engines.c` `deferred`).
- **RESET jobs carry up to eight packets** (`DL_RESET_DATA` 192 words, `resident_driver.py`;
`MW_CONTROLLER_RESET_CHUNK`). Each packet is the one the receiver took before, at its own offset.
- **MODULES LOADED waits** for `modwerk_controller_settling()` (`controller_board.c`) to read 0, ticked from ep0.c:
0.5 s at least, 10 s at most. A slot waits only while the bank uses its feature; a closed core (board transport or
owned channel), a lost core state, a key bus that cannot come up, or a stuck engine ends its wait. The notice is
dropped if the host changes the set during the wait. SOME MODULES FAILED and MODULES NOT LOADED are said at once.
- **Used from the Part.** A packaged feature counts as used once a track of the bank's Parts, working or saved, is
signed for its module's machine (`machine_registry.c` `modwerk_machine_bank_modules`, read by `use_tick`), not only
once its renderer asks for the write lease, which it does only from a track's first trig.
- **`bind_live`** keeps a module's retry when a later module in the same pass binds (`bind_again` was cleared by any
successful bind, so an earlier refused reservation waited for the next owner epoch, and the notice could come early).

**How it was checked.**
- **Host.** `test_resident_engines_board.py`: mode 1 (placed while playing, nothing while queued), 13 (core B places while
playing, core A waits for the stop to move code), 46 (stall), 54 (settling reads 0 when nothing is used), 64 (Analog BD
placed, reset and enabled while playing within 30 ticks; 158 reset packets a core; settling). `test_resident_transport.c`
(a reset past `DL_RESET_DATA` refused; a 50-word multi-packet job). `test_modset.c` (the notice's minimum, maximum,
once only; dropped on a set change; a partial restore said at once).
- **On the unit** (owner's MKII, development twin `1b913b5c` of this base, RAM-booted, trials only, nothing kept; USB
recordings of T1, T2, T3 and MAIN):
- *Switch while playing.* Bank A T1 FLEX → Analog BD at 18:55:06: placed on both cores within the second (offsets 3294
and 3420), reset complete and slot ACTIVE by 18:55:07.8 (about 1.8 s). T1 stayed silent until SRC CLEAR: the separate
FLEX-switch bug, not in this build.
- *Bank change while playing.* Bank D → bank B (Analog BD on T1), engine unplaced before: the transport kept playing
(T2 and T3 every 2.000 s across the change). Core 1 placed within 0.3 s, core 0 about 1 s later after waiting behind
core 1's jobs (wait 7, transport busy); reset complete 2.3 s after the change. T1's first Analog BD sound 1.50 s after the change, in
time; the first 12 of 16 steps of that bar were missed. No SRC CLEAR needed.
- *Other tracks.* T2 and T3 bit-exact loop to loop through both placements (largest difference 1.2e-6, the recorder's
floor): no click, gap or drop.
- *Level.* Analog BD on bank B at the same RMS as on bank A (0.033).
- **Code review** of the change: reset offsets, the last partial packet, range ends and ack counts checked against the
receiver; the 10 s cap always ends the wait. Its findings on the settling wait are fixed above.

- *After a power cycle, before the use-from-Part change* (personal base `d4658748` flashed, 14 modules kept; three
cold boots, PLAY on the notice): Analog BD came in 1.50 s after PLAY (boot 1 recorded: the first 6 of its trigs
missed), about the 13th trig in the others. Its engine started loading only at PLAY: the restored track asks for
the lease only from its first trig, so MODULES LOADED had nothing to wait for. Hence the use-from-Part change.

- **After a power cycle, with the use-from-Part change** (personal base `1db64a78`, 14 modules kept, blank project; 28
October): MODULES LOADED, then PLAY: Analog BD sounded on the first step (boot 1 recorded over USB). T1 showed
"SRC → ANALOG BD" without a tap on every boot (the SRC label fix, in this change).
- **Flashed `ccc53692`** (this branch at `9c9ff6bd` plus Analog BD 0.1.6, 14 modules, the owner's own project; 28
October, Nathaniel): SRC label, first trig after MODULES LOADED and the FLEX ↔ Analog BD switch all pass. On one of
four cold boots the early-boot halt below struck 16 s in, at a bank change while playing (stall record 21, the same
signature); the next boot played Analog BD on its first trig.

**Open.**
- **Early-boot DSP halt, not fixed here.** Within about 10-40 s of a cold boot, at the project load or a bank change
while playing, the frame chain stopped: 0.5 s later the watchdog showed DSP STOPPED, revived the frames and turned
residency off for the boot (no engine loads or retires until a power cycle). It also happened on a boot with FUNC
held (no modules restored), so it is in the base, not in Analog BD, and `dsp.c` and `status.c` are main's. All five
stall records agree: stock transfer state 7, our transfer idle (`dl_phase` 0, TCD0 and TCD1 DONE, eDMA ES 0), the
frame interrupt unmasked (IMRL bit 1 clear) with nothing pending, EPORT pin 1 high and its edge flag clear, DSP
select 0, one job (a status-meter PEEK in four of five) waiting for its answer. So the chain ended state 7 normally
and the next frame's edge was lost, which fits the 200 µs hold-off trap in `docs/OCTATRACK_HARDWARE_TRAPS.md`
rather than a host-port access of ours. Not reproduced on a RAM-booted development twin; seen again on `ccc53692` (above). A 10 s watchdog
(`37473105`, reverted in `b0d3e493`) made the halt permanent (a drone): keep the 0.5 s recovery.
- **Ringmod SC refused at boot restore** with `RUNTIME_BUSY` on most cold boots of `d4658748` and `1db64a78`, though
it restores on `5afa623e` and installs live. Candidates: the key-bus admission at the restore (`controller_board.c`)
or `rebind_tick`. Parked.
- **First trig after Analog BD → FLEX stutters** (the switch fix); an FM Synth switch and a switch in a full project
are untested.
- **USB drop.** One unprovoked ~2 s USB drop mid-trial (twin, Analog BD placed; possibly the shared hub re-enumerating)
rolled the trial back and left both cores' engine claims at wait 7 and the key bus closed (`0x80`) until a power cycle.
A review points at `journal_fail()` returning before `dl_owned_lease_release` in `mw_manager_journal_release`
(`resident_journal.py`), which is on main: a separate fix.
- **Faster still** (proposals, not here): run both cores' jobs side by side (about 1 s), or place while a bank change is
queued against the queued bank's Parts, free words only, so the first trig sounds.
Binary file modified public/octatrack-modules/analog-bassdrum/0.1.6-experimental.mwrm
Binary file not shown.
Binary file modified public/octatrack-modules/output-matrix/0.1.1-experimental.mwrm
Binary file not shown.
Binary file not shown.
9 changes: 8 additions & 1 deletion scripts/octatrack-usb-bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,14 @@ def dispose_device(dev):

def open_unit():
"""(device, vendor interface), claimed; None while no unit runs the test base."""
dev = usb.core.find(idVendor=0x1935)
# The first Elektron device on the bus may be another instrument (an Analog Rytm on Overbridge): take the one
# that offers the Modwerk vendor interface.
def modwerk(d):
try:
return any((i.bInterfaceClass, i.bInterfaceSubClass, i.bInterfaceProtocol) == (0xff, 0x4d, 1) for c in d for i in c)
except usb.core.USBError:
return False
dev = usb.core.find(idVendor=0x1935, custom_match=modwerk)
if dev is None:
return None
claimed = False
Expand Down
Loading
Loading