Migration guide¶
This guide covers the breaking changes shipping in 0.2.0 (see CHANGELOG [Unreleased]): the AudioBuffer / numpy-optional migration, and the routing type rename (PluginGraph -> PluginBus, GraphV2 -> PluginGraph).
The AudioBuffer changes each have a one-keyword fix to keep existing code working; the recommended patterns shown below are improvements, not requirements. The routing rename (section 5) is the exception -- it is a hard rename with no compatibility shim, because minihost is still alpha (0.x).
TL;DR¶
Install:
-
Was:
pip install minihost(numpy was a hard dependency, installed transparently) -
Now:
pip install minihost(no numpy) orpip install minihost[numpy](with numpy interop)
Smallest possible code change: add as_=np.ndarray to your read_audio / render_midi* calls and you're done.
data, sr = minihost.read_audio(path, as_=np.ndarray)
audio = minihost.render_midi(plugin, "song.mid", as_=np.ndarray)
Recommended: drop the as_= and migrate to AudioBuffer. It's the canonical type, accepted directly by every other minihost API via DLPack, and a .as_ndarray() call gets you a zero-copy numpy view at the boundary.
data, sr = minihost.read_audio(path) # AudioBuffer
plugin.process(data, output_buffer) # AudioBuffer in directly
np_data = data.as_ndarray() # zero-copy numpy view if you need one
Routing rename (no shim): PluginGraph (the parallel bus) is now PluginBus; GraphV2 (the DAG) is now PluginGraph. Mind the swap -- PluginGraph now means the DAG. Full details and the C/C++ ABI rename are in breaking change 5 below.
Breaking changes¶
1. pip install minihost no longer installs numpy¶
The default install has zero required Python dependencies. Pass [numpy] if you want numpy-typed APIs:
What requires the [numpy] extra:
-
read_audio(path, as_=np.ndarray)(and the equivalent onrender_midi*) -
AudioBuffer.as_ndarray() -
Passing a
np.ndarrayas input towrite_audio,resample,process_audio*,Plugin.process*,PluginChain.process*
What does not require numpy:
-
All
Plugin/PluginChainplugin loading and processing (when fedAudioBufferinputs) -
read_audio(path)(returnsAudioBuffer) -
write_audio(path, audio_buffer, sr) -
resample(audio_buffer, sr_in, sr_out) -
process_audio/process_audio_to_file -
render_midi*/MidiRenderer(with defaultas_=AudioBuffer) -
MidiMapper,AudioDevice,MidiIn,MidiFile, all CLI commands
Symptom if you forget the extra: the package imports fine, but the first call into a numpy-typed code path raises:
ImportError: AudioBuffer.as_ndarray() requires numpy. Install minihost
with the numpy extra: 'pip install minihost[numpy]'.
2. read_audio() returns AudioBuffer by default¶
# Old:
data, sr = minihost.read_audio("input.wav")
# data was a numpy.ndarray of shape (channels, samples), dtype float32
# Minimum fix:
data, sr = minihost.read_audio("input.wav", as_=np.ndarray)
# Recommended:
data, sr = minihost.read_audio("input.wav")
# data is an AudioBuffer. Use data.frames instead of data.shape[1].
# Pass data directly to plugin.process / write_audio / resample / etc.
3. render_midi() and render_midi_stream() return AudioBuffer by default¶
# Old:
audio = minihost.render_midi(plugin, "song.mid")
# Minimum fix:
audio = minihost.render_midi(plugin, "song.mid", as_=np.ndarray)
# Recommended:
audio = minihost.render_midi(plugin, "song.mid")
# AudioBuffer; use audio.frames, audio.channels.
render_midi_stream accepts the same as_= selector (yields AudioBuffer blocks by default).
4. MidiRenderer.render_block() returns AudioBuffer (no as_=)¶
The block-level API does not have an as_= parameter. Call .as_ndarray() on the returned block if you need a numpy view.
# Old:
while not renderer.is_finished:
block = renderer.render_block() # numpy.ndarray
process_block_with_numpy(block)
# Minimum fix:
while not renderer.is_finished:
block = renderer.render_block() # AudioBuffer
process_block_with_numpy(block.as_ndarray())
# Or use AudioBuffer-native ops:
while not renderer.is_finished:
block = renderer.render_block()
peak = block.magnitude() # JUCE-backed, no numpy
MidiRenderer.render_all() accepts as_=numpy.ndarray for the whole-buffer return.
5. Routing types renamed (PluginGraph -> PluginBus, GraphV2 -> PluginGraph)¶
0.2.0 gives the routing types a clean three-tier model. Unlike the AudioBuffer changes above, this is a hard rename with no compatibility shim -- the old names are gone in both Python and the C/C++ ABI.
| Concept | Old name | New name |
|---|---|---|
| Series (A -> B -> C) | PluginChain |
PluginChain (unchanged) |
| Parallel, summed (a mix bus) | PluginGraph |
PluginBus |
| Arbitrary node-to-node DAG | GraphV2 |
PluginGraph |
Watch the swap: PluginGraph now means the DAG executor, not the parallel bus. Code that used the old PluginGraph (the bus) must move to PluginBus; code that used GraphV2 must move to PluginGraph.
Python:
# Old: parallel-branches-summed
bus = minihost.PluginGraph(2, 2, max_block_size=512, sample_rate=48000.0)
# New:
bus = minihost.PluginBus(2, 2, max_block_size=512, sample_rate=48000.0)
# Old: general DAG executor
g = minihost.GraphV2(512, 48000.0)
# New:
g = minihost.PluginGraph(512, 48000.0)
Project files (load_project / render_project) are unaffected: the JSON schema is identical; only the Python class backing it was renamed.
New in the same release: PluginBus.process_midi(input, output, midi_in) fans the same MIDI to every branch, so one part can layer across parallel instruments. See the README "Parallel routing (PluginBus)" section.
C / C++ ABI (MH_API_VERSION bumped to 2.0.0):
| Old | New | |
|---|---|---|
| bus functions | mh_graph_* |
mh_bus_* |
| bus type | MH_PluginGraph |
MH_PluginBus |
| DAG functions | mh_graph_v2_* |
mh_graph_* |
| DAG type | MH_GraphV2 |
MH_PluginGraph |
| DAG C++ RAII wrapper | minihost::GraphV2 |
minihost::PluginGraph |
Source file names are retained for git history (minihost_graph.{h,cpp} is the bus; minihost_graph_v2.{h,cpp,hpp} is the DAG); a header note in each maps the file to its symbol family. Binaries linked against minihost should validate the ABI at startup: if (mh_api_version() < MH_API_VERSION_NUMBER) { /* mismatch */ }.
To migrate a C/C++ codebase, apply the substitutions in this order -- the ordering matters because mh_graph_ is a prefix of mh_graph_v2_, so a naive pass would corrupt the DAG symbols:
# function symbols (placeholder avoids the prefix collision)
perl -pi -e 's/mh_graph_v2_/MHTMP_/g; s/mh_graph_/mh_bus_/g; s/MHTMP_/mh_graph_/g' FILES...
# types and the C++ wrapper class
perl -pi -e 's/MH_GraphV2/MHTMP2/g; s/\bGraphV2\b/PluginGraph/g; s/MH_PluginGraph/MH_PluginBus/g; s/MHTMP2/MH_PluginGraph/g' FILES...
"If your old code did X, do Y" table¶
| Was | Quick fix | Recommended |
|---|---|---|
data, sr = read_audio(path) |
data, sr = read_audio(path, as_=np.ndarray) |
data, sr = read_audio(path) then use data.frames / data.channels |
data.shape (on read_audio result) |
unchanged (works on both types) | (data.channels, data.frames) |
data.shape[1] |
unchanged | data.frames |
data.shape[0] |
unchanged | data.channels |
audio = render_midi(p, mf) |
audio = render_midi(p, mf, as_=np.ndarray) |
audio = render_midi(p, mf) (returns AudioBuffer) |
for block in render_midi_stream(p, mf) |
for block in render_midi_stream(p, mf, as_=np.ndarray) |
for block in render_midi_stream(p, mf) (yields AudioBuffer) |
block = renderer.render_block() (was numpy) |
block = renderer.render_block().as_ndarray() |
block = renderer.render_block() (AudioBuffer) |
pip install minihost (relied on numpy) |
pip install minihost[numpy] |
same — there's no "no-numpy" recommendation if you actually use numpy |
| Custom DSP on the result (FFT, plot, etc.) | call .as_ndarray() once at the boundary |
same |
minihost.PluginGraph(in, out, ...) (parallel bus) |
minihost.PluginBus(in, out, ...) |
same — plus bus.process_midi(...) for layering |
minihost.GraphV2(block, sr) (DAG) |
minihost.PluginGraph(block, sr) |
same |
C: mh_graph_* / MH_PluginGraph (bus) |
mh_bus_* / MH_PluginBus |
same |
C: mh_graph_v2_* / MH_GraphV2 (DAG) |
mh_graph_* / MH_PluginGraph |
same |
C++: minihost::GraphV2 |
minihost::PluginGraph |
same |
AudioBuffer differences worth knowing¶
If you migrate to AudioBuffer, a few semantic differences from np.ndarray:
-
Slicing returns copies, not views.
buf[:, 100:200]allocates a new buffer. To mutate the parent through a slice, write to it via__setitem__:buf[:, 100:200] = source. -
2-axis indexing only.
buf[0]raisesTypeError; usebuf[0, :]. The reasoning: a 2D buffer's single-axis indexing is ambiguous (channel? frame? a 1D array?), and the explicit form is unambiguous. -
No strided slicing, fancy indexing, boolean indexing, or
Ellipsis. Each raises a clearTypeErrordirecting you to.as_ndarray(). -
.shapeis a tuple(channels, frames). Same as numpy.len(buf)returnschannels(matches numpy'slen()on 2D arrays). -
np.asarray(buf)andbuf.as_ndarray()both work (zero-copy via DLPack /__array__). Use them at the boundary where you actually need numpy semantics.
JUCE-backed DSP ops are exposed for the common cases so you rarely need numpy:
-
buf.clear(),buf.apply_gain(g),buf.apply_gain_ramp(start, count, lo, hi),buf.apply_gain_per_channel([g0, g1, ...]) -
buf.magnitude(),buf.get_rms_level(channel) -
buf.add_from(...),buf.add_from_with_ramp(...) -
buf.reverse(),buf.reverse_channel(ch) -
buf.copy()
What's new that you should consider adopting¶
These additions ship in the same release. They don't break existing code but they collapse common patterns:
process_audio_to_file — file → chain → file in one call¶
If you currently open read_audio, build a block loop, and call write_audio:
# Old:
data, sr = minihost.read_audio("in.wav")
output = np.zeros(...)
for start in range(0, ..., block_size):
plugin.process(data[:, start:start+block_size], output[:, start:start+block_size])
minihost.write_audio("out.wav", output, sr)
# New:
minihost.process_audio_to_file(plugin, "in.wav", "out.wav", tail_seconds=4.0)
process_audio_to_file handles block iteration, latency compensation, sample-rate matching, channel layout, and tail rendering.
MidiMapper — control-surface CCs to plugin parameters¶
For mapping a USB MIDI control surface onto plugin parameters at runtime:
plugin = minihost.Plugin("/path/to/synth.vst3", sample_rate=48000)
mapper = minihost.MidiMapper(plugin)
mapper.map_cc(channel=0, cc=7, param="Volume")
mapper.map_cc(channel=0, cc=74, param="Cutoff", curve="exp")
with minihost.AudioDevice(plugin) as audio:
with minihost.MidiIn.open(0, mapper):
input("Press Enter to stop...\n")
Or via the CLI:
minihost play /path/to/synth.vst3 --midi 0 \
--map 0:7:Volume \
--map 0:74:Cutoff:0:1:exp \
--map-file ~/.config/minihost/launch_control.json
--loop-midi and --loop-audio on minihost play¶
For live parameter tweaking against a repeating source:
minihost play /path/to/synth.vst3 --midi 0 \
--map 0:74:Cutoff:0:1:exp \
--loop-midi tests/_wav/test_pattern.mid
minihost play /path/to/reverb.vst3 --midi 0 \
--map 0:7:Mix \
--loop-audio guitar_dry.wav
Why the breaking change?¶
minihost's primary user is a plugin host user: render MIDI to WAV, process files through chains, drive real-time playback with a control surface, build automated audio pipelines. The DSP is in the plugin; the user's code is glue.
For that usage, numpy was overhead — a 25 MB transitive install, a 50-150 ms cold import, ABI churn (numpy 2.0) — paying for a feature most users didn't reach for. Numpy is genuinely useful when minihost output crosses into analysis (FFT, plotting, ML preprocessing); for everyone else it's incidental.
AudioBuffer matches what you actually move around — a fixed (channels, frames) float32 audio block — and lets the boundary with numpy be explicit and minimal.