API Reference

fluidpatcher.FluidPatcher

An interface for running FluidSynth using patches

Provides methods for:

  • loading/saving the config file and bank files
  • applying/creating/copying/deleting patches
  • directly controlling the Synth by modifying fluidsettings, manually adding router rules, and sending MIDI events
  • loading a single soundfont and browsing its presets
Attributes:
  • midi_callback

    a function that takes a pfluidsynth.Midisignal instance as its argument. Will be called when MIDI events are received or custom router rules are triggered. This allows scripts to define and handle their own custom router rules and/or monitor incoming events.
    MidiSignal events have type, chan, par1, and par2 events matching the triggering event. MidiSignals generated by rules have extra attributes corresponding to the rule parameters, plus a val attribute that is the result of parameter routing. Rules with a patch parameter will be modified by FluidPatcher so that the patch attribute corresponds to the patch index. If patch is -1, val is set to the patch increment.

See the documentation for information on bank file format.

Source code in fluidpatcher/__init__.py
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
class FluidPatcher:
    """An interface for running FluidSynth using patches

    Provides methods for:

    - loading/saving the config file and bank files
    - applying/creating/copying/deleting patches
    - directly controlling the Synth by modifying fluidsettings,
      manually adding router rules, and sending MIDI events
    - loading a single soundfont and browsing its presets

    Attributes:
      midi_callback: a function that takes a pfluidsynth.Midisignal instance
        as its argument. Will be called when MIDI events are received or
        custom router rules are triggered. This allows scripts to define
        and handle their own custom router rules and/or monitor incoming events.    
        MidiSignal events have `type`, `chan`, `par1`, and `par2` events matching
        the triggering event. MidiSignals generated by rules have extra attributes
        corresponding to the rule parameters, plus a `val` attribute that is the
        result of parameter routing. Rules with a `patch` parameter will be modified
        by FluidPatcher so that the `patch` attribute corresponds to the patch index.
        If `patch` is -1, `val` is set to the patch increment.

    See the documentation for information on bank file format.
    """

    def __init__(self, cfgfile, **fluidsettings):
        """Creates FluidPatcher and starts FluidSynth

        Starts fluidsynth using settings found in yaml-formatted `cfgfile`.
        Settings passed via `fluidsettings` will override those in config file.
        See https://www.fluidsynth.org/api/fluidsettings.xml for a
        full list and explanation of settings. See documentation
        for config file format.

        Args:
          cfgfile: Path object pointing to config file
          fluidsettings: dictionary of additional fluidsettings
        """

        self.cfgfile = Path(cfgfile)
        self.cfg = parseyaml(self.cfgfile.read_text())
        self.bank = {}
        self.soundfonts = set()
        self.fsynth = Synth(**{**self.cfg.get('fluidsettings', {}), **fluidsettings})
        self.fsynth.midi_callback = self._midisignal_handler
        self.max_channels = self.fluidsetting_get('synth.midi-channels')
        self.patchcord = {'patchcordxxx': {'lib': self.plugindir / 'patchcord', 'audio': 'mono'}}
        self.midi_callback = None

    @property
    def currentbank(self):
        """a Path object pointing to the current bank file"""
        return Path(self.cfg['currentbank']) if 'currentbank' in self.cfg else ''

    @property
    def bankdir(self):
        """Path to bank files"""
        return Path(self.cfg.get('bankdir', 'banks')).resolve()

    @property
    def sfdir(self):
        """Path to soundfonts"""
        return Path(self.cfg.get('soundfontdir', self.bankdir / '../sf2')).resolve()

    @property
    def mfilesdir(self):
        """Path to MIDI files"""
        return Path(self.cfg.get('mfilesdir', self.bankdir / '../midi')).resolve()

    @property
    def plugindir(self):
        """Path to LADSPA effects"""
        return Path(self.cfg.get('plugindir', '')).resolve()

    @property
    def patches(self):
        """List of patch names in the current bank"""
        return list(self.bank.get('patches', {})) if self.bank else []

    def update_config(self):
        """Write current configuration stored in `cfg` to file.
        """
        self.cfgfile.write_text(renderyaml(self.cfg))

    def load_bank(self, bankfile='', raw=''):
        """Load a bank from a file or from raw yaml text

        Parses a yaml stream from a string or file and stores as a
        nested collection of dict and list objects. The top-level
        dict must have at minimum a `patches` element or an error
        is raised. If loaded from a file successfully, that file
        is set as `currentbank` in the config - call update_config()
        to make it persistent.

        Upon loading, resets the synth, loads all necessary soundfonts,
        and applies settings in the `init` element. Returns the yaml stream
        as a string. If called with no arguments, resets the synth and
        restores the current bank from memory.

        Args:
          bankfile: bank file to load, absolute or relative to `bankdir`
          raw: string to parse directly

        Returns: yaml stream that was loaded
        """
        if bankfile:
            try:
                raw = (self.bankdir / bankfile).read_text()
                bank = parseyaml(raw)
            except:
                if Path(bankfile).as_posix() == self.cfg['currentbank']:
                    self.cfg.pop('currentbank', None)
                raise
            else:
                self.bank = bank
                self.cfg['currentbank'] = Path(bankfile).as_posix()
        elif raw:
            bank = parseyaml(raw)
            self.bank = bank
        self._reset_synth()
        self._refresh_bankfonts()
        for zone in self.bank, *self.bank.get('patches', {}).values():
            for midi in zone.get('midiplayers', {}).values():
                midi['file'] = self.mfilesdir / midi['file']
            for fx in zone.get('ladspafx', {}).values():
                fx['lib'] = self.plugindir / fx['lib']
        for syx in self.bank.get('init', {}).get('sysex', []):
            self.fsynth.send_sysex(syx)
        for opt, val in self.bank.get('init', {}).get('fluidsettings', {}).items():
            self.fluidsetting_set(opt, val)
        for msg in self.bank.get('init', {}).get('messages', []):
            self.send_event(msg)
        return raw

    def save_bank(self, bankfile, raw=''):
        """Save a bank file

        Saves the current bank in memory to `bankfile` after rendering it as
        a yaml stream. If `raw` is provided, it is parsed as the new bank and
        its exact contents are written to the file.

        Args:
          bankfile: file to save, absolute or relative to `bankdir`
          raw: exact text to save
        """
        if raw:
            bank = parseyaml(raw)
            self.bank = bank
        else:
            raw = renderyaml(self.bank)
        (self.bankdir / bankfile).write_text(raw)
        self.cfg['currentbank'] = Path(bankfile).as_posix()

    def apply_patch(self, patch):
        """Select a patch and apply its settings

        Read the settings for the patch specified by index or name and combine
        them with bank-level settings. Select presets on specified channels and
        unsets others, clears router rules and applies new ones, activates 
        players and effects and deactivates unused ones, send messages, and
        applies fluidsettings. Patch settings are applied after bank settings.
        If the specified patch isn't found, only bank settings are applied.

        Args:
          patch: patch index or name

        Returns: a list of warnings, if any
        """
        warnings = []
        patch = self._resolve_patch(patch)
        def mrg(kw):
            try: return self.bank.get(kw, {}) | patch.get(kw, {})
            except TypeError: return self.bank.get(kw, []) + patch.get(kw, [])
        # presets
        for ch in range(1, self.max_channels + 1):
            if p := self.bank.get(ch) or patch.get(ch):
                if not self.fsynth.program_select(ch, self.sfdir / p.sfont, p.bank, p.prog):
                    warnings.append(f"Unable to select preset {p} on channel {ch}")
            else: self.fsynth.program_unset(ch)
        # sysex
        for syx in mrg('sysex'):
            self.fsynth.send_sysex(syx)
        # fluidsettings
        for opt, val in mrg('fluidsettings').items():
            self.fluidsetting_set(opt, val)
        # sequencers, arpeggiators, midiplayers
        self.fsynth.players_clear(save=[*mrg('sequencers'), *mrg('arpeggiators'), *mrg('midiplayers')])
        for name, seq in mrg('sequencers').items():
            self.fsynth.sequencer_add(name, **seq)
        for name, arp in mrg('arpeggiators').items():
            self.fsynth.arpeggiator_add(name, **arp)
        for name, midi in mrg('midiplayers').items():
            self.fsynth.midiplayer_add(name, **midi)
        # ladspa effects
        self.fsynth.fxchain_clear(save=mrg('ladspafx'))
        for name, fx in (mrg('ladspafx') | self.patchcord).items():
            self.fsynth.fxchain_add(name, **fx)
        self.fsynth.fxchain_connect()
        # router rules -- invert b/c fluidsynth applies rules last-first
        self.fsynth.router_default()
        rules = [*mrg('router_rules')][::-1]
        if 'clear' in rules:
            self.fsynth.router_clear()
            rules = rules[:rules.index('clear')]
        for rule in rules:
            rule.add(self.fsynth.router_addrule)
        # midi messages
        for msg in mrg('messages'):
            self.send_event(msg)
        return warnings

    def add_patch(self, name, addlike=None):
        """Add a new patch

        Create a new empty patch, or one that copies all settings
        other than instruments from an existing patch

        Args:
          name: a name for the new patch
          addlike: number or name of an existing patch

        Returns: the index of the new patch
        """
        if 'patches' not in self.bank: self.bank['patches'] = {}
        self.bank['patches'][name] = {}
        if addlike:
            addlike = self._resolve_patch(addlike)
            for x in addlike:
                if not isinstance(x, int):
                    self.bank['patches'][name][x] = deepcopy(addlike[x])
        return self.patches.index(name)

    def update_patch(self, patch):
        """Update the current patch

        Instruments and controller values can be changed by program change (PC)
        and continuous controller (CC) messages, but these will not persist
        in the patch unless this function is called. Settings can be saved to
        a new patch by first calling add_patch(), then update_patch() on the
        new patch. The bank file must be saved for updated patches to become
        permanent.

        Args:
          patch: index or name of the patch to update
        """
        patch = self._resolve_patch(patch)
        messages = set(patch.get('messages', []))
        for channel in range(1, self.max_channels + 1):
            for cc, default in enumerate(_CC_DEFAULTS):
                if default < 0: continue
                val = self.fsynth.get_cc(channel, cc)
                if val != default:
                    messages.add(MidiMessage('cc', channel, cc, val))
            info = self.fsynth.program_info(channel)
            if not info:
                patch.pop(channel, None)
                continue
            sfont, bank, prog = info
            sfrel = Path(sfont).relative_to(self.sfdir).as_posix()
            patch[channel] = SFPreset(sfrel, bank, prog)
        if messages:
            patch['messages'] = list(messages)

    def delete_patch(self, patch):
        """Delete a patch from the bank in memory

        Bank file must be saved for deletion to be permanent

        Args:
          patch: index or name of the patch to delete
        """
        if isinstance(patch, int):
            name = self.patches[patch]
        else:
            name = patch
        del self.bank['patches'][name]
        self._refresh_bankfonts()

    def fluidsetting_get(self, opt):
        """Get the current value of a FluidSynth setting

        Args:
          opt: setting name

        Returns: the setting's current value as float, int, or str
        """
        return self.fsynth.get_setting(opt)

    def fluidsetting_set(self, opt, val, patch=None):
        """Change a FluidSynth setting

        Modifies a FluidSynth setting. Settings without a "synth." prefix
        are ignored. If `patch` is provided, these settings are also added to
        the current bank in memory at bank level, and any conflicting
        settings are removed from the specified patch - which should ideally
        be the current patch so that the changes can be heard. The bank file
        must be saved for the changes to become permanent.

        Args:
          opt: setting name
          val: new value to set, type depends on setting
          patch: patch name or index
        """
        if not opt.startswith('synth.'): return
        self.fsynth.setting(opt, val)
        if patch != None:
            if 'fluidsettings' not in self.bank:
                self.bank['fluidsettings'] = {}
            self.bank['fluidsettings'][opt] = val
            patch = self._resolve_patch(patch)
            if 'fluidsettings' in patch and opt in patch['fluidsettings']:
                del patch['fluidsettings'][opt]

    def add_router_rule(self, **pars):
        """Add a router rule to the Synth

        Directly add a router rule to the Synth. This rule will be added
        after the current bank- and patch-level rules. The rule is not
        saved to the bank, and will disappear if a patch is applied
        or the synth is reset.

        Returns:
          pars: router rule as a set of key=value pairs
        """
        RouterRule(**pars).add(self.fsynth.router_addrule)

    def send_event(self, msg=None, type='note', chan=1, par1=0, par2=0):
        """Send a MIDI event to the Synth

        Sends a MidiMessage, or constructs one from a bank file-styled string
        (<type>:<channel>:<par1>:<par2>) or keywords and sends it
        to the Synth, which will apply all current router rules.

        Args:
          msg: MidiMessage instance or string
          type: event type as string
          chan: MIDI channel
          par1: first parameter, integer or note name
          par2: second parameter for valid types
        """
        if isinstance(msg, str):
            msg = parseyaml(msg)
        elif msg == None:
            msg = MidiMessage(type, chan, par1, par2)
        self.fsynth.send_event(*msg)

    def solo_soundfont(self, soundfont):
        """Suspend the current bank and load a single soundfont

        Resets the Synth, loads a single soundfont, and creates router
        rules that route messages from all channels to channel 1.
        Scans through each bank and program in order and retrieves the
        preset name. After this, select_sfpreset() can be used to play
        any instrument in the soundfont. Call load_bank() with no
        arguments to restore the current bank.

        Args:
          soundfont: soundfont file to load, absolute or relative to `sfdir`

        Returns: a list of (bank, prog, name) tuples for each preset
        """
        for sfont in self.soundfonts - {soundfont}:
            self.fsynth.unload_soundfont(self.sfdir / sfont)
        if {soundfont} - self.soundfonts:
            if not self.fsynth.load_soundfont(self.sfdir / soundfont):
                self.soundfonts = set()
                return []
        self.soundfonts = {soundfont}
        self._reset_synth()
        for channel in range(1, self.max_channels + 1):
            self.fsynth.program_unset(channel)
        for type in 'note', 'cc', 'pbend', 'cpress', 'kpress':
            self.add_router_rule(type=type, chan=f"2-{self.max_channels}=1")
        return self.fsynth.get_sfpresets(self.sfdir / soundfont)

    def select_sfpreset(self, sfont, bank, prog, *_):
        """Select a preset on channel 1

        Call to select one of the presets in the soundfont loaded
        by solo_soundfount(). The variable-length garbage argument
        allows this function to be called by unpacking one of the
        tuples returned by solo_soundfont().

        Args:
          sfont: the soundfont file loaded by solo_soundfont(),
            absolute or relative to `sfdir`
          bank: the bank to select
          prog: the program to select from bank

        Returns: a list of warnings, empty if none
        """
        if sfont not in self.soundfonts:
            return [f"{str(sfont)} is not loaded"]
        if self.fsynth.program_select(1, self.sfdir / sfont, bank, prog):
            return []
        else: return [f"Unable to select preset {str(sfont)}:{bank:03d}:{prog:03d}"]

    def _midisignal_handler(self, sig):
        if 'patch' in sig:
            if sig.patch in self.patches:
                sig.patch = self.patches.index(sig.patch)
            elif sig.patch == 'select':
                sig.patch = int(sig.val - 1) % len(self.patches)
            elif str(sig.patch)[-1] in '+-':
                sig.val = int(sig.patch[-1] + sig.patch[:-1])
                sig.patch = -1
            elif isinstance(sig.patch, int):
                sig.patch -= 1
            else:
                sig.patch = -1
                sig.val = 0
        if self.midi_callback: self.midi_callback(sig)

    def _refresh_bankfonts(self):
        sfneeded = set()
        for zone in self.bank, *self.bank.get('patches', {}).values():
            for sfont in [zone[ch].sfont for ch in zone if isinstance(ch, int)]:
                sfneeded.add(sfont)
        missing = set()
        for sfont in self.soundfonts - sfneeded:
            self.fsynth.unload_soundfont(self.sfdir / sfont)
        for sfont in sfneeded - self.soundfonts:
            if not self.fsynth.load_soundfont(self.sfdir / sfont):
                missing.add(sfont)
        self.soundfonts = sfneeded - missing

    def _resolve_patch(self, patch):
        if isinstance(patch, int):
            if 0 <= patch < len(self.patches):
                patch = self.patches[patch]
            else: patch = {}
        if isinstance(patch, str):
            patch = self.bank.get('patches', {}).get(patch, {})
        return patch

    def _reset_synth(self):
        self.fsynth.players_clear()
        self.fsynth.fxchain_clear()
        self.fsynth.router_default()
        self.fsynth.reset()
        for opt, val in {**_SYNTH_DEFAULTS, **self.cfg.get('fluidsettings', {})}.items():
            self.fluidsetting_set(opt, val)

currentbank property

a Path object pointing to the current bank file

bankdir property

Path to bank files

sfdir property

Path to soundfonts

mfilesdir property

Path to MIDI files

plugindir property

Path to LADSPA effects

patches property

List of patch names in the current bank

__init__(cfgfile, **fluidsettings)

Creates FluidPatcher and starts FluidSynth

Starts fluidsynth using settings found in yaml-formatted cfgfile. Settings passed via fluidsettings will override those in config file. See https://www.fluidsynth.org/api/fluidsettings.xml for a full list and explanation of settings. See documentation for config file format.

Parameters:
  • cfgfile

    Path object pointing to config file

  • fluidsettings

    dictionary of additional fluidsettings

Source code in fluidpatcher/__init__.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
def __init__(self, cfgfile, **fluidsettings):
    """Creates FluidPatcher and starts FluidSynth

    Starts fluidsynth using settings found in yaml-formatted `cfgfile`.
    Settings passed via `fluidsettings` will override those in config file.
    See https://www.fluidsynth.org/api/fluidsettings.xml for a
    full list and explanation of settings. See documentation
    for config file format.

    Args:
      cfgfile: Path object pointing to config file
      fluidsettings: dictionary of additional fluidsettings
    """

    self.cfgfile = Path(cfgfile)
    self.cfg = parseyaml(self.cfgfile.read_text())
    self.bank = {}
    self.soundfonts = set()
    self.fsynth = Synth(**{**self.cfg.get('fluidsettings', {}), **fluidsettings})
    self.fsynth.midi_callback = self._midisignal_handler
    self.max_channels = self.fluidsetting_get('synth.midi-channels')
    self.patchcord = {'patchcordxxx': {'lib': self.plugindir / 'patchcord', 'audio': 'mono'}}
    self.midi_callback = None

update_config()

Write current configuration stored in cfg to file.

Source code in fluidpatcher/__init__.py
108
109
110
111
def update_config(self):
    """Write current configuration stored in `cfg` to file.
    """
    self.cfgfile.write_text(renderyaml(self.cfg))

load_bank(bankfile='', raw='')

Load a bank from a file or from raw yaml text

Parses a yaml stream from a string or file and stores as a nested collection of dict and list objects. The top-level dict must have at minimum a patches element or an error is raised. If loaded from a file successfully, that file is set as currentbank in the config - call update_config() to make it persistent.

Upon loading, resets the synth, loads all necessary soundfonts, and applies settings in the init element. Returns the yaml stream as a string. If called with no arguments, resets the synth and restores the current bank from memory.

Parameters:
  • bankfile

    bank file to load, absolute or relative to bankdir

  • raw

    string to parse directly

Returns: yaml stream that was loaded

Source code in fluidpatcher/__init__.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
def load_bank(self, bankfile='', raw=''):
    """Load a bank from a file or from raw yaml text

    Parses a yaml stream from a string or file and stores as a
    nested collection of dict and list objects. The top-level
    dict must have at minimum a `patches` element or an error
    is raised. If loaded from a file successfully, that file
    is set as `currentbank` in the config - call update_config()
    to make it persistent.

    Upon loading, resets the synth, loads all necessary soundfonts,
    and applies settings in the `init` element. Returns the yaml stream
    as a string. If called with no arguments, resets the synth and
    restores the current bank from memory.

    Args:
      bankfile: bank file to load, absolute or relative to `bankdir`
      raw: string to parse directly

    Returns: yaml stream that was loaded
    """
    if bankfile:
        try:
            raw = (self.bankdir / bankfile).read_text()
            bank = parseyaml(raw)
        except:
            if Path(bankfile).as_posix() == self.cfg['currentbank']:
                self.cfg.pop('currentbank', None)
            raise
        else:
            self.bank = bank
            self.cfg['currentbank'] = Path(bankfile).as_posix()
    elif raw:
        bank = parseyaml(raw)
        self.bank = bank
    self._reset_synth()
    self._refresh_bankfonts()
    for zone in self.bank, *self.bank.get('patches', {}).values():
        for midi in zone.get('midiplayers', {}).values():
            midi['file'] = self.mfilesdir / midi['file']
        for fx in zone.get('ladspafx', {}).values():
            fx['lib'] = self.plugindir / fx['lib']
    for syx in self.bank.get('init', {}).get('sysex', []):
        self.fsynth.send_sysex(syx)
    for opt, val in self.bank.get('init', {}).get('fluidsettings', {}).items():
        self.fluidsetting_set(opt, val)
    for msg in self.bank.get('init', {}).get('messages', []):
        self.send_event(msg)
    return raw

save_bank(bankfile, raw='')

Save a bank file

Saves the current bank in memory to bankfile after rendering it as a yaml stream. If raw is provided, it is parsed as the new bank and its exact contents are written to the file.

Parameters:
  • bankfile

    file to save, absolute or relative to bankdir

  • raw

    exact text to save

Source code in fluidpatcher/__init__.py
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
def save_bank(self, bankfile, raw=''):
    """Save a bank file

    Saves the current bank in memory to `bankfile` after rendering it as
    a yaml stream. If `raw` is provided, it is parsed as the new bank and
    its exact contents are written to the file.

    Args:
      bankfile: file to save, absolute or relative to `bankdir`
      raw: exact text to save
    """
    if raw:
        bank = parseyaml(raw)
        self.bank = bank
    else:
        raw = renderyaml(self.bank)
    (self.bankdir / bankfile).write_text(raw)
    self.cfg['currentbank'] = Path(bankfile).as_posix()

apply_patch(patch)

Select a patch and apply its settings

Read the settings for the patch specified by index or name and combine them with bank-level settings. Select presets on specified channels and unsets others, clears router rules and applies new ones, activates players and effects and deactivates unused ones, send messages, and applies fluidsettings. Patch settings are applied after bank settings. If the specified patch isn't found, only bank settings are applied.

Parameters:
  • patch

    patch index or name

Returns: a list of warnings, if any

Source code in fluidpatcher/__init__.py
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
def apply_patch(self, patch):
    """Select a patch and apply its settings

    Read the settings for the patch specified by index or name and combine
    them with bank-level settings. Select presets on specified channels and
    unsets others, clears router rules and applies new ones, activates 
    players and effects and deactivates unused ones, send messages, and
    applies fluidsettings. Patch settings are applied after bank settings.
    If the specified patch isn't found, only bank settings are applied.

    Args:
      patch: patch index or name

    Returns: a list of warnings, if any
    """
    warnings = []
    patch = self._resolve_patch(patch)
    def mrg(kw):
        try: return self.bank.get(kw, {}) | patch.get(kw, {})
        except TypeError: return self.bank.get(kw, []) + patch.get(kw, [])
    # presets
    for ch in range(1, self.max_channels + 1):
        if p := self.bank.get(ch) or patch.get(ch):
            if not self.fsynth.program_select(ch, self.sfdir / p.sfont, p.bank, p.prog):
                warnings.append(f"Unable to select preset {p} on channel {ch}")
        else: self.fsynth.program_unset(ch)
    # sysex
    for syx in mrg('sysex'):
        self.fsynth.send_sysex(syx)
    # fluidsettings
    for opt, val in mrg('fluidsettings').items():
        self.fluidsetting_set(opt, val)
    # sequencers, arpeggiators, midiplayers
    self.fsynth.players_clear(save=[*mrg('sequencers'), *mrg('arpeggiators'), *mrg('midiplayers')])
    for name, seq in mrg('sequencers').items():
        self.fsynth.sequencer_add(name, **seq)
    for name, arp in mrg('arpeggiators').items():
        self.fsynth.arpeggiator_add(name, **arp)
    for name, midi in mrg('midiplayers').items():
        self.fsynth.midiplayer_add(name, **midi)
    # ladspa effects
    self.fsynth.fxchain_clear(save=mrg('ladspafx'))
    for name, fx in (mrg('ladspafx') | self.patchcord).items():
        self.fsynth.fxchain_add(name, **fx)
    self.fsynth.fxchain_connect()
    # router rules -- invert b/c fluidsynth applies rules last-first
    self.fsynth.router_default()
    rules = [*mrg('router_rules')][::-1]
    if 'clear' in rules:
        self.fsynth.router_clear()
        rules = rules[:rules.index('clear')]
    for rule in rules:
        rule.add(self.fsynth.router_addrule)
    # midi messages
    for msg in mrg('messages'):
        self.send_event(msg)
    return warnings

add_patch(name, addlike=None)

Add a new patch

Create a new empty patch, or one that copies all settings other than instruments from an existing patch

Parameters:
  • name

    a name for the new patch

  • addlike

    number or name of an existing patch

Returns: the index of the new patch

Source code in fluidpatcher/__init__.py
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
def add_patch(self, name, addlike=None):
    """Add a new patch

    Create a new empty patch, or one that copies all settings
    other than instruments from an existing patch

    Args:
      name: a name for the new patch
      addlike: number or name of an existing patch

    Returns: the index of the new patch
    """
    if 'patches' not in self.bank: self.bank['patches'] = {}
    self.bank['patches'][name] = {}
    if addlike:
        addlike = self._resolve_patch(addlike)
        for x in addlike:
            if not isinstance(x, int):
                self.bank['patches'][name][x] = deepcopy(addlike[x])
    return self.patches.index(name)

update_patch(patch)

Update the current patch

Instruments and controller values can be changed by program change (PC) and continuous controller (CC) messages, but these will not persist in the patch unless this function is called. Settings can be saved to a new patch by first calling add_patch(), then update_patch() on the new patch. The bank file must be saved for updated patches to become permanent.

Parameters:
  • patch

    index or name of the patch to update

Source code in fluidpatcher/__init__.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
def update_patch(self, patch):
    """Update the current patch

    Instruments and controller values can be changed by program change (PC)
    and continuous controller (CC) messages, but these will not persist
    in the patch unless this function is called. Settings can be saved to
    a new patch by first calling add_patch(), then update_patch() on the
    new patch. The bank file must be saved for updated patches to become
    permanent.

    Args:
      patch: index or name of the patch to update
    """
    patch = self._resolve_patch(patch)
    messages = set(patch.get('messages', []))
    for channel in range(1, self.max_channels + 1):
        for cc, default in enumerate(_CC_DEFAULTS):
            if default < 0: continue
            val = self.fsynth.get_cc(channel, cc)
            if val != default:
                messages.add(MidiMessage('cc', channel, cc, val))
        info = self.fsynth.program_info(channel)
        if not info:
            patch.pop(channel, None)
            continue
        sfont, bank, prog = info
        sfrel = Path(sfont).relative_to(self.sfdir).as_posix()
        patch[channel] = SFPreset(sfrel, bank, prog)
    if messages:
        patch['messages'] = list(messages)

delete_patch(patch)

Delete a patch from the bank in memory

Bank file must be saved for deletion to be permanent

Parameters:
  • patch

    index or name of the patch to delete

Source code in fluidpatcher/__init__.py
292
293
294
295
296
297
298
299
300
301
302
303
304
305
def delete_patch(self, patch):
    """Delete a patch from the bank in memory

    Bank file must be saved for deletion to be permanent

    Args:
      patch: index or name of the patch to delete
    """
    if isinstance(patch, int):
        name = self.patches[patch]
    else:
        name = patch
    del self.bank['patches'][name]
    self._refresh_bankfonts()

fluidsetting_get(opt)

Get the current value of a FluidSynth setting

Parameters:
  • opt

    setting name

Returns: the setting's current value as float, int, or str

Source code in fluidpatcher/__init__.py
307
308
309
310
311
312
313
314
315
def fluidsetting_get(self, opt):
    """Get the current value of a FluidSynth setting

    Args:
      opt: setting name

    Returns: the setting's current value as float, int, or str
    """
    return self.fsynth.get_setting(opt)

fluidsetting_set(opt, val, patch=None)

Change a FluidSynth setting

Modifies a FluidSynth setting. Settings without a "synth." prefix are ignored. If patch is provided, these settings are also added to the current bank in memory at bank level, and any conflicting settings are removed from the specified patch - which should ideally be the current patch so that the changes can be heard. The bank file must be saved for the changes to become permanent.

Parameters:
  • opt

    setting name

  • val

    new value to set, type depends on setting

  • patch

    patch name or index

Source code in fluidpatcher/__init__.py
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
def fluidsetting_set(self, opt, val, patch=None):
    """Change a FluidSynth setting

    Modifies a FluidSynth setting. Settings without a "synth." prefix
    are ignored. If `patch` is provided, these settings are also added to
    the current bank in memory at bank level, and any conflicting
    settings are removed from the specified patch - which should ideally
    be the current patch so that the changes can be heard. The bank file
    must be saved for the changes to become permanent.

    Args:
      opt: setting name
      val: new value to set, type depends on setting
      patch: patch name or index
    """
    if not opt.startswith('synth.'): return
    self.fsynth.setting(opt, val)
    if patch != None:
        if 'fluidsettings' not in self.bank:
            self.bank['fluidsettings'] = {}
        self.bank['fluidsettings'][opt] = val
        patch = self._resolve_patch(patch)
        if 'fluidsettings' in patch and opt in patch['fluidsettings']:
            del patch['fluidsettings'][opt]

add_router_rule(**pars)

Add a router rule to the Synth

Directly add a router rule to the Synth. This rule will be added after the current bank- and patch-level rules. The rule is not saved to the bank, and will disappear if a patch is applied or the synth is reset.

Returns:
  • pars

    router rule as a set of key=value pairs

Source code in fluidpatcher/__init__.py
342
343
344
345
346
347
348
349
350
351
352
353
def add_router_rule(self, **pars):
    """Add a router rule to the Synth

    Directly add a router rule to the Synth. This rule will be added
    after the current bank- and patch-level rules. The rule is not
    saved to the bank, and will disappear if a patch is applied
    or the synth is reset.

    Returns:
      pars: router rule as a set of key=value pairs
    """
    RouterRule(**pars).add(self.fsynth.router_addrule)

send_event(msg=None, type='note', chan=1, par1=0, par2=0)

Send a MIDI event to the Synth

Sends a MidiMessage, or constructs one from a bank file-styled string (:::) or keywords and sends it to the Synth, which will apply all current router rules.

Parameters:
  • msg

    MidiMessage instance or string

  • type

    event type as string

  • chan

    MIDI channel

  • par1

    first parameter, integer or note name

  • par2

    second parameter for valid types

Source code in fluidpatcher/__init__.py
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
def send_event(self, msg=None, type='note', chan=1, par1=0, par2=0):
    """Send a MIDI event to the Synth

    Sends a MidiMessage, or constructs one from a bank file-styled string
    (<type>:<channel>:<par1>:<par2>) or keywords and sends it
    to the Synth, which will apply all current router rules.

    Args:
      msg: MidiMessage instance or string
      type: event type as string
      chan: MIDI channel
      par1: first parameter, integer or note name
      par2: second parameter for valid types
    """
    if isinstance(msg, str):
        msg = parseyaml(msg)
    elif msg == None:
        msg = MidiMessage(type, chan, par1, par2)
    self.fsynth.send_event(*msg)

solo_soundfont(soundfont)

Suspend the current bank and load a single soundfont

Resets the Synth, loads a single soundfont, and creates router rules that route messages from all channels to channel 1. Scans through each bank and program in order and retrieves the preset name. After this, select_sfpreset() can be used to play any instrument in the soundfont. Call load_bank() with no arguments to restore the current bank.

Parameters:
  • soundfont

    soundfont file to load, absolute or relative to sfdir

Returns: a list of (bank, prog, name) tuples for each preset

Source code in fluidpatcher/__init__.py
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
def solo_soundfont(self, soundfont):
    """Suspend the current bank and load a single soundfont

    Resets the Synth, loads a single soundfont, and creates router
    rules that route messages from all channels to channel 1.
    Scans through each bank and program in order and retrieves the
    preset name. After this, select_sfpreset() can be used to play
    any instrument in the soundfont. Call load_bank() with no
    arguments to restore the current bank.

    Args:
      soundfont: soundfont file to load, absolute or relative to `sfdir`

    Returns: a list of (bank, prog, name) tuples for each preset
    """
    for sfont in self.soundfonts - {soundfont}:
        self.fsynth.unload_soundfont(self.sfdir / sfont)
    if {soundfont} - self.soundfonts:
        if not self.fsynth.load_soundfont(self.sfdir / soundfont):
            self.soundfonts = set()
            return []
    self.soundfonts = {soundfont}
    self._reset_synth()
    for channel in range(1, self.max_channels + 1):
        self.fsynth.program_unset(channel)
    for type in 'note', 'cc', 'pbend', 'cpress', 'kpress':
        self.add_router_rule(type=type, chan=f"2-{self.max_channels}=1")
    return self.fsynth.get_sfpresets(self.sfdir / soundfont)

select_sfpreset(sfont, bank, prog, *_)

Select a preset on channel 1

Call to select one of the presets in the soundfont loaded by solo_soundfount(). The variable-length garbage argument allows this function to be called by unpacking one of the tuples returned by solo_soundfont().

Parameters:
  • sfont

    the soundfont file loaded by solo_soundfont(), absolute or relative to sfdir

  • bank

    the bank to select

  • prog

    the program to select from bank

Returns: a list of warnings, empty if none

Source code in fluidpatcher/__init__.py
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
def select_sfpreset(self, sfont, bank, prog, *_):
    """Select a preset on channel 1

    Call to select one of the presets in the soundfont loaded
    by solo_soundfount(). The variable-length garbage argument
    allows this function to be called by unpacking one of the
    tuples returned by solo_soundfont().

    Args:
      sfont: the soundfont file loaded by solo_soundfont(),
        absolute or relative to `sfdir`
      bank: the bank to select
      prog: the program to select from bank

    Returns: a list of warnings, empty if none
    """
    if sfont not in self.soundfonts:
        return [f"{str(sfont)} is not loaded"]
    if self.fsynth.program_select(1, self.sfdir / sfont, bank, prog):
        return []
    else: return [f"Unable to select preset {str(sfont)}:{bank:03d}:{prog:03d}"]

The full API reference can be found in the official documentation