Nikolay-Lysenko/sinethesizer

View on GitHub
sinethesizer/envelopes/ahdsr.py

Summary

Maintainability
A
3 hrs
Test Coverage
"""
Define AHDSR (Attack-Hold-Decay-Sustain-Release) envelopes.

Author: Nikolay Lysenko
"""


from math import ceil, floor

import numpy as np


def create_generic_ahdsr_envelope(
        event: 'sinethesizer.synth.core.Event',
        attack_to_ahds_max_ratio: float = 0.2,
        max_attack_duration: float = 0.2,
        attack_degree: float = 1.0,
        hold_to_hds_max_ratio: float = 0.05,
        max_hold_duration: float = 0.05,
        decay_to_ds_max_ratio: float = 0.25,
        max_decay_duration: float = 0.4,
        decay_degree: float = 1.0,
        sustain_level: float = 0.6,
        max_sustain_duration: float = 10.0,
        max_release_duration: float = 0.5,
        release_duration_on_velocity_order: float = 0.0,
        release_degree: float = 1.0,
        peak_value: float = 1.0,
        ratio_at_zero_velocity: float = 0.0,
        envelope_values_on_velocity_order: float = 0.0
) -> np.ndarray:
    """
    Create AHDSR envelope of shape that depends on numerous parameters.

    :param event:
        parameters of sound event for which this function is called;
        this argument provides information about duration, frame rate, and velocity
    :param attack_to_ahds_max_ratio:
        maximum fraction of frames with attack amongst frames with attack,
        hold, decay, and sustain
    :param max_attack_duration:
        maximum duration of attack in seconds
    :param attack_degree:
        degree of attack dynamic; if it is 1, attack is linear; if it is
        greater than 1, attack is concave; if it is less than 1, attack is convex
    :param hold_to_hds_max_ratio:
        maximum fraction of frames with hold amongst frames with hold, decay,
        and sustain
    :param max_hold_duration:
        maximum duration of hold in seconds
    :param decay_to_ds_max_ratio:
        maximum fraction of frames with decay amongst frames with decay and sustain
    :param max_decay_duration:
        maximum duration of decay in seconds
    :param decay_degree:
        degree of decay dynamic; if it is 1, decay is linear; if it is
        greater than 1, decay is concave; if it is less than 1, decay is convex
    :param sustain_level:
        amplitude level at sustain stage; a float between 0 and 1 where 1 is
        the peak level (i.e., level at the end of attack)
    :param max_sustain_duration:
        maximum duration of sustain in seconds
    :param max_release_duration:
        maximum duration of release in seconds
    :param release_duration_on_velocity_order:
        coefficient that determines actual duration of release depending on
        velocity; given non-maximum positive velocity, the higher it is,
        the shorter release is; if it is 0, velocity does not affect
        release duration
    :param release_degree:
        degree of release dynamic; if it is 1, release is linear; if it is
        greater than 1, release is concave; if it is less than 1, release is convex
    :param peak_value:
        peak envelope value given maximum velocity; usually, this argument
        should be passed only if output envelope is used as modulation index envelope
        or if the function is called by envelope shaper
    :param ratio_at_zero_velocity:
        ratio of envelope values at zero velocity to envelope values at maximum
        velocity; usually, this argument should be passed only if output
        envelope is used as modulation index envelope or if the function is called by
        envelope shaper
    :param envelope_values_on_velocity_order:
        coefficient that determines dependence of envelope values on velocity;
        given non-maximum positive velocity, the higher it is, the lower
        envelope values are; if it is 0, velocity does not affect envelope;
        usually, this argument should be passed only if output envelope is
        used as modulation index envelope or if the function is called by envelope shaper
    :return:
        envelope
    """
    frame_rate = event.frame_rate
    remaining_duration_in_frames = ceil(event.duration * frame_rate)

    n_frames_with_attack = min(
        floor(attack_to_ahds_max_ratio * remaining_duration_in_frames),
        floor(max_attack_duration * frame_rate)
    )
    if n_frames_with_attack > 0:
        xs = np.linspace(1, 0, n_frames_with_attack)
        attack = 1 - xs ** attack_degree
    else:
        attack = np.array([])
    remaining_duration_in_frames -= len(attack)

    n_frames_with_hold = min(
        floor(hold_to_hds_max_ratio * remaining_duration_in_frames),
        floor(max_hold_duration * frame_rate)
    )
    hold = np.ones(n_frames_with_hold)
    remaining_duration_in_frames -= len(hold)

    n_frames_with_decay = min(
        floor(decay_to_ds_max_ratio * remaining_duration_in_frames),
        floor(max_decay_duration * frame_rate)
    )
    if n_frames_with_decay > 0:
        xs = np.linspace(0, 1, n_frames_with_decay)
        decay = 1 - (1 - sustain_level) * xs ** decay_degree
    else:
        decay = np.array([])
    remaining_duration_in_frames -= len(decay)

    n_frames_with_sustain = min(
        remaining_duration_in_frames,
        floor(max_sustain_duration * frame_rate)
    )
    if n_frames_with_sustain > 0:
        sustain = sustain_level * np.ones(n_frames_with_sustain)
    else:
        sustain = np.array([])

    release_ratio = event.velocity ** release_duration_on_velocity_order
    release_duration = release_ratio * max_release_duration
    n_frames_with_release = floor(release_duration * frame_rate)
    if n_frames_with_release > 0:
        xs = np.linspace(0, 1, n_frames_with_release)
        release = sustain_level * (1 - xs ** release_degree)
    else:
        release = np.array([])

    envelope = np.concatenate((attack, hold, decay, sustain, release))
    envelope *= peak_value
    coef = event.velocity ** envelope_values_on_velocity_order
    envelope *= ratio_at_zero_velocity + coef * (1 - ratio_at_zero_velocity)
    return envelope


def create_relative_ahdsr_envelope(
        event: 'sinethesizer.synth.core.Event',
        attack_to_ahds_ratio: float = 0.2,
        attack_degree: float = 1.0,
        hold_to_ahds_ratio: float = 0.05,
        decay_to_ahds_ratio: float = 0.25,
        decay_degree: float = 1.0,
        sustain_level: float = 0.6,
        max_release_duration: float = 0.5,
        release_duration_on_velocity_order: float = 0.0,
        release_degree: float = 1.0,
        peak_value: float = 1.0,
        ratio_at_zero_velocity: float = 0.0,
        envelope_values_on_velocity_order: float = 0.0
) -> np.ndarray:
    """
    Create AHDSR envelope with proportional durations of stages.

    :param event:
        parameters of sound event for which this function is called;
        this argument provides information about duration, frame rate, and velocity
    :param attack_to_ahds_ratio:
        fraction of frames with attack amongst frames with attack, hold,
        decay, and sustain
    :param attack_degree:
        degree of attack dynamic; if it is 1, attack is linear; if it is
        greater than 1, attack is concave; if it is less than 1, attack is convex
    :param hold_to_ahds_ratio:
        fraction of frames with hold amongst frames with attack, hold,
        decay, and sustain
    :param decay_to_ahds_ratio:
        fraction of frames with decay amongst frames with attack, hold,
        decay, and sustain
    :param decay_degree:
        degree of decay dynamic; if it is 1, decay is linear; if it is
        greater than 1, decay is concave; if it is less than 1, decay is convex
    :param sustain_level:
        amplitude level at sustain stage; a float between 0 and 1 where 1 is
        the peak level (i.e., level at the end of attack)
    :param max_release_duration:
        maximum duration of release in seconds
    :param release_duration_on_velocity_order:
        coefficient that determines actual duration of release depending on
        velocity; given non-maximum positive velocity, the higher it is,
        the shorter release is; if it is 0, velocity does not affect
        release duration
    :param release_degree:
        degree of release dynamic; if it is 1, release is linear; if it is
        greater than 1, release is concave; if it is less than 1, release is convex
    :param peak_value:
        peak envelope value given maximum velocity; usually, this argument
        should be passed only if output envelope is used as modulation index envelope
        or if the function is called by envelope shaper
    :param ratio_at_zero_velocity:
        ratio of envelope values at zero velocity to envelope values at maximum
        velocity; usually, this argument should be passed only if output
        envelope is used as modulation index envelope or if the function is called by
        envelope shaper
    :param envelope_values_on_velocity_order:
        coefficient that determines dependence of envelope values on velocity;
        given non-maximum positive velocity, the higher it is, the lower
        envelope values are; if it is 0, velocity does not affect envelope;
        usually, this argument should be passed only if output envelope is
        used as modulation index envelope or if the function is called by envelope shaper
    :return:
        envelope
    """
    frame_rate = event.frame_rate
    ahds_duration_in_frames = ceil(event.duration * frame_rate)

    n_frames_with_attack = floor(attack_to_ahds_ratio * ahds_duration_in_frames)
    if n_frames_with_attack > 0:
        xs = np.linspace(1, 0, n_frames_with_attack)
        attack = 1 - xs ** attack_degree
    else:
        attack = np.array([])

    n_frames_with_hold = floor(hold_to_ahds_ratio * ahds_duration_in_frames)
    hold = np.ones(n_frames_with_hold)

    n_frames_with_decay = floor(
        decay_to_ahds_ratio * ahds_duration_in_frames
    )
    if n_frames_with_decay > 0:
        xs = np.linspace(0, 1, n_frames_with_decay)
        decay = 1 - (1 - sustain_level) * xs ** decay_degree
    else:
        decay = np.array([])

    n_frames_with_sustain = ahds_duration_in_frames - len(attack) - len(hold) - len(decay)
    if n_frames_with_sustain > 0:
        sustain = sustain_level * np.ones(n_frames_with_sustain)
    else:
        sustain = np.array([])

    release_ratio = event.velocity ** release_duration_on_velocity_order
    release_duration = release_ratio * max_release_duration
    n_frames_with_release = floor(release_duration * frame_rate)
    if n_frames_with_release > 0:
        xs = np.linspace(0, 1, n_frames_with_release)
        release = sustain_level * (1 - xs ** release_degree)
    else:
        release = np.array([])

    envelope = np.concatenate((attack, hold, decay, sustain, release))
    envelope *= peak_value
    coef = event.velocity ** envelope_values_on_velocity_order
    envelope *= ratio_at_zero_velocity + coef * (1 - ratio_at_zero_velocity)
    return envelope


def create_trapezoid_envelope(
        event: 'sinethesizer.synth.core.Event',
        attack_share: float = 0.2,
        attack_degree: float = 1.0,
        decay_share: float = 0.1,
        decay_degree: float = 1.0,
        peak_value: float = 1.0,
        ratio_at_zero_velocity: float = 0.0,
        envelope_values_on_velocity_order: float = 0.0
) -> np.ndarray:
    """
    Create AHD envelope (so called trapezoid).

    :param event:
        parameters of sound event for which this function is called;
        this argument provides information about duration, frame rate, and velocity
    :param attack_share:
        fraction of frames with attack amongst all frames
    :param attack_degree:
        degree of attack dynamic; if it is 1, attack is linear; if it is
        greater than 1, attack is concave; if it is less than 1, attack is convex
    :param decay_share:
        fraction of frames with decay amongst all frames
    :param decay_degree:
        degree of decay dynamic; if it is 1, decay is linear; if it is
        greater than 1, decay is concave; if it is less than 1, decay is convex
    :param peak_value:
        peak envelope value given maximum velocity; usually, this argument
        should be passed only if output envelope is used as modulation index envelope
        or if the function is called by envelope shaper
    :param ratio_at_zero_velocity:
        ratio of envelope values at zero velocity to envelope values at maximum
        velocity; usually, this argument should be passed only if output
        envelope is used as modulation index envelope or if the function is called by
        envelope shaper
    :param envelope_values_on_velocity_order:
        coefficient that determines dependence of envelope values on velocity;
        given non-maximum positive velocity, the higher it is, the lower
        envelope values are; if it is 0, velocity does not affect envelope;
        usually, this argument should be passed only if output envelope is
        used as modulation index envelope or if the function is called by envelope shaper
    :return:
        envelope
    """
    envelope = create_relative_ahdsr_envelope(
        event,
        attack_to_ahds_ratio=attack_share,
        attack_degree=attack_degree,
        hold_to_ahds_ratio=max(1 - attack_share - decay_share, 0),
        decay_to_ahds_ratio=decay_share,
        decay_degree=decay_degree,
        sustain_level=0,
        max_release_duration=0,
        peak_value=peak_value,
        ratio_at_zero_velocity=ratio_at_zero_velocity,
        envelope_values_on_velocity_order=envelope_values_on_velocity_order
    )
    return envelope