brainblow dpe1 commited on
Commit
df5dab1
0 Parent(s):

Duplicate from dpe1/beat_manipulator

Browse files

Co-authored-by: Ivan Nikishev <dpe1@users.noreply.huggingface.co>

.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ **/__pycache__/
3
+ beat_manipulator/beatmaps/
4
+ 1/
5
+ flagged/
6
+ /*.mp3
7
+ /*.wav
8
+ /*.flac
9
+ /*.png
README.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: BeatManipulator
3
+ emoji: 🥁
4
+ colorFrom: blue
5
+ colorTo: blue
6
+ sdk: gradio
7
+ sdk_version: 3.11.0
8
+ app_file: app.py
9
+ pinned: true
10
+ license: cc-by-nc-sa-4.0
11
+ duplicated_from: dpe1/beat_manipulator
12
+ ---
13
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr, numpy as np
2
+ from gradio.components import Audio, Textbox, Checkbox, Image
3
+ import beat_manipulator as bm
4
+ import cv2
5
+
6
+ def BeatSwap(audiofile, pattern: str = 'test', scale:float = 1, shift:float = 0, caching:bool = True, variableBPM:bool = False):
7
+ print()
8
+ print(f'path = {audiofile}, pattern = "{pattern}", scale = {scale}, shift = {shift}, caching = {caching}, variable BPM = {variableBPM}')
9
+ if pattern == '' or pattern is None: pattern = 'test'
10
+ if caching is not False: caching == True
11
+ if variableBPM is not True: variableBPM == False
12
+ try:
13
+ scale=bm.utils._safer_eval(scale)
14
+ except: scale = 1
15
+ try:
16
+ shift=bm.utils._safer_eval(shift)
17
+ except: shift = 0
18
+ if scale <0: scale = -scale
19
+ if scale < 0.02: scale = 0.02
20
+ print('Loading auidofile...')
21
+ if audiofile is not None:
22
+ try:
23
+ song=bm.song(audio=audiofile,log=False)
24
+ except Exception as e:
25
+ print(f'Failed to load audio, retrying: {e}')
26
+ song=bm.song(audio=audiofile, log=False)
27
+ else:
28
+ print(f'Audiofile is {audiofile}')
29
+ return
30
+ try:
31
+ print(f'Scale = {scale}, shift = {shift}, length = {len(song.audio[0])/song.sr}')
32
+ if len(song.audio[0]) > (song.sr*1800):
33
+ song.audio = np.array(song.audio, copy=False)
34
+ song.audio = song.audio[:,:song.sr*1800]
35
+ except Exception as e: print(f'Reducing audio size failed, why? {e}')
36
+ lib = 'madmom.BeatDetectionProcessor' if variableBPM is False else 'madmom.BeatTrackingProcessor'
37
+ song.path = '.'.join(song.path.split('.')[:-1])[:-8]+'.'+song.path.split('.')[-1]
38
+ print(f'path: {song.path}')
39
+ print('Generating beatmap...')
40
+ song.beatmap_generate(lib=lib, caching=caching)
41
+ song.beatmap_shift(shift)
42
+ song.beatmap_scale(scale)
43
+ print('Generating image...')
44
+ try:
45
+ song.image_generate()
46
+ image = bm.image.bw_to_colored(song.image)
47
+ y=min(len(image), len(image[0]), 2048)
48
+ y=max(y, 2048)
49
+ image = np.rot90(np.clip(cv2.resize(image, (y,y), interpolation=cv2.INTER_NEAREST), -1, 1))
50
+ #print(image)
51
+ except Exception as e:
52
+ print(f'Image generation failed: {e}')
53
+ image = np.asarray([[0.5,-0.5],[-0.5,0.5]])
54
+ print('Beatswapping...')
55
+ song.beatswap(pattern=pattern, scale=1, shift=0)
56
+ song.audio = (np.clip(np.asarray(song.audio), -1, 1) * 32766).astype(np.int16).T
57
+ #song.write_audio(output=bm.outputfilename('',song.filename, suffix=' (beatswap)'))
58
+ print('___ SUCCESS ___')
59
+ return ((song.sr, song.audio), image)
60
+
61
+ audiofile=Audio(source='upload', type='filepath')
62
+ patternbox = Textbox(label="Pattern:", placeholder="1, 3, 2, 4!", value="1, 2>0.5, 3, 4>0.5, 5, 6>0.5, 3, 4>0.5, 7, 8", lines=1)
63
+ scalebox = Textbox(value=1, label="Beatmap scale. At 2, every two beat positions will be merged, at 0.5 - a beat position added between every two existing ones.", placeholder=1, lines=1)
64
+ shiftbox = Textbox(value=0, label="Beatmap shift, in beats (applies before scaling):", placeholder=0, lines=1)
65
+ cachebox = Checkbox(value=True, label="Enable caching generated beatmaps for faster loading. Saves a file with beat positions and loads it when you open same audio again.")
66
+ beatdetectionbox = Checkbox(value=False, label='Enable support for variable BPM, however this makes beat detection slightly less accurate')
67
+
68
+ gr.Interface (fn=BeatSwap,inputs=[audiofile,patternbox,scalebox,shiftbox, cachebox, beatdetectionbox],outputs=[Audio(type='numpy'), Image(type='numpy')],theme="default",
69
+ title = "Stunlocked's Beat Manipulator"
70
+ ,description = """Remix music using AI-powered beat detection and advanced beat swapping. Make \"every other beat is missing\" remixes, or completely change beat of the song.
71
+
72
+ Github - https://github.com/stunlocked1/beat_manipulator.
73
+
74
+ Colab version - https://colab.research.google.com/drive/1gEsZCCh2zMKqLmaGH5BPPLrImhEGVhv3?usp=sharing"""
75
+ ,article="""# <h1><p style='text-align: center'><a href='https://github.com/stunlocked1/beat_manipulator' target='_blank'>Github</a></p></h1>
76
+ ### Basic usage
77
+ Upload your audio, enter the beat swapping pattern, change scale and shift if needed, and run it.
78
+
79
+ ### pattern syntax
80
+ patterns are sequences of **beats**, separated by **commas** or other separators. You can use spaces freely in patterns to make them look prettier.
81
+ - `1, 3, 2, 4` - swap 2nd and 3rd beat every four beats. Repeats every four beats because `4` is the biggest number in it.
82
+ - `1, 3, 4` - skip 2nd beat every four beats
83
+ - `1, 2, 3, 4!` - skip 4th beat every four beats. `!` skips the beat.
84
+
85
+ **slicing:**
86
+ - `1>0.5` - plays first half of 1st beat
87
+ - `1<0.5` - plays last half of 1st beat
88
+ - `1 > 1/3, 2, 3, 4` - every four beats, plays first third of the first beat - you can use math expressions anywhere in your pattern.
89
+ - also instead of slicing beats you can use a smaller `scale` parameter to make more precise beat edits
90
+
91
+ **merging beats:**
92
+ - `1; 2, 3, 4` - every four beats, play 1st and 2nd beats at the same time.
93
+
94
+ **effects:**
95
+ - `1, 2r` - 2nd beat will be reversed
96
+ - `1, 2s0.5` - 2nd beat will be played at 0.5x speed
97
+ - `1, 2d10` - 2nd beat will have 8-bit effect (downsampled)
98
+
99
+ You can do much more with the syntax - shuffle/randomize beats, use samples, mix two songs, etc. Syntax is described in detail at https://github.com/stunlocked1/beat_manipulator
100
+ ### scale
101
+ `scale = 0.5` will insert a new beat position between every existing beat position in the beatmap. That allows you to make patterns on smaller intervals.
102
+
103
+ `scale = 2`, on the other hand, will merge every two beat positions in the beatmap. Useful, for example, when beat map detection puts sees BPM as two times faster than it actually is, and puts beats in between every actual beat.
104
+ ### shift
105
+ Shifts the beatmap, in beats. For example, if you want to remove 4th beat every four beats, you can do it by writing `1, 2, 3, 4!`. However sometimes it doesn't properly detect which beat is first, and for example remove 2nd beat every 4 beats instead. In that case, if you want 4th beat, use `shift = 2`. Also sometimes beats are detected right in between actual beats, so shift = 0.5 or -0.5 will fix it.
106
+ ### creating images
107
+ You can create cool images based on beat positions. Each song produces its own unique image. This gradio app creates a 2048x2048 image from each song.
108
+ ### presets
109
+ A bunch of example patterns: https://github.com/stunlocked1/beat_manipulator/blob/main/beat_manipulator/presets.yaml
110
+
111
+ Those are supposed to be used on normalized beat maps, where kick + snare is two beats, so make sure to adjust beatmaps using `scale` and `shift`.
112
+
113
+ ### Changelog:
114
+ - play two beats at the same time by using `;` instead of `,`
115
+ - significantly reduced clicking
116
+ - shuffle and randomize beats
117
+ - gradient effect, similar to high pass
118
+ - add samples to beats
119
+ - use beats from other songs
120
+
121
+ ### My soundcloud https://soundcloud.com/stunlocked
122
+ """
123
+ ).launch(share=False)
beat_manipulator/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from .main import *
2
+ from . import beatmap, effects, image, io, metrics, presets, osu, utils
beat_manipulator/beatmap.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from . import utils
3
+
4
+
5
+ def scale(beatmap:np.ndarray, scale:float, log = True, integer = True) -> np.ndarray:
6
+ if isinstance(scale, str): scale = utils._safer_eval(scale)
7
+ assert scale>0, f"scale should be > 0, your scale is {scale}"
8
+ if scale == 1: return beatmap
9
+ else:
10
+ import math
11
+ if log is True: print(f'scale={scale}; ')
12
+ a = 0
13
+ b = np.array([], dtype=int)
14
+ if scale%1==0:
15
+ while a < len(beatmap):
16
+ b = np.append(b, beatmap[int(a)])
17
+ a += scale
18
+ else:
19
+ if integer is True:
20
+ while a + 1 < len(beatmap):
21
+ b = np.append(b, int((1 - (a % 1)) * beatmap[math.floor(a)] + (a % 1) * beatmap[math.ceil(a)]))
22
+ a += scale
23
+ else:
24
+ while a + 1 < len(beatmap):
25
+ b = np.append(b, (1 - (a % 1)) * beatmap[math.floor(a)] + (a % 1) * beatmap[math.ceil(a)])
26
+ a += scale
27
+ return b
28
+
29
+ def shift(beatmap:np.ndarray, shift:float, log = True, mode = 1) -> np.ndarray:
30
+ if isinstance(shift, str): shift = utils._safer_eval(shift)
31
+ if shift == 0: return beatmap
32
+ # positive shift
33
+ elif shift > 0:
34
+ # full value of beats is removed from the beginning
35
+ if shift >= 1: beatmap = beatmap[int(shift//1):]
36
+ # shift beatmap by the decimal value
37
+ if shift%1 != 0:
38
+ shift = shift%1
39
+ for i in range(len(beatmap) - int(shift) - 1):
40
+ beatmap[i] = int(beatmap[i] + shift * (beatmap[i + 1] - beatmap[i]))
41
+
42
+ # negative shift
43
+ else:
44
+ shift = -shift
45
+ # full values are inserted in between first beats
46
+ if shift >= 1:
47
+ if mode == 1:
48
+ step = int((beatmap[1] - beatmap[0]) / (int(shift//1) + 1))
49
+ beatmap = np.insert(arr = beatmap, obj = 1, values = np.linspace(start = beatmap[0] + step - 1, stop = 1 + beatmap[1] - step, num = int(shift//1)))
50
+ elif mode == 2:
51
+ for i in range(int(shift//1)):
52
+ beatmap = np.insert(arr = beatmap, obj = (i*2)+1, values = int((beatmap[i*2] + beatmap[(i*2)+1])/2))
53
+ # shift beatmap by the decimal value
54
+ if shift%1 != 0:
55
+ shift = shift%1
56
+ for i in reversed(range(len(beatmap))):
57
+ if i==0: continue
58
+ beatmap[i] = int(beatmap[i] - shift * (beatmap[i] - beatmap[i-1]))
59
+ return beatmap
60
+
61
+ def generate(audio: np.ndarray, sr: int, lib='madmom.BeatDetectionProcessor', caching=True, filename: str = None, log = True, load_settings = True, split=None):
62
+ """Creates beatmap attribute with a list of positions of beats in samples."""
63
+ if log is True: print(f'Analyzing beats using {lib}; ', end='')
64
+
65
+ # load a beatmap if it is cached:
66
+ if caching is True and filename is not None:
67
+ audio_id=hex(len(audio[0]))
68
+ import os
69
+ if not os.path.exists('beat_manipulator/beatmaps'):
70
+ os.mkdir('beat_manipulator/beatmaps')
71
+ cacheDir="beat_manipulator/beatmaps/" + ''.join(filename.replace('\\', '/').split('/')[-1]) + "_"+lib+"_"+audio_id+'.txt'
72
+ try:
73
+ beatmap=np.loadtxt(cacheDir, dtype=int)
74
+ if log is True: print('loaded cached beatmap.')
75
+ except OSError:
76
+ if log is True:print("beatmap hasn't been generated yet. Generating...")
77
+ beatmap = None
78
+
79
+ #generate the beatmap
80
+ if beatmap is None:
81
+ if 'madmom' in lib.lower():
82
+ from collections.abc import MutableMapping, MutableSequence
83
+ import madmom
84
+ assert len(audio[0])>sr*2, f'Audio file is too short, len={len(audio[0])} samples, or {len(audio[0])/sr} seconds. Minimum length is 2 seconds, audio below that breaks madmom processors.'
85
+ if lib=='madmom.BeatTrackingProcessor':
86
+ proc = madmom.features.beats.BeatTrackingProcessor(fps=100)
87
+ act = madmom.features.beats.RNNBeatProcessor()(madmom.audio.signal.Signal(audio.T, sr))
88
+ beatmap= proc(act)*sr
89
+ elif lib=='madmom.BeatTrackingProcessor.constant':
90
+ proc = madmom.features.beats.BeatTrackingProcessor(fps=100, look_ahead=None)
91
+ act = madmom.features.beats.RNNBeatProcessor()(madmom.audio.signal.Signal(audio.T, sr))
92
+ beatmap= proc(act)*sr
93
+ elif lib=='madmom.BeatTrackingProcessor.consistent':
94
+ proc = madmom.features.beats.BeatTrackingProcessor(fps=100, look_ahead=None, look_aside=0)
95
+ act = madmom.features.beats.RNNBeatProcessor()(madmom.audio.signal.Signal(audio.T, sr))
96
+ beatmap= proc(act)*sr
97
+ elif lib=='madmom.BeatDetectionProcessor':
98
+ proc = madmom.features.beats.BeatDetectionProcessor(fps=100)
99
+ act = madmom.features.beats.RNNBeatProcessor()(madmom.audio.signal.Signal(audio.T, sr))
100
+ beatmap= proc(act)*sr
101
+ elif lib=='madmom.BeatDetectionProcessor.consistent':
102
+ proc = madmom.features.beats.BeatDetectionProcessor(fps=100, look_aside=0)
103
+ act = madmom.features.beats.RNNBeatProcessor()(madmom.audio.signal.Signal(audio.T, sr))
104
+ beatmap= proc(act)*sr
105
+ elif lib=='madmom.CRFBeatDetectionProcessor':
106
+ proc = madmom.features.beats.CRFBeatDetectionProcessor(fps=100)
107
+ act = madmom.features.beats.RNNBeatProcessor()(madmom.audio.signal.Signal(audio.T, sr))
108
+ beatmap= proc(act)*sr
109
+ elif lib=='madmom.CRFBeatDetectionProcessor.constant':
110
+ proc = madmom.features.beats.CRFBeatDetectionProcessor(fps=100, use_factors=True, factors=[0.5, 1, 2])
111
+ act = madmom.features.beats.RNNBeatProcessor()(madmom.audio.signal.Signal(audio.T, sr))
112
+ beatmap= proc(act)*sr
113
+ elif lib=='madmom.DBNBeatTrackingProcessor':
114
+ proc = madmom.features.beats.DBNBeatTrackingProcessor(fps=100)
115
+ act = madmom.features.beats.RNNBeatProcessor()(madmom.audio.signal.Signal(audio.T, sr))
116
+ beatmap= proc(act)*sr
117
+ elif lib=='madmom.DBNBeatTrackingProcessor.1000':
118
+ proc = madmom.features.beats.DBNBeatTrackingProcessor(fps=100, transition_lambda=1000)
119
+ act = madmom.features.beats.RNNBeatProcessor()(madmom.audio.signal.Signal(audio.T, sr))
120
+ beatmap= proc(act)*sr
121
+ elif lib=='madmom.DBNDownBeatTrackingProcessor':
122
+ proc = madmom.features.downbeats.DBNDownBeatTrackingProcessor(beats_per_bar=[4], fps=100)
123
+ act = madmom.features.downbeats.RNNDownBeatProcessor()(madmom.audio.signal.Signal(audio.T, sr))
124
+ beatmap= proc(act)*sr
125
+ beatmap=beatmap[:,0]
126
+ elif lib=='madmom.PatternTrackingProcessor': #broken
127
+ from madmom.models import PATTERNS_BALLROOM
128
+ proc = madmom.features.downbeats.PatternTrackingProcessor(PATTERNS_BALLROOM, fps=50)
129
+ from madmom.audio.spectrogram import LogarithmicSpectrogramProcessor, SpectrogramDifferenceProcessor, MultiBandSpectrogramProcessor
130
+ from madmom.processors import SequentialProcessor
131
+ log = LogarithmicSpectrogramProcessor()
132
+ diff = SpectrogramDifferenceProcessor(positive_diffs=True)
133
+ mb = MultiBandSpectrogramProcessor(crossover_frequencies=[270])
134
+ pre_proc = SequentialProcessor([log, diff, mb])
135
+ act = pre_proc(madmom.audio.signal.Signal(audio.T, sr))
136
+ beatmap= proc(act)*sr
137
+ beatmap=beatmap[:,0]
138
+ elif lib=='madmom.DBNBarTrackingProcessor': #broken
139
+ beats = generate(audio=audio, sr=sr, filename=filename, lib='madmom.DBNBeatTrackingProcessor', caching = caching)
140
+ proc = madmom.features.downbeats.DBNBarTrackingProcessor(beats_per_bar=[4], fps=100)
141
+ act = madmom.features.downbeats.RNNBarProcessor()(((madmom.audio.signal.Signal(audio.T, sr)), beats))
142
+ beatmap= proc(act)*sr
143
+ elif lib=='librosa': #broken in 3.9, works in 3.8
144
+ import librosa
145
+ beat_frames = librosa.beat.beat_track(y=audio[0], sr=sr, hop_length=512)
146
+ beatmap = librosa.frames_to_samples(beat_frames[1])
147
+
148
+ # save the beatmap and return
149
+ if caching is True: np.savetxt(cacheDir, beatmap.astype(int), fmt='%d')
150
+ if not isinstance(beatmap, np.ndarray): beatmap=np.asarray(beatmap, dtype=int)
151
+ else: beatmap=beatmap.astype(int)
152
+
153
+ if load_settings is True:
154
+ settingsDir="beat_manipulator/beatmaps/" + ''.join(filename.split('/')[-1]) + "_"+lib+"_"+audio_id+'_settings.txt'
155
+ if os.path.exists(settingsDir):
156
+ with open(settingsDir, 'r') as f:
157
+ settings = f.read().split(',')
158
+ if settings[0] != 'None': beatmap = scale(beatmap, settings[0], log = False)
159
+ if settings[1] != 'None': beatmap = shift(beatmap, settings[1], log = False)
160
+ if settings[2] != 'None': beatmap = np.sort(np.absolute(beatmap - int(settings[2])))
161
+
162
+ return beatmap
163
+
164
+
165
+
166
+ def save_settings(audio: np.ndarray, filename: str = None, lib: str = 'madmom.BeatDetectionProcessor', scale: float = None, shift: float = None, adjust: int = None, normalized: str = None, log = True, overwrite = 'ask'):
167
+ if isinstance(overwrite, str): overwrite = overwrite.lower()
168
+ audio_id=hex(len(audio[0]))
169
+ cacheDir="beat_manipulator/beatmaps/" + ''.join(filename.split('/')[-1]) + "_"+lib+"_"+audio_id+'.txt'
170
+ import os
171
+ assert os.path.exists(cacheDir), f"Beatmap `{cacheDir}` doesn't exist"
172
+ settingsDir="beat_manipulator/beatmaps/" + ''.join(filename.split('/')[-1]) + "_"+lib+"_"+audio_id+'_settings.txt'
173
+
174
+ try:
175
+ a = utils._safer_eval_strict(scale)
176
+ if a == 1: scale = None
177
+ except Exception as e: assert scale is None, f'scale = `{scale}` - Not a valid scale, should be either a number, a math expression, or None: {e}'
178
+ try:
179
+ a = utils._safer_eval_strict(shift)
180
+ if a == 0: shift = None
181
+ except Exception as e: assert shift is None, f'shift = `{shift}` - Not a valid shift: {e}'
182
+ assert isinstance(adjust, int) or adjust is None, f'adjust = `{adjust}` should be int, but it is `{type(adjust)}`'
183
+
184
+ if adjust == 0: adjust = None
185
+
186
+ if os.path.exists(settingsDir):
187
+ if overwrite == 'ask' or overwrite =='a':
188
+ what = input(f'`{settingsDir}` already exists. Overwrite (y/n)?: ')
189
+ if not (what.lower() == 'y' or what.lower() == 'yes'): return
190
+ elif not (overwrite == 'true' or overwrite =='y' or overwrite =='yes' or overwrite is True): return
191
+
192
+ with open(settingsDir, 'w') as f:
193
+ f.write(f'{scale},{shift},{adjust},{normalized}')
194
+ if log is True: print(f"Saved scale = `{scale}`, shift = `{shift}`, adjust = `{adjust}` to `{settingsDir}`")
195
+
beat_manipulator/effects.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from . import io
3
+
4
+ def deco_abs(effect):
5
+ def stuff(*args, **kwargs):
6
+ if len(args)>0: audio = args[0]
7
+ else: audio = kwargs['audio']
8
+ if not isinstance(audio, np.ndarray): audio = io._load(audio)
9
+ audio_signs = np.sign(audio)
10
+ audio = np.abs(audio)
11
+ if len(args)>0: args[0] = audio
12
+ else: kwargs['audio'] = audio
13
+ audio = effect(*args, **kwargs)
14
+ audio *= audio_signs
15
+ return stuff
16
+
17
+
18
+
19
+ def volume(audio: np.ndarray, v: float):
20
+ return audio*v
21
+
22
+ def speed(audio: np.ndarray, s: float = 2, precision:int = 24):
23
+ if s%1 != 0 and (1/s)%1 != 0:
24
+ import fractions
25
+ s = fractions.Fraction(s).limit_denominator(precision)
26
+ audio = np.repeat(audio, s.denominator, axis=1)
27
+ return audio[:,::s.numerator]
28
+ elif s%1 == 0:
29
+ return audio[:,::int(s)]
30
+ else:
31
+ return np.repeat(audio, int(1/s), axis=1)
32
+
33
+ def channel(audio: np.ndarray, c:int = None):
34
+ if c is None:
35
+ audio[0], audio[1] = audio[1], audio[0]
36
+ return audio
37
+ elif c == 0:
38
+ audio[0] = 0
39
+ return audio
40
+ else:
41
+ audio[1] = 0
42
+ return audio
43
+
44
+ def downsample(audio: np.ndarray, d:int = 10):
45
+ return np.repeat(audio[:,::d], d, axis=1)
46
+
47
+ def gradient(audio: np.ndarray, number: int = 1):
48
+ for _ in range(number):
49
+ audio = np.gradient(audio, axis=1)
50
+ return audio
51
+
52
+ def bitcrush(audio: np.ndarray, b:float = 4):
53
+ if 1/b > 1:
54
+ return np.around(audio, decimals=int(1/b))
55
+ else:
56
+ return np.around(audio*b, decimals = 1)
57
+
58
+ def reverse(audio: np.ndarray):
59
+ return audio[:,::-1]
60
+
61
+ def normalize(audio: np.ndarray):
62
+ return audio*(1/np.max(np.abs(audio)))
63
+
64
+ def clip(audio: np.ndarray):
65
+ return np.clip(audio, -1, 1)
66
+
67
+ def to_sidechain(audio: np.ndarray):
68
+ audio = np.clip(np.abs(audio), -1, 1)
69
+ for channel in range(len(audio)):
70
+ audio[channel] = np.abs(1 - np.convolve(audio[channel], np.ones(shape=(1000)), mode = 'same'))
71
+ return audio
72
+
73
+
74
+
75
+ # some stuff is defined in main.py to reduce function calls for 1 line stuff
76
+ BM_EFFECTS = {
77
+ "v": "volume",
78
+ "s": speed,
79
+ "c": channel,
80
+ "d": "downsample",
81
+ "g": "gradient",
82
+ "b": bitcrush,
83
+ "r": "reverse",
84
+ }
beat_manipulator/google colab.ipynb ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {
6
+ "id": "ldVF6dMTbqB7"
7
+ },
8
+ "source": [
9
+ "#Beat Manipulator"
10
+ ]
11
+ },
12
+ {
13
+ "cell_type": "code",
14
+ "source": [
15
+ "#@title 1. Run this cell to install all necessary libraries. This only needs to be done once each time you open this collab.\n",
16
+ "import shutil, os\n",
17
+ "try:\n",
18
+ " if os.path.exists('BeatManipulator'): shutil.rmtree('BeatManipulator', ignore_errors=True)\n",
19
+ "except: pass\n",
20
+ "!pip install numpy cython soundfile ffmpeg-python pedalboard librosa\n",
21
+ "!pip install madmom\n",
22
+ "!git clone https://github.com/stunlocked1/BeatManipulator\n",
23
+ "%cd BeatManipulator\n",
24
+ "import beat_manipulator as bm"
25
+ ],
26
+ "metadata": {
27
+ "id": "E-gDjnzBby5-",
28
+ "cellView": "form"
29
+ },
30
+ "execution_count": null,
31
+ "outputs": []
32
+ },
33
+ {
34
+ "cell_type": "markdown",
35
+ "metadata": {
36
+ "id": "FYVs2fGzbqB9"
37
+ },
38
+ "source": [
39
+ "***\n",
40
+ "Use cells below as many times as you wish. Pattern syntax, scale and shift are explained here https://github.com/stunlocked1/BeatManipulator\n",
41
+ "\n",
42
+ "Enter pattern, scale and shift, run the cell, and it will let you upload your audio file.\n",
43
+ "\n",
44
+ "Analyzing beats for the first time will take some time, but if you open the same file for the second time, it will load a saved beat map."
45
+ ]
46
+ },
47
+ {
48
+ "cell_type": "code",
49
+ "execution_count": null,
50
+ "metadata": {
51
+ "id": "TFOLf-vrbqB-",
52
+ "cellView": "form"
53
+ },
54
+ "outputs": [],
55
+ "source": [
56
+ "#@title 2. Beatswapping. Enter pattern, scale and shift, run the cell, and it will let you upload your audio file.\n",
57
+ "pattern = \"1, 3, 2, 4\" #@param {type:\"string\"}\n",
58
+ "scale = 1 #@param {type:\"number\"}\n",
59
+ "shift = 0 #@param {type:\"number\"}\n",
60
+ "\n",
61
+ "pattern_length = None # Length of the pattern. If None, this will be inferred from the highest number in the pattern\n",
62
+ "\n",
63
+ "\n",
64
+ "import beat_manipulator as bm, IPython\n",
65
+ "from google.colab import files\n",
66
+ "uploaded = list(files.upload().keys())[0]\n",
67
+ "path = bm.beatswap(audio=uploaded, pattern = pattern, scale = scale, shift = shift, length = pattern_length)\n",
68
+ "IPython.display.Audio(path)"
69
+ ]
70
+ },
71
+ {
72
+ "cell_type": "markdown",
73
+ "source": [
74
+ "***\n",
75
+ "## Other stuff\n",
76
+ "Those operate the same as the above cell"
77
+ ],
78
+ "metadata": {
79
+ "id": "fQVrYbQ_rQzm"
80
+ }
81
+ },
82
+ {
83
+ "cell_type": "markdown",
84
+ "source": [
85
+ "**Song to image**\n",
86
+ "\n",
87
+ "creates an image based on beat positions. Each song will generate a unique image. The image will be a square, you can specify maximum size.` "
88
+ ],
89
+ "metadata": {
90
+ "id": "7gc2jbelrTMb"
91
+ }
92
+ },
93
+ {
94
+ "cell_type": "code",
95
+ "source": [
96
+ "#@title Song to image\n",
97
+ "image_size = 512 #@param {type:\"integer\"}\n",
98
+ "\n",
99
+ "\n",
100
+ "import beat_manipulator as bm, IPython\n",
101
+ "from google.colab import files\n",
102
+ "uploaded = list(files.upload().keys())[0]\n",
103
+ "path = bm.image(audio=uploaded, max_size = image_size)\n",
104
+ "IPython.display.Image(path)"
105
+ ],
106
+ "metadata": {
107
+ "id": "M2ufXQaxrWZT",
108
+ "cellView": "form"
109
+ },
110
+ "execution_count": null,
111
+ "outputs": []
112
+ },
113
+ {
114
+ "cell_type": "markdown",
115
+ "source": [
116
+ "***\n",
117
+ "**osu! beatmap generator**\n",
118
+ "\n",
119
+ "generates an osu! beatmap from your song. This generates a hitmap, probabilities of hits at each sample, picks all ones above a threshold, and turns them into osu circles, trying to emulate actual osu beatmap. This doesn't generate sliders, however, because no known science has been able to comprehend the complexity of those.\n",
120
+ "\n",
121
+ "The .osz file will be downloaded automatically, open with osu! to install like any other beatmap."
122
+ ],
123
+ "metadata": {
124
+ "id": "bqOYyiCisAjM"
125
+ }
126
+ },
127
+ {
128
+ "cell_type": "code",
129
+ "source": [
130
+ "difficulties = [0.2, 0.1, 0.05, 0.025, 0.01, 0.0075, 0.005, 0.0025, 0.0001] # all difficulties will be embedded in one beatmap, Lower is typically harder, but not always.\n",
131
+ "\n",
132
+ "\n",
133
+ "import beat_manipulator.osu\n",
134
+ "from google.colab import files\n",
135
+ "uploaded = list(files.upload().keys())[0]\n",
136
+ "path = beat_manipulator.osu.generate(song=uploaded, difficulties = difficulties)\n",
137
+ "files.download(f'/content/BeatManipulator/{path}')"
138
+ ],
139
+ "metadata": {
140
+ "id": "T1wLIS1_sB_K"
141
+ },
142
+ "execution_count": null,
143
+ "outputs": []
144
+ }
145
+ ],
146
+ "metadata": {
147
+ "kernelspec": {
148
+ "display_name": "audio310",
149
+ "language": "python",
150
+ "name": "python3"
151
+ },
152
+ "language_info": {
153
+ "codemirror_mode": {
154
+ "name": "ipython",
155
+ "version": 3
156
+ },
157
+ "file_extension": ".py",
158
+ "mimetype": "text/x-python",
159
+ "name": "python",
160
+ "nbconvert_exporter": "python",
161
+ "pygments_lexer": "ipython3",
162
+ "version": "3.10.9"
163
+ },
164
+ "orig_nbformat": 4,
165
+ "vscode": {
166
+ "interpreter": {
167
+ "hash": "f56da36b984886453ea677d340712034d0bd218b2dc7a53ab7c38da0c6f67f35"
168
+ }
169
+ },
170
+ "colab": {
171
+ "provenance": []
172
+ }
173
+ },
174
+ "nbformat": 4,
175
+ "nbformat_minor": 0
176
+ }
beat_manipulator/image.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from . import io, main
2
+ import numpy as np
3
+ def generate(song, beatmap = None, mode='median', sr = None, log = True):
4
+ if log is True: print(f'Generating an image from beats...', end = ' ')
5
+ song = main.song(song, sr=sr)
6
+ if song.beatmap is None: song.beatmap = beatmap
7
+ if song.beatmap is None: song.beatmap_generate()
8
+ if isinstance(song.audio, np.ndarray): song.audio = song.audio.tolist()
9
+ # create the image
10
+ image = [[],[]]
11
+ for i in range(1, len(song.beatmap)):
12
+ beat = song[i]
13
+ image[0].append(beat[0])
14
+ image[1].append(beat[1])
15
+
16
+ # find image width
17
+ lengths = [len(i) for i in image[0]]
18
+ mode = mode.lower()
19
+ if 'max' in mode:
20
+ width = max(lengths)
21
+ elif 'med' in mode:
22
+ width = int(np.median(lengths))
23
+ elif 'av' in mode:
24
+ width = int(np.average(lengths))
25
+
26
+ # fill or crop rows:
27
+ for i in range(len(image[0])):
28
+ difference = lengths[i] - width
29
+ if difference<0:
30
+ image[0][i].extend([np.nan]*(-difference))
31
+ image[1][i].extend([np.nan]*(-difference))
32
+ elif difference>0:
33
+ image[0][i] = image[0][i][:-difference]
34
+ image[1][i] = image[1][i][:-difference]
35
+
36
+ song.audio = np.array(song.audio, copy=False)
37
+ if log is True: print('Done!')
38
+ return np.array(image, copy=False)
39
+
40
+ def bw_to_colored(image, channel = 2, fill = True):
41
+ if fill is True:
42
+ combined = image[0] * image[1]
43
+ combined = (np.abs(combined)**0.5)*np.sign(combined)
44
+ else: channel = np.zeros(shape = image[0].shape)
45
+ image = image.tolist()
46
+ if channel == 2: image.append(combined)
47
+ else: image.insert(channel, combined)
48
+ return np.rot90(np.array(image, copy=False).T)
49
+
50
+ def colored_to_bw(image, l=0, r=1):
51
+ image = np.array(image, copy=False)
52
+ return np.array([image[:,:,l],image[:,:,r]])
53
+
54
+ def write(image, output, mode = 'r', max_size = 4096, rotate = True, contrast=1):
55
+ import cv2
56
+ if max_size is not None:
57
+ w = max_size
58
+ h = min(len(image[0][0]), max_size)
59
+ if mode == 'color':
60
+ image = bw_to_colored(image)
61
+ elif mode == 'r':
62
+ image = image[0]
63
+ elif mode == 'l':
64
+ image = image[1]
65
+ elif mode == 'combine':
66
+ image = image[0] + image[1]
67
+ image = image*(255*contrast)
68
+ image = cv2.resize(src=image, dsize=(w, h), interpolation = cv2.INTER_NEAREST)
69
+ if rotate is True: image = np.rot90(image)
70
+ cv2.imwrite(output, image)
beat_manipulator/io.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import numpy as np
3
+ from . import main
4
+
5
+ def open_audio(path:str = None, lib:str = 'auto', normalize = True) -> tuple:
6
+ """Opens audio from path, returns (audio, samplerate) tuple.
7
+
8
+ Audio is returned as an array with normal volume range between -1, 1.
9
+
10
+ Example of returned audio:
11
+
12
+ [
13
+ [0.35, -0.25, ... -0.15, -0.15],
14
+
15
+ [0.31, -0.21, ... -0.11, -0.07]
16
+ ]"""
17
+
18
+ if path is None:
19
+ from tkinter.filedialog import askopenfilename
20
+ path = askopenfilename(title='select song', filetypes=[("mp3", ".mp3"),("wav", ".wav"),("flac", ".flac"),("ogg", ".ogg"),("wma", ".wma")])
21
+
22
+ path=path.replace('\\', '/')
23
+
24
+ if lib=='pedalboard.io':
25
+ import pedalboard.io
26
+ with pedalboard.io.AudioFile(path) as f:
27
+ audio = f.read(f.frames)
28
+ sr = f.samplerate
29
+
30
+ elif lib=='librosa':
31
+ import librosa
32
+ audio, sr = librosa.load(path, sr=None, mono=False)
33
+
34
+ elif lib=='soundfile':
35
+ import soundfile
36
+ audio, sr = soundfile.read(path)
37
+ audio=audio.T
38
+
39
+ elif lib=='madmom':
40
+ import madmom
41
+ audio, sr = madmom.io.audio.load_audio_file(path, dtype=float)
42
+ audio=audio.T
43
+
44
+ # elif lib=='pydub':
45
+ # from pydub import AudioSegment
46
+ # song=AudioSegment.from_file(filename)
47
+ # audio = song.get_array_of_samples()
48
+ # samplerate=song.frame_rate
49
+ # print(audio)
50
+ # print(filename)
51
+
52
+ elif lib=='auto':
53
+ for i in ('madmom', 'soundfile', 'librosa', 'pedalboard.io'):
54
+ try:
55
+ audio,sr=open_audio(path, i)
56
+ break
57
+ except Exception as e:
58
+ print(f'open_audio with {i}: {e}')
59
+
60
+ if len(audio)>16: audio=np.array([audio, audio], copy=False)
61
+ if normalize is True:
62
+ audio = np.clip(audio, -1, 1)
63
+ audio = audio*(1/np.max(np.abs(audio)))
64
+ return audio.astype(np.float32),sr
65
+
66
+ def _sr(sr):
67
+ try: return int(sr)
68
+ except (ValueError, TypeError): assert False, f"Audio is an array, but `sr` argument is not valid. If audio is an array, you have to provide samplerate as an integer in the `sr` argument. Currently sr = {sr} of type {type(sr)}"
69
+
70
+ def write_audio(audio:np.ndarray, sr:int, output:str, lib:str='auto', libs=('pedalboard.io', 'soundfile'), log = True):
71
+ """"writes audio to path specified by output. Path should end with file extension, for example `folder/audio.mp3`"""
72
+ if log is True: print(f'Writing {output}...', end=' ')
73
+ assert _iterable(audio), f"audio should be an array/iterable object, but it is {type(audio)}"
74
+ sr = _sr(sr)
75
+ if not isinstance(audio, np.ndarray): audio = np.array(audio, copy=False)
76
+ if lib=='pedalboard.io':
77
+ #print(audio)
78
+ import pedalboard.io
79
+ with pedalboard.io.AudioFile(output, 'w', sr, audio.shape[0]) as f:
80
+ f.write(audio)
81
+ elif lib=='soundfile':
82
+ audio=audio.T
83
+ import soundfile
84
+ soundfile.write(output, audio, sr)
85
+ del audio
86
+ elif lib=='auto':
87
+ for i in libs:
88
+ try:
89
+ write_audio(audio=audio, sr=sr, output=output, lib=i, log = False)
90
+ break
91
+ except Exception as e:
92
+ print(e)
93
+ else: assert False, 'Failed to write audio, chances are there is something wrong with it...'
94
+ if log is True: print(f'Done!')
95
+
96
+ def _iterable(a):
97
+ try:
98
+ _ = iter(a)
99
+ return True
100
+ except TypeError: return False
101
+
102
+ def _load(audio, sr:int = None, lib:str = 'auto', channels:int = 2, transpose3D:bool = False) -> tuple:
103
+ """Automatically converts audio from path or any format to [[...],[...]] array. Returns (audio, samplerate) tuple."""
104
+ # path
105
+ if isinstance(audio, str): return(open_audio(path=audio, lib=lib))
106
+ # array
107
+ if _iterable(audio):
108
+ if isinstance(audio, main.song):
109
+ if sr is None: sr = audio.sr
110
+ audio = audio.audio
111
+ # sr is provided in a tuple
112
+ if sr is None and len(audio) == 2:
113
+ if not _iterable(audio[0]):
114
+ sr = audio[0]
115
+ audio = audio[1]
116
+ elif not _iterable(audio[1]):
117
+ sr = audio[1]
118
+ audio = audio[0]
119
+ if not isinstance(audio, np.ndarray): audio = np.array(audio, copy=False)
120
+ sr = _sr(sr)
121
+ if _iterable(audio[0]):
122
+ # image
123
+ if _iterable(audio[0][0]):
124
+ audio2 = []
125
+ if transpose3D is True: audio = audio.T
126
+ for i in audio:
127
+ audio2.extend(_load(audio=i, sr=sr, lib=lib, channels=channels, transpose3D=transpose3D)[0])
128
+ return audio2, sr
129
+ # transposed
130
+ if len(audio) > 16:
131
+ audio = audio.T
132
+ return _load(audio=audio, sr=sr, lib=lib, channels=channels, transpose3D=transpose3D)
133
+ # multi channel
134
+ elif isinstance(channels, int):
135
+ if len(audio) >= channels:
136
+ return audio[:channels], sr
137
+ # masked mono
138
+ else: return np.array([audio[0] for _ in range(channels)], copy=False), sr
139
+ else: return audio, sr
140
+ else:
141
+ # mono
142
+ return (np.array([audio for _ in range(channels)], copy=False) if channels is not None else audio), sr
143
+ # unknown
144
+ else: assert False, f"Audio should be either a string with path, an array/iterable object, or a song object, but it is {type(audio)}"
145
+
146
+ def _tosong(audio, sr=None):
147
+ if isinstance(audio, main.song): return audio
148
+ else:
149
+ audio, sr = _load(audio = audio, sr = sr)
150
+ return main.song(audio=audio, sr = sr)
151
+
152
+ def _outputfilename(path:str = None, filename:str = None, suffix:str = None, ext:str = None):
153
+ """If path has file extension, returns `path + suffix + ext`. Else returns `path + filename + suffix + .ext`. If nothing is specified, returns `output.mp3`"""
154
+ if ext is not None:
155
+ if not ext.startswith('.'): ext = '.'+ext
156
+ if path is None: path = ''
157
+ if path.endswith('/') or path.endswith('\\'): path=path[:-1]
158
+ if '.' in path:
159
+ path = path.split('.')
160
+ if path[-1].lower() in ['mp3', 'wav', 'flac', 'ogg', 'wma', 'aac', 'ac3', 'aiff']:
161
+ if ext is not None:
162
+ path[-1] = ext
163
+ if suffix is not None: path[len(path)-2]+=suffix
164
+ return ''.join(path)
165
+ else: path = ''.join(path)
166
+ if filename is not None:
167
+ filename = filename.replace('\\','/').split('/')[-1]
168
+ if '.' in filename:
169
+ filename = filename.split('.')
170
+ if filename[-1].lower() in ['mp3', 'wav', 'flac', 'ogg', 'wma', 'aac', 'ac3', 'aiff']:
171
+ if ext is not None:
172
+ filename[-1] = ext
173
+ if suffix is not None: filename.insert(len(filename)-1, suffix)
174
+ else: filename += [ext]
175
+ filename = ''.join(filename)
176
+ return f'{path}/{filename}' if path != '' else filename
177
+ return f'{(path + "/") * (path != "")}{filename}{suffix if suffix is not None else ""}.{ext if ext is not None else "mp3"}'
178
+ else: return f'{path}/output.mp3'
beat_manipulator/main.py ADDED
@@ -0,0 +1,531 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np, scipy.interpolate
2
+ from . import io, utils
3
+ from .effects import BM_EFFECTS
4
+ from .metrics import BM_METRICS
5
+ from .presets import BM_SAMPLES
6
+
7
+
8
+ class song:
9
+ def __init__(self, audio = None, sr:int=None, log=True):
10
+ if audio is None:
11
+ from tkinter import filedialog
12
+ audio = filedialog.askopenfilename()
13
+
14
+ if isinstance(audio, song): self.path = audio.path
15
+ self.audio, self.sr = io._load(audio=audio, sr=sr)
16
+
17
+ # unique filename is needed to generate/compare filenames for cached beatmaps
18
+ if isinstance(audio, str):
19
+ self.path = audio
20
+ elif not isinstance(audio, song):
21
+ self.path = f'unknown_{hex(int(np.sum(self.audio) * 10**18))}'
22
+
23
+ self.log = log
24
+ self.beatmap = None
25
+ self.normalized = None
26
+
27
+ def _slice(self, a):
28
+ if a is None: return None
29
+ elif isinstance(a, float):
30
+ if (a_dec := a % 1) == 0: return self.beatmap[int(a)]
31
+ a_int = int(int(a)//1)
32
+ start = self.beatmap[a_int]
33
+ return int(start + a_dec * (self.beatmap[a_int+1] - start))
34
+ elif isinstance(a, int): return self.beatmap[a]
35
+ else: raise TypeError(f'slice indices must be int, float, or None, not {type(a)}. Indice is {a}')
36
+
37
+ def __getitem__(self, s):
38
+ if isinstance(s, slice):
39
+ start = s.start
40
+ stop = s.stop
41
+ step = s.step
42
+ if start is not None and stop is not None:
43
+ if start > stop:
44
+ is_reversed = -1
45
+ start, stop = stop, start
46
+ else: is_reversed = None
47
+ if step is None or step == 1:
48
+ start = self._slice(start)
49
+ stop = self._slice(stop)
50
+ if isinstance(self.audio, list): return [self.audio[0][start:stop:is_reversed],self.audio[1][start:stop:is_reversed]]
51
+ else: return self.audio[:,start:stop:is_reversed]
52
+ else:
53
+ i = s.start if s.start is not None else 0
54
+ end = s.stop if s.stop is not None else len(self.beatmap)
55
+ if i > end:
56
+ step = -step
57
+ if step > 0: i, end = end-2, i
58
+ elif step < 0: i, end = end-2, i
59
+ if step < 0:
60
+ is_reversed = True
61
+ end -= 1
62
+ else: is_reversed = False
63
+ pattern = ''
64
+ while ((i > end) if is_reversed else (i < end)):
65
+ pattern+=f'{i},'
66
+ i+=step
67
+ song_copy = song(audio = self.audio, sr = self.sr, log = False)
68
+ song_copy.beatmap = self.beatmap.copy()
69
+ song_copy.beatmap = np.insert(song_copy.beatmap, 0, 0)
70
+ result = song_copy.beatswap(pattern = pattern, return_audio = True)
71
+ return result if isinstance(self.audio, np.ndarray) else result.tolist()
72
+
73
+
74
+ elif isinstance(s, float):
75
+ start = self._slice(s-1)
76
+ stop = self._slice(s)
77
+ if isinstance(self.audio, list): return [self.audio[0][start:stop],self.audio[1][start:stop]]
78
+ else: return self.audio[:,start:stop]
79
+ elif isinstance(s, int):
80
+ start = self.beatmap[s-1]
81
+ stop = self.beatmap[s]
82
+ if isinstance(self.audio, list): return [self.audio[0][start:stop],self.audio[1][start:stop]]
83
+ else: return self.audio[:,start:stop]
84
+ elif isinstance(s, tuple):
85
+ start = self._slice(s[0])
86
+ stop = self._slice(s[0] + s[1])
87
+ if stop<0:
88
+ start -= stop
89
+ stop = -stop
90
+ step = -1
91
+ else: step = None
92
+ if isinstance(self.audio, list): return [self.audio[0][start:stop:step],self.audio[1][start:stop:step]]
93
+ else: return self.audio[:,start:stop:step]
94
+ elif isinstance(s, list):
95
+ start = s[0]
96
+ stop = s[1] if len(s) > 1 else None
97
+ if start > stop:
98
+ step = -1
99
+ start, stop = stop, start
100
+ else: step = None
101
+ start = self._slice(start)
102
+ stop = self._slice(stop)
103
+ if step is not None and stop is None: stop = self._slice(start + s.step)
104
+ if isinstance(self.audio, list): return [self.audio[0][start:stop:step],self.audio[1][start:stop:step]]
105
+ else: return self.audio[:,start:stop:step]
106
+ elif isinstance(s, str):
107
+ return self.beatswap(pattern = s, return_audio = True)
108
+
109
+
110
+ else: raise TypeError(f'list indices must be int/float/slice/tuple, not {type(s)}; perhaps you missed a comma? Slice is `{s}`')
111
+
112
+
113
+ def _print(self, *args, end=None, sep=None):
114
+ if self.log: print(*args, end=end, sep=sep)
115
+
116
+
117
+ def write(self, output='', ext='mp3', suffix=' (beatswap)', literal_output=False):
118
+ """writes"""
119
+ if literal_output is False: output = io._outputfilename(output, filename=self.path, suffix=suffix, ext=ext)
120
+ io.write_audio(audio=self.audio, sr=self.sr, output=output, log=self.log)
121
+ return output
122
+
123
+
124
+ def beatmap_generate(self, lib='madmom.BeatDetectionProcessor', caching = True, load_settings = True):
125
+ """Find beat positions"""
126
+ from . import beatmap
127
+ self.beatmap = beatmap.generate(audio = self.audio, sr = self.sr, lib=lib, caching=caching, filename = self.path, log = self.log, load_settings = load_settings)
128
+ if load_settings is True:
129
+ audio_id=hex(len(self.audio[0]))
130
+ settingsDir="beat_manipulator/beatmaps/" + ''.join(self.path.split('/')[-1]) + "_"+lib+"_"+audio_id+'_settings.txt'
131
+ import os
132
+ if os.path.exists(settingsDir):
133
+ with open(settingsDir, 'r') as f:
134
+ settings = f.read().split(',')
135
+ if settings[3] != None: self.normalized = settings[3]
136
+ self.beatmap_default = self.beatmap.copy()
137
+ self.lib = lib
138
+
139
+ def beatmap_scale(self, scale:float):
140
+ from . import beatmap
141
+ self.beatmap = beatmap.scale(beatmap = self.beatmap, scale = scale, log = self.log)
142
+
143
+ def beatmap_shift(self, shift:float, mode = 1):
144
+ from . import beatmap
145
+ self.beatmap = beatmap.shift(beatmap = self.beatmap, shift = shift, log = self.log, mode = mode)
146
+
147
+ def beatmap_reset(self):
148
+ self.beatmap = self.beatmap_default.copy()
149
+
150
+ def beatmap_adjust(self, adjust = 500):
151
+ self.beatmap = np.append(np.sort(np.absolute(self.beatmap - adjust)), len(self.audio[0]))
152
+
153
+ def beatmap_save_settings(self, scale: float = None, shift: float = None, adjust: int = None, normalized = None, overwrite = 'ask'):
154
+ from . import beatmap
155
+ if self.beatmap is None: self.beatmap_generate()
156
+ 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)
157
+
158
+ 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',
159
+ 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):
160
+
161
+ if normalize is True:
162
+ self.normalize_beats()
163
+ if self.beatmap is None: self.beatmap_generate()
164
+ beatmap_default = self.beatmap.copy()
165
+ self.beatmap = np.append(np.sort(np.absolute(self.beatmap - adjust)), len(self.audio[0]))
166
+ self.beatmap_shift(shift)
167
+ self.beatmap_scale(scale)
168
+
169
+ # baked in presets
170
+ #reverse
171
+ if pattern.lower() == 'reverse':
172
+ if return_audio is False:
173
+ self.audio = self[::-1]
174
+ self.beatmap = beatmap_default.copy()
175
+ return
176
+ else:
177
+ result = self[::-1]
178
+ self.beatmap = beatmap_default.copy()
179
+ return result
180
+ # shuffle
181
+ elif pattern.lower() == 'shuffle':
182
+ import random
183
+ beats = list(range(len(self.beatmap)))
184
+ random.shuffle(beats)
185
+ beats = ','.join(list(str(i) for i in beats))
186
+ if return_audio is False:
187
+ self.beatswap(beats)
188
+ self.beatmap = beatmap_default.copy()
189
+ return
190
+ else:
191
+ result = self.beatswap(beats, return_audio = True)
192
+ self.beatmap = beatmap_default.copy()
193
+ return result
194
+ # test
195
+ elif pattern.lower() == 'test':
196
+ if return_audio is False:
197
+ 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')
198
+ self.beatmap = beatmap_default.copy()
199
+ return
200
+ else:
201
+ 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)
202
+ self.beatmap = beatmap_default.copy()
203
+ return result
204
+ # random
205
+ elif pattern.lower() == 'random':
206
+ import random,math
207
+ pattern = ''
208
+ rand_length=0
209
+ while True:
210
+ rand_num = int(math.floor(random.triangular(1, 16, rand_length-1)))
211
+ if random.uniform(0, rand_num)>rand_length: rand_num = rand_length+1
212
+ rand_slice = random.choices(['','>0.5','>0.25', '<0.5', '<0.25', '<1/3', '<2/3', '>1/3', '>2/3', '<0.75', '>0.75',
213
+ 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]
214
+
215
+ rand_effect = random.choices(['', 's0.5', 's2', f's{random.triangular(0.1,1,4)}', 'r','v0.5', 'v2', 'v0',
216
+ f'd{int(random.triangular(1,8,16))}', 'g', 'c', 'c0', 'c1', f'b{int(random.triangular(1,8,4))}'],
217
+ weights=[30, 2, 2, 2, 2, 1, 1, 2, 2, 1, 2, 2, 2, 1], k=1)[0]
218
+
219
+ rand_join = random.choices([', ', ';'], weights = [5, 1], k=1)[0]
220
+ pattern += f'{rand_num}{rand_slice}{rand_effect}{rand_join}'
221
+ if rand_join == ',': rand_length+=1
222
+ if rand_length in [4, 8, 16]:
223
+ if random.uniform(rand_num,16)>14: break
224
+ else:
225
+ if random.uniform(rand_num,16)>15.5: break
226
+ pattern_length = 4
227
+ if rand_length > 6: pattern_length = 8
228
+ if rand_length > 12: pattern_length = 16
229
+ if rand_length > 24: pattern_length = 32
230
+
231
+
232
+
233
+ from . import parse
234
+ 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)
235
+
236
+ #print(f'pattern length = {pattern_length}')
237
+
238
+ # beatswap
239
+ n=-1
240
+ tries = 0
241
+ metric = None
242
+ result=[self.audio[:,:self.beatmap[0]]]
243
+ #for i in pattern: print(i)
244
+
245
+
246
+ stop = False
247
+ total_length = 0
248
+
249
+ # loop over pattern until it reaches the last beat
250
+ while n*pattern_length <= len(self.beatmap):
251
+ n+=1
252
+
253
+ if stop is True: break
254
+
255
+ # Every time pattern loops, shuffles beats with #
256
+ if len(shuffle_beats) > 0:
257
+ pattern = parse._shuffle(pattern, shuffle_beats, shuffle_groups)
258
+
259
+ # Loops over all beats in pattern
260
+ for num, b in enumerate(pattern):
261
+
262
+ # check if beats limit has been reached
263
+ if limit_beats is not None and len(result) >= limit_beats:
264
+ stop = True
265
+ break
266
+
267
+ if len(b) == 4: beat = b[3] # Sample has length 4
268
+ else: beat = b[0] # Else take the beat
269
+
270
+ if beat is not None:
271
+ beat_as_string = ''.join(beat) if isinstance(beat, list) else beat
272
+ # Skips `!` beats
273
+ if c_misc[9] in beat_as_string: continue
274
+
275
+ # Audio is a sample or a song
276
+ if len(b) == 4:
277
+ audio = b[0]
278
+
279
+ # Audio is a song
280
+ if b[2] == c_misc[10]:
281
+ try:
282
+
283
+ # Song slice is a single beat, takes it
284
+ if isinstance(beat, str):
285
+ # random beat if `@` in beat (`_` is separator)
286
+ if c_misc[4] in beat: beat = parse._random(beat, rchar = c_misc[4], schar = c_misc[5], length = pattern_length)
287
+ beat = utils._safer_eval(beat) + pattern_length*n
288
+ while beat > len(audio.beatmap)-1: beat = 1 + beat - len(audio.beatmap)
289
+ beat = audio[beat]
290
+
291
+ # Song slice is a range of beats, takes the beats
292
+ elif isinstance(beat, list):
293
+ beat = beat.copy()
294
+ for i in range(len(beat)-1): # no separator
295
+ if c_misc[4] in beat[i]: beat[i] = parse._random(beat[i], rchar = c_misc[4], schar = c_misc[5], length = pattern_length)
296
+ beat[i] = utils._safer_eval(beat[i])
297
+ while beat[i] + pattern_length*n > len(audio.beatmap)-1: beat[i] = 1 + beat[i] - len(audio.beatmap)
298
+ if beat[2] == c_slice[0]: beat = audio[beat[0] + pattern_length*n : beat[1] + pattern_length*n]
299
+ elif beat[2] == c_slice[1]: beat = audio[beat[0] - 1 + pattern_length*n: beat[0] - 1 + beat[1] + pattern_length*n]
300
+ elif beat[2] == c_slice[2]: beat = audio[beat[0] - beat[1] + pattern_length*n : beat[0] + pattern_length*n]
301
+
302
+ # No Song slice, take whole song
303
+ elif beat is None: beat = audio.audio
304
+
305
+ except IndexError as e:
306
+ print(e)
307
+ tries += 1
308
+ if tries > 30: break
309
+ continue
310
+
311
+ # Audio is an audio file
312
+ else:
313
+ # No audio slice, takes whole audio
314
+ if beat is None: beat = audio
315
+
316
+ # Audio slice, takes part of the audio
317
+ elif isinstance(beat, list):
318
+ audio_length = len(audio[0])
319
+ 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)]
320
+ if beat[0] > beat[1]:
321
+ beat[0], beat[1] = beat[1], beat[0]
322
+ step = -1
323
+ else: step = None
324
+ beat = audio[:, beat[0] : beat[1] : step]
325
+
326
+ # Audio is a beat
327
+ else:
328
+ try:
329
+ beat_str = beat if isinstance(beat, str) else ''.join(beat)
330
+ # Takes a single beat
331
+ if isinstance(beat, str):
332
+ if c_misc[4] in beat: beat = parse._random(beat, rchar = c_misc[4], schar = c_misc[5], length = pattern_length)
333
+ beat = self[utils._safer_eval(beat) + pattern_length*n]
334
+
335
+ # Takes a range of beats
336
+ elif isinstance(beat, list):
337
+ beat = beat.copy()
338
+ for i in range(len(beat)-1): # no separator
339
+ if c_misc[4] in beat[i]: beat[i] = parse._random(beat[i], rchar = c_misc[4], schar = c_misc[5], length = pattern_length)
340
+ beat[i] = utils._safer_eval(beat[i])
341
+ if beat[2] == c_slice[0]: beat = self[beat[0] + pattern_length*n : beat[1] + pattern_length*n]
342
+ elif beat[2] == c_slice[1]: beat = self[beat[0] - 1 + pattern_length*n: beat[0] - 1 + beat[1] + pattern_length*n]
343
+ elif beat[2] == c_slice[2]: beat = self[beat[0] - beat[1] + pattern_length*n : beat[0] + pattern_length*n]
344
+
345
+ # create a variable if `%` in beat
346
+ if c_misc[7] in beat_str: metric = parse._metric_get(beat_str, beat, metrics, c_misc[7])
347
+
348
+ except IndexError:
349
+ tries += 1
350
+ if tries > 30: break
351
+ continue
352
+
353
+ if len(beat[0])<1: continue #Ignores empty beats
354
+
355
+ # Applies effects
356
+ effect = b[1]
357
+ for e in effect:
358
+ if e[0] in effects:
359
+ v = e[1]
360
+ e = effects[e[0]]
361
+ # parse effect value
362
+ if isinstance(v, str):
363
+ if metric is not None: v = parse._metric_replace(v, metric, c_misc[7])
364
+ v = utils._safer_eval(v)
365
+
366
+ # effects
367
+ if e == 'volume':
368
+ if v is None: v = 0
369
+ beat = beat * v
370
+ elif e == 'downsample':
371
+ if v is None: v = 8
372
+ beat = np.repeat(beat[:,::v], v, axis=1)
373
+ elif e == 'gradient':
374
+ beat = np.gradient(beat, axis=1)
375
+ elif e == 'reverse':
376
+ beat = beat[:,::-1]
377
+ else:
378
+ beat = e(beat, v)
379
+
380
+ # clip beat to -1, 1
381
+ beat = np.clip(beat, -1, 1)
382
+
383
+ # checks if length limit has been reached
384
+ if limit_length is not None:
385
+ total_length += len(beat[0])
386
+ if total_length>= limit_length:
387
+ stop = True
388
+ break
389
+
390
+ # Adds the processed beat to list of beats.
391
+ # Separator is `,`
392
+ if operators[num] == c_join[0]:
393
+ result.append(beat)
394
+
395
+ # 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.
396
+ elif tries<2:
397
+
398
+ # Separator is `;` - always use first beat length, normalizes volume to 1.5
399
+ if operators[num] == c_join[1]:
400
+ length = len(beat[0])
401
+ prev_length = len(result[-1][0])
402
+ if length > prev_length:
403
+ result[-1] += beat[:,:prev_length]
404
+ else:
405
+ result[-1][:,:length] += beat
406
+ limit = np.max(result[-1])
407
+ if limit > 1.5:
408
+ result[-1] /= limit*0.75
409
+
410
+ # Separator is `~` - cuts to shortest
411
+ elif operators[num] == c_join[2]:
412
+ minimum = min(len(beat[0]), len(result[-1][0]))
413
+ result[-1] = beat[:,:minimum-1] + result[-1][:,:minimum-1]
414
+
415
+ # Separator is `&` - extends to longest
416
+ elif operators[num] == c_join[3]:
417
+ length = len(beat[0])
418
+ prev_length = len(result[-1][0])
419
+ if length > prev_length:
420
+ beat[:,:prev_length] += result[-1]
421
+ result[-1] = beat
422
+ else:
423
+ result[-1][:,:length] += beat
424
+
425
+ # Separator is `^` - uses first beat length and multiplies beats, used for sidechain
426
+ elif operators[num] == c_join[4]:
427
+ length = len(beat[0])
428
+ prev_length = len(result[-1][0])
429
+ if length > prev_length:
430
+ result[-1] *= beat[:,:prev_length]
431
+ else:
432
+ result[-1][:,:length] *= beat
433
+
434
+
435
+ # Separator is `$` - always use first beat length, additionally sidechains first beat by second
436
+ elif operators[num] == c_join[5]:
437
+ from . import effects
438
+ length = len(beat[0])
439
+ prev_length = len(result[-1][0])
440
+ if length > prev_length:
441
+ result[-1] *= effects.to_sidechain(beat[:,:prev_length])
442
+ result[-1] += beat[:,:prev_length]
443
+ else:
444
+ result[-1][:,:length] *= effects.to_sidechain(beat)
445
+ result[-1][:,:length] += beat
446
+
447
+ # Separator is `}` - always use first beat length
448
+ elif operators[num] == c_join[6]:
449
+ length = len(beat[0])
450
+ prev_length = len(result[-1][0])
451
+ if length > prev_length:
452
+ result[-1] += beat[:,:prev_length]
453
+ else:
454
+ result[-1][:,:length] += beat
455
+
456
+
457
+ # smoothing
458
+ for i in range(len(result)-1):
459
+ current1 = result[i][0][-2]
460
+ current2 = result[i][0][-1]
461
+ following1 = result[i+1][0][0]
462
+ following2 = result[i+1][0][1]
463
+ num = (abs(following1 - (current2 + (current2 - current1))) + abs(current2 - (following1 + (following1 - following2))))/2
464
+ if num > 0.0:
465
+ num = int(smoothing*num)
466
+ if num>3:
467
+ try:
468
+ line = scipy.interpolate.CubicSpline([0, num+1], [0, following1], bc_type='clamped')(np.arange(0, num, 1))
469
+ #print(line)
470
+ line2 = np.linspace(1, 0, num)**0.5
471
+ result[i][0][-num:] *= line2
472
+ result[i][1][-num:] *= line2
473
+ result[i][0][-num:] += line
474
+ result[i][1][-num:] += line
475
+ except (IndexError, ValueError): pass
476
+
477
+ self.beatmap = beatmap_default.copy()
478
+ # Beats are conjoined into a song
479
+ import functools
480
+ import operator
481
+ # Makes a [l, r, l, r, ...] list of beats (left and right channels)
482
+ result = functools.reduce(operator.iconcat, result, [])
483
+
484
+ # Every first beat is conjoined into left channel, every second beat is conjoined into right channel
485
+ if return_audio is False: self.audio = np.array([functools.reduce(operator.iconcat, result[::2], []), functools.reduce(operator.iconcat, result[1:][::2], [])])
486
+ else: return np.array([functools.reduce(operator.iconcat, result[::2], []), functools.reduce(operator.iconcat, result[1:][::2], [])])
487
+
488
+ def normalize_beats(self):
489
+ if self.normalized is not None:
490
+ if ',' in self.normalized:
491
+ self.beatswap(pattern = self.normalized)
492
+ else:
493
+ from . import presets
494
+ self.beatswap(*presets.get(self.normalized))
495
+
496
+ def image_generate(self, scale=1, shift=0, mode = 'median'):
497
+ if self.beatmap is None: self.beatmap_generate()
498
+ beatmap_default = self.beatmap.copy()
499
+ self.beatmap_shift(shift)
500
+ self.beatmap_scale(scale)
501
+ from .image import generate as image_generate
502
+ self.image = image_generate(song = self, mode = mode, log = self.log)
503
+ self.beatmap = beatmap_default.copy()
504
+
505
+ def image_write(self, output='', mode = 'color', max_size = 4096, ext = 'png', rotate=True, suffix = ''):
506
+ from .image import write as image_write
507
+ output = io._outputfilename(output, self.path, ext=ext, suffix = suffix)
508
+ image_write(self.image, output = output, mode = mode, max_size = max_size , rotate = rotate)
509
+ return output
510
+
511
+
512
+
513
+ def beatswap(audio = None, pattern = 'test', scale = 1, shift = 0, length = None, sr = None, output = '', log = True, suffix = ' (beatswap)', copy = True):
514
+ if not isinstance(audio, song): audio = song(audio = audio, sr = sr, log = log)
515
+ elif copy is True:
516
+ beatmap = audio.beatmap
517
+ path = audio.path
518
+ audio = song(audio = audio.audio, sr = audio.sr)
519
+ audio.beatmap = beatmap
520
+ audio.path = path
521
+ audio.beatswap(pattern = pattern, scale = scale, shift = shift, length = length)
522
+ if output is not None:
523
+ return audio.write(output = output, suffix = suffix)
524
+ else: return audio
525
+
526
+ def image(audio, scale = 1, shift = 0, sr = None, output = '', log = True, suffix = '', max_size = 4096):
527
+ if not isinstance(audio, song): audio = song(audio = audio, sr = sr, log = log)
528
+ audio.image_generate(scale = scale, shift = shift)
529
+ if output is not None:
530
+ return audio.image_write(output = output, max_size=max_size, suffix=suffix)
531
+ else: return audio.image
beat_manipulator/metrics.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from . import effects
3
+
4
+ def volume(audio: np.ndarray) -> float:
5
+ return np.average(np.abs(audio))
6
+
7
+ def volume_gradient(audio: np.ndarray, number:int = 1) -> float:
8
+ audio = effects.gradient(audio, number = number)
9
+ return np.average(np.abs(audio))
10
+
11
+ def maximum_high(audio: np.ndarray, number:int = 1) -> float:
12
+ audio = effects.gradient(audio, number = number)
13
+ return np.max(np.abs(audio))
14
+
15
+ def locate_1st_hit(audio: np.ndarray, number: int = 1):
16
+ audio = effects.gradient(audio, number = number)
17
+ return np.argmax(audio, axis=1) / len(audio[0])
18
+
19
+ def is_hit(audio: np.ndarray, threshold: float = 0.5, number:int = 1) -> int:
20
+ return 1 if maximum_high(audio, number=number) > threshold else 0
21
+
22
+ def hit_at_start(audio: np.ndarray, diff = 0.1) -> int:
23
+ return is_hit(audio) * (locate_1st_hit(audio) <= diff)
24
+
25
+ def hit_in_middle(audio: np.ndarray, diff = 0.1) -> int:
26
+ return is_hit(audio) * ((0.5 - diff) <= locate_1st_hit(audio) <= (0.5 + diff))
27
+
28
+ def hit_at_end(audio: np.ndarray, diff = 0.1) -> int:
29
+ return is_hit(audio) * (locate_1st_hit(audio) >= (1-diff))
30
+
31
+ BM_METRICS = {
32
+ "v": volume,
33
+ "g": volume_gradient,
34
+ "m": maximum_high,
35
+ "l": locate_1st_hit,
36
+ "h": is_hit,
37
+ "s": hit_at_start,
38
+ "a": hit_in_middle,
39
+ "e": hit_at_end,
40
+ }
beat_manipulator/osu.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from . import main
2
+ import numpy as np
3
+
4
+ # L L L L L L L L L
5
+ def generate(song, difficulties = [0.2, 0.1, 0.05, 0.025, 0.01, 0.0075, 0.005, 0.0025], lib='madmom.MultiModelSelectionProcessor', caching=True, log = True, output = '', add_peaks = True):
6
+ # for i in difficulties:
7
+ # if i<0.005: print(f'Difficulties < 0.005 may result in broken beatmaps, found difficulty = {i}')
8
+ if lib.lower == 'stunlocked': add_peaks = False
9
+
10
+ if not isinstance(song, main.song): song = main.song(song)
11
+ if log is True: print(f'Using {lib}; ', end='')
12
+
13
+ filename = song.path.replace('\\', '/').split('/')[-1]
14
+ if ' - ' in filename and len(filename.split(' - '))>1:
15
+ artist = filename.split(' - ')[0]
16
+ title = ' - '.join(filename.split(' - ')[1:])
17
+ else:
18
+ artist = ''
19
+ title = filename
20
+
21
+ if caching is True:
22
+ audio_id=hex(len(song.audio[0]))
23
+ import os
24
+ if not os.path.exists('beat_manipulator/beatmaps'):
25
+ os.mkdir('beat_manipulator/beatmaps')
26
+ cacheDir="beat_manipulator/beatmaps/" + filename + "_"+lib+"_"+audio_id+'.txt'
27
+ try:
28
+ beatmap=np.loadtxt(cacheDir)
29
+ if log is True: print('loaded cached beatmap.')
30
+ except OSError:
31
+ if log is True:print("beatmap hasn't been generated yet. Generating...")
32
+ beatmap = None
33
+
34
+ if beatmap is None:
35
+ if 'madmom' in lib.lower():
36
+ from collections.abc import MutableMapping, MutableSequence
37
+ import madmom
38
+ assert len(song.audio[0])>song.sr*2, f'Audio file is too short, len={len(song.audio[0])} samples, or {len(song.audio[0])/song.sr} seconds. Minimum length is 2 seconds, audio below that breaks madmom processors.'
39
+ if lib=='madmom.RNNBeatProcessor':
40
+ proc = madmom.features.beats.RNNBeatProcessor()
41
+ beatmap = proc(madmom.audio.signal.Signal(song.audio.T, song.sr))
42
+ elif lib=='madmom.MultiModelSelectionProcessor':
43
+ proc = madmom.features.beats.RNNBeatProcessor(post_processor=None)
44
+ predictions = proc(madmom.audio.signal.Signal(song.audio.T, song.sr))
45
+ mm_proc = madmom.features.beats.MultiModelSelectionProcessor(num_ref_predictions=None)
46
+ beatmap= mm_proc(predictions)*song.sr
47
+ beatmap/= np.max(beatmap)
48
+ elif lib=='stunlocked':
49
+ spikes = np.abs(np.gradient(np.clip(song.audio[0], -1, 1)))[:int(len(song.audio[0]) - (len(song.audio[0])%int(song.sr/100)))]
50
+ spikes = spikes.reshape(-1, (int(song.sr/100)))
51
+ spikes = np.asarray(list(np.max(i) for i in spikes))
52
+ if len(beatmap) > len(spikes): beatmap = beatmap[:len(spikes)]
53
+ elif len(spikes) > len(beatmap): spikes = spikes[:len(beatmap)]
54
+ zeroing = 0
55
+ for i in range(len(spikes)):
56
+ if zeroing > 0:
57
+ if spikes[i] <= 0.1: zeroing -=1
58
+ spikes[i] = 0
59
+ elif spikes[i] >= 0.1:
60
+ spikes[i] = 1
61
+ zeroing = 7
62
+ if spikes[i] <= 0.1: spikes[i] = 0
63
+ beatmap = spikes
64
+
65
+ if caching is True: np.savetxt(cacheDir, beatmap)
66
+
67
+ if add_peaks is True:
68
+ spikes = np.abs(np.gradient(np.clip(song.audio[0], -1, 1)))[:int(len(song.audio[0]) - (len(song.audio[0])%int(song.sr/100)))]
69
+ spikes = spikes.reshape(-1, (int(song.sr/100)))
70
+ spikes = np.asarray(list(np.max(i) for i in spikes))
71
+ if len(beatmap) > len(spikes): beatmap = beatmap[:len(spikes)]
72
+ elif len(spikes) > len(beatmap): spikes = spikes[:len(beatmap)]
73
+ zeroing = 0
74
+ for i in range(len(spikes)):
75
+ if zeroing > 0:
76
+ if spikes[i] <= 0.1: zeroing -=1
77
+ spikes[i] = 0
78
+ elif spikes[i] >= 0.1:
79
+ spikes[i] = 1
80
+ zeroing = 7
81
+ if spikes[i] <= 0.1: spikes[i] = 0
82
+ else: spikes = None
83
+
84
+ def _process(song: main.song, beatmap, spikes, threshold):
85
+ '''ඞ'''
86
+ if add_peaks is True: beatmap += spikes
87
+ hitmap=[]
88
+ actual_samplerate=int(song.sr/100)
89
+ beat_middle=int(actual_samplerate/2)
90
+ for i in range(len(beatmap)):
91
+ if beatmap[i]>threshold: hitmap.append(i*actual_samplerate + beat_middle)
92
+ hitmap=np.asarray(hitmap)
93
+ clump=[]
94
+ for i in range(len(hitmap)-1):
95
+ #print(i, abs(song.beatmap[i]-song.beatmap[i+1]), clump)
96
+ if abs(hitmap[i] - hitmap[i+1]) < song.sr/16 and i != len(hitmap)-2: clump.append(i)
97
+ elif clump!=[]:
98
+ clump.append(i)
99
+ actual_time=hitmap[clump[0]]
100
+ hitmap[np.array(clump)]=0
101
+ #print(song.beatmap)
102
+ hitmap[clump[0]]=actual_time
103
+ clump=[]
104
+
105
+ hitmap=hitmap[hitmap!=0]
106
+ return hitmap
107
+
108
+ osufile=lambda title,artist,version: ("osu file format v14\n"
109
+ "\n"
110
+ "[General]\n"
111
+ f"AudioFilename: {song.path.split('/')[-1]}\n"
112
+ "AudioLeadIn: 0\n"
113
+ "PreviewTime: -1\n"
114
+ "Countdown: 0\n"
115
+ "SampleSet: Normal\n"
116
+ "StackLeniency: 0.5\n"
117
+ "Mode: 0\n"
118
+ "LetterboxInBreaks: 0\n"
119
+ "WidescreenStoryboard: 0\n"
120
+ "\n"
121
+ "[Editor]\n"
122
+ "DistanceSpacing: 1.1\n"
123
+ "BeatDivisor: 4\n"
124
+ "GridSize: 8\n"
125
+ "TimelineZoom: 1.6\n"
126
+ "\n"
127
+ "[Metadata]\n"
128
+ f"Title:{title}\n"
129
+ f"TitleUnicode:{title}\n"
130
+ f"Artist:{artist}\n"
131
+ f"ArtistUnicode:{artist}\n"
132
+ f'Creator:{lib} + BeatManipulator\n'
133
+ f'Version:{version} {lib}\n'
134
+ 'Source:\n'
135
+ 'Tags:BeatManipulator\n'
136
+ 'BeatmapID:0\n'
137
+ 'BeatmapSetID:-1\n'
138
+ '\n'
139
+ '[Difficulty]\n'
140
+ 'HPDrainRate:4\n'
141
+ 'CircleSize:4\n'
142
+ 'OverallDifficulty:5\n'
143
+ 'ApproachRate:10\n'
144
+ 'SliderMultiplier:3.3\n'
145
+ 'SliderTickRate:1\n'
146
+ '\n'
147
+ '[Events]\n'
148
+ '//Background and Video events\n'
149
+ '//Break Periods\n'
150
+ '//Storyboard Layer 0 (Background)\n'
151
+ '//Storyboard Layer 1 (Fail)\n'
152
+ '//Storyboard Layer 2 (Pass)\n'
153
+ '//Storyboard Layer 3 (Foreground)\n'
154
+ '//Storyboard Layer 4 (Overlay)\n'
155
+ '//Storyboard Sound Samples\n'
156
+ '\n'
157
+ '[TimingPoints]\n'
158
+ '0,140.0,4,1,0,100,1,0\n'
159
+ '\n'
160
+ '\n'
161
+ '[HitObjects]\n')
162
+ # remove the clumps
163
+ #print(self.beatmap)
164
+
165
+ #print(self.beatmap)
166
+
167
+
168
+ #print(len(osumap))
169
+ #input('banana')
170
+ import shutil, os
171
+ if os.path.exists('beat_manipulator/temp'): shutil.rmtree('beat_manipulator/temp')
172
+ os.mkdir('beat_manipulator/temp')
173
+ hitmap=[]
174
+ import random
175
+ for difficulty in difficulties:
176
+ for i in range(4):
177
+ #print(i)
178
+ this_difficulty=_process(song, beatmap, spikes, difficulty)
179
+ hitmap.append(this_difficulty)
180
+
181
+ for k in range(len(hitmap)):
182
+ osumap=np.vstack((hitmap[k],np.zeros(len(hitmap[k])),np.zeros(len(hitmap[k])))).T
183
+ difficulty= difficulties[k]
184
+ for i in range(len(osumap)-1):
185
+ if i==0:continue
186
+ dist=(osumap[i,0]-osumap[i-1,0])*(1-(difficulty**0.3))
187
+ if dist<1000: dist=0.005
188
+ elif dist<2000: dist=0.01
189
+ elif dist<3000: dist=0.015
190
+ elif dist<4000: dist=0.02
191
+ elif dist<5000: dist=0.25
192
+ elif dist<6000: dist=0.35
193
+ elif dist<7000: dist=0.45
194
+ elif dist<8000: dist=0.55
195
+ elif dist<9000: dist=0.65
196
+ elif dist<10000: dist=0.75
197
+ elif dist<12500: dist=0.85
198
+ elif dist<15000: dist=0.95
199
+ elif dist<20000: dist=1
200
+ #elif dist<30000: dist=0.8
201
+ prev_x=osumap[i-1,1]
202
+ prev_y=osumap[i-1,2]
203
+ if prev_x>0: prev_x=prev_x-dist*0.1
204
+ elif prev_x<0: prev_x=prev_x+dist*0.1
205
+ if prev_y>0: prev_y=prev_y-dist*0.1
206
+ elif prev_y<0: prev_y=prev_y+dist*0.1
207
+ dirx=random.uniform(-dist,dist)
208
+ diry=dist-abs(dirx)*random.choice([-1, 1])
209
+ if abs(prev_x+dirx)>1: dirx=-dirx
210
+ if abs(prev_y+diry)>1: diry=-diry
211
+ x=prev_x+dirx
212
+ y=prev_y+diry
213
+ #print(dirx,diry,x,y)
214
+ #print(x>1, x<1, y>1, y<1)
215
+ if x>1: x=0.8
216
+ if x<-1: x=-0.8
217
+ if y>1: y=0.8
218
+ if y<-1: y=-0.8
219
+ #print(dirx,diry,x,y)
220
+ osumap[i,1]=x
221
+ osumap[i,2]=y
222
+
223
+ osumap[:,1]*=300
224
+ osumap[:,1]+=300
225
+ osumap[:,2]*=180
226
+ osumap[:,2]+=220
227
+
228
+ file=osufile(artist, title, difficulty)
229
+ for j in osumap:
230
+ #print('285,70,'+str(int(int(i)*1000/self.samplerate))+',1,0')
231
+ file+=f'{int(j[1])},{int(j[2])},{str(int(int(j[0])*1000/song.sr))},1,0\n'
232
+ with open(f'beat_manipulator/temp/{artist} - {title} (BeatManipulator {difficulty} {lib}].osu', 'x', encoding="utf-8") as f:
233
+ f.write(file)
234
+ from . import io
235
+ import shutil, os
236
+ shutil.copyfile(song.path, 'beat_manipulator/temp/'+filename)
237
+ shutil.make_archive('beat_manipulator_osz', 'zip', 'beat_manipulator/temp')
238
+ outputname = io._outputfilename(path = output, filename = song.path, suffix = ' ('+lib + ')', ext = 'osz')
239
+ if not os.path.exists(outputname):
240
+ os.rename('beat_manipulator_osz.zip', outputname)
241
+ if log is True: print(f'Created `{outputname}`')
242
+ else: print(f'{outputname} already exists!')
243
+ shutil.rmtree('beat_manipulator/temp')
244
+ return outputname
beat_manipulator/parse.py ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .utils import C_SLICE, C_JOIN, C_MISC, C_MATH
2
+ import numpy as np
3
+ from . import io, utils, main
4
+ def _getnum(pattern, cur, symbols = '+-*/'):
5
+ number = ''
6
+ while pattern[cur].isdecimal() or pattern[cur] in symbols:
7
+ number+=pattern[cur]
8
+ cur+=1
9
+ return number, cur-1
10
+
11
+ def parse(pattern:str, samples:dict, pattern_length:int = None,
12
+ c_slice:str = C_SLICE,
13
+ c_join:str = C_JOIN,
14
+ c_misc:str = C_MISC,
15
+ log = True,
16
+ simple_mode = False):
17
+ """Returns (beats, operators, pattern_length, c_slice, c_misc, c_join)"""
18
+ if log is True: print(f'Beatswapping with `{pattern}`')
19
+
20
+ #load samples:
21
+ if isinstance(samples, str): samples = (samples,)
22
+ if not isinstance(samples, dict):
23
+ samples = {str(i+1):samples[i] for i in range(len(samples))}
24
+
25
+ #preprocess pattern
26
+ separator = c_join[0]
27
+ #forgot separator
28
+ if simple_mode is True:
29
+ if c_join[0] not in pattern and c_join[1] not in pattern and c_join[2] not in pattern and c_join[3] not in pattern: pattern = pattern.replace(' ', separator)
30
+ if ' ' not in c_join: pattern = pattern.replace(' ', '') # ignore spaces
31
+ for i in c_join:
32
+ while i+i in pattern: pattern = pattern.replace(i+i, i) #double separator
33
+ while pattern.startswith(i): pattern = pattern[1:]
34
+ while pattern.endswith(i): pattern = pattern[:-1]
35
+
36
+ # Creates a list of beat strings so that I can later see if there is a `!` in the string
37
+ separated = pattern
38
+ for i in c_join:
39
+ separated = separated.replace(i, c_join[0])
40
+ separated = separated.split(c_join[0])
41
+ pattern = pattern.replace(c_misc[6], '')
42
+
43
+ # parsing
44
+ length = 0
45
+ num = ''
46
+ cur = 0
47
+ beats = []
48
+ operators = [separator]
49
+ shuffle_beats = []
50
+ shuffle_groups = []
51
+ current_beat = 0
52
+ effect = None
53
+ pattern += ' '
54
+ sample_toadd = None
55
+
56
+ # Loops over all characters
57
+ while cur < len(pattern):
58
+ char = pattern[cur]
59
+ #print(f'char = {char}, cur = {cur}, num = {num}, current_beat = {current_beat}, effect = {effect}, len(beats) = {len(beats)}, length = {length}')
60
+ if char == c_misc[3]: char = str(current_beat+1) # Replaces `i` with current number
61
+
62
+ # If character is `", ', `, or [`: searches for closing quote and gets the sample rate,
63
+ # moves cursor to the character after last quote/bracket, creates a sample_toadd variable with the sample.
64
+ elif char in c_misc[0:3]+c_misc[10:12]:
65
+ quote = char
66
+ if quote == c_misc[10]: quote = c_misc[11] # `[` is replaced with `]`
67
+ cur += 1
68
+ sample = ''
69
+
70
+ # Gets sample name between quote characters, moves cursor to the ending quote.
71
+ while pattern[cur] != quote:
72
+ sample += pattern[cur]
73
+ cur += 1
74
+ assert sample in samples, f"No sample named `{sample}` found in samples. Available samples: {samples.keys()}"
75
+
76
+ # If sample is a song, it will be converted to a song if needed, and beatmap will be generated
77
+ if quote == c_misc[11]:
78
+ if not isinstance(samples[sample], main.song): samples[sample] = main.song(samples[sample])
79
+ if samples[sample].beatmap is None:
80
+ samples[sample].beatmap_generate()
81
+ samples[sample].beatmap_adjust()
82
+
83
+ # Else sample is a sound file
84
+ elif not isinstance(samples[sample], np.ndarray): samples[sample] = io._load(samples[sample])[0]
85
+
86
+ sample_toadd = [samples[sample], [], quote, None] # Creates the sample_toadd variable
87
+ cur += 1
88
+ char = pattern[cur]
89
+
90
+ # If character is a math character, a slice character, or `@_?!%` - random, not count, skip, create variable -
91
+ # - it gets added to `num`, and the loop repeats.
92
+ # _safer_eval only takes part of the expression to the left of special characters (@%#), so it won't affect length calculation
93
+ if char.isdecimal() or char in (C_MATH + c_slice + c_misc[4:8] + c_misc[9]):
94
+ num += char
95
+ #print(f'char = {char}, added it to num: num = {num}')
96
+
97
+ # If character is `%` and beat hasn't been created yet, it takes the next character as well
98
+ if char == c_misc[7] and len(beats) == current_beat:
99
+ cur += 1
100
+ char = pattern[cur]
101
+ num += char
102
+
103
+ # If character is a shuffle character `#` + math expression, beat number gets added to `shuffle_beats`,
104
+ # beat shuffle group gets added to `shuffle_groups`, cursor is moved to the character after the math expression, and loop repeats.
105
+ # That means operations after this will only execute once character is not a math character.
106
+ elif char == c_misc[8]:
107
+ cur+=1
108
+ number, cur = _getnum(pattern, cur)
109
+ char = pattern[cur]
110
+ shuffle_beats.append(current_beat)
111
+ shuffle_groups.append(number)
112
+
113
+ # If character is not math/shuffle, that means math expression has ended. Now it tries to figure out where the expression belongs,
114
+ # and parses the further characters
115
+ else:
116
+
117
+ # If the beat has not been added, it adds the beat. Also figures out pattern length.
118
+ if len(beats) == current_beat and len(num) > 0:
119
+ # Checks all slice characters in the beat expression. If slice character is found, splits the slice and breaks.
120
+ for c in c_slice:
121
+ if c in num:
122
+ num = num.split(c)[:2] + [c]
123
+ #print(f'slice: split num by `{c}`, num = {num}, whole beat is {separated[current_beat]}')
124
+ if pattern_length is None and c_misc[6] not in separated[current_beat]:
125
+ num0, num1 = utils._safer_eval(num[0]), utils._safer_eval(num[1])
126
+ if c == c_slice[0]: length = max(num0, num1, length)
127
+ if c == c_slice[1]: length = max(num0-1, num0+num1-1, length)
128
+ if c == c_slice[2]: length = max(num0-num1, num0, length)
129
+ break
130
+ # If it didn't break, the expression is not a slice, so it pattern length is just compared with the beat number.
131
+ else:
132
+ #print(f'single beat: {num}. Whole beat is {separated[current_beat]}')
133
+ if c_misc[6] not in separated[current_beat]: length = max(utils._safer_eval(num), length)
134
+
135
+ # If there no sample saved in `sample_toadd`, adds the beat to list of beats.
136
+ if sample_toadd is None: beats.append([num, []])
137
+ # If `sample_toadd` is not None, beat is a sample/song. Adds the beat and sets sample_toadd to None
138
+ else:
139
+ sample_toadd[3] = num
140
+ beats.append(sample_toadd)
141
+ sample_toadd = None
142
+ #print(f'char = {char}, got num = {num}, appended beat {len(beats)}')
143
+
144
+ # Sample might not have a `num` with a slice, this adds the sample without a slice
145
+ elif len(beats) == current_beat and len(num) == 0 and sample_toadd is not None:
146
+ beats.append(sample_toadd)
147
+ sample_toadd = None
148
+
149
+ # If beat has been added, it now parses beats.
150
+ if len(beats) == current_beat+1:
151
+ #print(f'char = {char}, parsing effects:')
152
+
153
+ # If there is an effect and current character is not a math character, effect and value are added to current beat, and effect is set to None
154
+ if effect is not None:
155
+ #print(f'char = {char}, adding effect: type = {effect}, value = {num}')
156
+ beats[current_beat][1].append([effect, num if num!='' else None])
157
+ effect = None
158
+
159
+ # If current character is a letter, it sets that letter to `effect` variable.
160
+ # Since loop repeats after that, that while current character is a math character, it gets added to `num`.
161
+ if char.isalpha() and effect is None:
162
+ #print(f'char = {char}, effect type is {effect}')
163
+ effect = char
164
+
165
+ # If character is a beat separator, it starts parsing the next beat in the next loop.
166
+ if char in c_join and len(beats) == current_beat + 1:
167
+ #print(f'char = {char}, parsing next beat')
168
+ current_beat += 1
169
+ effect = None
170
+ operators.append(char)
171
+
172
+ num = '' # `num` is set to empty string. btw `num` is only used in this loop so it needs to be here
173
+
174
+ cur += 1 # cursor goes to the next character
175
+
176
+
177
+ #for i in beats: print(i)
178
+ import math
179
+ if pattern_length is None: pattern_length = int(math.ceil(length))
180
+
181
+ return beats, operators, pattern_length, shuffle_groups, shuffle_beats, c_slice, c_misc, c_join
182
+
183
+ # I can't be bothered to annotate this one. It just works, okay?
184
+ def _random(beat:str, length:int, rchar = C_MISC[4], schar = C_MISC[5]) -> str:
185
+ """Takes a string and replaces stuff like `@1_4_0.5` with randomly generated number where 1 - start, 4 - stop, 0.5 - step. Returns string."""
186
+ import random
187
+ beat+=' '
188
+ while rchar in beat:
189
+ rand_index = beat.find(rchar)+1
190
+ char = beat[rand_index]
191
+ number = ''
192
+ while char.isdecimal() or char in '.+-*/':
193
+ number += char
194
+ rand_index+=1
195
+ char = beat[rand_index]
196
+ if number != '': start = utils._safer_eval(number)
197
+ else: start = 0
198
+ if char == schar:
199
+ rand_index+=1
200
+ char = beat[rand_index]
201
+ number = ''
202
+ while char.isdecimal() or char in '.+-*/':
203
+ number += char
204
+ rand_index+=1
205
+ char = beat[rand_index]
206
+ if number != '': stop = utils._safer_eval(number)
207
+ else: stop = length
208
+ if char == schar:
209
+ rand_index+=1
210
+ char = beat[rand_index]
211
+ number = ''
212
+ while char.isdecimal() or char in '.+-*/':
213
+ number += char
214
+ rand_index+=1
215
+ char = beat[rand_index]
216
+ if number != '': step = utils._safer_eval(number)
217
+ else: step = length
218
+ choices = []
219
+ while start <= stop:
220
+ choices.append(start)
221
+ start+=step
222
+ beat = list(beat)
223
+ beat[beat.index(rchar):rand_index] = list(str(random.choice(choices)))
224
+ beat = ''.join(beat)
225
+ return beat
226
+
227
+ def _shuffle(pattern: list, shuffle_beats: list, shuffle_groups: list) -> list:
228
+ """Shuffles pattern according to shuffle_beats and shuffle_groups"""
229
+ import random
230
+ done = []
231
+ result = pattern.copy()
232
+ for group in shuffle_groups:
233
+ if group not in done:
234
+ shuffled = [i for n, i in enumerate(shuffle_beats) if shuffle_groups[n] == group]
235
+ unshuffled = shuffled.copy()
236
+ random.shuffle(shuffled)
237
+ for i in range(len(shuffled)):
238
+ result[unshuffled[i]] = pattern[shuffled[i]]
239
+ done.append(group)
240
+ return result
241
+
242
+ def _metric_get(v, beat, metrics, c_misc7 = C_MISC[7]):
243
+ assert v[v.find(c_misc7)+1] in metrics, f'`%{v[v.find(c_misc7)+1]}`: No metric called `{v[v.find(c_misc7)+1]}` found in metrics. Available metrics: {metrics.keys()}'
244
+ metric = metrics[v[v.find(c_misc7)+1]](beat)
245
+ return metric
246
+
247
+
248
+ def _metric_replace(v, metric, c_misc7 = C_MISC[7]):
249
+ for _ in range(v.count(c_misc7)):
250
+ v= v[:v.find(c_misc7)] + str(metric) + v[v.find(c_misc7)+2:]
251
+ return v
beat_manipulator/presets.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from . import main, utils
2
+ BM_SAMPLES = {'cowbell' : 'beat_manipulator/samples/cowbell.flac',
3
+ }
4
+
5
+ presets = {}
6
+
7
+ def presets_load(path, mode = 'add'):
8
+ global presets
9
+ import yaml
10
+ with open(path, 'r') as f:
11
+ yaml_presets = yaml.safe_load(f.read())
12
+
13
+ # if mode.lower() == 'add':
14
+ # presets = presets | yaml_presets
15
+ # elif mode.lower() == 'replace':
16
+ presets = yaml_presets
17
+
18
+ presets_load('beat_manipulator/presets.yaml')
19
+
20
+ def _beatswap(song, pattern, pattern_name, scale = 1, shift = 0, output = '', modify = False):
21
+ if isinstance(scale, str):
22
+ if ',' in scale: scale = scale.replace(' ', '').split(',')
23
+ elif not isinstance(scale, list): scale = [scale]
24
+ if modify is False:
25
+ for i in scale:
26
+ main.beatswap(song, pattern = pattern, scale = i, shift = shift, output=output, suffix = f' ({pattern_name}{(" x"+str(round(utils._safer_eval(i), 4))) * (len(scale)>1)})', copy = True)
27
+ else:
28
+ assert isinstance(song, main.song), f"In order to modify a song, it needs to be of a main.song type, but it is {type(song)}"
29
+ song.beatswap(pattern, scale = scale[0], shift = shift)
30
+ return song
31
+
32
+ def get(preset):
33
+ """returns (pattern, scale, shift)"""
34
+ global presets
35
+ assert preset in presets, f"{preset} not found in presets."
36
+ preset = presets[preset]
37
+ return preset['pattern'], preset['scale'] if 'scale' in preset else 1, preset['shift'] if 'shift' in preset else 0
38
+
39
+ def use(song, preset, output = '', scale = 1, shift = 0):
40
+ global presets
41
+ assert preset in presets, f"{preset} not found in presets."
42
+ preset_name = preset
43
+ preset = presets[preset]
44
+ if not isinstance(song, main.song): song = main.song(song)
45
+ if isinstance(list(preset.values())[0], dict):
46
+ for i in preset.values():
47
+ if 'sample' in i:
48
+ pass
49
+ elif 'sidechain' in i:
50
+ pass
51
+ else:
52
+ song = _beatswap(song, pattern = i['pattern'], scale = scale*(i['scale'] if 'scale' in i else 1), shift = shift*(i['shift'] if 'shift' in i else 0), output = output, modify = True, pattern_name = preset_name)
53
+ song.write(output, suffix = f' ({preset})')
54
+ else:
55
+ if 'sample' in preset:
56
+ pass
57
+ elif 'sidechain' in preset:
58
+ pass
59
+ else:
60
+ _beatswap(song, pattern = preset['pattern'], scale = scale*(preset['scale'] if 'scale' in preset else 1), shift = shift*(preset['shift'] if 'shift' in preset else 0), output = output, modify = False, pattern_name = preset_name)
61
+
62
+ def use_all(song, output = ''):
63
+ if not isinstance(song, main.song): song = main.song(song)
64
+ for key in presets.keys():
65
+ print(f'__ {key} __')
66
+ use(song, key, output = output)
67
+ print()
68
+
69
+ def test(song, scale = 1, shift = 0, adjust = 0, output = '', load_settings = False):
70
+ song = main.song(song)
71
+ song.beatmap_generate(load_settings = load_settings)
72
+ song.beatswap('test', scale = scale, shift = shift, adjust = 500+adjust)
73
+ song.write(output = output, suffix = ' (test)')
74
+
75
+ def save(song, scale = 1, shift = 0, adjust = 0):
76
+ song = main.song(song)
77
+ song.beatmap_save_settings(scale = scale, shift = shift, adjust = adjust)
78
+
79
+ def savetest(song, scale = 1, shift = 0, adjust = 0, output = '', load_settings = False):
80
+ song = main.song(song)
81
+ song.beatmap_generate(load_settings = load_settings)
82
+ song.beatswap('test', scale = scale, shift = shift, adjust = 500+adjust)
83
+ song.write(output = output, suffix = ' (test)')
84
+ song.beatmap_save_settings(scale = scale, shift = shift, adjust = adjust)
beat_manipulator/presets.yaml ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Presets. `Scales` can be a list of all scales that the pattern works with.
2
+ # ________________ BASIC ________________
3
+ 2x speed:
4
+ pattern: 1>0.5
5
+ scale: 1, 0.5, 1/3, 0.25
6
+
7
+ 3x speed:
8
+ pattern: 1>1/3
9
+ scale: 1, 0.5
10
+
11
+ 4x speed:
12
+ pattern: 1>0.25
13
+ scale: 1, 0.5
14
+
15
+ 6x speed:
16
+ pattern: 1>1/6
17
+ scale: 1, 0.5
18
+
19
+ 8x speed:
20
+ pattern: 1>0.125
21
+ scale: 1, 0.5
22
+
23
+ 1.33x faster:
24
+ pattern: 1>0.75
25
+ scale: 1, 2/3, 0.5, 1/3, 0.25
26
+
27
+ 1.5x faster:
28
+ pattern: 1>2/3
29
+ scale: 1, 2/3
30
+
31
+ 1.5x slower:
32
+ pattern: 1>0.5, 1<0.5r, 1<0.5
33
+ scale: 2, 1, 0.75, 0.5
34
+
35
+ 1.33x slower:
36
+ pattern: 1>2/3, 1<1/3r, 1<1/3
37
+ scale: 2, 1
38
+
39
+ reverse:
40
+ pattern: reverse
41
+ scale: 8, 4, 2, 1, 0.5, 1/3, 0.25, 0.2, 1/7, 0.125
42
+
43
+ reverse 8 beats:
44
+ pattern: 8, 7, 6, 5, 4, 3, 2, 1
45
+ scale: 1, 0.5
46
+
47
+ shuffle:
48
+ pattern: shuffle
49
+
50
+ shuffle 3 beats:
51
+ pattern: 1#1, 2#1, 3#1
52
+ scale: 2, 1, 0.75, 0.5, 0.25, 0.2, 0.125
53
+
54
+ shuffle 4 beats:
55
+ pattern: 1#1, 2#1, 3#1, 4#1
56
+ scale: 2, 1, 0.75, 0.5, 0.25, 0.125
57
+
58
+ shuffle 8 beats:
59
+ pattern: 1#1, 2#1, 3#1, 4#1, 5#1, 6#1, 7#1, 8#1
60
+ scale: 2, 1, 0.75, 0.5, 0.25
61
+
62
+ shuffle alternate:
63
+ pattern: 1#1, 2#2, 3#1, 4#2, 5#1, 6#2, 7#1, 8#2
64
+ scale: 1, 0.125
65
+
66
+ shuffle mix:
67
+ pattern: i#1, i#2, i#3, i#4, i#1, i#2, i#3, i#4, i#1, i#2, i#3, i#4, i#1, i#2, i#3, i#4
68
+ scale: 1, 0.5
69
+
70
+ 3 bars mix:
71
+ pattern: i, i+4?, i+8?, 3!
72
+ scale: 1, 0.5, 1/3
73
+
74
+ 4 bars mix:
75
+ pattern: i, i+4?, i+8?, i+12?, 4!
76
+ scale: 1, 0.5
77
+
78
+ 6 bars mix:
79
+ pattern: i, i+4?, i+8?, i+12?, i+16?, i+20?, 6!
80
+ scale: 1, 0.5
81
+
82
+ 8 bars mix:
83
+ pattern: i, i+4?, i+8?, i+12?, i+16?, i+20?, i+24?, i+28?, 8!
84
+ scale: 1, 0.5
85
+
86
+ 2 in 1:
87
+ pattern: 1; 2
88
+ scale: 8, 6, 4, 3, 2, 1, 0.5, 0.25
89
+
90
+ 3 in 1:
91
+ pattern: 1; 2; 3
92
+ scale: 4, 3, 1
93
+
94
+ 4 in 1:
95
+ pattern: 1; 2; 3; 4
96
+ scale: 4, 1
97
+
98
+ 5 in 1:
99
+ pattern: 1;2;3;4;5
100
+ scale: 2, 1
101
+
102
+ 2 in 1 reverse:
103
+ pattern: 1;2r?
104
+ scale: 8, 3, 1, 0.75, 0.5, 0.25, 0.125
105
+
106
+ reverse mix:
107
+ pattern: 1;1r
108
+ scale: 4, 1, 0.75, 0.5
109
+
110
+ random:
111
+ pattern: random
112
+ scale: 2, 1, 0.5, 0.25, 0.125
113
+ description: generates a new random pattern each time
114
+
115
+ kicks only:
116
+ pattern: 1>0.5
117
+ scale: 2
118
+ description: plays only kicks
119
+
120
+ kicks only double-time:
121
+ pattern: 1>0.25
122
+ scale: 2
123
+ description: plays only kicks
124
+
125
+ snares only:
126
+ pattern: 1<0.5
127
+ scale: 2
128
+ description: plays only snares
129
+
130
+ no main drums:
131
+ pattern: 1<0.5
132
+ scale: 1, 0.5, 0.25, 0.125
133
+ description: skips kicks and snares
134
+
135
+ # ________________ STRUCTURES ________________
136
+ half-time:
137
+ pattern: 1,2,4,5, | 3,6,8,7, | 9,11,12,13, | 15,13,14,16
138
+ scale: 1, 0.5 #0.25
139
+ description: halves the BPM
140
+
141
+ quarter-time 1:
142
+ pattern: 1,2,4,5,|6,8,9,10,|11,12,13,14,|16,14,15,16
143
+ scale: 0.5
144
+ description: 4 times lower BPM
145
+
146
+ quarter-time 2:
147
+ pattern: 1,2,4,5, | 6,8,10,12, | 11,10,12,13, | 9,13,14,16
148
+ scale: 0.5 #0.25
149
+ description: 4 times lower BPM, with a syncopated structure
150
+
151
+ dotted snares 1:
152
+ pattern: 1, 2>0.5, 3, 4>0.5, 5, 6>0.5, 3, 4>0.5, 7, 8
153
+ scale: 2, 1
154
+ description: Plays 5 snares in a 4/3 syncopation, a rhythm commonly used in drum&bass
155
+
156
+ dotted snares 2:
157
+ pattern: 1, 2, 3 , 4, | 5, 7 , 6, 8, | 11 , 10, 9, 11 , | 13, 14, 15 , 16
158
+ scale: 0.5 #0.25
159
+ description: Plays 5 snares in a 4/3 syncopation, a rhythm commonly used in drum&bass. This one only swaps snares and preserves the original rhythm better.
160
+
161
+ dotted snares 2 long:
162
+ pattern: 1, 2, 3 , 4, | 5, 7 , 6, 8, | 11 , 10, 9, 11 , | 13, 14, 15 , 16, | 17, 19 , 18, 20, | 23 , 22, 21, 23 , | 25, 26, 27 , 28, | 29, 31 , 30, 32
163
+ scale: 0.5 #0.25
164
+ description: Plays 10 snares in a long 4/3 syncopation, a rhythm commonly used in drum&bass/darkstep.
165
+
166
+ dotted snares 2 longer:
167
+ pattern: 1, 2, 3 , 4, | 5, 7 , 6, 8, | 11 , 10, 9, 11 , | 13, 14, 15 , 16, | 17, 19 , 18, 20, | 23 , 22, 21, 23 , | 25, 26, 27 , 28, | 29, 31 , 30, 32
168
+ scale: 0.5 #0.25
169
+ description: Plays 20 snares in a very long 4/3 syncopation to create a rolling, jazzy feel.
170
+
171
+ dotted snares fast 1:
172
+ pattern: 1, 2, 3, 5>0.5, 7, 5>0.5, 11, 5>0.5, 7, 5>0.5, 11, 9>0.5, 7, 9>0.5, 11, 9>0.5, 7, 9>0.5, 11, 16
173
+ scale: 0.5
174
+ description: Plays 10 snares in a fast 4/3 syncopation, a rhythm commonly used in drum&bass
175
+
176
+ dotted snares fast 2:
177
+ pattern: 1, 2>0.75, 2>0.25, 1.25:1.75;3, 4>0.75, 4>0.75, 4>0.75, 4>0.25, 3.25:3.75;5, 6>0.75, 6>0.75;7, 8>0.75, 6>0.25;8<0.25
178
+ scale: 1
179
+ description: Plays 10 snares in a fast 4/3 syncopation, a rhythm commonly used in drum&bass
180
+
181
+ dotted kicks:
182
+ pattern: 1>0.75, 1>0.25, 2>0.5, 1>0.75, 1>0.75, 4>0.75, 1>0.75, 1>0.5, 6>0.25, 1>0.75, 1>0.75, 1>0.25, 8>0.5, 1>0.5
183
+ scale: 1
184
+ description: Plays the first beat in a 4/3 syncopation while preserving snare beats.
185
+
186
+ dotted kicks 2:
187
+ pattern: 0:0.75, 0.75:1.5;0:0.75, 1.5:2.25;0:0.75, 2.25:3;0:0.75, 3:3.75;0:0.75, 3.75:4.5;0:0.75, 4.5:5.25;0:0.75, 5.25:6;0:0.75, 6:6.75;0:0.75, 6.75:7.5;0:0.75, 7.5:8;0:0.5
188
+ scale: 1
189
+ description: Plays a 4/3 syncopated first beat on top of normal beats.
190
+
191
+ tripple dotted: #try shifts
192
+ pattern: 1>0.375, 1>0.375, 1>0.25
193
+ scale: 8, 4, 2, 1, 0.5
194
+ description: Each beat turns into three 4/3 syncopated notes. Can be somewhat similar to footwork.
195
+
196
+ tripple dotted snares:
197
+ pattern: 1>0.375, 1>0.375, 1>0.25
198
+ scale: 8, 4, 2
199
+ shift: 1
200
+ description: plays only the snare beats in three 4/3 syncopated notes, a rhythm used in drum&bass/jungle.
201
+
202
+ dotted structure:
203
+ pattern: 1>0.75, 2>0.75, 4>0.5
204
+ scale: 4, 2, 1 #0.5
205
+ description: plays the significant drum beats as three 4/3 syncopated notes. Similar to moombahton but without the second kick.
206
+
207
+ dotted chaos 1:
208
+ pattern: 1>1/3
209
+ scale: 0.75
210
+
211
+ dotted chaos 2:
212
+ pattern: 1>1/6
213
+ scale: 0.75
214
+
215
+ dotted pattern 1:
216
+ pattern: 1, 2>0.75, 2>0.25, 1.25:1.75;3, 2>0.75, 2>0.75, 2>0.75, 2>0.25, 1.25:1.75;5, 2>0.75, 2>0.75;7, 2>0.75, 2>0.25;8<0.25
217
+ scale: 0.5
218
+ description: plays part between first kick and snare in a 4/3 syncopation, with original drums on top.
219
+
220
+ dotted pattern 2:
221
+ pattern: 1, 2>0.75, 2>0.25, 1.25:1.75;3, 4>0.75, 4>0.75, 4>0.75, 4>0.25, 3.25:3.75;5, 6>0.75, 6>0.75;7, 8>0.75, 6>0.25;8<0.25
222
+ scale: 0.5
223
+ description: plays parts between each kick and snare in a 4/3 syncopation, with original drums on top.
224
+
225
+ # ________________ TIME SIGNATURES ________________
226
+ 4-3:
227
+ pattern: 1>2/3, 2>2/3, 2>2/3
228
+ scale: 2, 1, 0.5
229
+ description: 4/3 time signature, preserves length
230
+
231
+ 3-4:
232
+ pattern: 1>0.75
233
+ scale: 8, 4, 3, 2
234
+ description: plays 3 beats out of each 4, creating 3/4 time signature
235
+
236
+ 4-7 1:
237
+ pattern: 1, 2, 3, 4>0.5
238
+ scale: 2, 1, 0.5, 0.25, 0.125
239
+ description: cuts 4th beat in half, creating 4/7 time signature
240
+
241
+ 4-7 2:
242
+ pattern: 1, 2, 3, 4>0.25, 3.75:4, 5,6>0.5, 7, 8
243
+ scale: 2, 1, 0.5, 0.25, 0.125
244
+ description: alternates between cutting 2nd and 4th beats in half, creating a natural 4/7 time signature
245
+
246
+ 4-13:
247
+ pattern: 1, 2, 3, 4>0.25
248
+ scale: 4, 2, 1, 0.5
249
+ description: abruptly stops on quarter of the 4th beat, creating 4/13 time signature
250
+
251
+ # ________________ GENRES ________________
252
+ moombahton:
253
+ pattern: 1>0.75, 2>0.25, 1>0.5, 4>0.5
254
+ scale: 3, 2, 1
255
+ description: a distinct popular moombathon/dutch house rhythm.
256
+
257
+ four-on-the-floor 1:
258
+ pattern: 1, 2, 1, 4, 1, 6, 1, 8, 1, 10, 1, 12, 1, 14, 1, 16
259
+ scale: 0.5
260
+ description: replaces snares with kicks
261
+
262
+ four-on-the-floor 1 double-time:
263
+ pattern: 1, 2, 1, 4, 1, 6, 1, 8, 1, 10, 1, 12, 1, 14, 1, 16, 1, 18, 1, 20, 1, 22, 1, 24, 1, 26, 1, 28, 1, 30, 1, 32
264
+ scale: 0.25
265
+ description: replaces snares with kicks
266
+
267
+ house 1:
268
+ pattern: 1, 2, 3, 4, 1, 6, 7, 8, 1, 10, 11, 12, 1, 14, 15, 16
269
+ scale: 0.5
270
+
271
+ house 1 double-time:
272
+ pattern: 1, 2, 5, 4, 1, 6, 5, 8, 1, 10, 13, 12, 1, 14, 13, 16, 1, 18, 21, 20, 1, 22, 21, 24, 1, 26, 29, 28, 1, 30, 29, 32
273
+ scale: 0.25
274
+
275
+ house 2:
276
+ pattern: 1>0.5
277
+ scale: 4
278
+
279
+ house 2 double-time:
280
+ pattern: 1>0.25
281
+ scale: 4
282
+
283
+ drill:
284
+ pattern: 1>0.75, 2>0.75, 2>0.5, 3>0.75, 4>0.75, 4>0.5, 5>0.75, 6>0.75, 6>0.5, 6, 7>0.75, 8<0.25
285
+ scale: 1, 0.5, 0.25 #2
286
+ description: distinct drill rhythm with 4/3 syncopatied notes and a shifted second snare.
287
+
288
+ jungle 1:
289
+ pattern: 1, 2, 3, 4, 5, 7, 6, 8, | 11, 10, 11, 12, 13, 15, 14, 16, | 19, 18, 19, 20, 21, 23, 22, 24, | 27, 26, 27, 28, 29, 31, 30, 32
290
+ scale: 1.5, 0.75, 0.5
291
+ description: Rhythm commonly used in jungle, otherwise sounds like a buildup snare pattern.
292
+
293
+ jungle 2:
294
+ pattern: 1, 2, 1, 2, | 3>0.5, 3>0.5, 1>0.5, 7>0.5, | 7>0.5, 7>0.5, 5>0.5, 11>0.5, | 11>0.5, 7>0.5, 0>0.5, 11>0.5, | 14>0.5, 11>0.5, 13>0.5, 15>0.5, 16!
295
+ scale: 0.5
296
+ description: 4/3 syncopated snares with additional snare-rolls
297
+
298
+ drumfunk:
299
+ pattern: 1, 2, 3 , 4, | 3 , 4, 9, 7 , | 8, 10, 11 , 0>0.5, 11>0.5 , | 0>0.5, 15>0.5, 14, 15 , 16
300
+ scale: 1, 0.5
301
+ description: pattern commonly used in drumfunk
302
+
303
+ jazzy:
304
+ pattern: 1, 2>0.5, 3, 4>0.5, 5, 6>0.5, 3, 4>0.5, 7, 8
305
+ scale: 0.5, 0.25
306
+ description: seamlessly adds a 4/3 syncopation without disrupting the original rhythm
307
+
308
+ darkstep:
309
+ pattern: 1,1,3,1, | 1,7,1,1, | 11,9,9,11, | 9,9,15,16
310
+ scale: 1, 0.5
311
+
312
+ darkstep long:
313
+ pattern: 1,1,3,1, | 1,7,1,1, | 11,9,9,11, | 9,9,15,9, | 17,19,17,17, | 19,17,17,23, | 25,25,27,25, | 25,31,25,32
314
+ scale: 0.5
315
+
316
+ darkstep fast:
317
+ pattern: 1,1,5,1, | 1,13,1,1, | 21,17,17,21, | 17,17,29,32
318
+ scale: 0.25
319
+
320
+ # ________________ EFFECTS ________________
321
+ staccato reverese:
322
+ pattern: 1>0.5, 0.5:0.8r, 0.5:0.8, 0.5:0.8r, 0.5:0.8, 0.5:0.8r, 3>0.6, 0.5:0.8r, 0.5:0.8, 0.5:0.8r, 0.5:0.8, 0.5:0.7r, 5>0.5, 0.5:0.8r, 0.5:0.8, 0.5:0.8r, 0.5:0.8, 0.5:0.8r, 7>0.6, 0.5:0.8r, 0.5:0.8, 0.5:0.8r, 7.6:7.9, 7.0>0
323
+ scale: 0.5, 0.25
324
+
325
+ staccato reverese syncopated 1:
326
+ pattern: 1, 2>0.5, 2.5:4r, 2.5:4, 2.5:4r, 2.5:4, 2.5:3r, | 9, 10.5:12, 10.5:12r, 10.5:12, 10.5:12r, 10.5:11.5, | 16:16.5, 18.5:20r, 18.5:20, 18.5:20r, 18.5:20, 18.5:20r, | 25, 25:25.5, 26.5:28r, 26.5:28, 26.5:28r, 26.5:28, 31.5:32
327
+ scale: 1/8, 1/16
328
+
329
+ staccato reverese syncopated 2:
330
+ pattern: 1, 2>0.5, 2.5:4r, 2.5:4, 2.5:4r, 2.5:4, 2.5:3r, | 9, 2.5:4, 2.5:4r, 2.5:4, 2.5:4r, 2.5:3.5, | 16:16.5, 2.5:4r, 2.5:4, 2.5:4r, 2.5:4, 2.5:4r, | 25, 25:25.5, 2.5:4r, 2.5:4, 2.5:4r, 2.5:4, 31.5:32
331
+ scale: 1/8, 1/16
332
+
333
+ staccato reverese syncopated 3:
334
+ pattern: 1, 2>0.5, 2.5:4s2,2.5:4s2r, 2.5:4, 2.5:4r, 2.5:4s2,2.5:4s2r, 2.5:3, | 9, 2.5:4r, 2.5:4s2,2.5:4s2r, 2.5:4, 2.5:4r, 2.5:4s2,2.5:4s2r, | 16:17.5, 2.5:4r, 2.5:4s2,2.5:4s2r, 2.5:4, 2.5:4r, | 24:25.5, 2.5:4, 2.5:4r, 2.5:4s2,2.5:4s2r, 2.5:4, 31.5:32r
335
+ scale: 1/8, 1/16
336
+
337
+ # ________________ SONGS ________________
338
+ BS6:
339
+ pattern: 1, 1, | 3>1.5, 3>1.5, 3>1.5, 3>1.5, | 9, 9>0.5 | 14.5>1.5, 14.5>1.5, 14.5>1.5, 14.5>1.5, 14.5>0.5, 16!
340
+ scale: 1, 0.5
341
+ description: Hyroglifics & Sinistarr - BS6
342
+
343
+ Poison:
344
+ pattern: 0:1/3, 0:1/3, 1:4/3, 1:4/3, 2:7/3, 2:7/3, 3:10/3, 3:10/3, 1/3:2/3, 4/3:5/3, 10/3:4
345
+ scale: 1
346
+ description: Stray & Halogenix - Poison
347
+
348
+ Szamar Madar:
349
+ pattern: 1, 2, 3, 4, 4, 1, 2, 3, 1, 1v0, 1v0, 1, 7, 8,|9, 10, 11, 12, 13, 14, 15, 16, 15, 10, 10, 10, 11, 16,|17, 18, 19, 20, 20, 17, 18, 19, 20, 20, 17, 17v0, 23, 24,|25, 25:25.5, 24:24.5, 27, 25, 28, 25, 31, 24:24.5, 27.5:28, 24:24.5, 27.5:28, 25, 25, 25, 31, 32
350
+ scale: 0.5
351
+ description: Venetian Snares - Szamár Madár (11/4)
352
+
353
+ Conceivability:
354
+ pattern: 1, 2, 3, 4, 9, 10, 11, 12, 13, 14, 16<0.5, 16>0.5, | 17, 18, 19>0.5, 20, 21, 21<0.5, 22, 23, 23<0.5, 24, 30, 31, 32>0.5
355
+ scale: 1
356
+
357
+ Rhythm Era:
358
+ pattern: 1, 2>0.75, 2>0.75, 2>0.75, 2>0.25, | 5, 6>0.75, 8>0.75, 6>0.75, 8>0.25, | 9, 10>0.75, 10>0.75, 10>0.75, 10>0.25, | 13, 14>0.75v0.6; 16>0.75v0.6, 14>0.75, 14>0.5, 14>0.5v0.6; 16>0.5v0.6
359
+ scale: 1
360
+ description: stunlocked - Rhythm Era (7/4)
361
+
362
+ # ________________ OTHER ________________
363
+ test:
364
+ pattern: test
365
+ description: puts cowbells on beats
beat_manipulator/samples/cowbell.flac ADDED
Binary file (16.4 kB). View file
 
beat_manipulator/samples/oh_live.ogg ADDED
Binary file (60.1 kB). View file
 
beat_manipulator/utils.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ C_SLICE = ":><" # 0 - range, 1 - first, 2 - last
2
+ C_JOIN = ",;~&^$}" # 0 - append, 1 - first length, 2 - cut, 3 - maximum, 4 - sidechain
3
+ C_MISC = "'\"`i@_?%#![]"
4
+ # 0 ',1 " - sample, 2 ` - sample uncut, 3 i - current, 4 @ - random,
5
+ # # 5 _ - random sep, 6 ? - not count, 7 % - create variable, 8 # - shuffle, 9 ! skip
6
+ # 10, 11 [] - song
7
+ C_MATH = '+-*/.'
8
+ C_MATH_STRICT = '.+-*/'
9
+
10
+ def _safer_eval(string:str) -> float:
11
+ if isinstance(string, str):
12
+ try:
13
+ for i in (C_MISC[4], C_MISC[7], C_MISC[8]):
14
+ if i in string: string = string[:string.find(i)]
15
+ string = string.replace('{', '<').replace('}', '>')
16
+ string = eval(''.join([i for i in string if i.isdecimal() or i in C_MATH]))
17
+ except (NameError, SyntaxError): string = 1
18
+ return string
19
+
20
+ def _safer_eval_strict(string:str) -> float:
21
+ if isinstance(string, str):
22
+ for n, v in enumerate(string):
23
+ assert v in C_MATH_STRICT or v == ' ' or v.isdecimal, f"_safer_eval_strict error: {string}[{n}] = {v}, which isn't a decimal, isn't in {C_MATH_STRICT} and isn't a space"
24
+ string = eval(''.join([i for i in string if i.isdecimal() or i in C_MATH_STRICT]))
25
+ return string
examples.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import beat_manipulator as bm, os, random
2
+
3
+ path = 'F:/Stuff/Music/Tracks/'
4
+ song = 'Phonetick - You.mp3'
5
+ song = path + song
6
+
7
+ #bm.presets.savetest(song, scale = 1, shift = 0)
8
+
9
+ bm.beatswap(song, 'random', scale = 1, shift = 0)
10
+
11
+ #bm.presets.use(song = song, preset = 'dotted snares fast 1', scale = 1)
jupiter.ipynb ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "attachments": {},
5
+ "cell_type": "markdown",
6
+ "metadata": {},
7
+ "source": [
8
+ "<h1><b><center>Beat Manipulator</center></b></h1>"
9
+ ]
10
+ },
11
+ {
12
+ "attachments": {},
13
+ "cell_type": "markdown",
14
+ "metadata": {},
15
+ "source": [
16
+ "Simply put your pattern in the cell below and run it as many times as you wish. Pattern syntax, scale and shift are explained here https://github.com/stunlocked1/BeatManipulator\n",
17
+ "\n",
18
+ "A file selection dialog will open. Note: you might need to Alt+Tab to it due to how Jupiter works (press Alt+Tab and select file selection dialog). Alternatively you can add `audio=\"path/to/audio\"` attribute into bm.beatswap to load a specified audio file.\n",
19
+ "\n",
20
+ "Choose any audio file, and the beatswapped version will be displayed, as well as saved next to this Jupiter Notebook file.\n",
21
+ "\n",
22
+ "Analyzing beats for the first time will take some time, but if you open the same file for the second time, it will load a saved beat map."
23
+ ]
24
+ },
25
+ {
26
+ "cell_type": "code",
27
+ "execution_count": null,
28
+ "metadata": {},
29
+ "outputs": [],
30
+ "source": [
31
+ "pattern = '1, 4' # Replace this with your pattern. You can write \"test\" as a pattern to test where each beat is.\n",
32
+ "scale = 1\n",
33
+ "shift = 0\n",
34
+ "audio = None # If you want to skip select file dialog, replace None with a path to your audio file\n",
35
+ "\n",
36
+ "pattern_length = None # Length of the pattern. If None, this will be inferred from the highest number in the pattern\n",
37
+ "\n",
38
+ "\n",
39
+ "import beat_manipulator as bm, IPython\n",
40
+ "print(\"Press alt+tab if file selection dialog didn't show\")\n",
41
+ "path = bm.beatswap(audio=audio, pattern = pattern, scale = scale, shift = shift, length = pattern_length)\n",
42
+ "IPython.display.Audio(path)"
43
+ ]
44
+ },
45
+ {
46
+ "attachments": {},
47
+ "cell_type": "markdown",
48
+ "metadata": {},
49
+ "source": [
50
+ "***\n",
51
+ "## Other stuff\n",
52
+ "Those operate the same as the above cell"
53
+ ]
54
+ },
55
+ {
56
+ "attachments": {},
57
+ "cell_type": "markdown",
58
+ "metadata": {},
59
+ "source": [
60
+ "**Song to image**\n",
61
+ "\n",
62
+ "creates an image based on beat positions, so each song will generate a unique image."
63
+ ]
64
+ },
65
+ {
66
+ "cell_type": "code",
67
+ "execution_count": null,
68
+ "metadata": {},
69
+ "outputs": [],
70
+ "source": [
71
+ "image_size = 512 # image will be a square with this size in pixels\n",
72
+ "audio = None # If you want to skip select file dialog, replace None with a path to your audio file\n",
73
+ "\n",
74
+ "\n",
75
+ "import beat_manipulator as bm, IPython\n",
76
+ "print(\"Press alt+tab if file selection dialog didn't show\")\n",
77
+ "path = bm.image(audio=audio, max_size = image_size)\n",
78
+ "IPython.display.Image(path)"
79
+ ]
80
+ },
81
+ {
82
+ "attachments": {},
83
+ "cell_type": "markdown",
84
+ "metadata": {},
85
+ "source": [
86
+ "***\n",
87
+ "**osu! beatmap generator**\n",
88
+ "\n",
89
+ "generates an osu! beatmap from your song. This generates a hitmap, probabilities of hits at each sample, picks all ones above a threshold, and turns them into osu circles, trying to emulate actual osu beatmap. This doesn't generate sliders, however, because no known science has been able to comprehend the complexity of those.\n",
90
+ "\n",
91
+ "The .osz file will be generated next to this notebook, open it with osu! to install it as any other beatmap."
92
+ ]
93
+ },
94
+ {
95
+ "cell_type": "code",
96
+ "execution_count": null,
97
+ "metadata": {},
98
+ "outputs": [],
99
+ "source": [
100
+ "difficulties = [0.2, 0.1, 0.05, 0.025, 0.01, 0.0075, 0.005, 0.0025, 0.0001] # all difficulties will be embedded in one beatmap, lower = harder.\n",
101
+ "audio = None # If you want to skip select file dialog, replace None with a path to your audio file\n",
102
+ "\n",
103
+ "\n",
104
+ "import beat_manipulator.osu\n",
105
+ "print(\"Press alt+tab if file selection dialog didn't show\")\n",
106
+ "beat_manipulator.osu.generate(song=audio, difficulties = difficulties)"
107
+ ]
108
+ }
109
+ ],
110
+ "metadata": {
111
+ "kernelspec": {
112
+ "display_name": "audio310",
113
+ "language": "python",
114
+ "name": "python3"
115
+ },
116
+ "language_info": {
117
+ "codemirror_mode": {
118
+ "name": "ipython",
119
+ "version": 3
120
+ },
121
+ "file_extension": ".py",
122
+ "mimetype": "text/x-python",
123
+ "name": "python",
124
+ "nbconvert_exporter": "python",
125
+ "pygments_lexer": "ipython3",
126
+ "version": "3.10.9"
127
+ },
128
+ "orig_nbformat": 4,
129
+ "vscode": {
130
+ "interpreter": {
131
+ "hash": "f56da36b984886453ea677d340712034d0bd218b2dc7a53ab7c38da0c6f67f35"
132
+ }
133
+ }
134
+ },
135
+ "nbformat": 4,
136
+ "nbformat_minor": 2
137
+ }
packages.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ ffmpeg
2
+ cython3
3
+ python3-opencv
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ cython
2
+ mido
3
+ numpy
4
+ scipy
5
+ pytest
6
+ pyfftw
7
+ soundfile
8
+ ffmpeg-python
9
+ librosa
10
+ pedalboard
11
+ opencv-python
12
+ git+https://github.com/CPJKU/madmom