surge-synthesizer / surge

Synthesizer plug-in (previously released as Vember Audio Surge)
https://surge-synthesizer.github.io/
GNU General Public License v3.0
3.15k stars 400 forks source link

Implement MIDI Program Change support #576

Closed mkruselj closed 1 year ago

mkruselj commented 5 years ago

Right now Surge doesn't respond at all to Program Change messages. Eventually this would be a great thing to have, especially for live performance.

I hold u-he plugins as the gold standard here. Here's an excerpt directly from the manual for one of their super-popular synths, Zebra 2:

"MIDI Programs As well as factory presets, ‘Local’ also contains a special folder called ‘MIDI Programs’. When the first instance of Zebra2 starts, all presets in that folder (up to 128) are loaded into memory so they can be selected via MIDI Program Change messages. It is best to rename them to be sorted by number e.g. ‘000 rest-of-name’ to ‘127 rest-of-name’. ‘MIDI Programs’ can contain up to 127 sub-folders, switchable via MIDI Bank Select messages (CC#0). Send Bank Select first, then Program Change. ‘MIDI Programs’ itself is bank 0, then sub-folders are addressed in alphabetical order starting with bank 1. Important - ‘MIDI Programs’ cannot be added, removed or renamed on the fly – any changes to that directory will only be updated after the host application is restarted."

baconpaul commented 5 years ago

I guess the surge equivalent of that is have a user category called “midi” and just order them alphabetically for midi program changes. Then you just save your patches with the names you want to make the order and change messages send you to that folder if it exists for traversal. Agree?

mkruselj commented 5 years ago

Yes. But it should def support subfolders in there (one level of subfolders only), for having more than 128 presets available there for switching.

baconpaul commented 5 years ago

So Surge actually does support program change now in the AU and VST. The VST3 does not.

progctl

but it's not very useful.

Code wise, right now the AU and VST2 both send the program change method and call SurgeSynthesizer::programChange. That implementation is

void SurgeSynthesizer::programChange(char channel, int value)
{
   PCH = value;
   // load_patch((CC0<<7) + PCH);
   patchid_queue = (CC0 << 7) + PCH;
}

CC0 is the value of channel control 0. So you can address any of the patches by direct index as "CC0 * 128 + program_change". That's an odd API since the ordering in patch id space is dependent on the order on the file system. So not super duper useful. But stable-ish.

Seems a better approach would to use the convention that CC0 selects your category in sorted order and Program Change selects your patch inside that category.

Thoughts?

mkruselj commented 5 years ago

Seems a better approach would to use the convention that CC0 selects your category in sorted order and Program Change selects your patch inside that category.

I think that u-he method is still much better because it is deterministic. You get to sort the patches in the exact order that you want, and this cannot be inadvertly changed during a session if you store a preset somewhere in the folder that's already being traversed via program changes (stuff is preloaded into RAM only when plugin is instantiated, cache is created, and only that cache is used).

baconpaul commented 5 years ago

Oh that’s interesting. I had assumed if you had a folder you were traversing with patch a b c and you saved “aa” into it your program changes would shift in uhe. Not so?

In surge seems the obvious thing to do is to have program change do an ordered traverse of current category. Then it’s up to the user to order their patches. I don’t see where else we could store information but ordered traverse of current category is way better than the current system which is basically random program assignment.

Thanks!

baconpaul commented 5 years ago

@mkruselj and I chatted about this on slack this morning. There's lots we can do here but for a 1.6, since the current behavior is so random, here's the concrete proposal

  1. On startup of surge, if it doesn't exist, create ~/Documents/Surge/MIDI Programs/000 Bank 0 (with the right platform name of course)
  2. CC#0 moves you to the n-th sorted category inside MIDI Programs
  3. PC moves you to the n-th sorted patch inside the MIDI Programs subfolder
  4. Trying to save into MIDI Programs/blah category pops up a warning saying "Saving into midi programs can invalidate program change messages by reordering patches. Are you sure"

At a later date we want to consider adding these features

  1. Changing the category display name for the MIDI Patches to be bank number and program number rather than the folder name
  2. Pre-loading the XML for all the midi patches at startup to avoid file IO during performance program change

When this issue is closed add those two items as future issues

@mkruselj agree?

mkruselj commented 5 years ago
  1. Methinks this should actually be handled by the installer. No?

2./3. Just to add clarity: if CC #0 is not sent, PC will use the root of MIDI Programs folder to choose which patch to load. If CC# 0 is sent, then it picks a sorted subfolder within MIDI Programs - and in that case PC will look in that folder, not root (obviously). Sorry if stating the obvious, but to me this reads more clearly.

  1. Should also pop up even when saving to MIDI Programs root folder, of course.

Agree!

baconpaul commented 5 years ago

Yeah on 1 surge currently creates the documents folder if it isn’t there so we can just do this at the same spot!

Agree on all the others.

itsmedavep commented 5 years ago

Per some slack conversation this might be a good one for @rghvdberg . Tagging you over here.

SoliDeoGloria2 commented 4 years ago

This is really important, I have to say! A must, I should add... Particularly for live performances, proper program change support - as stated by Mario above - is crucial.

Does anybody know if this feature is about to be implemented anytime soon? I´m considering this instrument for a live setup...

mkruselj commented 4 years ago

Not planned any time soon unfortunately.

But since you're a Reaper user, you can actually store Surge presets as Reaper presets and program change those instead :)

SoliDeoGloria2 commented 4 years ago

Yes, you´re right! The only thing that Reaper misses in my opinion (although I remember that, once, you disagreed with this) is a dedicated bank per plugin instance, as found in Fm8, Absynth, etc... I´ve more than once ended up with a looong, long list of presets of many different projects, all mixed and messsed up, which is rather annoying for my taste.

Since I´m also a Bidule user, I´ll get by with it, of course. I have been streamlining during these years a live Bidule setup which allows complex changes with a single button push, so I was evaluating the most simple way to include Surge in it. Anyway, I thought it was Ok to do some lobby for the program change support cause :) . It really helps a lot to streamline live setups; I hope it can be reconsidered by the devs...

baconpaul commented 4 years ago

Yeah unfortunately lately the devs have not been that big a group and as @mkruselj said this one just hasn’t made the cut, since most of us have workaround like config in reaper or main stage or whatever, and we have a bunch of bugs and feature requests where there’s no way to get an approximation of the feature.

However this is a great issue for a new dev to tackle. Since Surge is entirely open source and volunteer community driven, one way you could help is - well - tell all your friends who are devs that there’s a cool and friendly open source synth project out there where a person could help us implement a fairly neat and compact feature if they know a bit of C++ and have run a virtual synth. Happy to help someone get up to speed starting here.

But barring someone showing up and making this their priority, I think it’s still going to stay mid/bottom of list for the near future since, as you point out, we have workarounds, and things like a UI skinning engine and expanding/fixing the FM section of the DSP engine don’t have alternative solutions with external tools.

SoliDeoGloria2 commented 4 years ago

Well, that´s a fair reasoning for sure! Unfortunately, the few devs I personally know won´t ever get into the synth world at all, and learning C++ at this point would burn my head entirely. Anyway, there´s this open topic here with a good bunch of replies. Maybe at some point...

baconpaul commented 4 years ago

OK that’s great. I figure it always helps to ask, since we really would welcome developers to the project!

hems commented 4 years ago

I guess the surge equivalent of that is have a user category called “midi” and just order them alphabetically for midi program changes. Then you just save your patches with the names you want to make the order and change messages send you to that folder if it exists for traversal. Agree?

Surely it's a workaround, but unfortunately, this is not ideal and brings other issues for the end-user.

I think the ideal situation is to have a list of presets assigned to Program Change Messages on each instance of the synth, so every instance can have its own list.

If you look this video on youtube you will see that's how Massive works, i believe FM8 is the same.

mkruselj commented 4 years ago

That's a more complicated way to go about it, it seems to me.

Going the regular folder route is very similar to how u-he does it, and the great thing about it is that it's not limited to 128 presets like Massive or FM8 are, since you can subfolder patches and use Bank Select message to point to a particular subfolder, essentially supporting over 16k patches through BS+PC messages...

IMHO it's a superior method of dealing with this sort of thing.

hems commented 4 years ago

As far as my experience goes the latest Surge version already supports way more than 128 presets and you don't need a "midi folder".. Just send Program, Sub Bank and Bank messages to scroll through hundreds and hundreds of presets.

if you're comfortable with ordering your presets alphabetically in order to don't mess the Program Number then AFAIK you don't need anything to be done other than this issue I reported yesterday: https://github.com/surge-synthesizer/surge/issues/1601

I think having a Program List is the best way, because if you use a "midi folder" and you change something then the sequence of your live performances projects will break, sequences I do on my external hardware sequencer will break, etcs.

It's not a live-ready approach to have to assign Program Change numbers to your presets by alphabetically ordering them, if you change the order by accident, then you're done..

IMHO, Ideally every instance holds it's own preset list..

Also, you can have a Program List that supports more than 128 presets...

Also, ideally, you need each instance to hold it's own program list.

SoliDeoGloria2 commented 4 years ago

The Massive/FM8/Absynth way - which is the one I´m used to -, while not as comprehensive as the U-HE system in terms of quantity of available presets, is quite useful in itself and more than enough for a lot of live situations. The main thing about this is the ability to have a dedicated custom list of user presets per instance. Be it 128 or preferably more, a list per instance is in my opinion the way to go. It´s true that the subfolder system can easily wreck things if it´s used in a global context (for example, if a live set is highly dependent on the order of the subfolders used for different projects), as hems says, but of course nothing prevents from using the subfolders´system only in the context of each song/piece. I mean, subfolders can be used exclusively to extend the available programs for each project/instance. Any implementation, though, will be far better than the current system in my opinion...

baconpaul commented 4 years ago

Hey folks

Like I said this one isn’t top of my list but happy for folks to work on it. Thought it might be useful to just recap what surge does now and why it is inadequate

Right now at startup surge does a directory scan for presets which ends up with the presets in basically random order. For all the displays and stuff we sort

When we get a midi program change event we basically do 127*bank + patch and then see if that number is within the range of lists of presets in the unordered list (I think; I may have changed it to use the ordered list in the past but I don’t think so). It then loads that patch

This is obviously a bad solution. If you add a patch all your banks change

From reading your comments I rather like the idea of having patches be assigned virtual banks and programs rather than having to move things in folders, but I don’t use surge in this way and my next few months of work are on other bits of the synth. But if a person showed up and implemented either that or the uhe style solution I would be happy to help them walk the change through and so on.

Hope that helps!

baconpaul commented 4 years ago

https://github.com/surge-synthesizer/surge/blob/da96f74b1cc96f936192282e300d93cf65d16c58/src/common/SurgeSynthesizer.cpp#L813 And there’s the spot where the id gets set. Pretty clear how to add an indirection there into whatever data structure you have (and the fact that it sets the queue Id stops surge from crashing with a memory corruption but also what creates the tuning error in #1601 I bet. Probably need to send changes before notes with at least some buffer although 1601 is a different issue obviously)

hems commented 4 years ago

Like I said this one isn’t top of my list but happy for folks to work on it. Thought it might be useful to just recap what surge does now and why it is inadequate

Thanks for the help!

Right now at startup surge does a directory scan for presets which ends up with the presets in basically random order. For all the displays and stuff we sort

In order for the approach I'm suggesting to work I believe the following steps would be necessary for a Proof of Concept:

  1. Add a link on the menu "Save preset to Program List"
  2. Open a popup when the button is clicked to insert Bank and Program number ( or just program number for simplicity )
  3. Save a map ( Program Number -> Preset Filename ) on the plugin state
  4. When the DAW store the current state of the plugin it should store not only the preset assigned and parameter values, but also the PGM map.
  5. Prefix the preset names on the Preset Menu with the Program Number in case they are assigned, so a user can see what's assigned to which slot.

My CPP is super rusty ( no pun intended ) and I don't even have Surge compiling on my current Machine. I would love to try set it up and try to draft a PR for that feature as it's super essential for me.

The main challenge for me would be #4 as i'm not aware of how this works. When does the DAW ask for the state of the plugin in order to save it on the project and where is the cpp code associated with that? I would assume the DAW already stores a state that contains more than simply the current preset ( i.e. all parameters of the plugin including the preset name ).

If @baconpaul or anyone else would be able to feedback on this approach it would be a cool challenge.

Thanks a lot!

baconpaul commented 4 years ago

Oh sure. Let me take 10 minutes later tonight and outline how you would do those steps. The only problem is you would store presets by name since they don’t have stable id so when you restore in step 4 it would be a bit of c++

We are planning a 1.6.6 release probably weds and after that we are going to be pushing quite a few changes into the synth. It would be a good time to do this one too and I appreciate your offer to help

Out of curiosity what is your os?

baconpaul commented 4 years ago

Oh and I like your simple design of being able to set up one bank in a session. May also want to think about ways to display it and share it later and I have a few ideas about that I will write up too

hems commented 4 years ago

Oh sure. Let me take 10 minutes later tonight and outline how you would do those steps. The only problem is you would store presets by name since they don’t have stable id so when you restore in step 4 it would be a bit of c++

indeed that was the idea to either store the PRESET filename relative to the PRESETS PATH as a starting point.

I think there's anther problem that would emerge which is:

Because of that, I wonder:

  1. What to do if the user try to load a project with a program list with a preset that was deleted
  2. If it would be possible/necessary to actually store all the preset values together with the Program List to avoid this sort of issues.

We are planning a 1.6.6 release probably weds and after that we are going to be pushing quite a few changes into the synth.

amazing, I'm looking forward to checking that out. I love the new-ish micro tunning stuff

It would be a good time to do this one too and I appreciate your offer to help

As I said, don't expect much from me as a CPP guy, I do know how to code and google tough so if I'm lucky I would be able to do a half-presentable PR ( :

Out of curiosity what is your os?

OSX 10.12 my Ableton is 64 bits tough!

hems commented 4 years ago

Oh and I like your simple design of being able to set up one bank in a session. May also want to think about ways to display it and share it later and I have a few ideas about that I will write up too

I remember seeing a very simple "pop up" when editing values somewhere, so i assume this functionality is already implemented somewhere and I could just copy that?

My idea here is basically to be able to implement this feature without having to implement nothing more than the saving a list on the DAW session, everything else should be made out of things that are already implemented.

Of course, it would be nice to have a GUI for the program list and a tesla delivered to my house, but that will need a lot more work : D

baconpaul commented 4 years ago

Yeah that’s right. But with one easy extra idea you can get a ui for free - namely make midi bank show up as a category in the menu so you can see what’s mapped there.

Lemme get home and I will write it all up including an answer to your questions in a way which is codavle if you can google and compile. I’m more than happy for you to get it working with some edges and I can help fix them up too. I just don’t want to dedicate a couple of days of dev to it when I’m in the middle of some very exciting tuning and skinning stuff

Oh and good you are on Mac. The build tools work super well there. You are running a version of live that loads the vst3 yeah? Building the vst2 is hard

hems commented 4 years ago

Yeah that’s right. But with one easy extra idea you can get a ui for free - namely make midi bank show up as a category in the menu so you can see what’s mapped there.

yeah very good idea!

Lemme get home and I will write it all up including an answer to your questions in a way which is codavle if you can google and compile. I’m more than happy for you to get it working with some edges and I can help fix them up too. I just don’t want to dedicate a couple of days of dev to it when I’m in the middle of some very exciting tuning and skinning stuff

great! I also would love to get it building on my computer so i can hopefully do more contributions in the future.

Oh and good you are on Mac. The build tools work super well there.

It does require xCode though which is a bit inconvenient as i'm fighting with free space in my laptop and slow wifi as i'm abroad!

I'm now "in the process" of downloading Xcode. I was hoping i woul be able to edit in VSCode and build with command-line tools, but after reading the README I think that's not the case?

I can't actually officially donwload it from App Store as they require an earlier version of the system than the one i have now.

image

So I'll need to look into downloading an older version or some other alternative as I'm not willing to update my system as it generally breaks my music making setup.

You are running a version of live that loads the vst3 yeah?

I'm using the AU version, Ableton introduced VST3 kinda recently, so I did not get to try it yet..

I would be willing to give it a go if I build it locally!

Building the vst2 is hard

Not necessary for me at the moment as the AU works.

baconpaul commented 4 years ago

Ahh yeah you need Xcode; that's the way you get the compiler even if you don't use Xcode (I use emacs myself). Hmm. There may be a way you can brew install clang but that's hard.

Well look let me write up my notes quickly and then you or me or someone else can develop of them when we get an env that works and turn to it!

  1. Data Structure to store the preset

    • The right storage form is patch ID numbers which are indices into storage->patch_list
    • add int midiProgramBank[128] as a member to SurgeStorage
    • in the SurgeStorage constructor in SurgeStorage do a
      for ( i= 0; i < 128; ++i ) midiPrgramBank[i] = -1
  2. add an 'add to program' menu

    • this is in src/common/gui/CPatchBrowser.cpp
    • you can see around line 130 where we make the RefreshPatchList option
    • to prompt for a number use spawn_miniedit_text. An example of this in a menu callback is the MPE Pitchbend Menu which is in SurgeGUIEditor.cpp (just search for mpePitchBendRange
    • In pseudocode
      auto m = createMenu( "Add to patch bank" )
      m->setAction( [this](() {
      char c[256];
      snprintf(c, 256, "%d", synth->mpePitchBendRange);
      spawn_miniedit_text(c, 16);
      int newVal = ::atoi(c);
      this->storage->patchthing[newval] = /* reference to current patch id */
      } );
      contextMenu->add(m)
  3. Make the program change load the patch based on this index. That's pretty clear from above. If the value of the array is -1 just do nothing.

  4. Save and retrieve it.

    • add an array of strings (probably) to the SurgeStorage struct DAWExtraState
    • in SurgeStorage.h you can see two areas where we set and unset from that. When you set use the patch name; when you unset linearly scan the patches looking for a name match and if there isn't one set to -1. This -1 when you unload combined with the third item above means that remvoging patches doesn't break you but does mean you ignore that program cahnge
    • in SurgePatch.cpp you can see where the dawExtraState is serialized and unserialized as XML. Do the right thing. Especially do the right thing when loading if there is no XML block.
  5. If you want to indicate on the preset UI two options

    • Modify CPatchBrowser when it paints if my patch id is anywhere in the list. Simple scan but scan when the patch changes not at each draw
    • Add a menu group after the categories where you expand along the patch thing making each loadable and prepend 0: 1: etc... to the name so you can mouse and see "midi bank 0" as a (virtual) category
  6. Add a streaming regtest. In src/headless/UnitTestsIO there are various places where we stream and upstream a surge after setting internal state. Test this there in a new test case

And that's the project complete!

Like I said if you can't install Xcode you probably won't be able to do this but I figured the notes were useful to some future one of us!

hems commented 4 years ago

thanks, that’s very helpful! as a patch id i reckon we should use the relative file path.

another way of making unique ids for patches is to store a md5 of the actual patch preset so then even if the patch is renamed or moved in the folder structure it would have a unique id, that sounds ideal but would require an aditional md5 step when saving presets so they get a unique id and also perhaps md5 patches on load if they don’t have an md5 id so it would be backwards compatible.

baconpaul commented 4 years ago

yeah I've thought about things like: scanning all the patches into a sqlite database at startup so we could do things like tagging and hashing. but that's a way bigger project.

hems commented 4 years ago

yeah I've thought about things like: scanning all the patches into a sqlite database at startup so we could do things like tagging and hashing. but that's a way bigger project.

yeah, my idea was to hash it and save it on the preset file, but indeed that would require saving a map on disk and updating it every time the preset is saved

baconpaul commented 4 years ago

Yup. And that's exactly the sort of problem that sqlite is for.

mkruselj commented 4 years ago

When we get a midi program change event we basically do 127*bank + patch and then see if that number is within the range of lists of presets in the unordered list (I think; I may have changed it to use the ordered list in the past but I don’t think so). It then loads that patch

I just gave it a shot - it IS using the ordered list!

But... only in VST2! Program changes are completely broken/not functioning in VST3 from what I can tell.

hems commented 4 years ago

i remember when i tried PGM messages would work but if there was a NOTE at the same time the note would not sound or something among those lines

mkruselj commented 4 years ago

That was likely with VST2 then, because Surge VST3 program changes are completely nonfunctional as I said.

baconpaul commented 4 years ago

Surge does an all notes off before any action which loads a patch. It kinda has to because of the shared data structures in the patches. Midi Pgm change would be the same

I think the au supports it too. Although it’s not correctly implemented anywhere

mkruselj commented 4 years ago

I mean sure, the way it is currently implemented (in VST2 or AU) is not ideal. But even that non-ideal system is not at all working in VST3. Maybe it should?

baconpaul commented 4 years ago

Vst3 will require another fake param it seems. You don’t get the message you get midi interception just like cc. I can’t imagine bothering to do this myself but here’s a pointer

https://sdk.steinberg.net/viewtopic.php?t=651

mkruselj commented 4 years ago

What a load of excrement VST3 is...

baconpaul commented 4 years ago

well we already make the 16*128 fake params. What's another 16 I guess? But it is code one would need to write.

mkruselj commented 4 years ago

OK after a bit of pondering today and a discussion with @K0rrid0r, I think I am leaning away from the u-he model (as good as it is and as much as I personally like it).

For Surge, here's what I envision regarding Program Change support:

This would be a really great implementation for Surge, methinks.

hems commented 4 years ago

This would be a really great implementation for Surge, methinks.

sounds amazing

baconpaul commented 4 years ago

That does sound amazing. The bank manager basically

K0rrid0r commented 4 years ago

well i can say right now im hearing completely ridiculous things (sound wise) from the vst2!

MIDI shifting presets 2

example of what it looks like.

so that is a very fast midi program change, now imagine i slow it down to minutes etc. one can really build really big livesets and sound structures from very few surge instances in this way with some smart midi change programming! super neat!

hems commented 4 years ago

so that is a very fast midi program change, now imagine i slow it down to minutes etc. one can really build really big livesets and sound structures from very few surge instances in this way with some smart midi change programming! super neat!

Indeed!

It would also be great on a future version to have interpolation between presets, like Camel Audio Alchemy

image

mkruselj commented 4 years ago

That one seems pretty darn difficult to do. (especially when you have non-continuous parameters that can't easily be morphed).

hems commented 4 years ago

That one seems pretty darn difficult to do. (especially when you have non-continuous parameters that can't easily be morphed).

agreed, definitely very complicated

hems commented 4 years ago

That one seems pretty darn difficult to do. (especially when you have non-continuous parameters that can't easily be morphed).

agreed, definitely very complicated

A non-efficient way of implementing is to have the 4 presets running at the same time ( 1 for each corner of the square ) and then just crossfade the outputs accordingly based on the X/Y position.

So in "morphing mode" it would be basically running 4 instances of the engine.

baconpaul commented 4 years ago

The code has bits and bobs of a scene morph kicking around in there for I think that reason. Basically a dual mode where the output is controlled by a macro. Although you could just map a macro to scene master volume and get the same thing (but it's a wee bit tricky ahead of mod mappers to make it sound OK)