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.
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
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.
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
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
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:
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?
| 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:
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)
PropConfig (8 bytes each)
Event Structure (48 bytes each)
Each event tells a prop what to do and when:
Supported LED Types
Color Orders
Effect Codes
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):
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:
app.go → bingen/ (native Go)
BinaryGeneratorWasm.js → bingen.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:
currentTime 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.