EYThe LogEmre Yakut
← all entries
Simulation · Sound & light · deep

render(t): no accumulated state, so it can rewind

The light show had to rewind together with the track. What made that work was not a complex synchronisation scheme but a single rule: no effect may remember the previous frame.

t = 90 s’e atlamotor t−1’i hiç görmemiş olabilirdurum biriktiren:her karede ışığı bir ilerlet → t=90’da nerede?saf fonksiyon:phase = f(t) → her t bağımsız hesaplanırışık şovu zamanın saf fonksiyonu olmalı: render(t) → universe

I was writing my own engine to replace a paid DJ lighting product. The behaviour I was matching: pause the track and the show pauses, rewind and the show rewinds, jump into the middle and the show jumps with you.

To a user that is “synchronised”. To the code it means something far stricter.

The natural approach is the wrong one

The most natural way to describe a lighting effect is step by step:

// a chase effect
every frame:
  active = (active + 1) % lightCount
  turn all off, light the active one

It works. It looks good. Right up until the user grabs the timeline and jumps to second 90.

At that moment you have asked the engine: “which light should be on at second 90?” And it cannot know, because that number is the accumulated result of every frame before it. Computing it means replaying every frame from 0 to 90 — four thousand steps, on every seek.

One rule

the engine's only contract
render(t) → universe

For a given timecode t, return every channel’s value. The only input is t. The engine remembers nothing, accumulates nothing, and never needs t−1 to have been computed.

The same chase under that constraint:

active = floor(t × speed) % lightCount

A one-line difference. But now asking for second 90 costs exactly what asking for second 0 costs.

Every oscillation takes its phase from time

effectstateful (wrong)pure (right)
chasei = i + 1i = ⌊t·s⌋ mod n
strobeif(count%4==0) toggleon = (t·f) mod 1 < 0.5
fadev += stepv = (t−t₀)/duration
movementangle += speedangle = sin(2π·f·t)

What every expression on the right shares: it looks at nothing but t.

The test for any new effect is: can it be evaluated at an arbitrary t without having evaluated anything before it? If not, the effect gets rewritten.

What comes for free

Setting this rule at the start cost nothing. What it returned:

  • Seek and scrub — the original goal, solved by construction.
  • Pause — simply stop advancing t. No separate logic.
  • Slow and fast playback — advance t at a different rate; effects follow automatically.
  • Preview — show what happens at any t without playing anything.
  • Testability — testing an effect needs no 44-frame simulation; call render(37.5) and inspect.
  • Frame dropping — if the machine stalls, dropping frames is harmless; the next frame resumes in the right place.

The last one matters on a stage. In a stateful engine, one dropped frame leaves the show permanently behind the music and it never realigns.

The show itself is another layer

The same separation exists one level up. The show generator does not think about DMX channels. It produces abstract intent:

t=42.0  "warm amber wash, 60%"
t=45.5  "accent hit on the downbeat"
t=48.0  "haze four bars before the drop"

A separate render layer turns that intent into output. So the same choreography can drive DMX fixtures or the ZigBee bulbs at home.

Analysis is a separate layer too: analysing a track is slow (about half a minute), generating the show is fast (milliseconds). You will try the choreography dozens of times; re-analysing the track each time is pointless. The file that sits between them both enforces that separation and is hand-editable — if the algorithm misses a drop, you can intervene.

What I learned

Some constraints produce freedom. “Hold no state” looks like it makes effects harder to write; in reality it hands you six features for nothing.

And constraints like this have to be set at the beginning. Adding one later is not “a bit of refactoring”; it is rewriting the entire effect engine.

SimulationSound & light