- Sections
- Introduction
- API
- Basics
- Reserved variables
- Stereo output vs keyboard polyphony
- Module's documentation
- Examples
- Generic class to use as a startup
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.
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.
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)
-
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.
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)
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
___________________________________________________________________________________________________
"""
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]
},
}
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]
},
}