Module tinkersynth.synthesizer
This module contains the generators/nodes for creating and manipulating wave forms and building synth patches.
See the documentation of the individual functions below for more information, and examples.py for examples.
The nodes are created and chained so that the audio signal flows through them. Example::
# Connect to a MIDI device or the PC keyboard
# <code>freqs</code> is a generator/node that gives us the frequencies of the individual played notes
# <code>gates</code> is a generator/node providing the information whether the key is currently pressed down (and how hard)
freqs, gates = midi_note_source()
# Create an oscillator with a sawtooth wave form, using the played frequencies
saw = sawtooth(freqs)
# In the code until now, nothing has really happened. We just defined stuff.
# Now we play our pipeline:
play_live(saw)
As you can hear, this example generates a continuous sound, where the pitch changes when you press
different notes. The notes actually starting and stopping is handled by an envelope_generator().
In its default configuration, the envelope generator turns on and off the notes as we expect::
# Replacing the last play command with these lines:
env = envelope_generator(saw)
# you can also use the shorthand envelope_generator()
play_live(env)
# <– note that we changed the input from saw to env
But an envelope generator can do much more. It models Attack, Decay, Sustain and Release (ADSR) of a played note. For details please search the Web on adsr envelopes. By varying these values, the sound can get very different properties – anything between dreamy and percussive is possible.
Here's a slightly more sophisticated example::
# Connect to a MIDI device or the PC keyboard
# <code>freqs</code> is a generator/node that gives us the frequencies of the individual played notes
# <code>gates</code> is a generator/node providing the information whether the key is currently pressed down (and how hard)
freqs, gates = midi_note_source()
# Create an oscillator with a sawtooth wave form, using the played frequencies
saw = sawtooth(freqs)
# Make another node which has the same frequency information, but one octave lower, and use it in
# a square wave oscillator
lower_freq = transpose(freqs, -12) # could also have scaled by 0.5 or 2.0 for octaves: scale(freqs, 0.5)
squ = square(lower_freq)
# Now mix the two sounds together
m = mix([saw, squ])
# Using the <code>gates</code> info from the beginning, we can send our signal through an envelope generator
env = eg(m, gate=gates, adsr=[0.005, 0.05, 0.35, 0.3]) # Adds a slightly percussive quality
# Add some drive and reverb
drv = drive(env, gain=0.8, mixin=0.5)
rev = reverb_hall(drv)
# Before playing, scale down a bit to be safe from clipping
scaled = gain(rev, gain=0.8)
# In the code until now, nothing has really happened. We just defined stuff.
# Now we play our pipeline:
play_live(scaled)
Expand source code
"""
This module contains the generators/nodes for creating and manipulating wave forms and building synth patches.
See the documentation of the individual functions below for more information, and `examples.py` for examples.
The nodes are created and chained so that the audio signal flows through them. Example::
# Connect to a MIDI device or the PC keyboard
# `freqs` is a generator/node that gives us the frequencies of the individual played notes
# `gates` is a generator/node providing the information whether the key is currently pressed down (and how hard)
freqs, gates = midi_note_source()
# Create an oscillator with a sawtooth wave form, using the played frequencies
saw = sawtooth(freqs)
# In the code until now, nothing has really happened. We just defined stuff.
# Now we play our pipeline:
play_live(saw)
As you can hear, this example generates a continuous sound, where the pitch changes when you press
different notes. The notes actually starting and stopping is handled by an `envelope_generator`.
In its default configuration, the envelope generator turns on and off the notes as we expect::
# Replacing the last play command with these lines:
env = envelope_generator(saw) # you can also use the shorthand `eg`
play_live(env) # <-- note that we changed the input from `saw` to `env`
But an envelope generator can do much more. It models Attack, Decay, Sustain and Release (ADSR)
of a played note. For details please search the Web on adsr envelopes. By varying these values,
the sound can get very different properties -- anything between dreamy and percussive is possible.
Here's a slightly more sophisticated example::
# Connect to a MIDI device or the PC keyboard
# `freqs` is a generator/node that gives us the frequencies of the individual played notes
# `gates` is a generator/node providing the information whether the key is currently pressed down (and how hard)
freqs, gates = midi_note_source()
# Create an oscillator with a sawtooth wave form, using the played frequencies
saw = sawtooth(freqs)
# Make another node which has the same frequency information, but one octave lower, and use it in
# a square wave oscillator
lower_freq = transpose(freqs, -12) # could also have scaled by 0.5 or 2.0 for octaves: scale(freqs, 0.5)
squ = square(lower_freq)
# Now mix the two sounds together
m = mix([saw, squ])
# Using the `gates` info from the beginning, we can send our signal through an envelope generator
env = eg(m, gate=gates, adsr=[0.005, 0.05, 0.35, 0.3]) # Adds a slightly percussive quality
# Add some drive and reverb
drv = drive(env, gain=0.8, mixin=0.5)
rev = reverb_hall(drv)
# Before playing, scale down a bit to be safe from clipping
scaled = gain(rev, gain=0.8)
# In the code until now, nothing has really happened. We just defined stuff.
# Now we play our pipeline:
play_live(scaled)
"""
import os
import sys
import threading
from array import array
from itertools import tee
from math import sin, pi, copysign, sqrt
from queue import SimpleQueue as Queue
from random import random
from time import sleep
from typing import Union, Optional, Iterable, Collection
import numpy as np
import miniaudio as ma
ATTACK = 0
DECAY = 1
SUSTAIN = 2
RELEASE = 3
TWELVE_EQUAL = 0
WELL_TEMPERED = 1 # support is todo
HIGH = 1
LOW = 0
temperament = TWELVE_EQUAL
channels = 1
sample_rate = 44100
sample_width = 2 # 16 bit pcm
buffersize_msec = 10
sample_max = (2 ** (8 * sample_width) / 2) - 1
stop = False
# Run management (optional):
def run_and_reload_on_change(func: callable):
"""Execute a function and monitor the executed Python script for file changes.
This is completely optional and just to make exploration easier and faster.
If the script file changes, the complete script will be re-run with the original parameters.
:param func: function to execute
:type func: callable
"""
thread = threading.Thread(target=func)
thread.start()
mod_time = os.path.getmtime(sys.argv[0])
while True:
# Check if the script file has been modified
if mod_time != os.path.getmtime(sys.argv[0]):
# If the file has been modified, stop the thread and restart the script
while thread.is_alive():
print("Signalling audio stream to stop...")
stop_stream()
sleep(0.3)
print("Restarting...")
os.execv(sys.executable, [sys.executable] + sys.argv)
else:
# If the file has not been modified, sleep for a while
sleep(0.5)
# Audio output
def play_static(samples: Iterable):
"""Play a finite iterable of audio samples.
TODO: Adjust to be compatible with generators.
:param samples: list/tuple/... of samples
:type samples: Iterable
"""
samples_np = (np.array(samples) * sample_max).astype(np.int16)
# print(samples_np.shape)
# print(samples_np[:10])
# print(samples_np.dtype)
arr = array('h', samples_np)
stream = ma.stream_raw_pcm_memory(arr, channels, sample_width)
stream_with_callbacks = ma.stream_with_callbacks(
stream,
progress_callback=None,
frame_process_method=None,
end_callback=stop_stream,
)
with ma.PlaybackDevice(
output_format=ma.SampleFormat.SIGNED16,
nchannels=channels,
sample_rate=sample_rate,
buffersize_msec=buffersize_msec,
) as device:
next(stream_with_callbacks) # start the generator
device.start(stream_with_callbacks)
while not stop:
sleep(0.1)
def play_live(generator: Iterable):
"""Consume the specified `generator` node as audio samples to play using system audio.
See the constants in `synthesizer.py` for sampling rate and similar settings.
The generator can be the last in a potentially long and complex chain (see `examples.py`).
This might be a `gain()` node using cc["master_volume"] to set the output volume.
:param generator: Sample generator node to play
:type generator: generator
"""
sample_format = {1: ma.SampleFormat.UNSIGNED8, 2: ma.SampleFormat.SIGNED16, 4: ma.SampleFormat.SIGNED32}[
sample_width]
stream = _my_audio_stream(generator)
with ma.PlaybackDevice(
output_format=sample_format,
nchannels=channels,
sample_rate=sample_rate,
buffersize_msec=buffersize_msec,
) as device:
next(stream) # start the generator
device.start(stream)
while not stop:
sleep(0.5)
def _my_audio_stream(data: Iterable):
type_code = {1: "b", 2: "h", 4: "l"}[sample_width]
did_warn = False
def prep(x):
nonlocal did_warn
x_clipped = max(min(x, 1.0), -1.0)
if x != x_clipped and not did_warn:
print(f"Warning: Audio stream is clipping (value {x} encountered). Please scale or otherwise constrain it.")
did_warn = True
return int(x_clipped * sample_max)
requested_frames = yield b"" # how many frames do we need to provide
while not stop:
frames = array(type_code, [prep(next(data)) for _ in range(requested_frames)])
requested_frames = yield frames
def stop_stream():
"""Signal the audio player to stop playing"""
global stop
stop = True
print("Stopping")
# Helper functions:
def gen_iter(*args):
"""Helper function to make sure the `*args` are iterable, wrapping non-iterables in a generator which
returns the same value over and over.
"""
arg_gens = [ensure_iterable(arg) for arg in args]
for curr_vals in zip(*arg_gens):
if len(curr_vals) == 1:
yield curr_vals[0]
else:
yield curr_vals
def static_generator(signal):
"""Wrap a value in a generator which returns the same value over and over."""
while True:
yield signal
def ensure_iterable(signal: Union[Iterable, float], force=False):
"""If `signal` is not iterable, wrap it in a generator returning the same value over and over."""
if isinstance(signal, Iterable) and not force:
return signal
else:
return static_generator(signal)
# DSP nodes:
def envelope_generator(
signal: Union[Iterable, float] = 1.0,
gate: Union[Iterable, float] = HIGH,
adsr: Union[Iterable[Iterable[float]], Iterable[float]] = (0.0, 0.0, 1.0, 0.0),
auto_trigger: Union[Iterable, int] = LOW,
):
"""Create an Envelope Generator (Attack/Decay/Sustain/Release)
Creates an amplitude scaling envelope between 0.0 and 1.0. If `signal` is provided, the result will
be this signal scaled according to the envelope progression. If `signal` is not provided, the output
will be the [0.0, 1.0] scaling envelope itself, which can be used as parameter for other nodes (e.g.
`low_pass_filter` cutoff).
Scaling and movement from one envelope step to the next is linear.
:param signal: Optional signal to apply envelope to (default 1.0 constant)
:type signal: Iterable or float
:param gate: The gate signal (> 0.0 if key is held down, 0.0 (=LOW) otherwise)
:type gate: float
:param adsr: Tuple/list of the four values for Attack, Decay, Sustain and Release, or a generator providing a stream of tuples/lists.
:type adsr: Union[Iterable[Iterable[float]], Iterable[float]]
:param auto_trigger: Repeat the Attack-Decay sequence as long as `gate` is > 0.0
:type auto_trigger: int
:return: Yields envelope-scaled signal, or envelope signal itself if signal is not provided
:rtype: float
"""
adsr_phase = ATTACK
prev_adsr = (-1, -1, -1, -1)
prev_gate = LOW
curr_velocity = 0
curr_env = 0.0
attack_step = None
decay_step = None
release_step = None
max_scaled = None
attack_step_scaled = None
decay_step_scaled = None
sustain_val_scaled = None
release_step_scaled = None
if not isinstance(adsr, list) and not isinstance(adsr, tuple):
raise ValueError(
f"Parameter `adsr` must be a flat list/tuple of 4 envelope values (attack, decay, sustain, release) or of 4 Iterables/Generators which provides a, d, s and r respectively. It was {adsr}, type {type(adsr)}")
# if not isinstance(adsr[0], Iterable):
# # Make flat adsr list a generator (the usual Iterable check does not suffice)
# adsr = static_generator(adsr)
for sig_val, gate_val, a, d, s, r, retrig in gen_iter(signal, gate, *adsr, auto_trigger):
adsr_val = (a, d, s, r)
if adsr_val != prev_adsr:
# For immediate envelope changes, we still use a change time of 10ms
# (step size < 1.0) to avoid popping noise:
minimum_step = 1.0 / (0.01 * sample_rate)
attack_step = 1.0 / (adsr_val[ATTACK] * sample_rate) if adsr_val[ATTACK] > 0.0 else minimum_step
decay_step = (1.0 - adsr_val[SUSTAIN]) / (adsr_val[DECAY] * sample_rate) if adsr_val[
DECAY] > 0.0 else minimum_step
if adsr_val[DECAY] > 0.0 and adsr_val[SUSTAIN] == 0.0:
# AD mode:
# If A and D are set, but S is 0.0, R should be ignored
# (release as fast as possible = ignore release time)
release_step = minimum_step
elif adsr_val[DECAY] == 0.0 and adsr_val[SUSTAIN] == 0.0:
# AR mode:
# If A and R are set, but D and S are zero, we should immediately move from
# the ATTACK phase to the RELEASE phase. The end value of ATTACK (= 1.0)
# will be the RELEASE starting value:
release_step = 1.0 / (adsr_val[RELEASE] * sample_rate) if adsr_val[RELEASE] > 0.0 else minimum_step
else:
# Normal ADSR mode
release_step = adsr_val[SUSTAIN] / (adsr_val[RELEASE] * sample_rate) if adsr_val[
RELEASE] > 0.0 else minimum_step
# Gate carries velocity information (don't test for HIGH, but for gate_val != LOW)
if prev_gate == LOW and gate_val != LOW:
# gate_val > 0 carries the velocity signal
attack_step_scaled = attack_step * gate_val
decay_step_scaled = decay_step * gate_val
release_step_scaled = release_step * gate_val
sustain_val_scaled = adsr_val[SUSTAIN] * gate_val
max_scaled = gate_val
if prev_gate != LOW and gate_val == LOW:
# print("starting release")
adsr_phase = RELEASE
if adsr_phase == ATTACK and gate_val != LOW:
if curr_env < max_scaled:
curr_env = min(curr_env + attack_step_scaled, max_scaled)
elif adsr_val[DECAY] == 0.0 and adsr_val[SUSTAIN] == 0.0:
# AR mode
adsr_phase = RELEASE
else:
# print("starting decay")
adsr_phase = DECAY
elif adsr_phase == DECAY and gate_val != LOW:
if curr_env > sustain_val_scaled:
curr_env = max(curr_env - decay_step_scaled, sustain_val_scaled)
elif retrig:
# print("retriggering attack")
adsr_phase = ATTACK
else:
# print("starting sustain")
adsr_phase = SUSTAIN
if adsr_phase == RELEASE:
if curr_env > 0.0 and gate_val == LOW:
curr_env = max(curr_env - release_step_scaled, 0.0)
else:
# print("end of release")
# curr_env = 0.0 # leads to popping sound
adsr_phase = ATTACK
# print(prev_gate, gate_val, adsr_phase, curr_env)
prev_gate = gate_val
prev_adsr = adsr_val
yield sig_val * curr_env
# Shorthand
eg = envelope_generator
def sine(f: Union[Iterable, float] = 440):
"""Create an oscillator node generating a sine wave for the given frequency `f`::
--
/ \\
\\ /
--
Sine waves have no harmonic overtones. They are the vanilla flavour of oscillators.
:param f: Frequency in Hz
:type f: Iterable (generator) or constant float
:return: Yields samples between -1.0 and 1.0
:rtype: float.
"""
# print(f"sine-init: {threading.active_count(), threading.current_thread()}")
wave = 2 * pi
step = 0
f_prev = -1
prev_duration = None
duration_in_samples = 0
for f_val in gen_iter(f):
# print(f"sine_loop: {threading.active_count(), threading.current_thread()}")
if f_val != f_prev and f_val > 0:
duration_in_samples = round(sample_rate / f_val)
if prev_duration is not None:
step = round(step / prev_duration * duration_in_samples)
yield sin(f_val * wave * step / sample_rate)
step += 1
if step >= duration_in_samples:
step = 0
f_prev = f_val
prev_duration = duration_in_samples
def square(f: Union[Iterable, float]):
"""Create an oscillator node generating a square wave for the given frequency `f`::
------ -------
| | | |
| |
------
Square waves possess a theoretically infinite cascade of harmonics. They literally have some edge to them.
:param f: Frequency in Hz
:type f: Iterable (generator) or constant float
:return: Yields samples between -1.0 and 1.0
:rtype: float.
"""
step = 0
amplitude = 1.0
for f_val in gen_iter(f):
half_duration = round(sample_rate / f_val / 2)
if step > half_duration:
amplitude *= -1.0
step = 0
else:
step += 1
yield amplitude
def sawtooth(f: Union[Iterable, float]):
"""Create an oscillator node generating a sawtooth wave for the given frequency `f`::
. .
| \\ | \\
| \\ | \\
\\ | \\ |
\\| \\|
Sawtooth waves have a very raspy sound. Many harmonics to go around with and play with filters.
If you like 8-bit game music, this oscillator will be important.
:param f: Frequency in Hz
:type f: Iterable (generator) or constant float
:return: Yields samples between -1.0 and 1.0
:rtype: float.
"""
amplitude = 1.0
amp_step = 0.0
for f_val in gen_iter(f):
duration = round(sample_rate / f_val)
amp_step = copysign(2.0 / duration, amp_step)
if amplitude == -1.0:
amplitude = 1.0
else:
amplitude = max(amplitude - amp_step, -1.0)
yield amplitude
def triangle(f: Union[Iterable, float]):
"""Create an oscillator node generating a triangle wave for the given frequency `f`::
/\\ /\\
/ \\ / \\
\\ / \\ /
\\/ \\/
Triangle waves are pretty close sine waves, but possess a natural quality which is useful when
attempting to mimic woodwind instruments or xylophones.
:param f: Frequency in Hz
:type f: Iterable (generator) or constant float
:return: Yields samples between -1.0 and 1.0
:rtype: float.
"""
amplitude = 0.0
amp_step = 0.0
for f_val in gen_iter(f):
duration = round(sample_rate / f_val)
amp_step = copysign(2.0 / duration, amp_step)
if amplitude >= 1.0 or amplitude == -1.0:
amp_step = -amp_step
if amp_step > 0:
amplitude = min(amplitude + amp_step, 1.0)
else:
amplitude = max(amplitude + amp_step, -1.0)
yield amplitude
def noise(f: Optional[Union[Iterable, float]]):
"""Create an oscillator node generating white noise, ignoring the frequency parameter `f`::
. .
. .
. . .
. . .
. . .
Noise is often mixed to other wave forms to add some texture. When adding
(possibly modulated) filters, the character can change dramatically.
Frequently combined with a suitable envelope to model percussive instruments.
:param f: (just present for compatibility, ignored)
:type f: (ignored)
:return: Yields samples between -1.0 and 1.0
:rtype: float.
"""
for f_val in gen_iter(f):
yield 2.0 * random() - 1.0
def low_pass_filter( # naïve implementation
signal: Union[Iterable, float],
alpha: Union[Iterable, float] = 1.0,
resonance: Union[Iterable, float] = 0.0, # TODO: doesn't work yet
poles: int = 4):
"""Creates a generator to attenuate (reduce) higher frequencies in the input signal.
This removes higher harmonics, giving sounds a (literally) smoother quality.
Can also be used for smoothing non-sound data (e.g. raw sensor values).
`alpha` indirectly selects the cutoff frequency (alpha = Xc/sqrt(R^2 + Xc^2), where
Xc = 1 / (2 pi f C)).
Higher pole numbers (4 and 8 are good numbers) increase the sharpness of the cutoff.
Currently, the undesired attenuation of lower frequencies is not compensated for (this
is a passive IIR filter).
Neither is there any key tracking (i.e. shift of the cutoff according to the frequency played).
:param signal: Signal to be filtered
:type signal: Iterable or float
:param alpha: Select cutoff frequency. 1.0 = all-pass (default), 0.0 = no-pass
:type alpha: Iterable or float
:param resonance: Not implemented
:type resonance: Iterable or float
:param poles: Number of times the filter is applied to the signal (poles/stages/degrees).
Every pole *should* lead to a -45 degree phase shift around the cutoff frequency and -90 above,
but numeric results don't (yet) agree with that.
:type poles: int
:return: Yields the low-pass signal
:rtype: float
"""
last = [0.0] * poles
filtered = [0.0] * poles
for sig, alph, res in gen_iter(signal, alpha, resonance):
curr_sig = sig
for p in range(poles):
# filtered[p] = (1.0 - alph) * filtered[p] + alph * (curr_sig + last[p])/2.0
filtered[p] = (1.0 - alph) * filtered[p] + alph * curr_sig
last[p] = curr_sig
curr_sig = filtered[p]
curr_sig -= res * curr_sig # inverted feedback (should work due to 4-pole filter's phase shift of -180, but doesn't)
yield curr_sig
# Example LPF with resonance: https://www.kvraudio.com/forum/viewtopic.php?p=2090514#p2090514
# // TODO: Moog-style lowpass filter:
# //Init
# cutoff = cutoff freq in Hz
# fs = sampling frequency //(e.g. 44100Hz)
# res = resonance [0 - 1] //(minimum - maximum)
#
# f = 2 * cutoff / fs; //[0 - 1]
# k = 3.6*f - 1.6*f*f -1; //(Empirical tunning)
# p = (k+1)*0.5;
# scale = e^((1-p)*1.386249;
# r = res*scale;
# y4 = output;
#
# y1=y2=y3=y4=oldx=oldy1=oldy2=oldy3=0;
#
# //Loop
# //--Inverted feed back for corner peaking
# x = input - r*y4;
#
# //Four cascaded onepole filters (bilinear transform)
# y1=x*p + oldx*p - k*y1;
# y2=y1*p+oldy1*p - k*y2;
# y3=y2*p+oldy2*p - k*y3;
# y4=y3*p+oldy3*p - k*y4;
#
# //Clipper band limited sigmoid
# y4 = y4 - (y4^3)/6;
#
# oldx = x;
# oldy1 = y1;
# oldy2 = y2;
# oldy3 = y3;
#
def high_pass_filter(signal: Union[Iterable, float], alpha: Union[Iterable, float] = 1.0, poles: int = 2):
"""Creates a generator to attenuate (reduce) the lower frequencies in the input signal.
This makes higher harmonics stand out more in relation.
Can also be used for isolating momentary changes from other data (e.g. raw sensor values).
`alpha` indirectly selects the cutoff frequency.
Higher pole numbers (4 and 8 are good numbers) increase the sharpness of the cutoff (Not sure
this works properly, though. It sounds weird).
Currently, the undesired attenuation of higher frequencies is not compensated for (this
is a passive IIR filter).
Neither is there any key tracking (i.e. shift of the cutoff according to the frequency played).
:param signal: Signal to be filtered
:type signal: Iterable or float
:param alpha: Select cutoff frequency. 1.0 = all-pass (default), 0.0 = no-pass
:type alpha: Iterable or float
:param poles: Number of times the filter is applied to the signal (poles/stages/degrees).
:type poles: int
:return: Yields the high-pass signal
:rtype: float
"""
last = [0.0] * poles
filtered = [0.0] * poles
for sig, alph in gen_iter(signal, alpha):
curr_sig = sig
for p in range(poles):
filtered[p] = alph * (filtered[p] + curr_sig - last[p])
# filtered[p] = (1.0 - alph) * filtered[p] + alph * curr_sig
last[p] = curr_sig
curr_sig = filtered[p]
yield curr_sig
def mix(signals: Union[Iterable[Collection[float]], Collection[float]],
weights: Optional[Union[Iterable[Collection[float]], Collection[float]]] = None):
"""Creates a generator that combines several signals into one by averaging (optionally weighted averaging).
Typically used to combine different wave forms into one. But other creative uses are
possible, such as fusing different controller inputs (e.g. using one as coarse and the
other as fine input for the same parameter).
Before using `weights`, please consider the alternative of applying `gain` to the
various input signals before mixing. This often makes the patch code easier to manage.
Because a simple averaging would reduce the overall volume, the gain is scaled to retain
the average (RMS) power of the signal (if no weights are provided).
This is only correct on average, though, and might lead to occasional audio clipping.
To make sure that the output is never clipped, consider
outputting less than 100% gain, e.g. scale to 0.8 as the last step of your patch.
:param signals: Tuple/list of signals to mix
:type signals: Collection of Iterables or floats
:param weights: Optional relative weights, overriding RMS scaling. Should sum up to 1.0
:type weights: Optional Collection of Iterables or floats
:return: Yields the mixed signal
:rtype: float
"""
n = len(signals)
if weights is None:
weights = [1 / sqrt(n)] * n # e.g. 1/sqrt(2)=0.7 RMS -- 3dB per additional signal
for vals_and_weights in gen_iter(*signals, *weights):
vals = vals_and_weights[:n]
weights = vals_and_weights[n:]
yield sum([v * w for v, w in zip(vals, weights)])
def gain(signal: Union[Iterable, float], gain: Union[Iterable, float] = 1.0):
"""Creates a generator that reduces or boosts a `signal` by the factor `gain`.
Scaling is linear (multiplication by `gain`) and unbounded.
Typically used with `gain` values between 0.0 and 1.0, but you can get creative, of course.
Just make sure that the audio output signal does not clip (is between -1.0 and 1.0).
:param signal: Signal to scale.
:type signal: Iterable or float
:param gain: Factor to scale `signal` by, e.g. 1.0 = no change (default); 0.5 = half amplitude;
0.0 = mute completely; -1.0 = invert
:type gain: Iterable or float
:return: Yields the scaled signal
:rtype: float
"""
for sig, g in gen_iter(signal, gain):
yield g * sig
def scale(
signal: Union[Iterable, float],
target_min: Union[Iterable, float] = -1.0, target_max: Union[Iterable, float] = 1.0,
source_min: Union[Iterable, float] = -1.0, source_max: Union[Iterable, float] = 1.0
):
"""Creates a generator that linearly resizes a `signal` between two values.
Please note that this LERP transformation is linear and unbounded. To limit it to not exceed `target_min` and
`target_max`, please combine it with the `constrain` generator function.
The parameters `target_min` and `target_max` come first so that scaling normal [-1.0, 1.0] signals
does not require much typing::
osc = square(440)
scaled = scale(osc, 0.0, 1.0) # create a DC square wave signal
:param signal: Signal to be scaled
:type signal: Iterable or float
:param target_min: New lower bound (default -1.0)
:type target_min: Iterable or float
:param target_max: New upper bound (default 1.0)
:type target_max: Iterable or float
:param source_min: Original lower bound (default -1.0)
:type source_min: Iterable or float
:param source_max: Original upper bound (default 1.0)
:type source_max: Iterable or float
:return: Yield the scaled signal
:rtype: float
"""
for sig, t_min, t_max, s_min, s_max in gen_iter(signal, target_min, target_max, source_min, source_max):
scale = (t_max - t_min) / (s_max - s_min)
yield t_min + (sig - s_min) * scale
def transpose_factor_12eq(s: float):
"""Get the transposition factor to multiply with any given frequency (equal temperament).
:param s: Semitones to transpose up (positive s) or down (negative s). Need not be integer.
:type s: float
:return: Transposition factor
:rtype: float
"""
return 2 ** (s / 12)
def transpose(freq: Union[Iterable, float], semitones: Union[Iterable, float] = 0.0):
"""Create a generator that transposes a frequency `freq` by a given number of `semitones`.
:param freq: Frequency signal (one frequency value, like 440.0, or an Iterable/node providing frequency values)
:type freq: Iterable or float
:param semitones: Semitones (>0.0 for transposing up, <0.0 for transposing down), non-integers allowed.
:type semitones: Iterable or float
:return: Yields the transposed frequency value
:rtype: float
"""
if temperament != TWELVE_EQUAL:
raise NotImplementedError("Only 12 Equal Temperament is supported")
prev_s = None
prev_f = None
factor = None
transposed_f = None
for f, s in gen_iter(freq, semitones):
if s != prev_s:
factor = transpose_factor_12eq(s)
transposed_f = f * factor
if f != prev_f:
transposed_f = f * factor
yield transposed_f
prev_f = f
prev_s = s
def split(signal: Union[Iterable, float], n: int):
"""Creates `n` generators from one single `signal`.
Necessary if the same signal has to be used more than once, because e.g. using an oscillator
in two places would consume it at double speed, increasing its pitch by an octave.
Particularly useful for using control signals in several places
(e.g. multiple oscillators with `freq`, or using gate signals in `eg` and in `portamento`).
But also commonly used in effects like `reverb`.
Be advised that if the n generators are not consumed evenly,
the size of the required buffer can grow extremely large.
Example 1::
freq1, freq2, freq3 = split(orig_freq, 3)
Example 2::
detuned_saws = [sawtooth(transpose(f, random()-0.5)) for f in split(orig_freq, 10)]
output = mix(detuned_saws)
:param signal: Signal to split up into n signals
:type signal: Iterable or float
:param n: Number of signals to create
:type n: int
:return: Returns a tuple with `n` separately usable copies of the original signal
:rtype: tuple of Iterables or floats
"""
return tuple(tee(ensure_iterable(signal), n))
def constrain(signal: Union[Iterable, float], minimum: float = -1.0, maximum: float = 1.0):
"""Creates a generator to limit a signal to hard minimum and maximum boundaries.
This clips the signal at those boundaries. For less audible clipping, see `drive`.
:param signal: Signal to constrain
:type signal: Iterable or float
:param minimum: Lower bound
:type minimum: float
:param maximum: Upper bound
:type maximum: float
:return: Yields clipped signal
:rtype: float
"""
for s in signal:
yield max(min(s, maximum), minimum)
def if_else(
bool_exp: Union[Iterable, float], # >=0.5 = true, else false
if_true: Iterable, # Generator (/chain) to use as signal if true
if_false: Iterable, # Generator (/chain) to use as signal if false
):
"""Creates a generator which selects one or the other upstream signals to pass through.
Can for example be used to turn on/off effects (and save CPU cycles)::
plain = square(440)
with_reverb = reverb_hall(plain)
out = if_else(cc["some_button"], with_reverb, plain)
Keep in mind that you can create your own Python generators for the `bool_exp` part, e.g.::
def on_off():
'''Needs to be initialized with paranthesis: if_else(on_off(), ...)'''
global some_variable
while True:
if some_variable is True:
yield 1.0
else:
yield 0.0
# or with generator expressions (assuming global scope for this example)
on_off = (float(v) for v in repeat(some_variable))
Note that the non-used signal node (`if_true` or `if_false`) is not running while not selected. This
might sometimes have unintended consequences. For example, if one of these or both have a `split`
somewhere upstream, you might encounter large memory consumption/buffer overflow in the inactive
branch.
To be completely safe (at the cost of wasting CPU cycles), you can use a combination of `gain`
and `mix` to achieve the same. The basic arithmetic generator functions `add`, `sub`, `mult` and `div`,
as well as the all-purpose `apply`, can be useful building blocks as well::
plain = square(440)
with_reverb = reverb_hall(plain)
out = mix(
gain(with_reverb, cc["some_button"])
gain(plain, sub(1.0, cc["some_button"]))
)
:param bool_exp: Expression to use for the choice. >=0.5 means True and <0.5 means False.
:type bool_exp: float
:param if_true: Stream to pass through if `bool_exp` is greater than or equal to 0.5
:type if_true: Iterable
:param if_false: Stream to pass through if `bool_exp` is less than 0.5
:type if_false: Iterable
:return: Yields selected stream's data
:rtype: Probably float, but hard to say, really
"""
for b in gen_iter(bool_exp):
if b >= 0.5:
yield next(if_true)
else:
yield next(if_false)
def apply(signal: Union[Iterable, float], func: callable):
"""Create a generator that applies a function to the signal value and yields its output
This helper exists to give you greater flexibility when creating your patches,
so there's no need to define custom generators for every little unsupported transformation.
:param signal: Signal whose values to modify
:type signal: Iterable or float
:param func: Function to apply to the signal
:type func: callable
:return: Yield value-by-value output of `func` applied to `signal`
:rtype: float
"""
for s in signal:
yield func(s)
def mult(signal: Union[Iterable, float], other_val: Union[Iterable, float]):
"""Create a generator that multiplies one signal with another.
This helper exists to give you greater flexibility when creating your patches,
so there's no need to define custom generators for every little unsupported operation.
:param signal: Signal whose values to modify
:type signal: Iterable or float
:param other_val: Other value of the arithmetic operation
:type other_val: Iterable or float
:return: Yield value-by-value output of `signal` multiplied by `other_val`
:rtype: float
"""
for s, v in gen_iter(signal, other_val):
yield s * v
def div(signal: Union[Iterable, float], other_val: Union[Iterable, float]):
"""Create a generator that divides one signal by another.
This helper exists to give you greater flexibility when creating your patches,
so there's no need to define custom generators for every little unsupported operation.
:param signal: Signal whose values to modify
:type signal: Iterable or float
:param other_val: Other value of the arithmetic operation
:type other_val: Iterable or float
:return: Yield value-by-value output of `signal` divided by `other_val`
:rtype: float
"""
for s, v in gen_iter(signal, other_val):
yield s / v
def add(signal: Union[Iterable, float], other_val: Union[Iterable, float]):
"""Create a generator that adds one signal to another.
This helper exists to give you greater flexibility when creating your patches,
so there's no need to define custom generators for every little unsupported operation.
:param signal: Signal whose values to modify
:type signal: Iterable or float
:param other_val: Other value of the arithmetic operation
:type other_val: Iterable or float
:return: Yield value-by-value output of `signal` added to `other_val`
:rtype: float
"""
for s, v in gen_iter(signal, other_val):
yield s + v
def sub(signal: Union[Iterable, float], other_val: Union[Iterable, float]):
"""Create a generator that subtracts one signal from another.
This helper exists to give you greater flexibility when creating your patches,
so there's no need to define custom generators for every little unsupported operation.
:param signal: Signal whose values to modify
:type signal: Iterable or float
:param other_val: Other value of the arithmetic operation
:type other_val: Iterable or float
:return: Yield value-by-value output of `signal` minus `other_val`
:rtype: float
"""
for s, v in gen_iter(signal, other_val):
yield s - v
def drive(
signal: Union[Iterable, float],
gain: Union[Iterable, float] = 2.0,
mixin: Union[Iterable, float] = 0.0):
"""Creates a generator boosting a signal to make it sound 'fatter'. Depending on the parameters,
This can range from a clean boost, via overdrive (if you mix in undriven signal),
to distortion (without any simulated physical tube amp effects, though).
:param signal: Source signal iterable/generator
:param gain: amplitude scaling factor (default 2.0). Higher gains lead to more drastic cutoffs. A gain value of 0.0 turns this node into a linear pass-through.
:param mixin: mix this much (0.0 to 1.0) of the unmodified signal back in (default 0.0)
"""
for s, g, m in gen_iter(signal, gain, mixin):
# Apply gain, then apply smoothing function x/(1+|x|), then scale result back to [-1, 1]
# s_ = s * g
# smoothed = s_ / (1 + abs(s_))
# yield smoothed * (g + 1)/g
# save one multiplication and division, do it in one step:
driven = s / (1 + abs(s * g)) * (g + 1)
# print(g, s, driven)
# yield m * s + (1.0 - m) * driven # results in attenuation (e.g. at 0.5, the two parts are not suddenly half as loud only because they are going to be mixed)
yield driven + m * s * 0.75 # attenuate to avoid clipping
def drive_deprecated(signal: Union[Iterable, float], gain: float = 1.5, soften: float = 0.5, exp: int = 3):
"""Signal boosting generator to make it sound 'fatter'. Depending on the parameters,
This can range from a clean boost, via overdrive (if you mix in undriven signal afterwards),
to distortion (without any simulated physical tube amp effects, though).
:param signal: source signal iterable/generator
:param gain: linear amplitude scaling factor (default 1.5)
:param soften: part of the saturation amplitude range which is rounded off towards the peak (default 0.5 as 1.5-0.5 = 1.0 which results in an unclipped signal)
:param exp: steepness of the rounding-off (default 3). 1 would result in no effect, 5 results in sharper rounding, but overshoots and requires reducing both the `boost` and `softing` parameters, e.g. to 1.25 and 0.25 respectively.
https://www.wolframalpha.com/input?i=plot+1.25x+-+0.25*x%5E5+between+-1+and+1
"""
if exp % 2 == 0:
raise ValueError("exp must be an uneven integer number")
max_val = abs(gain - soften)
if max_val != 0.0:
print("Warning: assuming a [+1,-1] signal, your drive settings might "
f"lead to {'clipping' if max_val > 1.0 else 'attenuation'} "
f"(gain {gain} - soften {soften} = max {max_val})")
for s in signal:
yield gain * s - soften * s ** exp
def delay(signal: Union[Iterable, float],
delay: Union[Iterable, float] = 1.0, # in seconds
mixin: Union[Iterable, float] = 0.6, # mixin
feedback: Union[Iterable, float] = 0.5, # feedback mixin
):
"""Creates a generator which adds a delay effect to the `signal`.
This echo effect can be used for some interesting synthesizer patches (just don't overdo it).
It is also the basis of the `reverb_room/hall/church` effects.
If you're technical, you might be able to use this for phase-shifting.
Limitations:
- Changing the delay's time while sound is playing creates audible popping noises
due to dropped samples.
- To avoid clipping, the original signal is attenuated (reduced) by ca. -3dB. This should
adapt to the mixin and feedback, but currently does not.
:param signal: Signal to process
:type signal: Iterable or float
:param delay: Delay in seconds (default 1.0)
:type delay: Iterable or float
:param mixin: Gain with which to mix the delayed signal back into the original signal (default 0.6)
:type mixin: Iterable or float
:param feedback: Gain with which the result (including delay) will be, in turn, used as the source
signal for the coming delay (default 0.5).
:type feedback: Iterable or float
:return: Signal with added delay effect
:rtype: float
"""
q = Queue()
prev_delay = None
# prev_mixin = None
# prev_feedback = None
delay_in_samples = None
for s, d, m, f in gen_iter(signal, delay, mixin, feedback):
if d != prev_delay:
delay_in_samples = int(d * sample_rate)
# if m != prev_mixin:
# # At mixin == 0.5, both weights are 1.0.
# # With mixin -> 1.0, m_ds stays at 1.0, m_s decreases towards 0.0
# # With mixin -> 0.0, m_ds decreases towards 0.0, m_s stays at 1.0
# m_ds = min(m * 2.0, 1.0)
# m_s = min(max(2.0 - m * 2.0, 0.0), 1.0)
# if f != prev_feedback:
# f_out = min(f * 2.0, 1.0)
# f_s = min(max(2.0 - f * 2.0, 0.0), 1.0)
mismatch = q.qsize() - delay_in_samples
output = s
if mismatch >= 0:
if mismatch >= 2:
delayed_sample = (q.get(block=False) + q.get(block=False)) / 2.0
else:
delayed_sample = q.get(block=False)
if m != 0.0: # attempt to reduce CPU cycles used
# output = m * delayed_sample + (1.0 - m) * s
# output = m_ds * delayed_sample + m_s * s
output = m * delayed_sample + s * 0.75 # attenuate to avoid clipping
if f != 0.0: # attempt to reduce CPU cycles used
# s = f * output + (1.0 - f) * s
# s = f_out * output + f_s * s
s = f * output + s * 0.75 # attenuate to avoid clipping
else:
output = s
yield output
q.put(s)
prev_delay = d
# prev_mixin = m
# prev_feedback = f
def reverb_room(signal: Union[Iterable, float]):
"""Create a generator with a prefab "room reverb" effect. Nothing fancy, feel free to improve!
:param signal: Signal to add reverb to
:type signal: Iterable or float
:return: Yields signal with reverb effect
:rtype: float
"""
# use double the delay everywhere and mixin = 0.3 for church-like reverb
d1 = delay(signal, 0.05, 0.3, 0.2)
d2 = delay(d1, 0.08, 0.2, 0.2)
yield from d2
def reverb_hall(signal: Union[Iterable, float]):
"""Create a generator with a prefab "hall reverb" effect. Nothing fancy, feel free to improve!
:param signal: Signal to add reverb to
:type signal: Iterable or float
:return: Yields signal with reverb effect
:rtype: float
"""
# use double the delay everywhere and mixin = 0.3 for church-like reverb
d1 = delay(signal, 0.05, 0.3, 0.2)
d2 = delay(d1, 0.08, 0.2, 0.2)
d3 = delay(d2, 0.1, 0.2, 0.2)
d31, d32 = split(d3, 2)
d3f = low_pass_filter(d31, alpha=0.85)
d4 = delay(d3f, 0.115, 0.28, 0.2)
d5 = delay(d4, 0.15, 0.15, 0.2)
d5f = low_pass_filter(d5, alpha=0.6)
m = mix([d5f, d32], [1.0, 0.4])
yield from m
def reverb_church(signal: Union[Iterable, float]):
"""Create a generator with a prefab "church reverb" effect. Nothing fancy, feel free to improve!
Sounds currently a bit too echo-y, particularly for percussive instruments. Works rather well for
floating soundscapes, though.
:param signal: Signal to add reverb to
:type signal: Iterable or float
:return: Yields signal with reverb effect
:rtype: float
"""
# use double the delay everywhere and mixin = 0.3 for church-like reverb
d1 = delay(signal, 0.1, 0.3, 0.2)
d2 = delay(d1, 0.16, 0.3, 0.2)
d3 = delay(d2, 0.2, 0.3, 0.2)
d31, d32 = split(d3, 2)
d3f = low_pass_filter(d31, alpha=0.85)
d4 = delay(d3f, 0.23, 0.3, 0.2)
d5 = delay(d4, 0.3, 0.3, 0.2)
d5f = low_pass_filter(d5, alpha=0.6)
m = mix([d5f, d32], [1.0, 0.2])
yield from m
def reverb_todo(signal: Union[Iterable, float],
spread: Union[Iterable, float] = 0.5, # 0.5 = default delay lengths
mixin: Union[Iterable, float] = 0.3,
# mixin % of total signal (>0.5 is higher than original signal. For 1.0, nothing of the original signal will be left -- can be used for phase shifting)
feedback: Union[Iterable, int] = 0.2, # feedback mixed back in
room_hall: str = "room",
):
"""TODO: This should become a reverb that can be easily modified by turning one "time" and one "intensity" knob
"""
s1, s2, s3, s4 = split(signal, 4)
d1 = delay(signal, mult(spread, 0.05 / 0.5), mixin, feedback)
d2 = delay(d1, mult(spread, 0.08 / 0.5), mixin, feedback)
if room_hall.lower() == "room":
yield from d2
elif room_hall.lower() == "hall":
d3 = delay(d2, mult(spread, 0.1 / 0.5), mixin, feedback)
d31, d32 = split(d3, 2)
d3f = low_pass_filter(d31, alpha=0.85)
d4 = delay(d3f, mult(spread, 0.115 / 0.5), mixin, feedback)
d5 = delay(d4, mult(spread, 0.15 / 0.5), mixin, feedback)
d5f = low_pass_filter(d5, alpha=0.6)
m = mix([d5f, d2], [0.8, 0.2])
yield from m
def portamento(
freq: Union[Iterable, float], # Hz
gate: Union[Iterable, float], # 0 == low, >0 == high
duration: Union[Iterable, float] = 0.1, # in seconds
):
"""Creates a generator to add portamento between notes.
Portamento "carries" the frequency from one note to the other, adding a smooth, sweeping quality.
Note that, because it needs to be able to influence frequency values, it acts on the frequency values
(upstream of oscillators), while many other effects act on sample values (downstream of oscillators).
This particular implementation uses legato portamento. This means than only notes that are played
in a connected way will have portamento added between them (the new one pressed before the old one is released).
You can use this to more precisely control the sound in the same piece: Use a more normal or stakkato-like playing
style to get non-portamento transitions, and a legato style for portamento transitions. This distinction
is what the gate signal is needed for.
TODO: Add a parameter to change styles from legato to always.
:param freq: Frequency signal to carry between notes, in Hz
:type freq: Iterable or float
:param gate: Gate signal
:type gate: Iterable or float
:param duration: Transition time between two notes, in seconds
:type duration: Iterable or float
:return: Yields the modified frequency
:rtype: float
"""
curr_freq = 440.0
target_freq = 440.0
step = 0.0
prev_gate = 0
for f, g, d in gen_iter(freq, gate, duration):
if prev_gate == 0.0 and g > 0.0:
curr_freq = f
target_freq = f
step = 0.0
if g > 0:
if f != target_freq:
# Starting new slide
target_freq = f
num_steps = max(d * sample_rate, 1)
step = (target_freq - curr_freq) / num_steps
elif curr_freq != target_freq:
# Continuing slide
if step > 0:
curr_freq = min(curr_freq + step, target_freq)
elif step < 0:
# Continuing downwards slide
curr_freq = max(curr_freq + step, target_freq)
prev_gate = g
yield curr_freq
Functions
def add(signal: Union[Iterable, float], other_val: Union[Iterable, float])-
Create a generator that adds one signal to another.
This helper exists to give you greater flexibility when creating your patches, so there's no need to define custom generators for every little unsupported operation.
:param signal: Signal whose values to modify :type signal: Iterable or float :param other_val: Other value of the arithmetic operation :type other_val: Iterable or float :return: Yield value-by-value output of
signaladded toother_val:rtype: floatExpand source code
def add(signal: Union[Iterable, float], other_val: Union[Iterable, float]): """Create a generator that adds one signal to another. This helper exists to give you greater flexibility when creating your patches, so there's no need to define custom generators for every little unsupported operation. :param signal: Signal whose values to modify :type signal: Iterable or float :param other_val: Other value of the arithmetic operation :type other_val: Iterable or float :return: Yield value-by-value output of `signal` added to `other_val` :rtype: float """ for s, v in gen_iter(signal, other_val): yield s + v def apply(signal: Union[Iterable, float], func:) -
Create a generator that applies a function to the signal value and yields its output
This helper exists to give you greater flexibility when creating your patches, so there's no need to define custom generators for every little unsupported transformation.
:param signal: Signal whose values to modify :type signal: Iterable or float :param func: Function to apply to the signal :type func: callable :return: Yield value-by-value output of
funcapplied tosignal:rtype: floatExpand source code
def apply(signal: Union[Iterable, float], func: callable): """Create a generator that applies a function to the signal value and yields its output This helper exists to give you greater flexibility when creating your patches, so there's no need to define custom generators for every little unsupported transformation. :param signal: Signal whose values to modify :type signal: Iterable or float :param func: Function to apply to the signal :type func: callable :return: Yield value-by-value output of `func` applied to `signal` :rtype: float """ for s in signal: yield func(s) def constrain(signal: Union[Iterable, float], minimum: float = -1.0, maximum: float = 1.0)-
Creates a generator to limit a signal to hard minimum and maximum boundaries. This clips the signal at those boundaries. For less audible clipping, see
drive().:param signal: Signal to constrain :type signal: Iterable or float :param minimum: Lower bound :type minimum: float :param maximum: Upper bound :type maximum: float :return: Yields clipped signal :rtype: float
Expand source code
def constrain(signal: Union[Iterable, float], minimum: float = -1.0, maximum: float = 1.0): """Creates a generator to limit a signal to hard minimum and maximum boundaries. This clips the signal at those boundaries. For less audible clipping, see `drive`. :param signal: Signal to constrain :type signal: Iterable or float :param minimum: Lower bound :type minimum: float :param maximum: Upper bound :type maximum: float :return: Yields clipped signal :rtype: float """ for s in signal: yield max(min(s, maximum), minimum) def delay(signal: Union[Iterable, float], delay: Union[Iterable, float] = 1.0, mixin: Union[Iterable, float] = 0.6, feedback: Union[Iterable, float] = 0.5)-
Creates a generator which adds a delay effect to the
signal.This echo effect can be used for some interesting synthesizer patches (just don't overdo it). It is also the basis of the
reverb_room/hall/churcheffects. If you're technical, you might be able to use this for phase-shifting.Limitations: - Changing the delay's time while sound is playing creates audible popping noises due to dropped samples. - To avoid clipping, the original signal is attenuated (reduced) by ca. -3dB. This should adapt to the mixin and feedback, but currently does not.
:param signal: Signal to process :type signal: Iterable or float :param delay: Delay in seconds (default 1.0) :type delay: Iterable or float :param mixin: Gain with which to mix the delayed signal back into the original signal (default 0.6) :type mixin: Iterable or float :param feedback: Gain with which the result (including delay) will be, in turn, used as the source signal for the coming delay (default 0.5). :type feedback: Iterable or float :return: Signal with added delay effect :rtype: float
Expand source code
def delay(signal: Union[Iterable, float], delay: Union[Iterable, float] = 1.0, # in seconds mixin: Union[Iterable, float] = 0.6, # mixin feedback: Union[Iterable, float] = 0.5, # feedback mixin ): """Creates a generator which adds a delay effect to the `signal`. This echo effect can be used for some interesting synthesizer patches (just don't overdo it). It is also the basis of the `reverb_room/hall/church` effects. If you're technical, you might be able to use this for phase-shifting. Limitations: - Changing the delay's time while sound is playing creates audible popping noises due to dropped samples. - To avoid clipping, the original signal is attenuated (reduced) by ca. -3dB. This should adapt to the mixin and feedback, but currently does not. :param signal: Signal to process :type signal: Iterable or float :param delay: Delay in seconds (default 1.0) :type delay: Iterable or float :param mixin: Gain with which to mix the delayed signal back into the original signal (default 0.6) :type mixin: Iterable or float :param feedback: Gain with which the result (including delay) will be, in turn, used as the source signal for the coming delay (default 0.5). :type feedback: Iterable or float :return: Signal with added delay effect :rtype: float """ q = Queue() prev_delay = None # prev_mixin = None # prev_feedback = None delay_in_samples = None for s, d, m, f in gen_iter(signal, delay, mixin, feedback): if d != prev_delay: delay_in_samples = int(d * sample_rate) # if m != prev_mixin: # # At mixin == 0.5, both weights are 1.0. # # With mixin -> 1.0, m_ds stays at 1.0, m_s decreases towards 0.0 # # With mixin -> 0.0, m_ds decreases towards 0.0, m_s stays at 1.0 # m_ds = min(m * 2.0, 1.0) # m_s = min(max(2.0 - m * 2.0, 0.0), 1.0) # if f != prev_feedback: # f_out = min(f * 2.0, 1.0) # f_s = min(max(2.0 - f * 2.0, 0.0), 1.0) mismatch = q.qsize() - delay_in_samples output = s if mismatch >= 0: if mismatch >= 2: delayed_sample = (q.get(block=False) + q.get(block=False)) / 2.0 else: delayed_sample = q.get(block=False) if m != 0.0: # attempt to reduce CPU cycles used # output = m * delayed_sample + (1.0 - m) * s # output = m_ds * delayed_sample + m_s * s output = m * delayed_sample + s * 0.75 # attenuate to avoid clipping if f != 0.0: # attempt to reduce CPU cycles used # s = f * output + (1.0 - f) * s # s = f_out * output + f_s * s s = f * output + s * 0.75 # attenuate to avoid clipping else: output = s yield output q.put(s) prev_delay = d def div(signal: Union[Iterable, float], other_val: Union[Iterable, float])-
Create a generator that divides one signal by another.
This helper exists to give you greater flexibility when creating your patches, so there's no need to define custom generators for every little unsupported operation.
:param signal: Signal whose values to modify :type signal: Iterable or float :param other_val: Other value of the arithmetic operation :type other_val: Iterable or float :return: Yield value-by-value output of
signaldivided byother_val:rtype: floatExpand source code
def div(signal: Union[Iterable, float], other_val: Union[Iterable, float]): """Create a generator that divides one signal by another. This helper exists to give you greater flexibility when creating your patches, so there's no need to define custom generators for every little unsupported operation. :param signal: Signal whose values to modify :type signal: Iterable or float :param other_val: Other value of the arithmetic operation :type other_val: Iterable or float :return: Yield value-by-value output of `signal` divided by `other_val` :rtype: float """ for s, v in gen_iter(signal, other_val): yield s / v def drive(signal: Union[Iterable, float], gain: Union[Iterable, float] = 2.0, mixin: Union[Iterable, float] = 0.0)-
Creates a generator boosting a signal to make it sound 'fatter'. Depending on the parameters, This can range from a clean boost, via overdrive (if you mix in undriven signal), to distortion (without any simulated physical tube amp effects, though).
:param signal: Source signal iterable/generator :param gain: amplitude scaling factor (default 2.0). Higher gains lead to more drastic cutoffs. A gain value of 0.0 turns this node into a linear pass-through. :param mixin: mix this much (0.0 to 1.0) of the unmodified signal back in (default 0.0)
Expand source code
def drive( signal: Union[Iterable, float], gain: Union[Iterable, float] = 2.0, mixin: Union[Iterable, float] = 0.0): """Creates a generator boosting a signal to make it sound 'fatter'. Depending on the parameters, This can range from a clean boost, via overdrive (if you mix in undriven signal), to distortion (without any simulated physical tube amp effects, though). :param signal: Source signal iterable/generator :param gain: amplitude scaling factor (default 2.0). Higher gains lead to more drastic cutoffs. A gain value of 0.0 turns this node into a linear pass-through. :param mixin: mix this much (0.0 to 1.0) of the unmodified signal back in (default 0.0) """ for s, g, m in gen_iter(signal, gain, mixin): # Apply gain, then apply smoothing function x/(1+|x|), then scale result back to [-1, 1] # s_ = s * g # smoothed = s_ / (1 + abs(s_)) # yield smoothed * (g + 1)/g # save one multiplication and division, do it in one step: driven = s / (1 + abs(s * g)) * (g + 1) # print(g, s, driven) # yield m * s + (1.0 - m) * driven # results in attenuation (e.g. at 0.5, the two parts are not suddenly half as loud only because they are going to be mixed) yield driven + m * s * 0.75 # attenuate to avoid clipping def drive_deprecated(signal: Union[Iterable, float], gain: float = 1.5, soften: float = 0.5, exp: int = 3)-
Signal boosting generator to make it sound 'fatter'. Depending on the parameters, This can range from a clean boost, via overdrive (if you mix in undriven signal afterwards), to distortion (without any simulated physical tube amp effects, though).
:param signal: source signal iterable/generator :param gain: linear amplitude scaling factor (default 1.5) :param soften: part of the saturation amplitude range which is rounded off towards the peak (default 0.5 as 1.5-0.5 = 1.0 which results in an unclipped signal) :param exp: steepness of the rounding-off (default 3). 1 would result in no effect, 5 results in sharper rounding, but overshoots and requires reducing both the
boostandsoftingparameters, e.g. to 1.25 and 0.25 respectively. https://www.wolframalpha.com/input?i=plot+1.25x+-+0.25*x%5E5+between+-1+and+1Expand source code
def drive_deprecated(signal: Union[Iterable, float], gain: float = 1.5, soften: float = 0.5, exp: int = 3): """Signal boosting generator to make it sound 'fatter'. Depending on the parameters, This can range from a clean boost, via overdrive (if you mix in undriven signal afterwards), to distortion (without any simulated physical tube amp effects, though). :param signal: source signal iterable/generator :param gain: linear amplitude scaling factor (default 1.5) :param soften: part of the saturation amplitude range which is rounded off towards the peak (default 0.5 as 1.5-0.5 = 1.0 which results in an unclipped signal) :param exp: steepness of the rounding-off (default 3). 1 would result in no effect, 5 results in sharper rounding, but overshoots and requires reducing both the `boost` and `softing` parameters, e.g. to 1.25 and 0.25 respectively. https://www.wolframalpha.com/input?i=plot+1.25x+-+0.25*x%5E5+between+-1+and+1 """ if exp % 2 == 0: raise ValueError("exp must be an uneven integer number") max_val = abs(gain - soften) if max_val != 0.0: print("Warning: assuming a [+1,-1] signal, your drive settings might " f"lead to {'clipping' if max_val > 1.0 else 'attenuation'} " f"(gain {gain} - soften {soften} = max {max_val})") for s in signal: yield gain * s - soften * s ** exp def eg(signal: Union[Iterable, float] = 1.0, gate: Union[Iterable, float] = 1, adsr: Union[Iterable[Iterable[float]], Iterable[float]] = (0.0, 0.0, 1.0, 0.0), auto_trigger: Union[Iterable, int] = 0)-
Create an Envelope Generator (Attack/Decay/Sustain/Release)
Creates an amplitude scaling envelope between 0.0 and 1.0. If
signalis provided, the result will be this signal scaled according to the envelope progression. Ifsignalis not provided, the output will be the [0.0, 1.0] scaling envelope itself, which can be used as parameter for other nodes (e.g.low_pass_filter()cutoff).Scaling and movement from one envelope step to the next is linear.
:param signal: Optional signal to apply envelope to (default 1.0 constant) :type signal: Iterable or float :param gate: The gate signal (> 0.0 if key is held down, 0.0 (=LOW) otherwise) :type gate: float :param adsr: Tuple/list of the four values for Attack, Decay, Sustain and Release, or a generator providing a stream of tuples/lists. :type adsr: Union[Iterable[Iterable[float]], Iterable[float]] :param auto_trigger: Repeat the Attack-Decay sequence as long as
gateis > 0.0 :type auto_trigger: int :return: Yields envelope-scaled signal, or envelope signal itself if signal is not provided :rtype: floatExpand source code
def envelope_generator( signal: Union[Iterable, float] = 1.0, gate: Union[Iterable, float] = HIGH, adsr: Union[Iterable[Iterable[float]], Iterable[float]] = (0.0, 0.0, 1.0, 0.0), auto_trigger: Union[Iterable, int] = LOW, ): """Create an Envelope Generator (Attack/Decay/Sustain/Release) Creates an amplitude scaling envelope between 0.0 and 1.0. If `signal` is provided, the result will be this signal scaled according to the envelope progression. If `signal` is not provided, the output will be the [0.0, 1.0] scaling envelope itself, which can be used as parameter for other nodes (e.g. `low_pass_filter` cutoff). Scaling and movement from one envelope step to the next is linear. :param signal: Optional signal to apply envelope to (default 1.0 constant) :type signal: Iterable or float :param gate: The gate signal (> 0.0 if key is held down, 0.0 (=LOW) otherwise) :type gate: float :param adsr: Tuple/list of the four values for Attack, Decay, Sustain and Release, or a generator providing a stream of tuples/lists. :type adsr: Union[Iterable[Iterable[float]], Iterable[float]] :param auto_trigger: Repeat the Attack-Decay sequence as long as `gate` is > 0.0 :type auto_trigger: int :return: Yields envelope-scaled signal, or envelope signal itself if signal is not provided :rtype: float """ adsr_phase = ATTACK prev_adsr = (-1, -1, -1, -1) prev_gate = LOW curr_velocity = 0 curr_env = 0.0 attack_step = None decay_step = None release_step = None max_scaled = None attack_step_scaled = None decay_step_scaled = None sustain_val_scaled = None release_step_scaled = None if not isinstance(adsr, list) and not isinstance(adsr, tuple): raise ValueError( f"Parameter `adsr` must be a flat list/tuple of 4 envelope values (attack, decay, sustain, release) or of 4 Iterables/Generators which provides a, d, s and r respectively. It was {adsr}, type {type(adsr)}") # if not isinstance(adsr[0], Iterable): # # Make flat adsr list a generator (the usual Iterable check does not suffice) # adsr = static_generator(adsr) for sig_val, gate_val, a, d, s, r, retrig in gen_iter(signal, gate, *adsr, auto_trigger): adsr_val = (a, d, s, r) if adsr_val != prev_adsr: # For immediate envelope changes, we still use a change time of 10ms # (step size < 1.0) to avoid popping noise: minimum_step = 1.0 / (0.01 * sample_rate) attack_step = 1.0 / (adsr_val[ATTACK] * sample_rate) if adsr_val[ATTACK] > 0.0 else minimum_step decay_step = (1.0 - adsr_val[SUSTAIN]) / (adsr_val[DECAY] * sample_rate) if adsr_val[ DECAY] > 0.0 else minimum_step if adsr_val[DECAY] > 0.0 and adsr_val[SUSTAIN] == 0.0: # AD mode: # If A and D are set, but S is 0.0, R should be ignored # (release as fast as possible = ignore release time) release_step = minimum_step elif adsr_val[DECAY] == 0.0 and adsr_val[SUSTAIN] == 0.0: # AR mode: # If A and R are set, but D and S are zero, we should immediately move from # the ATTACK phase to the RELEASE phase. The end value of ATTACK (= 1.0) # will be the RELEASE starting value: release_step = 1.0 / (adsr_val[RELEASE] * sample_rate) if adsr_val[RELEASE] > 0.0 else minimum_step else: # Normal ADSR mode release_step = adsr_val[SUSTAIN] / (adsr_val[RELEASE] * sample_rate) if adsr_val[ RELEASE] > 0.0 else minimum_step # Gate carries velocity information (don't test for HIGH, but for gate_val != LOW) if prev_gate == LOW and gate_val != LOW: # gate_val > 0 carries the velocity signal attack_step_scaled = attack_step * gate_val decay_step_scaled = decay_step * gate_val release_step_scaled = release_step * gate_val sustain_val_scaled = adsr_val[SUSTAIN] * gate_val max_scaled = gate_val if prev_gate != LOW and gate_val == LOW: # print("starting release") adsr_phase = RELEASE if adsr_phase == ATTACK and gate_val != LOW: if curr_env < max_scaled: curr_env = min(curr_env + attack_step_scaled, max_scaled) elif adsr_val[DECAY] == 0.0 and adsr_val[SUSTAIN] == 0.0: # AR mode adsr_phase = RELEASE else: # print("starting decay") adsr_phase = DECAY elif adsr_phase == DECAY and gate_val != LOW: if curr_env > sustain_val_scaled: curr_env = max(curr_env - decay_step_scaled, sustain_val_scaled) elif retrig: # print("retriggering attack") adsr_phase = ATTACK else: # print("starting sustain") adsr_phase = SUSTAIN if adsr_phase == RELEASE: if curr_env > 0.0 and gate_val == LOW: curr_env = max(curr_env - release_step_scaled, 0.0) else: # print("end of release") # curr_env = 0.0 # leads to popping sound adsr_phase = ATTACK # print(prev_gate, gate_val, adsr_phase, curr_env) prev_gate = gate_val prev_adsr = adsr_val yield sig_val * curr_env def ensure_iterable(signal: Union[Iterable, float], force=False)-
If
signalis not iterable, wrap it in a generator returning the same value over and over.Expand source code
def ensure_iterable(signal: Union[Iterable, float], force=False): """If `signal` is not iterable, wrap it in a generator returning the same value over and over.""" if isinstance(signal, Iterable) and not force: return signal else: return static_generator(signal) def envelope_generator(signal: Union[Iterable, float] = 1.0, gate: Union[Iterable, float] = 1, adsr: Union[Iterable[Iterable[float]], Iterable[float]] = (0.0, 0.0, 1.0, 0.0), auto_trigger: Union[Iterable, int] = 0)-
Create an Envelope Generator (Attack/Decay/Sustain/Release)
Creates an amplitude scaling envelope between 0.0 and 1.0. If
signalis provided, the result will be this signal scaled according to the envelope progression. Ifsignalis not provided, the output will be the [0.0, 1.0] scaling envelope itself, which can be used as parameter for other nodes (e.g.low_pass_filter()cutoff).Scaling and movement from one envelope step to the next is linear.
:param signal: Optional signal to apply envelope to (default 1.0 constant) :type signal: Iterable or float :param gate: The gate signal (> 0.0 if key is held down, 0.0 (=LOW) otherwise) :type gate: float :param adsr: Tuple/list of the four values for Attack, Decay, Sustain and Release, or a generator providing a stream of tuples/lists. :type adsr: Union[Iterable[Iterable[float]], Iterable[float]] :param auto_trigger: Repeat the Attack-Decay sequence as long as
gateis > 0.0 :type auto_trigger: int :return: Yields envelope-scaled signal, or envelope signal itself if signal is not provided :rtype: floatExpand source code
def envelope_generator( signal: Union[Iterable, float] = 1.0, gate: Union[Iterable, float] = HIGH, adsr: Union[Iterable[Iterable[float]], Iterable[float]] = (0.0, 0.0, 1.0, 0.0), auto_trigger: Union[Iterable, int] = LOW, ): """Create an Envelope Generator (Attack/Decay/Sustain/Release) Creates an amplitude scaling envelope between 0.0 and 1.0. If `signal` is provided, the result will be this signal scaled according to the envelope progression. If `signal` is not provided, the output will be the [0.0, 1.0] scaling envelope itself, which can be used as parameter for other nodes (e.g. `low_pass_filter` cutoff). Scaling and movement from one envelope step to the next is linear. :param signal: Optional signal to apply envelope to (default 1.0 constant) :type signal: Iterable or float :param gate: The gate signal (> 0.0 if key is held down, 0.0 (=LOW) otherwise) :type gate: float :param adsr: Tuple/list of the four values for Attack, Decay, Sustain and Release, or a generator providing a stream of tuples/lists. :type adsr: Union[Iterable[Iterable[float]], Iterable[float]] :param auto_trigger: Repeat the Attack-Decay sequence as long as `gate` is > 0.0 :type auto_trigger: int :return: Yields envelope-scaled signal, or envelope signal itself if signal is not provided :rtype: float """ adsr_phase = ATTACK prev_adsr = (-1, -1, -1, -1) prev_gate = LOW curr_velocity = 0 curr_env = 0.0 attack_step = None decay_step = None release_step = None max_scaled = None attack_step_scaled = None decay_step_scaled = None sustain_val_scaled = None release_step_scaled = None if not isinstance(adsr, list) and not isinstance(adsr, tuple): raise ValueError( f"Parameter `adsr` must be a flat list/tuple of 4 envelope values (attack, decay, sustain, release) or of 4 Iterables/Generators which provides a, d, s and r respectively. It was {adsr}, type {type(adsr)}") # if not isinstance(adsr[0], Iterable): # # Make flat adsr list a generator (the usual Iterable check does not suffice) # adsr = static_generator(adsr) for sig_val, gate_val, a, d, s, r, retrig in gen_iter(signal, gate, *adsr, auto_trigger): adsr_val = (a, d, s, r) if adsr_val != prev_adsr: # For immediate envelope changes, we still use a change time of 10ms # (step size < 1.0) to avoid popping noise: minimum_step = 1.0 / (0.01 * sample_rate) attack_step = 1.0 / (adsr_val[ATTACK] * sample_rate) if adsr_val[ATTACK] > 0.0 else minimum_step decay_step = (1.0 - adsr_val[SUSTAIN]) / (adsr_val[DECAY] * sample_rate) if adsr_val[ DECAY] > 0.0 else minimum_step if adsr_val[DECAY] > 0.0 and adsr_val[SUSTAIN] == 0.0: # AD mode: # If A and D are set, but S is 0.0, R should be ignored # (release as fast as possible = ignore release time) release_step = minimum_step elif adsr_val[DECAY] == 0.0 and adsr_val[SUSTAIN] == 0.0: # AR mode: # If A and R are set, but D and S are zero, we should immediately move from # the ATTACK phase to the RELEASE phase. The end value of ATTACK (= 1.0) # will be the RELEASE starting value: release_step = 1.0 / (adsr_val[RELEASE] * sample_rate) if adsr_val[RELEASE] > 0.0 else minimum_step else: # Normal ADSR mode release_step = adsr_val[SUSTAIN] / (adsr_val[RELEASE] * sample_rate) if adsr_val[ RELEASE] > 0.0 else minimum_step # Gate carries velocity information (don't test for HIGH, but for gate_val != LOW) if prev_gate == LOW and gate_val != LOW: # gate_val > 0 carries the velocity signal attack_step_scaled = attack_step * gate_val decay_step_scaled = decay_step * gate_val release_step_scaled = release_step * gate_val sustain_val_scaled = adsr_val[SUSTAIN] * gate_val max_scaled = gate_val if prev_gate != LOW and gate_val == LOW: # print("starting release") adsr_phase = RELEASE if adsr_phase == ATTACK and gate_val != LOW: if curr_env < max_scaled: curr_env = min(curr_env + attack_step_scaled, max_scaled) elif adsr_val[DECAY] == 0.0 and adsr_val[SUSTAIN] == 0.0: # AR mode adsr_phase = RELEASE else: # print("starting decay") adsr_phase = DECAY elif adsr_phase == DECAY and gate_val != LOW: if curr_env > sustain_val_scaled: curr_env = max(curr_env - decay_step_scaled, sustain_val_scaled) elif retrig: # print("retriggering attack") adsr_phase = ATTACK else: # print("starting sustain") adsr_phase = SUSTAIN if adsr_phase == RELEASE: if curr_env > 0.0 and gate_val == LOW: curr_env = max(curr_env - release_step_scaled, 0.0) else: # print("end of release") # curr_env = 0.0 # leads to popping sound adsr_phase = ATTACK # print(prev_gate, gate_val, adsr_phase, curr_env) prev_gate = gate_val prev_adsr = adsr_val yield sig_val * curr_env def gain(signal: Union[Iterable, float], gain: Union[Iterable, float] = 1.0)-
Creates a generator that reduces or boosts a
signalby the factorgain().Scaling is linear (multiplication by
gain()) and unbounded.Typically used with
gain()values between 0.0 and 1.0, but you can get creative, of course. Just make sure that the audio output signal does not clip (is between -1.0 and 1.0).:param signal: Signal to scale. :type signal: Iterable or float :param gain: Factor to scale
signalby, e.g. 1.0 = no change (default); 0.5 = half amplitude; 0.0 = mute completely; -1.0 = invert :type gain: Iterable or float :return: Yields the scaled signal :rtype: floatExpand source code
def gain(signal: Union[Iterable, float], gain: Union[Iterable, float] = 1.0): """Creates a generator that reduces or boosts a `signal` by the factor `gain`. Scaling is linear (multiplication by `gain`) and unbounded. Typically used with `gain` values between 0.0 and 1.0, but you can get creative, of course. Just make sure that the audio output signal does not clip (is between -1.0 and 1.0). :param signal: Signal to scale. :type signal: Iterable or float :param gain: Factor to scale `signal` by, e.g. 1.0 = no change (default); 0.5 = half amplitude; 0.0 = mute completely; -1.0 = invert :type gain: Iterable or float :return: Yields the scaled signal :rtype: float """ for sig, g in gen_iter(signal, gain): yield g * sig def gen_iter(*args)-
Helper function to make sure the
*argsare iterable, wrapping non-iterables in a generator which returns the same value over and over.Expand source code
def gen_iter(*args): """Helper function to make sure the `*args` are iterable, wrapping non-iterables in a generator which returns the same value over and over. """ arg_gens = [ensure_iterable(arg) for arg in args] for curr_vals in zip(*arg_gens): if len(curr_vals) == 1: yield curr_vals[0] else: yield curr_vals def high_pass_filter(signal: Union[Iterable, float], alpha: Union[Iterable, float] = 1.0, poles: int = 2)-
Creates a generator to attenuate (reduce) the lower frequencies in the input signal. This makes higher harmonics stand out more in relation. Can also be used for isolating momentary changes from other data (e.g. raw sensor values).
alphaindirectly selects the cutoff frequency. Higher pole numbers (4 and 8 are good numbers) increase the sharpness of the cutoff (Not sure this works properly, though. It sounds weird).Currently, the undesired attenuation of higher frequencies is not compensated for (this is a passive IIR filter). Neither is there any key tracking (i.e. shift of the cutoff according to the frequency played).
:param signal: Signal to be filtered :type signal: Iterable or float :param alpha: Select cutoff frequency. 1.0 = all-pass (default), 0.0 = no-pass :type alpha: Iterable or float :param poles: Number of times the filter is applied to the signal (poles/stages/degrees). :type poles: int :return: Yields the high-pass signal :rtype: float
Expand source code
def high_pass_filter(signal: Union[Iterable, float], alpha: Union[Iterable, float] = 1.0, poles: int = 2): """Creates a generator to attenuate (reduce) the lower frequencies in the input signal. This makes higher harmonics stand out more in relation. Can also be used for isolating momentary changes from other data (e.g. raw sensor values). `alpha` indirectly selects the cutoff frequency. Higher pole numbers (4 and 8 are good numbers) increase the sharpness of the cutoff (Not sure this works properly, though. It sounds weird). Currently, the undesired attenuation of higher frequencies is not compensated for (this is a passive IIR filter). Neither is there any key tracking (i.e. shift of the cutoff according to the frequency played). :param signal: Signal to be filtered :type signal: Iterable or float :param alpha: Select cutoff frequency. 1.0 = all-pass (default), 0.0 = no-pass :type alpha: Iterable or float :param poles: Number of times the filter is applied to the signal (poles/stages/degrees). :type poles: int :return: Yields the high-pass signal :rtype: float """ last = [0.0] * poles filtered = [0.0] * poles for sig, alph in gen_iter(signal, alpha): curr_sig = sig for p in range(poles): filtered[p] = alph * (filtered[p] + curr_sig - last[p]) # filtered[p] = (1.0 - alph) * filtered[p] + alph * curr_sig last[p] = curr_sig curr_sig = filtered[p] yield curr_sig def if_else(bool_exp: Union[Iterable, float], if_true: Iterable, if_false: Iterable)-
Creates a generator which selects one or the other upstream signals to pass through.
Can for example be used to turn on/off effects (and save CPU cycles)::
plain = square(440) with_reverb = reverb_hall(plain) out = if_else(cc["some_button"], with_reverb, plain)
Keep in mind that you can create your own Python generators for the
bool_exppart, e.g.::def on_off(): '''Needs to be initialized with paranthesis: if_else(on_off(), …)''' global some_variable while True: if some_variable is True: yield 1.0 else: yield 0.0
# or with generator expressions (assuming global scope for this example) on_off = (float(v) for v in repeat(some_variable))
Note that the non-used signal node (
if_trueorif_false) is not running while not selected. This might sometimes have unintended consequences. For example, if one of these or both have asplit()somewhere upstream, you might encounter large memory consumption/buffer overflow in the inactive branch.To be completely safe (at the cost of wasting CPU cycles), you can use a combination of
gain()andmix()to achieve the same. The basic arithmetic generator functionsadd(),sub(),mult()anddiv(), as well as the all-purposeapply(), can be useful building blocks as well::plain = square(440) with_reverb = reverb_hall(plain) out = mix( gain(with_reverb, cc["some_button"]) gain(plain, sub(1.0, cc["some_button"])) )
:param bool_exp: Expression to use for the choice. >=0.5 means True and <0.5 means False. :type bool_exp: float :param if_true: Stream to pass through if
bool_expis greater than or equal to 0.5 :type if_true: Iterable :param if_false: Stream to pass through ifbool_expis less than 0.5 :type if_false: Iterable :return: Yields selected stream's data :rtype: Probably float, but hard to say, reallyExpand source code
def if_else( bool_exp: Union[Iterable, float], # >=0.5 = true, else false if_true: Iterable, # Generator (/chain) to use as signal if true if_false: Iterable, # Generator (/chain) to use as signal if false ): """Creates a generator which selects one or the other upstream signals to pass through. Can for example be used to turn on/off effects (and save CPU cycles):: plain = square(440) with_reverb = reverb_hall(plain) out = if_else(cc["some_button"], with_reverb, plain) Keep in mind that you can create your own Python generators for the `bool_exp` part, e.g.:: def on_off(): '''Needs to be initialized with paranthesis: if_else(on_off(), ...)''' global some_variable while True: if some_variable is True: yield 1.0 else: yield 0.0 # or with generator expressions (assuming global scope for this example) on_off = (float(v) for v in repeat(some_variable)) Note that the non-used signal node (`if_true` or `if_false`) is not running while not selected. This might sometimes have unintended consequences. For example, if one of these or both have a `split` somewhere upstream, you might encounter large memory consumption/buffer overflow in the inactive branch. To be completely safe (at the cost of wasting CPU cycles), you can use a combination of `gain` and `mix` to achieve the same. The basic arithmetic generator functions `add`, `sub`, `mult` and `div`, as well as the all-purpose `apply`, can be useful building blocks as well:: plain = square(440) with_reverb = reverb_hall(plain) out = mix( gain(with_reverb, cc["some_button"]) gain(plain, sub(1.0, cc["some_button"])) ) :param bool_exp: Expression to use for the choice. >=0.5 means True and <0.5 means False. :type bool_exp: float :param if_true: Stream to pass through if `bool_exp` is greater than or equal to 0.5 :type if_true: Iterable :param if_false: Stream to pass through if `bool_exp` is less than 0.5 :type if_false: Iterable :return: Yields selected stream's data :rtype: Probably float, but hard to say, really """ for b in gen_iter(bool_exp): if b >= 0.5: yield next(if_true) else: yield next(if_false) def low_pass_filter(signal: Union[Iterable, float], alpha: Union[Iterable, float] = 1.0, resonance: Union[Iterable, float] = 0.0, poles: int = 4)-
Creates a generator to attenuate (reduce) higher frequencies in the input signal. This removes higher harmonics, giving sounds a (literally) smoother quality. Can also be used for smoothing non-sound data (e.g. raw sensor values).
alphaindirectly selects the cutoff frequency (alpha = Xc/sqrt(R^2 + Xc^2), where Xc = 1 / (2 pi f C)). Higher pole numbers (4 and 8 are good numbers) increase the sharpness of the cutoff.Currently, the undesired attenuation of lower frequencies is not compensated for (this is a passive IIR filter). Neither is there any key tracking (i.e. shift of the cutoff according to the frequency played).
:param signal: Signal to be filtered :type signal: Iterable or float :param alpha: Select cutoff frequency. 1.0 = all-pass (default), 0.0 = no-pass :type alpha: Iterable or float :param resonance: Not implemented :type resonance: Iterable or float :param poles: Number of times the filter is applied to the signal (poles/stages/degrees). Every pole should lead to a -45 degree phase shift around the cutoff frequency and -90 above, but numeric results don't (yet) agree with that. :type poles: int :return: Yields the low-pass signal :rtype: float
Expand source code
def low_pass_filter( # naïve implementation signal: Union[Iterable, float], alpha: Union[Iterable, float] = 1.0, resonance: Union[Iterable, float] = 0.0, # TODO: doesn't work yet poles: int = 4): """Creates a generator to attenuate (reduce) higher frequencies in the input signal. This removes higher harmonics, giving sounds a (literally) smoother quality. Can also be used for smoothing non-sound data (e.g. raw sensor values). `alpha` indirectly selects the cutoff frequency (alpha = Xc/sqrt(R^2 + Xc^2), where Xc = 1 / (2 pi f C)). Higher pole numbers (4 and 8 are good numbers) increase the sharpness of the cutoff. Currently, the undesired attenuation of lower frequencies is not compensated for (this is a passive IIR filter). Neither is there any key tracking (i.e. shift of the cutoff according to the frequency played). :param signal: Signal to be filtered :type signal: Iterable or float :param alpha: Select cutoff frequency. 1.0 = all-pass (default), 0.0 = no-pass :type alpha: Iterable or float :param resonance: Not implemented :type resonance: Iterable or float :param poles: Number of times the filter is applied to the signal (poles/stages/degrees). Every pole *should* lead to a -45 degree phase shift around the cutoff frequency and -90 above, but numeric results don't (yet) agree with that. :type poles: int :return: Yields the low-pass signal :rtype: float """ last = [0.0] * poles filtered = [0.0] * poles for sig, alph, res in gen_iter(signal, alpha, resonance): curr_sig = sig for p in range(poles): # filtered[p] = (1.0 - alph) * filtered[p] + alph * (curr_sig + last[p])/2.0 filtered[p] = (1.0 - alph) * filtered[p] + alph * curr_sig last[p] = curr_sig curr_sig = filtered[p] curr_sig -= res * curr_sig # inverted feedback (should work due to 4-pole filter's phase shift of -180, but doesn't) yield curr_sig def mix(signals: Union[Iterable[Collection[float]], Collection[float]], weights: Union[Iterable[Collection[float]], Collection[float], ForwardRef(None)] = None)-
Creates a generator that combines several signals into one by averaging (optionally weighted averaging).
Typically used to combine different wave forms into one. But other creative uses are possible, such as fusing different controller inputs (e.g. using one as coarse and the other as fine input for the same parameter).
Before using
weights, please consider the alternative of applyinggain()to the various input signals before mixing. This often makes the patch code easier to manage.Because a simple averaging would reduce the overall volume, the gain is scaled to retain the average (RMS) power of the signal (if no weights are provided). This is only correct on average, though, and might lead to occasional audio clipping. To make sure that the output is never clipped, consider outputting less than 100% gain, e.g. scale to 0.8 as the last step of your patch.
:param signals: Tuple/list of signals to mix :type signals: Collection of Iterables or floats :param weights: Optional relative weights, overriding RMS scaling. Should sum up to 1.0 :type weights: Optional Collection of Iterables or floats :return: Yields the mixed signal :rtype: float
Expand source code
def mix(signals: Union[Iterable[Collection[float]], Collection[float]], weights: Optional[Union[Iterable[Collection[float]], Collection[float]]] = None): """Creates a generator that combines several signals into one by averaging (optionally weighted averaging). Typically used to combine different wave forms into one. But other creative uses are possible, such as fusing different controller inputs (e.g. using one as coarse and the other as fine input for the same parameter). Before using `weights`, please consider the alternative of applying `gain` to the various input signals before mixing. This often makes the patch code easier to manage. Because a simple averaging would reduce the overall volume, the gain is scaled to retain the average (RMS) power of the signal (if no weights are provided). This is only correct on average, though, and might lead to occasional audio clipping. To make sure that the output is never clipped, consider outputting less than 100% gain, e.g. scale to 0.8 as the last step of your patch. :param signals: Tuple/list of signals to mix :type signals: Collection of Iterables or floats :param weights: Optional relative weights, overriding RMS scaling. Should sum up to 1.0 :type weights: Optional Collection of Iterables or floats :return: Yields the mixed signal :rtype: float """ n = len(signals) if weights is None: weights = [1 / sqrt(n)] * n # e.g. 1/sqrt(2)=0.7 RMS -- 3dB per additional signal for vals_and_weights in gen_iter(*signals, *weights): vals = vals_and_weights[:n] weights = vals_and_weights[n:] yield sum([v * w for v, w in zip(vals, weights)]) def mult(signal: Union[Iterable, float], other_val: Union[Iterable, float])-
Create a generator that multiplies one signal with another.
This helper exists to give you greater flexibility when creating your patches, so there's no need to define custom generators for every little unsupported operation.
:param signal: Signal whose values to modify :type signal: Iterable or float :param other_val: Other value of the arithmetic operation :type other_val: Iterable or float :return: Yield value-by-value output of
signalmultiplied byother_val:rtype: floatExpand source code
def mult(signal: Union[Iterable, float], other_val: Union[Iterable, float]): """Create a generator that multiplies one signal with another. This helper exists to give you greater flexibility when creating your patches, so there's no need to define custom generators for every little unsupported operation. :param signal: Signal whose values to modify :type signal: Iterable or float :param other_val: Other value of the arithmetic operation :type other_val: Iterable or float :return: Yield value-by-value output of `signal` multiplied by `other_val` :rtype: float """ for s, v in gen_iter(signal, other_val): yield s * v def noise(f: Union[Iterable, float, ForwardRef(None)])-
Create an oscillator node generating white noise, ignoring the frequency parameter
f::. .. . . . . . . . . . .
Noise is often mixed to other wave forms to add some texture. When adding (possibly modulated) filters, the character can change dramatically. Frequently combined with a suitable envelope to model percussive instruments.
:param f: (just present for compatibility, ignored) :type f: (ignored) :return: Yields samples between -1.0 and 1.0 :rtype: float.
Expand source code
def noise(f: Optional[Union[Iterable, float]]): """Create an oscillator node generating white noise, ignoring the frequency parameter `f`:: . . . . . . . . . . . . . Noise is often mixed to other wave forms to add some texture. When adding (possibly modulated) filters, the character can change dramatically. Frequently combined with a suitable envelope to model percussive instruments. :param f: (just present for compatibility, ignored) :type f: (ignored) :return: Yields samples between -1.0 and 1.0 :rtype: float. """ for f_val in gen_iter(f): yield 2.0 * random() - 1.0 def play_live(generator: Iterable)-
Consume the specified
generatornode as audio samples to play using system audio. See the constants insynthesizer.pyfor sampling rate and similar settings.The generator can be the last in a potentially long and complex chain (see
examples.py). This might be again()node using cc["master_volume"] to set the output volume.:param generator: Sample generator node to play :type generator: generator
Expand source code
def play_live(generator: Iterable): """Consume the specified `generator` node as audio samples to play using system audio. See the constants in `synthesizer.py` for sampling rate and similar settings. The generator can be the last in a potentially long and complex chain (see `examples.py`). This might be a `gain()` node using cc["master_volume"] to set the output volume. :param generator: Sample generator node to play :type generator: generator """ sample_format = {1: ma.SampleFormat.UNSIGNED8, 2: ma.SampleFormat.SIGNED16, 4: ma.SampleFormat.SIGNED32}[ sample_width] stream = _my_audio_stream(generator) with ma.PlaybackDevice( output_format=sample_format, nchannels=channels, sample_rate=sample_rate, buffersize_msec=buffersize_msec, ) as device: next(stream) # start the generator device.start(stream) while not stop: sleep(0.5) def play_static(samples: Iterable)-
Play a finite iterable of audio samples. TODO: Adjust to be compatible with generators. :param samples: list/tuple/… of samples :type samples: Iterable
Expand source code
def play_static(samples: Iterable): """Play a finite iterable of audio samples. TODO: Adjust to be compatible with generators. :param samples: list/tuple/... of samples :type samples: Iterable """ samples_np = (np.array(samples) * sample_max).astype(np.int16) # print(samples_np.shape) # print(samples_np[:10]) # print(samples_np.dtype) arr = array('h', samples_np) stream = ma.stream_raw_pcm_memory(arr, channels, sample_width) stream_with_callbacks = ma.stream_with_callbacks( stream, progress_callback=None, frame_process_method=None, end_callback=stop_stream, ) with ma.PlaybackDevice( output_format=ma.SampleFormat.SIGNED16, nchannels=channels, sample_rate=sample_rate, buffersize_msec=buffersize_msec, ) as device: next(stream_with_callbacks) # start the generator device.start(stream_with_callbacks) while not stop: sleep(0.1) def portamento(freq: Union[Iterable, float], gate: Union[Iterable, float], duration: Union[Iterable, float] = 0.1)-
Creates a generator to add portamento between notes.
Portamento "carries" the frequency from one note to the other, adding a smooth, sweeping quality.
Note that, because it needs to be able to influence frequency values, it acts on the frequency values (upstream of oscillators), while many other effects act on sample values (downstream of oscillators).
This particular implementation uses legato portamento. This means than only notes that are played in a connected way will have portamento added between them (the new one pressed before the old one is released). You can use this to more precisely control the sound in the same piece: Use a more normal or stakkato-like playing style to get non-portamento transitions, and a legato style for portamento transitions. This distinction is what the gate signal is needed for.
TODO: Add a parameter to change styles from legato to always.
:param freq: Frequency signal to carry between notes, in Hz :type freq: Iterable or float :param gate: Gate signal :type gate: Iterable or float :param duration: Transition time between two notes, in seconds :type duration: Iterable or float :return: Yields the modified frequency :rtype: float
Expand source code
def portamento( freq: Union[Iterable, float], # Hz gate: Union[Iterable, float], # 0 == low, >0 == high duration: Union[Iterable, float] = 0.1, # in seconds ): """Creates a generator to add portamento between notes. Portamento "carries" the frequency from one note to the other, adding a smooth, sweeping quality. Note that, because it needs to be able to influence frequency values, it acts on the frequency values (upstream of oscillators), while many other effects act on sample values (downstream of oscillators). This particular implementation uses legato portamento. This means than only notes that are played in a connected way will have portamento added between them (the new one pressed before the old one is released). You can use this to more precisely control the sound in the same piece: Use a more normal or stakkato-like playing style to get non-portamento transitions, and a legato style for portamento transitions. This distinction is what the gate signal is needed for. TODO: Add a parameter to change styles from legato to always. :param freq: Frequency signal to carry between notes, in Hz :type freq: Iterable or float :param gate: Gate signal :type gate: Iterable or float :param duration: Transition time between two notes, in seconds :type duration: Iterable or float :return: Yields the modified frequency :rtype: float """ curr_freq = 440.0 target_freq = 440.0 step = 0.0 prev_gate = 0 for f, g, d in gen_iter(freq, gate, duration): if prev_gate == 0.0 and g > 0.0: curr_freq = f target_freq = f step = 0.0 if g > 0: if f != target_freq: # Starting new slide target_freq = f num_steps = max(d * sample_rate, 1) step = (target_freq - curr_freq) / num_steps elif curr_freq != target_freq: # Continuing slide if step > 0: curr_freq = min(curr_freq + step, target_freq) elif step < 0: # Continuing downwards slide curr_freq = max(curr_freq + step, target_freq) prev_gate = g yield curr_freq def random()-
random() -> x in the interval [0, 1).
def reverb_church(signal: Union[Iterable, float])-
Create a generator with a prefab "church reverb" effect. Nothing fancy, feel free to improve!
Sounds currently a bit too echo-y, particularly for percussive instruments. Works rather well for floating soundscapes, though.
:param signal: Signal to add reverb to :type signal: Iterable or float :return: Yields signal with reverb effect :rtype: float
Expand source code
def reverb_church(signal: Union[Iterable, float]): """Create a generator with a prefab "church reverb" effect. Nothing fancy, feel free to improve! Sounds currently a bit too echo-y, particularly for percussive instruments. Works rather well for floating soundscapes, though. :param signal: Signal to add reverb to :type signal: Iterable or float :return: Yields signal with reverb effect :rtype: float """ # use double the delay everywhere and mixin = 0.3 for church-like reverb d1 = delay(signal, 0.1, 0.3, 0.2) d2 = delay(d1, 0.16, 0.3, 0.2) d3 = delay(d2, 0.2, 0.3, 0.2) d31, d32 = split(d3, 2) d3f = low_pass_filter(d31, alpha=0.85) d4 = delay(d3f, 0.23, 0.3, 0.2) d5 = delay(d4, 0.3, 0.3, 0.2) d5f = low_pass_filter(d5, alpha=0.6) m = mix([d5f, d32], [1.0, 0.2]) yield from m def reverb_hall(signal: Union[Iterable, float])-
Create a generator with a prefab "hall reverb" effect. Nothing fancy, feel free to improve!
:param signal: Signal to add reverb to :type signal: Iterable or float :return: Yields signal with reverb effect :rtype: float
Expand source code
def reverb_hall(signal: Union[Iterable, float]): """Create a generator with a prefab "hall reverb" effect. Nothing fancy, feel free to improve! :param signal: Signal to add reverb to :type signal: Iterable or float :return: Yields signal with reverb effect :rtype: float """ # use double the delay everywhere and mixin = 0.3 for church-like reverb d1 = delay(signal, 0.05, 0.3, 0.2) d2 = delay(d1, 0.08, 0.2, 0.2) d3 = delay(d2, 0.1, 0.2, 0.2) d31, d32 = split(d3, 2) d3f = low_pass_filter(d31, alpha=0.85) d4 = delay(d3f, 0.115, 0.28, 0.2) d5 = delay(d4, 0.15, 0.15, 0.2) d5f = low_pass_filter(d5, alpha=0.6) m = mix([d5f, d32], [1.0, 0.4]) yield from m def reverb_room(signal: Union[Iterable, float])-
Create a generator with a prefab "room reverb" effect. Nothing fancy, feel free to improve!
:param signal: Signal to add reverb to :type signal: Iterable or float :return: Yields signal with reverb effect :rtype: float
Expand source code
def reverb_room(signal: Union[Iterable, float]): """Create a generator with a prefab "room reverb" effect. Nothing fancy, feel free to improve! :param signal: Signal to add reverb to :type signal: Iterable or float :return: Yields signal with reverb effect :rtype: float """ # use double the delay everywhere and mixin = 0.3 for church-like reverb d1 = delay(signal, 0.05, 0.3, 0.2) d2 = delay(d1, 0.08, 0.2, 0.2) yield from d2 def reverb_todo(signal: Union[Iterable, float], spread: Union[Iterable, float] = 0.5, mixin: Union[Iterable, float] = 0.3, feedback: Union[Iterable, int] = 0.2, room_hall: str = 'room')-
TODO: This should become a reverb that can be easily modified by turning one "time" and one "intensity" knob
Expand source code
def reverb_todo(signal: Union[Iterable, float], spread: Union[Iterable, float] = 0.5, # 0.5 = default delay lengths mixin: Union[Iterable, float] = 0.3, # mixin % of total signal (>0.5 is higher than original signal. For 1.0, nothing of the original signal will be left -- can be used for phase shifting) feedback: Union[Iterable, int] = 0.2, # feedback mixed back in room_hall: str = "room", ): """TODO: This should become a reverb that can be easily modified by turning one "time" and one "intensity" knob """ s1, s2, s3, s4 = split(signal, 4) d1 = delay(signal, mult(spread, 0.05 / 0.5), mixin, feedback) d2 = delay(d1, mult(spread, 0.08 / 0.5), mixin, feedback) if room_hall.lower() == "room": yield from d2 elif room_hall.lower() == "hall": d3 = delay(d2, mult(spread, 0.1 / 0.5), mixin, feedback) d31, d32 = split(d3, 2) d3f = low_pass_filter(d31, alpha=0.85) d4 = delay(d3f, mult(spread, 0.115 / 0.5), mixin, feedback) d5 = delay(d4, mult(spread, 0.15 / 0.5), mixin, feedback) d5f = low_pass_filter(d5, alpha=0.6) m = mix([d5f, d2], [0.8, 0.2]) yield from m def run_and_reload_on_change(func:) -
Execute a function and monitor the executed Python script for file changes. This is completely optional and just to make exploration easier and faster.
If the script file changes, the complete script will be re-run with the original parameters.
:param func: function to execute :type func: callable
Expand source code
def run_and_reload_on_change(func: callable): """Execute a function and monitor the executed Python script for file changes. This is completely optional and just to make exploration easier and faster. If the script file changes, the complete script will be re-run with the original parameters. :param func: function to execute :type func: callable """ thread = threading.Thread(target=func) thread.start() mod_time = os.path.getmtime(sys.argv[0]) while True: # Check if the script file has been modified if mod_time != os.path.getmtime(sys.argv[0]): # If the file has been modified, stop the thread and restart the script while thread.is_alive(): print("Signalling audio stream to stop...") stop_stream() sleep(0.3) print("Restarting...") os.execv(sys.executable, [sys.executable] + sys.argv) else: # If the file has not been modified, sleep for a while sleep(0.5) def sawtooth(f: Union[Iterable, float])-
Create an oscillator node generating a sawtooth wave for the given frequency
f::. . | \ | \ | \ | \ \ | \ | | |
Sawtooth waves have a very raspy sound. Many harmonics to go around with and play with filters. If you like 8-bit game music, this oscillator will be important.
:param f: Frequency in Hz :type f: Iterable (generator) or constant float :return: Yields samples between -1.0 and 1.0 :rtype: float.
Expand source code
def sawtooth(f: Union[Iterable, float]): """Create an oscillator node generating a sawtooth wave for the given frequency `f`:: . . | \\ | \\ | \\ | \\ \\ | \\ | \\| \\| Sawtooth waves have a very raspy sound. Many harmonics to go around with and play with filters. If you like 8-bit game music, this oscillator will be important. :param f: Frequency in Hz :type f: Iterable (generator) or constant float :return: Yields samples between -1.0 and 1.0 :rtype: float. """ amplitude = 1.0 amp_step = 0.0 for f_val in gen_iter(f): duration = round(sample_rate / f_val) amp_step = copysign(2.0 / duration, amp_step) if amplitude == -1.0: amplitude = 1.0 else: amplitude = max(amplitude - amp_step, -1.0) yield amplitude def scale(signal: Union[Iterable, float], target_min: Union[Iterable, float] = -1.0, target_max: Union[Iterable, float] = 1.0, source_min: Union[Iterable, float] = -1.0, source_max: Union[Iterable, float] = 1.0)-
Creates a generator that linearly resizes a
signalbetween two values.Please note that this LERP transformation is linear and unbounded. To limit it to not exceed
target_minandtarget_max, please combine it with theconstrain()generator function.The parameters
target_minandtarget_maxcome first so that scaling normal [-1.0, 1.0] signals does not require much typing::osc = square(440) scaled = scale(osc, 0.0, 1.0) # create a DC square wave signal
:param signal: Signal to be scaled :type signal: Iterable or float :param target_min: New lower bound (default -1.0) :type target_min: Iterable or float :param target_max: New upper bound (default 1.0) :type target_max: Iterable or float :param source_min: Original lower bound (default -1.0) :type source_min: Iterable or float :param source_max: Original upper bound (default 1.0) :type source_max: Iterable or float :return: Yield the scaled signal :rtype: float
Expand source code
def scale( signal: Union[Iterable, float], target_min: Union[Iterable, float] = -1.0, target_max: Union[Iterable, float] = 1.0, source_min: Union[Iterable, float] = -1.0, source_max: Union[Iterable, float] = 1.0 ): """Creates a generator that linearly resizes a `signal` between two values. Please note that this LERP transformation is linear and unbounded. To limit it to not exceed `target_min` and `target_max`, please combine it with the `constrain` generator function. The parameters `target_min` and `target_max` come first so that scaling normal [-1.0, 1.0] signals does not require much typing:: osc = square(440) scaled = scale(osc, 0.0, 1.0) # create a DC square wave signal :param signal: Signal to be scaled :type signal: Iterable or float :param target_min: New lower bound (default -1.0) :type target_min: Iterable or float :param target_max: New upper bound (default 1.0) :type target_max: Iterable or float :param source_min: Original lower bound (default -1.0) :type source_min: Iterable or float :param source_max: Original upper bound (default 1.0) :type source_max: Iterable or float :return: Yield the scaled signal :rtype: float """ for sig, t_min, t_max, s_min, s_max in gen_iter(signal, target_min, target_max, source_min, source_max): scale = (t_max - t_min) / (s_max - s_min) yield t_min + (sig - s_min) * scale def sine(f: Union[Iterable, float] = 440)-
Create an oscillator node generating a sine wave for the given frequency
f::– / \ \ / –
Sine waves have no harmonic overtones. They are the vanilla flavour of oscillators.
:param f: Frequency in Hz :type f: Iterable (generator) or constant float :return: Yields samples between -1.0 and 1.0 :rtype: float.
Expand source code
def sine(f: Union[Iterable, float] = 440): """Create an oscillator node generating a sine wave for the given frequency `f`:: -- / \\ \\ / -- Sine waves have no harmonic overtones. They are the vanilla flavour of oscillators. :param f: Frequency in Hz :type f: Iterable (generator) or constant float :return: Yields samples between -1.0 and 1.0 :rtype: float. """ # print(f"sine-init: {threading.active_count(), threading.current_thread()}") wave = 2 * pi step = 0 f_prev = -1 prev_duration = None duration_in_samples = 0 for f_val in gen_iter(f): # print(f"sine_loop: {threading.active_count(), threading.current_thread()}") if f_val != f_prev and f_val > 0: duration_in_samples = round(sample_rate / f_val) if prev_duration is not None: step = round(step / prev_duration * duration_in_samples) yield sin(f_val * wave * step / sample_rate) step += 1 if step >= duration_in_samples: step = 0 f_prev = f_val prev_duration = duration_in_samples def split(signal: Union[Iterable, float], n: int)-
Creates
ngenerators from one singlesignal.Necessary if the same signal has to be used more than once, because e.g. using an oscillator in two places would consume it at double speed, increasing its pitch by an octave.
Particularly useful for using control signals in several places (e.g. multiple oscillators with
freq, or using gate signals inenvelope_generator()and inportamento()). But also commonly used in effects likereverb.Be advised that if the n generators are not consumed evenly, the size of the required buffer can grow extremely large.
Example 1::
freq1, freq2, freq3 = split(orig_freq, 3)
Example 2::
detuned_saws = [sawtooth(transpose(f, random()-0.5)) for f in split(orig_freq, 10)] output = mix(detuned_saws)
:param signal: Signal to split up into n signals :type signal: Iterable or float :param n: Number of signals to create :type n: int :return: Returns a tuple with
nseparately usable copies of the original signal :rtype: tuple of Iterables or floatsExpand source code
def split(signal: Union[Iterable, float], n: int): """Creates `n` generators from one single `signal`. Necessary if the same signal has to be used more than once, because e.g. using an oscillator in two places would consume it at double speed, increasing its pitch by an octave. Particularly useful for using control signals in several places (e.g. multiple oscillators with `freq`, or using gate signals in `eg` and in `portamento`). But also commonly used in effects like `reverb`. Be advised that if the n generators are not consumed evenly, the size of the required buffer can grow extremely large. Example 1:: freq1, freq2, freq3 = split(orig_freq, 3) Example 2:: detuned_saws = [sawtooth(transpose(f, random()-0.5)) for f in split(orig_freq, 10)] output = mix(detuned_saws) :param signal: Signal to split up into n signals :type signal: Iterable or float :param n: Number of signals to create :type n: int :return: Returns a tuple with `n` separately usable copies of the original signal :rtype: tuple of Iterables or floats """ return tuple(tee(ensure_iterable(signal), n)) def square(f: Union[Iterable, float])-
Create an oscillator node generating a square wave for the given frequency
f::------ ------- | | | | | | ------
Square waves possess a theoretically infinite cascade of harmonics. They literally have some edge to them.
:param f: Frequency in Hz :type f: Iterable (generator) or constant float :return: Yields samples between -1.0 and 1.0 :rtype: float.
Expand source code
def square(f: Union[Iterable, float]): """Create an oscillator node generating a square wave for the given frequency `f`:: ------ ------- | | | | | | ------ Square waves possess a theoretically infinite cascade of harmonics. They literally have some edge to them. :param f: Frequency in Hz :type f: Iterable (generator) or constant float :return: Yields samples between -1.0 and 1.0 :rtype: float. """ step = 0 amplitude = 1.0 for f_val in gen_iter(f): half_duration = round(sample_rate / f_val / 2) if step > half_duration: amplitude *= -1.0 step = 0 else: step += 1 yield amplitude def static_generator(signal)-
Wrap a value in a generator which returns the same value over and over.
Expand source code
def static_generator(signal): """Wrap a value in a generator which returns the same value over and over.""" while True: yield signal def stop_stream()-
Signal the audio player to stop playing
Expand source code
def stop_stream(): """Signal the audio player to stop playing""" global stop stop = True print("Stopping") def sub(signal: Union[Iterable, float], other_val: Union[Iterable, float])-
Create a generator that subtracts one signal from another.
This helper exists to give you greater flexibility when creating your patches, so there's no need to define custom generators for every little unsupported operation.
:param signal: Signal whose values to modify :type signal: Iterable or float :param other_val: Other value of the arithmetic operation :type other_val: Iterable or float :return: Yield value-by-value output of
signalminusother_val:rtype: floatExpand source code
def sub(signal: Union[Iterable, float], other_val: Union[Iterable, float]): """Create a generator that subtracts one signal from another. This helper exists to give you greater flexibility when creating your patches, so there's no need to define custom generators for every little unsupported operation. :param signal: Signal whose values to modify :type signal: Iterable or float :param other_val: Other value of the arithmetic operation :type other_val: Iterable or float :return: Yield value-by-value output of `signal` minus `other_val` :rtype: float """ for s, v in gen_iter(signal, other_val): yield s - v def transpose(freq: Union[Iterable, float], semitones: Union[Iterable, float] = 0.0)-
Create a generator that transposes a frequency
freqby a given number ofsemitones.:param freq: Frequency signal (one frequency value, like 440.0, or an Iterable/node providing frequency values) :type freq: Iterable or float :param semitones: Semitones (>0.0 for transposing up, <0.0 for transposing down), non-integers allowed. :type semitones: Iterable or float :return: Yields the transposed frequency value :rtype: float
Expand source code
def transpose(freq: Union[Iterable, float], semitones: Union[Iterable, float] = 0.0): """Create a generator that transposes a frequency `freq` by a given number of `semitones`. :param freq: Frequency signal (one frequency value, like 440.0, or an Iterable/node providing frequency values) :type freq: Iterable or float :param semitones: Semitones (>0.0 for transposing up, <0.0 for transposing down), non-integers allowed. :type semitones: Iterable or float :return: Yields the transposed frequency value :rtype: float """ if temperament != TWELVE_EQUAL: raise NotImplementedError("Only 12 Equal Temperament is supported") prev_s = None prev_f = None factor = None transposed_f = None for f, s in gen_iter(freq, semitones): if s != prev_s: factor = transpose_factor_12eq(s) transposed_f = f * factor if f != prev_f: transposed_f = f * factor yield transposed_f prev_f = f prev_s = s def transpose_factor_12eq(s: float)-
Get the transposition factor to multiply with any given frequency (equal temperament).
:param s: Semitones to transpose up (positive s) or down (negative s). Need not be integer. :type s: float :return: Transposition factor :rtype: float
Expand source code
def transpose_factor_12eq(s: float): """Get the transposition factor to multiply with any given frequency (equal temperament). :param s: Semitones to transpose up (positive s) or down (negative s). Need not be integer. :type s: float :return: Transposition factor :rtype: float """ return 2 ** (s / 12) def triangle(f: Union[Iterable, float])-
Create an oscillator node generating a triangle wave for the given frequency
f::/\ /\ / \ / \ \ / \ / \/ \/
Triangle waves are pretty close sine waves, but possess a natural quality which is useful when attempting to mimic woodwind instruments or xylophones.
:param f: Frequency in Hz :type f: Iterable (generator) or constant float :return: Yields samples between -1.0 and 1.0 :rtype: float.
Expand source code
def triangle(f: Union[Iterable, float]): """Create an oscillator node generating a triangle wave for the given frequency `f`:: /\\ /\\ / \\ / \\ \\ / \\ / \\/ \\/ Triangle waves are pretty close sine waves, but possess a natural quality which is useful when attempting to mimic woodwind instruments or xylophones. :param f: Frequency in Hz :type f: Iterable (generator) or constant float :return: Yields samples between -1.0 and 1.0 :rtype: float. """ amplitude = 0.0 amp_step = 0.0 for f_val in gen_iter(f): duration = round(sample_rate / f_val) amp_step = copysign(2.0 / duration, amp_step) if amplitude >= 1.0 or amplitude == -1.0: amp_step = -amp_step if amp_step > 0: amplitude = min(amplitude + amp_step, 1.0) else: amplitude = max(amplitude + amp_step, -1.0) yield amplitude