ko2-io
Terminal sample I/O, project editing + FUSE filesystem for the Teenage Engineering EP-133 K.O. II
ko2 talks the device's USB-MIDI SysEx FILE protocol directly — no browser,
no official sample tool, no cloud. Samples, projects, pads, patterns and
scenes are all first-class: upload and download samples, mount the library
as a filesystem, assign pads, and round-trip whole projects through a
human-readable YAML format. Everything shown in the screenshot above is real
output against real hardware.
Highlights
- Full sample I/O — list, upload, download, rename, delete, audition, whole-library backup; slots 1–999.
- Filesystem mount — the sample library as a directory of WAV files;
uploads happen on close so
cpreports real errors. - Project management — select, copy, download, upload and back up whole
projects;
ko2 padsprints every pad assignment,ko2 assign a7 42puts a sample on a pad,ko2 project snapshot -m/restore/diffversion them in place, andko2 project editwraps the whole loop in $EDITOR. - Projects as YAML —
ko2 project exportturns a project into a human-readable document (pads, patterns, scenes, faders, FX);ko2 project compiledeterministically turns it back into the exact same bytes. Perfect for reproducible test cases, presets and generated content. - Any WAV in — 8/16/24/32-bit int or float, any rate, mono or stereo; normalized and Catmull-Rom-resampled to the device-native 46875 Hz s16.
- Byte-exact verification everywhere — sample uploads (
--verify) and every project write are read back and compared; project writes are checkpointed first and roll back automatically on mismatch. - Scriptable —
--jsononls,info,pads,treeandproject ls; transfer progress only on a TTY; errors exit 1 with the message on stderr; Unix-friendly SIGPIPE behavior (ko2 ls | headjust works). - Defensive by design — exclusive device locking, firmware debug-frame detection, passive preflight before mutations, firmware-invariant validation before anything is sent. See Playing it safe.
Why not a real mass-storage device?
The EP-133 exposes exactly one USB interface: a class-compliant USB-MIDI
endpoint. There is no mass-storage class interface in its descriptors, so it
can never appear as an actual disk without a firmware change by TE. All file
access goes through a SysEx-tunneled file protocol (the same one the official
web tool uses). ko2 mount bridges the gap: a FUSE filesystem that exposes
the sample library as a directory of WAV files.
Install
Linux only for now — the MIDI transport is raw file I/O on ALSA rawmidi device nodes and the mount is FUSE. (A macOS CoreMIDI transport would slot in behind the same protocol client; contributions welcome.)
cargo install --git https://github.com/trusch/ko2-io
# or from a checkout:
cargo install --path .
Installs the ko2 binary (ko2 completions zsh|bash|fish generates shell
completions). Requirements:
- libfuse3 (Arch
fuse3, Debian/Ubuntulibfuse3-dev, Fedorafuse3-devel) — linked at build time, used byko2 mount. - Device node access — read/write on
/dev/snd/midiC*D*; being in theaudiogroup or on an active seat with logind ACLs is enough. There is no ALSA library dependency: the tool does raw file I/O on the device node.
Samples
The device is autodetected (sound card id EP133). Pass
--port /dev/snd/midiCxD0 to override.
ko2 ls # list occupied sample slots (fast)
ko2 ls -l # + display names and durations (slower: one
# metadata round trip per slot)
ko2 ls --json
ko2 info 1 # file info + full metadata JSON for a slot
ko2 get 1 # download slot 1 -> 001_<name>.wav
ko2 get 1 -o kick.wav
ko2 backup samples/ # download every slot into samples/
ko2 put 42 kick.wav # upload (auto-resampled to 46875 Hz s16)
ko2 put 42 kick.wav --name "my kick" --force # replace an occupied slot
ko2 put 42 kick.wav --mono --verify # downmix; byte-exact readback check
ko2 name 42 "fat kick" # rename a slot's display name
ko2 play 42 # audition the raw sample on the device
ko2 rm 42 --force # delete a slot (destructive)
ko2 mount ~/ko2 # mount the library as a filesystem
The filesystem
ko2 mount ~/ko2 # foreground; Ctrl-C or fusermount3 -u ~/ko2
ls ~/ko2 # 001 micro kick.wav, 002 rubber kick.wav, ...
cp ~/ko2/001*.wav . # download
cp kick.wav ~/ko2/042.wav # upload to slot 42
cp kick.wav ~/ko2/ # upload to the first free slot
mv ~/ko2/"042 kick.wav" ~/ko2/"042 fat kick.wav" # rename display name
rm ~/ko2/"042 fat kick.wav" # delete slot 42 (no undo!)
Filename convention: leading digits select the slot (042.wav,
42 my kick.wav); the rest of the stem becomes the on-device display name.
Without digits the first free slot is used. The first ls fetches all
display names from the device (a few seconds on a full library; --numeric
skips this and names files NNN.wav). Uploads happen when the file is
closed, so cp returns only after the transfer — errors surface as failed
writes. Renames that change the slot number and partial in-place edits are
not supported; replace the file whole instead. Sizes, df output, and free
slot counts are real; timestamps are synthetic.
Audio format
The device stores raw interleaved little-endian s16 PCM, native rate
46875 Hz, mono or stereo. put accepts any WAV (8/16/24/32-bit int or float,
any rate) and converts: normalize, optional mono downmix, Catmull-Rom
resample to 46875 Hz, s16. get writes a WAV at the rate/channel count the
slot metadata reports. For non-WAV sources, convert first, e.g.
ffmpeg -i in.mp3 out.wav.
Projects and pads
Projects live in the same device filesystem as samples: each of the nine project slots is one node that reads and writes as a single archive holding pad assignments, patterns, scenes, fader settings and master FX. Sample slots are global — projects only reference them.
ko2 tree # walk the whole device filesystem
ko2 project ls # the nine projects + which one is active
ko2 project select 3 # switch the device to project 3
ko2 project get # download the active project
ko2 project backup projects/ # download all nine
ko2 project cp 1 5 # copy project 1 into slot 5
ko2 pads # every pad assignment, all four groups
ko2 pads -p 2 --json # ... of project 2, as JSON
ko2 assign a7 42 # put sample slot 42 on pad 7 of group A
ko2 assign b. 17 --force # replace an occupied pad (group B, pad ".")
ko2 assign a7 --clear # back to a factory-blank pad
ko2 project put edited.tar -p 2 --force
Every project is versionable in place:
ko2 project snapshot -m "gig set v2" # checkpoint on demand, with a label
ko2 project restore --list # every checkpoint: age, label, contents
ko2 project restore --force # roll back to the newest checkpoint
ko2 project restore "gig set v2" --force # ... or to a label, or @2 = 2nd newest
ko2 project diff # what changed since the last checkpoint
ko2 project diff edited.yaml # what uploading this file would change
diff prints field-level changes (groups.a.pads.7.slot: 15 -> 17) and
exits 1 when the sides differ, like diff(1). Labeled snapshots are never
auto-pruned; unlabeled checkpoints keep the newest 20 per project.
Commands take -p/--project 1-9; without it they target the project
currently active on the device. Pads are addressed by group letter + the
label printed on the keypad (a1-a9, a0, a., ae for enter).
assign seeds the full pad record from the sample's metadata — trim, play
mode, envelope, root — exactly like assigning on the device, never leaving
stale parameters behind.
Every project write follows the same safety protocol: the current archive is
checkpointed to ~/.local/state/ko2/checkpoints/, the new archive is
written and read back byte-for-byte, and on any mismatch the checkpoint
is restored automatically. Writes to the active project trigger the reload
cycle the official app uses; other projects load fresh when selected.
Low-level metadata access
Every node carries a JSON metadata blob (0x07 sub-command): samples have a
19-field blob (name, trim, loop points, play mode, envelope, …), pads have a
12-field one, directories track UI state (active).
ko2 meta get 42 # a sample slot's full metadata
ko2 meta get a7 # pad 7, group A, active project
ko2 meta get 1:a7 # the same pad in project 1 explicitly
ko2 meta get 2000 active # which project is active (fid works too)
ko2 meta set 42 '{"sound.playmode":"key","envelope.release":40}'
meta set enforces the write rules the firmware only fails on silently:
enums must be strings, and sound.playmode must be paired with
envelope.release in the same write.
Projects as YAML
ko2 project export decompiles a project into a human-readable,
version-controllable document; ko2 project compile builds it back —
deterministically and byte-exact. A 62 KB device archive becomes ~4 KB
of YAML, and export | compile reproduces the original archive bit for bit.
This is the workflow for reproducible test cases, presets, and generated
content:
format: ko2-project/1
bpm: 140.0
groups:
a:
pads:
"1": {slot: 2} # nt kick
"2": {slot: 100} # nt snare
"3": {slot: 205, choke: true} # closed hat
patterns:
1:
bars: 1
events: # "<tick> <pad> <note> <vel> <len>"
- "0 1 60 100 24" # ticks are 96 PPQN: a 16th = 24
- "24 3 60 80 12"
- "48 2 60 110 24"
- "72 3 60 80 12"
- "auto 0 lvl 32767" # fader automation, latching
scenes:
- "1 1 1 1 4/4" # pattern per group + time signature
song:
order: [1]
ko2 project new case.yaml # commented starter document
ko2 project lint case.yaml # every validation issue at once
ko2 project export # active project -> projectP.yaml
ko2 project export --from p1.tar # offline: archive -> YAML
ko2 project compile case.yaml # offline: YAML -> case.tar
ko2 project put case.yaml -p 9 --force --select # compile + upload + activate
ko2 project edit # $EDITOR round trip on the active project
edit is the tightest loop: it exports to a temp file, opens $EDITOR,
validates on save (errors reopen the editor), shows the field-level changes,
and uploads after one confirmation. lint collects every problem in one
pass — including, when the device is attached, referenced sample slots that
are empty (the most common cause of a silent pad).
Everything decoded is editable: pad parameters (mode, volume, pan, pitch,
envelope, time-stretch, choke, trim, root, per-pad BPM), note events, fader
automation, scenes with per-scene time signatures, song order, project BPM,
fader bases and assignment, master FX and output compressor. Byte regions
that are not fully decoded (they vary by firmware era) ride along as hex
baselines under raw: — decoded fields are patched onto those baselines,
so editing bpm can never disturb bytes the tool does not understand, and
nothing is ever lost in a round trip.
compile validates the firmware's invariants before anything reaches the
device: dangling scene→pattern references, out-of-range automation values,
oversized patterns and invalid pad parameters are rejected with messages
that say what to fix.
One workflow note: export regenerates the document, so hand-written YAML
comments do not survive a device round trip. Keep annotated documents as
the source of truth and treat exports as snapshots — compile is
deterministic, so the YAML in version control always reproduces the exact
archive.
Playing it safe
The device's FILE session is fragile under concurrent or malformed access,
and malformed content can be worse — community reverse-engineering efforts
have wedged devices to the point of flash formats. ko2 is deliberately
paranoid:
- One session at a time. An exclusive lock (in
$XDG_RUNTIME_DIR) serializes device access across allko2processes — overlapping FILE sessions can wedge the device into a debug loop until power-cycle, so the tool refuses to run concurrently instead. A mount holds the lock for its whole lifetime; unmount before using the CLI. The lock cannot see the official web tool, so don't run both at once. - Debug-frame detection. If the firmware starts emitting debug frames, the transfer aborts immediately and tells you to power-cycle instead of ploughing on.
- Preflight listen. Mutating commands passively listen for ~1.2 s first to make sure the device is not already misbehaving before sending anything.
- Checkpoint, verify, roll back. Every project write saves the current
archive to
~/.local/state/ko2/checkpoints/first (the newest 20 per project are kept), verifies the write by reading it back, and restores the checkpoint automatically if verification fails.ko2 project restorerolls back on demand —--listshows every checkpoint with its age and contents. - Firmware invariants enforced client-side. Pattern-header and pad-record validity bytes, dangling scene references, automation ranges and settings-member shape are validated before a single byte is sent — each of these has bricked projects or devices when violated.
--forcegates.rm, replacing an occupied slot, pad or project — nothing destructive happens by default, andko2 backup/ko2 project backupmake full backups one command away.
Protocol notes
ko2 CLI / FUSE mount
│ FILE protocol: init/list/stat/get/put/metadata/delete/play
│ SysEx-framed, 7-bit-packed, request/response with 12-bit ids
▼
/dev/snd/midiCxD0 ──USB-MIDI──▶ EP-133 K.O. II
The device exposes a real filesystem over SysEx (walk it with ko2 tree):
0 /
1000 /sounds sample slots 1-999 (fid = slot number)
2000 /projects
3000+ /projects/PP project P as one TAR blob; fid = 3000 + (P-1)*1000
+100 groups/ per-group dirs, 12 pad nodes each
pad fid = 3200 + (P-1)*1000 + group*100 + pad
Frame: F0 00 20 76 33 40 <0x60|id_hi5> <id_lo7> 05 <packed7 payload> F7 —
TE manufacturer id, product 0x33, 12-bit request id, FILE command 0x05,
payload packed 7 raw bytes + 1 high-bit flag byte per group. FILE
sub-commands used: INIT 0x01, PUT 0x02 0x00/0x01, GET 0x03 0x00/0x01,
LIST 0x04, PLAY 0x05 0x01, DELETE 0x06, METADATA 0x07 0x01/0x02,
STAT 0x0B. Byte-level details live in the module headers:
src/fileclient.rs (wire protocol),
src/project.rs (archive dialect, pad records),
src/members.rs (patterns, scenes, settings, FX).
Protocol knowledge builds on the community reverse-engineering work in kmorrill/ep-series-sysex and ZacharySBrown/ep133-ppak. This implementation is verified against real hardware: byte-exact upload/download round trips for samples and whole projects, YAML round trips of factory content, and from-scratch compiled projects accepted and loaded by the device.
Contributing
Issues and pull requests are welcome — protocol findings, additional device
support and a macOS transport especially. CI runs cargo fmt --check,
cargo clippy -- -D warnings and cargo test; the protocol parsers, codecs
for every archive member, the TAR dialect writer and the YAML round trip are
covered by offline unit tests, so most changes can be validated without
hardware.
Disclaimer
This is an unofficial community project, released as open source with teenage engineering's permission. It is not affiliated with, endorsed or supported by teenage engineering. teenage engineering, EP-133 and K.O. II are trademarks of teenage engineering AB. Use at your own risk.