Using macro parameters in tracktion

Hi!

I’m working on an application using macro parameters to control plugin parameters in a rack.
Everything works as expected until the edit is saved to a file and then loaded again. After reloading the parameter modified by the macro seems to have the wrong value.
Looks like the offset coming from the macro is added to the base value of the modified parameter each time the edit is saved and loaded (see comments and assertions below for details).

Here is an example application for replicating the issue

        te::Engine engine("app");

        const juce::File editFile(juce::File::getCurrentWorkingDirectory().getChildFile("macros.tracktionedit"));

        const float macroParameterValue = 0.3f;
        const float modifierValue = 0.5f;
        const float modifierOffset = 0.2f;

        std::unique_ptr<te::Edit> edit;

        // 1. create a new edit and add rack with a macro parameter
        {
            editFile.deleteFile();

            edit = te::createEmptyEdit(engine, editFile);

            auto rackType = edit->getRackList().addNewRack();

            if (auto volumePlugin = edit->getPluginCache().createNewPlugin(te::VolumeAndPanPlugin::xmlTypeName, {})) {
                // Add plugin to rack
                rackType->addPlugin(volumePlugin, {}, true);

                // Add macro parameter
                const auto macroParameter = rackType->macroParameterList.createMacroParameter();
                macroParameter->setNormalisedParameter(macroParameterValue, juce::NotificationType::sendNotification);

                if (auto volumeAndPan = dynamic_cast<te::VolumeAndPanPlugin*>(volumePlugin.get())) {
                    auto volParam = volumeAndPan->volParam;

                    volParam->setNormalisedParameter(0.0f, juce::NotificationType::sendNotification);

                    volParam->addModifier(*macroParameter, modifierValue, modifierOffset, 0.5f);

                    // Run the dispatch loop so the ValueTree properties attached to the parameter are updated. (Otherwise the assertions below will fail)
                    // This happens implicitly in the real application.
                    juce::MessageManager::getInstance()->runDispatchLoopUntil(1000);

                    jassert(juce::approximatelyEqual(volParam->getCurrentBaseValue()    , volParam->valueRange.convertFrom0to1(0.0f)));
                    jassert(juce::approximatelyEqual(volParam->getCurrentValue()        , volParam->valueRange.convertFrom0to1(modifierOffset + modifierValue * macroParameterValue)));
                    jassert(juce::approximatelyEqual(volParam->getCurrentModifierValue(), volParam->valueRange.convertFrom0to1(modifierOffset + modifierValue * macroParameterValue) - volParam->getCurrentBaseValue()));

                    DBG(volParam->getCurrentValue()); // prints 0.35 As expected
                }
            }

            // volume="0.35" is saved in the plugin state xml. (Similar behaviour is seen in Waveform12)
            te::EditFileOperations(*edit).save(true, true, false);
        }

        // 2. Load previously saved edit and check the parameter value
        {
            edit = te::loadEditFromFile(engine, editFile);

            if (auto rackType = edit->getRackList().getRackType(0)) {
                if (auto volumeAndPan = dynamic_cast<te::VolumeAndPanPlugin*>(rackType->getPlugins().getFirst())) {
                    auto volParam = volumeAndPan->volParam;

                    // These assertions were OK above after the modifier is added but fail after loading the edit again from file.
                    jassert(juce::approximatelyEqual(volParam->getCurrentBaseValue()    , volParam->valueRange.convertFrom0to1(0.0f)));
                    jassert(juce::approximatelyEqual(volParam->getCurrentValue()        , volParam->valueRange.convertFrom0to1(modifierOffset + modifierValue * macroParameterValue)));
                    jassert(juce::approximatelyEqual(volParam->getCurrentModifierValue(), volParam->valueRange.convertFrom0to1(modifierOffset + modifierValue * macroParameterValue) - volParam->getCurrentBaseValue()));

                    // This passes: The modifier value coming from the macro modifier seems to be added to the base parameter value.
                    jassert(juce::approximatelyEqual(volParam->getCurrentValue(), 2.0f * volParam->valueRange.convertFrom0to1(modifierOffset + modifierValue * macroParameterValue)));

                    DBG(volParam->getCurrentValue());// prints 0.7. Seems like the modifier value is added again to the previous value (already modified by the macro)
                }
            }

            // Run the dispatch loop so the ValueTree properties attached to the parameter are updated just like in a real
            // application.
            juce::MessageManager::getInstance()->runDispatchLoopUntil(1000);

            te::EditFileOperations(*edit).save(true, true, false);
        }

Waveform 12 also seems to save the parameter value taking into account the base value and the offset added by the macro parameter but does not seem to have the issue described above when the edit is loaded again.

Tried the example above with the latest commit on develop too (64229cf14) but no changes compared to the version I’m using in the actual application (bf2bf46e1).

Not sure if this is a bug or if I’m somehow using the tracktion classes incorrectly. Any help on this would be appreciated

Take a look at AutomatableEditItem::saveChangedParametersToState, this saves the explicit value (i.e. the one set by moving a direct parameter control) to the state which is then reloaded with AutomatableEditItem::restoreChangedParametersFromState if it exists for a parameter.

The Engine should take care of this automatically. Is this not being set for some reason for you?
Maybe saveChangedParametersToState isn’t getting called for the MacroParameterList?

The example you have there looks like a good unit test candidate.
I’ll try and get it added if we can figure out the cause.

I can see the explicit parameter values being saved for the volume plugin when calling EditFileOperations::save, I think that part is working as expected.

But AutomatableEditItem::restoreChangedParametersFromState is not called for the same plugin (the volume in the rack) meaning that the explicit parameters are never restored.
Could this be a bug in the engine or is there anything I need to do to ensure that parameters are restored correctly? I tried creating a rack instance and adding it to an audio track but it does not make any difference.

To confirm that restoring the parameters solves the problem I can call Plugin::initialiseFully() on the volume plugin which will end up calling restoreChangedParametersFromState. After this the volume parameter will have the correct value.

If you call Edit::initialiseAllPlugins() after loading it, does that also fix the problem?

I think the Engine should handle this, I’m just trying to think of the best place to do so…

Yes calling Edit::initialiseAllPlugins() works as expected.
Indeed it would be nice if it was handled by the engine but until then this works for me.

Thanks for looking into this

Ok, this should be fixed now:

Thanks Dave this seems to fix the issue.

The test looks good too! My only suggestion would be to remove the comment in line 244 about failing assertions as these are no longer expected to fail after the fix.

Cheers, must have overlooked that comment. Removed now.