Skip to content

Latest commit

 

History

28 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

laser-morph

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.


Safety first

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-floor to 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.0 confines the show to the top half of the scan field.
  • Leave --brightness at 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.


Getting started

cd laser-morph
cargo build --release

Requirements: 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-morph

Calibration

Framing, 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 --calibrate

This 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 = -3

Width and height are fractions of the full scan field, where 1.0 spans everything. --config <path> relocates the file, --no-config ignores it.


Live controls

Single keypresses, no Enter needed. These work during the show.

Key Effect
space blackout toggle — the panic key
m cycle visual mode
16 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 blackout

Visual modes

Cycle 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.


Modulations

Six independent toggles on keys 16; 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.


Colour

--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.


Two projectors

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@14000

Each 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.


Troubleshooting

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.


How it works

Three decoupled threads, so an audio hiccup can never stall the point stream:

  1. 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.
  2. 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.
  3. 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

  • 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.

About

audioreactive laser show with ILDA output

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages