ADR-0002: Gamma Belongs to the Firmware, Not the Wire

StatusAccepted
Date2026-09-07
ScopeWhich layer makes which color decision between a cover image and the strip.
BackfilledDecided with the first vertical and argued in the project READMEs and Boundaries; written up here on 2026-10-10.
Corrected2026-10-13: the consequence about OKLab said the conversion is implemented three times, in Python, Go and C. It is in two, and never was in the firmware. The decision is unchanged.
RelatedADR-0001

Context

A color passes through three programs on its way from a sleeve to a strip: palette extracts it, lightd turns swatches into a scene, and the firmware drives the LEDs. Several corrections have to happen somewhere along the way — rejecting dull swatches, boosting chroma, mapping into the strip’s gamut, and gamma, because WS2812 output is linear in duty cycle and perception is not.

Options considered

Correct everything in lightd. One place, easy to tune. But the right gamma curve is a property of one driver and one diffuser, so lightd would need to know which hardware each stand has, and the colors on the wire would no longer be the colors intended.

Correct everything in palette. It already does color math. But then it could not be tuned against a folder of JPEGs with no hardware in mind.

Split by what each layer knows.

Decision

Each layer owns the decisions it has the knowledge for, and is deliberately ignorant of the layer below:

  • palette knows nothing about music, MQTT or LEDs. Image bytes in, perceptual color data out. No role assignment, no LED correction.
  • lightd owns the hardware-facing color choices that are the same for any strip: rejecting low-chroma swatches, chroma boost in OKLab, and gamut mapping by walking chroma down, never by clipping channels.
  • The firmware owns gamma.

Consequences

  • palette can be developed and tested with no infrastructure at all.
  • What mosquitto_sub shows is the color actually intended.
  • A stand with a different strip or diffuser needs only its own curve.
  • palette and lightd both work in OKLab, a color space built to match what a person sees, because the questions they ask (which colors a sleeve is made of, whether a swatch is a color or a gray, how to make one more vivid without shifting its hue) have no good answer in red, green and blue. Each carries its own copy of the conversion, in Python and in Go, so that neither needs the other to build or run. A test in lightd holds its copy to values taken from palette’s; a change to the math in one is a change to the other and to those values. The firmware has no copy: it is sent colors already chosen and mixes them as they are (Layer Boundaries).
  • New knowledge has to be placed deliberately. Track and album metadata, for instance, belong to none of these three layers (ADR-0006, ADR-0009).