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:
+32
-16
@@ -44,25 +44,41 @@ verified (PR https://git.jayfield.org/jens/JaySynth/pulls/1).
|
||||
`noteOn` with no matching `noteOff`) is untested.
|
||||
- [ ] **Polyphony/multiple simultaneous notes untested** - only ever a single
|
||||
held note in all smoke tests so far.
|
||||
- [ ] **`tests/run_tests.sh` never asserts specific expected values for
|
||||
`PluginHost::load()`/`getNumInputChannels()`/`getNumOutputChannels()`/
|
||||
`getNumParameters()`** - every test exercises them (they're printed in
|
||||
the load banner), but nothing pins e.g. "JaySynth must report exactly
|
||||
0 in / 2 out / 140 parameters," so a regression there would only be
|
||||
caught if it also broke something else. Same underlying gap as "no
|
||||
automated regression baseline comparison" above, just called out
|
||||
separately since it's about the load-time metadata rather than the
|
||||
rendered audio. See `tests/README.md`'s `PluginHost` coverage table.
|
||||
|
||||
## Investigate
|
||||
|
||||
- [ ] **JaySynth's compiled VST2 wrapper never advertises chunk support to
|
||||
the host, so real `.fxp`/`.fxb` sample files silently have no effect when
|
||||
loaded generically.** Found while building `tests/run_tests.sh`: loading
|
||||
a bundled sample under `extras/sounds/` via `--patch` produces byte-
|
||||
identical output to not loading anything (confirmed by diffing
|
||||
`--printParams`). The files are legitimately in VST2's opaque-chunk
|
||||
("FPCh") format, but JUCE 3.1.1's plugin-side VST wrapper
|
||||
(`juce_VST_Wrapper.cpp`, compiled into `JaySynth.so`) never sets
|
||||
`effFlagsProgramChunks` on the `AEffect` struct, so JUCE's own host-side
|
||||
`VSTPluginInstance::usesChunks()` returns false and silently no-ops both
|
||||
load and save. Not one of the tracked `TODO.md` (repo root) findings, and
|
||||
not attempted here - fixing it means patching JUCE's own VST wrapper,
|
||||
a bigger and riskier change than this pass's scope. See
|
||||
`tests/README.md` for the full writeup; `tests/run_tests.sh`'s round-trip
|
||||
tests work around it by using `testhost`'s own save output as the
|
||||
fixture instead of an external sample file.
|
||||
- [ ] **Bundled sample `.fxp` files under `extras/sounds/` silently have no
|
||||
effect when loaded via `--patch`** (confirmed by diffing `--printParams`
|
||||
output with and without `--patch` - zero parameters differ), even though
|
||||
loading doesn't crash. **Correction**: an earlier version of this entry
|
||||
claimed this was because JaySynth's compiled VST2 wrapper never sets
|
||||
`effFlagsProgramChunks`, so JUCE's host-side `usesChunks()` would see
|
||||
false and skip the chunk mechanism entirely. That diagnosis was wrong -
|
||||
disproved directly: `--saveBank`/`--savePatch` (the exact same generic
|
||||
`AudioProcessor::getStateInformation` path) produce real `FBCh`/`FPCh`-
|
||||
magic, GZIP-compressed-XML files (confirmed via hex dump and by feeding
|
||||
the raw inner chunk bytes straight to `effSetChunk` via the new
|
||||
`Vst2RawChunk` bypass, added specifically to rule this out - still no
|
||||
effect). So `usesChunks()` genuinely is true, and `test_chunk_roundtrip`
|
||||
in `tests/run_tests.sh` proves the real `bankEncodeXml`/`bankDecodeXml`
|
||||
path round-trips correctly for files this build produces. The actual
|
||||
cause of the *external* sample files failing is still unidentified -
|
||||
most likely those specific (10+ year old) files use a compression or
|
||||
XML-schema shape that `JaySynthAudioProcessor::setCurrentProgramState-
|
||||
Information`/`getXmlFromBinary` no longer parses, not a host/plugin
|
||||
capability mismatch. Not one of the tracked repo-root `TODO.md` findings.
|
||||
Worth a closer look at some point (e.g. hex-diff one of these files'
|
||||
decompressed XML against what this build produces), but not chased
|
||||
further here. See `tests/README.md`'s "corrected finding" note.
|
||||
- [ ] **DPF-based plugins print a non-fatal assertion on load.** All three
|
||||
Zam*-vst.so plugins tested (built with the DISTRHO Plugin Framework) print
|
||||
`assertion failure: "fIsActive" in .../DistrhoPluginInternal.hpp` during
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -63,40 +63,88 @@ restructuring. Sets a continuous parameter (index 0 = `SYNTH_PARAM_VOLUME`)
|
||||
to a distinctive value, saves, reloads in a fresh process, and checks the
|
||||
value reads back within a small tolerance (not exact equality — the
|
||||
slider/internal-value scaling curve, `toParam`/`toSlider`, isn't bit-exact
|
||||
across a round trip). Uses `testhost`'s own save→load cycle as the fixture,
|
||||
not an externally-provided `.fxp`/`.fxb` — see the chunk-format note below
|
||||
for why.
|
||||
across a round trip). Goes through JUCE's generic `AudioProcessor::get/
|
||||
setStateInformation`/`get/setCurrentProgramStateInformation` - see the
|
||||
corrected-finding note below for what that path actually does.
|
||||
|
||||
**`bank_chunk_roundtrip`** — same round-trip, but via `effGetChunk`/
|
||||
`effSetChunk` called *directly* on the raw `AEffect*` (`Vst2RawChunk.h`/
|
||||
`--saveBankChunk`/`--loadBankChunk`), bypassing JUCE's host-side wrapper
|
||||
entirely. Also positively confirms the real chunk path was used (not just
|
||||
that a parameter round-trips): checks the saved file starts with a zlib
|
||||
stream header (`0x78`) and is far larger than a raw 140-parameter float
|
||||
array could be (140×4 = 560 bytes) - a plain per-parameter fallback
|
||||
wouldn't look anything like this.
|
||||
|
||||
**`stability_sweep`** — loads every bundled `.fxp` under `extras/sounds/`
|
||||
and renders a held note through each, checking for crashes/hangs/NaN. A
|
||||
broad regression net, not a targeted test for any one finding.
|
||||
broad regression net, not a targeted test for any one finding. See the
|
||||
corrected-finding note below for why this only checks "doesn't crash," not
|
||||
"changes the patch."
|
||||
|
||||
## Two things discovered while building this suite that are worth knowing
|
||||
## `PluginHost` method coverage table
|
||||
|
||||
**The VCF coefficient-buffer bug above** (already fixed, see TODO.md).
|
||||
Every method on `PluginHost` (`src/PluginHost.h`), what it actually wires
|
||||
to on the VST2 side for a hosted plugin, and which test(s) exercise it.
|
||||
"Exercised" means the call happens during that test; "asserted" means the
|
||||
test actually checks something about its result, not just that it ran
|
||||
without crashing.
|
||||
|
||||
**JaySynth's compiled VST2 wrapper never advertises chunk support to the
|
||||
host**, so loading the bundled sample `.fxp` files through `testhost`'s
|
||||
generic `--patch` (i.e. through `AudioProcessor::setCurrentProgramState-
|
||||
Information`, exactly what any real VST2 host uses) has **zero effect** —
|
||||
confirmed by diffing `--printParams` output with and without `--patch`.
|
||||
Those sample files are legitimately in VST2's opaque-chunk ("FPCh") format
|
||||
(verified via hex dump: correct `CcnK`/`FPCh`/`Jsy1` header), but JUCE
|
||||
3.1.1's plugin-side VST wrapper (`sdk/juce/JUCE-3.1.1/modules/
|
||||
juce_audio_plugin_client/VST/juce_VST_Wrapper.cpp`, compiled into
|
||||
`JaySynth.so`) never sets the VST2 `effFlagsProgramChunks` flag on the
|
||||
`AEffect` struct, so JUCE's own host-side `VSTPluginInstance::usesChunks()`
|
||||
sees false and `setChunkData()` silently no-ops — for both loading *and*
|
||||
saving. This is **not** one of the tracked `TODO.md` findings and this
|
||||
suite doesn't attempt to fix it (it would mean patching JUCE's own VST
|
||||
wrapper, a materially bigger and riskier change). It's why
|
||||
`stability_sweep` only checks "doesn't crash," not "changes the patch," and
|
||||
why the round-trip tests use `testhost`'s own save output as the fixture
|
||||
rather than a real sample file — `testhost`'s save path has the same
|
||||
`usesChunks()`-is-false behaviour, so save and load are at least
|
||||
consistent with *each other*, even though neither talks to the external
|
||||
`.fxp` files' actual chunk data. Tracked as a discovered issue in
|
||||
`tools/testhost/TODO.md`'s "Investigate" section.
|
||||
| `PluginHost` method | VST2 wiring | Covering test(s) |
|
||||
|---|---|---|
|
||||
| `load()` | `effOpen` (via `createPluginInstance`), then inside `prepareToPlay`: `effSetSampleRate`, `effSetBlockSize`, `effMainsChanged(1)`, `effStartProcess` | Every test - exercised, not specifically asserted beyond "didn't return false" |
|
||||
| `releaseResources()` | `effMainsChanged(0)`/`effStopProcess` (`VSTPluginInstance::releaseResources`) | Every test calls it before exit (`main.cpp`) - exercised only, no assertion |
|
||||
| `getNumInputChannels()` / `getNumOutputChannels()` | Reads cached `AEffect::numInputs`/`numOutputs`, populated at `effOpen` | Every test (printed in the `"loaded ... (N in/N out...)"` banner) - exercised, not asserted against an expected value for any specific plugin |
|
||||
| `getNumParameters()` | Reads cached `AEffect::numParams` | Every test (same load banner; also implicit in every `--printParams` use) - exercised, not asserted |
|
||||
| `getParameter(index)` | `AEffect::getParameter` callback | `patch_save_load_roundtrip`, `bank_save_load_roundtrip`, `bank_chunk_roundtrip`, `patch_chunk_roundtrip` (all via `--printParams`) - asserted (value checked within tolerance) |
|
||||
| `setParameter(index, value)` | `AEffect::setParameter` callback | Same four round-trip tests (via `--param`) - asserted indirectly (the value it sets is what gets read back) |
|
||||
| `process()` - audio only | `processReplacing`/`processDoubleReplacing` | Every test - asserted only for NaN/Inf absence, not correctness of the DSP output itself |
|
||||
| `process()` - MIDI note on/off | `effProcessEvents` (`VstMidiEvent`) | `oversized_blocksize`, `nrpn_out_of_range`, `nrpn_in_range`, `stability_sweep` (all use `--note`) - exercised (audio is produced), not asserted beyond non-silence/no-NaN |
|
||||
| `process()` - MIDI CC/NRPN | `effProcessEvents` (`VstMidiEvent`, controller sub-type) | `nrpn_out_of_range`, `nrpn_in_range` - exercised and the specific regression (bounds check) is asserted via clean completion |
|
||||
| `getCurrentProgramStateInformation()` | `saveToFXBFile(isFXB=false)` → `effGetChunk(index=1)` (confirmed chunk-based, see below) | `patch_save_load_roundtrip` - asserted |
|
||||
| `setCurrentProgramStateInformation()` | `loadFromFXBFile` → `effSetChunk(index=1)` | `patch_save_load_roundtrip` - asserted; also used by `stability_sweep` via `--patch`, but **not asserted there** - see the corrected-finding note, those particular files don't actually change anything |
|
||||
| `getStateInformation()` | `saveToFXBFile(isFXB=true)` → `effGetChunk(index=0)` | `bank_save_load_roundtrip` - asserted |
|
||||
| `setStateInformation()` | `loadFromFXBFile` → `effSetChunk(index=0)` | `bank_save_load_roundtrip` - asserted |
|
||||
| `getInstance()` | n/a (plain accessor, returns the raw `AudioPluginInstance*`) | Used internally by every `Vst2RawChunk`-based test to reach the raw `AEffect*` |
|
||||
| *(bypass, not on `PluginHost`)* `Vst2RawChunk::getChunk`/`setChunk` | `effGetChunk`/`effSetChunk` called directly on the raw `AEffect*`, bypassing `usesChunks()` and `AudioProcessor` entirely | `bank_chunk_roundtrip`, `patch_chunk_roundtrip` - asserted, plus the zlib-header/size check that positively confirms the real chunk path ran |
|
||||
|
||||
**Known gaps**: `load()`/`releaseResources()`/`getNumInputChannels()`/
|
||||
`getNumOutputChannels()`/`getNumParameters()` are exercised by literally
|
||||
every test run but never *asserted* against a specific expected value for
|
||||
a specific plugin (e.g. nothing currently pins "JaySynth must report
|
||||
exactly 0 in / 2 out / 140 parameters" - a change to any of those would go
|
||||
unnoticed by this suite unless it also broke something else). Same for
|
||||
`process()`'s actual DSP correctness - the suite checks "ran without
|
||||
crashing/NaN," never "the audio sounds right." Both are reasonable
|
||||
follow-ups noted in `tools/testhost/TODO.md`.
|
||||
|
||||
## Corrected finding: it's not a chunk-advertisement gap after all
|
||||
|
||||
An earlier version of this suite (and `tools/testhost/TODO.md`) claimed
|
||||
that JaySynth's compiled VST2 wrapper never sets `effFlagsProgramChunks`,
|
||||
so JUCE's host-side `usesChunks()` would see false and silently skip the
|
||||
chunk mechanism - offered as the reason bundled sample `.fxp` files have no
|
||||
effect when loaded via `--patch`. **That diagnosis was wrong.** It's
|
||||
disproved directly: `--saveBank`/`--savePatch` (the exact same generic
|
||||
`AudioProcessor::getStateInformation` call) produce real `FBCh`/`FPCh`-
|
||||
magic files containing a genuine zlib-compressed XML blob (confirmed via
|
||||
hex dump), not a per-parameter fallback - meaning `usesChunks()` is
|
||||
genuinely `true`. To be certain, `Vst2RawChunk` was added specifically to
|
||||
rule out any remaining JUCE-side gating: it calls `effGetChunk`/
|
||||
`effSetChunk` straight on the raw `AEffect*`, and feeding a bundled sample
|
||||
file's inner chunk bytes through it *this way* still has zero effect.
|
||||
`test_chunk_roundtrip` above proves this exact mechanism round-trips
|
||||
correctly for files this build produces.
|
||||
|
||||
So the real, still-open question is narrower: **why do these specific
|
||||
(10+ year old) external sample files fail to apply**, given the mechanism
|
||||
that's supposed to load them demonstrably works? Most likely a compression
|
||||
or XML-schema shape from an older JaySynth/JUCE combination that
|
||||
`JaySynthAudioProcessor::setCurrentProgramStateInformation`/
|
||||
`getXmlFromBinary` no longer parses - not investigated further here (would
|
||||
mean decompressing and diffing one of these files' XML against what this
|
||||
build produces). Not one of the tracked repo-root `TODO.md` findings.
|
||||
Tracked in `tools/testhost/TODO.md`'s "Investigate" section.
|
||||
|
||||
## Debugging notes: how the VCF bug was actually found
|
||||
|
||||
|
||||
@@ -235,21 +235,108 @@ test_bank_roundtrip() {
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Test 6: stability sweep over bundled sample patches ------------------
|
||||
# --- Test 6: bank/patch round-trip via the raw VST2 chunk dispatcher ------
|
||||
# test_bank_roundtrip/test_patch_roundtrip above go through JUCE's generic
|
||||
# AudioProcessor::get/setStateInformation - which, it turns out, JaySynth's
|
||||
# usesChunks() flag genuinely is true for, so those already exercise the
|
||||
# real bankEncodeXml/bankDecodeXml (chunk) path, not a per-parameter
|
||||
# fallback (see tests/README.md's "corrected finding" note - an earlier
|
||||
# version of this comment claimed otherwise and was wrong). This test goes
|
||||
# one step further and calls effGetChunk/effSetChunk *directly* via
|
||||
# Vst2RawChunk (bypassing JUCE's host-side wrapper entirely), so it keeps
|
||||
# testing the real chunk path even if some future JUCE version's
|
||||
# usesChunks() gating ever changes. It also positively *proves* the chunk
|
||||
# path was used - not just that parameters round-trip - by checking the
|
||||
# saved file starts with a zlib stream header (0x78) and is far larger
|
||||
# than a raw 140-parameter float array (140*4=560 bytes) could be; the
|
||||
# regular/fallback format would look nothing like this.
|
||||
test_chunk_roundtrip() {
|
||||
# $1: "bank" or "patch"; $2: target parameter value (kept distinct
|
||||
# per call so a mixup between the two wouldn't silently pass)
|
||||
local kind="$1"
|
||||
local target_value="$2"
|
||||
local save_flag load_flag
|
||||
if [ "$kind" = "bank" ]; then
|
||||
save_flag="--saveBankChunk"; load_flag="--loadBankChunk"
|
||||
else
|
||||
save_flag="--savePatchChunk"; load_flag="--loadPatchChunk"
|
||||
fi
|
||||
|
||||
local name="${kind}_chunk_roundtrip (raw effGetChunk/effSetChunk, index=$([ "$kind" = bank ] && echo 0 || echo 1))"
|
||||
local chunk_file="$WORKDIR/roundtrip_${kind}.chunk"
|
||||
local log1="$WORKDIR/roundtrip_${kind}_chunk_save.log"
|
||||
local log2="$WORKDIR/roundtrip_${kind}_chunk_load.log"
|
||||
local tolerance="0.02"
|
||||
|
||||
if ! run_testhost 10 --param 0 "$target_value" $save_flag "$chunk_file" --duration 0.05 > "$log1" 2>&1; then
|
||||
fail "$name (save step failed, exit $? - see $log1)"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ ! -s "$chunk_file" ]; then
|
||||
fail "$name (no chunk file written)"
|
||||
return
|
||||
fi
|
||||
|
||||
local first_byte
|
||||
first_byte="$(od -An -tx1 -N1 "$chunk_file" | tr -d ' ')"
|
||||
local chunk_size
|
||||
chunk_size="$(stat -c%s "$chunk_file" 2>/dev/null || echo 0)"
|
||||
|
||||
if [ "$first_byte" != "78" ]; then
|
||||
fail "$name (saved file doesn't start with a zlib header (0x78) - got 0x$first_byte, this didn't go through bankEncodeXml/GZIP at all)"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$chunk_size" -le 1000 ]; then
|
||||
fail "$name (saved file is only $chunk_size bytes - too small to be the real XML/chunk format, looks like a per-parameter fallback)"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! run_testhost 10 $load_flag "$chunk_file" --printParams --duration 0.01 > "$log2" 2>&1; then
|
||||
fail "$name (load step failed, exit $? - see $log2)"
|
||||
return
|
||||
fi
|
||||
|
||||
local readback
|
||||
readback="$(grep '^PARAM 0 ' "$log2" | awk '{print $3}')"
|
||||
|
||||
if [ -z "$readback" ]; then
|
||||
fail "$name (PARAM 0 not found in output - see $log2)"
|
||||
return
|
||||
fi
|
||||
|
||||
local diff
|
||||
diff="$(awk -v a="$readback" -v b="$target_value" 'BEGIN { d = a - b; if (d < 0) d = -d; print d }')"
|
||||
local within_tolerance
|
||||
within_tolerance="$(awk -v d="$diff" -v t="$tolerance" 'BEGIN { print (d <= t) ? "1" : "0" }')"
|
||||
|
||||
if [ "$within_tolerance" = "1" ]; then
|
||||
pass "$name (chunk is $chunk_size bytes, starts 0x78; wrote $target_value, read back $readback)"
|
||||
else
|
||||
fail "$name (wrote $target_value, read back $readback - outside +-$tolerance tolerance - see $log1 / $log2)"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Test 7: stability sweep over bundled sample patches ------------------
|
||||
# Broad regression net: load every bundled .fxp under extras/sounds/ and
|
||||
# render a held note through it. NOTE: as discovered while building this
|
||||
# suite, these particular sample files are in VST2's opaque-chunk ("FPCh")
|
||||
# format, but JUCE 3.1.1's plugin-side VST wrapper (compiled into
|
||||
# JaySynth.so) never sets effFlagsProgramChunks, so JUCE's *host*-side
|
||||
# VSTPluginInstance::usesChunks() sees false and silently ignores the
|
||||
# chunk data entirely - confirmed by diffing --printParams output with and
|
||||
# without --patch: zero parameters differ. This is a real, separate
|
||||
# finding (see tests/README.md and testhost/TODO.md) - NOT one of the
|
||||
# tracked TODO.md findings, and NOT something this suite attempts to fix.
|
||||
# So this test only checks "loading this file doesn't crash the process",
|
||||
# not "loading this file changes the patch" - it still exercises the
|
||||
# malloc/delete[] paths touched by the Memory-management fixes as a broad
|
||||
# smoke test, just not the patch-content-application path.
|
||||
# render a held note through it. NOTE: these particular sample files
|
||||
# (10+ years old) load without crashing but - confirmed by diffing
|
||||
# --printParams output with and without --patch - have no effect on the
|
||||
# patch's parameters. This is NOT the effFlagsProgramChunks/usesChunks()
|
||||
# gap an earlier version of this comment claimed (that was wrong - see
|
||||
# tests/README.md's "corrected finding" note; usesChunks() genuinely is
|
||||
# true, and test_chunk_roundtrip above proves the real chunk path works
|
||||
# with files this same build produces). The actual cause is still
|
||||
# unidentified - most likely these specific old files use a compression
|
||||
# or XML-schema shape that JaySynthAudioProcessor::setCurrentProgramState-
|
||||
# Information/getXmlFromBinary no longer parses, not a host-vs-plugin
|
||||
# capability mismatch. Tracked in tools/testhost/TODO.md's Investigate
|
||||
# section. So this test only checks "loading this file doesn't crash the
|
||||
# process", not "loading this file changes the patch" - it still
|
||||
# exercises the malloc/delete[] paths touched by the Memory-management
|
||||
# fixes as a broad smoke test, just not the patch-content-application
|
||||
# path.
|
||||
test_stability_sweep() {
|
||||
local sounds_dir="$REPO_ROOT/extras/sounds"
|
||||
local any=0
|
||||
@@ -298,6 +385,11 @@ test_patch_roundtrip
|
||||
test_bank_roundtrip
|
||||
log ""
|
||||
|
||||
log "Raw VST2 chunk dispatcher - proves the actual chunk-based path:"
|
||||
test_chunk_roundtrip bank 0.81
|
||||
test_chunk_roundtrip patch 0.44
|
||||
log ""
|
||||
|
||||
log "General stability sweep over bundled sample patches:"
|
||||
test_stability_sweep
|
||||
log ""
|
||||
|
||||
Reference in New Issue
Block a user