354 lines
12 KiB
Python
354 lines
12 KiB
Python
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()
|