Add generic headless plugin test host #1

Merged
jens merged 4 commits from testhost into master 2026-07-27 20:44:49 +02:00
14 changed files with 862 additions and 0 deletions
Showing only changes of commit 97b1c34cee - Show all commits
@@ -0,0 +1,66 @@
/*
AppConfig.h for the testhost tool.
Unlike JaySynth's own JuceLibraryCode/AppConfig.h, this one carries no
JucePlugin_* macros at all - those only matter to the plugin-client side
(juce_audio_plugin_client), which this executable never compiles or links.
*/
#ifndef __JUCE_APPCONFIG_TESTHOST__
#define __JUCE_APPCONFIG_TESTHOST__
//==============================================================================
#define JUCE_MODULE_AVAILABLE_juce_audio_basics 1
#define JUCE_MODULE_AVAILABLE_juce_audio_devices 1
#define JUCE_MODULE_AVAILABLE_juce_audio_formats 1
#define JUCE_MODULE_AVAILABLE_juce_audio_processors 1
#define JUCE_MODULE_AVAILABLE_juce_core 1
#define JUCE_MODULE_AVAILABLE_juce_data_structures 1
#define JUCE_MODULE_AVAILABLE_juce_events 1
#define JUCE_MODULE_AVAILABLE_juce_graphics 1
#define JUCE_MODULE_AVAILABLE_juce_gui_basics 1
#define JUCE_MODULE_AVAILABLE_juce_gui_extra 1
//==============================================================================
// juce_audio_devices flags: none of the real device backends are needed -
// the host never opens a soundcard, it only calls processBlock() directly.
#ifndef JUCE_ALSA
//#define JUCE_ALSA
#endif
#ifndef JUCE_JACK
//#define JUCE_JACK
#endif
//==============================================================================
// juce_audio_processors flags: this is the whole point of this tool - enable
// plugin hosting. VST2 today; VST3/LADSPA/(future LV2) can be added here the
// same way without touching any host source code.
#ifndef JUCE_PLUGINHOST_VST
#define JUCE_PLUGINHOST_VST 1
#endif
#ifndef JUCE_PLUGINHOST_VST3
//#define JUCE_PLUGINHOST_VST3
#endif
#ifndef JUCE_PLUGINHOST_AU
//#define JUCE_PLUGINHOST_AU
#endif
//==============================================================================
// juce_gui_basics flags: no window is ever shown, but the module still needs
// to compile/link (juce_audio_processors.h unconditionally includes it).
#ifndef JUCE_USE_XSHM
//#define JUCE_USE_XSHM
#endif
#ifndef JUCE_USE_XRENDER
//#define JUCE_USE_XRENDER
#endif
#ifndef JUCE_USE_XCURSOR
//#define JUCE_USE_XCURSOR
#endif
#endif // __JUCE_APPCONFIG_TESTHOST__
@@ -0,0 +1,31 @@
/*
JuceHeader.h for the testhost tool.
Deliberately omits juce_audio_plugin_client/juce_audio_plugin_client.h -
this executable hosts plugins, it isn't one - and the ProjectInfo block
that JaySynth's own JuceHeader.h derives from JucePlugin_* macros, since
none of those exist here.
*/
#ifndef __TESTHOST_JUCEHEADER__
#define __TESTHOST_JUCEHEADER__
#include "AppConfig.h"
#include "modules/juce_core/juce_core.h"
#include "modules/juce_events/juce_events.h"
#include "modules/juce_data_structures/juce_data_structures.h"
#include "modules/juce_graphics/juce_graphics.h"
#include "modules/juce_gui_basics/juce_gui_basics.h"
#include "modules/juce_gui_extra/juce_gui_extra.h"
#include "modules/juce_audio_basics/juce_audio_basics.h"
#include "modules/juce_audio_formats/juce_audio_formats.h"
#include "modules/juce_audio_devices/juce_audio_devices.h"
#include "modules/juce_audio_processors/juce_audio_processors.h"
#if ! DONT_SET_USING_JUCE_NAMESPACE
using namespace juce;
#endif
#include <stdint.h>
#endif // __TESTHOST_JUCEHEADER__
+11
View File
@@ -0,0 +1,11 @@
include $(MAKE_HOME)/defaults.mk
JUCE_PATH ?= $(realpath ../../../sdk/juce/JUCE-3.1.1)
VST2_SDK_PATH ?= $(realpath ../../../sdk/vst/vstsdk2.4)
include config.mk
CXX_SRCS := $(addprefix $(JUCE_MODULES)/, $(SOURCES))
INCLUDES += -I . -I $(JUCE_PATH) -I $(JUCE_PATH)/modules -I $(VST2_SDK_PATH) -I /usr/include/freetype2
include $(MAKE_HOME)/compile.mk
+19
View File
@@ -0,0 +1,19 @@
NAME := JuceLibraryCode
JUCE_MODULES := ${JUCE_PATH}/modules
# Deliberately does NOT include anything under juce_audio_plugin_client/ -
# those are the plugin-side VST/VST3 entry points (define main/VSTPluginMain,
# expect JucePlugin_* macros) and would collide with this executable's own
# main(). This package only compiles the module amalgams the host actually
# needs to load and drive a plugin.
SOURCES := juce_audio_basics/juce_audio_basics.cpp
SOURCES += juce_audio_devices/juce_audio_devices.cpp
SOURCES += juce_audio_formats/juce_audio_formats.cpp
SOURCES += juce_audio_processors/juce_audio_processors.cpp
SOURCES += juce_core/juce_core.cpp
SOURCES += juce_data_structures/juce_data_structures.cpp
SOURCES += juce_events/juce_events.cpp
SOURCES += juce_graphics/juce_graphics.cpp
SOURCES += juce_gui_basics/juce_gui_basics.cpp
SOURCES += juce_gui_extra/juce_gui_extra.cpp
+42
View File
@@ -0,0 +1,42 @@
include ../../make/defaults.mk
CONFIG ?= release
TARGET ?= testhost
JUCE_PATH := $(realpath ../../sdk/juce/JUCE-3.1.1)
VST2_SDK_PATH := $(realpath ../../sdk/vst/vstsdk2.4)
JUCE_LIBCODE_PATH := $(realpath ./JuceLibraryCode)
SRC_PATH := $(realpath ./src)
DEFINES := -D__cdecl=""
DEFINES += -DJUCE_GCC=1 -DLINUX=1
DEFINES += -DHAVE_LROUND
DEFINES += -DJUCE_PLUGINHOST_VST=1
DEFINES_debug += -DDEBUG=1 -D_DEBUG=1
DEFINES_release += -DNDEBUG
CXXFLAGS += -std=c++11
LIBS := -lstdc++ -lm -lGL -lX11 -lXext -lXinerama -lasound -ldl -lfreetype -lpthread -lrt
LDFLAGS += $(TARGET_ARCH) $(LIBDIR) -L/usr/X11R6/lib/
INCLUDES += -I $(JUCE_PATH) -I $(JUCE_PATH)/modules -I $(VST2_SDK_PATH) -I $(JUCE_LIBCODE_PATH) -I $(SRC_PATH)
PACKAGES := JuceLibraryCode host
export
all: app
app: objects link
.PHONY: ${PACKAGES}
objects: ${PACKAGES}
host:
@$(MAKE) -C $(SRC_PATH)
JuceLibraryCode:
@$(MAKE) -C $@
include $(MAKE_HOME)/link.mk
+123
View File
@@ -0,0 +1,123 @@
#include "AudioInputSource.h"
#include <cmath>
AudioInputSource::AudioInputSource()
: type (silence), frequencyHz (440.0), amplitude (0.5), sampleRate (44100.0),
phase (0.0), samplePosition (0)
{
zeromem (pinkState, sizeof (pinkState));
}
void AudioInputSource::setSilence()
{
type = silence;
}
void AudioInputSource::setSine (double frequencyHzToUse, float amplitudeToUse)
{
type = sine;
frequencyHz = frequencyHzToUse;
amplitude = amplitudeToUse;
phase = 0.0;
}
void AudioInputSource::setWhiteNoise (float amplitudeToUse)
{
type = whiteNoise;
amplitude = amplitudeToUse;
}
void AudioInputSource::setPinkNoise (float amplitudeToUse)
{
type = pinkNoise;
amplitude = amplitudeToUse;
zeromem (pinkState, sizeof (pinkState));
}
void AudioInputSource::setImpulse (float amplitudeToUse)
{
type = impulse;
amplitude = amplitudeToUse;
samplePosition = 0;
}
void AudioInputSource::prepare (double sampleRateToUse)
{
sampleRate = sampleRateToUse;
phase = 0.0;
samplePosition = 0;
}
// Paul Kellet's "economy" pink noise filter - good enough for exercising a
// plugin's frequency-dependent behaviour, not intended to be metrologically
// accurate.
float AudioInputSource::nextPinkSample (float white)
{
pinkState[0] = 0.99886f * pinkState[0] + white * 0.0555179f;
pinkState[1] = 0.99332f * pinkState[1] + white * 0.0750759f;
pinkState[2] = 0.96900f * pinkState[2] + white * 0.1538520f;
pinkState[3] = 0.86650f * pinkState[3] + white * 0.3104856f;
pinkState[4] = 0.55000f * pinkState[4] + white * 0.5329522f;
pinkState[5] = -0.7616f * pinkState[5] - white * 0.0168980f;
float pink = pinkState[0] + pinkState[1] + pinkState[2] + pinkState[3]
+ pinkState[4] + pinkState[5] + pinkState[6] + white * 0.5362f;
pinkState[6] = white * 0.115926f;
return pink * 0.11f; // roughly normalises the summed gain back towards +-1
}
void AudioInputSource::fillNextBlock (AudioSampleBuffer &buffer, int numChannels, int numSamples)
{
switch (type)
{
case silence:
for (int ch = 0; ch < numChannels; ++ch)
buffer.clear (ch, 0, numSamples);
break;
case sine:
{
const double twoPi = 2.0 * double_Pi;
const double phaseIncrement = twoPi * frequencyHz / sampleRate;
double p = phase;
for (int i = 0; i < numSamples; ++i)
{
const float sample = amplitude * (float) std::sin (p);
for (int ch = 0; ch < numChannels; ++ch)
buffer.setSample (ch, i, sample);
p += phaseIncrement;
}
phase = std::fmod (p, twoPi);
break;
}
case whiteNoise:
for (int i = 0; i < numSamples; ++i)
{
const float sample = amplitude * (2.0f * random.nextFloat() - 1.0f);
for (int ch = 0; ch < numChannels; ++ch)
buffer.setSample (ch, i, sample);
}
break;
case pinkNoise:
for (int i = 0; i < numSamples; ++i)
{
const float white = 2.0f * random.nextFloat() - 1.0f;
const float sample = amplitude * nextPinkSample (white);
for (int ch = 0; ch < numChannels; ++ch)
buffer.setSample (ch, i, sample);
}
break;
case impulse:
for (int i = 0; i < numSamples; ++i)
{
const float sample = (samplePosition == 0) ? amplitude : 0.0f;
for (int ch = 0; ch < numChannels; ++ch)
buffer.setSample (ch, i, sample);
++samplePosition;
}
break;
}
}
+58
View File
@@ -0,0 +1,58 @@
#ifndef TESTHOST_AUDIOINPUTSOURCE_H_INCLUDED
#define TESTHOST_AUDIOINPUTSOURCE_H_INCLUDED
#include <JuceHeader.h>
/** Fills a plugin's input channels before each processBlock() call.
silence (the default) is the correct choice for MIDI-driven instruments
like JaySynth, which never read their input at all. sine/whiteNoise/
pinkNoise/impulse exist so the same host can also test audio-effect
plugins (reverbs, EQs, compressors) that need a real input signal to do
anything meaningful.
*/
class AudioInputSource
{
public:
enum Type
{
silence = 0,
sine,
whiteNoise,
pinkNoise,
impulse
};
AudioInputSource();
void setSilence();
void setSine (double frequencyHz, float amplitude);
void setWhiteNoise (float amplitude);
void setPinkNoise (float amplitude);
/** A single amplitude-1 sample at t=0 followed by silence - the standard
stimulus for capturing an effect's impulse response. */
void setImpulse (float amplitude);
void prepare (double sampleRateToUse);
/** Fills channels [0, numChannels) of buffer with the next numSamples of
signal, advancing internal phase/position state across calls. Leaves
buffer's content beyond numChannels untouched. */
void fillNextBlock (AudioSampleBuffer &buffer, int numChannels, int numSamples);
private:
Type type;
double frequencyHz;
float amplitude;
double sampleRate;
double phase;
Random random;
int64 samplePosition;
float pinkState[7];
float nextPinkSample (float whiteSample);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioInputSource)
};
#endif // TESTHOST_AUDIOINPUTSOURCE_H_INCLUDED
+2
View File
@@ -0,0 +1,2 @@
include ./config.mk
include $(MAKE_HOME)/compile.mk
+107
View File
@@ -0,0 +1,107 @@
#include "PluginHost.h"
#include <cstdio>
PluginHost::PluginHost()
{
// VST2 today. Adding VST3PluginFormat/LADSPAPluginFormat (or, later, an
// LV2 format) is just another addFormat() call here - load()/process()/
// the parameter and state APIs below never need to change.
formatManager.addFormat (new VSTPluginFormat());
}
PluginHost::~PluginHost()
{
releaseResources();
}
bool PluginHost::load (const String &pluginPath, double sampleRate, int blockSize)
{
for (int i = 0; i < formatManager.getNumFormats(); ++i)
{
AudioPluginFormat *format = formatManager.getFormat (i);
OwnedArray<PluginDescription> found;
format->findAllTypesForFile (found, pluginPath);
if (found.size() == 0)
continue;
String error;
plugin = formatManager.createPluginInstance (*found[0], sampleRate, blockSize, error);
if (plugin == nullptr)
{
fprintf (stderr, "testhost: %s recognised '%s' but failed to load it: %s\n",
format->getName().toRawUTF8(), pluginPath.toRawUTF8(), error.toRawUTF8());
return false;
}
plugin->prepareToPlay (sampleRate, blockSize);
return true;
}
fprintf (stderr, "testhost: no registered plugin format recognised '%s'\n", pluginPath.toRawUTF8());
return false;
}
void PluginHost::releaseResources()
{
if (plugin != nullptr)
plugin->releaseResources();
}
int PluginHost::getNumInputChannels() const
{
return plugin != nullptr ? plugin->getNumInputChannels() : 0;
}
int PluginHost::getNumOutputChannels() const
{
return plugin != nullptr ? plugin->getNumOutputChannels() : 0;
}
void PluginHost::process (AudioSampleBuffer &buffer, MidiBuffer &midiMessages)
{
jassert (plugin != nullptr);
plugin->processBlock (buffer, midiMessages);
}
int PluginHost::getNumParameters() const
{
return plugin != nullptr ? plugin->getNumParameters() : 0;
}
float PluginHost::getParameter (int index) const
{
return plugin != nullptr ? plugin->getParameter (index) : 0.0f;
}
void PluginHost::setParameter (int index, float value)
{
if (plugin != nullptr)
plugin->setParameter (index, value);
}
void PluginHost::getCurrentProgramStateInformation (MemoryBlock &destData)
{
if (plugin != nullptr)
plugin->getCurrentProgramStateInformation (destData);
}
void PluginHost::setCurrentProgramStateInformation (const void *data, int sizeInBytes)
{
if (plugin != nullptr)
plugin->setCurrentProgramStateInformation (data, sizeInBytes);
}
void PluginHost::getStateInformation (MemoryBlock &destData)
{
if (plugin != nullptr)
plugin->getStateInformation (destData);
}
void PluginHost::setStateInformation (const void *data, int sizeInBytes)
{
if (plugin != nullptr)
plugin->setStateInformation (data, sizeInBytes);
}
+62
View File
@@ -0,0 +1,62 @@
#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
+55
View File
@@ -0,0 +1,55 @@
#include "WavRecorder.h"
#include <cstdio>
WavRecorder::WavRecorder()
{
}
WavRecorder::~WavRecorder()
{
stop();
}
bool WavRecorder::start (const File &destFile, double sampleRate, int numChannels)
{
if (numChannels <= 0)
{
fprintf (stderr, "testhost: cannot record - plugin reports %d output channels\n", numChannels);
return false;
}
destFile.deleteFile();
ScopedPointer<FileOutputStream> stream (destFile.createOutputStream());
if (stream == nullptr)
{
fprintf (stderr, "testhost: could not open '%s' for writing\n", destFile.getFullPathName().toRawUTF8());
return false;
}
WavAudioFormat wavFormat;
AudioFormatWriter *newWriter = wavFormat.createWriterFor (stream, sampleRate,
(unsigned int) numChannels, 24, StringPairArray(), 0);
if (newWriter == nullptr)
{
fprintf (stderr, "testhost: WavAudioFormat rejected the requested format (rate=%.0f, channels=%d)\n",
sampleRate, numChannels);
return false;
}
stream.release(); // now owned by newWriter
writer = newWriter;
return true;
}
void WavRecorder::write (const AudioSampleBuffer &buffer, int numSamples)
{
if (writer != nullptr)
writer->writeFromAudioSampleBuffer (buffer, 0, numSamples);
}
void WavRecorder::stop()
{
writer = nullptr; // AudioFormatWriter's destructor flushes and finalises the file
}
+34
View File
@@ -0,0 +1,34 @@
#ifndef TESTHOST_WAVRECORDER_H_INCLUDED
#define TESTHOST_WAVRECORDER_H_INCLUDED
#include <JuceHeader.h>
/** Thin wrapper over WavAudioFormat/AudioFormatWriter. Only active once
start() has been called with a destination file - a pure performance run
has no reason to pay for disk I/O.
*/
class WavRecorder
{
public:
WavRecorder();
~WavRecorder();
/** Channel count comes from whatever the plugin under test actually
reports (PluginHost::getNumOutputChannels()) - never assumed. */
bool start (const File &destFile, double sampleRate, int numChannels);
bool isRecording() const { return writer != nullptr; }
/** Writes numSamples samples from buffer's first numChannels channels. */
void write (const AudioSampleBuffer &buffer, int numSamples);
/** Flushes and finalises the file. Also happens automatically on destruction. */
void stop();
private:
ScopedPointer<AudioFormatWriter> writer;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WavRecorder)
};
#endif // TESTHOST_WAVRECORDER_H_INCLUDED
+6
View File
@@ -0,0 +1,6 @@
NAME := host
CXX_SRCS := main.cpp
CXX_SRCS += PluginHost.cpp
CXX_SRCS += AudioInputSource.cpp
CXX_SRCS += WavRecorder.cpp
+246
View File
@@ -0,0 +1,246 @@
#include <JuceHeader.h>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include "PluginHost.h"
#include "AudioInputSource.h"
#include "WavRecorder.h"
namespace
{
void printUsage()
{
printf (
"testhost - headless plugin test host\n"
"\n"
"Usage:\n"
" testhost --plugin <path.so> [options]\n"
"\n"
"Options:\n"
" --patch <file> load a .fxp/.fxb byte blob as the current program state\n"
" before rendering (see note below)\n"
" --note <note> <vel> MIDI note-on (0-127, 0-127) at sample 0, held for the\n"
" whole render, note-off in the final block\n"
" --param <index> <value> set parameter <index> to <value> (0.0-1.0) before rendering\n"
" --input <mode> fill the plugin's input channels with: silence (default),\n"
" sine[:freqHz], white, pink, or impulse\n"
" --duration <seconds> how much audio to render (default 2.0)\n"
" --sampleRate <hz> default 44100\n"
" --blockSize <n> default 512\n"
" --wav <file> record the plugin's output to a WAV file\n"
"\n"
"Note: --patch/--bank load the raw bytes JUCE's own AudioPluginInstance state\n"
"calls read/write (AudioProcessor::setCurrentProgramStateInformation), not\n"
"JaySynth's own GUI-triggered .fxp/.xmp file parsing - a real .fxp/.fxb file's\n"
"bytes are exactly what those calls expect for a hosted VST2 plugin.\n"
);
}
struct Args
{
String pluginPath;
String patchFile;
String wavFile;
double durationSeconds = 2.0;
double sampleRate = 44100.0;
int blockSize = 512;
bool hasNote = false;
int note = 60;
int velocity = 100;
bool hasParam = false;
int paramIndex = 0;
float paramValue = 0.0f;
AudioInputSource::Type inputType = AudioInputSource::silence;
double inputFrequencyHz = 440.0;
};
bool parseArgs (int argc, char *argv[], Args &args)
{
for (int i = 1; i < argc; ++i)
{
const String arg (argv[i]);
if (arg == "--plugin" && i + 1 < argc)
args.pluginPath = argv[++i];
else if (arg == "--patch" && i + 1 < argc)
args.patchFile = argv[++i];
else if (arg == "--wav" && i + 1 < argc)
args.wavFile = argv[++i];
else if (arg == "--duration" && i + 1 < argc)
args.durationSeconds = String (argv[++i]).getDoubleValue();
else if (arg == "--sampleRate" && i + 1 < argc)
args.sampleRate = String (argv[++i]).getDoubleValue();
else if (arg == "--blockSize" && i + 1 < argc)
args.blockSize = String (argv[++i]).getIntValue();
else if (arg == "--note" && i + 2 < argc)
{
args.hasNote = true;
args.note = String (argv[++i]).getIntValue();
args.velocity = String (argv[++i]).getIntValue();
}
else if (arg == "--param" && i + 2 < argc)
{
args.hasParam = true;
args.paramIndex = String (argv[++i]).getIntValue();
args.paramValue = String (argv[++i]).getFloatValue();
}
else if (arg == "--input" && i + 1 < argc)
{
const String mode (argv[++i]);
if (mode.startsWith ("sine"))
{
args.inputType = AudioInputSource::sine;
const int colon = mode.indexOfChar (':');
if (colon >= 0)
args.inputFrequencyHz = mode.substring (colon + 1).getDoubleValue();
}
else if (mode == "white")
args.inputType = AudioInputSource::whiteNoise;
else if (mode == "pink")
args.inputType = AudioInputSource::pinkNoise;
else if (mode == "impulse")
args.inputType = AudioInputSource::impulse;
else if (mode == "silence")
args.inputType = AudioInputSource::silence;
else
{
fprintf (stderr, "testhost: unknown --input mode '%s'\n", mode.toRawUTF8());
return false;
}
}
else if (arg == "--help" || arg == "-h")
return false;
else
{
fprintf (stderr, "testhost: unrecognised argument '%s'\n", arg.toRawUTF8());
return false;
}
}
if (args.pluginPath.isEmpty())
{
fprintf (stderr, "testhost: --plugin is required\n");
return false;
}
return true;
}
}
int main (int argc, char *argv[])
{
Args args;
if (! parseArgs (argc, argv, args))
{
printUsage();
return 1;
}
// Even a headless console app must bring up JUCE's message/GUI plumbing
// before touching any AudioProcessor/AudioPluginFormat class - no window
// is ever created, but juce_audio_processors.h unconditionally depends
// on juce_gui_basics.
ScopedJuceInitialiser_GUI juceInit;
PluginHost host;
if (! host.load (args.pluginPath, args.sampleRate, args.blockSize))
return 1;
const int numInputChannels = host.getNumInputChannels();
const int numOutputChannels = host.getNumOutputChannels();
printf ("testhost: loaded '%s' (%d in / %d out, %d parameters)\n",
args.pluginPath.toRawUTF8(), numInputChannels, numOutputChannels, host.getNumParameters());
if (args.patchFile.isNotEmpty())
{
MemoryBlock patchData;
if (! File (args.patchFile).loadFileAsData (patchData) || patchData.getSize() == 0)
{
fprintf (stderr, "testhost: could not read patch file '%s'\n", args.patchFile.toRawUTF8());
return 1;
}
host.setCurrentProgramStateInformation (patchData.getData(), (int) patchData.getSize());
}
if (args.hasParam)
host.setParameter (args.paramIndex, args.paramValue);
AudioInputSource inputSource;
inputSource.prepare (args.sampleRate);
switch (args.inputType)
{
case AudioInputSource::sine: inputSource.setSine (args.inputFrequencyHz, 0.5f); break;
case AudioInputSource::whiteNoise: inputSource.setWhiteNoise (0.5f); break;
case AudioInputSource::pinkNoise: inputSource.setPinkNoise (0.5f); break;
case AudioInputSource::impulse: inputSource.setImpulse (1.0f); break;
case AudioInputSource::silence:
default: inputSource.setSilence(); break;
}
WavRecorder recorder;
if (args.wavFile.isNotEmpty())
{
if (! recorder.start (File (args.wavFile), args.sampleRate, numOutputChannels))
return 1;
}
const int numChannels = jmax (numInputChannels, numOutputChannels, 1);
AudioSampleBuffer buffer (numChannels, args.blockSize);
const int64 totalSamples = (int64) (args.durationSeconds * args.sampleRate);
int64 samplesDone = 0;
bool sawNaN = false;
while (samplesDone < totalSamples)
{
const int numThisBlock = (int) jmin ((int64) args.blockSize, totalSamples - samplesDone);
buffer.clear();
if (numInputChannels > 0)
inputSource.fillNextBlock (buffer, numInputChannels, numThisBlock);
MidiBuffer midi;
if (args.hasNote)
{
if (samplesDone == 0)
midi.addEvent (MidiMessage::noteOn (1, args.note, (uint8) args.velocity), 0);
if (samplesDone + numThisBlock >= totalSamples)
midi.addEvent (MidiMessage::noteOff (1, args.note), jmax (0, numThisBlock - 1));
}
host.process (buffer, midi);
for (int ch = 0; ch < numOutputChannels && ! sawNaN; ++ch)
{
const float *data = buffer.getReadPointer (ch);
for (int i = 0; i < numThisBlock; ++i)
{
if (! std::isfinite (data[i]))
{
sawNaN = true;
break;
}
}
}
if (recorder.isRecording())
recorder.write (buffer, numThisBlock);
samplesDone += numThisBlock;
}
recorder.stop();
host.releaseResources();
if (sawNaN)
fprintf (stderr, "testhost: WARNING - output contained NaN/Inf samples\n");
printf ("testhost: rendered %.3fs (%lld samples)%s%s\n",
(double) samplesDone / args.sampleRate, (long long) samplesDone,
args.wavFile.isNotEmpty() ? " -> " : "",
args.wavFile.isNotEmpty() ? args.wavFile.toRawUTF8() : "");
return sawNaN ? 2 : 0;
}