JustPaste
HomeCategoriesAboutDonateContactTerms of UsePrivacy Policy
JustPaste

Free online notepad — write and share instantly

Navigate

  • Home
  • Timeline
  • Categories

Info

  • About
  • Donate
  • Contact

Legal

  • Terms of Use
  • Privacy Policy

© 2026 JustPaste.app. All rights reserved.

Made with ♥ by JustPaste

DAW in Python using PyGame and Tkinter | JustPaste.app
4 days ago1 views
IForgot
👨‍💻Programming

DAW in Python using PyGame and Tkinter

import json
import os
import time
import wave
import tkinter as tk
from tkinter import filedialog

import numpy as np
import pygame
import sounddevice as sd

# App setup
pygame.init()
pygame.mixer.init(frequency=44100, size=-16, channels=2)
sample_rate = 44100
WIDTH = 1000
HEIGHT = 650
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption('JRYBeats')
clock = pygame.time.Clock()
BACKGROUND = (245, 245, 242)
TEXT_COLOR = (45, 45, 45)
SECONDARY_TEXT = (120, 120, 120)
LINE_COLOR = (70, 70, 70)
LIGHT_LINE = (205, 205, 200)
BLUE = (35, 85, 170)
PLAYHEAD_BLUE = (75, 135, 220)
GREEN = (40, 150, 65)
GREEN_HOVER = (55, 175, 80)
RED = (210, 60, 60)
PURPLE = (110, 95, 210)
PURPLE_HOVER = (130, 115, 225)
ORANGE = (215, 130, 45)
BUTTON_BACKGROUND = (250, 250, 247)
STEP_BACKGROUND = (252, 252, 250)
STEP_HOVER = (225, 225, 220)
BLACK_KEY = (45, 45, 48)
WHITE_KEY = (245, 245, 242)
title_font = font(40)
section_font = font(27)
track_font = font(25)
small_font = font(21)
tiny_font = font(17)
step_font = font(18)

def font(size: int):
    return pygame.font.Font(None, size)

# Drum synthesis
def make_sound(wave):
    wave = np.clip(wave, -1, 1)
    audio = (wave * 32767).astype(np.int16)
    stereo = np.column_stack((audio, audio))
    stereo = np.ascontiguousarray(stereo)
    return pygame.sndarray.make_sound(stereo)
duration = 0.5
t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
frequency = 50 + 210 * np.exp(-35 * t)
phase = 2 * np.pi * np.cumsum(frequency) / sample_rate
kick_wave = np.sin(phase)
kick_wave += 0.18 * np.sin(2 * phase)
kick_wave *= np.exp(-7 * t)
click = np.random.uniform(-1, 1, len(t))
click *= np.exp(-100 * t)
kick_wave += click * 0.12
kick_wave *= 0.85
kick = make_sound(kick_wave)

def eq_noise(noise, low_cut=0, high_cut=None, peak_freq=None, peak_gain=0.0):
    spectrum = np.fft.rfft(noise)
    frequencies = np.fft.rfftfreq(len(noise), 1 / sample_rate)
    shape = np.ones_like(frequencies)
    if low_cut > 0:
        shape *= np.clip(frequencies / low_cut, 0.0, 1.0)
    if high_cut:
        shape *= np.clip(high_cut / np.maximum(frequencies, 1), 0.0, 1.0)
    if peak_freq and peak_gain:
        width = max(1.0, peak_freq * 0.55)
        bell = np.exp(-0.5 * ((frequencies - peak_freq) / width) ** 2)
        shape *= 1.0 + bell * peak_gain
    spectrum *= shape
    filtered = np.fft.irfft(spectrum, n=len(noise))
    peak = np.max(np.abs(filtered))
    if peak > 0:
        filtered /= peak
    return filtered

# The snare keeps more midrange; the hat is pushed much higher.
snare_duration = 0.32
snare_t = np.linspace(0, snare_duration, int(sample_rate * snare_duration), endpoint=False)
snare_noise = np.random.uniform(-1, 1, len(snare_t))
snare_noise = eq_noise(snare_noise, low_cut=550, high_cut=9500, peak_freq=2400, peak_gain=1.2)
snare_body = np.sin(2 * np.pi * 185 * snare_t)
snare_body += 0.35 * np.sin(2 * np.pi * 330 * snare_t)
noise_envelope = np.exp(-13 * snare_t)
body_envelope = np.exp(-18 * snare_t)
snare_attack = np.random.uniform(-1, 1, len(snare_t))
snare_attack = eq_noise(snare_attack, low_cut=1800, high_cut=12000, peak_freq=4500, peak_gain=0.8)
snare_attack *= np.exp(-90 * snare_t)
snare_wave = snare_noise * noise_envelope * 0.72 + snare_body * body_envelope * 0.38 + snare_attack * 0.2
snare_wave *= 0.75
snare = make_sound(snare_wave)
hihat_duration = 0.1
hihat_t = np.linspace(0, hihat_duration, int(sample_rate * hihat_duration), endpoint=False)
hihat_noise = np.random.uniform(-1, 1, len(hihat_t))
hihat_noise = eq_noise(hihat_noise, low_cut=5500, high_cut=18000, peak_freq=10500, peak_gain=1.5)
hihat_envelope = np.exp(-48 * hihat_t)
hihat_wave = hihat_noise * hihat_envelope * 0.48
for metallic_frequency in (6400, 7900, 10100, 12400):
    hihat_wave += np.sin(2 * np.pi * metallic_frequency * hihat_t) * np.exp(-55 * hihat_t) * 0.025
hihat = make_sound(hihat_wave)
perc_duration = 0.12
perc_t = np.linspace(0, perc_duration, int(sample_rate * perc_duration), endpoint=False)
perc_noise = np.random.uniform(-1, 1, len(perc_t))
perc_wave = perc_noise * np.exp(-28 * perc_t) * 0.35
perc = make_sound(perc_wave)

# Two fast hits followed by a longer noisy tail gives the clap its shape.
clap_duration = 0.42
clap_t = np.linspace(0, clap_duration, int(sample_rate * clap_duration), endpoint=False)
clap_noise = np.random.uniform(-1, 1, len(clap_t))
clap_noise = eq_noise(clap_noise, low_cut=900, high_cut=12500, peak_freq=3200, peak_gain=1.5)
first_start = 0.0
first_decay = 0.008
first_burst = np.where(clap_t >= first_start, np.exp(-(clap_t - first_start) / first_decay), 0)
first_burst *= clap_t < 0.025
second_start = 0.022
second_decay = 0.01
second_burst = np.where(clap_t >= second_start, np.exp(-(clap_t - second_start) / second_decay), 0)
second_burst *= clap_t < second_start + 0.03
tail_start = 0.045
tail = np.where(clap_t >= tail_start, np.exp(-(clap_t - tail_start) / 0.105), 0)
clap_envelope = first_burst * 1.0 + second_burst * 0.95 + tail * 0.55
clap_wave = clap_noise * clap_envelope
clap_body = np.sin(2 * np.pi * 1150 * clap_t) + 0.5 * np.sin(2 * np.pi * 1750 * clap_t)
clap_body *= np.where(clap_t >= tail_start, np.exp(-(clap_t - tail_start) / 0.055), 0)
clap_wave += clap_body * 0.08
second_texture = np.random.uniform(-1, 1, len(clap_t))
second_texture = eq_noise(second_texture, low_cut=1200, high_cut=11000, peak_freq=4000, peak_gain=1.0)
second_texture *= second_burst
clap_wave += second_texture * 0.22
clap_wave *= 0.72
clap = make_sound(clap_wave)
drum_tracks = ['KICK', 'SNARE', 'HI-HAT', 'CLAP', 'PERC']
drum_sounds = [kick, snare, hihat, clap, perc]
drum_mixer = [{'volume': 0.85, 'muted': False, 'solo': False, 'pan': 0.0} for _ in drum_tracks]
melody_mixer = {'volume': 0.7, 'muted': False, 'solo': False, 'pan': 0.0}
mixer_drag = None


# Mixer helpers
def pan_to_lr(volume, pan):
    pan    = max(-1.0, min(1.0,   pan ))
    volume = max( 0.0, min(1.0, volume))
    if pan < 0:
        left  = volume
        right = volume * (1.0 + pan)
    else:
        left  = volume * (1.0 - pan)
        right = volume
    return (left, right)

def any_track_soloed():
    return any((track['solo'] for track in drum_mixer)) or melody_mixer['solo'] or any((track.get('solo', False) for track in audio_tracks))

def mixer_track_audible(track):
    if track.get('muted', False):
        return False
    if any_track_soloed():
        return track.get('solo', False)
    return True

def play_sound_with_mixer(sound, track):
    if not mixer_track_audible(track):
        return
    channel = sound.play()
    if channel is not None:
        left, right = pan_to_lr(track.get('volume', 1.0), track.get('pan', 0.0))
        channel.set_volume(left, right)


# Simple built-in icons keep the app self-contained.
def make_icon_surface(size=(48, 48)):
    return pygame.Surface(size, pygame.SRCALPHA)

def create_kick_icon():
    surface = make_icon_surface()
    draw_circle(surface, TEXT_COLOR, (24, 24), 18, 3)
    draw_circle(surface, SECONDARY_TEXT, (24, 24), 5, 2)
    draw_line(surface, TEXT_COLOR, (12, 38), (8, 46), 3)
    draw_line(surface, TEXT_COLOR, (36, 38), (40, 46), 3)
    return surface

def create_snare_icon():
    surface = make_icon_surface()
    draw_elipse(surface, TEXT_COLOR, (7, 10, 34, 11), 2)
    draw_rect(surface, TEXT_COLOR, (7, 15, 34, 20), 2)
    draw_elipse(surface, TEXT_COLOR, (7, 29, 34, 11), 2)
    draw_line(surface, SECONDARY_TEXT, (10, 20), (38, 31), 2)
    draw_line(surface, SECONDARY_TEXT, (10, 31), (38, 20), 2)
    return surface

def create_hihat_icon():
    surface = make_icon_surface()
    draw_line(surface, TEXT_COLOR, (24, 12), (24, 42), 3)
    draw_line(surface, TEXT_COLOR, (10, 19), (38, 19), 3)
    draw_line(surface, SECONDARY_TEXT, (13, 23), (35, 23), 2)
    draw_line(surface, TEXT_COLOR, (17, 42), (31, 42), 3)
    return surface

def create_clap_icon():
    surface = make_icon_surface()
    pygame.draw.polygon(surface, TEXT_COLOR, [(8, 28), (14, 15), (18, 17), (16, 27), (22, 13), (26, 15), (22, 30), (29, 18), (33, 21), (27, 35), (17, 39)], 2)
    pygame.draw.polygon(surface, SECONDARY_TEXT, [(40, 26), (35, 14), (31, 17), (33, 27), (27, 13), (24, 16), (29, 31), (22, 20), (19, 23), (25, 37), (35, 39)], 2)
    return surface

def create_perc_icon():
    surface = make_icon_surface()
    draw_elipse(surface, TEXT_COLOR, (9, 5, 24, 28), 3)
    draw_line(surface, TEXT_COLOR, (27, 29), (39, 44), 5)
    draw_circle(surface, SECONDARY_TEXT, (19, 16), 3)
    draw_circle(surface, SECONDARY_TEXT, (25, 22), 3)
    return surface

def create_microphone_icon():
    surface = pygame.Surface((24, 24), pygame.SRCALPHA)
    draw_rect(surface, TEXT_COLOR, (8, 2, 8, 13), border_radius=4)
    pygame.draw.arc(surface, TEXT_COLOR, (5, 7, 14, 11), 3.14159, 6.28318, 2)
    draw_line(surface, TEXT_COLOR, (12, 17), (12, 22), 2)
    draw_line(surface, TEXT_COLOR, (8, 22), (16, 22), 2)
    return surface
kick_image = create_kick_icon()
snare_image = create_snare_icon()
hihat_image = create_hihat_icon()
clap_image = create_clap_icon()
perc_image = create_perc_icon()
microphone_image = create_microphone_icon()
drum_images = [kick_image, snare_image, hihat_image, clap_image, perc_image]
num_steps = 16
step_size = 28
step_gap = 8
sequencer_start_x = 300
track_start_y = 235
row_height = 68
pattern = []
for track in range(len(drum_tracks)):
    row = []
    for step in range(num_steps):
        row.append(False)
    pattern.append(row)
NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']

def note_to_midi(note):
    if '#' in note:
        note_name = note[:2]
        octave = int(note[2:])
    else:
        note_name = note[0]
        octave = int(note[1:])
    return 12 * (octave + 1) + NOTE_NAMES.index(note_name)

def note_frequency(note):
    midi = note_to_midi(note)
    return 440 * 2 ** ((midi - 69) / 12)
piano_notes = []
for midi in range(note_to_midi('C3'), note_to_midi('C6') + 1):
    octave = midi // 12 - 1
    name = NOTE_NAMES[midi % 12]
    piano_notes.append(f'{name}{octave}')
piano_notes.reverse()
instruments = ['SOFT', 'PLUCK', 'BASS', 'KEYS']
melody_instrument = 'SOFT'
melody_volume = 0.7
INSTRUMENT_COLORS = {'SOFT': (110, 95, 210), 'PLUCK': (45, 155, 95), 'BASS': (215, 130, 45), 'KEYS': (45, 125, 190)}
INSTRUMENT_HOVER_COLORS = {'SOFT': (130, 115, 225), 'PLUCK': (65, 175, 115), 'BASS': (230, 150, 65), 'KEYS': (65, 145, 210)}

def create_synth_wave(note, instrument):
    freq = note_frequency(note)
    duration = 0.4
    if instrument == 'PLUCK':
        duration = 0.25
    elif instrument == 'BASS':
        freq /= 2
        duration = 0.45
    elif instrument == 'KEYS':
        duration = 0.5
    count = int(sample_rate * duration)
    note_t = np.linspace(0, duration, count, endpoint=False)
    if instrument == 'SOFT':
        note_wave = np.sin(2 * np.pi * freq * note_t) * 0.75
        note_wave += np.sin(2 * np.pi * freq * 2 * note_t) * 0.15
        fade = np.exp(-5 * note_t)
    elif instrument == 'PLUCK':
        note_wave = np.sin(2 * np.pi * freq * note_t) * 0.65
        note_wave += np.sin(2 * np.pi * freq * 2 * note_t) * 0.25
        fade = np.exp(-14 * note_t)
    elif instrument == 'BASS':
        note_wave = np.sin(2 * np.pi * freq * note_t) * 0.8
        note_wave += np.sin(2 * np.pi * freq * 2 * note_t) * 0.12
        fade = np.exp(-6 * note_t)
    else:
        note_wave = np.sin(2 * np.pi * freq * note_t) * 0.55
        note_wave += np.sin(2 * np.pi * freq * 2 * note_t) * 0.22
        note_wave += np.sin(2 * np.pi * freq * 3 * note_t) * 0.1
        fade = np.exp(-4 * note_t)
    attack = np.minimum(1, note_t / 0.01)
    note_wave = note_wave * fade * attack * 0.65
    return note_wave
melody_sounds = {}
for instrument in instruments:
    for note in piano_notes:
        melody_sounds[note, instrument] = make_sound(create_synth_wave(note, instrument))
melody_pattern = []
for note in piano_notes:
    row = []
    for step in range(num_steps):
        row.append(None)
    melody_pattern.append(row)
piano_grid_start_x = 130
piano_step_width = 45
piano_grid_top = 225
piano_row_height = 28
visible_piano_rows = 12
piano_scroll = 11
audio_tracks = []
supported_audio_extensions = ['.wav', '.mp3', '.ogg']
audio_timeline_x = 210
audio_timeline_width = 740
audio_track_top = 270
audio_track_height = 72
recording_microphone = False
microphone_chunks = []
microphone_stream = None
microphone_record_samplerate = sample_rate
microphone_devices = [(index, device['name']) for index, device in enumerate(sd.query_devices()) if device['max_input_channels'] > 0]
selected_microphone_position = 0
try:
    default_input_device = sd.default.device[0]
    for position, (device_index, device_name) in enumerate(microphone_devices):
        if device_index == default_input_device:
            selected_microphone_position = position
            break
except Exception:
    pass

def get_selected_microphone():
    if not microphone_devices:
        return (None, 'NO MICROPHONE FOUND')
    return microphone_devices[selected_microphone_position]

def import_audio_file(path):
    extension = os.path.splitext(path)[1].lower()
    if extension not in supported_audio_extensions:
        print('Unsupported audio file:', path)
        return
    try:
        sound = pygame.mixer.Sound(path)
        sound_array = pygame.sndarray.array(sound)
        if len(sound_array.shape) == 2:
            waveform = np.mean(sound_array, axis=1)
        else:
            waveform = sound_array
        waveform = waveform.astype(np.float32)
        peak = np.max(np.abs(waveform))
        if peak > 0:
            waveform /= peak
        audio_tracks.append({'name': os.path.basename(path), 'path': path, 'sound': sound, 'waveform': waveform, 'length': sound.get_length(), 'start_step': 0, 'muted': False, 'solo': False, 'pan': 0.0, 'volume': 0.8})
        print('Imported:', os.path.basename(path))
    except Exception as error:
        print("Couldn't import:", path)
        print(error)


# Project files
def save_project():
    if recording_microphone:
        print('Stop microphone recording before saving.')
        return
    root = tk.Tk()
    root.withdraw()
    root.attributes('-topmost', True)
    path = filedialog.asksaveasfilename(title='Save JRYBeats Project', defaultextension='.jry', filetypes=[('JRYBeats Project', '*.jry'), ('JSON', '*.json'), ('All Files', '*.*')])
    root.destroy()
    if not path:
        return
    project = {'version': 2, 'bpm': bpm, 'current_view': current_view, 'pattern': pattern, 'melody_pattern': melody_pattern, 'melody_instrument': melody_instrument, 'piano_scroll': piano_scroll, 'drum_mixer': drum_mixer, 'melody_mixer': melody_mixer, 'audio_tracks': []}
    for track in audio_tracks:
        project['audio_tracks'].append({'name': track.get('name', 'Audio'), 'path': os.path.abspath(track.get('path', '')), 'start_step': track.get('start_step', 0), 'muted': track.get('muted', False), 'solo': track.get('solo', False), 'pan': track.get('pan', 0.0), 'volume': track.get('volume', 0.8)})
    try:
        with open(path, 'w', encoding='utf-8') as project_file:
            json.dump(project, project_file, indent=2)
        print('Project saved:', path)
    except Exception as error:
        print("Couldn't save project:", path)
        print(error)

def load_project():
    global bpm
    global current_view
    global pattern
    global melody_pattern
    global melody_instrument
    global piano_scroll
    global drum_mixer
    global melody_mixer
    global dragging_audio
    global mixer_drag
    if recording_microphone:
        print('Stop microphone recording before loading a project.')
        return
    root = tk.Tk()
    root.withdraw()
    root.attributes('-topmost', True)
    path = filedialog.askopenfilename(title='Load JRYBeats Project', filetypes=[('JRYBeats Project', '*.jry'), ('JSON', '*.json'), ('All Files', '*.*')])
    root.destroy()
    if not path:
        return
    try:
        with open(path, 'r', encoding='utf-8') as project_file:
            project = json.load(project_file)
        pygame.mixer.stop()
        bpm = max(min_bpm, min(max_bpm, int(project.get('bpm', bpm))))
        loaded_pattern = project.get('pattern')
        if isinstance(loaded_pattern, list) and len(loaded_pattern) == len(drum_tracks) and all((len(row) == num_steps for row in loaded_pattern)):
            pattern = loaded_pattern
        saved_instrument = project.get('melody_instrument', melody_instrument)
        if saved_instrument in instruments:
            melody_instrument = saved_instrument
        loaded_melody = project.get('melody_pattern')
        if isinstance(loaded_melody, list) and len(loaded_melody) == len(piano_notes) and all((len(row) == num_steps for row in loaded_melody)):
            converted_melody = []
            for row in loaded_melody:
                converted_row = []
                for cell in row:
                    if cell in instruments:
                        converted_row.append(cell)
                    elif cell is True:
                        converted_row.append(melody_instrument)
                    else:
                        converted_row.append(None)
                converted_melody.append(converted_row)
            melody_pattern = converted_melody
        piano_scroll = max(0, min(len(piano_notes) - visible_piano_rows, int(project.get('piano_scroll', piano_scroll))))
        loaded_drum_mixer = project.get('drum_mixer')
        if isinstance(loaded_drum_mixer, list) and len(loaded_drum_mixer) == len(drum_tracks):
            for i in range(len(drum_tracks)):
                drum_mixer[i].update(loaded_drum_mixer[i])
        loaded_melody_mixer = project.get('melody_mixer')
        if isinstance(loaded_melody_mixer, dict):
            melody_mixer.update(loaded_melody_mixer)
        audio_tracks.clear()
        missing_audio = []
        for saved_track in project.get('audio_tracks', []):
            audio_path = saved_track.get('path', '')
            if not os.path.exists(audio_path):
                missing_audio.append(audio_path)
                continue
            before_count = len(audio_tracks)
            import_audio_file(audio_path)
            if len(audio_tracks) > before_count:
                loaded_track = audio_tracks[-1]
                loaded_track['start_step'] = max(0, min(num_steps - 1, int(saved_track.get('start_step', 0))))
                loaded_track['muted'] = bool(saved_track.get('muted', False))
                loaded_track['solo'] = bool(saved_track.get('solo', False))
                loaded_track['pan'] = max(-1.0, min(1.0, float(saved_track.get('pan', 0.0))))
                loaded_track['volume'] = max(0.0, min(1.0, float(saved_track.get('volume', 0.8))))
        saved_view = project.get('current_view', 'SEQUENCER')
        if saved_view in ('SEQUENCER', 'PIANO', 'AUDIO', 'MIXER'):
            current_view = saved_view
        dragging_audio = None
        mixer_drag = None
        print('Project loaded:', path)
        if missing_audio:
            print('Missing audio files:')
            for missing_path in missing_audio:
                print(' -', missing_path)
    except Exception as error:
        print("Couldn't load project:", path)
        print(error)


# Microphone recording
def microphone_callback(indata, frames, time_info, status):
    if status:
        print(status)
    if recording_microphone:
        microphone_chunks.append(indata.copy())

def start_microphone_recording():
    global recording_microphone
    global microphone_chunks
    global microphone_stream
    global microphone_record_samplerate
    microphone_chunks = []
    device_index, device_name = get_selected_microphone()
    if device_index is None:
        print('No microphone input device found.')
        return
    try:
        device_info = sd.query_devices(device_index)
        record_samplerate = int(device_info['default_samplerate'])
        record_channels = min(1, device_info['max_input_channels'])
        if record_channels < 1:
            print('Selected device has no input channels:', device_name)
            return
        microphone_record_samplerate = record_samplerate
        microphone_stream = sd.InputStream(device=device_index, samplerate=record_samplerate, channels=record_channels, dtype='float32', callback=microphone_callback)
        microphone_stream.start()
        recording_microphone = True
        print('Recording microphone:', device_name, '@', record_samplerate, 'Hz')
    except Exception as error:
        microphone_stream = None
        recording_microphone = False
        print("Couldn't start microphone:", device_name)
        print(error)

def stop_microphone_recording():
    global recording_microphone
    global microphone_stream
    recording_microphone = False
    if microphone_stream is not None:
        microphone_stream.stop()
        microphone_stream.close()
        microphone_stream = None
    if len(microphone_chunks) == 0:
        print('No microphone audio recorded.')
        return
    recording = np.concatenate(microphone_chunks, axis=0)
    recording = np.clip(recording, -1, 1)
    recording_int16 = (recording * 32767).astype(np.int16)
    filename = time.strftime('JRYBeats_recording_%Y%m%d_%H%M%S.wav')
    with wave.open(filename, 'wb') as wav_file:
        wav_file.setnchannels(1)
        wav_file.setsampwidth(2)
        wav_file.setframerate(microphone_record_samplerate)
        wav_file.writeframes(recording_int16.tobytes())
    print('Saved recording:', filename)
    import_audio_file(filename)

def choose_audio_file():
    root = tk.Tk()
    root.withdraw()
    root.attributes('-topmost', True)
    path = filedialog.askopenfilename(title='Import Audio', filetypes=[('Audio Files', '*.wav *.mp3 *.ogg'), ('WAV', '*.wav'), ('MP3', '*.mp3'), ('OGG', '*.ogg'), ('All Files', '*.*')])
    root.destroy()
    if path:
        import_audio_file(path)
        
def draw_rect(surface, color, rect, width: int = 0, border_radius: int = -1,
              border_top_left_radius: int = -1, border_top_right_radius: int = -1, border_bottom_left_radius: int = -1, border_bottom_right_radius: int = -1,):
    pygame.draw.rect(surface, color, rect, width, 
                     border_radius, border_top_left_radius, border_top_right_radius, border_bottom_left_radius, border_bottom_right_radius)
    
def draw_line(surface, color, start_pos, end_pos, width: int = 1):
    pygame.draw.line(surface, color, start_pos, end_pos, width)
    
def draw_circle(surface, color, center, radius, width):
    return pygame.draw.circle(surface, color, center, radius, width)

def draw_elipse(surface, color, rect, width):
    return pygame.draw.ellipse(surface, color, rect, width)

def draw_waveform(waveform, rect, color):
    if len(waveform) == 0:
        return
    center_y = rect.centery
    samples_per_pixel = max(1, len(waveform) // max(1, rect.width))
    for x in range(rect.width):
        start = x * samples_per_pixel
        end = min(start + samples_per_pixel, len(waveform))
        if start >= len(waveform):
            break
        section = waveform[start:end]
        amplitude = np.max(np.abs(section))
        height = int(amplitude * (rect.height / 2 - 4))
        draw_line(screen, color, (rect.left + x, center_y - height), (rect.left + x, center_y + height), 1)
bpm = 120
min_bpm = 60
max_bpm = 200
playing = False
current_step = 0
next_step_time = 0
current_view = 'SEQUENCER'
dragging_audio = None

def play_step(step):
    for track in range(len(drum_tracks)):
        if pattern[track][step]:
            play_sound_with_mixer(drum_sounds[track], drum_mixer[track])
    for note_index in range(len(piano_notes)):
        cell_instrument = melody_pattern[note_index][step]
        if cell_instrument in instruments:
            sound = melody_sounds[piano_notes[note_index], cell_instrument]
            play_sound_with_mixer(sound, melody_mixer)
    for audio_track in audio_tracks:
        if audio_track['start_step'] == step and (not audio_track['muted']):
            play_sound_with_mixer(audio_track['sound'], audio_track)
play_rect = pygame.Rect(30, 82, 70, 42)
stop_rect = pygame.Rect(115, 82, 70, 42)
bpm_minus_rect = pygame.Rect(215, 82, 35, 42)
bpm_rect = pygame.Rect(250, 82, 70, 42)
bpm_plus_rect = pygame.Rect(320, 82, 35, 42)
sequencer_tab_rect = pygame.Rect(35, 150, 120, 40)
piano_tab_rect = pygame.Rect(185, 150, 130, 40)
audio_tab_rect = pygame.Rect(335, 150, 90, 40)
mixer_tab_rect = pygame.Rect(445, 150, 90, 40)
save_project_rect = pygame.Rect(805, 153, 72, 32)
load_project_rect = pygame.Rect(885, 153, 72, 32)
import_audio_rect = pygame.Rect(25, 215, 82, 36)
record_audio_rect = pygame.Rect(115, 215, 88, 36)
mic_prev_rect = pygame.Rect(455, 88, 30, 30)
mic_device_rect = pygame.Rect(490, 88, 420, 30)
mic_next_rect = pygame.Rect(915, 88, 30, 30)

# Main loop
running = True
while running:
    now = pygame.time.get_ticks()
    mouse_position = pygame.mouse.get_pos()
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.DROPFILE:
            dropped_file = event.file
            import_audio_file(dropped_file)
            current_view = 'AUDIO'
        elif event.type == pygame.MOUSEWHEEL:
            if current_view == 'PIANO':
                piano_scroll -= event.y
                piano_scroll = max(0, min(piano_scroll, len(piano_notes) - visible_piano_rows))
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_SPACE:
                if playing:
                    playing = False
                    pygame.mixer.stop()
                else:
                    playing = True
                    current_step = 0
                    play_step(current_step)
                    step_interval = 60000 / bpm / 4
                    next_step_time = now + step_interval
            elif event.key == pygame.K_LEFT:
                bpm = max(min_bpm, bpm - 5)
            elif event.key == pygame.K_RIGHT:
                bpm = min(max_bpm, bpm + 5)
        elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
            if play_rect.collidepoint(event.pos):
                if not playing:
                    playing = True
                    current_step = 0
                    play_step(current_step)
                    step_interval = 60000 / bpm / 4
                    next_step_time = now + step_interval
            elif stop_rect.collidepoint(event.pos):
                playing = False
                current_step = 0
                pygame.mixer.stop()
            elif bpm_minus_rect.collidepoint(event.pos):
                bpm = max(min_bpm, bpm - 5)
            elif bpm_plus_rect.collidepoint(event.pos):
                bpm = min(max_bpm, bpm + 5)
            elif sequencer_tab_rect.collidepoint(event.pos):
                current_view = 'SEQUENCER'
            elif piano_tab_rect.collidepoint(event.pos):
                current_view = 'PIANO'
            elif audio_tab_rect.collidepoint(event.pos):
                current_view = 'AUDIO'
            elif mixer_tab_rect.collidepoint(event.pos):
                current_view = 'MIXER'
            elif save_project_rect.collidepoint(event.pos):
                save_project()
            elif load_project_rect.collidepoint(event.pos):
                playing = False
                current_step = 0
                pygame.mixer.stop()
                load_project()
            elif current_view == 'AUDIO' and mic_prev_rect.collidepoint(event.pos):
                if microphone_devices and (not recording_microphone):
                    selected_microphone_position = (selected_microphone_position - 1) % len(microphone_devices)
            elif current_view == 'AUDIO' and (mic_next_rect.collidepoint(event.pos) or mic_device_rect.collidepoint(event.pos)):
                if microphone_devices and (not recording_microphone):
                    selected_microphone_position = (selected_microphone_position + 1) % len(microphone_devices)
            elif current_view == 'AUDIO' and import_audio_rect.collidepoint(event.pos):
                choose_audio_file()
            elif current_view == 'AUDIO' and record_audio_rect.collidepoint(event.pos):
                if recording_microphone:
                    stop_microphone_recording()
                else:
                    start_microphone_recording()
            elif current_view == 'MIXER':
                mixer_tracks = []
                for i, name in enumerate(drum_tracks):
                    mixer_tracks.append((name, drum_mixer[i]))
                mixer_tracks.append(('PIANO', melody_mixer))
                for i, audio_track in enumerate(audio_tracks):
                    mixer_tracks.append((audio_track['name'][:10], audio_track))
                strip_width = 105
                strip_start_x = 20
                strip_top = 225
                for i, (name, track_state) in enumerate(mixer_tracks[:9]):
                    x = strip_start_x + i * strip_width
                    mute_rect = pygame.Rect(x + 12, strip_top + 28, 34, 26)
                    solo_rect = pygame.Rect(x + 54, strip_top + 28, 34, 26)
                    volume_rect = pygame.Rect(x + 45, strip_top + 82, 14, 230)
                    pan_rect = pygame.Rect(x + 12, strip_top + 345, 76, 18)
                    if mute_rect.collidepoint(event.pos):
                        track_state['muted'] = not track_state.get('muted', False)
                        break
                    elif solo_rect.collidepoint(event.pos):
                        track_state['solo'] = not track_state.get('solo', False)
                        break
                    elif volume_rect.inflate(18, 0).collidepoint(event.pos):
                        mixer_drag = (track_state, 'volume', volume_rect)
                        ratio = (volume_rect.bottom - event.pos[1]) / volume_rect.height
                        track_state['volume'] = max(0.0, min(1.0, ratio))
                        break
                    elif pan_rect.inflate(0, 10).collidepoint(event.pos):
                        mixer_drag = (track_state, 'pan', pan_rect)
                        ratio = (event.pos[0] - pan_rect.left) / pan_rect.width
                        track_state['pan'] = max(-1.0, min(1.0, ratio * 2.0 - 1.0))
                        break
            elif current_view == 'SEQUENCER':
                for track in range(len(drum_tracks)):
                    y = track_start_y + track * row_height
                    for step in range(num_steps):
                        x = sequencer_start_x + step * (step_size + step_gap)
                        rect = pygame.Rect(x, y + 16, step_size, step_size)
                        if rect.collidepoint(event.pos):
                            pattern[track][step] = not pattern[track][step]
                            if pattern[track][step]:
                                play_sound_with_mixer(drum_sounds[track], drum_mixer[track])
            elif current_view == 'PIANO':
                clicked_something = False
                for visible_row in range(visible_piano_rows):
                    note_index = piano_scroll + visible_row
                    if note_index >= len(piano_notes):
                        continue
                    y = piano_grid_top + visible_row * piano_row_height
                    key_rect = pygame.Rect(20, y, 100, piano_row_height)
                    if key_rect.collidepoint(event.pos):
                        note = piano_notes[note_index]
                        sound = melody_sounds[note, melody_instrument]
                        play_sound_with_mixer(sound, melody_mixer)
                        clicked_something = True
                    for step in range(num_steps):
                        x = piano_grid_start_x + step * piano_step_width
                        cell_rect = pygame.Rect(x, y, piano_step_width, piano_row_height)
                        if cell_rect.collidepoint(event.pos):
                            current_cell_instrument = melody_pattern[note_index][step]
                            if current_cell_instrument == melody_instrument:
                                melody_pattern[note_index][step] = None
                            else:
                                melody_pattern[note_index][step] = melody_instrument
                                note = piano_notes[note_index]
                                sound = melody_sounds[note, melody_instrument]
                                play_sound_with_mixer(sound, melody_mixer)
                            clicked_something = True
                if not clicked_something:
                    instrument_y = 585
                    for i in range(len(instruments)):
                        rect = pygame.Rect(130 + i * 100, instrument_y, 85, 30)
                        if rect.collidepoint(event.pos):
                            melody_instrument = instruments[i]
            elif current_view == 'AUDIO':
                for index in range(len(audio_tracks)):
                    track = audio_tracks[index]
                    y = audio_track_top + index * audio_track_height
                    mute_rect = pygame.Rect(30, y + 20, 30, 25)
                    delete_rect = pygame.Rect(70, y + 20, 30, 25)
                    loop_duration = 60 / bpm / 4 * num_steps
                    clip_width = int(track['length'] / loop_duration * audio_timeline_width)
                    clip_width = max(50, clip_width)
                    clip_width = min(audio_timeline_width, clip_width)
                    clip_x = audio_timeline_x + int(track['start_step'] / num_steps * audio_timeline_width)
                    clip_rect = pygame.Rect(clip_x, y + 10, clip_width, 48)
                    if mute_rect.collidepoint(event.pos):
                        track['muted'] = not track['muted']
                        break
                    elif delete_rect.collidepoint(event.pos):
                        del audio_tracks[index]
                        break
                    elif clip_rect.collidepoint(event.pos):
                        dragging_audio = index
                        break
        elif event.type == pygame.MOUSEBUTTONUP and event.button == 1:
            dragging_audio = None
            mixer_drag = None
        elif event.type == pygame.MOUSEMOTION:
            if mixer_drag is not None:
                track_state, control_type, control_rect = mixer_drag
                if control_type == 'volume':
                    ratio = (control_rect.bottom - event.pos[1]) / control_rect.height
                    track_state['volume'] = max(0.0, min(1.0, ratio))
                elif control_type == 'pan':
                    ratio = (event.pos[0] - control_rect.left) / control_rect.width
                    track_state['pan'] = max(-1.0, min(1.0, ratio * 2.0 - 1.0))
            elif dragging_audio is not None and dragging_audio < len(audio_tracks):
                relative_x = event.pos[0] - audio_timeline_x
                ratio = relative_x / audio_timeline_width
                ratio = max(0, min(0.999, ratio))
                new_step = int(ratio * num_steps)
                audio_tracks[dragging_audio]['start_step'] = new_step
    if playing:
        step_interval = 60000 / bpm / 4
        while now >= next_step_time:
            current_step += 1
            if current_step >= num_steps:
                current_step = 0
            play_step(current_step)
            next_step_time += step_interval
    screen.fill(BACKGROUND)
    title = title_font.render('JRYBeats', True, TEXT_COLOR)
    screen.blit(title, (25, 18))
    draw_line(screen, LINE_COLOR, (0, 68), (WIDTH, 68), 1)
    draw_rect(screen, BUTTON_BACKGROUND, play_rect)
    draw_rect(screen, LINE_COLOR, play_rect, 2)
    pygame.draw.polygon(screen, GREEN, [(53, 91), (53, 115), (79, 103)])
    draw_rect(screen, BUTTON_BACKGROUND, stop_rect)
    draw_rect(screen, LINE_COLOR, stop_rect, 2)
    draw_rect(screen, RED, (140, 92, 20, 20))
    draw_rect(screen, BUTTON_BACKGROUND, bpm_minus_rect)
    draw_rect(screen, LINE_COLOR, bpm_minus_rect, 2)
    minus = small_font.render('-', True, TEXT_COLOR)
    screen.blit(minus, minus.get_rect(center=bpm_minus_rect.center))
    draw_rect(screen, BUTTON_BACKGROUND, bpm_rect)
    draw_rect(screen, LINE_COLOR, bpm_rect, 2)
    bpm_text = small_font.render(str(bpm), True, TEXT_COLOR)
    screen.blit(bpm_text, bpm_text.get_rect(center=bpm_rect.center))
    draw_rect(screen, BUTTON_BACKGROUND, bpm_plus_rect)
    draw_rect(screen, LINE_COLOR, bpm_plus_rect, 2)
    plus = small_font.render('+', True, TEXT_COLOR)
    screen.blit(plus, plus.get_rect(center=bpm_plus_rect.center))
    bpm_label = small_font.render('BPM', True, TEXT_COLOR)
    screen.blit(bpm_label, (365, 93))
    draw_line(screen, LINE_COLOR, (0, 140), (WIDTH, 140), 1)
    sequencer_color = TEXT_COLOR if current_view == 'SEQUENCER' else SECONDARY_TEXT
    piano_color = TEXT_COLOR if current_view == 'PIANO' else SECONDARY_TEXT
    audio_color = TEXT_COLOR if current_view == 'AUDIO' else SECONDARY_TEXT
    mixer_color = TEXT_COLOR if current_view == 'MIXER' else SECONDARY_TEXT
    sequencer_text = section_font.render('SEQUENCER', True, sequencer_color)
    piano_text = section_font.render('PIANO ROLL', True, piano_color)
    audio_text = section_font.render('AUDIO', True, audio_color)
    mixer_text = section_font.render('MIXER', True, mixer_color)
    screen.blit(sequencer_text, (35, 155))
    screen.blit(piano_text, (185, 155))
    screen.blit(audio_text, (335, 155))
    screen.blit(mixer_text, (445, 155))
    draw_rect(screen, BUTTON_BACKGROUND, save_project_rect)
    draw_rect(screen, LINE_COLOR, save_project_rect, 1)
    draw_rect(screen, BUTTON_BACKGROUND, load_project_rect)
    draw_rect(screen, LINE_COLOR, load_project_rect, 1)
    save_text = tiny_font.render('SAVE', True, TEXT_COLOR)
    load_text = tiny_font.render('LOAD', True, TEXT_COLOR)
    screen.blit(save_text, save_text.get_rect(center=save_project_rect.center))
    screen.blit(load_text, load_text.get_rect(center=load_project_rect.center))
    if current_view == 'SEQUENCER':
        draw_line(screen, BLUE, (35, 185), (153, 185), 3)
    elif current_view == 'PIANO':
        draw_line(screen, BLUE, (185, 185), (305, 185), 3)
    elif current_view == 'AUDIO':
        draw_line(screen, BLUE, (335, 185), (405, 185), 3)
    else:
        draw_line(screen, BLUE, (445, 185), (510, 185), 3)
    draw_line(screen, LINE_COLOR, (0, 198), (WIDTH, 198), 1)
    if current_view == 'SEQUENCER':
        for step in range(num_steps):
            x = sequencer_start_x + step * (step_size + step_gap)
            number = step_font.render(str(step + 1), True, TEXT_COLOR)
            screen.blit(number, number.get_rect(center=(x + step_size // 2, 216)))
        if playing:
            playhead_x = sequencer_start_x + current_step * (step_size + step_gap)
            draw_line(screen, PLAYHEAD_BLUE, (playhead_x + step_size // 2, 225), (playhead_x + step_size // 2, 575), 3)
        for track in range(len(drum_tracks)):
            y = track_start_y + track * row_height
            draw_line(screen, LINE_COLOR, (20, y + 60), (WIDTH - 20, y + 60), 1)
            screen.blit(drum_images[track], (30, y + 6))
            track_name = track_font.render(drum_tracks[track], True, TEXT_COLOR)
            screen.blit(track_name, (95, y + 20))
            for step in range(num_steps):
                x = sequencer_start_x + step * (step_size + step_gap)
                rect = pygame.Rect(x, y + 16, step_size, step_size)
                if pattern[track][step]:
                    color = GREEN
                    if rect.collidepoint(mouse_position):
                        color = GREEN_HOVER
                else:
                    color = STEP_BACKGROUND
                    if rect.collidepoint(mouse_position):
                        color = STEP_HOVER
                draw_rect(screen, color, rect)
                draw_rect(screen, LINE_COLOR, rect, 2)
    elif current_view == 'PIANO':
        for step in range(num_steps):
            x = piano_grid_start_x + step * piano_step_width
            number = step_font.render(str(step + 1), True, TEXT_COLOR)
            screen.blit(number, number.get_rect(center=(x + piano_step_width // 2, 213)))
        for visible_row in range(visible_piano_rows):
            note_index = piano_scroll + visible_row
            if note_index >= len(piano_notes):
                continue
            note = piano_notes[note_index]
            y = piano_grid_top + visible_row * piano_row_height
            sharp = '#' in note
            key_rect = pygame.Rect(20, y, 100, piano_row_height)
            if sharp:
                key_color = BLACK_KEY
                key_text_color = (240, 240, 240)
            else:
                key_color = WHITE_KEY
                key_text_color = TEXT_COLOR
            draw_rect(screen, key_color, key_rect)
            draw_rect(screen, LINE_COLOR, key_rect, 1)
            note_text = tiny_font.render(note, True, key_text_color)
            screen.blit(note_text, (35, y + 7))
            for step in range(num_steps):
                x = piano_grid_start_x + step * piano_step_width
                cell_rect = pygame.Rect(x, y, piano_step_width, piano_row_height)
                cell_instrument = melody_pattern[note_index][step]
                if cell_instrument in instruments:
                    cell_color = INSTRUMENT_COLORS[cell_instrument]
                    if cell_rect.collidepoint(mouse_position):
                        cell_color = INSTRUMENT_HOVER_COLORS[cell_instrument]
                else:
                    if sharp:
                        cell_color = (232, 232, 228)
                    else:
                        cell_color = (248, 248, 245)
                    if cell_rect.collidepoint(mouse_position):
                        cell_color = STEP_HOVER
                draw_rect(screen, cell_color, cell_rect)
                draw_rect(screen, LIGHT_LINE, cell_rect, 1)
        if playing:
            playhead_x = piano_grid_start_x + current_step * piano_step_width
            draw_line(screen, PLAYHEAD_BLUE, (playhead_x, piano_grid_top), (playhead_x, piano_grid_top + visible_piano_rows * piano_row_height), 3)
        instrument_y = 585
        for i in range(len(instruments)):
            rect = pygame.Rect(130 + i * 100, instrument_y, 85, 30)
            if melody_instrument == instruments[i]:
                draw_rect(screen, INSTRUMENT_COLORS[instruments[i]], rect)
                instrument_text_color = (255, 255, 255)
            else:
                draw_rect(screen, BUTTON_BACKGROUND, rect)
                instrument_text_color = TEXT_COLOR
            draw_rect(screen, LINE_COLOR, rect, 1)
            label = tiny_font.render(instruments[i], True, instrument_text_color)
            screen.blit(label, label.get_rect(center=rect.center))
    elif current_view == 'MIXER':
        mixer_tracks = []
        for i, name in enumerate(drum_tracks):
            mixer_tracks.append((name, drum_mixer[i]))
        mixer_tracks.append(('PIANO', melody_mixer))
        for audio_track in audio_tracks:
            mixer_tracks.append((audio_track['name'][:10], audio_track))
        strip_width = 105
        strip_start_x = 20
        strip_top = 225
        header = small_font.render('TRACK MIXER', True, TEXT_COLOR)
        screen.blit(header, (20, 207))
        for i, (name, track_state) in enumerate(mixer_tracks[:9]):
            x = strip_start_x + i * strip_width
            strip_rect = pygame.Rect(x, strip_top, 96, 390)
            draw_rect(screen, BUTTON_BACKGROUND, strip_rect)
            draw_rect(screen, LIGHT_LINE, strip_rect, 1)
            name_text = tiny_font.render(name[:11], True, TEXT_COLOR)
            screen.blit(name_text, name_text.get_rect(center=(x + 48, strip_top + 15)))
            mute_rect = pygame.Rect(x + 12, strip_top + 28, 34, 26)
            solo_rect = pygame.Rect(x + 54, strip_top + 28, 34, 26)
            mute_color = RED if track_state.get('muted', False) else BUTTON_BACKGROUND
            solo_color = GREEN if track_state.get('solo', False) else BUTTON_BACKGROUND
            draw_rect(screen, mute_color, mute_rect)
            draw_rect(screen, LINE_COLOR, mute_rect, 1)
            draw_rect(screen, solo_color, solo_rect)
            draw_rect(screen, LINE_COLOR, solo_rect, 1)
            mute_text = tiny_font.render('M', True, TEXT_COLOR)
            solo_text = tiny_font.render('S', True, TEXT_COLOR)
            screen.blit(mute_text, mute_text.get_rect(center=mute_rect.center))
            screen.blit(solo_text, solo_text.get_rect(center=solo_rect.center))
            volume_rect = pygame.Rect(x + 45, strip_top + 82, 14, 230)
            draw_rect(screen, LIGHT_LINE, volume_rect)
            volume = track_state.get('volume', 1.0)
            knob_y = int(volume_rect.bottom - volume * volume_rect.height)
            draw_rect(screen, BLUE, pygame.Rect(x + 37, knob_y - 5, 30, 10))
            volume_text = tiny_font.render(str(int(volume * 100)), True, SECONDARY_TEXT)
            screen.blit(volume_text, volume_text.get_rect(center=(x + 52, strip_top + 325)))
            pan_rect = pygame.Rect(x + 12, strip_top + 345, 76, 18)
            draw_line(screen, LIGHT_LINE, (pan_rect.left, pan_rect.centery), (pan_rect.right, pan_rect.centery), 3)
            pan = track_state.get('pan', 0.0)
            pan_x = int(pan_rect.left + (pan + 1.0) / 2.0 * pan_rect.width)
            draw_circle(screen, PURPLE, (pan_x, pan_rect.centery), 7)
            pan_label = tiny_font.render('PAN', True, SECONDARY_TEXT)
            screen.blit(pan_label, pan_label.get_rect(center=(x + 50, strip_top + 378)))
    elif current_view == 'AUDIO':
        draw_rect(screen, BUTTON_BACKGROUND, mic_prev_rect)
        draw_rect(screen, LINE_COLOR, mic_prev_rect, 1)
        draw_rect(screen, BUTTON_BACKGROUND, mic_device_rect)
        draw_rect(screen, LINE_COLOR, mic_device_rect, 1)
        draw_rect(screen, BUTTON_BACKGROUND, mic_next_rect)
        draw_rect(screen, LINE_COLOR, mic_next_rect, 1)
        previous_mic_text = small_font.render('<', True, TEXT_COLOR)
        next_mic_text = small_font.render('>', True, TEXT_COLOR)
        screen.blit(previous_mic_text, previous_mic_text.get_rect(center=mic_prev_rect.center))
        screen.blit(next_mic_text, next_mic_text.get_rect(center=mic_next_rect.center))
        selected_device_index, selected_device_name = get_selected_microphone()
        display_mic_name = selected_device_name
        while tiny_font.size('MIC: ' + display_mic_name)[0] > mic_device_rect.width - 18 and len(display_mic_name) > 4:
            display_mic_name = display_mic_name[:-1]
        if display_mic_name != selected_device_name:
            display_mic_name = display_mic_name[:-3] + '...'
        microphone_selector_text = tiny_font.render('MIC: ' + display_mic_name, True, TEXT_COLOR)
        screen.blit(microphone_selector_text, microphone_selector_text.get_rect(center=mic_device_rect.center))
        draw_rect(screen, BUTTON_BACKGROUND, import_audio_rect)
        draw_rect(screen, LINE_COLOR, import_audio_rect, 2)
        import_text = tiny_font.render('IMPORT', True, TEXT_COLOR)
        screen.blit(import_text, import_text.get_rect(center=import_audio_rect.center))
        record_button_color = BUTTON_BACKGROUND
        if recording_microphone:
            record_button_color = RED
        draw_rect(screen, record_button_color, record_audio_rect)
        draw_rect(screen, LINE_COLOR, record_audio_rect, 2)
        record_label = 'STOP' if recording_microphone else record_label = 'MIC'
        record_text = tiny_font.render(record_label, True, TEXT_COLOR)
        icon_x = record_audio_rect.x + 8
        icon_y = record_audio_rect.centery - microphone_image.get_height() // 2
        screen.blit(microphone_image, (icon_x, icon_y))
        record_text_rect = record_text.get_rect(midleft=(icon_x + microphone_image.get_width() + 5, record_audio_rect.centery))
        screen.blit(record_text, record_text_rect)
        for step in range(num_steps):
            x = audio_timeline_x + int(step / num_steps * audio_timeline_width)
            draw_line(screen, LIGHT_LINE, (x, 255), (x, HEIGHT - 25), 1)
            number = tiny_font.render(str(step + 1), True, SECONDARY_TEXT)
            screen.blit(number, (x + 4, 238))
        if playing:
            playhead_x = audio_timeline_x + int(current_step / num_steps * audio_timeline_width)
            draw_line(screen, PLAYHEAD_BLUE, (playhead_x, 255), (playhead_x, HEIGHT - 20), 3)
        for index in range(len(audio_tracks)):
            track = audio_tracks[index]
            y = audio_track_top + index * audio_track_height
            draw_line(screen, LIGHT_LINE, (20, y + 62), (WIDTH - 20, y + 62), 1)
            mute_rect = pygame.Rect(30, y + 20, 30, 25)
            mute_color = RED if track['muted'] else BUTTON_BACKGROUND
            draw_rect(screen, mute_color, mute_rect)
            draw_rect(screen, LINE_COLOR, mute_rect, 1)
            mute_text = tiny_font.render('M', True, TEXT_COLOR)
            screen.blit(mute_text, mute_text.get_rect(center=mute_rect.center))
            delete_rect = pygame.Rect(70, y + 20, 30, 25)
            draw_rect(screen, BUTTON_BACKGROUND, delete_rect)
            draw_rect(screen, LINE_COLOR, delete_rect, 1)
            delete_text = tiny_font.render('X', True, RED)
            screen.blit(delete_text, delete_text.get_rect(center=delete_rect.center))
            name_text = tiny_font.render(track['name'][:16], True, TEXT_COLOR)
            screen.blit(name_text, (108, y + 25))
            loop_duration = 60 / bpm / 4 * num_steps
            clip_width = int(track['length'] / loop_duration * audio_timeline_width)
            clip_width = max(50, clip_width)
            clip_width = min(audio_timeline_width, clip_width)
            clip_x = audio_timeline_x + int(track['start_step'] / num_steps * audio_timeline_width)
            clip_rect = pygame.Rect(clip_x, y + 10, clip_width, 48)
            draw_rect(screen, (240, 220, 195), clip_rect)
            draw_rect(screen, ORANGE, clip_rect, 2)
            draw_waveform(track['waveform'], clip_rect.inflate(-6, -6), ORANGE)
    pygame.display.update()
    clock.tick(60)
pygame.quit()
← Back to timeline