Skip to content

erodola/nodmod

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

200 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NodMOD

CI Ruff Release Python versions License Last Commit Formats

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:

  • MODSong for MOD modules
  • XMSong for XM modules
  • S3MSong for S3M modules
  • Pattern for pattern data
  • Note, XMNote, and S3MNote for note cells
  • Sample, XMSample, and S3MSample for waveform data
  • Instrument for 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 sample 00 inherit 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) and save(..., validate_samples=True) for strict pre-save validation.
  • Use get_note_raw(...) (and resolved=False where 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-lts is pulled automatically on Python 3.13+
  • Optional: openmpt123 or ffmpeg for 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.

About

Python library to read, edit, and write tracker modules (MOD, XM, S3M).

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages