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
This commit is contained in:
@@ -149,12 +149,68 @@ make -C firmware EXTRADEFINE=-DSTRIP_REVERSED=1 # Circle
|
|||||||
cmake -B pico/build -S pico -DCMAKE_CXX_FLAGS=-DSTRIP_REVERSED=1 # Pico
|
cmake -B pico/build -S pico -DCMAKE_CXX_FLAGS=-DSTRIP_REVERSED=1 # Pico
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Known limitation:** the note-to-LED mapping is linear in semitone index,
|
### Note-to-LED mapping
|
||||||
> but a real keybed is not — 52 white keys span the same 1222mm, so one white
|
|
||||||
> key is ~3.38 LEDs rather than 2. This drifts within each octave, worst at F,
|
`NOTE_MAP_GEOMETRIC` (default 1) derives each key's position from white-key
|
||||||
> by up to ~0.87 LEDs (~6mm) even after an optimal offset and scale. A
|
geometry: 52 white keys span the strip, so a white key is `LED_COUNT / 52`
|
||||||
> geometric map derived from white-key positions would remove it. Not yet
|
pixels — **~3.38 at 176 LEDs, not 2** — with black keys on the boundaries.
|
||||||
> implemented.
|
|
||||||
|
Setting it to 0 restores the plan's original `(note - 21) * 2`. That map is
|
||||||
|
linear in semitone index, but a keybed is not: it drifts within each octave,
|
||||||
|
worst at F, by up to ~0.87 LEDs (~6mm) even after an optimal offset and scale.
|
||||||
|
Keep it only to reproduce the original behaviour.
|
||||||
|
|
||||||
|
One consequence of the geometric map: adjacent key spans **overlap**, because
|
||||||
|
the semitone pitch (~1.7 LEDs) is narrower than `LEDS_PER_KEY`. That is
|
||||||
|
expected, and the renderer paints only lit keys so a neighbour cannot erase
|
||||||
|
them.
|
||||||
|
|
||||||
|
## Calibration
|
||||||
|
|
||||||
|
This is a headless appliance, so calibration runs over MIDI — the one channel
|
||||||
|
that already exists. `tools/calibrate.sh` drives it from the PC:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
tools/calibrate.sh list # find the port
|
||||||
|
tools/calibrate.sh ends # pixel 0 (red), last pixel (green)
|
||||||
|
tools/calibrate.sh octaves # every C, middle C in red
|
||||||
|
tools/calibrate.sh keys # every key: white green, black blue
|
||||||
|
tools/calibrate.sh walk 37 # one pixel only
|
||||||
|
tools/calibrate.sh sweep # walk every pixel in turn
|
||||||
|
tools/calibrate.sh all # every pixel — voltage droop test
|
||||||
|
tools/calibrate.sh off # back to normal
|
||||||
|
```
|
||||||
|
|
||||||
|
A pattern replaces the note display entirely while it is active; `off`
|
||||||
|
restores it.
|
||||||
|
|
||||||
|
### Procedure
|
||||||
|
|
||||||
|
Work in this order — each step depends on the one before.
|
||||||
|
|
||||||
|
1. **`ends`** — one pixel lights at each end of the strip. If red is at the
|
||||||
|
treble end, set `STRIP_REVERSED 1` and rebuild. If either end is dark, the
|
||||||
|
strip is not the length `LED_COUNT` assumes.
|
||||||
|
2. **`all`** — every pixel white. Watch the far end: if it drifts dim or warm,
|
||||||
|
the strip needs 5V injected at that end too. This draws roughly
|
||||||
|
`LED_COUNT × 3 × GLOBAL_BRIGHTNESS/255 × 20mA` — about 4A at the defaults,
|
||||||
|
inside a 6A supply but well beyond normal play, which caps at
|
||||||
|
`MAX_LIT_KEYS`.
|
||||||
|
3. **`keys`** — every key lit, whites and blacks in different colours. Check
|
||||||
|
the colours line up with the actual keys across the whole span. This is the
|
||||||
|
fastest way to see a mapping or length error.
|
||||||
|
4. **`octaves`** — every C, middle C in red. Drift shows up as the marks
|
||||||
|
walking off the keys as you move up the keyboard. With the geometric map
|
||||||
|
they should stay put.
|
||||||
|
5. **`sweep`** or **`walk <n>`** — step one pixel at a time until you find the
|
||||||
|
pixel sitting over A0. If that is not the pixel the firmware expects, the
|
||||||
|
difference is your `LED_OFFSET`. Set it and rebuild.
|
||||||
|
6. **`chromatic`** — plays every key in turn. Watch for the lit span leading
|
||||||
|
or lagging the key as it climbs.
|
||||||
|
|
||||||
|
Then decide the product questions the firmware cannot: colours *through the
|
||||||
|
diffuser* (not bare), brightness, and whether velocity should modulate
|
||||||
|
anything. All of them live in `src/config.h`.
|
||||||
|
|
||||||
## MIDI behaviour
|
## MIDI behaviour
|
||||||
|
|
||||||
@@ -165,6 +221,8 @@ cmake -B pico/build -S pico -DCMAKE_CXX_FLAGS=-DSTRIP_REVERSED=1 # Pico
|
|||||||
for the plan's Phase 3 "light the next key to play". A key actually being
|
for the plan's Phase 3 "light the next key to play". A key actually being
|
||||||
played takes precedence over a hint on the same key.
|
played takes precedence over a hint on the same key.
|
||||||
- Notes held when the USB host suspends are cleared, so nothing stays lit.
|
- Notes held when the USB host suspends are cleared, so nothing stays lit.
|
||||||
|
- CC 20 selects a calibration pattern; CC 21/22 set the pixel for the walk
|
||||||
|
pattern. See **Calibration** above.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,34 @@
|
|||||||
#define STRIP_REVERSED 0
|
#define STRIP_REVERSED 0
|
||||||
#endif
|
#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
|
// Power safety (plan section 7) - NOT optional
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
@@ -130,6 +158,38 @@
|
|||||||
#define HINT_MIDI_CHANNEL 15 // channel 16 in a DAW
|
#define HINT_MIDI_CHANNEL 15 // channel 16 in a DAW
|
||||||
#endif
|
#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
|
// Hardware wiring - platform specific
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
|
|||||||
+219
-31
@@ -16,7 +16,10 @@
|
|||||||
|
|
||||||
CPianoLEDs::CPianoLEDs (ILEDStrip &Strip)
|
CPianoLEDs::CPianoLEDs (ILEDStrip &Strip)
|
||||||
: m_Strip (Strip),
|
: m_Strip (Strip),
|
||||||
m_bDirty (true)
|
m_bDirty (true),
|
||||||
|
m_nCalibPattern (CALIB_PATTERN_OFF),
|
||||||
|
m_nCalibIndex (0),
|
||||||
|
m_nCalibIndexHi (0)
|
||||||
{
|
{
|
||||||
memset ((void *) m_KeyVelocity, 0, sizeof m_KeyVelocity);
|
memset ((void *) m_KeyVelocity, 0, sizeof m_KeyVelocity);
|
||||||
memset ((void *) m_HintVelocity, 0, sizeof m_HintVelocity);
|
memset ((void *) m_HintVelocity, 0, sizeof m_HintVelocity);
|
||||||
@@ -35,6 +38,8 @@ bool CPianoLEDs::Initialize (void)
|
|||||||
|
|
||||||
assert (m_Strip.GetLEDCount () >= LED_COUNT);
|
assert (m_Strip.GetLEDCount () >= LED_COUNT);
|
||||||
|
|
||||||
|
BuildKeyMap ();
|
||||||
|
|
||||||
// Start from a known-dark strip rather than whatever the pixels held
|
// Start from a known-dark strip rather than whatever the pixels held
|
||||||
// when power came up.
|
// when power came up.
|
||||||
return m_Strip.Blackout ();
|
return m_Strip.Blackout ();
|
||||||
@@ -65,10 +70,32 @@ void CPianoLEDs::OnMIDIPacket (const uint8_t *pPacket, unsigned nLength)
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case MIDI_CONTROL_CHANGE:
|
case MIDI_CONTROL_CHANGE:
|
||||||
if ( pPacket[1] == MIDI_CC_ALL_SOUND_OFF
|
switch (pPacket[1])
|
||||||
|| pPacket[1] == MIDI_CC_ALL_NOTES_OFF)
|
|
||||||
{
|
{
|
||||||
|
case MIDI_CC_ALL_SOUND_OFF:
|
||||||
|
case MIDI_CC_ALL_NOTES_OFF:
|
||||||
AllOff ();
|
AllOff ();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CALIB_CC_PATTERN:
|
||||||
|
// Leaving calibration must not strand a lit pattern.
|
||||||
|
m_nCalibPattern = pPacket[2];
|
||||||
|
m_bDirty = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CALIB_CC_INDEX_HI:
|
||||||
|
m_nCalibIndexHi = pPacket[2];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CALIB_CC_INDEX_LO:
|
||||||
|
// Low byte last, so the 14-bit value updates atomically
|
||||||
|
// from the renderer's point of view.
|
||||||
|
m_nCalibIndex = (m_nCalibIndexHi << 7) | pPacket[2];
|
||||||
|
m_bDirty = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -77,6 +104,62 @@ void CPianoLEDs::OnMIDIPacket (const uint8_t *pPacket, unsigned nLength)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pitch classes of the white keys, C through B.
|
||||||
|
static bool IsWhiteKey (uint8_t ucNote)
|
||||||
|
{
|
||||||
|
switch (ucNote % 12)
|
||||||
|
{
|
||||||
|
case 0: case 2: case 4: case 5: case 7: case 9: case 11:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CPianoLEDs::BuildKeyMap (void)
|
||||||
|
{
|
||||||
|
#if NOTE_MAP_GEOMETRIC
|
||||||
|
// A white key is LED_COUNT / WHITE_KEY_COUNT pixels wide - 3.38 at the
|
||||||
|
// nominal 176 LEDs, not 2. Held as a 1/256 fixed-point value so the
|
||||||
|
// mapping needs no floating point.
|
||||||
|
const unsigned nWhitePitch = (LED_COUNT * 256u) / WHITE_KEY_COUNT;
|
||||||
|
|
||||||
|
unsigned nWhitesBelow = 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
for (unsigned nKey = 0; nKey < KEY_COUNT; nKey++)
|
||||||
|
{
|
||||||
|
uint8_t ucNote = (uint8_t) (MIDI_NOTE_MIN + nKey);
|
||||||
|
|
||||||
|
#if NOTE_MAP_GEOMETRIC
|
||||||
|
// A white key's centre sits half a key past the whites below it;
|
||||||
|
// a black key sits on the boundary between its neighbours.
|
||||||
|
unsigned nCentre = nWhitesBelow * nWhitePitch;
|
||||||
|
if (IsWhiteKey (ucNote))
|
||||||
|
{
|
||||||
|
nCentre += nWhitePitch / 2;
|
||||||
|
nWhitesBelow++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round to the nearest pixel, then centre the lit span on it.
|
||||||
|
int nCentreLED = (int) ((nCentre + 128) / 256);
|
||||||
|
int nStart = nCentreLED - (int) (LEDS_PER_KEY / 2);
|
||||||
|
#else
|
||||||
|
(void) ucNote;
|
||||||
|
int nStart = (int) (nKey * LEDS_PER_KEY);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
nStart += LED_OFFSET;
|
||||||
|
|
||||||
|
#if STRIP_REVERSED
|
||||||
|
// Mirror the whole strip, keeping the span left-to-right.
|
||||||
|
nStart = (int) LED_COUNT - nStart - (int) LEDS_PER_KEY;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
m_KeyLED[nKey] = nStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void CPianoLEDs::SetKey (uint8_t ucNote, uint8_t ucVelocity, bool bHint)
|
void CPianoLEDs::SetKey (uint8_t ucNote, uint8_t ucVelocity, bool bHint)
|
||||||
{
|
{
|
||||||
// Drop anything off the ends of the keybed rather than trusting the
|
// Drop anything off the ends of the keybed rather than trusting the
|
||||||
@@ -143,18 +226,31 @@ uint8_t CPianoLEDs::Scale (uint8_t ucChannel, uint8_t ucVelocity)
|
|||||||
return (uint8_t) nValue;
|
return (uint8_t) nValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CPianoLEDs::Update (void)
|
void CPianoLEDs::PaintPixel (int nLED, uint8_t nRed, uint8_t nGreen, uint8_t nBlue)
|
||||||
{
|
{
|
||||||
if (!m_bDirty)
|
// A non-zero LED_OFFSET can push a key's span off either end. Drop
|
||||||
|
// those pixels rather than wrapping them to the wrong end of the strip.
|
||||||
|
if ( nLED < 0
|
||||||
|
|| nLED >= (int) LED_COUNT)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the flag before reading state, not after. An event arriving
|
m_Strip.SetLED ((unsigned) nLED, nRed, nGreen, nBlue);
|
||||||
// mid-render then leaves the flag set and we render again next pass,
|
}
|
||||||
// rather than being dropped.
|
|
||||||
m_bDirty = false;
|
|
||||||
|
|
||||||
|
void CPianoLEDs::PaintKey (unsigned nKey, uint8_t nRed, uint8_t nGreen, uint8_t nBlue)
|
||||||
|
{
|
||||||
|
assert (nKey < KEY_COUNT);
|
||||||
|
|
||||||
|
for (unsigned i = 0; i < LEDS_PER_KEY; i++)
|
||||||
|
{
|
||||||
|
PaintPixel (m_KeyLED[nKey] + (int) i, nRed, nGreen, nBlue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CPianoLEDs::RenderNotes (void)
|
||||||
|
{
|
||||||
unsigned nLit = 0;
|
unsigned nLit = 0;
|
||||||
|
|
||||||
for (unsigned nKey = 0; nKey < KEY_COUNT; nKey++)
|
for (unsigned nKey = 0; nKey < KEY_COUNT; nKey++)
|
||||||
@@ -169,44 +265,136 @@ void CPianoLEDs::Update (void)
|
|||||||
bHint = true;
|
bHint = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t ucRed = 0;
|
|
||||||
uint8_t ucGreen = 0;
|
|
||||||
uint8_t ucBlue = 0;
|
|
||||||
|
|
||||||
// Bound the number of simultaneously lit keys, so no sequence of
|
// Bound the number of simultaneously lit keys, so no sequence of
|
||||||
// MIDI events can drive the strip past the supply's budget.
|
// MIDI events can drive the strip past the supply's budget.
|
||||||
if ( ucVelocity != 0
|
if ( ucVelocity == 0
|
||||||
&& nLit < MAX_LIT_KEYS)
|
|| nLit >= MAX_LIT_KEYS)
|
||||||
{
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
nLit++;
|
nLit++;
|
||||||
|
|
||||||
|
// Only lit keys are painted. Under the geometric map adjacent
|
||||||
|
// keys' spans overlap, so painting unlit keys black here would
|
||||||
|
// erase a lit neighbour's pixels.
|
||||||
if (bHint)
|
if (bHint)
|
||||||
{
|
{
|
||||||
ucRed = Scale (HINT_COLOR_R, ucVelocity);
|
PaintKey (nKey, Scale (HINT_COLOR_R, ucVelocity),
|
||||||
ucGreen = Scale (HINT_COLOR_G, ucVelocity);
|
Scale (HINT_COLOR_G, ucVelocity),
|
||||||
ucBlue = Scale (HINT_COLOR_B, ucVelocity);
|
Scale (HINT_COLOR_B, ucVelocity));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ucRed = Scale (NOTE_COLOR_R, ucVelocity);
|
PaintKey (nKey, Scale (NOTE_COLOR_R, ucVelocity),
|
||||||
ucGreen = Scale (NOTE_COLOR_G, ucVelocity);
|
Scale (NOTE_COLOR_G, ucVelocity),
|
||||||
ucBlue = Scale (NOTE_COLOR_B, ucVelocity);
|
Scale (NOTE_COLOR_B, ucVelocity));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if STRIP_REVERSED
|
void CPianoLEDs::RenderCalibration (void)
|
||||||
unsigned nBase = (KEY_COUNT - 1 - nKey) * LEDS_PER_KEY;
|
|
||||||
#else
|
|
||||||
unsigned nBase = nKey * LEDS_PER_KEY;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
for (unsigned i = 0; i < LEDS_PER_KEY; i++)
|
|
||||||
{
|
{
|
||||||
unsigned nLED = nBase + i;
|
// Patterns run at the same ceiling as normal operation, so nothing here
|
||||||
assert (nLED < LED_COUNT);
|
// can draw more current than the design already allows.
|
||||||
|
const uint8_t W = GLOBAL_BRIGHTNESS;
|
||||||
|
|
||||||
m_Strip.SetLED (nLED, ucRed, ucGreen, ucBlue);
|
switch (m_nCalibPattern)
|
||||||
|
{
|
||||||
|
case CALIB_PATTERN_ENDS:
|
||||||
|
// Confirms orientation and that LED_COUNT matches the strip you
|
||||||
|
// actually cut. Red is pixel 0, green is the last pixel.
|
||||||
|
PaintPixel (0, W, 0, 0);
|
||||||
|
PaintPixel ((int) LED_COUNT - 1, 0, W, 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CALIB_PATTERN_OCTAVES:
|
||||||
|
// Every C. Mapping drift shows up immediately as the marks
|
||||||
|
// walking off the keys; middle C is picked out in red.
|
||||||
|
for (unsigned nKey = 0; nKey < KEY_COUNT; nKey++)
|
||||||
|
{
|
||||||
|
uint8_t ucNote = (uint8_t) (MIDI_NOTE_MIN + nKey);
|
||||||
|
if (ucNote % 12 != 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ucNote == 60)
|
||||||
|
{
|
||||||
|
PaintKey (nKey, W, 0, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PaintKey (nKey, 0, 0, W);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CALIB_PATTERN_KEYS:
|
||||||
|
// Every key, white keys and black keys in different colours, so
|
||||||
|
// the whole mapping can be checked against the keybed at once.
|
||||||
|
for (unsigned nKey = 0; nKey < KEY_COUNT; nKey++)
|
||||||
|
{
|
||||||
|
if (IsWhiteKey ((uint8_t) (MIDI_NOTE_MIN + nKey)))
|
||||||
|
{
|
||||||
|
PaintKey (nKey, 0, W, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PaintKey (nKey, 0, 0, W);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CALIB_PATTERN_WALK:
|
||||||
|
// One pixel at a time, stepped from the PC. This is how
|
||||||
|
// LED_OFFSET gets its value: walk to the pixel sitting over A0.
|
||||||
|
PaintPixel ((int) m_nCalibIndex, W, W, W);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CALIB_PATTERN_ALL:
|
||||||
|
// Voltage droop test. Every pixel lit is well beyond normal
|
||||||
|
// operation, which caps at MAX_LIT_KEYS, so watch the far end
|
||||||
|
// for the colour shifting warm - that is the injection point
|
||||||
|
// telling you it is needed.
|
||||||
|
for (unsigned i = 0; i < LED_COUNT; i++)
|
||||||
|
{
|
||||||
|
PaintPixel ((int) i, W, W, W);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CPianoLEDs::Update (void)
|
||||||
|
{
|
||||||
|
if (!m_bDirty)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the flag before reading state, not after. An event arriving
|
||||||
|
// mid-render then leaves the flag set and we render again next pass,
|
||||||
|
// rather than being dropped.
|
||||||
|
m_bDirty = false;
|
||||||
|
|
||||||
|
// Start from black, then paint only what should be lit. Key spans can
|
||||||
|
// overlap under the geometric map, so nothing may paint black over a
|
||||||
|
// region a neighbour has already claimed.
|
||||||
|
for (unsigned i = 0; i < LED_COUNT; i++)
|
||||||
|
{
|
||||||
|
m_Strip.SetLED (i, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_nCalibPattern == CALIB_PATTERN_OFF)
|
||||||
|
{
|
||||||
|
RenderNotes ();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RenderCalibration ();
|
||||||
}
|
}
|
||||||
|
|
||||||
m_Strip.Update ();
|
m_Strip.Update ();
|
||||||
|
|||||||
@@ -35,9 +35,28 @@ public:
|
|||||||
// (re)connection, so notes held at disconnect do not stay lit.
|
// (re)connection, so notes held at disconnect do not stay lit.
|
||||||
void AllOff (void);
|
void AllOff (void);
|
||||||
|
|
||||||
|
// Currently active calibration pattern, CALIB_PATTERN_OFF when running
|
||||||
|
// normally. Exposed for tests.
|
||||||
|
unsigned GetCalibrationPattern (void) const { return m_nCalibPattern; }
|
||||||
|
|
||||||
|
// First pixel of a key's span, after mapping, offset and orientation.
|
||||||
|
// KEY_COUNT entries, valid once Initialize() has run. Exposed for tests.
|
||||||
|
int GetKeyLED (unsigned nKey) const { return m_KeyLED[nKey]; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void SetKey (uint8_t ucNote, uint8_t ucVelocity, bool bHint);
|
void SetKey (uint8_t ucNote, uint8_t ucVelocity, bool bHint);
|
||||||
|
|
||||||
|
// Fill m_KeyLED from the configured mapping.
|
||||||
|
void BuildKeyMap (void);
|
||||||
|
|
||||||
|
void RenderNotes (void);
|
||||||
|
void RenderCalibration (void);
|
||||||
|
|
||||||
|
// Light one key's span, clamping to the strip.
|
||||||
|
void PaintKey (unsigned nKey, uint8_t nRed, uint8_t nGreen, uint8_t nBlue);
|
||||||
|
|
||||||
|
void PaintPixel (int nLED, uint8_t nRed, uint8_t nGreen, uint8_t nBlue);
|
||||||
|
|
||||||
// Scale a colour channel by velocity and the global brightness ceiling.
|
// Scale a colour channel by velocity and the global brightness ceiling.
|
||||||
static uint8_t Scale (uint8_t ucChannel, uint8_t ucVelocity);
|
static uint8_t Scale (uint8_t ucChannel, uint8_t ucVelocity);
|
||||||
|
|
||||||
@@ -52,6 +71,15 @@ private:
|
|||||||
volatile uint8_t m_HintVelocity[KEY_COUNT];
|
volatile uint8_t m_HintVelocity[KEY_COUNT];
|
||||||
|
|
||||||
volatile bool m_bDirty;
|
volatile bool m_bDirty;
|
||||||
|
|
||||||
|
// Calibration overlay, set from the MIDI callback.
|
||||||
|
volatile unsigned m_nCalibPattern;
|
||||||
|
volatile unsigned m_nCalibIndex;
|
||||||
|
volatile unsigned m_nCalibIndexHi;
|
||||||
|
|
||||||
|
// Start pixel of each key's span. Signed, because a negative
|
||||||
|
// LED_OFFSET can push low keys off the end of the strip.
|
||||||
|
int m_KeyLED[KEY_COUNT];
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ for CFG in \
|
|||||||
"single LED per key:-DLEDS_PER_KEY=1" \
|
"single LED per key:-DLEDS_PER_KEY=1" \
|
||||||
"three LEDs per key:-DLEDS_PER_KEY=3" \
|
"three LEDs per key:-DLEDS_PER_KEY=3" \
|
||||||
"hints disabled:-DHINT_MIDI_CHANNEL=MIDI_CHANNEL_NONE" \
|
"hints disabled:-DHINT_MIDI_CHANNEL=MIDI_CHANNEL_NONE" \
|
||||||
|
"linear map:-DNOTE_MAP_GEOMETRIC=0" \
|
||||||
|
"linear map reversed:-DNOTE_MAP_GEOMETRIC=0 -DSTRIP_REVERSED=1" \
|
||||||
|
"positive offset:-DLED_OFFSET=3" \
|
||||||
|
"negative offset:-DLED_OFFSET=-2" \
|
||||||
|
"offset reversed:-DLED_OFFSET=4 -DSTRIP_REVERSED=1" \
|
||||||
; do
|
; do
|
||||||
NAME=${CFG%%:*}
|
NAME=${CFG%%:*}
|
||||||
FLAGS=${CFG#*:}
|
FLAGS=${CFG#*:}
|
||||||
|
|||||||
+168
-28
@@ -27,12 +27,20 @@ static unsigned CountLit (void)
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool Dark (unsigned i)
|
static bool Dark (int i)
|
||||||
{
|
{
|
||||||
auto &p = Strip.m_Pixels.at (i);
|
// Off-strip is not lit. Signed, because a negative LED_OFFSET can push
|
||||||
|
// a key's start below zero.
|
||||||
|
if (i < 0 || i >= (int) Strip.m_Pixels.size ()) return true;
|
||||||
|
auto &p = Strip.m_Pixels[i];
|
||||||
return !p[0] && !p[1] && !p[2];
|
return !p[0] && !p[1] && !p[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool OnStrip (int nBase)
|
||||||
|
{
|
||||||
|
return nBase >= 0 && nBase + (int) LEDS_PER_KEY <= (int) LED_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
// Deliver a plain MIDI message the way a platform backend would.
|
// Deliver a plain MIDI message the way a platform backend would.
|
||||||
static void Inject (uint8_t a, uint8_t b, uint8_t c)
|
static void Inject (uint8_t a, uint8_t b, uint8_t c)
|
||||||
{
|
{
|
||||||
@@ -41,23 +49,26 @@ static void Inject (uint8_t a, uint8_t b, uint8_t c)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// every pixel of one key's span is lit
|
// every pixel of one key's span is lit
|
||||||
static bool Span (unsigned nBase)
|
static bool Span (int nBase)
|
||||||
{
|
{
|
||||||
for (unsigned i = 0; i < LEDS_PER_KEY; i++)
|
for (unsigned i = 0; i < LEDS_PER_KEY; i++)
|
||||||
if (Dark (nBase + i)) return false;
|
if (Dark (nBase + (int) i)) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static unsigned LedFor (uint8_t ucNote)
|
static int LedFor (uint8_t ucNote)
|
||||||
{
|
{
|
||||||
unsigned nKey = ucNote - MIDI_NOTE_MIN;
|
return LEDs.GetKeyLED (ucNote - MIDI_NOTE_MIN);
|
||||||
#if STRIP_REVERSED
|
|
||||||
return (KEY_COUNT - 1 - nKey) * LEDS_PER_KEY;
|
|
||||||
#else
|
|
||||||
return nKey * LEDS_PER_KEY;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if LED_OFFSET != 0
|
||||||
|
static void AllOffAndClear (void)
|
||||||
|
{
|
||||||
|
LEDs.AllOff ();
|
||||||
|
LEDs.Update ();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
int main (void)
|
int main (void)
|
||||||
{
|
{
|
||||||
printf ("STRIP_REVERSED=%d LED_COUNT=%d MAX_LIT_KEYS=%d GLOBAL_BRIGHTNESS=%d\n\n",
|
printf ("STRIP_REVERSED=%d LED_COUNT=%d MAX_LIT_KEYS=%d GLOBAL_BRIGHTNESS=%d\n\n",
|
||||||
@@ -68,25 +79,25 @@ int main (void)
|
|||||||
// --- lowest key, A0 = note 21 -------------------------------------
|
// --- lowest key, A0 = note 21 -------------------------------------
|
||||||
Inject (0x90, 21, 127);
|
Inject (0x90, 21, 127);
|
||||||
LEDs.Update ();
|
LEDs.Update ();
|
||||||
#if STRIP_REVERSED
|
int nLow = LedFor (21);
|
||||||
unsigned nLow = (KEY_COUNT - 1) * LEDS_PER_KEY; // 174
|
if (OnStrip (nLow))
|
||||||
#else
|
{
|
||||||
unsigned nLow = 0;
|
|
||||||
#endif
|
|
||||||
Check ("note 21 lights its whole key span", Span (nLow));
|
Check ("note 21 lights its whole key span", Span (nLow));
|
||||||
Check ("note 21 lights exactly LEDS_PER_KEY LEDs", CountLit () == LEDS_PER_KEY);
|
Check ("note 21 lights exactly LEDS_PER_KEY LEDs",
|
||||||
|
CountLit () == LEDS_PER_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
// --- highest key, C8 = note 108 -----------------------------------
|
// --- highest key, C8 = note 108 -----------------------------------
|
||||||
Inject (0x80, 21, 0);
|
Inject (0x80, 21, 0);
|
||||||
Inject (0x90, 108, 127);
|
Inject (0x90, 108, 127);
|
||||||
LEDs.Update ();
|
LEDs.Update ();
|
||||||
#if STRIP_REVERSED
|
int nHigh = LedFor (108);
|
||||||
unsigned nHigh = 0;
|
if (OnStrip (nHigh))
|
||||||
#else
|
{
|
||||||
unsigned nHigh = (KEY_COUNT - 1) * LEDS_PER_KEY; // 174
|
|
||||||
#endif
|
|
||||||
Check ("note 108 lights its whole key span", Span (nHigh));
|
Check ("note 108 lights its whole key span", Span (nHigh));
|
||||||
Check ("note 108 lights exactly LEDS_PER_KEY LEDs", CountLit () == LEDS_PER_KEY);
|
Check ("note 108 lights exactly LEDS_PER_KEY LEDs",
|
||||||
|
CountLit () == LEDS_PER_KEY);
|
||||||
|
}
|
||||||
Check ("the two extremes are at opposite ends", nLow != nHigh);
|
Check ("the two extremes are at opposite ends", nLow != nHigh);
|
||||||
|
|
||||||
// --- note off ------------------------------------------------------
|
// --- note off ------------------------------------------------------
|
||||||
@@ -140,10 +151,10 @@ int main (void)
|
|||||||
// --- velocity sensitivity -------------------------------------------
|
// --- velocity sensitivity -------------------------------------------
|
||||||
Inject (0x90, 60, 127);
|
Inject (0x90, 60, 127);
|
||||||
LEDs.Update ();
|
LEDs.Update ();
|
||||||
auto Loud = Strip.m_Pixels.at (LedFor (60));
|
auto Loud = Strip.m_Pixels.at (((LedFor (60)) + LED_COUNT) % LED_COUNT);
|
||||||
Inject (0x90, 60, 1);
|
Inject (0x90, 60, 1);
|
||||||
LEDs.Update ();
|
LEDs.Update ();
|
||||||
auto Soft = Strip.m_Pixels.at (LedFor (60));
|
auto Soft = Strip.m_Pixels.at (((LedFor (60)) + LED_COUNT) % LED_COUNT);
|
||||||
#if VELOCITY_SENSITIVE
|
#if VELOCITY_SENSITIVE
|
||||||
Check ("a soft note is dimmer than a loud one", Soft[2] < Loud[2]);
|
Check ("a soft note is dimmer than a loud one", Soft[2] < Loud[2]);
|
||||||
Check ("a soft note is still visible", Soft[2] > 0);
|
Check ("a soft note is still visible", Soft[2] > 0);
|
||||||
@@ -156,19 +167,19 @@ int main (void)
|
|||||||
#if HINT_MIDI_CHANNEL != MIDI_CHANNEL_NONE
|
#if HINT_MIDI_CHANNEL != MIDI_CHANNEL_NONE
|
||||||
Inject (0x90 | HINT_MIDI_CHANNEL, 64, 127);
|
Inject (0x90 | HINT_MIDI_CHANNEL, 64, 127);
|
||||||
LEDs.Update ();
|
LEDs.Update ();
|
||||||
auto Hint = Strip.m_Pixels.at (LedFor (64));
|
auto Hint = Strip.m_Pixels.at (((LedFor (64)) + LED_COUNT) % LED_COUNT);
|
||||||
Check ("a hint note lights in the hint colour", Hint != Loud && (Hint[0] || Hint[1] || Hint[2]));
|
Check ("a hint note lights in the hint colour", Hint != Loud && (Hint[0] || Hint[1] || Hint[2]));
|
||||||
|
|
||||||
// a key actually played wins over a hint on the same key
|
// a key actually played wins over a hint on the same key
|
||||||
Inject (0x90, 64, 127);
|
Inject (0x90, 64, 127);
|
||||||
LEDs.Update ();
|
LEDs.Update ();
|
||||||
auto Both = Strip.m_Pixels.at (LedFor (64));
|
auto Both = Strip.m_Pixels.at (((LedFor (64)) + LED_COUNT) % LED_COUNT);
|
||||||
Check ("a played note overrides a hint on the same key", Both == Loud);
|
Check ("a played note overrides a hint on the same key", Both == Loud);
|
||||||
|
|
||||||
// releasing the played note falls back to the still-pending hint
|
// releasing the played note falls back to the still-pending hint
|
||||||
Inject (0x80, 64, 0);
|
Inject (0x80, 64, 0);
|
||||||
LEDs.Update ();
|
LEDs.Update ();
|
||||||
auto Back = Strip.m_Pixels.at (LedFor (64));
|
auto Back = Strip.m_Pixels.at (((LedFor (64)) + LED_COUNT) % LED_COUNT);
|
||||||
Check ("releasing a played note reveals the hint again", Back == Hint);
|
Check ("releasing a played note reveals the hint again", Back == Hint);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -184,6 +195,135 @@ int main (void)
|
|||||||
LEDs.Update ();
|
LEDs.Update ();
|
||||||
Check ("a 1-byte realtime message lights nothing", CountLit () == 0);
|
Check ("a 1-byte realtime message lights nothing", CountLit () == 0);
|
||||||
|
|
||||||
|
// --- every key lands on the strip -----------------------------------
|
||||||
|
bool bOnStrip = true;
|
||||||
|
for (unsigned k = 0; k < KEY_COUNT; k++)
|
||||||
|
{
|
||||||
|
int nStart = LEDs.GetKeyLED (k);
|
||||||
|
if (nStart < 0 || nStart + (int) LEDS_PER_KEY > (int) LED_COUNT)
|
||||||
|
bOnStrip = false;
|
||||||
|
}
|
||||||
|
#if LED_OFFSET == 0
|
||||||
|
Check ("every key maps onto the strip", bOnStrip);
|
||||||
|
#else
|
||||||
|
// A non-zero offset deliberately shifts an end key past the strip. The
|
||||||
|
// property that must hold is that those pixels are clipped, never
|
||||||
|
// wrapped round to the far end.
|
||||||
|
(void) bOnStrip;
|
||||||
|
bool bClipped = true;
|
||||||
|
for (unsigned k = 0; k < KEY_COUNT; k++)
|
||||||
|
{
|
||||||
|
int nStart = LEDs.GetKeyLED (k);
|
||||||
|
if (OnStrip (nStart))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count how many of this key's pixels are actually on the strip.
|
||||||
|
unsigned nExpect = 0;
|
||||||
|
for (unsigned i = 0; i < LEDS_PER_KEY; i++)
|
||||||
|
{
|
||||||
|
int nLED = nStart + (int) i;
|
||||||
|
if (nLED >= 0 && nLED < (int) LED_COUNT) nExpect++;
|
||||||
|
}
|
||||||
|
|
||||||
|
AllOffAndClear ();
|
||||||
|
Inject (0x90, (uint8_t) (MIDI_NOTE_MIN + k), 127);
|
||||||
|
LEDs.Update ();
|
||||||
|
|
||||||
|
if (CountLit () != nExpect) bClipped = false;
|
||||||
|
}
|
||||||
|
Check ("an offset clips off-strip pixels rather than wrapping", bClipped);
|
||||||
|
AllOffAndClear ();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// --- keys are monotonic across the keyboard --------------------------
|
||||||
|
bool bMonotonic = true;
|
||||||
|
for (unsigned k = 1; k < KEY_COUNT; k++)
|
||||||
|
{
|
||||||
|
#if STRIP_REVERSED
|
||||||
|
if (LEDs.GetKeyLED (k) > LEDs.GetKeyLED (k - 1)) bMonotonic = false;
|
||||||
|
#else
|
||||||
|
if (LEDs.GetKeyLED (k) < LEDs.GetKeyLED (k - 1)) bMonotonic = false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
Check ("key positions advance monotonically", bMonotonic);
|
||||||
|
|
||||||
|
#if NOTE_MAP_GEOMETRIC && !STRIP_REVERSED && LED_OFFSET == 0
|
||||||
|
// --- geometric map tracks real key positions -------------------------
|
||||||
|
// White keys should sit one white-key pitch apart, ~3.38 LEDs, not 2.
|
||||||
|
double dPitch = (double) LED_COUNT / WHITE_KEY_COUNT;
|
||||||
|
double dWorst = 0.0;
|
||||||
|
int nPrevWhite = -1;
|
||||||
|
for (unsigned k = 0; k < KEY_COUNT; k++)
|
||||||
|
{
|
||||||
|
uint8_t note = (uint8_t) (MIDI_NOTE_MIN + k);
|
||||||
|
switch (note % 12)
|
||||||
|
{
|
||||||
|
case 0: case 2: case 4: case 5: case 7: case 9: case 11:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (nPrevWhite >= 0)
|
||||||
|
{
|
||||||
|
double d = LEDs.GetKeyLED (k) - nPrevWhite;
|
||||||
|
double e = d - dPitch;
|
||||||
|
if (e < 0) e = -e;
|
||||||
|
if (e > dWorst) dWorst = e;
|
||||||
|
}
|
||||||
|
nPrevWhite = LEDs.GetKeyLED (k);
|
||||||
|
}
|
||||||
|
Check ("white keys sit one white-key pitch apart", dWorst <= 1.0);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// --- calibration patterns --------------------------------------------
|
||||||
|
Inject (0xB0, CALIB_CC_PATTERN, CALIB_PATTERN_ENDS);
|
||||||
|
LEDs.Update ();
|
||||||
|
Check ("pattern ENDS lights exactly the two end pixels",
|
||||||
|
CountLit () == 2 && !Dark (0) && !Dark (LED_COUNT - 1));
|
||||||
|
|
||||||
|
Inject (0xB0, CALIB_CC_PATTERN, CALIB_PATTERN_ALL);
|
||||||
|
LEDs.Update ();
|
||||||
|
Check ("pattern ALL lights the whole strip", CountLit () == LED_COUNT);
|
||||||
|
|
||||||
|
bool bAllWithinCeiling = true;
|
||||||
|
#if GLOBAL_BRIGHTNESS < 255
|
||||||
|
for (auto &p : Strip.m_Pixels)
|
||||||
|
for (int c = 0; c < 3; c++)
|
||||||
|
if (p[c] > GLOBAL_BRIGHTNESS) bAllWithinCeiling = false;
|
||||||
|
#endif
|
||||||
|
Check ("pattern ALL still respects the brightness ceiling", bAllWithinCeiling);
|
||||||
|
|
||||||
|
Inject (0xB0, CALIB_CC_PATTERN, CALIB_PATTERN_WALK);
|
||||||
|
Inject (0xB0, CALIB_CC_INDEX_HI, 0);
|
||||||
|
Inject (0xB0, CALIB_CC_INDEX_LO, 5);
|
||||||
|
LEDs.Update ();
|
||||||
|
Check ("pattern WALK lights only the selected pixel",
|
||||||
|
CountLit () == 1 && !Dark (5));
|
||||||
|
|
||||||
|
// a 14-bit index beyond the strip must not paint anything
|
||||||
|
Inject (0xB0, CALIB_CC_INDEX_HI, 127);
|
||||||
|
Inject (0xB0, CALIB_CC_INDEX_LO, 127);
|
||||||
|
LEDs.Update ();
|
||||||
|
Check ("pattern WALK ignores an out-of-range index", CountLit () == 0);
|
||||||
|
|
||||||
|
Inject (0xB0, CALIB_CC_PATTERN, CALIB_PATTERN_OCTAVES);
|
||||||
|
LEDs.Update ();
|
||||||
|
Check ("pattern OCTAVES lights something", CountLit () > 0);
|
||||||
|
|
||||||
|
// notes held while a pattern runs must not survive it
|
||||||
|
Inject (0x90, 60, 127);
|
||||||
|
LEDs.Update ();
|
||||||
|
Check ("a pattern overrides note display", CountLit () > 0);
|
||||||
|
|
||||||
|
Inject (0xB0, CALIB_CC_PATTERN, CALIB_PATTERN_OFF);
|
||||||
|
LEDs.Update ();
|
||||||
|
Check ("leaving calibration restores note display",
|
||||||
|
CountLit () > 0 && !Dark (LedFor (60)));
|
||||||
|
Inject (0x80, 60, 0);
|
||||||
|
LEDs.Update ();
|
||||||
|
|
||||||
printf ("\n%s\n", g_nFail ? "FAILURES" : "all tests passed");
|
printf ("\n%s\n", g_nFail ? "FAILURES" : "all tests passed");
|
||||||
return g_nFail != 0;
|
return g_nFail != 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Executable
+127
@@ -0,0 +1,127 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Drives the firmware's calibration patterns from the PC over ALSA MIDI.
|
||||||
|
# See "Calibration" in README.md for the procedure.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# tools/calibrate.sh list list MIDI ports
|
||||||
|
# tools/calibrate.sh ends pixel 0 (red) and last pixel (green)
|
||||||
|
# tools/calibrate.sh octaves every C, middle C in red
|
||||||
|
# tools/calibrate.sh keys every key, white green / black blue
|
||||||
|
# tools/calibrate.sh walk <n> light pixel n only
|
||||||
|
# tools/calibrate.sh sweep [ms] walk every pixel in turn
|
||||||
|
# tools/calibrate.sh all every pixel, for voltage droop
|
||||||
|
# tools/calibrate.sh off back to normal operation
|
||||||
|
# tools/calibrate.sh note <n> [vel] play one MIDI note
|
||||||
|
# tools/calibrate.sh chromatic [ms] play every key, low to high
|
||||||
|
#
|
||||||
|
# Set PORT to the device's ALSA port (e.g. PORT=24:0). If unset, the script
|
||||||
|
# picks the first port whose name matches PORT_MATCH.
|
||||||
|
#
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PORT_MATCH=${PORT_MATCH:-Piano LED}
|
||||||
|
|
||||||
|
CC_PATTERN=20
|
||||||
|
CC_INDEX_HI=21
|
||||||
|
CC_INDEX_LO=22
|
||||||
|
|
||||||
|
PAT_OFF=0; PAT_ENDS=1; PAT_OCTAVES=2; PAT_KEYS=3; PAT_WALK=4; PAT_ALL=5
|
||||||
|
|
||||||
|
need() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || {
|
||||||
|
echo "error: $1 not found (install alsa-utils)" >&2; exit 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
find_port() {
|
||||||
|
if [ -n "$PORT" ]; then echo "$PORT"; return; fi
|
||||||
|
local p
|
||||||
|
p=$(aconnect -l | awk -v m="$PORT_MATCH" '
|
||||||
|
/^client /{ cl=$2; sub(":","",cl); name=$0 }
|
||||||
|
/^ +[0-9]+ / { if (name ~ m) { port=$1; print cl ":" port; exit } }')
|
||||||
|
if [ -z "$p" ]; then
|
||||||
|
echo "error: no MIDI port matching \"$PORT_MATCH\"." >&2
|
||||||
|
echo " Run '$0 list', then set PORT=client:port" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "$p"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send raw MIDI bytes to the port.
|
||||||
|
send() {
|
||||||
|
need amidi
|
||||||
|
local hex="$*"
|
||||||
|
amidi -p "$(alsa_rawmidi_port)" -S "$hex" 2>/dev/null && return 0
|
||||||
|
# amidi needs a rawmidi device; fall back to the sequencer via aplaymidi
|
||||||
|
echo "error: could not send. Set PORT and ensure the device is connected." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# amidi addresses rawmidi (hw:X,Y), not sequencer ports.
|
||||||
|
alsa_rawmidi_port() {
|
||||||
|
if [ -n "$RAWMIDI" ]; then echo "$RAWMIDI"; return; fi
|
||||||
|
need amidi
|
||||||
|
local p
|
||||||
|
p=$(amidi -l | awk -v m="$PORT_MATCH" '$0 ~ m { print $2; exit }')
|
||||||
|
if [ -z "$p" ]; then
|
||||||
|
echo "error: no rawmidi device matching \"$PORT_MATCH\"." >&2
|
||||||
|
echo " Run 'amidi -l', then set RAWMIDI=hw:X,Y" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "$p"
|
||||||
|
}
|
||||||
|
|
||||||
|
cc() { printf 'B0 %02X %02X' "$1" "$2"; }
|
||||||
|
|
||||||
|
pattern() { send "$(cc $CC_PATTERN $1)"; }
|
||||||
|
|
||||||
|
walk() {
|
||||||
|
local n=$1
|
||||||
|
send "$(cc $CC_PATTERN $PAT_WALK) $(cc $CC_INDEX_HI $((n >> 7))) $(cc $CC_INDEX_LO $((n & 127)))"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
list)
|
||||||
|
echo "--- sequencer ports (aconnect -l) ---"; aconnect -l || true
|
||||||
|
echo; echo "--- rawmidi devices (amidi -l) ---"; amidi -l || true
|
||||||
|
;;
|
||||||
|
ends) pattern $PAT_ENDS; echo "pattern: ends" ;;
|
||||||
|
octaves) pattern $PAT_OCTAVES; echo "pattern: octaves" ;;
|
||||||
|
keys) pattern $PAT_KEYS; echo "pattern: keys" ;;
|
||||||
|
all) pattern $PAT_ALL; echo "pattern: all (watch the far end for warm colour)" ;;
|
||||||
|
off) pattern $PAT_OFF; echo "pattern: off" ;;
|
||||||
|
walk)
|
||||||
|
[ -n "${2:-}" ] || { echo "usage: $0 walk <pixel>" >&2; exit 1; }
|
||||||
|
walk "$2"; echo "pixel $2"
|
||||||
|
;;
|
||||||
|
sweep)
|
||||||
|
MS=${2:-120}
|
||||||
|
COUNT=${LED_COUNT:-176}
|
||||||
|
echo "sweeping 0..$((COUNT-1)); Ctrl-C to stop"
|
||||||
|
for ((i=0; i<COUNT; i++)); do
|
||||||
|
walk "$i"; printf '\rpixel %3d' "$i"; sleep "$(echo "$MS/1000" | bc -l)"
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
;;
|
||||||
|
note)
|
||||||
|
[ -n "${2:-}" ] || { echo "usage: $0 note <midi-note> [velocity]" >&2; exit 1; }
|
||||||
|
V=${3:-100}
|
||||||
|
send "$(printf '90 %02X %02X' "$2" "$V")"
|
||||||
|
echo "note $2 on (velocity $V); '$0 off' or send note-off to clear"
|
||||||
|
;;
|
||||||
|
chromatic)
|
||||||
|
MS=${2:-150}
|
||||||
|
echo "playing notes 21..108; Ctrl-C to stop"
|
||||||
|
for ((n=21; n<=108; n++)); do
|
||||||
|
send "$(printf '90 %02X 64' "$n")"
|
||||||
|
printf '\rnote %3d' "$n"
|
||||||
|
sleep "$(echo "$MS/1000" | bc -l)"
|
||||||
|
send "$(printf '80 %02X 00' "$n")"
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
sed -n "2,/^# Set PORT/p" "$0" | sed 's/^# \{0,1\}//'
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user