import threading import mido import pygame import glob import numpy import os import sys from subprocess import call class Data(): def __init__(self, *args, **kwargs): #stores point in sequence and bpm self.time = 120 self.started = False self.tick = 31 return super().__init__(*args, **kwargs) class Sequencer(threading.Thread): def __init__(self, event): threading.Thread.__init__(self) self.stopped = event threading.Thread.daemon = True def run(self): while not self.stopped.wait((60000/d.time) / 5000): if d.started == True: seqAdvance() # call a function class ThreadedMidi(threading.Thread): def __init__(self, *args, **kwargs): threading.Thread.__init__(self, *args, **kwargs) self.daemon = True self.start() def run(self): # get mido going ins = mido.get_input_names() print("Available MIDI inputs:", ins) # Look specifically for the LPD8 lpd8_ports = [port for port in ins if 'LPD8' in port] if not lpd8_ports: print("Warning: LPD8 not found in MIDI devices") return try: print(f"Attempting to connect to {lpd8_ports[0]}") inport = mido.open_input(lpd8_ports[0]) print("Successfully connected to LPD8") print("Waiting for MIDI messages... (Press pads or knobs on LPD8)") for msg in inport: print(f"Received MIDI message: {msg}") if msg.type != "program_change": if msg.type == "note_on": playSound(msg.note) else: toggleSeq() except Exception as e: print(f"Error in MIDI handling: {e}") ##FUNCTIONS def playSound(note): #Func is sent a note num, plays note and assigns it to sequence global delCur; global OVRHT; tok = d.tick i = 0; makeNoise(note) if d.started == True: if delCur == False: if OVRHT == True: while i < 5: if i < 4: if seq[i][tok] == 0: if re == True: if d.tick % 2: seq[i][tok - 1] = note; else: seq[i][tok] = note; else: seq[i][d.tick] = note; break; if seq[i][d.tick] == note: break; else: #first in is the first out seq[1][d.tick] = seq[0][d.tick] seq[2][d.tick] = seq[1][d.tick] seq[3][d.tick] = seq[2][d.tick] seq[0][d.tick] = note i = i + 1 def makeNoise(note): j = 0 while j < len(noteList): if note == noteList[j]: try: if s[j] and cha[j]: # Check if sound and channel exist cha[j].play(s[j]) else: print(f"Warning: Missing sound or channel for note {note}") except Exception as e: print(f"Error playing note {note}: {e}") break j = j + 1 def toggleSeq(): #Func turns on and off sequence if d.started == True: d.started = 0 print(d.started) else: d.started = 1 def seqAdvance(): #Func is called to cycle through the sequence if d.tick < seqSize - 1: d.tick += 1 else: d.tick = 0 if met == True: if d.tick == 0 or d.tick == 8 or d.tick == 16 or d.tick == 24: if d.tick == 0: pygame.mixer.Channel(8).set_volume(1); pygame.mixer.Channel(8).play(metro); else: pygame.mixer.Channel(8).set_volume(.4); pygame.mixer.Channel(8).play(metro); if seq[0][d.tick] != 0: makeNoise(seq[0][d.tick]) if seq[1][d.tick] != 0: makeNoise(seq[1][d.tick]) if seq[2][d.tick] != 0: makeNoise(seq[2][d.tick]) if seq[3][d.tick] != 0: makeNoise(seq[3][d.tick]) def recall(sequenceNum,re): if re == True: print("recalled sequence: " + str(sequenceNum)) seq[0] = sav[sequenceNum][0] seq[1] = sav[sequenceNum][1] seq[2] = sav[sequenceNum][2] seq[3] = sav[sequenceNum][3] else: print("saved sequence: " + str(sequenceNum)) sav[sequenceNum][0] = seq[0].copy() sav[sequenceNum][1] = seq[1].copy() sav[sequenceNum][2] = seq[2].copy() sav[sequenceNum][3] = seq[3].copy() def setWavs(pathnm): if re == True: pathnm = pathnm + 9 if pathnm <= foldNum: wavs = glob.glob(paths[pathnm - 1]) print("Loaded Bank " + str(paths[pathnm - 1])) wavs = sorted(wavs) for i in range(len(wavs)): if i > 15: break else: s[i] = pygame.mixer.Sound(wavs[i]) ##INIT PYGAME MIXER # Initialize with explicit settings and error handling try: pygame.mixer.pre_init(22050, -16, 8, 512) pygame.init() mixer_settings = pygame.mixer.get_init() if mixer_settings: print(f"Audio initialized - Sample rate: {mixer_settings[0]}Hz, Format: {mixer_settings[1]}, Channels: {mixer_settings[2]}") else: print("Warning: Mixer not properly initialized") except Exception as e: print(f"Error initializing audio: {e}") sys.exit(1) swing = 0 setMidi = False met = True ##GET WAVES FROM CURRENT FOLDER chnWav = False foldNum = 1 #CHANGE TO NUMBER OF SUBFOLDERS paths = [] # Initialize as empty list extension = '/*.wav' iya = 0 # Change from "." to "samples" directory for root, dirs, files in os.walk("samples"): for dir in dirs: print(dir) paths.append(os.path.join("samples", dir + extension)) # Use os.path.join for proper path handling iya = iya + 1 # If no directories found, use samples directory itself if not paths: paths = [os.path.join("samples", extension[1:])] # Remove leading slash from extension wavs = glob.glob(paths[0]) wavs = sorted(wavs) mets = glob.glob('*.wav') ##MIDI NOTE NUMBERS TO PLAY noteList = [36, 37, 38, 39, 40, 41, 42, 43] # noteList = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15] ##SEQUENCER SIZE seqSize = 32 ##SEQUENCES delCur = False stopper = False OVRHT = False seq = numpy.zeros(shape=(4,seqSize)) sav = numpy.zeros(shape=(7,4,seqSize)) ##SETUP MAIN VARIABLES IN DATA d = Data() ##SETUP SEQUENCER CLASS stopFlag = threading.Event() sequence = Sequencer(stopFlag) sequence.start() pygame.mixer.set_num_channels(9) ##SETUP ARRAYS FOR CHANNELS AND SOUNDFILES s = [0] * 16 cha = [0] * 16 metro = pygame.mixer.Sound(mets[0]) for i in range(len(wavs)): if i > 15: break else: try: print(f"Loading sound {i}: {wavs[i]}") if i > 7: cha[i] = pygame.mixer.Channel(i - 8) else: cha[i] = pygame.mixer.Channel(i) s[i] = pygame.mixer.Sound(wavs[i]) except Exception as e: print(f"Error loading sound {i}: {e}") s[i] = None cha[i] = None ##START RUNNING MIDI tm = ThreadedMidi(name='Play-Thread') re = True screen = pygame.display.set_mode((800, 600), pygame.RESIZABLE) pygame.display.set_caption("Sample Sequencer") # Add this helper function at the top with other functions def print_key_press(key_name, action): print(f"Key press: {key_name} - {action}") while True: for event in pygame.event.get(): if event.type == pygame.KEYDOWN: # Print raw key info for debugging print(f"Raw key event - key: {event.key}, unicode: {event.unicode}") # Main Functions if event.key in [pygame.K_KP8, pygame.K_8]: print_key_press("8", "Toggle Metronome") if not chnWav: # Only if not in sample change mode met = not met print(f"Metronome: {'ON' if met else 'OFF'}") elif event.key in [pygame.K_KP9, pygame.K_9]: print_key_press("9", "Toggle Recording Mode") if not chnWav: OVRHT = not OVRHT print(f"Recording Mode: {'ON' if OVRHT else 'OFF'}") elif event.key in [pygame.K_KP_ENTER, pygame.K_RETURN]: print_key_press("Enter", "Play/Pause sequence") toggleSeq() d.tick = 31 elif event.key in [pygame.K_KP0, pygame.K_0]: print_key_press("0", "Delete Current Note") if re: # When not in store mode delCur = True # Tempo Functions elif event.key in [pygame.K_KP_PLUS, pygame.K_PLUS, pygame.K_EQUALS]: if chnWav: print_key_press("+ *", "Rapidly Speed Up BPM") d.time = d.time + 15 elif not re: print_key_press("+", "Course Speed Up BPM") d.time = d.time + 1 else: print_key_press("+ .", "Fine Speed Up BPM") d.time = d.time + 5 print(f"New BPM: {d.time}") elif event.key in [pygame.K_KP_MINUS, pygame.K_MINUS]: if chnWav: print_key_press("- *", "Rapidly Slow Down BPM") d.time = d.time - 15 elif not re: print_key_press("-", "Course Slow Down BPM") d.time = d.time - 1 else: print_key_press("- .", "Fine Slow Down BPM") d.time = d.time - 5 print(f"New BPM: {d.time}") # Function key toggles elif event.key in [pygame.K_KP_MULTIPLY, pygame.K_ASTERISK]: print_key_press("*", "Function key pressed") chnWav = True elif event.key in [pygame.K_KP_PERIOD, pygame.K_PERIOD]: print_key_press(".", "Function key pressed") re = False # Sequence and Sample Pack Functions (1-6 for sequences, 1-9 for sample packs) for num in range(1, 10): if event.key in [getattr(pygame, f'K_KP{num}'), getattr(pygame, f'K_{num}')]: if chnWav: print_key_press(f"{num} *", f"Change to Sample Pack {num}") setWavs(num) elif num <= 6: # Sequence operations only work for 1-6 if re: print_key_press(f"{num}", f"Recall Sequence {num}") recall(num, True) else: print_key_press(f"{num} .", f"Store Sequence {num}") recall(num, False) # Shutdown combination if event.key == pygame.K_NUMLOCK and chnWav: print_key_press("Num Lock + *", "Shutdown triggered") call("touch numlock.test", shell=True) # call("sudo shutdown -h now", shell=True) # pygame.quit() # sys.exit() if event.type == pygame.KEYUP: if event.key in [pygame.K_KP_PERIOD, pygame.K_PERIOD]: print_key_press(".", "Function key released") re = True if event.key in [pygame.K_KP0, pygame.K_0]: print_key_press("0", "Delete key released") delCur = False if event.key in [pygame.K_KP_MULTIPLY, pygame.K_ASTERISK]: print_key_press("*", "Function key released") chnWav = False if event.type == pygame.VIDEORESIZE: screen = pygame.display.set_mode((event.w, event.h), pygame.RESIZABLE) print(f"Window resized to {event.w}x{event.h}") if event.type == pygame.QUIT: pygame.quit() sys.exit()