testhost: prove the actual chunk-based state path via raw VST2 dispatcher #3
+32
-16
@@ -44,25 +44,41 @@ verified (PR https://git.jayfield.org/jens/JaySynth/pulls/1).
|
|||||||
`noteOn` with no matching `noteOff`) is untested.
|
`noteOn` with no matching `noteOff`) is untested.
|
||||||
- [ ] **Polyphony/multiple simultaneous notes untested** - only ever a single
|
- [ ] **Polyphony/multiple simultaneous notes untested** - only ever a single
|
||||||
held note in all smoke tests so far.
|
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
|
## Investigate
|
||||||
|
|
||||||
- [ ] **JaySynth's compiled VST2 wrapper never advertises chunk support to
|
- [ ] **Bundled sample `.fxp` files under `extras/sounds/` silently have no
|
||||||
the host, so real `.fxp`/`.fxb` sample files silently have no effect when
|
effect when loaded via `--patch`** (confirmed by diffing `--printParams`
|
||||||
loaded generically.** Found while building `tests/run_tests.sh`: loading
|
output with and without `--patch` - zero parameters differ), even though
|
||||||
a bundled sample under `extras/sounds/` via `--patch` produces byte-
|
loading doesn't crash. **Correction**: an earlier version of this entry
|
||||||
identical output to not loading anything (confirmed by diffing
|
claimed this was because JaySynth's compiled VST2 wrapper never sets
|
||||||
`--printParams`). The files are legitimately in VST2's opaque-chunk
|
`effFlagsProgramChunks`, so JUCE's host-side `usesChunks()` would see
|
||||||
("FPCh") format, but JUCE 3.1.1's plugin-side VST wrapper
|
false and skip the chunk mechanism entirely. That diagnosis was wrong -
|
||||||
(`juce_VST_Wrapper.cpp`, compiled into `JaySynth.so`) never sets
|
disproved directly: `--saveBank`/`--savePatch` (the exact same generic
|
||||||
`effFlagsProgramChunks` on the `AEffect` struct, so JUCE's own host-side
|
`AudioProcessor::getStateInformation` path) produce real `FBCh`/`FPCh`-
|
||||||
`VSTPluginInstance::usesChunks()` returns false and silently no-ops both
|
magic, GZIP-compressed-XML files (confirmed via hex dump and by feeding
|
||||||
load and save. Not one of the tracked `TODO.md` (repo root) findings, and
|
the raw inner chunk bytes straight to `effSetChunk` via the new
|
||||||
not attempted here - fixing it means patching JUCE's own VST wrapper,
|
`Vst2RawChunk` bypass, added specifically to rule this out - still no
|
||||||
a bigger and riskier change than this pass's scope. See
|
effect). So `usesChunks()` genuinely is true, and `test_chunk_roundtrip`
|
||||||
`tests/README.md` for the full writeup; `tests/run_tests.sh`'s round-trip
|
in `tests/run_tests.sh` proves the real `bankEncodeXml`/`bankDecodeXml`
|
||||||
tests work around it by using `testhost`'s own save output as the
|
path round-trips correctly for files this build produces. The actual
|
||||||
fixture instead of an external sample file.
|
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
|
- [ ] **DPF-based plugins print a non-fatal assertion on load.** All three
|
||||||
Zam*-vst.so plugins tested (built with the DISTRHO Plugin Framework) print
|
Zam*-vst.so plugins tested (built with the DISTRHO Plugin Framework) print
|
||||||
`assertion failure: "fIsActive" in .../DistrhoPluginInternal.hpp` during
|
`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 += AudioInputSource.cpp
|
||||||
CXX_SRCS += WavRecorder.cpp
|
CXX_SRCS += WavRecorder.cpp
|
||||||
CXX_SRCS += TestScenario.cpp
|
CXX_SRCS += TestScenario.cpp
|
||||||
|
CXX_SRCS += Vst2RawChunk.cpp
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "WavRecorder.h"
|
#include "WavRecorder.h"
|
||||||
#include "TestScenario.h"
|
#include "TestScenario.h"
|
||||||
#include "PerformanceStats.h"
|
#include "PerformanceStats.h"
|
||||||
|
#include "Vst2RawChunk.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -46,6 +47,12 @@ namespace
|
|||||||
"Patch/bank state is exactly what JUCE's AudioPluginInstance::get/setCurrentProgram-\n"
|
"Patch/bank state is exactly what JUCE's AudioPluginInstance::get/setCurrentProgram-\n"
|
||||||
"StateInformation and get/setStateInformation read and write for a hosted VST2\n"
|
"StateInformation and get/setStateInformation read and write for a hosted VST2\n"
|
||||||
"plugin - i.e. real .fxp/.fxb file bytes.\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):
|
// Ad-hoc mode only (ignored if scenarioFile is set):
|
||||||
String patchFile, bankFile, savePatchFile, saveBankFile, wavFile;
|
String patchFile, bankFile, savePatchFile, saveBankFile, wavFile;
|
||||||
|
String loadBankChunkFile, saveBankChunkFile, loadPatchChunkFile, savePatchChunkFile;
|
||||||
double durationSeconds = 2.0;
|
double durationSeconds = 2.0;
|
||||||
double sampleRate = 44100.0;
|
double sampleRate = 44100.0;
|
||||||
int blockSize = 512;
|
int blockSize = 512;
|
||||||
@@ -90,6 +98,14 @@ namespace
|
|||||||
args.savePatchFile = argv[++i];
|
args.savePatchFile = argv[++i];
|
||||||
else if (arg == "--saveBank" && i + 1 < argc)
|
else if (arg == "--saveBank" && i + 1 < argc)
|
||||||
args.saveBankFile = argv[++i];
|
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)
|
else if (arg == "--wav" && i + 1 < argc)
|
||||||
args.wavFile = argv[++i];
|
args.wavFile = argv[++i];
|
||||||
else if (arg == "--duration" && i + 1 < argc)
|
else if (arg == "--duration" && i + 1 < argc)
|
||||||
@@ -227,6 +243,40 @@ namespace
|
|||||||
fprintf (stderr, "testhost: could not write '%s'\n", path.toRawUTF8());
|
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)
|
void applyEvent (PluginHost &host, const TestScenario::Event &event, MidiBuffer &midi, int sampleOffsetInBlock)
|
||||||
{
|
{
|
||||||
switch (event.type)
|
switch (event.type)
|
||||||
@@ -300,6 +350,12 @@ int main (int argc, char *argv[])
|
|||||||
if (scenario.initialLoadPatchFile.isNotEmpty() && ! loadStateFile (scenario.initialLoadPatchFile, false, host))
|
if (scenario.initialLoadPatchFile.isNotEmpty() && ! loadStateFile (scenario.initialLoadPatchFile, false, host))
|
||||||
return 1;
|
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)
|
if (args.printParams)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < host.getNumParameters(); ++i)
|
for (int i = 0; i < host.getNumParameters(); ++i)
|
||||||
@@ -394,6 +450,10 @@ int main (int argc, char *argv[])
|
|||||||
saveStateFile (args.savePatchFile, false, host);
|
saveStateFile (args.savePatchFile, false, host);
|
||||||
if (args.saveBankFile.isNotEmpty())
|
if (args.saveBankFile.isNotEmpty())
|
||||||
saveStateFile (args.saveBankFile, true, host);
|
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();
|
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
|
to a distinctive value, saves, reloads in a fresh process, and checks the
|
||||||
value reads back within a small tolerance (not exact equality — the
|
value reads back within a small tolerance (not exact equality — the
|
||||||
slider/internal-value scaling curve, `toParam`/`toSlider`, isn't bit-exact
|
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,
|
across a round trip). Goes through JUCE's generic `AudioProcessor::get/
|
||||||
not an externally-provided `.fxp`/`.fxb` — see the chunk-format note below
|
setStateInformation`/`get/setCurrentProgramStateInformation` - see the
|
||||||
for why.
|
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/`
|
**`stability_sweep`** — loads every bundled `.fxp` under `extras/sounds/`
|
||||||
and renders a held note through each, checking for crashes/hangs/NaN. A
|
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
|
| `PluginHost` method | VST2 wiring | Covering test(s) |
|
||||||
host**, so loading the bundled sample `.fxp` files through `testhost`'s
|
|---|---|---|
|
||||||
generic `--patch` (i.e. through `AudioProcessor::setCurrentProgramState-
|
| `load()` | `effOpen` (via `createPluginInstance`), then inside `prepareToPlay`: `effSetSampleRate`, `effSetBlockSize`, `effMainsChanged(1)`, `effStartProcess` | Every test - exercised, not specifically asserted beyond "didn't return false" |
|
||||||
Information`, exactly what any real VST2 host uses) has **zero effect** —
|
| `releaseResources()` | `effMainsChanged(0)`/`effStopProcess` (`VSTPluginInstance::releaseResources`) | Every test calls it before exit (`main.cpp`) - exercised only, no assertion |
|
||||||
confirmed by diffing `--printParams` output with and without `--patch`.
|
| `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 |
|
||||||
Those sample files are legitimately in VST2's opaque-chunk ("FPCh") format
|
| `getNumParameters()` | Reads cached `AEffect::numParams` | Every test (same load banner; also implicit in every `--printParams` use) - exercised, not asserted |
|
||||||
(verified via hex dump: correct `CcnK`/`FPCh`/`Jsy1` header), but JUCE
|
| `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) |
|
||||||
3.1.1's plugin-side VST wrapper (`sdk/juce/JUCE-3.1.1/modules/
|
| `setParameter(index, value)` | `AEffect::setParameter` callback | Same four round-trip tests (via `--param`) - asserted indirectly (the value it sets is what gets read back) |
|
||||||
juce_audio_plugin_client/VST/juce_VST_Wrapper.cpp`, compiled into
|
| `process()` - audio only | `processReplacing`/`processDoubleReplacing` | Every test - asserted only for NaN/Inf absence, not correctness of the DSP output itself |
|
||||||
`JaySynth.so`) never sets the VST2 `effFlagsProgramChunks` flag on the
|
| `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 |
|
||||||
`AEffect` struct, so JUCE's own host-side `VSTPluginInstance::usesChunks()`
|
| `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 |
|
||||||
sees false and `setChunkData()` silently no-ops — for both loading *and*
|
| `getCurrentProgramStateInformation()` | `saveToFXBFile(isFXB=false)` → `effGetChunk(index=1)` (confirmed chunk-based, see below) | `patch_save_load_roundtrip` - asserted |
|
||||||
saving. This is **not** one of the tracked `TODO.md` findings and this
|
| `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 |
|
||||||
suite doesn't attempt to fix it (it would mean patching JUCE's own VST
|
| `getStateInformation()` | `saveToFXBFile(isFXB=true)` → `effGetChunk(index=0)` | `bank_save_load_roundtrip` - asserted |
|
||||||
wrapper, a materially bigger and riskier change). It's why
|
| `setStateInformation()` | `loadFromFXBFile` → `effSetChunk(index=0)` | `bank_save_load_roundtrip` - asserted |
|
||||||
`stability_sweep` only checks "doesn't crash," not "changes the patch," and
|
| `getInstance()` | n/a (plain accessor, returns the raw `AudioPluginInstance*`) | Used internally by every `Vst2RawChunk`-based test to reach the raw `AEffect*` |
|
||||||
why the round-trip tests use `testhost`'s own save output as the fixture
|
| *(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 |
|
||||||
rather than a real sample file — `testhost`'s save path has the same
|
|
||||||
`usesChunks()`-is-false behaviour, so save and load are at least
|
**Known gaps**: `load()`/`releaseResources()`/`getNumInputChannels()`/
|
||||||
consistent with *each other*, even though neither talks to the external
|
`getNumOutputChannels()`/`getNumParameters()` are exercised by literally
|
||||||
`.fxp` files' actual chunk data. Tracked as a discovered issue in
|
every test run but never *asserted* against a specific expected value for
|
||||||
`tools/testhost/TODO.md`'s "Investigate" section.
|
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
|
## Debugging notes: how the VCF bug was actually found
|
||||||
|
|
||||||
|
|||||||
@@ -235,21 +235,108 @@ test_bank_roundtrip() {
|
|||||||
fi
|
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
|
# Broad regression net: load every bundled .fxp under extras/sounds/ and
|
||||||
# render a held note through it. NOTE: as discovered while building this
|
# render a held note through it. NOTE: these particular sample files
|
||||||
# suite, these particular sample files are in VST2's opaque-chunk ("FPCh")
|
# (10+ years old) load without crashing but - confirmed by diffing
|
||||||
# format, but JUCE 3.1.1's plugin-side VST wrapper (compiled into
|
# --printParams output with and without --patch - have no effect on the
|
||||||
# JaySynth.so) never sets effFlagsProgramChunks, so JUCE's *host*-side
|
# patch's parameters. This is NOT the effFlagsProgramChunks/usesChunks()
|
||||||
# VSTPluginInstance::usesChunks() sees false and silently ignores the
|
# gap an earlier version of this comment claimed (that was wrong - see
|
||||||
# chunk data entirely - confirmed by diffing --printParams output with and
|
# tests/README.md's "corrected finding" note; usesChunks() genuinely is
|
||||||
# without --patch: zero parameters differ. This is a real, separate
|
# true, and test_chunk_roundtrip above proves the real chunk path works
|
||||||
# finding (see tests/README.md and testhost/TODO.md) - NOT one of the
|
# with files this same build produces). The actual cause is still
|
||||||
# tracked TODO.md findings, and NOT something this suite attempts to fix.
|
# unidentified - most likely these specific old files use a compression
|
||||||
# So this test only checks "loading this file doesn't crash the process",
|
# or XML-schema shape that JaySynthAudioProcessor::setCurrentProgramState-
|
||||||
# not "loading this file changes the patch" - it still exercises the
|
# Information/getXmlFromBinary no longer parses, not a host-vs-plugin
|
||||||
# malloc/delete[] paths touched by the Memory-management fixes as a broad
|
# capability mismatch. Tracked in tools/testhost/TODO.md's Investigate
|
||||||
# smoke test, just not the patch-content-application path.
|
# 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() {
|
test_stability_sweep() {
|
||||||
local sounds_dir="$REPO_ROOT/extras/sounds"
|
local sounds_dir="$REPO_ROOT/extras/sounds"
|
||||||
local any=0
|
local any=0
|
||||||
@@ -298,6 +385,11 @@ test_patch_roundtrip
|
|||||||
test_bank_roundtrip
|
test_bank_roundtrip
|
||||||
log ""
|
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:"
|
log "General stability sweep over bundled sample patches:"
|
||||||
test_stability_sweep
|
test_stability_sweep
|
||||||
log ""
|
log ""
|
||||||
|
|||||||
Reference in New Issue
Block a user