Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import tkinter as tk
- from tkinter import ttk
- import numpy as np
- import aubio
- import pyaudio
- import threading
- import mido
- import time
- from collections import deque
- class BPMDetector:
- def __init__(self, app):
- self.app = app
- self.running = False
- self.buffer_size = 1024
- self.sample_rate = 44100
- self.tempo_obj = aubio.tempo("default", 2048, 1024, self.sample_rate)
- self.p = pyaudio.PyAudio()
- self.device_indices = self._get_input_devices()
- self.bpm_history = deque(maxlen=5)
- def _get_input_devices(self):
- devices = {}
- for i in range(self.p.get_device_count()):
- info = self.p.get_device_info_by_index(i)
- if info['maxInputChannels'] > 0:
- devices[info['name']] = i
- return devices
- def get_device_names(self):
- return list(self.device_indices.keys())
- def start(self):
- self.running = True
- threading.Thread(target=self._process_audio, daemon=True).start()
- def stop(self):
- self.running = False
- def _send_midi_clock_loop(self, bpm):
- if not self.app.send_midi.get():
- return
- midi_out = mido.open_output() # Default MIDI output
- interval = 60.0 / bpm / 24.0 # 24 MIDI clocks per quarter note
- while self.running and self.app.send_midi.get():
- midi_out.send(mido.Message('clock'))
- time.sleep(interval)
- def _process_audio(self):
- device_name = self.app.selected_device.get()
- device_index = self.device_indices.get(device_name, None)
- if device_index is None:
- return
- device_info = self.p.get_device_info_by_index(device_index)
- num_channels = int(device_info.get('maxInputChannels', 1))
- selected_channel = int(self.app.selected_channel.get()) - 1 # 0-based
- stream = self.p.open(format=pyaudio.paFloat32,
- channels=num_channels,
- rate=self.sample_rate,
- input=True,
- input_device_index=device_index,
- frames_per_buffer=self.buffer_size)
- last_beat_time = 0
- prev_interval = None
- self.bpm_history.clear()
- while self.running:
- try:
- audio_data = stream.read(self.buffer_size, exception_on_overflow=False)
- samples = np.frombuffer(audio_data, dtype=aubio.float_type)
- if num_channels > 1:
- samples = samples[selected_channel::num_channels]
- is_beat = self.tempo_obj(samples)
- if is_beat:
- current_time = time.time()
- interval = current_time - last_beat_time
- plausible = True
- if prev_interval is not None:
- if abs(interval - 2 * prev_interval) < 0.15:
- interval = interval / 2
- elif interval > 1.8 * prev_interval:
- plausible = False
- if plausible:
- last_beat_time = current_time
- prev_interval = interval
- if 0.3 < interval < 2.0: # BPM between 30 and 200
- bpm = 60.0 / interval
- self.bpm_history.append(bpm)
- if len(self.bpm_history) >= 2:
- avg_bpm = np.median(self.bpm_history)
- if avg_bpm < 80:
- double_bpm = avg_bpm * 2
- if 80 <= double_bpm <= 180:
- avg_bpm = double_bpm
- self.app.update_bpm_display(f"{avg_bpm:.0f}")
- elif len(self.bpm_history) < 2:
- self.app.update_bpm_display("---")
- except Exception as e:
- self.app.update_bpm_display("---")
- stream.stop_stream()
- stream.close()
- class BPMApp:
- def __init__(self, root):
- self.root = root
- self.root.title("BPM Detector")
- self.root.geometry("360x240")
- self.root.resizable(False, False)
- self.selected_device = tk.StringVar()
- self.selected_channel = tk.StringVar(value="1")
- self.send_midi = tk.BooleanVar(value=True)
- self.label_bpm = tk.Label(root, text="---", font=("Helvetica", 48))
- self.label_bpm.pack(pady=10)
- frm = tk.Frame(root)
- frm.pack()
- tk.Label(frm, text="Audio Device:").grid(row=0, column=0, sticky="e", padx=5, pady=5)
- self.device_menu = ttk.Combobox(frm, textvariable=self.selected_device, width=25, state="readonly")
- self.device_menu.grid(row=0, column=1, pady=5)
- tk.Label(frm, text="Channel:").grid(row=1, column=0, sticky="e", padx=5, pady=5)
- self.channel_menu = ttk.Combobox(frm, textvariable=self.selected_channel, width=25, state="readonly",
- values=[str(i) for i in range(1, 17)])
- self.channel_menu.grid(row=1, column=1, pady=5)
- self.checkbox = tk.Checkbutton(root, text="Send MIDI Clock", variable=self.send_midi)
- self.checkbox.pack(pady=10)
- self.detector = BPMDetector(self)
- devices = self.detector.get_device_names()
- self.device_menu['values'] = devices
- if devices:
- self.device_menu.set(devices[0])
- self.device_menu.bind("<<ComboboxSelected>>", self.update_channel_menu)
- self.update_channel_menu() # Initial call
- self.detector.start()
- self.root.protocol("WM_DELETE_WINDOW", self.on_close)
- def update_bpm_display(self, bpm):
- self.label_bpm.config(text=bpm)
- def on_close(self):
- self.detector.stop()
- self.root.destroy()
- def update_channel_menu(self, event=None):
- device_name = self.selected_device.get()
- if not device_name:
- return
- device_index = self.detector.device_indices.get(device_name, None)
- if device_index is None:
- return
- info = self.detector.p.get_device_info_by_index(device_index)
- channels = int(info.get('maxInputChannels', 1))
- self.channel_menu['values'] = [str(i) for i in range(1, channels + 1)]
- if int(self.selected_channel.get()) > channels:
- self.selected_channel.set("1")
- root = tk.Tk()
- app = BPMApp(root)
- root.mainloop()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement