Nikolay-Lysenko/sinethesizer

View on GitHub
sinethesizer/envelopes/misc.py

Summary

Maintainability
A
1 hr
Test Coverage
"""
Define additional envelopes.

Author: Nikolay Lysenko
"""


from math import ceil, floor
from typing import Optional

import numpy as np


def create_constant_envelope(
        event: 'sinethesizer.synth.core.Event', value: float
) -> np.ndarray:
    """
    Create constant envelope.

    :param event:
        parameters of sound event for which this function is called;
        this argument provides information about duration, frame rate, and velocity
    :param value:
        value of the envelope
    :return:
        constant envelope
    """
    return value * np.ones(ceil(event.duration * event.frame_rate))


def create_exponentially_decaying_envelope(
        event: 'sinethesizer.synth.core.Event',
        attack_to_ad_max_ratio: float = 0.025,
        max_attack_duration: float = 0.025,
        attack_degree: float = 1.0,
        decay_half_life: Optional[float] = None,
        decay_half_life_ratio: Optional[float] = 0.125,
        max_release_duration: float = 0.01,
        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 ADR (Attack-Decay-Release) envelope with exponential decay.

    :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_ad_max_ratio:
        maximum fraction of frames with attack amongst frames with attack and decay
    :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 decay_half_life:
        half-life of decay in seconds; if this argument is passed,
        `decay_half_life_ratio` is ignored
    :param decay_half_life_ratio:
        half-life of decay as ratio to decay duration; this argument is used
        only if `decay_half_life` is `None`
    :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
    ad_duration_in_frames = ceil(event.duration * frame_rate)

    n_frames_with_attack = min(
        floor(attack_to_ad_max_ratio * ad_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([])
    decay_duration_in_frames = ad_duration_in_frames - len(attack)

    if decay_half_life is not None:
        half_life_in_frames = decay_half_life * frame_rate
    else:
        half_life_in_frames = decay_half_life_ratio * decay_duration_in_frames
    xs = np.linspace(0, decay_duration_in_frames, decay_duration_in_frames)
    decay = 2 ** -(xs / half_life_in_frames)

    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 = decay[-1] * (1 - xs ** release_degree)
    else:
        release = np.array([])

    envelope = np.concatenate((attack, decay, 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