Add Phase 1 Circle firmware
Greenfield bare-metal firmware for a Pi Zero that lights a WS2812B strip above an 88-key keybed. The Pi is a USB MIDI gadget; the PC is the host and owns the piano connection and everything else. No network stack, no filesystem, no shell. Circle is a submodule pinned to Step51. Builds kernel.img (RASPPI=1, Zero / Zero W) and kernel7.img (RASPPI=2, Zero 2 / Zero 2 W); both coexist on one card, so either model runs from the same SD. Resolves two open assumptions from the plan against the Circle sources rather than by guessing: - CWS28XXStripe clocks the waveform out over SPI at a fixed 6.4MHz, one SPI byte per LED bit. On device 0 that puts data on MOSI = GPIO10 = physical pin 19. The implied 5.28ms frame time matches the plan's arithmetic. - The USB gadget lifecycle follows sample/29-miniorgan, which already carries a USB_GADGET_MODE path. Three details the hardware forces: - The gadget destroys and recreates its CUSBMIDIDevice across a USB suspend, so the kernel re-fetches it and clears notes held at that moment. Otherwise a chord would stay lit forever when the PC sleeps. - Rendering blocks for ~5.3ms of SPI traffic, so the MIDI packet handler only records state and the main loop draws. - MAX_LIT_KEYS complements the global brightness ceiling. 176 LEDs at full white would draw ~10.5A against a 6A supply; together the two clamps make that unreachable rather than merely unlikely. Every Phase 0 product decision has a named slot in firmware/config.h, all overridable at build time via EXTRADEFINE. tests/run.sh compiles the real pianoleds.cpp against stubbed Circle headers and checks the mapping, note-off paths, range clamping and both power clamps across nine configuration variants. It verifies arithmetic, not wiring, and does not replace bench-testing on real hardware. Claude-Session: https://claude.ai/code/session_01TVCB25LBsmeteWvaSMz4Ne
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
#
|
||||
# Makefile
|
||||
#
|
||||
|
||||
CIRCLEHOME = ../circle
|
||||
|
||||
OBJS = main.o kernel.o pianoleds.o
|
||||
|
||||
LIBS = $(CIRCLEHOME)/addon/WS28XX/libws28xx.a \
|
||||
$(CIRCLEHOME)/lib/usb/gadget/libusbgadget.a \
|
||||
$(CIRCLEHOME)/lib/usb/libusb.a \
|
||||
$(CIRCLEHOME)/lib/input/libinput.a \
|
||||
$(CIRCLEHOME)/lib/fs/libfs.a \
|
||||
$(CIRCLEHOME)/lib/sched/libsched.a \
|
||||
$(CIRCLEHOME)/lib/libcircle.a
|
||||
|
||||
# Build-time overrides for config.h, e.g.
|
||||
# make EXTRADEFINE=-DSTRIP_REVERSED=1
|
||||
# Appended, so Circle's own defines (-DRASPPI=... etc.) survive.
|
||||
DEFINE += $(EXTRADEFINE)
|
||||
|
||||
include $(CIRCLEHOME)/sample/Rules.mk
|
||||
|
||||
-include $(DEPS)
|
||||
@@ -0,0 +1,146 @@
|
||||
//
|
||||
// config.h
|
||||
//
|
||||
// Piano LED Visualizer on Circle - all tunable parameters.
|
||||
//
|
||||
// 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
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 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
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Hardware wiring (VERIFIED against circle/addon/WS28XX, do not guess)
|
||||
// --------------------------------------------------------------------------
|
||||
//
|
||||
// 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 puts the
|
||||
// data line on:
|
||||
//
|
||||
// MOSI = GPIO10 (BCM) = physical pin 19
|
||||
//
|
||||
// Feed that through a 74AHCT125 to get a 5V logic level at the strip, and tie
|
||||
// the Pi's ground to the LED supply ground. See plan section 7.
|
||||
#ifndef SPI_MASTER_DEVICE
|
||||
#define SPI_MASTER_DEVICE 0
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// kernel.cpp
|
||||
//
|
||||
// Piano LED Visualizer for Circle.
|
||||
//
|
||||
// The Pi is a USB MIDI *gadget*: the PC is the host, and this firmware appears
|
||||
// on the PC as an ordinary ALSA MIDI output port. Circle has no OTG support, so
|
||||
// the USB controller is gadget-only in this build and the piano can never be
|
||||
// plugged in here directly - all MIDI arrives from the PC. See section 2 of
|
||||
// PIANO-LED-CIRCLE-PLAN.md.
|
||||
//
|
||||
#include "kernel.h"
|
||||
#include <circle/usb/gadget/usbmidigadget.h>
|
||||
#include <circle/devicenameservice.h>
|
||||
#include <assert.h>
|
||||
|
||||
static const char FromKernel[] = "kernel";
|
||||
|
||||
CKernel::CKernel (void)
|
||||
: m_Timer (&m_Interrupt),
|
||||
m_Logger (m_Options.GetLogLevel (), &m_Timer),
|
||||
m_pUSB (new CUSBMIDIGadget (&m_Interrupt)),
|
||||
m_pMIDIDevice (0)
|
||||
{
|
||||
m_ActLED.Blink (5); // show we are alive
|
||||
}
|
||||
|
||||
CKernel::~CKernel (void)
|
||||
{
|
||||
}
|
||||
|
||||
boolean CKernel::Initialize (void)
|
||||
{
|
||||
boolean bOK = TRUE;
|
||||
|
||||
if (bOK)
|
||||
{
|
||||
bOK = m_Serial.Initialize (115200);
|
||||
}
|
||||
|
||||
if (bOK)
|
||||
{
|
||||
// Headless appliance: there is no screen, so the log goes to the
|
||||
// serial port and nowhere else.
|
||||
bOK = m_Logger.Initialize (&m_Serial);
|
||||
}
|
||||
|
||||
if (bOK)
|
||||
{
|
||||
bOK = m_Interrupt.Initialize ();
|
||||
}
|
||||
|
||||
if (bOK)
|
||||
{
|
||||
bOK = m_Timer.Initialize ();
|
||||
}
|
||||
|
||||
if (bOK)
|
||||
{
|
||||
// Bring the strip up before USB, so the LEDs are known-dark by the
|
||||
// time the host can start sending us notes.
|
||||
bOK = m_PianoLEDs.Initialize ();
|
||||
}
|
||||
|
||||
if (bOK)
|
||||
{
|
||||
assert (m_pUSB != 0);
|
||||
bOK = m_pUSB->Initialize ();
|
||||
}
|
||||
|
||||
return bOK;
|
||||
}
|
||||
|
||||
void CKernel::UpdateMIDIDevice (void)
|
||||
{
|
||||
assert (m_pUSB != 0);
|
||||
|
||||
if (!m_pUSB->UpdatePlugAndPlay ())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// The gadget deletes its CUSBMIDIDevice when the host suspends the bus
|
||||
// and builds a new one on the next enumeration, so the pointer we hold
|
||||
// is only valid until the next status change. Re-fetch it every time.
|
||||
CUSBMIDIDevice *pMIDIDevice =
|
||||
(CUSBMIDIDevice *) m_DeviceNameService.GetDevice ("umidi1", FALSE);
|
||||
|
||||
if (pMIDIDevice == m_pMIDIDevice)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_pMIDIDevice = pMIDIDevice;
|
||||
|
||||
if (m_pMIDIDevice != 0)
|
||||
{
|
||||
m_PianoLEDs.AttachMIDIDevice (m_pMIDIDevice);
|
||||
|
||||
m_Logger.Write (FromKernel, LogNotice, "USB MIDI gadget connected");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Host went away mid-chord. Do not leave keys lit.
|
||||
m_PianoLEDs.AllOff ();
|
||||
|
||||
m_Logger.Write (FromKernel, LogNotice, "USB MIDI gadget disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
TShutdownMode CKernel::Run (void)
|
||||
{
|
||||
m_Logger.Write (FromKernel, LogNotice, "Compile time: " __DATE__ " " __TIME__);
|
||||
m_Logger.Write (FromKernel, LogNotice,
|
||||
"%u LEDs, %u keys, notes %u-%u, %s orientation",
|
||||
(unsigned) LED_COUNT, (unsigned) KEY_COUNT,
|
||||
(unsigned) MIDI_NOTE_MIN, (unsigned) MIDI_NOTE_MAX,
|
||||
STRIP_REVERSED ? "reversed" : "normal");
|
||||
m_Logger.Write (FromKernel, LogNotice,
|
||||
"Brightness ceiling %u/255, at most %u keys lit at once",
|
||||
(unsigned) GLOBAL_BRIGHTNESS, (unsigned) MAX_LIT_KEYS);
|
||||
m_Logger.Write (FromKernel, LogNotice, "Waiting for USB host");
|
||||
|
||||
for (;;)
|
||||
{
|
||||
UpdateMIDIDevice ();
|
||||
|
||||
// Rendering blocks for ~5.3ms of SPI traffic, which is why it runs
|
||||
// here and not in the MIDI packet handler's IRQ context. Update()
|
||||
// returns immediately when nothing has changed.
|
||||
m_PianoLEDs.Update ();
|
||||
}
|
||||
|
||||
return ShutdownHalt;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// kernel.h
|
||||
//
|
||||
#ifndef _kernel_h
|
||||
#define _kernel_h
|
||||
|
||||
#include <circle/actled.h>
|
||||
#include <circle/koptions.h>
|
||||
#include <circle/devicenameservice.h>
|
||||
#include <circle/serial.h>
|
||||
#include <circle/exceptionhandler.h>
|
||||
#include <circle/interrupt.h>
|
||||
#include <circle/timer.h>
|
||||
#include <circle/logger.h>
|
||||
#include <circle/types.h>
|
||||
#include <circle/usb/usbcontroller.h>
|
||||
#include <circle/usb/usbmidi.h>
|
||||
#include "pianoleds.h"
|
||||
|
||||
enum TShutdownMode
|
||||
{
|
||||
ShutdownNone,
|
||||
ShutdownHalt,
|
||||
ShutdownReboot
|
||||
};
|
||||
|
||||
class CKernel
|
||||
{
|
||||
public:
|
||||
CKernel (void);
|
||||
~CKernel (void);
|
||||
|
||||
boolean Initialize (void);
|
||||
|
||||
TShutdownMode Run (void);
|
||||
|
||||
private:
|
||||
// Pick up the MIDI device after the host has enumerated us, and again
|
||||
// after every re-enumeration.
|
||||
void UpdateMIDIDevice (void);
|
||||
|
||||
private:
|
||||
// do not change this order
|
||||
CActLED m_ActLED;
|
||||
CKernelOptions m_Options;
|
||||
CDeviceNameService m_DeviceNameService;
|
||||
CSerialDevice m_Serial;
|
||||
CExceptionHandler m_ExceptionHandler;
|
||||
CInterruptSystem m_Interrupt;
|
||||
CTimer m_Timer;
|
||||
CLogger m_Logger;
|
||||
|
||||
CUSBController *m_pUSB;
|
||||
CUSBMIDIDevice *m_pMIDIDevice;
|
||||
|
||||
CPianoLEDs m_PianoLEDs;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// main.cpp
|
||||
//
|
||||
#include "kernel.h"
|
||||
#include <circle/startup.h>
|
||||
|
||||
int main (void)
|
||||
{
|
||||
// cannot return here because some destructors used in CKernel are not implemented
|
||||
|
||||
CKernel Kernel;
|
||||
if (!Kernel.Initialize ())
|
||||
{
|
||||
halt ();
|
||||
return EXIT_HALT;
|
||||
}
|
||||
|
||||
TShutdownMode ShutdownMode = Kernel.Run ();
|
||||
|
||||
switch (ShutdownMode)
|
||||
{
|
||||
case ShutdownReboot:
|
||||
reboot ();
|
||||
return EXIT_REBOOT;
|
||||
|
||||
case ShutdownHalt:
|
||||
default:
|
||||
halt ();
|
||||
return EXIT_HALT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
//
|
||||
// pianoleds.cpp
|
||||
//
|
||||
#include "pianoleds.h"
|
||||
#include <circle/util.h>
|
||||
#include <assert.h>
|
||||
|
||||
// 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 (void)
|
||||
: m_Stripe (WS2812B, LED_COUNT, 4000000, SPI_MASTER_DEVICE),
|
||||
m_bDirty (TRUE)
|
||||
{
|
||||
memset ((void *) m_KeyVelocity, 0, sizeof m_KeyVelocity);
|
||||
memset ((void *) m_HintVelocity, 0, sizeof m_HintVelocity);
|
||||
}
|
||||
|
||||
CPianoLEDs::~CPianoLEDs (void)
|
||||
{
|
||||
}
|
||||
|
||||
boolean CPianoLEDs::Initialize (void)
|
||||
{
|
||||
if (!m_Stripe.Initialize ())
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Start from a known-dark strip rather than whatever the pixels held
|
||||
// when power came up.
|
||||
return m_Stripe.Blackout ();
|
||||
}
|
||||
|
||||
void CPianoLEDs::AttachMIDIDevice (CUSBMIDIDevice *pMIDIDevice)
|
||||
{
|
||||
assert (pMIDIDevice != 0);
|
||||
|
||||
// The gadget destroys and recreates its CUSBMIDIDevice across a suspend,
|
||||
// so any notes held at that moment would otherwise stay lit forever.
|
||||
AllOff ();
|
||||
|
||||
pMIDIDevice->RegisterPacketHandler (MIDIPacketHandler, this);
|
||||
}
|
||||
|
||||
void CPianoLEDs::MIDIPacketHandler (unsigned nCable, u8 *pPacket, unsigned nLength,
|
||||
unsigned nDevice, void *pParam)
|
||||
{
|
||||
CPianoLEDs *pThis = static_cast<CPianoLEDs *> (pParam);
|
||||
assert (pThis != 0);
|
||||
|
||||
pThis->OnMIDIPacket (pPacket, nLength);
|
||||
}
|
||||
|
||||
void CPianoLEDs::OnMIDIPacket (const u8 *pPacket, unsigned nLength)
|
||||
{
|
||||
// Circle 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;
|
||||
}
|
||||
|
||||
u8 ucStatus = pPacket[0] & 0xF0;
|
||||
u8 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:
|
||||
if ( pPacket[1] == MIDI_CC_ALL_SOUND_OFF
|
||||
|| pPacket[1] == MIDI_CC_ALL_NOTES_OFF)
|
||||
{
|
||||
AllOff ();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void CPianoLEDs::SetKey (u8 ucNote, u8 ucVelocity, boolean 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::AllOff (void)
|
||||
{
|
||||
memset ((void *) m_KeyVelocity, 0, sizeof m_KeyVelocity);
|
||||
memset ((void *) m_HintVelocity, 0, sizeof m_HintVelocity);
|
||||
|
||||
m_bDirty = TRUE;
|
||||
}
|
||||
|
||||
boolean CPianoLEDs::ChannelMatches (u8 ucChannel, u8 ucWanted)
|
||||
{
|
||||
if (ucWanted == MIDI_CHANNEL_NONE)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (ucWanted == MIDI_CHANNEL_ANY)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return ucChannel == ucWanted;
|
||||
}
|
||||
|
||||
u8 CPianoLEDs::Scale (u8 ucChannel, u8 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 (u8) nValue;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
unsigned nLit = 0;
|
||||
|
||||
for (unsigned nKey = 0; nKey < KEY_COUNT; nKey++)
|
||||
{
|
||||
u8 ucVelocity = m_KeyVelocity[nKey];
|
||||
boolean bHint = FALSE;
|
||||
|
||||
if (ucVelocity == 0)
|
||||
{
|
||||
// A key being played wins over a "next note" hint on it.
|
||||
ucVelocity = m_HintVelocity[nKey];
|
||||
bHint = TRUE;
|
||||
}
|
||||
|
||||
u8 ucRed = 0;
|
||||
u8 ucGreen = 0;
|
||||
u8 ucBlue = 0;
|
||||
|
||||
// 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)
|
||||
{
|
||||
nLit++;
|
||||
|
||||
if (bHint)
|
||||
{
|
||||
ucRed = Scale (HINT_COLOR_R, ucVelocity);
|
||||
ucGreen = Scale (HINT_COLOR_G, ucVelocity);
|
||||
ucBlue = Scale (HINT_COLOR_B, ucVelocity);
|
||||
}
|
||||
else
|
||||
{
|
||||
ucRed = Scale (NOTE_COLOR_R, ucVelocity);
|
||||
ucGreen = Scale (NOTE_COLOR_G, ucVelocity);
|
||||
ucBlue = Scale (NOTE_COLOR_B, ucVelocity);
|
||||
}
|
||||
}
|
||||
|
||||
#if STRIP_REVERSED
|
||||
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;
|
||||
assert (nLED < LED_COUNT);
|
||||
|
||||
m_Stripe.SetLED (nLED, ucRed, ucGreen, ucBlue);
|
||||
}
|
||||
}
|
||||
|
||||
m_Stripe.Update ();
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// pianoleds.h
|
||||
//
|
||||
// Maps incoming MIDI note events onto a WS2812B strip mounted above an
|
||||
// 88-key keybed.
|
||||
//
|
||||
#ifndef _pianoleds_h
|
||||
#define _pianoleds_h
|
||||
|
||||
#include <circle/usb/usbmidi.h>
|
||||
#include <circle/types.h>
|
||||
#include <WS28XX/ws28xxstripe.h>
|
||||
#include "config.h"
|
||||
|
||||
class CPianoLEDs
|
||||
{
|
||||
public:
|
||||
CPianoLEDs (void);
|
||||
~CPianoLEDs (void);
|
||||
|
||||
boolean Initialize (void);
|
||||
|
||||
// Attach to a USB MIDI device. Safe to call again after the gadget has
|
||||
// been re-enumerated, which destroys and recreates the device object.
|
||||
void AttachMIDIDevice (CUSBMIDIDevice *pMIDIDevice);
|
||||
|
||||
// Push pending state to the strip. Call from the main loop only; this
|
||||
// blocks for ~5.3ms of SPI traffic and must never run in IRQ context.
|
||||
// Does nothing when no state has changed since the last call.
|
||||
void Update (void);
|
||||
|
||||
// Extinguish every pixel and forget all held notes.
|
||||
void AllOff (void);
|
||||
|
||||
private:
|
||||
// Called in IRQ context by the USB MIDI driver.
|
||||
static void MIDIPacketHandler (unsigned nCable, u8 *pPacket, unsigned nLength,
|
||||
unsigned nDevice, void *pParam);
|
||||
|
||||
void OnMIDIPacket (const u8 *pPacket, unsigned nLength);
|
||||
|
||||
void SetKey (u8 ucNote, u8 ucVelocity, boolean bHint);
|
||||
|
||||
// Scale a colour channel by velocity and the global brightness ceiling.
|
||||
static u8 Scale (u8 ucChannel, u8 ucVelocity);
|
||||
|
||||
static boolean ChannelMatches (u8 ucChannel, u8 ucWanted);
|
||||
|
||||
private:
|
||||
CWS28XXStripe m_Stripe;
|
||||
|
||||
// Written in IRQ context, read by Update(). Index is
|
||||
// note - MIDI_NOTE_MIN. Zero means the key is not lit.
|
||||
volatile u8 m_KeyVelocity[KEY_COUNT];
|
||||
volatile u8 m_HintVelocity[KEY_COUNT];
|
||||
|
||||
volatile boolean m_bDirty;
|
||||
};
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user