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 (ADSR
)
envelope controlling the amplitude. More complicated architectures
can be created using the same ideas.
You can also view this example in Colab.
Creating the SimpleSynth class¶
All synths in torchsynth derive from
AbstractSynth
, which provides helpful
functionality for managing children
SynthModule
s and their
ModuleParameter
s.
There are two steps involved in creating a class that derives from
AbstractSynth
:
The
__init__
method instantiates theSynthModule
s that will be used.The
output()
method defines how individualSynthModule
s are connected: Which modules’ output is the input to other modules, and the final output.forward
wrapsoutput
, ensuring reproducibility if desired.
Defining the modules¶
Here we create our SimpleSynth
class that derives from
AbstractSynth
. Override the __init__
method and include an optional parameter for
SynthConfig
.
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
add_synth_modules()
. This
list contains tuples with the name that we want to have for the
module in the synth as well as the SynthModule
.
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),
]
)
Connecting Modules¶
Now that we have registered the modules that we are going to use.
We define how they all are connected together in the overridden
output()
method.
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.