🎼 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):
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:
Próximos Passos¶
- Notas Ativas → — quais notas estão pressionadas agora
- GingoAdapter → — identificar nome do acorde
- Configuração → — ajustar
chordTimeWindow