Skip to content

Persistence

passivist edited this page Jan 16, 2017 · 2 revisions

Persistence

As it is now, our plugin has all the parameters we need. But one thing it lacks is parameter persistence. This is crucial when working on real life projects in your DAW: you want the plugin to remember its state between sessions. Luckily JUCE provides two functions to handle this: the getStateInformation and setStateInformation functions. These functions are called whenever the user saves or loads a project in his DAW.

What we want to do here is on a call to getStateInformation iterate through all the plugin parameters and store them to a xml file. When setStateInformation is called we want to parse that file and restore the values of our parameters to these values.

Persistent Buttons and Sliders

Let's look at the implementation of these two functions:

void GrrnlrrAudioProcessor::getStateInformation (MemoryBlock& destData)
{
  // [1]
  std::cout << "Save Settings" << std::endl;

  // [2]
  // Create an outer XML element..
  XmlElement xml ("GRRNLRRPLUGINSETTINGS");

  // [3.1]
  // Store the values of all our parameters, using their param ID as the XML attribute
  for (int i = 0; i < getNumParameters(); ++i)
  {
    // [3.2]
    if (AudioProcessorParameterWithID* p = dynamic_cast<AudioProcessorParameterWithID*> (getParameters().getUnchecked(i)))
    {
      // [3.3]
      xml.setAttribute (p->paramID, p->getValue());

      std::cout << p->paramID << " " << p->getValue() << std::endl;
    }
  }

  // [4]
  // then use this helper function to stuff it into the binary blob and return it..
  copyXmlToBinary (xml, destData);
}
  1. First we post a message to the console for debugging purposes.
  2. We create an object representing the xml data that we want to write to the disk later. We also give the object a name to identify it later when loading the data.
  3. Here we parse the names and values of our parameters into the XmlElement object
  4. We iterate through the number of parameters.
  5. We create a pointer p to a parameter if p points to a valid object we continue.
  6. We write the parameter ID and the parameter value into xml. We also post the value for debugging.
  7. Finally we write the XmlElement object to disk. The location of this file is provided by the arguement of the getStateInformation function.

setStateInformation looks similar:

void GrrnlrrAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
  // You should use this method to restore your parameters from this memory block,
  // whose contents will have been created by the getStateInformation() call.

  std::cout << "Load Settings" << std::endl;

  // [1]
  // This getXmlFromBinary() helper function retrieves our XML from the binary blob..
  ScopedPointer<XmlElement> xmlState (getXmlFromBinary (data, sizeInBytes));

  // [2.1]
  if (xmlState != nullptr)
  {
    // [2.2]
    // make sure that it's actually our type of XML object..
    if (xmlState->hasTagName ("GRRNLRRPLUGINSETTINGS"))
    {
      // [2.3]
      // Now reload our parameters..
      for (int i = 0; i < getNumParameters(); ++i)
      {
        if (AudioProcessorParameterWithID* p = dynamic_cast<AudioProcessorParameterWithID*> (getParameters().getUnchecked(i)))
        {
          // [2.4]
          p->setValueNotifyingHost ((float) xmlState->getDoubleAttribute (p->paramID, p->getValue()));

          std::cout << p->paramID << " " << p->getValue() << std::endl;
        }
      }
    }
  }
}
  1. Here we load the binary data provided by the function's first argument into a XmlElement object xmlState. We don't access the data directly but instead create a pointer to it that is local to this function, this way we don't have to worry about deleting the object later.
  2. Here we set our parameters to the values in the xmlState object similar to how we wrote them:
  3. We check if xmlState points to something.
  4. We also check if the data is actually meant for our plugin.
  5. If so we iterate through our parameters and set the value notifying the host application to the value in the xmlState object. We also post all the values again.

Persistent File Path

In addition to the parameters we need one special value to persist: the path of our sample. Since the path isn't a parameter (and shouldn't be visible to the host application) we have to make it persistent manually. Also we need to modify the mechanism for loading samples a bit so it loads a sample from a restored path.

First we declare two variables in the header of our PluginAudioProcessor:

// Sample Path
String filePath;
String restoredPath;

The first variable filePath will hold the path to our file. It will be permanent for the session once the file is loaded and we will use it to write the path to the xml file. The second variable restoredPath will hold the path of a sample restored from the setStateInformation function. It will hold that value until the sample is loaded.

Now we will need to modify our openPath function in the PluginAudioProcessorEditor to set filePath once a file has been chosen:

void GrrnlrrAudioProcessorEditor::openButtonClicked()
{
    FileChooser chooser ("Select a File to open...",
                         File::nonexistent,
                         "*.wav", "*.aif", "*.aiff");

    if(chooser.browseForFileToOpen()){
        const File file (chooser.getResult());
        String path (file.getFullPathName());
        swapVariables (chosenPath, path);

        processor.filePath = chosenPath;

        notify();
    }
}

Then we will add a statement for writing the path of the file to the xml file in getStateInformation:

// [...]
xml.setAttribute("FilePath", filePath);
std::cout << "Save Path: " << filePath << std::endl;

// then use this helper function to stuff it into the binary blob and return it..
copyXmlToBinary (xml, destData);
// [...]

We also modify setStateInformation:

// [...]
if (xmlState != nullptr)
    {
        // make sure that it's actually our type of XML object..
        if (xmlState->hasTagName ("GRRNLRRPLUGINSETTINGS"))
        {
            // Now reload our parameters..
            for (int i = 0; i < getNumParameters(); ++i)
            {
                if (AudioProcessorParameterWithID* p = dynamic_cast<AudioProcessorParameterWithID*> (getParameters().getUnchecked(i)))
                {
                    p->setValueNotifyingHost ((float) xmlState->getDoubleAttribute (p->paramID, p->getValue()));

                    std::cout << p->paramID << " " << p->getValue() << std::endl;
                }
            }
            restoredPath = xmlState->getStringAttribute("FilePath");
            std::cout << "Load Path: " << filePath << std::endl;
        }
    }
// [...]

After setting restoredPath we have to to get the sample to actually load. For this we declare another function in the header of our PluginAudioProcessorEditor: void checkForRestoredPath(); We define the function in PluginEditor.cpp:

void GrrnlrrAudioProcessorEditor::checkForRestoredPath()
{
    String path;
    path = processor.restoredPath;
    if(path.isNotEmpty()){
        swapVariables(chosenPath, path);
        processor.restoredPath = "";
    }
}

This function is very similar to checkForPathToOpen we define a local variable path to which we copy the contents of the restoredPath member variable of our PluginAudioProcessor. We then check if that string is empty. If it is not we swap the value of chosenPath and path. We then assign restoredPath to be an empty string.

Finally we have to add a call to checkForRestoredPath to the run function of our PluginAudioProcessorEditor Object:

void GrrnlrrAudioProcessorEditor::run()
{
    while(! threadShouldExit()){
        checkForRestoredPath();
        checkForPathToOpen();
        checkForBuffersToFree();
        wait(500);
    }
}

When you compile now the path to your plugin should be restored once you open the plugins GUI for the first time. With that the sample should also start to play.

<<< last Chapter

Clone this wiki locally