Cross Platform presets - any ideas?


#1

Hi all:

My latest plugin is almost complete, but I need to figure out a cross-platform preset solutions. I want to have a preset browser that looks in an OS-dependent locations for the presets, and allows the user to choose and save the presets in a format that isn’t dependent on the DAW.

First of all, does anyone have any suggested techniques that they want to share? I am totally into not reinventing the wheel.

Assuming that this is a more private matter, or that others have done what I have done in the past (i.e. just use the host based preset system), where would be a good place to start? Would a PropertiesFile be a good format for presets, where the data is saved as XML? And what would be the best way of displaying the presets in the plugin GUI?

Also, what functions should be implemented in the AudioProcessor plugin code to save/recall presets? I am thinking that I will want to implement

getNumPrograms()
getCurrentProgram()
setCurrentProgram()
getProgramName()
changeProgramName()

I am trying to figure if getStateInformation() and setStateInformation() should be part of this custom code, or if they should be fairly close to the existing Juce Demo Plugin example, with the addition of saving the current preset number/name.

Thanks for any and all help.

Sean Costello


#2

Hi Sean, I’ve only just started with Juce but maybe can offer a little bit here.

As per the plugin demo, I was using an XML structure in getStateInformation() and setStateInformation(). Having set this up I wanted to add functionality to store and load presets from my plugin GUI and realised that I could do so using the same technique, simply storing the XML as a file. So I defined serialiseStateInformation() which returns an XmlElement and is called by both getStateInformation() and my save presets button. I also defined deSerialiseStateInformation(ScopedPointer xmlState) which reads the state and updates all internal variables. This gets called by both setStateInformation() and my load presets button.

The presets xml file is stored in the userApplicationDataDirectory, using File::getSpecialLocation(). Have only verified this on Win 7 so far.

[code]//-------------------------------------------
// PluginProcessor.cpp
//-------------------------------------------

void HyperdriveAudioProcessor::getStateInformation (MemoryBlock& destData)
{
copyXmlToBinary (serialiseStateInformation(), destData);
}

void HyperdriveAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
// Fix problem with Live storing last preset chosen rather than current state
// see http://www.rawmaterialsoftware.com/viewtopic.php?f=8&t=6138
// or http://www.kvraudio.com/forum/viewtopic.php?p=1096418
setChunkCalled = Time::getMillisecondCounter();

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

if (xmlState != 0)
{
	deSerialiseStateInformation(xmlState);
}

}

XmlElement HyperdriveAudioProcessor::serialiseStateInformation()
{
// Create an outer XML element…
XmlElement xmlState (“MYPLUGINSETTINGS”);

// add some attributes to it..
xmlState.setAttribute ("uiWidth", lastUIWidth);
xmlState.setAttribute ("uiHeight", lastUIHeight);
XmlElement* xmlChild[numPrograms];
for (int i = 0; i<numPrograms-1; ++i) {
	xmlChild[i] = xmlState.createNewChildElement ("ProgramState_" + String(i));
        xmlChild[i]->setAttribute ("ProgramName", programName[i]);
	xmlChild[i]->setAttribute ("Drive", drive[i]);
	xmlChild[i]->setAttribute ("Attack", attack[i]);
	xmlChild[i]->setAttribute ("Release", release[i]);
}
return xmlState;

}

//-------------------------------------------
// PluginEditor.cpp
//-------------------------------------------

1 if (buttonThatWasClicked == m_btnSavePresets)
{
//[UserButtonCode_m_btnSavePresets] – add your button handler code here…
String fyleSpec = File::getSpecialLocation(File::userApplicationDataDirectory).getFullPathName() + T("\Hyperdrive\presets.xml");
File fyle(fyleSpec);
fyle.create();
XmlElement xmlState = m_PluginProcessor->serialiseStateInformation();
xmlState.writeToFile(fyle,"");
//[/UserButtonCode_m_btnSavePresets]
}
else if (buttonThatWasClicked == m_btnLoadPresets)
{
//[UserButtonCode_m_btnLoadPresets] – add your button handler code here…
String fyleSpec = File::getSpecialLocation(File::userApplicationDataDirectory).getFullPathName() + T("\Hyperdrive\presets.xml");
File fyle(fyleSpec);
if (fyle.existsAsFile())
{
XmlDocument xmlDoc(fyle);
m_PluginProcessor->deSerialiseStateInformation(xmlDoc.getDocumentElement());
}
//}
}[/code]

Please note that this is still a work in progress. You might notice that when the presets are loaded, I just update the state in the plugin processor and don’t notify the host. I guess I’ll need to dig around in setParameterNotifyingHost() to figure out how to manage that properly.

-Andrew

PS - My aim is to provide basic host independent functionality for editing presets to make it easier for users to restore factory presets or to share presets with other users. It also allows me a convenient method to manage factory presets during development.


#3

If you want cross-platform presets, you have to manage you presets inside the plugin.
Make two methods to save and restore whole relevant data into a xml-memoryblock(use xml for endian-correctness, and save all automable plugin parameters too)
You can reuse the methods in getStateInformation()/setStateInformation() to save the current plugin state (which has primarily nothing todo with (you own) presets), but if the user prefers to use the host-preset functionality, it will also work.
Then you can use PropertiesFile::getDefaultAppSettingsFile(…).getParentFolder() to find a good place to store you presets for your own preset-managing system. This is the way i do.


#4

I think chkn said pretty much what I did, so that’s a comfort. Anyhow, to inform the host of the change I ended up creating the following function and calling it from my load presets button. I initially implemented the notification in deSerialiseStateInformation() itself, but that of course would trigger notifications back to the host just after the host triggers a call to setStateInformation().

void HyperdriveAudioProcessor::deSerialiseStateInformationAndNotify(ScopedPointer<XmlElement> xmlState) { deSerialiseStateInformation(xmlState); // Notify host of changes to parameters sendParamChangeMessageToListeners (driveParam, param2Float(driveParam, drive[currentProgram])); sendParamChangeMessageToListeners (attackParam, param2Float(attackParam, attack[currentProgram])); sendParamChangeMessageToListeners (releaseParam, param2Float(releaseParam, release[currentProgram])); }

Everything seems to be working OK so far 8)

Note - param2Float() and float2Param() are used to map between my internal parameter ranges back and the standard 0…1 range.


#5

[quote=“chkn”]
Then you can use PropertiesFile::getDefaultAppSettingsFile(…).getParentFolder() to find a good place to store you presets for your own preset-managing system. This is the way i do.[/quote]

That is a great idea. I use the default app settings file for my plugin authorization, so that seems like a good parent directory for my presets.

Sean Costello


#6

[quote=“Andrew J”]Hi Sean, I’ve only just started with Juce but maybe can offer a little bit here.
[/quote]

That’s a lot of good information for a “litte bit”! Thanks!


#7

Hi Sean,

I’d be interested in collaborating on something. I’ve already done some work on the AU side of things. You’ll notice you can load and save all the Aalto presets from the generic mechanisms in Logic and Live. As far as I can tell, nobody else has bothered to implement this. I had to since I didn’t have my own patch loading UI in 1.0. I’ve also added chunk names and proper range info for automation in AU hosts. I was trying to write a good AU and not reinvent the wheel, but users have all gotten used to the idiocy of each plugin having its own load/save UI, so I had to add one in 1.1.

I was just about to begin translating my preset work to the VST side. My plan is to add code in the VST wrapper to read the XML files, so I think it is basically the same approach everyone’s talking about here.

On the Mac, the files should go in the two Library/Audio/Presets directories. This is the standard, and most devs seem to abide by it, so there’s no excuse for not doing this. Is there a similar consensus on Windows?

The files could be totally cross platform as I’m doing it now, with the unfortunate hitch that the extensions would all have to “.aupreset” because Live and Logic won’t read them otherwise. This could function OK on Windows but if we’re making a cross-platform solution I don’t really want to see “.aupreset” on the Windows side. One idea is just to save out copies of the preset with another extension name each time, in other words, put our .jucepreset (or whatever) files next to the .aupreset files. Then at least users can just copy a whole preset directory from a PC to a mac or vice versa to migrate patches.


#8

Doing this stuff requires extending the wrapper code, which I’m now doing with patches. Of course, this make updating to the JUCE tip quite a pain. Please respond to ‘Subclassing plugin wrapper code’ thread if you have any better ideas.


#9

I don’t know what folder is used for presets on Windows. As far as I know, there is no standard VST directory, so there probably isn’t a standard presets directory. It might make sense to follow Digidesign’s lead, and put things in the Common Files directory (or whatever they actually use - I’m away from the Windows machine).

[quote]
The files could be totally cross platform as I’m doing it now, with the unfortunate hitch that the extensions would all have to “.aupreset” because Live and Logic won’t read them otherwise. This could function OK on Windows but if we’re making a cross-platform solution I don’t really want to see “.aupreset” on the Windows side.[/quote]

The .aupreset files also seem to have some sort of binary data or something going on - they aren’t human readable, as far as I can tell. Your preset code that you use when copying the preset to the clipboard has nice, readable key/value pairs, which would be good for a cross platform solution.

What Juce class do you use within your GUI for the drop-down presets menu? It looks very nice. [EDIT: looks like PopupMenu - dang, there are a lot of Juce classes!] For cross-platform solutions,having a preset selector within the GUI will probably be critical, due to the lack of a common VST folder for Windows. In addition, RTAS doesn’t have the built-in preset mechanism of AU and VST, in that no plugin ships with default presets baked into the plugin itself - the presets have to be in an external preset folder.


#10

I’m sure someone will chime in with advice.

[quote]
The .aupreset files also seem to have some sort of binary data or something going on - they aren’t human readable, as far as I can tell. Your preset code that you use when copying the preset to the clipboard has nice, readable key/value pairs, which would be good for a cross platform solution.[/quote]

The unreadable blobs contain key/value pairs in XML, but loaded and saved using CFURLCreateDataAndPropertiesFromResource / CFURLWriteDataAndPropertiesToResource. This is how AU hosts load and save parameters, so these files can be written and all the parameters sent to my plugin without running any plugin-specific code. To add human-readable parameters, we can write the same XML to the same file again but in plain text format.

Re: GUI stuff, I wouldn’t want to confuse the issue by thinking about GUI code at the same time as preset management. (I did use PopupMenu for Aalto.)


#11

Would it be a good idea to have the presets write to two different places - the central cross-platform repository for the plugin (written in whatever XML or key/value format seems appropriate) and the AU/VST/RTAS location (written in the host-specific method)?

[quote]
Re: GUI stuff, I wouldn’t want to confuse the issue by thinking about GUI code at the same time as preset management. (I did use PopupMenu for Aalto.)[/quote]

Well, for me they are kinda tied in. What I am presuming is that you had a few fixed menu items for PopupMenu (copy to clipboard, paste from clipboard, Save as…), several variable items where the groupings were determined by the contents of your Aalto presets folder, and then some fixed presets that you have “baked in” to your plugin code itself. Does this sound about right?


#12

I’m getting a little confused here. Isn’t it up to the host to store presets according to it’s own methods? The plugin only needs to be able to answer the correct function calls so the host can do it’s thing.

Notwithstanding the fact that I’ve never spent much time with AU or RTAS plugins, the only scenario I envisage where you’d want the plugin to be able to write presets in host format is where you want to put presets in a host library - and even then, the host should really be able to manage that shouldn’t it? If we look at Logic for example, does the host provide the functionality to save the aupreset?


#13

[quote]I’m getting a little confused here. Isn’t it up to the host to store presets according to it’s own methods? The plugin only needs to be able to answer the correct function calls so the host can do it’s thing.

Notwithstanding the fact that I’ve never spent much time with AU or RTAS plugins, the only scenario I envisage where you’d want the plugin to be able to write presets in host format is where you want to put presets in a host library - and even then, the host should really be able to manage that shouldn’t it? If we look at Logic for example, does the host provide the functionality to save the aupreset?[/quote]

Well, yes, let’s define the problem we want to solve here.

The hosts all have ways of storing presets but they don’t interoperate. If I want to copy my Logic presets to Live on Windows, I should be able to just copy the files over. This means making a common preset file format that can be used by all the flavors of the plugin on different operating systems.

Another goal I think Sean and I share is to make the preset file human-readable (probably XML) so that you can modify a preset with a text editor if you want, or write a random patch generator as a stand-alone program, or share a patch on a web forum.

Any other goals?

[EDIT:] Also, re: Sean’s other thread, handle host automation in all flavors of plugin correctly (in reasonably well-behaved hosts).


#14

[quote]
Would it be a good idea to have the presets write to two different places - the central cross-platform repository for the plugin (written in whatever XML or key/value format seems appropriate) and the AU/VST/RTAS location (written in the host-specific method)?[/quote]

I think if you create multiple files when you do a single action like save a preset, you’re pretty much inviting confusion on the part of the user. All the versions (AU/VST/RTAS) can use the same human-readable file, if we just use the cross-platform, cross-flavor solution, and forget for the moment about my code that does the nifty AU stuff. As I noted before it’s impossible to both do a truly transparent cross-platform solution and play nice with the AU host code unless all the files are named *.aupreset, which I can’t really stomach. I can write a utility later for migrating presets made natively in AUs.

All I’m saying is that if we are designing some reusable functionality for making plugin states persistent, this should not embody any assumptions about what kind of GUI might be using it. There might not be a GUI at all. It may make sense to write “helper interfaces” that do support a particular kind of GUI, if these are common use cases like a menu.

This is right except I don’t have any presets hardwired into the plugin. I populate the menu with a few fixed items, then the System presets, then the user presets. We could add an interface to the preset code that would facilitate this. A good general design would be to return a ValueTree of preset names and files. This way clients (of our preset code) could add Listeners to update whatever GUI objects they implement when the list of presets changes.

A simpler but less flexible design would be to make an interface that just populates a PopupMenu synchronously. This is what I’m doing with Aalto and seems like a good starting point, as you suggest.


#15

[quote=“madronalabs”].
Any other goals?[/quote]

I would add that having a more robust preset system than the host’s will help plugin developers in creating a larger preset set for their plugins, which is a big selling point for a lot of people.


#16

Robust in what way?


#17

“Robust” probably isn’t the best word. What I have found on Windows is that it isn’t immediately obvious how to import VST presets for given hosts. I shipped V1.0.1 of ValhallaShimmer for Windows VST with a presets folder, and many people had no idea how to use the presets with their host. Audio Unit presets tend to work better between hosts.


#18

Robust is a great word but just makes me look forward to tomorrow’s coffee. OK, so presets are a massive mess, because different hosts have different paradigms for them and put them in different places on different platforms. There are lessons to learn from this, and people to bitch at, but that’s beyond the scope of our discussion.

We can put together some code that works cross-platform, so Juce plugin writers can easily implement cross-platform preset files. This will save all of a plugin’s parameters, and we’ll have to define what parameters are and make those play nice with automation. That should all be reasonably clean to do and fit well into the Juce plugin demo.

The platform specific stuff is always going to be uglier, I don’t know whether it’s best to try to work it into the demo, or just share it somewhere like this forum in case people want to take it on. it seems like the cross-platform version will have a better UX and be more useful for all the reasons we are talking about. Look at Urs’ ACE for a good example.

hasta mañana…


#19

Just a quick status report before putting this on the back burner for a bit. I did some minor updates to the parameter handling in the demo plugin and sent them Jules’ way. In doing this I realized I have a bunch of additions that make more sense to contribute along with each other, and I want to revamp this code in my shipping plugin as well. So after I get my beta out I’ll revisit the demo plugin project.


#20

[quote=“Andrew J”]Hi Sean, I’ve only just started with Juce but maybe can offer a little bit here.
[/quote]

Thanks for this Andrew! Would you be able to post your deSerialiseStateInformation() function?

I’m working on my own code today, and will post anything that seems particularly useful.

Sean Costello