Skip to main content

Read, edit, and write tracker modules (MOD, XM, S3M).

Project description

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nodmod-1.0.5.tar.gz (111.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nodmod-1.0.5-py3-none-any.whl (76.3 kB view details)

Uploaded Python 3

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

Hashes for nodmod-1.0.5.tar.gz
Algorithm Hash digest
SHA256 da7a57c168c1a3afccf78fbe9411f897ef3addb07f02397814642b613ae29097
MD5 1f3f50d744c1561d51b7081fc8b5cf38
BLAKE2b-256 913041edf4882dc2b1851cbca9bb8752459dc50246a1fec4661431d6f3130c52

See more details on using hashes here.

Provenance

The following attestation bundles were made for nodmod-1.0.5.tar.gz:

Publisher: publish.yml on erodola/nodmod

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for nodmod-1.0.5-py3-none-any.whl
Algorithm Hash digest
SHA256 6a28dfd8982e726da813a3df81fc01b18ee87bbb839746fc646b5eff320cf8ae
MD5 b0261e2560065bbda415bf94bae3a721
BLAKE2b-256 d976ad6157d83f65dceb6bd38c573b0b8c56572e19210c5f673e414861add8b6

See more details on using hashes here.

Provenance

The following attestation bundles were made for nodmod-1.0.5-py3-none-any.whl:

Publisher: publish.yml on erodola/nodmod

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page