mirror of
https://github.com/Architeuthis-Flux/JumperlessV5.git
synced 2025-09-05 10:47:58 +00:00
353 lines
11 KiB
Python
353 lines
11 KiB
Python
"""
|
|
Jumperless Stylophone
|
|
====================
|
|
|
|
A musical instrument using the Jumperless probe and GPIO to generate audio tones.
|
|
Touch different breadboard pads to play different musical notes!
|
|
|
|
Hardware Setup:
|
|
1. Connect a small speaker or piezo buzzer between breadboard holes 30 and 60
|
|
2. The positive lead goes to hole 30 (GPIO output)
|
|
3. The negative lead goes to hole 60 (GND)
|
|
|
|
Usage:
|
|
exec(open('examples/stylophone.py').read())
|
|
|
|
Then call: stylophone()
|
|
"""
|
|
|
|
import time
|
|
|
|
# Musical note frequencies in Hz (4th octave)
|
|
NOTE_FREQUENCIES = {
|
|
1: 261.63, # C4
|
|
2: 277.18, # C#4/Db4
|
|
3: 293.66, # D4
|
|
4: 311.13, # D#4/Eb4
|
|
5: 329.63, # E4
|
|
6: 349.23, # F4
|
|
7: 369.99, # F#4/Gb4
|
|
8: 392.00, # G4
|
|
9: 415.30, # G#4/Ab4
|
|
10: 440.00, # A4 (concert pitch)
|
|
11: 466.16, # A#4/Bb4
|
|
12: 493.88, # B4
|
|
13: 523.25, # C5
|
|
14: 554.37, # C#5/Db5
|
|
15: 587.33, # D5
|
|
16: 622.25, # D#5/Eb5
|
|
17: 659.25, # E5
|
|
18: 698.46, # F5
|
|
19: 739.99, # F#5/Gb5
|
|
20: 783.99, # G5
|
|
21: 830.61, # G#5/Ab5
|
|
22: 880.00, # A5
|
|
23: 932.33, # A#5/Bb5
|
|
24: 987.77, # B5
|
|
25: 1046.50, # C6
|
|
}
|
|
|
|
# Extended pad mapping for more notes
|
|
EXTENDED_NOTE_MAP = {
|
|
# Bottom row (lower octave)
|
|
31: 130.81, # C3
|
|
32: 138.59, # C#3
|
|
33: 146.83, # D3
|
|
34: 155.56, # D#3
|
|
35: 164.81, # E3
|
|
36: 174.61, # F3
|
|
37: 185.00, # F#3
|
|
38: 196.00, # G3
|
|
39: 207.65, # G#3
|
|
40: 220.00, # A3
|
|
|
|
# Top row (middle octave) - pads 1-25
|
|
**NOTE_FREQUENCIES,
|
|
|
|
# Special pads for different octaves
|
|
26: 1108.73, # C#6
|
|
27: 1174.66, # D6
|
|
28: 1244.51, # D#6
|
|
29: 1318.51, # E6
|
|
30: 1396.91, # F6
|
|
|
|
# Arduino pin pads for special notes/effects
|
|
70: 65.41, # C2 (D0 pad) - very low
|
|
71: 69.30, # C#2 (D1 pad)
|
|
72: 73.42, # D2 (D2 pad)
|
|
73: 77.78, # D#2 (D3 pad)
|
|
74: 82.41, # E2 (D4 pad)
|
|
75: 87.31, # F2 (D5 pad)
|
|
76: 92.50, # F#2 (D6 pad)
|
|
77: 98.00, # G2 (D7 pad)
|
|
78: 103.83, # G#2 (D8 pad)
|
|
79: 110.00, # A2 (D9 pad)
|
|
80: 116.54, # A#2 (D10 pad)
|
|
81: 123.47, # B2 (D11 pad)
|
|
82: 130.81, # C3 (D12 pad)
|
|
83: 2093.00, # C7 (D13 pad) - very high
|
|
}
|
|
|
|
def setup_audio_output():
|
|
"""Set up GPIO for audio output and connect to breadboard"""
|
|
print("🔧 Setting up audio output...")
|
|
|
|
# Clear any existing connections
|
|
nodes_clear()
|
|
|
|
# Set GPIO1 as output for audio
|
|
gpio_set_dir(1, True) # Set as output
|
|
gpio_set(1, False) # Start low
|
|
|
|
# Connect GPIO1 to breadboard hole 30 (speaker positive)
|
|
connect(GPIO_1, 30)
|
|
print(" ✓ Connected GPIO1 to hole 30 (speaker +)")
|
|
|
|
# Connect GND to breadboard hole 60 (speaker negative)
|
|
connect(GND, 60)
|
|
print(" ✓ Connected GND to hole 60 (speaker -)")
|
|
|
|
print("\n📢 HARDWARE SETUP REQUIRED:")
|
|
print(" 1. Connect a small speaker or piezo buzzer:")
|
|
print(" • Positive lead → Breadboard hole 30")
|
|
print(" • Negative lead → Breadboard hole 60")
|
|
print(" 2. Press any key when ready...")
|
|
|
|
# Wait for user confirmation
|
|
input()
|
|
|
|
oled_clear()
|
|
oled_print("Stylophone Ready!")
|
|
print("✓ Setup complete!")
|
|
|
|
def play_tone(frequency, duration_ms=100):
|
|
"""Generate a tone by toggling GPIO at the specified frequency"""
|
|
if frequency <= 0:
|
|
return
|
|
|
|
# Calculate timing for frequency
|
|
period_s = 1.0 / frequency # Period in seconds
|
|
half_period_s = period_s / 2 # Half period for 50% duty cycle
|
|
|
|
# Convert duration to number of cycles
|
|
cycles = int((duration_ms / 1000.0) / period_s)
|
|
|
|
# Generate the tone by toggling GPIO
|
|
for _ in range(cycles):
|
|
gpio_set(1, True)
|
|
time.sleep(half_period_s)
|
|
gpio_set(1, False)
|
|
time.sleep(half_period_s)
|
|
|
|
def play_startup_melody():
|
|
"""Play a welcome melody"""
|
|
print("🎵 Playing startup melody...")
|
|
oled_clear()
|
|
oled_print("Welcome!")
|
|
|
|
# Simple ascending scale
|
|
notes = [261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25]
|
|
for freq in notes:
|
|
play_tone(freq, 200)
|
|
time.sleep(0.05)
|
|
|
|
time.sleep(0.5)
|
|
|
|
def show_note_map():
|
|
"""Display the note mapping"""
|
|
print("\n🎹 STYLOPHONE NOTE MAP:")
|
|
print("="*50)
|
|
print("📍 Breadboard Pads (1-30):")
|
|
print(" Pads 1-12: C4-B4 (middle octave)")
|
|
print(" Pads 13-25: C5-C6 (higher octave)")
|
|
print(" Pads 26-30: Extended high notes")
|
|
print(" Pads 31-40: C3-A3 (lower octave)")
|
|
|
|
print("\n🔌 Arduino Pin Pads:")
|
|
print(" D0-D12 pads: Very low notes (C2-C3)")
|
|
print(" D13 pad: Very high note (C7)")
|
|
|
|
print("\n🎵 Special Notes:")
|
|
print(f" Pad 10: A4 (Concert pitch - {NOTE_FREQUENCIES[10]:.1f} Hz)")
|
|
print(f" Pad 1: C4 (Middle C - {NOTE_FREQUENCIES[1]:.1f} Hz)")
|
|
print(f" Pad 25: C6 (High C - {EXTENDED_NOTE_MAP[25]:.1f} Hz)")
|
|
|
|
def stylophone():
|
|
"""Main stylophone function"""
|
|
print("╭─────────────────────────────────────────────────────────────────╮")
|
|
print("│ JUMPERLESS STYLOPHONE │")
|
|
print("╰─────────────────────────────────────────────────────────────────╯")
|
|
|
|
# Setup
|
|
setup_audio_output()
|
|
play_startup_melody()
|
|
show_note_map()
|
|
|
|
print("\n🎼 STYLOPHONE CONTROLS:")
|
|
print(" • Touch breadboard pads to play notes")
|
|
print(" • Touch and hold for sustained notes")
|
|
print(" • Touch D13 pad for special high note")
|
|
print(" • Press Ctrl+Q to exit")
|
|
print("\n🎹 Now playing! Touch the pads...")
|
|
|
|
oled_clear()
|
|
oled_print("Touch pads!")
|
|
|
|
current_note = None
|
|
last_pad = None
|
|
note_start_time = 0
|
|
|
|
try:
|
|
while True:
|
|
# Read probe non-blocking
|
|
pad = probe_read_nonblocking()
|
|
|
|
# Check if we have a valid touch
|
|
if pad and pad != -1:
|
|
# Try to get the pad number - could be the object itself or its string representation
|
|
try:
|
|
if hasattr(pad, 'value'):
|
|
pad_num = pad.value
|
|
else:
|
|
pad_str = str(pad)
|
|
if pad_str.isdigit():
|
|
pad_num = int(pad_str)
|
|
else:
|
|
pad_num = int(pad)
|
|
except (ValueError, AttributeError):
|
|
continue # Skip this iteration if we can't get a valid pad number
|
|
|
|
# Get frequency for this pad
|
|
frequency = EXTENDED_NOTE_MAP.get(pad_num, 0)
|
|
|
|
if frequency > 0 and pad_num != last_pad:
|
|
# New note touched
|
|
print(f"🎵 Playing pad {pad_num}: {frequency:.1f} Hz")
|
|
|
|
# Display on OLED
|
|
oled_clear()
|
|
if pad_num in NOTE_FREQUENCIES:
|
|
# Show note name for standard notes
|
|
note_names = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
|
|
note_index = (pad_num - 1) % 12
|
|
octave = 4 + (pad_num - 1) // 12
|
|
note_name = f"{note_names[note_index]}{octave}"
|
|
oled_print(f"{note_name} ({frequency:.0f}Hz)")
|
|
else:
|
|
oled_print(f"Pad {pad_num}")
|
|
|
|
current_note = frequency
|
|
last_pad = pad_num
|
|
note_start_time = time.time()
|
|
|
|
# Continue playing the current note
|
|
if current_note:
|
|
play_tone(current_note, 50)
|
|
|
|
else:
|
|
# No touch detected
|
|
if current_note:
|
|
# Note released
|
|
duration = (time.time() - note_start_time) * 1000 # Convert to ms
|
|
print(f" Note released after {duration:.0f}ms")
|
|
current_note = None
|
|
last_pad = None
|
|
|
|
oled_clear()
|
|
oled_print("Touch pads!")
|
|
|
|
# Small delay when not playing
|
|
time.sleep(0.01)
|
|
|
|
except KeyboardInterrupt:
|
|
print("\n🎵 Stylophone stopped")
|
|
|
|
finally:
|
|
# Cleanup
|
|
gpio_set(1, False) # Make sure audio is off
|
|
oled_clear()
|
|
oled_print("Goodbye!")
|
|
print("✓ Audio output disabled")
|
|
|
|
def demo_all_notes():
|
|
"""Play all available notes in sequence"""
|
|
print("🎼 Playing all notes...")
|
|
setup_audio_output()
|
|
|
|
oled_clear()
|
|
oled_print("Note Demo")
|
|
|
|
# Play notes in order
|
|
for pad_num in sorted(EXTENDED_NOTE_MAP.keys()):
|
|
frequency = EXTENDED_NOTE_MAP[pad_num]
|
|
print(f"Pad {pad_num}: {frequency:.1f} Hz")
|
|
|
|
oled_clear()
|
|
oled_print(f"Pad {pad_num}")
|
|
|
|
play_tone(frequency, 300)
|
|
time.sleep(0.2)
|
|
|
|
print("✓ Note demo complete")
|
|
|
|
def chord_demo():
|
|
"""Demonstrate playing chords by rapidly alternating notes"""
|
|
print("🎼 Chord demonstration...")
|
|
setup_audio_output()
|
|
|
|
# Define some chords (as frequency lists)
|
|
chords = {
|
|
"C Major": [261.63, 329.63, 392.00], # C-E-G
|
|
"F Major": [349.23, 440.00, 523.25], # F-A-C
|
|
"G Major": [392.00, 493.88, 587.33], # G-B-D
|
|
"A Minor": [440.00, 523.25, 659.25], # A-C-E
|
|
}
|
|
|
|
for chord_name, frequencies in chords.items():
|
|
print(f"Playing {chord_name}...")
|
|
oled_clear()
|
|
oled_print(chord_name)
|
|
|
|
# Play chord by rapidly alternating between notes
|
|
for _ in range(20): # 20 cycles of the chord
|
|
for freq in frequencies:
|
|
play_tone(freq, 30) # Short duration per note
|
|
|
|
time.sleep(0.5)
|
|
|
|
print("✓ Chord demo complete")
|
|
|
|
def test_audio_setup():
|
|
"""Test the audio setup with simple tones"""
|
|
print("🔧 Testing audio setup...")
|
|
|
|
setup_audio_output()
|
|
|
|
test_frequencies = [440, 880, 1320] # A4, A5, E6
|
|
|
|
for i, freq in enumerate(test_frequencies):
|
|
print(f"Test tone {i+1}: {freq} Hz")
|
|
oled_clear()
|
|
oled_print(f"Test {freq}Hz")
|
|
play_tone(freq, 1000) # 1 second tone
|
|
time.sleep(0.5)
|
|
|
|
print("✓ Audio test complete")
|
|
|
|
# Quick access functions
|
|
def quick_stylophone():
|
|
"""Quick start stylophone with minimal setup"""
|
|
setup_audio_output()
|
|
stylophone()
|
|
|
|
if __name__ == "__main__":
|
|
print("Jumperless Stylophone loaded!")
|
|
print("\nAvailable functions:")
|
|
print(" stylophone() - Start the full stylophone")
|
|
print(" quick_stylophone() - Quick start")
|
|
print(" demo_all_notes() - Play all notes in sequence")
|
|
print(" chord_demo() - Demonstrate chord playing")
|
|
print(" test_audio_setup() - Test audio output")
|
|
print(" show_note_map() - Show pad-to-note mapping")
|
|
else:
|
|
print("Stylophone module loaded. Call stylophone() to start playing!") |