Files
PianoLED-Circle-Edition/src/config.h
T
prosolis 202adacf7a Add calibration patterns and a geometric note-to-LED map
Hardware arrives tomorrow, which makes this the blocking work: the appliance
has no console, so without it there is no way to answer "is pixel 0 at the end
I think it is" except by guessing.

Geometric mapping. NOTE_MAP_GEOMETRIC (default on) derives each key from
white-key geometry rather than semitone index: 52 white keys span the strip,
so a white key is LED_COUNT/52 pixels - about 3.38 at 176 LEDs, not 2 - with
black keys on the boundaries. The old linear map drifts within each octave,
worst at F, by up to ~0.87 LEDs (~6mm) even after an optimal offset and scale.
Set NOTE_MAP_GEOMETRIC=0 to restore it.

This exposed a real bug. Under the geometric map adjacent key spans overlap,
because the semitone pitch (~1.7 LEDs) is narrower than LEDS_PER_KEY. The
renderer painted unlit keys black, so a key erased its lit neighbour's pixels.
It now clears once and paints only lit keys. The linear map never overlapped,
so this could not have been found without the geometry change.

LED_OFFSET shifts every key, absorbing where the strip was actually cut and
where the profile ended up. Off-strip pixels are clipped, never wrapped.

Calibration patterns, selected by CC 20, with CC 21/22 setting the pixel for
the walk: ends (orientation and length), octaves (mapping drift), keys (whole
mapping at once), walk (finding LED_OFFSET), all (voltage droop at the far
end). Patterns run at the same brightness ceiling as normal operation, so none
can exceed the current budget the design already allows.

tools/calibrate.sh drives all of it from the PC over ALSA MIDI, and README
carries the six-step procedure in dependency order.

Verified: tests pass across fourteen configurations, now including both
mapping modes and positive, negative and reversed offsets. Both platforms
build clean - pianoled.uf2 for RP2350 and both Circle kernel images - with no
warnings from project sources.

Claude-Session: https://claude.ai/code/session_01TVCB25LBsmeteWvaSMz4Ne
2026-08-27 23:06:11 -07:00

226 lines
8.0 KiB
C

//
// config.h
//
// Piano LED Visualizer - all tunable parameters.
//
// Shared by every platform backend (Circle on a Pi Zero, RP2040/RP2350 on a
// Pico). Nothing here is platform-specific except the wiring block at the end.
//
// Every value in this file is a product decision that Phase 0 of
// PIANO-LED-CIRCLE-PLAN.md exists to answer. Bench-test on Raspberry Pi OS
// first, then transcribe the answers here and build once.
//
#ifndef _config_h
#define _config_h
// --------------------------------------------------------------------------
// Keybed and strip geometry (plan section 6)
// --------------------------------------------------------------------------
// An 88-key keybed spans MIDI notes 21 (A0) through 108 (C8).
#define MIDI_NOTE_MIN 21
#define MIDI_NOTE_MAX 108
#define KEY_COUNT (MIDI_NOTE_MAX - MIDI_NOTE_MIN + 1) // 88
// LEDs per key. At 144 LEDs/m, 2 per key spans 1.222m, which lines up with a
// standard 88-key keybed almost exactly.
#ifndef LEDS_PER_KEY
#define LEDS_PER_KEY 2
#endif
#define LED_COUNT (KEY_COUNT * LEDS_PER_KEY) // 176
// Strip orientation. Pixel 0 of a WS2812B strip is at the end the data line
// enters. Decide this AFTER the strip is physically mounted, then flip this
// one flag.
//
// 0 = pixel 0 is at the bass end -> led = (note - 21) * 2
// 1 = pixel 0 is at the treble end -> led = (108 - note) * 2
#ifndef STRIP_REVERSED
#define STRIP_REVERSED 0
#endif
// Global shift, in pixels, applied after mapping. Absorbs where the strip was
// actually cut and where the profile ended up on the instrument - things the
// geometry cannot know. Positive moves every key towards higher pixel indices.
// Find it with calibration pattern 4 (single-LED walk); see README.
#ifndef LED_OFFSET
#define LED_OFFSET 0
#endif
// Note-to-LED mapping.
//
// 1 = geometric. Derives each key's position from white-key geometry:
// 52 white keys span the strip, so one white key is LED_COUNT/52
// pixels (~3.38 at 176 LEDs) with black keys on the boundaries.
// 0 = linear. The plan's original (note - 21) * LEDS_PER_KEY.
//
// Linear is wrong on a real keybed, because semitones are not evenly spaced:
// it drifts within each octave, worst at F, by up to ~0.87 LEDs (~6mm) even
// after an optimal offset and scale. Geometric removes that. Keep linear only
// to reproduce the original behaviour.
#ifndef NOTE_MAP_GEOMETRIC
#define NOTE_MAP_GEOMETRIC 1
#endif
// Number of white keys spanned by the strip. 52 for a standard 88-key keybed.
#ifndef WHITE_KEY_COUNT
#define WHITE_KEY_COUNT 52
#endif
// --------------------------------------------------------------------------
// Power safety (plan section 7) - NOT optional
// --------------------------------------------------------------------------
//
// 176 LEDs at full white draw ~60mA each = 10.56A theoretical maximum, against
// a 6A supply. Real playing never approaches that (a ten-finger chord lights 20
// LEDs, ~1.2A), but a firmware bug that whites out the strip would brown out
// the rail. These two clamps make that unreachable rather than unlikely.
// Global brightness ceiling, applied to every channel of every pixel.
// 0-255. At 96 a full-strip white would draw roughly 4A, still inside 6A.
#ifndef GLOBAL_BRIGHTNESS
#define GLOBAL_BRIGHTNESS 96
#endif
// Hard cap on simultaneously lit keys. Beyond this, further held notes are
// tracked but not lit, so current draw stays bounded no matter what arrives
// on the wire. 20 keys is a ten-finger chord; 30 leaves room for pedal-held
// passages without ever approaching the supply limit.
#ifndef MAX_LIT_KEYS
#define MAX_LIT_KEYS 30
#endif
// --------------------------------------------------------------------------
// Colour (Phase 0 decides these against the actual diffuser)
// --------------------------------------------------------------------------
//
// Colours look substantially different through a diffuser than on bare strip.
// Do not finalise these from a photo.
// Colour for a played key, before brightness scaling.
#ifndef NOTE_COLOR_R
#define NOTE_COLOR_R 0
#endif
#ifndef NOTE_COLOR_G
#define NOTE_COLOR_G 140
#endif
#ifndef NOTE_COLOR_B
#define NOTE_COLOR_B 255
#endif
// Distinct colour for a "next note to play" hint driven by learning software
// on the PC (plan Phase 3). Reached over MIDI channel HINT_MIDI_CHANNEL.
#ifndef HINT_COLOR_R
#define HINT_COLOR_R 255
#endif
#ifndef HINT_COLOR_G
#define HINT_COLOR_G 80
#endif
#ifndef HINT_COLOR_B
#define HINT_COLOR_B 0
#endif
// --------------------------------------------------------------------------
// Velocity response
// --------------------------------------------------------------------------
// 1 = velocity scales pixel brightness, 0 = every key lights at full
// GLOBAL_BRIGHTNESS regardless of how hard it was struck.
#ifndef VELOCITY_SENSITIVE
#define VELOCITY_SENSITIVE 1
#endif
// Floor for velocity scaling, as a percentage. A pianissimo note should still
// be clearly visible, so velocity maps onto [VELOCITY_FLOOR_PCT, 100] rather
// than onto [0, 100].
#ifndef VELOCITY_FLOOR_PCT
#define VELOCITY_FLOOR_PCT 35
#endif
// --------------------------------------------------------------------------
// MIDI routing
// --------------------------------------------------------------------------
// Channel carrying notes actually played on the piano. 0-15 on the wire
// (channel 1 in a DAW), or MIDI_CHANNEL_ANY to accept every channel.
#define MIDI_CHANNEL_ANY 0xFF
#ifndef NOTE_MIDI_CHANNEL
#define NOTE_MIDI_CHANNEL MIDI_CHANNEL_ANY
#endif
// Channel reserved for Phase 3 "light the next key" hints from the PC. Kept
// separate from played notes so the two never overwrite each other. Set to
// MIDI_CHANNEL_NONE to ignore hints entirely.
#define MIDI_CHANNEL_NONE 0xFE
#ifndef HINT_MIDI_CHANNEL
#define HINT_MIDI_CHANNEL 15 // channel 16 in a DAW
#endif
// --------------------------------------------------------------------------
// Calibration (plan Phase 0)
// --------------------------------------------------------------------------
//
// This is a headless appliance with no console, so calibration is driven over
// MIDI - the one channel that already exists. Send these CCs from the PC; see
// tools/calibrate.sh.
//
// Patterns are a diagnostic overlay: while one is active it replaces the note
// display entirely, and pattern 0 restores normal operation.
// CC selecting the active pattern.
#ifndef CALIB_CC_PATTERN
#define CALIB_CC_PATTERN 20
#endif
// CCs setting the pixel index for CALIB_PATTERN_WALK, as a 14-bit value:
// index = (CC21 << 7) | CC22.
#ifndef CALIB_CC_INDEX_HI
#define CALIB_CC_INDEX_HI 21
#endif
#ifndef CALIB_CC_INDEX_LO
#define CALIB_CC_INDEX_LO 22
#endif
#define CALIB_PATTERN_OFF 0 // normal operation
#define CALIB_PATTERN_ENDS 1 // first and last pixel only
#define CALIB_PATTERN_OCTAVES 2 // every C, to expose mapping drift
#define CALIB_PATTERN_KEYS 3 // every key, alternating colour
#define CALIB_PATTERN_WALK 4 // one pixel, chosen by CC21/CC22
#define CALIB_PATTERN_ALL 5 // every pixel, for voltage droop testing
// --------------------------------------------------------------------------
// Hardware wiring - platform specific
// --------------------------------------------------------------------------
//
// Whichever board is used, plan section 7 still applies in full: the data line
// needs a 74AHCT125 to reach 5V logic, the board ground must be tied to the LED
// supply ground, and 5V must be injected at both ends of the strip. Neither
// board can power the strip itself.
#ifdef PLATFORM_PICO
// RP2040 / RP2350. The PIO state machine can drive the WS2812B waveform from
// any GPIO, so this is a free choice rather than a constraint.
#ifndef WS2812_PIN
#define WS2812_PIN 2
#endif
#else
// Circle on a Raspberry Pi. VERIFIED against circle/addon/WS28XX: CWS28XXStripe
// clocks the WS2812B waveform out over SPI at a fixed 6.4MHz, encoding each LED
// bit as one SPI byte. On SPI master device 0 that fixes the data line at
//
// MOSI = GPIO10 (BCM) = physical pin 19
//
// It is not a free choice on this platform.
#ifndef SPI_MASTER_DEVICE
#define SPI_MASTER_DEVICE 0
#endif
#endif
#endif