Firmware

te-op1

Loud90/100

Potion - an OP-1 Field IDE and an Online Community Library for OP-1 Field patches

te-op1 is custom firmware for the Teenage Engineering OP-1 field, by @jstnfst. Latest release v0.1.0 (October 2026), 3 stars on GitHub.

forTeenage Engineering OP-1 field·by@jstnfst

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

Downloads
0
Stars
3
Forks
0
Commits
322
Last commit
Download v0.1.0

potion-0.1.0-win64.zip· 1.1 MB· +1 file

Custom license· jstnfst/te-op1·te-op1.pages.dev/·downloads come from the creator’s GitHub releases

308
Commits
1
Releases
308 commits and 1 release in the last year
Activity · last 12 months

te-op1

A community patch library and a preset-format reference for the Teenage Engineering OP-1 Field (firmware 1.7.3), live at https://te-op1.pages.dev. This repo also holds mondo, the C toolkit that reads, writes, and moves OP-1 patch files.

TL;DR

  • Want sounds? Open the Library, download a .aif, and copy it into synth/ on your OP-1.
  • Want to know how a preset works? Open the Lab, or read The preset format.
  • Want to work on the code? ./master-control.sh setup, then ./master-control.sh dev up. See For developers.

Two paths

The site splits in two at its home page, and so does this README.

Library: for musiciansLab: for developers
WhoOP-1 Field owners who want soundsSound designers and tool builders who want the format
On the siteBrowse, open in the editor, download, like, build packs, uploadFormat map, knob layouts, value mappings, .aif inspector
In this READMEFor musiciansFor developers

For musicians

Get a patch onto your OP-1

  1. Find a patch in the Library and download its .aif, or select several to get one .zip.
  2. Connect the OP-1 Field over USB-C. It appears as a portable device (MTP), not a drive. On Windows, it's under This PC.
  3. Copy the .aif into synth/, or into a folder inside it such as synth/mine/. The OP-1 allows one folder level.

From a Windows terminal, mondo op1 does the same thing: mondo op1 push patch.aif synth/mine. See mondo op1.

Back up and restore the whole OP-1

mondo project pull mybackup saves everything you can change on the device (patches, slots, tapes, mixdowns) into projects/mybackup/, and mondo project push mybackup -f puts it all back. mondo project push factory-reset -f returns the OP-1 to its factory state. See mondo project.

The desktop app

potion.exe (Windows; it began as mondo-gui) does all of this without a terminal, in six tabs:

  • Projects: back the OP-1 up into a project, restore one onto it, compare the two, or start a new project as a copy of another. Every back up and restore shows its changes first; nothing is copied until you apply them
  • Editor: browse the archive, a project or a pack as a folder tree and edit its patches with the same knobs, ADSR scope, drum-pad keyboard, copy and paste, and undo as the site's editor. Save writes the .aif back in place and keeps its audio (a sampler's sound, a drum kit's samples). A sampler shows its sound with START, LOOP IN, LOOP OUT and END to drag; a drum kit shows each pad's sound and its key settings (pitch, play mode, direction, volume, pan, attack). Load a sound with Load… or by dropping audio files (.wav, .aif, .mp3, .ogg, .flac) on the window: a kit takes them on pads in a row from the selected one, and rebuilds its audio so each pad keeps its own sound. Clicking a pad or ▷ plays it. Synth patches pick from the synth engines and drum patches from dbox and drum (a sample kit), because synth/ holds synths and drum/ holds drums; + New patch starts either one (Ctrl+N, Ctrl+Shift+N). The SLOT 1–8 buttons put the open patch into that slot of the project, and the patch already there goes to the recycle bin. Under the tree, Backup and Restore sync the folder or patch picked in it with the OP-1: Backup copies the OP-1's version into the project, Restore makes the OP-1's match the project, each previewed first (the whole project is on the Projects page). The tree also holds the project's notes: a readme.txt goes onto the OP-1 with the next push, a NOTES.md (the Notes button) or any other .md/.txt stays on the computer; click one to edit it. Stars and tags go with a sound; the side panel holds the clipboard, the history, and Sounds (what you've copied this run: a sampler's sound, a kit pad's, a mixdown's range)
  • Mixdowns: one copy of each project's recordings in mixdowns/<project>/. Pick a range, find its hits, and make a sampler or a drum kit from it (or append it to the kit that's open)
  • Packs: sets of patches in packs/<name>/. Add patches from the editor or by dropping .aif files on the window, export a .zip to share, or install a pack into a project (synths to synth/<pack>/, drums to drum/<pack>/), then push that project. Drop a .zip from the site's Packs page to import it
  • Transfer: two places side by side (the archive/, any project, any pack), each listed by patch name and file name. Select patches with a click, a drag, Shift or Ctrl, then move or copy them across, or drag the selection onto a folder on either side (Ctrl copies). Cut, Copy and Paste (Ctrl+X, Ctrl+C, Ctrl+V) paste into the folder last clicked, so patches move between folders of the same project or the archive too; drums always land under drum/ and synths under synth/. The folder button makes a new synth/ or drum/ folder. A patch the other side already has (the same settings and audio, whatever it's named) isn't copied twice. One that would land on a different patch, by name or in a taken slot, shows what differs and asks you to skip, keep both, or replace. The archive has no patch limit; a project shows its count against the 500 the OP-1 Field holds. Whatever a move takes off a side goes to the recycle bin
  • Search: every patch in the archive, the projects and the packs in one list, grouped by place. Find one by its name or file, engine, FX, LFO, tag, stars or place, and open it in the Editor

Download the zip from Releases, unzip it anywhere and run potion.exe. It isn't signed, so Windows SmartScreen may say "Windows protected your PC": More info, then Run anyway. Or build it with desktop\build.bat; see Potion. What it does, feature by feature, is in desktop/CHANGELOG.md.

On the site

  • Library: search by name, or filter by engine and tag
  • Patch editor: open any patch, shape it, and download the .aif. Signed in, you can save a copy, update your own patches, or start from + New patch
  • Packs: curated sets of patches, downloaded as one .zip
  • Favorites and Mine: what you've liked, and what you've uploaded or built
  • Upload: shares the patch settings from an .aif. Sampler audio isn't stored, so a downloaded sampler patch carries a placeholder tone

Browsing and downloading are open to everyone. Sign in (Google, Microsoft, Yahoo, or GitHub) to upload, like, and build packs. A GitHub sign-in also lets you file site issues.


For developers

What's in the repo

te-op1/
  src/                 React SPA: home, Library, Packs, Favorites, Mine, Upload, Lab, Issues, Mod
  functions/           Cloudflare Pages Functions: the /api (auth, patches, packs, likes, issues)
  functions/_shared/   validation, tagging, and .aif rebuild: TypeScript ports of mondo.c
  public/              static pages and the shared format data:
                         patch.html    patch editor (Library) and inspector (Lab)
                         params.html   Lab knob layout
                         display.html  Lab value mappings
                         layout-params.js, mapping-data.js, knob-model.js   format data
                         op1-stats.js  coverage numbers shared by the Lab pages
                         aif.js        .aif <-> JSON in the browser
  migrations/          D1 schema
  scripts/             database seeding, master-control helpers, gen-desktop-data.mjs
  master-control.sh    dev toolbox: setup, dev server, builds, tests, deploy
  mondo.c              the C toolkit, one source file (with op1_aif.h, cJSON.c/.h)
  build.bat            build mondo.exe with MSVC
  desktop/             Potion, the raylib desktop app (desktop\build.bat)
  raylib/, raygui/     submodules Potion builds against, pinned
  op1-params.json      knob name database read by mondo dump
  oracle/              exports saved on the hardware: the ground truth, never regenerated
  presets/             .aif patches and their .json sidecars
  collection/          patch collections: .aif, .json, and .tags per preset
  sandbox/             hardware probe sets (gitignored; ./master-control.sh probe)
  projects/            whole-device OP-1 backups (gitignored but for its README; mondo project)
  packs/               Potion's packs, one folder of .aif each (gitignored but for its README)
  explore/             generated boundary patches (gitignored; mondo build explore)

Run the website locally

./master-control.sh setup     # deps, .dev.vars, local DB schema
./master-control.sh dev up    # http://localhost:8788
./master-control.sh help      # every command: build, test, db, deploy

The stack is Vite + React on Cloudflare Pages, with Pages Functions for the API and D1 for storage. Setup by hand, OAuth, mock logins, and Cloudflare configuration are in README-WEB.md.

The preset format

What each knob is on every engine, and how its raw value turns into the number on the screen, lives on the site: Layout for every engine's eight array slots, Mappings for raw-to-display values, and the Inspector to read back any .aif. The data behind them is public/layout-params.js, public/mapping-data.js, and public/knob-model.js; edit those, not a copy. This section only covers the container and the JSON shape.

File structure

OP-1 Field patch files are standard AIFF-C containers (IFF with big-endian chunk lengths). Every patch file contains exactly four chunks in this order:

ChunkSizeContents
FVER4 bytesAIFC version constant 0xA2805140 - always this value
COMM64 bytesAudio format: mono, 16-bit, 22050 Hz, sowt (little-endian signed PCM)
APPL1028 bytesOP-1 synth metadata (see below)
SSNDvariesRaw PCM audio sample data used by the synth engine

APPL chunk layout

[4 bytes]  "op-1"          application signature
[N bytes]  {...}           JSON string with synth parameters
[pad]      spaces          space-padded to fill exactly 1028 bytes total

The JSON is always compact (no whitespace), followed by \n then spaces to reach the 1028-byte boundary.

Audio (SSND)

For sample-based synths like cluster, the SSND contains the actual waveform the engine operates on. Replacing it with silence produces a valid but soundless patch. mondo build writes 28896 frames of silence at 22050 Hz (~1.310 s), which matches hardware-exported patches.

JSON schema

Every array value is a 16-bit integer: most run 0–32767, some are signed (Mappings says which).

{
  "name":          "epiphany",      // patch name shown on device
  "type":          "cluster",       // synth engine
  "synth_version": 3,               // engine version
  "octave":        0,               // octave offset (-2 to +2)
  "mtime":         1709403000.0,    // Unix modification timestamp

  "knobs":         [7104, 29657, 7872, 454, 0, 0, 0, 0],   // 8 synth knobs
  "adsr":          [64, 16320, 22016, 16320, 2048, 7232, 18432, 13568],

  "fx_type":       "mother",        // effect engine
  "fx_active":     true,
  "fx_params":     [21064, 0, 25574, 17000, 0, 0, 0, 0],   // 8 FX knobs

  "lfo_type":      "tremolo",       // LFO engine
  "lfo_active":    true,
  "lfo_params":    [5476, 1017, 4190, 15696, 0, 0, 0, 0]   // 8 LFO knobs
}

Key order: The OP-1 Field firmware uses a streaming JSON parser that requires keys in strict alphabetical order. mondo build passes JSON through as-is - callers must order keys correctly. Required order: adsr, fx_active, fx_params, fx_type, knobs, lfo_active, lfo_params, lfo_type, mtime, name, octave, synth_version, type

dbox (drum box) presets differ: drum_version: 2 in place of synth_version, no adsr, and per-pad arrays instead - dbox_data (24 pads × 8 values), dyna_env (8), and the optional pan, pan_ab, attack, fademode, stereo.

mondo: the C toolkit

One source file (mondo.c), one executable, one subcommand per job. Build it with build.bat (MSVC) or ./master-control.sh build c (cl, else clang, else gcc).

SubcommandWhat it does
dumpprint a patch's metadata and write a .json sidecar
build.json → device-loadable .aif; boundary-patch generator
testcheck an .aif against the file spec
diffshow which fields and indices differ between two patches
explorelow-level chunk and byte inspector
summarizeper-type knob ranges across all presets
renameset each patch's "name" to its filename
sortfile patches into synth/<engine>/
tagderive musical descriptor tags
wrapturn an audio file into a sampler patch
samplesgenerate samples.js from preset JSON
op1list, copy, and delete files on a connected OP-1 (Windows)
projectback up, restore, or reset the whole OP-1 as a project folder (Windows)
tapeeach tape's name, type, and clip count from its tape.json

mondo dump

Reads a .aif patch file, prints all synth metadata with named parameters, and writes a .json sidecar. Pass a directory to process all .aif files recursively.

mondo dump <file.aif>
mondo dump <directory>
mondo dump epiphany.aif         → prints metadata, writes epiphany.json
mondo dump presets/             → dumps all .aif files recursively, prints stats
  • Reads op1-params.json from the current working directory (not the file's directory)
  • Console output: container info, audio format, synth knobs, ADSR, FX params, LFO params, raw JSON
  • Each parameter printed as name : raw_value (normalized) where normalized = raw / 32767
  • Non-zero unnamed slots printed as knob N [unknown] - add them to op1-params.json once identified
  • Always writes a .json sidecar at the same path as the input file
  • Directory mode prints a final summary: === Done: N processed, N failed (X.XX sec) ===

mondo build

Creates a valid OP-1 Field .aif from a .json patch file. Pass a directory to convert all .json files recursively. Also generates all 756×2 boundary patches via the explore subcommand.

Note: mondo build explore (this boundary-patch generator) is unrelated to the top-level mondo explore subcommand (the low-level binary .aif inspector, formerly explore-aif.exe) - the shared word is a naming coincidence, not a typo.

mondo build <patch.json> [output.aif]
mondo build zero <patch.json> [output.aif]
mondo build max  <patch.json> [output.aif]
mondo build <directory>
mondo build explore [-dest <synth|envelope|fx|mix>] [-param <N>]
mondo build mypatch.json                → writes mypatch.aif
mondo build mypatch.json out.aif        → writes out.aif
mondo build zero mypatch.json           → zeros knobs/fx_params/lfo_params
mondo build max  mypatch.json           → sets all three arrays to 32767
mondo build presets/                    → converts all .json files recursively
mondo build explore                     → generates all 3024 files in explore\
mondo build explore -dest fx            → min patches with dest = FX
mondo build explore -dest fx -param 5824
  • Output format: AIFC with FVER + COMM (mono, 16-bit, 22050 Hz, sowt, 28896 frames ~1.310 s) + APPL (1028 bytes) + SSND (silence)
  • JSON must be ≤ 1024 bytes; exits with an error message if too large
  • Automatically injects "mtime":<unix_ts>.0 at write time
  • Single-file mode refuses to overwrite an existing output file; directory mode overwrites
  • Strips UTF-8 BOM from input JSON if present
  • Sampler patches: looks for a .wav sidecar at the same path; embeds it if found, falls back to a 440 Hz sine tone if not

explore subcommand - generates min and max boundary patches for all 15 × 9 × 6 = 810 synth/FX/LFO combinations (3240 files total):

  • Deletes and recreates explore\ at the start of each run
  • Output structure: explore\aif\[mode]\[synth]\[lfo]\*.aif and explore\json\[mode]\[synth]\[lfo]\*.json
  • min uses hardware minimums per parameter type; types with non-zero floors use explicit per-type values
  • max uses hardware maximums per type; types with ceilings below 32767 use explicit per-type values
  • ADSR is fixed to instant-attack / full-sustain values so every combination produces audible output

mondo test

Validates that a generated .aif file conforms to the OP-1 Field file spec (29 checks).

mondo test <file.aif>

Checks include: file size, chunk order and sizes (FVER/COMM/APPL/SSND), COMM spec (22050 Hz, 28896 frames, 16-bit sowt), APPL JSON (all required keys present, alphabetical key order, mtime presence and validity, no UTF-8 BOM), and SSND audio byte count. Exits 0 on pass, non-zero on any failure.

mondo diff

Diffs two patch files and shows exactly which fields and array indices changed.

mondo diff <file_a> <file_b>
mondo diff presets\name0001.json presets\name0002.json
mondo diff sandbox\epiphany.aif  presets\epiphany0005.aif
  • Accepts .json or .aif; if .aif is given, it looks for a .json sidecar next to it - run mondo dump first if the sidecar is missing
  • Skips name, mtime, and _file automatically (they always differ between snapshots)
  • For array fields: lists each changed index with before and after values and signed delta (+N / -N)
  • Prints "No differences" if the patches are identical (excluding skipped fields)

mondo explore

Low-level binary inspector for .aif file internals. Multiple flags can be combined in one call.

mondo explore <file.aif> [flags]
FlagDescription
--read-bytes [N]Hex + ASCII dump of the first N bytes of the file (default: 128)
--parse-chunksWalk the IFF chunk tree - prints chunk ID, file offset, and size for every chunk
--dump-chunk <ID>Hex + ASCII dump of a specific chunk by its 4-char ID (e.g. APPL, COMM, SSND, FVER)
--show-jsonScan the file for {...} blocks and print each one found
--decode-fverPrint the FVER version constant and confirm whether it matches 0xA2805140
--decode-commFull COMM decode: channels, frame count, bit depth, 80-bit extended sample rate, compression type
--analyze-ssndSSND audio stats: byte count, sample count, min/max sample, silence %, peak level in dBFS
mondo explore epiphany.aif --parse-chunks
mondo explore epiphany.aif --dump-chunk APPL
mondo explore epiphany.aif --decode-comm
mondo explore epiphany.aif --analyze-ssnd
mondo explore epiphany.aif --show-json
mondo explore epiphany.aif --parse-chunks --decode-comm --show-json

mondo summarize

Reads all parsed presets and produces a grouped report.

mondo summarize
  • Prerequisite: run dump-all.bat first to generate the .json sidecars
  • Reads: presets/*.json, op1-params.json, op1-params-ok.json
  • Output sections: Synth Engines, FX, LFO - each type shows per-knob min/max raw + normalized ranges and patch count
  • Trailing "Missing from op1-params.json" section prints exact stub lines to paste in

mondo rename

Renames the "name" field inside each .json patch to match the file's own filename (without extension). Edits in place.

mondo rename <patch.json> [patch2.json ...]
mondo rename <directory>
mondo rename presets\epiphany.json         → sets name: "epiphany"
mondo rename collection\_anonymized-popular → renames all .json files recursively
  • Useful for normalising a batch of anonymised or exported patches before loading them on device
  • Directory mode prints a final summary: === Done: N processed, N failed ===

mondo sort

Moves .json and .aif patch pairs into a synth/<engine>/ folder tree, organised by synth engine type. The synth/ folder is created as a sibling of the directory passed in.

mondo sort <patch.json>
mondo sort <directory>
mondo sort collection\_anonymized-popular
  → collection\synth\cluster\patch (1).{json,aif}
  → collection\synth\dimension\patch (2).{json,aif}
  ...
  • Skips any subdirectory named synth to avoid re-processing already-sorted files
  • Never overwrites an existing destination file - prints skip (exists) and moves on
  • .aif sidecars are moved alongside their .json; missing sidecars are silently skipped
  • Directory mode prints a final summary: === Done: N moved, N skipped, N failed ===

mondo tag

Interprets OP-1 patch parameters and outputs a comma-separated list of musical descriptor tags (e.g. pad, metallic, lush, percussive). Also writes a .tags sidecar file alongside each .json for easy grepping.

mondo tag <patch.json> [patch2.json ...]
mondo tag <directory>
mondo tag presets\epiphany.json
  → epiphany: cluster, morphing, pad, lush, spacious, slow tremolo, ambient

mondo tag collection\synth\
  → (tags every .json recursively, writes .tags sidecars, prints stats)

Tags are derived from six domains - engine type, envelope shape, FX character, LFO motion, octave register, and composite combinations:

DomainExample tags
Enginecluster, fm, dimension, morphing, wide, inharmonic
Envelopepad, percussive, sustained, swelling, tight, legato
FXlush, spacious, reverb-heavy, echo, chorus, vintage
LFOtremolo, vibrato, slow tremolo, animated, unpredictable
Octavelow register, sub, upper register
Compositeambient, stab, cinematic, lead, bell

The .tags sidecar is a single comma-separated line - grep-friendly:

grep "bell\|metallic" collection\synth\fm\*.tags

The website tags uploads with a simplified port of this (functions/_shared/tags.ts), and caps hand-added tags at 7 per patch.

mondo wrap

Wraps an audio file into an OP-1 Field sampler patch. The root note defaults to middle C, and it warns about clips longer than the ~6 s the hardware's own sampler patches use.

mondo wrap <input.aif | input.wav> [output.aif]
mondo wrap <directory>

mondo samples

Generates samples.js from preset JSON files. That file feeds the patch editor's sample list and the local database seed (node scripts/seed.mjs).

mondo samples <preset.json | directory> [output.js]
mondo samples collection samples.js

mondo op1

List, copy, create, rename, and delete files on a connected OP-1 Field (Windows only).

TL;DR: mondo op1 ls -r synth to look around, mondo op1 pull synth presets\op1 to back up, mondo op1 push mypatch.aif synth/myfolder to load a patch.

The OP-1 Field connects over MTP, not USB mass storage. Explorer lists it under This PC, but it never gets a drive letter, so copy, xcopy, and plain fopen can't reach it. mondo op1 uses the Windows Portable Devices API instead. ./master-control.sh op1 <verb> ... runs the same commands.

mondo op1 [-d <device>] ls    [-r] [path]
mondo op1 [-d <device>] pull  <path> [local dest]
mondo op1 [-d <device>] push  [-f] <local file|folder> <device folder>
mondo op1 [-d <device>] mkdir [-p] <path>
mondo op1 [-d <device>] rm    [-r] <path>
mondo op1 [-d <device>] mv    <path> <new name>
mondo op1 ls                                      → drum/ mixdown/ synth/ tapes/
mondo op1 ls -r synth                             → every synth folder and patch, with size and date
mondo op1 pull synth/_cluster/bass-4440.aif .     → copies one patch into the current folder
mondo op1 pull synth backup\synth                 → copies the whole synth tree (recursive)
mondo op1 push presets\epiphany.aif synth/mine    → uploads a patch into synth/mine
mondo op1 push -f presets\epiphany.aif synth/mine → replaces it if it already exists
mondo op1 push sandbox\mypatches synth            → uploads a folder as synth/mypatches
mondo op1 mkdir synth/mine                        → creates a patch folder
mondo op1 mv synth/mine/old.aif new.aif           → renames in place
mondo op1 rm synth/mine/new.aif                   → deletes a patch
mondo op1 rm -r synth/mine                        → deletes a folder and its contents
  • Device paths use /, start at the OP-1's root (synth, drum, tapes, mixdown), and match case-insensitively
  • pull works like cp: an existing local folder (or a dest ending in \) gets the item placed inside it; otherwise the dest is the new name
  • push won't replace an existing file unless you pass -f. MTP has no in-place overwrite, so -f deletes the old file and then uploads the new one
  • rm needs -r for folders, and it refuses to delete the root or the four top-level folders
  • The OP-1 allows only one folder level (synth/<folder>/patch.aif), so a deeper mkdir or folder push fails
  • Renaming a patch also changes its "name" field. The OP-1 rewrites the APPL JSON to match the new file name, so the bytes after a rename differ from the original
  • -d <name> picks a device by its friendly name. By default it matches OP-1, or any teenage engineering USB device (VID 2367)

mondo project

Back up, restore, or reset everything you can change on a connected OP-1 Field (Windows only).

TL;DR: mondo project pull mybackup to back up, mondo project push mybackup -f to restore, mondo project push factory-reset -f to start over from factory state.

A project is a folder in projects/ (gitignored) holding the device's whole changeable state:

projects/<name>/
  drum/     slots/1-8/ and your folders, every .aif at any depth
  synth/    the same
  mixdown/  side_a.wav, side_b.wav
  tapes/    <tape>/tape.json, track_1.wav ... track_4.wav
  README.md written by mondo: what the folder is and how to restore it
mondo project list                          projects in projects/, with file counts and sizes
mondo project pull <name> [-f]              back up the OP-1 into projects/<name>/
mondo project push <name> [-f]              restore projects/<name>/ onto the OP-1
mondo project diff <name>                   what differs between the project and the OP-1
... --only tapes|mixdown|<path>              just tapes/, mixdown/, or one folder or patch (synth/user, drum/slots/1/kit.aif)
mondo project init <name> [--from <p>] [-f] a new project copied from projects/factory-reset/ (or <p>)
  • -f applies, its absence previews. Pulling into a new folder copies at once. Pulling into an existing project, or any push, without -f only lists the changes (+ add, ~ replace, - delete) and exits.
  • Push -f mirrors. The OP-1 ends up exactly like the project: files and folders on the device that aren't in the project are deleted. Take a pull backup first if the device holds anything you want to keep.
  • Only changes move. Files match by path and size, so updating a backup of large tapes copies only what changed.
  • Notes. A project can describe itself in two ways:
    • On the computer only: the generated README.md and any other file you add that the device can't hold (NOTES.md, tapes/<tape>/notes.md, …). These are never pulled, pushed, or deleted. Inside drum/ and synth/, only audio files (.aif, .aiff, .aifc, .wav) sync.
    • On the device too: the OP-1's own text files — each folder's readme.txt and the you may create folders here marker. Edit them in the project, and push writes them over the device's copies, which come with the factory text. The OP-1 lets these be rewritten but refuses to create any new non-audio file. So a hint the device doesn't have stays in the project, marked ! in the output. A tape folder made by a push starts empty of hints, but opening it once on the OP-1 fills it in (a factory readme.txt, a fresh tape.json, empty tracks). Push again after that and its notes go across. Hints are never deleted on either side, and a pull keeps the project's text when it differs from the device's (= in the output).
  • Folder deletes are never recursive, so a folder still holding a hint or your notes stays.
  • factory-reset is a capture, not a template we invented. Factory-reset the OP-1, then run mondo project pull factory-reset. init copies that capture as the starting point for new projects, and pushing it resets the device.
  • ./master-control.sh project <verb> ... runs the same commands.
  • mondo project list shows each project's tapes by type (see mondo tape).

mondo tape

mondo tape [-v] <tape.json | tape folder | tapes folder | project name>

Prints each tape's name, type, and clip count. -v adds every field stored in tape.json.

mondo tape tape-types
  d                    Disk Mini          0 clips   (firmware layout)
  s                    Studio 4-Track     0 clips   (firmware layout)
  • A tape's folder name is its name on the OP-1: naming a tape on the device renames tapes/<name>/.
  • tape_type is 0 Studio 4-Track, 1 Vintage 4-Track, 2 Porta 4-Track, 3 Disk Mini, captured from one tape of each type made on the device. The other fields (tempo, loop, mixer, metronome, sync, clips) print as stored until they're probed one setting at a time.
  • tape.json has two layouts: the one in the factory image (no mixer_params, loop stored as 0) and the one the firmware writes when it creates or opens a tape (mixer_params, loop as true/false).

Potion: the desktop app

TL;DR: install Visual Studio 2022's C++ tools, open an x64 Native Tools Command Prompt for VS 2022, and run the six lines below. potion.exe lands in the repo root, beside mondo.exe.

Build it on Windows

You need

  • Windows 10 or 11, 64-bit
  • Git for Windows
  • Visual Studio 2022 (Community is free) or the Build Tools for Visual Studio 2022, with the Desktop development with C++ workload. It brings the compiler (cl.exe) and the Windows SDK (rc.exe, which builds the app's icon)

Steps. Open x64 Native Tools Command Prompt for VS 2022 from the Start menu (a plain Command Prompt or PowerShell doesn't have the compiler on its PATH), then:

git clone https://github.com/jstnfst/te-op1.git
cd te-op1
git submodule update --init --depth 1
build.bat
desktop\build.bat
potion.exe
  1. git clone and cd: the repo, and into it. Every later step runs from the repo root
  2. git submodule update --init --depth 1: fetches raylib and raygui into raylib/ and raygui/, each at the commit the repo pins
  3. build.bat: builds mondo.exe. Potion runs it for every backup, restore and compare, and finds its own folder by it
  4. desktop\build.bat: builds potion.exe. The first build also compiles raylib into desktop\build\raylib.lib, which takes a while; later builds reuse it (desktop\build.bat clean rebuilds it)
  5. potion.exe: starts the app. It works from the folder that holds mondo.exe, so projects/ and packs/ are the same folders the command line uses

If it fails

  • 'cl' is not recognized: the prompt isn't a Visual Studio one. Open the x64 Native Tools prompt, or run vcvars64.bat from Visual Studio's VC\Auxiliary\Build folder in the one you have
  • 'rc' is not recognized: the Windows SDK is missing. In the Visual Studio Installer, Modify and add a Windows 11 SDK under Individual components
  • Cannot open include file: 'raylib.h': the submodules aren't there. Run the git submodule update line
  • Building in a git worktree: point it at the main checkout's submodules with set RAYLIB_DIR=<checkout>\raylib and set RAYGUI_DIR=<checkout>\raygui

To hand it to someone, copy it into a folder that runs on a PC without Visual Studio: potion.exe, mondo.exe, the words, the license files and vcruntime140.dll. Run it from Git Bash or Cygwin:

bash desktop/dist.sh <folder>

Running it

potion.exe                             open on the tab it was closed on
potion.exe --version                   print the version and quit (mondo.exe --version too)
potion.exe --tab projects              open on a tab (projects, editor, packs, transfer, mixdowns, search; doctor)
potion.exe synth\user\pad.aif          open a patch; a .zip imports as a pack
potion.exe --lang fr                   its words from desktop/lang/fr.txt (or set MONDO_LANG)

The app works from the folder that holds mondo.exe, so projects/ and packs/ are the same folders the command line uses. It keeps its settings, clipboard, tags and stars, and an index of the patch files in .appdata.bin there (gitignored), written through a temp file. Sounds copied in a run stay in memory only.

How it's built. Everything that touches the OP-1 runs mondo.exe on a background thread and streams its output into the console under every view: project init, pull, push, diff. Pull and push run without -f first, and the apply button only lights once that preview finds changes. Patches, packs, and zips are handled in-process:

FileWhat it does
main.cwindow, tabs, console, the mondo job runner's output, drag and drop
projects.c / editor.c / packs.c / transfer.c / mixdowns.cthe five tabs (tapes.c is the tape editor, doctor.c the Doctor)
patch.cload and save .aif / .json; section paste; the LFO PARAMETER options (port of patch.html)
knob.cthe knob model: travel, on-screen value, dial position (port of knob-model.js)
op1_data.hknob names, value mappings, and knob tables, generated from public/*.js
store.c / zip.cprojects/, packs/, archive/, and .zip read/write (DEFLATE via raylib)
meta.c / hash.c.appdata.bin: tags and stars per sound, the patch index; a sound's fingerprint
sample.c / wave.ca patch's audio: AIFF read/write, kit layout, the sampler's 6 s scale, preview, waveforms
undo.cthe last file action, once (Transfer, Packs, the editor's tree)
i18n.c / lang/en.txtevery word the app shows, by key
ui.c / sys_win.cwidgets and theme (DESIGN.md's tokens, raygui restyled); Win32 calls kept apart from raylib.h

Saving keeps the file. An opened patch is saved by swapping the JSON inside its own .aif, so every other chunk (the audio above all) stays byte for byte; the APPL chunk grows only if the JSON outgrows it. Unedited numbers keep the text they were read with ("base_freq":261.625550), and a file that's opened and saved without edits gets no new keys, so a drum sample kit ("type":"drum") or an older patch stays as the device wrote it. The site's editor fills in missing keys on load; the desktop app only does that when you change the engine or paste.

Keep the data in step. op1_data.h comes from the site's format data, plus scripts/desktop-data-extra.js for what the site doesn't show yet (a drum kit's per-key settings). After editing public/layout-params.js, mapping-data.js, or knob-model.js, regenerate it and rebuild:

node scripts/gen-desktop-data.mjs
desktop\build.bat

Releasing

echo 0.2.0> VERSION
git commit -am "Potion 0.2.0"
git tag v0.2.0
git push origin master v0.2.0
  1. VERSION holds the version. Both build scripts read it into the exes (--version, and Properties in Explorer). Add a ## 0.2.0 (<date>) section to the top of desktop/CHANGELOG.md in the same commit: it becomes the release notes
  2. The v0.2.0 tag runs .github/workflows/release.yml: it builds both exes on Windows, checks --version matches the tag, packages them with desktop/dist.sh, and publishes potion-0.2.0-win64.zip and SHA256SUMS.txt as a GitHub Release. A tag that doesn't match VERSION, or a missing CHANGELOG section, fails the run
  3. Running the workflow by hand (Actions, Run workflow) builds the zip as an artifact, with no Release

Testing on the hardware

Two folders hold patches made for, or saved by, the OP-1 itself.

sandbox/: probe sets (gitignored, yours to make and throw away). A probe set is a folder of patches that differ in one array slot, so loading them one by one on the device shows what that slot does at each raw value. master-control.sh runs the whole loop (Windows, with the OP-1 connected):

# one patch per value of lfo_params[2], from a base preset, built to .aif
./master-control.sh probe sweep presets/x.json lfo_params 2 1024,2144,4160,7168 rd
./master-control.sh probe push rd     # onto the OP-1 as synth/_rd/
#   ...load each on the device and read the screen; save any you change...
./master-control.sh probe pull rd     # what the device saved -> sandbox/rd/from-device/, dumped to JSON
./master-control.sh probe clean rd    # remove synth/_rd/ from the OP-1
./master-control.sh probe list        # the sets you have

Each patch is named <set> i<index> <value> (e.g. rd i2 1024) so it reads on the device's screen. A sweep from an .aif keeps that file's audio, and a drum set goes to drum/_<set>/. For a drum kit's per-key settings, one patch can carry a whole sweep, one value per pad:

# pad 1 = 0, pad 2 = 4096, ...: load it, play each key, read its PLAY MODE
./master-control.sh probe pads projects/x/drum/slots/1/cherry.aif playmode 0,4096,8192,12288 pm

To go the other way, from a change on the device to the JSON: load a patch on the OP-1, change one thing, hold a number key to save a snapshot, and repeat. scripts/snapshot-diff.sh drum <the patch you loaded>.aif then pulls drum/snapshot/ and prints what each snapshot changed from the one before it (synth for synth snapshots). probe build <set> rebuilds a set after you edit its .json files by hand. The pulled copies are how firmware quirks show up: the random LFO's DESTINATION, for one, doesn't survive a save on the device.

oracle/: ground truth (tracked in git, never generated). Reference exports saved directly from the OP-1 Field with known knob positions.

  • Never regenerate oracle files from JSON. mondo build refuses to overwrite them.
  • To capture one: set the knobs on the hardware → save the preset → copy the .aif into oracle/ (./master-control.sh op1 pull synth/<folder>/<file>.aif oracle/) → mondo dump oracle\<file>.aif and read the raw values from the JSON sidecar.
  • Naming: <synth>-<fx>-<lfo>-<tag>.aif, where tag is 0000 (all-min), ffff (all-max), or a short descriptor.

License

Copyright © 2026 Justin. Licensed under the PolyForm Noncommercial License 1.0.0: you may use, change and share it for any noncommercial purpose, such as personal use, study, hobby projects, or a charity or school. Any commercial use, selling it included, needs a separate license from the copyright holder. Pass on the Required Notice: line at the top of LICENSE with any copy. raylib, raygui and cJSON keep their own licenses (THIRD_PARTY_LICENSES).

OP-1 and OP-1 Field are trademarks of teenage engineering. This project is not affiliated with or endorsed by teenage engineering.