Source code for sound.envelope

from typing import Union
import math

from . import SAMPLE_RATE
from .signal import ConstantSignal, Signal

__all__ = ('Envelope', 'ADSR', 'Decay', 'Line', 'envelope')

[docs] class Envelope(Signal): """ An envelope. This signal should not be a periodic waveform, but rather a broad shape. You can use this base class to specify a square pulse for the given duration, in seconds. """ def __init__(self, duration): self.duration = duration * SAMPLE_RATE self.pure = True def amplitude(self, frame: int) -> float: if frame < 0: return 0. if frame < self.duration: return 1. return 0.
[docs] def apply_adsr(self, attack, decay, release, attack_level=1, sustain_level=0.5): """ Return the current envelope with an additional ADSR component. Parameters are the same as for the ADSR class initializer. """ sub = 1 if type(self) is Envelope else self return sub * ADSR(attack, decay, self.duration / SAMPLE_RATE - attack - decay - release, release, attack_level, sustain_level)
[docs] def apply_decay(self, decay_param): """ Add an exponential decay to the current envelope. :param decay_param: The speed of the decay, between 0 and 1. Lower values decay faster. """ decay = Decay(0, self.duration / SAMPLE_RATE, 0, decay_param) if type(self) is Envelope: return decay else: result = self * decay assert not isinstance(result, ConstantSignal) return result
[docs] class ADSR(Envelope): """ An envelope with portions for Attack[1], Decay[2], Sustain[3], and Release[4]. You can also specify the attack level[5] and the sustain level[6]. Timing parameters are in seconds, level parameters are between 0 and 1. :: /\\ [5] / \\______________ [6] / \\ / \\ [1] [2] [3] [4] """ def __init__(self, attack, decay, sustain, release, attack_level: Union[int, float]=1, sustain_level=0.5): super(ADSR, self).__init__(attack + decay + sustain + release) self.attack = attack * SAMPLE_RATE self.decay = decay * SAMPLE_RATE self.sustain = sustain * SAMPLE_RATE self.release = release * SAMPLE_RATE self.attack_level = float(attack_level) self.sustain_level = float(sustain_level) def amplitude(self, frame): if frame < 0: return 0. if frame < self.attack: return float(frame) / self.attack * self.attack_level frame -= self.attack if frame < self.decay: return float(frame) / self.decay * -(self.attack_level - self.sustain_level) + self.attack_level frame -= self.decay if frame < self.sustain: return self.sustain_level frame -= self.sustain if frame < self.release: return float(frame) / self.release * -self.sustain_level + self.sustain_level return 0.
[docs] class Decay(Envelope): """ An enevelope with an attack[1], an exponential decay[2], and a release[3]. You can specify the decay parameter[4] (between 0 and 1, smaller decays faster, and the height of the attack peak[5]. Times are given in seconds. :: /*. [5] / *._ / **-..___ f = [4]^x / \\ [1] [2] [3] """ def __init__(self, attack, sustain, release, decay_param=0.5, attack_level=1.0): super(Decay, self).__init__(attack + sustain + release) self.attack = attack * SAMPLE_RATE self.sustain = sustain * SAMPLE_RATE self.release = release * SAMPLE_RATE self.decay_param = float(decay_param) self.attack_level = float(attack_level) self.release_level = self.decay_param ** (float(self.attack + self.sustain) / SAMPLE_RATE) * self.attack_level def amplitude(self, frame): if frame < 0: return 0 if frame < self.attack: return float(frame) / self.attack * self.attack_level frame -= self.attack if frame < self.sustain: return self.decay_param ** (float(frame) / SAMPLE_RATE) * self.attack_level frame -= self.sustain if frame < self.release: return float(frame) / self.release * -self.release_level + self.release_level return 0
[docs] def envelope(sustain=None, attack=0.01, decay=None, release=None, attack_level: Union[int, float]=1, sustain_level=0.7, decaying_sustain=True): """ A convenience method to produce an envelope for a musical instrument note. All parameters are optional and will pick sane defaults if not provided. :param sustain: The length of the note (seconds) :param atack: The length of the note's attack (seconds) :param decay: if decaying_sustain: the exponential decay parameter. else: the length of the note's decay (seconds) :param attack_level: The intensity of the initial note attack :param sustain_level: Only if not decaying_sustain, the intensity of the note's sustained body :param decaying_sustain: Whether the note's sustaining period should involve exponential decay """ if decaying_sustain: if decay is None: decay = 0.5 if release is None: release = 0.1 if sustain is None: sustain = math.log(0.001, decay) return Decay(attack, sustain, release, decay, attack_level=attack_level) else: if decay is None: decay = 0.1 if release is None: release = 0.3 if sustain is None: sustain = 1 return ADSR(attack, decay, sustain, release, attack_level, sustain_level)
[docs] class Line(Envelope): """ A linear line running from start[1] to end[2] in a given time[3] in seconds. :: *--__ [1] *--__ *--__ *--__ [2] [3] """ def __init__(self, start, end, duration): super(Line, self).__init__(duration) self.start = start self.end = end def amplitude(self, frame): #if frame == 0: #print 'line starting!', self.start, self.end, self.duration/SAMPLE_RATE #if self.duration == 5 * SAMPLE_RATE: # print 'AAAA', frame # import ipdb; ipdb.set_trace() if frame < 0: return self.start if frame >= self.duration: return self.end return (float(frame) / self.duration) * (self.end - self.start) + self.start