diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2570c4b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +mido +threading diff --git a/README.md b/README.md index fb30d29..03f44a1 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,56 @@ sample sequencer for raspberry pi Video Example here! https://youtu.be/zX5hSGyLj7c + +The sequencer has 4 polyphony and allows the user to store and cue up 6 different sequences +that they can alternate between in real time, and supports the ability to change between different samples. + +The Asterisk * and the Period. both operate as function keys. + +MAINFUNCTIONS + +[8] - Toggle Metronome on and off + +[9] - Toggle Recording Mode on and off + +[Enter] - Play/Pause sequence + +[0] - Delete Current Note in Sequence + +[Num Lock and *] - Shut down + +[MIDI Note and .] - Don't quantize to even numbers + +TEMPO FUNCTIONS + +[+] - Course Speed Up BPM + +[-] - Course Slow Down BPM + +[+ and *] - Rapidly Speed Up BPM + +[- and *] - Rapidly Slow Down BPM + +[+ and .] - Fine Speed Up BPM + +[- and .] - Fine Slow Down BPM + +SEQUENCE FUNCTIONS + +[1-6] Recall Sequence 1-6 + +[1-6 and .] Store Sequence 1-6 + +[0 and .] Clear Current Sequence + +CHANGING SAMPLE FOLDERS + +[1-9 and *] - Change to Sample Pack in Folders 1-9 + +[1-9 and * and .] - Change to Sample Pack in Folders 10-18 + +SEQUENCE TIPS: + +-Clear the current sequence [0 and .] and store it to any sequences you want cleared out of memory. + +-Recall a sequence and store it to a different number to copy it. diff --git a/audiohelp.py b/audiohelp.py new file mode 100644 index 0000000..3b60c8e --- /dev/null +++ b/audiohelp.py @@ -0,0 +1,79 @@ +import pygame +import sys +import os +import time + +def test_audio(): + print("=== Pygame Audio System Test ===") + print("Initializing Pygame Audio System...") + + # Initialize pygame mixer with specific settings + try: + pygame.mixer.pre_init(22050, -16, 8, 512) + pygame.init() + print("✓ Pygame initialized successfully") + except Exception as e: + print(f"✗ Failed to initialize pygame: {e}") + return False + + # Check mixer initialization + try: + print("\nChecking audio configuration...") + mixer_settings = pygame.mixer.get_init() + if mixer_settings: + freq, format, channels = mixer_settings + print(f"✓ Sample rate: {freq}Hz") + print(f"✓ Format: {format}") + print(f"✓ Channels: {channels}") + else: + print("✗ Mixer not initialized") + return False + except Exception as e: + print(f"✗ Failed to get mixer info: {e}") + return False + + # Try to load and play the metronome sound + try: + print("\nTesting sound playback...") + + if not os.path.exists("metronome.wav"): + print("✗ Could not find metronome.wav") + return False + + test_sound = pygame.mixer.Sound("metronome.wav") + print("✓ Metronome sound loaded successfully") + print("\nPlaying metronome sound on loop...") + print("Can you hear the sound? (y/n)") + + # Play sound on loop + channel = test_sound.play(-1) # -1 means loop indefinitely + + response = input().lower() + channel.stop() # Stop the sound + + if response == 'y': + print("✓ Audio test successful!") + return True + else: + print("✗ Audio test failed - no sound heard") + return False + + except Exception as e: + print(f"✗ Failed to play test sound: {e}") + return False + +if __name__ == "__main__": + success = test_audio() + + if success: + print("\n✓ Audio system is working correctly!") + else: + print("\n✗ Audio system test failed. Please check your audio configuration.") + print("Troubleshooting tips:") + print("1. Check if your speakers/headphones are connected and turned on") + print("2. Check if system volume is unmuted and turned up") + print("3. Verify metronome.wav exists in the same directory as this script") + print("4. Try running 'pygame.mixer.quit()' and reinitializing if audio stops working") + + print("\nPress Enter to exit...") + input() \ No newline at end of file diff --git a/midihelp.py b/midihelp.py old mode 100644 new mode 100755 index 5d73ea7..7dd3762 --- a/midihelp.py +++ b/midihelp.py @@ -1,9 +1,69 @@ -import threading import mido +import time +import sys +def test_midi(): + print("=== MIDI Connection Test ===") + + # Check available ports + try: + ins = mido.get_input_names() + print("\nAvailable MIDI input ports:") + for i, port in enumerate(ins): + print(f"{i + 1}. {port}") + + if not ins: + print("No MIDI input ports found!") + return False + + # Look for LPD8 + lpd8_ports = [port for port in ins if 'LPD8' in port] + if lpd8_ports: + print("\nFound LPD8 device:", lpd8_ports[0]) + port_name = lpd8_ports[0] + else: + print("\nLPD8 not found. Please select a port number:") + choice = input("> ") + try: + port_name = ins[int(choice) - 1] + except (ValueError, IndexError): + print("Invalid selection!") + return False + + # Try to open the port + print(f"\nAttempting to connect to: {port_name}") + with mido.open_input(port_name) as inport: + print("✓ Successfully opened MIDI port") + print("\nNow listening for MIDI messages...") + print("Press pads/knobs on your controller (Ctrl+C to exit)") + + try: + while True: + msg = inport.poll() + if msg: + print(f"Received: {msg}") + time.sleep(0.1) # Small delay to prevent CPU hogging + + except KeyboardInterrupt: + print("\nTest completed!") + return True + + except Exception as e: + print(f"\nError: {e}") + print("\nTroubleshooting tips:") + print("1. Make sure your MIDI device is properly connected") + print("2. Try unplugging and reconnecting the USB cable") + print("3. Check if device shows up in your system's MIDI devices") + print("4. Ensure no other applications are using the MIDI port") + return False -ins = mido.get_input_names() -print(ins) -inport = mido.open_input(ins[0]) -for msg in inport: - print(msg) +if __name__ == "__main__": + success = test_midi() + + if success: + print("\n✓ MIDI system is working correctly!") + else: + print("\n✗ MIDI test failed. Please check your MIDI configuration.") + + print("\nPress Enter to exit...") + input() diff --git a/sampSeq.py b/sampSeq.py index 0a8b768..3fcb3aa 100644 --- a/sampSeq.py +++ b/sampSeq.py @@ -31,18 +31,34 @@ class ThreadedMidi(threading.Thread): threading.Thread.__init__(self, *args, **kwargs) self.daemon = True self.start() + def run(self): # get mido going ins = mido.get_input_names() - print(ins) - inport = mido.open_input(ins[0]) - for msg in inport: - print(msg) - if msg.type != "program_change": - if msg.type == "note_on": - playSound(msg.note) - else: - toggleSeq() + 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): @@ -78,12 +94,18 @@ def playSound(note): def makeNoise(note): - j = 0; + j = 0 while j < len(noteList): - if note==noteList[j]: - cha[j].play(s[j]) - break; - j = j + 1 + 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 @@ -139,8 +161,18 @@ def setWavs(pathnm): ##INIT PYGAME MIXER -pygame.mixer.pre_init(22050, -16, 8, 512) -pygame.init() +# 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 @@ -148,21 +180,27 @@ met = True ##GET WAVES FROM CURRENT FOLDER chnWav = False foldNum = 1 #CHANGE TO NUMBER OF SUBFOLDERS -paths = [0] * foldNum +paths = [] # Initialize as empty list extension = '/*.wav' iya = 0 -for root, dirs, files in os.walk("."): + +# Change from "." to "samples" directory +for root, dirs, files in os.walk("samples"): for dir in dirs: print(dir) - paths[iya] = (dir + extension) + 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, 38, 40, 41, 43, 45, 47, 48, 50, 52, 53, 55, 57, 59, 60, 62] +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 @@ -186,91 +224,131 @@ cha = [0] * 16 metro = pygame.mixer.Sound(mets[0]) for i in range(len(wavs)): - if i > 15: break + if i > 15: + break else: - print(str(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]) + 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 -pygame.display.set_mode() +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: - if event.key == pygame.K_KP_PLUS: - if chnWav == True: d.time = d.time + 15 - else: - if re == False: d.time = d.time + 1 - else: d.time = d.time + 5 + # 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'}") - if event.key == pygame.K_KP_MINUS: - if chnWav == True: d.time = d.time - 15 - else: - if re == False: d.time = d.time - 1 - else: d.time = d.time - 5 - if event.key == pygame.K_KP_ENTER: + 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 - if event.key == pygame.K_KP_MULTIPLY: - chnWav = True - if event.key == pygame.K_NUMLOCK: - if chnWav == True: - call("sudo shutdown -h now", shell=True) - pygame.quit() - sys.exit() - - if event.key == pygame.K_KP0: - if re == False: - seq[0] = [0] * seqSize; - seq[1] = [0] * seqSize; - seq[2] = [0] * seqSize; - seq[3] = [0] * seqSize; - else: + + 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 - if event.key == pygame.K_KP_PERIOD: + # 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 - if event.key == pygame.K_KP8: - if chnWav == True: setWavs(8) - else: - if met == True: met = False - else: met = True - if event.key == pygame.K_KP9: - if chnWav == True: setWavs(9) - else: - if OVRHT == True: OVRHT = False - else: OVRHT = True - if event.key == pygame.K_KP1: - if chnWav == True: setWavs(1) - else: recall(1,re) - if event.key == pygame.K_KP2: - if chnWav == True: setWavs(2) - else: recall(2,re) - if event.key == pygame.K_KP3: - if chnWav == True: setWavs(3) - else: recall(3,re) - if event.key == pygame.K_KP4: - if chnWav == True: setWavs(4) - else: recall(4,re) - if event.key == pygame.K_KP5: - if chnWav == True: setWavs(5) - else: recall(5,re) - if event.key == pygame.K_KP6: - if chnWav == True: setWavs(6) - else: recall(6,re) - if event.key == pygame.K_KP7: - if chnWav == True: setWavs(7) - + + # 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 == pygame.K_KP_PERIOD: + if event.key in [pygame.K_KP_PERIOD, pygame.K_PERIOD]: + print_key_press(".", "Function key released") re = True - if event.key == pygame.K_KP0: + if event.key in [pygame.K_KP0, pygame.K_0]: + print_key_press("0", "Delete key released") delCur = False - if event.key == pygame.K_KP_MULTIPLY: + 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() diff --git a/808/000.wav b/samples/808/000.wav similarity index 100% rename from 808/000.wav rename to samples/808/000.wav diff --git a/808/001.wav b/samples/808/001.wav similarity index 100% rename from 808/001.wav rename to samples/808/001.wav diff --git a/808/002.wav b/samples/808/002.wav similarity index 100% rename from 808/002.wav rename to samples/808/002.wav diff --git a/808/003.wav b/samples/808/003.wav similarity index 100% rename from 808/003.wav rename to samples/808/003.wav diff --git a/808/004.wav b/samples/808/004.wav similarity index 100% rename from 808/004.wav rename to samples/808/004.wav diff --git a/808/005.wav b/samples/808/005.wav similarity index 100% rename from 808/005.wav rename to samples/808/005.wav diff --git a/808/006.wav b/samples/808/006.wav similarity index 100% rename from 808/006.wav rename to samples/808/006.wav diff --git a/808/007.wav b/samples/808/007.wav similarity index 100% rename from 808/007.wav rename to samples/808/007.wav diff --git a/808/008.wav b/samples/808/008.wav similarity index 100% rename from 808/008.wav rename to samples/808/008.wav diff --git a/808/009.wav b/samples/808/009.wav similarity index 100% rename from 808/009.wav rename to samples/808/009.wav diff --git a/808/010.wav b/samples/808/010.wav similarity index 100% rename from 808/010.wav rename to samples/808/010.wav diff --git a/808/011.wav b/samples/808/011.wav similarity index 100% rename from 808/011.wav rename to samples/808/011.wav diff --git a/808/012.wav b/samples/808/012.wav similarity index 100% rename from 808/012.wav rename to samples/808/012.wav diff --git a/808/013.wav b/samples/808/013.wav similarity index 100% rename from 808/013.wav rename to samples/808/013.wav diff --git a/808/014.wav b/samples/808/014.wav similarity index 100% rename from 808/014.wav rename to samples/808/014.wav diff --git a/808/015.wav b/samples/808/015.wav similarity index 100% rename from 808/015.wav rename to samples/808/015.wav