rytm-mods-src's Analog Rytm MKII OS 1.73 mods, for OS 1.73 and 1.74
(MrScruff, published with permission), ported one by one to the elemod format so they
link with elekloader's core-ar2 and with each other,
instead of being patched into code caves at fixed addresses.
No firmware is included: no OS files, sections, dumps or built .syx/.elemod files. The
mods name firmware routines by address only (include/ar2-1.73.inc, include/ar2-1.74.inc), and their mod.json
sites carry the stock instruction bytes they replace, as elekloader's own mods do.
| path | what |
|---|---|
include/ar2-1.73.inc |
every main-OS symbol of rytm-mods-src's re/symbols.toml as an absolute .equ (generated, 276 symbols) |
include/ar2-1.74.inc |
the same symbols at their OS 1.74 addresses (generated by tools/port_os.py) |
shared/shared.s |
rytm-mods-src's 0000-shared runtime, included into each mod that uses it |
<mod>/ |
one elemod mod: mod.json, its source, README.md; os1.74/ holds its source for OS 1.74 (generated) |
tools/gen_symbols.py |
regenerates the include from a rytm-mods-src checkout |
tools/port_mod.py |
ports one rytm-mods mod mechanically (source and mod.json; the README is by hand) |
tools/port_os.py |
makes every mod's OS 1.74 port from its 1.73 one: include/ar2-1.74.inc, <mod>/os1.74/, and the "1.74" entry of mod.json's ports |
tools/verify_port.py |
checks a port against rytm-mods' own build, label by label (re-assembles the original, then compares disassembly with every address named) |
| rytm-mods | here |
|---|---|
a [[detour]] in registry/allocations.toml (addr, expect bytes, entry) |
a sites entry: {"addr", "stock": <the whole instructions displaced>, "op": "jmp", "target": <entry>}; where the 6-byte jmp lands across an instruction the stub re-emits, the site takes it whole |
a [[patch]] with replace |
a bytes site ("kind": "code" in .text), widened to whole instructions |
a [[patch]] with from_symbol, in a data table |
a ptr site |
a [[patch]] with from_symbol, the address operand of a jsr/lea/move.l # |
a keep2 site: the opcode word kept, the address the mod's (any other instruction: a bytes code site with an abs32 reloc) |
a stub at a fixed cave address, or split over several caves by .section |
ordinary code, placed by the linker in the 128 KB DDR area: every section becomes .text → .run |
| state kept in a cave | stays with the code in .run (DDR is RAM) |
a gate on a claim |
applied with the default build's params |
build/symbols.inc |
include/ar2-1.73.inc |
build/shared.inc + mod 0000 |
.include "../shared/shared.s" in the mod itself |
build params |
fixed at the default build's values |
Rules that matter (elekloader docs/ADAPTING.md): only labels that a site or event names are
.globl, and they start with the mod's id; everything else stays local, so two mods never
export the same name. port_mod.py adds a prefixed global alias beside each such label, so the
stub's own references stay local and assemble as they did in rytm-mods (the hand-ported
slice-loop and env-reset-amp rename their entry label instead). A site covers whole stock instructions; the stub re-emits what it
displaced, exactly as in rytm-mods. Two mods may never patch the same bytes.
0000-shared is not a mod here: its ~234 bytes are assembled into each mod that calls it
(velocity-humanise, lfo-rnd and voice-dac), so every mod stands alone. velocity-humanise does
need euclid-accents (its amount lives in bits only euclid-accents saves), and says so in
requires; the linker refuses it alone.
The gate chain: euclid-accents, velocity-humanise, lfo-rnd, voice-dac and ctrl-all detour consecutive instructions of param_apply_delta (0x400378a6..0x400378c2) and euclid-accents, velocity-humanise, lfo-rnd and voice-dac those of param_value_text (0x400a85e0..0x400a85f8). Each is its own site, and each stub rejoins at the next instruction, which is the next mod's jmp when that mod is linked and stock when it is not - as in rytm-mods.
Elektron's 1.74 changes only the main OS, and that is 1.73's with code added in two places (Outbox 8's main-output routing): the code after 0x4005702a sits 0x6c further on, after 0x40123e2a 0x1cc, and the data after the code by other amounts. RAM from 0x402ce000 on and the SRAM did not move. Building for 1.74 needs an elekloader that knows the Rytm's 1.74 (its release in the profile, and core-ar2's 1.74 port).
Every mod's 1.73 source is its source of truth. tools/port_os.py (run with both stock files)
writes the 1.74 port from it: each firmware address in the include, the sources' code and the
sites, moved and checked (see the tool), and each site's stock bytes read from 1.74. It writes
nothing if any address fails its check. Run it again after changing a 1.73 source. elekloader
builds the port when given the 1.74 stock file, as <id>-<version>-os1.74.elemod.
Checked with both stock files: core-ar2 with all eleven ports, built for each, changes the same
instructions at the same (moved) sites, and the mods' code differs only in the firmware
addresses it names, each the moved one. A word in the mods' code that holds a 1.73 address
whose 1.74 address differs, left unmoved, is a port error: the include once kept two vtable
addresses tagged const (ctrl-all's set checks) at their 1.73 values, which made TRK + turn
reach only the selected track on 1.74.
You need:
- an elekloader checkout at
main(Analog Rytm MKII support andcore-ar2arrived in elekloader/elekloader#89) and the Python 3 it asks for; - ColdFire binutils on
PATH(m68k-linux-gnu-asand-ld; on Debian or Ubuntu,apt install binutils-m68k-linux-gnu); - your own copy of Elektron's stock Analog Rytm MKII OS 1.73 or 1.74
.syx. It is never part of this repository or of anything built from it.
EL=path/to/elekloader # the elekloader checkout
R2=path/to/rytm2_mods # this repository
STOCK=path/to/Analog-Rytm_MKII_OS1.74.syx # your stock OS file (1.73 or 1.74)
OS=-os1.74 # with a 1.73 file: OS=
cd "$EL"
python3 -m elekloader.sdk.build mods/core-ar2 --stock "$STOCK"
MODS="filter-hz ctrl-all env-reset-amp slice-loop euclid-accents velocity-humanise lfo-rnd
comp-preview main-rms kit-ab voice-dac"
ARGS=""
for m in $MODS; do
python3 -m elekloader.sdk.build "$R2/$m" --stock "$STOCK"
V=$(python3 -c "import json; print(json.load(open('$R2/$m/mod.json'))['version'])")
ARGS="$ARGS --mod $R2/$m/out/$m-$V$OS.elemod"
done
python3 -m elekloader.patch --stock "$STOCK" --mod mods/core-ar2/out/core-0.1$OS.elemod $ARGS \
--out rytm-mods.syx --version R2ALLeave out any mod you don't want from MODS, but keep euclid-accents whenever
velocity-humanise is in: elekloader refuses velocity-humanise without it. elekloader.patch
checks that the selected mods combine before it writes anything. --version is the
four characters the unit shows as its OS version. The result is your own firmware file:
keep it, and the out/ build folders, out of git and out of any upload.
| rytm-mods-src | here | source status | port |
|---|---|---|---|
| 0000-shared | shared/shared.s |
built | ported (included by velocity-humanise, lfo-rnd, voice-dac) |
| 0008-filter-hz | filter-hz/ |
hardware-verified | matches label by label; confirmed on a unit |
| 0012-slice-loop | slice-loop/ |
hardware-verified | matches label by label; confirmed on a unit |
| 0014-ctrl-all | ctrl-all/ |
hardware-verified | 1.2, MrScruff's SYNTH knob fix (9831bdf): matches label by label, confirmed on a unit on OS 1.74; 1.0 and 1.1 confirmed on a unit |
| 0015-env-reset-amp | env-reset-amp/ |
hardware-verified | bit probe left out; matches label by label; confirmed on a unit |
| 0002-euclid-accents | euclid-accents/ |
hardware-verified | matches label by label; confirmed on a unit |
| 0003-velocity-humanise | velocity-humanise/ |
built (MANUAL: verified) | needs euclid-accents; matches label by label; confirmed on a unit |
| 0004-lfo-rnd | lfo-rnd/ |
built (MANUAL: verified) | matches label by label; confirmed on a unit |
| 0006-comp-preview | comp-preview/ |
hardware-verified | matches label by label; confirmed on a unit |
| 0009-main-rms | main-rms/ |
built (MANUAL: display checked on the device) | matches label by label; confirmed on a unit |
| 0010-kit-ab | kit-ab/ |
hardware-verified | matches label by label; confirmed on a unit |
| 0013-voice-dac | voice-dac/ |
hardware-verified | default build only (no timing, prof, burn or dbg); 1.1, MrScruff's update (76ae299): matches label by label but for three names where sections meet; 1.0 confirmed on a unit |
Every mod on rytm-mods-src main is now ported. "Matches label by label": tools/verify_port.py
re-assembles rytm-mods' own code, checks it equals rytm-mods-src's default build ("T2"), then
compares each label's instructions with the port's, every address named by its label. For all
eleven, in the full build (core-ar2 with every port, as in Build above): every byte it changes
in the OS is inside one of the ports' sites, every one of those sites is also patched by
rytm-mods' default build (T2), and the bytes sites write T2's bytes exactly. rytm-mods'
diagnostic builds (its non-default params and the claims they gate) are not ported.
GPL-2.0, as rytm-mods-src (LICENSE). The mods are MrScruff's work; the elemod ports keep his
code and note every change.