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

Hello,

I am trying to add the project version (as set in projucer) to the AudioProcessor parameters file.
I succeeded but encountered an old problem, my major/minor/revision version ints are being recorded as floats.

Here is my code that gets the version info and adds the parameters as AudioParameterInts:

AudioProcessorValueTreeState::ParameterLayout SmplcompAudioProcessor::createParameterLayout()
{
    std::vector<std::unique_ptr<RangedAudioParameter>> params;

    // other parameters added...

    findVersionInfo();

    params.push_back(std::make_unique<AudioParameterInt>("globeLoveVerMaj", "Globe Loveler Major Version",
                                                        1,
                                                        5,
                                                        globeLovelerVersion[0]));

    params.push_back(std::make_unique<AudioParameterInt>("globeLoveVerMin", "Globe Loveler Minor Version",
                                                        1,
                                                        10,
                                                        globeLovelerVersion[1]));

    params.push_back(std::make_unique<AudioParameterInt>("globeLoveVerRev", "Globe Loveler Revision",
                                                        1,
                                                        20,
                                                        globeLovelerVersion[2]));
}

void SmplcompAudioProcessor::findVersionInfo()
{
    // Get program version -KGK
    juce::String versionString = JUCEApplication::getInstance()->getApplicationVersion();
    // * Split the version string into major, minor, and revision parts
    juce::StringArray versionParts;
    versionParts.addTokens(versionString, ".", "");
    // * Ensure there are at least three parts (M, m, r)
    while (versionParts.size() < 3)
        versionParts.add("0");
    // * Convert from str to int 
    for (int i = 0; i < versionParts.size(); ++i) {
        int verN = versionParts[i].getIntValue();
        globeLovelerVersion[i] = verN;
    }
}

I haven’t modified the code to get/setStateInformation() so it’s pretty boilerplate.

This is the output of the saved parameters file:

VC2!ĂĽ  
<?xml version="1.0" encoding="UTF-8"?>
<PARAMETERS>
  <PARAM id="attack" value="2.0"/>               <!--Not normalized-->
  <PARAM id="globeLoveVerMaj" value="1.0"/>      <!--Not an int! :( -->
  <PARAM id="globeLoveVerMin" value="2.0"/>      <!--Not an int!-->
  <PARAM id="globeLoveVerRev" value="2.0"/>      <!--Not an int!-->
  <PARAM id="inputgain" value="0.0"/>
  <PARAM id="knee" value="6.0"/>                 <!--Not normalized-->
  <PARAM id="makeup" value="19.60000228881836"/> <!--Not normalized-->
  <PARAM id="mix" value="0.7440000176429749"/>
  <PARAM id="power" value="1.0"/>
  <PARAM id="ratio" value="2.0"/>                <!--Not normalized-->
  <PARAM id="release" value="140.0"/>            <!--Not normalized-->
  <PARAM id="threshold" value="-40.0"/>          <!--Not normalized-->
</PARAMETERS>

My version parameters are the 2nd-4th recorded here. They are recorded as floats, furthermore, they are not normalized.

  1. It seems I’ll just have to work around them being saved as floats, is that correct?
  2. Why aren’t these parameters normalized? They are all given normalizable ranges when added to the parameter layout (params).
  3. Side question, how does the order in which parameters are added to a parameterLayout matter?

Thank you :slight_smile:


Some threads I read on this topic -

The fact that they are not ints is perhaps a little odd but not necessarily cause for concern, since 32-bit floats can represent integers up to 2^24 without loss of precision.

But why are you storing the version number using AudioParameters? Parameters are typically used for things that can be automated by the host, which seems strange for the version number.

You can serialize your plug-in’s state to XML something like this:

<?xml version="1.0" encoding="UTF-8"?>
<PLUGIN version="1.2.2">
  <... other stuff ...>
</PLUGIN>
<PARAMETERS>
  <PARAM id="attack" value="2.0"/> 
  ...
</PARAMETERS>

Here in the PLUGIN section you’d store any non-parameter data such as the version number, UI state, and so on.

2 Likes

These version numbers would be shown and be changeable in Bitwig for instance by default. Other hosts (such as Cubase and Reaper) would easily allow the user to switch off the plugin UI and expose these parameters. They would also be automatable and thus easily changed by most hosts without much effort.

Definitely should be storing non-parameter information elsewhere in the XML.

2 Likes

Indeed, it’s just odd

Thank you, that’s a very helpful observation. I will just add/remove version info to XML before the parameters’ ValueTree is read.

:warning: !
Great counter example of why not to do what I did.

One more reason: staying away from the value tree is a lot easier!

Thank you both. :slight_smile:


ninja edit: Can any explain why the parameter set isn’t normalized? :thinking:

It’s likely just so that they are more human readable, and I suppose when going from XML → APVTS it’s using convertTo0to1. Which I assume would also help when if for some (bizarre) reason you changed the range of a parameter it will restrict it to the new range (if you made the range smaller) or keep the same value (if you made the range larger). If it was using a normalised record of the parameter it would just convert that to some “incorrect” value in the new range. Total speculation on my part though, maybe someone from JUCE team could answer definitively, I also suspect perhaps the only person who could explain why is Jules.

1 Like

I worked on some plugins which stored parameters as normalised values and at some point someone had tried to fix a bug by very slightly changing the range (catch me at a conference one time I’ll explain), the pain these two things caused us is in-describable an astonishing number of bugs caused by it. I strongly suggest everyone works with real values wherever possible!

6 Likes

Interesting, good to know… I will try to stay away from normalized parameters and hope nothing goes wrong.

Some of the discussion in the threads I linked has me confused about when and why values may be normalized but I will try to stay away from it myself.

This has led me back to the root of the documentation, which is quite detailed.

https://docs.juce.com/master/classAudioProcessorParameter.html

Thank you both for the insight.


Update: Made the program break by adding the version number to the PARAMETERS element:

I touched up the XML to create a proper tree:


//==============================================================================
void SmplcompAudioProcessor::getStateInformation(MemoryBlock& destData)
{
    // Prepare to read XML data
    auto state = parameters.copyState();
    // Create the root element
    std::unique_ptr<juce::XmlElement> rootElement = std::make_unique<juce::XmlElement>("AudioProcessorState");
    // Create the parameters element
    std::unique_ptr<juce::XmlElement> parametersElement(state.createXml());
    
    // Create the status & version elements
    std::unique_ptr<juce::XmlElement> statusElement = std::make_unique<juce::XmlElement>("STATUS");
    std::unique_ptr<juce::XmlElement> versionElement = std::make_unique<juce::XmlElement>("STAT");
    // * Add version number to status
    DBG(JUCEApplication::getInstance()->getApplicationVersion());
    versionElement.get()->setAttribute("id", "version");
    versionElement.get()->setAttribute("value", JUCEApplication::getInstance()->getApplicationVersion());
    // * Add version element to status element
    statusElement.get()->addChildElement(versionElement.release());

    // Add parameters and status element to root element
    rootElement->addChildElement(parametersElement.release());
    rootElement->addChildElement(statusElement.release());

    // Convert XML data to binary
    MemoryBlock binaryData;
    copyXmlToBinary(*rootElement, binaryData);

    // Resize dest block to fit binary data plus null terminator
    const int dataSize = binaryData.getSize();
    destData.setSize(dataSize + 1);
    // * Copy data and write null terminator
    memcpy(destData.getData(), binaryData.getData(), dataSize);
    destData[dataSize] = '\0';
}

Which is producing very nice output:

VC2!u  
<?xml version="1.0" encoding="UTF-8"?>
<AudioProcessorState>
  <PARAMETERS>
    <PARAM id="attack" value="2.0"/>
    <PARAM id="globeLoveVerMaj" value="1.0"/>
    <PARAM id="globeLoveVerMin" value="2.0"/>
    <PARAM id="globeLoveVerRev" value="2.0"/>
    <PARAM id="inputgain" value="0.0"/>
    <PARAM id="knee" value="3.799999952316284"/>
    <PARAM id="makeup" value="-29.60000038146973"/>
    <PARAM id="mix" value="0.5960000157356262"/>
    <PARAM id="power" value="1.0"/>
    <PARAM id="ratio" value="1.350000023841858"/>
    <PARAM id="release" value="140.0"/>
    <PARAM id="threshold" value="0.0"/>
  </PARAMETERS>
  <STATUS>
    <STAT id="version" value="1.2.2"/>
  </STATUS>
</AudioProcessorState>  

Unfortunately when I load the saved file, it doesn’t detect the STATUS element…

Console readout shows that the data isn’t corrupted. Is there something wrong with how I’m looking for the PARAMETERS tag?

void SmplcompAudioProcessor::setStateInformation(const void* data, int sizeInBytes)
{
    std::unique_ptr<juce::XmlElement> xmlState(getXmlFromBinary(data, sizeInBytes));

    if (xmlState.get() != nullptr) {
        DBG(xmlState->toString());

        if (xmlState->hasTagName("STATUS")) {
            DBG("Has Status Element");
        }
        else {
            DBG("No Status Element");
        }

        if (xmlState->hasTagName(parameters.state.getType())) {
            parameters.replaceState(juce::ValueTree::fromXml(*xmlState));
        }
    }
}

STATUS is a child element of the xmlState.

xmlState->hasTagName("STATUS") is asking if the <AudioProcessorState> element has the tag name STATUS, which of course is false, since it has the tag name AudioProcessorState.

You’ll need to do something like:

if(auto statusXml = xmlState->getChildByName("STATUS"))
{
     // has status element, can be accessed via statusXml pointer
}
else
{
    // no status element
}
1 Like

And still very, very real :sob:

2 Likes

Thank you for the help. I got it working like so

void SmplcompAudioProcessor::setStateInformation(const void* data, int sizeInBytes)
{
    std::unique_ptr<juce::XmlElement> xmlState(getXmlFromBinary(data, sizeInBytes));

    if (xmlState.get() != nullptr) {
        if (auto statusXml = xmlState->getChildByName("STATUS"))
        {
            // has status element, can be accessed via statusXml pointer
            DBG("Has Status Element");
            for (int i = 0; i < statusXml->getNumChildElements(); ++i) {
                auto childStat = statusXml->getChildElement(i);
                auto idIndex = childStat->getAttributeValue(0);
                if (idIndex == "version") {
                    DBG("Version " << childStat->getAttributeValue(1));
                }
                else {
                    DBG("Unknown STAT id " << idIndex);
                }
            }
        }

        if (xmlState->hasTagName(parameters.state.getType())) {
            parameters.replaceState(juce::ValueTree::fromXml(*xmlState));
        }
    }
}

Is there really no more elegant solution than looking up indexes by name, or names by index?

Very glad to have it working, thanks again!

1 Like

You should just be able to use getStringAttribute

eg.

if(auto statusXml = xmlState->getChildByName("STATUS"))
{
    // get the version or 0.0.0 if it doesn't exist
    auto versionString = statusXml->getStringAttribute("version", "0.0.0");
    // now you can split the string by . to get the version parts
}
1 Like

Thanks @asimilon, I ended up using that method. I had to refactor the code because (a) when loading, I was looking for the parameters in the root node (which was no longer PARAMETERS, but AudioProcessorState); and (b) to use the method you shared in a more streamlined way.

Here is the final (?) code if anyone is interested

Getting (saving) state

void GlobeLoveler::getStateInformation(MemoryBlock& destData)
{
    // Prepare to read XML data
    auto state = parameters.copyState();
    // Create the root element
    std::unique_ptr<juce::XmlElement> rootElement = std::make_unique<juce::XmlElement>("GlobeLovelerState");
    
    // Create the parameters element
    std::unique_ptr<juce::XmlElement> parametersElement(state.createXml());
    // * Add parameters element to root element
    rootElement->addChildElement(parametersElement.release());
    
    // Create the status & version elements
    std::unique_ptr<juce::XmlElement> statusElement = std::make_unique<juce::XmlElement>("STATUS");
    // * Add version number to status
    statusElement.get()->setAttribute("version", JUCEApplication::getInstance()->getApplicationVersion());
    // * Add status element to root element
    rootElement->addChildElement(statusElement.release());

    // Convert XML data to binary
    MemoryBlock binaryData;
    copyXmlToBinary(*rootElement, binaryData);

    // Resize dest block to fit binary data plus null terminator
    const int dataSize = binaryData.getSize();
    destData.setSize(dataSize + 1);
    // * Copy data and write null terminator
    memcpy(destData.getData(), binaryData.getData(), dataSize);
    destData[dataSize] = '\0';
}

Setting (loading) state

void GlobeLoveler::setStateInformation(const void* data, int sizeInBytes)
{
    std::unique_ptr<juce::XmlElement> xmlState(getXmlFromBinary(data, sizeInBytes));

    if (xmlState.get() != nullptr) {
        if (auto statusXml = xmlState->getChildByName("STATUS"))
        {
            auto versionString = statusXml->getStringAttribute("version", "0.0.0");
            DBG("Version " << versionString);
        }

        if (auto parametersXml = xmlState->getChildByName("PARAMETERS"))
        {
            if (parametersXml->hasTagName(parameters.state.getType())) {
                parameters.replaceState(juce::ValueTree::fromXml(*parametersXml));
            }
        }
    }
}

File format

VC2!W  
<?xml version="1.0" encoding="UTF-8"?>
<GlobeLovelerState>
  <PARAMETERS>
    <PARAM id="attack" value="0.0"/>
    <PARAM id="inputgain" value="0.3999996185302734"/>
    <PARAM id="knee" value="0.0"/>
    <PARAM id="makeup" value="-30.0"/>
    <PARAM id="mix" value="0.0"/>
    <PARAM id="power" value="1.0"/>
    <PARAM id="ratio" value="1.0"/>
    <PARAM id="release" value="5.0"/>
    <PARAM id="reverbRoomDamping" value="0.0"/>
    <PARAM id="reverbRoomSize" value="0.0"/>
    <PARAM id="reverbRoomWidth" value="0.0"/>
    <PARAM id="reverbWetLevel" value="0.0"/>
    <PARAM id="threshold" value="-60.0"/>
  </PARAMETERS>
  <STATUS version="1.3.1"/>
</GlobeLovelerState>  

It’s interesting that you mentioned this, it’s always confused/frustrated me which is which due to the names, and still does even after 5+ years of working with JUCE! I guess it makes sense from the plugins’ hosts’ perspective, but I always think of it in terms of the plugin telling the host about state, in which case it feels counterintuitive.

2 Likes

The audio processor is subsidiary to both the plugin and the host, no?

It is simple if you consider, that the plugin never calls those functions.
Only the host can query and set a state, and with this mental model it is intuitive.
I know some plugin developers wish the plugin could save on its own accord, but that’s not how the api works.

2 Likes

Yeah, I totally get it, and in fact I worded the above incorrectly for my mistaken mental model (and just edited it). One day I’ll finally not have to look at the code in the methods to know which one is which. :joy:

2 Likes

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)

or you just write your own save/loadPatch methods and make them called by load/saveState to avoid having to think inverted. i also do the same with jasserts, because i usually wanna know when it breaks, not when it passes

1 Like