๐Ÿช‡ Gingo[duino]

June 6, 2026 ยท View on GitHub

Gingoduino

๐Ÿช‡ Gingo[duino]

Music theory engine for embedded systems. Build musical instruments and controllers that interact with people and speak MIDI 1.0 and 2.0.

Mentioned in Awesome Music

MIDI 2.0 Platform License: MIT

Sponsor

PT-BR


Contents

Overview

Gingoduino is a music theory engine for embedded systems. It owns the musical domain (notes, intervals, chords, scales, harmonic fields, harmonic trees, progression analysis, fretboard engine), a real-time harmonic monitor, a live UMP stream interpreter (GingoFlow), and stateless output adapters that translate musical structures into serialized formats (raw MIDI 1.0 bytes and MIDI 2.0 UMP Flex Data).

It does not own the wire. Byte stream parsing, UMP receive dispatch, and MIDI-CI discovery and Property Exchange are protocol concerns delegated to dedicated libraries: midi2cpp for UMP, Arduino MIDI Library or your own parser for MIDI 1.0 byte streams, the midi2 C99 library for MIDI-CI responders.

Zero-heap architecture, PROGMEM lookup tables, C++11 compatible.

0.4.0 is a breaking release. See CHANGELOG.md for the migration guide if you are upgrading from 0.3.x.

Roles in the ecosystem

Gingoduino is one of three companion libraries with distinct, complementary roles.

LibraryRoleLanguageSpeaks
midi2Infrastructure: UMP wire, parsing, dispatch, MIDI-CIC99MIDI 2.0
midi2cppPlatform: USB MIDI 2.0 device, host, bridgeC++17MIDI 2.0
gingoduinoMusic theory engine. Notes, intervals, chords, scales, fields, harmonic monitor, output adaptersC++11MIDI 1.0 and 2.0
            โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
            โ”‚                Application                  โ”‚
            โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚                              โ”‚
                  โ”‚ uses for theory              โ”‚ uses for transport
                  โ–ผ                              โ–ผ
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚     gingoduino     โ”‚         โ”‚       midi2cpp       โ”‚
        โ”‚  Music theory      โ”‚         โ”‚  USB MIDI 2.0        โ”‚
        โ”‚  Notes ยท Chords    โ”‚         โ”‚  Device ยท Host       โ”‚
        โ”‚  Scales ยท Fields   โ”‚         โ”‚  Bridge ยท CI         โ”‚
        โ”‚  MIDI 1.0 + 2.0    โ”‚         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
        โ”‚  output adapters   โ”‚                    โ”‚ uses
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                    โ–ผ
                                       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                                       โ”‚        midi2         โ”‚
                                       โ”‚  UMP wire ยท MIDI-CI  โ”‚
                                       โ”‚  Parsing ยท Dispatch  โ”‚
                                       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Gingoduino is the only one of the three with a foot in MIDI 1.0. GingoMIDI1 serializes musical structures to raw MIDI 1.0 bytes (DIN, USB MIDI 1.0, virtual ports); GingoMIDI2 serializes the same structures to MIDI 2.0 UMP Flex Data, ready to feed into midi2cpp or any UMP transport.

Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    musical event      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   Transport     โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ   โ”‚   GingoMonitor   โ”‚
โ”‚  (midi2cpp,     โ”‚ noteOn / noteOff /    โ”‚  chord, field,   โ”‚
โ”‚   Arduino MIDI, โ”‚ sustainOn / Off       โ”‚  per-note ctx    โ”‚
โ”‚   ESP32_Host_   โ”‚                       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”‚   MIDI, ...)    โ”‚                                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                โ”‚
        โ–ฒ                                          โ–ผ
        โ”‚            โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚            โ”‚      Theory + Analysis           โ”‚
        โ”‚            โ”‚  Note ยท Interval ยท Chord ยท       โ”‚
        โ”‚            โ”‚  Scale ยท Field ยท Tree ยท          โ”‚
        โ”‚            โ”‚  Progression ยท Comparison ยท      โ”‚
        โ”‚            โ”‚  Fretboard ยท Event ยท Sequence    โ”‚
        โ”‚            โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
        โ”‚                              โ”‚
        โ”‚                              โ–ผ
        โ”‚            โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚            โ”‚      Output adapters             โ”‚
        โ”‚  bytes /   โ”‚  GingoMIDI1::fromEvent           โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค  GingoMIDI1::fromSequence        โ”‚
            UMP      โ”‚  GingoMIDI2::chordName           โ”‚
                     โ”‚  GingoMIDI2::keySignature        โ”‚
                     โ”‚  GingoMIDI2::perNoteController   โ”‚
                     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The transport sits outside the library. It parses bytes or UMP packets and feeds the Monitor through the musical-event API. Anything Gingoduino computes can be turned back into bytes or UMP through the namespaced output adapters.

Modules

Theory:

  • GingoNote: chromatic note, frequency, MIDI number, transposition
  • GingoInterval: labeled intervals, consonance, full name
  • GingoChord: 42 chord formulas, identification, transposition
  • GingoScale: scale types, modes, signature, brightness, formalNotes (diatonic spelling)
  • GingoField: harmonic field, T/S/D functions, role, relative/parallel, branchOf, appliedChords (secondary dominants), compare (21-dim GingoFieldComparison)
  • GingoTree: harmonic graph (classical and jazz traditions)
  • GingoProgression: identify, deduce, predict
  • GingoChordComparison: 17 dimensions, Neo-Riemannian, Forte vectors
  • GingoFretboard: guitar, violao, cavaquinho, mandolim/bandolim, ukulele; alternate tunings

Time and events:

  • GingoDuration (rational, operator+, operator*), GingoTempo, GingoTimeSig
  • GingoEvent, GingoSequence

Real-time analysis:

  • GingoMonitor: chord detection, field deduction, per-note context, onPrediction with rolling branch history
  • GingoFlow: live UMP stream interpreter; non-blocking capture, a logical event stream with note pairing, chord grouping and durations, an opt-in raw-UMP forward face, and a calibratable chord-grouping rule

Output adapters (stateless):

  • GingoMIDI1::fromEvent, GingoMIDI1::fromSequence: MIDI 1.0 bytes
  • GingoMIDI2::chordName, keySignature, perNoteController: MIDI 2.0 UMP Flex Data

Portability:

Gingoduino is portable C++11 with zero heap and PROGMEM lookup tables. It does not depend on any board API and should build on any toolchain that provides standard C++11. The families that ship in library.properties (esp32, esp8266, samd, nrf52, rp2040, mbed_rp2040, renesas_uno, renesas_portenta, stm32, teensy) are the targets we exercise; other Cortex-M / RISC-V / desktop targets compile cleanly too. See Hardware validation for the boards we ran the on-device proof on.

A couple of practical notes:

  • <functional> is included only when the toolchain ships it. AVR (avr-libc) does not, so the std::function lambda-with-capture overloads on GingoMonitor are unavailable on AVR; the function-pointer overloads still work.
  • AVR (Uno, Nano, Leonardo, etc., 32 KB Flash / 2-2.5 KB SRAM) cannot fit the full library together with the Field/Monitor/Progression pipeline. The practical AVR scope is GingoNote, GingoInterval, GingoChord, GingoScale (see examples/Foundation_AVR).

Features

  • 12-note chromatic system with enharmonic equivalents
  • 42 chord formulas with reverse lookup (identify)
  • 40+ scale types and modes with signature, brightness, relative and parallel
  • Harmonic field analysis with T/S/D functions and roles, plus deduction from notes and chords
  • Harmonic tree (directed graph, major and minor, classical and jazz traditions)
  • Progression analysis: identify, deduce (ranked), predict (next branch)
  • Fretboard engine: guitar, violao, cavaquinho, mandolim, ukulele; alternate tunings (Drop D, Open G, DADGAD); common chords and open-position fingerings
  • Musical events (note, chord, rest) and sequences with tempo and time signature
  • Real-time harmonic monitor with chord and field detection plus per-note context
  • Live UMP stream interpreter (GingoFlow): non-blocking capture, logical events with note pairing, chord grouping and durations, and an opt-in raw-UMP forward face
  • MIDI 1.0 output adapters: GingoMIDI1::fromEvent, GingoMIDI1::fromSequence
  • MIDI 2.0 UMP Flex Data output adapters: GingoMIDI2::chordName, keySignature, perNoteController
  • Chord comparison across 17 dimensions, including Neo-Riemannian transforms and Forte vectors
  • Fixed-size arrays, no dynamic allocation, PROGMEM support
  • Compatible with Arduino IDE, PlatformIO, ESP-IDF, and CMake (native)
  • 585 native tests passing under -Wall -Wextra -Werror

Spec coverage

SpecDocumentSurface in gingoduino
MIDI 1.0MIDI 1.0 Detailed SpecificationOutput: NoteOn/NoteOff bytes from GingoEvent and GingoSequence via GingoMIDI1::fromEvent, GingoMIDI1::fromSequence
MIDI 2.0 UMPM2-104-UM v1.1.2Output: Flex Data (Set Chord Name, Set Key Signature, Set Per-Note Controller) via GingoMIDI2
MIDI 2.0 Bit ScalingM2-115-U v1.0.2Internal: round-trip safe value scaling between 7/14/16/32-bit fields

Input parsing (MIDI 1.0 byte streams, UMP receive dispatch, MIDI-CI) is delegated to the transport library.

Installation

Arduino IDE Library Manager:

  • Sketch > Include Library > Manage Libraries > search Gingoduino > Install

PlatformIO:

; platformio.ini
lib_deps = sauloverissimo/Gingoduino

ESP-IDF Component:

idf.py add-dependency "sauloverissimo/gingoduino"

CMake / native:

include(FetchContent)
FetchContent_Declare(gingoduino
  GIT_REPOSITORY https://github.com/sauloverissimo/gingoduino.git
  GIT_TAG        v0.6.0)
FetchContent_MakeAvailable(gingoduino)
target_link_libraries(my_target PRIVATE gingoduino::gingoduino)

Or install it (cmake --install) and use find_package(gingoduino CONFIG).

Manual:

  • Download and copy to your Arduino libraries folder (~/Arduino/libraries/).

Quick start

#include <Gingoduino.h>

using namespace gingoduino;

void setup() {
    Serial.begin(9600);

    GingoNote note("C");
    Serial.println(note.name());           // "C"
    Serial.println(note.midiNumber(4));    // 60
    Serial.println(note.frequency(4), 1);  // 261.6

    GingoChord chord("Dm7");
    GingoNote notes[7];
    chord.notes(notes, 7);                 // D, F, A, C

    GingoScale scale("C", SCALE_MAJOR);
    GingoField field("C", SCALE_MAJOR);
    GingoChord triads[7];
    field.chords(triads, 7);               // CM, Dm, Em, FM, GM, Am, Bdim
}

void loop() {}

API reference

GingoNote

GingoNote note("C#");
note.name();              // "C#"
note.natural();           // "C#" (sharp canonical: Bb -> A#, Eb -> D#)
note.semitone();          // 1 (0-11)
note.frequency(4);        // Hz (float)
note.midiNumber(4);       // 0-127
note.transpose(7);        // GingoNote
note.distance(other);     // shortest distance on the circle of fifths (0-6)
note.isEnharmonic(other); // bool
GingoNote::fromMIDI(60);  // "C"
GingoNote::octaveFromMIDI(60); // 4

GingoInterval

GingoInterval iv("5J");          // or GingoInterval(7) or GingoInterval(noteA, noteB)
char buf[32];
iv.label(buf, sizeof(buf));      // "5J"
iv.semitones();                  // 7
iv.degree();                     // 5
iv.consonance(buf, sizeof(buf)); // "perfect", "imperfect" or "dissonant"
iv.isConsonant();                // true
iv.fullName(buf, sizeof(buf));   // "Perfect Fifth"
iv.fullNamePt(buf, sizeof(buf)); // "Quinta Justa"
iv.simple();                     // reduce compound to simple
iv.invert();                     // complement within octave

GingoChord

GingoChord chord("Dm7");
chord.name();                          // "Dm7"
chord.root();                          // GingoNote("D")
chord.type();                          // "m7"
chord.size();                          // 4

GingoNote notes[7];
chord.notes(notes, 7);                 // fill array with chord tones

GingoNote arr[3] = {GingoNote("C"), GingoNote("E"), GingoNote("G")};
char name[16];
GingoChord::identify(arr, 3, name, 16); // "CM"

GingoScale

GingoScale scale("C", SCALE_MAJOR);    // or GingoScale("C", "dorian")
char buf[22];
scale.modeName(buf, sizeof(buf));      // "Ionian"
scale.quality();                       // "major" or "minor"
scale.signature();                     // 0 (sharps > 0, flats < 0)
scale.brightness();                    // 1-7 (higher = brighter)

GingoNote notes[12];
scale.notes(notes, 12);                // fill with scale degrees
scale.mode(2);                         // Dorian
scale.pentatonic();                    // pentatonic version
scale.relative();                      // relative major or minor
scale.parallel();                      // parallel major or minor

GingoField

GingoField field("C", SCALE_MAJOR);
GingoChord triads[7];  field.chords(triads, 7);    // CM, Dm, Em, FM, GM, Am, Bdim
GingoChord sevs[7];    field.sevenths(sevs, 7);    // C7M, Dm7, Em7, F7M, G7, Am7, Bm7(b5)

field.function(5);                     // FUNC_DOMINANT
field.functionOf(GingoChord("GM"));    // FUNC_DOMINANT
char buf[12];
field.role(1, buf, sizeof(buf));       // "primary"

GingoNoteContext ctx = field.noteContext(GingoNote("E"));
ctx.degree;                            // 3
ctx.function;                          // FUNC_TONIC
ctx.inScale;                           // true
ctx.interval.semitones();              // 4

GingoFretboard

GingoFretboard guitar = GingoFretboard::guitar();   // 6 strings, E A D G B E
// Also: ::violao(), ::cavaquinho(), ::mandolin(), ::bandolim(), ::ukulele()
// Alternate tunings: ::dropD(), ::openG(), ::dadgad()

guitar.noteAt(0, 5);                  // GingoNote("A"), string 0, fret 5
guitar.midiAt(0, 0);                  // 40 (E2)

GingoFingering fgs[5];
guitar.fingerings(GingoChord("CM"), fgs, 5);    // up to 5 fingerings, sorted by score

GingoFingering opens[5];
guitar.openFingerings(GingoChord("GM"), opens, 5);  // open-position only

GingoFingering ccs[7];
guitar.commonChords(GingoScale("G", SCALE_MAJOR), ccs, 7);
// ccs[]: GM, Am, Bm, CM, DM, Em, F#dim sorted by field degree

GingoFretboard capo2 = guitar.capo(2);

GingoEvent and GingoSequence

GingoEvent ne = GingoEvent::noteEvent(GingoNote("C"), GingoDuration("quarter"), 4);
GingoEvent ce = GingoEvent::chordEvent(GingoChord("CM"), GingoDuration("half"));
GingoEvent re = GingoEvent::rest(GingoDuration("quarter"));

GingoSequence seq(GingoTempo(120), GingoTimeSig(4, 4));
seq.add(ne);
seq.totalBeats();   // 1.0
seq.totalSeconds(); // 0.5 at 120 BPM
seq.transpose(5);   // transpose all events

GingoMonitor

The Monitor receives musical events and tracks held notes, sustain pedal, detected chord, deduced field, and per-note context. Inputs come from any external transport. The Monitor itself does not parse MIDI.

GingoMonitor monitor;

monitor.setChannel(0xFF);   // accept all channels (default), or 0-15 to filter

// Feed events from your transport callbacks:
monitor.noteOn(0, 60, 100);   // channel 0, C4, velocity 100
monitor.noteOn(0, 64, 100);   // channel 0, E4
monitor.noteOn(0, 67, 100);   // channel 0, G4
monitor.sustainOn();
monitor.sustainOff();
monitor.reset();              // all notes off

// Poll state:
monitor.hasChord();           // true
monitor.currentChord();       // GingoChord("CM")
monitor.currentField();       // GingoField

// Callbacks (std::function lambdas, not on AVR):
monitor.onChordDetected([](const GingoChord& c)            { /* ... */ });
monitor.onFieldChanged([](const GingoField& f)             { /* ... */ });
monitor.onNoteOn      ([](const GingoNoteContext& ctx)     { /* ... */ });

GingoFlow

GingoFlow is a live UMP stream interpreter. It captures raw Universal MIDI Packets without blocking or dropping, then weaves a logical event stream structured by the rhythmic axis: each event carries full MIDI 2.0 resolution, a duration once its note-off pairs, and an index family that links note-on/note-off pairs and groups simultaneous notes into chords. Capture is always on; the logical and raw-forward faces are opt-in. Header-only template (N = capture ring size), available where the toolchain ships <functional> (not AVR).

GingoFlow<256> flow;            // N = capture ring size

// Logical face: structured events with note pairing, chord grouping, duration.
flow.onEvent([](const GingoFlowEvent& ev) {
    ev.kind;        // NOTE_ON / NOTE_OFF / CC / ...
    ev.note;        // note number
    ev.velocity;    // full 16-bit MIDI 2.0 resolution
    ev.idx.event;   // global order
    ev.idx.note;    // pairs on/off; the off carries ev.durationMs
    ev.idx.chord;   // simultaneous-note group
});

// Optional raw-UMP forward face (bridge or chain):
flow.onForward([](const uint32_t* words, uint8_t n)        { /* re-emit */ });

// Feed raw UMP from any transport (USB MIDI 2.0, MIDI 1.0 upconverted, bridge):
uint32_t words[2] = { 0x40903C00u, 0x80000000u };  // MIDI 2.0 note-on, C4
flow.ingest(words, 2, millis());                    // non-blocking (goes to the spine)

// Drain on your loop; capture never blocks, interpretation is opt-in:
while (flow.process()) { /* events fire via onEvent */ }

flow.activeNoteCount();   // held notes right now
flow.droppedTotal();      // packets the capture ring had to drop (backpressure)

// Calibratable chord-grouping rule (onset-cluster by default):
flow.config().chordRule     = CHORD_ONSET_CLUSTER;  // or CHORD_SIMULTANEITY
flow.config().onsetWindowMs = 50;

Raw UMP comes from any transport: midi2cpp (USB MIDI 2.0), an ESP32_Host_MIDI connection, or MIDI 1.0 bytes upconverted to UMP. See the Flow and T-Display-S3-Piano-Flow examples.

GingoMIDI1, output adapters

// Single event -> MIDI 1.0 bytes (NoteOn + NoteOff, 6 bytes for note events).
uint8_t buf[6];
uint8_t n = GingoMIDI1::fromEvent(noteEvent, buf, sizeof(buf));

// Sequence -> MIDI 1.0 byte stream. Default keeps each event's own
// channel; pass an explicit 0-15 to override every event.
uint8_t out[256];
uint16_t total = GingoMIDI1::fromSequence(seq, out, sizeof(out));
// Or with explicit override:
uint16_t total2 = GingoMIDI1::fromSequence(seq, out, sizeof(out), 5);

Input from MIDI 1.0 byte streams is intentionally not in scope. Use any external parser (Arduino MIDI Library, your own, etc.) and call GingoMonitor directly.

GingoMIDI2, UMP Flex Data adapters

auto chordUMP  = GingoMIDI2::chordName(GingoChord("CM"));
auto keySigUMP = GingoMIDI2::keySignature(scale);                   // group=0, channel=0 default
auto keySigCh5 = GingoMIDI2::keySignature(scale, /*group=*/0, /*channel=*/5);

GingoNoteContext ctx = field.noteContext(GingoNote("E"));
auto rccUMP = GingoMIDI2::perNoteController(ctx, /*midiNote=*/64);

chordUMP.wordCount;    // 4 (128-bit Flex Data)
rccUMP.wordCount;      // 2 (64-bit per-note CC)
chordUMP.byteCount();  // 16
uint8_t bytes[16];
chordUMP.toBytesBE(bytes, sizeof(bytes));   // big-endian wire serialization

UMP receive dispatch and MIDI-CI are out of scope. Use midi2cpp (or any UMP library) for receive callbacks, and the midi2 C99 library for MIDI-CI responder/initiator flows.

GingoChordComparison

GingoChordComparison cmp(GingoChord("CM"), GingoChord("Am"));
cmp.common_count;          // 2 (C and E shared)
cmp.root_distance;         // 3 semitones
cmp.same_quality;          // false
cmp.voice_leading;         // min semitone movement
cmp.transformation;        // NEO_R (Relative)
cmp.interval_vector_a[6];  // Forte interval vector

MIDI integration

The Monitor is the single entry point for musical events. Glue between an external transport and the Monitor takes a few lines and lives in your sketch.

MIDI 2.0 UMP via midi2cpp

Receive UMP through midi2cpp and forward each event to the Monitor (noteOn, noteOff, sustainOn / sustainOff). On onChordDetected, render the chord with GingoMIDI2::chordName(...) and send the resulting UMP back through midi2cpp. See the midi2cpp examples for its receive and send API.

MIDI 1.0 via Arduino MIDI Library

MIDI.setHandleNoteOn ([](byte ch, byte note, byte vel) {
    monitor.noteOn(ch - 1, note, vel);   // 1-16 -> 0-15 (UMP convention)
});
MIDI.setHandleNoteOff([](byte ch, byte note, byte vel) {
    monitor.noteOff(ch - 1, note);
});
MIDI.setHandleControlChange([](byte ch, byte cc, byte val) {
    if (cc == 64)  { (val >= 64) ? monitor.sustainOn() : monitor.sustainOff(); }
    else if (cc == 123) { monitor.reset(); }
});

Raw DIN MIDI on UART

Parse the UART byte stream inline (running status, SysEx absorption, real-time bytes) and forward the musical events to the Monitor with noteOn / noteOff / sustainOn / sustainOff. A ~30-line parser is enough.

Examples

ExampleDescriptionTier
BasicNoteNote creation, transposition, MIDI, frequency1
ChordNotesChord notes, intervals, identify1
ScaleExplorerScales, modes, pentatonic2
HarmonicFieldTriads, sevenths, harmonic functions2
Gingoduino_to_MIDIBuild a sequence and serialize via GingoMIDI1::fromSequence3
FlowRP2040 flow harness: a device emits a paced UMP gabarito, a host interprets it live with GingoFlow (capture + interpretation)3
T-Display-S3-Piano-FlowLive flow piano on the T-Display S3: chord with inversion, held notes, duration, and a scrolling processed-event log, source-agnostic over a USB MIDI 2.0 host (GingoFlow)3
V06_SelfTestOn-device acceptance suite covering the public surface through v0.6.0, including GingoFlow3
FoundationExhaustive on-device foundation proof: 398 musical claims (every INTERVAL_TABLE entry, every CHORD_FORMULAS entry, every SCALE_MASKS scale, full field analysis pipeline)3
Foundation_AVRReduced subset that fits 8-bit AVR: Note + Interval + Chord + Scale + MIDI primitives, 93 claims1
Foundation_DaisylibDaisy native port of the foundation proof for Daisy Seed (STM32H750, Cortex-M7). Builds with make, flashes via DFU3

Looking for USB/BLE MIDI input examples? They live in the transport library, not here. See:

This split is deliberate: gingoduino is a music theory engine and stays out of the wire; the transport library owns the byte-level integration examples.

Native testing

g++ -std=c++11 -I. -Wall -Wextra -Werror \
    -o extras/tests/test_native extras/tests/test_native.cpp \
    && ./extras/tests/test_native

585 tests pass under -Wall -Wextra -Werror, plus 398 additional musical correctness claims in extras/tests/foundation.cpp that exhaustively cover the INTERVAL_TABLE, every CHORD_FORMULAS entry, every CHORD_TYPE_MAP alias and every SCALE_MASKS scale. Both suites pass under gcc 13, clang LLVM 22 and gcc + AddressSanitizer + UndefinedBehaviorSanitizer.

Hardware validation

The library is verified on the host (PC native gcc/clang + sanitizers) and on real boards across the MCU families it claims to support. Same Foundation sketch, only the FQBN changes; bytes-identical results across compilers and architectures. On 8-bit AVR the full Foundation does not fit, so a reduced Foundation_AVR sketch exercises the Note + Interval + Chord + Scale + MIDI primitives (Field, Monitor, Progression and Compare are out of scope on AVR).

BoardMCUFoundation resultFlash usedToolchain
Arduino LeonardoATmega32U4, 8-bit AVR @16 MHzAVR subset 93/93 PASS51% (14.7 KB / 28.6 KB), SRAM 14%avr-gcc
Seeed XIAO SAMD21ARM Cortex-M0+ @48 MHzFull 398/398 PASS25% (67 KB / 256 KB)arm-none-eabi (Seeed SAMD core)
Adafruit Feather RP2040 USB HostRP2040, ARM Cortex-M0+ dual @133 MHzFull 398/398 PASS1% (94 KB / 8 MB)arm-none-eabi (arduino-pico)
Nice!Nano nRF52840 (Pro Micro class)nRF52840, ARM Cortex-M4F @64 MHz + SoftDevice S140Full 398/398 PASS15% (125 KB / 815 KB)arm-none-eabi 9.2 (adafruit-nrf52)
Daisy SeedSTM32H750, ARM Cortex-M7 @480 MHzFull 398/398 PASS (libDaisy, Foundation_Daisy)91% (119 KB / 128 KB)arm-none-eabi 13.2 + libDaisy
Teensy 4.1NXP i.MX RT1062, ARM Cortex-M7 @600 MHzFull 398/398 PASS<1% (82 KB / 8 MB)arm-none-eabi (Teensyduino)
ESP32-S3 DevKitCEspressif Xtensa LX7 dual @240 MHzFull 398/398 PASS24% (319 KB / 1.25 MB)xtensa-esp32-elf-g++ (arduino-esp32)

Total: 980 PC-native asserts + 5ร—398 + 93 = 3 063 musical claims exercised on silicon across six MCU families (AVR 8-bit, Cortex-M0+, Cortex-M4F, Cortex-M7, Xtensa LX7) and five toolchains, validated on 2026-06-02.

What this library is not

The boundary is drawn so the engine stays focused. A few things deliberately do not belong here.

  • Not a UMP parser. UMP receive dispatch belongs to a transport library. Use midi2cpp for USB MIDI 2.0 or the midi2 C99 core directly, and forward decoded callbacks into GingoMonitor.
  • Not a USB stack. Gingoduino does not own descriptors, endpoints, or alt settings. Pair it with midi2cpp (USB MIDI 2.0), Arduino MIDI Library (MIDI 1.0 DIN/Serial), or your own transport.
  • Not a MIDI-CI responder. Capability Inquiry, Profile negotiation, and Property Exchange live in midi2. Gingoduino exposes no MIDI-CI surface.
  • Not a synthesizer. Musical events are detected and analyzed; sound generation is application territory.
  • Not a MIDI File reader or writer. Standard MIDI File (SMF) I/O is out of scope. GingoSequence is a runtime structure, not a file format.
  • Not a desktop library. It targets MCU boards. Tests run on desktop with g++, but the API and memory model assume embedded constraints.

License

MIT License. See LICENSE.

Author

Saulo Verissimo