diff --git a/tools/testhost/JuceLibraryCode/AppConfig.h b/tools/testhost/JuceLibraryCode/AppConfig.h new file mode 100644 index 0000000..5146c9e --- /dev/null +++ b/tools/testhost/JuceLibraryCode/AppConfig.h @@ -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__ diff --git a/tools/testhost/JuceLibraryCode/JuceHeader.h b/tools/testhost/JuceLibraryCode/JuceHeader.h new file mode 100644 index 0000000..0967cad --- /dev/null +++ b/tools/testhost/JuceLibraryCode/JuceHeader.h @@ -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 + +#endif // __TESTHOST_JUCEHEADER__ diff --git a/tools/testhost/JuceLibraryCode/Makefile b/tools/testhost/JuceLibraryCode/Makefile new file mode 100644 index 0000000..0a21697 --- /dev/null +++ b/tools/testhost/JuceLibraryCode/Makefile @@ -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 diff --git a/tools/testhost/JuceLibraryCode/config.mk b/tools/testhost/JuceLibraryCode/config.mk new file mode 100644 index 0000000..bbffea0 --- /dev/null +++ b/tools/testhost/JuceLibraryCode/config.mk @@ -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 diff --git a/tools/testhost/Makefile b/tools/testhost/Makefile new file mode 100644 index 0000000..7c796cf --- /dev/null +++ b/tools/testhost/Makefile @@ -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 diff --git a/tools/testhost/src/AudioInputSource.cpp b/tools/testhost/src/AudioInputSource.cpp new file mode 100644 index 0000000..ad8d080 --- /dev/null +++ b/tools/testhost/src/AudioInputSource.cpp @@ -0,0 +1,123 @@ +#include "AudioInputSource.h" +#include + +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; + } +} diff --git a/tools/testhost/src/AudioInputSource.h b/tools/testhost/src/AudioInputSource.h new file mode 100644 index 0000000..17ca7ec --- /dev/null +++ b/tools/testhost/src/AudioInputSource.h @@ -0,0 +1,58 @@ +#ifndef TESTHOST_AUDIOINPUTSOURCE_H_INCLUDED +#define TESTHOST_AUDIOINPUTSOURCE_H_INCLUDED + +#include + +/** 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 diff --git a/tools/testhost/src/Makefile b/tools/testhost/src/Makefile new file mode 100644 index 0000000..dfe86f6 --- /dev/null +++ b/tools/testhost/src/Makefile @@ -0,0 +1,2 @@ +include ./config.mk +include $(MAKE_HOME)/compile.mk diff --git a/tools/testhost/src/PluginHost.cpp b/tools/testhost/src/PluginHost.cpp new file mode 100644 index 0000000..1de9eee --- /dev/null +++ b/tools/testhost/src/PluginHost.cpp @@ -0,0 +1,107 @@ +#include "PluginHost.h" +#include + +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 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); +} diff --git a/tools/testhost/src/PluginHost.h b/tools/testhost/src/PluginHost.h new file mode 100644 index 0000000..dfe6cb2 --- /dev/null +++ b/tools/testhost/src/PluginHost.h @@ -0,0 +1,62 @@ +#ifndef TESTHOST_PLUGINHOST_H_INCLUDED +#define TESTHOST_PLUGINHOST_H_INCLUDED + +#include + +/** 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 plugin; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PluginHost) +}; + +#endif // TESTHOST_PLUGINHOST_H_INCLUDED diff --git a/tools/testhost/src/WavRecorder.cpp b/tools/testhost/src/WavRecorder.cpp new file mode 100644 index 0000000..4b941ac --- /dev/null +++ b/tools/testhost/src/WavRecorder.cpp @@ -0,0 +1,55 @@ +#include "WavRecorder.h" +#include + +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 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 +} diff --git a/tools/testhost/src/WavRecorder.h b/tools/testhost/src/WavRecorder.h new file mode 100644 index 0000000..c8c6112 --- /dev/null +++ b/tools/testhost/src/WavRecorder.h @@ -0,0 +1,34 @@ +#ifndef TESTHOST_WAVRECORDER_H_INCLUDED +#define TESTHOST_WAVRECORDER_H_INCLUDED + +#include + +/** 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 writer; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WavRecorder) +}; + +#endif // TESTHOST_WAVRECORDER_H_INCLUDED diff --git a/tools/testhost/src/config.mk b/tools/testhost/src/config.mk new file mode 100644 index 0000000..fc5ed94 --- /dev/null +++ b/tools/testhost/src/config.mk @@ -0,0 +1,6 @@ +NAME := host + +CXX_SRCS := main.cpp +CXX_SRCS += PluginHost.cpp +CXX_SRCS += AudioInputSource.cpp +CXX_SRCS += WavRecorder.cpp diff --git a/tools/testhost/src/main.cpp b/tools/testhost/src/main.cpp new file mode 100644 index 0000000..0eda398 --- /dev/null +++ b/tools/testhost/src/main.cpp @@ -0,0 +1,246 @@ +#include +#include +#include +#include + +#include "PluginHost.h" +#include "AudioInputSource.h" +#include "WavRecorder.h" + +namespace +{ + void printUsage() + { + printf ( + "testhost - headless plugin test host\n" + "\n" + "Usage:\n" + " testhost --plugin [options]\n" + "\n" + "Options:\n" + " --patch load a .fxp/.fxb byte blob as the current program state\n" + " before rendering (see note below)\n" + " --note MIDI note-on (0-127, 0-127) at sample 0, held for the\n" + " whole render, note-off in the final block\n" + " --param set parameter to (0.0-1.0) before rendering\n" + " --input fill the plugin's input channels with: silence (default),\n" + " sine[:freqHz], white, pink, or impulse\n" + " --duration how much audio to render (default 2.0)\n" + " --sampleRate default 44100\n" + " --blockSize default 512\n" + " --wav 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; +}