README
NAME
OpenMPT::Bindings ā Raku bindings for libopenmpt (module music rendering)
SYNOPSIS
use OpenMPT::Bindings::Module;
use OpenMPT::Bindings::Types;
my $mod = OpenMPT::Bindings::Module.from-file('song.xm');
# Metadata
say $mod.title;
say $mod.duration;
# Render S16_LE interleaved stereo (ready-to-write bytes)
my ($frames, $blob) = $mod.read-interleaved-stereo(48000, 1024);
# Float stereo (separate Blob channels)
my ($count, $left, $right) = $mod.read-float-stereo(44100, 512);
# Seek
$mod.set-position(10.5);
# Library class methods
say OpenMPT::Bindings::Module.library-version;
say OpenMPT::Bindings::Module.is-extension-supported('xm');DESCRIPTION
OpenMPT::Bindings provides low-level and high-level Raku bindings for libopenmpt, a library for decoding tracked music files (MOD, XM, S3M, IT, and many more).
The bindings wrap the C API ā all ~90 exported functions are available. The
OpenMPT::Bindings::Module class wraps the opaque openmpt_module* handle
and provides idiomatic Raku access to audio rendering, metadata, playback
control, CTL parameters, and file probing. OpenMPT::Bindings::ModuleExt
adds the libopenmpt extension API (interactive playback, pattern
visualization).
REQUIREMENTS
libopenmpt (shared library, version 0.7+)
Raku (6.d or later)
Some features (current-sequence) require libopenmpt 0.9+ and gracefully
return -1 on older versions.
The library is not bundled ā you must install libopenmpt via your
system package manager:
# Debian/Ubuntu
apt install libopenmpt0t64
# Fedora
dnf install libopenmpt
# Arch
pacman -S libopenmpt
# macOS (Homebrew)
brew install libopenmpt
# Windows
Download the libopenmpt DLL bundle from
https://lib.openmpt.org/libopenmpt/download/ and place these files in
a directory on your PATH (e.g. C:\Windows\System32):
- libopenmpt.dll
- openmpt-mpg123.dll
- openmpt-ogg.dll
- openmpt-vorbis.dll
- openmpt-zlib.dll
If you place the DLLs in a custom directory, set the LIBOPENMPT_DIR
environment variable to that path before loading any bindings. This
also works on Linux/macOS for AppImage, .app bundles, etc.INSTALLATION
zef install OpenMPT::BindingsUSAGE
Loading modules
# From file
my $xm = OpenMPT::Bindings::Module.from-file('track.xm');
# From memory (Blob)
my $data = 'track.it'.IO.slurp(:bin);
my $it = OpenMPT::Bindings::Module.from-blob($data);Construction with initial CTL parameters
my $mod = OpenMPT::Bindings::Module.from-file('track.xm',
ctls => { 'play.tempo_factor' => 2.0 });Metadata
say $xm.title;
say $xm.artist;
say $xm.type; # "xm", "mod", "s3m", "it"
say $xm.type-long; # "FastTracker 2 v1.04"
say $xm.module-message; # song message / comments
say $xm.tracker; # tracker that created the file
say $xm.date; # creation date
say $xm.originaltype;
# All available metadata keys
.say for $xm.metadata-keys;Audio rendering
All render methods return Blob buffers of raw bytes. Every call returns
($frames-returned, $buf...).
# Float stereo (separate buffers)
my ($frames, $left, $right) = $xm.read-float-stereo(44100, 1024);
# Float interleaved stereo
my ($frames, $buf) = $xm.read-interleaved-float-stereo(48000, 512);
# Float mono
my ($frames, $buf) = $xm.read-float-mono(44100, 256);
# Float quad (4-channel surround)
my ($frames, $fl, $fr, $rl, $rr) = $xm.read-float-quad(44100, 512);
# Float interleaved quad
my ($frames, $buf) = $xm.read-interleaved-float-quad(44100, 512);
# Int16 stereo
my ($frames, $left, $right) = $xm.read-stereo(44100, 1024);
# Int16 interleaved stereo
my ($frames, $buf) = $xm.read-interleaved-stereo(44100, 512);
# Int16 mono
my ($frames, $buf) = $xm.read-mono(44100, 256);
# Low-overhead rendering (avoid Blob copies)
my ($frames, $p, $bytes) = $xm.read-interleaved-stereo-raw(48000, 1024);
# Int16 quad
my ($frames, $fl, $fr, $rl, $rr) = $xm.read-quad(44100, 512);
# Int16 interleaved quad
my ($frames, $buf) = $xm.read-interleaved-quad(44100, 512);Low-overhead rendering (avoid Blob copies)
All methods above allocate a new Blob on every call. For
performance-sensitive hot paths (real-time playback), use the raw
variant instead. It returns a Pointer[void] into an internal
pre-allocated buffer ā no allocation, no memcpy per call:
my ($frames, $p, $bytes) = $xm.read-interleaved-stereo-raw(48000, 1024);The pointer is valid only until the next render call on the same module. Write it directly to a file descriptor or audio API without an intermediate Blob:
snd_pcm_writei($pcm, nativecast(Pointer[void], $p), $got);Available raw variant:
| Method | Returns | Buffer lifetime |
|---|---|---|
read-interleaved-stereo-raw | ($count, Pointer[void], $bytes) | Until next render on this module |
read-interleaved-float-stereo-raw | ($count, Pointer[void], $bytes) | Until next render on this module |
Playback control
$xm.select-subsong(0);
$xm.set-repeat-count(-1); # -1 = forever, 0 = once, 1 = twice
$xm.set-position(15.5); # seek to 15.5 seconds (Numeric)
$xm.set-position-order-row(3, 0); # seek to order 3, row 0Position and duration
say $xm.duration;
say $xm.position;
say $xm.time-at-position(0, 0);Render parameters
$xm.set-mastergain(0); # millibel
$xm.set-stereo-separation(100); # percent
say $xm.interpolation-filter-length;
say $xm.volume-ramping-strength;Playback status
say $xm.current-bpm;
say $xm.current-speed;
say $xm.current-tempo;
say $xm.current-order;
say $xm.current-pattern;
say $xm.current-row;
say $xm.current-playing-channels;
say $xm.current-sequence; # 0.9+; returns -1 on older libopenmpt
# Per-channel VU meters
say $xm.current-channel-vu-mono(0);
say $xm.current-channel-vu-left(0);
say $xm.current-channel-vu-right(0);
say $xm.current-channel-vu-rear-left(2);
say $xm.current-channel-vu-rear-right(2);Module information
say $xm.num-subsongs;
say $xm.num-channels;
say $xm.num-orders;
say $xm.num-patterns;
say $xm.num-instruments;
say $xm.num-samples;
say $xm.restart-order(0);
say $xm.restart-row(0);
# Names
say $xm.subsong-name(0);
say $xm.channel-name(0);
say $xm.order-name(0);
say $xm.pattern-name(0);
say $xm.instrument-name(0);
say $xm.sample-name(0);Pattern / order access
say $xm.order-pattern(2); # pattern index at order 2
say $xm.is-order-skip(3); # Bool ā skip entry?
say $xm.is-order-stop(3); # Bool ā stop entry?
say $xm.is-pattern-skip(1); # Bool ā skip item?
say $xm.is-pattern-stop(1); # Bool ā stop item?
say $xm.pattern-num-rows(0); # row count
say $xm.pattern-rows-per-beat(0);
say $xm.pattern-rows-per-measure(0);
say $xm.get-pattern-cell(0, 0, 0, OPENMPT_MODULE_COMMAND_NOTE);
say $xm.format-pattern-cell(0, 0, 0, OPENMPT_MODULE_COMMAND_NOTE);
say $xm.highlight-pattern-cell(0, 0, 0, OPENMPT_MODULE_COMMAND_NOTE);CTL parameters
# Get (type dispatch via :type parameter)
say $xm.get-ctl('dither'); # Str (text)
say $xm.get-ctl('play.tempo_factor', :type(Numeric)); # Num
say $xm.get-ctl('dither', :type(Int)); # Int
say $xm.get-ctl('dither', :type(Bool)); # Bool
# Set (auto-dispatch via Raku type)
$xm.set-ctl('play.tempo_factor', 2.0); # Numeric ā set_floatingpoint
$xm.set-ctl('dither', 0); # Int ā set_integer
$xm.set-ctl('dither', False); # Bool ā set_boolean
$xm.set-ctl('load.skip_plugin', '1'); # Str ā set_text
# List all supported CTL keys
.say for $xm.ctl-keys;Library class methods
say OpenMPT::Bindings::Module.library-version; # uint32
say OpenMPT::Bindings::Module.core-version; # uint32
say OpenMPT::Bindings::Module.library-string('url'); # Str
my @exts = OpenMPT::Bindings::Module.supported-extensions;
say OpenMPT::Bindings::Module.is-extension-supported('xm'); # BoolFile probing
my $data = 'unknown.bin'.IO.slurp(:bin);
my $result = OpenMPT::Bindings::Module.probe-file-header($data, $data.bytes);
# Compare with enum constants
if $result == OPENMPT_PROBE_FILE_HEADER_RESULT_SUCCESS {
say "Recognised!";
}
# Probe without knowing the file size
my $partial = OpenMPT::Bindings::Module.probe-file-header-without-filesize(
$data.subbuf(0, 64));Extension API (ModuleExt)
The ModuleExt class wraps openmpt_module_ext* and provides interactive
playback and pattern visualization interfaces.
use OpenMPT::Bindings::ModuleExt;
my $ext = OpenMPT::Bindings::ModuleExt.from-file('track.xm');
say $ext.module.title; # access the underlying Module
# Interactive interface
$ext.set-current-speed(2); # 2x speed
$ext.set-tempo-factor(1.5);
$ext.set-pitch-factor(0.8);
$ext.set-global-volume(0.75);
$ext.set-channel-volume(0, 0.5);
$ext.set-channel-mute(1, True);
$ext.play-note(0, 60, 0.8, 0.5); # instrument, note, volume, panning
$ext.stop-note(0);
# Interactive2 interface
$ext.note-off(0);
$ext.note-fade(0);
$ext.set-channel-panning(0, 0.3);
$ext.get-channel-panning(0);
$ext.set-note-finetune(0, 0.05);
# Interactive3 interface
$ext.set-current-tempo2(180);
# Pattern visualization
$ext.get-pattern-row-channel-volume-effect-type(0, 0, 0);
$ext.get-pattern-row-channel-effect-type(0, 0, 0);Error handling
use X::OpenMPT::Bindings;
try {
OpenMPT::Bindings::Module.from-blob(Buf.new(0xFF, 0xFE).Blob);
CATCH {
when X::OpenMPT::Bindings {
say .function; # "openmpt_module_create_from_memory2"
say .code; # error code (int32)
say .message; # human-readable error string
}
}
}EXAMPLES
Two example scripts are included:
Real-time playback
raku -I lib examples/play-module.raku track.xmAudio backends (select via --backend):
pulseā Linux (default) ā libpulse-simple FFI (low-latency)alsaā Linux ā native ALSA FFI viasnd_pcm_writeiwaveoutā Windows ā winmm FFI
The pulse backend uses pa_simple_write which blocks at the server
buffer boundary, providing jitter-free playback without timing hacks.
If PulseAudio is unavailable it falls back to alsa.
File conversion
raku -I lib examples/convert-module.raku track.xmOutputs a 16-bit stereo WAV file named track.wav by default.
Supported formats (select via --format):
wavā native (RIFF header + raw PCM)mp3ā pipes toffmpeg(libmp3lame)flacā pipes toffmpeg(flac)oggā pipes toffmpeg(libvorbis)opusā pipes toffmpeg(libopus)
Common options:
# Cap duration and pick sample rate
raku -I lib examples/convert-module.raku --seconds=30 --rate=44100 track.it
# Batch-convert with quiet progress
raku -I lib examples/convert-module.raku --format=flac --output-dir=/tmp *.xm --quiet
# Explicit output path, subsong selection, gain/tempo
# --gain takes decibels (e.g. -2.0 = -2 dB, internally converted to millibel)
raku -I lib examples/convert-module.raku --subsong=1 --gain=-2 --tempo=1.5 --output=out.wav track.xmThe --force flag applies to all output formats: without it, the tool
refuses to overwrite an existing file. ffmpeg-based formats would silently
overwrite on their own; the Raku-side guard catches them early instead.
Compressed formats (mp3, flac, ogg, opus) require ffmpeg on PATH.
SEE ALSO
AUTHOR
Sasha Abbott [email protected]
COPYRIGHT AND LICENSE
This software is dedicated to the public domain under the CC0 1.0 Universal (CC0 1.0) Public Domain Dedication.
To the extent possible under law, the author has waived all copyright and related or neighboring rights to this work.