audio-fm-mummer/thyaudio.py

378 lines
16 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, np.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]
"""
def __init__(self, data, bar_size, attack=100, flip=False,
decay_min=-1, decay_log_base=10, pattern=[1, 1, 1, 1]):
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)}")
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, np.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))
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