Skip to main content

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

Project description

NodMOD

CI Ruff Release API Stability Python License Last Commit Formats

NodMOD is a Python library for reading, editing, and writing tracker modules.

Current stable release: v1.0.4. Current tested Python support: 3.11, 3.12.

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

git clone https://github.com/erodola/nodmod.git
cd nodmod
uv venv
uv pip install -e .

Verify with:

source .venv/bin/activate  # macOS/Linux
# OR
.venv\Scripts\activate     # Windows

python -c "import nodmod; print('nodmod installed successfully!')"

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.11 or 3.12 (current tested support)
  • Python 3.13+ compatibility is tracked in #30
  • pydub 0.25.1+
  • 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.4.tar.gz (109.0 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.4-py3-none-any.whl (75.8 kB view details)

Uploaded Python 3

File details

Details for the file nodmod-1.0.4.tar.gz.

File metadata

  • Download URL: nodmod-1.0.4.tar.gz
  • Upload date:
  • Size: 109.0 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.4.tar.gz
Algorithm Hash digest
SHA256 90cb75d0331ecc5922ae148827758e0e0830ea801e340bb11fe5176adffd206a
MD5 4cd57b6fe2065bd37e07fb6c2249231d
BLAKE2b-256 39bd59e79ae4615b4c382b5bb2c4bcb972e4304d0f38e2dd02fce3899a989d62

See more details on using hashes here.

Provenance

The following attestation bundles were made for nodmod-1.0.4.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.4-py3-none-any.whl.

File metadata

  • Download URL: nodmod-1.0.4-py3-none-any.whl
  • Upload date:
  • Size: 75.8 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.4-py3-none-any.whl
Algorithm Hash digest
SHA256 f4f6ee3d4b49d68aa46ec05c1f54c3637d4c162a66591924954e9a00472d6449
MD5 537ba0460095c4c8be3b6a33cd3d7719
BLAKE2b-256 a6ec06f34f5836bb8a7b76fd78bc4c47b20e4ae45b3fbbd772f36c1d8f505522

See more details on using hashes here.

Provenance

The following attestation bundles were made for nodmod-1.0.4-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