![Gingoduino](https://raw.githubusercontent.com/sauloverissimo/gingoduino/main/extras/gingoduino.png) # ๐Ÿช‡ 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](README_ptbr.md) --- ## Contents - [๐Ÿช‡ Gingo\[duino\]](#-gingoduino) - [Contents](#contents) - [Overview](#overview) - [Roles in the ecosystem](#roles-in-the-ecosystem) - [Architecture](#architecture) - [Modules](#modules) - [Features](#features) - [Spec coverage](#spec-coverage) - [Installation](#installation) - [Quick start](#quick-start) - [API reference](#api-reference) - [GingoNote](#gingonote) - [GingoInterval](#gingointerval) - [GingoChord](#gingochord) - [GingoScale](#gingoscale) - [GingoField](#gingofield) - [GingoFretboard](#gingofretboard) - [GingoEvent and GingoSequence](#gingoevent-and-gingosequence) - [GingoMonitor](#gingomonitor) - [GingoFlow](#gingoflow) - [GingoMIDI1, output adapters](#gingomidi1-output-adapters) - [GingoMIDI2, UMP Flex Data adapters](#gingomidi2-ump-flex-data-adapters) - [GingoChordComparison](#gingochordcomparison) - [MIDI integration](#midi-integration) - [MIDI 2.0 UMP via midi2cpp](#midi-20-ump-via-midi2cpp) - [MIDI 1.0 via Arduino MIDI Library](#midi-10-via-arduino-midi-library) - [Raw DIN MIDI on UART](#raw-din-midi-on-uart) - [Examples](#examples) - [Native testing](#native-testing) - [Hardware validation](#hardware-validation) - [What this library is not](#what-this-library-is-not) - [License](#license) - [Author](#author) ## 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`](https://github.com/sauloverissimo/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](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. | Library | Role | Language | Speaks | |---------|------|----------|--------| | [`midi2`](https://github.com/sauloverissimo/midi2) | Infrastructure: UMP wire, parsing, dispatch, MIDI-CI | C99 | MIDI 2.0 | | [`midi2cpp`](https://github.com/sauloverissimo/midi2cpp) | Platform: USB MIDI 2.0 device, host, bridge | C++17 | MIDI 2.0 | | **gingoduino** | **Music theory engine.** Notes, intervals, chords, scales, fields, harmonic monitor, output adapters | C++11 | **MIDI 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](#hardware-validation) for the boards we ran the on-device proof on. A couple of practical notes: - `` 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 | Spec | Document | Surface in gingoduino | |------|----------|------------------------| | **MIDI 1.0** | [MIDI 1.0 Detailed Specification](https://midi.org/specifications/midi-1-0-specifications) | Output: NoteOn/NoteOff bytes from `GingoEvent` and `GingoSequence` via `GingoMIDI1::fromEvent`, `GingoMIDI1::fromSequence` | | **MIDI 2.0 UMP** | [M2-104-UM v1.1.2](https://midi.org/specifications/midi-2-0-specifications) | Output: Flex Data (Set Chord Name, Set Key Signature, Set Per-Note Controller) via `GingoMIDI2` | | **MIDI 2.0 Bit Scaling** | [M2-115-U v1.0.2](https://midi.org/specifications/midi-2-0-specifications) | Internal: 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:** ```ini ; platformio.ini lib_deps = sauloverissimo/Gingoduino ``` **ESP-IDF Component:** ```bash idf.py add-dependency "sauloverissimo/gingoduino" ``` **CMake / native:** ```cmake 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 ```cpp #include 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 ```cpp 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 ```cpp 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 ```cpp 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 ```cpp 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 ```cpp 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 ```cpp 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 ```cpp 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. ```cpp 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 `` (not AVR). ```cpp 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 ```cpp // 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 ```cpp 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`](https://github.com/sauloverissimo/midi2) C99 library for MIDI-CI responder/initiator flows. ### GingoChordComparison ```cpp 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 ```cpp 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 | Example | Description | Tier | |---------|-------------|------| | BasicNote | Note creation, transposition, MIDI, frequency | 1 | | ChordNotes | Chord notes, intervals, identify | 1 | | ScaleExplorer | Scales, modes, pentatonic | 2 | | HarmonicField | Triads, sevenths, harmonic functions | 2 | | Gingoduino_to_MIDI | Build a sequence and serialize via `GingoMIDI1::fromSequence` | 3 | | Flow | RP2040 flow harness: a device emits a paced UMP gabarito, a host interprets it live with `GingoFlow` (capture + interpretation) | 3 | | T-Display-S3-Piano-Flow | Live 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_SelfTest | On-device acceptance suite covering the public surface through v0.6.0, including `GingoFlow` | 3 | | Foundation | Exhaustive 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_AVR | Reduced subset that fits 8-bit AVR: Note + Interval + Chord + Scale + MIDI primitives, 93 claims | 1 | | Foundation_Daisy | libDaisy native port of the foundation proof for Daisy Seed (STM32H750, Cortex-M7). Builds with `make`, flashes via DFU | 3 | **Looking for USB/BLE MIDI input examples?** They live in the transport library, not here. See: - [`ESP32_Host_MIDI/examples/USB-Host-MIDI2/`](https://github.com/sauloverissimo/ESP32_Host_MIDI/tree/main/examples/USB-Host-MIDI2) - USB host that receives raw UMP from a MIDI 2.0 device and decodes Channel Voice - [`ESP32_Host_MIDI/examples/T-Display-S3-BLE-Receiver/`](https://github.com/sauloverissimo/ESP32_Host_MIDI/tree/main/examples/T-Display-S3-BLE-Receiver) - BLE MIDI input with a real-time piano visualizer 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 ```bash 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). | Board | MCU | Foundation result | Flash used | Toolchain | |-------|-----|------------------|------------|-----------| | Arduino Leonardo | ATmega32U4, 8-bit AVR @16 MHz | AVR subset 93/93 PASS | 51% (14.7 KB / 28.6 KB), SRAM 14% | avr-gcc | | Seeed XIAO SAMD21 | ARM Cortex-M0+ @48 MHz | Full 398/398 PASS | 25% (67 KB / 256 KB) | arm-none-eabi (Seeed SAMD core) | | Adafruit Feather RP2040 USB Host | RP2040, ARM Cortex-M0+ dual @133 MHz | Full 398/398 PASS | 1% (94 KB / 8 MB) | arm-none-eabi (arduino-pico) | | Nice!Nano nRF52840 (Pro Micro class) | nRF52840, ARM Cortex-M4F @64 MHz + SoftDevice S140 | Full 398/398 PASS | 15% (125 KB / 815 KB) | arm-none-eabi 9.2 (adafruit-nrf52) | | Daisy Seed | STM32H750, ARM Cortex-M7 @480 MHz | Full 398/398 PASS (libDaisy, `Foundation_Daisy`) | 91% (119 KB / 128 KB) | arm-none-eabi 13.2 + libDaisy | | Teensy 4.1 | NXP i.MX RT1062, ARM Cortex-M7 @600 MHz | Full 398/398 PASS | <1% (82 KB / 8 MB) | arm-none-eabi (Teensyduino) | | ESP32-S3 DevKitC | Espressif Xtensa LX7 dual @240 MHz | Full 398/398 PASS | 24% (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`](https://github.com/sauloverissimo/midi2cpp) for USB MIDI 2.0 or the [`midi2`](https://github.com/sauloverissimo/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`](https://github.com/sauloverissimo/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](LICENSE). ## Author **Saulo Verissimo** - https://github.com/sauloverissimo - sauloverissimo@gmail.com