Skip to content

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_available property
  • 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(), MidiRenderer all accept tail_seconds="auto"
  • Configurable tail_threshold (default: -80 dB / 1e-4 linear) and max_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.capture field; 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) -- new capture parameter on both Plugin and PluginChain constructors
  • 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/--overwrite is 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() in minihost_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 process and batch mode automatically resample mismatched input files to match the plugin's sample rate; use --no-resample to error instead
  • CLI: minihost resample input.wav -o output.wav -r 48000 -- standalone resampling subcommand with --bit-depth and -y/--overwrite options
  • 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, --input capture flag for play, glob expansion, batch output detection, batch error paths, --no-resample flag, and resample subcommand (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_ChainParamChange struct with plugin_index field 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: .wav for WAV, .flac for 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 process subcommand that relied on get_audio_info() returning a subtype key (removed in 0.1.2); now defaults to 24-bit when --bit-depth is 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)
  • MidiIn class for standalone MIDI input monitoring (no plugin required)
  • MidiIn.open(port_index, callback) -- open a hardware MIDI input port with raw bytes callback
  • MidiIn.open_virtual(name, callback) -- create a virtual MIDI input port with raw bytes callback
  • close() method and context manager (with) support
  • Python: minihost.MidiIn
  • MIDI monitor mode in minihost midi CLI subcommand
  • minihost midi -m N -- monitor incoming MIDI on hardware port N
  • minihost 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 probe CLI subcommand into info -- use minihost info <plugin> --probe for lightweight metadata-only mode
  • minihost info now shows full runtime details by default (was already doing this)
  • minihost info --probe replaces the old minihost probe (no full plugin load)
  • minihost info --json outputs combined probe + runtime data as JSON
  • Merged render CLI subcommand into process -- use minihost process <plugin> -m song.mid -o output.wav instead of minihost render
  • Added -t, --tail option to process subcommand (default: 2.0s) for configurable tail length in MIDI-only synth mode
  • Renamed midi-ports CLI subcommand to midi

Removed

  • soundfile runtime 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 returns format or subtype keys

[0.1.1]

Added

  • MIDI capability queries on live plugin instances via MH_Info fields: accepts_midi, produces_midi, is_midi_effect, supports_mpe
  • Python: Plugin.accepts_midi, Plugin.produces_midi, Plugin.is_midi_effect, Plugin.supports_mpe read-only properties
  • Stable parameter IDs via MH_ParamInfo.id field (uses getParameterID() for version-safe state management)
  • Python: get_param_info() dict now includes "id" key
  • Parameter categories via MH_ParamInfo.category field with MH_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 AudioProcessorListener integration
  • mh_set_change_callback() -- processor-level changes (latency, param info, program, non-param state) with MH_CHANGE_* bitmask flags
  • mh_set_param_value_callback() -- plugin-initiated parameter value changes
  • mh_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_precision read/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 .lv2 bundles
  • CLI updated with LV2 examples for probe, info, and scan commands
  • Headless build mode (MINIHOST_HEADLESS, default ON)
  • Builds without GUI dependencies using JUCE's juce_audio_processors_headless module (requires JUCE 8.0.11+)
  • Uses headless format classes (VST3PluginFormatHeadless, AudioUnitPluginFormatHeadless, LV2PluginFormatHeadless)
  • Disable with cmake -DMINIHOST_HEADLESS=OFF

Changed

  • addFormat() calls now use std::make_unique instead of raw new
  • Removed unused KnownPluginList variable from findFirstTypeForFile
  • 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_PluginChain opaque struct for managing chains of plugins
  • mh_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 chain
  • mh_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 chain
  • mh_chain_get_plugin() - Get plugin from chain by index
  • mh_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 size
  • mh_chain_reset() - Reset all plugins in the chain
  • mh_chain_set_non_realtime() - Set non-realtime mode for all plugins
  • mh_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 PluginChain class with process(), process_midi(), reset(), set_non_realtime(), get_plugin() methods
  • Python PluginChain properties: num_plugins, latency_samples, num_input_channels, num_output_channels, sample_rate, tail_seconds
  • AudioDevice now accepts either Plugin or PluginChain
  • render_midi(), render_midi_stream(), render_midi_to_file(), and MidiRenderer now accept either Plugin or PluginChain

Real-time Audio Playback (miniaudio integration)

  • MH_AudioDevice opaque struct for audio device management
  • MH_AudioConfig struct for device configuration (sample_rate, buffer_frames, output_channels, midi_input_port, midi_output_port)
  • MH_AudioInputCallback typedef for effect plugin input audio
  • mh_audio_open() - Open audio device for real-time playback through a plugin
  • mh_audio_close() - Close audio device
  • mh_audio_start() / mh_audio_stop() - Start/stop audio playback
  • mh_audio_is_playing() - Check if audio is currently playing
  • mh_audio_set_input_callback() - Set input callback for effect plugins
  • mh_audio_get_sample_rate() - Get actual device sample rate
  • mh_audio_get_buffer_frames() - Get actual buffer size
  • mh_audio_get_channels() - Get number of output channels
  • New libminihost_audio library using miniaudio for cross-platform audio I/O

Real-time MIDI I/O (libremidi integration)

  • MH_MidiPortInfo struct for MIDI port information
  • MH_MidiPortCallback typedef for port enumeration callbacks
  • mh_midi_enumerate_inputs() / mh_midi_enumerate_outputs() - Enumerate available MIDI ports
  • mh_midi_get_num_inputs() / mh_midi_get_num_outputs() - Get MIDI port count
  • mh_midi_get_input_name() / mh_midi_get_output_name() - Get MIDI port name by index
  • mh_audio_connect_midi_input() / mh_audio_connect_midi_output() - Connect MIDI ports to AudioDevice
  • mh_audio_disconnect_midi_input() / mh_audio_disconnect_midi_output() - Disconnect MIDI
  • mh_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 AudioDevice
  • mh_audio_create_virtual_midi_output() - Create virtual MIDI output for AudioDevice
  • mh_audio_is_midi_input_virtual() / mh_audio_is_midi_output_virtual() - Check if MIDI port is virtual
  • mh_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 midifile library for Standard MIDI File (SMF) read/write capability
  • Python MidiFile class 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 array
  • render_midi_stream() - Generator yielding audio blocks for streaming/large files
  • render_midi_to_file() - Render MIDI file directly to WAV file (16/24/32-bit)
  • MidiRenderer class - 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 processing
  • mh_probe() - Get plugin metadata without full instantiation
  • MH_PluginDesc struct 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 rate
  • MH_ScanCallback typedef for plugin scanning callback
  • mh_scan_directory() - Recursively scan directory for VST3/AudioUnit/LV2 plugins
  • MH_PluginDesc.path field added for scan results
  • mh_process_double() - Process audio with 64-bit double precision
  • mh_supports_double() - Check if plugin supports native double precision
  • MH_LoadCallback typedef for async loading callback
  • mh_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 value
  • mh_get_num_programs() - Get number of factory presets
  • mh_get_program_name() - Get factory preset name by index
  • mh_get_program() / mh_set_program() - Get/set current factory preset

Bus Layout & Sidechain

  • MH_BusInfo struct for bus information (name, channels, is_main, is_enabled)
  • mh_get_num_buses() - Query number of input/output buses
  • mh_get_bus_info() - Get detailed bus information
  • mh_open_ex() - Open plugin with sidechain channel configuration
  • mh_process_sidechain() - Process audio with sidechain input
  • mh_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 of std::make_unique<>() (JUCE's API expects raw pointers)
  • Added POSITION_INDEPENDENT_CODE ON to libminihost CMakeLists.txt for linking into shared libraries (e.g., Python module)

Command Line Interface

  • minihost CLI tool with subcommands for common operations:
  • probe - Get plugin metadata without full instantiation
  • scan - Recursively scan directory for VST3/AudioUnit/LV2 plugins
  • info - Show detailed plugin info (buses, presets, latency)
  • params - List plugin parameters with current values
  • midi - List available MIDI input/output ports
  • play - Real-time audio playback with MIDI input
  • process - 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.AudioDevice class 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 ports
  • minihost.midi_get_output_ports() - Get list of available MIDI output ports
  • minihost.MidiFile class 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 metadata
  • minihost.scan_directory(path) - Scan directory for VST3/AudioUnit/LV2 plugins, returns list of metadata dicts
  • Plugin constructor now accepts sidechain_channels parameter
  • 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 threading module with the regular Plugin() constructor