Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
167 changes: 166 additions & 1 deletion docs/VERIFICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1170,7 +1170,6 @@ Waives fresh physical reboot, Part/project restore after unsaved edits,
distinct-track isolation and stock/flood performance tests. Retain the
author's MKII functional report; missing checks stay unverified.


## Air Chorus 0.1.0 — 9 October 2026

Chris Johnson’s MIT-licensed Airwindows Chorus is ported from pinned published
Expand Down Expand Up @@ -1388,3 +1387,169 @@ E-Verb is in none of them.

No hardware result exists for 0.1.1; the owner's MKII report covers 0.1.0.
Publication needs the owner's approval of this exact source.

## FM Synth 0.1.4 (octatrick 2.12) — 11 October 2026

Local draft by the module author's port; not published, no owner approval.
FM Synth moves to the author's octatrick 2.12 line
(`timhastie/octatrick-modules` `ca13ed8`, tag `v2.12`). The engine, page,
the author's FM SYNTH chooser (`machine.s`, adapted from Modwerk's 0.1.1
chooser, same Part signature) and its machine-list table are vendored byte
for byte. Modwerk's `machine.s` / `registration.s` and their hooks are
dropped; the bundled quantizer keeps 0.1.2's drawer fix. The six chooser
row-count pokes of the 2.10 line are detours reading the table, five more
widen the machine window's row clamp, and SELECT MACHINE TYPE can scroll.
2.12 removes the engine's hook on the metronome's click word (with CLICK ON
it released held notes at every beat) and adds a machine-change init: a
change to or from FM SYNTH resets the track's PLAYBACK, PLAYBACK SETUP, AMP,
AMP SETUP, LFO and LFO SETUP to init, keeping step and scene locks and FX1 /
FX2 as a stock machine change does (one new detour, `po_unload` at
`0x40022732`). The engine carries the author's 6 dB of headroom in place of
0.1.2's 18.06 dB `OUTPUT_SHIFT`: 0.1.4 plays about 12 dB louder than 0.1.2.
Inventory: `sdk/imports/synth-ca13ed8.json`; native source SHA-256 `4fc06072…`.

The native exporter built the standalone image, MAIN OS `00fa6caa…`
(update file 462,396 B, `2267af13…`; Modwerk's TS packer decodes it to that
MAIN OS and re-encodes the same bytes). The author's page linked to his
ratified bytes, and the author's `tools/align_check.py` (v2.12) found every
instruction and every detour target at an even address. The module's own
`verify.py` Part smoke passed on that image. Seven of the eight LCD captures
are byte-identical to the 2.11 build's; the LFO page's VOIC dial now shows
the init byte. In the emulator on the same image, a held key is
sample-identical with the metronome on and off and a held VOIC 4 chord's
voices are never released (the 2.11 build `248d987e…` cut both), and
FLEX → FM SYNTH → FLEX → FM SYNTH gives FM SYNTH's init both times with FX1
kept. Emulator qualification: one VOIC 4 track at 81,422 of 95,779
cycles/block (the largest of five runs, + the wrapper allowance), exact
memory of 10,505,016 bytes, of which 43,812 are module-owned, and a 352-byte
stack bound from the assembled runtime. Two FM tracks with distinct
settings pass Part save/reload, project save/reload and a fresh project
load in the emulator; the battery-RAM-only restart needs the unit.
0.1.4's hardware fields were never filled: 0.1.5 (below) superseded it before
submission, and its report covers the 0.1.5 image only.

POLY8 0.2.6's shared-chooser bridge is bound to 0.1.2's chooser claims, which
this update replaces: a selection with POLY8 and FM Synth stops at the
bridge's seam check on both sides. `module:verify -- synth` therefore stops at
the first such selection, and two POLY8 composition tests fail; the
`sdk/native-comparisons/synth.json` record is not refreshed. Without POLY8 in
the pool (a local run on main `3151975`, not committed) the comparison gave
122 selections: 35 builds match outside the platform writes, 87 refusals
match, 0 mismatches, the native side on the host toolchain instead of Docker.

Packages were rebuilt from source with `--include-requested` and imported
with `--development` on main `3151975`; only the Synth objects change. The
DSP assembler was Modwerk's pinned dsp56300 with the current patch. The
retained Analog BD record was re-verified against the new packages (162
native OS and GNU bootloader comparisons, 50 matching refusals, 6 firmware
round trips) before its source-inventory binding was updated. No firmware is
committed.

## FM Synth 0.1.5 (octatrick 2.13 quantizer) — 12 October 2026

Local draft by the module author's port; not published, no owner approval;
it follows the unpublished 0.1.4 above. FM Synth's bundled quantizer moves to
the author's 2.13 line (`timhastie/octatrick-modules` `48b89e7`, tag `v2.13`,
which leaves `synth/` byte-identical to `v2.12`): 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`,
`0x40050b98`, `0x400436a8`); chord offsets, TRAN, the arpeggiator, MIDI IN
and SCALE OFF are unchanged. The CHROMATIC drawer hook at `0x400449be` is
now the author's own form of Modwerk's #264 fix, so `quantizer.s` is his
file byte for byte; its manifest is his 2.13 manifest in stock-guard form
(23 guards, each checked against the developer's MAIN OS). Inventory:
`sdk/imports/synth-48b89e7.json`; native source SHA-256 `401d3b61…`.

The native exporter built MAIN OS `37661aee…` (update file 462,628 B,
`b42048d5…`; Modwerk's TS packer decodes it to that MAIN OS and re-encodes
the same bytes). The FM engine and chooser objects are byte-identical to
0.1.4's and link to the same addresses; the page links to the author's
ratified bytes; the author's 2.13 `tools/align_check.py` found every
instruction and every detour target at an even address. In the pinned
emulator on that image: the NOTE knob and NOTE locks matched a model of the
rules in 409 of 409 cases over eight SCALE / ROOT settings (0.1.4's image
matches the stock clamp in all 409), the MIDI CHROMATIC keys in 245 of 245
(note-offs balanced, the arpeggiator fed snapped notes), CHROMATIC on a MIDI
track draws and plays, the sequencer, MIDI IN and the arpeggiator send the same MIDI
events as on 0.1.4's image under SCALE OFF and three scales. The quantizer's added cost is 49–77
instructions per CHROMATIC key and up to 278 / 302 per NOTE knob / lock
turn of three clicks, all in the UI task. The module's `verify.py` Part
smoke, two-track Part / project save and reload, a fresh project load and
the 2.12 machine-change init passed on this image. Exact memory: 44,404
module-owned bytes (+592: the quantizer's DRAM unit +360, three hook sites,
the larger loader); stack bound 352 B, unchanged. Cycles: 0.1.4's table
(same FM objects); the bounding cards rerun twice on this image stay inside
the rig's spread, and the record's bounds over all seven runs give one VOIC
4 track 81,459 of 95,779 cycles/block. Seven LCD captures (five
byte-identical to 0.1.4's; a MIDI track's CHROMATIC page is new). The
hardware fields hold the author's MKI report of this exact image
(timhastie, 2026-10-13).

POLY8's bridge still stops on the author's chooser, as for 0.1.4, so
`sdk/native-comparisons/synth.json` is not refreshed. Without POLY8 in the pool (a local
run on main `263aa1b` in a scratch copy, not committed) the comparison gave
122 selections: 35 builds match outside the platform writes, 87 refusals
match, 0 mismatches, the native side on the host toolchain instead of Docker.

Packages were rebuilt from source with `--include-requested` and imported
with `--development` on main `263aa1b`; 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, so these packages serve both.
A fresh import of the same build reproduces the committed assets
(`--check-only --verify-existing`). The retained Analog BD record was
re-verified against the new packages (162 native OS and GNU bootloader
comparisons, 50 matching refusals, 6 firmware round trips) before its
source-inventory binding was updated. No firmware is committed.

## SY DRUM 0.1.0 (octatrick 2.14, with FM Synth 0.1.5) — 13 October 2026

Local draft by the module author's port; not published, no owner approval.
A new module, `sy-drum` `0.1.0-experimental`: Tim Hastie's octatrick 2.14
`sy-drum/` (`timhastie/octatrick-modules` `39dc5d8`, tag `v2.14`), an
analog-style drum voice in the machine list after FM SYNTH. It needs FM Synth
0.1.5, which stays pinned to the author's v2.13 (`48b89e7`; v2.14 changes only
`sy-drum/` and the README). Every stock expectation of the author's manifest is a
`stock_guard` (53 sites, 410 bytes, each recomputed from MAIN OS `164f3122…`);
the import inventory is `sdk/imports/sy-drum-39dc5d8.json`. 2.14 is the author's
fix of his hardware finding on the previous test image (`d0dab5fe…`): the step
locks' working file is now written while the sequencer plays too (one hook more,
the UI tick's transport test), so a power-off without SAVE keeps the PLAYBACK
SETUP locks edited during playback, and the lock files load when a stock bank
file is missing or damaged. In the image the SY DRUM unit differs from the
previous build's only in the code assembled from `locks_io.inc.s` (+346 B).

Shared code (unchanged by the re-pin): the configurator refuses SY DRUM without
FM Synth (`sy-drum-needs-synth`), and so does the browser engine, from the native
manifest's `requires` carried in the requested group; FM Synth's per-remix units
gain one `whenModules: ["sy-drum"]` variant each; the core logger leaves out its
`job` hook when a selected module owns that site (SY DRUM's card-file dispatch,
`0x4008485e`) and names it in its identity; `module:doctor` accepts a module that
is refused alone by such a rule.

Image: MAIN OS `27c213b2…`, FM Synth 0.1.5 + SY DRUM with all stock effects, built
by Modwerk's exporter (update file 521,012 B, `1644e5fa…`;
Modwerk's TS packer decodes it to that MAIN OS and re-encodes the same bytes;
the author's `tools/align_check.py` finds every instruction and detour target at
an even address). On the pinned emulator: the module's Part smoke, the
machine-list walk, the machine change SY DRUM → FM SYNTH → SY DRUM (21 of 21
checks), two SY DRUM tracks isolated, Part SAVE/RELOAD, SAVE PROJECT, the
project's load from the card and PROJECT > RELOAD with the `sylockNN` files pass;
2 SY DRUM tracks at their worst keep every frame interrupt inside the budget with room for the run-to-run spread; 3 stay inside it by less than that spread and are not qualified (emulated instructions at the CF METER calibration, not chip timing; the
maximum over five runs of each load). On an emulator build that carries the
battery-backed memory over a power-off (`07a7c08e…`): locks edited while
playing come back whole after a power cut while playing, 0.6 s after pressing STOP and
when stopped (the previous image, as the control, loses the PLAYBACK SETUP
edits in the two play cases), and a removed, cut or damaged `bank05.work` at
the power-on load keeps every bank's step locks (the previous image: none).
Native/browser: 126 selections, 4 builds matching outside the
platform writes, 122 matching refusals, 0 mismatches (POLY8 left out of the
pool locally). The hardware fields hold the author's MKI report of this exact
image (timhastie, 2026-10-13); the earlier builds (`d0dab5fe…`, `199e9b09…`,
`b9b65cd9…`) are superseded. Against `d0dab5fe…`: the captures, the walks other
than the power-off ones, and the renders are the same; the peak blocks are
s1 29,017 (-16), s2 33,419 (+14), s3 37,749 (-154), s4 42,304 (-14), s8 59,601 (-344) instructions against its maxima, inside the larger run-to-run spread (418).
On its base, `modules:generate` stops at Tape Echo 0.1.3's retained-evidence
gate: SY DRUM's shared code and packages change its protected build inputs
(open, for the owner; TESTING.md).
5 changes: 3 additions & 2 deletions public/licenses/THIRD_PARTY_NOTICES.html
Original file line number Diff line number Diff line change
Expand Up @@ -2764,11 +2764,12 @@

========================================================================

octatrick Scale Quantizer and FM Synth
octatrick Scale Quantizer, FM Synth and SY DRUM
SPDX: MIT
Used in: modules/quantizer/upstream; modules/synth/upstream
Used in: modules/quantizer/upstream; modules/synth/upstream; modules/sy-drum/upstream
Notice source: sdk/octabam/modules/quantizer/upstream/LICENSE
Notice source: sdk/octabam/modules/synth/upstream/LICENSE
Notice source: sdk/octabam/modules/sy-drum/upstream/LICENSE

MIT License

Expand Down
5 changes: 3 additions & 2 deletions public/licenses/THIRD_PARTY_NOTICES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2763,11 +2763,12 @@ targets.

========================================================================

octatrick Scale Quantizer and FM Synth
octatrick Scale Quantizer, FM Synth and SY DRUM
SPDX: MIT
Used in: modules/quantizer/upstream; modules/synth/upstream
Used in: modules/quantizer/upstream; modules/synth/upstream; modules/sy-drum/upstream
Notice source: sdk/octabam/modules/quantizer/upstream/LICENSE
Notice source: sdk/octabam/modules/synth/upstream/LICENSE
Notice source: sdk/octabam/modules/sy-drum/upstream/LICENSE

MIT License

Expand Down
22 changes: 19 additions & 3 deletions scripts/build-module-packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
ORDER = ['spectrum', 'modulation', 'character', 'miniverb', 'tapeecho', 'euclid', 'repitch', 'tapehead', 'airwindows-chorus', 'everb']
# DSP code that stock code reaches through hooks. Output Matrix has no FX menu entry and is also a requested ColdFire module.
HOOKED = ['sidechain-compressor', 'output-matrix']
REQUESTED = ['analog-bassdrum', 'midi-scenes', 'usb-audio-out-tracks-main-cue', 'quantizer', 'synth', 'vector', 'playmodes', 'mute-modes', 'recorder-loop-fix', 'poly8', 'output-matrix']
REQUESTED = ['analog-bassdrum', 'midi-scenes', 'usb-audio-out-tracks-main-cue', 'quantizer', 'synth', 'vector', 'playmodes', 'mute-modes', 'recorder-loop-fix', 'poly8', 'output-matrix', 'sy-drum']
UTILITIES = ['previewvol', 'cc-map']
ASSET_NAMES = ['dsp-packages.json', 'coldfire-packages.json', 'resident-dsp.json', 'rom-packages.json',
'bootstrap-package.json', 'menu-recipes.json', 'descriptor-recipes.json', 'platform-writes.json', 'requested-packages.json', 'utility-packages.json', 'usb-audio-packages.json']
Expand Down Expand Up @@ -127,7 +127,9 @@ def compile_requested(root, known, documents, versions, revision, provenance, na
text = (root / u.source).read_text(); validate_source(text.replace('.include "remix.inc"', ''))
extra = []
if u.include:
(work / 'remix.inc').write_text(u.include(selection)); extra = ['-I', work]
# FM Synth's ordinary objects are FM Synth alone; with SY DRUM they are the variants below.
own = {k: v for k, v in selection.items() if not (m.name == 'synth' and k == 'SY DRUM')}
(work / 'remix.inc').write_text(u.include(own)); extra = ['-I', work]
obj = work / 'unit.o'; cpu = '54455' if u.dram else u.cpu
from remix.platform_build import as_defsyms, redefined
run(['m68k-elf-as', '-mcpu=' + cpu, *extra, *as_defsyms(u.defsyms), '-o', obj, root / u.source], root)
Expand Down Expand Up @@ -170,6 +172,18 @@ def compile_requested(root, known, documents, versions, revision, provenance, na
promote_symbols(obj,exports.get('symbols',[]) if sidechain and exports.get('whenModule')=='sidechain-compressor' else [])
raw=obj.read_bytes()
pkg['variants'].append(dict(whenModules=['poly8']+(['sidechain-compressor'] if sidechain else []),withoutModules=[] if sidechain or m.name=='repitch' else ['sidechain-compressor'],bytes=len(raw),code=raw.hex(),sha256=HASH(raw)))
if m.name == 'synth' and 'sy-drum' in ids:
# SY DRUM runs on FM Synth's engine and machine list (its native manifest requires SYNTH MACHINE).
# With SY DRUM selected, each FM Synth unit written per remix is assembled with SY DRUM in its
# remix.inc (HAVE_SYDRUM 1: the machine-list row and the engine's calls into SY DRUM's unit).
from remix.platform_build import as_defsyms
for unit in m.linked:
if not unit.include: continue
work = root / 'requested' / unit.label; obj = work / 'sy-drum.o'
(work / 'remix.inc').write_text(unit.include(selection))
run(['m68k-elf-as', '-mcpu=' + ('54455' if unit.dram else unit.cpu), '-I', work, *as_defsyms(unit.defsyms), '-o', obj, root / unit.source], root)
raw = obj.read_bytes(); pkg = next(row for row in objects if row['label'] == unit.label)
pkg['variants'].append(dict(whenModules=['sy-drum'], withoutModules=['poly8'], bytes=len(raw), code=raw.hex(), sha256=HASH(raw)))
if m.name == 'synth' and 'poly8' in ids:
recipe=next(r for r in composition['groups'] if r['moduleId']==m.name)
for unit in m.linked:
Expand Down Expand Up @@ -237,7 +251,9 @@ def compile_requested(root, known, documents, versions, revision, provenance, na
obj=work/'loader.o';run(['m68k-elf-as','-mcpu=5475','-I',work,'--defsym','PREBOOT=1','-o',obj,work/'loader.S'],root)
raw=obj.read_bytes(); bootstrap=dict(bytes=len(raw),code=raw.hex(),sha256=HASH(raw))
print(f'Compiled {len(objects)} requested ColdFire objects, both Analog BD engines and stock-free pre-boot skeleton.',flush=True)
return dict(schema=1,revision=revision,**provenance,objects=objects,groups=groups,composition=composition,analog=analog,bootstrap=bootstrap)
# A module whose native manifest requires another (SY DRUM: SYNTH MACHINE); the browser refuses it without that module.
requires={m.name:list(m.requires) for m in selected if m.requires}
return dict(schema=1,revision=revision,**provenance,objects=objects,groups=groups,composition=composition,analog=analog,bootstrap=bootstrap,**({'requires':requires} if requires else {}))

def main():
parser = argparse.ArgumentParser(description=__doc__)
Expand Down
6 changes: 5 additions & 1 deletion scripts/module-doctor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//
// Each line is a point where a module meets an existing workflow, with the command that fixes it when it fails. Run it until
// it is green, then walk the checklist in docs/module-guides/<category>.md, which covers what a program cannot see.
import { selectionConflicts } from '../src/catalog/selection-conflicts.ts'
import { existsSync, readFileSync, readdirSync } from 'node:fs'
import { spawnSync } from 'node:child_process'
import { dirname, resolve } from 'node:path'
Expand Down Expand Up @@ -122,7 +123,10 @@ function octatrack(id) {
}

const { checks, pairs: pairCounts } = nativeChecks(), pairs = pairCounts.get(id) ?? 0
if (!(id in checks)) fail('declaration checks', 'the module alone has no recorded ledger check, so a build of it is refused', 'npm run module:verify -- ' + id + ' --os <your OCTATRACK_OS1.40C.bin> (it records them)')
// A module the configurator refuses alone (SY DRUM needs FM Synth) has no record alone, by design.
const alone = id in checks ? [] : selectionConflicts([id])
if (alone.length) ok('declaration checks', 'refused alone by design (' + alone[0].title + '); recorded beside ' + pairs + ' other modules')
else if (!(id in checks)) fail('declaration checks', 'the module alone has no recorded ledger check, so a build of it is refused', 'npm run module:verify -- ' + id + ' --os <your OCTATRACK_OS1.40C.bin> (it records them)')
else ok('declaration checks', 'recorded alone and beside ' + pairs + ' other modules')

const conflicts = document.compatibility.conflicts ?? []
Expand Down
2 changes: 1 addition & 1 deletion scripts/module-release-scope.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('source compilation with standalone MIDISC2.0', () => {
ids = [m['id'] for m in catalog['modules'] if json.load(open('sdk/octabam/modules/' + m['id'] + '/octamod.module.json')).get('build', {}).get('status') != 'pending']
print(json.dumps(scope(ids)))
`)
expect(JSON.parse(actual)).toEqual(['analog-bassdrum', 'usb-audio-out-tracks-main-cue', 'quantizer', 'synth', 'vector', 'playmodes', 'mute-modes', 'recorder-loop-fix', 'poly8', 'output-matrix'])
expect(JSON.parse(actual)).toEqual(['analog-bassdrum', 'usb-audio-out-tracks-main-cue', 'quantizer', 'synth', 'vector', 'playmodes', 'mute-modes', 'recorder-loop-fix', 'poly8', 'output-matrix', 'sy-drum'])
})
it('removes legacy 8.2 objects and preserves the other authored package inventory', () => {
expect(metadataCheck(`baseline = json.load(open('src/engine/assets/requested-packages.json'))
Expand Down
Loading