Skip to content

SY DRUM 0.1.0: a new machine module (needs FM Synth; stacked on the FM Synth 0.1.5 PR) - #558

Closed
timhastie wants to merge 13 commits into
repeat98:mainfrom
timhastie:sy-drum-0.1.0
Closed

timhastie wants to merge 13 commits into
repeat98:mainfrom
timhastie:sy-drum-0.1.0

Conversation

@timhastie

Copy link
Copy Markdown
Contributor

Draft, stacked on the FM Synth 0.1.5 PR: #555. Until that PR is merged this branch also carries its commits; please review only the SY DRUM commits listed below.

SY DRUM 0.1.0: a new machine module (needs FM Synth)

SY DRUM is a new Octatrack module: a drum machine for audio tracks, chosen in the track machine list in the row after FM SYNTH. Each SY DRUM track is one voice — two oscillators, a pitch sweep, a two-pole low-pass filter opened by the decay envelope, six modes (A–F) and a dedicated LFO with sample-and-hold. It plays without a sample. It is my octatrick 2.14 module, timhastie/octatrick-modules at 39dc5d8ca285c14674b66a452f58a5a0364efb96 (tag v2.14), packaged for Modwerk as sy-drum 0.1.0-experimental.

SY DRUM is modelled on the 1979 Pearl Syncussion SY-1, written from a study of that unit's circuit. It is not affiliated with or endorsed by Pearl or Behringer. Syncussion is a trademark of its owner.

This PR is stacked on the FM Synth 0.1.5 PR (#555), the one FM Synth PR (it carries 0.1.4's two commits and 0.1.5's five). SY DRUM runs on FM Synth's engine, so the first seven commits of this branch are that PR's, up to its head 8798df4; please review only the six SY DRUM commits on top: fded9ed (shared code), 341b779 (the module), 6dc3a5b (packages and records), 6fb4adb (the evidence revision), 2e6e69d (TESTING, documentation only) and 71d9da9 (my hardware report, the evidence revision pointed at 341b779, the README and TESTING wording). The FM Synth branch is rebased onto Modwerk main c181b76; SY DRUM's first five commits were made on main 263aa1b and replay onto the FM Synth head without a conflict. (6fb4adb's message names 7a135ff, the measured source before an earlier rewrite of the branch; the module folder at 341b779 is byte-identical to it, and the record now names 341b779.) Modwerk main at the time I opened this PR: 4c0dda3 (fetched 13 Oct 2026, 09:50 PDT). Since c181b76 it has changed documentation, the elekloader USB link (src/engine/elekloader/octatrack-link.ts, src/components/LoadDialog.tsx and their tests) and Tape Echo's preservation binding, none of them a file this branch touches; a trial merge of this branch into 4c0dda3 has no conflict.

The pins. SY DRUM pins my v2.14; FM Synth 0.1.5 stays on my v2.13 (48b89e7). v2.14 changes only sy-drum/ (and my top-level README), so FM Synth's synth/ is the same bytes at both tags, and the FM Synth PR is unchanged by this one. FM Synth 0.1.5's change, my 2.13 scale quantizer on MIDI tracks, does not touch SY DRUM, a machine for audio tracks; in the test image FM Synth's engine and chooser objects are the same bytes as in the previous SY DRUM build.

What it does

  • Machine lists. SELECT MACHINE TYPE and SRC SETUP gain SY DRUM after FM SYNTH; SELECT MACHINE TYPE scrolls to the seventh row (FM Synth's row table; SY DRUM adds one row). A chosen track is FLEX plus S, Y, 1 in unused NEIGHBOR PLAYBACK bytes, the FM Synth mechanism (F, M, 1).
  • PLAYBACK: PTCH (semitones) MODE WDTH SWEP SPED DEC. These are the FLEX PLAYBACK bytes, so stock p-locks, the three LFOs and scenes reach them.
  • PLAYBACK SETUP (FUNC+SRC; FUNC+PLAYBACK on the MKI): LSPD LDEP WAVE S&H, the Part's defaults, and each lockable per step: in GRID RECORDING hold a trig and turn A–D (a locked control is drawn inverted while its trig is held), in LIVE RECORDING turn them while the pattern plays. The locks follow copy, paste, clear, shift, duplicate and undo, and are saved beside the project in sylock01–sylock16 .work / .strd files (my development builds' format, so a project moves between them and this module with its locks).
  • AMP, LEG MONO, the CHROMATIC keys, MIDI IN and the bundled scale quantizer work as on FM Synth.

What 2.14 changes: my hardware finding on the previous test image, and the fix

On the previous test image of this package (MAIN OS d0dab5fe…, my octatrick 2.12 SY DRUM beside FM Synth 0.1.5) I found on my MKI that PLAYBACK SETUP step locks edited while the pattern played came back without those edits after a power-off with no SAVE PROJECT, in about half of my runs, while the stock PLAYBACK locks always came back; with SAVE PROJECT both came back. The cause: stock keeps the current bank, and so its own locks, in battery-backed memory and writes its working files only while the sequencer is stopped; SY DRUM's working file sylockNN.work was written by that same stopped-only background save, so a power-off while playing, or within about a second of STOP, found it behind. A second cause gives the same symptom: when one stock bank file of the project is missing or unreadable at load, the SY DRUM files were not read at all.

2.14 (v2.14, SY DRUM only):

  • the working file is written while the sequencer plays too: one more stock hook, on the UI tick's transport test (0x4005224a), counts stock's 60-tick save cadence while a lock edit is unwritten and posts one job of the module's own (at most one write every 60 UI ticks, 1.2 s on the emulator; none while nothing is unwritten; the card write is an engine-task job like stock's own save);
  • the lock files load whatever the stock bank load returned.

A lock edited less than about 1.2 s before a power-off can still come back without that edit; SAVE PROJECT closes that.

Measured on the test image of this PR (MAIN OS 27c213b2…) with my previous image (d0dab5fe…) as the control, on an emulator build that carries the battery-backed memory over a power-off and loads the project at power-on as the unit does (ot_emu 07a7c08e…; everything else in this PR used the pinned 9dc31d12…). A project with Flex samples and three SY DRUM tracks; four PLAYBACK locks and five PLAYBACK SETUP locks recorded in GRID RECORDING, no SAVE:

after the edits this image previous image
playing, about 10 s more, power cut while playing 5 of 5 PLAYBACK SETUP locks back, 4 of 4 stock lock values 0 of 5 PLAYBACK SETUP locks back, 4 of 4 stock lock values
playing, STOP, power cut 0.6 s after pressing STOP 5 of 5 PLAYBACK SETUP locks back, 4 of 4 stock lock values 0 of 5 PLAYBACK SETUP locks back, 4 of 4 stock lock values
stopped, power cut about 4 s after the last edit 5 of 5 PLAYBACK SETUP locks back, 4 of 4 stock lock values 5 of 5 PLAYBACK SETUP locks back, 4 of 4 stock lock values
playing, one more lock, power cut 0.6 s after it every earlier lock back, 4 of 4 stock lock values; the last lock (still unwritten at the cut) lost: the residue —

And with bank05.work of the project removed, cut short or damaged before that power-on (stock's own message, answered NO): this image loads every bank's step locks (ready mask 0000ffff); the previous image loads none (00000000). Details: TESTING.md, "Power-off without SAVE".

On my MKI I also ran the play-time write in my own octatrick build on 12 October (not a Modwerk image, without the bank-load change): the locks came back after a power-off while playing, and I heard no crackling on a STATIC track streaming from the card while the lock file was written. On 13 October the test sheet of my octatrick 2.14 build (MAIN OS c367d8bc…) worked as well. Neither is a report of this image; the report under Hardware is.

What 2.12 changes (against my 2.11 SY DRUM)

  • A machine change resets the track's machine parameters. When a track changes to or from SY DRUM (or FM SYNTH), in either list, its PLAYBACK, PLAYBACK SETUP, AMP, LFO and their SETUP pages get the new machine's init; nothing of the old machine's settings carries over. Step locks and scene locks are kept, as the stock firmware keeps them on a machine change, and so are SY DRUM's own setup-LFO step locks (they play again when the track returns to SY DRUM); the FX1 / FX2 slots are kept. A change between two stock machines is unchanged. This is FM Synth's machine-list code, which serves both rows.
  • The metronome. FM Synth's engine no longer hooks the metronome's click word, which with CLICK ON released held engine notes at every beat. That is measured in the FM Synth PR; for SY DRUM it is in my hardware check.

The dependency on FM Synth, and the six shared-code changes (for your decision)

My manifest says requires=("SYNTH MACHINE",): SY DRUM's engine is called from FM Synth's engine paths (start, frame, render, the sample-free voice, the AMP envelope, keys, MIDI) and its row lives in FM Synth's machine list. Native octabam refuses SY DRUM without FM Synth. Octatrack modules in contract v2 have no requires, so each change below is the smallest I could find; each comes with the smallest alternative I see, so you can take it, change it or turn it down. The fallback for all six together is SY DRUM folded into the FM Synth package as an option of that module. The move to 2.14 changes none of them.

  1. src/catalog/selection-conflicts.ts: one rule, sy-drum-needs-synth ("SY DRUM needs FM Synth", fix: remove SY DRUM), with a test.
    Smallest alternative: leave the rule out and let change 3 refuse the build; the configurator then explains the refusal only when the image is built, not while choosing.
  2. scripts/build-module-packages.py: sy-drum joins REQUESTED. FM Synth's three units written per remix (poly, fmmachine, the bundled quantizer fm_qz; my HAVE_SYDRUM flag) are built for FM Synth alone as their ordinary objects, plus one variant each, whenModules: ["sy-drum"], withoutModules: ["poly8"], assembled with SY DRUM in the remix: the mechanism the POLY8 / sidechain variants already use. The requested packages carry, at their root, the native requires of the modules that have one ("requires": {"sy-drum": ["SYNTH MACHINE"]}); no group or object of another module changes.
    Smallest alternative: keep the variants (FM Synth alone must stay byte-for-byte as it is) but drop the requires field at the package root, and hard-code the one pair in change 3 instead.
  3. src/engine/requested-modules.ts: a selection holding a module whose native manifest requires a key that no selected module has is refused as a declaration collision ("SY DRUM requires SYNTH MACHINE"), the class native reports, so module:verify sees matching refusals. Test in requested-modules.test.ts.
    Smallest alternative: the same refusal as a hard-coded sy-drum / synth pair (one line, no package field; see 2).
  4. src/engine/core-logger.ts / static-compose.ts: SY DRUM's card-file code hooks the stock engine-job dispatch at 0x4008485e (its lock files must follow every load / save / reload / sync job, and since 2.14 it also runs its own play-time job there), the same 6 bytes the core logger's job hook takes. Native has no logger, so native builds; the browser refused with overlapping guards. The logger now leaves out a hook whose site a selected module owns and names it (yielded: ["job"]) in its configuration identity. For every selection that builds today nothing changes (a module writing inside a logger hook's span was refused with overlapping guards until now, so none does). A build with SY DRUM therefore logs no engine jobs. Test in core-logger.test.ts.
    Smallest alternative: none on Modwerk's side short of dropping the logger's job hook for everyone; on mine, I move SY DRUM's hook off that site in a later version of my source. Tell me which you prefer.
  5. src/engine/compose-os.ts: sy-drum joins the loader-free list.
    Smallest alternative: none smaller; it is one list entry, as for every requested ColdFire module (Output Matrix was added the same way).
  6. scripts/module-doctor.mjs and src/catalog/availability.test.ts: a module the selection rules refuse alone has no declaration record alone; the doctor says "refused alone by design" instead of failing, and the availability test builds SY DRUM beside FM Synth.
    Smallest alternative: leave the doctor unchanged and accept one red declaration checks row for SY DRUM (explained in TESTING.md); the availability test still needs its one companion entry, or SY DRUM listed as an exception there.

Besides these six: the scope tests (module-release-scope.test.mjs, module-source.test.mjs) and the licence inventory (SY DRUM's upstream LICENSE under the existing octa-panel component) name the new module, and the catalogue and package assets are regenerated.

One more item for your decision: Tape Echo's retained evidence. Tape Echo 0.1.3 keeps 0.1.2's evidence (since fdabc28e), and scripts/retained-evidence.mjs requires the build inputs it protects (src/engine with its assets, scripts/build-module-packages.py, scripts/module-source.mjs and the SDK's platform, DSP and build tools) to stay as they were at its evidence commit 32ac46b, unless a preservation binding covers the change. On current main that binding is the Tape Echo browser preservation record (sdk/browser-infrastructure-preservation/tapeecho-usb-integration.json, added in 62cf7ab0, read through scripts/browser-infrastructure-preservation.mjs): it lists by hash the protected files main itself has changed, and any other change to a protected input fails it. This branch changes four of those engine files (changes 3 to 5), the package builder (change 2) and the package assets (SY DRUM's units, with the first memoryRegions entry in coldfire-packages.json: the 2 MiB lock table, the first module with a DRAM region). What ran, 13 October 2026: on this branch as pushed (FM Synth's head on main c181b76), npm run modules:generate and npm run modules:check -- --base origin/main both stop with "Invalid or stale Tape Echo browser preservation binding; full qualification is required"; on a local trial merge of this branch into current main 4c0dda3 (whose commit 7d6d173e refreshed that binding), modules:generate stops with the same message. What it stops: modules:generate (so src/catalog/module-documents.json, compatibility-checks.json and public/module-media/ are not regenerated in this PR, and the catalogue does not list SY DRUM yet), modules:check, and therefore npm run check -- --base origin/main after its licence and machine checks, and npm run module:doctor -- sy-drum, which does not find SY DRUM in the catalogue ("Unknown module: sy-drum"). FM Synth 0.1.5's PR stops at the same gate, so this is not SY DRUM's alone. I cannot clear it from my side: it needs your binding for these changes, as you did for VECTOR and for Tape Echo's USB work, or a fresh Tape Echo qualification. A modules:generate after that produces the catalogue files; I am glad to push that commit once the gate passes.

A second item for your decision: main's Octatrack base. src/engine/elekloader/octatrack-base.test.ts (new on main since this branch's packages were made) reports the committed Octatrack base stale, because the base records src/engine/assets/chooser-metadata.json among its inputs and this branch's packages add SY DRUM's row to it. The test's own fix is npm run octatrack:base -- --write; I have not run it, since it rewrites the shared base package (octatrack-base.elemod and its fills) that you generate. Tell me if you want that commit in this PR.

Conflicts and limits

  • FM Synth's exclusions apply (Analog BD, VECTOR, the public Scale Quantizer). SY DRUM is not built with POLY8 (its bridge owns the machine list; see the FM Synth PR's open POLY8 question).
  • My manifest also declares conflicts with three octabam modules this catalogue does not carry: KITS and PLOCKS P2 (the same project-file, clipboard and lock-queue hooks) and STEM REC (the 2 MiB lock table and STEM REC's buffers do not fit the platform reserve together). Modwerk's native declaration drops them (as FM Synth's does for its conflict) and octamod.module.json lists them.
  • A build without SY DRUM reads a SY DRUM track as an empty FLEX track; choose a stock machine and save before downgrading.
  • Stock-flow changes (README, "Changes to stock flows"), for your review: the SY DRUM row; the machine change's init; the setup page of a SY DRUM track and its held-trig step locks; the card's sylockNN companion files on save / reload / sync / save-to-new / export / delete — a file format of mine, which the module guide asks contributors not to invent, so this one needs your decision — and, since 2.14, a write of the working file while the sequencer plays (at most every 1.2 s while a lock edit is unwritten) and their load beside a missing stock bank file; the sequence edits that carry the companion locks; the trig LEDs.

Qualification (emulator, exact image)

Image: MAIN OS 27c213b2dca96e071200d97441de821c900bf9f87556af0918da200ef623aed9, FM Synth 0.1.5-experimental + SY DRUM 0.1.0-experimental with all stock effects, built by scripts/export-composition-proofs.py --vendored-sdk --static-stock --modules synth,sy-drum --select synth+sy-drum:true from this branch (rebuilt from this branch's head after the restack onto the FM Synth PR: the same MAIN OS and the same update file). Update file 1644e5fa… (OCTAMOD79). Pinned ot_emu 9dc31d12… for everything but the power-off walks (07a7c08e…, above). My tools/align_check.py (2.14) finds no instruction and no hook target at an odd address in it. Details: TESTING.md.

  • Cycles. Emulated ColdFire instructions per 16-sample frame interrupt, converted at the CF METER calibration (9.44 ns an instruction, 264 MHz), as FM Synth's record does; not chip wall-clock cycles, and not hardware percentages. The load: a trig on every step, the LFO at full speed and depth with S&H, step locks and p-locks on every step, LFO1/LFO2 and scenes. Every load ran five times on this image (a fresh emulator each time) and the record takes the maximum.
    • One track's START block peaks at most 29,017 instructions (5 runs): 14,246 cycles over idle (5,716 instructions).
    • maxInstances is 2: 2 tracks peak at most 33,419, 33,791 with FM Synth's 372-instruction allowance, 84,213 of 95,779 cycles/block.
    • 3 tracks stay inside the block in every run (5 runs, at most 37,749), but with 311 instructions to spare, less than the 418-instruction margin (the largest run-to-run spread seen on this frame-interrupt code, on the previous image's runs, where three tracks peaked at 37,903; 208 on this image's), so I do not claim them. 4 tracks reach 1.110x and 8 tracks reach 1.560x the block on their START blocks (sustained inside it); not qualified.
    • Idle cost of the module: sd_tick is 606 emulator instructions a frame with the module in the image and no SY DRUM track. The 2.14 hook is on the UI tick, outside the audio interrupt: 34 instructions a UI tick while playing, 2 while stopped (static count).
    • Against the previous image (d0dab5fe…): the peak blocks moved by s1 -16, s2 +14, s3 -154, s4 -14, s8 -344 instructions, inside the larger run-to-run spread (418; 208 here); the frame interrupt runs the same code on both images; maxConfiguration 84,178 -> 84,213, maxInstances 2 -> 2.
  • Memory. Exact from the link map: 136,061 B SY DRUM unit (698 B a track x 8 preallocated; 346 B more than the previous image's, the 2.14 card code), the 2 MiB lock table at the top of the existing platform reserve, 488 B of FM Synth growth, 410 B of hook sites (53), 53,869 B more appended payload; 2,297,463 B in total.
  • Stack. Static bounds from the linked disassembly: frame path 72 B, UI hooks 128 B, engine-task card jobs 1,216 B (stock file-system callees excluded); 1,288 B recorded, as before.
  • Persistence and two instances (8 October rules), on the emulator: two SY DRUM tracks with different settings and step locks stay isolated through edits and a FLEX round trip; Part SAVE / RELOAD restores both; SAVE PROJECT, the project's load from the card and PROJECT > RELOAD bring the saved locks and Part bytes back; audio plays after each. The power-off without SAVE with the power-on load is measured above.

Load, plainly

FM SYNTH and SY DRUM tracks are heavy on processor load, and track freezes with many instances of either are expected. SY DRUM runs on the ColdFire inside the audio interrupt. Two SY DRUM tracks take about 88 % of an audio block on the emulator (84,213 of 95,779 cycles, this record's maxConfiguration); the record qualifies two instances and not three. FM SYNTH tracks in the same project add their own load (one at VOIC 4 is about 85 % by itself). Beyond that the block overruns, and then the unit can stall or freeze. That is expected with many instances of SY DRUM or FM SYNTH; it is not a defect of a particular project. The figures are emulator cycle counts at the record's calibration, not hardware times; the Processor Load module (submitted separately) shows the headroom on the unit.

Hardware

I tested the exact image above (MAIN OS 27c213b2…, FM Synth 0.1.5-experimental + SY DRUM 0.1.0-experimental, the update file 1644e5fa…) on my Octatrack, working through every item of its test sheet. The same facts are in tests.qualification.hardware (hardwareStatus reported, my own report) and in TESTING.md:

  • tested on: 2026-10-13
  • tester: timhastie
  • model: MKI
  • summary: every item of the image's test sheet worked. SY DRUM and FM SYNTH appeared in both machine lists and chose their machines; SY DRUM → FM SYNTH → SY DRUM gave each machine's init and kept the PLAYBACK SETUP step locks; a held SY DRUM note rang out with CLICK ON; after a power-off without SAVE PROJECT while playing (and about a second after STOP), the stock and PLAYBACK SETUP step locks on two SY DRUM tracks were kept; two SY DRUM tracks with their own step locks came back and played after SAVE PROJECT and a power-cycle; a STATIC track streaming from the card played without crackling while PLAYBACK SETUP locks were edited.
  • limitations: play time, the MIDI gear used, the number of tracks played at once and the number of power-off runs were not recorded, nor whether the one lock edited just before the quick power-off (the sheet's known limit) came back. It is a functional check of the sheet's items, not a load or stress qualification; the cycle and memory figures above are emulator measurements, not hardware times.

manifest.py's proof_note still says the hardware report is pending: that file is part of the native source fingerprint the measurements are bound to, so I left it as measured; README.md, TESTING.md, docs/VERIFICATION.md and the record carry the report.

History, not evidence for this image: the previous test image of this package (d0dab5fe…) gave the finding above; the play-time write ran on my MKI in my own octatrick build (above); on 10 October 2026 I tested the 2.11 build of this package (b9b65cd9…) on my MKI and saw no issues other than the machine change not resetting a track's parameters, which 2.12 changes. Those builds are superseded and their reports are not carried over; the report above is for this image.

Verification

  • Machine list and Part bytes. The module's own verify.py passes on the image (evidence/native-smoke.json): SY DRUM chosen on all eight tracks, the Part and battery-RAM shadow signature S, Y, 1 and the seeds, sample slots untouched, reselection keeps an edit, FM SYNTH one row up still writes F, M, 1, FLEX clears it. A walk of both windows on a project card reads the same bytes back as on the previous image.
  • Machine change (2.12). On a project with two SY DRUM tracks, T1 edited on every page with a setup-LFO lock recorded, then SY DRUM → FM SYNTH → SY DRUM in SRC SETUP: FM SYNTH's init, then SY DRUM's init (the same bytes as an untouched FLEX track choosing SY DRUM), FX1 kept, FX2 untouched, the setup-LFO lock rows unchanged throughout, the other SY DRUM track untouched, audio after (21 of 21 checks). The stock step and scene locks were not read back in that walk.
  • Sound. Seven of my verification cards rendered on this image match my own octabam 2.11 renders, 17 of 17 windows on every active channel (at most one sample of lag), and the previous image's renders the same way; five of them I had compared with my on-board development engine the same way.
  • Persistence and two instances (emulator). Two SY DRUM tracks stay isolated through edits, a FLEX round trip and back (each change giving the new machine's init, the step locks kept); Part SAVE / RELOAD restores both; SAVE PROJECT writes sylock01.strd / .work beside the stock files, a project load from the card brings the working locks, PROJECT > RELOAD the saved ones; audio after each step. (The previous record called that project load a cold boot; it is the menu's load. The power-on path is the power-off section above.)
  • Browser vs native. npm run module:verify -- sy-drum on the host toolchain (no Docker here; run on main 263aa1b, before the restack): 126 selections, 4 builds matching outside the platform writes, 122 matching refusals, 0 mismatches. POLY8 is left out of that pool locally (the FM Synth PR's open POLY8 question). Analog BD's matrix re-verified before rebinding its source identity (162 native OS and bootloader comparisons, 50 matching refusals, 6 firmware round trips).
  • Captures. Five black-and-white LCD captures from the image (media/capture.json), byte-identical to the previous image's: 2.14 changes no page.
  • Checks on this branch as pushed (13 October 2026, Node 24.21.0, main's gates as they are, nothing switched off): licenses:check, machines:check, elekloader:check, sdk:imports:check, typecheck and lint (0 errors; 7 warnings, all in files this branch does not touch) pass. SDK tests (python3 -m unittest discover -s sdk/tests, as in sdk:check): 231 run, 7 skipped, one failure, test_measured_release_keeps_hardware_attribution_separate (FM Synth's POLY8 item, as on its branch alone); sdk/tests/test_sy_drum_import.py passes. modules:check, modules:generate and module:doctor -- sy-drum stop as described under the Tape Echo item. npx vitest run: 2,249 of 2,286 pass. The 37 failures: 18 are the ones FM Synth 0.1.5's branch shows for the catalogue modules:generate could not regenerate (compiled versions and changelog against the old catalogue: module-build, compose-os, core-logger, session x4, build-support x2, module-changelogs, downloaded-module-guide, src/tooling/check.test.ts x7); 8 more for the same reason with SY DRUM, which is not in that catalogue yet (selection-conflicts, requested-modules "SY DRUM on FM Synth" x2, availability, compatibility-checks x2, module-contract, resource-indicators); 4 are FM Synth's POLY8 item (requested-modules x2, native-comparisons, synth-release); 2 are the gate itself (module-qualification "preserves tapeecho qualification", scripts/module-doctor.test.mjs); 4 are scripts/module-pages.test.mjs, which needs public/module-media/ (written only by modules:generate; these also fail on main in the same environment); and 1 is the Octatrack base item above. In a throwaway clone of this head, npm run licenses:generate, the packages built from source (scripts/build-module-packages.py --include-requested, host toolchain) and their --development import change no committed file, and --check-only --verify-existing passes; Modwerk's exporter rebuilds MAIN OS 27c213b2… and update file 1644e5fa…. The native comparison (module:verify) was not rerun after the restack; since its run on 263aa1b main has changed nothing in sdk/octabam/ or the package builder.
  • Stock bytes. My tools/stock_scan.py (2.14) finds no run of 16 or more stock bytes in any data file of the module folder; over upstream/ assembled it finds the same 4 runs as my reviewed 2.14 scan of sy-drum/ (displaced-instruction replays and assembler idioms, at most 19 bytes); over every other file this branch changes, the runs added against the FM Synth head are all in the package assets and every one is among the reviewed runs of my 2.14 scan, the SY DRUM scan above and FM Synth's reviewed 2.13 scan.
  • Earlier, on main 263aa1b, before the restack (a scratch copy with test values in both hardware fields and that one Tape Echo comparison switched off, two lines of scripts/retained-evidence.mjs, never committed): the import --check-only --verify-existing, modules:generate and modules:check passed; module:doctor -- sy-drum was green except SY DRUM's performance row (no static per-frame bound, no MIDI flood); npm run check -- --base origin/main passed licenses:check, machines:check, modules:check, elekloader:check, licenses:generate, machines:generate, modules:generate, typecheck, build:bundle; 1,768 of 1,786 tests passed, the failures explained in TESTING.md.
  • One open item from main's library warning (found in that earlier run, with SY DRUM in the catalogue). src/catalog/add-blocks.test.ts ("flags every other module beside standalone MIDI Scenes and MIDI Scenes beside anything") fails with SY DRUM in the catalogue. The add-warning reads the selection rules, and sy-drum-needs-synth is a "needs" rule whose only fix removes SY DRUM itself: beside MIDI Scenes the warning finds no swap that keeps SY DRUM, and with FM Synth not selected the chip says "Conflicts with" and names no module. I have not changed the warning; the smallest fix I see is for it to treat a rule whose fixes all remove the candidate as "Needs FM Synth" (or for the rule to offer "Add FM Synth", the addIds field I left out of change 1). Your call.

Review scope

New module: the owner reviews the first release. Besides the module folder this changes the shared code listed above, docs/VERIFICATION.md, the release notes and the regenerated assets. The two automatic-publication boxes are left unticked.

Checklist

Ticked only where the facts above show it for this branch as it stands; the rest are open for the reasons under the Tape Echo item, Verification and the POLY8 note, or are your decisions.

  • Automatically merge and publish my module update after checks pass.

  • I verified the source, licences, UI and qualification reports, including instance isolation and Part/project/reboot persistence where applicable; no required acceptance check is failed or untested.

  • New or changed module: I read the guide for its category in docs/module-guides/ (and sequencing.md if it acts in time), walked its checklists, and TESTING.md lists what I did not test. npm run module:doctor -- <id> is green.

  • Every code change to a module has a greater semantic version and exact catalog pin (documentation and media edits need none).

  • Source and media are original or properly licensed; authors and full licence texts are preserved.

  • No Elektron firmware, extracted routines/tables, upgrade files or other infringing material is included.

  • Every new/changed module with OT UI has real screenshots of its location and relevant control pages, manifest access instructions, and media[].otUi version/build/setup provenance. Automatic USB modules without OT UI use the narrow access.noUiReason declaration for reviewer verification.

  • I verified the captions, button/menu steps, screenshot authenticity and page coverage against the current module UI.

  • TESTING.md records commands, results, exact source, conditions and limitations; emulator and hardware evidence are separate.

  • CPU, DSP core and memory gauges are populated in resources.impact with rough load tiers, workload, rationale and source records. Reviewer: I checked the estimates; they do not imply exact hardware headroom percentages.

  • Runtime/resource changes and new modules: tests.qualification contains worst-case cycle counts for every processor used, under parameter extremes, simultaneous modulation, mode changes and maximum load, within the declared real-time budget.

  • Runtime/resource changes and new modules: exact memory regions include code, state, tables, buffers, stack/heap, padding and shared allocations; words, word widths, bytes and maximum-instance totals agree with the native allocation/build report.

  • Runtime/resource changes and new modules: this version/source/build has attributed real-hardware test evidence verified by the reviewer (the owner for first releases; the registered author for scoped updates). Record tester, date, source/build identity, observed behavior, workload and limitations. A reported functional test must stay labelled as reported; unknown model, duration and maximum load must stay explicit.

  • Reviewer: for new modules and runtime/resource changes, I verified the actual cycle/memory reports, coverage of modulation and maximum load, and attributed hardware evidence and its actual coverage against the submitted source/build. A declaration or emulator-only result is insufficient.

  • Complete README/TESTING/licence/control/compatibility documentation, at least three practical tutorial steps and real black-and-white PNG screenshots document the current behavior with honest version/build provenance; README and the applicable qualification/retained-evidence documentation record are synchronized. Yellow captures do not qualify.

  • Reviewer: I verified tutorial usefulness, all relevant control/page coverage, screenshot authenticity, exact access instructions and full documentation completeness.

  • I rebased onto current main and ran module/version checks on this revision.

Credits

  • Tim Hastie: SY DRUM — the engine, its pages, the setup LFO, the step locks and their card files — and this packaging. MIT; full terms in the module's LICENSE, the original in upstream/LICENSE.

How to test

  1. npm ci, then npm run check -- --base origin/main and npm run module:doctor -- sy-drum (until the Tape Echo gate is bound, the first stops at modules:check and the second does not find SY DRUM in the catalogue; see the Tape Echo item).
  2. npm run module:verify -- sy-drum --os <your OCTATRACK_OS1.40C.bin> (see the POLY8 note above).
  3. In the configurator select SY DRUM alone: it is refused ("SY DRUM needs FM Synth"). Add FM Synth and download. Install it with PROJECT > OS UPGRADE.
  4. On an empty track press FUNC + SRC, go down past FM SYNTH to SY DRUM and press YES. Place trigs and press PLAY: a drum at C4, no sample. Turn MODE through A–F.
  5. FUNC + SRC: set WAVE TRI and LDEP up. In GRID RECORDING hold a trig and turn LSPD: that step's LFO is faster; holding the trig shows LSPD inverted.
  6. Turn some knobs, then FUNC + SRC, UP to FM SYNTH, YES, and DOWN to SY DRUM, YES: the track is back at SY DRUM's init, and the LSPD lock from step 5 still plays on its step.
  7. With the pattern playing, lock LSPD on another step and keep playing for two seconds, then switch the unit off without SAVE PROJECT. Switch it on: both locks are back. SAVE PROJECT writes sylock01.strd beside sylock01.work in the project folder.

…13ed8)

The engine, page and the author's FM SYNTH chooser are vendored byte for byte from
timhastie/octatrick-modules v2.12 (ca13ed8): the 2.10 engine with 6 dB of headroom, the 2.11
machine list (a row table, six row-count detours in place of the 2.10 pokes, five row-clamp
detours, the SELECT MACHINE TYPE scroll) and the two 2.12 changes: the metronome click no
longer cuts a held note (the engine's hook on the click word is removed), and a machine change
to or from FM SYNTH resets the track's machine parameters to init, step and scene locks kept as
in stock (one new detour, po_unload). With FM Synth alone the list keeps its six rows.
Modwerk's machine.s / registration.s are dropped; the bundled quantizer keeps 0.1.2's drawer
fix (repeat98#264). Every stock expectation is a stock_guard (73). The author's native Analog BD
conflict is cleared in the wrapper (Modwerk declares it in octamod.module.json).

Emulator evidence on the image built from this source (MAIN OS 00fa6caa): cycles (one VOIC 4
track 81,422 of 95,779 cycles/block, the largest of five runs), exact memory (43,812
module-owned bytes), stack bound 352 B, two-instance Part/project save and reload, the
metronome case and the machine-change init, captures, Part smoke, odd-address check. Hardware
fields are placeholders for the author's report. The native/browser record is not refreshed:
POLY8's bridge is bound to 0.1.2's chooser hooks (TESTING.md).
…racks)

The bundled quantizer moves to timhastie/octatrick-modules 48b89e7 (tag v2.13; its synth/
files are the same bytes as v2.12 ca13ed8, so the engine, chooser and page are 0.1.4's). With a
scale on, a MIDI track's NOTE knob and NOTE locks step through the scale from ROOT and its
CHROMATIC keys play the scale from ROOT (three new detours: 0x40055382 qz_mknob, 0x40050b98
qz_mplock, 0x400436a8 qz_mkey); NOT2..4, TRAN, the arpeggiator, MIDI IN and SCALE OFF are
unchanged. 2.13 takes Modwerk's repeat98#264 form of the CHROMATIC drawer hook (0x400449be), so
quantizer.s, core.s, keys.s, scale.s and README.md are the author's files byte for byte; the
quantizer manifest is his 2.13 manifest in stock_guard form (23 guards). The module LICENSE
carries his 2.13 LICENSE (one sentence crediting Modwerk's form). Inventory
sdk/imports/synth-48b89e7.json, following 0.1.4's synth-ca13ed8.json.

Emulator evidence on the image built from this source (MAIN OS 37661aee): the FM objects are
byte-identical to 0.1.4's at the same addresses; NOTE knob / locks 409 of 409 and CHROMATIC keys
245 of 245 against a model of the rules, SCALE OFF as stock, the sequencer unchanged, the
quantizer's added cost per call; Part smoke, two-instance persistence, machine-change init;
exact memory (44,404 module-owned bytes), stack bound 352 B; 0.1.4's cycle table with its
bounding cards rerun here; seven captures (a MIDI track's CHROMATIC page new; two 0.1.4
captures dropped for the eight-media limit). Hardware fields are placeholders for the author's
report.
…nly); Analog BD record re-verified and rebound

build-module-packages.py --include-requested on this source, imported with --development: only
the Synth objects and the Synth version change against main's 0.1.2 packages (Modwerk's
fmregistration object is gone, the author's poly and fmmachine objects come in, the bundled
quantizer objects change; 0.1.4 has no package commit of its own on this branch). The Analog BD
record's moduleSourceTreeSha256 is rebound after verify-analog-bd-native.mjs passed against the
new packages (162 native OS and GNU bootloader comparisons, 50 matching refusals, 6 firmware
round trips).
… scale, and a per-track exemption, are planned; a drum track is programmed with SCALE OFF until then
…evidence revision bcf3b7f (the measured source on main c181b76); import inventory refreshed
…e it alone, as native octabam does (its manifest requires SYNTH MACHINE); FM Synth's per-remix units gain a SY DRUM variant; the core logger yields a hook site a selected module owns; module:doctor accepts a module refused alone by design
…module (stock_guard form, README, TESTING, captures, smoke, emulator qualification)

upstream/sy-drum/ is timhastie/octatrick-modules v2.14 (39dc5d8) sy-drum/ byte for byte except
manifest.py, whose 53 stock expectations are stock_guard(address, length, sha256) of the user's
1.40C (410 bytes, each recomputed and checked against the literal it replaces);
upstream/synth/engine_abi.inc and upstream/LICENSE are the same tag's. SY DRUM needs FM Synth 0.1.5,
which stays pinned to v2.13 (48b89e7): v2.14 changes only sy-drum/ and the author's README, so
synth/ is the same bytes at both tags.

2.14 is the author's fix of his hardware finding on the previous test image of this package (MAIN OS
d0dab5fe, the 2.12 SY DRUM): PLAYBACK SETUP step locks edited while the pattern played were lost by
a power-off without SAVE PROJECT, while the stock locks came back. The step locks' WORK file is now
written while the sequencer plays too (one more hook, the UI tick's transport test at 0x4005224a: at
most one write every 60 UI ticks while a lock edit is unwritten), and the lock files load whatever
the stock bank load returned. The SY DRUM unit differs from the previous build's only in the code
assembled from locks_io.inc.s (+346 B).

Emulator evidence on the image built from this source with FM Synth 0.1.5 (MAIN OS 27c213b2): cycles
(2 tracks 84,213 of 95,779 cycles/block, the maximum over five runs of each load), exact memory,
stack bound, the machine change SY DRUM -> FM SYNTH -> SY DRUM, two-instance Part / project-load
checks with the sylock files, the power-off without SAVE (while playing, 0.6 s after pressing STOP,
stopped) with the power-on load and the battery-backed memory carried over (an emulator build that
keeps it; the previous image as the failing control), a removed / cut / damaged stock bank file at
that load, the machine-list walk, seven renders identical to the author's, captures, Part smoke,
odd-address check. Hardware fields are placeholders for the author's report of this image.
…main 263aa1b (beside FM Synth 0.1.5); Analog BD's matrix re-verified and rebound
…he records as committed (main 263aa1b), and main's Tape Echo retained-evidence gate, also on the current main; the comparison with the previous image names the larger run-to-run spread
…vidence revision 341b779 (the measured source, restacked on FM Synth 0.1.5 at 8798df4 on main c181b76); import inventory refreshed; TESTING's repository validation states the checks on the restacked branch
@repeat98

Copy link
Copy Markdown
Owner

Hey Tim, Modwerk can't have cross-dependency between modules.

@timhastie

timhastie commented Oct 14, 2026 via email

Copy link
Copy Markdown
Contributor Author

@repeat98

Copy link
Copy Markdown
Owner

Thanks for SY DRUM. I can't take it in its current form:

  1. It depends on FM Synth. Every module now has to build, install and work when it is the only module chosen: no requires, no hooks into another module, no "needs X" selection rule, and no pull request stacked on another module's (AGENTS.md and cross-compatibility, added today). SY DRUM requires FM Synth, runs on its engine, and is stacked on FM Synth 0.1.5: scale quantizer on MIDI tracks (the author's 2.13 quantizer) #555.
  2. It is still a draft, and its checks have not run.
  3. USB lifecycle. A new Octatrack module needs the base/USB lifecycle next to the card build (requirements). I don't see it in this PR.

If SY DRUM stands on its own, most of the shared-code changes in the description go away: the "needs FM Synth" rule, the requires handling and the FM Synth variant packages. I'd look at what is left then.

Please don't spend time rebasing it for now. FM Synth 0.1.5 (#555) is blocked by its own checks; see my comment there.

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.

2 participants