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 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}")
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.