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::Bindings

USAGE

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:

MethodReturnsBuffer 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 0

Position 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');  # Bool

File 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.xm

Audio backends (select via --backend):

  • pulse — Linux (default) — libpulse-simple FFI (low-latency)

  • alsa — Linux — native ALSA FFI via snd_pcm_writei

  • waveout — 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.xm

Outputs a 16-bit stereo WAV file named track.wav by default. Supported formats (select via --format):

  • wav — native (RIFF header + raw PCM)

  • mp3 — pipes to ffmpeg (libmp3lame)

  • flac — pipes to ffmpeg (flac)

  • ogg — pipes to ffmpeg (libvorbis)

  • opus — pipes to ffmpeg (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.xm

The --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.

OpenMPT::Bindings v0.0.3

Raku bindings for libopenmpt (module music rendering)

Authors

  • Sasha Abbott

License

CC0-1.0

Dependencies

Test Dependencies

Provides

  • OpenMPT::Bindings
  • OpenMPT::Bindings::Exception
  • OpenMPT::Bindings::Module
  • OpenMPT::Bindings::ModuleExt
  • OpenMPT::Bindings::Native
  • OpenMPT::Bindings::NativeLib
  • OpenMPT::Bindings::Types
  • OpenMPT::Bindings::Util

Documentation

The Camelia image is copyright 2009 by Larry Wall. "Raku" is trademark of the Yet Another Society. All rights reserved.