Skip to content

Commit b6a51ef

Browse files
feat(monitor): add MIDI channel filter via setChannel()
One Monitor = one harmonic context = one channel. noteOn/noteOff now carry channel (1–16); GingoMIDI1 and GingoMIDI2 dispatch extract the channel nibble and forward it. setChannel(0) accepts all channels (default, backward-compatible). 7 new tests, 416 total, 0 failures.
1 parent d0ff4a7 commit b6a51ef

5 files changed

Lines changed: 103 additions & 32 deletions

File tree

β€Žextras/tests/test_native.cppβ€Ž

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,9 +1273,9 @@ void testMonitor() {
12731273
// Test basic note tracking via polling
12741274
{
12751275
GingoMonitor mon;
1276-
mon.noteOn(60, 100); // C4
1277-
mon.noteOn(64, 100); // E4
1278-
mon.noteOn(67, 100); // G4
1276+
mon.noteOn(1, 60, 100); // C4
1277+
mon.noteOn(1, 64, 100); // E4
1278+
mon.noteOn(1, 67, 100); // G4
12791279
// Should detect CM chord
12801280
CHECK(mon.hasChord(), "3 notes β†’ chord detected");
12811281
CHECK(strcmp(mon.currentChord().name(), "CM") == 0, "C+E+G = CM");
@@ -1284,23 +1284,23 @@ void testMonitor() {
12841284
// Note off removes note, chord may change
12851285
{
12861286
GingoMonitor mon;
1287-
mon.noteOn(60, 100); // C
1288-
mon.noteOn(64, 100); // E
1289-
mon.noteOn(67, 100); // G
1287+
mon.noteOn(1, 60, 100); // C
1288+
mon.noteOn(1, 64, 100); // E
1289+
mon.noteOn(1, 67, 100); // G
12901290
CHECK(mon.hasChord(), "CM detected before noteOff");
1291-
mon.noteOff(67); // remove G
1291+
mon.noteOff(1, 67); // remove G
12921292
// C+E alone β€” not enough for a chord
12931293
CHECK(!mon.hasChord(), "C+E alone not a chord");
12941294
}
12951295

12961296
// Sustain pedal keeps notes
12971297
{
12981298
GingoMonitor mon;
1299-
mon.noteOn(60, 100); // C
1300-
mon.noteOn(64, 100); // E
1301-
mon.noteOn(67, 100); // G
1299+
mon.noteOn(1, 60, 100); // C
1300+
mon.noteOn(1, 64, 100); // E
1301+
mon.noteOn(1, 67, 100); // G
13021302
mon.sustainOn();
1303-
mon.noteOff(67); // G sustained
1303+
mon.noteOff(1, 67); // G sustained
13041304
// Chord should still be detected (G is sustained)
13051305
CHECK(mon.hasChord(), "sustain keeps chord");
13061306
CHECK(strcmp(mon.currentChord().name(), "CM") == 0, "sustained chord still CM");
@@ -1311,14 +1311,62 @@ void testMonitor() {
13111311
// Reset clears everything
13121312
{
13131313
GingoMonitor mon;
1314-
mon.noteOn(60, 100);
1315-
mon.noteOn(64, 100);
1316-
mon.noteOn(67, 100);
1314+
mon.noteOn(1, 60, 100);
1315+
mon.noteOn(1, 64, 100);
1316+
mon.noteOn(1, 67, 100);
13171317
CHECK(mon.hasChord(), "chord before reset");
13181318
mon.reset();
13191319
CHECK(!mon.hasChord(), "reset clears chord");
13201320
}
13211321

1322+
// Channel filter β€” setChannel / channel()
1323+
{
1324+
GingoMonitor mon;
1325+
mon.setChannel(2);
1326+
CHECK(mon.channel() == 2, "setChannel(2) stored");
1327+
1328+
// Events on channel 2 must pass
1329+
mon.noteOn(2, 60, 100);
1330+
mon.noteOn(2, 64, 100);
1331+
mon.noteOn(2, 67, 100);
1332+
CHECK(mon.hasChord(), "channel 2 events accepted");
1333+
1334+
// Events on channel 1 must be silently ignored
1335+
mon.reset();
1336+
mon.noteOn(1, 60, 100);
1337+
mon.noteOn(1, 64, 100);
1338+
mon.noteOn(1, 67, 100);
1339+
CHECK(!mon.hasChord(), "channel 1 events rejected by filter");
1340+
}
1341+
1342+
// Channel filter 0 = accept all
1343+
{
1344+
GingoMonitor mon;
1345+
mon.setChannel(0);
1346+
mon.noteOn(3, 60, 100);
1347+
mon.noteOn(5, 64, 100);
1348+
mon.noteOn(9, 67, 100);
1349+
CHECK(mon.hasChord(), "channel filter 0 accepts all channels");
1350+
}
1351+
1352+
// noteOff filtered too
1353+
{
1354+
GingoMonitor mon;
1355+
mon.setChannel(1);
1356+
mon.noteOn(1, 60, 100);
1357+
mon.noteOn(1, 64, 100);
1358+
mon.noteOn(1, 67, 100);
1359+
CHECK(mon.hasChord(), "noteOff filter test: CM detected ch1");
1360+
1361+
// noteOff on wrong channel must not remove note
1362+
mon.noteOff(2, 67);
1363+
CHECK(mon.hasChord(), "noteOff on ch2 ignored, chord remains");
1364+
1365+
// noteOff on correct channel removes note
1366+
mon.noteOff(1, 67);
1367+
CHECK(!mon.hasChord(), "noteOff on ch1 removes note, chord gone");
1368+
}
1369+
13221370
#if GINGODUINO_TIER >= 3
13231371
// Lambda callback (std::function, Tier 3)
13241372
{
@@ -1328,16 +1376,16 @@ void testMonitor() {
13281376
(void)ctx;
13291377
noteCount++;
13301378
});
1331-
mon.noteOn(60, 100);
1332-
mon.noteOn(64, 100);
1379+
mon.noteOn(1, 60, 100);
1380+
mon.noteOn(1, 64, 100);
13331381
CHECK(noteCount == 2, "onNoteOn lambda called 2 times");
13341382

13351383
bool chordFired = false;
13361384
mon.onChordDetected([&chordFired](const GingoChord& c) {
13371385
(void)c;
13381386
chordFired = true;
13391387
});
1340-
mon.noteOn(67, 100); // completes CM
1388+
mon.noteOn(1, 67, 100); // completes CM
13411389
CHECK(chordFired, "onChordDetected lambda fired");
13421390
}
13431391
#endif

β€Žsrc/GingoMIDI1.hβ€Ž

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ namespace gingoduino {
5454
/// Stateless MIDI 1.0 dispatcher.
5555
///
5656
/// Accepts pre-parsed (status, data1, data2) tuples and routes them to a
57-
/// GingoMonitor. All channels are accepted (channel nibble is ignored).
57+
/// GingoMonitor. The channel nibble is extracted and forwarded β€” the monitor
58+
/// applies its own channel filter (see GingoMonitor::setChannel).
5859
///
5960
/// Handled messages:
6061
/// β€’ 0x9n Note On β€” vel > 0 β†’ noteOn, vel == 0 β†’ noteOff (running-status trick)
@@ -73,16 +74,17 @@ class GingoMIDI1 {
7374
static bool dispatch(uint8_t status, uint8_t data1, uint8_t data2,
7475
GingoMonitor& mon) {
7576
uint8_t type = status & 0xF0;
77+
uint8_t ch = (status & 0x0F) + 1; // MIDI channel 1–16
7678

7779
// Note On β€” vel=0 treated as Note Off (running-status convention)
7880
if (type == 0x90) {
79-
if (data2 > 0) { mon.noteOn(data1, data2); return true; }
80-
mon.noteOff(data1); return true;
81+
if (data2 > 0) { mon.noteOn(ch, data1, data2); return true; }
82+
mon.noteOff(ch, data1); return true;
8183
}
8284

8385
// Note Off
8486
if (type == 0x80) {
85-
mon.noteOff(data1); return true;
87+
mon.noteOff(ch, data1); return true;
8688
}
8789

8890
// Control Change

β€Žsrc/GingoMIDI2.hβ€Ž

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,12 @@ class GingoMIDI2 {
233233
// Word 0: [MT][Group][Status byte][Data1][Data2]
234234
if (mt == 0x2) {
235235
uint8_t opcode = (uint8_t)((words[0] >> 20) & 0xF);
236+
uint8_t ch = (uint8_t)((words[0] >> 16) & 0xF) + 1; // channel 1–16
236237
uint8_t data1 = (uint8_t)((words[0] >> 8) & 0x7F);
237238
uint8_t data2 = (uint8_t)(words[0] & 0x7F);
238239

239-
if (opcode == 0x9 && data2 > 0) { mon.noteOn(data1, data2); return true; }
240-
if (opcode == 0x8 || (opcode == 0x9 && data2 == 0)) { mon.noteOff(data1); return true; }
240+
if (opcode == 0x9 && data2 > 0) { mon.noteOn(ch, data1, data2); return true; }
241+
if (opcode == 0x8 || (opcode == 0x9 && data2 == 0)) { mon.noteOff(ch, data1); return true; }
241242
// CC (opcode 0xB): data1=CC number, data2=value
242243
if (opcode == 0xB) {
243244
if (data1 == 64) { (data2 >= 64) ? mon.sustainOn() : mon.sustainOff(); return true; }
@@ -251,14 +252,15 @@ class GingoMIDI2 {
251252
// Word 1: [Value 32-bit]
252253
if (mt == 0x4) {
253254
uint8_t opcode = (uint8_t)((words[0] >> 20) & 0xF);
255+
uint8_t ch = (uint8_t)((words[0] >> 16) & 0xF) + 1; // channel 1–16
254256
uint8_t index = (uint8_t)((words[0] >> 8) & 0x7F);
255257

256258
// Note On/Off
257259
if (opcode == 0x9 || opcode == 0x8) {
258260
uint16_t vel16 = (uint16_t)((words[1] >> 16) & 0xFFFF);
259261
uint8_t vel7 = (uint8_t)(vel16 >> 9);
260-
if (opcode == 0x9 && vel16 > 0) { mon.noteOn(index, vel7); return true; }
261-
mon.noteOff(index); return true;
262+
if (opcode == 0x9 && vel16 > 0) { mon.noteOn(ch, index, vel7); return true; }
263+
mon.noteOff(ch, index); return true;
262264
}
263265
// CC (opcode 0xB): index=CC number, word1=32-bit value
264266
if (opcode == 0xB) {

β€Žsrc/GingoMonitor.cppβ€Ž

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ namespace gingoduino {
1616
// ---------------------------------------------------------------------------
1717

1818
GingoMonitor::GingoMonitor()
19-
: heldCount_(0)
19+
: channelFilter_(0)
20+
, heldCount_(0)
2021
, sustainHeld_(false)
2122
, chordValid_(false)
2223
, fieldValid_(false)
@@ -182,7 +183,8 @@ void GingoMonitor::analyse_() {
182183
// MIDI event feed
183184
// ---------------------------------------------------------------------------
184185

185-
void GingoMonitor::noteOn(uint8_t midiNum, uint8_t velocity) {
186+
void GingoMonitor::noteOn(uint8_t channel, uint8_t midiNum, uint8_t velocity) {
187+
if (channelFilter_ != 0 && channel != channelFilter_) return;
186188
(void)velocity;
187189

188190
// Add note (avoid duplicates); re-analyse only if new
@@ -218,7 +220,8 @@ void GingoMonitor::noteOn(uint8_t midiNum, uint8_t velocity) {
218220
fireNote_(ctx);
219221
}
220222

221-
void GingoMonitor::noteOff(uint8_t midiNum) {
223+
void GingoMonitor::noteOff(uint8_t channel, uint8_t midiNum) {
224+
if (channelFilter_ != 0 && channel != channelFilter_) return;
222225
if (sustainHeld_) {
223226
// Mark note as sustained instead of removing
224227
for (uint8_t i = 0; i < heldCount_; i++) {

β€Žsrc/GingoMonitor.hβ€Ž

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,17 @@ class GingoMonitor {
8787

8888
GingoMonitor();
8989

90+
// ------------------------------------------------------------------
91+
// Channel filter
92+
// ------------------------------------------------------------------
93+
94+
/// Set the MIDI channel filter (1–16). Pass 0 to accept all channels (default).
95+
/// A Monitor with setChannel(1) only processes events from MIDI channel 1.
96+
void setChannel(uint8_t ch) { channelFilter_ = ch; }
97+
98+
/// Currently configured channel filter. 0 = all channels accepted.
99+
uint8_t channel() const { return channelFilter_; }
100+
90101
// ------------------------------------------------------------------
91102
// Callback registration β€” function pointer style (all tiers)
92103
// ------------------------------------------------------------------
@@ -123,15 +134,17 @@ class GingoMonitor {
123134
// ------------------------------------------------------------------
124135

125136
/// Process a MIDI Note On event.
126-
/// Adds the note, updates chord/field state, fires callbacks.
137+
/// Silently ignored if the monitor has a channel filter and channel != filter.
138+
/// @param channel MIDI channel (1–16).
127139
/// @param midiNum MIDI note number (0–127).
128140
/// @param velocity MIDI velocity (1–127; ignored for state but stored).
129-
void noteOn(uint8_t midiNum, uint8_t velocity = 100);
141+
void noteOn(uint8_t channel, uint8_t midiNum, uint8_t velocity = 100);
130142

131143
/// Process a MIDI Note Off event.
132-
/// Removes the note and re-evaluates state.
144+
/// Silently ignored if the monitor has a channel filter and channel != filter.
145+
/// @param channel MIDI channel (1–16).
133146
/// @param midiNum MIDI note number (0–127).
134-
void noteOff(uint8_t midiNum);
147+
void noteOff(uint8_t channel, uint8_t midiNum);
135148

136149
/// Reset all held notes and clear chord/field state.
137150
void reset();
@@ -172,6 +185,9 @@ class GingoMonitor {
172185
const GingoField& currentField() const { return field_; }
173186

174187
private:
188+
// Channel filter (0 = all channels, 1–16 = specific channel)
189+
uint8_t channelFilter_;
190+
175191
// Held MIDI note numbers (max 16 simultaneous β€” practical theory limit)
176192
static const uint8_t MAX_HELD = 16;
177193
uint8_t held_[MAX_HELD];

0 commit comments

Comments
Β (0)