Read, edit, and write tracker modules (MOD, XM, S3M).
Project description
NodMOD
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.
What It Does
- 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
Installation
From PyPI (recommended):
uv add nodmod
or:
pip install nodmod
Verify 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 .
Quick Start
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")
Core Model
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.
Format Notes
- 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.
Extras
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)
New API Additions
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.
Requirements
- 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
Project Status
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.
Contributing
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
Reference Material
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.
License
MIT License. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nodmod-1.0.5.tar.gz.
File metadata
- Download URL: nodmod-1.0.5.tar.gz
- Upload date:
- Size: 111.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
da7a57c168c1a3afccf78fbe9411f897ef3addb07f02397814642b613ae29097
|
|
| MD5 |
1f3f50d744c1561d51b7081fc8b5cf38
|
|
| BLAKE2b-256 |
913041edf4882dc2b1851cbca9bb8752459dc50246a1fec4661431d6f3130c52
|
Provenance
The following attestation bundles were made for nodmod-1.0.5.tar.gz:
Publisher:
publish.yml on erodola/nodmod
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nodmod-1.0.5.tar.gz -
Subject digest:
da7a57c168c1a3afccf78fbe9411f897ef3addb07f02397814642b613ae29097 - Sigstore transparency entry: 1279427107
- Sigstore integration time:
-
Permalink:
erodola/nodmod@107a5091b0341192124a437da348269db11aead0 -
Branch / Tag:
refs/tags/v1.0.5 - Owner: https://github.com/erodola
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@107a5091b0341192124a437da348269db11aead0 -
Trigger Event:
release
-
Statement type:
File details
Details for the file nodmod-1.0.5-py3-none-any.whl.
File metadata
- Download URL: nodmod-1.0.5-py3-none-any.whl
- Upload date:
- Size: 76.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6a28dfd8982e726da813a3df81fc01b18ee87bbb839746fc646b5eff320cf8ae
|
|
| MD5 |
b0261e2560065bbda415bf94bae3a721
|
|
| BLAKE2b-256 |
d976ad6157d83f65dceb6bd38c573b0b8c56572e19210c5f673e414861add8b6
|
Provenance
The following attestation bundles were made for nodmod-1.0.5-py3-none-any.whl:
Publisher:
publish.yml on erodola/nodmod
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nodmod-1.0.5-py3-none-any.whl -
Subject digest:
6a28dfd8982e726da813a3df81fc01b18ee87bbb839746fc646b5eff320cf8ae - Sigstore transparency entry: 1279427196
- Sigstore integration time:
-
Permalink:
erodola/nodmod@107a5091b0341192124a437da348269db11aead0 -
Branch / Tag:
refs/tags/v1.0.5 - Owner: https://github.com/erodola
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@107a5091b0341192124a437da348269db11aead0 -
Trigger Event:
release
-
Statement type: