Merge branch 'main' of https://github.com/adafruit/Adafruit_Learning_System_Guides
|
|
@ -11,7 +11,7 @@ LOOP = False # Update to True loop WAV playback. False plays once.
|
|||
|
||||
audio = audiobusio.I2SOut(board.A2, board.A1, board.A0)
|
||||
|
||||
with open("chikken.wav", "rb") as wave_file:
|
||||
with open("booploop.wav", "rb") as wave_file:
|
||||
wav = audiocore.WaveFile(wave_file)
|
||||
|
||||
print("Playing wav file!")
|
||||
|
|
|
|||
27
Adafruit_IO_Reed_Switch/rpi-pico-w112213141.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"exportVersion": "1.0.0",
|
||||
"exportedBy": "tyeth_demo",
|
||||
"exportedAt": "2025-05-02T17:08:03.857Z",
|
||||
"exportedFromDevice": {
|
||||
"board": "rpi-pico-w",
|
||||
"firmwareVersion": "1.0.0-beta.100"
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"name": "Reed Switch",
|
||||
"pinName": "D13",
|
||||
"type": "reed_switch",
|
||||
"mode": "DIGITAL",
|
||||
"direction": "INPUT",
|
||||
"period": 0,
|
||||
"pull": "UP",
|
||||
"isPin": true,
|
||||
"visualization": {
|
||||
"offLabel": "Open",
|
||||
"offIcon": "fa6:solid:door-open",
|
||||
"onLabel": "Closed",
|
||||
"onIcon": "fa6:regular:door-closed"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -98,11 +98,11 @@ def on_message(client, feed_id, payload):
|
|||
|
||||
def on_relay_msg(client, topic, message):
|
||||
# Method called whenever user/feeds/relay has a new value
|
||||
if message == "morning":
|
||||
print("Morning - turning outlet ON")
|
||||
if message == "1":
|
||||
print("Received 1 - turning outlet ON")
|
||||
power_pin.value = True
|
||||
elif message == "night":
|
||||
print("Night - turning outlet OFF")
|
||||
elif message == "0":
|
||||
print("Received 0 - turning outlet OFF")
|
||||
power_pin.value = False
|
||||
else:
|
||||
print("Unexpected value received on relay feed.")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
// SPDX-FileCopyrightText: 2025 Limor Fried for Adafruit Industries
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
#include <Arduino.h>
|
||||
#include "WiFi.h"
|
||||
#include <Adafruit_TestBed.h>
|
||||
#include "ESP_I2S.h"
|
||||
extern Adafruit_TestBed TB;
|
||||
|
||||
// I2S pin definitions
|
||||
const uint8_t I2S_SCK = 14; // BCLK
|
||||
const uint8_t I2S_WS = 12; // LRCLK
|
||||
const uint8_t I2S_DIN = 13; // DATA_IN
|
||||
I2SClass i2s;
|
||||
|
||||
// the setup routine runs once when you press reset:
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
pinMode(LED_BUILTIN, OUTPUT);
|
||||
digitalWrite(LED_BUILTIN, HIGH);
|
||||
i2s.setPins(I2S_SCK, I2S_WS, -1, I2S_DIN);
|
||||
if (!i2s.begin(I2S_MODE_STD, 44100, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO, I2S_STD_SLOT_LEFT)) {
|
||||
Serial.println("Failed to initialize I2S bus!");
|
||||
return;
|
||||
}
|
||||
// TestBed will handle the neopixel swirl for us
|
||||
TB.neopixelPin = PIN_NEOPIXEL;
|
||||
TB.neopixelNum = 1;
|
||||
TB.begin();
|
||||
|
||||
// Set WiFi to station mode and disconnect from an AP if it was previously connected
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.disconnect();
|
||||
}
|
||||
|
||||
// the loop routine runs over and over again forever:
|
||||
uint8_t wheelColor=0;
|
||||
void loop() {
|
||||
if (wheelColor == 0) {
|
||||
// Test WiFi Scan!
|
||||
// WiFi.scanNetworks will return the number of networks found
|
||||
int n = WiFi.scanNetworks();
|
||||
Serial.print("WiFi AP scan done...");
|
||||
if (n == 0) {
|
||||
Serial.println("no networks found");
|
||||
} else {
|
||||
Serial.print(n);
|
||||
Serial.println(" networks found");
|
||||
for (int i = 0; i < n; ++i) {
|
||||
// Print SSID and RSSI for each network found
|
||||
Serial.print(i + 1);
|
||||
Serial.print(": ");
|
||||
Serial.print(WiFi.SSID(i));
|
||||
Serial.print(" (");
|
||||
Serial.print(WiFi.RSSI(i));
|
||||
Serial.print(")");
|
||||
Serial.println((WiFi.encryptionType(i) == WIFI_AUTH_OPEN)?" ":"*");
|
||||
delay(10);
|
||||
}
|
||||
}
|
||||
Serial.println("");
|
||||
for (int i=0; i < 5; i++) {
|
||||
int32_t sample = i2s.read();
|
||||
if (sample >= 0){
|
||||
Serial.print("Amplitude: ");
|
||||
Serial.println(sample);
|
||||
|
||||
// Delay to avoid printing too quickly
|
||||
delay(200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TB.setColor(TB.Wheel(wheelColor++)); // swirl NeoPixel
|
||||
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
|
||||
|
||||
delay(5);
|
||||
}
|
||||
116
Fruit_Jam/Larsio_Paint_Music/code.py
Executable file
|
|
@ -0,0 +1,116 @@
|
|||
# SPDX-FileCopyrightText: 2025 John Park and Claude AI for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""
|
||||
Larsio Paint Music
|
||||
Fruit Jam w mouse, HDMI, audio out
|
||||
or Metro RP2350 with EYESPI DVI breakout and TLV320DAC3100 breakout on STEMMA_I2C,
|
||||
pin D7 reset, 9/10/11 = BCLC/WSEL/DIN
|
||||
"""
|
||||
# pylint: disable=invalid-name,too-few-public-methods,broad-except,redefined-outer-name
|
||||
|
||||
# Main application file for Larsio Paint Music
|
||||
|
||||
import time
|
||||
import gc
|
||||
from sound_manager import SoundManager
|
||||
from note_manager import NoteManager
|
||||
from ui_manager import UIManager
|
||||
|
||||
# Configuration
|
||||
AUDIO_OUTPUT = "i2s" # Options: "pwm" or "i2s"
|
||||
|
||||
class MusicStaffApp:
|
||||
"""Main application class that ties everything together"""
|
||||
|
||||
def __init__(self, audio_output="pwm"):
|
||||
# Initialize the sound manager with selected audio output
|
||||
# Calculate tempo parameters
|
||||
BPM = 120 # Beats per minute
|
||||
SECONDS_PER_BEAT = 60 / BPM
|
||||
SECONDS_PER_EIGHTH = SECONDS_PER_BEAT / 2
|
||||
|
||||
# Initialize components in a specific order
|
||||
# First, force garbage collection to free memory
|
||||
gc.collect()
|
||||
|
||||
# Initialize the sound manager
|
||||
print("Initializing sound manager...")
|
||||
self.sound_manager = SoundManager(
|
||||
audio_output=audio_output,
|
||||
seconds_per_eighth=SECONDS_PER_EIGHTH
|
||||
)
|
||||
|
||||
# Give hardware time to stabilize
|
||||
time.sleep(0.5)
|
||||
gc.collect()
|
||||
|
||||
# Initialize the note manager
|
||||
print("Initializing note manager...")
|
||||
self.note_manager = NoteManager(
|
||||
start_margin=25, # START_MARGIN
|
||||
staff_y_start=int(240 * 0.1), # STAFF_Y_START
|
||||
line_spacing=int((240 - int(240 * 0.1) - int(240 * 0.2)) * 0.95) // 8 # LINE_SPACING
|
||||
)
|
||||
|
||||
gc.collect()
|
||||
|
||||
# Initialize the UI manager
|
||||
print("Initializing UI manager...")
|
||||
self.ui_manager = UIManager(self.sound_manager, self.note_manager)
|
||||
|
||||
def run(self):
|
||||
"""Set up and run the application"""
|
||||
# Setup the display and UI
|
||||
print("Setting up display...")
|
||||
self.ui_manager.setup_display()
|
||||
|
||||
# Give hardware time to stabilize
|
||||
time.sleep(0.5)
|
||||
gc.collect()
|
||||
|
||||
# Try to find the mouse with multiple attempts
|
||||
MAX_ATTEMPTS = 5
|
||||
RETRY_DELAY = 1 # seconds
|
||||
|
||||
mouse_found = False
|
||||
for attempt in range(MAX_ATTEMPTS):
|
||||
print(f"Mouse detection attempt {attempt+1}/{MAX_ATTEMPTS}")
|
||||
if self.ui_manager.find_mouse():
|
||||
mouse_found = True
|
||||
print("Mouse found successfully!")
|
||||
break
|
||||
|
||||
print(f"Mouse detection attempt {attempt+1} failed, retrying...")
|
||||
time.sleep(RETRY_DELAY)
|
||||
|
||||
if not mouse_found:
|
||||
print("WARNING: Mouse not found after multiple attempts.")
|
||||
print("The application will run, but mouse control may be limited.")
|
||||
|
||||
# Enter the main loop
|
||||
self.ui_manager.main_loop()
|
||||
|
||||
|
||||
# Create and run the application
|
||||
if __name__ == "__main__":
|
||||
# Start with garbage collection
|
||||
gc.collect()
|
||||
print("Starting Music Staff Application...")
|
||||
|
||||
try:
|
||||
app = MusicStaffApp(audio_output=AUDIO_OUTPUT)
|
||||
app.run()
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
print(f"Error with I2S audio: {e}")
|
||||
|
||||
# Force garbage collection
|
||||
gc.collect()
|
||||
time.sleep(1)
|
||||
|
||||
# Fallback to PWM
|
||||
try:
|
||||
app = MusicStaffApp(audio_output="pwm")
|
||||
app.run()
|
||||
except Exception as e2: # pylint: disable=broad-except
|
||||
print(f"Fatal error: {e2}")
|
||||
353
Fruit_Jam/Larsio_Paint_Music/control_panel.py
Executable file
|
|
@ -0,0 +1,353 @@
|
|||
# SPDX-FileCopyrightText: 2025 John Park and Claude AI for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""
|
||||
# control_panel.py: CircuitPython Music Staff Application component
|
||||
"""
|
||||
|
||||
# pylint: disable=import-error
|
||||
from displayio import Group, Bitmap, Palette, TileGrid
|
||||
from adafruit_display_text.bitmap_label import Label
|
||||
import terminalio
|
||||
|
||||
|
||||
# pylint: disable=invalid-name,no-member,too-many-instance-attributes,too-many-arguments
|
||||
# pylint: disable=too-many-branches,too-many-statements, trailing-whitespace
|
||||
class ControlPanel:
|
||||
"""Manages transport controls and channel selectors"""
|
||||
|
||||
def __init__(self, screen_width, screen_height):
|
||||
self.SCREEN_WIDTH = screen_width
|
||||
self.SCREEN_HEIGHT = screen_height
|
||||
|
||||
# Button dimensions
|
||||
self.BUTTON_WIDTH = 64 # Updated for bitmap buttons
|
||||
self.BUTTON_HEIGHT = 48 # Updated for bitmap buttons
|
||||
self.BUTTON_SPACING = 10
|
||||
|
||||
# Channel button dimensions
|
||||
self.CHANNEL_BUTTON_SIZE = 20
|
||||
self.CHANNEL_BUTTON_SPACING = 5
|
||||
self.CHANNEL_BUTTON_Y = 5
|
||||
|
||||
# Transport area
|
||||
self.TRANSPORT_AREA_Y = (int(screen_height * 0.1) +
|
||||
int((screen_height - int(screen_height * 0.1) -
|
||||
int(screen_height * 0.2)) * 0.95) + 10)
|
||||
|
||||
# State
|
||||
self.is_playing = False
|
||||
self.loop_enabled = False
|
||||
|
||||
# Channel colors (reduced from 8 to 6)
|
||||
self.CHANNEL_COLORS = [
|
||||
0x000000, # Channel 1: Black (default)
|
||||
0xFF0000, # Channel 2: Red
|
||||
0x00FF00, # Channel 3: Green
|
||||
0x0000FF, # Channel 4: Blue
|
||||
0xFF00FF, # Channel 5: Magenta
|
||||
0xFFAA00, # Channel 6: Orange
|
||||
]
|
||||
self.current_channel = 0
|
||||
|
||||
# UI elements
|
||||
self.play_button = None
|
||||
self.stop_button = None
|
||||
self.loop_button = None
|
||||
self.clear_button = None
|
||||
self.play_button_bitmap = None
|
||||
self.stop_button_bitmap = None
|
||||
self.loop_button_bitmap = None
|
||||
self.clear_button_bitmap = None
|
||||
self.channel_selector = None
|
||||
|
||||
# For bitmap buttons
|
||||
self.button_sprites = None
|
||||
|
||||
# Center points for fallback play/loop buttons
|
||||
self.play_center_x = self.BUTTON_WIDTH // 2
|
||||
self.play_center_y = self.BUTTON_HEIGHT // 2
|
||||
self.play_size = 10
|
||||
self.loop_center_x = self.BUTTON_WIDTH // 2
|
||||
self.loop_center_y = self.BUTTON_HEIGHT // 2
|
||||
self.loop_radius = 6
|
||||
|
||||
def create_channel_buttons(self):
|
||||
"""Create channel selector buttons at the top of the screen using sprites"""
|
||||
channel_group = Group()
|
||||
|
||||
# Add a highlight indicator for the selected channel (yellow outline only)
|
||||
# Create bitmap for channel selector with appropriate dimensions
|
||||
btn_size = self.CHANNEL_BUTTON_SIZE
|
||||
channel_select_bitmap = Bitmap(btn_size + 6, btn_size + 6, 2)
|
||||
channel_select_palette = Palette(2)
|
||||
channel_select_palette[0] = 0x444444 # Same as background color (dark gray)
|
||||
channel_select_palette[1] = 0xFFFF00 # Yellow highlight
|
||||
channel_select_palette.make_transparent(0) # Make background transparent
|
||||
|
||||
# Draw just the outline (no filled background)
|
||||
bitmap_size = btn_size + 6
|
||||
for x in range(bitmap_size):
|
||||
for y in range(bitmap_size):
|
||||
# Draw only the border pixels
|
||||
if (x == 0 or x == bitmap_size - 1 or
|
||||
y == 0 or y == bitmap_size - 1):
|
||||
channel_select_bitmap[x, y] = 1 # Yellow outline
|
||||
else:
|
||||
channel_select_bitmap[x, y] = 0 # Transparent background
|
||||
|
||||
self.channel_selector = TileGrid(
|
||||
channel_select_bitmap,
|
||||
pixel_shader=channel_select_palette,
|
||||
x=7,
|
||||
y=self.CHANNEL_BUTTON_Y - 3
|
||||
)
|
||||
channel_group.append(self.channel_selector)
|
||||
|
||||
return channel_group, self.channel_selector
|
||||
|
||||
def create_transport_controls(self, sprite_manager):
|
||||
"""Create transport controls using bitmap buttons"""
|
||||
transport_group = Group()
|
||||
|
||||
# Check if button sprites were successfully loaded
|
||||
if (sprite_manager.play_up is None or sprite_manager.stop_up is None or
|
||||
sprite_manager.loop_up is None or sprite_manager.clear_up is None):
|
||||
print("Warning: Button sprites not loaded, using fallback buttons")
|
||||
return self._create_fallback_transport_controls()
|
||||
|
||||
# Button spacing based on the new size (64x48)
|
||||
button_spacing = 10
|
||||
button_y = self.SCREEN_HEIGHT - 50 # Allow some margin at bottom
|
||||
|
||||
# Create TileGrids for each button using the "up" state initially
|
||||
self.stop_button = TileGrid(
|
||||
sprite_manager.stop_up,
|
||||
pixel_shader=sprite_manager.stop_up_palette,
|
||||
x=10,
|
||||
y=button_y
|
||||
)
|
||||
|
||||
self.play_button = TileGrid(
|
||||
sprite_manager.play_up,
|
||||
pixel_shader=sprite_manager.play_up_palette,
|
||||
x=10 + 64 + button_spacing,
|
||||
y=button_y
|
||||
)
|
||||
|
||||
self.loop_button = TileGrid(
|
||||
sprite_manager.loop_up,
|
||||
pixel_shader=sprite_manager.loop_up_palette,
|
||||
x=10 + 2 * (64 + button_spacing),
|
||||
y=button_y
|
||||
)
|
||||
|
||||
self.clear_button = TileGrid(
|
||||
sprite_manager.clear_up,
|
||||
pixel_shader=sprite_manager.clear_up_palette,
|
||||
x=10 + 3 * (64 + button_spacing),
|
||||
y=button_y
|
||||
)
|
||||
|
||||
# Store references to the button bitmaps and palettes
|
||||
self.button_sprites = {
|
||||
'play': {
|
||||
'up': (sprite_manager.play_up, sprite_manager.play_up_palette),
|
||||
'down': (sprite_manager.play_down, sprite_manager.play_down_palette)
|
||||
},
|
||||
'stop': {
|
||||
'up': (sprite_manager.stop_up, sprite_manager.stop_up_palette),
|
||||
'down': (sprite_manager.stop_down, sprite_manager.stop_down_palette)
|
||||
},
|
||||
'loop': {
|
||||
'up': (sprite_manager.loop_up, sprite_manager.loop_up_palette),
|
||||
'down': (sprite_manager.loop_down, sprite_manager.loop_down_palette)
|
||||
},
|
||||
'clear': {
|
||||
'up': (sprite_manager.clear_up, sprite_manager.clear_up_palette),
|
||||
'down': (sprite_manager.clear_down, sprite_manager.clear_down_palette)
|
||||
}
|
||||
}
|
||||
|
||||
# Save the button dimensions
|
||||
self.BUTTON_WIDTH = 64
|
||||
self.BUTTON_HEIGHT = 48
|
||||
|
||||
# Add buttons to the group
|
||||
transport_group.append(self.stop_button)
|
||||
transport_group.append(self.play_button)
|
||||
transport_group.append(self.loop_button)
|
||||
transport_group.append(self.clear_button)
|
||||
|
||||
return (transport_group, self.play_button, self.stop_button,
|
||||
self.loop_button, self.clear_button)
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def _create_fallback_transport_controls(self):
|
||||
"""Create fallback transport controls using drawn buttons (original implementation)"""
|
||||
transport_group = Group()
|
||||
|
||||
# Create button bitmaps
|
||||
self.play_button_bitmap = Bitmap(self.BUTTON_WIDTH, self.BUTTON_HEIGHT, 3)
|
||||
self.stop_button_bitmap = Bitmap(self.BUTTON_WIDTH, self.BUTTON_HEIGHT, 3)
|
||||
self.loop_button_bitmap = Bitmap(self.BUTTON_WIDTH, self.BUTTON_HEIGHT, 3)
|
||||
self.clear_button_bitmap = Bitmap(self.BUTTON_WIDTH, self.BUTTON_HEIGHT, 3)
|
||||
|
||||
# Button palettes with custom colors
|
||||
play_button_palette = Palette(3)
|
||||
play_button_palette[0] = 0x444444 # Dark gray background
|
||||
play_button_palette[1] = 0x000000 # Black text/border
|
||||
play_button_palette[2] = 0xFFD700 # Golden yellow for active state
|
||||
|
||||
stop_button_palette = Palette(3)
|
||||
stop_button_palette[0] = 0x444444 # Dark gray background
|
||||
stop_button_palette[1] = 0x000000 # Black text/border
|
||||
stop_button_palette[2] = 0xFF00FF # Magenta for active state
|
||||
|
||||
loop_button_palette = Palette(3)
|
||||
loop_button_palette[0] = 0x444444 # Dark gray background
|
||||
loop_button_palette[1] = 0x000000 # Black text/border
|
||||
loop_button_palette[2] = 0xFFD700 # Golden yellow for active state
|
||||
|
||||
clear_button_palette = Palette(3)
|
||||
clear_button_palette[0] = 0x444444 # Dark gray background
|
||||
clear_button_palette[1] = 0x000000 # Black text/border
|
||||
clear_button_palette[2] = 0xFF0000 # Red for pressed state
|
||||
|
||||
# Create Stop button
|
||||
for x in range(self.BUTTON_WIDTH):
|
||||
for y in range(self.BUTTON_HEIGHT):
|
||||
# Draw border
|
||||
if (x == 0 or x == self.BUTTON_WIDTH - 1 or
|
||||
y == 0 or y == self.BUTTON_HEIGHT - 1):
|
||||
self.stop_button_bitmap[x, y] = 1
|
||||
# Fill with magenta (active state)
|
||||
else:
|
||||
self.stop_button_bitmap[x, y] = 2
|
||||
|
||||
# Create Play button
|
||||
for x in range(self.BUTTON_WIDTH):
|
||||
for y in range(self.BUTTON_HEIGHT):
|
||||
# Draw border
|
||||
if (x == 0 or x == self.BUTTON_WIDTH - 1 or
|
||||
y == 0 or y == self.BUTTON_HEIGHT - 1):
|
||||
self.play_button_bitmap[x, y] = 1
|
||||
# Fill with gray (inactive state)
|
||||
else:
|
||||
self.play_button_bitmap[x, y] = 0
|
||||
|
||||
# Draw play symbol (triangle)
|
||||
for y in range(
|
||||
self.play_center_y - self.play_size//2,
|
||||
self.play_center_y + self.play_size//2
|
||||
):
|
||||
width = (y - (self.play_center_y - self.play_size//2)) // 2
|
||||
for x in range(
|
||||
self.play_center_x - self.play_size//4,
|
||||
self.play_center_x - self.play_size//4 + width
|
||||
):
|
||||
if 0 <= x < self.BUTTON_WIDTH and 0 <= y < self.BUTTON_HEIGHT:
|
||||
self.play_button_bitmap[x, y] = 1
|
||||
|
||||
# Create Loop button
|
||||
for x in range(self.BUTTON_WIDTH):
|
||||
for y in range(self.BUTTON_HEIGHT):
|
||||
# Draw border
|
||||
if (x == 0 or x == self.BUTTON_WIDTH - 1 or
|
||||
y == 0 or y == self.BUTTON_HEIGHT - 1):
|
||||
self.loop_button_bitmap[x, y] = 1
|
||||
# Fill with gray (inactive state)
|
||||
else:
|
||||
self.loop_button_bitmap[x, y] = 0
|
||||
|
||||
# Draw loop symbol (circle with arrow)
|
||||
for x in range(self.BUTTON_WIDTH):
|
||||
for y in range(self.BUTTON_HEIGHT):
|
||||
dx = x - self.loop_center_x
|
||||
dy = y - self.loop_center_y
|
||||
# Draw circle outline
|
||||
if self.loop_radius - 1 <= (dx*dx + dy*dy)**0.5 <= self.loop_radius + 1:
|
||||
if 0 <= x < self.BUTTON_WIDTH and 0 <= y < self.BUTTON_HEIGHT:
|
||||
self.loop_button_bitmap[x, y] = 1
|
||||
|
||||
# Add arrow to loop symbol
|
||||
for i in range(4):
|
||||
x = self.loop_center_x + int(self.loop_radius * 0.7) - i
|
||||
y = self.loop_center_y - self.loop_radius - 1 + i
|
||||
if 0 <= x < self.BUTTON_WIDTH and 0 <= y < self.BUTTON_HEIGHT:
|
||||
self.loop_button_bitmap[x, y] = 1
|
||||
|
||||
x = self.loop_center_x + int(self.loop_radius * 0.7) - i
|
||||
y = self.loop_center_y - self.loop_radius - 1 - i + 2
|
||||
if 0 <= x < self.BUTTON_WIDTH and 0 <= y < self.BUTTON_HEIGHT:
|
||||
self.loop_button_bitmap[x, y] = 1
|
||||
|
||||
# Create Clear button
|
||||
for x in range(self.BUTTON_WIDTH):
|
||||
for y in range(self.BUTTON_HEIGHT):
|
||||
# Draw border
|
||||
if (x == 0 or x == self.BUTTON_WIDTH - 1 or
|
||||
y == 0 or y == self.BUTTON_HEIGHT - 1):
|
||||
self.clear_button_bitmap[x, y] = 1
|
||||
# Fill with gray background
|
||||
else:
|
||||
self.clear_button_bitmap[x, y] = 0
|
||||
|
||||
# Create button TileGrids
|
||||
x_offset = 10
|
||||
y_pos = self.SCREEN_HEIGHT - 40
|
||||
|
||||
self.stop_button = TileGrid(
|
||||
self.stop_button_bitmap,
|
||||
pixel_shader=stop_button_palette,
|
||||
x=x_offset,
|
||||
y=y_pos
|
||||
)
|
||||
|
||||
x_offset += self.BUTTON_WIDTH + self.BUTTON_SPACING
|
||||
self.play_button = TileGrid(
|
||||
self.play_button_bitmap,
|
||||
pixel_shader=play_button_palette,
|
||||
x=x_offset,
|
||||
y=y_pos
|
||||
)
|
||||
|
||||
x_offset += self.BUTTON_WIDTH + self.BUTTON_SPACING
|
||||
self.loop_button = TileGrid(
|
||||
self.loop_button_bitmap,
|
||||
pixel_shader=loop_button_palette,
|
||||
x=x_offset,
|
||||
y=y_pos
|
||||
)
|
||||
|
||||
x_offset += self.BUTTON_WIDTH + self.BUTTON_SPACING
|
||||
self.clear_button = TileGrid(
|
||||
self.clear_button_bitmap,
|
||||
pixel_shader=clear_button_palette,
|
||||
x=x_offset,
|
||||
y=y_pos
|
||||
)
|
||||
|
||||
# Add buttons to group
|
||||
transport_group.append(self.stop_button)
|
||||
transport_group.append(self.play_button)
|
||||
transport_group.append(self.loop_button)
|
||||
transport_group.append(self.clear_button)
|
||||
|
||||
# Add "CLEAR" text to clear button
|
||||
text_color = 0x000000 # Black text
|
||||
label_x = self.clear_button.x + self.BUTTON_WIDTH // 2
|
||||
label_y = self.clear_button.y + self.BUTTON_HEIGHT // 2
|
||||
|
||||
clear_label = Label(
|
||||
terminalio.FONT,
|
||||
text="CLEAR",
|
||||
color=text_color,
|
||||
scale=1
|
||||
)
|
||||
clear_label.anchor_point = (0.5, 0.5) # Center the text
|
||||
clear_label.anchored_position = (label_x, label_y)
|
||||
transport_group.append(clear_label)
|
||||
|
||||
return (transport_group, self.play_button, self.stop_button,
|
||||
self.loop_button, self.clear_button)
|
||||
85
Fruit_Jam/Larsio_Paint_Music/cursor_manager.py
Executable file
|
|
@ -0,0 +1,85 @@
|
|||
# SPDX-FileCopyrightText: 2025 John Park and Claude AI for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""
|
||||
# cursor_manager.py: CircuitPython Music Staff Application component
|
||||
"""
|
||||
|
||||
# pylint: disable=import-error
|
||||
from displayio import Bitmap, Palette, TileGrid
|
||||
|
||||
|
||||
# pylint: disable=invalid-name,no-member,too-many-instance-attributes
|
||||
# pylint: disable=too-many-arguments,too-many-branches,too-many-statements
|
||||
class CursorManager:
|
||||
"""Manages cursor appearance and position"""
|
||||
|
||||
def __init__(self, bg_color=0x8AAD8A):
|
||||
self.bg_color = bg_color
|
||||
|
||||
# Cursors
|
||||
self.crosshair_cursor = None
|
||||
self.triangle_cursor = None
|
||||
self.current_cursor = None
|
||||
|
||||
self.create_cursors()
|
||||
|
||||
def create_cursors(self):
|
||||
"""Create custom cursor bitmaps for different areas"""
|
||||
# Regular crosshair cursor for staff area
|
||||
crosshair_cursor_bitmap = Bitmap(8, 8, 2)
|
||||
crosshair_cursor_palette = Palette(2)
|
||||
crosshair_cursor_palette[0] = self.bg_color # Background color (sage green)
|
||||
crosshair_cursor_palette[1] = 0x000000 # Cursor color (black)
|
||||
crosshair_cursor_palette.make_transparent(0) # Make background transparent
|
||||
|
||||
for i in range(8):
|
||||
crosshair_cursor_bitmap[i, 3] = 1
|
||||
crosshair_cursor_bitmap[i, 4] = 1
|
||||
crosshair_cursor_bitmap[3, i] = 1
|
||||
crosshair_cursor_bitmap[4, i] = 1
|
||||
|
||||
# Triangle cursor for controls area
|
||||
triangle_cursor_bitmap = Bitmap(12, 12, 2)
|
||||
triangle_cursor_palette = Palette(2)
|
||||
triangle_cursor_palette[0] = 0x000000 # Background color
|
||||
triangle_cursor_palette[1] = 0x000000 # Cursor color (black)
|
||||
triangle_cursor_palette.make_transparent(0) # Make background transparent
|
||||
|
||||
# Draw a triangle cursor
|
||||
for y in range(12):
|
||||
width = y // 2 + 1 # Triangle gets wider as y increases
|
||||
for x in range(width):
|
||||
triangle_cursor_bitmap[x, y] = 1
|
||||
|
||||
# Create a TileGrid for each cursor type
|
||||
self.crosshair_cursor = TileGrid(
|
||||
crosshair_cursor_bitmap,
|
||||
pixel_shader=crosshair_cursor_palette
|
||||
)
|
||||
self.triangle_cursor = TileGrid(
|
||||
triangle_cursor_bitmap,
|
||||
pixel_shader=triangle_cursor_palette
|
||||
)
|
||||
|
||||
# Initially use crosshair cursor
|
||||
self.current_cursor = self.crosshair_cursor
|
||||
self.triangle_cursor.hidden = True
|
||||
|
||||
return self.crosshair_cursor, self.triangle_cursor
|
||||
|
||||
def set_cursor_position(self, x, y):
|
||||
"""Set the position of the current cursor"""
|
||||
self.current_cursor.x = x
|
||||
self.current_cursor.y = y
|
||||
|
||||
def switch_cursor(self, use_triangle=False):
|
||||
"""Switch between crosshair and triangle cursor"""
|
||||
if use_triangle and self.current_cursor != self.triangle_cursor:
|
||||
self.crosshair_cursor.hidden = True
|
||||
self.triangle_cursor.hidden = False
|
||||
self.current_cursor = self.triangle_cursor
|
||||
elif not use_triangle and self.current_cursor != self.crosshair_cursor:
|
||||
self.triangle_cursor.hidden = True
|
||||
self.crosshair_cursor.hidden = False
|
||||
self.current_cursor = self.crosshair_cursor
|
||||
64
Fruit_Jam/Larsio_Paint_Music/display_manager.py
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
# SPDX-FileCopyrightText: 2025 John Park and Claude AI for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""
|
||||
# display_manager.py: CircuitPython Music Staff Application component
|
||||
"""
|
||||
# pylint: disable=import-error,invalid-name,no-member,too-many-instance-attributes,too-many-arguments,too-many-branches,too-many-statements
|
||||
|
||||
import displayio
|
||||
import picodvi
|
||||
import framebufferio
|
||||
import board
|
||||
|
||||
|
||||
|
||||
class DisplayManager:
|
||||
"""Manages the display initialization and basic display operations"""
|
||||
|
||||
|
||||
def __init__(self, width=320, height=240):
|
||||
self.SCREEN_WIDTH = width
|
||||
self.SCREEN_HEIGHT = height
|
||||
self.display = None
|
||||
self.main_group = None
|
||||
|
||||
def initialize_display(self):
|
||||
"""Initialize the DVI display"""
|
||||
# Release any existing displays
|
||||
displayio.release_displays()
|
||||
|
||||
# Initialize the DVI framebuffer
|
||||
fb = picodvi.Framebuffer(self.SCREEN_WIDTH, self.SCREEN_HEIGHT,
|
||||
clk_dp=board.CKP, clk_dn=board.CKN,
|
||||
red_dp=board.D0P, red_dn=board.D0N,
|
||||
green_dp=board.D1P, green_dn=board.D1N,
|
||||
blue_dp=board.D2P, blue_dn=board.D2N,
|
||||
color_depth=16)
|
||||
|
||||
# Create the display
|
||||
self.display = framebufferio.FramebufferDisplay(fb)
|
||||
|
||||
# Create main group
|
||||
self.main_group = displayio.Group()
|
||||
|
||||
# Set the display's root group
|
||||
self.display.root_group = self.main_group
|
||||
|
||||
return self.main_group, self.display
|
||||
|
||||
def create_background(self, color=0x888888):
|
||||
"""Create a background with the given color"""
|
||||
bg_bitmap = displayio.Bitmap(self.SCREEN_WIDTH, self.SCREEN_HEIGHT, 1)
|
||||
bg_palette = displayio.Palette(1)
|
||||
bg_palette[0] = color
|
||||
|
||||
# Fill the bitmap with the background color
|
||||
for x in range(self.SCREEN_WIDTH):
|
||||
for y in range(self.SCREEN_HEIGHT):
|
||||
bg_bitmap[x, y] = 0
|
||||
|
||||
# Create a TileGrid with the background bitmap
|
||||
bg_grid = displayio.TileGrid(bg_bitmap, pixel_shader=bg_palette, x=0, y=0)
|
||||
|
||||
return bg_grid
|
||||
221
Fruit_Jam/Larsio_Paint_Music/input_handler.py
Executable file
|
|
@ -0,0 +1,221 @@
|
|||
# SPDX-FileCopyrightText: 2025 John Park and Claude AI for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""
|
||||
# input_handler.py: CircuitPython Music Staff Application component
|
||||
"""
|
||||
|
||||
import array
|
||||
import time
|
||||
import gc
|
||||
|
||||
# pylint: disable=import-error
|
||||
import usb.core
|
||||
|
||||
|
||||
# pylint: disable=invalid-name,no-member,too-many-instance-attributes,too-many-arguments
|
||||
# pylint: disable=too-many-branches,too-many-statements,broad-except
|
||||
# pylint: disable=too-many-nested-blocks,too-many-locals,no-self-use
|
||||
class InputHandler:
|
||||
"""Handles user input through mouse and interactions with UI elements"""
|
||||
|
||||
def __init__(self, screen_width, screen_height, staff_y_start, staff_height):
|
||||
self.SCREEN_WIDTH = screen_width
|
||||
self.SCREEN_HEIGHT = screen_height
|
||||
self.STAFF_Y_START = staff_y_start
|
||||
self.STAFF_HEIGHT = staff_height
|
||||
|
||||
# Mouse state
|
||||
self.last_left_button_state = 0
|
||||
self.last_right_button_state = 0
|
||||
self.left_button_pressed = False
|
||||
self.right_button_pressed = False
|
||||
self.mouse = None
|
||||
self.buf = None
|
||||
self.in_endpoint = None
|
||||
|
||||
# Mouse position
|
||||
self.mouse_x = screen_width // 2
|
||||
self.mouse_y = screen_height // 2
|
||||
|
||||
def find_mouse(self):
|
||||
"""Find the mouse device with multiple retry attempts"""
|
||||
MAX_ATTEMPTS = 5
|
||||
RETRY_DELAY = 1 # seconds
|
||||
|
||||
for attempt in range(MAX_ATTEMPTS):
|
||||
try:
|
||||
print(f"Mouse detection attempt {attempt+1}/{MAX_ATTEMPTS}")
|
||||
|
||||
# Constants for USB control transfers
|
||||
DIR_OUT = 0
|
||||
# DIR_IN = 0x80 # Unused variable
|
||||
REQTYPE_CLASS = 1 << 5
|
||||
REQREC_INTERFACE = 1 << 0
|
||||
HID_REQ_SET_PROTOCOL = 0x0B
|
||||
|
||||
# Find all USB devices
|
||||
devices_found = False
|
||||
for device in usb.core.find(find_all=True):
|
||||
devices_found = True
|
||||
print(f"Found device: {device.idVendor:04x}:{device.idProduct:04x}")
|
||||
|
||||
try:
|
||||
# Try to get device info
|
||||
try:
|
||||
manufacturer = device.manufacturer
|
||||
product = device.product
|
||||
except Exception: # pylint: disable=broad-except
|
||||
manufacturer = "Unknown"
|
||||
product = "Unknown"
|
||||
|
||||
# Just use whatever device we find
|
||||
self.mouse = device
|
||||
|
||||
# Try to detach kernel driver
|
||||
try:
|
||||
has_kernel_driver = hasattr(device, 'is_kernel_driver_active')
|
||||
if has_kernel_driver and device.is_kernel_driver_active(0):
|
||||
device.detach_kernel_driver(0)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
print(f"Error detaching kernel driver: {e}")
|
||||
|
||||
# Set configuration
|
||||
try:
|
||||
device.set_configuration()
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
print(f"Error setting configuration: {e}")
|
||||
continue # Try next device
|
||||
|
||||
# Just assume endpoint 0x81 (common for mice)
|
||||
self.in_endpoint = 0x81
|
||||
print(f"Using mouse: {manufacturer}, {product}")
|
||||
|
||||
# Set to report protocol mode
|
||||
try:
|
||||
bmRequestType = DIR_OUT | REQTYPE_CLASS | REQREC_INTERFACE
|
||||
bRequest = HID_REQ_SET_PROTOCOL
|
||||
wValue = 1 # 1 = report protocol
|
||||
wIndex = 0 # First interface
|
||||
|
||||
buf = bytearray(1)
|
||||
device.ctrl_transfer(bmRequestType, bRequest, wValue, wIndex, buf)
|
||||
print("Set to report protocol mode")
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
print(f"Could not set protocol: {e}")
|
||||
|
||||
# Buffer for reading data
|
||||
self.buf = array.array("B", [0] * 4)
|
||||
print("Created 4-byte buffer for mouse data")
|
||||
|
||||
# Verify mouse works by reading from it
|
||||
try:
|
||||
# Try to read some data with a short timeout
|
||||
data = device.read(self.in_endpoint, self.buf, timeout=100)
|
||||
print(f"Mouse test read successful: {data} bytes")
|
||||
return True
|
||||
except usb.core.USBTimeoutError:
|
||||
# Timeout is normal if mouse isn't moving
|
||||
print("Mouse connected but not sending data (normal)")
|
||||
return True
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
print(f"Mouse test read failed: {e}")
|
||||
# Continue to try next device or retry
|
||||
self.mouse = None
|
||||
self.in_endpoint = None
|
||||
continue
|
||||
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
print(f"Error initializing device: {e}")
|
||||
continue
|
||||
|
||||
if not devices_found:
|
||||
print("No USB devices found")
|
||||
|
||||
# If we get here without returning, no suitable mouse was found
|
||||
print(f"No working mouse found on attempt {attempt+1}, retrying...")
|
||||
gc.collect()
|
||||
time.sleep(RETRY_DELAY)
|
||||
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
print(f"Error during mouse detection: {e}")
|
||||
gc.collect()
|
||||
time.sleep(RETRY_DELAY)
|
||||
|
||||
print("Failed to find a working mouse after multiple attempts")
|
||||
return False
|
||||
|
||||
def process_mouse_input(self):
|
||||
"""Process mouse input - simplified version without wheel support"""
|
||||
try:
|
||||
# Attempt to read data from the mouse (10ms timeout)
|
||||
count = self.mouse.read(self.in_endpoint, self.buf, timeout=10)
|
||||
|
||||
if count >= 3: # We need at least buttons, X and Y
|
||||
# Extract mouse button states
|
||||
buttons = self.buf[0]
|
||||
x = self.buf[1]
|
||||
y = self.buf[2]
|
||||
|
||||
# Convert to signed values if needed
|
||||
if x > 127:
|
||||
x = x - 256
|
||||
if y > 127:
|
||||
y = y - 256
|
||||
|
||||
# Extract button states
|
||||
current_left_button_state = buttons & 0x01
|
||||
current_right_button_state = (buttons & 0x02) >> 1
|
||||
|
||||
# Detect button presses
|
||||
if current_left_button_state == 1 and self.last_left_button_state == 0:
|
||||
self.left_button_pressed = True
|
||||
else:
|
||||
self.left_button_pressed = False
|
||||
|
||||
if current_right_button_state == 1 and self.last_right_button_state == 0:
|
||||
self.right_button_pressed = True
|
||||
else:
|
||||
self.right_button_pressed = False
|
||||
|
||||
# Update button states
|
||||
self.last_left_button_state = current_left_button_state
|
||||
self.last_right_button_state = current_right_button_state
|
||||
|
||||
# Update position
|
||||
self.mouse_x += x
|
||||
self.mouse_y += y
|
||||
|
||||
# Ensure position stays within bounds
|
||||
self.mouse_x = max(0, min(self.SCREEN_WIDTH - 1, self.mouse_x))
|
||||
self.mouse_y = max(0, min(self.SCREEN_HEIGHT - 1, self.mouse_y))
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except usb.core.USBError as e:
|
||||
# Handle timeouts silently
|
||||
if e.errno == 110: # Operation timed out
|
||||
return False
|
||||
|
||||
# Handle disconnections
|
||||
if e.errno == 19: # No such device
|
||||
print("Mouse disconnected")
|
||||
self.mouse = None
|
||||
self.in_endpoint = None
|
||||
gc.collect()
|
||||
|
||||
return False
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
print(f"Error reading mouse: {type(e).__name__}")
|
||||
return False
|
||||
|
||||
def point_in_rect(self, x, y, rect_x, rect_y, rect_width, rect_height):
|
||||
"""Check if a point is inside a rectangle"""
|
||||
return (rect_x <= x < rect_x + rect_width and
|
||||
rect_y <= y < rect_y + rect_height)
|
||||
|
||||
def is_over_staff(self, y):
|
||||
"""Check if mouse is over the staff area"""
|
||||
return self.STAFF_Y_START <= y <= self.STAFF_Y_START + self.STAFF_HEIGHT
|
||||
BIN
Fruit_Jam/Larsio_Paint_Music/lpm_icon.bmp
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
4
Fruit_Jam/Larsio_Paint_Music/metadata.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"title": "LarsioPant",
|
||||
"icon": "lpm_icon.bmp"
|
||||
}
|
||||
425
Fruit_Jam/Larsio_Paint_Music/note_manager.py
Executable file
|
|
@ -0,0 +1,425 @@
|
|||
# SPDX-FileCopyrightText: 2025 John Park and Claude AI for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""
|
||||
# note_manager.py: CircuitPython Music Staff Application component
|
||||
"""
|
||||
|
||||
# pylint: disable=import-error
|
||||
from displayio import Group, Bitmap, Palette, TileGrid
|
||||
|
||||
|
||||
# pylint: disable=invalid-name,no-member,too-many-instance-attributes,too-many-arguments
|
||||
# pylint: disable=too-many-branches,too-many-statements,protected-access,too-many-locals
|
||||
# pylint: disable=trailing-whitespace
|
||||
class NoteManager:
|
||||
"""Manages notes, their positions, and related data"""
|
||||
|
||||
def __init__(self, start_margin, staff_y_start, line_spacing):
|
||||
self.note_data = [] # List of (x_position, y_position, midi_note, midi_channel)
|
||||
self.notes_group = Group()
|
||||
self.ledger_lines_group = Group()
|
||||
self.note_to_ledger = {} # Mapping from note indices to ledger line indices
|
||||
|
||||
# Key staff parameters
|
||||
self.START_MARGIN = start_margin
|
||||
self.STAFF_Y_START = staff_y_start
|
||||
self.LINE_SPACING = line_spacing
|
||||
|
||||
# Note positions and their MIDI values
|
||||
self.note_positions = self._create_note_positions()
|
||||
self.x_positions = [] # Will be populated by the UI manager
|
||||
|
||||
# Create note bitmaps
|
||||
self.NOTE_WIDTH = (line_spacing // 2) - 2
|
||||
self.NOTE_HEIGHT = (line_spacing // 2) - 2
|
||||
self.note_bitmap = self._create_note_bitmap()
|
||||
|
||||
# Create ledger line bitmap
|
||||
self.ledger_line_width = 14
|
||||
self.ledger_line_height = 2
|
||||
self.ledger_bitmap = Bitmap(self.ledger_line_width, self.ledger_line_height, 2)
|
||||
for x in range(self.ledger_line_width):
|
||||
for y in range(self.ledger_line_height):
|
||||
self.ledger_bitmap[x, y] = 1
|
||||
|
||||
self.ledger_palette = Palette(2)
|
||||
self.ledger_palette[0] = 0x8AAD8A # Transparent (sage green background)
|
||||
self.ledger_palette[1] = 0x000000 # Black for ledger lines
|
||||
|
||||
# MIDI note mapping for each position
|
||||
self.midi_notes = {
|
||||
0: 59, # B3
|
||||
1: 60, # C4 (middle C)
|
||||
2: 62, # D4
|
||||
3: 64, # E4
|
||||
4: 65, # F4
|
||||
5: 67, # G4
|
||||
6: 69, # A4
|
||||
7: 71, # B4
|
||||
8: 72, # C5
|
||||
9: 74, # D5
|
||||
10: 76, # E5
|
||||
11: 77, # F5
|
||||
12: 79 # G5
|
||||
}
|
||||
|
||||
# Map of positions to note names (for treble clef)
|
||||
self.note_names = {
|
||||
0: "B3", # B below middle C (ledger line)
|
||||
1: "C4", # Middle C (ledger line below staff)
|
||||
2: "D4", # Space below staff
|
||||
3: "E4", # Bottom line
|
||||
4: "F4", # First space
|
||||
5: "G4", # Second line
|
||||
6: "A4", # Second space
|
||||
7: "B4", # Middle line
|
||||
8: "C5", # Third space
|
||||
9: "D5", # Fourth line
|
||||
10: "E5", # Fourth space
|
||||
11: "F5", # Top line
|
||||
12: "G5" # Space above staff
|
||||
}
|
||||
|
||||
def _create_note_positions(self):
|
||||
"""Create the vertical positions for notes on the staff"""
|
||||
note_positions = []
|
||||
|
||||
# Calculate positions from the bottom up
|
||||
bottom_line_y = self.STAFF_Y_START + 5 * self.LINE_SPACING # Bottom staff line (E)
|
||||
|
||||
# B3 (ledger line below staff)
|
||||
note_positions.append(bottom_line_y + self.LINE_SPACING + self.LINE_SPACING // 2)
|
||||
|
||||
# Middle C4 (ledger line below staff)
|
||||
note_positions.append(bottom_line_y + self.LINE_SPACING)
|
||||
|
||||
# D4 (space below staff)
|
||||
note_positions.append(bottom_line_y + self.LINE_SPACING // 2)
|
||||
|
||||
# E4 (bottom line)
|
||||
note_positions.append(bottom_line_y)
|
||||
|
||||
# F4 (first space)
|
||||
note_positions.append(bottom_line_y - self.LINE_SPACING // 2)
|
||||
|
||||
# G4 (second line)
|
||||
note_positions.append(bottom_line_y - self.LINE_SPACING)
|
||||
|
||||
# A4 (second space)
|
||||
note_positions.append(bottom_line_y - self.LINE_SPACING - self.LINE_SPACING // 2)
|
||||
|
||||
# B4 (middle line)
|
||||
note_positions.append(bottom_line_y - 2 * self.LINE_SPACING)
|
||||
|
||||
# C5 (third space)
|
||||
note_positions.append(bottom_line_y - 2 * self.LINE_SPACING - self.LINE_SPACING // 2)
|
||||
|
||||
# D5 (fourth line)
|
||||
note_positions.append(bottom_line_y - 3 * self.LINE_SPACING)
|
||||
|
||||
# E5 (fourth space)
|
||||
note_positions.append(bottom_line_y - 3 * self.LINE_SPACING - self.LINE_SPACING // 2)
|
||||
|
||||
# F5 (top line)
|
||||
note_positions.append(bottom_line_y - 4 * self.LINE_SPACING)
|
||||
|
||||
# G5 (space above staff)
|
||||
note_positions.append(bottom_line_y - 4 * self.LINE_SPACING - self.LINE_SPACING // 2)
|
||||
|
||||
return note_positions
|
||||
|
||||
def _create_note_bitmap(self):
|
||||
"""Create a bitmap for a quarter note (circular shape)"""
|
||||
note_bitmap = Bitmap(self.NOTE_WIDTH, self.NOTE_HEIGHT, 2)
|
||||
|
||||
# Draw a circular shape for the note head
|
||||
cx = self.NOTE_WIDTH // 2
|
||||
cy = self.NOTE_HEIGHT // 2
|
||||
radius = self.NOTE_WIDTH // 2
|
||||
|
||||
for y in range(self.NOTE_HEIGHT):
|
||||
for x in range(self.NOTE_WIDTH):
|
||||
# Use the circle equation (x-cx)² + (y-cy)² ≤ r² to determine if pixel is in circle
|
||||
if ((x - cx) ** 2 + (y - cy) ** 2) <= (radius ** 2):
|
||||
note_bitmap[x, y] = 1
|
||||
|
||||
return note_bitmap
|
||||
|
||||
def find_closest_position(self, y):
|
||||
"""Find the closest valid note position to a given y-coordinate"""
|
||||
closest_pos = 0
|
||||
min_distance = abs(y - self.note_positions[0])
|
||||
|
||||
for i, pos in enumerate(self.note_positions):
|
||||
distance = abs(y - pos)
|
||||
if distance < min_distance:
|
||||
min_distance = distance
|
||||
closest_pos = i
|
||||
|
||||
return closest_pos
|
||||
|
||||
def find_closest_x_position(self, x):
|
||||
"""Find the closest valid horizontal position"""
|
||||
# Only allow positions after the double bar at beginning
|
||||
if x < self.START_MARGIN:
|
||||
return self.x_positions[0] # Return first valid position
|
||||
|
||||
closest_x = self.x_positions[0]
|
||||
min_distance = abs(x - closest_x)
|
||||
|
||||
for pos in self.x_positions:
|
||||
distance = abs(x - pos)
|
||||
if distance < min_distance:
|
||||
min_distance = distance
|
||||
closest_x = pos
|
||||
|
||||
return closest_x
|
||||
|
||||
def note_exists_at_position(self, x_pos, y_pos, mario_head, mario_palette):
|
||||
"""Check if a note exists at the exact position (for adding new notes)"""
|
||||
# Only check for exact overlap, not proximity
|
||||
for note_tg in self.notes_group:
|
||||
# Check if this is a Mario head note or a regular note
|
||||
is_mario = (hasattr(note_tg.pixel_shader, "_palette") and
|
||||
len(note_tg.pixel_shader._palette) > 1 and
|
||||
note_tg.pixel_shader._palette[0] == mario_palette[0])
|
||||
|
||||
if is_mario:
|
||||
note_width = mario_head.width
|
||||
note_height = mario_head.height
|
||||
else:
|
||||
note_width = self.NOTE_WIDTH
|
||||
note_height = self.NOTE_HEIGHT
|
||||
|
||||
note_x = note_tg.x + note_width // 2
|
||||
note_y = note_tg.y + note_height // 2
|
||||
|
||||
# Only prevent notes from being in the exact same position (with a tiny tolerance)
|
||||
if abs(note_x - x_pos) < 2 and abs(note_y - y_pos) < 2:
|
||||
return True
|
||||
return False
|
||||
|
||||
def find_note_at(self, x, y, mario_head, mario_palette):
|
||||
"""Check if a note already exists at a position and return its index"""
|
||||
for i, note_tg in enumerate(self.notes_group):
|
||||
# Check if this is a Mario head note or a regular note
|
||||
is_mario = (hasattr(note_tg.pixel_shader, "_palette") and
|
||||
len(note_tg.pixel_shader._palette) > 1 and
|
||||
note_tg.pixel_shader._palette[0] == mario_palette[0])
|
||||
|
||||
if is_mario:
|
||||
note_width = mario_head.width
|
||||
note_height = mario_head.height
|
||||
else:
|
||||
note_width = self.NOTE_WIDTH
|
||||
note_height = self.NOTE_HEIGHT
|
||||
|
||||
# Check if the note's center is within a reasonable distance of the cursor
|
||||
note_center_x = note_tg.x + note_width // 2
|
||||
note_center_y = note_tg.y + note_height // 2
|
||||
|
||||
# Use a slightly larger hit box for easier clicking
|
||||
hit_box_width = max(self.NOTE_WIDTH, note_width)
|
||||
hit_box_height = max(self.NOTE_HEIGHT, note_height)
|
||||
|
||||
if (abs(x-note_center_x) < hit_box_width) and (abs(y - note_center_y) < hit_box_height):
|
||||
return i
|
||||
return None
|
||||
|
||||
def add_note(
|
||||
self,
|
||||
x,
|
||||
y,
|
||||
current_channel,
|
||||
note_palettes,
|
||||
mario_head,
|
||||
mario_palette,
|
||||
heart_note,
|
||||
heart_palette,
|
||||
sound_manager
|
||||
):
|
||||
"""Add a note at the specified position"""
|
||||
# Enforce the minimum x position (after the double bar at beginning)
|
||||
if x < self.START_MARGIN:
|
||||
return (False, "Notes must be after the double bar")
|
||||
|
||||
# Find the closest valid position
|
||||
position_index = self.find_closest_position(y)
|
||||
y_position = self.note_positions[position_index]
|
||||
|
||||
# Find the closest valid horizontal position
|
||||
x_position = self.find_closest_x_position(x)
|
||||
|
||||
# Check if a note already exists at this exact position
|
||||
if self.note_exists_at_position(x_position, y_position, mario_head, mario_palette):
|
||||
return (False, "Note already exists here")
|
||||
|
||||
# Get the corresponding MIDI note number
|
||||
midi_note = self.midi_notes[position_index]
|
||||
|
||||
# Create a TileGrid for the note based on channel
|
||||
if current_channel == 0: # Channel 1 (index 0) uses Mario head
|
||||
note_tg = TileGrid(mario_head, pixel_shader=mario_palette)
|
||||
# Adjust position offset based on the size of mario_head bitmap
|
||||
note_width = mario_head.width
|
||||
note_height = mario_head.height
|
||||
note_tg.x = x_position - note_width // 2
|
||||
note_tg.y = y_position - note_height // 2
|
||||
elif current_channel == 1: # Channel 2 uses Heart note
|
||||
note_tg = TileGrid(heart_note, pixel_shader=heart_palette)
|
||||
# Adjust position offset based on the size of heart_note bitmap
|
||||
note_width = heart_note.width
|
||||
note_height = heart_note.height
|
||||
note_tg.x = x_position - note_width // 2
|
||||
note_tg.y = y_position - note_height // 2
|
||||
elif current_channel == 2: # Channel 3 uses Drum note
|
||||
note_tg = TileGrid(mario_head, pixel_shader=mario_palette)
|
||||
# Adjust position offset based on the size
|
||||
note_width = mario_head.width
|
||||
note_height = mario_head.height
|
||||
note_tg.x = x_position - note_width // 2
|
||||
note_tg.y = y_position - note_height // 2
|
||||
elif current_channel in (3, 4, 5): # Channels 4-6 use custom sprites
|
||||
# We'll pass appropriate sprites in ui_manager
|
||||
note_tg = TileGrid(mario_head, pixel_shader=mario_palette)
|
||||
note_width = mario_head.width
|
||||
note_height = mario_head.height
|
||||
note_tg.x = x_position - note_width // 2
|
||||
note_tg.y = y_position - note_height // 2
|
||||
else: # Other channels use the colored circle
|
||||
note_tg = TileGrid(self.note_bitmap, pixel_shader=note_palettes[current_channel])
|
||||
note_tg.x = x_position - self.NOTE_WIDTH // 2
|
||||
note_tg.y = y_position - self.NOTE_HEIGHT // 2
|
||||
|
||||
# Play the appropriate sound
|
||||
sound_manager.play_note(midi_note, current_channel)
|
||||
|
||||
# Add the note to the notes group
|
||||
note_index = len(self.notes_group)
|
||||
self.notes_group.append(note_tg)
|
||||
|
||||
# Store the note data for playback with channel information
|
||||
self.note_data.append((x_position, y_position, midi_note, current_channel))
|
||||
|
||||
# Add a ledger line if it's the B3 or C4 below staff
|
||||
if position_index <= 1: # B3 or C4
|
||||
ledger_tg = TileGrid(self.ledger_bitmap, pixel_shader=self.ledger_palette)
|
||||
ledger_tg.x = x_position - self.ledger_line_width // 2
|
||||
ledger_tg.y = y_position
|
||||
ledger_index = len(self.ledger_lines_group)
|
||||
self.ledger_lines_group.append(ledger_tg)
|
||||
|
||||
# Track association between note and its ledger line
|
||||
self.note_to_ledger[note_index] = ledger_index
|
||||
|
||||
note_name = self.note_names[position_index]
|
||||
return (True, f"Added: Ch{current_channel+1} {note_name}")
|
||||
|
||||
def erase_note(self, x, y, mario_head, mario_palette, sound_manager=None):
|
||||
"""Erase a note at the clicked position"""
|
||||
# Try to find a note at the click position
|
||||
note_index = self.find_note_at(x, y, mario_head, mario_palette)
|
||||
|
||||
if note_index is not None:
|
||||
# Get the position of the note
|
||||
note_tg = self.notes_group[note_index]
|
||||
|
||||
# Check if this is a Mario head note or a regular note
|
||||
is_mario = (hasattr(note_tg.pixel_shader, "_palette") and
|
||||
len(note_tg.pixel_shader._palette) > 1 and
|
||||
note_tg.pixel_shader._palette[0] == mario_palette[0])
|
||||
|
||||
if is_mario:
|
||||
note_width = mario_head.width
|
||||
note_height = mario_head.height
|
||||
else:
|
||||
note_width = self.NOTE_WIDTH
|
||||
note_height = self.NOTE_HEIGHT
|
||||
|
||||
note_x = note_tg.x + note_width // 2
|
||||
note_y = note_tg.y + note_height // 2
|
||||
|
||||
# Find the corresponding note data
|
||||
found_data_index = None
|
||||
# found_channel = None # Unused variable
|
||||
|
||||
for i, (x_pos, y_pos, _midi_note, _channel) in enumerate(self.note_data):
|
||||
# Increased tolerance for position matching
|
||||
if abs(x_pos - note_x) < 5 and abs(y_pos - note_y) < 5:
|
||||
found_data_index = i
|
||||
break
|
||||
|
||||
# If we found the note data and have a sound manager reference
|
||||
if found_data_index is not None and sound_manager is not None:
|
||||
# Extract note data
|
||||
x_pos, y_pos, _midi_note, channel = self.note_data[found_data_index]
|
||||
|
||||
# If this is a sample-based note (channels 0, 1, or 2), stop it
|
||||
if channel in [0, 1, 2]:
|
||||
sound_manager.stop_sample_at_position(x_pos, y_pos, channel)
|
||||
|
||||
# Remove the note data
|
||||
self.note_data.pop(found_data_index)
|
||||
print(f"Erased note at position ({x_pos}, {y_pos}) ch {channel+1}")
|
||||
else:
|
||||
# Still remove the note data if found (for backward compatibility)
|
||||
if found_data_index is not None:
|
||||
self.note_data.pop(found_data_index)
|
||||
|
||||
# Check if this note has an associated ledger line
|
||||
if note_index in self.note_to_ledger:
|
||||
ledger_index = self.note_to_ledger[note_index]
|
||||
|
||||
# Remove the ledger line
|
||||
self.ledger_lines_group.pop(ledger_index)
|
||||
|
||||
# Update ledger line mappings after removing a ledger line
|
||||
new_note_to_ledger = {}
|
||||
|
||||
# Process each mapping
|
||||
for n_idx, l_idx in self.note_to_ledger.items():
|
||||
# Skip the note we're removing
|
||||
if n_idx != note_index:
|
||||
# Adjust indices for ledger lines after the removed one
|
||||
if l_idx > ledger_index:
|
||||
new_note_to_ledger[n_idx] = l_idx - 1
|
||||
else:
|
||||
new_note_to_ledger[n_idx] = l_idx
|
||||
|
||||
self.note_to_ledger = new_note_to_ledger
|
||||
|
||||
# Remove the note
|
||||
self.notes_group.pop(note_index)
|
||||
|
||||
# Update mappings for notes with higher indices
|
||||
new_note_to_ledger = {}
|
||||
for n_idx, l_idx in self.note_to_ledger.items():
|
||||
if n_idx > note_index:
|
||||
new_note_to_ledger[n_idx - 1] = l_idx
|
||||
else:
|
||||
new_note_to_ledger[n_idx] = l_idx
|
||||
|
||||
self.note_to_ledger = new_note_to_ledger
|
||||
|
||||
return (True, "Note erased")
|
||||
|
||||
return (False, "No note found at this position")
|
||||
|
||||
def clear_all_notes(self, sound_manager=None):
|
||||
"""Clear all notes from the staff"""
|
||||
# Stop all sample playback if we have a sound manager
|
||||
if sound_manager is not None:
|
||||
sound_manager.stop_all_notes()
|
||||
|
||||
# Remove all notes
|
||||
while len(self.notes_group) > 0:
|
||||
self.notes_group.pop()
|
||||
|
||||
# Remove all ledger lines
|
||||
while len(self.ledger_lines_group) > 0:
|
||||
self.ledger_lines_group.pop()
|
||||
|
||||
# Clear note data and ledger line mappings
|
||||
self.note_data = []
|
||||
self.note_to_ledger = {}
|
||||
143
Fruit_Jam/Larsio_Paint_Music/playback_controller.py
Executable file
|
|
@ -0,0 +1,143 @@
|
|||
# SPDX-FileCopyrightText: 2025 John Park and Claude AI for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""
|
||||
Playback controller for CircuitPython Music Staff Application.
|
||||
Manages the playback state, button displays, and sound triggering.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
# pylint: disable=trailing-whitespace, too-many-instance-attributes
|
||||
class PlaybackController:
|
||||
"""Manages playback state and controls"""
|
||||
|
||||
def __init__(self, sound_manager, note_manager, seconds_per_eighth=0.25):
|
||||
"""Initialize the playback controller with sound and note managers"""
|
||||
self.sound_manager = sound_manager
|
||||
self.note_manager = note_manager
|
||||
self.seconds_per_eighth = seconds_per_eighth
|
||||
|
||||
# Playback state
|
||||
self.is_playing = False
|
||||
self.playhead_position = -1
|
||||
self.last_playhead_time = 0
|
||||
self.loop_enabled = False
|
||||
|
||||
# UI elements (to be set externally)
|
||||
self.playhead = None
|
||||
self.play_button = None
|
||||
self.play_button_bitmap = None
|
||||
self.stop_button = None
|
||||
self.stop_button_bitmap = None
|
||||
|
||||
# Button sprites (will be set in set_ui_elements)
|
||||
self.button_sprites = None
|
||||
|
||||
def set_ui_elements(self, playhead, play_button, stop_button, button_sprites=None):
|
||||
"""Set references to UI elements needed for playback control"""
|
||||
self.playhead = playhead
|
||||
self.play_button = play_button
|
||||
self.stop_button = stop_button
|
||||
self.button_sprites = button_sprites
|
||||
|
||||
def start_playback(self, start_margin=25):
|
||||
"""Start playback"""
|
||||
self.is_playing = True
|
||||
self.playhead_position = -1 # Start at -1 so first note plays immediately
|
||||
self.last_playhead_time = time.monotonic()
|
||||
|
||||
# Set playhead position to just before the first note
|
||||
self.playhead.x = start_margin - 5
|
||||
|
||||
# Update button states using bitmaps
|
||||
if hasattr(self, 'button_sprites') and self.button_sprites is not None:
|
||||
# Update play button to "down" state
|
||||
self.play_button.bitmap = self.button_sprites['play']['down'][0]
|
||||
self.play_button.pixel_shader = self.button_sprites['play']['down'][1]
|
||||
|
||||
# Update stop button to "up" state
|
||||
self.stop_button.bitmap = self.button_sprites['stop']['up'][0]
|
||||
self.stop_button.pixel_shader = self.button_sprites['stop']['up'][1]
|
||||
else:
|
||||
# Fallback implementation for drawn buttons
|
||||
# Note: This section is for backward compatibility but has issues
|
||||
# Ideally, button_sprites should always be provided
|
||||
print("Warning: Using fallback button display (not fully supported)")
|
||||
# The fallback code is intentionally omitted as it has errors
|
||||
# and requires refactoring of the bitmap handling
|
||||
|
||||
print("Playback started")
|
||||
|
||||
def stop_playback(self):
|
||||
"""Stop playback"""
|
||||
self.sound_manager.stop_all_notes()
|
||||
self.is_playing = False
|
||||
self.playhead.x = -10 # Move off-screen
|
||||
|
||||
# Update button states using bitmaps
|
||||
if hasattr(self, 'button_sprites') and self.button_sprites is not None:
|
||||
# Update play button to "up" state
|
||||
self.play_button.bitmap = self.button_sprites['play']['up'][0]
|
||||
self.play_button.pixel_shader = self.button_sprites['play']['up'][1]
|
||||
|
||||
# Update stop button to "down" state
|
||||
self.stop_button.bitmap = self.button_sprites['stop']['down'][0]
|
||||
self.stop_button.pixel_shader = self.button_sprites['stop']['down'][1]
|
||||
else:
|
||||
# Fallback implementation for drawn buttons
|
||||
# Note: This section is for backward compatibility but has issues
|
||||
# Ideally, button_sprites should always be provided
|
||||
print("Warning: Using fallback button display (not fully supported)")
|
||||
# The fallback code is intentionally omitted as it has errors
|
||||
# and requires refactoring of the bitmap handling
|
||||
|
||||
print("Playback stopped")
|
||||
|
||||
def set_tempo(self, seconds_per_eighth):
|
||||
"""Update the playback tempo"""
|
||||
self.seconds_per_eighth = seconds_per_eighth
|
||||
print(f"Playback tempo updated: {60 / (seconds_per_eighth * 2)} BPM")
|
||||
|
||||
def update_playback(self, x_positions):
|
||||
"""Update playback state and play notes at current position"""
|
||||
if not self.is_playing:
|
||||
return
|
||||
|
||||
current_time = time.monotonic()
|
||||
elapsed = current_time - self.last_playhead_time
|
||||
|
||||
# Move at tempo rate
|
||||
if elapsed >= self.seconds_per_eighth:
|
||||
# Stop all current active notes
|
||||
self.sound_manager.stop_all_notes()
|
||||
|
||||
# Move playhead to next eighth note position
|
||||
self.playhead_position += 1
|
||||
self.last_playhead_time = current_time
|
||||
|
||||
# Check if we've reached the end
|
||||
if self.playhead_position >= len(x_positions):
|
||||
if self.loop_enabled:
|
||||
# Loop back to the beginning
|
||||
self.playhead_position = 0
|
||||
self.playhead.x = x_positions[0] - 1
|
||||
else:
|
||||
# Stop playback if not looping
|
||||
self.stop_playback()
|
||||
return
|
||||
|
||||
# Update playhead position
|
||||
self.playhead.x = x_positions[self.playhead_position] - 1
|
||||
|
||||
# Find all notes at current playhead position
|
||||
current_x = x_positions[self.playhead_position]
|
||||
notes_at_position = []
|
||||
|
||||
for x_pos, y_pos, midi_note, channel in self.note_manager.note_data:
|
||||
if abs(x_pos - current_x) < 2: # Note is at current position
|
||||
notes_at_position.append((x_pos, y_pos, midi_note, channel))
|
||||
|
||||
# Play all notes at the current position
|
||||
if notes_at_position:
|
||||
self.sound_manager.play_notes_at_position(notes_at_position)
|
||||
BIN
Fruit_Jam/Larsio_Paint_Music/samples/chat_01.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/crash_01.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/kick_01.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/larso_A4.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/larso_B3.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/larso_B4.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/larso_C4.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/larso_C5.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/larso_D4.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/larso_D5.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/larso_E4.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/larso_E5.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/larso_F4.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/larso_F5.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/larso_G4.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/larso_G5.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/musicnote16_A4.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/musicnote16_B3.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/musicnote16_B4.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/musicnote16_C4.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/musicnote16_C5.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/musicnote16_D4.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/musicnote16_D5.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/musicnote16_E4.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/musicnote16_E5.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/musicnote16_F4.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/musicnote16_F5.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/musicnote16_G4.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/musicnote16_G5.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/ohat_01.wav
Executable file
BIN
Fruit_Jam/Larsio_Paint_Music/samples/snare_01.wav
Executable file
613
Fruit_Jam/Larsio_Paint_Music/sound_manager.py
Executable file
|
|
@ -0,0 +1,613 @@
|
|||
# SPDX-FileCopyrightText: 2025 John Park and Claude AI for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""
|
||||
# sound_manager.py: CircuitPython Music Staff Application component
|
||||
"""
|
||||
# pylint: disable=import-error, trailing-whitespace
|
||||
#
|
||||
import math
|
||||
import time
|
||||
import array
|
||||
import gc
|
||||
import os
|
||||
import digitalio
|
||||
import busio
|
||||
|
||||
|
||||
import adafruit_midi
|
||||
import audiocore
|
||||
import audiopwmio
|
||||
import audiobusio
|
||||
import audiomixer
|
||||
import synthio
|
||||
import board
|
||||
import adafruit_tlv320
|
||||
from adafruit_midi.note_on import NoteOn
|
||||
from adafruit_midi.note_off import NoteOff
|
||||
import usb_midi
|
||||
|
||||
|
||||
# pylint: disable=invalid-name,no-member,too-many-instance-attributes,too-many-arguments
|
||||
# pylint: disable=too-many-branches,too-many-statements,too-many-locals,broad-except
|
||||
# pylint: disable=cell-var-from-loop,undefined-loop-variable
|
||||
class SoundManager:
|
||||
"""Handles playback of both MIDI notes and WAV samples, and synthio for channels 3-5"""
|
||||
|
||||
def __init__(self, audio_output="pwm", seconds_per_eighth=0.25):
|
||||
"""
|
||||
Initialize the sound manager
|
||||
|
||||
Parameters:
|
||||
audio_output (str): The type of audio output to use - "pwm" or "i2s"
|
||||
seconds_per_eighth (float): Duration of an eighth note in seconds
|
||||
"""
|
||||
# Initialize USB MIDI
|
||||
self.midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0)
|
||||
self.active_notes = {} # {note_number: channel}
|
||||
|
||||
# Store timing information
|
||||
self.seconds_per_eighth = seconds_per_eighth
|
||||
|
||||
# Initialize audio output based on selected type
|
||||
self.audio_output_type = audio_output
|
||||
self.tlv = None
|
||||
|
||||
# Initialize these variables to avoid use-before-assignment issues
|
||||
i2c = None
|
||||
bclck_pin = None
|
||||
wsel_pin = None
|
||||
din_pin = None
|
||||
|
||||
if self.audio_output_type == "pwm":
|
||||
# Setup PWM audio output on D10
|
||||
self.audio = audiopwmio.PWMAudioOut(board.D10)
|
||||
else: # i2s
|
||||
try:
|
||||
# Import libraries needed for I2S
|
||||
#check for Metro RP2350 vs. Fruit Jam
|
||||
board_type = os.uname().machine
|
||||
|
||||
if 'Metro RP2350' in board_type:
|
||||
print("Metro setup")
|
||||
reset_pin = digitalio.DigitalInOut(board.D7)
|
||||
reset_pin.direction = digitalio.Direction.OUTPUT
|
||||
reset_pin.value = False # Set low to reset
|
||||
time.sleep(0.1) # Pause 100ms
|
||||
reset_pin.value = True # Set high to release from reset
|
||||
|
||||
i2c = board.STEMMA_I2C() # initialize I2C
|
||||
|
||||
bclck_pin = board.D9
|
||||
wsel_pin = board.D10
|
||||
din_pin = board.D11
|
||||
|
||||
elif 'Fruit Jam' in board_type:
|
||||
print("Fruit Jam setup")
|
||||
reset_pin = digitalio.DigitalInOut(board.PERIPH_RESET)
|
||||
reset_pin.direction = digitalio.Direction.OUTPUT
|
||||
reset_pin.value = False
|
||||
time.sleep(0.1)
|
||||
reset_pin.value = True
|
||||
|
||||
i2c = busio.I2C(board.SCL, board.SDA)
|
||||
|
||||
bclck_pin = board.I2S_BCLK
|
||||
wsel_pin = board.I2S_WS
|
||||
din_pin = board.I2S_DIN
|
||||
|
||||
# Initialize TLV320
|
||||
self.tlv = adafruit_tlv320.TLV320DAC3100(i2c)
|
||||
self.tlv.configure_clocks(sample_rate=11025, bit_depth=16)
|
||||
self.tlv.headphone_output = True
|
||||
self.tlv.headphone_volume = -15 # dB
|
||||
|
||||
# Setup I2S audio output - important to do this AFTER configuring the DAC
|
||||
self.audio = audiobusio.I2SOut(
|
||||
bit_clock=bclck_pin,
|
||||
word_select=wsel_pin,
|
||||
data=din_pin
|
||||
)
|
||||
|
||||
print("TLV320 I2S DAC initialized successfully")
|
||||
except Exception as e:
|
||||
print(f"Error initializing TLV320 DAC: {e}")
|
||||
print("Falling back to PWM audio output")
|
||||
# Fallback to PWM if I2S initialization fails
|
||||
self.audio = audiopwmio.PWMAudioOut(board.D10)
|
||||
|
||||
# Create an audio mixer with multiple voices
|
||||
self.mixer = audiomixer.Mixer(
|
||||
voice_count=6,
|
||||
sample_rate=11025,
|
||||
channel_count=1,
|
||||
bits_per_sample=16,
|
||||
samples_signed=True
|
||||
)
|
||||
self.audio.play(self.mixer)
|
||||
|
||||
# Track which voices are being used for samples
|
||||
# First 3 for regular samples, next 3 for playback-only
|
||||
self.active_voices = [False, False, False, False, False, False]
|
||||
|
||||
# Track which note position corresponds to which voice
|
||||
# This will help us stop samples when notes are erased
|
||||
self.position_to_voice = {} # {(x_pos, y_pos): voice_index}
|
||||
|
||||
# Track which voice is used for which channel during playback
|
||||
self.playback_voice_mapping = {} # {(x_pos, y_pos, channel): voice_index}
|
||||
|
||||
# Load multiple WAV samples at different pitches
|
||||
try:
|
||||
# Channel 1 samples
|
||||
self.samples = {
|
||||
59: audiocore.WaveFile("/samples/larso_B3.wav"), # B3
|
||||
60: audiocore.WaveFile("/samples/larso_C4.wav"), # C4
|
||||
62: audiocore.WaveFile("/samples/larso_D4.wav"), # D4
|
||||
64: audiocore.WaveFile("/samples/larso_E4.wav"), # E4
|
||||
65: audiocore.WaveFile("/samples/larso_F4.wav"), # F4
|
||||
67: audiocore.WaveFile("/samples/larso_G4.wav"), # G4
|
||||
69: audiocore.WaveFile("/samples/larso_A4.wav"), # A4
|
||||
71: audiocore.WaveFile("/samples/larso_B4.wav"), # B4
|
||||
72: audiocore.WaveFile("/samples/larso_C5.wav"), # C5
|
||||
74: audiocore.WaveFile("/samples/larso_D5.wav"), # D5
|
||||
76: audiocore.WaveFile("/samples/larso_E5.wav"), # E5
|
||||
77: audiocore.WaveFile("/samples/larso_F5.wav"), # F5
|
||||
79: audiocore.WaveFile("/samples/larso_G5.wav"), # G5
|
||||
}
|
||||
print("Loaded channel 1 WAV samples")
|
||||
|
||||
# Load samples for channel 2
|
||||
self.heart_samples = {
|
||||
59: audiocore.WaveFile("/samples/musicnote16_B3.wav"), # B3
|
||||
60: audiocore.WaveFile("/samples/musicnote16_C4.wav"), # C4
|
||||
62: audiocore.WaveFile("/samples/musicnote16_D4.wav"), # D4
|
||||
64: audiocore.WaveFile("/samples/musicnote16_E4.wav"), # E4
|
||||
65: audiocore.WaveFile("/samples/musicnote16_F4.wav"), # F4
|
||||
67: audiocore.WaveFile("/samples/musicnote16_G4.wav"), # G4
|
||||
69: audiocore.WaveFile("/samples/musicnote16_A4.wav"), # A4
|
||||
71: audiocore.WaveFile("/samples/musicnote16_B4.wav"), # B4
|
||||
72: audiocore.WaveFile("/samples/musicnote16_C5.wav"), # C5
|
||||
74: audiocore.WaveFile("/samples/musicnote16_D5.wav"), # D5
|
||||
76: audiocore.WaveFile("/samples/musicnote16_E5.wav"), # E5
|
||||
77: audiocore.WaveFile("/samples/musicnote16_F5.wav"), # F5
|
||||
79: audiocore.WaveFile("/samples/musicnote16_G5.wav"), # G5
|
||||
}
|
||||
print("Loaded channel 2 WAV samples")
|
||||
|
||||
# Load samples for channel 3 (drum samples)
|
||||
self.drum_samples = {}
|
||||
try:
|
||||
self.drum_samples = {
|
||||
59: audiocore.WaveFile("/samples/kick_01.wav"),
|
||||
60: audiocore.WaveFile("/samples/kick_01.wav"),
|
||||
62: audiocore.WaveFile("/samples/kick_01.wav"),
|
||||
64: audiocore.WaveFile("/samples/snare_01.wav"),
|
||||
65: audiocore.WaveFile("/samples/snare_01.wav"),
|
||||
67: audiocore.WaveFile("/samples/snare_01.wav"),
|
||||
69: audiocore.WaveFile("/samples/chat_01.wav"),
|
||||
71: audiocore.WaveFile("/samples/chat_01.wav"),
|
||||
72: audiocore.WaveFile("/samples/chat_01.wav"),
|
||||
74: audiocore.WaveFile("/samples/ohat_01.wav"),
|
||||
76: audiocore.WaveFile("/samples/ohat_01.wav"),
|
||||
77: audiocore.WaveFile("/samples/crash_01.wav"),
|
||||
79: audiocore.WaveFile("/samples/crash_01.wav"),
|
||||
}
|
||||
print("Loaded channel 3 WAV samples (drums)")
|
||||
except Exception as e:
|
||||
print(f"Error loading drum samples: {e}")
|
||||
# Fallback - use the same samples as channel 1
|
||||
self.drum_samples = self.samples
|
||||
print("Using fallback samples for channel 3")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading WAV samples: {e}")
|
||||
# Fallback to basic samples if there's an error
|
||||
self.samples = {
|
||||
65: audiocore.WaveFile("/samples/musicnote01.wav"), # Default sample
|
||||
}
|
||||
self.heart_samples = self.samples # Use same samples as fallback
|
||||
self.drum_samples = self.samples # Use same samples as fallback
|
||||
|
||||
# Initialize synthio for channels 4-6
|
||||
self.synth = synthio.Synthesizer(sample_rate=11025)
|
||||
# Use the last voice for synthio
|
||||
self.mixer.voice[5].play(self.synth)
|
||||
|
||||
# Set lower volume for synthio channel
|
||||
self.mixer.voice[5].level = 0.3
|
||||
|
||||
# Create waveforms for different synthio channels
|
||||
SAMPLE_SIZE = 512
|
||||
SAMPLE_VOLUME = 30000 # Slightly lower to avoid overflow
|
||||
half_period = SAMPLE_SIZE // 2
|
||||
|
||||
# Sine wave for channel 4
|
||||
self.wave_sine = array.array("h", [0] * SAMPLE_SIZE)
|
||||
for i in range(SAMPLE_SIZE):
|
||||
# Use max() and min() to ensure we stay within bounds
|
||||
value = int(math.sin(math.pi * 2 * (i/2) / SAMPLE_SIZE) * SAMPLE_VOLUME)
|
||||
self.wave_sine[i] = max(-32768, min(32767, value))
|
||||
|
||||
# Triangle wave for channel 5
|
||||
self.wave_tri = array.array("h", [0] * SAMPLE_SIZE)
|
||||
for i in range(SAMPLE_SIZE):
|
||||
if i < half_period:
|
||||
value = int(((i / (half_period)) * 2 - 1) * SAMPLE_VOLUME)
|
||||
else:
|
||||
value = int(((2 - (i / (half_period)) * 2)) * SAMPLE_VOLUME)
|
||||
self.wave_tri[i] = max(-32768, min(32767, value))
|
||||
|
||||
# Sawtooth wave for channel 6
|
||||
self.wave_saw = array.array("h", [0] * SAMPLE_SIZE)
|
||||
for i in range(SAMPLE_SIZE):
|
||||
value = int(((i / SAMPLE_SIZE) * 2 - 1) * SAMPLE_VOLUME)
|
||||
self.wave_saw[i] = max(-32768, min(32767, value))
|
||||
|
||||
# Map channels to waveforms
|
||||
self.channel_waveforms = {
|
||||
3: self.wave_sine, # Channel 4: Sine wave (soft, pure tone)
|
||||
4: self.wave_tri, # Channel 5: Triangle wave (mellow, soft)
|
||||
5: self.wave_saw, # Channel 6: Sawtooth wave (brassy, sharp)
|
||||
}
|
||||
|
||||
# Set different amplitudes for each waveform to balance volumes
|
||||
self.channel_amplitudes = {
|
||||
3: 1.0, # Sine wave - normal volume
|
||||
4: 0.8, # Triangle wave - slightly quieter
|
||||
5: 0.3, # Sawtooth wave - much quieter (harmonically rich)
|
||||
}
|
||||
|
||||
# Track active synth notes by channel and note
|
||||
self.active_synth_notes = {
|
||||
3: [], # Channel 4
|
||||
4: [], # Channel 5
|
||||
5: [], # Channel 6
|
||||
}
|
||||
|
||||
# Variables for timed release of preview notes
|
||||
self.note_release_time = 0
|
||||
self.note_to_release = None
|
||||
self.note_to_release_channel = None
|
||||
self.preview_mode = False
|
||||
|
||||
def play_note(self, midi_note, channel):
|
||||
"""Play a note using either MIDI, WAV, or synthio based on channel"""
|
||||
if channel == 0: # Channel 1 uses WAV samples
|
||||
self.play_multi_sample(midi_note, channel)
|
||||
elif channel == 1: # Channel 2 uses Heart note WAV samples
|
||||
self.play_multi_sample(midi_note, channel)
|
||||
elif channel == 2: # Channel 3 uses Drum WAV samples
|
||||
self.play_multi_sample(midi_note, channel)
|
||||
elif channel in [3, 4, 5]: # Channels 4-6 use synthio with different waveforms
|
||||
self.preview_mode = True
|
||||
self.play_synth_note(midi_note, channel)
|
||||
# Schedule note release
|
||||
self.note_release_time = time.monotonic() + self.seconds_per_eighth
|
||||
self.note_to_release_channel = channel
|
||||
else:
|
||||
# Send note on the correct MIDI channel (channels are 0-based in adafruit_midi)
|
||||
self.midi.send(NoteOn(midi_note, 100), channel=channel)
|
||||
# Store note with its channel for proper Note Off later
|
||||
self.active_notes[midi_note] = channel
|
||||
# print(f"Playing note: {midi_note} on channel {channel + 1}")
|
||||
|
||||
def play_notes_at_position(self, notes_data):
|
||||
"""Play all notes at a specific position simultaneously"""
|
||||
# Stop all sample voices first
|
||||
for i in range(5): # Use first 5 voices for WAV samples (0-4)
|
||||
self.mixer.voice[i].stop()
|
||||
self.active_voices[i] = False
|
||||
|
||||
# Clear the position to voice mapping
|
||||
self.position_to_voice = {}
|
||||
self.playback_voice_mapping = {}
|
||||
|
||||
# Group notes by channel type
|
||||
sample_notes = {
|
||||
0: [], # Channel 1 (Lars WAV samples)
|
||||
1: [], # Channel 2 (Heart WAV samples)
|
||||
2: [] # Channel 3 (Drum WAV samples)
|
||||
}
|
||||
|
||||
# Synthio channels (4-6)
|
||||
synth_notes = {
|
||||
3: [], # Channel 4 (Sine wave)
|
||||
4: [], # Channel 5 (Triangle wave)
|
||||
5: [], # Channel 6 (Sawtooth wave)
|
||||
}
|
||||
|
||||
midi_notes = {} # Other channels (MIDI)
|
||||
|
||||
for x_pos, y_pos, note_val, channel in notes_data:
|
||||
if channel in [0, 1, 2]: # Sample-based channels
|
||||
sample_notes[channel].append((x_pos, y_pos, note_val))
|
||||
elif channel in [3, 4, 5]: # Synthio channels
|
||||
synth_notes[channel].append(note_val)
|
||||
else: # Other channels (MIDI)
|
||||
midi_notes[note_val] = channel
|
||||
|
||||
# Voice allocation - we have 5 voices to distribute among sample notes
|
||||
remaining_voices = 5
|
||||
voice_index = 0
|
||||
|
||||
# Play sample notes for channels 1-3
|
||||
for channel, notes in sample_notes.items():
|
||||
for x_pos, y_pos, midi_note in notes:
|
||||
if remaining_voices <= 0:
|
||||
print(f"Warning: No more voices available for channel {channel+1}")
|
||||
break
|
||||
|
||||
# Get the appropriate sample set
|
||||
sample_set = None
|
||||
if channel == 0:
|
||||
sample_set = self.samples
|
||||
elif channel == 1:
|
||||
sample_set = self.heart_samples
|
||||
elif channel == 2:
|
||||
sample_set = self.drum_samples
|
||||
|
||||
# Find the closest sample
|
||||
closest_note = min(sample_set.keys(), key=lambda x: abs(x - midi_note))
|
||||
sample = sample_set[closest_note]
|
||||
|
||||
# Play the sample
|
||||
self.mixer.voice[voice_index].play(sample, loop=False)
|
||||
self.active_voices[voice_index] = True
|
||||
|
||||
# Store the position to voice mapping
|
||||
position_key = (x_pos, y_pos)
|
||||
self.position_to_voice[position_key] = voice_index
|
||||
self.playback_voice_mapping[(x_pos, y_pos, channel)] = voice_index
|
||||
|
||||
# Adjust volume
|
||||
total_notes = sum(len(notes) for notes in sample_notes.values())
|
||||
volume_factor = 0.9 if total_notes <= 3 else 0.7 if total_notes <= 6 else 0.5
|
||||
self.mixer.voice[voice_index].level = 0.7 * volume_factor
|
||||
|
||||
voice_index += 1
|
||||
remaining_voices -= 1
|
||||
|
||||
# Log what we're playing
|
||||
# Channel names commented out as it was unused
|
||||
# channel_names = ["Lars", "Heart", "Drum"]
|
||||
# print(f"Playing {channel_names[channel]} sample {closest_note} for note {midi_note}")
|
||||
|
||||
# Play synth notes for each channel (4-6)
|
||||
self.preview_mode = False
|
||||
for channel, notes in synth_notes.items():
|
||||
for note in notes:
|
||||
self.play_synth_note(note, channel)
|
||||
|
||||
# Play MIDI notes
|
||||
for midi_note, channel in midi_notes.items():
|
||||
self.midi.send(NoteOn(midi_note, 100), channel=channel)
|
||||
self.active_notes[midi_note] = channel
|
||||
|
||||
def play_multi_sample(self, midi_note, channel=0):
|
||||
"""Play the most appropriate sample for the given MIDI note"""
|
||||
try:
|
||||
# Find an available voice (use first 3 voices for interactive play)
|
||||
voice_index = -1
|
||||
for i in range(3): # Only use the first 3 voices for interactive playback
|
||||
if not self.active_voices[i]:
|
||||
voice_index = i
|
||||
break
|
||||
|
||||
# If all voices are active, use the first one
|
||||
if voice_index == -1:
|
||||
voice_index = 0
|
||||
|
||||
# Stop any currently playing sample in this voice
|
||||
self.mixer.voice[voice_index].stop()
|
||||
|
||||
# Select the appropriate sample set based on channel
|
||||
if channel == 1: # Heart samples
|
||||
sample_set = self.heart_samples
|
||||
elif channel == 2: # Drum samples
|
||||
sample_set = self.drum_samples
|
||||
else: # Default to channel 1 samples
|
||||
sample_set = self.samples
|
||||
|
||||
# Find the closest sample
|
||||
closest_note = min(sample_set.keys(), key=lambda x: abs(x - midi_note))
|
||||
|
||||
# Get the sample
|
||||
sample = sample_set[closest_note]
|
||||
|
||||
# Play the sample
|
||||
self.mixer.voice[voice_index].play(sample, loop=False)
|
||||
self.active_voices[voice_index] = True
|
||||
|
||||
# Adjust volume based on which sample we're using
|
||||
if closest_note == 65: # F4
|
||||
self.mixer.voice[voice_index].level = 0.8
|
||||
elif closest_note == 69: # A4
|
||||
self.mixer.voice[voice_index].level = 0.7
|
||||
elif closest_note == 72: # C5
|
||||
self.mixer.voice[voice_index].level = 0.6
|
||||
else:
|
||||
self.mixer.voice[voice_index].level = 0.7
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error playing multi-sample: {e}")
|
||||
# Try to play any available sample as a fallback
|
||||
if len(self.samples) > 0:
|
||||
first_sample = next(iter(self.samples.values()))
|
||||
self.mixer.voice[0].play(first_sample, loop=False)
|
||||
|
||||
def play_synth_note(self, midi_note, channel):
|
||||
"""Play a note using synthio with different waveforms per channel"""
|
||||
try:
|
||||
# Convert MIDI note to frequency
|
||||
frequency = 440 * math.pow(2, (midi_note - 69) / 12)
|
||||
|
||||
# Get the appropriate waveform for this channel
|
||||
waveform = self.channel_waveforms.get(channel, self.wave_sine)
|
||||
|
||||
# Get the appropriate amplitude for this channel
|
||||
amplitude = self.channel_amplitudes.get(channel, 1.0)
|
||||
|
||||
# Create synthio note with the specific waveform and amplitude
|
||||
note = synthio.Note(
|
||||
frequency,
|
||||
waveform=waveform,
|
||||
amplitude=amplitude
|
||||
)
|
||||
|
||||
# Add to synth
|
||||
self.synth.press(note)
|
||||
|
||||
# If we have an existing preview note to release, release it first
|
||||
if self.preview_mode and self.note_to_release and self.note_to_release_channel==channel:
|
||||
try:
|
||||
self.synth.release(self.note_to_release)
|
||||
except Exception as e:
|
||||
print(f"Error releasing previous note: {e}")
|
||||
|
||||
# Store the new note for scheduled release if in preview mode
|
||||
if self.preview_mode:
|
||||
self.note_to_release = note
|
||||
self.note_to_release_channel = channel
|
||||
else:
|
||||
self.active_synth_notes[channel].append(note)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error playing synthio note: {e}")
|
||||
# If there's an error with custom waveforms, fall back to default note
|
||||
try:
|
||||
frequency = 440 * math.pow(2, (midi_note - 69) / 12)
|
||||
note = synthio.Note(frequency)
|
||||
self.synth.press(note)
|
||||
|
||||
# Store for later release
|
||||
if self.preview_mode:
|
||||
self.note_to_release = note
|
||||
self.note_to_release_channel = channel
|
||||
else:
|
||||
self.active_synth_notes[channel].append(note)
|
||||
|
||||
except Exception as e2:
|
||||
print(f"Fallback note error: {e2}")
|
||||
|
||||
def stop_sample_at_position(self, x_pos, y_pos, channel):
|
||||
"""Stop a sample that's playing at the given position for a specific channel"""
|
||||
position_key = (x_pos, y_pos, channel)
|
||||
if position_key in self.playback_voice_mapping:
|
||||
voice_index = self.playback_voice_mapping[position_key]
|
||||
|
||||
# Stop the sample
|
||||
self.mixer.voice[voice_index].stop()
|
||||
self.active_voices[voice_index] = False
|
||||
|
||||
# Remove from mappings
|
||||
del self.playback_voice_mapping[position_key]
|
||||
return True
|
||||
|
||||
# Also check the simple position mapping
|
||||
simple_key = (x_pos, y_pos)
|
||||
if simple_key in self.position_to_voice:
|
||||
voice_index = self.position_to_voice[simple_key]
|
||||
|
||||
# Stop the sample
|
||||
self.mixer.voice[voice_index].stop()
|
||||
self.active_voices[voice_index] = False
|
||||
|
||||
# Remove from mapping
|
||||
del self.position_to_voice[simple_key]
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
"""Update function to handle timed note releases"""
|
||||
# Check if we need to release a preview note
|
||||
if self.note_to_release and time.monotonic() >= self.note_release_time:
|
||||
try:
|
||||
self.synth.release(self.note_to_release)
|
||||
self.note_to_release = None
|
||||
self.note_to_release_channel = None
|
||||
except Exception as e:
|
||||
print(f"Error releasing preview note: {e}")
|
||||
self.note_to_release = None
|
||||
self.note_to_release_channel = None
|
||||
|
||||
def stop_all_notes(self):
|
||||
"""Stop all currently playing notes"""
|
||||
# Stop all MIDI notes
|
||||
for note_number, channel in self.active_notes.items():
|
||||
self.midi.send(NoteOff(note_number, 0), channel=channel)
|
||||
self.active_notes = {}
|
||||
|
||||
# Stop all WAV samples
|
||||
for i in range(5): # Use first 5 voices for WAV samples
|
||||
self.mixer.voice[i].stop()
|
||||
self.active_voices[i] = False
|
||||
|
||||
# Clear position mappings
|
||||
self.position_to_voice = {}
|
||||
self.playback_voice_mapping = {}
|
||||
|
||||
# Stop all synth notes
|
||||
try:
|
||||
# Release notes from all channels
|
||||
for channel, notes in self.active_synth_notes.items():
|
||||
for note in notes:
|
||||
self.synth.release(note)
|
||||
self.active_synth_notes[channel] = []
|
||||
|
||||
# Also release preview note if there is one
|
||||
if self.note_to_release:
|
||||
self.synth.release(self.note_to_release)
|
||||
self.note_to_release = None
|
||||
self.note_to_release_channel = None
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error releasing synth notes: {e}")
|
||||
# Reinitialize the synth as a fallback
|
||||
try:
|
||||
self.synth.deinit()
|
||||
self.synth = synthio.Synthesizer(sample_rate=11025)
|
||||
self.mixer.voice[5].play(self.synth)
|
||||
|
||||
# Reset all active notes
|
||||
self.active_synth_notes = {
|
||||
3: [], # Channel 4
|
||||
4: [], # Channel 5
|
||||
5: [], # Channel 6
|
||||
}
|
||||
except Exception as e2:
|
||||
print(f"Error reinitializing synth: {e2}")
|
||||
|
||||
def deinit(self):
|
||||
"""Clean up resources when shutting down"""
|
||||
# Stop all sounds
|
||||
self.stop_all_notes()
|
||||
|
||||
# Clean up audio resources
|
||||
try:
|
||||
self.audio.deinit()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Power down the TLV320 if applicable
|
||||
if self.tlv:
|
||||
try:
|
||||
# For TLV320DAC3100, headphone_output = False will power down the output
|
||||
self.tlv.headphone_output = False
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Clean up synth
|
||||
try:
|
||||
self.synth.deinit()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Force garbage collection
|
||||
gc.collect()
|
||||
|
||||
def set_tempo(self, seconds_per_eighth):
|
||||
"""Update the playback tempo"""
|
||||
self.seconds_per_eighth = seconds_per_eighth
|
||||
print(f"Playback tempo updated: {60 / (seconds_per_eighth * 2)} BPM")
|
||||
219
Fruit_Jam/Larsio_Paint_Music/sprite_manager.py
Executable file
|
|
@ -0,0 +1,219 @@
|
|||
# SPDX-FileCopyrightText: 2025 John Park and Claude AI for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""
|
||||
Sprite manager for CircuitPython Music Staff Application.
|
||||
Handles loading and managing sprite images and palettes.
|
||||
"""
|
||||
|
||||
# pylint: disable=import-error, trailing-whitespace
|
||||
import adafruit_imageload
|
||||
from displayio import Palette, TileGrid
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes,invalid-name,broad-except
|
||||
class SpriteManager:
|
||||
"""Manages sprites and palettes for note display"""
|
||||
|
||||
def __init__(self, bg_color=0x8AAD8A):
|
||||
"""Initialize the sprite manager"""
|
||||
self.bg_color = bg_color
|
||||
|
||||
# Initialize palettes as empty lists first
|
||||
self.note_palettes = []
|
||||
self.preview_palettes = []
|
||||
|
||||
# Sprites
|
||||
self.mario_head = None
|
||||
self.mario_palette = None
|
||||
self.heart_note = None
|
||||
self.heart_palette = None
|
||||
self.drum_note = None
|
||||
self.drum_palette = None
|
||||
# Add new sprite variables
|
||||
self.meatball_note = None
|
||||
self.meatball_palette = None
|
||||
self.star_note = None
|
||||
self.star_palette = None
|
||||
self.bot_note = None
|
||||
self.bot_palette = None
|
||||
|
||||
# Channel colors (still need these for palette management)
|
||||
self.channel_colors = [
|
||||
0x000000, # Channel 1: Black (default)
|
||||
0xFF0000, # Channel 2: Red
|
||||
0x00FF00, # Channel 3: Green
|
||||
0x0000FF, # Channel 4: Blue
|
||||
0xFF00FF, # Channel 5: Magenta
|
||||
0xFFAA00, # Channel 6: Orange
|
||||
]
|
||||
|
||||
# Add button sprites
|
||||
self.play_up = None
|
||||
self.play_up_palette = None
|
||||
self.play_down = None
|
||||
self.play_down_palette = None
|
||||
self.stop_up = None
|
||||
self.stop_up_palette = None
|
||||
self.stop_down = None
|
||||
self.stop_down_palette = None
|
||||
self.loop_up = None
|
||||
self.loop_up_palette = None
|
||||
self.loop_down = None
|
||||
self.loop_down_palette = None
|
||||
self.clear_up = None
|
||||
self.clear_up_palette = None
|
||||
self.clear_down = None
|
||||
self.clear_down_palette = None
|
||||
|
||||
# Load sprites
|
||||
self.load_sprites()
|
||||
|
||||
# Load button sprites
|
||||
self.load_button_sprites()
|
||||
|
||||
# Create palettes
|
||||
self.create_palettes()
|
||||
|
||||
def load_sprites(self):
|
||||
"""Load sprite images"""
|
||||
try:
|
||||
# Load the Lars note bitmap for channel 1 notes
|
||||
self.mario_head, self.mario_palette = adafruit_imageload.load(
|
||||
"/sprites/lars_note.bmp"
|
||||
)
|
||||
# Make the background color transparent (not just the same color)
|
||||
self.mario_palette.make_transparent(0)
|
||||
|
||||
# Load the Heart note bitmap for channel 2 notes
|
||||
self.heart_note, self.heart_palette = adafruit_imageload.load(
|
||||
"/sprites/heart_note.bmp"
|
||||
)
|
||||
# Make the background color transparent
|
||||
self.heart_palette.make_transparent(0)
|
||||
|
||||
# Load the Drum note bitmap for channel 3 notes
|
||||
self.drum_note, self.drum_palette = adafruit_imageload.load(
|
||||
"/sprites/drum_note.bmp"
|
||||
)
|
||||
# Make the background color transparent
|
||||
self.drum_palette.make_transparent(0)
|
||||
|
||||
# Load the new sprites for channels 4, 5, and 6
|
||||
# Meatball for channel 4
|
||||
self.meatball_note, self.meatball_palette = adafruit_imageload.load(
|
||||
"/sprites/meatball.bmp"
|
||||
)
|
||||
self.meatball_palette.make_transparent(0)
|
||||
|
||||
# Star for channel 5
|
||||
self.star_note, self.star_palette = adafruit_imageload.load(
|
||||
"/sprites/star.bmp"
|
||||
)
|
||||
self.star_palette.make_transparent(0)
|
||||
|
||||
# Bot for channel 6
|
||||
self.bot_note, self.bot_palette = adafruit_imageload.load("/sprites/bot.bmp")
|
||||
self.bot_palette.make_transparent(0)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading sprites: {e}")
|
||||
|
||||
def create_palettes(self):
|
||||
"""Create palettes for notes and preview"""
|
||||
# Create a palette for music notes with multiple colors
|
||||
for channel_color in self.channel_colors:
|
||||
palette = Palette(2)
|
||||
palette[0] = self.bg_color # Transparent (sage green background)
|
||||
palette[1] = channel_color # Note color for this channel
|
||||
self.note_palettes.append(palette)
|
||||
|
||||
# Create a preview palette with multiple colors
|
||||
for channel_color in self.channel_colors:
|
||||
palette = Palette(2)
|
||||
palette[0] = self.bg_color # Transparent (sage green background)
|
||||
# For preview, use a lighter version of the channel color
|
||||
r = ((channel_color >> 16) & 0xFF) // 2 + 0x40
|
||||
g = ((channel_color >> 8) & 0xFF) // 2 + 0x40
|
||||
b = (channel_color & 0xFF) // 2 + 0x40
|
||||
preview_color = (r << 16) | (g << 8) | b
|
||||
palette[1] = preview_color
|
||||
self.preview_palettes.append(palette)
|
||||
|
||||
def create_preview_note(self, current_channel, note_bitmap):
|
||||
"""Create preview note based on channel"""
|
||||
if current_channel == 0: # Channel 1 uses Lars note
|
||||
preview_tg = TileGrid(self.mario_head, pixel_shader=self.mario_palette)
|
||||
elif current_channel == 1: # Channel 2 uses Heart note
|
||||
preview_tg = TileGrid(self.heart_note, pixel_shader=self.heart_palette)
|
||||
elif current_channel == 2: # Channel 3 uses Drum note
|
||||
preview_tg = TileGrid(self.drum_note, pixel_shader=self.drum_palette)
|
||||
elif current_channel == 3: # Channel 4 uses Meatball
|
||||
preview_tg = TileGrid(self.meatball_note, pixel_shader=self.meatball_palette)
|
||||
elif current_channel == 4: # Channel 5 uses Star
|
||||
preview_tg = TileGrid(self.star_note, pixel_shader=self.star_palette)
|
||||
elif current_channel == 5: # Channel 6 uses Bot
|
||||
preview_tg = TileGrid(self.bot_note, pixel_shader=self.bot_palette)
|
||||
else: # Fallback to colored circle
|
||||
preview_tg = TileGrid(
|
||||
note_bitmap,
|
||||
pixel_shader=self.preview_palettes[current_channel]
|
||||
)
|
||||
|
||||
preview_tg.x = 0
|
||||
preview_tg.y = 0
|
||||
preview_tg.hidden = True # Start with preview hidden
|
||||
|
||||
return preview_tg
|
||||
|
||||
def load_button_sprites(self):
|
||||
"""Load button sprites for transport controls"""
|
||||
try:
|
||||
# Load play button images
|
||||
self.play_up, self.play_up_palette = adafruit_imageload.load(
|
||||
"/sprites/play_up.bmp"
|
||||
)
|
||||
self.play_up_palette.make_transparent(0)
|
||||
|
||||
self.play_down, self.play_down_palette = adafruit_imageload.load(
|
||||
"/sprites/play_down.bmp"
|
||||
)
|
||||
self.play_down_palette.make_transparent(0)
|
||||
|
||||
# Load stop button images
|
||||
self.stop_up, self.stop_up_palette = adafruit_imageload.load(
|
||||
"/sprites/stop_up.bmp"
|
||||
)
|
||||
self.stop_up_palette.make_transparent(0)
|
||||
|
||||
self.stop_down, self.stop_down_palette = adafruit_imageload.load(
|
||||
"/sprites/stop_down.bmp"
|
||||
)
|
||||
self.stop_down_palette.make_transparent(0)
|
||||
|
||||
# Load loop button images
|
||||
self.loop_up, self.loop_up_palette = adafruit_imageload.load(
|
||||
"/sprites/loop_up.bmp"
|
||||
)
|
||||
self.loop_up_palette.make_transparent(0)
|
||||
|
||||
self.loop_down, self.loop_down_palette = adafruit_imageload.load(
|
||||
"/sprites/loop_down.bmp"
|
||||
)
|
||||
self.loop_down_palette.make_transparent(0)
|
||||
|
||||
# Load clear button images
|
||||
self.clear_up, self.clear_up_palette = adafruit_imageload.load(
|
||||
"/sprites/clear_up.bmp"
|
||||
)
|
||||
self.clear_up_palette.make_transparent(0)
|
||||
|
||||
self.clear_down, self.clear_down_palette = adafruit_imageload.load(
|
||||
"/sprites/clear_down.bmp"
|
||||
)
|
||||
self.clear_down_palette.make_transparent(0)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error loading button sprites: {e}")
|
||||
return False
|
||||
BIN
Fruit_Jam/Larsio_Paint_Music/sprites/bot.bmp
Executable file
|
After Width: | Height: | Size: 246 B |
BIN
Fruit_Jam/Larsio_Paint_Music/sprites/clear_down.bmp
Executable file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
Fruit_Jam/Larsio_Paint_Music/sprites/clear_up.bmp
Executable file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
Fruit_Jam/Larsio_Paint_Music/sprites/drum_note.bmp
Executable file
|
After Width: | Height: | Size: 246 B |
BIN
Fruit_Jam/Larsio_Paint_Music/sprites/heart_note.bmp
Executable file
|
After Width: | Height: | Size: 246 B |
BIN
Fruit_Jam/Larsio_Paint_Music/sprites/lars_note.bmp
Executable file
|
After Width: | Height: | Size: 348 B |
BIN
Fruit_Jam/Larsio_Paint_Music/sprites/loop_down.bmp
Executable file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
Fruit_Jam/Larsio_Paint_Music/sprites/loop_up.bmp
Executable file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
Fruit_Jam/Larsio_Paint_Music/sprites/meatball.bmp
Executable file
|
After Width: | Height: | Size: 246 B |
BIN
Fruit_Jam/Larsio_Paint_Music/sprites/play_down.bmp
Executable file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
Fruit_Jam/Larsio_Paint_Music/sprites/play_up.bmp
Executable file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
Fruit_Jam/Larsio_Paint_Music/sprites/star.bmp
Executable file
|
After Width: | Height: | Size: 246 B |
BIN
Fruit_Jam/Larsio_Paint_Music/sprites/stop_down.bmp
Executable file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
Fruit_Jam/Larsio_Paint_Music/sprites/stop_up.bmp
Executable file
|
After Width: | Height: | Size: 4.1 KiB |
220
Fruit_Jam/Larsio_Paint_Music/staff_view.py
Executable file
|
|
@ -0,0 +1,220 @@
|
|||
# SPDX-FileCopyrightText: 2025 John Park and Claude AI for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""
|
||||
# staff_view.py: Larsio Paint Music component
|
||||
"""
|
||||
|
||||
# pylint: disable=import-error, trailing-whitespace
|
||||
from displayio import Group, Bitmap, Palette, TileGrid
|
||||
|
||||
|
||||
# pylint: disable=invalid-name,no-member,too-many-instance-attributes,too-many-arguments
|
||||
# pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-nested-blocks
|
||||
class StaffView:
|
||||
"""Manages the music staff display and related elements"""
|
||||
|
||||
def __init__(self, screen_width, screen_height, note_manager):
|
||||
self.SCREEN_WIDTH = screen_width
|
||||
self.SCREEN_HEIGHT = screen_height
|
||||
self.note_manager = note_manager
|
||||
|
||||
# Staff dimensions
|
||||
self.TOP_MARGIN = int(self.SCREEN_HEIGHT * 0.1)
|
||||
self.BOTTOM_MARGIN = int(self.SCREEN_HEIGHT * 0.2)
|
||||
self.STAFF_HEIGHT = int((self.SCREEN_HEIGHT - self.TOP_MARGIN - self.BOTTOM_MARGIN) * 0.95)
|
||||
self.STAFF_Y_START = self.TOP_MARGIN
|
||||
self.LINE_SPACING = self.STAFF_HEIGHT // 8
|
||||
|
||||
# Margins and spacing
|
||||
self.START_MARGIN = 25 # Pixels from left edge for the double bar
|
||||
|
||||
# Note spacing
|
||||
self.EIGHTH_NOTE_SPACING = self.SCREEN_WIDTH // 40
|
||||
self.QUARTER_NOTE_SPACING = self.EIGHTH_NOTE_SPACING * 2
|
||||
|
||||
# Measure settings
|
||||
self.NOTES_PER_MEASURE = 4
|
||||
self.MEASURE_WIDTH = self.QUARTER_NOTE_SPACING * self.NOTES_PER_MEASURE
|
||||
self.MEASURES_PER_LINE = 4
|
||||
|
||||
# Playback elements
|
||||
self.playhead = None
|
||||
self.highlight_grid = None
|
||||
|
||||
# X positions for notes
|
||||
self.x_positions = []
|
||||
self._generate_x_positions()
|
||||
|
||||
def _generate_x_positions(self):
|
||||
"""Generate horizontal positions for notes"""
|
||||
self.x_positions = []
|
||||
for measure in range(self.MEASURES_PER_LINE):
|
||||
measure_start = self.START_MARGIN + (measure * self.MEASURE_WIDTH)
|
||||
for eighth_pos in range(8):
|
||||
x_pos = (measure_start + (eighth_pos * self.EIGHTH_NOTE_SPACING) +
|
||||
self.EIGHTH_NOTE_SPACING // 2)
|
||||
if x_pos < self.SCREEN_WIDTH:
|
||||
self.x_positions.append(x_pos)
|
||||
|
||||
# Share positions with note manager
|
||||
self.note_manager.x_positions = self.x_positions
|
||||
|
||||
def create_staff(self):
|
||||
"""Create the staff with lines and background"""
|
||||
staff_group = Group()
|
||||
|
||||
# Create staff background
|
||||
staff_bg_bitmap = Bitmap(self.SCREEN_WIDTH, self.STAFF_HEIGHT, 2)
|
||||
staff_bg_palette = Palette(2)
|
||||
staff_bg_palette[0] = 0xF5F5DC # Light beige (transparent)
|
||||
staff_bg_palette[1] = 0x657c95 # 8AAD8A
|
||||
|
||||
# Fill staff background with sage green
|
||||
for x in range(self.SCREEN_WIDTH):
|
||||
for y in range(self.STAFF_HEIGHT):
|
||||
staff_bg_bitmap[x, y] = 1
|
||||
|
||||
# Create a TileGrid for staff background
|
||||
staff_bg_grid = TileGrid(
|
||||
staff_bg_bitmap,
|
||||
pixel_shader=staff_bg_palette,
|
||||
x=0,
|
||||
y=self.STAFF_Y_START
|
||||
)
|
||||
staff_group.append(staff_bg_grid)
|
||||
|
||||
# Create staff lines
|
||||
staff_bitmap = Bitmap(self.SCREEN_WIDTH, self.STAFF_HEIGHT, 4)
|
||||
staff_palette = Palette(4)
|
||||
staff_palette[0] = 0x657c95 #
|
||||
staff_palette[1] = 0x000000 # Black for horizontal staff lines
|
||||
staff_palette[2] = 0x888888 # Medium gray for measure bar lines
|
||||
staff_palette[3] = 0xAAAAAA # Lighter gray for quarter note dividers
|
||||
|
||||
# Draw 5 horizontal staff lines
|
||||
for i in range(5):
|
||||
y_pos = (i + 1) * self.LINE_SPACING
|
||||
for x in range(self.SCREEN_WIDTH):
|
||||
staff_bitmap[x, y_pos] = 1
|
||||
|
||||
# Add double bar at the beginning
|
||||
for x in range(self.START_MARGIN - 5, self.START_MARGIN - 2):
|
||||
for y in range(self.STAFF_HEIGHT):
|
||||
if self.LINE_SPACING <= y <= 5 * self.LINE_SPACING:
|
||||
staff_bitmap[x, y] = 1
|
||||
|
||||
for x in range(self.START_MARGIN - 1, self.START_MARGIN + 2):
|
||||
for y in range(self.STAFF_HEIGHT):
|
||||
if self.LINE_SPACING <= y <= 5 * self.LINE_SPACING:
|
||||
staff_bitmap[x, y] = 1
|
||||
|
||||
# Add measure bar lines (thicker, darker)
|
||||
bar_line_width = 2
|
||||
|
||||
# For each measure (except after the last one)
|
||||
for i in range(1, self.MEASURES_PER_LINE):
|
||||
# Calculate measure bar position
|
||||
measure_bar_x = self.START_MARGIN + (i * self.MEASURE_WIDTH)
|
||||
|
||||
if measure_bar_x < self.SCREEN_WIDTH:
|
||||
# Draw the measure bar line
|
||||
for y in range(self.STAFF_HEIGHT):
|
||||
if self.LINE_SPACING <= y <= 5 * self.LINE_SPACING:
|
||||
for thickness in range(bar_line_width):
|
||||
if measure_bar_x + thickness < self.SCREEN_WIDTH:
|
||||
staff_bitmap[measure_bar_x + thickness, y] = 2
|
||||
|
||||
# Add quarter note divider lines within each measure
|
||||
for measure in range(self.MEASURES_PER_LINE):
|
||||
measure_start_x = self.START_MARGIN + (measure * self.MEASURE_WIDTH)
|
||||
|
||||
# Calculate quarter note positions (divide measure into 4 equal parts)
|
||||
quarter_width = self.MEASURE_WIDTH // 4
|
||||
|
||||
# Draw lines at the first, second, and third quarter positions
|
||||
for q in range(1, 4): # Draw at positions 1, 2, and 3 (not at 0 or 4)
|
||||
quarter_x = measure_start_x + (q * quarter_width)
|
||||
|
||||
if quarter_x < self.SCREEN_WIDTH:
|
||||
for y in range(self.STAFF_HEIGHT):
|
||||
if self.LINE_SPACING <= y <= 5 * self.LINE_SPACING:
|
||||
staff_bitmap[quarter_x, y] = 3 # Use color 3 (light gray)
|
||||
|
||||
# Add double bar line at the end
|
||||
double_bar_width = 5
|
||||
double_bar_x = self.START_MARGIN + (self.MEASURES_PER_LINE * self.MEASURE_WIDTH) + 5
|
||||
if double_bar_x + double_bar_width < self.SCREEN_WIDTH:
|
||||
# First thick line
|
||||
for x in range(3):
|
||||
for y in range(self.STAFF_HEIGHT):
|
||||
if self.LINE_SPACING <= y <= 5 * self.LINE_SPACING:
|
||||
staff_bitmap[double_bar_x + x, y] = 1
|
||||
|
||||
# Second thick line (with gap)
|
||||
for x in range(3):
|
||||
for y in range(self.STAFF_HEIGHT):
|
||||
if self.LINE_SPACING <= y <= 5 * self.LINE_SPACING:
|
||||
staff_bitmap[double_bar_x + x + 4, y] = 1
|
||||
|
||||
# Create a TileGrid with the staff bitmap
|
||||
staff_grid = TileGrid(
|
||||
staff_bitmap,
|
||||
pixel_shader=staff_palette,
|
||||
x=0,
|
||||
y=self.STAFF_Y_START
|
||||
)
|
||||
staff_group.append(staff_grid)
|
||||
|
||||
return staff_group
|
||||
|
||||
def create_grid_lines(self):
|
||||
"""Add vertical grid lines to show note spacing"""
|
||||
grid_bitmap = Bitmap(self.SCREEN_WIDTH, self.STAFF_HEIGHT, 2)
|
||||
grid_palette = Palette(2)
|
||||
grid_palette[0] = 0x657c95 # Transparent
|
||||
grid_palette[1] = 0xAAAAAA # Faint grid lines (light gray)
|
||||
|
||||
# Draw vertical grid lines at each eighth note position
|
||||
for x_pos in self.x_positions:
|
||||
for y in range(self.STAFF_HEIGHT):
|
||||
if self.LINE_SPACING <= y <= 5 * self.LINE_SPACING:
|
||||
grid_bitmap[x_pos, y] = 1
|
||||
|
||||
return TileGrid(grid_bitmap, pixel_shader=grid_palette, x=0, y=self.STAFF_Y_START)
|
||||
|
||||
def create_playhead(self):
|
||||
"""Create a playhead indicator"""
|
||||
playhead_bitmap = Bitmap(2, self.STAFF_HEIGHT, 2)
|
||||
playhead_palette = Palette(2)
|
||||
playhead_palette[0] = 0x657c95 # Transparent
|
||||
playhead_palette[1] = 0xFF0000 # Red playhead line
|
||||
|
||||
for y in range(self.STAFF_HEIGHT):
|
||||
playhead_bitmap[0, y] = 1
|
||||
playhead_bitmap[1, y] = 1
|
||||
|
||||
self.playhead = TileGrid(
|
||||
playhead_bitmap,
|
||||
pixel_shader=playhead_palette,
|
||||
x=0,
|
||||
y=self.STAFF_Y_START
|
||||
)
|
||||
self.playhead.x = -10 # Start off-screen
|
||||
|
||||
return self.playhead
|
||||
|
||||
def create_highlight(self):
|
||||
"""Create a highlight marker for the closest valid note position"""
|
||||
highlight_bitmap = Bitmap(self.SCREEN_WIDTH, 3, 2)
|
||||
highlight_palette = Palette(2)
|
||||
highlight_palette[0] = 0x657c95 # Transparent
|
||||
highlight_palette[1] = 0x007700 # Highlight color (green)
|
||||
|
||||
for x in range(self.SCREEN_WIDTH):
|
||||
highlight_bitmap[x, 1] = 1
|
||||
|
||||
self.highlight_grid = TileGrid(highlight_bitmap, pixel_shader=highlight_palette)
|
||||
self.highlight_grid.y = self.note_manager.note_positions[0] # Start at first position
|
||||
|
||||
return self.highlight_grid
|
||||
644
Fruit_Jam/Larsio_Paint_Music/ui_manager.py
Executable file
|
|
@ -0,0 +1,644 @@
|
|||
# SPDX-FileCopyrightText: 2025 John Park and Claude AI for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""
|
||||
# ui_manager.py: CircuitPython Music Staff Application component
|
||||
"""
|
||||
|
||||
import time
|
||||
import gc
|
||||
|
||||
# pylint: disable=import-error, trailing-whitespace, line-too-long, superfluous-parens
|
||||
from adafruit_display_text.bitmap_label import Label
|
||||
import terminalio
|
||||
from displayio import TileGrid
|
||||
|
||||
from display_manager import DisplayManager
|
||||
from staff_view import StaffView
|
||||
from control_panel import ControlPanel
|
||||
from input_handler import InputHandler
|
||||
from sprite_manager import SpriteManager
|
||||
from cursor_manager import CursorManager
|
||||
from playback_controller import PlaybackController
|
||||
|
||||
|
||||
# pylint: disable=invalid-name,no-member,too-many-instance-attributes,too-many-arguments
|
||||
# pylint: disable=too-many-branches,too-many-statements,too-many-public-methods
|
||||
# pylint: disable=too-many-locals,attribute-defined-outside-init
|
||||
# pylint: disable=consider-using-in,too-many-return-statements,no-else-return
|
||||
class UIManager:
|
||||
"""Manages the UI elements, input, and user interaction"""
|
||||
|
||||
def __init__(self, sound_manager, note_manager):
|
||||
"""Initialize the UI manager with sound and note managers"""
|
||||
self.sound_manager = sound_manager
|
||||
self.note_manager = note_manager
|
||||
|
||||
# Screen dimensions
|
||||
self.SCREEN_WIDTH = 320
|
||||
self.SCREEN_HEIGHT = 240
|
||||
|
||||
# Staff dimensions
|
||||
self.TOP_MARGIN = int(self.SCREEN_HEIGHT * 0.1)
|
||||
self.BOTTOM_MARGIN = int(self.SCREEN_HEIGHT * 0.2)
|
||||
self.STAFF_HEIGHT = int((self.SCREEN_HEIGHT - self.TOP_MARGIN - self.BOTTOM_MARGIN) * 0.95)
|
||||
self.STAFF_Y_START = self.TOP_MARGIN
|
||||
self.LINE_SPACING = self.STAFF_HEIGHT // 8
|
||||
|
||||
# Start margin
|
||||
self.START_MARGIN = 25
|
||||
|
||||
# Tempo and timing
|
||||
self.BPM = 120
|
||||
self.SECONDS_PER_BEAT = 60 / self.BPM
|
||||
self.SECONDS_PER_EIGHTH = self.SECONDS_PER_BEAT / 2
|
||||
|
||||
# Initialize components
|
||||
self.display_manager = DisplayManager(self.SCREEN_WIDTH, self.SCREEN_HEIGHT)
|
||||
self.staff_view = StaffView(self.SCREEN_WIDTH, self.SCREEN_HEIGHT, self.note_manager)
|
||||
self.control_panel = ControlPanel(self.SCREEN_WIDTH, self.SCREEN_HEIGHT)
|
||||
self.input_handler = InputHandler(self.SCREEN_WIDTH, self.SCREEN_HEIGHT,
|
||||
self.STAFF_Y_START, self.STAFF_HEIGHT)
|
||||
self.sprite_manager = SpriteManager()
|
||||
self.cursor_manager = CursorManager()
|
||||
self.playback_controller = PlaybackController(self.sound_manager, self.note_manager,
|
||||
self.SECONDS_PER_EIGHTH)
|
||||
|
||||
# UI elements
|
||||
self.main_group = None
|
||||
self.note_name_label = None
|
||||
self.tempo_label = None
|
||||
self.preview_tg = None
|
||||
self.highlight_grid = None
|
||||
self.playhead = None
|
||||
self.channel_buttons = []
|
||||
self.channel_selector = None
|
||||
|
||||
# Initialize attributes that will be defined later
|
||||
self.display = None
|
||||
self.play_button = None
|
||||
self.stop_button = None
|
||||
self.loop_button = None
|
||||
self.clear_button = None
|
||||
self.crosshair_cursor = None
|
||||
self.triangle_cursor = None
|
||||
self.tempo_minus_label = None
|
||||
self.tempo_plus_label = None
|
||||
|
||||
# Channel setting
|
||||
self.current_channel = 0
|
||||
|
||||
def setup_display(self):
|
||||
"""Initialize the display and create visual elements"""
|
||||
# Initialize display
|
||||
self.main_group, self.display = self.display_manager.initialize_display()
|
||||
|
||||
# Create background
|
||||
bg_grid = self.display_manager.create_background()
|
||||
self.main_group.append(bg_grid)
|
||||
|
||||
# Create staff
|
||||
staff_group = self.staff_view.create_staff()
|
||||
self.main_group.append(staff_group)
|
||||
|
||||
# Create grid lines
|
||||
grid_tg = self.staff_view.create_grid_lines()
|
||||
self.main_group.insert(1, grid_tg) # Insert before staff so it appears behind
|
||||
|
||||
# Create channel buttons using sprites
|
||||
self._create_sprite_channel_buttons()
|
||||
|
||||
# Create transport controls
|
||||
transport_group, self.play_button, self.stop_button, self.loop_button, self.clear_button = \
|
||||
self.control_panel.create_transport_controls(self.sprite_manager)
|
||||
self.main_group.append(transport_group)
|
||||
|
||||
# Create cursors
|
||||
self.crosshair_cursor, self.triangle_cursor = self.cursor_manager.create_cursors()
|
||||
self.main_group.append(self.crosshair_cursor)
|
||||
self.main_group.append(self.triangle_cursor)
|
||||
|
||||
# Create note name label
|
||||
self._create_note_name_label()
|
||||
|
||||
# Create tempo display
|
||||
self._create_tempo_display()
|
||||
|
||||
# Create highlight
|
||||
self.highlight_grid = self.staff_view.create_highlight()
|
||||
self.main_group.append(self.highlight_grid)
|
||||
|
||||
# Create playhead
|
||||
self.playhead = self.staff_view.create_playhead()
|
||||
self.main_group.append(self.playhead)
|
||||
|
||||
# Set playback controller elements
|
||||
self.playback_controller.set_ui_elements(
|
||||
self.playhead,
|
||||
self.play_button,
|
||||
self.stop_button,
|
||||
self.control_panel.button_sprites
|
||||
)
|
||||
|
||||
# Create preview note
|
||||
self.preview_tg = self.sprite_manager.create_preview_note(
|
||||
self.current_channel, self.note_manager.note_bitmap)
|
||||
self.main_group.append(self.preview_tg)
|
||||
|
||||
# Add note groups to main group
|
||||
self.main_group.append(self.note_manager.notes_group)
|
||||
self.main_group.append(self.note_manager.ledger_lines_group)
|
||||
|
||||
def _create_sprite_channel_buttons(self):
|
||||
"""Create channel buttons using sprites instead of numbered boxes"""
|
||||
# Get a reference to the channel selector from control panel
|
||||
channel_group, self.channel_selector = self.control_panel.create_channel_buttons()
|
||||
|
||||
# Add sprite-based channel buttons
|
||||
button_sprites = [
|
||||
(self.sprite_manager.mario_head, self.sprite_manager.mario_palette),
|
||||
(self.sprite_manager.heart_note, self.sprite_manager.heart_palette),
|
||||
(self.sprite_manager.drum_note, self.sprite_manager.drum_palette),
|
||||
(self.sprite_manager.meatball_note, self.sprite_manager.meatball_palette),
|
||||
(self.sprite_manager.star_note, self.sprite_manager.star_palette),
|
||||
(self.sprite_manager.bot_note, self.sprite_manager.bot_palette)
|
||||
]
|
||||
|
||||
# Create and position the sprite buttons
|
||||
self.channel_buttons = []
|
||||
|
||||
for i, (sprite, palette) in enumerate(button_sprites):
|
||||
button_x = 10 + i * (self.control_panel.CHANNEL_BUTTON_SIZE +
|
||||
self.control_panel.CHANNEL_BUTTON_SPACING)
|
||||
|
||||
# Create TileGrid for the sprite
|
||||
button_tg = TileGrid(
|
||||
sprite,
|
||||
pixel_shader=palette,
|
||||
x=button_x,
|
||||
y=self.control_panel.CHANNEL_BUTTON_Y
|
||||
)
|
||||
|
||||
# Center the sprite if it's not exactly the button size
|
||||
if sprite.width != self.control_panel.CHANNEL_BUTTON_SIZE:
|
||||
offset_x = (self.control_panel.CHANNEL_BUTTON_SIZE - sprite.width) // 2
|
||||
button_tg.x += offset_x
|
||||
|
||||
if sprite.height != self.control_panel.CHANNEL_BUTTON_SIZE:
|
||||
offset_y = (self.control_panel.CHANNEL_BUTTON_SIZE - sprite.height) // 2
|
||||
button_tg.y += offset_y
|
||||
|
||||
self.channel_buttons.append(button_tg)
|
||||
channel_group.append(button_tg)
|
||||
|
||||
# Add the channel_group to main_group
|
||||
self.main_group.append(channel_group)
|
||||
|
||||
def _create_note_name_label(self):
|
||||
"""Create a label to show the current note name"""
|
||||
self.note_name_label = Label(
|
||||
terminalio.FONT,
|
||||
text="",
|
||||
color=0x000000, # Black text for beige background
|
||||
scale=1
|
||||
)
|
||||
self.note_name_label.anchor_point = (0, 0)
|
||||
self.note_name_label.anchored_position = (10, self.SCREEN_HEIGHT - 70)
|
||||
self.main_group.append(self.note_name_label)
|
||||
|
||||
def _create_tempo_display(self):
|
||||
"""Create a label for the tempo display with + and - buttons"""
|
||||
gc.collect() # Force garbage collection before creating the label
|
||||
|
||||
# Create plus and minus buttons for tempo adjustment
|
||||
self.tempo_minus_label = Label(
|
||||
terminalio.FONT,
|
||||
text="-",
|
||||
color=0xaaaaaa, # White text
|
||||
background_color=0x444444, # Dark gray background
|
||||
scale=1
|
||||
)
|
||||
self.tempo_minus_label.anchor_point = (0.5, 0.5)
|
||||
self.tempo_minus_label.anchored_position = (self.SCREEN_WIDTH - 24, 10)
|
||||
self.main_group.append(self.tempo_minus_label)
|
||||
|
||||
self.tempo_plus_label = Label(
|
||||
terminalio.FONT,
|
||||
text="+",
|
||||
color=0xaaaaaa, # gray text
|
||||
background_color=0x444444, # Dark gray background
|
||||
scale=1
|
||||
)
|
||||
self.tempo_plus_label.anchor_point = (0.5, 0.5)
|
||||
self.tempo_plus_label.anchored_position = (self.SCREEN_WIDTH - 7, 10)
|
||||
self.main_group.append(self.tempo_plus_label)
|
||||
|
||||
# Create the tempo display label
|
||||
self.tempo_label = Label(
|
||||
terminalio.FONT,
|
||||
text=f"Tempo~ {self.BPM} BPM",
|
||||
color=0x222222, # gray text
|
||||
scale=1
|
||||
)
|
||||
self.tempo_label.anchor_point = (0, 0.5)
|
||||
self.tempo_label.anchored_position = (self.SCREEN_WIDTH - 114, 10)
|
||||
self.main_group.append(self.tempo_label)
|
||||
|
||||
print(f"Created tempo display: {self.tempo_label.text}")
|
||||
|
||||
def find_mouse(self):
|
||||
"""Find the mouse device"""
|
||||
return self.input_handler.find_mouse()
|
||||
|
||||
def change_channel(self, channel_idx):
|
||||
"""Change the current MIDI channel"""
|
||||
if 0 <= channel_idx < 6: # Ensure valid channel index
|
||||
self.current_channel = channel_idx
|
||||
|
||||
# Update channel selector position
|
||||
channel_offset = (self.control_panel.CHANNEL_BUTTON_SIZE +
|
||||
self.control_panel.CHANNEL_BUTTON_SPACING)
|
||||
self.channel_selector.x = 7 + channel_idx * channel_offset
|
||||
|
||||
# Update preview note color/image based on channel
|
||||
self.main_group.remove(self.preview_tg)
|
||||
self.preview_tg = self.sprite_manager.create_preview_note(
|
||||
self.current_channel, self.note_manager.note_bitmap)
|
||||
self.main_group.append(self.preview_tg)
|
||||
|
||||
# Update status text
|
||||
channel_names = ["Lars", "Heart", "Drum", "Meatball", "Star", "Bot"]
|
||||
channel_text = f"Channel {self.current_channel + 1}: {channel_names[self.current_channel]}"
|
||||
self.note_name_label.text = f"{channel_text} selected"
|
||||
|
||||
print(f"Changed to MIDI channel {self.current_channel + 1}")
|
||||
|
||||
def toggle_loop(self):
|
||||
"""Toggle loop button state"""
|
||||
self.playback_controller.loop_enabled = not self.playback_controller.loop_enabled
|
||||
self.control_panel.loop_enabled = self.playback_controller.loop_enabled
|
||||
|
||||
# Update loop button appearance using bitmap if button_sprites are available
|
||||
if hasattr(self.control_panel, 'button_sprites') and self.control_panel.button_sprites is not None:
|
||||
state = 'down' if self.playback_controller.loop_enabled else 'up'
|
||||
loop_bitmap, loop_palette = self.control_panel.button_sprites['loop'][state]
|
||||
self.loop_button.bitmap = loop_bitmap
|
||||
self.loop_button.pixel_shader = loop_palette
|
||||
else:
|
||||
# Fallback to original implementation
|
||||
for x in range(1, self.control_panel.BUTTON_WIDTH - 1):
|
||||
for y in range(1, self.control_panel.BUTTON_HEIGHT - 1):
|
||||
skip_corners = (x, y) in [
|
||||
(0, 0),
|
||||
(0, self.control_panel.BUTTON_HEIGHT-1),
|
||||
(self.control_panel.BUTTON_WIDTH-1, 0),
|
||||
(self.control_panel.BUTTON_WIDTH-1, self.control_panel.BUTTON_HEIGHT-1)
|
||||
]
|
||||
|
||||
if not skip_corners:
|
||||
# Skip pixels that are part of the loop symbol
|
||||
dx = x - self.control_panel.BUTTON_WIDTH // 2
|
||||
dy = y - self.control_panel.BUTTON_HEIGHT // 2
|
||||
# Is pixel on the circle outline?
|
||||
is_on_circle = (self.control_panel.loop_radius - 1 <=
|
||||
(dx*dx + dy*dy)**0.5 <=
|
||||
self.control_panel.loop_radius + 1)
|
||||
|
||||
# Calculate arrow point positions
|
||||
arrow_y1 = (self.control_panel.BUTTON_HEIGHT // 2 -
|
||||
self.control_panel.loop_radius - 1)
|
||||
arrow_y2 = arrow_y1 + 2
|
||||
|
||||
# Is pixel part of the arrow?
|
||||
arrow_x = (self.control_panel.BUTTON_WIDTH // 2 +
|
||||
int(self.control_panel.loop_radius * 0.7))
|
||||
is_arrow = x == arrow_x and (y == arrow_y1 or y == arrow_y2)
|
||||
|
||||
if not (is_on_circle or is_arrow):
|
||||
# Fill with active color if loop enabled, else inactive
|
||||
val = 2 if self.playback_controller.loop_enabled else 0
|
||||
self.control_panel.loop_button_bitmap[x, y] = val
|
||||
|
||||
self.note_name_label.text = "Loop: " + ("ON" if self.playback_controller.loop_enabled else "OFF")
|
||||
|
||||
def press_clear_button(self):
|
||||
"""Handle clear button pressing effect"""
|
||||
# Show pressed state
|
||||
if hasattr(self.control_panel, 'button_sprites') and self.control_panel.button_sprites is not None:
|
||||
self.clear_button.bitmap = self.control_panel.button_sprites['clear']['down'][0]
|
||||
self.clear_button.pixel_shader = self.control_panel.button_sprites['clear']['down'][1]
|
||||
else:
|
||||
# Fallback to original implementation
|
||||
for x in range(1, self.control_panel.BUTTON_WIDTH - 1):
|
||||
for y in range(1, self.control_panel.BUTTON_HEIGHT - 1):
|
||||
self.control_panel.clear_button_bitmap[x, y] = 2 # Red
|
||||
|
||||
# Small delay for visual feedback
|
||||
time.sleep(0.1)
|
||||
|
||||
# Return to up state
|
||||
if hasattr(self.control_panel, 'button_sprites') and self.control_panel.button_sprites is not None:
|
||||
self.clear_button.bitmap = self.control_panel.button_sprites['clear']['up'][0]
|
||||
self.clear_button.pixel_shader = self.control_panel.button_sprites['clear']['up'][1]
|
||||
else:
|
||||
# Fallback to original implementation
|
||||
for x in range(1, self.control_panel.BUTTON_WIDTH - 1):
|
||||
for y in range(1, self.control_panel.BUTTON_HEIGHT - 1):
|
||||
self.control_panel.clear_button_bitmap[x, y] = 0 # Gray
|
||||
|
||||
def clear_all_notes(self):
|
||||
"""Clear all notes"""
|
||||
# Stop playback if it's running
|
||||
if self.playback_controller.is_playing:
|
||||
self.playback_controller.stop_playback()
|
||||
|
||||
# Visual feedback for button press
|
||||
self.press_clear_button()
|
||||
|
||||
# Clear notes using note manager
|
||||
self.note_manager.clear_all_notes(self.sound_manager)
|
||||
|
||||
self.note_name_label.text = "All notes cleared"
|
||||
|
||||
def adjust_tempo(self, direction):
|
||||
"""Adjust the tempo based on button press"""
|
||||
# direction should be +1 for increase, -1 for decrease
|
||||
|
||||
# Adjust BPM
|
||||
new_bpm = self.BPM + (direction * 5) # Change by 5 BPM increments
|
||||
|
||||
# Constrain to valid range
|
||||
new_bpm = max(40, min(280, new_bpm))
|
||||
|
||||
# Only update if changed
|
||||
if new_bpm != self.BPM:
|
||||
self.BPM = new_bpm
|
||||
self.SECONDS_PER_BEAT = 60 / self.BPM
|
||||
self.SECONDS_PER_EIGHTH = self.SECONDS_PER_BEAT / 2
|
||||
|
||||
# Update playback controller with new tempo
|
||||
self.playback_controller.set_tempo(self.SECONDS_PER_EIGHTH)
|
||||
|
||||
# Update display
|
||||
self.tempo_label.text = f"Tempo~ {self.BPM} BPM"
|
||||
|
||||
print(f"Tempo adjusted to {self.BPM} BPM")
|
||||
|
||||
def handle_mouse_position(self):
|
||||
"""Handle mouse movement and cursor updates"""
|
||||
mouse_x = self.input_handler.mouse_x
|
||||
mouse_y = self.input_handler.mouse_y
|
||||
|
||||
# Check if mouse is over channel buttons area
|
||||
is_over_channel_buttons = (
|
||||
self.control_panel.CHANNEL_BUTTON_Y <= mouse_y <=
|
||||
self.control_panel.CHANNEL_BUTTON_Y + self.control_panel.CHANNEL_BUTTON_SIZE
|
||||
)
|
||||
|
||||
# Check if we're over the staff area or transport controls area
|
||||
is_over_staff = self.input_handler.is_over_staff(mouse_y)
|
||||
is_over_transport = (mouse_y >= self.control_panel.TRANSPORT_AREA_Y)
|
||||
|
||||
# Switch cursor based on area
|
||||
self.cursor_manager.switch_cursor(use_triangle=(is_over_transport or is_over_channel_buttons))
|
||||
self.cursor_manager.set_cursor_position(mouse_x, mouse_y)
|
||||
|
||||
# Handle staff area differently from other areas
|
||||
if not is_over_staff:
|
||||
# Hide highlight and preview when not over staff
|
||||
self.highlight_grid.hidden = True
|
||||
self.preview_tg.hidden = True
|
||||
|
||||
# Show channel info if over channel buttons
|
||||
if is_over_channel_buttons:
|
||||
self._update_channel_button_info(mouse_x, mouse_y)
|
||||
return
|
||||
|
||||
# Process staff area interactions
|
||||
# Find closest position and update highlight
|
||||
closest_pos = self.note_manager.find_closest_position(mouse_y)
|
||||
y_position = self.note_manager.note_positions[closest_pos]
|
||||
self.highlight_grid.y = y_position - 1 # Center the highlight
|
||||
self.highlight_grid.hidden = False
|
||||
|
||||
# Find closest horizontal position (enforce minimum x position)
|
||||
x_position = self.note_manager.find_closest_x_position(mouse_x)
|
||||
|
||||
# Define sprite dimensions for each channel
|
||||
sprite_width, sprite_height = self._get_sprite_dimensions(self.current_channel)
|
||||
|
||||
# Update preview note position
|
||||
self.preview_tg.x = x_position - sprite_width // 2
|
||||
self.preview_tg.y = y_position - sprite_height // 2
|
||||
self.preview_tg.hidden = False
|
||||
|
||||
# Update note name label
|
||||
if x_position < self.START_MARGIN:
|
||||
self.note_name_label.text = "Invalid position - after double bar only"
|
||||
else:
|
||||
channel_names = ["Lars", "Heart", "Drum", "Meatball", "Star", "Bot"]
|
||||
channel_text = f"Ch{self.current_channel+1} ({channel_names[self.current_channel]})"
|
||||
note_text = self.note_manager.note_names[closest_pos]
|
||||
self.note_name_label.text = f"{channel_text}: {note_text}"
|
||||
|
||||
def _update_channel_button_info(self, mouse_x, mouse_y):
|
||||
"""Update the note name label based on which channel button the mouse is over"""
|
||||
# Calculate which channel button we're over (if any)
|
||||
for i in range(6):
|
||||
button_x = 10 + i * (self.control_panel.CHANNEL_BUTTON_SIZE +
|
||||
self.control_panel.CHANNEL_BUTTON_SPACING)
|
||||
|
||||
# Get sprite dimensions for hit testing
|
||||
sprite_width, sprite_height = self._get_sprite_dimensions(i)
|
||||
|
||||
# Calculate the centered position of the sprite
|
||||
offset_x = (self.control_panel.CHANNEL_BUTTON_SIZE - sprite_width) // 2
|
||||
offset_y = (self.control_panel.CHANNEL_BUTTON_SIZE - sprite_height) // 2
|
||||
sprite_x = button_x + offset_x
|
||||
sprite_y = self.control_panel.CHANNEL_BUTTON_Y + offset_y
|
||||
|
||||
# Check if mouse is over the sprite
|
||||
rect_check = self.input_handler.point_in_rect(
|
||||
mouse_x, mouse_y, sprite_x, sprite_y,
|
||||
sprite_width, sprite_height)
|
||||
|
||||
if rect_check:
|
||||
channel_names = ["Lars", "Heart", "Drum", "Meatball", "Star", "Bot"]
|
||||
self.note_name_label.text = f"Channel {i+1}: {channel_names[i]}"
|
||||
break
|
||||
|
||||
def _get_sprite_dimensions(self, channel_idx):
|
||||
"""Get the width and height of a sprite based on channel index"""
|
||||
if channel_idx == 0:
|
||||
return self.sprite_manager.mario_head.width, self.sprite_manager.mario_head.height
|
||||
if channel_idx == 1:
|
||||
return self.sprite_manager.heart_note.width, self.sprite_manager.heart_note.height
|
||||
if channel_idx == 2:
|
||||
return self.sprite_manager.drum_note.width, self.sprite_manager.drum_note.height
|
||||
if channel_idx == 3:
|
||||
return self.sprite_manager.meatball_note.width, self.sprite_manager.meatball_note.height
|
||||
if channel_idx == 4:
|
||||
return self.sprite_manager.star_note.width, self.sprite_manager.star_note.height
|
||||
if channel_idx == 5:
|
||||
return self.sprite_manager.bot_note.width, self.sprite_manager.bot_note.height
|
||||
# Default fallback if channel_idx is out of range
|
||||
return self.note_manager.NOTE_WIDTH, self.note_manager.NOTE_HEIGHT
|
||||
|
||||
def handle_mouse_buttons(self):
|
||||
"""Handle mouse button presses"""
|
||||
mouse_x = self.input_handler.mouse_x
|
||||
mouse_y = self.input_handler.mouse_y
|
||||
|
||||
# Check for staff area
|
||||
is_over_staff = self.input_handler.is_over_staff(mouse_y)
|
||||
|
||||
if self.input_handler.left_button_pressed:
|
||||
# Check for tempo button clicks
|
||||
minus_button_x, minus_button_y = self.tempo_minus_label.anchored_position
|
||||
plus_button_x, plus_button_y = self.tempo_plus_label.anchored_position
|
||||
button_radius = 8 # Allow a bit of space around the button for easier clicking
|
||||
|
||||
if ((mouse_x - minus_button_x)**2 + (mouse_y - minus_button_y)**2) < button_radius**2:
|
||||
# Clicked minus button - decrease tempo
|
||||
self.adjust_tempo(-1)
|
||||
return
|
||||
|
||||
if ((mouse_x - plus_button_x)**2 + (mouse_y - plus_button_y)**2) < button_radius**2:
|
||||
# Clicked plus button - increase tempo
|
||||
self.adjust_tempo(1)
|
||||
return
|
||||
|
||||
# Check if a channel button was clicked
|
||||
channel_clicked = False
|
||||
for i in range(6):
|
||||
button_x = 10 + i * (self.control_panel.CHANNEL_BUTTON_SIZE +
|
||||
self.control_panel.CHANNEL_BUTTON_SPACING)
|
||||
|
||||
# Get sprite dimensions for hit testing
|
||||
sprite_width, sprite_height = self._get_sprite_dimensions(i)
|
||||
|
||||
# Calculate the centered position of the sprite
|
||||
offset_x = (self.control_panel.CHANNEL_BUTTON_SIZE - sprite_width) // 2
|
||||
offset_y = (self.control_panel.CHANNEL_BUTTON_SIZE - sprite_height) // 2
|
||||
sprite_x = button_x + offset_x
|
||||
sprite_y = self.control_panel.CHANNEL_BUTTON_Y + offset_y
|
||||
|
||||
# Check if click is within the sprite area
|
||||
if self.input_handler.point_in_rect(
|
||||
mouse_x, mouse_y, sprite_x, sprite_y,
|
||||
sprite_width, sprite_height):
|
||||
self.change_channel(i)
|
||||
channel_clicked = True
|
||||
break
|
||||
|
||||
if not channel_clicked:
|
||||
# Handle play/stop button clicks
|
||||
if self.input_handler.point_in_rect(
|
||||
mouse_x, mouse_y, self.play_button.x, self.play_button.y,
|
||||
self.control_panel.BUTTON_WIDTH, self.control_panel.BUTTON_HEIGHT):
|
||||
if not self.playback_controller.is_playing:
|
||||
self.playback_controller.start_playback(self.START_MARGIN)
|
||||
else:
|
||||
self.playback_controller.stop_playback()
|
||||
elif self.input_handler.point_in_rect(
|
||||
mouse_x, mouse_y, self.stop_button.x, self.stop_button.y,
|
||||
self.control_panel.BUTTON_WIDTH, self.control_panel.BUTTON_HEIGHT):
|
||||
self.playback_controller.stop_playback()
|
||||
elif self.input_handler.point_in_rect(
|
||||
mouse_x, mouse_y, self.loop_button.x, self.loop_button.y,
|
||||
self.control_panel.BUTTON_WIDTH, self.control_panel.BUTTON_HEIGHT):
|
||||
self.toggle_loop()
|
||||
elif self.input_handler.point_in_rect(
|
||||
mouse_x, mouse_y, self.clear_button.x, self.clear_button.y,
|
||||
self.control_panel.BUTTON_WIDTH, self.control_panel.BUTTON_HEIGHT):
|
||||
self.clear_all_notes()
|
||||
# Handle staff area clicks - left button adds notes only
|
||||
elif is_over_staff:
|
||||
self._add_note_based_on_channel(mouse_x, mouse_y)
|
||||
|
||||
# Handle right mouse button for note deletion
|
||||
elif self.input_handler.right_button_pressed and is_over_staff:
|
||||
_, message = self.note_manager.erase_note(
|
||||
mouse_x, mouse_y,
|
||||
self.sprite_manager.mario_head, self.sprite_manager.mario_palette,
|
||||
self.sound_manager
|
||||
)
|
||||
self.note_name_label.text = message
|
||||
|
||||
def _add_note_based_on_channel(self, x, y):
|
||||
"""Add a note based on the current channel"""
|
||||
if self.current_channel == 0:
|
||||
_, message = self.note_manager.add_note(
|
||||
x, y, self.current_channel,
|
||||
self.sprite_manager.note_palettes,
|
||||
self.sprite_manager.mario_head, self.sprite_manager.mario_palette,
|
||||
self.sprite_manager.heart_note, self.sprite_manager.heart_palette,
|
||||
self.sound_manager
|
||||
)
|
||||
elif self.current_channel == 1:
|
||||
_, message = self.note_manager.add_note(
|
||||
x, y, self.current_channel,
|
||||
self.sprite_manager.note_palettes,
|
||||
self.sprite_manager.heart_note, self.sprite_manager.heart_palette,
|
||||
self.sprite_manager.heart_note, self.sprite_manager.heart_palette,
|
||||
self.sound_manager
|
||||
)
|
||||
elif self.current_channel == 2:
|
||||
_, message = self.note_manager.add_note(
|
||||
x, y, self.current_channel,
|
||||
self.sprite_manager.note_palettes,
|
||||
self.sprite_manager.drum_note, self.sprite_manager.drum_palette,
|
||||
self.sprite_manager.heart_note, self.sprite_manager.heart_palette,
|
||||
self.sound_manager
|
||||
)
|
||||
elif self.current_channel == 3:
|
||||
_, message = self.note_manager.add_note(
|
||||
x, y, self.current_channel,
|
||||
self.sprite_manager.note_palettes,
|
||||
self.sprite_manager.meatball_note, self.sprite_manager.meatball_palette,
|
||||
self.sprite_manager.heart_note, self.sprite_manager.heart_palette,
|
||||
self.sound_manager
|
||||
)
|
||||
elif self.current_channel == 4:
|
||||
_, message = self.note_manager.add_note(
|
||||
x, y, self.current_channel,
|
||||
self.sprite_manager.note_palettes,
|
||||
self.sprite_manager.star_note, self.sprite_manager.star_palette,
|
||||
self.sprite_manager.heart_note, self.sprite_manager.heart_palette,
|
||||
self.sound_manager
|
||||
)
|
||||
elif self.current_channel == 5:
|
||||
_, message = self.note_manager.add_note(
|
||||
x, y, self.current_channel,
|
||||
self.sprite_manager.note_palettes,
|
||||
self.sprite_manager.bot_note, self.sprite_manager.bot_palette,
|
||||
self.sprite_manager.heart_note, self.sprite_manager.heart_palette,
|
||||
self.sound_manager
|
||||
)
|
||||
else:
|
||||
_, message = self.note_manager.add_note(
|
||||
x, y, self.current_channel,
|
||||
self.sprite_manager.note_palettes,
|
||||
self.sprite_manager.mario_head, self.sprite_manager.mario_palette,
|
||||
self.sprite_manager.heart_note, self.sprite_manager.heart_palette,
|
||||
self.sound_manager
|
||||
)
|
||||
self.note_name_label.text = message
|
||||
|
||||
def main_loop(self):
|
||||
"""Main application loop"""
|
||||
while True:
|
||||
# Update playback if active
|
||||
if self.playback_controller.is_playing:
|
||||
self.playback_controller.update_playback(self.staff_view.x_positions)
|
||||
|
||||
# Update sound manager for timed releases
|
||||
self.sound_manager.update()
|
||||
|
||||
# Process mouse input - simplified version without wheel tracking
|
||||
if self.input_handler.process_mouse_input():
|
||||
# Handle mouse position and update cursor
|
||||
self.handle_mouse_position()
|
||||
|
||||
# Handle mouse button presses
|
||||
self.handle_mouse_buttons()
|
||||
|
|
@ -14,9 +14,12 @@ import displayio
|
|||
import supervisor
|
||||
from displayio import Group, TileGrid
|
||||
from tilepalettemapper import TilePaletteMapper
|
||||
from adafruit_fruitjam.peripherals import request_display_config
|
||||
import adafruit_imageload
|
||||
|
||||
|
||||
# use the built-in HSTX display
|
||||
request_display_config(320, 240)
|
||||
display = supervisor.runtime.display
|
||||
|
||||
# screen size in tiles, tiles are 16x16
|
||||
|
|
@ -64,7 +67,12 @@ for i in range(0, len(COLORS)):
|
|||
shader_palette[i + 1] = COLORS[i]
|
||||
|
||||
# mapper to change colors of tiles within the grid
|
||||
grid_color_shader = TilePaletteMapper(shader_palette, 2, SCREEN_WIDTH, SCREEN_HEIGHT)
|
||||
if sys.implementation.version[0] == 9:
|
||||
grid_color_shader = TilePaletteMapper(
|
||||
shader_palette, 2, SCREEN_WIDTH, SCREEN_HEIGHT
|
||||
)
|
||||
elif sys.implementation.version[0] >= 10:
|
||||
grid_color_shader = TilePaletteMapper(shader_palette, 2)
|
||||
|
||||
# load the spritesheet
|
||||
katakana_bmp, katakana_pixelshader = adafruit_imageload.load("matrix_characters.bmp")
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import array
|
|||
import atexit
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import board
|
||||
|
|
@ -217,9 +218,11 @@ for i in range(2):
|
|||
|
||||
# create tile palette mappers
|
||||
for i in range(2):
|
||||
palette_mapper = TilePaletteMapper(remap_palette, 3, 1, 1)
|
||||
# remap index 2 to each of the colors in mouse colors list
|
||||
palette_mapper[0] = [0, 1, i + 3]
|
||||
if sys.implementation.version[0] == 9:
|
||||
palette_mapper = TilePaletteMapper(remap_palette, 3, 1, 1)
|
||||
elif sys.implementation.version[0] >= 10:
|
||||
palette_mapper = TilePaletteMapper(remap_palette, 3)
|
||||
|
||||
palette_mappers.append(palette_mapper)
|
||||
|
||||
# create tilegrid for each mouse
|
||||
|
|
@ -228,6 +231,9 @@ for i in range(2):
|
|||
mouse_tg.y = display.height // scale_factor // 2
|
||||
mouse_tgs.append(mouse_tg)
|
||||
|
||||
# remap index 2 to each of the colors in mouse colors list
|
||||
palette_mapper[0] = [0, 1, i + 3]
|
||||
|
||||
# USB info lists
|
||||
mouse_interface_indexes = []
|
||||
mouse_endpoint_addresses = []
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
# SPDX-License-Identifier: MIT
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
from io import BytesIO
|
||||
|
||||
|
|
@ -133,7 +134,10 @@ class Match3Card(Group):
|
|||
|
||||
def __init__(self, card_tuple, **kwargs):
|
||||
# tile palette mapper to color the card
|
||||
self._mapper = TilePaletteMapper(kwargs["pixel_shader"], 5, 1, 1)
|
||||
if sys.implementation.version[0] == 9:
|
||||
self._mapper = TilePaletteMapper(kwargs["pixel_shader"], 5, 1, 1)
|
||||
elif sys.implementation.version[0] >= 10:
|
||||
self._mapper = TilePaletteMapper(kwargs["pixel_shader"], 5)
|
||||
kwargs["pixel_shader"] = self._mapper
|
||||
# tile grid to for the visible sprite
|
||||
self._tilegrid = TileGrid(**kwargs)
|
||||
|
|
@ -580,9 +584,11 @@ class Match3Game(Group):
|
|||
# if 3 cards have been clicked
|
||||
if len(self.clicked_cards) == 3:
|
||||
# check if the 3 cards make a valid set
|
||||
valid_set = validate_set(self.clicked_cards[0],
|
||||
self.clicked_cards[1],
|
||||
self.clicked_cards[2])
|
||||
valid_set = validate_set(
|
||||
self.clicked_cards[0],
|
||||
self.clicked_cards[1],
|
||||
self.clicked_cards[2],
|
||||
)
|
||||
|
||||
# if they are a valid set
|
||||
if valid_set:
|
||||
|
|
@ -660,7 +666,7 @@ class Match3Game(Group):
|
|||
# load the game from the given game state
|
||||
self.load_from_game_state(self.game_state)
|
||||
# hide the title screen
|
||||
self.title_screen.hidden = True # pylint: disable=attribute-defined-outside-init
|
||||
self.title_screen.hidden = True
|
||||
# set the current state to open play
|
||||
self.cur_state = STATE_PLAYING_OPEN
|
||||
|
||||
|
|
@ -676,7 +682,7 @@ class Match3Game(Group):
|
|||
# initialize a new game
|
||||
self.init_new_game()
|
||||
# hide the title screen
|
||||
self.title_screen.hidden = True # pylint: disable=attribute-defined-outside-init
|
||||
self.title_screen.hidden = True
|
||||
# set the current state to open play
|
||||
self.cur_state = STATE_PLAYING_OPEN
|
||||
|
||||
|
|
@ -727,6 +733,7 @@ class Match3TitleScreen(Group):
|
|||
|
||||
def __init__(self, display_size):
|
||||
super().__init__()
|
||||
self.hidden = False
|
||||
self.display_size = display_size
|
||||
# background bitmap color
|
||||
bg_bmp = Bitmap(display_size[0] // 10, display_size[1] // 10, 1)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
import sys
|
||||
|
||||
import supervisor
|
||||
from displayio import Group, OnDiskBitmap, TileGrid
|
||||
|
|
@ -20,16 +21,28 @@ display.root_group = main_group
|
|||
spritesheet_bmp = OnDiskBitmap("match3_cards_spritesheet.bmp")
|
||||
|
||||
# create a TilePaletteMapper
|
||||
tile_palette_mapper = TilePaletteMapper(
|
||||
spritesheet_bmp.pixel_shader, # input pixel_shader
|
||||
5, # input color count
|
||||
3, # grid width
|
||||
1 # grid height
|
||||
)
|
||||
if sys.implementation.version[0] == 9:
|
||||
tile_palette_mapper = TilePaletteMapper(
|
||||
spritesheet_bmp.pixel_shader, # input pixel_shader
|
||||
5, # input color count
|
||||
3, # grid width
|
||||
1, # grid height
|
||||
)
|
||||
elif sys.implementation.version[0] >= 10:
|
||||
tile_palette_mapper = TilePaletteMapper(
|
||||
spritesheet_bmp.pixel_shader, # input pixel_shader
|
||||
5, # input color count
|
||||
)
|
||||
|
||||
# create a TileGrid to show some cards
|
||||
cards_tilegrid = TileGrid(spritesheet_bmp, pixel_shader=tile_palette_mapper,
|
||||
width=3, height=1, tile_width=24, tile_height=32)
|
||||
cards_tilegrid = TileGrid(
|
||||
spritesheet_bmp,
|
||||
pixel_shader=tile_palette_mapper,
|
||||
width=3,
|
||||
height=1,
|
||||
tile_width=24,
|
||||
tile_height=32,
|
||||
)
|
||||
|
||||
# set each tile in the grid to a different sprite index
|
||||
cards_tilegrid[0, 0] = 10
|
||||
|
|
|
|||
151
Mother_Of_All_Demos_Keyset/code.py
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
# SPDX-FileCopyrightText: Copyright (c) 2025 Liz Clark for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import time
|
||||
import board
|
||||
import keypad
|
||||
import supervisor
|
||||
import usb_hid
|
||||
from adafruit_hid.keyboard import Keyboard
|
||||
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
|
||||
|
||||
# Dictionary of macros for single keys and combinations
|
||||
macros = {
|
||||
# Single key macros
|
||||
(0,): "good",
|
||||
(1,): "great",
|
||||
(2,): "nice",
|
||||
(3,): "awesome",
|
||||
(4,): "cool",
|
||||
|
||||
# Combination macros
|
||||
(0, 2, 4): "looks good to me",
|
||||
(0, 2): "be right back",
|
||||
(2, 4): "see you soon",
|
||||
(1, 3): "sounds good",
|
||||
}
|
||||
|
||||
KEY_PINS = (
|
||||
board.A1,
|
||||
board.A2,
|
||||
board.A3,
|
||||
board.MISO,
|
||||
board.MOSI,
|
||||
)
|
||||
|
||||
keys = keypad.Keys(
|
||||
KEY_PINS,
|
||||
value_when_pressed=False,
|
||||
pull=True,
|
||||
interval=0.01,
|
||||
max_events=64,
|
||||
debounce_threshold=3
|
||||
)
|
||||
|
||||
keyboard = Keyboard(usb_hid.devices)
|
||||
keyboard_layout = KeyboardLayoutUS(keyboard)
|
||||
|
||||
# need to wait longer for 3 key combos
|
||||
LARGER_COMBOS = {
|
||||
(0, 2): (0, 2, 4),
|
||||
(2, 4): (0, 2, 4),
|
||||
}
|
||||
|
||||
# How long to wait for possible additional keys in a combo (ms)
|
||||
COMBO_WAIT_TIME = 150 # Wait 150ms to see if more keys are coming
|
||||
|
||||
# How long to wait for a single key before executing (ms)
|
||||
SINGLE_KEY_TIMEOUT_MS = 80
|
||||
|
||||
# Minimum time between macro executions (ms)
|
||||
MACRO_COOLDOWN_MS = 300
|
||||
|
||||
# Store the current state of all keys
|
||||
key_states = {i: False for i in range(len(KEY_PINS))}
|
||||
|
||||
# Create a reusable Event object to avoid memory allocations
|
||||
reusable_event = keypad.Event()
|
||||
|
||||
# Track timing and state
|
||||
last_macro_time = 0
|
||||
key_combo_start_time = 0
|
||||
waiting_for_combo = False
|
||||
last_executed_combo = None
|
||||
|
||||
while True:
|
||||
# Process all events in the queue
|
||||
keys_changed = False
|
||||
|
||||
while keys.events:
|
||||
if keys.events.get_into(reusable_event):
|
||||
# Check if key state actually changed
|
||||
old_state = key_states[reusable_event.key_number]
|
||||
key_states[reusable_event.key_number] = reusable_event.pressed
|
||||
|
||||
if old_state != reusable_event.pressed:
|
||||
print(f"Key {reusable_event.key_number} " +
|
||||
f"{'pressed' if reusable_event.pressed else 'released'}")
|
||||
keys_changed = True
|
||||
|
||||
# Get currently pressed keys as a sorted tuple
|
||||
current_pressed_keys = tuple(sorted(k for k, v in key_states.items() if v))
|
||||
current_time = supervisor.ticks_ms()
|
||||
|
||||
# When all keys are released, reset tracking
|
||||
if not current_pressed_keys:
|
||||
waiting_for_combo = False
|
||||
last_executed_combo = None
|
||||
time.sleep(0.01)
|
||||
continue
|
||||
|
||||
# If this is a new key pattern or we just started
|
||||
if keys_changed:
|
||||
# If we weren't tracking before, start now
|
||||
if not waiting_for_combo:
|
||||
key_combo_start_time = current_time
|
||||
waiting_for_combo = True
|
||||
|
||||
# If the pressed keys have changed, update the timer
|
||||
if current_pressed_keys != last_executed_combo:
|
||||
key_combo_start_time = current_time
|
||||
|
||||
# Skip if we've already executed this exact combination
|
||||
if current_pressed_keys == last_executed_combo:
|
||||
time.sleep(0.01)
|
||||
continue
|
||||
|
||||
# Determine if we should execute a macro now
|
||||
should_execute = False
|
||||
wait_more = False
|
||||
|
||||
# If this is a potential part of a larger combo, wait longer
|
||||
if current_pressed_keys in LARGER_COMBOS:
|
||||
# Only wait if we've been waiting less than the combo wait time
|
||||
if (current_time - key_combo_start_time) < COMBO_WAIT_TIME:
|
||||
wait_more = True
|
||||
else:
|
||||
# We've waited long enough, go ahead and execute
|
||||
should_execute = True
|
||||
# Immediate execution for multi-key combinations that aren't potential parts of larger combos
|
||||
elif len(current_pressed_keys) > 1:
|
||||
should_execute = True
|
||||
# Execute single key after timeout
|
||||
elif waiting_for_combo and (current_time - key_combo_start_time) >= SINGLE_KEY_TIMEOUT_MS:
|
||||
should_execute = True
|
||||
|
||||
# If we need to wait more, skip to the next iteration
|
||||
if wait_more:
|
||||
time.sleep(0.01)
|
||||
continue
|
||||
|
||||
# Execute the macro if conditions are met
|
||||
if should_execute and current_pressed_keys in macros:
|
||||
# Only execute if cooldown period has passed
|
||||
if current_time - last_macro_time >= MACRO_COOLDOWN_MS:
|
||||
print(f"MACRO: {macros[current_pressed_keys]}")
|
||||
keyboard_layout.write(macros[current_pressed_keys])
|
||||
last_macro_time = current_time
|
||||
last_executed_combo = current_pressed_keys
|
||||
|
||||
time.sleep(0.01)
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
// SPDX-FileCopyrightText: 2025 Limor Fried for Adafruit Industries
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "ESP_I2S.h"
|
||||
|
||||
// I2S pin definitions for Sparklemotion
|
||||
const uint8_t I2S_SCK = 14; // BCLK
|
||||
const uint8_t I2S_WS = 12; // LRCLK
|
||||
const uint8_t I2S_DIN = 13; // DATA_IN
|
||||
|
||||
// Create I2S instance
|
||||
I2SClass i2s;
|
||||
|
||||
void setup() {
|
||||
// Fast serial for plotting
|
||||
Serial.begin(500000);
|
||||
|
||||
// Initialize I2S
|
||||
i2s.setPins(I2S_SCK, I2S_WS, -1, I2S_DIN);
|
||||
if (!i2s.begin(I2S_MODE_STD, 44100, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO, I2S_STD_SLOT_LEFT)) {
|
||||
Serial.println("Failed to initialize I2S bus!");
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.println("I2S Mic Plotter Ready");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
static uint32_t lastPlot = 0;
|
||||
|
||||
// Get a sample
|
||||
int32_t sample = i2s.read();
|
||||
|
||||
// Only plot every 1ms (1000 samples/sec is plenty for visualization)
|
||||
if (millis() - lastPlot >= 1) {
|
||||
if (sample >= 0) { // Valid sample
|
||||
// Plot both raw and absolute values
|
||||
Serial.printf("%d,%d\n", (int16_t)sample, abs((int16_t)sample));
|
||||
}
|
||||
lastPlot = millis();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
// SPDX-FileCopyrightText: 2024 Liz Clark for Adafruit Industries
|
||||
// SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/*
|
||||
* Based on the SimpleReceiver.cpp and SimpleSender.cpp from the
|
||||
* Arduino-IRremote https://github.com/Arduino-IRremote/Arduino-IRremote.
|
||||
* by Armin Joachimsmeyer
|
||||
************************************************************************************
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-2023 Armin Joachimsmeyer
|
||||
*
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include <IRremote.hpp> // include the library
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
|
||||
#define NEOPIXEL_STRIP_PIN 21
|
||||
#define NUM_PIXELS 8
|
||||
|
||||
#define IR_RECEIVE_PIN 10
|
||||
|
||||
Adafruit_NeoPixel NEOPIXEL_STRIP(NUM_PIXELS, NEOPIXEL_STRIP_PIN, NEO_GRB + NEO_KHZ800);
|
||||
|
||||
uint8_t upCmd = 0x5;
|
||||
uint8_t downCmd = 0xD;
|
||||
uint8_t rightCmd = 0xA;
|
||||
uint8_t leftCmd = 0x8;
|
||||
|
||||
uint16_t pixelHue = 0;
|
||||
uint8_t brightness = 25;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
//while (!Serial);
|
||||
Serial.println("Adafruit Sparkle Motion IR Remote Control NeoPixels Demo");
|
||||
IrReceiver.begin(IR_RECEIVE_PIN);
|
||||
Serial.print("IRin on pin ");
|
||||
Serial.print(IR_RECEIVE_PIN);
|
||||
NEOPIXEL_STRIP.begin();
|
||||
NEOPIXEL_STRIP.setBrightness(25);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
/*
|
||||
* Check if received data is available and if yes, try to decode it.
|
||||
* When left or right buttons are pressed, change the pixelHue.
|
||||
* When up or down buttons are pressed, change the brightness.
|
||||
*/
|
||||
if (IrReceiver.decode()) {
|
||||
if (IrReceiver.decodedIRData.protocol == UNKNOWN) {
|
||||
Serial.println("unknown");
|
||||
IrReceiver.printIRResultRawFormatted(&Serial, true);
|
||||
IrReceiver.resume();
|
||||
} else {
|
||||
IrReceiver.resume();
|
||||
//IrReceiver.printIRResultShort(&Serial);
|
||||
|
||||
// Ignore repeat codes from holding down the button
|
||||
if (IrReceiver.decodedIRData.flags == 0){
|
||||
//Serial.printf("Command: %d\n",IrReceiver.decodedIRData.command);
|
||||
if (IrReceiver.decodedIRData.command == upCmd){
|
||||
Serial.println("UP btn");
|
||||
brightness = min(brightness + 25, 255);
|
||||
}else if (IrReceiver.decodedIRData.command == downCmd){
|
||||
Serial.println("DOWN btn");
|
||||
brightness = max(brightness - 25, 0);
|
||||
}else if (IrReceiver.decodedIRData.command == leftCmd){
|
||||
Serial.println("LEFT btn");
|
||||
pixelHue = (pixelHue - 8192) % 65536;
|
||||
}else if (IrReceiver.decodedIRData.command == rightCmd){
|
||||
Serial.println("RIGHT btn");
|
||||
pixelHue = (pixelHue + 8192) % 65536;
|
||||
}
|
||||
|
||||
NEOPIXEL_STRIP.setBrightness(brightness);
|
||||
NEOPIXEL_STRIP.fill(NEOPIXEL_STRIP.gamma32(NEOPIXEL_STRIP.ColorHSV(pixelHue)));
|
||||
NEOPIXEL_STRIP.show();
|
||||
delay(100);
|
||||
}
|
||||
}
|
||||
Serial.println();
|
||||
}
|
||||
}
|
||||
97
Toy_Robot_Xylophone/code.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# SPDX-FileCopyrightText: 2025 Liz Clark for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import os
|
||||
from random import randint
|
||||
import board
|
||||
import usb_midi
|
||||
import keypad
|
||||
from adafruit_mcp230xx.mcp23017 import MCP23017
|
||||
import adafruit_midi
|
||||
from adafruit_midi.note_on import NoteOn
|
||||
from adafruit_midi.note_off import NoteOff
|
||||
import adafruit_midi_parser
|
||||
|
||||
# music_box plays back MIDI files on CP drive
|
||||
# set to false for live MIDI over USB control
|
||||
music_box = True
|
||||
# define the notes that correspond to each solenoid
|
||||
notes = [48, 50, 52, 53, 55, 57, 59, 60]
|
||||
|
||||
key = keypad.Keys((board.BUTTON,), value_when_pressed=False, pull=True)
|
||||
|
||||
i2c = board.STEMMA_I2C()
|
||||
mcp = MCP23017(i2c)
|
||||
noids = []
|
||||
for i in range(8):
|
||||
noid = mcp.get_pin(i)
|
||||
noid.switch_to_output(value=False)
|
||||
noids.append(noid)
|
||||
# pylint: disable=used-before-assignment, unused-argument, global-statement, no-self-use
|
||||
if not music_box:
|
||||
midi = adafruit_midi.MIDI(
|
||||
midi_in=usb_midi.ports[0], in_channel=0, midi_out=usb_midi.ports[1], out_channel=0
|
||||
)
|
||||
else:
|
||||
midi_files = []
|
||||
for filename in os.listdir('/'):
|
||||
if filename.lower().endswith('.mid') and not filename.startswith('.'):
|
||||
midi_files.append("/"+filename)
|
||||
print(midi_files)
|
||||
|
||||
class Custom_Player(adafruit_midi_parser.MIDIPlayer):
|
||||
def on_note_on(self, note, velocity, channel): # noqa: PLR6301
|
||||
for z in range(len(notes)):
|
||||
if notes[z] == note:
|
||||
print(f"Playing note: {note}")
|
||||
noids[z].value = True
|
||||
|
||||
def on_note_off(self, note, velocity, channel): # noqa: PLR6301
|
||||
for z in range(len(notes)):
|
||||
if notes[z] == note:
|
||||
noids[z].value = False
|
||||
|
||||
def on_end_of_track(self, track): # noqa: PLR6301
|
||||
print(f"End of track {track}")
|
||||
for z in range(8):
|
||||
noids[z].value = False
|
||||
|
||||
def on_playback_complete(self): # noqa: PLR6301
|
||||
global now_playing
|
||||
now_playing = False
|
||||
for z in range(8):
|
||||
noids[z].value = False
|
||||
parser = adafruit_midi_parser.MIDIParser()
|
||||
parser.parse(midi_files[randint(0, (len(midi_files) - 1))])
|
||||
player = Custom_Player(parser)
|
||||
new_file = False
|
||||
now_playing = False
|
||||
|
||||
while True:
|
||||
if music_box:
|
||||
event = key.events.get()
|
||||
if event:
|
||||
if event.pressed:
|
||||
now_playing = not now_playing
|
||||
if now_playing:
|
||||
new_file = True
|
||||
if new_file:
|
||||
parser.parse(midi_files[randint(0, (len(midi_files) - 1))])
|
||||
print(f"Successfully parsed! Found {len(parser.events)} events.")
|
||||
print(f"BPM: {parser.bpm:.1f}")
|
||||
print(f"Note Count: {parser.note_count}")
|
||||
new_file = False
|
||||
if now_playing:
|
||||
player.play(loop=False)
|
||||
|
||||
else:
|
||||
msg = midi.receive()
|
||||
if msg is not None:
|
||||
for i in range(8):
|
||||
noid_output = noids[i]
|
||||
notes_played = notes[i]
|
||||
if isinstance(msg, NoteOn) and msg.note == notes_played:
|
||||
noid_output.value = True
|
||||
elif isinstance(msg, NoteOff) and msg.note == notes_played:
|
||||
noid_output.value = False
|
||||
BIN
Toy_Robot_Xylophone/song_1.mid
Normal file
BIN
Toy_Robot_Xylophone/song_2.mid
Normal file
BIN
Toy_Robot_Xylophone/song_3.mid
Normal file
BIN
Toy_Robot_Xylophone/song_4.mid
Normal file
BIN
World_Clock_Round_Display/circuitpython_world.bmp
Normal file
|
After Width: | Height: | Size: 87 KiB |