CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
effects.py342 linesDownload Raw Back to pydub
1import sys2import math3import array4from .utils import (5    db_to_float,6    ratio_to_db,7    register_pydub_effect,8    make_chunks,9    audioop,10    get_min_max_value11)12from .silence import split_on_silence13from .exceptions import TooManyMissingFrames, InvalidDuration14 15if sys.version_info >= (3, 0):16    xrange = range17 18 19@register_pydub_effect20def apply_mono_filter_to_each_channel(seg, filter_fn):21    n_channels = seg.channels22 23    channel_segs = seg.split_to_mono()24    channel_segs = [filter_fn(channel_seg) for channel_seg in channel_segs]25 26    out_data = seg.get_array_of_samples()27    for channel_i, channel_seg in enumerate(channel_segs):28        for sample_i, sample in enumerate(channel_seg.get_array_of_samples()):29            index = (sample_i * n_channels) + channel_i30            out_data[index] = sample31 32    return seg._spawn(out_data)33 34 35@register_pydub_effect36def normalize(seg, headroom=0.1):37    """38    headroom is how close to the maximum volume to boost the signal up to (specified in dB)39    """40    peak_sample_val = seg.max41    42    # if the max is 0, this audio segment is silent, and can't be normalized43    if peak_sample_val == 0:44        return seg45    46    target_peak = seg.max_possible_amplitude * db_to_float(-headroom)47 48    needed_boost = ratio_to_db(target_peak / peak_sample_val)49    return seg.apply_gain(needed_boost)50 51 52@register_pydub_effect53def speedup(seg, playback_speed=1.5, chunk_size=150, crossfade=25):54    # we will keep audio in 150ms chunks since one waveform at 20Hz is 50ms long55    # (20 Hz is the lowest frequency audible to humans)56 57    # portion of AUDIO TO KEEP. if playback speed is 1.25 we keep 80% (0.8) and58    # discard 20% (0.2)59    atk = 1.0 / playback_speed60 61    if playback_speed < 2.0:62        # throwing out more than half the audio - keep 50ms chunks63        ms_to_remove_per_chunk = int(chunk_size * (1 - atk) / atk)64    else:65        # throwing out less than half the audio - throw out 50ms chunks66        ms_to_remove_per_chunk = int(chunk_size)67        chunk_size = int(atk * chunk_size / (1 - atk))68 69    # the crossfade cannot be longer than the amount of audio we're removing70    crossfade = min(crossfade, ms_to_remove_per_chunk - 1)71 72    # DEBUG73    #print("chunk: {0}, rm: {1}".format(chunk_size, ms_to_remove_per_chunk))74 75    chunks = make_chunks(seg, chunk_size + ms_to_remove_per_chunk)76    if len(chunks) < 2:77        raise Exception("Could not speed up AudioSegment, it was too short {2:0.2f}s for the current settings:\n{0}ms chunks at {1:0.1f}x speedup".format(78            chunk_size, playback_speed, seg.duration_seconds))79 80    # we'll actually truncate a bit less than we calculated to make up for the81    # crossfade between chunks82    ms_to_remove_per_chunk -= crossfade83 84    # we don't want to truncate the last chunk since it is not guaranteed to be85    # the full chunk length86    last_chunk = chunks[-1]87    chunks = [chunk[:-ms_to_remove_per_chunk] for chunk in chunks[:-1]]88 89    out = chunks[0]90    for chunk in chunks[1:]:91        out = out.append(chunk, crossfade=crossfade)92 93    out += last_chunk94    return out95    96 97@register_pydub_effect98def strip_silence(seg, silence_len=1000, silence_thresh=-16, padding=100):99    if padding > silence_len:100        raise InvalidDuration("padding cannot be longer than silence_len")101 102    chunks = split_on_silence(seg, silence_len, silence_thresh, padding)103    crossfade = padding / 2104 105    if not len(chunks):106        return seg[0:0]107 108    seg = chunks[0]109    for chunk in chunks[1:]:110        seg = seg.append(chunk, crossfade=crossfade)111 112    return seg113 114 115@register_pydub_effect116def compress_dynamic_range(seg, threshold=-20.0, ratio=4.0, attack=5.0, release=50.0):117    """118    Keyword Arguments:119        120        threshold - default: -20.0121            Threshold in dBFS. default of -20.0 means -20dB relative to the122            maximum possible volume. 0dBFS is the maximum possible value so123            all values for this argument sould be negative.124 125        ratio - default: 4.0126            Compression ratio. Audio louder than the threshold will be 127            reduced to 1/ratio the volume. A ratio of 4.0 is equivalent to128            a setting of 4:1 in a pro-audio compressor like the Waves C1.129        130        attack - default: 5.0131            Attack in milliseconds. How long it should take for the compressor132            to kick in once the audio has exceeded the threshold.133 134        release - default: 50.0135            Release in milliseconds. How long it should take for the compressor136            to stop compressing after the audio has falled below the threshold.137 138    139    For an overview of Dynamic Range Compression, and more detailed explanation140    of the related terminology, see: 141 142        http://en.wikipedia.org/wiki/Dynamic_range_compression143    """144 145    thresh_rms = seg.max_possible_amplitude * db_to_float(threshold)146    147    look_frames = int(seg.frame_count(ms=attack))148    def rms_at(frame_i):149        return seg.get_sample_slice(frame_i - look_frames, frame_i).rms150    def db_over_threshold(rms):151        if rms == 0: return 0.0152        db = ratio_to_db(rms / thresh_rms)153        return max(db, 0)154 155    output = []156 157    # amount to reduce the volume of the audio by (in dB)158    attenuation = 0.0159    160    attack_frames = seg.frame_count(ms=attack)161    release_frames = seg.frame_count(ms=release)162    for i in xrange(int(seg.frame_count())):163        rms_now = rms_at(i)164        165        # with a ratio of 4.0 this means the volume will exceed the threshold by166        # 1/4 the amount (of dB) that it would otherwise167        max_attenuation = (1 - (1.0 / ratio)) * db_over_threshold(rms_now)168        169        attenuation_inc = max_attenuation / attack_frames170        attenuation_dec = max_attenuation / release_frames171        172        if rms_now > thresh_rms and attenuation <= max_attenuation:173            attenuation += attenuation_inc174            attenuation = min(attenuation, max_attenuation)175        else:176            attenuation -= attenuation_dec177            attenuation = max(attenuation, 0)178        179        frame = seg.get_frame(i)180        if attenuation != 0.0:181            frame = audioop.mul(frame,182                                seg.sample_width,183                                db_to_float(-attenuation))184        185        output.append(frame)186    187    return seg._spawn(data=b''.join(output))188 189 190# Invert the phase of the signal.191 192@register_pydub_effect193 194def invert_phase(seg, channels=(1, 1)):195    """196    channels- specifies which channel (left or right) to reverse the phase of.197    Note that mono AudioSegments will become stereo.198    """199    if channels == (1, 1):200        inverted = audioop.mul(seg._data, seg.sample_width, -1.0)  201        return seg._spawn(data=inverted)202    203    else:204        if seg.channels == 2:205            left, right = seg.split_to_mono()206        else:207            raise Exception("Can't implicitly convert an AudioSegment with " + str(seg.channels) + " channels to stereo.")208            209        if channels == (1, 0):    210            left = left.invert_phase()211        else:212            right = right.invert_phase()213        214        return seg.from_mono_audiosegments(left, right)215        216 217 218# High and low pass filters based on implementation found on Stack Overflow:219#   http://stackoverflow.com/questions/13882038/implementing-simple-high-and-low-pass-filters-in-c220 221@register_pydub_effect222def low_pass_filter(seg, cutoff):223    """224        cutoff - Frequency (in Hz) where higher frequency signal will begin to225            be reduced by 6dB per octave (doubling in frequency) above this point226    """227    RC = 1.0 / (cutoff * 2 * math.pi)228    dt = 1.0 / seg.frame_rate229 230    alpha = dt / (RC + dt)231    232    original = seg.get_array_of_samples()233    filteredArray = array.array(seg.array_type, original)234    235    frame_count = int(seg.frame_count())236 237    last_val = [0] * seg.channels238    for i in range(seg.channels):239        last_val[i] = filteredArray[i] = original[i]240 241    for i in range(1, frame_count):242        for j in range(seg.channels):243            offset = (i * seg.channels) + j244            last_val[j] = last_val[j] + (alpha * (original[offset] - last_val[j]))245            filteredArray[offset] = int(last_val[j])246 247    return seg._spawn(data=filteredArray)248 249 250@register_pydub_effect251def high_pass_filter(seg, cutoff):252    """253        cutoff - Frequency (in Hz) where lower frequency signal will begin to254            be reduced by 6dB per octave (doubling in frequency) below this point255    """256    RC = 1.0 / (cutoff * 2 * math.pi)257    dt = 1.0 / seg.frame_rate258 259    alpha = RC / (RC + dt)260 261    minval, maxval = get_min_max_value(seg.sample_width * 8)262    263    original = seg.get_array_of_samples()264    filteredArray = array.array(seg.array_type, original)265    266    frame_count = int(seg.frame_count())267 268    last_val = [0] * seg.channels269    for i in range(seg.channels):270        last_val[i] = filteredArray[i] = original[i]271 272    for i in range(1, frame_count):273        for j in range(seg.channels):274            offset = (i * seg.channels) + j275            offset_minus_1 = ((i-1) * seg.channels) + j276 277            last_val[j] = alpha * (last_val[j] + original[offset] - original[offset_minus_1])278            filteredArray[offset] = int(min(max(last_val[j], minval), maxval))279 280    return seg._spawn(data=filteredArray)281    282    283@register_pydub_effect284def pan(seg, pan_amount):285    """286    pan_amount should be between -1.0 (100% left) and +1.0 (100% right)287    288    When pan_amount == 0.0 the left/right balance is not changed.289    290    Panning does not alter the *perceived* loundness, but since loudness291    is decreasing on one side, the other side needs to get louder to292    compensate. When panned hard left, the left channel will be 3dB louder.293    """294    if not -1.0 <= pan_amount <= 1.0:295        raise ValueError("pan_amount should be between -1.0 (100% left) and +1.0 (100% right)")296    297    max_boost_db = ratio_to_db(2.0)298    boost_db = abs(pan_amount) * max_boost_db299    300    boost_factor = db_to_float(boost_db)301    reduce_factor = db_to_float(max_boost_db) - boost_factor302    303    reduce_db = ratio_to_db(reduce_factor)304    305    # Cut boost in half (max boost== 3dB) - in reality 2 speakers306    #   do not sum to a full 6 dB.307    boost_db = boost_db / 2.0308    309    if pan_amount < 0:310        return seg.apply_gain_stereo(boost_db, reduce_db)311    else:312        return seg.apply_gain_stereo(reduce_db, boost_db)313        314    315@register_pydub_effect316def apply_gain_stereo(seg, left_gain=0.0, right_gain=0.0):317    """318    left_gain - amount of gain to apply to the left channel (in dB)319    right_gain - amount of gain to apply to the right channel (in dB)320    321    note: mono audio segments will be converted to stereo322    """323    if seg.channels == 1:324        left = right = seg325    elif seg.channels == 2:326        left, right = seg.split_to_mono()327    328    l_mult_factor = db_to_float(left_gain)329    r_mult_factor = db_to_float(right_gain)330    331    left_data = audioop.mul(left._data, left.sample_width, l_mult_factor)332    left_data = audioop.tostereo(left_data, left.sample_width, 1, 0)333    334    right_data = audioop.mul(right._data, right.sample_width, r_mult_factor)335    right_data = audioop.tostereo(right_data, right.sample_width, 0, 1)336    337    output = audioop.add(left_data, right_data, seg.sample_width)338    339    return seg._spawn(data=output,340                overrides={'channels': 2,341                           'frame_width': 2 * seg.sample_width})342 
Aluode/PerceptionLabPortable · CoolFace