Changelog¶
[Unreleased]¶
[0.1.4]¶
Added¶
- Audio input for effect processing -- lock-free ring buffer API for feeding audio through effect plugins in real time, without GIL contention on the audio thread
- C API:
mh_audio_enable_input(),mh_audio_disable_input(),mh_audio_write_input(),mh_audio_input_available() - C internals:
MH_AudioRingBuffer-- SPSC lock-free ring buffer (audio_ringbuffer.h/.cpp) - Python:
AudioDevice.enable_input(capacity_frames=0),disable_input(),write_input(data),input_availableproperty -
Example:
examples/audio_input.py-- sine wave and file-through-effect demos -
Offline bounce with automatic tail detection --
tail_seconds="auto"in render functions monitors output amplitude after MIDI ends and stops when it decays below a threshold render_midi_stream(),render_midi(),render_midi_to_file(),MidiRendererall accepttail_seconds="auto"- Configurable
tail_threshold(default: -80 dB /1e-4linear) andmax_tail_seconds(default: 30s safety cap) - Stops after 4 consecutive blocks below threshold to avoid cutting during brief silences
-
Example:
examples/auto_tail.py-- compares fixed vs auto tail at different thresholds -
Duplex audio (capture) for real-time effect processing -- system audio input routed through plugin via miniaudio duplex mode
- C layer:
MH_AudioConfig.capturefield; when set, audio device opens in duplex mode and the audio callback de-interleaves capture input directly into the plugin's input buffers (zero additional latency) - Python:
AudioDevice(plugin, capture=True)-- newcaptureparameter on bothPluginandPluginChainconstructors -
CLI:
minihost play /path/to/effect.vst3 --input(-i) enables duplex mode for live effect processing (guitar through amp sim, vocal processing, etc.) -
Batch / multi-file processing in CLI -- glob pattern expansion and directory output for
minihost process -i "*.wav"expands glob patterns;-o output/writes each result to the output directory- Batch mode detected when output path is a directory (exists or ends with
/) and input contains audio files (no MIDI) - Plugin loaded once, state saved and restored between files for consistent processing
- Skips existing output files unless
-y/--overwriteis set -
Example:
minihost process reverb.vst3 -i "drums/*.wav" -o processed/ -
Sample rate conversion / resampling -- built-in resampling using miniaudio's
ma_resampler(linear interpolation with 4th-order low-pass anti-aliasing filter) - C API:
mh_audio_resample()inminihost_audiofile.h-- resamples interleaved float32 audio between any two sample rates - Python:
minihost.resample(data, sr_in, sr_out)-- takes(channels, frames)numpy array, returns resampled array - CLI:
minihost processand batch mode automatically resample mismatched input files to match the plugin's sample rate; use--no-resampleto error instead - CLI:
minihost resample input.wav -o output.wav -r 48000-- standalone resampling subcommand with--bit-depthand-y/--overwriteoptions - No-op fast path when source and target rates are equal (memcpy, no resampler init)
Tests¶
- Render internals unit tests (
tests/test_render_internals.py) -- 55 tests covering_build_tempo_map,_tick_to_seconds,_collect_midi_events,_event_to_midi_tuple,_seconds_to_samples, and end-to-end tempo map integration - CLI unit tests (
tests/test_cli.py) -- 84 tests covering argument parsing for all 7 subcommands, global options, error paths,--inputcapture flag forplay, glob expansion, batch output detection, batch error paths,--no-resampleflag, andresamplesubcommand (arg parsing + functional tests) - Resampling tests (
tests/test_audio_io.py) -- 7 tests covering upsample, downsample, identity (same rate), stereo, silence preservation, large ratio (8k to 48k), and round-trip frame count
[0.1.3]¶
Added¶
mh_chain_process_auto()for sample-accurate parameter automation across plugin chains- New
MH_ChainParamChangestruct withplugin_indexfield to target specific plugins in the chain -
Python:
PluginChain.process_auto(input, output, midi_in, param_changes)with 4-tuple param changes(sample_offset, plugin_index, param_index, value) -
FLAC write support in
mh_audio_write()via vendored tflac encoder (BSD-0, single-header C89) - Supports 16-bit and 24-bit FLAC output; 32-bit raises an error (FLAC max is 24-bit)
- Format selected by file extension:
.wavfor WAV,.flacfor FLAC - Python:
write_audio("out.flac", data, sr, bit_depth=24)works without any API changes
Fixed¶
- Fixed stale version assertion in
test_minihost.py("0.1.1"->"0.1.2") - Removed dead bit-depth auto-detection code in CLI
processsubcommand that relied onget_audio_info()returning asubtypekey (removed in 0.1.2); now defaults to 24-bit when--bit-depthis not specified - Fixed
render_midi_to_file()docstring listing unsupported output formats (FLAC, AIFF, OGG); only WAV is supported
[0.1.2]¶
Added¶
- Audio file I/O via miniaudio (C layer):
mh_audio_read(),mh_audio_write(),mh_audio_get_file_info() - Python:
minihost.read_audio(),minihost.write_audio(),minihost.get_audio_info()now backed by miniaudio - Read support: WAV, FLAC, MP3, Vorbis
- Write support: WAV only (16-bit PCM, 24-bit PCM, 32-bit float)
MidiInclass for standalone MIDI input monitoring (no plugin required)MidiIn.open(port_index, callback)-- open a hardware MIDI input port with raw bytes callbackMidiIn.open_virtual(name, callback)-- create a virtual MIDI input port with raw bytes callbackclose()method and context manager (with) support- Python:
minihost.MidiIn - MIDI monitor mode in
minihost midiCLI subcommand minihost midi -m N-- monitor incoming MIDI on hardware port Nminihost midi --virtual-midi NAME-- create a virtual MIDI port and monitor it- Human-readable output: Note On/Off (with note names), CC, Pitch Bend, Program Change, Channel Pressure, Poly Aftertouch, SysEx
Changed¶
- Merged
probeCLI subcommand intoinfo-- useminihost info <plugin> --probefor lightweight metadata-only mode minihost infonow shows full runtime details by default (was already doing this)minihost info --probereplaces the oldminihost probe(no full plugin load)minihost info --jsonoutputs combined probe + runtime data as JSON- Merged
renderCLI subcommand intoprocess-- useminihost process <plugin> -m song.mid -o output.wavinstead ofminihost render - Added
-t, --tailoption toprocesssubcommand (default: 2.0s) for configurable tail length in MIDI-only synth mode - Renamed
midi-portsCLI subcommand tomidi
Removed¶
soundfileruntime dependency -- audio file I/O now uses miniaudio (already vendored)- AIFF and OGG write support removed (miniaudio encoder is WAV-only)
get_audio_info()no longer returnsformatorsubtypekeys
[0.1.1]¶
Added¶
- MIDI capability queries on live plugin instances via
MH_Infofields:accepts_midi,produces_midi,is_midi_effect,supports_mpe - Python:
Plugin.accepts_midi,Plugin.produces_midi,Plugin.is_midi_effect,Plugin.supports_mperead-only properties - Stable parameter IDs via
MH_ParamInfo.idfield (usesgetParameterID()for version-safe state management) - Python:
get_param_info()dict now includes"id"key - Parameter categories via
MH_ParamInfo.categoryfield withMH_PARAM_CATEGORY_*constants - Python:
get_param_info()dict now includes"category"key - Bus layout validation via
mh_check_buses_layout()-- query whether a bus layout is supported before attempting to apply it - Python:
Plugin.check_buses_layout(input_channels, output_channels) -> bool - Change notifications via
AudioProcessorListenerintegration mh_set_change_callback()-- processor-level changes (latency, param info, program, non-param state) withMH_CHANGE_*bitmask flagsmh_set_param_value_callback()-- plugin-initiated parameter value changesmh_set_param_gesture_callback()-- parameter gesture begin/end from plugin UI- Python:
Plugin.set_change_callback(),Plugin.set_param_value_callback(),Plugin.set_param_gesture_callback() - Python: module-level constants
MH_CHANGE_LATENCY,MH_CHANGE_PARAM_INFO,MH_CHANGE_PROGRAM,MH_CHANGE_NON_PARAM_STATE - Parameter gesture bracketing via
mh_begin_param_gesture()/mh_end_param_gesture()-- signal start/end of automation drags to plugins - Python:
Plugin.begin_param_gesture(index),Plugin.end_param_gesture(index) - Current program state save/restore via
mh_get_program_state_size()/mh_get_program_state()/mh_set_program_state()-- lighter-weight per-program state - Python:
Plugin.get_program_state() -> bytes,Plugin.set_program_state(data) - Processing precision selection via
mh_get_processing_precision()/mh_set_processing_precision()-- explicitly select float vs double processing mode - Python:
Plugin.processing_precisionread/write property - Module-level constants
MH_PRECISION_SINGLE,MH_PRECISION_DOUBLE - Track properties via
mh_set_track_properties()-- pass track name/color metadata to plugins -
Python:
Plugin.set_track_properties(name=None, colour=None) -
LV2 plugin format support (
JUCE_PLUGINHOST_LV2=1) - Load and process LV2 plugins on all platforms
mh_scan_directory()now finds.lv2bundles- CLI updated with LV2 examples for
probe,info, andscancommands - Headless build mode (
MINIHOST_HEADLESS, default ON) - Builds without GUI dependencies using JUCE's
juce_audio_processors_headlessmodule (requires JUCE 8.0.11+) - Uses headless format classes (
VST3PluginFormatHeadless,AudioUnitPluginFormatHeadless,LV2PluginFormatHeadless) - Disable with
cmake -DMINIHOST_HEADLESS=OFF
Changed¶
addFormat()calls now usestd::make_uniqueinstead of rawnew- Removed unused
KnownPluginListvariable fromfindFirstTypeForFile - Added cross-platform Python script (
scripts/download_juce.py) for downloading JUCE - Works on Windows, macOS, and Linux without requiring bash
- Uses only Python standard library (no external dependencies)
- Handles Python 3.14+ tarfile deprecation warning
- Bumped default JUCE version from 8.0.6 to 8.0.12 (required for
juce_audio_processors_headless) - Updated CI workflow to use Python script instead of bash for JUCE download
- Updated Makefile to prefer Python script with bash fallback
[0.1.0]¶
Added¶
Plugin Chaining¶
MH_PluginChainopaque struct for managing chains of pluginsmh_chain_create()- Create a chain from an array of plugins (all must have same sample rate)mh_chain_close()- Close chain and free resources (does not close individual plugins)mh_chain_process()- Process audio through the chainmh_chain_process_midi_io()- Process audio with MIDI I/O (MIDI goes to first plugin only)mh_chain_get_latency_samples()- Get total chain latency (sum of all plugin latencies)mh_chain_get_num_plugins()- Get number of plugins in the chainmh_chain_get_plugin()- Get plugin from chain by indexmh_chain_get_num_input_channels()- Get input channel count (from first plugin)mh_chain_get_num_output_channels()- Get output channel count (from last plugin)mh_chain_get_sample_rate()- Get sample rate (all plugins share same rate)mh_chain_get_max_block_size()- Get maximum block sizemh_chain_reset()- Reset all plugins in the chainmh_chain_set_non_realtime()- Set non-realtime mode for all pluginsmh_chain_get_tail_seconds()- Get maximum tail length (max of all plugin tails)mh_audio_open_chain()- Open audio device for real-time playback through a plugin chain- Python
PluginChainclass withprocess(),process_midi(),reset(),set_non_realtime(),get_plugin()methods - Python
PluginChainproperties:num_plugins,latency_samples,num_input_channels,num_output_channels,sample_rate,tail_seconds AudioDevicenow accepts eitherPluginorPluginChainrender_midi(),render_midi_stream(),render_midi_to_file(), andMidiRenderernow accept eitherPluginorPluginChain
Real-time Audio Playback (miniaudio integration)¶
MH_AudioDeviceopaque struct for audio device managementMH_AudioConfigstruct for device configuration (sample_rate, buffer_frames, output_channels, midi_input_port, midi_output_port)MH_AudioInputCallbacktypedef for effect plugin input audiomh_audio_open()- Open audio device for real-time playback through a pluginmh_audio_close()- Close audio devicemh_audio_start()/mh_audio_stop()- Start/stop audio playbackmh_audio_is_playing()- Check if audio is currently playingmh_audio_set_input_callback()- Set input callback for effect pluginsmh_audio_get_sample_rate()- Get actual device sample ratemh_audio_get_buffer_frames()- Get actual buffer sizemh_audio_get_channels()- Get number of output channels- New
libminihost_audiolibrary using miniaudio for cross-platform audio I/O
Real-time MIDI I/O (libremidi integration)¶
MH_MidiPortInfostruct for MIDI port informationMH_MidiPortCallbacktypedef for port enumeration callbacksmh_midi_enumerate_inputs()/mh_midi_enumerate_outputs()- Enumerate available MIDI portsmh_midi_get_num_inputs()/mh_midi_get_num_outputs()- Get MIDI port countmh_midi_get_input_name()/mh_midi_get_output_name()- Get MIDI port name by indexmh_audio_connect_midi_input()/mh_audio_connect_midi_output()- Connect MIDI ports to AudioDevicemh_audio_disconnect_midi_input()/mh_audio_disconnect_midi_output()- Disconnect MIDImh_audio_get_midi_input_port()/mh_audio_get_midi_output_port()- Query connected MIDI ports- Lock-free ring buffer for thread-safe MIDI transfer between MIDI and audio threads
Virtual MIDI Ports¶
mh_midi_in_open_virtual()- Create a virtual MIDI input port (other apps can send MIDI to it)mh_midi_out_open_virtual()- Create a virtual MIDI output port (other apps can receive MIDI from it)mh_audio_create_virtual_midi_input()- Create virtual MIDI input for AudioDevicemh_audio_create_virtual_midi_output()- Create virtual MIDI output for AudioDevicemh_audio_is_midi_input_virtual()/mh_audio_is_midi_output_virtual()- Check if MIDI port is virtualmh_audio_send_midi()- Send MIDI events programmatically during real-time playback- Virtual ports appear in system MIDI port lists, allowing DAWs and other apps to connect
- Platform support: macOS (CoreMIDI), Linux (ALSA); not supported on Windows
MIDI File Read/Write (midifile integration)¶
- Integrated
midifilelibrary for Standard MIDI File (SMF) read/write capability - Python
MidiFileclass for creating, loading, and saving MIDI files - Create MIDI files programmatically with note on/off, tempo, control change, program change, pitch bend events
- Load existing MIDI files and iterate through events
- Save MIDI files to disk
- Access event timing in both ticks and seconds
MIDI File Rendering¶
render_midi()- Render MIDI file through plugin to numpy arrayrender_midi_stream()- Generator yielding audio blocks for streaming/large filesrender_midi_to_file()- Render MIDI file directly to WAV file (16/24/32-bit)MidiRendererclass - Stateful renderer with progress tracking and fine-grained control- Properties:
duration_seconds,progress,is_finished,current_time - Methods:
render_block(),render_all(),reset() - Automatic tempo map handling for correct timing
- Configurable tail length for reverb/delay tails
Core Utilities¶
mh_reset()- Reset plugin internal state (clears delay lines, reverb tails, filter states)mh_set_non_realtime()- Enable higher-quality algorithms for offline/batch processingmh_probe()- Get plugin metadata without full instantiationMH_PluginDescstruct for plugin metadata (name, vendor, version, format, unique_id, MIDI flags, channel counts)mh_set_sample_rate()- Change sample rate without reloading plugin (preserves parameter state)mh_get_sample_rate()- Query current sample rateMH_ScanCallbacktypedef for plugin scanning callbackmh_scan_directory()- Recursively scan directory for VST3/AudioUnit/LV2 pluginsMH_PluginDesc.pathfield added for scan resultsmh_process_double()- Process audio with 64-bit double precisionmh_supports_double()- Check if plugin supports native double precisionMH_LoadCallbacktypedef for async loading callbackmh_open_async()- Load plugin in background thread
Parameter & Preset Access¶
mh_param_to_text()- Convert normalized parameter value to display string (e.g., "2500 Hz")mh_param_from_text()- Convert display string to normalized valuemh_get_num_programs()- Get number of factory presetsmh_get_program_name()- Get factory preset name by indexmh_get_program()/mh_set_program()- Get/set current factory preset
Bus Layout & Sidechain¶
MH_BusInfostruct for bus information (name, channels, is_main, is_enabled)mh_get_num_buses()- Query number of input/output busesmh_get_bus_info()- Get detailed bus informationmh_open_ex()- Open plugin with sidechain channel configurationmh_process_sidechain()- Process audio with sidechain inputmh_get_sidechain_channels()- Query configured sidechain channel count
Fixed¶
Linux Compilation¶
- Added Linux build dependencies to README.md (JUCE requires freetype, fontconfig, webkit2gtk, gtk3, etc.)
- Fixed
addFormat()calls to use raw pointers instead ofstd::make_unique<>()(JUCE's API expects raw pointers) - Added
POSITION_INDEPENDENT_CODE ONto libminihost CMakeLists.txt for linking into shared libraries (e.g., Python module)
Command Line Interface¶
minihostCLI tool with subcommands for common operations:probe- Get plugin metadata without full instantiationscan- Recursively scan directory for VST3/AudioUnit/LV2 pluginsinfo- Show detailed plugin info (buses, presets, latency)params- List plugin parameters with current valuesmidi- List available MIDI input/output portsplay- Real-time audio playback with MIDI inputprocess- Offline audio processing through effects, or MIDI-to-audio rendering for synths- Global options:
--sample-rate,--block-size - JSON output support (
--json) for probe, scan, params, midi - Plugin state and preset loading for process command
- Virtual MIDI port creation for play command
Python Bindings¶
All C API additions are exposed in the Python minihost module:
minihost.AudioDeviceclass for real-time audio playback with MIDI- Constructor:
AudioDevice(plugin, sample_rate=0, buffer_frames=0, output_channels=0, midi_input_port=-1, midi_output_port=-1) - Methods:
start(),stop(),connect_midi_input(),connect_midi_output(),disconnect_midi_input(),disconnect_midi_output(),create_virtual_midi_input(),create_virtual_midi_output(),send_midi() - Properties:
is_playing,sample_rate,buffer_frames,channels,midi_input_port,midi_output_port,is_midi_input_virtual,is_midi_output_virtual - Context manager support (
with AudioDevice(plugin) as audio:) minihost.midi_get_input_ports()- Get list of available MIDI input portsminihost.midi_get_output_ports()- Get list of available MIDI output portsminihost.MidiFileclass for MIDI file read/write- Methods:
load(),save(),add_track(),add_tempo(),add_note_on(),add_note_off(),add_control_change(),add_program_change(),add_pitch_bend(),get_events(),join_tracks(),split_tracks() - Properties:
num_tracks,ticks_per_quarter,duration_seconds minihost.probe(path)- Module-level function for plugin metadataminihost.scan_directory(path)- Scan directory for VST3/AudioUnit/LV2 plugins, returns list of metadata dictsPluginconstructor now acceptssidechain_channelsparameter- New properties:
non_realtime,num_programs,program,sidechain_channels,num_input_buses,num_output_buses,sample_rate(read/write),supports_double - New methods:
reset(),param_to_text(),param_from_text(),get_program_name(),get_bus_info(),process_sidechain(),process_double() - Note: For async loading in Python, use Python's
threadingmodule with the regularPlugin()constructor