Making a Synth - Quefumas/gensound GitHub Wiki

Using Gensound as a synthesizer

Gensound already has a collection of features which enable simulating a synthesizer. Below is a collection of supported features and example constructions, as well as plans for the future or works in progress. If there's something you wish to add, or want to have sooner, feel free to open a thread in the Discussions page.

Some of the features here may be subject to changes in the near future, or may be not up to date.

Generating Melodies using Oscillators

This is covered mostly in Melodic Shorthand Notation, and note that you may use any of Sine, Triangle, Square, Sawtooth with this feature.

Note that by default, an Oscillator signal will try to start from the phase where the previous (concatenated directly before it) Oscillator left off (phase inference). For more on this, refer to the Signals guide.

Envelopes

Use ADSR transform to apply ADSR envelopes To Signal object:

from gensound.transforms import ADSR

s = Sine("D", duration=1e3)*ADSR(attack=0.002e3, decay=0.03e3, sustain=0.8, release=0.2e3)
s.play()

The attack, decay, release arguments are all provided as milliseconds (TODO ensure this can also support num samples as time duration). sustain should be a non-negative float, typically somewhere between 0 and 1, indicating the ratio between the signal peak amplitude and the sustain amplitude. For more details, refer to the Wikipedia page.

Also, it's nicer to write using the following syntax:

env = {"attack":0.002e3, "decay":0.03e3, "sustain":0.2, "release":0.2e3}

s = Sine("D", duration=1e3)*ADSR(**env)

Finally, the hold argument (accepting milliseconds) is also available. This indicates the time pause between the Attack and Decay phases.

Using this can raise an error if the total time required for attack, decay, hold and release exceeds the Signal's duration. This behaviour may change in the future.

Additive Synthesis

TODO

Detuning

There are numerous ways to achieve this. Here are some suggestions.

s = Square("D3", duration=0.5e3) + Square("D3-5", duration=0.5e3) + Square("D3+5", duration=0.5e3)
# mix three copies of D, tuned 5 cents apart

If we want the fundamental note to be an argument:

def detuned(pitch, duration):
  return Square(f"{pitch}+5", duration) + Square(f"{pitch}+5", duration) + Square(f"{pitch}-5", duration)

detuned("D4", duration=0.5e3).play()

We can make this into a variable-size array of signals:

def detuned_array(pitch, duration, width, amount):
  # amount = how many oscillators in the array
  # width = the difference in cents between the highest and lowest oscillators in the array
  all_cents = [i*width/amount - width/2 for i in range(amount)] # how much to detune each signal in the array
  return gensound.mix([ Square(f"{pitch}{round(cents):+}", duration) for cents in all_cents ])

detuned_array("D4", duration=0.5e3, width=0, amount=1).play() # just one
detuned_array("E4", duration=0.5e3, width=10, amount=3).play() # three, across 10 cents
detuned_array("F#4", duration=0.5e3, width=40, amount=30).play() # many

To do so for a given frequency f in Hz (rather than a string), we can use f*2**(cents/1200) to get the detuned frequency and obtain similar results.

AM

Modulating amplitude with a Sine shape.

from gensound.transforms import Amplitude
from gensound.curve import SineCurve

s = WAV(test_wav)[10e3:20e3]
s *= Amplitude(SineCurve(frequency=3, depth=0.3, baseline=0.7, duration=10e3))
s.play()

WIP: FM Synthesis

This works but the interface is likely to change:

from gensound.curve import SineCurve

fm = SineCurve(frequency=10, depth=1.01, baseline=330, duration=10e3)
Sine(fm, 10e3).play()

WIP: Applying Transforms to all individual Signals in a sequence

Applying ADSR to a sequence of notes, for example: Sine('C3 Eb G Ab B,', 0.5e3)*ADSR(...), will affect the envelope of the entire resulting melody - the attack and decay will affect only the first note, and the release will only affect the last note. The only solution as it stands is to split the sequence and apply the transform to each note individually, which is very inconvenient. Since this is a very likely use case, I'm experimenting with several approaches:

  • Adding a new operator which is an alternative apply action, except that it applies the Transform to each individual Signal in the sequence. For example, Sine('C3 Eb G Ab B,', 0.5e3)@ADSR(...) will apply the envelope to each note in the sequence. This has an easy implementation, but I'm not sure how I feel about overloading this operator, since it will not make intuitive sense to most users. Perhaps that shouldn't be an issue since it's likely there's no immediately intuitive way to implement it in the first place, so either way the users will have to refer to the guide. We can also consider overloading some other operator for this operation, such as &,^ or even **.
  • Implementing a new type similar to Sequence, for which this apply is the default. This has some issues, since it's not always visible which kind of Sequence is being handled at any moment.

Future

  • ADSR should perhaps implicitly extend underlying Signal object, overlapping into the next (this requires new internal logic)
  • Use any Signal to modulate
  • Filters