Example of how to build new synths¶
In this example we’ll create a new synthesizer using modules
SynthModule). Synths in torchsynth are
created using the approach modular synthesis that involves connecting
individual modules. We’ll create a simple single oscillator synth
with an attack-decay-sustain-release (
envelope controlling the amplitude. More complicated architectures
can be created using the same ideas.
Creating the SimpleSynth class¶
There are two steps involved in creating a class that derives from
__init__method instantiates the
SynthModules that will be used.
output, ensuring reproducibility if desired.
Defining the modules¶
Here we create our
SimpleSynth class that derives from
AbstractSynth. Override the
method and include an optional parameter for
SynthConfig holds the global configuration
information for the synth and its modules, including the batch size,
sample rate, buffer rate, etc.
To register modules for use within
SimpleSynth, we pass them in
as a list to the class method
list contains tuples with the name that we want to have for the
module in the synth as well as the
Each module passed in this list will be instantiated using the same
SynthConfig object and added as a class
attribute with the name defined by the first item in the tuple.
from typing import Optional import torch from torchsynth.synth import AbstractSynth from torchsynth.config import SynthConfig from torchsynth.module import ( ADSR, ControlRateUpsample, MonophonicKeyboard, SquareSawVCO, VCA, ) class SimpleSynth(AbstractSynth): def __init__(self, synthconfig: Optional[SynthConfig] = None): # Call the constructor in the parent AbstractSynth class super().__init__(synthconfig=synthconfig) # Add all the modules that we'll use for this synth self.add_synth_modules( [ ("keyboard", MonophonicKeyboard), ("adsr", ADSR), ("upsample", ControlRateUpsample), ("vco", SquareSawVCO), ("vca", VCA), ] )
Now that we have registered the modules that we are going to use.
We define how they all are connected together in the overridden
def output(self) -> torch.Tensor: # Keyboard is parameter module, it returns parameter # values for the midi_f0 note value and the duration # that note is held for. midi_f0, note_on_duration = self.keyboard() # The amplitude envelope is generated based on note duration envelope = self.adsr(note_on_duration) # The envelope that we get from ADSR is at the control rate, # which is by default 100x less than the sample rate. This # reduced control rate is used for performance reasons. # We need to upsample the envelope prior to use with the VCO output. envelope = self.upsample(envelope) # Generate SquareSaw output at frequency for the midi note out = self.vco(midi_f0) # Apply the amplitude envelope to the oscillator output out = self.vca(out, envelope) return out
Playing our SimpleSynth¶
That’s out simple synth! Let’s test it out now.
If we instantiate
SimpleSynth without passing in a
SynthConfig object then it will create
one with the default options. We don’t need to render a full batch
size for this example, so let’s use the smallest batch size that
will support reproducible output. All the parameters in a synth are
randomly assigned values, with reproducible mode on, we pass a
batch_id value into our synth when calling it. The same sounds will
always be returned for the same batch_id.
from torchsynth.config import BASE_REPRODUCIBLE_BATCH_SIZE # Create SynthConfig with smallest reproducible batch size. # Reproducible mode is on by default. synthconfig = SynthConfig(batch_size=BASE_REPRODUCIBLE_BATCH_SIZE) synth = SimpleSynth(synthconfig) # If you have access to a GPU. if torch.cuda.is_available(): synth.to("cuda")
Now, let’s make some sounds! We just call synth with a batch_id.
audio = synth(0)
Here are the results of the first 32 sounds concatenated together. Each sound is four seconds long and was generated by randomly sampling the parameters of SimpleSynth.