Internals

How the instrument is built. What it does is Features; how to install it is Manual installation.


The control path

A pad press reaches the sequencer through five hops, and every one of them has broken at least once:

flowchart LR HID["Maschine MK2
USB HID"] --> D["maschine daemon
Rust"] D --> A["a2jmidid
virtual port"] A --> Z["ZynMidiRouter
zmip slot"] Z --> C["ctrldev driver
Python"] C --> S["zynseq
Zynthian's sequencer"]

The daemon owns the hardware: it reads HID reports, drives both 255×64 displays and the LEDs, and exposes an ALSA sequencer port. a2jmidid lifts that port into JACK. Zynthian then has to be persuaded to treat it as a hardware MIDI source so it is given a zmip slot — and that is the patch below.

Encoders are relative. The daemon holds each encoder's value as device state and moves it by the hardware delta. Real movement is 0-4 units per report and a counter wrap reads as −38 to −40, so the wrap guard is 8. The big encoder sends 8 units per detent and wraps 120 → 0, sitting exactly on that threshold, so it needs its own delta path.


Why zynautoconnect has to be patched

It is the only Zynthian core file this project changes, and it ships as an idempotent patcher rather than a copy, so it edits your version instead of overwriting it.

Two changes. The Pads port joins the list of ports treated as hardware MIDI sources, which is what earns it a zmip slot. And the uid virtual:maschine.rs/Maschine MK2 Pads is pinned, because the ALSA client number embedded in the port name changes across boots while a ctrldev driver binds by device id.

Unpatched, the driver never binds: Zynthian gets as far as Found and never reaches Loaded. The rig does nothing, and no log anywhere says why — literally, because both of those messages are logged at INFO while ZYNTHIAN_LOG_LEVEL defaults to WARNING, so neither is written on a stock rig. The check that works is the JACK route: exactly one ZynMidiRouter:devN_in under the Pads port.

Re-run the patcher after every Zynthian update: an update replaces the file and takes the binding with it.


The driver

The generator owns the pattern. The driver does not synthesise notes at play time — it writes them into zynseq, which is why patterns survive in snapshots and why the touchscreen editor shows the same thing the pads do. Playing is Zynthian's job; the driver only decides what is written.

Three facts hold the design together.

Everything testable lives outside the driver. The driver itself cannot be imported off the Pi — it needs zynlibs.zynseq — so the Turing register and its mutation, the undo ring, register-to-pitch quantisation, euclidean placement, the gate mask, and the whole page and column model were pushed into techno_lib.py, which has no Zynthian imports. That module carries 271 tests, including the invariant the instrument rests on: RANDOM at 0 produces a byte-identical register over 500 iterations.

Every zynseq call holds one lock. libzynseq is not thread-safe and the driver reaches it from three threads — the MIDI handler, a queued signal handler and a 30 Hz playhead poll. Without the lock the entire Zynthian UI died with a segfault about 95 seconds into a jam.

Nothing slow runs on the MIDI thread. Loading a preset blocks on a socket for seconds while the MIDI handler holds the lock for the whole event, so preset and kit changes are deferred to the poll thread.

A silent channel must say why. Play chance 0 emits nothing and looked exactly like a hang, so a channel that cannot sound draws its tab dashed. It is the one mechanism the surface has for explaining silence.


The factory snapshot

The eight chains

ChainTitle in the snapshotEngineMIDI channel (0-based in the file)
AKickLS/LinuxSampler0
BSnareLS/LinuxSampler1
CClapLS/LinuxSampler2
DClosed HatLS/LinuxSampler3
EOpen HatLS/LinuxSampler4
FBASSJV/JC3035
GLEADJV/Obxd6
HPADSJV/padthv17

The MIDI channel is the contract, not the title and not the order — the driver resolves a channel to a chain by MIDI channel, so titles are free and numbers are not. All five drum chains share one LinuxSampler process, so eight SFZ kits cost about 250 MB in total rather than per channel.

The insert pair, measured on the wire

zynmixer:output_NNa/b → TAP_Stereo_Echo-NN → TAP_Reverberator-NN → zynmixer:input_17

Echo first, reverb second, so the reverb hears the echo's repeats. The order was read out of jack_lsp -c on the running rig, not assumed.

The inserts are fed from the strip's output, which is what makes them post-fader: they inherit the fader and the mute, so muting a channel takes its reverb and delay tail with it.

Both plugins have a true wet level, not a dry/wet crossfade — sweep the wet to maximum and the dry is still there at the same level. That is what lets encoders 7 and 8 behave like sends. Every cheaper candidate measured turned out to be a crossfade.

Four ports are forced rather than left at their defaults:

PluginPortValueWhy
TAP Stereo EchodryLevel0.0 dBShips at −4 dB; across the pair that quietly costs every channel about 8 dB
TAP Stereo Echolecholevel, recholevel−70.0 dBWet starts closed; encoder 8 opens it
TAP Reverberatordrylevel0.0 dBSame reason
TAP Reverberatorwetlevel−70.0 dBWet starts closed; encoder 7 opens it

Gain staging

Channel strips 0.19, main 0.80. That is measurement, not caution: one sampler channel peaks at 1.24 before the mixer, and eight summed to 2.92 on the main bus — nearly three times full scale. The sampler's own volume is not the lever; taking it from 96 to 40 moved the bus peak by about 1.5 dB.

Sixteen inserts without placing sixteen processors

Build one channel by hand on the touchscreen — add TAP Stereo Echo, then TAP Reverberator, set the four values, save — and let tools/build-techno-snapshot.py replicate it. The cloner finds the chain carrying both inserts, appends the same two to every other chain that has a MIDI channel, gives each a fresh processor id, copies the template's fader position, and backs the file up first. Then load and save once more from the touchscreen, so what is on disk is Zynthian's own output rather than the script's.

It deliberately does not build the whole snapshot. Nothing outside the UI process can reach Zynthian's live state manager, and hand-maintaining fader positions in JSON is exactly the kind of guess this project avoids.


Back to: Requirements · Features