NodMOD is a Python library for reading, editing, and writing tracker modules.
It currently focuses on three classic formats:
- MOD
- XM
- S3M
The project is built around direct programmatic editing. You load or create a song, manipulate patterns, notes, samples, instruments, and effects in Python, then save the result back to disk.
- Load MOD, XM, and S3M files
- Save edited or newly created MOD, XM, and S3M files
- Create, duplicate, resize, clear, and reorder patterns
- Edit notes, effects, rows, channels, samples, and XM instruments
- Import WAV audio into MOD samples or XM instrument samples
- Export human-readable ASCII dumps for inspection
- Render modules to WAV through external tools when available
- Validate/sanitize MOD sample-loop metadata before strict saves
From PyPI (recommended):
uv add nodmodor:
pip install nodmodVerify with:
source .venv/bin/activate # macOS/Linux
# OR
.venv\Scripts\activate # Windows
python -c "import nodmod; print('nodmod installed successfully!')"For development from source:
git clone https://github.com/erodola/nodmod.git
cd nodmod
uv venv
uv pip install -e .from nodmod import MODSong
song = MODSong()
song.load("music/spice_it_up.mod")
# MOD channels are 0-based (0-3)
song.mute_channel(2)
song.mute_channel(3)
song.save("music/ch1_2.mod")from nodmod import XMSong
from nodmod.types import XMSample
song = XMSong()
song.set_n_channels(4)
inst = song.new_instrument("Lead")
smp = XMSample()
import array
smp.waveform = array.array('b', [0, 10, -10, 0])
song.add_sample(inst, smp)
song.set_sample_map_all(inst, 1)
song.set_note(0, 0, 0, inst, "C-4", "")
song.set_sample_panning(inst, 1, 192)
song.set_global_volume(0, 0, 0, 64)
song.save("music/lead.xm")from nodmod import S3MSong
song = S3MSong()
song.set_n_channels(8)
song.set_note(0, 0, 0, 1, "C-4", effect="A03", volume=32)
song.set_note(0, 1, 4, 2, "G-4", effect="T96", volume=40)
song.save("music/sketch.s3m")At a high level, the library exposes a small set of central objects:
MODSongfor MOD modulesXMSongfor XM modulesS3MSongfor S3M modulesPatternfor pattern dataNote,XMNote, andS3MNotefor note cellsSample,XMSample, andS3MSamplefor waveform dataInstrumentfor XM instruments and sample maps
The API is intentionally close to tracker structure rather than trying to hide it behind a higher-level composition DSL.
- MOD notes reference samples directly.
- MOD read APIs (
get_note,iter_cells,iter_rows,get_used_samples) resolve sample-memory semantics by default: note rows with sample00inherit the last latched sample in the same channel. - In MOD, sample-only rows (sample set with empty note) are valid and update that channel's latched sample for later note rows.
- MOD sample slots expose loop safety helpers (
Sample.validate_loop,Sample.sanitize_loop,MODSong.validate_samples,MODSong.sanitize_samples) andsave(..., validate_samples=True)for strict pre-save validation. - Use
get_note_raw(...)(andresolved=Falsewhere available) when you need exact raw MOD cell sample nibbles. - XM notes reference instruments, and instruments contain samples.
- S3M notes reference sample / instrument slots directly for PCM modules.
- MOD sample slots are 1-based in the public API.
- XM instrument indices and XM sample indices are 1-based in the public API.
- S3M sample slots are 1-based in the public API.
- Pattern order and pattern storage are separate concepts, as they are in tracker files.
Current S3M scope:
- PCM S3M modules are supported for load, edit, save, and round-trip tests.
- Adlib / OPL S3M instruments are detected and rejected explicitly; they are not supported yet.
For most day-to-day use, the practical rule is simple: sequence positions, rows, and channels behave like normal Python indices, while tracker sample and instrument slots follow tracker conventions.
ASCII dumps are available for both formats:
song.save_ascii("music/debug.txt")Rendering can target mono or multi-channel output when openmpt123 or a compatible ffmpeg build is available:
song.render("music/render.wav", channels=2)Recent enhancements add inspection-focused, additive APIs without breaking existing method signatures.
The v1.0.3 release hardens MOD/XM/S3M edge-case behavior, including fixed MOD channel invariants, XM serialization correctness, and timing/documentation consistency improvements.
from nodmod import MODSong
song = MODSong()
song.set_restart_position(3) # normalized view
raw = song.get_restart_position(raw=True) # exact MOD header byte# Canonical coordinate order: (sequence_idx, row, channel, ...)
song.set_note_rc(0, 8, 1, 4, "C-4", "F06")
song.set_effect_rc(0, 8, 1, "B01")
cell = song.get_note_rc(0, 8, 1)from nodmod import decode_mod_effect, encode_mod_effect
info = decode_mod_effect("E6F")
assert info.extended_cmd == "E6"
assert encode_mod_effect("F", 125) == "F7D"# Immutable snapshots for read-only analysis
summary = song.view()
cells = list(song.iter_cells(sequence_only=True))
rows = list(song.iter_rows(sequence_only=True))
effects = list(song.iter_effects(sequence_only=True, include_empty=False))
samples = list(song.iter_samples(include_empty=False))# MOD raw signed 8-bit PCM helpers
pcm = song.get_sample_pcm_i8(1)
song.set_sample_pcm_i8(1, pcm, reset_meta=False)
song.set_sample_loop_bytes(1, start_byte=128, length_byte=256)from nodmod import probe_file
probe = probe_file("music/demo.mod")
print(probe.detected_format, probe.supported, probe.metadata)# In-memory ASCII dump (no temp files needed)
text = song.to_ascii(sequence_only=True, include_headers=False)# Playback-aware row timeline with source coordinates
playback = list(song.iter_playback_rows(max_steps=250_000))
first = playback[0]
print(first.sequence_idx, first.pattern_idx, first.row, first.start_sec, first.end_sec)# Reachability-aware used-resource scans
mod_used = song.get_used_samples(scope="reachable", order="first_use")
xm_insts = xm_song.get_used_instruments(scope="sequence", order="sorted")
xm_samples = xm_song.get_used_samples(scope="reachable", order="sorted")
s3m_used = s3m_song.get_used_samples(scope="reachable", order="sorted")# One-call loading with format dispatch
from nodmod import load_song
song = load_song("music/demo.mod")Scope semantics:
scope="sequence"inspects every row in sequence-referenced patterns.scope="reachable"inspects rows actually visited during playback flow.
- Python 3.10, 3.11, 3.12, 3.13, or 3.14
- pydub 0.25.1+
audioop-ltsis pulled automatically on Python 3.13+- Optional:
openmpt123orffmpegfor WAV rendering
This is an editing-focused library, not yet a full tracker toolkit. The codebase is strongest in direct manipulation of existing songs and in generating small-to-medium scripted edits. The public API is still evolving.
Useful contributions include:
- bug fixes and behavioral cleanup
- stronger round-trip coverage for MOD, XM, and S3M files
- better public examples
- support for more tracker operations and formats
Large collections of legal-to-study modules can be found at The Mod Archive and Amiga Music Preservation.
Good players and editors for checking output include XMPlay, Qmmp, and MilkyTracker.
MIT License. See LICENSE.