v0.2.4

PicoLume Studio

A visual timeline editor for designing choreographed LED light shows.

17
Built-in Effects
224
Prop IDs
50
Undo Steps
ZIP
Project Format

Overview

Studio is where PicoLume shows come to life. It's a visual timeline editor — if you've ever used video editing software, you'll feel right at home. Import your show audio, drag effects onto tracks, and watch your lighting design take shape.

The key insight behind Studio is that designing light shows shouldn't require writing code. Earlier versions of PicoLume required hand-coding every lighting sequence directly into the firmware. Studio changed everything — now anyone can design a full show by dragging and dropping.

Two Ways to Run Studio

Studio comes in two flavors: a desktop application (Windows, Mac, Linux) with full file system access and hardware upload capability, and an online version you can run directly in your browser for quick edits and experimentation.

Timeline Editing

The timeline is the heart of Studio. It displays your show audio as a waveform along the top, with lighting tracks stacked below. Each track contains clips — individual effect instances with a start time, duration, color, and parameters.

Working with the timeline feels natural:

Adding Effects

Drag an effect from the library panel and drop it onto a track. The clip appears at the drop position, ready to be sized and colored.

Editing Clips

Click a clip to select it and open the inspector panel. Adjust color, duration, speed, width, and other parameters in real-time.

Moving & Resizing

Drag clips to reposition them. Drag the edges to resize. Hold Shift for precision snapping to the grid.

Copy & Paste

Select clips and use Ctrl+C / Ctrl+V to duplicate them. Great for repeating patterns across the timeline.

Effects Library

Studio includes 17 built-in effects that cover most lighting scenarios you'll encounter in a marching show. Each effect can be customized with color, speed, and width parameters.

Off
Turn LEDs off
Solid Color
Static fill with any color
Camera Flash
Bright burst like a camera flash
Strobe
Rapid on/off flashing
Rainbow Chase
Colors flowing along the strip
Rainbow Hold
Static rainbow gradient
Chase
Single color running along strip
Wipe
Color fills then clears
Scanner
Larson scanner / Knight Rider
Meteor
Falling comet with trail
Fire
Flickering flame simulation
Heartbeat
Pulsing glow pattern
Glitch
Random digital glitch effect
Energy
Pulsing energy waves
Sparkle
Random twinkling points
Breathe
Slow fade in/out
Alternate
Two-color alternating pattern

All effects support configurable speed (0.1x to 5x) and width parameters where applicable. Colors are full 24-bit RGB with a visual color picker.

Prop Groups

Real shows have structure: flags do one thing, percussion does another, and the front ensemble has its own patterns. Prop Groups let you organize your 224 available prop IDs into logical collections.

Each track can target a specific group, so when you place a clip on the "Flags" track, only the props assigned to that group will execute the effect. This makes designing complex, multi-section choreography much more manageable.

Example Groups

6
Small Props (Props 1-6)
8
Large Props (Props 7-14)
4
Feature Props (Props 15-18)

Preview & Playback

Studio includes a real-time preview panel that simulates how your props will look during playback. As the timeline plays, the preview renders each active effect so you can see timing and color choices before ever loading the show onto hardware.

The preview isn't pixel-perfect (your actual LED strips might be longer or use different color orders), but it's close enough to catch timing issues and design problems early.

Audio Sync

Playback syncs to your imported audio file, so you can see exactly how effects line up with musical cues.

Scrubbing

Click anywhere on the timeline to jump to that moment. The preview updates immediately.

Export & Upload

When your design is ready, Studio exports two types of files:

.lum Project File

A ZIP archive containing your project data and audio. Use this for saving, sharing, and continuing work later.

show.bin Hardware Binary

A compact binary file that receivers load via USB. Contains all events, timing, and per-prop configuration.

The desktop version of Studio can also upload show.bin directly to connected receivers via USB Mass Storage mode — no file manager needed.

Keyboard Shortcuts

Power users will appreciate the keyboard shortcuts. All the standard editing commands are supported, plus a few specific to timeline work.

Ctrl + N New project
Ctrl + O Open project
Ctrl + S Save project
Ctrl + Shift + S Save as...
Ctrl + Z Undo
Ctrl + Y Redo
Ctrl + C Copy selected clips
Ctrl + V Paste clips
Ctrl + A Select all clips
Delete Delete selected clips
Space Play / Pause
Escape Clear selection

Under the Hood

For the curious, here's a deep dive into how Studio is built. PicoLume Studio follows a restaurant-like separation of concerns — just like a restaurant has clear boundaries between the dining room, waiters, kitchen, and pantry, the app has clean separation between components.

Desktop Framework

Built with Wails — a Go + web UI framework that produces ~10MB apps (vs 100MB+ for Electron). WebView2 on Windows gives native performance.

Frontend

Vanilla JavaScript with a custom StateManager. No React or Vue — just clean, direct DOM manipulation for maximum control over our single-page timeline editor.

Rendering

Timeline and preview use HTML5 Canvas for smooth, high-performance rendering at 60fps. Interactive clips use DOM for accessibility.

Backend

Go handles file I/O, ZIP compression, binary generation, and USB communication. Fast, reliable, and cross-platform.

Single Source of Truth

All application data lives in a centralized StateManager. Controllers handle user input, services do the work, and renderers paint the screen — but they all read from and write to the same state. This pattern eliminates "the UI shows one thing but the data says another" bugs.

Architecture Overview

PicoLume Studio's architecture mirrors a restaurant's organization. Each layer has a clear job, and data flows in one direction:

Restaurant Studio Component Role
Dining Room Frontend (HTML/CSS/JS) What users see and interact with
Waiters Controllers Handle user actions, route requests
Kitchen Services Business logic, audio, file operations
Pantry StateManager Central data store, single source of truth
Back Office Go Backend File I/O, USB, binary generation
Delivery Truck Wails Bridge Connects JavaScript to Go

File Structure

studio/
├── main.go ← App entry (Go)
├── app.go ← Wails API layer
├── device.go ← Hardware I/O & interfaces
├── project.go ← File validation & MIME
├── bingen/ ← Shared binary generation
├── wasm/ ← WebAssembly entry point
├── scripts/ ← Build scripts
└── frontend/
├── index.html
└── src/
├── main.js ← App entry (JS)
├── core/ ← StateManager, Backend
├── services/ ← Audio, Project
├── controllers/ ← Timeline, Keyboard, Cue
├── views/ ← Renderers
└── wasm/ ← Compiled WASM assets

The naming convention tells you what a file does: *Service.js files handle business logic, *Controller.js files handle user actions, and *Renderer.js files draw to the screen.

Unidirectional Data Flow

Data flows one direction: User Input → Controller → Service → StateManager → Renderer. This makes bugs easy to trace. UI showing wrong data? Check the renderer. Renderer correct but data wrong? Check the service that updated state.

Wails Bridge

Wails creates a bridge between JavaScript and Go — two different languages with different paradigms. When JavaScript needs something from Go (like saving a file or uploading to hardware), it goes through this bridge.

How It Works

Go functions on the App struct become callable from JavaScript as window.go.main.App.FunctionName(). Wails auto-generates the bindings.

JSON Serialization

All parameters are JSON-serialized when crossing the bridge. This means no functions, DOM elements, or circular references can cross — only plain data.

Promise Returns

Every Go function returns a Promise in JavaScript. Use await to get the result. Go errors become rejected promises.

Events for Progress

Long operations use Wails events for progress updates. Go emits upload:status events, and JavaScript subscribes to receive them.

Go Type JavaScript Type Notes
string string Direct mapping
int, float64 number All become JS number
bool boolean Direct mapping
[]T (slice) Array Slices become arrays
map[string]T Object Maps become objects
struct Object JSON tags control property names

Backend Adapter Pattern

Studio runs in two modes: desktop app (Wails) and online browser version. The Backend adapter (getBackend()) returns the right implementation. Desktop gets full file system and USB access; online uses browser APIs with limited capability. Code stays the same — only the backend changes.

Available Backend Functions

RequestSavePath() Open save dialog, get path
SaveProjectToPath() Save .lum project file
LoadProject() Load .lum from file picker
SaveBinaryData() Export show.bin file
UploadToPico() Generate + upload to device
GetPicoConnectionStatus() Check device connection

Service Layer

Services are the "kitchen" of our restaurant analogy — they know how to do things. Controllers know what the user wants; services know how to make it happen.

Aspect Service Controller
Purpose Business logic User interaction handling
Knows About How to do things What the user wants
Calls Backend, StateManager Services, StateManager
Example "Save project to ZIP format" "User clicked Save button"

AudioService

Manages everything sound-related using the Web Audio API:

  • Load and decode audio files
  • Create AudioBuffer from data URLs
  • Start/stop playback with timing
  • Manage AudioContext and GainNode
  • Retry with exponential backoff

ProjectService

Handles the project lifecycle — everything about saving and loading:

  • Create new projects
  • Save/load .lum files
  • Export show.bin binaries
  • Upload to hardware
  • Debounced auto-save (2s delay)

Resilience Patterns

Services use timeout and retry patterns for reliability. Audio decoding gets 60 seconds with 2 retries (exponential backoff). File reads get 30 seconds. Network operations timeout after 30 seconds. These patterns prevent the app from hanging on failed operations.

Controllers

Controllers are like air traffic controllers — they don't fly the planes or fuel them, they coordinate. They route user actions to the right services and ensure nothing collides.

TimelineController

The biggest controller — handles all timeline operations: adding/deleting tracks, clip CRUD, selection management, copy/paste, and drag operations.

KeyboardController

Registers global keyboard shortcuts at startup. Routes Ctrl+S to save, Space to play/pause, Delete to remove clips, etc.

UndoController

Manages undo/redo UI state — enables/disables buttons based on history stack depth. StateManager does the actual undo logic.

ThemeManager

Handles theme switching with localStorage persistence. Remembers last-used light and dark themes for quick toggling.

SidebarModeManager

Controls sidebar state — switches between menu mode and inspector mode based on user actions.

MenuRenderer

Renders menu items and executes actions. Actions are defined as handlers passed during initialization.

Custom Events (Pub/Sub Pattern)

Controllers dispatch custom DOM events to notify other parts of the app. This decouples components — controllers don't need to know who's listening:

app:timeline-changed → TimelineRenderer
app:selection-changed → InspectorRenderer
app:time-changed → Playhead update
app:zoom-changed → Timeline resize

skipHistory for Ephemeral Changes

Not all state changes should be undoable. Selection changes, playhead position, and audio source tracking use skipHistory: true to avoid polluting the undo stack with hundreds of rapid updates.

State Management

At the heart of Studio is the StateManager — a single JavaScript object that holds every piece of data the application needs. Think of it like a restaurant's pantry: everything is stored in one place, and everyone knows where to look.

What Lives in State?

project — tracks, clips, groups, settings
playback — isPlaying, currentTime
selection — selectedClipIds array
ui — zoom, snapEnabled, gridSize
audioLibrary — bufferId → data URL
assets — bufferId → AudioBuffer
isDirty — unsaved changes flag
filePath — current project path
Saved to .lum NOT Saved (Ephemeral)
project.* selection
audioLibrary playback
ui (zoom, snap)
isDirty, filePath

The pattern is simple but powerful: components subscribe to specific paths, and when those paths change, the component re-renders. This reactive system means you never have to manually synchronize UI with data.

Path-Based Access

Access nested data with dot notation: state.get('project.tracks.0.clips'). Subscribe to specific paths for targeted updates.

Immutable Updates

State is never mutated directly. Use state.update(draft => {...}) — the draft is a copy, keeping originals for undo.

50-Step Undo History

Every state change is pushed to a history stack (up to 50 entries). Ctrl+Z pops the stack. Redo works too.

Audio Objects Excepted

Web Audio API objects (AudioBuffer, AudioContext) aren't clonable — they're shallow-copied by reference.

The Observer Pattern

StateManager uses the Observer pattern: call state.subscribeTo('project.tracks', callback) to register a listener for specific changes. When state.update() modifies that path, subscribers are notified. This decouples data storage from UI rendering — the same pattern you'll find in Redux, Vue's reactivity, and DOM events.

File Formats

Studio uses two distinct file formats, each optimized for its purpose. Understanding them helps when troubleshooting or building tools that integrate with PicoLume.

Format Analogy Use Case
JSON (in memory) Your thoughts Working data in JavaScript
.lum (ZIP) Written document Saving projects for humans
show.bin Machine code Instructions for hardware
.lum Project File

A .lum file is just a ZIP archive with a different extension. Inside you'll find:

myshow.lum (ZIP)
├── project.json ← tracks, clips, groups, settings
└── audio/ ← folder with audio files
└── show_music.mp3

You can rename any .lum to .zip and extract it with any archive tool. Handy for debugging or batch-modifying projects.

Why ZIP Format?

  • Single JSON can't embed binary audio efficiently
  • Base64 in JSON bloats file size 33%
  • Folder of files gets lost when moving
  • ZIP: compression + standard tooling

Safety Limits

Zip bomb protection when loading .lum files:

  • Max .lum: 500 MB
  • Max project.json: 10 MB
  • Max audio file: 200 MB
  • Max files: 100
show.bin Hardware Binary

The compiled binary that receivers load at boot. Contains all timing events, prop configurations, and effect parameters in a compact format designed for the Pico's limited memory (2MB flash, 264KB RAM). No audio — receivers play effects based on timecode broadcast from the remote.

Binary Format

The show.bin format is the language receivers speak. It's a carefully designed binary structure that balances compactness with random access. Here's how it's organized:

Offset Size Section Purpose
0x0000 16 bytes Header Magic number, version, event count, duration
0x0010 1,792 bytes PropConfig LUT 8-byte config for each of 224 props
0x0710 48 bytes × N Events Lighting events with timing and parameters

Header (16 bytes)

4B Magic: "LUME"
2B Version (currently 3)
2B Reserved
4B Total events (uint32)
4B Duration in ms (uint32)

PropConfig (8 bytes each)

2B LED count (uint16)
1B LED type (WS2812B, etc.)
1B Color order (RGB/GRB/etc.)
1B Brightness (0-255)
3B Reserved

Event Structure (48 bytes each)

Each event tells a prop what to do and when:

4B Start time (ms)
4B Duration (ms)
1B Effect ID (0-16)
3B Primary RGB color
1B Speed (0-255)
1B Width (0-255)
28B Prop bitmask (224 bits = 1 bit per prop)

Supported LED Types

0 = WS2812B (most common)
1 = SK6812
2 = SK6812_RGBW
3 = WS2811
4 = WS2813
5 = WS2815

Color Orders

0 = GRB (most common)
1 = RGB
2 = BRG
3 = RBG
4 = GBR
5 = BGR

Effect Codes

1 solid
2 flash
3 strobe
4 rainbow
5 rainbowHold
6 chase
9 wipe
10 scanner
11 meteor
12 fire
13 heartbeat
14 glitch
15 energy
16 sparkle
17 breathe
18 alternate

The Prop Bitmask (28 bytes)

Instead of duplicating events for each prop, one event targets any combination of 224 props. The 28-byte bitmask is split into 7 × uint32 (4 bytes each):

propMask[0] bits 0-31 → props 1-32
propMask[1] bits 0-31 → props 33-64
propMask[2] bits 0-31 → props 65-96
... and so on to 224

File Size Calculation

Formula: 16 + 1792 + (eventCount × 48)

Example with 50 events: 16 (header) + 1,792 (prop config) + 2,400 (events) = 4,208 bytes (4.1 KB)

Binary Generation Architecture

Binary generation uses a single source of truth pattern. The same Go code powers both the desktop app and the browser version via WebAssembly:

Desktop (Wails)
app.gobingen/ (native Go)
Browser (Online)
BinaryGeneratorWasm.jsbingen.wasm

This ensures identical binary output across all environments — same code, same results.

Rendering Pipeline

Studio uses a hybrid rendering approach: standard DOM elements for panels, buttons, and forms, but HTML5 Canvas for the performance-critical parts — the timeline ruler, waveforms, and LED preview.

Use Case Technology Why
Interactive clips DOM Click handling, accessibility
Ruler tick marks Canvas Many elements, no interaction
Preview LEDs Canvas Custom drawing, animation
Waveforms Canvas Dense data visualization

TimelineRenderer

The most complex renderer. Handles track headers (DOM), track lanes with clips (DOM), time ruler (Canvas), grid (Canvas), waveforms (Canvas), and playhead (DOM + animation).

PreviewRenderer

Simulates LED output in real-time. Finds active clips at current time, calculates effect colors using the Strategy Pattern (different algorithm per effect), and draws LEDs with glow.

InspectorRenderer

Shows different content based on selection: project settings (nothing selected), clip properties (single), or batch operations (multiple).

The Render Loop

During playback, Studio runs a tight render loop using requestAnimationFrame:

1 Get current audio time from AudioService
2 Update StateManager with currentTime
3 TimelineRenderer redraws playhead position
4 PreviewRenderer evaluates active events and draws LEDs
5 Schedule next frame → repeat at 60fps

Drag & Drop State Machine

Clip dragging uses direct DOM manipulation for smooth feedback — transform during drag, then commits to state on mouseup. Single undo entry for entire operation.

Throttling

Preview rendering throttles to ~60fps (16ms minimum between frames). app:time-changed fires many times per second — we skip renders if under 16ms since last.

Renderers Are Painters

The mental model: renderers are painters. They read the script (state), paint the scene (DOM/Canvas), but never write the script (no state mutations). They update when cued by events like app:timeline-changed or app:selection-changed.

Try It

Ready to design a show?

Try the online version right in your browser — no download required.