Audio-reactive laser show driven by an electric guitar. Audio comes in through a Focusrite Scarlett, gets analysed for pitch, chord, dynamics and attacks, and goes out as point data to an Ether Dream 4 DAC feeding an RGB projector. Built for a blues-rock band playing through haze, where the audience sees the beams in the air rather than a picture on a wall.
Everything reacts to what is actually being played: silence means darkness, notes place and colour the figures, chords add cones, bends move the light, and pick attacks punch it.
The projector is a Class 4 laser. It can cause permanent eye injury, and a reflection off a cymbal, a chrome mic stand or a glossy guitar top can send a beam somewhere you did not plan.
- Mount high and aim level or upward. Never scan the audience or the band.
- Use
--y-floorto make that a software guarantee as well as an aiming one. It is enforced on every point of every mode, after all modulations:--y-floor 0.0confines the show to the top half of the scan field. - Leave
--brightnessat its 0.25 default while rehearsing and setting up. Raise it deliberately, and only once the beam paths are walked. - Spacebar is the panic key — instant blackout, any time.
- Walk the room with haze running and the brightness low before the audience is in it, looking for reflections off hardware.
--y-floor protects the room even if the projector is wired with inverted Y,
because the axis flips are applied after the safety clamp.
cd laser-morph
cargo build --releaseRequirements: Rust (1.94+), ALSA headers (libasound2-dev), an Ether Dream 4
on the LAN or on a direct Ethernet link, and an audio interface.
Check the machine can see your interface, then look at the analysis without touching the laser:
./target/release/laser-morph --list-devices
./target/release/laser-morph --dry-run # play guitar, watch the numbers--dry-run prints the detected notes, onset strength, loudness and the
measured noise floor. If the notes match what you are playing, the hard part
is already working.
Then calibrate the projector once (see below), and run the show:
./target/release/laser-morphFraming, point rate and scan polarity are properties of a projector, not of a run, so they are calibrated once and saved.
./target/release/laser-morph --calibrateThis streams a framing figure — a square that traces the exact window border, plus a circle and diagonals for judging scanner fidelity — and takes live keys:
| Key | Effect |
|---|---|
| arrows | move the window (shrink it first — a full-size window has nowhere to go) |
[ ] |
scale the window down / up |
w W |
narrower / wider |
h H |
shorter / taller |
p P |
point rate down / up in 1 kpps steps (playback restarts at the new rate) |
x y |
mirror the X / Y axis |
c C |
colour delay down / up |
0 |
reset the window to the full field |
s |
save |
q |
save and quit |
Point rate. Galvo ratings ("30 kpps") are quoted at a small scan angle; at
show-sized angles the usable rate is lower. Walk P upward until the square's
corners round off, the circle goes lumpy, or the diagonals ripple, then back
off about 15%.
Colour delay. Galvos lag the colour modulation, so RGB can land further
along the scan path than the geometry it belongs to and a moving trace shows
offset coloured copies. Nudge c/C until the copies merge into one trace.
Settings are written to ~/.config/laser-morph/calibration.conf, keyed by head
IP, and loaded automatically on every later run — so swapping projectors needs
no flags. The file is plain text and safe to hand-edit at the venue:
[192.168.23.88]
pps = 24000
center_x = 0.020
center_y = 0.065
width = 0.840
height = 0.819
flip_x = false
flip_y = true
color_delay = -3Width and height are fractions of the full scan field, where 1.0 spans
everything. --config <path> relocates the file, --no-config ignores it.
Single keypresses, no Enter needed. These work during the show.
| Key | Effect |
|---|---|
| space | blackout toggle — the panic key |
m |
cycle visual mode |
1–6 |
toggle modulations (they stack) |
+ - |
brightness |
| arrows | move the output window |
[ ] |
resize the output window |
, . |
scope height (direct mode waveform gain) |
| PgUp/PgDn | scope vertical offset |
c C |
colour delay |
x y |
mirror X / Y |
s |
save current framing to the calibration file |
q |
quit, stopping the DAC cleanly |
The show can also be driven from another shell, which is handy when your hands are on the guitar:
kill -USR1 <pid> # next visual mode
kill -USR2 <pid> # toggle blackoutCycle with m, or start in one with --mode <name>.
direct — a triggered oscilloscope. A fixed-rate horizontal sweep with the
beam deflected vertically by the raw audio signal, no interpolation, about
100 ms behind your hands. It triggers on a rising zero crossing so sustained
notes stand still instead of scrolling. In haze the sweep is a sheet of
light. This is the money mode for solos.
fan — nine stationary beams fanned across the stage, one per slice of the
spectrum, low frequencies on the left. The galvos dwell on each point, so the
beams read as solid bright shafts rather than a thin moving trace. The rack
sweeps slowly for parallax, its spread breathes with loudness, and pick attacks
flash every beam at once.
cone — nested cones overhead, one per sounding note. The lowest note sets
the outer aperture and every note above nests inward by frequency ratio, so
the gap between cones is the interval being played. The innermost cone carries
the live waveform, bends steer the whole nest sideways, and attacks fire
expanding shockwave halos.
pitch-axis — one Lissajous figure per sounding note, positioned on a
log-frequency axis (low notes left, high right) and sized by amplitude. Bends
slide the figures along the axis.
scope — the live waveform wrapped into a ring; note frequency sets the
lobe count, amplitude the ring size.
weave — harmonograph. Every sounding note adds its own oscillation into
one braided curve; chords produce intricate weaves.
mandala — the 48-band spectrum as a mirrored, slowly rotating radial
figure that bulges wherever the tone has energy.
Through haze, direct, fan and cone are the strongest: they paint large
surfaces and bright shafts in the air. The intricate figure modes read better
against a surface behind the band.
Six independent toggles on keys 1–6; any combination can be active at once,
and they are additive. Set defaults at launch with repeated --mod flags.
| Key | Name | Driven by | Effect |
|---|---|---|---|
| 1 | rise | bend | lifts the trace (never downward) |
| 2 | slide | bend | displaces it horizontally |
| 3 | hue | bend | rotates the palette, ~60°/semitone |
| 4 | flare | bend | brightness surges with bend depth |
| 5 | height | register | low notes fat, high notes a thin ribbon |
| 6 | elevator | register | trace rides up and down with pitch |
A good starting point for a solo: 1 + 4 + 6.
--palette chromatic (default) maps pitch class to a colour wheel;
--palette blues maps register through amber lows, red, violet and ice-blue
highs — moodier, and register reads instantly.
Hue follows continuous pitch, so bends glide the colour rather than snapping at semitone boundaries, and hue changes are eased along the shortest arc of the wheel so the several sweeps sitting in the DAC buffer are always close in colour.
--air-color (default 0.8) evens out perceived brightness across hues. This
matters in haze: saturated blue is roughly a twentieth as visible as green per
watt, so without compensation the ice-blue high notes would be the dimmest
thing in the show. Dim hues desaturate toward white until they read as bright
as the rest. Set 0 for raw saturated colour.
Heads are declared as IP@PPS and the flag repeats:
./target/release/laser-morph \
--head 192.168.23.88@24000 \
--head 192.168.23.89@14000Each head gets its own discovery, stream thread and audio tap, and all draw the same frame. Frame length is sized for the slowest head so every projector meets the refresh target, while blanking is computed at the fastest head's rate so the quickest mirrors still get their full slew time.
The interface does not appear at all. Check lsusb for it. If nothing
shows up on plug/unplug, the cable is almost certainly charge-only — this cost
us an evening. Swap it before suspecting drivers.
"no capture device matching..." ALSA presents the Scarlett to cpal as
plughw:CARD=USB,DEV=0, never as "Scarlett"; the app resolves the card ID from
/proc/asound/cards automatically. --list-devices shows what it can see, and
--device <substring> forces a choice.
The laser stays lit when nothing is playing. Run --dry-run and look at
floor. The gate adapts to your rig's measured noise floor, but it caps at
-34 dBFS, so genuinely loud interference will hold it open. A 60 Hz comb filter
already removes mains buzz; if you are testing on a laptop mic, check its
capture gain, which is often set absurdly high.
Notes are not detected. Heavy distortion smears the spectrum and fewer
distinct notes survive; clean and edge-of-breakup tones track best. Check the
--dry-run note readout to see what the analyser actually hears.
The image is upside down or mirrored. Projector heads differ in scan
polarity. Press x/y in --calibrate and save.
One trace appears as offset coloured copies. Nudge the colour delay with
c/C until they merge, then save.
Arrows do not move the window. A full-size window has nowhere to go —
shrink it with [ first. The app says so when it happens.
Three decoupled threads, so an audio hiccup can never stall the point stream:
- Capture (
audio.rs) — cpal pulls mono audio from the interface, runs a 16-notch 60 Hz comb filter to kill mains buzz, and feeds both a ring buffer for analysis and one decimated tap per projector for direct mode. - Analysis and geometry (
analysis.rs,shapes.rs) — a 4096-point FFT at ~30 Hz yields band energies, spectral centroid, onsets from spectral flux, an adaptive noise gate, and polyphonic notes by iterative harmonic salience. The active mode turns those into a frame of points. - Streaming (
stream.rs) — keeps the DAC buffer fed, maps every point into the output window, applies the safety floor, colour compensation, colour delay and axis flips, and converts to DAC coordinates.
config.rs handles the per-projector calibration file. Run cargo test for
the suite (58 tests), which covers pitch and onset detection, gate behaviour,
frame geometry for every mode, the safety clamps, and the DAC buffer
arithmetic.
-
docs/manual.html— the operating manual, laid out for use at the rig: safety, calibration steps, the live key reference, what each mode is for, and symptom-to-fix troubleshooting. A single self-contained file with no external assets, so it works offline. Open it straight from disk:xdg-open docs/manual.html # or just drag it into a browser -
docs/superpowers/specs/2026-08-13-laser-fft-morphs-design.md— the original design document. -
ed_find.py,ed_hello.py— diagnostic scripts for the DAC itself: listen for its broadcasts, and open a control connection to read its status and firmware version.