// // Host-side tests for the note-to-LED mapping and the power clamps. // Compiles the real firmware/pianoleds.cpp against stubbed Circle headers. // #include "pianoleds.h" #include "capturestrip.h" #include #include static int g_nFail = 0; static void Check (const char *pName, bool bCond) { printf ("%-58s %s\n", pName, bCond ? "ok" : "FAIL"); if (!bCond) g_nFail++; } static CCaptureLEDStrip Strip (LED_COUNT); static CPianoLEDs LEDs (Strip); static unsigned CountLit (void) { unsigned n = 0; for (auto &p : Strip.m_Pixels) if (p[0] || p[1] || p[2]) n++; return n; } static bool Dark (int 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]; } 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. static void Inject (uint8_t a, uint8_t b, uint8_t c) { uint8_t packet[3] = {a, b, c}; LEDs.OnMIDIPacket (packet, 3); } // every pixel of one key's span is lit static bool Span (int nBase) { for (unsigned i = 0; i < LEDS_PER_KEY; i++) if (Dark (nBase + (int) i)) return false; return true; } static int LedFor (uint8_t ucNote) { return LEDs.GetKeyLED (ucNote - MIDI_NOTE_MIN); } #if LED_OFFSET != 0 static void AllOffAndClear (void) { LEDs.AllOff (); LEDs.Update (); } #endif static unsigned g_nDelayCalls = 0; static void CountingDelay (unsigned) { g_nDelayCalls++; } int main (void) { printf ("STRIP_REVERSED=%d LED_COUNT=%d MAX_LIT_KEYS=%d GLOBAL_BRIGHTNESS=%d\n\n", STRIP_REVERSED, LED_COUNT, MAX_LIT_KEYS, GLOBAL_BRIGHTNESS); LEDs.Initialize (); LEDs.SetHostConnected (true); // tests exercise the connected path // --- lowest key, A0 = note 21 ------------------------------------- Inject (0x90, 21, 127); LEDs.Update (); int nLow = LedFor (21); if (OnStrip (nLow)) { Check ("note 21 lights its whole key span", Span (nLow)); Check ("note 21 lights exactly LEDS_PER_KEY LEDs", CountLit () == LEDS_PER_KEY); } // --- highest key, C8 = note 108 ----------------------------------- Inject (0x80, 21, 0); Inject (0x90, 108, 127); LEDs.Update (); int nHigh = LedFor (108); if (OnStrip (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 ("the two extremes are at opposite ends", nLow != nHigh); // --- note off ------------------------------------------------------ Inject (0x80, 108, 0); LEDs.Update (); Check ("note off extinguishes the key", CountLit () == 0); // --- note on with velocity 0 is a note off ------------------------- Inject (0x90, 60, 100); LEDs.Update (); Check ("note on lights middle C", CountLit () == LEDS_PER_KEY); Inject (0x90, 60, 0); LEDs.Update (); Check ("note on velocity 0 acts as note off", CountLit () == 0); // --- out-of-range notes are dropped, not clamped into the strip ---- Inject (0x90, 20, 127); Inject (0x90, 109, 127); Inject (0x90, 0, 127); Inject (0x90, 127, 127); LEDs.Update (); Check ("notes outside 21-108 are ignored", CountLit () == 0); // --- brightness ceiling -------------------------------------------- for (uint8_t n = MIDI_NOTE_MIN; n <= MIDI_NOTE_MAX; n++) Inject (0x90, n, 127); LEDs.Update (); bool bWithinCeiling = true; #if GLOBAL_BRIGHTNESS < 255 // at 255 a u8 channel cannot exceed the ceiling by construction for (auto &p : Strip.m_Pixels) for (int c = 0; c < 3; c++) if (p[c] > GLOBAL_BRIGHTNESS) bWithinCeiling = false; #endif Check ("no channel ever exceeds GLOBAL_BRIGHTNESS", bWithinCeiling); // --- simultaneous-key cap ------------------------------------------ Check ("all 88 keys held stays within MAX_LIT_KEYS", CountLit () <= MAX_LIT_KEYS * LEDS_PER_KEY); // --- all notes off -------------------------------------------------- Inject (0xB0, 123, 0); LEDs.Update (); Check ("CC 123 (all notes off) clears the strip", CountLit () == 0); for (uint8_t n = MIDI_NOTE_MIN; n <= MIDI_NOTE_MAX; n++) Inject (0x90, n, 127); Inject (0xB0, 120, 0); LEDs.Update (); Check ("CC 120 (all sound off) clears the strip", CountLit () == 0); // --- velocity sensitivity ------------------------------------------- Inject (0x90, 60, 127); LEDs.Update (); auto Loud = Strip.m_Pixels.at (((LedFor (60)) + LED_COUNT) % LED_COUNT); Inject (0x90, 60, 1); LEDs.Update (); auto Soft = Strip.m_Pixels.at (((LedFor (60)) + LED_COUNT) % LED_COUNT); #if VELOCITY_SENSITIVE Check ("a soft note is dimmer than a loud one", Soft[2] < Loud[2]); Check ("a soft note is still visible", Soft[2] > 0); #else Check ("velocity does not change brightness", Soft[2] == Loud[2]); #endif Inject (0x80, 60, 0); // --- hint channel ---------------------------------------------------- #if HINT_MIDI_CHANNEL != MIDI_CHANNEL_NONE Inject (0x90 | HINT_MIDI_CHANNEL, 64, 127); LEDs.Update (); 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])); // a key actually played wins over a hint on the same key Inject (0x90, 64, 127); LEDs.Update (); 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); // releasing the played note falls back to the still-pending hint Inject (0x80, 64, 0); LEDs.Update (); auto Back = Strip.m_Pixels.at (((LedFor (64)) + LED_COUNT) % LED_COUNT); Check ("releasing a played note reveals the hint again", Back == Hint); #endif // --- reconnect clears held notes ------------------------------------- Inject (0x90, 60, 127); LEDs.AllOff (); LEDs.Update (); Check ("re-enumeration clears notes held at suspend", CountLit () == 0); // --- short packets are not parsed as notes ---------------------------- uint8_t Short[1] = {0xF8}; // clock, 1 byte LEDs.OnMIDIPacket (Short, 1); LEDs.Update (); 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 (); // --- idle indicator --------------------------------------------------- LEDs.AllOff (); LEDs.SetHostConnected (false); LEDs.Update (); #if IDLE_INDICATOR Check ("with no host, one dim pixel marks the strip alive", CountLit () == 1 && !Dark (0)); #else Check ("with no host and no indicator, the strip is dark", CountLit () == 0); #endif // notes cannot arrive without a host, and must not display if they do Inject (0x90, 60, 127); LEDs.Update (); Check ("notes do not display while no host is connected", CountLit () <= 1); LEDs.SetHostConnected (true); LEDs.Update (); Check ("reconnecting restores note display", !Dark (LedFor (60))); LEDs.AllOff (); LEDs.Update (); // --- self test -------------------------------------------------------- g_nDelayCalls = 0; LEDs.RunSelfTest (CountingDelay); #if BOOT_SELF_TEST Check ("self test steps once per pixel", g_nDelayCalls == LED_COUNT); Check ("self test leaves the strip dark", CountLit () == 0); // and the renderer must not think the strip still holds what it drew Inject (0x90, 60, 127); LEDs.Update (); Check ("display works after the self test", !Dark (LedFor (60))); LEDs.AllOff (); LEDs.Update (); #else Check ("self test is compiled out", g_nDelayCalls == 0); #endif printf ("\n%s\n", g_nFail ? "FAILURES" : "all tests passed"); return g_nFail != 0; }