Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ed6188e
FM Synth (work in progress): the render path reads the Part, GLIDE an…
repeat98 Nov 12, 2026
641f559
Make Windows set up the Octatrack's WebUSB interface again: new bcdDe…
repeat98 Nov 12, 2026
98170a6
Merge main
repeat98 Nov 12, 2026
9f57140
Fix: the regenerated base, its descriptor test, and a busy line that …
repeat98 Nov 12, 2026
811f1c2
Merge main
repeat98 Nov 12, 2026
2c7525e
Fix: base-bound packages stamped for the new base 87258f15
repeat98 Nov 12, 2026
2eed068
Octatrack base: a machine engine's DSP price lives in its own package
twwwy Nov 12, 2026
de359a1
Merge upstream/main; rebuild the beta base and relink its packages
twwwy Nov 12, 2026
07d16ac
Docs: test base-bound modules on a dev twin; the site mirrors the loa…
twwwy Nov 12, 2026
8a3d5e2
Improvement: the Octatrack base keeps the frames before a stall
repeat98 Nov 12, 2026
3b5e27c
Improvement: the stall frames' line lives with the diagnostics contract
repeat98 Nov 12, 2026
1955316
Improvement: Sidechain's package names the rebuilt base, and the guid…
repeat98 Nov 12, 2026
6c34ee0
Merge remote-tracking branch 'origin/main' into improvement/stall-fra…
repeat98 Nov 13, 2026
050ad41
Improvement: a busy Octatrack refuses a further machine with CPU FULL
repeat98 Nov 13, 2026
08ba494
Merge remote-tracking branch 'origin/main' into improvement/ui-cpu-guard
repeat98 Nov 13, 2026
afab646
Improvement: a busy Octatrack also refuses a further module effect
repeat98 Nov 13, 2026
4362394
Improvement: show the base update's progress in blue and tell the use…
repeat98 Nov 13, 2026
4949b78
WIP base: a full-screen, key-locked BASE UPDATE screen and a MODWERK …
repeat98 Nov 13, 2026
0c25d8e
Merge remote-tracking branch 'origin/main' into module/fm-synth-0.1.3
repeat98 Nov 13, 2026
c291310
Merge remote-tracking branch 'origin/main' into improve/base-update-ux
repeat98 Nov 13, 2026
677deb3
Cut the update view down: a slim step row, a short checklist, shorter…
repeat98 Nov 13, 2026
d6d7c7c
Merge remote-tracking branch 'origin/improvement/ui-cpu-guard' into b…
repeat98 Nov 13, 2026
ec4cc31
Merge remote-tracking branch 'origin/improve/base-update-ux' into bas…
repeat98 Nov 13, 2026
1a2a695
Merge remote-tracking branch 'origin/improvement/stall-frame-trace' i…
repeat98 Nov 13, 2026
1ac1ec4
Merge remote-tracking branch 'origin/module/fm-synth-0.1.3' into base…
repeat98 Nov 13, 2026
c53e0a3
Merge branch 'pr-1487' into base/update-and-sync-screens
repeat98 Nov 13, 2026
cb52118
FM Synth 0.1.3: version, release notes, README, TESTING with the MKII…
repeat98 Nov 13, 2026
65242a2
Merge remote-tracking branch 'origin/fix/windows-winusb-rev' into bas…
repeat98 Nov 13, 2026
59805ac
Merge remote-tracking branch 'origin/module/fm-synth-0.1.3' into base…
repeat98 Nov 13, 2026
277c6ab
diag.h: a UI tick comes 60 times a second, not every 2.7 frames
repeat98 Nov 13, 2026
5ec2ceb
Regenerate the beta base for the combined tree (be13282e) and recaptu…
repeat98 Nov 13, 2026
1b79cbe
A finished base save is final on the unit's screen: no WAITING after …
repeat98 Nov 13, 2026
66f90f7
Regenerate the beta base with the screen fix (71e143d2)
repeat98 Nov 13, 2026
ae7b161
Module packages for base 71e143d2: FM Synth 0.1.3's row, and the base…
repeat98 Nov 13, 2026
8a780f1
Raise the RAM boot stage to 1.5 MiB, and hold released bases to what …
repeat98 Nov 13, 2026
ff05030
FM Synth 0.1.3 leaves the base update: it ships as its own pull reque…
repeat98 Nov 13, 2026
acb398d
Regenerate the beta base (4e9a87ea) with the 1.5 MiB stage and withou…
repeat98 Nov 13, 2026
a4a824d
Follow the update dialog's new intro wording in the guide test
repeat98 Nov 13, 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-11-12-stall-frame-trace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Octatrack base: when the audio stops, the unit now also keeps the timing of the last 128 audio frames before it. With shared diagnostics that shows whether a module's work or something else held the audio up.
1 change: 1 addition & 0 deletions changelog/2026-11-12-windows-usb-reconnect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Octatrack base: on some Windows computers the site could never connect and said Modwerk was open in another tab. The base now reports a new USB device revision, so Windows sets up its USB connection again by itself. Where a Windows computer still can't connect, the message now says to install the latest base from the card once, instead of only blaming another tab.
1 change: 1 addition & 0 deletions changelog/2026-11-13-base-update-progress-hands-off.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Site: the base update over USB now shows one blue progress bar for the whole update and a "Hands off your Octatrack" warning for as long as it runs, since the Octatrack can look ready after its restart before the base is saved. Syncing modules shows the same bar.
1 change: 1 addition & 0 deletions changelog/2026-11-13-octatrack-engine-price-in-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Octatrack base (beta): a machine module that brings its own DSP engine (Analog BD) now carries its DSP price in its own package, so a new price no longer needs a new base. Until now the price sat in the part of the package the base trusts by hash, and every change to it meant rebuilding the base. Nothing changes for players yet: Analog BD 0.1.7 reserves the same DSP room as before (411 per voice and 3 per core, now one figure of 414 per voice), so DSP FULL appears where it did; a lower, measured price follows as an Analog BD update. The base refuses an engine that names no price or one taken from a model. Analog BD's package is rebuilt for the new base; Sidechain Compressor, Output Matrix, CC Map and Recorder Loop Fix keep their packages byte for byte and name the new base.
1 change: 1 addition & 0 deletions changelog/2026-11-13-octatrack-update-and-sync-screens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Base: a base update now takes the Octatrack's whole screen (BASE UPDATE, a progress bar, DO NOT TOUCH) and holds its keys and knobs until the base is saved, and a module sync shows a MODWERK SYNC box with a progress bar. A base write that failed no longer leaves its box on the screen until the next start.
2 changes: 1 addition & 1 deletion docs/DEVELOPER_WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ These stumbling blocks have each cost a contributor and the owner a repair round
- **A narrow clone kept a stale ref.** `pr:ready` updates the remote's `main` tracking ref even in a single-branch clone. A manual `git fetch origin main` can update only `FETCH_HEAD` under a narrow fetch refspec; fetch `refs/heads/main:refs/remotes/origin/main` before following a `git merge origin/main` instruction in that setup.
- **Stale generated files.** `src/catalog/module-documents.json` and the compatibility files go stale whenever `main` moves under your branch. `pr:ready` regenerates them; commit what it names. Never edit them by hand.
- **Letter case.** macOS and Windows find `Foo.s` when you wrote `foo.s`; Linux CI does not. `modules:check` refuses a manifest path in the wrong case, but not an `.include` line inside an assembly source, so check those yourself.
- **Conflicts in Octatrack base and package assets.** Take `main`'s base files and rebuild them; never take one side's module asset JSONs (`dsp-packages.json`, `coldfire-packages.json`, `octatrack-module-packages.json` and the like) whole, since each side carries its own modules' rows. Merge them row by row, keep one row per module, then rebuild as a base change does: the beta base, `octatrack:module-packages -- --base-only`, the isolated build and import (only `sourceCommit` and the source fingerprint should move), the standalone recipe pins and Analog BD's proofs.
- **Conflicts in Octatrack base and package assets.** Take `main`'s base files and rebuild them; never take one side's module asset JSONs (`dsp-packages.json`, `coldfire-packages.json`, `octatrack-module-packages.json` and the like) whole, since each side carries its own modules' rows. Merge them row by row, keep one row per module, then rebuild as a base change does: the beta base, `octatrack:module-packages -- --base-only`, the isolated build and import (only `sourceCommit` and the source fingerprint should move), the standalone recipe pins and Analog BD's proofs. After a base rebuild, `octatrack:module-packages -- --base-only --stock OS.bin` holds back a base-bound package that lags its catalogue version (`lagsBehind`): set that row's `requiredBase` to the new identity by hand; `sdk/tests/test_base_bound.py` rebuilds it and fails unless its published bytes still match.
- **A merge that breaks the code.** Merging `main` can leave a duplicate declaration or a stale call without any conflict marker. `pr:ready` runs the full check after the merge for exactly this reason.
- **Two pieces of work in one PR.** A guide edit, shared code or another module inside an author update takes it off the automatic path; send a finding, fix or improvement on a different pull request ([Fixes and improvements](#fixes-and-improvements)).
- **The first failure hides the next.** `npm run check` runs every catalogue, licence and changelog check of a stage and reports them together, but stops before the tests and build when one fails, so a fix can still uncover another. Run `pr:ready` until it is Ready instead of pushing after each fix. Between fixes, `npm run check -- --base origin/main --rerun-failed` skips what passed in the last run; it is a shortcut, not a pass.
Expand Down
4 changes: 4 additions & 0 deletions docs/MODULE_PERFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ New FX should aim for DSP cost in the **same ballpark as stock SPRING REV at its

**A machine that renders on the ColdFire names its CPU price.** The Octatrack base admits a Part's machines against a CPU budget, as it admits effects against the DSP's: a pick past the allowance is refused with a CPU FULL box, and a track past it after a load renders silence, instead of the frame guard dropping renders unannounced. The price is `reserved[0]` of the machine's descriptor (`modwerk_events.h`): the track's worst render in percent of a frame, both passes together, with every voice playing, read with `npm run device -- cpu` on a unit. A machine that names none is charged the base's default; a DSP voice that names none, nothing. A renderer a module writes into stock's renderer table or a track's slots itself is still refused and guarded: the base checks every renderer at the frame builder's two calls, and a stock track that reaches the module's renderer falls back to stock's own when the frame is full.

**A busy unit takes no further machine or module effect.** The prices cover a machine's renders, not what else an installed set costs the ColdFire: modules' hooks, an effect's control code, a DSP voice's glue, a USB recording. So the base also refuses a pick of a module's machine or effect, with the same CPU FULL box and the measured load ("UNIT IS BUSY"), while the CPU meter stands at 85 % or more over its last four seconds. The line comes from 32 units' diagnostics (30 October to 13 November 2026): a five-minute window at 85 % or more had a gap between UI ticks past 100 ms more often than not, and under 65 % one window in twenty did. It reads the moment of the pick, so a Part picked while stopped can still run hot once it plays. Stock's machines and effects and NONE are never refused, so a busy unit can always be made lighter.

**Judge the panel by the CPU meter, not by the frame guard's share.** The panel lags when main's idle time runs out, which the meter measures (`npm run device -- stats`, CUE + PROJ); `guard`'s "in the frame interrupt" is the guarded window only and can move without the panel noticing. On an MKII (13 November 2026) an Euclid in use on one track raised the guard's figure by 10 points while the CPU meter rose 2.5 and the UI tick chain took 4 to 10 % longer. Lowering the guard's share does not buy the panel time either when the cost is not in a guarded render: with FM Synth 0.1.2 installed on no track, a share of 50 % skipped over 20,000 renders a second and the load stayed at 77 %.

**In the frame interrupt, read battery RAM and the bank once a frame.** On the Octatrack one read of the current Part index (`0x100b14cf`, battery RAM) costs about half a microsecond. A test such as "is this track mine" that reads it, the bank pointer and the Part's bytes at every site, for every track, costs points of a frame whether the module is on a track or not: installed on no track with eight tracks playing, POLY8 and FM Synth each added about 8 points of frame interrupt and Repitch 5, in each case mostly one such read (an MKII, 10 November 2026). Nothing below the interrupt runs inside it, so what you read at a frame's first use holds until its exit; the base's FLEX render does that (`machine_registry.c`, `look`). To find such a line on a development base, sample the interrupted PC from a spare timer above level 5 (PIT2, INTC1 source 45, is free): the slow instruction is the one before the PC that piles up. Compare whole builds with `npm run device -- guard`, not two builds' profiles: code placement alone moves the interrupt's share by up to a point.

**Bound only the module's own code.** A record prices what the module adds: its own code, stack and memory. The stock routines it calls, stock's stack and the stock interrupts are the instrument's, and no module has bounded them. When a number would need that analysis, write "not measured" with the reason and ask the owner the same day, on the pull request or in Discord's dev-support channel before there is one, instead of writing an audit.
Expand Down
14 changes: 10 additions & 4 deletions docs/OCTATRACK_DRIVING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ How to work a real Octatrack MKII running a Modwerk development base (`build_cor
USB: start the link, send steps, read the unit, change things without harming the owner's project, and keep
runs short. The flashed beta base refuses these steps; a twin of it is `build_core.py` with the flags that
`scripts/octatrack-base.mjs` passes for `--beta`, without the controller rows, plus `--dev` and an `--output` outside Git,
then `npm run device -- boot` that folder. The rows are left out because Analog BD's reviewed recipe refuses a twin's
layout, so test the base-bound modules (Analog BD, Sidechain, Output Matrix) on the tester image instead. Everything here was done on the owner's MKII on 11 October 2026. Why things fail is in
then `npm run device -- boot` that folder. Without the controller rows the twin trusts no base-bound module (Analog BD,
Sidechain, Output Matrix). To test one on a twin, build it as `octatrack-base.mjs` does, plus `--dev`: the empty-row pass,
`base_bound.py rows` against its `symbols.json`, then the build with `--controller-policies` and `--controller-layout`.
Analog BD's reviewed recipe refuses the twin's layout, so capture a twin-only one (`build.py --octabam-machine
analog-bassdrum --base-symbols … --capture-stock-recipe`), swap it into `machine-recipes.json` for those two steps and put
the committed file back (13 November 2026, MKII, twin `80f62ad6`). Boot with the one-shot `npm run device -- boot DIR
--chunk 4096`: the same day a boot sent through `serve` left the unit running but gone from USB until a power cycle, and
the same image booted one-shot came back. Everything else here was done on the owner's MKII on 11 October 2026. Why things fail is in
[OCTATRACK_HARDWARE_TRAPS.md](OCTATRACK_HARDWARE_TRAPS.md).

## The rules
Expand Down Expand Up @@ -143,7 +149,7 @@ error and moves on. macOS `nc -U` hangs up before the reply, so use the script.
| `dspmap`, `dspmap:watch:N` | each core's DSP code arena drawn as 64 cells, a letter per placed block and `.` per free words, then each block's name, start, words and the slots running it (else `preloaded`: code another Part of the bank names). `watch:N` redraws it for N seconds (at most 15) whenever a block comes, goes or moves, so compaction shows: on 24 October 2026 removing a Mini Verb between SPRING REV and DARK REV left a 538-word hole and DARK REV moved down into it 2.1 s later |
| `profile`, `profile:N` | the base's tick profile (`modwerk_cost`, diag.h): each UI tick hook's µs a tick over N seconds (3, at most 15) and the frame transfer's µs a frame. It reads the address from the build `boot DIR` last started (`~/.cache/modwerk-octatrack/last-boot`) and refuses when another base runs. On 24 October 2026 it showed the controller tick at 814 µs with 26 modules installed, 212 µs after the fix in #880 |
| `selftest:trace` | the same, printing each half second's phase, progress, the CPU the test recorded and the meter's latest window beside it: what a CPU or core peak came from |
| `stall` | the last DSP stall record: why, the loader's whole report, each core's last packet header, the live FX ids, as they stood when the stall was seen (it survives the power cycle); `stalltest` writes one now |
| `stall` | the last DSP stall record: why, the loader's whole report, each core's last packet header, the live FX ids, as they stood when the stall was seen (it survives the power cycle), then the 128 frames before it (46 ms): each frame's gap to the one before (363 µs on time) and how long the frame interrupt before it ran. A long gap after a long run is the frame interrupt's own work, after a short one something else held it off; `stalltest` writes one now |
| `guard`, `guard:MILLE` | the frame guard since the last read: the frame interrupt's longest frame, latest start and share of the CPU, the machine renders it skipped and on which tracks, and its share; `guard:MILLE` sets the share (per mille of a frame, `guard:65535` turns it off) |
| `try FILE`, `remove FILE` | install or remove a module package, accepted and kept at once: each one writes the card's set, so on a twin they replace the owner's kept modules (25 October 2026). For a trial that rolls back by itself, use `npm run device -- try FILE --seconds N --socket /tmp/modwerk-share.sock`; `npm run device:test` drives a package through such a trial ([the same run without a Keep](ADD_A_MODULE.md#octatrack-base-and-usb-lifecycle)) |
| `cpu`, `cpu:PERCENT` | the CPU budget: its allowance, what the Part's machines book and the tracks refused; then each machine track's longest render since the last read, both passes of a frame, in percent of a frame: what a machine's CPU price is set from. `cpu:PERCENT` sets the allowance |
Expand Down Expand Up @@ -209,7 +215,7 @@ and stock modifier release, and stop on a mismatch.
| effect ids and chooser rows | FX1_IDS `0x400d5f58`, FX2_IDS `0x400d5fdc` (id -> descriptor, name at +9), FX1_ID2POS `0x400d60d0`, ID2POS `0x400d6150` (id -> row) |
| chooser cursor | FX1 `0x460d5c94`, FX2 `0x460d5ca8` |
| input layers (menus, windows) | list at `0x460d165c`: two entries with no menu open |
| crash record, stall record | `0x100ff800`, `0x100ff900` (battery RAM) |
| crash record, stall record, the frames before the stall | `0x100ff800`, `0x100ff900`, `0x100ffb30` (battery RAM) |

To compare a Part's working and saved copies: `mem:` both addresses with length 6322 and compare the output.

Expand Down
29 changes: 23 additions & 6 deletions docs/OCTATRACK_TWO_SLOT_BASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ October 2026); the base still offers the other:
from RAM (the trial, the update's one restart), then writes itself and runs on. A base takes another base's
container here only on two slots (refusal 106 otherwise). Stock's routines need no quiesce: they mask
interrupts only around their command writes, the OS runs from RAM, and the upload engine has stopped the
transport. So the base writes while the OS runs on, with its own box (BASE UPDATE, SAVING THE NEW BASE, KEEP
THE POWER ON, a bar) and a BASE SAVED popup; the next start is from the new slot. Conversion comes first:
transport. So the base writes while the OS runs on, with its own screen (BASE UPDATE, SAVING NEW BASE,
a bar, below) and BASE SAVED; the next start is from the new slot. Conversion comes first:
the base the unit started from NOR is copied to slot B, confirmed (above any old container, so it stays whole
meanwhile), then the stub's sector, then this base into slot A. The stub's sector is the only moment a power cut
can still cost a startup-menu recovery: its erase and the pages up to the end of the stub's code, under a
Expand All @@ -103,10 +103,24 @@ the container, nothing is written (the site's repeat after a lost reply).

## Showing it on the Octatrack

- Writing: stock's own upgrade screen when quiesced (another base, two slots), the base's box otherwise.
- After it: BASE SAVED (it ran on), or, at the start from the new slot, BASE UPDATED; UPDATE UNDONE when the stub
went back to the previous base.
- Not built: anything on the unit while the base is sent and while the new base starts from RAM.
A base update takes the whole screen and the panel (owner, 13 November 2026); a module sync shows a box with a bar
and holds no key. `status.c` works the step out each tick from the upload (`ep0.c`), an armed RAM boot (`boot.c`)
and the write (`flash.c`):

- On the base being replaced: BASE UPDATE, RECEIVING NEW BASE with a bar, then RESTARTING. Only from the first base
that carries this screen on; older bases show nothing while the new one is sent.
- On the new base, started from RAM: WAITING FOR MODWERK, on two slots only (the slot NOR starts next names another
base, which one copy cannot tell); then RECEIVING NEW BASE as its container arrives, SAVING NEW BASE with the
write's progress, and BASE SAVED or NOT SAVED for three seconds or until a key.
- From the first packet of a base until the write is over, every key and knob is held back, PLAY and STOP too: after
its restart the unit loads the project and looks ready while nothing is saved. A release reaches stock only if its
press did. WAITING gives up 25 s after the start (a development RAM boot has no write to wait for) and 20 s after
the last sign of a container; a failed write frees the panel at once, and the page sends it again.
- The missing-modules page and the restore's notice wait until the screen is gone.
- Another base's write, quiesced (two slots): stock's own upgrade screen, as before.
- After a start from the new slot: BASE UPDATED; UPDATE UNDONE when the stub went back to the previous base.
- Not built: saving before the project loads. The container reaches the new base over USB, which is up only once
stock has started, so the save follows the restart by the time the page needs to reconnect and send it.
- A restart hangs at the logo when a DSP stalled before it in that run (the soft reset cannot park a hung core);
a power cycle starts the newest whole slot.

Expand All @@ -131,3 +145,6 @@ the container, nothing is written (the site's repeat after a lost reply).
off and on (BASE UPDATED, the confirm), then update A to B and back on two slots (stock's screen, one restart).
3. Pull the power during the slot write, several times at different points; pull the USB cable during the write.
4. Install a base that fails to start: after two power cycles the stub must start the previous one.
5. The update screen (not yet run on a unit): keys and knobs held back from the first packet to BASE SAVED, a key held
down across the start of the lock releasing cleanly, the 25 s give-up after a development RAM boot, the screen over
the startup animation and the project load, and a module sync's box while playback is stopped.
Binary file modified public/octatrack-modules/analog-bassdrum/0.1.7-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.
Binary file not shown.
Loading
Loading