A Python library for reading and writing DirtyWave M8 tracker files.
pym8 is a Python package for programmatically creating, reading, and manipulating M8 project files (.m8s). The M8 is a portable music tracker and synthesizer by Dirtywave.
This library provides a high-level API for working with M8 projects, including:
- Creating and modifying instruments (samplers)
- Building phrases with notes and FX commands
- Arranging chains and song sequences
- Reading and writing .m8s project files
pip install -e .Or for development:
pip install -r requirements-dev.txtfrom m8.api.project import M8Project
from m8.api.instruments.sampler import M8Sampler, M8SamplerParam
from m8.api.phrase import M8Phrase, M8PhraseStep, M8Note
from m8.api.chain import M8Chain, M8ChainStep
from m8.api.fx import M8FXTuple, M8SequenceFX, M8SamplerFX
# Initialize a new project from template
project = M8Project.initialise()
project.metadata.name = "MY-SONG"
project.metadata.tempo = 140
# Create a sampler instrument
kick = M8Sampler(name="KICK", sample_path="samples/kick.wav")
kick.set(M8SamplerParam.DELAY_SEND, 0x80) # Add delay send
project.instruments[0] = kick
# Create a phrase with beats
phrase = M8Phrase()
for step in [0, 4, 8, 12]: # Four-on-the-floor
phrase[step] = M8PhraseStep(
note=M8Note.C_4, # C4
velocity=0x6F, # Full velocity
instrument=0x00 # Use instrument slot 0
)
project.phrases[0] = phrase
# Create a chain referencing the phrase
chain = M8Chain()
chain[0] = M8ChainStep(phrase=0x00, transpose=0x00)
project.chains[0] = chain
# Add chain to song
project.song[0][0] = 0x00 # Chain 0 at row 0, track 0
# Save project
project.write_to_file("MY-SONG.m8s")from m8.api.project import M8Project
# Read project from file
project = M8Project.read_from_file("existing-song.m8s")
# Access project data
print(f"Project: {project.metadata.name}")
print(f"Tempo: {project.metadata.tempo}")
# Inspect instruments
for i, instrument in enumerate(project.instruments):
if instrument and hasattr(instrument, 'name'):
print(f"Instrument {i}: {instrument.name}")Currently supports M8 Sampler instruments with full parameter control:
from m8.api.instruments.sampler import M8Sampler, M8SamplerParam, M8PlayMode
sampler = M8Sampler(name="SNARE", sample_path="samples/snare.wav")
# Set playback parameters
sampler.set(M8SamplerParam.VOLUME, 0xFF)
sampler.set(M8SamplerParam.PLAY_MODE, M8PlayMode.FWDLOOP)
sampler.set(M8SamplerParam.START, 0x00)
sampler.set(M8SamplerParam.LENGTH, 0xFF)
# Set filter parameters
sampler.set(M8SamplerParam.FILTER_TYPE, 0x01) # LP filter
sampler.set(M8SamplerParam.CUTOFF, 0xC0)
sampler.set(M8SamplerParam.RESONANCE, 0x40)
# Set mixer/sends
sampler.set(M8SamplerParam.PAN, 0x80) # Center
sampler.set(M8SamplerParam.CHORUS_SEND, 0x40)
sampler.set(M8SamplerParam.DELAY_SEND, 0x80)
sampler.set(M8SamplerParam.REVERB_SEND, 0x60)Create phrases with notes and apply FX commands using typed enums:
from m8.api.phrase import M8Phrase, M8PhraseStep
from m8.api.fx import M8FXTuple, M8SequenceFX, M8SamplerFX
phrase = M8Phrase()
# Add a note with retrigger FX
step = M8PhraseStep(note=0x30, velocity=0x6F, instrument=0x00)
step.fx[0] = M8FXTuple(key=M8SequenceFX.RET, value=0x40) # Retrigger
step.fx[1] = M8FXTuple(key=M8SamplerFX.LEN, value=0xC0) # Sample length
phrase[0] = step
# Common sequence FX
step.fx[0] = M8FXTuple(key=M8SequenceFX.ARP, value=0x03) # Arpeggio
step.fx[0] = M8FXTuple(key=M8SequenceFX.HOP, value=0x08) # Jump to step 8
step.fx[0] = M8FXTuple(key=M8SequenceFX.KIL, value=0x00) # Kill note
# Common sampler FX
step.fx[0] = M8FXTuple(key=M8SamplerFX.VOL, value=0xFF) # Volume
step.fx[0] = M8FXTuple(key=M8SamplerFX.PIT, value=0x0C) # Pitch up
step.fx[0] = M8FXTuple(key=M8SamplerFX.PLY, value=0x01) # Reverse playback
step.fx[0] = M8FXTuple(key=M8SamplerFX.CUT, value=0xC0) # Filter cutofffrom m8.api.chain import M8Chain, M8ChainStep
# Create a chain with multiple phrases
chain = M8Chain()
chain[0] = M8ChainStep(phrase=0x00, transpose=0x00) # Phrase 0, no transpose
chain[1] = M8ChainStep(phrase=0x01, transpose=0x0C) # Phrase 1, up 12 semitones
chain[2] = M8ChainStep(phrase=0x00, transpose=0xF4) # Phrase 0, down 12 semitones
project.chains[0] = chain
# Add chains to song matrix (rows x 8 tracks)
project.song[0][0] = 0x00 # Chain 0 on track 0
project.song[0][1] = 0x01 # Chain 1 on track 1
project.song[1][0] = 0x00 # Chain 0 repeats on row 1# Set project metadata
project.metadata.name = "MY-TRACK"
project.metadata.tempo = 140.0
project.metadata.directory = "/Songs/my-tracks/"
project.metadata.transpose = 0x00
project.metadata.key = 0x00pym8 provides typed enum classes for FX commands and sampler parameters, making code more readable and reducing errors:
- M8SequenceFX: Global FX commands (ARP, RET, HOP, KIL, etc.)
- M8SamplerFX: Sampler-specific FX commands (VOL, PIT, PLY, CUT, etc.)
- M8SamplerParam: Byte offsets for sampler parameters (VOLUME, PITCH, FILTER_TYPE, CUTOFF, etc.)
- M8PlayMode: Sample playback modes (FWD, REV, FWDLOOP, OSC, etc.)
- M8Note: Note values from C1 (0) to G11 (127)
- Dynamically generated with format:
C_1,CS_1,D_1, ...,C_4(36/0x24), ...,G_11 - M8 octave numbering: byte 0 = C1, so C4 = 36 (0x24)
- Dynamically generated with format:
These enums are defined directly in the source code (m8/api/fx.py, m8/api/sampler.py, and m8/api/phrase.py) based on the M8 file format specification.
See the demos/ directory for complete working examples:
-
acid_banger_909_demo.py: Algorithmic 909 drum patterns inspired by vitling's acid-banger
- Uses pattern generators from
acid_banger_909_patterns.py - Random sample selection from Erica Pico sample packs (download first with
download_erica_pico_samples.py) - Random FX on hats (cut, reverse, retrigger)
- 48 instruments across 16 rows with proper M8 song/chain/phrase structure
- Uses pattern generators from
-
euclid_909_demo.py: Euclidean rhythm patterns using the Bjorklund algorithm
- Uses Euclidean rhythm generators from
euclidean_patterns.py - Based on Bjorklund algorithm and Toussaint's research
- Traditional rhythms from world music (Cuban tresillo, African bell patterns, Brazilian rhythms)
- Groove algorithms inspired by Erica Synths Perkons HD-01 for volume variation
- Random sample selection from Erica Pico sample packs
- 48 instruments across 16 rows with proper M8 song/chain/phrase structure
- Uses Euclidean rhythm generators from
-
download_erica_pico_samples.py: Downloads Erica Synths Pico Drum sample packs to
tmp/erica-pico-samples/
Run demos with:
# Download samples first (required for both demos)
python demos/download_erica_pico_samples.py
# Run demos
python demos/acid_banger_909_demo.py
python demos/euclid_909_demo.pyManage demos on your M8 device:
# Copy demos to M8
python tools/copy_demos_to_m8.py
# Clean demos from M8
python tools/clean_demos_from_m8.pypym8/
├── m8/
│ ├── api/ # Core API modules
│ │ ├── project.py # M8Project - top-level container
│ │ ├── instrument.py # Base instrument class
│ │ ├── sampler.py # M8Sampler + enums
│ │ ├── phrase.py # M8Phrase, M8PhraseStep
│ │ ├── chain.py # M8Chain, M8ChainStep
│ │ ├── song.py # M8SongMatrix
│ │ ├── fx.py # M8FXTuple + FX enums
│ │ ├── metadata.py # Project metadata
│ │ ├── modulator.py # Modulator support
│ │ └── version.py # Version handling
│ └── templates/ # Template .m8s files for different firmware versions
├── demos/ # Example scripts
├── tests/ # Unit tests
└── tools/ # Utility scripts
The M8 file format is a binary format with fixed offsets for different data sections. pym8 is based on research from these excellent open-source projects:
-
m8-file-parser (Rust)
The most complete and authoritative M8 file format parser. This project documents:
- Binary structure layouts
- FX command codes and their values
- Instrument parameter offsets
- Version-specific format changes (firmware 4.0 - 6.2+)
All FX enums and sampler parameter offsets in pym8 are based on this project.
-
m8-js - JavaScript implementation with comprehensive enum documentation
This project is particularly valuable for enum information (FX commands, parameter values, play modes, etc.). However, it targets M8 firmware version 4.0 and is no longer actively maintained. Since the M8 is now at firmware 6.2+, do not use m8-js for offset information as binary structure layouts change between versions. The enum data (command codes, parameter ranges, mode values) remains reliable and stable.
-
m8-files - Original Rust parser that inspired m8-file-parser
- Dirtywave M8 Tracker - Official M8 hardware
- M8 Headless - Official headless M8 firmware
pym8 targets M8 firmware version 6.2+ by default. Template files are included for different firmware versions:
TEMPLATE-4-0-2.m8s- Firmware 4.0.2TEMPLATE-5-0-1.m8s- Firmware 5.0.1TEMPLATE-6-2-1.m8s- Firmware 6.2.1 (current)
Key differences across versions:
- V2: Initial format (23 sequence commands)
- V3: Added RND, RNL, RMX, PBN, TBX, OFF commands
- V4: Expanded instrument commands
- V6.2: Latest format with additional mixer commands (XRH, XMT, OTT, OTC, OTI)
All M8 data uses:
- Little-endian byte order for multi-byte values
- Unsigned 8-bit integers (0x00-0xFF) for most parameters
- Null-terminated strings for text fields
- Float32 for tempo (4 bytes, little-endian)
python run_tests.pyThe project uses pyproject.toml for configuration (PEP 518) with setuptools as the build backend.
MIT License
Created by jhw ([email protected])
Special thanks to:
- Twinside for the comprehensive m8-file-parser Rust library
- whitlockjc for m8-js
- AlexCharlton for m8-files
- Dirtywave for creating the M8 Tracker
Contributions are welcome! When adding new features:
- Verify against m8-file-parser for accuracy
- Include practical examples in code
- Add tests for new functionality
- Document any firmware version-specific features
- Issues: GitHub Issues
- Examples: See
demos/directory - Tests: Check
tests/directory for API usage examples