NAME
====

OpenMPT::Bindings — Raku bindings for libopenmpt (module music rendering)

SYNOPSIS
========

```raku
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](https://lib.openmpt.org/), 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
---------------

```raku
# 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
-----------------------------------------

```raku
my $mod = OpenMPT::Bindings::Module.from-file('track.xm',
    ctls => { 'play.tempo_factor' => 2.0 });
```

Metadata
--------

```raku
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...)`.

```raku
# 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:

```raku
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:

```raku
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
----------------

```raku
$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
---------------------

```raku
say $xm.duration;
say $xm.position;
say $xm.time-at-position(0, 0);
```

Render parameters
-----------------

```raku
$xm.set-mastergain(0);              # millibel
$xm.set-stereo-separation(100);     # percent
say $xm.interpolation-filter-length;
say $xm.volume-ramping-strength;
```

Playback status
---------------

```raku
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
------------------

```raku
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
----------------------

```raku
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
--------------

```raku
# 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
---------------------

```raku
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
------------

```raku
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.

```raku
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
--------------

```raku
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
========

- [libopenmpt](https://lib.openmpt.org/)
- [OpenMPT](https://openmpt.org/)

# AUTHOR

Sasha Abbott <sashaa@disroot.org>

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.
