├── BpmAnalizer.py
├── ExtractBpmPatterns.py
├── LICENSE
├── README.md
├── UI.png
├── UserInterface.py
├── bpm.ico
├── bpm.png
└── bpm_tray.png
/BpmAnalizer.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import warnings
3 |
4 | warnings.filterwarnings("ignore", category=DeprecationWarning)
5 | warnings.filterwarnings("ignore", category=RuntimeWarning)
6 | from threading import Thread
7 | from time import sleep
8 |
9 | import link
10 | import numpy as np
11 | import pyaudio
12 | import PySimpleGUI as sg
13 | import rtmidi
14 | from psgtray import SystemTray
15 | from collections import deque
16 | import threading
17 | import struct
18 | import UserInterface
19 | import ExtractBpmPatterns
20 | import re
21 | import json
22 | import os
23 | from scipy import signal
24 |
25 | print("----")
26 | print("Live BPM Analyzer Version 2.0")
27 | print("© 2023 Matthias Schmid")
28 | print("----")
29 |
30 | FRAME_RATE = int(11025)
31 |
32 | try:
33 | BPM_PATTERN = np.load("bpm_pattern.npy")
34 | BPM_PATTERN_FINE = np.load("bpm_pattern_fine.npy")
35 | except FileNotFoundError:
36 | ExtractBpmPatterns.extract(FRAME_RATE)
37 | BPM_PATTERN = np.load("bpm_pattern.npy")
38 | BPM_PATTERN_FINE = np.load("bpm_pattern_fine.npy")
39 |
40 |
41 | class ThreadingEvents:
42 | def __init__(self):
43 | self.stop_analyzer = threading.Event()
44 | self.stop_trigger_set_bpm = threading.Event()
45 | self.stop_update_link_button = threading.Event()
46 | self.stop_refresh_main_window = threading.Event()
47 | self.bpm_updated = threading.Event()
48 |
49 | def stop_threads(self) -> None:
50 | self.stop_analyzer.set()
51 | self.stop_trigger_set_bpm.set()
52 | self.stop_update_link_button.set()
53 | self.stop_refresh_main_window.set()
54 |
55 | def start_update_link_button_thread(main_window: object, modules: object) -> None:
56 | Thread(
57 | target=UserInterface.update_link_button,
58 | args=(main_window, modules),
59 | ).start()
60 |
61 | def start_refresh_main_window_thread(main_window: object, modules: object) -> None:
62 | Thread(
63 | target=WindowReader.refresh_main_window,
64 | args=(main_window, modules),
65 | ).start()
66 |
67 | def start_trigger_set_bpm_thread(modules: object, user_mapping: object) -> None:
68 | Thread(
69 | target=modules.midi_interface.trigger_set_bpm,
70 | args=(modules, user_mapping),
71 | ).start()
72 |
73 | def start_run_analyzer_thread(modules: object) -> None:
74 | Thread(
75 | target=BpmAnalyzer.run_analyzer, args=(modules,), daemon=True
76 | ).start()
77 |
78 |
79 | class BpmStorage:
80 | def __init__(self):
81 | self._float = 120.00 # default
82 | self._str = "***.**" # default
83 | self.average_window = deque(maxlen=3)
84 |
85 |
86 | class AbletonLink:
87 | def __init__(self):
88 | self.link = link.Link(120.00)
89 | self.link.startStopSyncEnabled = True
90 | self.link.enabled = False
91 |
92 | def enable(self, bool: bool) -> None:
93 | self.link.enabled = bool
94 |
95 | def num_peers(self) -> int:
96 | return self.link.numPeers()
97 |
98 | def set_bpm(self, bpm: float) -> None:
99 | for value in [0.001, -0.001]:
100 | bpm += value
101 | s = self.link.captureSessionState()
102 | link_time = self.link.clock().micros()
103 | s.setTempo(bpm, link_time)
104 | self.link.commitSessionState(s)
105 | sleep(0.03)
106 |
107 |
108 | class AudioStreamer:
109 | def __init__(self, frame_rate: int, operating_range_seconds=12):
110 | self.frame_rate = frame_rate
111 | self.format = pyaudio.paInt16
112 | self.chunk = 10240
113 | self.audio = pyaudio.PyAudio()
114 | self.signal_buffer = deque(maxlen=int(frame_rate * operating_range_seconds))
115 | self.operating_range_seconds = operating_range_seconds
116 | self.buffer_updated = threading.Event()
117 | self.stream = None
118 |
119 | def audio_callback(self, in_data: bytes, frame_count, time_info, status) -> None:
120 | num_int16_values = len(in_data) // 2
121 | signal_buffer_int = struct.unpack(f"<{num_int16_values}h", in_data)
122 | self.signal_buffer.extend(signal_buffer_int)
123 | self.buffer_updated.set()
124 | return (None, pyaudio.paContinue)
125 |
126 | def start_stream(self, input_device_index) -> None:
127 | self.stream = self.audio.open(
128 | format=self.format,
129 | channels=1,
130 | rate=self.frame_rate,
131 | input=True,
132 | frames_per_buffer=self.chunk,
133 | input_device_index=input_device_index,
134 | stream_callback=self.audio_callback,
135 | start=False,
136 | )
137 | self.stream.start_stream()
138 |
139 | def get_buffer(self) -> np.ndarray:
140 | self.buffer_updated.wait()
141 | buffer = np.array(self.signal_buffer, dtype=np.int16)
142 | self.buffer_updated.clear()
143 | return buffer
144 |
145 | def stop_stream(self):
146 | self.stream.stop_stream()
147 | self.stream.close()
148 | self.audio.terminate()
149 |
150 | def available_audio_devices(self) -> list:
151 | devices = []
152 | indices_of_devices = []
153 | info = self.audio.get_host_api_info_by_index(0)
154 | numdevices = info.get("deviceCount")
155 | for i in range(0, numdevices):
156 | if (
157 | self.audio.get_device_info_by_host_api_device_index(0, i).get(
158 | "maxInputChannels"
159 | )
160 | ) > 0:
161 | device = self.audio.get_device_info_by_host_api_device_index(0, i).get(
162 | "name"
163 | )
164 | index_of_device = self.audio.get_device_info_by_host_api_device_index(
165 | 0, i
166 | ).get("index")
167 | devices.append(device)
168 | indices_of_devices.append(index_of_device)
169 | return [devices, indices_of_devices]
170 |
171 |
172 | class BpmAnalyzer:
173 | def search_beat_events(signal_array: np.ndarray, frame_rate: int) -> np.ndarray:
174 | step_size = frame_rate // 2
175 | events = []
176 | for step_start in range(0, len(signal_array), step_size):
177 | signal_array_window = signal_array[step_start : step_start + step_size]
178 | signal_array_window[signal_array_window < signal_array_window.max()] = 0
179 | signal_array_window[signal_array_window > 0] = 1
180 | event = np.argmax(signal_array_window) + step_start
181 | events.append(event)
182 | return np.array(events, dtype=np.int64)
183 |
184 | def bpm_container(beat_events: np.ndarray, bpm_pattern: np.ndarray, steps: int) -> list[list]:
185 | bpm_container = [list(np.zeros((1,), dtype=np.int64))for _ in range(beat_events.size * steps)]
186 | for i, beat_event in enumerate(beat_events):
187 | found_in_pattern = np.where(np.logical_and(bpm_pattern >= beat_event - 20, bpm_pattern <= beat_event + 20))
188 | for x, q in enumerate(found_in_pattern[0]):
189 | bpm_container[i * steps + q].append(found_in_pattern[1][x])
190 | return bpm_container
191 |
192 | def wrap_bpm_container(bpm_container: list, steps: int) -> list[list]:
193 | def flatten(input_list: list) -> list:
194 | return [item for sublist in input_list for item in sublist]
195 |
196 | bpm_container_wrapped = [list(np.zeros((1,), dtype=np.int64)) for _ in range(steps)]
197 | for i, w in enumerate(bpm_container_wrapped):
198 | w.extend(flatten(bpm_container[i::steps]))
199 | w.remove(0)
200 | bpm_container_wrapped[i] = list(filter(lambda num: num != 0, w))
201 | return bpm_container_wrapped
202 |
203 | def finalise_bpm_container(bpm_container_wrapped: list, steps: int) -> np.ndarray:
204 | bpm_container_final = np.zeros((steps, 1), dtype=np.int64)
205 | for i, w in enumerate(bpm_container_wrapped):
206 | values, counts = np.unique(w, return_counts=True)
207 | values = values[counts == counts.max()]
208 | if values[0] > 0:
209 | count = np.count_nonzero(w == values[0])
210 | bpm_container_final[i] = count
211 | return bpm_container_final
212 |
213 | def get_bpm_wrapped(bpm_container_final: np.ndarray) -> np.ndarray:
214 | return np.where(bpm_container_final == np.amax(bpm_container_final))
215 |
216 | def check_bpm_wrapped(bpm_wrapped: np.ndarray, bpm_container_final: np.ndarray) -> bool:
217 | count = np.count_nonzero(bpm_container_final == bpm_wrapped[0][0])
218 | if count > 1 or bpm_container_final[int(bpm_wrapped[0][0])] < 6:
219 | return 0
220 | else:
221 | return 1
222 |
223 | def get_bpm_pattern_fine_window(bpm_wrapped: np.ndarray) -> int:
224 | start = int(((bpm_wrapped[0][0] / 4) / 0.05) - 20)
225 | end = int(start + 40)
226 | return start, end
227 |
228 | def bpm_wrapped_to_float_str(bpm: np.ndarray, bpm_fine: np.ndarray) -> float:
229 | bpm_float = round(
230 | float((((bpm[0][0] / 4) + 100) - 1) + (bpm_fine[0][0] * 0.05)), 2
231 | )
232 | bpm_str = format(bpm_float, ".2f")
233 | return bpm_float, bpm_str
234 |
235 | def search_bpm(signal_array: np.ndarray, frame_rate: int) -> tuple:
236 | bpm_pattern = BPM_PATTERN
237 | bpm_pattern_fine = BPM_PATTERN_FINE
238 | beat_events = BpmAnalyzer.search_beat_events(signal_array, frame_rate)
239 | for switch_pattern in [240, 40]:
240 | bpm_container = BpmAnalyzer.bpm_container(
241 | beat_events, bpm_pattern, switch_pattern
242 | )
243 | bpm_container_wrapped = BpmAnalyzer.wrap_bpm_container(
244 | bpm_container, switch_pattern
245 | )
246 | try:
247 | bpm_container_final = BpmAnalyzer.finalise_bpm_container(
248 | bpm_container_wrapped, switch_pattern
249 | )
250 | except ValueError:
251 | return 0
252 | bpm_wrapped = BpmAnalyzer.get_bpm_wrapped(bpm_container_final)
253 | if not BpmAnalyzer.check_bpm_wrapped(bpm_wrapped, bpm_container_final):
254 | return 0
255 | if switch_pattern == 240:
256 | start, end = BpmAnalyzer.get_bpm_pattern_fine_window(bpm_wrapped)
257 | bpm_pattern = bpm_pattern_fine[start:end]
258 | bpm_wrapped_full_range = bpm_wrapped
259 | else:
260 | bpm_wrapped_fine_range = bpm_wrapped
261 | return BpmAnalyzer.bpm_wrapped_to_float_str(
262 | bpm_wrapped_full_range, bpm_wrapped_fine_range
263 | )
264 |
265 | def run_analyzer(modules: object) -> None:
266 | while not modules.threading_events.stop_analyzer.is_set():
267 | buffer = modules.audio_streamer.get_buffer()
268 | buffer = bandpass_filter(buffer)
269 | if bpm_float_str := BpmAnalyzer.search_bpm(buffer, FRAME_RATE):
270 | modules.bpm_storage.average_window.append(bpm_float_str[0])
271 | bpm_average = round(
272 | (
273 | sum(modules.bpm_storage.average_window)
274 | / len(modules.bpm_storage.average_window)
275 | ),
276 | 2,
277 | )
278 | (
279 | modules.bpm_storage._float,
280 | modules.bpm_storage._str,
281 | ) = bpm_average, format(bpm_average, ".2f")
282 |
283 |
284 | class MidiInterface:
285 | def __init__(self):
286 | self.midi_in = rtmidi.MidiIn()
287 | self.midi_out = rtmidi.MidiOut()
288 |
289 | def get_available_devices(self):
290 | available_devices = {
291 | "midi_devices_in": [],
292 | "midi_devices_out": [],
293 | "midi_devices_in_str": [],
294 | "midi_devices_out_str": [],
295 | }
296 | for midi_device in self.midi_in.get_ports():
297 | if matches := re.search(r"(.+) ([0-9]+)", midi_device):
298 | available_devices["midi_devices_in"].append(
299 | {matches.group(1): matches.group(2)}
300 | )
301 | available_devices["midi_devices_in_str"].append(matches.group(1))
302 | for midi_device in self.midi_out.get_ports():
303 | if matches := re.search(r"(.+) ([0-9]+)", midi_device):
304 | available_devices["midi_devices_out"].append(
305 | {matches.group(1): matches.group(2)}
306 | )
307 | available_devices["midi_devices_out_str"].append(matches.group(1))
308 | return available_devices
309 |
310 | def set_in_device(self, choosen_midi_device_in: str, midi_devices: dict[str, list]):
311 | self.midi_in.close_port()
312 | for midi_device in midi_devices:
313 | if choosen_midi_device_in in midi_device:
314 | try:
315 | self.midi_in.open_port(int(midi_device[choosen_midi_device_in]))
316 | except: pass
317 |
318 | def set_out_device(self, choosen_midi_device_out: str, midi_devices: dict[str, list]):
319 | self.midi_out.close_port()
320 | for midi_device in midi_devices:
321 | if choosen_midi_device_out in midi_device:
322 | try:
323 | self.midi_out.open_port(int(midi_device[choosen_midi_device_out]))
324 | except: pass
325 |
326 | def learn(self):
327 | count = 0
328 | while True:
329 | sleep(0.2)
330 | midi_in_msg = self.midi_in.get_message()
331 | if midi_in_msg == None:
332 | if count >= 1:
333 | if midi_in_msg == None:
334 | break
335 | else:
336 | count =+ 1
337 | if count == 1:
338 | user_mapping = str(midi_in_msg)
339 | try:
340 | return convert_midi_msg(user_mapping)
341 | except: pass
342 |
343 | def trigger_set_bpm(self, modules: object, user_mapping):
344 | while not modules.threading_events.stop_trigger_set_bpm.is_set():
345 | sleep(0.02)
346 | midi_in_msg = str(self.midi_in.get_message())
347 | try:
348 | midi_in_msg = convert_midi_msg(midi_in_msg)
349 | except: pass
350 | if midi_in_msg == user_mapping:
351 | modules.ableton_link.set_bpm(modules.bpm_storage._float)
352 |
353 | def close_ports(self):
354 | self.midi_in.close_port
355 | self.midi_out.close_port
356 |
357 |
358 | class WindowReader:
359 | def audio_device_selection(choose_input_window: object, audio_devices: list) -> int:
360 | def get_choosen_audio_device(values, audio_devices):
361 | audio_device = values["board"]
362 | index_for_device = audio_devices[0].index(audio_device)
363 | return audio_devices[1][index_for_device]
364 |
365 | choose_input_window.un_hide()
366 | while True:
367 | event, values = choose_input_window.read()
368 | if event == sg.WIN_CLOSED:
369 | break
370 | if event == "Next":
371 | break
372 | if event == "board":
373 | choosen_audio_device = get_choosen_audio_device(values, audio_devices)
374 | choose_input_window["Next"].update(disabled=False)
375 | choose_input_window.hide()
376 | return choosen_audio_device
377 |
378 | def midi_device_selection(
379 | modules: object,
380 | midi_device_selection_window: object,
381 | midi_devices: dict[str, list],
382 | ):
383 | midi_device_selection_window.un_hide()
384 | set_in = False
385 | set_out = False
386 | while True:
387 | event, values = midi_device_selection_window.read()
388 | sleep(0.02)
389 | if event == sg.WIN_CLOSED:
390 | midi_device_selection_window.close()
391 | break
392 | if event == "Exit":
393 | midi_device_selection_window.close()
394 | break
395 | if event == "learnsendbpm":
396 | midi_device_selection_window.Element("learnsendbpm").Update(
397 | button_color="black on white"
398 | )
399 | user_mapping = modules.midi_interface.learn()
400 | midi_device_selection_window.close()
401 | return user_mapping
402 | if event == "midiinput":
403 | choosen_device = values["midiinput"]
404 | modules.midi_interface.set_in_device(
405 | choosen_device, midi_devices["midi_devices_in"]
406 | )
407 | set_in = True
408 | if set_out == True:
409 | midi_device_selection_window.Element("learnsendbpm").Update(
410 | disabled=False
411 | )
412 | if event == "midioutput":
413 | choosen_device = values["midioutput"]
414 | modules.midi_interface.set_out_device(
415 | choosen_device, midi_devices["midi_devices_out"]
416 | )
417 | set_out = True
418 | if set_in == True:
419 | midi_device_selection_window.Element("learnsendbpm").Update(
420 | disabled=False
421 | )
422 |
423 | def midi_device_selection_done(midi_device_selection_done_window: object):
424 | midi_device_selection_done_window.un_hide()
425 | while True:
426 | event, _ = midi_device_selection_done_window.read()
427 | sleep(0.02)
428 | if event == sg.WIN_CLOSED:
429 | midi_device_selection_done_window.close()
430 | break
431 | if event == "Exit":
432 | midi_device_selection_done_window.close()
433 | break
434 |
435 | def main_window(main_window: object, modules: object):
436 | menu = ["", ["Show Window"]]
437 | tray = SystemTray(
438 | menu=menu,
439 | single_click_events=True,
440 | window=main_window,
441 | tooltip="Live BPM Analyzer",
442 | icon="./bpm_tray.png",
443 | )
444 | switch_button = True
445 | while True:
446 | event, _ = main_window.read()
447 | sleep(0.02)
448 | if event == tray.key:
449 | main_window.BringToFront()
450 | if event == sg.WIN_CLOSED:
451 | modules.threading_events.stop_threads()
452 | modules.ableton_link.enable(False)
453 | tray.close()
454 | main_window.close()
455 | return 0
456 | if event == "link":
457 | switch_button = not switch_button
458 | main_window.Element("link").Update(
459 | ("LINK", "LINK")[switch_button],
460 | button_color=(("white on blue", "black on white")[switch_button]),
461 | )
462 | if switch_button == True:
463 | modules.threading_events.stop_update_link_button.set()
464 | modules.ableton_link.enable(False)
465 | if switch_button == False:
466 | modules.ableton_link.enable(True)
467 | ThreadingEvents.start_update_link_button_thread(main_window, modules)
468 | if event == "settings":
469 | modules.threading_events.stop_threads()
470 | modules.ableton_link.enable(False)
471 | tray.close()
472 | main_window.close()
473 | return 1
474 | if event == "sendbpm":
475 | modules.ableton_link.set_bpm(modules.bpm_storage._float)
476 |
477 | def refresh_main_window(main_window: object, modules: object) -> None:
478 | while not modules.threading_events.stop_refresh_main_window.is_set():
479 | sleep(1)
480 | main_window["bpm"].update(modules.bpm_storage._str)
481 |
482 |
483 | class OpenWindow:
484 | def __init__(self):
485 | self.resolution = UserInterface.check_screen_resolution()
486 |
487 | def audio_device_selection(self, modules: object) -> int:
488 | audio_devices = modules.audio_streamer.available_audio_devices()
489 | audio_device_selection_window = UserInterface.audio_device_selection(
490 | audio_devices, self.resolution
491 | )
492 | choosen_audio_device = WindowReader.audio_device_selection(
493 | audio_device_selection_window, audio_devices
494 | )
495 | Settings.save(choosen_audio_device=choosen_audio_device)
496 | return choosen_audio_device
497 |
498 | def midi_device_selection(self, modules: object) -> str:
499 | midi_devices = modules.midi_interface.get_available_devices()
500 | midi_device_selection_window = UserInterface.midi_device_selection(
501 | midi_devices["midi_devices_in_str"],
502 | midi_devices["midi_devices_out_str"],
503 | self.resolution,
504 | )
505 | if user_mapping := WindowReader.midi_device_selection(
506 | modules, midi_device_selection_window, midi_devices
507 | ):
508 | midi_device_selection_window_done = (
509 | UserInterface.midi_device_selection_done(self.resolution)
510 | )
511 | WindowReader.midi_device_selection_done(midi_device_selection_window_done)
512 | Settings.save(user_mapping=user_mapping)
513 | return user_mapping
514 |
515 | def main_window(self, modules) -> None:
516 | main_window = UserInterface.main_window(self.resolution)
517 | ThreadingEvents.start_refresh_main_window_thread(main_window, modules)
518 | if WindowReader.main_window(main_window, modules):
519 | return 1
520 | else:
521 | return 0
522 |
523 |
524 | class Settings:
525 | def check() -> bool:
526 | try:
527 | with open("settings.json", "r") as _:
528 | pass
529 | return 1
530 | except:
531 | content = {"choosen_audio_device": "", "user_mapping": ""}
532 | with open("settings.json", "w") as settings:
533 | json.dump(content, settings)
534 | return 0
535 |
536 | def save(choosen_audio_device=None, user_mapping=None) -> None:
537 | with open("settings.json", "r") as settings:
538 | content = json.load(settings)
539 | if choosen_audio_device is not None:
540 | content["choosen_audio_device"] = choosen_audio_device
541 | if user_mapping is not None:
542 | content["user_mapping"] = user_mapping
543 | with open("settings.json", "w") as settings:
544 | json.dump(content, settings)
545 |
546 | def open() -> None:
547 | settings_lst = []
548 | with open("settings.json", "r") as settings:
549 | data = json.load(settings)
550 | for key, value in data.items():
551 | settings_lst.append(value)
552 | return settings_lst
553 |
554 |
555 | class InitialiseModules:
556 | def __init__(self):
557 | self.bpm_storage = BpmStorage()
558 | self.threading_events = ThreadingEvents()
559 | self.audio_streamer = AudioStreamer(FRAME_RATE)
560 | self.ableton_link = AbletonLink()
561 | self.midi_interface = MidiInterface()
562 | self.open_window = OpenWindow()
563 |
564 |
565 | def convert_midi_msg(msg) -> str:
566 | for char in "()[]":
567 | msg = msg.replace(char, "")
568 | msg = msg.split(",", -1)
569 | del msg[3]
570 | for i, value in enumerate(msg):
571 | msg[i] = value.strip()
572 | return msg
573 |
574 |
575 | def bandpass_filter(audio_signal, lowcut=60.0, highcut=3000.0) -> np.ndarray:
576 | def butter_bandpass(lowcut, highcut, fs, order=10):
577 | nyq = 0.5 * fs
578 | low = lowcut / nyq
579 | high = highcut / nyq
580 | b, a = signal.butter(order, [low, high], btype='band')
581 | return b, a
582 |
583 | def butter_bandpass_filter(data, lowcut, highcut, fs, order=10):
584 | b, a = butter_bandpass(lowcut, highcut, fs, order=order)
585 | y = signal.lfilter(b, a, data)
586 | return y
587 |
588 | def _bandpass_filter(buffer):
589 | return butter_bandpass_filter(buffer, lowcut, highcut, FRAME_RATE, order=6)
590 |
591 | return np.apply_along_axis(_bandpass_filter, 0, audio_signal).astype('int16')
592 |
593 |
594 | def main() -> None:
595 | while True:
596 | modules = InitialiseModules()
597 | if not Settings.check():
598 | choosen_audio_device = modules.open_window.audio_device_selection(modules)
599 | user_mapping = modules.open_window.midi_device_selection(modules)
600 | else:
601 | settings = Settings.open()
602 | choosen_audio_device, user_mapping = int(settings[0]), settings[1]
603 | modules.audio_streamer.start_stream(choosen_audio_device)
604 | ThreadingEvents.start_trigger_set_bpm_thread(modules, user_mapping)
605 | ThreadingEvents.start_run_analyzer_thread(modules)
606 | if modules.open_window.main_window(modules): # Main loop
607 | modules.audio_streamer.stop_stream()
608 | modules.midi_interface.close_ports
609 | ThreadingEvents.stop_threads
610 | os.remove("settings.json")
611 | else:
612 | modules.audio_streamer.stop_stream()
613 | modules.midi_interface.close_ports
614 | ThreadingEvents.stop_threads
615 | sys.exit()
616 |
617 |
618 | if __name__ == "__main__":
619 | main()
620 |
--------------------------------------------------------------------------------
/ExtractBpmPatterns.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 |
4 | def extract_bpm_pattern(lengh: int, frame_rate: int) -> None:
5 | array = np.full((240, lengh, 32), 0, dtype=np.int64)
6 | jump = int(0)
7 | add = 0
8 |
9 | for i in range(240):
10 | add += 0.25
11 | timestamp = int(60 / (100 + add) * frame_rate)
12 | jump = int(0)
13 | for x in range(lengh):
14 | timestamp_next = 0
15 | jump += 20
16 | for y in range(32):
17 | array[i][x][y] = timestamp_next
18 | timestamp_next += timestamp
19 | array[i][x] = array[i][x] + jump
20 |
21 | np.save("bpm_pattern.npy", array)
22 |
23 |
24 | def extract_bpm_pattern_fine(lengh: int, frame_rate: int) -> None:
25 | array = np.full((1200, lengh, 32), 0, dtype=np.int64)
26 | jump = int(0)
27 | add = 0
28 |
29 | for i in range(1200):
30 | timestamp = int(60 / (100 + add) * frame_rate)
31 | add += 0.05
32 | jump = int(0)
33 | for x in range(lengh):
34 | timestamp_next = 0
35 | jump += 20
36 | for y in range(32):
37 | array[i][x][y] = timestamp_next
38 | timestamp_next += timestamp
39 | array[i][x] = array[i][x] + jump
40 |
41 | np.save("bpm_pattern_fine.npy", array)
42 |
43 |
44 | def extract(frame_rate: int):
45 | print("PATTERN CREATOR")
46 | print("extracting...")
47 | lengh = int((frame_rate / 2) / 20)
48 | extract_bpm_pattern(lengh, frame_rate)
49 | extract_bpm_pattern_fine(lengh, frame_rate)
50 | print("\033[92m" + "COMPLETED" + "\033[0m")
51 |
52 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Tempo analyzer for music.
2 | Copyright (C) 2023 Matthias Christopher Schmid
3 |
4 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
5 |
6 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
7 |
8 | You should have received a copy of the GNU General Public License along with this program. If not, see
3 |
4 | A BPM analyzer designed for live musicians using DAWs like Ableton or VJs who want to collaborate with other artists and focus more on performance instead of wasting time finding the right tempo of a source that cannot be digitally synced. The operating range is currently set between 110-160 BPM. 5 |
6 |
9 |
10 |