Some of the functionality used here (USB Host for mouse in particular) is hot off the press. You'll want to install CircuitPython 10.0.0-alpha.5 or higher on your board rather than the 9.2.7 release version mentioned in the CircuitPython Install page.
Download the Project Bundle
Your project will use a specific set of CircuitPython libraries, sprite assets, sound assets, and .py files. To get everything you need, click on the Download Project Bundle link below, and uncompress the .zip file.
Drag the contents of the uncompressed bundle directory onto your board's CIRCUITPY drive, replacing any existing files or directories with the same names, and adding any new ones that are necessary.
# 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
if self.ui_manager.find_mouse():
print("Mouse found successfully!")
else:
print("WARNING: Mouse not found.")
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}")
To keep things manageable, Larsio Paint Music uses a modular design with each component handling a specific set of related tasks:
| Module | Use |
|---|---|
code.py |
Main application entry point |
sound_manager.py |
Audio handling (WAV samples, and synthio) |
note_manager.py |
Manages note positions and properties |
ui_manager.py |
Coordinates UI elements and user interaction |
display_manager.py |
Configures and initializes the display |
staff_view.py |
Creates and manages the music staff visuals |
control_panel.py |
Handles buttons and controls |
input_handler.py |
Processes mouse input |
sprite_manager.py |
Loads and manages graphics assets |
cursor_manager.py |
Manages the mouse cursor |
playback_controller.py |
Controls playback and timing |
Here's how these modules work.
Main Application (code.py)
This file runs when the board starts up and then it coordinates the other modules.
Sound Manager
The SoundManager handles all audio playback, including WAV samples, MIDI output, and synthesized sounds. It's one of the more complex parts of the application.
It also auto-detects which board is being used (Metro RP2350 or Fruit Jam) and configures the I2S pins appropriately.
Audio Mixer
The app uses an audio mixer with multiple voices, enabling simultaneous sounds.
Multi-Channel Sound
The program supports multiple instrument channels:
- Channel 1 (Lars): Custom WAV samples of everyone's favorite sloth
- Channel 2 (Heart): Bass
- Channel 3 (Drums): Percussion sounds
- Channels 4-6: Synthesizer voices with different waveforms
Note Manager
The NoteManager handles the positions of notes on the staff and their pitch values. It maintains a mapping of staff positions to MIDI note numbers.
When a note is added, the manager:
- Finds the closest valid position
- Creates a visual note at that position
- Adds ledger lines if needed
- Stores the note's data for playback
UI Manager and Display Manger
These coordinate all user interface elements and interactions, as well as displaying them to the screen.
- Setting up the display
- Creating the staff view
- Handling the control panel
- Processing user input
- Managing the playback
Staff View
The StaffView class creates the musical staff display with proper music notation spacing. It draws the staff lines, measure bars, and quarter note dividers so you can more easily see the bar subdivisions.
Control Panel
The ControlPanel class handles all the UI controls for the application, including transport buttons and channel selectors.
Input Handling
The InputHandler processes mouse input for interacting with the application, including mouse position and interactions:
- Left-click to add notes
- Right-click to delete notes
- Click on channel icons to switch instruments
- Control playback with transport buttons
- Adjust tempo
Sprite Manager
The SpriteManager loads and manages all graphical assets using BMP files:
Each instrument channel has its own unique sprite:
- Channel 1: Lars
- Channel 2: Heart (bass)
- Channel 3: Drum
- Channel 4: Meatball sprite for sine wave notes
- Channel 5: star sprite for triangle wave notes
- Channel 6: Adabot Head sprite for sawtooth wave notes
The sprite manager also handles preview notes shown during mouse hover, and handles button sprites for the transport controls.
Cursor Manager
The CursorManager handles the mouse cursor visuals, switching between different cursor styles based on context:
- Crosshair Cursor: Used when over the staff for precise note placement
- Triangle Cursor: Used when over buttons or controls
The offsets for each cursor ensure that the "hot spot" or active point of the cursor is properly aligned with the actual mouse position, making interaction more intuitive.
Playback Controller
The PlaybackController manages the playback of notes:
- Moves a playhead across the staff
- Triggers all notes at the current position
- Handles looping when enabled
- Stops playback when finished
Main Loop
The application's main loop continuously:
- Updates the playback (if active)
- Processes mouse input
- Updates the cursor position
- Handles button clicks
Page last edited May 19, 2025
Text editor powered by tinymce.