Add generic headless plugin test host #1
@@ -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__
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
include ./config.mk
|
||||
include $(MAKE_HOME)/compile.mk
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
NAME := host
|
||||
|
||||
CXX_SRCS := main.cpp
|
||||
CXX_SRCS += PluginHost.cpp
|
||||
CXX_SRCS += AudioInputSource.cpp
|
||||
CXX_SRCS += WavRecorder.cpp
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user