ko2-io, image 1 from its README
Firmware

ko2-io

Hype23/100

Terminal sample I/O + FUSE filesystem for the Teenage Engineering EP-133 K.O. II

ko2-io is custom firmware for the Teenage Engineering EP-133 K.O. II, by @trusch. Open source under MIT, 2 stars on GitHub.

forTeenage Engineering EP-133 K.O. II·by@trusch

Unclaimed. Is this yours? Sign in with GitHub as @trusch to claim it.

Downloads
0
Stars
2
Forks
0
Commits
13
Last commit
Get the source

No release files yet. Build from source.

MIT· trusch/ko2-io·downloads come from the creator’s GitHub releases

13
Commits
0
Releases
13 commits and 0 releases in the last year
Activity · last 12 months

ko2-io

Terminal sample I/O, project editing + FUSE filesystem for the Teenage Engineering EP-133 K.O. II

CI device: EP-133 K.O. II platform: Linux license: MIT

ko2 terminal session against a live EP-133: ko2 ls -l listing the sample library, ko2 get downloading a slot, ko2 put uploading with byte-exact verify, ko2 play auditioning and ko2 rm deleting

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 cp reports real errors.
  • Project management — select, copy, download, upload and back up whole projects; ko2 pads prints every pad assignment, ko2 assign a7 42 puts a sample on a pad, ko2 project snapshot -m / restore / diff version them in place, and ko2 project edit wraps the whole loop in $EDITOR.
  • Projects as YAML — ko2 project export turns a project into a human-readable document (pads, patterns, scenes, faders, FX); ko2 project compile deterministically 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 — --json on ls, info, pads, tree and project ls; transfer progress only on a TTY; errors exit 1 with the message on stderr; Unix-friendly SIGPIPE behavior (ko2 ls | head just 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/Ubuntu libfuse3-dev, Fedora fuse3-devel) — linked at build time, used by ko2 mount.
  • Device node access — read/write on /dev/snd/midiC*D*; being in the audio group 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 all ko2 processes — 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 restore rolls back on demand — --list shows 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.
  • --force gates. rm, replacing an occupied slot, pad or project — nothing destructive happens by default, and ko2 backup / ko2 project backup make 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.

License

MIT