Shortcuts

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

Creating the SimpleSynth class

All synths in torchsynth derive from AbstractSynth, which provides helpful functionality for managing children SynthModules and their ModuleParameters.

There are two steps involved in creating a class that derives from AbstractSynth:

  1. The __init__ method instantiates the SynthModules that will be used.

  2. The output() method defines how individual SynthModules are connected: Which modules’ output is the input to other modules, and the final output.

  3. forward wraps output, 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.

Read the Docs v: stable
Versions
latest
stable
v1.0.1
v1.0.0
Downloads
pdf
html
epub
On Read the Docs
Project Home
Builds

Free document hosting provided by Read the Docs.