Spaces:
Runtime error
Runtime error
Duplicate from dpe1/beat_manipulator
Browse filesCo-authored-by: Ivan Nikishev <[email protected]>
- .gitignore +9 -0
- README.md +13 -0
- app.py +123 -0
- beat_manipulator/__init__.py +2 -0
- beat_manipulator/beatmap.py +195 -0
- beat_manipulator/effects.py +84 -0
- beat_manipulator/google colab.ipynb +176 -0
- beat_manipulator/image.py +70 -0
- beat_manipulator/io.py +178 -0
- beat_manipulator/main.py +531 -0
- beat_manipulator/metrics.py +40 -0
- beat_manipulator/osu.py +244 -0
- beat_manipulator/parse.py +251 -0
- beat_manipulator/presets.py +84 -0
- beat_manipulator/presets.yaml +365 -0
- beat_manipulator/samples/cowbell.flac +0 -0
- beat_manipulator/samples/oh_live.ogg +0 -0
- beat_manipulator/utils.py +25 -0
- examples.py +11 -0
- jupiter.ipynb +137 -0
- packages.txt +3 -0
- requirements.txt +12 -0
.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
|