PiBoom V0
A Museum-Worthy Raspberry Pi Media Player That Anyone Can Build

The Inspiration
When technology and design intersect, magic happens. Today, I’m excited to share a project that transforms humble hardware components into a sleek, exhibition-worthy media device. The BoomBox Pi combines the versatility of a Raspberry Pi Zero 2 W with a carefully curated selection of components to create something that’s both functional and beautiful.
This project started with a simple question: Could we create a DIY media device that wouldn’t look out of place in MoMA’s design collection? The answer is a resounding yes.
The Vision
The BoomBox Pi isn’t just another DIY project hidden in a plastic case. It’s a statement piece designed to be displayed prominently in your living space. Taking inspiration from iconic audio equipment manufacturers and minimalist design philosophy, we’ve created a device that balances form and function.
Show Image
Key design elements include:
- Clean, monolithic form with brushed aluminum finish
- Edge-to-edge display with minimal bezels
- Premium perforated speaker grills
- Subtle ambient lighting through smoked glass
- Touch-sensitive controls for modern interaction
The Components
The beauty of the BoomBox Pi is that it’s built from accessible components:
Core Hardware:
- Raspberry Pi Zero 2 W: The brain of the operation
- 2.8″ ILI9341 Touch Display: Our primary interface
- ILI9341 Driver: Powers our high-quality display
- Quality Speakers: For immersive audio experience
- Pi Camera Module: For photography and video capabilities
Additional Components:
- WS2812B LED Matrix: For customizable visual effects
- NES Power/Reset Buttons: Reimagined as elegant controls
- I2C Microphone: For voice command functionality
- 0.5″ Mini Display: For system status information
Show Image
The Build Process
Building your own BoomBox Pi is a rewarding weekend project. Let’s break it down into manageable steps:
1. Prepare Your Raspberry Pi
Start by installing a fresh copy of Raspberry Pi OS on your Pi Zero 2 W. You’ll need to enable SPI and I2C interfaces through raspi-config
to communicate with your display and other components.
bashCopysudo raspi-config
# Navigate to Interface Options
# Enable SPI and I2C
2. Connect the Display
The ILI9341 display connects via SPI, making it efficient for our compact design:
- MOSI → GPIO 10
- MISO → GPIO 9
- SCK → GPIO 11
- CS → GPIO 8
- D/C → GPIO 25
- Touch CS → GPIO 7
3. Set Up Audio
Connect your speaker module to the Raspberry Pi’s I2S pins (GPIO 18-21) for high-quality audio. If you prefer simplicity, the 3.5mm audio jack works too.
4. Add the Camera and LED Matrix
The Pi Camera connects to the dedicated camera port, while the WS2812B LED Matrix data pin connects to GPIO 12. Remember to provide adequate power for your LEDs!
5. Install Required Software
Our BoomBox Pi runs on custom Python software that handles everything from touch interface to media playback:
bashCopysudo apt update
sudo apt install -y python3-pip vlc chromium-browser
pip3 install adafruit-circuitpython-ili9341 rpi.gpio pillow pygame python-vlc neopixel picamera
6. Install the Software
Save our custom Python software to your Pi and set it to run on startup by adding it to /etc/rc.local
.
The Magic: What It Can Do
The BoomBox Pi is more than just a pretty face—it’s a versatile media center:
- YouTube Streaming: Watch your favorite videos with the touch interface
- Local Media Playback: Store and play videos directly from your Pi
- Camera Functionality: Take photos and videos on demand
- Synchronized LED Effects: Visual effects that dance to your music
- Voice Control: Basic commands via the I2C microphone
- Classic Control Feel: Reimagined NES buttons for nostalgic interaction
Show Image
The Code Behind the Magic
The BoomBox Pi’s functionality comes from elegant Python code. Here’s a snippet that handles the touch interface:
pythonCopydef check_touch():
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
if event.type == pygame.MOUSEBUTTONDOWN:
pos = pygame.mouse.get_pos()
# Check if a button was pressed
for button in buttons:
if (button[0] <= pos[0] <= button[0] + button[2] and
button[1] <= pos[1] <= button[1] + button[3]):
button_action(button[5])
return True
Making It Your Own
The beauty of DIY projects is personalization. Here are some ways to make your BoomBox Pi unique:
- Custom Enclosure: 3D print or CNC mill your own case design
- Alternative Displays: Try different sizes or even OLED displays
- Audio Upgrades: Add a DAC HAT for audiophile-quality sound
- Expanded Storage: Add USB storage for a larger media library
- IoT Integration: Connect to smart home systems for advanced control
Conclusion: Where Technology Meets Art
The BoomBox Pi exemplifies what happens when we approach technology as a design challenge. It’s proof that DIY electronics don’t have to look DIY—they can be sophisticated, elegant, and worthy of display.
What makes this project special is the balance it strikes: accessible enough for hobbyists but refined enough for design enthusiasts. Whether you’re a Raspberry Pi veteran or a newcomer looking for a compelling first project, the BoomBox Pi offers both technical satisfaction and aesthetic pleasure.
Perfect! You have the CJMCU-5102 PCM5102A DAC board, which is an excellent choice for your BoomBox Pi project. This is a high-quality I2S DAC that will give you much better audio than the Raspberry Pi’s built-in output.
Wiring This Specific PCM5102A Board
Looking at your board image, I can see the clearly labeled pins. Here’s how to connect this specific model to your Raspberry Pi Zero 2 W:
PCM5102A PinRaspberry Pi PinVIN3.3V (Pin 1)GNDGND (Pin 6)SCKGPIO 18 (PCM_CLK)BCKGPIO 18 (PCM_CLK)DINGPIO 21 (PCM_DOUT)LCKGPIO 19 (PCM_FS)GNDGND
For the other pins on your DAC board:
- FLT, DMP, SCL, FMT: Connect to GND
- XMT: Connect to 3.3V
Software Configuration for This Board
- Edit the boot config file:
bashCopysudo nano /boot/config.txt
- Add these specific lines for your board:
Copy# Enable I2S for PCM5102A
dtparam=i2s=on
dtoverlay=hifiberry-dac
dtoverlay=i2s-mmap
- Create or edit your ALSA config:
bashCopysudo nano /etc/asound.conf
- Add this configuration:
Copypcm.!default {
type hw
card 0
}
ctl.!default {
type hw
card 0
}
- Reboot your Pi:
bashCopysudo reboot
- After rebooting, test your DAC:
bashCopyspeaker-test -c2 -twav -l5
Using This DAC for Music Visualization
Since you’re using this specific DAC board, you have a couple of options for getting audio data for LED visualization:
- Direct audio sampling from the player software – This method doesn’t require additional hardware and is cleaner:
pythonCopydef audio_visualizer_thread(self):
"""Thread that analyzes audio for visualization"""
# Use ALSA to monitor the audio output
import alsaaudio
import audioop
import numpy as np
import time
# Setup audio monitoring
pcm = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, alsaaudio.PCM_NONBLOCK, 'default')
pcm.setchannels(2)
pcm.setrate(44100)
pcm.setformat(alsaaudio.PCM_FORMAT_S16_LE)
pcm.setperiodsize(1024)
max_volume = 1 # Dynamic adjustment
while self.visualization_active:
length, data = pcm.read()
if length > 0:
# Get volume
volume = audioop.rms(data, 2)
max_volume = max(volume, max_volume * 0.95)
# Simple frequency band separation simulation
bass = audioop.rms(data[0:length//3], 2) / max_volume if max_volume > 0 else 0
mids = audioop.rms(data[length//3:2*length//3], 2) / max_volume if max_volume > 0 else 0
highs = audioop.rms(data[2*length//3:], 2) / max_volume if max_volume > 0 else 0
# Update LED matrix based on audio levels
self._update_led_visualization(bass, mids, highs)
time.sleep(0.01)
- Add a simple microphone – This captures the ambient sound including what’s coming from your speakers:
pythonCopydef mic_visualizer_thread(self):
"""Thread that uses a microphone to visualize audio"""
import pyaudio
import numpy as np
import struct
CHUNK = 1024
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 44100
p = pyaudio.PyAudio()
stream = p.open(
format=FORMAT,
channels=CHANNELS,
rate=RATE,
input=True,
frames_per_buffer=CHUNK
)
try:
while self.visualization_active:
data = stream.read(CHUNK, exception_on_overflow=False)
audio_data = np.frombuffer(data, dtype=np.int16)
# Simple audio analysis (without FFT)
audio_data = np.abs(audio_data)
max_val = np.max(audio_data) if np.max(audio_data) > 0 else 1
# Divide the audio data into segments
segments = np.array_split(audio_data, 3)
bass = np.mean(segments[0]) / max_val
mids = np.mean(segments[1]) / max_val
highs = np.mean(segments[2]) / max_val
# Update LED visualization
self._update_led_visualization(bass, mids, highs)
finally:
stream.stop_stream()
stream.close()
p.terminate()
Final Integration Tips for Your Board
- Power considerations: Your PCM5102A board is very power-efficient but still needs clean power. Connect it directly to the Pi’s 3.3V line, not through a breadboard if possible.
- Audio quality settings: When playing music through VLC or other players, set the output to ALSA and configure for maximum quality:
pythonCopy# For VLC player in Python
player = vlc.MediaPlayer('your_music.mp3')
player.audio_output_set("alsa")
player.audio_output_device_set("alsa", "hw:0,0") # Direct hardware access
player.play()
- Ground loop prevention: If you hear any humming or noise, make sure all GND connections from the DAC, LEDs, and Pi are connected to the same ground point.
This PCM5102A DAC board is a great choice and will significantly enhance your BoomBox Pi’s audio quality. The combination of high-quality audio with synchronized LED effects will make for an impressive finished product!
LED Effects Synchronized with Music
There are many exciting LED effects you can create for your BoomBox Pi! Here’s how to set up different music-reactive effects and create your own custom ones:
Basic Music-Reactive LED Effects
1. Spectrum Analyzer
This effect displays the audio frequency spectrum across your LED matrix:
pythonCopydef spectrum_analyzer(audio_data, pixels, num_pixels):
"""Create a spectrum analyzer visualization"""
# Split audio data into frequency bands
bands = np.array_split(audio_data, num_pixels)
band_avgs = [np.mean(band) for band in bands]
# Map frequency bands to colors (blue for low, green for mid, red for high)
for i, intensity in enumerate(band_avgs):
# Color gradient based on position
r = int(max(0, min(255, intensity * 255 * (i / num_pixels) * 2)))
g = int(max(0, min(255, intensity * 255 * (1 - abs(2 * (i / num_pixels) - 1)))))
b = int(max(0, min(255, intensity * 255 * (1 - i / num_pixels) * 2)))
pixels[i] = (r, g, b)
2. VU Meter
Creates a classic volume unit meter effect:
pythonCopydef vu_meter(volume, pixels, num_pixels):
"""Create a VU meter visualization"""
# Calculate how many LEDs should be lit based on volume
lit_leds = int(volume * num_pixels)
for i in range(num_pixels):
if i < lit_leds:
# Gradient from green to red
if i < num_pixels * 0.6: # First 60% are green
pixels[i] = (0, 255, 0)
elif i < num_pixels * 0.8: # Next 20% are yellow
pixels[i] = (255, 255, 0)
else: # Last 20% are red
pixels[i] = (255, 0, 0)
else:
pixels[i] = (0, 0, 0) # Off
3. Pulse Effect
Makes all LEDs pulse in brightness with the beat:
pythonCopydef pulse_effect(bass, pixels, num_pixels):
"""Create pulsing effect based on bass"""
# Base color (can be adjusted)
r, g, b = 100, 50, 255
# Modulate brightness with bass level
brightness = 0.2 + bass * 0.8 # Minimum 0.2, max 1.0
# Apply to all LEDs
for i in range(num_pixels):
pixels[i] = (int(r * brightness), int(g * brightness), int(b * brightness))
4. Rainbow Wave
A rainbow pattern that speeds up or slows down with the music:
pythonCopydef rainbow_wave(volume, pixels, num_pixels, position):
"""Create a moving rainbow that responds to volume"""
# Speed based on volume
speed = 1 + int(volume * 10)
# Update position
position = (position + speed) % (256 * 5)
# Draw rainbow
for i in range(num_pixels):
pixel_index = (i * 256 // num_pixels) + position
pixels[i] = wheel(pixel_index & 255)
return position # Return updated position for next frame
def wheel(pos):
"""Color wheel to generate rainbow colors"""
if pos < 85:
return (pos * 3, 255 - pos * 3, 0)
elif pos < 170:
pos -= 85
return (255 - pos * 3, 0, pos * 3)
else:
pos -= 170
return (0, pos * 3, 255 - pos * 3)
Setting Up Custom Effects
1. Create an Effects Manager Class
Here’s how to set up a framework for creating and switching between custom effects:
pythonCopyclass LEDEffectsManager:
def __init__(self, pixel_pin, num_pixels):
"""Initialize the LED effects manager"""
import board
import neopixel
# Initialize NeoPixel strip
self.pixels = neopixel.NeoPixel(
getattr(board, f"D{pixel_pin}"),
num_pixels,
brightness=0.3,
auto_write=False
)
self.num_pixels = num_pixels
# Initialize effects state
self.current_effect = "spectrum"
self.effect_position = 0 # For effects that need positional state
self.effects = {
"spectrum": self.spectrum_analyzer,
"vu_meter": self.vu_meter,
"pulse": self.pulse_effect,
"rainbow": self.rainbow_wave,
"color_bounce": self.color_bounce,
"strobe": self.strobe_effect,
# Add your custom effects here
}
def update(self, audio_data, bass, mids, highs, volume):
"""Update LEDs based on current effect and audio data"""
# Call the current effect function
if self.current_effect in self.effects:
if self.current_effect in ["rainbow", "color_bounce"]:
# Effects that need to track position
self.effect_position = self.effects[self.current_effect](
volume, self.pixels, self.num_pixels, self.effect_position
)
else:
# Standard effects
self.effects[self.current_effect](
audio_data, bass, mids, highs, volume,
self.pixels, self.num_pixels
)
# Update the LEDs
self.pixels.show()
def change_effect(self, effect_name):
"""Change to a different effect"""
if effect_name in self.effects:
self.current_effect = effect_name
self.effect_position = 0 # Reset position
print(f"Changed to effect: {effect_name}")
else:
print(f"Effect {effect_name} not found")
# Effect implementations go here...
def spectrum_analyzer(self, audio_data, bass, mids, highs, volume, pixels, num_pixels):
# Implementation from above
pass
# Add custom effects...
def color_bounce(self, volume, pixels, num_pixels, position):
"""A ball of color bouncing back and forth, speed depends on volume"""
# Clear all pixels
for i in range(num_pixels):
pixels[i] = (0, 0, 0)
# Calculate speed based on volume
speed = 1 + int(volume * 5)
# Update position with bouncing behavior
position += speed
if position >= num_pixels * 2:
position = 0
# Calculate actual position with bounce
actual_pos = position if position < num_pixels else 2 * num_pixels - position
# Draw the "ball" with falloff glow
center = int(actual_pos)
for i in range(max(0, center - 5), min(num_pixels, center + 6)):
distance = abs(i - center)
brightness = 1.0 - (distance / 5.0)
if brightness > 0:
# Color based on volume
r = int(255 * volume)
g = int(100 * (1 - volume))
b = int(255 * (1 - volume))
pixels[i] = (
int(r * brightness),
int(g * brightness),
int(b * brightness)
)
return position
def strobe_effect(self, audio_data, bass, mids, highs, volume, pixels, num_pixels):
"""Strobe effect that triggers on bass hits"""
# Threshold for triggering a strobe
if bass > 0.7: # Strong bass hit
# Flash all LEDs white
for i in range(num_pixels):
pixels[i] = (255, 255, 255)
else:
# Fade or off
for i in range(num_pixels):
pixels[i] = (int(bass * 50), int(bass * 50), int(bass * 50))
2. Creating Your Own Custom Effects
To create a custom effect, add a new method to the LEDEffectsManager
class following this pattern:
pythonCopydef my_custom_effect(self, audio_data, bass, mids, highs, volume, pixels, num_pixels):
"""Description of your custom effect"""
# Clear previous state if needed
for i in range(num_pixels):
pixels[i] = (0, 0, 0)
# Your creative logic here!
# Example: Color blocks based on frequency ranges
bass_leds = int(num_pixels * 0.33)
mid_leds = int(num_pixels * 0.33)
high_leds = num_pixels - bass_leds - mid_leds
# Set colors based on intensity
for i in range(bass_leds):
pixels[i] = (int(bass * 255), 0, 0) # Red for bass
for i in range(bass_leds, bass_leds + mid_leds):
pixels[i] = (0, int(mids * 255), 0) # Green for mids
for i in range(bass_leds + mid_leds, num_pixels):
pixels[i] = (0, 0, int(highs * 255)) # Blue for highs
Then add your effect to the effects dictionary:
pythonCopyself.effects = {
# Existing effects...
"my_custom": self.my_custom_effect,
}
3. Creative Custom Effect Ideas
Here are some ideas for custom effects you can implement:
Fire Effect
pythonCopydef fire_effect(self, audio_data, bass, mids, highs, volume, pixels, num_pixels):
"""Create a fire effect that intensifies with bass"""
cooling = int((1.0 - bass) * 20)
sparking = int(bass * 120) + 50
# Array of temperature values at each pixel
if not hasattr(self, 'heat'):
self.heat = [0] * num_pixels
# Step 1. Cool down every cell a little
for i in range(num_pixels):
self.heat[i] = max(0, self.heat[i] - random.randint(0, cooling))
# Step 2. Heat from each cell drifts up and diffuses
for i in range(num_pixels - 1, 2, -1):
self.heat[i] = (self.heat[i - 1] + self.heat[i - 2] + self.heat[i - 3]) // 3
# Step 3. Randomly ignite new sparks near the bottom
if random.randint(0, 255) < sparking:
y = random.randint(0, 7)
self.heat[y] = min(255, self.heat[y] + random.randint(160, 255))
# Step 4. Map from heat cells to LED colors
for i in range(num_pixels):
self.set_pixel_heat_color(i, self.heat[i])
def set_pixel_heat_color(self, pixel, temperature):
"""Map temperature to RGB color for fire effect"""
# Scale temperature down - 0-255 => 0-191
t192 = round((temperature / 255.0) * 191)
# Calculate color based on temperature
if t192 < 64:
self.pixels[pixel] = (t192 * 3, 0, 0)
elif t192 < 128:
self.pixels[pixel] = (255, (t192 - 64) * 3, 0)
else:
self.pixels[pixel] = (255, 255, (t192 - 128) * 3)
Starfield Effect
pythonCopydef starfield_effect(self, audio_data, bass, mids, highs, volume, pixels, num_pixels):
"""Create a starfield that responds to music"""
# Initialize stars if not already done
if not hasattr(self, 'stars'):
self.stars = [(random.random(), random.random(), random.random())
for _ in range(int(num_pixels/4))]
# Clear all pixels
for i in range(num_pixels):
pixels[i] = (0, 0, 0)
# Update and draw stars
new_stars = []
for pos, speed, brightness in self.stars:
# Update position based on speed and volume
pos = pos + speed * (0.01 + volume * 0.05)
# Keep stars that are still in range
if pos < 1.0:
new_stars.append((pos, speed, brightness))
# Calculate pixel position and brightness
pixel = int(pos * num_pixels)
if 0 <= pixel < num_pixels:
# Brightness affected by mids
b = int(brightness * 255 * (0.5 + mids * 0.5))
pixels[pixel] = (b, b, b)
# Add new stars based on music intensity
new_star_chance = int(volume * 20)
for _ in range(new_star_chance):
if random.randint(0, 100) < 20 and len(new_stars) < num_pixels/3:
new_stars.append((0, random.uniform(0.005, 0.02), random.uniform(0.5, 1.0)))
self.stars = new_stars
Integration with Your BoomBox Pi
To integrate these effects with your main BoomBox Pi code, initialize the effects manager and call it from your audio processing thread:
pythonCopy# In your main initialization
self.led_effects = LEDEffectsManager(pixel_pin=12, num_pixels=64)
# In your audio processing thread
def audio_visualizer_thread(self):
# Audio setup code...
while self.visualization_active:
# Audio processing code to get audio_data, bass, mids, highs, volume...
# Update the LED effects
self.led_effects.update(audio_data, bass, mids, highs, volume)
# Small delay to avoid CPU hogging
time.sleep(0.01)
To change effects from your user interface, add buttons or menu options that call:
pythonCopyself.led_effects.change_effect("rainbow") # Or any other effect name
This framework makes it easy to experiment with different effects and create your own unique visualizations that respond to your music!
I’ll help you create a system for your BoomBox Pi that organizes videos into 4 folders with thumbnail grids. Here’s how to implement this functionality:
Video Library with Thumbnail Grid Interface
1. Folder Structure Setup
First, let’s create the folder structure:
bashCopymkdir -p ~/boombox_media/music_videos_1
mkdir -p ~/boombox_media/music_videos_2
mkdir -p ~/boombox_media/music_videos_3
mkdir -p ~/boombox_media/camera_recordings
mkdir -p ~/boombox_media/thumbnails
2. Generating Video Thumbnails
We’ll need to automatically generate thumbnails for each video. Here’s a Python function using FFmpeg:
pythonCopyimport os
import subprocess
import glob
def generate_thumbnails():
"""Generate thumbnails for all videos in the media folders"""
media_path = os.path.expanduser("~/boombox_media")
folders = ["music_videos_1", "music_videos_2", "music_videos_3", "camera_recordings"]
for folder in folders:
folder_path = os.path.join(media_path, folder)
# Find all video files
video_files = []
for ext in ['*.mp4', '*.mkv', '*.avi', '*.mov']:
video_files.extend(glob.glob(os.path.join(folder_path, ext)))
# Create thumbnails directory if it doesn't exist
thumbnail_dir = os.path.join(media_path, "thumbnails", folder)
os.makedirs(thumbnail_dir, exist_ok=True)
# Generate thumbnails
for video_file in video_files:
video_name = os.path.basename(video_file)
thumbnail_path = os.path.join(thumbnail_dir, f"{os.path.splitext(video_name)[0]}.jpg")
# Skip if thumbnail already exists
if os.path.exists(thumbnail_path):
continue
# Extract thumbnail at 3 seconds into video
try:
cmd = [
'ffmpeg', '-i', video_file,
'-ss', '00:00:03', '-vframes', '1',
'-vf', f'scale=240:-1', # Scale to fit your display width
thumbnail_path
]
subprocess.run(cmd, check=True, capture_output=True)
print(f"Generated thumbnail for {video_name}")
except subprocess.CalledProcessError as e:
print(f"Error generating thumbnail for {video_name}: {e}")
3. UI for Video Grid and Folder Selection
Now, let’s create the UI for browsing folders and displaying the video thumbnails:
pythonCopyimport pygame
import os
import math
class VideoLibraryUI:
def __init__(self, screen, width, height):
self.screen = screen
self.width = width
self.height = height
self.media_path = os.path.expanduser("~/boombox_media")
# Folder configuration
self.folders = [
{"name": "Music Videos 1", "path": "music_videos_1", "color": (255, 100, 100)},
{"name": "Music Videos 2", "path": "music_videos_2", "color": (100, 255, 100)},
{"name": "Music Videos 3", "path": "music_videos_3", "color": (100, 100, 255)},
{"name": "Camera Recordings", "path": "camera_recordings", "color": (255, 255, 100)}
]
# UI state
self.current_view = "folders" # "folders" or "videos"
self.current_folder = None
self.current_page = 0
self.videos_per_page = 9 # 3x3 grid
self.thumbnails = {} # Cache for loaded thumbnails
# Load fonts
pygame.font.init()
self.font = pygame.font.SysFont(None, 24)
self.title_font = pygame.font.SysFont(None, 30)
# Preload thumbnails
self.load_thumbnails()
def load_thumbnails(self):
"""Preload thumbnails into memory"""
thumbnail_dir = os.path.join(self.media_path, "thumbnails")
for folder in self.folders:
folder_path = os.path.join(thumbnail_dir, folder["path"])
if not os.path.exists(folder_path):
continue
for thumb_file in os.listdir(folder_path):
if thumb_file.endswith(".jpg"):
try:
thumb_path = os.path.join(folder_path, thumb_file)
# Load and scale the thumbnail
image = pygame.image.load(thumb_path)
# Store in dictionary with the video name as key
video_name = os.path.splitext(thumb_file)[0]
self.thumbnails[f"{folder['path']}/{video_name}"] = image
except Exception as e:
print(f"Error loading thumbnail {thumb_file}: {e}")
def draw(self):
"""Draw the current view"""
self.screen.fill((0, 0, 0)) # Black background
if self.current_view == "folders":
self.draw_folders()
elif self.current_view == "videos":
self.draw_videos()
pygame.display.flip()
def draw_folders(self):
"""Draw the folder selection screen"""
# Draw title
title = self.title_font.render("Select Media Folder", True, (255, 255, 255))
self.screen.blit(title, (self.width//2 - title.get_width()//2, 20))
# Draw folder buttons
button_height = 60
button_width = self.width - 40
for i, folder in enumerate(self.folders):
y = 80 + i * (button_height + 10)
# Draw button
pygame.draw.rect(self.screen, folder["color"],
(20, y, button_width, button_height),
border_radius=10)
pygame.draw.rect(self.screen, (255, 255, 255),
(20, y, button_width, button_height),
width=2, border_radius=10)
# Draw folder name
text = self.font.render(folder["name"], True, (0, 0, 0))
self.screen.blit(text, (self.width//2 - text.get_width()//2, y + button_height//2 - text.get_height()//2))
def draw_videos(self):
"""Draw the video grid for the current folder"""
if not self.current_folder:
return
# Draw title and back button
title = self.title_font.render(self.current_folder["name"], True, (255, 255, 255))
self.screen.blit(title, (self.width//2 - title.get_width()//2, 10))
# Draw back button
pygame.draw.rect(self.screen, (100, 100, 100), (10, 10, 60, 30), border_radius=5)
back_text = self.font.render("Back", True, (255, 255, 255))
self.screen.blit(back_text, (10 + 30 - back_text.get_width()//2, 10 + 15 - back_text.get_height()//2))
# Get videos for this folder
folder_path = os.path.join(self.media_path, self.current_folder["path"])
video_files = []
for ext in ['.mp4', '.mkv', '.avi', '.mov']:
video_files.extend([f for f in os.listdir(folder_path) if f.endswith(ext)])
# Pagination
start_idx = self.current_page * self.videos_per_page
end_idx = min(start_idx + self.videos_per_page, len(video_files))
page_videos = video_files[start_idx:end_idx]
# Draw video grid (3x3)
thumb_width = 70
thumb_height = 70
grid_top = 50
grid_left = 10
spacing = 5
for i, video in enumerate(page_videos):
row = i // 3
col = i % 3
x = grid_left + col * (thumb_width + spacing)
y = grid_top + row * (thumb_height + spacing + 20) # Extra space for title
# Draw thumbnail or placeholder
thumb_key = f"{self.current_folder['path']}/{os.path.splitext(video)[0]}"
if thumb_key in self.thumbnails:
thumb = self.thumbnails[thumb_key]
# Scale thumbnail to fit
thumb = pygame.transform.scale(thumb, (thumb_width, thumb_height))
self.screen.blit(thumb, (x, y))
else:
# Draw placeholder
pygame.draw.rect(self.screen, (50, 50, 50), (x, y, thumb_width, thumb_height))
no_img = self.font.render("No", True, (200, 200, 200))
thumb = self.font.render("thumb", True, (200, 200, 200))
self.screen.blit(no_img, (x + thumb_width//2 - no_img.get_width()//2,
y + thumb_height//3 - no_img.get_height()//2))
self.screen.blit(thumb, (x + thumb_width//2 - thumb.get_width()//2,
y + 2*thumb_height//3 - thumb.get_height()//2))
# Draw video name (truncated)
video_name = os.path.splitext(video)[0]
if len(video_name) > 15:
video_name = video_name[:12] + "..."
name_text = self.font.render(video_name, True, (200, 200, 200))
self.screen.blit(name_text, (x + thumb_width//2 - name_text.get_width()//2, y + thumb_height + 5))
# Draw pagination controls if needed
total_pages = math.ceil(len(video_files) / self.videos_per_page)
if total_pages > 1:
page_text = self.font.render(f"Page {self.current_page+1}/{total_pages}", True, (200, 200, 200))
self.screen.blit(page_text, (self.width//2 - page_text.get_width()//2, self.height - 40))
# Previous page button if not on first page
if self.current_page > 0:
pygame.draw.rect(self.screen, (100, 100, 100), (20, self.height - 40, 60, 30), border_radius=5)
prev_text = self.font.render("Prev", True, (255, 255, 255))
self.screen.blit(prev_text, (20 + 30 - prev_text.get_width()//2, self.height - 40 + 15 - prev_text.get_height()//2))
# Next page button if not on last page
if self.current_page < total_pages - 1:
pygame.draw.rect(self.screen, (100, 100, 100), (self.width - 80, self.height - 40, 60, 30), border_radius=5)
next_text = self.font.render("Next", True, (255, 255, 255))
self.screen.blit(next_text, (self.width - 80 + 30 - next_text.get_width()//2, self.height - 40 + 15 - next_text.get_height()//2))
def handle_click(self, pos):
"""Handle mouse clicks"""
x, y = pos
if self.current_view == "folders":
# Check if a folder was clicked
button_height = 60
button_width = self.width - 40
for i, folder in enumerate(self.folders):
button_y = 80 + i * (button_height + 10)
if 20 <= x <= 20 + button_width and button_y <= y <= button_y + button_height:
self.current_folder = folder
self.current_view = "videos"
self.current_page = 0
return True
elif self.current_view == "videos":
# Check if back button was clicked
if 10 <= x <= 70 and 10 <= y <= 40:
self.current_view = "folders"
return True
# Check pagination controls
folder_path = os.path.join(self.media_path, self.current_folder["path"])
video_files = []
for ext in ['.mp4', '.mkv', '.avi', '.mov']:
video_files.extend([f for f in os.listdir(folder_path) if f.endswith(ext)])
total_pages = math.ceil(len(video_files) / self.videos_per_page)
# Prev button
if self.current_page > 0 and 20 <= x <= 80 and self.height - 40 <= y <= self.height - 10:
self.current_page -= 1
return True
# Next button
if self.current_page < total_pages - 1 and self.width - 80 <= x <= self.width - 20 and self.height - 40 <= y <= self.height - 10:
self.current_page += 1
return True
# Check if a video thumbnail was clicked
thumb_width = 70
thumb_height = 70
grid_top = 50
grid_left = 10
spacing = 5
start_idx = self.current_page * self.videos_per_page
end_idx = min(start_idx + self.videos_per_page, len(video_files))
page_videos = video_files[start_idx:end_idx]
for i, video in enumerate(page_videos):
row = i // 3
col = i % 3
thumb_x = grid_left + col * (thumb_width + spacing)
thumb_y = grid_top + row * (thumb_height + spacing + 20)
if thumb_x <= x <= thumb_x + thumb_width and thumb_y <= y <= thumb_y + thumb_height:
video_path = os.path.join(folder_path, video)
self.play_video(video_path)
return True
return False
def play_video(self, video_path):
"""Play the selected video"""
print(f"Playing video: {video_path}")
# This will be implemented in the main BoomBox Pi code
# to integrate with VLC or other media player
4. Recording UI for Pi Camera
We also need a UI for recording videos with the Pi Camera:
pythonCopyclass CameraRecordingUI:
def __init__(self, screen, width, height):
self.screen = screen
self.width = width
self.height = height
self.recording = False
self.camera = None
self.recording_time = 0
self.recording_start_time = 0
self.font = pygame.font.SysFont(None, 24)
self.output_folder = os.path.expanduser("~/boombox_media/camera_recordings")
def initialize_camera(self):
"""Initialize the Pi Camera"""
try:
from picamera import PiCamera
self.camera = PiCamera()
self.camera.resolution = (640, 480)
self.camera.framerate = 30
return True
except Exception as e:
print(f"Error initializing camera: {e}")
return False
def start_recording(self):
"""Start recording video"""
if not self.camera:
if not self.initialize_camera():
return False
if not self.recording:
timestamp = time.strftime("%Y%m%d-%H%M%S")
video_path = os.path.join(self.output_folder, f"recording_{timestamp}.h264")
try:
self.camera.start_recording(video_path)
self.recording = True
self.recording_start_time = time.time()
print(f"Started recording to {video_path}")
return True
except Exception as e:
print(f"Error starting recording: {e}")
return False
return False
def stop_recording(self):
"""Stop recording video"""
if self.recording and self.camera:
try:
self.camera.stop_recording()
self.recording = False
self.recording_time = 0
print("Recording stopped")
# Convert to MP4 (H.264 to MP4 container)
timestamp = time.strftime("%Y%m%d-%H%M%S")
h264_file = os.path.join(self.output_folder, f"recording_{timestamp}.h264")
mp4_file = os.path.join(self.output_folder, f"recording_{timestamp}.mp4")
try:
cmd = ['ffmpeg', '-i', h264_file, '-c:v', 'copy', mp4_file]
subprocess.run(cmd, check=True, capture_output=True)
# Remove original h264 file
os.remove(h264_file)
print(f"Converted recording to MP4: {mp4_file}")
# Generate thumbnail
generate_thumbnails() # Re-generate all thumbnails including new recording
except Exception as e:
print(f"Error converting video: {e}")
return True
except Exception as e:
print(f"Error stopping recording: {e}")
return False
return False
def update(self):
"""Update recording time if recording"""
if self.recording:
self.recording_time = time.time() - self.recording_start_time
def draw(self):
"""Draw the camera UI"""
# Draw camera preview
# Note: On actual device, this would show a live camera feed
# For development, we'll just draw a placeholder
preview_rect = pygame.Rect(20, 20, self.width - 40, self.height - 100)
pygame.draw.rect(self.screen, (50, 50, 50), preview_rect)
# If not recording, show start button
if not self.recording:
record_btn = pygame.Rect(self.width//2 - 60, self.height - 70, 120, 40)
pygame.draw.rect(self.screen, (255, 50, 50), record_btn, border_radius=20)
record_text = self.font.render("Record", True, (255, 255, 255))
self.screen.blit(record_text, (record_btn.centerx - record_text.get_width()//2,
record_btn.centery - record_text.get_height()//2))
else:
# If recording, show recording indicator and stop button
# Recording time
minutes = int(self.recording_time // 60)
seconds = int(self.recording_time % 60)
time_text = self.font.render(f"Recording: {minutes:02d}:{seconds:02d}", True, (255, 50, 50))
self.screen.blit(time_text, (self.width//2 - time_text.get_width()//2, self.height - 80))
# Record indicator (flashing red circle)
if int(self.recording_time) % 2 == 0: # Flash every second
pygame.draw.circle(self.screen, (255, 0, 0), (30, 30), 10)
# Stop button
stop_btn = pygame.Rect(self.width//2 - 60, self.height - 50, 120, 40)
pygame.draw.rect(self.screen, (50, 50, 50), stop_btn, border_radius=20)
stop_text = self.font.render("Stop", True, (255, 255, 255))
self.screen.blit(stop_text, (stop_btn.centerx - stop_text.get_width()//2,
stop_btn.centery - stop_text.get_height()//2))
def handle_click(self, pos):
"""Handle mouse clicks for camera UI"""
x, y = pos
if not self.recording:
# Check if record button was clicked
record_btn = pygame.Rect(self.width//2 - 60, self.height - 70, 120, 40)
if record_btn.collidepoint(x, y):
return self.start_recording()
else:
# Check if stop button was clicked
stop_btn = pygame.Rect(self.width//2 - 60, self.height - 50, 120, 40)
if stop_btn.collidepoint(x, y):
return self.stop_recording()
return False
def cleanup(self):
"""Clean up camera resources"""
if self.camera:
if self.recording:
self.camera.stop_recording()
self.camera.close()
self.camera = None
5. Integrating with Main BoomBox Pi
Now, let’s modify the main BoomBox Pi code to include this video library system:
pythonCopyclass BoomBoxPi:
def __init__(self):
# Initialize display
pygame.init()
pygame.mouse.set_visible(True) # Show mouse cursor for touch interaction
self.width = 240 # Your ILI9341 display resolution
self.height = 320
self.screen = pygame.display.set_mode((self.width, self.height))
pygame.display.set_caption('BoomBox Pi')
# Initialize media player
self.player = None
# UI state
self.current_screen = "main_menu" # "main_menu", "video_library", "camera", "settings"
# Initialize UI components
self.video_library = VideoLibraryUI(self.screen, self.width, self.height)
self.camera_ui = CameraRecordingUI(self.screen, self.width, self.height)
# LED effects manager for music visualization
self.led_effects = LEDEffectsManager(pixel_pin=12, num_pixels=64)
# Main menu buttons [x, y, width, height, text, action]
self.main_menu_buttons = [
[20, 50, 200, 50, "Video Library", "video_library"],
[20, 120, 200, 50, "Camera", "camera"],
[20, 190, 200, 50, "LED Effects", "led_effects"],
[20, 260, 200, 50, "Settings", "settings"]
]
# Generate thumbnails at startup
generate_thumbnails()
def run(self):
"""Main application loop"""
running = True
clock = pygame.time.Clock()
while running:
# Handle events
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
self.handle_click(pygame.mouse.get_pos())
# Update current UI
if self.current_screen == "main_menu":
self.draw_main_menu()
elif self.current_screen == "video_library":
self.video_library.draw()
elif self.current_screen == "camera":
self.camera_ui.update()
self.camera_ui.draw()
elif self.current_screen == "led_effects":
self.draw_led_effects_menu()
elif self.current_screen == "settings":
self.draw_settings()
# Update display
pygame.display.flip()
clock.tick(30)
# Cleanup
self.cleanup()
def draw_main_menu(self):
"""Draw the main menu screen"""
self.screen.fill((0, 0, 0)) # Black background
# Draw title
font = pygame.font.SysFont(None, 36)
title = font.render("BoomBox Pi", True, (255, 255, 255))
self.screen.blit(title, (self.width//2 - title.get_width()//2, 10))
# Draw buttons
for button in self.main_menu_buttons:
pygame.draw.rect(self.screen, (50, 50, 200),
(button[0], button[1], button[2], button[3]),
border_radius=10)
font = pygame.font.SysFont(None, 24)
text = font.render(button[4], True, (255, 255, 255))
self.screen.blit(text, (button[0] + button[2]//2 - text.get_width()//2,
button[1] + button[3]//2 - text.get_height()//2))
def draw_led_effects_menu(self):
"""Draw LED effects selection menu"""
self.screen.fill((0, 0, 0))
# Draw title and back button
font = pygame.font.SysFont(None, 30)
title = font.render("LED Effects", True, (255, 255, 255))
self.screen.blit(title, (self.width//2 - title.get_width()//2, 10))
# Draw back button
pygame.draw.rect(self.screen, (100, 100, 100), (10, 10, 60, 30), border_radius=5)
back_font = pygame.font.SysFont(None, 24)
back_text = back_font.render("Back", True, (255, 255, 255))
self.screen.blit(back_text, (10 + 30 - back_text.get_width()//2,
10 + 15 - back_text.get_height()//2))
# Draw effect buttons
effects = [
{"name": "Spectrum", "effect": "spectrum", "color": (255, 100, 100)},
{"name": "VU Meter", "effect": "vu_meter", "color": (100, 255, 100)},
{"name": "Pulse", "effect": "pulse", "color": (100, 100, 255)},
{"name": "Rainbow", "effect": "rainbow", "color": (255, 255, 100)},
{"name": "Fire", "effect": "fire", "color": (255, 150, 50)},
{"name": "Starfield", "effect": "starfield", "color": (150, 150, 255)}
]
button_height = 40
for i, effect in enumerate(effects):
y = 60 + i * (button_height + 10)
pygame.draw.rect(self.screen, effect["color"],
(20, y, self.width - 40, button_height),
border_radius=5)
text = back_font.render(effect["name"], True, (0, 0, 0))
self.screen.blit(text, (self.width//2 - text.get_width()//2,
y + button_height//2 - text.get_height()//2))
def draw_settings(self):
"""Draw settings menu"""
self.screen.fill((0, 0, 0))
# Draw title and back button
font = pygame.font.SysFont(None, 30)
title = font.render("Settings", True, (255, 255, 255))
self.screen.blit(title, (self.width//2 - title.get_width()//2, 10))
# Draw back button
pygame.draw.rect(self.screen, (100, 100, 100), (10, 10, 60, 30), border_radius=5)
back_font = pygame.font.SysFont(None, 24)
back_text = back_font.render("Back", True, (255, 255, 255))
self.screen.blit(back_text, (10 + 30 - back_text.get_width()//2,
10 + 15 - back_text.get_height()//2))
# Draw settings options
settings = [
{"name": "Volume", "icon": "🔊"},
{"name": "Brightness", "icon": "☀️"},
{"name": "WiFi", "icon": "📶"},
{"name": "Update", "icon": "🔄"},
{"name": "Shutdown", "icon": "⏻"}
]
button_height = 40
for i, setting in enumerate(settings):
y = 60 + i * (button_height + 10)
pygame.draw.rect(self.screen, (80, 80, 80),
(20, y, self.width - 40, button_height),
border_radius=5)
text = back_font.render(f"{setting['icon']} {setting['name']}", True, (255, 255, 255))
self.screen.blit(text, (self.width//2 - text.get_width()//2,
y + button_height//2 - text.get_height()//2))
def handle_click(self, pos):
"""Handle mouse clicks based on current screen"""
x, y = pos
if self.current_screen == "main_menu":
# Check main menu buttons
for button in self.main_menu_buttons:
if (button[0] <= x <= button[0] + button[2] and
button[1] <= y <= button[1] + button[3]):
self.current_screen = button[5]
return
elif self.current_screen == "video_library":
# Let the video library handle its own clicks
if self.video_library.handle_click(pos):
# If a video was selected, it will be played
pass
elif self.current_screen == "camera":
# Let the camera UI handle its own clicks
if self.camera_ui.handle_click(pos):
pass
elif self.current_screen == "led_effects":
# Check back button
if 10 <= x <= 70 and 10 <= y <= 40:
self.current_screen = "main_menu"
return
# Check effect buttons
effects = ["spectrum", "vu_meter", "pulse", "rainbow", "fire", "starfield"]
button_height = 40
for i, effect in enumerate(effects):
y_pos = 60 + i * (button_height + 10)
if 20 <= x <= self.width - 20 and y_pos <= y <= y_pos + button_height:
self.led_effects.change_effect(effect)
print(f"Changed LED effect to
Retry
Claude hit the max length for a message and has paused its response. You can write Continue to keep the chat going.
E
can you finish?Edit
Let me finish the code for handling LED effect selection in your BoomBox Pi interface:
pythonCopy def handle_click(self, pos):
"""Handle mouse clicks based on current screen"""
x, y = pos
if self.current_screen == "main_menu":
# Check main menu buttons
for button in self.main_menu_buttons:
if (button[0] <= x <= button[0] + button[2] and
button[1] <= y <= button[1] + button[3]):
self.current_screen = button[5]
return
elif self.current_screen == "video_library":
# Let the video library handle its own clicks
if self.video_library.handle_click(pos):
# If a video was selected, it will be played
pass
elif self.current_screen == "camera":
# Let the camera UI handle its own clicks
if self.camera_ui.handle_click(pos):
pass
elif self.current_screen == "led_effects":
# Check back button
if 10 <= x <= 70 and 10 <= y <= 40:
self.current_screen = "main_menu"
return
# Check effect buttons
effects = ["spectrum", "vu_meter", "pulse", "rainbow", "fire", "starfield"]
button_height = 40
for i, effect in enumerate(effects):
y_pos = 60 + i * (button_height + 10)
if 20 <= x <= self.width - 20 and y_pos <= y <= y_pos + button_height:
self.led_effects.change_effect(effect)
print(f"Changed LED effect to {effect}")
return
elif self.current_screen == "settings":
# Check back button
if 10 <= x <= 70 and 10 <= y <= 40:
self.current_screen = "main_menu"
return
# Check settings buttons
settings = ["volume", "brightness", "wifi", "update", "shutdown"]
button_height = 40
for i, setting in enumerate(settings):
y_pos = 60 + i * (button_height + 10)
if 20 <= x <= self.width - 20 and y_pos <= y <= y_pos + button_height:
self.handle_setting(setting)
return
def handle_setting(self, setting):
"""Handle settings options"""
if setting == "volume":
# Volume control would be implemented here
print("Volume control")
elif setting == "brightness":
# Brightness control would be implemented here
print("Brightness control")
elif setting == "wifi":
# WiFi settings would be implemented here
print("WiFi settings")
elif setting == "update":
# Update system would be implemented here
print("Update system")
elif setting == "shutdown":
# Shutdown system
print("Shutting down...")
os.system("sudo shutdown -h now")
def play_video(self, video_path):
"""Play a video using VLC"""
if self.player:
self.player.stop()
import vlc
self.player = vlc.MediaPlayer(video_path)
self.player.set_fullscreen(True)
self.player.play()
def cleanup(self):
"""Clean up resources"""
# Stop any playing media
if self.player:
self.player.stop()
# Clean up camera
self.camera_ui.cleanup()
# Turn off LEDs
self.led_effects.pixels.fill((0, 0, 0))
self.led_effects.pixels.show()
# Quit pygame
pygame.quit()
print("BoomBox Pi cleaned up and exiting")
# Main entry point
if __name__ == "__main__":
# Ensure required directories exist
media_path = os.path.expanduser("~/boombox_media")
os.makedirs(os.path.join(media_path, "music_videos_1"), exist_ok=True)
os.makedirs(os.path.join(media_path, "music_videos_2"), exist_ok=True)
os.makedirs(os.path.join(media_path, "music_videos_3"), exist_ok=True)
os.makedirs(os.path.join(media_path, "camera_recordings"), exist_ok=True)
os.makedirs(os.path.join(media_path, "thumbnails"), exist_ok=True)
# Install required dependencies if needed
try:
import vlc
except ImportError:
print("Installing python-vlc...")
os.system("pip3 install python-vlc")
try:
from picamera import PiCamera
except ImportError:
print("Installing picamera...")
os.system("pip3 install picamera")
# Start the BoomBox Pi application
app = BoomBoxPi()
app.run()
6. Final Setup
To complete the setup, here are the dependencies you’ll need to install on your Raspberry Pi:
bashCopy# Basic dependencies
sudo apt update
sudo apt install -y python3-pip python3-pygame ffmpeg vlc
# Python libraries
sudo pip3 install picamera python-vlc RPi.GPIO numpy adafruit-circuitpython-neopixel pyalsaaudio
7. Running the BoomBox Pi
To run your BoomBox Pi with the video library setup:
- Save the complete code to a file named
boombox_pi.py
- Make it executable:
bashCopychmod +x boombox_pi.py
- Run it:
bashCopypython3 boombox_pi.py
- To have it start automatically on boot, add it to
/etc/rc.local
:
bashCopysudo nano /etc/rc.local
Add this line before exit 0
:
Copycd /home/pi && python3 boombox_pi.py &
8. Using Your Video Library System
Here’s how to use the completed system:
- Adding Videos:
- Connect to your Pi via SSH or SFTP
- Upload videos to the corresponding folders in
~/boombox_media/
- Thumbnails will be automatically generated
- Recording Videos:
- Select “Camera” from the main menu
- Press the “Record” button to start recording
- Press “Stop” to finish – video will be saved to the camera_recordings folder
- Playing Videos:
- Browse through the video library
- Tap on a thumbnail to play the video
- Use the back button to return to folders
- LED Effects:
- Choose different visualization patterns from the LED Effects menu
- Effects will automatically sync with music when videos are playing
Your BoomBox Pi now has a complete media center with organized video folders, thumbnail previews, camera recording capabilities, and music-reactive LED effects. The interface is designed to work well on your 2.8″ touch display, with large buttons and simple navigation for ease of use.