Pular para conteúdo

🎼 Detecção de Acordes

O MIDIHandler agrupa automaticamente notas simultâneas usando o campo chordIndex. Notas com o mesmo chordIndex foram pressionadas "ao mesmo tempo" (dentro da janela configurada).


Como Funciona

Cada evento MIDI recebe um chordIndex. Notas que chegam próximas no tempo compartilham o mesmo índice:

Notas pressionadas:   C4  E4  G4  (acorde Dó maior)
chordIndex:            1   1   1   ← mesmo índice
                                   ← B4 pressionado depois
B4 pressionado:       B4
chordIndex:            2           ← novo índice

Configuração

chordTimeWindow

Controla a janela de tempo (ms) para agrupar notas:

MIDIHandlerConfig cfg;
cfg.chordTimeWindow = 0;   // 0 ms (padrão): novo acorde só quando TODAS as notas são soltas
cfg.chordTimeWindow = 50;  // 50 ms: janela de tempo (ideal para teclados físicos)
midiHandler.begin(cfg);
sequenceDiagram
    participant KEY as 🎹 Teclado
    participant HANDLER as MIDIHandler

    Note over KEY,HANDLER: chordTimeWindow = 50ms

    KEY->>HANDLER: NoteOn C4  (t = 0ms)
    Note right of HANDLER: chordIndex = 1
    KEY->>HANDLER: NoteOn E4  (t = 20ms)
    Note right of HANDLER: chordIndex = 1 ✅ dentro da janela
    KEY->>HANDLER: NoteOn G4  (t = 45ms)
    Note right of HANDLER: chordIndex = 1 ✅ dentro da janela
    KEY->>HANDLER: NoteOn B4  (t = 120ms)
    Note right of HANDLER: chordIndex = 2 ❌ nova janela (>50ms)

API de Acordes

lastChord() — Índice do Último Acorde

Retorna o chordIndex mais recente na fila:

const auto& queue = midiHandler.getQueue();
int idx = midiHandler.lastChord(queue);  // -1 se a fila estiver vazia

getChord() — Notas do Acorde

Retorna os valores de um campo específico para todas as notas de um acorde:

const auto& queue = midiHandler.getQueue();
int idx = midiHandler.lastChord(queue);

// Lista de nomes de notas com oitava
std::vector<std::string> notas = midiHandler.getChord(idx, queue, {"noteOctave"});
// Exemplo: ["C4", "E4", "G4"]

// Lista de velocidades
std::vector<std::string> vels = midiHandler.getChord(idx, queue, {"velocity"});
// Exemplo: ["100", "95", "110"]

// Múltiplos campos com labels
std::vector<std::string> info = midiHandler.getChord(
    idx, queue, {"noteOctave", "velocity"}, /*includeLabels=*/true);
// Exemplo: ["noteOctave:C4", "noteOctave:E4", "velocity:100", "velocity:95"]

getAnswer() — Resposta Rápida do Último Acorde

Atalho para o acorde mais recente, sem precisar chamar lastChord():

// Nome das notas do último acorde
std::vector<std::string> resp = midiHandler.getAnswer("noteName");
// Exemplo: ["C", "E", "G"]

// Múltiplos campos
std::vector<std::string> multi = midiHandler.getAnswer({"noteName", "velocity"});

Exemplo Completo

#include <ESP32_Host_MIDI.h>
// Tools > USB Mode → "USB Host"

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

    MIDIHandlerConfig cfg;
    cfg.chordTimeWindow = 50;  // agrupa notas dentro de 50ms
    midiHandler.begin(cfg);
}

void loop() {
    midiHandler.task();

    const auto& queue = midiHandler.getQueue();
    if (queue.empty()) return;

    // Verificar se há novas notas
    int lastIdx = midiHandler.lastChord(queue);
    if (lastIdx < 0) return;

    // Pegar notas do acorde mais recente
    auto notas = midiHandler.getChord(lastIdx, queue, {"noteOctave"});

    if (!notas.empty()) {
        Serial.print("Acorde [" + String(lastIdx) + "]: ");
        for (const auto& n : notas) {
            Serial.print(n.c_str());
            Serial.print(" ");
        }
        Serial.println();
    }
}

Saída típica ao tocar Dó maior (C-E-G):

Acorde [1]: C4 E4 G4
Acorde [2]: C4 F4 A4    ← Fá maior
Acorde [3]: G3 B3 D4    ← Sol maior

Detectar Mudança de Acorde

void loop() {
    midiHandler.task();

    const auto& queue = midiHandler.getQueue();
    static int ultimoAcorde = -1;

    int idx = midiHandler.lastChord(queue);
    if (idx != ultimoAcorde && idx >= 0) {
        ultimoAcorde = idx;

        auto notas = midiHandler.getChord(idx, queue, {"noteOctave"});
        if (!notas.empty()) {
            Serial.print("Novo acorde: ");
            for (const auto& n : notas) Serial.print(String(n.c_str()) + " ");
            Serial.println();
        }
    }
}

Integração com Gingoduino

Para identificar o nome do acorde ("Cmaj7", "Dm7♭5"), use o GingoAdapter:

#include "src/GingoAdapter.h"  // requer Gingoduino ≥ v0.2.2

char chordName[16];
if (GingoAdapter::identifyLastChord(midiHandler, chordName, sizeof(chordName))) {
    Serial.printf("Acorde: %s\n", chordName);
    // Exemplo: "Cmaj7", "Dm", "G7sus4"
}

chordIndex no Loop de Eventos

O chordIndex é parte de cada MIDIEventData. Você pode usar diretamente na iteração:

for (const auto& ev : midiHandler.getQueue()) {
    if (ev.status == "NoteOn") {
        Serial.printf("Nota %s  acorde #%d  vel=%d\n",
            ev.noteOctave.c_str(),
            ev.chordIndex,
            ev.velocity);
    }
}

Bug Clássico: Analisar Apenas uma Nota

Atenção ao analisar acordes em tempo real

Analise o acorde sempre que o noteCount mudar, não apenas na chegada de um evento novo. Se você verificar apenas em chordIndex != lastIdx, a análise acontece na primeira nota e as seguintes são ignoradas.

Correto:

size_t count = midiHandler.getActiveNotesCount();
if (count != lastCount) {
    lastCount = count;
    // re-analisar acorde
}


Próximos Passos