Skip to content

jhw/octapy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

203 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

octapy

A Python library for reading, writing, and modifying Elektron Octatrack project files.

Overview

octapy lets you build and edit Octatrack projects (project.work, bank01..16.work, markers.work) programmatically. The Octatrack is a sampler/sequencer/looper by Elektron; octapy is a Python port of ot-tools-io, an open-source Rust library by Mike Robeson (dijksterhuis).

What it covers:

  • All five audio machine types (Flex, Static, Thru, Neighbor, Pickup)
  • Pattern sequencing for 8 audio + 8 MIDI tracks per pattern (16 patterns per bank × 16 banks)
  • Parts (4 per bank): machine config, AMP/FX1/FX2 pages, LFOs (3 per track), MIDI channel/note/arp settings
  • 16 scenes per part with parameter locks (machine, LFO, AMP, FX1, FX2) and crossfader assignments (XLV)
  • Per-step p-locks for audio (volume, pitch, start, length, rate, retrig, retrig_time, sample_lock, slice_index, LFOs) and MIDI (note, velocity, length, pitch_bend, aftertouch, CC1-10, LFOs)
  • Trig flags: active, trigless, swing, slide (audio); active, trigless, swing (MIDI)
  • Trig conditions: FILL/PRE/NEI/FIRST/probability/pattern-loop (TX_RY)
  • Recorder buffer setup (source, rlen, trig mode, qrec, qpl, fades, gains)
  • Per-slice markers in markers.work (slot 1-128 user samples + 129-136 recorder buffers)
  • Every field in project.work (mixer, metronome, memory, MIDI ports, pattern chain) preserved on round-trip

What it doesn't cover: arrangements (arr01..08.work), custom 16-step LFO designs, custom MIDI arp sequences, per-sample .ot setting files. See ot-tools-io for those.

Install

pip install -e .

Runtime dep: pydub (sample-pool handling). Dev deps under the dev extra: pytest, pytest-cov.

Quick start

from octapy import Project, RecordingSource, FX1Type, FX2Type, TrigCondition

project = Project.from_template("MY-PROJECT")
project.settings.tempo = 124.0
project.settings.master_track = True   # T8 sums T1-7

# Add a sample, configure track 1 as a Flex machine on it
slot = project.add_sample("samples/kick.wav")
project.bank(1).part(1).track(1).configure_flex(slot)

# Sequence — four-on-the-floor on track 1
pattern = project.bank(1).pattern(1)
pattern.audio_track(1).active_steps = [1, 5, 9, 13]

# P-lock a velocity ramp on each kick
for step_num, vol in zip([1, 5, 9, 13], [127, 80, 100, 80]):
    pattern.audio_track(1).step(step_num).volume = vol

# Probabilistic ghost kick on step 11
pattern.audio_track(1).step(11).active = True
pattern.audio_track(1).step(11).probability = 0.5

# Configure track 7 as a transition / resample buffer across the entire
# project (every part of every bank) in one call.
project.configure_recorder_buffer(7, RecordingSource.MAIN)

project.to_zip("tmp/projects/MY-PROJECT.zip")

Reading an existing project:

project = Project.from_directory("/Volumes/OCTATRACK/EXISTING")
print(project.name, project.settings.tempo)
for slot in project.sample_pool:
    print(f"  slot {slot.slot_number}: {slot.path}")

API hierarchy

Project
├── settings           # Tempo, master_track, plus .mixer / .midi / .metronome / .memory / .pattern_chain groups
├── sample_pool        # Sample slot assignments (Flex + Static)
├── markers            # Slice grids per sample slot
└── Bank (1-16)
    ├── Pattern (1-16)
    │   ├── audio_track(1..8)  # AudioPatternTrack — trigs + per-step p-locks
    │   │   └── step(1..64)    # AudioStep — active, trigless, swing, slide, condition, p-locks
    │   └── midi_track(1..8)   # MidiPatternTrack
    │       └── step(1..64)    # MidiStep — note, velocity, length, CCs
    └── Part (1-4)
        ├── audio_track(1..8)  # AudioPartTrack — machine type, slot, AMP, FX1/2, recorder, lfo(1..3)
        ├── midi_track(1..8)   # MidiPartTrack — channel, default note, arp, lfo(1..3)
        └── scene(1..16)       # Scene — per-track param locks + crossfader (XLV) assignments

Project is the entry point. Create from template, load from directory or zip.

Bank holds 16 patterns + 4 parts. Each pattern references one part for its sound settings.

Pattern → audio_track/midi_track → step is the sequencer path. Set active_steps to place trigs; mutate individual steps for per-step p-locks and conditions.

Part → audio_track/midi_track is the sound design path. Page-style accessors (track.src, track.setup, track.amp, track.fx1, track.fx2) mirror the device's FUNC+page buttons.

Machine types

from octapy import MachineType

track = project.bank(1).part(1).track(1)

# Configure helpers handle machine type + sample slot + recommended defaults
track.configure_flex(slot)            # FLEX — RAM-loaded sample playback
track.configure_static(slot)          # STATIC — streaming sample playback
track.configure_thru(in_ab="A_PLUS_B")# THRU — live input routing
track.configure_neighbor()            # NEIGHBOR — extra FX on the previous track
track.configure_pickup()              # PICKUP — live looper

# Page accessors — names are machine-type-aware
track.src.pitch = 64
track.src.length = 100
track.setup.loop = 0          # FLEX/STATIC: loop OFF
track.setup.length_mode = 1   # FLEX/STATIC: TIME
track.amp.attack = 10
track.amp.release = 64
track.fx1.base = 100          # name depends on track.fx1_type

P-locks and conditions

from octapy import TrigCondition

step = pattern.audio_track(1).step(5)
step.volume = 100
step.pitch = 72
step.length = 64
step.condition = TrigCondition.FILL
# Or use the float helper (quantizes to nearest valid percent):
step.probability = 0.5

# Swing and slide trig flags (audio); MIDI has swing only
step.swing = True
step.slide = True

# Sample-lock — change which sample plays on this step
step.sample_lock = other_slot

# MIDI CC p-locks
midi_step = pattern.midi_track(1).step(5)
midi_step.note = 60
midi_step.velocity = 90
midi_step.cc(1).value = 64    # CC1 value lock
midi_step.cc(3).value = 100

LFOs

3 LFOs per audio track and per MIDI track, with speed/depth on the main page and destination/waveform/multiplier/trig_mode on the setup page:

from octapy import LfoWaveform, LfoTrigMode

lfo = project.bank(1).part(1).track(1).lfo(1)
lfo.waveform = LfoWaveform.SIN
lfo.speed = 32
lfo.depth = 100
lfo.destination = 12             # raw OS-dependent index
lfo.multiplier = 7
lfo.trig_mode = LfoTrigMode.TRIG

# Per-step p-locks
pattern.audio_track(1).step(5).lfo(1).speed = 64
pattern.audio_track(1).step(5).lfo(1).depth = 127

Scenes

Each part has 16 scenes (the A/B targets the device's crossfader morphs between). A scene holds parameter locks per track plus per-track crossfader (XLV) assignments:

scene = project.bank(1).part(1).scene(1)

# Parameter locks per track
scene.track(1).amp_volume = 100
scene.track(1).pitch = 72
scene.track(2).fx2_param1 = 64

# Crossfader (XLV) assignments — 0-127 fade amounts per track, or None to clear
scene.set_crossfader(1, 0)       # T1 silent at this scene
scene.set_crossfader(7, 127)     # T7 (e.g. transition track) full

Settings

Project-level settings are organized into groups under project.settings:

project.settings.tempo = 124.0
project.settings.master_track = True

# MIDI ports — clock/transport/PC, per-track trig channels, CC routing
project.settings.midi.clock_send = True
project.settings.midi.transport_receive = True
project.settings.midi.set_trig_channel(1, 9)
project.settings.midi.auto_channel = 5

# Mixer — main/cue levels, phones mix, input gates and gains
project.settings.mixer.main_level = 100
project.settings.mixer.cue_level = 110

# Metronome — time sig, volumes, pitch
project.settings.metronome.enabled = True
project.settings.metronome.cue_volume = 64

# Memory — recorder allocation, 24-bit flags
project.settings.memory.record_24bit = True
project.settings.memory.reserved_recorder_count = 4

# Pattern chain — auto-silence, auto-trig LFOs
project.settings.pattern_chain.auto_trig_lfos = True

Every field in project.work survives a round-trip.

Recorder buffers — one-line setup

The classic transition-trick / live-resample pattern is one method call:

from octapy import RecordingSource

# Configure track 7 as a recorder buffer in every part of every bank.
# Eager — observable in project state immediately.
project.configure_recorder_buffer(7, RecordingSource.MAIN)

# With a slice grid: divide the buffer into N equal slices, enable slice
# mode, and place N trigs with slice_index p-locks in every pattern that
# already has activity. Call this AFTER setting up your patterns.
project.configure_recorder_buffer(7, RecordingSource.MAIN, slices=4)

When settings.master_track = True, two save-time fixups run automatically inside to_directory() / to_zip(): a step-1 trig is auto-added to track 8 in every pattern with activity, and any recorder source set to TRACK_8 is rewritten to MAIN (the OT can't actually record track-8 output when track 8 is master — this lets you write TRACK_8 as a logical reference).

Defaults — octapy baseline vs OT factory

octapy's default values for the SRC and recorder pages differ from the OT's factory values in a small number of places, chosen for programmatic one-shot sample workflows: length=127, length_mode=TIME, loop=OFF, recorder rlen=16, qrec=PLEN. These are applied consistently whether you construct a track directly, build a project from scratch, or load from template.

If you need byte-for-byte parity with the device's on-device factory state, call track.reset_to_factory_defaults() — the escape hatch is symmetric with apply_recommended_defaults().

Sample slots

from octapy import SamplePool

# Add an individual sample (auto-assigned to next free Flex slot)
slot = project.add_sample("samples/kick.wav")

# Or scan a directory and filter by regex
kicks = SamplePool("samples/drums", r"BD")
kick_slot = project.add_sample(kicks.random())

Slots 1-128 are user samples (Flex or Static); slots 129-136 are reserved for the 8 recorder buffers. configure_recorder_buffer() and configure_recorder() wire the corresponding recorder slot automatically.

Demos

Runnable scripts under demos/ produce .zip files in tmp/projects/:

Demo Demonstrates
demos/flex_live.py Flex-machine live-transition project: euclidean drum patterns, master_track + auto-master-trig, configure_recorder_buffer for the T7 transition trick

Run:

PYTHONPATH=. python demos/flex_live.py

Shipping to the Octatrack

tools/sync.py covers the local-to-device loop:

python tools/sync.py                       # status: local vs remote
python tools/sync.py push project          # copy tmp/projects/*.zip to the OT
python tools/sync.py push project flex     # filter by substring
python tools/sync.py push samples          # copy tmp/samples/*/ to AUDIO/samples/
python tools/sync.py clean local           # remove tmp/projects/*.zip
python tools/sync.py clean remote          # remove projects from the OT (+ AUDIO/projects/<name>)
python tools/sync.py clean samples         # remove sample packs from AUDIO/samples/

-f skips per-item prompts. Non-interactive stdin auto-confirms. Defaults assume the OT is mounted at /Volumes/OCTATRACK/Woldo.

Project layout

octapy/
├── api/
│   ├── enums.py             # MachineType, TrigCondition, NoteLength, MidiNote, FX/Lfo enums, …
│   ├── settings.py          # Settings + grouped sub-objects (mixer/midi/metronome/memory/pattern_chain)
│   ├── sample_pool.py       # SamplePool (directory scan + filter)
│   ├── slot_manager.py      # Sample slot allocation
│   ├── utils.py             # quantize_note_length, probability_to_condition
│   └── core/
│       ├── project.py        # Project — top-level container, configure_recorder_buffer, save pipeline
│       ├── bank.py           # Bank — patterns + parts + flex_count + checksum
│       ├── pattern.py        # Pattern — audio_track / midi_track containers + scale settings
│       ├── part.py           # Part — audio_track / midi_track + scenes
│       ├── scene.py          # Scene — param locks + crossfader (XLV)
│       ├── _page.py          # PageAccessor — named param access shared by SRC/SETUP/AMP/FX
│       ├── _lfo_plock.py     # LfoPlock — step.lfo(n) accessor
│       ├── _trig.py          # 8-byte trig mask <-> step list
│       ├── audio/            # AudioPartTrack, AudioPatternTrack, AudioRecorderSetup,
│       │                     # AudioSceneTrack, AudioStep, AudioLfo
│       └── midi/             # MidiPartTrack, MidiPatternTrack, MidiStep, MidiLfo
├── _io/                     # Low-level binary I/O — BankFile, MarkersFile, ProjectFile (INI)
└── templates/               # Embedded project-template-1.40B.zip (Octatrack factory template)

demos/                       # Runnable scripts
tests/                       # pytest suite
tools/sync.py                # Unified push / clean / status for projects + samples
tools/download_erica_pico_packs.py  # One-off sample-pack downloader

Development

python -m pytest tests/             # fast suite (~17s)
python -m pytest tests/ --slow      # full suite incl. project save/load roundtrips (~49s)

Tests live in tests/, organized by area (test_core, test_projects, test_scenes, test_markers, test_banks, test_slots, test_parts, test_templates, test_roundtrip). Slow tests are marked with @pytest.mark.slow and skipped by default.

File format references

License

MIT.

Credits

Heavily indebted to Mike Robeson (dijksterhuis) for ot-tools-io — without that Rust reference this Python port would not exist. Thanks also to Elektron for the Octatrack itself.

About

File IO utils for the Elektron Octatrack

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages