- Speaker warning
- Experimental, WIP, math
Python script that simulates a basic modular synthesizer. Requires a separate MIDI input device, however a virtual one may be used on supported platforms. Generates samples based on waveforms and processes them through modules. Allows control of the modules through MIDI messages.
Requires the following Python packages to be installed:
The file parts.py contains a series of classes used to implement the functionality of a synthesizer.
Algorithms for use throughout ppms. Implemented in the ppms_algs class.
Generates waveforms based on the following types:
- sawtooth /|
- triangle /\
- square |_|
- sine ~
These are implemented in the oscillator class. You can select which waveform is generated using MIDI program change.
Loads modules listed in settings and stores for processing. Implemented in the patchboard class. When a note is played, the data is passed through each loaded module in order.
Processes the note data. This allows implementation of effect processors. An abstract base class synthmod is used to create a new module. See the section on Modules for more information.
An abstract base class mod_control that Synth Module classes can extend to allow reading from a mod wheel on a keyboard.
Modules are used to process the waveform signal. These are loaded from the settings (see below) then the signal is passed through each in the order they were loaded. You can then set up bindings to control module parameters using MIDI controls.
File | Description |
---|---|
mod.test | For testing MIDI control bindings. |
mod.reverb | Adds reverberation effect. |
mod.bpass | Provides a high-pass and low-pass filter. |
Settings can be found in the file settings.json. One will be created automatically the first time the script is ran.
You can set the sample rate here. Defaults to 44100Hz. Value is a float.
"sample_rate": 44100.0,
The MIDI note on/off messages. Defaults to the following:
"sawtooth_on": 144,
"sawtooth_off": 128,
"triangle_on": 145,
"triangle_off": 129,
"square_on": 146,
"square_off": 130,
"sine_on": 147,
"sine_off": 131,
The load preset message is defined as:
"preset_msg": 192,
Set the impact weight. This is used for factoring keyboard velocity.
"impact_weight": 20000,
Folder to load preset files from.
"preset_folder": "presets",
Load modules to process the signal. The signal will be filtered through each module in order added.
"modules": [ "mod.test", "mod.another" ],
List preset files in order here. These files must be located in the folder as indicated by the preset_folder setting.
"presets": [ "example1.json", "example2.json" ],
Run with --build_presets to generate a list from the presets folder.
Bind MIDI controls to modules or general settings.
Format: binding_name, midi_msg[0], midi_msg[1]
"bindings": [
# Default bindings
[ "master_volume", 176, 29 ],
[ "pitch_wheel", 224, 0 ],
[ "mod_wheel", 176, 1 ],
# Module bindings
# Binding names should have the format class_name.member_name
[ "test_module.set_a_value", 176, 118 ]
],
Modules will store their setting data here on shutdown, then restore them on next run.
"module_data": []
To make a module, create a Python file in the mod folder. Define the module as a class and extend synthmod from parts.py. At minimum the process function needs to be defined. The class can then be composed as following:
- process function - Define what happens with the signal.
def process(self, note, signal):
# Do something with the signal
return signal
- save_data function - Return an array of binding names and the variable they are associated with.
def save_data(self):
return [
[ "example.control_a", self.value_a ],
[ "example.control_b", self.value_b ]
]
For each control in the module, create a seperate function to set its value. Then to create bindings to these controls, use the format class_name.function_name.
from .parts import synthmod
## PPMS Synth Module for testing the patchboard.
class test_module(synthmod):
## Store test_value
__test_value = 0
## Test process, simply print the test_value.
# @param self Object pointer
# @param note The note frequency being played
# @param signal Signal data to modify
# @return Modified signal data
def process(self, note, signal):
if self.__test_value > self.MIDI_MIN:
if self.__test_value == self.MIDI_MAX:
print("Text value at max: ", self.__test_value)
else:
print("Test value: ", self.__test_value)
return signal
## Build an array of save data for the module.
# Bindings should have the format class_name.member_name.
# @param self Object pointer
# @return Module data to save
def save_data(self):
return [
[ 'test_module.set_a_value', self.__test_value ]
]
## Set test value.
# @param self Object pointer
# @param val New value to set
def set_a_value(self, val):
self.__test_value = val
Preset files are used to store module parameters and can be loaded during runtime. When the MIDI message to load a preset is received, it selects the corresponding preset file and sets the active parameters.
For setting the preset folder and MIDI message, see Configuration above.
[
[ "envelope.set_attack", 100 ],
[ "envelope.set_decay", 27 ],
[ "envelope.set_sustain", 46 ],
[ "envelope.set_release", 90 ],
[ "band_pass.set_high_pass", 12 ],
[ "band_pass.set_low_pass", 42 ],
[ "reverberation.set_reverb", 38 ]
]