Source code for simvx.core._native.miniaudio_engine_build

"""CFFI build script for the SimVX miniaudio engine extension.

Run once after install (or any time miniaudio.h is updated):

    uv run python -m simvx.core._native.miniaudio_engine_build

Produces `_simvx_miniaudio_engine.<abi>.so` next to this file. The runtime
wrapper `simvx.core._native.miniaudio_engine` imports it.

`uv_build` (SimVX's build backend) doesn't run C-extension build hooks at
install time, so this script is invoked manually or via the `simvx
build-audio` CLI subcommand. Both `simvx-core` and CI run the same script.
"""

from __future__ import annotations

import importlib
import os
import sys
from pathlib import Path

HERE = Path(__file__).resolve().parent
GLUE_SOURCE = HERE / "_miniaudio_engine_glue.c"
VENDOR_INCLUDE = (HERE.parents[3] / "vendor" / "miniaudio").resolve()


def _require_build_deps():
    """Validate that cffi and setuptools are available before invoking cffi.

    Called from ``build()`` only: never at import time so this module can
    be imported by docs tooling (Sphinx autodoc, etc.) without the build
    dependencies installed.
    """
    try:
        cffi = importlib.import_module("cffi")
    except ImportError as exc:
        raise SystemExit(
            "cffi is required to build the miniaudio engine extension. "
            "Install with `uv pip install cffi`."
        ) from exc

    # cffi 2.x on Python 3.12+ requires setuptools at compile time. Check
    # up front so we fail fast with an actionable message instead of deep
    # in cffi.
    try:
        importlib.import_module("setuptools")
    except ImportError as exc:
        raise SystemExit(
            "setuptools is required to build the miniaudio engine extension on "
            "Python >= 3.12 (cffi >= 2.0). Install with `uv pip install setuptools` "
            "or `uv run --with setuptools simvx build-audio`."
        ) from exc

    return cffi


def _cdef() -> str:
    """C declarations exposed to Python via cffi.

    Opaque struct types (`ma_engine`, `ma_sound`, `ma_sound_group`,
    `ma_audio_buffer`, `ma_data_source`): Python only ever holds
    pointers, never reads fields. The glue C provides allocator/init
    helpers and a thin facade over miniaudio's API.
    """
    return """
    typedef struct ma_engine ma_engine;
    typedef struct ma_sound ma_sound;
    typedef struct ma_sound_group ma_sound_group;
    typedef struct ma_audio_buffer ma_audio_buffer;
    typedef struct ma_data_source ma_data_source;

    /* engine */
    ma_engine* simvx_engine_alloc(void);
    void simvx_engine_free(ma_engine* engine);
    int simvx_engine_init_default(ma_engine* engine, unsigned int sample_rate, unsigned int channels);
    int simvx_engine_init_no_device(ma_engine* engine, unsigned int sample_rate, unsigned int channels);
    void simvx_engine_uninit(ma_engine* engine);
    unsigned int simvx_engine_get_sample_rate(ma_engine* engine);
    unsigned int simvx_engine_get_channels(ma_engine* engine);
    int simvx_engine_set_volume(ma_engine* engine, float volume);
    int simvx_engine_read_pcm_frames_f32(ma_engine* engine, float* out,
                                          unsigned long long frame_count,
                                          unsigned long long* frames_read);

    /* listener */
    void simvx_listener_set_position(ma_engine* engine, unsigned int idx, float x, float y, float z);
    void simvx_listener_set_velocity(ma_engine* engine, unsigned int idx, float x, float y, float z);
    void simvx_listener_set_direction(ma_engine* engine, unsigned int idx, float x, float y, float z);
    void simvx_listener_set_world_up(ma_engine* engine, unsigned int idx, float x, float y, float z);

    /* sound */
    ma_sound* simvx_sound_alloc(void);
    void simvx_sound_free(ma_sound* sound);
    int simvx_sound_init_from_file(ma_engine* engine, ma_sound* sound,
                                    const char* path, unsigned int flags,
                                    ma_sound_group* group);
    int simvx_sound_init_from_data_source(ma_engine* engine, ma_sound* sound,
                                           ma_data_source* data_source,
                                           unsigned int flags,
                                           ma_sound_group* group);
    void simvx_sound_uninit(ma_sound* sound);
    int simvx_sound_start(ma_sound* sound);
    int simvx_sound_stop(ma_sound* sound);
    void simvx_sound_set_volume(ma_sound* sound, float volume);
    void simvx_sound_set_pan(ma_sound* sound, float pan);
    void simvx_sound_set_pitch(ma_sound* sound, float pitch);
    void simvx_sound_set_looping(ma_sound* sound, int looping);
    void simvx_sound_set_position(ma_sound* sound, float x, float y, float z);
    void simvx_sound_set_velocity(ma_sound* sound, float x, float y, float z);
    void simvx_sound_set_direction(ma_sound* sound, float x, float y, float z);
    void simvx_sound_set_spatialization_enabled(ma_sound* sound, int enabled);
    void simvx_sound_set_min_distance(ma_sound* sound, float distance);
    void simvx_sound_set_max_distance(ma_sound* sound, float distance);
    void simvx_sound_set_rolloff(ma_sound* sound, float rolloff);
    void simvx_sound_set_doppler_factor(ma_sound* sound, float factor);
    int simvx_sound_is_playing(ma_sound* sound);
    int simvx_sound_at_end(ma_sound* sound);
    int simvx_sound_seek_to_pcm_frame(ma_sound* sound, unsigned long long frame);
    int simvx_sound_get_cursor_pcm_frames(ma_sound* sound, unsigned long long* out_frame);
    int simvx_sound_get_length_pcm_frames(ma_sound* sound, unsigned long long* out_frames);

    /* group */
    ma_sound_group* simvx_sound_group_alloc(void);
    void simvx_sound_group_free(ma_sound_group* group);
    int simvx_sound_group_init(ma_engine* engine, ma_sound_group* group,
                                unsigned int flags, ma_sound_group* parent);
    void simvx_sound_group_uninit(ma_sound_group* group);
    void simvx_sound_group_set_volume(ma_sound_group* group, float volume);

    /* audio buffer (ndarray sources) */
    ma_audio_buffer* simvx_audio_buffer_alloc(void);
    void simvx_audio_buffer_free(ma_audio_buffer* buffer);
    int simvx_audio_buffer_init(ma_audio_buffer* buffer,
                                 unsigned int format, unsigned int channels,
                                 unsigned long long frame_count,
                                 const void* data, unsigned int sample_rate);
    void simvx_audio_buffer_uninit(ma_audio_buffer* buffer);
    ma_data_source* simvx_audio_buffer_as_data_source(ma_audio_buffer* buffer);

    /* format constants (resolved at compile time) */
    unsigned int simvx_format_f32(void);
    unsigned int simvx_format_s16(void);
    unsigned int simvx_format_s32(void);
    unsigned int simvx_format_u8(void);

    /* sound init flags */
    unsigned int simvx_flag_decode(void);
    unsigned int simvx_flag_async(void);
    unsigned int simvx_flag_no_spatialization(void);
    unsigned int simvx_flag_no_pitch(void);
    unsigned int simvx_flag_stream(void);

    /* PCM ring buffer (native streaming) */
    typedef struct ma_pcm_rb ma_pcm_rb;
    ma_pcm_rb* simvx_pcm_rb_alloc(void);
    void simvx_pcm_rb_free(ma_pcm_rb* rb);
    int simvx_pcm_rb_init(ma_pcm_rb* rb, unsigned int format, unsigned int channels,
                           unsigned int buffer_size_frames, unsigned int sample_rate);
    void simvx_pcm_rb_uninit(ma_pcm_rb* rb);
    ma_data_source* simvx_pcm_rb_as_data_source(ma_pcm_rb* rb);
    int simvx_pcm_rb_write(ma_pcm_rb* rb, const void* data,
                            unsigned int frame_count, unsigned int* frames_written);
    unsigned int simvx_pcm_rb_available_write(ma_pcm_rb* rb);
    unsigned int simvx_pcm_rb_available_read(ma_pcm_rb* rb);
    void simvx_pcm_rb_reset(ma_pcm_rb* rb);

    /* effect graph */
    typedef struct ma_notch_node ma_notch_node;
    typedef struct ma_peak_node ma_peak_node;
    typedef struct ma_loshelf_node ma_loshelf_node;
    typedef struct ma_hishelf_node ma_hishelf_node;
    typedef struct ma_delay_node ma_delay_node;

    void* simvx_engine_endpoint(ma_engine* engine);
    void* simvx_sound_group_as_node(ma_sound_group* group);
    int simvx_node_attach(void* src, unsigned int src_bus, void* dst, unsigned int dst_bus);
    int simvx_node_detach(void* src, unsigned int src_bus);
    int simvx_node_set_output_volume(void* node, unsigned int bus, float volume);

    /* Biquad LPF/HPF/BPF (custom nodes around ma_lpf2/hpf2/bpf2: Q-aware,
     * unlike the stock ma_*_node which hardcodes Q=0.707). */
    typedef struct simvx_biquad_lpf_node simvx_biquad_lpf_node;
    typedef struct simvx_biquad_hpf_node simvx_biquad_hpf_node;
    typedef struct simvx_biquad_bpf_node simvx_biquad_bpf_node;

    simvx_biquad_lpf_node* simvx_biquad_lpf_node_alloc(void);
    void simvx_biquad_lpf_node_free(simvx_biquad_lpf_node* n);
    int simvx_biquad_lpf_node_init(ma_engine* engine, simvx_biquad_lpf_node* node,
                                     double cutoff_hz, double q);
    void simvx_biquad_lpf_node_uninit(simvx_biquad_lpf_node* node);
    int simvx_biquad_lpf_node_update_params(simvx_biquad_lpf_node* node, ma_engine* engine,
                                              double cutoff_hz, double q);

    simvx_biquad_hpf_node* simvx_biquad_hpf_node_alloc(void);
    void simvx_biquad_hpf_node_free(simvx_biquad_hpf_node* n);
    int simvx_biquad_hpf_node_init(ma_engine* engine, simvx_biquad_hpf_node* node,
                                     double cutoff_hz, double q);
    void simvx_biquad_hpf_node_uninit(simvx_biquad_hpf_node* node);
    int simvx_biquad_hpf_node_update_params(simvx_biquad_hpf_node* node, ma_engine* engine,
                                              double cutoff_hz, double q);

    simvx_biquad_bpf_node* simvx_biquad_bpf_node_alloc(void);
    void simvx_biquad_bpf_node_free(simvx_biquad_bpf_node* n);
    int simvx_biquad_bpf_node_init(ma_engine* engine, simvx_biquad_bpf_node* node,
                                     double cutoff_hz, double q);
    void simvx_biquad_bpf_node_uninit(simvx_biquad_bpf_node* node);
    int simvx_biquad_bpf_node_update_params(simvx_biquad_bpf_node* node, ma_engine* engine,
                                              double cutoff_hz, double q);

    /* Notch */
    ma_notch_node* simvx_notch_node_alloc(void);
    void simvx_notch_node_free(ma_notch_node* n);
    int simvx_notch_node_init(ma_engine* engine, ma_notch_node* node, double cutoff_hz, double q);
    void simvx_notch_node_uninit(ma_notch_node* node);
    int simvx_notch_node_reinit(ma_notch_node* node, ma_engine* engine, double cutoff_hz, double q);

    /* Peak (parametric EQ band) */
    ma_peak_node* simvx_peak_node_alloc(void);
    void simvx_peak_node_free(ma_peak_node* n);
    int simvx_peak_node_init(ma_engine* engine, ma_peak_node* node, double gain_db, double q, double freq);
    void simvx_peak_node_uninit(ma_peak_node* node);

    /* Low shelf */
    ma_loshelf_node* simvx_loshelf_node_alloc(void);
    void simvx_loshelf_node_free(ma_loshelf_node* n);
    int simvx_loshelf_node_init(ma_engine* engine, ma_loshelf_node* node, double gain_db, double q, double freq);
    void simvx_loshelf_node_uninit(ma_loshelf_node* node);

    /* High shelf */
    ma_hishelf_node* simvx_hishelf_node_alloc(void);
    void simvx_hishelf_node_free(ma_hishelf_node* n);
    int simvx_hishelf_node_init(ma_engine* engine, ma_hishelf_node* node, double gain_db, double q, double freq);
    void simvx_hishelf_node_uninit(ma_hishelf_node* node);

    /* Delay */
    ma_delay_node* simvx_delay_node_alloc(void);
    void simvx_delay_node_free(ma_delay_node* n);
    int simvx_delay_node_init(ma_engine* engine, ma_delay_node* node, double delay_seconds, float decay);
    void simvx_delay_node_uninit(ma_delay_node* node);
    void simvx_delay_node_set_wet(ma_delay_node* node, float wet);
    void simvx_delay_node_set_dry(ma_delay_node* node, float dry);
    void simvx_delay_node_set_decay(ma_delay_node* node, float decay);

    /* Custom DSP nodes: opaque to Python */
    typedef struct simvx_softclip_node simvx_softclip_node;
    typedef struct simvx_compressor_node simvx_compressor_node;
    typedef struct simvx_freeverb_node simvx_freeverb_node;

    /* Soft clip */
    simvx_softclip_node* simvx_softclip_node_alloc(void);
    void simvx_softclip_node_free(simvx_softclip_node* n);
    int simvx_softclip_node_init(ma_engine* engine, simvx_softclip_node* node, float drive, float output_gain);
    void simvx_softclip_node_uninit(simvx_softclip_node* node);
    void simvx_softclip_node_set_params(simvx_softclip_node* node, float drive, float output_gain);

    /* Compressor */
    simvx_compressor_node* simvx_compressor_node_alloc(void);
    void simvx_compressor_node_free(simvx_compressor_node* n);
    int simvx_compressor_node_init(ma_engine* engine, simvx_compressor_node* node,
                                    float threshold_db, float ratio,
                                    float attack_ms, float release_ms,
                                    float knee_db, float makeup_db);
    void simvx_compressor_node_uninit(simvx_compressor_node* node);
    void simvx_compressor_node_set_params(simvx_compressor_node* node, ma_engine* engine,
                                            float threshold_db, float ratio,
                                            float attack_ms, float release_ms,
                                            float knee_db, float makeup_db);

    /* Freeverb */
    simvx_freeverb_node* simvx_freeverb_node_alloc(void);
    void simvx_freeverb_node_free(simvx_freeverb_node* n);
    int simvx_freeverb_node_init(ma_engine* engine, simvx_freeverb_node* node,
                                   float room_size, float damping,
                                   float wet, float dry, float width, int freeze);
    void simvx_freeverb_node_uninit(simvx_freeverb_node* node);
    void simvx_freeverb_node_set_params(simvx_freeverb_node* node,
                                          float room_size, float damping,
                                          float wet, float dry, float width,
                                          int freeze);
    """


def _source() -> str:
    """C source that cffi compiles + links into the extension.

    `set_source` accepts a body string; ours just `#include`s the glue C
    so the same file is used by the script and by anyone reading the
    code in their editor.
    """
    return f'#include "{GLUE_SOURCE.name}"\n'


def _extra_compile_args() -> list[str]:
    args = ["-O2", "-Wno-unused-function", "-Wno-unused-parameter"]
    if sys.platform == "darwin":
        # CoreAudio backend for miniaudio on macOS.
        args += ["-fPIC"]
    elif sys.platform.startswith("linux"):
        args += ["-fPIC", "-D_GNU_SOURCE"]
    return args


def _extra_link_args() -> list[str]:
    if sys.platform == "darwin":
        return ["-framework", "CoreFoundation", "-framework", "CoreAudio",
                "-framework", "AudioToolbox", "-framework", "AudioUnit"]
    if sys.platform.startswith("linux"):
        # ALSA/PulseAudio loaded via dlopen at runtime by miniaudio.
        return ["-ldl", "-lpthread", "-lm"]
    if sys.platform == "win32":
        return ["ole32.lib"]
    return []


[docs] def build(output_dir: str | None = None, *, verbose: bool = True) -> Path: """Compile the extension. Returns the path to the built .so/.dll.""" cffi = _require_build_deps() if not GLUE_SOURCE.exists(): raise FileNotFoundError(f"Missing glue source: {GLUE_SOURCE}") if not (VENDOR_INCLUDE / "miniaudio.h").exists(): raise FileNotFoundError( f"Missing vendored miniaudio.h at {VENDOR_INCLUDE / 'miniaudio.h'}" ) out_dir = Path(output_dir) if output_dir else HERE out_dir.mkdir(parents=True, exist_ok=True) ffi = cffi.FFI() ffi.cdef(_cdef()) ffi.set_source( "_simvx_miniaudio_engine", _source(), include_dirs=[str(VENDOR_INCLUDE), str(HERE)], sources=[], # glue is #included by _source() so cffi compiles it as one TU extra_compile_args=_extra_compile_args(), extra_link_args=_extra_link_args(), ) cwd = os.getcwd() os.chdir(out_dir) try: if verbose: print(f"Compiling _simvx_miniaudio_engine into {out_dir}/") result = ffi.compile(verbose=verbose) finally: os.chdir(cwd) built = Path(result) if verbose: print(f"Built: {built}") return built
[docs] def main() -> int: try: build() except Exception as exc: print(f"Build failed: {exc}", file=sys.stderr) return 1 return 0
if __name__ == "__main__": sys.exit(main())