Files
JaySynth/tools/testhost/src/PluginHost.h
T
jensandClaude Sonnet 5 97b1c34cee Add generic headless plugin test host (Phase 1: core + audio I/O)
tools/testhost/ is a standalone CLI executable, not a JaySynth-specific
tool: it drives any VST2 plugin offline (no audio device, no GUI event
loop) via JUCE's own format-agnostic AudioPluginFormatManager /
AudioPluginInstance hosting API, so adding another format later (VST3,
LADSPA, eventually LV2) is just another addFormat() call - none of the
host logic changes.

- PluginHost: format-agnostic load/prepare/process/release, parameter
  get/set, and patch/bank state I/O, all delegating straight to
  AudioProcessor. Always queries the plugin's real input/output channel
  counts rather than assuming "0 in" (instrument) or a fixed channel
  layout - the same code path drives JaySynth (0 in/2 out, MIDI-driven)
  and a stereo/mono effect identically.
- AudioInputSource: fills the plugin's input channels with silence
  (default, correct no-op for instruments), a sine tone, white/pink
  noise, or an impulse - what makes the host equally useful for
  effect plugins, which need a real input signal to do anything.
- WavRecorder: records the plugin's actual output channel count to a
  WAV file via WavAudioFormat/AudioFormatWriter.
- main.cpp: CLI (--plugin/--patch/--param/--note/--input/--duration/
  --wav/--sampleRate/--blockSize), with basic NaN/Inf output detection.

Has its own JuceLibraryCode-equivalent package that compiles only the
module amalgams needed for hosting (not juce_audio_plugin_client,
which is the plugin-side VST entry point and would collide with this
executable's own main()), reusing the already-vendored
sdk/juce/JUCE-3.1.1 rather than a second JUCE copy - the host and any
plugin it loads are decoupled at the VST2 ABI, not the JUCE source
level, so this doesn't need to track JaySynth's own future JUCE
version bump.

Verified: builds clean (no new warnings) via
`MAKE_HOME=... make -C tools/testhost`. Smoke-tested against two very
different real plugins:
- build/linux/release/JaySynth.so (0 in/2 out, MIDI-driven): --note
  60 100 --duration 2 --wav produces correct-length, non-silent,
  NaN-free stereo audio.
- /usr/lib/vst/ZamDelay-vst.so (1 in/1 out, real third-party VST2
  effect, no MIDI): --input pink --duration 2 --wav correctly sizes
  input/output to mono and produces non-silent, NaN-free audio -
  proof the host is generic, not JaySynth-shaped.
- /usr/lib/vst/ZamEQ2-vst.so: the host's own NaN/Inf detection caught
  a real NaN in this plugin's default state on first render - exactly
  the kind of regression this tool exists to catch without a GUI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dhtwRLARk4eiPngcQykLJ
2026-07-27 19:09:06 +02:00

63 lines
2.1 KiB
C++

#ifndef TESTHOST_PLUGINHOST_H_INCLUDED
#define TESTHOST_PLUGINHOST_H_INCLUDED
#include <JuceHeader.h>
/** Loads and drives a single plugin instance, offline (no audio device).
Deliberately format-agnostic: it talks only to AudioPluginFormatManager /
AudioPluginInstance / AudioProcessor, never to a specific format's SDK
structs. Adding another format later (VST3, LADSPA, eventually LV2) is
just another addFormat() call in the constructor - nothing here changes.
Also deliberately makes no assumption about the plugin's channel layout:
an instrument (0 inputs) and an effect (N inputs) are handled identically
by always asking the loaded AudioProcessor for its actual channel counts.
*/
class PluginHost
{
public:
PluginHost();
~PluginHost();
/** Scans pluginPath against every registered format and instantiates the
first match. Returns false (with a message printed to stderr) if no
registered format recognises the file, or if instantiation fails.
*/
bool load (const String &pluginPath, double sampleRate, int blockSize);
void releaseResources();
int getNumInputChannels() const;
int getNumOutputChannels() const;
/** Runs one block through the plugin. buffer must already be sized to
at least max(getNumInputChannels(), getNumOutputChannels()) channels
by numSamples samples; input channels should already be filled
(AudioInputSource's job), output channels are overwritten in place.
*/
void process (AudioSampleBuffer &buffer, MidiBuffer &midiMessages);
int getNumParameters() const;
float getParameter (int index) const;
void setParameter (int index, float value);
// Patch (single program) state.
void getCurrentProgramStateInformation (MemoryBlock &destData);
void setCurrentProgramStateInformation (const void *data, int sizeInBytes);
// Bank (full plugin) state.
void getStateInformation (MemoryBlock &destData);
void setStateInformation (const void *data, int sizeInBytes);
AudioPluginInstance *getInstance() const { return plugin; }
private:
AudioPluginFormatManager formatManager;
ScopedPointer<AudioPluginInstance> plugin;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PluginHost)
};
#endif // TESTHOST_PLUGINHOST_H_INCLUDED