424 lines
18 KiB
Python
424 lines
18 KiB
Python
|
|
# Copyright (C) 2020 harrysentonbury
|
|
# GNU General Public License v3.0
|
|
|
|
import numpy as np
|
|
|
|
|
|
class ThyRandomAudioSlices():
|
|
"""
|
|
Slices and randomly rearange audio arrays.
|
|
data - numpy array.
|
|
window_size - int, optional size of slice in samples. default=2**14
|
|
flip - boolean, optional reverse output. default=False.
|
|
fade_length - int, optional fade in and fade out length of slices in samples
|
|
default=300
|
|
"""
|
|
def __init__(self, data, window_size=2**14, flip=False, fade_length=500):
|
|
self.data = data
|
|
self.window_size = window_size
|
|
self.flip = flip
|
|
self.fade_length = fade_length
|
|
|
|
def chop_and_chuck(self):
|
|
self.fade_size = self.fade_length if self.fade_length < \
|
|
self.window_size else self.window_size
|
|
|
|
self.extra_size = np.size(self.data) % self.window_size
|
|
self.extra = np.zeros(self. window_size - self.extra_size)
|
|
self.data = np.concatenate((self.data, self.extra))
|
|
self.range_size = np.size(self.data) // self.window_size
|
|
self.fade_in = np.linspace(0, 1, self.fade_size)
|
|
self.fade_out = np.flip(self.fade_in)
|
|
self.bitties = np.zeros((self.range_size, self.window_size + self.fade_size))
|
|
self.result = np.zeros_like(self.data)
|
|
|
|
for i in range(self.range_size):
|
|
if i == 0:
|
|
self.bitties[i, :] = self.data[:self.window_size + self.fade_size]
|
|
else:
|
|
self.bitties[i, :] = self.data[(i*self.window_size) - \
|
|
self.fade_size:(i*self.window_size)+self.window_size]
|
|
# shuffle
|
|
rng = np.random.default_rng()
|
|
rng.shuffle(self.bitties)
|
|
|
|
self.bitties[1:, :self.fade_size] *= self.fade_in
|
|
self.bitties[1:, -self.fade_size:] *= self.fade_out
|
|
for i in range(self.range_size):
|
|
if i == 0:
|
|
self.result[:self.window_size + self.fade_size] += self.bitties[i, :]
|
|
else:
|
|
self.result[(i * self.window_size) - \
|
|
self.fade_size:(i * self.window_size) + \
|
|
self.window_size] += self.bitties[i, :]
|
|
return self.result
|
|
|
|
|
|
class ThyBeatifier():
|
|
"""
|
|
data - 1d numpy array
|
|
bar_size - int, number of samples per bar Eg:
|
|
for 1 second of 48kH audio bar_size=48000
|
|
beats_per_bar - int, number of beats per bar
|
|
attack - int, attack size in samples default=100
|
|
flip - boolean, reverse beat shape, default=False
|
|
decay_log_base - float, log base of decay
|
|
decay_min - float, sets decay_log_base of minimum decay value, default=-1
|
|
"""
|
|
def __init__(self, data, bar_size, beats_per_bar, attack=100, flip=False,
|
|
decay_log_base=10, decay_min=-1):
|
|
self.data = data
|
|
self.bar_size = bar_size
|
|
self.beats_per_bar = beats_per_bar
|
|
self.flip = flip
|
|
self.attack = attack
|
|
if decay_min > 1:
|
|
raise ValueError("Must be <= 1")
|
|
self.decay_min = decay_min
|
|
self.decay_log_base = decay_log_base
|
|
|
|
def beater(self):
|
|
"""stick sound array into drumming like envelope thingy"""
|
|
self.data = self.data / np.max(np.absolute(self.data))
|
|
self.duration = np.size(self.data) / self.bar_size
|
|
self.beats_duration = int(self.duration * self.beats_per_bar)
|
|
self.extra = np.zeros(np.size(self.data) % self.beats_duration)
|
|
self.ramp_decay = np.logspace(1, self.decay_min, int(np.size(
|
|
self.data) // self.beats_duration),
|
|
base=self.decay_log_base) / self.decay_log_base
|
|
self.ramp_attack = np.linspace(self.ramp_decay[-1], 1, self.attack)
|
|
self.ramp_decay[:self.attack] *= self.ramp_attack
|
|
self.beats = np.array([])
|
|
for i in range(self.beats_duration):
|
|
self.beats = np.concatenate((self.beats, self.ramp_decay))
|
|
if self.flip:
|
|
self.beats = np.flip(self.beats)
|
|
self.beats = np.concatenate((self.beats, self.extra))
|
|
return self.data * self.beats
|
|
|
|
|
|
|
|
class ThyRythmer():
|
|
"""
|
|
apply a percussion envelope to audio numpy array
|
|
data - 1d numpy array
|
|
bar_size - int, number of samples per bar Eg:
|
|
for 1 second of 48kH audio bar_size=48000
|
|
attack - int, attack size in samples default=100
|
|
flip - boolean, reverse beat shape, default=False
|
|
decay_log_base - float, log base of decay default=10
|
|
decay_min - float, <= 1, sets decay_log_base of minimum decay value, default=-1
|
|
pattern - python list of integers, 1=beat 0=miss a beat, default=[1, 1, 1, 1]
|
|
invert - boolean, invert envelope, default=False
|
|
"""
|
|
def __init__(self, data, bar_size, attack=100, flip=False,
|
|
decay_min=-1, decay_log_base=10, pattern=[1, 1, 1, 1], invert=False):
|
|
self.data = data
|
|
self.bar_size = bar_size
|
|
self.flip = flip
|
|
self.attack = attack
|
|
if decay_min > 1:
|
|
raise ValueError("Must be <= 1")
|
|
self.decay_min = decay_min
|
|
self.decay_log_base = decay_log_base
|
|
self.pattern = pattern
|
|
self.beats_per_bar = np.size(self.pattern)
|
|
if type(self.pattern) is not list:
|
|
raise TypeError(f"pattern must be a list of integers {type(self.pattern)}")
|
|
self.invert = invert
|
|
|
|
|
|
def beater(self):
|
|
self.data = self.data / np.max(np.absolute(self.data))
|
|
self.duration = np.size(self.data) / self.bar_size
|
|
self.beats_duration = int(self.duration * self.beats_per_bar)
|
|
self.extra = np.zeros(np.size(self.data) % self.beats_duration)
|
|
self.ramp_decay = np.logspace(1, self.decay_min, int(np.size(
|
|
self.data) // self.beats_duration),
|
|
base=self.decay_log_base) / self.decay_log_base
|
|
self.ramp_attack = np.linspace(self.ramp_decay[-1], 1, self.attack)
|
|
self.ramp_decay[:self.attack] *= self.ramp_attack
|
|
self.beats = np.array([])
|
|
self.decay_end_val = np.zeros(np.size(self.ramp_decay))
|
|
self.decay_end_val[:] = self.ramp_decay[-1]
|
|
for i in range(int(self.beats_duration / self.beats_per_bar)):
|
|
self.pattern.extend(self.pattern)
|
|
for i in range(self.beats_duration):
|
|
if self.pattern[i] >= 1:
|
|
self.beats = np.concatenate((self.beats, self.ramp_decay))
|
|
else:
|
|
self.beats = np.concatenate((self.beats, self.decay_end_val))
|
|
if self.flip:
|
|
self.beats = np.flip(self.beats)
|
|
self.beats = np.concatenate((self.beats, self.extra))
|
|
if self.invert:
|
|
self.beats = 1 - self.beats
|
|
return self.data * self.beats
|
|
|
|
|
|
class ThyEnveloper():
|
|
"""
|
|
Get the envelope of a sound.
|
|
data - 1d numpy array.
|
|
window_size - int, optional n samples. default=700.
|
|
invert - boolean, optional invert envelope. default=False.
|
|
"""
|
|
def __init__(self, data, window_size=700, invert=False):
|
|
self.data = data
|
|
self.window_size = window_size
|
|
self.invert = invert
|
|
|
|
def get_envelope(self):
|
|
"Returns the envelope"
|
|
self.data_size = np.size(self.data)
|
|
self.data = np.abs(self.data)
|
|
self.result = np.zeros(self.data_size)
|
|
self.iterations = int(self.data_size // self.window_size)
|
|
self.extra = self.data_size % self.window_size
|
|
|
|
for i in range(self.iterations):
|
|
self.ave = np.max(self.data[i * self.window_size:i * self.window_size + self.window_size])
|
|
self.slice = np.linspace(self.result[i * self.window_size-1] if i > 0 else np.max(self.data[i:self.window_size]),
|
|
self.ave, self.window_size)
|
|
self.result[i * self.window_size:i * self.window_size + self.window_size] = self.slice
|
|
|
|
self.slice = np.linspace(self.result[-self.extra - 1], np.max(self.data[-self.extra:]), self.extra)
|
|
if self.invert:
|
|
self.result = 1 - self.result
|
|
if self.extra == 0:
|
|
return self.result
|
|
else:
|
|
self.result[-self.extra:] = self.slice
|
|
return self.result
|
|
|
|
|
|
class ThyStretcher():
|
|
"""
|
|
stereo or mono audio stretcher using fft
|
|
snd_array - int16 1d or 2d numpy array
|
|
window_size - int, size of each fft window, default=2**13
|
|
hop - int, window offset, default=2**11
|
|
factor - int or float, shrinking or stretching factor, default=0.5
|
|
"""
|
|
def __init__(self, snd_array, window_size=2**13, hop=2**11, factor=0.5):
|
|
self.snd_array = snd_array
|
|
self.window_size = window_size
|
|
self.hop = hop
|
|
self.factor = factor
|
|
|
|
def stretching(self):
|
|
try:
|
|
if self.snd_array.shape[1] == 2:
|
|
pass
|
|
except IndexError:
|
|
self.snd_array = np.vstack((self.snd_array, self.snd_array)).T
|
|
self.phase_l = np.zeros(self.window_size)
|
|
self.phase_r = np.zeros(self.window_size)
|
|
self.hanning_window = np.hanning(self.window_size)
|
|
self.result = np.zeros((int(np.size(self.snd_array, axis=0) / self.factor + self.window_size), 2))
|
|
|
|
for i in np.arange(0, np.size(self.snd_array, axis=0) - (self.window_size + self.hop), self.hop*self.factor):
|
|
i = int(i)
|
|
self.a1 = self.snd_array[i: i + self.window_size, :]
|
|
self.a2 = self.snd_array[i + self.hop: i + self.window_size + self.hop, :]
|
|
|
|
# Frequency domain
|
|
self.fft_l1 = np.fft.fft(self.hanning_window * self.a1[:, 0])
|
|
self.fft_l2 = np.fft.fft(self.hanning_window * self.a2[:, 0])
|
|
|
|
self.fft_r1 = np.fft.fft(self.hanning_window * self.a1[:, 1])
|
|
self.fft_r2 = np.fft.fft(self.hanning_window * self.a2[:, 1])
|
|
|
|
# Rephase all frequencies
|
|
self.phase_l = (self.phase_l + np.angle(self.fft_l2/self.fft_l1)) % 2*np.pi
|
|
self.phase_r = (self.phase_r + np.angle(self.fft_r2/self.fft_r1)) % 2*np.pi
|
|
|
|
self.a2_l_rephased = np.fft.ifft(np.abs(self.fft_l2)*np.exp(1j*self.phase_l))
|
|
self.a2_r_rephased = np.fft.ifft(np.abs(self.fft_r2)*np.exp(1j*self.phase_r))
|
|
self.i2 = int(i/self.factor)
|
|
self.result[self.i2: self.i2 + self.window_size, 0] += self.hanning_window*self.a2_l_rephased.real
|
|
self.result[self.i2: self.i2 + self.window_size, 1] += self.hanning_window*self.a2_r_rephased.real
|
|
|
|
# normalize (16bit)
|
|
self.result = ((2**(16-4)) * self.result/self.result.max())
|
|
return self.result.astype('int16')
|
|
|
|
|
|
class ThyFoldinger():
|
|
""" folds positive half or negitive half of waves.
|
|
sound - numpy array
|
|
threshold - float, folding at a point between 0 and 1, 1 being max amplitude.
|
|
positive - boolean, which half of wave to be folded. default=True.
|
|
"""
|
|
def __init__(self, sound, threshold, positive=True):
|
|
self.sound = sound
|
|
self.threshold = threshold
|
|
self.positive = positive
|
|
if self.threshold > 1 or self.threshold < 0:
|
|
raise ValueError("threshold must be >= 0 and <=1")
|
|
|
|
def folding(self):
|
|
self.sound = self.sound / np.max(np.absolute(self.sound))
|
|
if self.positive:
|
|
self.sound = np.where(self.sound < self.threshold, self.sound,
|
|
-self.sound + (2 * self.threshold))
|
|
else:
|
|
self.sound = np.where(self.sound > (-self.threshold), self.sound,
|
|
-self.sound - (2 * self.threshold))
|
|
return self.sound
|
|
|
|
|
|
class ThyPhlazer():
|
|
"""
|
|
Phazing and flanging etc
|
|
data - 1d numpy array
|
|
sample_rate - int, sample rate of data
|
|
loops - int, how many times phlazed, default=1
|
|
depth - float, the depth of phlaze, default=100
|
|
speed - float, speed of phlaze effect Hz, default=0.05
|
|
sweep - boolean, sweep generator if True, default=False
|
|
phase - float, start phase of lfo in radians, default=0
|
|
loop_delay - int, delay the next iteration by n samples, default=0
|
|
control - 1d numpy array, custom control signal. default=None
|
|
length of control array must equal length of data.
|
|
"""
|
|
def __init__(self, data, sample_rate, loops=1, depth=100, speed=0.05,
|
|
sweep=False, phase=0, loop_delay=0, control=None
|
|
):
|
|
self.data = data
|
|
self.sample_rate = sample_rate
|
|
self.loops = loops
|
|
self.depth = depth
|
|
self.speed = speed
|
|
self.sweep = sweep
|
|
self.phase = phase
|
|
self.loop_delay = loop_delay
|
|
self.control = control
|
|
|
|
|
|
def phlaze(self):
|
|
self.fade_out_size = 500
|
|
self.fade_out = np.linspace(1, 0, self.fade_out_size)
|
|
self.data = self.data / np.max(np.absolute(self.data))
|
|
self.duration = int(np.size(self.data) / self.sample_rate)
|
|
self.x = np.linspace(0, 2 * np.pi * self.duration, np.size(self.data))
|
|
if self.control is not None:
|
|
self.control = (self.control / np.max(np.abs(self.control))) - 1
|
|
self.lfo = self.control * self.depth
|
|
elif self.sweep:
|
|
self.lfo = (0 - (np.abs(np.sin((self.x * self.speed * 0.5) + self.phase)))) * self.depth
|
|
else:
|
|
self.lfo = ((np.cos((self.x * self.speed) + self.phase) - 1)) * self.depth
|
|
self.result = np.copy(self.data)
|
|
for i in range(self.loops):
|
|
self.result = self.result + self.squishie(self.result, self.lfo[:np.size(self.result)])
|
|
self.result = self.result + self.data[:np.size(self.result)]
|
|
if self.loop_delay > 0 and i < self.loops:
|
|
self.result = np.roll(self.result, self.loop_delay)
|
|
self.result[:self.loop_delay] = 0
|
|
self.result[-self.fade_out_size:] *= self.fade_out
|
|
self.result = self.result / np.max(np.abs(self.result))
|
|
return self.result
|
|
|
|
|
|
def squishie(self, data, wot):
|
|
self.indices = np.round(np.arange(np.size(data)) + wot)
|
|
self.indices = self.indices[self.indices < np.size(data, axis=0)].astype(int)
|
|
return data[self.indices]
|
|
|
|
|
|
class ThyDelay():
|
|
"""
|
|
It's like an echo.
|
|
sound - 1d numpy array
|
|
sample_rate - int, sample rate of sound array
|
|
delay_time - float, a positive in seconds, default=0.2
|
|
repeats - int, a positive number of delays, default=5
|
|
decay - float, amount of decay of delay 1=no delay, default=2
|
|
reverse - boolean, reverse input and the returned sound, default=False
|
|
hanning - boolean, apply hanning_window to repeats, default=False
|
|
!hanning applied to whole input array length.
|
|
ramp - boolean, apply log ramp to input array, default=False
|
|
"""
|
|
|
|
|
|
def __init__(self, sound, sample_rate, delay_time=0.2, repeats=5, decay=2,
|
|
reverse=False, hanning=False, ramp=False):
|
|
self.sound = sound
|
|
self.sample_rate = sample_rate
|
|
self.delay_time = delay_time
|
|
self.repeats = repeats
|
|
self.decay = decay
|
|
self.reverse = reverse
|
|
self.hanning = hanning
|
|
self.ramp = ramp
|
|
self.log_base = 7
|
|
self.snd_size = np.size(self.sound, axis=0)
|
|
|
|
|
|
def delayer(self):
|
|
# normalize
|
|
self.sound = self.sound / np.max(np.abs(self.sound))
|
|
if self.reverse:
|
|
self.sound = np.flip(self.sound)
|
|
|
|
self.blox = round(self.delay_time * self.sample_rate)
|
|
self.sound = self.sound.reshape(self.snd_size,)
|
|
self.arr = np.zeros((self.repeats, (self.snd_size) + self.blox * self.repeats))
|
|
self.log_ramp = np.logspace(0, 1, self.snd_size, base=self.log_base) / self.log_base
|
|
for i in range(0, self.repeats):
|
|
self.arr[i, i * self.blox:i * self.blox + self.snd_size] = self.sound * 1 / (self.decay**i)
|
|
if self.hanning:
|
|
self.sound *= np.hanning(self.snd_size)
|
|
if self.ramp and not self.hanning:
|
|
self.sound *= self.log_ramp
|
|
|
|
self.result = np.sum(self.arr, axis=0)
|
|
self.result = self.result / np.max(np.abs(self.result))
|
|
if self.reverse:
|
|
self.result = np.flip(self.result)
|
|
return self.result
|
|
|
|
|
|
class ThyTremolator():
|
|
"""
|
|
A tremolo and ring modulator hybred.
|
|
sound - 1d numpy array
|
|
sample_rate - int, sample rate of sound
|
|
speed - float or int, speed of tremolating effect
|
|
amount - float or int, 0.0 to 1.0, <= 0.5 its a tremelo else it becomes
|
|
more ring modulator for values > 0.5
|
|
shape - string, 'sine' or 'triangle' or 'square' or 'cos'
|
|
"""
|
|
def __init__(self, sound, sample_rate, speed=10, amount=0.5, shape="sine"):
|
|
self.sound = sound
|
|
self.sample_rate = sample_rate
|
|
self.speed = speed
|
|
self.amount = amount
|
|
self.shape = shape
|
|
self.size = np.size(self.sound, axis=0)
|
|
self.x = np.linspace(0, 2 * np.pi * self.size / self.sample_rate, self.size)
|
|
if self.amount < 0.0 or self.amount > 1.0:
|
|
raise ValueError("amount must be >= 0.0 and <= 1.0")
|
|
if self.shape not in ["sine", "triangle", "square", "cos"]:
|
|
raise Exception(f"shape \"{self.shape}\" not recognized")
|
|
|
|
|
|
def tremolate(self):
|
|
self.sound = self.sound / np.max(np.abs(self.sound))
|
|
self.trem_adder = 1.0 - self.amount
|
|
if self.shape == "sine":
|
|
self.y = np.sin(self.x * self.speed) * self.amount + self.trem_adder
|
|
return self.sound * self.y
|
|
elif self.shape == "triangle":
|
|
self.y = 2 / np.pi * np.arcsin(np.sin(self.x * self.speed)) * self.amount + self.trem_adder
|
|
return self.sound * self.y
|
|
elif self.shape == "square":
|
|
self.y = np.clip(np.sin(self.x * self.speed), -0.3, 0.3)
|
|
self.y = self.y / np.max(np.abs(self.y)) * self.amount + self.trem_adder
|
|
return self.sound * self.y
|
|
elif self.shape == "cos":
|
|
self.y = np.cos(self.x * self.speed) * self.amount + self.trem_adder
|
|
return self.sound * self.y
|