SFZ engines: config file & parse controllers

Hi rompler lovers!

I’ve implemented some improvements in the SFZ engines (aka sfizz & linuxsamplers) that will allow to configure custom controllers for each SFZ file. It works like this:

  1. When loading a SFZ file, the engine looks for a config file with the same name, but with “yml” extension (lowercase!). This file contains a full description of the custom controllers for this SFZ. Something like this:
sfz_file: "SCC Expressive Strings-Key Switch-CC1.sfz"

controllers:
 custom:
  articulation:
   graph_path: 'note_on'
   labels: ['legato (cc1)', 'legato (vel)', 'staccato (vel)', 'tremolo (cc1)', 'tremolo (vel)', 'leg/trem xfade', 'pizzicato (vel)']
   ticks: [21, 22, 23, 105, 106, 107, 108]

  expression wheel:
   midi_cc: 1
   value: 64

  legato/trem xfade:
   midi_cc: 67
   value: 0

When the soundfont is loaded, you will get new pages of controllers according to the config, like this:

Note i have removed a lot of controllers from the default config because they didn’t work on most SFZs. I kept only the controllers that works on almost all SFZs.

Also note the “articulation” controller in this example. It’s a new type of controller that sends “note-on” messages and it’s specially thought for allowing “keyswitching” from the zynthian UI. It’s configured by setting the graph_path option to ‘note_on’, and then specifying the list of keyswitches (note numbers) as ticks and their labels.

  1. If a yaml config file is not found for the SFZ file, then the engine will parse the SFZ code and look for these 2 opcodes:

    • set_ccXX=YY
    • label_ccXX=controller_name_without_spaces

where XX is the midi CC numbers and YY is the default value.
If found, the controllers will be added, creating custom pages as needed.

For instance, tried the recently shared “Leisureland_Mellotron” and this is the result:

I decided to not enable the 2 described mechanisms simultaneously, as it would be challenging, although not impossible. My reasoning is that once you decide to write a config file, you should include all the custom controllers on it to get it displayed with the right page names, etc. The parsing mechanism is an automatic but suboptimal mechanism, although it could be improved too.

You can test all this on vangelis branch (testing).

Enjoy!

8 Likes

Wow - Far out!

Impressive @jofemodo :star:.

This Is a remarkable step forward, in the Zynthian equipment as a sampler libraries machine.

Kudos!

Wow thanks! This is great work! Will make SFZ creating for Zynthian much nicer!

That is nothing but fantastic! :heart_eyes:

I saw some controls wrongly attached and I could imagine these have to do with the fact that some (many) sfz use set_hdcc instead of set_cc which simply set the value by normalized numbers 0…1 instead of integer 0…127. Could we add that as well?

Another observation: I have a sfz with

label_cc7=Master Vol CC7 
label_cc10=Master Pan CC10 

Resulting in two controls labeled “Master” and assigned to cc7.

Maybe we could implement a functionality of overwriting existing controls on the basis of cc#. So cc7 implemented in main control page will be overriden by labeled control in sfz if existent, and this again will be overriden by a control in yml for cc7 if present. This would prevent having double controls for the same purpose if based on distinct controls one per cc#.

Automatic articulation control for keyswitches would be wonderful based on sw_lokey/sw_hikey and then sw_last and sw_label on the header.

Again, fantastic implementation! Much love for that! So exited!

2 Likes

Please, give more details. Could you specify or link the SFZs that failed?

Thanks

Certainly. Here’s learman jRhodes. The width of the vibrato samples are implemented like this:

label_cc1=Modwidth
set_hdcc1=0.5

which is basically just the same as stating set_ccN=64. The result is that is shown but not linked to cc. This is the same behaviour for any sfz with that kind of implementation. So something like value of set_hdccN * 127 = set_ccN should do?

There’s also this Noct-Salamander v5.2f:

<control> set_cc7=100  set_cc10=64  set_cc20=64  set_cc21=64    set_cc22=64   set_cc72=0
label_cc7=Vol
label_cc10=Pan
label_cc20=String Res
label_cc21=Hammer Noise
label_cc22=Pedal Noise
label_cc64=Sus Pedal
label_cc72=Release

Strange that here there is also no cc assigned to the control, also no function of it.

Then there is VG Northern Trumpet. Implementation:

<control>
label_cc1=Dynamics CC1
label_cc7=Master Vol CC7

label_cc10=Master Pan CC10
label_cc15=Humanize CC15
label_cc14=Vibrato CC14
set_cc1=127
set_cc7=95
set_cc10=64
set_cc14=0
set_cc15=0

Here are two controls added, labeled “Master” and both assigned to CC7. Moving either of them changes panning strangely.

I’ll add any information you’d need.

Try rewrite to

label_cc7=Master_Vol CC7 
label_cc10=Master_Pan CC10 

Thanks. This would solve the problem, but I just wanted that the algorithm @jofemodo implemented works with these files, because they are in the wild already in that form. Sfz label opcodes allow spaces.

I wonder if the problem could be solved also by having the CC assignment as an identifier.

If I understand your code, it only recognizes opcodes at the beginning of a new line right? As far as I read different sfz and the documentation, opcodes following newline and space should get all possibilities. (so probably something lile “^set_cc” and ’ +',“set_cc” or however that works.)

cc_config = {}
pat1 = re.compile("^set_cc(\d+)=(\d+)", re.MULTILINE)
pat2 = re.compile("^label_cc(\d+)=(\w+)", re.MULTILINE)

I don’t understand exactly, but it seems it incorporates the control only if set_cc and label_cc are present. There might be sfz instruments were not both are present. Maybe the label_cc opcode could be sufficient, if set_cc is not present, otherwise we have a default value.

EDIT:
I do not speak regex so well, but as far as I understand sfz script language if parsing a string value like label containing spaces one should easily distinguish the string because there are 3 possibilities where it could end:

  1. Another opcode, distinguishable by [a-zA-Z0-9_]=
  2. A <header opcode> (which would be typically after new line anyway)
  3. A new line
  4. A variable definition or include [a-zA-Z0-9_$#]
  5. A // comment or /* comment */

I guess you knew that anyway.

I just pushed a few improvements to the SFZ customization code:

  • Optional “common.yml” with the common configuration for all SFZs in the directory. It’s merged with the specific per-sfz config file (sfzfilename.yml), avoiding duplication. Specific config has priority over common config.

  • Parsing sfz has been improved to include “set_hdcc” opcode, manage labels with spaces, etc.

  • Controllers with duplicated CC number are removed. Custom config has priority over default config.

Tested with Noct-Salamander v5 and got this:

Simply update (vangelis) and enjoy!!

3 Likes

Unbelievable. :heart_eyes: I’ll test that immediatly tonight.

So if you consider to implement parsing for keyswitches to automatically build the ticked articulation control like stated in the yml file I’ll happily provide any insight I have about the sfz keyswitching system and the implementation of the corresponding opcodes.

I just tested the implemented features and I must say that these are a really huge step in working with sfz soundfonts. It just works perfect, thank you so much for that.

I only include some remarks on some minor things which could be implemented for fully featuring most of the sfz files found in the wild. Don’t take it as a “feature request”, because it is already so much rewarding to use in the current state. So only if there is a need for following improvements, I leave it for later consideration.

Current implementation (set_cc/label_cc)

  • Some more complex sfz use variable defines in this form (taken and shortened from this example). Maybe it would be possible to resolve these variables defined like this.
#define $MW 1
label_cc$MW=Mod Wheel
set_hdcc$MW=0.3
  • Some really complex sfz instruments use includes especially for key mappings or similar. These are implemented like the following example. They might not be so important for the controls, but some sfz may include some files like common header definitions. It might be difficult to implement because an algorithm for preventing circular includes might be needed.
#include "mappings/kickmic_all.txt"
  • Lastly still the comments (// comment or /* comment */are included in the labels. Actually I partly found them even helpful in some cases, since some sfz scripting people comment what controls are doing after the label opcode.

About Keyswitches

Having an automatically parsed keyswitch control like teasered in the yml example would be great especially for the case you sit on a controller where you cannot reach the C-1 key or you eventually don’t even know about the existence of possible keyswitches. For keyswitches there are five-ish important opcodes to consider. You can group them in two groups like that:

Group 1: These 3 should be present in a script once, typically under the header. Oftentimes you find them multiple times in different or even , but it doesn’t matter, because these definitions are duplicates and it shoud be sufficient to find them once:

  • sw_lokey=[MIDI_Note_Name_or_Number]and sw_hikey=[MIDI_Note_Name_or_Number] define the range in which the possible keyswitches exist. So if you happen to find sw_lokey=36 sw_hikey=38 you know there should 3 keyswitches; 36; 37; 38.
  • sw-default=[MIDI_Note_Name_or_Number] defines the default keyswitch on patch loading. With the example above sw_default=36 would define 36 as default value.

One tricky thing with an implementation would be that keyswitches can be implemented as “MIDI note numbers (0 to 127) or in MIDI note names (C-1 to G9)”. Maybe inside zynthian the triggered note on event for the articulation control can be presented as note names, otherwise some lookup table would be needed.

With labels it can be a bit trickier that with the range and default value. The involved opcodes are (Group 2):

  • sw_last=[MIDI_Note_Name_or_Number] defining the note number triggered connected to the current , or . There is also sw_lolast and sw_hilast to define a range for the same articulation, but that is not found often and might be ignored.
  • sw_label=[label_string_might_contain_spaces] defining the label, but without having a hint itself to which sw_lastit belongs. It rather applies to the keyswitch within the same level, so implementation should work in most cases where sw_last and sw_label have an occurance between two arbitrary <header> opcodes (e.g. <master>, <group> or <region>).

So implementation of the articulation control could be like this (tl;dr):

  • any single occurance of sw_lokey, sw_hikey and sw_default would present the values of the keyswitch range and its default value either as midi note number 0-127 or midi note name C-1 to G9. This would already provide a usable control but without labeling.
  • any occurance of sw_last and sw_label between two occurances of ‘<’[a-z]‘>’ (header opcode) would provide an assignment of a label to one of the keyswitches found above.
3 Likes

Hi @hannesmenzel !

This is already fixed.

For the rest, let’s wait until this development is well tested. Please, open a feature request with all the details, so your proposals are not lost in the forum.

All the best,

1 Like

I did that

1 Like