Skip to content

Latest commit

 

History

History
320 lines (243 loc) · 14.3 KB

CustomModule.md

File metadata and controls

320 lines (243 loc) · 14.3 KB

Welcome to the tutorial on how to create a custom zyne module.

1. Sections

  1. Sections
  2. Introduction
  3. API
  4. Basics
  5. Reserved variables
  6. Stereo output vs keyboard polyphony
  7. Module's documentation
  8. Examples
  9. Generic class to use as a startup

2. Introduction

What must be defined to include custom modules in the zyne application ?

  • In a python file:
    • Classes implementing custom dsp chains.
    • A dictionary, called MODULES, specifying the modules and their properties.
  • In the preferences panel:
    • The path to your python file. Several modules can be defined in the file.

3. API

class BaseSynth(self, config, mode=1)

Parameters :

config : dict

This is the user-defined configuration dictionnary automatically sent by the application to the module in order to properly initialize the module. It must be passed directly from the module's init method to the BaseSynth's init method.

mode : int {1, 2, 3}, optional

mode=1 (default) means that the pitches from the keyboard are directly converted in Hertz. mode=2 means that the pitches from the keyboard are converted into transposition factor with the Midi key 60 as 1.0, eg. no transposition. mode=3 means that the pitches from the keyboard keep their Midi note value.

The keyboard can be transposed in semitones before the midi-to-hertz or the midi-to-transpo conversion. This is automatically done when a slider is defined with "Transposition" as the param_name.

Attributes :

self.pitch : PyoObject

this variable contains frequencies, in Hertz, Midi notes or transposition factors, from the pitches played on keyboard.

self.amp : PyoObject

this variable contains the ADSR amplitude envelope, normalized between 0 and 1, from the velocities played on the keyboard.

self.panL, self.panR : PyoObject

These variables contains panning values for left and right channels. The user has to multiply his left and right signals with these variables in order to use the "Pan" slider.

self.trig : PyoObject

this variable contains trigger streams generated by the noteon played on the keyboard. Useful to trig an envelope or a sound at the beginning of a note.

self.p1, self.p2, self.p3 : PyoObject

user-defined slider's values.

self.module_path : string

Path of the "custom modules" folder if set in the preferences panel.

self.export_path : string

Path of the "exported sounds" folder if set in the preferences panel.

MODULES = {module_name : {  "title" : title_to_be_displayed,
                            "synth" : ref_to_custom_class,
                            "p1" : [param_name, init, min, max, is_integer, is_log],
                            "p2" : [param_name, init, min, max, is_integer, is_log],
                            "p3" : [param_name, init, min, max, is_integer, is_log]
                          },
          }

All custom module's properties are defined in a dictionary of dictionaries called "MODULES", one dictionary per module.

Syntax:

  • module_name : str

Reference name of the module, as it will appear in the Modules menu. Value for this key is the dictionary of properties for the module.

  • title_to_be_displayed : str

String that will appear at the top of the module panel.

  • ref_to_custom_class : class derived from BaseSynth

Reference to the class implementing the dsp chain.

  • param_name : str

Label of the slider for the parameter. If "Transposition" is used as the param_name, the slider will be automatically used to transpose the note before the the midi-to-hertz or the midi-to-transpo conversion. The slider's properties (init, min, max, is_int) must be integer.

  • init : int or float

Initial value of the slider.

  • min : int or float

Minimum value of the slider.

  • max : int or float

Maximum value of the slider.

  • is_integer : boolean

Set this value to True to create a slider of integers or False to create a slider of floats.

  • is_log : boolean

Set this value to True to create a logarithmic slider (min must be non-zero) or False to create a linear slider.

4. Basics

Each module must be derived from the class "BaseSynth" (you don't need to import specific modules since your file will be executed in the proper environment).

The "BaseSynth" class is where are handled "pitch", "amplitude", "polyphony", user-defined attributes (p1, p2, p3) and samples exportation.

Initialisation of the class BaseSynth:

BaseSynth.__init__(self, config, mode)

So your custom class should be defined like this:

class MySound(BaseSynth):
    def __init__(self, config):
        BaseSynth.__init__(self, config, mode=1)

5. Reserved variables

  • self.pitch : this variable contains frequencies, in Hertz, Midi notes or transposition factors, from the pitches played on keyboard.

  • self.amp : this variable contains the ADSR amplitude envelope, normalized between 0 and 1, derived from the velocities played on the keyboard.

  • self.panL, self.panR : These variables contains panning values for left and right channels. The user has to multiply his left and right signals with these variables in order to use the "Pan" slider.

  • self.trig : this variable contains trigger streams generated by the noteon played on the keyboard. Useful to trig an envelope or a sound at the beginning of the note.

  • self.p1, self.p2, self.p3 : user-defined slider's values.

  • self.module_path : Path of the "custom modules" folder if set in the preferences panel.

  • self.export_path : Path of the "exported sounds" folder if set in the preferences panel.

  • self.out : This variable must be the object that send the sound to the output. Although it is possible, the custom class should not called the out method of any object. Every signals must be mixed in the self.out variable, which will then be sent to the post-processing effects and finally to the soundcard.

To minimise conflicts between variable's names, all other variables used in the class "BaseSynth" begin with an underscore. If you don't use this syntax in your custom classes, you will avoid to override basic module's objects.

6. Stereo output vs keyboard polyphony

The best way to manage the keyboard polyphony without corrupting the stereo output is to process each channel independently and to mix everything at the very end. All reserved variables contains polyphony audio streams, that means that every object with one of these variables as argument will contains polyphony audio streams. Here is an example:

# old-school sample-and-hold synth
self.fr = Phasor(freq=self.p1, mul=self.pitch*self.p3, add=self.pitch)
self.ctl = Phasor(freq=self.p2)
self.realfreq = SampHold(self.fr, self.ctl)
# amplitude normalization
self.norm_amp = self.amp * 0.1
# left channel with `polyphony` streams
self.ampL = self.norm_amp * self.panL
self.lfo1 = LFO(self.realfreq, sharp=1, type=3, mul=self.ampL)
# right channel with `polyphony` streams
self.ampR = self.norm_amp * self.panR
self.lfo2 = LFO(self.realfreq*1.012, sharp=1, type=3, mul=self.ampR)
# mix all streams in each channel to mono
self.lfo1_mono = self.lfo1.mix()
self.lfo2_mono = self.lfo2.mix()
# take the two mono streams to create a stereo output
self.out = Mix([self.lfo1_mono, self.lfo2_mono], voices=2)

7. Module's documentation

The module documentation, accessible via the question mark on the interface must be included in the doc string variable of the custom class. The following syntax must be respected:

"""
Short description of the module.

Longer explication about the audio process implemented.

Parameters:

    First param : Description
    Second param : Description
    Third param : Description

___________________________________________________________________________________________________
Author : Your Name - year
___________________________________________________________________________________________________
"""

8. Examples

Example of a file containing two modules. First, a simple module implementing a chorus of sine waves using semitone transposition and second, a soundfile looper/slicer using transposition factor derived from the keyboard's pitches.

class ChoruSyn(BaseSynth):
    """
    Simple chorus of six sine waves.
    
    Six sine waves with control on the overall frequency deviation.

    Parameters:

        Transposition : Transposition, in semitones, of the pitches played on the keyboard.
        Deviation speed : Speed of the interpolated random applied on each wave.
        Deviation range : Amplitude of the interpolated random applied on each wave.
    
    _______________________________________________________________________________________
    Author : Olivier Bélanger - 2011
    _______________________________________________________________________________________
    """
    def __init__(self, config):
        BaseSynth.__init__(self, config, mode=1)
        # First slider is used as semitone transpo, so self.p1 is not defined
        # self.p2 = "Deviation speed"
        # self.p3 = "Deviation range"
        # 6 interpolated randoms
        self.pitchVar = Randi(min=0.-self.p3, max=self.p3, 
                              freq=self.p2*[random.uniform(.95, 1.05) for i in range(6)], add=1)
        # 6 oscillators (separated to properly handle keyboard polyphony)
        self.norm_amp = self.amp * .1
        self.leftamp = self.norm_amp*self.panL
        self.rightamp = self.norm_amp*self.panR
        self.osc1 = Sine(freq=self.pitch*self.pitchVar[0], mul=self.leftamp).mix(1)
        self.osc2 = Sine(freq=self.pitch*self.pitchVar[1], mul=self.rightamp).mix(1)
        self.osc3 = Sine(freq=self.pitch*self.pitchVar[2], mul=self.leftamp).mix(1)
        self.osc4 = Sine(freq=self.pitch*self.pitchVar[3], mul=self.rightamp).mix(1)
        self.osc5 = Sine(freq=self.pitch*self.pitchVar[4], mul=self.leftamp).mix(1)
        self.osc6 = Sine(freq=self.pitch*self.pitchVar[5], mul=self.rightamp).mix(1)

        # stereo mix of all oscillators
        self.out = Mix([self.osc1, self.osc2, self.osc3, self.osc4, self.osc5, self.osc6], voices=2).out()

class SndLooper(BaseSynth):
    """
    Soundfile looper/slicer.
    
    This module loads a soundfile in memory and reads it with a slicing algorithm. Each slice
    takes a new starting point and a new duration. The overall transposition is controled by 
    the pitches played on the keyboard. Midi key 60 (middle C) is the key where there is no 
    transposition.

    Parameters:

        Transposition : Transposition, in semitones, of the pitches played on the keyboard.
        Deviation speed : Speed of the interpolated random applied on the starting point.
        Deviation range : Amplitude of the interpolated random applied on the starting point.
    
    _______________________________________________________________________________________
    Author : Olivier Bélanger - 2011
    _______________________________________________________________________________________
    """
    def __init__(self, config):
        BaseSynth.__init__(self, config, mode=2)
        self.table = SndTable(SNDS_PATH+"/transparent.aif")
        self.st = Phasor(.1, mul=self.table.getDur()-.25)
        self.dur = Choice([.125, .125, .125, .25, .25, .5], freq=1)
        self.varFreq = self.p2*[random.uniform(.95, 1.05) for i in range(2)]
        self.startVar = Randi(min=0.-self.p3, max=self.p3, freq=self.varFreq, add=1)
        self.looper1 = Looper(self.table, pitch=self.pitch, start=self.st*self.startVar[0], 
                          dur=self.dur, interp=4, autosmooth=True, mul=self.amp*self.panL).mix(1)
        self.looper2 = Looper(self.table, pitch=self.pitch, start=self.st*self.startVar[1], 
                          dur=self.dur, interp=4, autosmooth=True, mul=self.amp*self.panR).mix(1)
        self.out = Mix([self.looper1, self.looper2], voices=2)

MODULES = {
            "ChoruSyn": { "title": "- Chorused sines -", "synth": ChoruSyn, 
                    "p1": ["Transposition", 0, -36, 36, True, False],
                    "p2": ["Deviation speed", 1, .1, 10, False, False],
                    "p3": ["Deviation range", 0.02, 0.001, .5, False, True]
                    },
            "SndLooper": { "title": "- Sound Looper -", "synth": SndLooper, 
                    "p1": ["Transposition", 0, -36, 36, True, False],
                    "p2": ["Deviation speed", 1, .1, 10, False, False],
                    "p3": ["Deviation range", 0.02, 0.001, .5, False, True]
                    },
          }

9. Generic class to use as a startup

class GenericModule(BaseSynth):
    """
    Simple frequency modulation synthesis.
    
    With frequency modulation, the timbre of a simple waveform is changed by 
    frequency modulating it with a modulating frequency that is also in the audio
    range, resulting in a more complex waveform and a different-sounding tone.

    Parameters:

        FM Ratio : Ratio between carrier frequency and modulation frequency.
        FM Index : Represents the number of sidebands on each side of the carrier frequency.
        Lowpass Cutoff : Cutoff frequency of the lowpass filter.
    
    ________________________________________________________________________________________
    Author : Olivier Bélanger - 2011
    ________________________________________________________________________________________
    """
    def __init__(self, config):
        # `mode` handles pitch conversion : 1 for hertz, 2 for transpo, 3 for midi
        BaseSynth.__init__(self, config, mode=1)
        self.fm1 = FM(carrier=self.pitch, ratio=self.p1, index=self.p2, mul=self.amp*self.panL).mix(1)
        self.fm2 = FM(carrier=self.pitch*0.997, ratio=self.p1, index=self.p2, mul=self.amp*self.panR).mix(1)
        self.mix = Mix([self.fm1, self.fm2], voices=2)
        self.out = Biquad(self.mix, freq=self.p3, q=1, type=0)

MODULES = {
            "GenericModule": { "title": "- Generic module -", "synth": GenericModule, 
                    "p1": ["Ratio", 0.5, 0, 10, False, False],
                    "p2": ["Index", 5, 0, 20, False, False],
                    "p3": ["LP cutoff", 4000, 100, 15000, False, True]
                    },
          }