brainblow's picture
Duplicate from dpe1/beat_manipulator
df5dab1
raw
history blame contribute delete
No virus
25.8 kB
import numpy as np, scipy.interpolate
from . import io, utils
from .effects import BM_EFFECTS
from .metrics import BM_METRICS
from .presets import BM_SAMPLES
class song:
def __init__(self, audio = None, sr:int=None, log=True):
if audio is None:
from tkinter import filedialog
audio = filedialog.askopenfilename()
if isinstance(audio, song): self.path = audio.path
self.audio, self.sr = io._load(audio=audio, sr=sr)
# unique filename is needed to generate/compare filenames for cached beatmaps
if isinstance(audio, str):
self.path = audio
elif not isinstance(audio, song):
self.path = f'unknown_{hex(int(np.sum(self.audio) * 10**18))}'
self.log = log
self.beatmap = None
self.normalized = None
def _slice(self, a):
if a is None: return None
elif isinstance(a, float):
if (a_dec := a % 1) == 0: return self.beatmap[int(a)]
a_int = int(int(a)//1)
start = self.beatmap[a_int]
return int(start + a_dec * (self.beatmap[a_int+1] - start))
elif isinstance(a, int): return self.beatmap[a]
else: raise TypeError(f'slice indices must be int, float, or None, not {type(a)}. Indice is {a}')
def __getitem__(self, s):
if isinstance(s, slice):
start = s.start
stop = s.stop
step = s.step
if start is not None and stop is not None:
if start > stop:
is_reversed = -1
start, stop = stop, start
else: is_reversed = None
if step is None or step == 1:
start = self._slice(start)
stop = self._slice(stop)
if isinstance(self.audio, list): return [self.audio[0][start:stop:is_reversed],self.audio[1][start:stop:is_reversed]]
else: return self.audio[:,start:stop:is_reversed]
else:
i = s.start if s.start is not None else 0
end = s.stop if s.stop is not None else len(self.beatmap)
if i > end:
step = -step
if step > 0: i, end = end-2, i
elif step < 0: i, end = end-2, i
if step < 0:
is_reversed = True
end -= 1
else: is_reversed = False
pattern = ''
while ((i > end) if is_reversed else (i < end)):
pattern+=f'{i},'
i+=step
song_copy = song(audio = self.audio, sr = self.sr, log = False)
song_copy.beatmap = self.beatmap.copy()
song_copy.beatmap = np.insert(song_copy.beatmap, 0, 0)
result = song_copy.beatswap(pattern = pattern, return_audio = True)
return result if isinstance(self.audio, np.ndarray) else result.tolist()
elif isinstance(s, float):
start = self._slice(s-1)
stop = self._slice(s)
if isinstance(self.audio, list): return [self.audio[0][start:stop],self.audio[1][start:stop]]
else: return self.audio[:,start:stop]
elif isinstance(s, int):
start = self.beatmap[s-1]
stop = self.beatmap[s]
if isinstance(self.audio, list): return [self.audio[0][start:stop],self.audio[1][start:stop]]
else: return self.audio[:,start:stop]
elif isinstance(s, tuple):
start = self._slice(s[0])
stop = self._slice(s[0] + s[1])
if stop<0:
start -= stop
stop = -stop
step = -1
else: step = None
if isinstance(self.audio, list): return [self.audio[0][start:stop:step],self.audio[1][start:stop:step]]
else: return self.audio[:,start:stop:step]
elif isinstance(s, list):
start = s[0]
stop = s[1] if len(s) > 1 else None
if start > stop:
step = -1
start, stop = stop, start
else: step = None
start = self._slice(start)
stop = self._slice(stop)
if step is not None and stop is None: stop = self._slice(start + s.step)
if isinstance(self.audio, list): return [self.audio[0][start:stop:step],self.audio[1][start:stop:step]]
else: return self.audio[:,start:stop:step]
elif isinstance(s, str):
return self.beatswap(pattern = s, return_audio = True)
else: raise TypeError(f'list indices must be int/float/slice/tuple, not {type(s)}; perhaps you missed a comma? Slice is `{s}`')
def _print(self, *args, end=None, sep=None):
if self.log: print(*args, end=end, sep=sep)
def write(self, output='', ext='mp3', suffix=' (beatswap)', literal_output=False):
"""writes"""
if literal_output is False: output = io._outputfilename(output, filename=self.path, suffix=suffix, ext=ext)
io.write_audio(audio=self.audio, sr=self.sr, output=output, log=self.log)
return output
def beatmap_generate(self, lib='madmom.BeatDetectionProcessor', caching = True, load_settings = True):
"""Find beat positions"""
from . import beatmap
self.beatmap = beatmap.generate(audio = self.audio, sr = self.sr, lib=lib, caching=caching, filename = self.path, log = self.log, load_settings = load_settings)
if load_settings is True:
audio_id=hex(len(self.audio[0]))
settingsDir="beat_manipulator/beatmaps/" + ''.join(self.path.split('/')[-1]) + "_"+lib+"_"+audio_id+'_settings.txt'
import os
if os.path.exists(settingsDir):
with open(settingsDir, 'r') as f:
settings = f.read().split(',')
if settings[3] != None: self.normalized = settings[3]
self.beatmap_default = self.beatmap.copy()
self.lib = lib
def beatmap_scale(self, scale:float):
from . import beatmap
self.beatmap = beatmap.scale(beatmap = self.beatmap, scale = scale, log = self.log)
def beatmap_shift(self, shift:float, mode = 1):
from . import beatmap
self.beatmap = beatmap.shift(beatmap = self.beatmap, shift = shift, log = self.log, mode = mode)
def beatmap_reset(self):
self.beatmap = self.beatmap_default.copy()
def beatmap_adjust(self, adjust = 500):
self.beatmap = np.append(np.sort(np.absolute(self.beatmap - adjust)), len(self.audio[0]))
def beatmap_save_settings(self, scale: float = None, shift: float = None, adjust: int = None, normalized = None, overwrite = 'ask'):
from . import beatmap
if self.beatmap is None: self.beatmap_generate()
beatmap.save_settings(audio = self.audio, filename = self.path, scale = scale, shift = shift,adjust = adjust, normalized = normalized, log=self.log, overwrite=overwrite, lib = self.lib)
def beatswap(self, pattern = '1;"cowbell"s3v2, 2;"cowbell"s2, 3;"cowbell", 4;"cowbell"s0.5, 5;"cowbell"s0.25, 6;"cowbell"s0.4, 7;"cowbell"s0.8, 8;"cowbell"s1.6',
scale:float = 1, shift:float = 0, length = None, samples:dict = BM_SAMPLES, effects:dict = BM_EFFECTS, metrics:dict = BM_METRICS, smoothing: int = 100, adjust=500, return_audio = False, normalize = False, limit_beats=10000, limit_length = 52920000):
if normalize is True:
self.normalize_beats()
if self.beatmap is None: self.beatmap_generate()
beatmap_default = self.beatmap.copy()
self.beatmap = np.append(np.sort(np.absolute(self.beatmap - adjust)), len(self.audio[0]))
self.beatmap_shift(shift)
self.beatmap_scale(scale)
# baked in presets
#reverse
if pattern.lower() == 'reverse':
if return_audio is False:
self.audio = self[::-1]
self.beatmap = beatmap_default.copy()
return
else:
result = self[::-1]
self.beatmap = beatmap_default.copy()
return result
# shuffle
elif pattern.lower() == 'shuffle':
import random
beats = list(range(len(self.beatmap)))
random.shuffle(beats)
beats = ','.join(list(str(i) for i in beats))
if return_audio is False:
self.beatswap(beats)
self.beatmap = beatmap_default.copy()
return
else:
result = self.beatswap(beats, return_audio = True)
self.beatmap = beatmap_default.copy()
return result
# test
elif pattern.lower() == 'test':
if return_audio is False:
self.beatswap('1;"cowbell"s3v2, 2;"cowbell"s2, 3;"cowbell", 4;"cowbell"s0.5, 5;"cowbell"s0.25, 6;"cowbell"s0.4, 7;"cowbell"s0.8, 8;"cowbell"s1.6')
self.beatmap = beatmap_default.copy()
return
else:
result = self.beatswap('1;"cowbell"s3v2, 2;"cowbell"s2, 3;"cowbell", 4;"cowbell"s0.5, 5;"cowbell"s0.25, 6;"cowbell"s0.4, 7;"cowbell"s0.8, 8;"cowbell"s1.6', return_audio = True)
self.beatmap = beatmap_default.copy()
return result
# random
elif pattern.lower() == 'random':
import random,math
pattern = ''
rand_length=0
while True:
rand_num = int(math.floor(random.triangular(1, 16, rand_length-1)))
if random.uniform(0, rand_num)>rand_length: rand_num = rand_length+1
rand_slice = random.choices(['','>0.5','>0.25', '<0.5', '<0.25', '<1/3', '<2/3', '>1/3', '>2/3', '<0.75', '>0.75',
f'>{random.uniform(0.01,2)}', f'<{random.uniform(0.01,2)}'], weights = [13,1,1,1,1,1,1,1,1,1,1,1,1], k=1)[0]
rand_effect = random.choices(['', 's0.5', 's2', f's{random.triangular(0.1,1,4)}', 'r','v0.5', 'v2', 'v0',
f'd{int(random.triangular(1,8,16))}', 'g', 'c', 'c0', 'c1', f'b{int(random.triangular(1,8,4))}'],
weights=[30, 2, 2, 2, 2, 1, 1, 2, 2, 1, 2, 2, 2, 1], k=1)[0]
rand_join = random.choices([', ', ';'], weights = [5, 1], k=1)[0]
pattern += f'{rand_num}{rand_slice}{rand_effect}{rand_join}'
if rand_join == ',': rand_length+=1
if rand_length in [4, 8, 16]:
if random.uniform(rand_num,16)>14: break
else:
if random.uniform(rand_num,16)>15.5: break
pattern_length = 4
if rand_length > 6: pattern_length = 8
if rand_length > 12: pattern_length = 16
if rand_length > 24: pattern_length = 32
from . import parse
pattern, operators, pattern_length, shuffle_groups, shuffle_beats, c_slice, c_misc, c_join = parse.parse(pattern = pattern, samples = samples, pattern_length = length, log = self.log)
#print(f'pattern length = {pattern_length}')
# beatswap
n=-1
tries = 0
metric = None
result=[self.audio[:,:self.beatmap[0]]]
#for i in pattern: print(i)
stop = False
total_length = 0
# loop over pattern until it reaches the last beat
while n*pattern_length <= len(self.beatmap):
n+=1
if stop is True: break
# Every time pattern loops, shuffles beats with #
if len(shuffle_beats) > 0:
pattern = parse._shuffle(pattern, shuffle_beats, shuffle_groups)
# Loops over all beats in pattern
for num, b in enumerate(pattern):
# check if beats limit has been reached
if limit_beats is not None and len(result) >= limit_beats:
stop = True
break
if len(b) == 4: beat = b[3] # Sample has length 4
else: beat = b[0] # Else take the beat
if beat is not None:
beat_as_string = ''.join(beat) if isinstance(beat, list) else beat
# Skips `!` beats
if c_misc[9] in beat_as_string: continue
# Audio is a sample or a song
if len(b) == 4:
audio = b[0]
# Audio is a song
if b[2] == c_misc[10]:
try:
# Song slice is a single beat, takes it
if isinstance(beat, str):
# random beat if `@` in beat (`_` is separator)
if c_misc[4] in beat: beat = parse._random(beat, rchar = c_misc[4], schar = c_misc[5], length = pattern_length)
beat = utils._safer_eval(beat) + pattern_length*n
while beat > len(audio.beatmap)-1: beat = 1 + beat - len(audio.beatmap)
beat = audio[beat]
# Song slice is a range of beats, takes the beats
elif isinstance(beat, list):
beat = beat.copy()
for i in range(len(beat)-1): # no separator
if c_misc[4] in beat[i]: beat[i] = parse._random(beat[i], rchar = c_misc[4], schar = c_misc[5], length = pattern_length)
beat[i] = utils._safer_eval(beat[i])
while beat[i] + pattern_length*n > len(audio.beatmap)-1: beat[i] = 1 + beat[i] - len(audio.beatmap)
if beat[2] == c_slice[0]: beat = audio[beat[0] + pattern_length*n : beat[1] + pattern_length*n]
elif beat[2] == c_slice[1]: beat = audio[beat[0] - 1 + pattern_length*n: beat[0] - 1 + beat[1] + pattern_length*n]
elif beat[2] == c_slice[2]: beat = audio[beat[0] - beat[1] + pattern_length*n : beat[0] + pattern_length*n]
# No Song slice, take whole song
elif beat is None: beat = audio.audio
except IndexError as e:
print(e)
tries += 1
if tries > 30: break
continue
# Audio is an audio file
else:
# No audio slice, takes whole audio
if beat is None: beat = audio
# Audio slice, takes part of the audio
elif isinstance(beat, list):
audio_length = len(audio[0])
beat = [min(int(utils._safer_eval(beat[0])*audio_length), audio_length-1), min(int(utils._safer_eval(beat[1])*audio_length), audio_length-1)]
if beat[0] > beat[1]:
beat[0], beat[1] = beat[1], beat[0]
step = -1
else: step = None
beat = audio[:, beat[0] : beat[1] : step]
# Audio is a beat
else:
try:
beat_str = beat if isinstance(beat, str) else ''.join(beat)
# Takes a single beat
if isinstance(beat, str):
if c_misc[4] in beat: beat = parse._random(beat, rchar = c_misc[4], schar = c_misc[5], length = pattern_length)
beat = self[utils._safer_eval(beat) + pattern_length*n]
# Takes a range of beats
elif isinstance(beat, list):
beat = beat.copy()
for i in range(len(beat)-1): # no separator
if c_misc[4] in beat[i]: beat[i] = parse._random(beat[i], rchar = c_misc[4], schar = c_misc[5], length = pattern_length)
beat[i] = utils._safer_eval(beat[i])
if beat[2] == c_slice[0]: beat = self[beat[0] + pattern_length*n : beat[1] + pattern_length*n]
elif beat[2] == c_slice[1]: beat = self[beat[0] - 1 + pattern_length*n: beat[0] - 1 + beat[1] + pattern_length*n]
elif beat[2] == c_slice[2]: beat = self[beat[0] - beat[1] + pattern_length*n : beat[0] + pattern_length*n]
# create a variable if `%` in beat
if c_misc[7] in beat_str: metric = parse._metric_get(beat_str, beat, metrics, c_misc[7])
except IndexError:
tries += 1
if tries > 30: break
continue
if len(beat[0])<1: continue #Ignores empty beats
# Applies effects
effect = b[1]
for e in effect:
if e[0] in effects:
v = e[1]
e = effects[e[0]]
# parse effect value
if isinstance(v, str):
if metric is not None: v = parse._metric_replace(v, metric, c_misc[7])
v = utils._safer_eval(v)
# effects
if e == 'volume':
if v is None: v = 0
beat = beat * v
elif e == 'downsample':
if v is None: v = 8
beat = np.repeat(beat[:,::v], v, axis=1)
elif e == 'gradient':
beat = np.gradient(beat, axis=1)
elif e == 'reverse':
beat = beat[:,::-1]
else:
beat = e(beat, v)
# clip beat to -1, 1
beat = np.clip(beat, -1, 1)
# checks if length limit has been reached
if limit_length is not None:
total_length += len(beat[0])
if total_length>= limit_length:
stop = True
break
# Adds the processed beat to list of beats.
# Separator is `,`
if operators[num] == c_join[0]:
result.append(beat)
# Makes sure beat doesn't get added on top of previous beat multiple times when pattern is out of range of song beats, to avoid distorted end.
elif tries<2:
# Separator is `;` - always use first beat length, normalizes volume to 1.5
if operators[num] == c_join[1]:
length = len(beat[0])
prev_length = len(result[-1][0])
if length > prev_length:
result[-1] += beat[:,:prev_length]
else:
result[-1][:,:length] += beat
limit = np.max(result[-1])
if limit > 1.5:
result[-1] /= limit*0.75
# Separator is `~` - cuts to shortest
elif operators[num] == c_join[2]:
minimum = min(len(beat[0]), len(result[-1][0]))
result[-1] = beat[:,:minimum-1] + result[-1][:,:minimum-1]
# Separator is `&` - extends to longest
elif operators[num] == c_join[3]:
length = len(beat[0])
prev_length = len(result[-1][0])
if length > prev_length:
beat[:,:prev_length] += result[-1]
result[-1] = beat
else:
result[-1][:,:length] += beat
# Separator is `^` - uses first beat length and multiplies beats, used for sidechain
elif operators[num] == c_join[4]:
length = len(beat[0])
prev_length = len(result[-1][0])
if length > prev_length:
result[-1] *= beat[:,:prev_length]
else:
result[-1][:,:length] *= beat
# Separator is `$` - always use first beat length, additionally sidechains first beat by second
elif operators[num] == c_join[5]:
from . import effects
length = len(beat[0])
prev_length = len(result[-1][0])
if length > prev_length:
result[-1] *= effects.to_sidechain(beat[:,:prev_length])
result[-1] += beat[:,:prev_length]
else:
result[-1][:,:length] *= effects.to_sidechain(beat)
result[-1][:,:length] += beat
# Separator is `}` - always use first beat length
elif operators[num] == c_join[6]:
length = len(beat[0])
prev_length = len(result[-1][0])
if length > prev_length:
result[-1] += beat[:,:prev_length]
else:
result[-1][:,:length] += beat
# smoothing
for i in range(len(result)-1):
current1 = result[i][0][-2]
current2 = result[i][0][-1]
following1 = result[i+1][0][0]
following2 = result[i+1][0][1]
num = (abs(following1 - (current2 + (current2 - current1))) + abs(current2 - (following1 + (following1 - following2))))/2
if num > 0.0:
num = int(smoothing*num)
if num>3:
try:
line = scipy.interpolate.CubicSpline([0, num+1], [0, following1], bc_type='clamped')(np.arange(0, num, 1))
#print(line)
line2 = np.linspace(1, 0, num)**0.5
result[i][0][-num:] *= line2
result[i][1][-num:] *= line2
result[i][0][-num:] += line
result[i][1][-num:] += line
except (IndexError, ValueError): pass
self.beatmap = beatmap_default.copy()
# Beats are conjoined into a song
import functools
import operator
# Makes a [l, r, l, r, ...] list of beats (left and right channels)
result = functools.reduce(operator.iconcat, result, [])
# Every first beat is conjoined into left channel, every second beat is conjoined into right channel
if return_audio is False: self.audio = np.array([functools.reduce(operator.iconcat, result[::2], []), functools.reduce(operator.iconcat, result[1:][::2], [])])
else: return np.array([functools.reduce(operator.iconcat, result[::2], []), functools.reduce(operator.iconcat, result[1:][::2], [])])
def normalize_beats(self):
if self.normalized is not None:
if ',' in self.normalized:
self.beatswap(pattern = self.normalized)
else:
from . import presets
self.beatswap(*presets.get(self.normalized))
def image_generate(self, scale=1, shift=0, mode = 'median'):
if self.beatmap is None: self.beatmap_generate()
beatmap_default = self.beatmap.copy()
self.beatmap_shift(shift)
self.beatmap_scale(scale)
from .image import generate as image_generate
self.image = image_generate(song = self, mode = mode, log = self.log)
self.beatmap = beatmap_default.copy()
def image_write(self, output='', mode = 'color', max_size = 4096, ext = 'png', rotate=True, suffix = ''):
from .image import write as image_write
output = io._outputfilename(output, self.path, ext=ext, suffix = suffix)
image_write(self.image, output = output, mode = mode, max_size = max_size , rotate = rotate)
return output
def beatswap(audio = None, pattern = 'test', scale = 1, shift = 0, length = None, sr = None, output = '', log = True, suffix = ' (beatswap)', copy = True):
if not isinstance(audio, song): audio = song(audio = audio, sr = sr, log = log)
elif copy is True:
beatmap = audio.beatmap
path = audio.path
audio = song(audio = audio.audio, sr = audio.sr)
audio.beatmap = beatmap
audio.path = path
audio.beatswap(pattern = pattern, scale = scale, shift = shift, length = length)
if output is not None:
return audio.write(output = output, suffix = suffix)
else: return audio
def image(audio, scale = 1, shift = 0, sr = None, output = '', log = True, suffix = '', max_size = 4096):
if not isinstance(audio, song): audio = song(audio = audio, sr = sr, log = log)
audio.image_generate(scale = scale, shift = shift)
if output is not None:
return audio.image_write(output = output, max_size=max_size, suffix=suffix)
else: return audio.image