# encoding: utf-8
''' 
---- 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

---- Introduction ----

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

- In one or more python files:
    1 - Classes implementing custom dsp chains.
    2 - A dictionary, called `MODULES`, specifying the modules and their properties.
- In the preferences panel:
    3 - The path to the directory where you saved your python files. Several modules can be defined in a single file.


---- 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, derived
        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.


---- 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)


---- 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.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.
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.

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.


---- 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)


---- 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
___________________________________________________________________________________________________
"""


---- 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.pitchVar = 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.pitchVar[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.pitchVar[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]
                    },
          }


---- 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.filt1 = Biquad(self.fm1, freq=self.p3, q=1, type=0)
        self.filt2 = Biquad(self.fm2, freq=self.p3, q=1, type=0)
        self.out = Mix([self.filt1, self.filt2], voices=2)

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]
                    },
          }

'''
