"""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())