Skip to content

Commit c3cbc1f

Browse files
authored
Merge pull request #191 from Tracktion/dev/discrete-params-validation-b488be
Add a discrete parameter state restoration test and fix false failures in the existing one
2 parents 0175cbf + 9569da2 commit c3cbc1f

11 files changed

Lines changed: 276 additions & 14 deletions

File tree

‎.github/workflows/acceptance.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ jobs:
156156
shell: bash
157157
run: |
158158
check() { echo "$1: got '$2', expected '$3'"; [ "$2" = "$3" ] || exit 1; }
159-
check "validate passed" "${{ steps.validate.outputs.passed }}" 3
159+
check "validate passed" "${{ steps.validate.outputs.passed }}" 4
160160
check "validate failed" "${{ steps.validate.outputs.failed }}" 0
161161
check "validate-missing outcome" "${{ steps.validate-missing.outcome }}" failure
162162
check "validate-missing passed" "${{ steps.validate-missing.outputs.passed }}" 1

‎.github/workflows/build.yaml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,9 @@ jobs:
128128
shell: bash
129129
run: |
130130
# Paths must be single quoted for bash not to escape the Windows backslash character \ used in absolute paths
131-
${{ env.APP_DIR }}/${{ matrix.test-binary }} validate --strictness-level 10 '${{ env.PLUGIN_CACHE_PATH }}/DSPModulePluginDemo_artefacts/Release/VST3/DSPModulePluginDemo.vst3'
131+
# "Discrete parameter state restoration" is disabled for DSPModulePluginDemo until juce::AudioParameterBool::setValue
132+
# snaps to 0 or 1: https://forum.juce.com/t/pluginval-can-set-an-audioparameterbools-to-odd-values/50971
133+
${{ env.APP_DIR }}/${{ matrix.test-binary }} validate --strictness-level 10 --disabled-tests "Discrete parameter state restoration" '${{ env.PLUGIN_CACHE_PATH }}/DSPModulePluginDemo_artefacts/Release/VST3/DSPModulePluginDemo.vst3'
132134
${{ env.APP_DIR }}/${{ matrix.test-binary }} validate --strictness-level 10 '${{ env.PLUGIN_CACHE_PATH }}/MultiOutSynthPlugin_artefacts/Release/VST3/MultiOutSynthPlugin.vst3'
133135
134136
- name: Validate JUCE Plugin examples (AU)
@@ -143,7 +145,7 @@ jobs:
143145
killall -9 AudioComponentRegistrar # kick the AU registrar
144146
# Intentionally uses the deprecated "--validate" flat flag to keep the
145147
# backwards-compatible alias covered (this step is continue-on-error).
146-
${{ env.APP_DIR }}/${{ matrix.test-binary }} --strictness-level 10 --validate ~/Library/Audio/Plug-Ins/Components/DSPModulePluginDemo.component
148+
${{ env.APP_DIR }}/${{ matrix.test-binary }} --strictness-level 10 --disabled-tests "Discrete parameter state restoration" --validate ~/Library/Audio/Plug-Ins/Components/DSPModulePluginDemo.component
147149
148150
- name: Codesign (macOS)
149151
if: ${{ matrix.name == 'macOS' }}

‎CHANGELIST.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
- **Breaking:** invalid option values now error instead of silently falling back (e.g. `--rtcheck banana`, `--strictness-level abc`)
1818
- **Breaking:** falsey flag environment variables now mean off (e.g. `SKIP_GUI_TESTS=0` no longer enables skipping)
1919
- `--help` output is now generated by CLI11
20+
- Added a "Discrete parameter state restoration" test (strictness 8) which checks a discrete parameter has the same value after restoring a state, whatever value it held beforehand. This fails for parameters that keep the raw normalised value from the host instead of mapping it to a step (e.g. `juce::AudioParameterBool`)
21+
- Fixed false failures in the "Plugin state restoration" test where the first parameter was compared against the last value sent by a previous test rather than the plugin's own value
22+
- Below strictness level 8, two-step (on/off) parameters are now only sent 0 or 1 when tests randomise parameter values. Arbitrary values are still sent to them at level 8 and above, and to all other parameters at every level
23+
- Fixed the "Plugin state restoration" test's exact binary state comparison never running. It was documented as running at strictness level 8 and above but checked the test's own level instead of the run's
2024
- Enabled the editor stress and extreme (real-time allocation / oversized block) tests that were present in the source tree but had not been compiled into the build
2125

2226
### 1.0.5

‎CLAUDE.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ pluginval/
117117
│ ├── test_plugins/ # Test plugin files
118118
│ │ ├── tone_generator/ # Deterministic dogfood generator (PLUGINVAL_BUILD_TEST_PLUGINS)
119119
│ │ ├── gain/ # Deterministic dogfood gain effect (for the input.audio path)
120-
│ │ └── playhead_probe/ # Writes the host transport to output (for the playhead path)
120+
│ │ ├── playhead_probe/ # Writes the host transport to output (for the playhead path)
121+
│ │ └── discrete_params/ # Bool/choice-only plugin (for the discrete parameter state test)
121122
│ ├── acceptance/ # Acceptance self-tests: configs (*.json.in), inputs/, checked-in refs/ WAVs
122123
│ ├── mac_tests/ # macOS-specific tests
123124
│ └── windows_tests.bat # Windows test scripts
@@ -379,7 +380,7 @@ struct Requirements {
379380
380381
| File | Tests Included |
381382
|------|----------------|
382-
| `BasicTests.cpp` | PluginInfo, Programs, Editor, AudioProcessing, PluginState, Automation, auval, VST3validator |
383+
| `BasicTests.cpp` | PluginInfo, Programs, Editor, AudioProcessing, PluginState, DiscreteParameterState, Automation, auval, VST3validator |
383384
| `BusTests.cpp` | Bus layout, channel configuration |
384385
| `EditorTests.cpp` | Editor creation, resizing |
385386
| `ParameterFuzzTests.cpp` | Random parameter value testing |
@@ -443,6 +444,7 @@ static MyNewTest myNewTest;
443444

444445
Located in `TestUtilities.h`:
445446
- `getNonBypassAutomatableParameters()` - Get automatable params
447+
- `getRandomParameterValue()` - Random value to set on a param (only 0 or 1 for two-step params below strictness 8)
446448
- `fillNoise()` - Fill buffer with random audio
447449
- `countNaNs()`, `countInfs()`, `countSubnormals()` - Audio validation
448450
- `ScopedEditorShower` - RAII editor creation/destruction

‎CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,5 +289,6 @@ if (PLUGINVAL_BUILD_TEST_PLUGINS)
289289
add_subdirectory(tests/test_plugins/tone_generator)
290290
add_subdirectory(tests/test_plugins/gain)
291291
add_subdirectory(tests/test_plugins/playhead_probe)
292+
add_subdirectory(tests/test_plugins/discrete_params)
292293
add_subdirectory(tests/acceptance)
293294
endif()

‎source/TestUtilities.h‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,23 @@ static inline juce::Array<juce::AudioProcessorParameter*> getNonBypassAutomatabl
5353
return parameters;
5454
}
5555

56+
/** Returns a random normalised value to set on a parameter.
57+
58+
Below strictness level 8, two-step (on/off) parameters are only sent 0 or 1.
59+
Any value is valid, but some plugins keep the values in between rather than
60+
mapping them to a step, which the state restoration tests then pick up.
61+
*/
62+
static inline float getRandomParameterValue (juce::Random& r, const juce::AudioProcessorParameter& parameter, int strictnessLevel)
63+
{
64+
// N.B. hosted VST3 parameters don't report isBoolean so also check the number of steps
65+
const bool isTwoStep = parameter.isBoolean() || parameter.getNumSteps() == 2;
66+
67+
if (isTwoStep && strictnessLevel < 8)
68+
return r.nextBool() ? 1.0f : 0.0f;
69+
70+
return r.nextFloat();
71+
}
72+
5673
//==============================================================================
5774
template<typename UnaryFunction>
5875
void iterateAudioBuffer (juce::AudioBuffer<float>& ab, UnaryFunction fn)

‎source/tests/BasicTests.cpp‎

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ struct PluginStateTest : public PluginTest
347347

348348
// Set random parameter values
349349
for (auto parameter : getNonBypassAutomatableParameters (instance))
350-
parameter->setValue (r.nextFloat());
350+
parameter->setValue (getRandomParameterValue (r, *parameter, ut.getOptions().strictnessLevel));
351351

352352
// Restore original state
353353
callSetStateInformationOnMessageThreadIfVST3 (instance, originalState);
@@ -378,13 +378,17 @@ struct PluginStateTestRestoration : public PluginTest
378378
// Read state
379379
auto originalState = callGetStateInformationOnMessageThreadIfVST3 (instance);
380380

381+
// Hosted parameters report the last value sent to them until a state is
382+
// restored, so restore once here to start from the plugin's own values
383+
callSetStateInformationOnMessageThreadIfVST3 (instance, originalState);
384+
381385
const auto tolaratedDiff = 0.1f;
382386

383387
// Set random parameter values
384388
for (auto parameter : getNonBypassAutomatableParameters(instance))
385389
{
386390
const auto originalValue = parameter->getValue();
387-
parameter->setValue(r.nextFloat());
391+
parameter->setValue (getRandomParameterValue (r, *parameter, ut.getOptions().strictnessLevel));
388392

389393
// Restore original state
390394
callSetStateInformationOnMessageThreadIfVST3(instance, originalState);
@@ -394,7 +398,7 @@ struct PluginStateTestRestoration : public PluginTest
394398
parameter->getName(1024) + juce::String(" not restored on setStateInformation"));
395399
}
396400

397-
if (strictnessLevel >= 8)
401+
if (ut.getOptions().strictnessLevel >= 8)
398402
{
399403
// Read state again and compare to what we set
400404
auto duplicateState = callGetStateInformationOnMessageThreadIfVST3 (instance);
@@ -417,6 +421,67 @@ struct PluginStateTestRestoration : public PluginTest
417421
static PluginStateTestRestoration pluginStateTestRestoration;
418422

419423

424+
//==============================================================================
425+
/**
426+
Tests that a discrete parameter reports the same value after a state restore
427+
regardless of the value it held beforehand.
428+
429+
A host can send any normalised value to a discrete parameter and only the
430+
plugin knows which step that maps to. If the plugin keeps the raw value for
431+
a single parameter change but saves/restores the step, the value it reports
432+
depends on how it got there.
433+
See https://forum.juce.com/t/pluginval-can-set-an-audioparameterbools-to-odd-values/50971
434+
*/
435+
struct DiscreteParameterStateTest : public PluginTest
436+
{
437+
DiscreteParameterStateTest()
438+
: PluginTest ("Discrete parameter state restoration", 8)
439+
{
440+
}
441+
442+
void runTest (PluginTests& ut, juce::AudioPluginInstance& instance) override
443+
{
444+
auto r = ut.getRandom();
445+
446+
for (auto parameter : getNonBypassAutomatableParameters (instance))
447+
{
448+
if (! parameter->isDiscrete())
449+
continue;
450+
451+
// Any normalised value is valid so use ones that are unlikely to be on a step
452+
const auto value = 0.1f + 0.8f * r.nextFloat();
453+
const auto otherValue = value < 0.5f ? value + 0.4f : value - 0.4f;
454+
455+
parameter->setValue (value);
456+
const auto state = callGetStateInformationOnMessageThreadIfVST3 (instance);
457+
458+
// Restore the state with the parameter still at the value it was saved with
459+
callSetStateInformationOnMessageThreadIfVST3 (instance, state);
460+
const auto restoredFromValue = parameter->getValue();
461+
462+
// Then restore it again from a different value
463+
parameter->setValue (otherValue);
464+
callSetStateInformationOnMessageThreadIfVST3 (instance, state);
465+
const auto restoredFromOtherValue = parameter->getValue();
466+
467+
ut.logVerboseMessage (parameter->getName (1024) + ": set " + juce::String (value) + ", restored as " + juce::String (restoredFromValue)
468+
+ "; set " + juce::String (otherValue) + ", restored as " + juce::String (restoredFromOtherValue));
469+
470+
ut.expectWithinAbsoluteError (restoredFromOtherValue, restoredFromValue, 0.0001f,
471+
parameter->getName (1024) + " has a different value after restoring the same state");
472+
}
473+
}
474+
475+
std::vector<TestDescription> getDescription (int) const override
476+
{
477+
return { { name, "For each discrete param: sets a random value, saves state, then restores that state twice - "
478+
"once unchanged and once after setting a different value - and expects the same value both times" } };
479+
}
480+
};
481+
482+
static DiscreteParameterStateTest discreteParameterStateTest;
483+
484+
420485
//==============================================================================
421486
struct AutomationTest : public PluginTest
422487
{
@@ -474,7 +539,7 @@ struct AutomationTest : public PluginTest
474539
for (int i = 0; i < juce::jmin (10, parameters.size()); ++i)
475540
{
476541
const int paramIndex = r.nextInt (parameters.size());
477-
parameters[paramIndex]->setValue (r.nextFloat());
542+
parameters[paramIndex]->setValue (getRandomParameterValue (r, *parameters[paramIndex], ut.getOptions().strictnessLevel));
478543
}
479544
}
480545

@@ -552,7 +617,7 @@ struct EditorAutomationTest : public PluginTest
552617
while (--numBlocks >= 0)
553618
{
554619
for (auto parameter : parameters)
555-
parameter->setValue (r.nextFloat());
620+
parameter->setValue (getRandomParameterValue (r, *parameter, ut.getOptions().strictnessLevel));
556621

557622
ut.resetTimeout();
558623
juce::Thread::sleep (10);
@@ -702,7 +767,7 @@ struct BackgroundThreadStateTest : public PluginTest
702767

703768
// Set random parameter values
704769
for (auto parameter : parameters)
705-
parameter->setValue (r.nextFloat());
770+
parameter->setValue (getRandomParameterValue (r, *parameter, ut.getOptions().strictnessLevel));
706771

707772
// Restore original state
708773
callSetStateInformationOnMessageThreadIfVST3 (instance, originalState);
@@ -739,6 +804,7 @@ struct ParameterThreadSafetyTest : public PluginTest
739804
auto parameters = getNonBypassAutomatableParameters (instance);
740805
const bool isPluginInstrument = instance.getPluginDescription().isInstrument;
741806
const int numBlocks = 500;
807+
const int runStrictnessLevel = ut.getOptions().strictnessLevel;
742808

743809
// This emulates the plugin itself setting a value for example from a slider within its UI
744810
juce::MessageManager::callAsync ([&, threadRandom = r]() mutable
@@ -747,7 +813,7 @@ struct ParameterThreadSafetyTest : public PluginTest
747813

748814
for (int i = 0; i < numBlocks; ++i)
749815
for (auto param : parameters)
750-
param->setValueNotifyingHost (threadRandom.nextFloat());
816+
param->setValueNotifyingHost (getRandomParameterValue (threadRandom, *param, runStrictnessLevel));
751817

752818
endWaiter.signal();
753819
});
@@ -777,7 +843,7 @@ struct ParameterThreadSafetyTest : public PluginTest
777843
addNoteOff (mb, noteChannel, noteNumber, 0);
778844

779845
for (auto param : parameters)
780-
param->setValue (r.nextFloat());
846+
param->setValue (getRandomParameterValue (r, *param, runStrictnessLevel));
781847

782848
fillNoise (ab);
783849

‎source/tests/ParameterFuzzTests.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ struct FuzzParametersTest : public PluginTest
4444

4545
for (int i = 0; i < 5; ++i)
4646
{
47-
const float value = r.nextFloat();
47+
const float value = getRandomParameterValue (r, parameter, ut.getOptions().strictnessLevel);
4848

4949
parameter.setValue (value);
5050
const float v = parameter.getValue();
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# A minimal plugin with only discrete (bool / choice) parameters, used to
2+
# dogfood the "Discrete parameter state restoration" test.
3+
# Built only when PLUGINVAL_BUILD_TEST_PLUGINS is ON.
4+
5+
set(discrete_params_formats VST3)
6+
7+
if (APPLE)
8+
list(APPEND discrete_params_formats AU)
9+
endif()
10+
11+
juce_add_plugin(pluginval_discrete_params
12+
PRODUCT_NAME "pluginval Discrete Params"
13+
COMPANY_NAME Tracktion
14+
BUNDLE_ID com.Tracktion.pluginvalDiscreteParams
15+
PLUGIN_MANUFACTURER_CODE Trkt
16+
PLUGIN_CODE Pdp1
17+
FORMATS ${discrete_params_formats}
18+
IS_SYNTH FALSE
19+
NEEDS_MIDI_INPUT FALSE
20+
COPY_PLUGIN_AFTER_BUILD FALSE)
21+
22+
target_sources(pluginval_discrete_params PRIVATE
23+
DiscreteParamsPlugin.cpp
24+
DiscreteParamsPlugin.h)
25+
26+
target_compile_features(pluginval_discrete_params PRIVATE cxx_std_20)
27+
28+
target_compile_definitions(pluginval_discrete_params PRIVATE
29+
JUCE_WEB_BROWSER=0
30+
JUCE_USE_CURL=0
31+
JUCE_VST3_CAN_REPLACE_VST2=0)
32+
33+
target_link_libraries(pluginval_discrete_params PRIVATE
34+
juce::juce_audio_utils
35+
juce::juce_audio_plugin_client
36+
juce::juce_recommended_config_flags
37+
juce::juce_recommended_warning_flags)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*==============================================================================
2+
3+
Copyright 2018 by Tracktion Corporation.
4+
For more information visit www.tracktion.com
5+
6+
You may also use this code under the terms of the GPL v3 (see
7+
www.gnu.org/licenses).
8+
9+
pluginval IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
10+
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
11+
DISCLAIMED.
12+
13+
==============================================================================*/
14+
15+
#include "DiscreteParamsPlugin.h"
16+
17+
//==============================================================================
18+
DiscreteParamsProcessor::DiscreteParamsProcessor()
19+
: juce::AudioProcessor (BusesProperties().withInput ("Input", juce::AudioChannelSet::stereo(), true)
20+
.withOutput ("Output", juce::AudioChannelSet::stereo(), true))
21+
{
22+
for (int i = 1; i <= 3; ++i)
23+
{
24+
bools.push_back (new juce::AudioParameterBool (juce::ParameterID ("bool" + juce::String (i), 1),
25+
"Bool " + juce::String (i), false));
26+
addParameter (bools.back());
27+
}
28+
29+
for (int i = 1; i <= 2; ++i)
30+
{
31+
choices.push_back (new juce::AudioParameterChoice (juce::ParameterID ("choice" + juce::String (i), 1),
32+
"Choice " + juce::String (i),
33+
{ "One", "Two", "Three", "Four" }, 0));
34+
addParameter (choices.back());
35+
}
36+
}
37+
38+
//==============================================================================
39+
void DiscreteParamsProcessor::getStateInformation (juce::MemoryBlock& destData)
40+
{
41+
juce::MemoryOutputStream mos (destData, false);
42+
43+
for (auto p : bools)
44+
mos.writeBool (p->get());
45+
46+
for (auto p : choices)
47+
mos.writeInt (p->getIndex());
48+
}
49+
50+
void DiscreteParamsProcessor::setStateInformation (const void* data, int sizeInBytes)
51+
{
52+
juce::MemoryInputStream mis (data, (size_t) sizeInBytes, false);
53+
54+
for (auto p : bools)
55+
*p = mis.readBool();
56+
57+
for (auto p : choices)
58+
*p = mis.readInt();
59+
}
60+
61+
//==============================================================================
62+
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
63+
{
64+
return new DiscreteParamsProcessor();
65+
}

0 commit comments

Comments
 (0)