testhost: prove the actual chunk-based state path via raw VST2 dispatcher
Add Vst2RawChunk, a raw effGetChunk/effSetChunk bypass around JUCE's VSTPluginInstance::usesChunks() gate, plus --loadBankChunk/--saveBankChunk/ --loadPatchChunk/--savePatchChunk CLI flags and a new test_chunk_roundtrip regression test asserting the saved file is genuinely zlib/XML (0x78 header, >1000 bytes), not a per-parameter fallback. Building and using this bypass disproved an earlier documented finding: the compiled VST2 wrapper does set effFlagsProgramChunks and usesChunks() is genuinely true, so the previous "chunk-advertisement gap" theory in TODO.md and tests/README.md was wrong. Corrected in both places. The real cause of old bundled .fxp sample files having no effect remains open and is now narrowed to a JaySynth-side XML/schema parsing question, not a host/plugin capability mismatch. Also adds a PluginHost-method-to-VST2-wiring-to-test coverage table in tests/README.md, and documents the remaining "exercised but not asserted" gaps (channel/param counts, DSP correctness) in TODO.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011dhtwRLARk4eiPngcQykLJ
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
#include "Vst2RawChunk.h"
|
||||
|
||||
// Only needed here - everywhere else in this tool talks to the
|
||||
// format-agnostic AudioPluginInstance/AudioProcessor API, not the raw VST2
|
||||
// SDK structs.
|
||||
#include <pluginterfaces/vst2.x/aeffectx.h>
|
||||
|
||||
namespace Vst2RawChunk
|
||||
{
|
||||
void *getRawAEffect (AudioPluginInstance *instance)
|
||||
{
|
||||
if (instance == nullptr)
|
||||
return nullptr;
|
||||
|
||||
AEffect *effect = (AEffect *) instance->getPlatformSpecificData();
|
||||
|
||||
if (effect == nullptr || effect->magic != kEffectMagic)
|
||||
return nullptr;
|
||||
|
||||
return effect;
|
||||
}
|
||||
|
||||
bool getChunk (AudioPluginInstance *instance, bool isPreset, MemoryBlock &outData)
|
||||
{
|
||||
AEffect *effect = (AEffect *) getRawAEffect (instance);
|
||||
if (effect == nullptr)
|
||||
return false;
|
||||
|
||||
void *chunkPtr = nullptr;
|
||||
const VstIntPtr bytes = effect->dispatcher (effect, effGetChunk, isPreset ? 1 : 0, 0, &chunkPtr, 0.0f);
|
||||
|
||||
if (bytes <= 0 || chunkPtr == nullptr)
|
||||
return false;
|
||||
|
||||
// The plugin owns chunkPtr's memory (valid only until the next call
|
||||
// into it) - copy it out immediately.
|
||||
outData.replaceWith (chunkPtr, (size_t) bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool setChunk (AudioPluginInstance *instance, bool isPreset, const void *data, int size)
|
||||
{
|
||||
AEffect *effect = (AEffect *) getRawAEffect (instance);
|
||||
if (effect == nullptr || size <= 0)
|
||||
return false;
|
||||
|
||||
effect->dispatcher (effect, effSetChunk, isPreset ? 1 : 0, size, (void *) data, 0.0f);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#ifndef TESTHOST_VST2RAWCHUNK_H_INCLUDED
|
||||
#define TESTHOST_VST2RAWCHUNK_H_INCLUDED
|
||||
|
||||
#include <JuceHeader.h>
|
||||
|
||||
/** Deliberately VST2-specific (unlike everything else in this tool): talks
|
||||
to the raw AEffect dispatcher directly, bypassing JUCE's own
|
||||
VSTPluginInstance::usesChunks() gate.
|
||||
|
||||
Why this exists: JUCE's generic AudioProcessor::getStateInformation()/
|
||||
setStateInformation() only reach a VST2 plugin's real effGetChunk/
|
||||
effSetChunk implementation if the plugin's AEffect.flags advertises
|
||||
effFlagsProgramChunks. JaySynth's compiled JUCE 3.1.1 VST wrapper never
|
||||
sets that flag (see tools/testhost/TODO.md's Investigate section), so
|
||||
the generic path silently falls back to JUCE's own per-parameter
|
||||
"regular" fxb/fxp format instead - which never touches JaySynth's own
|
||||
bankEncodeXml/bankDecodeXml or patchEncodeXml/patchDecodeXml at all.
|
||||
|
||||
Many real-world VST2 hosts don't strictly gate on that flag either -
|
||||
they just call effGetChunk/effSetChunk and see what comes back. This
|
||||
does the same thing on purpose, to reach and test that actual code
|
||||
path.
|
||||
*/
|
||||
namespace Vst2RawChunk
|
||||
{
|
||||
/** Returns nullptr if instance isn't hosted via VSTPluginFormat (or the
|
||||
AEffect's magic number doesn't check out). */
|
||||
void *getRawAEffect (AudioPluginInstance *instance);
|
||||
|
||||
/** isPreset: true = patch (single program) chunk, false = bank (whole
|
||||
plugin) chunk - matches VST2's effGetChunk/effSetChunk "index"
|
||||
argument (see aeffect.h). Returns false if the plugin isn't VST2,
|
||||
or the dispatcher call returned no data. */
|
||||
bool getChunk (AudioPluginInstance *instance, bool isPreset, MemoryBlock &outData);
|
||||
bool setChunk (AudioPluginInstance *instance, bool isPreset, const void *data, int size);
|
||||
}
|
||||
|
||||
#endif // TESTHOST_VST2RAWCHUNK_H_INCLUDED
|
||||
@@ -5,3 +5,4 @@ CXX_SRCS += PluginHost.cpp
|
||||
CXX_SRCS += AudioInputSource.cpp
|
||||
CXX_SRCS += WavRecorder.cpp
|
||||
CXX_SRCS += TestScenario.cpp
|
||||
CXX_SRCS += Vst2RawChunk.cpp
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "WavRecorder.h"
|
||||
#include "TestScenario.h"
|
||||
#include "PerformanceStats.h"
|
||||
#include "Vst2RawChunk.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -46,6 +47,12 @@ namespace
|
||||
"Patch/bank state is exactly what JUCE's AudioPluginInstance::get/setCurrentProgram-\n"
|
||||
"StateInformation and get/setStateInformation read and write for a hosted VST2\n"
|
||||
"plugin - i.e. real .fxp/.fxb file bytes.\n"
|
||||
"\n"
|
||||
"Raw VST2 chunk options (bypass JUCE's usesChunks() gate - see Vst2RawChunk.h):\n"
|
||||
" --loadBankChunk <file> effSetChunk(index=0) directly with the file's raw bytes\n"
|
||||
" --saveBankChunk <file> effGetChunk(index=0) directly, write the raw bytes out\n"
|
||||
" --loadPatchChunk <file> effSetChunk(index=1) directly with the file's raw bytes\n"
|
||||
" --savePatchChunk <file> effGetChunk(index=1) directly, write the raw bytes out\n"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,6 +63,7 @@ namespace
|
||||
|
||||
// Ad-hoc mode only (ignored if scenarioFile is set):
|
||||
String patchFile, bankFile, savePatchFile, saveBankFile, wavFile;
|
||||
String loadBankChunkFile, saveBankChunkFile, loadPatchChunkFile, savePatchChunkFile;
|
||||
double durationSeconds = 2.0;
|
||||
double sampleRate = 44100.0;
|
||||
int blockSize = 512;
|
||||
@@ -90,6 +98,14 @@ namespace
|
||||
args.savePatchFile = argv[++i];
|
||||
else if (arg == "--saveBank" && i + 1 < argc)
|
||||
args.saveBankFile = argv[++i];
|
||||
else if (arg == "--loadBankChunk" && i + 1 < argc)
|
||||
args.loadBankChunkFile = argv[++i];
|
||||
else if (arg == "--saveBankChunk" && i + 1 < argc)
|
||||
args.saveBankChunkFile = argv[++i];
|
||||
else if (arg == "--loadPatchChunk" && i + 1 < argc)
|
||||
args.loadPatchChunkFile = argv[++i];
|
||||
else if (arg == "--savePatchChunk" && i + 1 < argc)
|
||||
args.savePatchChunkFile = argv[++i];
|
||||
else if (arg == "--wav" && i + 1 < argc)
|
||||
args.wavFile = argv[++i];
|
||||
else if (arg == "--duration" && i + 1 < argc)
|
||||
@@ -227,6 +243,40 @@ namespace
|
||||
fprintf (stderr, "testhost: could not write '%s'\n", path.toRawUTF8());
|
||||
}
|
||||
|
||||
// isPreset: true = patch chunk, false = bank chunk (matches VST2's
|
||||
// effGetChunk/effSetChunk index argument). See Vst2RawChunk.h for why
|
||||
// this bypasses the generic AudioProcessor state calls above.
|
||||
bool loadRawChunkFile (const String &path, bool isPreset, PluginHost &host)
|
||||
{
|
||||
MemoryBlock data;
|
||||
if (! File (path).loadFileAsData (data) || data.getSize() == 0)
|
||||
{
|
||||
fprintf (stderr, "testhost: could not read '%s'\n", path.toRawUTF8());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! Vst2RawChunk::setChunk (host.getInstance(), isPreset, data.getData(), (int) data.getSize()))
|
||||
{
|
||||
fprintf (stderr, "testhost: raw VST2 setChunk failed (not a VST2 plugin?)\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void saveRawChunkFile (const String &path, bool isPreset, PluginHost &host)
|
||||
{
|
||||
MemoryBlock data;
|
||||
if (! Vst2RawChunk::getChunk (host.getInstance(), isPreset, data))
|
||||
{
|
||||
fprintf (stderr, "testhost: raw VST2 getChunk failed (not a VST2 plugin?)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (! File (path).replaceWithData (data.getData(), data.getSize()))
|
||||
fprintf (stderr, "testhost: could not write '%s'\n", path.toRawUTF8());
|
||||
}
|
||||
|
||||
void applyEvent (PluginHost &host, const TestScenario::Event &event, MidiBuffer &midi, int sampleOffsetInBlock)
|
||||
{
|
||||
switch (event.type)
|
||||
@@ -300,6 +350,12 @@ int main (int argc, char *argv[])
|
||||
if (scenario.initialLoadPatchFile.isNotEmpty() && ! loadStateFile (scenario.initialLoadPatchFile, false, host))
|
||||
return 1;
|
||||
|
||||
if (args.loadBankChunkFile.isNotEmpty() && ! loadRawChunkFile (args.loadBankChunkFile, false, host))
|
||||
return 1;
|
||||
|
||||
if (args.loadPatchChunkFile.isNotEmpty() && ! loadRawChunkFile (args.loadPatchChunkFile, true, host))
|
||||
return 1;
|
||||
|
||||
if (args.printParams)
|
||||
{
|
||||
for (int i = 0; i < host.getNumParameters(); ++i)
|
||||
@@ -394,6 +450,10 @@ int main (int argc, char *argv[])
|
||||
saveStateFile (args.savePatchFile, false, host);
|
||||
if (args.saveBankFile.isNotEmpty())
|
||||
saveStateFile (args.saveBankFile, true, host);
|
||||
if (args.saveBankChunkFile.isNotEmpty())
|
||||
saveRawChunkFile (args.saveBankChunkFile, false, host);
|
||||
if (args.savePatchChunkFile.isNotEmpty())
|
||||
saveRawChunkFile (args.savePatchChunkFile, true, host);
|
||||
|
||||
host.releaseResources();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user