python-drum-sequencer/sampSeq.py

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()