// // pianoleds.cpp // #include "pianoleds.h" #include #include // MIDI status nibbles #define MIDI_NOTE_OFF 0x80 #define MIDI_NOTE_ON 0x90 #define MIDI_CONTROL_CHANGE 0xB0 // Control numbers that mean "stop everything" #define MIDI_CC_ALL_SOUND_OFF 120 #define MIDI_CC_ALL_NOTES_OFF 123 CPianoLEDs::CPianoLEDs (ILEDStrip &Strip) : m_Strip (Strip), m_bDirty (true), m_bHostConnected (false), m_nCalibPattern (CALIB_PATTERN_OFF), m_nCalibIndex (0), m_nCalibIndexHi (0) { memset ((void *) m_KeyVelocity, 0, sizeof m_KeyVelocity); memset ((void *) m_HintVelocity, 0, sizeof m_HintVelocity); } CPianoLEDs::~CPianoLEDs (void) { } bool CPianoLEDs::Initialize (void) { if (!m_Strip.Initialize ()) { return false; } assert (m_Strip.GetLEDCount () >= LED_COUNT); BuildKeyMap (); // Start from a known-dark strip rather than whatever the pixels held // when power came up. return m_Strip.Blackout (); } void CPianoLEDs::OnMIDIPacket (const uint8_t *pPacket, unsigned nLength) { // The platform hands us one already-framed MIDI message of 1-3 bytes. // Anything // shorter than a channel message cannot be a note event. if (nLength < 3) { return; } uint8_t ucStatus = pPacket[0] & 0xF0; uint8_t ucChannel = pPacket[0] & 0x0F; switch (ucStatus) { case MIDI_NOTE_ON: // Note On with velocity 0 is the conventional Note Off. SetKey (pPacket[1], pPacket[2], ChannelMatches (ucChannel, HINT_MIDI_CHANNEL)); break; case MIDI_NOTE_OFF: SetKey (pPacket[1], 0, ChannelMatches (ucChannel, HINT_MIDI_CHANNEL)); break; case MIDI_CONTROL_CHANGE: switch (pPacket[1]) { case MIDI_CC_ALL_SOUND_OFF: case MIDI_CC_ALL_NOTES_OFF: 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; default: break; } } // 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) { // Drop anything off the ends of the keybed rather than trusting the // input; an out-of-range note would index past the strip. if ( ucNote < MIDI_NOTE_MIN || ucNote > MIDI_NOTE_MAX) { return; } unsigned nKey = ucNote - MIDI_NOTE_MIN; if (bHint) { m_HintVelocity[nKey] = ucVelocity; } else { m_KeyVelocity[nKey] = ucVelocity; } m_bDirty = true; } void CPianoLEDs::SetHostConnected (bool bConnected) { if (m_bHostConnected != bConnected) { m_bHostConnected = bConnected; m_bDirty = true; } } void CPianoLEDs::RunSelfTest (TDelayMs *pDelay) { #if BOOT_SELF_TEST assert (pDelay != nullptr); // Sweep in strip order, not key order, so what you watch is the strip's // own geometry: it starts at pixel 0 wherever that physically is. for (unsigned i = 0; i < LED_COUNT; i++) { if (i > 0) { m_Strip.SetLED (i - 1, 0, 0, 0); } m_Strip.SetLED (i, GLOBAL_BRIGHTNESS, GLOBAL_BRIGHTNESS, GLOBAL_BRIGHTNESS); m_Strip.Update (); pDelay (BOOT_SELF_TEST_MS); } m_Strip.Blackout (); // The sweep left the strip in a state the renderer does not know about. m_bDirty = true; #else (void) pDelay; #endif } void CPianoLEDs::RenderIdle (void) { #if IDLE_INDICATOR // One dim pixel at the strip's start: powered and running, no host yet. const uint8_t B = GLOBAL_BRIGHTNESS / 8 ? GLOBAL_BRIGHTNESS / 8 : 1; PaintPixel (0, B, B, B); #endif } void CPianoLEDs::AllOff (void) { memset ((void *) m_KeyVelocity, 0, sizeof m_KeyVelocity); memset ((void *) m_HintVelocity, 0, sizeof m_HintVelocity); m_bDirty = true; } bool CPianoLEDs::ChannelMatches (uint8_t ucChannel, uint8_t ucWanted) { if (ucWanted == MIDI_CHANNEL_NONE) { return false; } if (ucWanted == MIDI_CHANNEL_ANY) { return true; } return ucChannel == ucWanted; } uint8_t CPianoLEDs::Scale (uint8_t ucChannel, uint8_t ucVelocity) { unsigned nValue = ucChannel; // Global brightness ceiling. This is the clamp that keeps a whited-out // strip inside the supply's current budget; see config.h. nValue = nValue * GLOBAL_BRIGHTNESS / 255; #if VELOCITY_SENSITIVE // Map velocity 1-127 onto [VELOCITY_FLOOR_PCT, 100] percent, so even the // softest note stays visible. unsigned nPercent = VELOCITY_FLOOR_PCT + (100 - VELOCITY_FLOOR_PCT) * ucVelocity / 127; nValue = nValue * nPercent / 100; #endif return (uint8_t) nValue; } void CPianoLEDs::PaintPixel (int nLED, uint8_t nRed, uint8_t nGreen, uint8_t nBlue) { // 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; } m_Strip.SetLED ((unsigned) nLED, nRed, nGreen, nBlue); } 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; for (unsigned nKey = 0; nKey < KEY_COUNT; nKey++) { uint8_t ucVelocity = m_KeyVelocity[nKey]; bool bHint = false; if (ucVelocity == 0) { // A key being played wins over a "next note" hint on it. ucVelocity = m_HintVelocity[nKey]; bHint = true; } // Bound the number of simultaneously lit keys, so no sequence of // MIDI events can drive the strip past the supply's budget. if ( ucVelocity == 0 || nLit >= MAX_LIT_KEYS) { continue; } 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) { PaintKey (nKey, Scale (HINT_COLOR_R, ucVelocity), Scale (HINT_COLOR_G, ucVelocity), Scale (HINT_COLOR_B, ucVelocity)); } else { PaintKey (nKey, Scale (NOTE_COLOR_R, ucVelocity), Scale (NOTE_COLOR_G, ucVelocity), Scale (NOTE_COLOR_B, ucVelocity)); } } } void CPianoLEDs::RenderCalibration (void) { // Patterns run at the same ceiling as normal operation, so nothing here // can draw more current than the design already allows. const uint8_t W = GLOBAL_BRIGHTNESS; 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) { RenderCalibration (); } else if (!m_bHostConnected) { // No host means no notes can arrive, so show a heartbeat rather // than a strip that looks identical to an unpowered one. RenderIdle (); } else { RenderNotes (); } m_Strip.Update (); }