Aluode/PerceptionLabPortable
0
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 