[Solved] AudioParameterInt saves (non-normalized) float in parameter settings

Hello jucers,
I needed to add a non-parameter processor variable to my XML file. This question has been asked many times (see here for sliders, here and here for discussions on the looping it can trigger, etc…)

I chose to use a non-parameter because this is a standalone app, so nothing needs to be automatable, and I’m not a fan of how booleans as parameters are implemented as floats that you cast to boolean values.

Anyways it took me a little while to figure this out, so I thought I would share as some of those threads don’t have closure (i.e. a working code example).

Additionally, I have one last problem, which is loading the previous configuration upon startup.



But, here’s the XML parts:

  1. Adding parameters to the save file:

I added another xml node to the rootElement in processor.getStateInformation:

    // Create the hooverb element
    std::unique_ptr<juce::XmlElement> hooverbElement = std::make_unique<juce::XmlElement>("HOOVERB");
    addStateInfoForHooverb(hooverbElement.get());
    rootElement->addChildElement(hooverbElement.release());

right before creating the MemoryBlock binaryData.

This uses a companion method to populate the child node:

//==============================================================================
void GlobeLoveler::addStateInfoForHooverb(juce::XmlElement* hooverbElement) {
    for (int channel = 0; channel < Constants::Reverb::numChannels; channel++) {
        for (int combNum = 0; combNum < Constants::Reverb::combCount; combNum++) {
            auto combElement = hooverbElement->createNewChildElement("COMB");
            combElement->setAttribute("CHANNEL", channel);
            combElement->setAttribute("INDEX", combNum);
            combElement->setAttribute("ECHO_EN", reverb.get()->getCombEchoEnable(channel, combNum));
        }
    }
}
  1. Of course we want to load the parameters as well, so we check for the child node in processor.setStateInformation:
        if (auto hooverbXml = xmlState->getChildByName("HOOVERB"))
        {
            setStateInfoForHooverb(hooverbXml);
        }

And if found, call this helper method:

//==============================================================================
// Loads non-parameter settings for GlobeRoomReverb aka "Hooverb" -KGK v1.5.12
void GlobeLoveler::setStateInfoForHooverb(juce::XmlElement* hooverbElement) {
    for (int channel = 0; channel < Constants::Reverb::numChannels; channel++) {
        for (auto* combElement : hooverbElement->getChildIterator()) {
            int channel = combElement->getIntAttribute("CHANNEL", -1);
            int index = combElement->getIntAttribute("INDEX", -1);
            bool echoEn = combElement->getBoolAttribute("ECHO_EN");

            if (channel >= 0 && channel < Constants::Reverb::numChannels &&
                index >= 0 && index < Constants::Reverb::combCount) {
                reverb.get()->setCombClusterEnable(channel, index, echoEn);
            }
        }
    }

    // Triggers callback GlobeLovelerEditor.audioProcessorChanged(this, changeDetails) -KGK v1.5.12
    std::unique_ptr<ChangeDetails> changeDetails = std::make_unique<ChangeDetails>();
    updateHostDisplay(changeDetails.get()->withNonParameterStateChanged(true));
}

Which gets the channel, index, and bool from each element and calls the reverb setter (omitted for brevity).

  1. That was the easy part, while updating the GUI from the processor was more challenging (but I’m glad to be learning about Juce’s broadcast system!).
    It starts with the last two lines of code in the previous method, which will trigger a callback in the editor, once it’s set to inherit from juce::ChangeBroadcaster:
class GlobeLoveler : public AudioProcessor, 
    public AudioProcessorValueTreeState::Listener, 
    juce::ChangeBroadcaster
{
    // code...

and implement these pure virtual methods (the first two, plus there is a helper method):


//==============================================================================
// Triggered in globbeLoveler.setStateInformation() -KGK v1.5.12
void GlobeLovelerEditor::audioProcessorChanged(AudioProcessor* source, const ChangeDetails& details) {
    // TODO: check details fields and flags -KGK v1.5.12
    loadCombEchoEnableButtonStates();
}

//==============================================================================
// NON-OPERATIVE -KGK v1.5.12
void GlobeLovelerEditor::audioProcessorParameterChanged(AudioProcessor* processor,
    int parameterIndex, float newValue)
{ return; }

//==============================================================================
void GlobeLovelerEditor::loadCombEchoEnableButtonStates() {
    for (int i = 0; i < Constants::Reverb::combCount; i++) {

        // Operative code here!
        enableClusterL[i].setToggleState(processor.getCombEchoEnable(0, i), sendNotification);
        enableClusterR[i].setToggleState(processor.getCombEchoEnable(1, i), sendNotification);
    }
}

It uses a method in the processor which gets the enable state from its reverb object (also omitted for brevity).

There you have it, that’s an Ikea-ready example of how to save and load non-parameter values in your settings files.
But it only updates the editor when loading files from in the app, not on startup, so I haven’t gotten very much further than everyone else…



So… can anyone tell me how to ensure this code runs when loading the lastStateFile on startup? From setting breakpoints, I see in the StandalonePluginHolder the lastStateFile is loaded as part of creating the processor before the editor is even created from createEditorIfNeeded.

I’ve copied the updateHostDisplay call into processor.createEditor which feels like a hack, but a hack that should work, however, evidently there are steps that need to be completed in StandaloneFilterApp.createWindow() that need to be completed before the editor can listen to broadcasts and/or repaint…

//==============================================================================
AudioProcessorEditor* GlobeLoveler::createEditor()
{
    return new GlobeLovelerEditor(*this, parameters);
#ifdef JUCE_DEBUG
    // Hacky fix: force GUI update to enable buttons whenever we call prepareToPlay -KGK v1.5.12
    // Hacky fix does NOT work -KGK
    std::unique_ptr<ChangeDetails> changeDetails = std::make_unique<ChangeDetails>();
    updateHostDisplay(changeDetails.get()->withNonParameterStateChanged(true));
#endif
}

(I also tried in the constructor and prepareToPlay, but again, same problem: the processor is created and initialized before the editor fully exists.)

Cheers all!


edit: very low effort fix, just called the message callback’s helper function at the end of initializing in my editor:

    // Force update of UI
    loadCombEchoEnableButtonStates();

(duh)