AllenDowney / ThinkDSP

Think DSP: Digital Signal Processing in Python, by Allen B. Downey.
https://allendowney.github.io/ThinkDSP/
3.93k stars 3.2k forks source link

Suggestion for ADSR (and other) envelopes #97

Open dfkettle opened 4 months ago

dfkettle commented 4 months ago

I'm still working my way through your book, so maybe I missed it, but looking through the source code for ThinkDSP, I couldn't find any way to apply an ADSR envelope to a 'Wave' object. So I wrote this as a starting point. It could probably be improved (there isn't much validation being done), but if you think it might be useful for musical applications, feel free to include it.

Two classes are defined, Envelope, and a sub-class, ADSR. The envelope class could be used for evelopes that don't fit the classic ADSR pattern. Instances of either class can then be multiplied with a Wave object, returning another Wave object.

For example:

import thinkdsp
import envelope

cos_sig = thinkdsp.CosSignal(freq=440, amp=1.0, offset=0)
wave = cos_sig.make_wave(duration=1.0, start=0, framerate=11025)
env = envelope.ADSR([1000,500,0.5,4000],11025)
new = env * wave
new.play()

Here's the source code:

"""@author: David Kettle.

Copyright 2024 David Kettle
License: MIT License (https://opensource.org/licenses/MIT)
"""

from thinkdsp import Wave
import numpy as np

class Envelope(Wave):
    """Represents an amplitude envelope."""

    def __init__(self, ys):
        """Initialize the envelope.

        ys: array of amplitudes
        """
        Wave.__init__(self,ys)

    def __mul__(self, other):
        """Multiply an envelope with a wave elementwise.

        other: Wave
        returns: new Wave
        """
        # the envelope and the wave have to have the same duration
        assert len(self) == len(other)

        ys = self.ys * other.ys
        return Wave(ys, other.ts, other.framerate)

    __rmul__ = __mul__

class ADSR(Envelope):
    """Represents an attack, decay, sustain and release envelope.

    adsr: array or tuple representing attack (# of frames), decay (# of frames),
          sustain (amplitude level) and release (# of frames)
    duration: total duration of envelope (# of frames)
    returns: new Wave
    """

    def __init__(self, adsr, duration):
        """Initialize the envelope."""
        assert len(adsr) == 4
        (att_dur,dec_dur,sus_lvl,rel_dur) = adsr
        assert sus_lvl >= 0 and sus_lvl <= 1
        sus_dur = duration - (att_dur + dec_dur + rel_dur)
        if sus_dur < 0:
            sus_dur = 0

        attack = np.linspace(0,1,int(att_dur))
        decay = np.linspace(1,sus_lvl,int(dec_dur))
        sustain = np.linspace(sus_lvl,sus_lvl,int(sus_dur))
        release = np.linspace(sus_lvl,0,int(rel_dur))

        ys = np.concatenate((attack,decay,sustain,release))[0:int(duration)]
        Envelope.__init__(self,ys)