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