BandMate lets you build your own band and jump right in — no experience needed, just play.
BandMate lets you build a backing band from scratch. Choose a drum groove, time signature, instrument, key, and scale — then layer in chord progressions by playing live, sequencing, or loop recording. Your performance is automatically quantized, keeping everything tight while you focus on playing.
Overall Scale and Proportion: measures approximately 6.5 × 5.0 × 2inch inches — a compact footprint designed to fit naturally into any playing setup.
Tight tolerancing and CAD precision make for clean, seamless component mounting throughout.
Heat set inserts and precision cutouts on the underside of the top panel keep assembly clean and every component perfectly positioned.
Dedicated heat set insert mounting positions throughout the enclosure keep components secure, vibration-free, and neatly organized — simplifying both assembly and debugging.
This demo walks through all three of device's chord modes using just a single drum groove, and shows how tempo and volume can be adjusted on the fly during playback. Note: the OLED display may appear to flicker in the video — this is a camera frame rate artifact and not representative of the actual device.
Early ideation sketches covering interface organization, form exploration, code architecture, and system flow. Rough block diagrams helped map out how everything would connect before any design decisions were locked in — with hardware constraints and intuitiveness guiding the direction throughout.
A works-like prototype built across two connected breadboards. This prototype was the backbone of the entire project — worked through component by component from the start, it allowed the code to be developed and refined against a fully functioning system. By the time the prototype reached its near-final form, every component and every line of code had been validated, meaning the transition to the final housing was done with complete confidence in the electronics and software beneath it.
With components and wiring fully validated, CAD development could be approached with confidence. Component layout was informed directly by the prototype, and sourcing accurate CAD files from GrabCAD meant most parts were modeled at true dimensions. Three components — the speaker, MX-style breakout boards, and protoboard — were modeled from scratch. Having every part accurately represented in the assembly allowed for a precise and deliberate mounting strategy, using M2 and M3 heat set inserts throughout for strong, vibration-free connections. This was especially important given the speaker's output, where reducing extraneous noise and rattle was a key concern. Note: keycaps and knobs are not yet modeled but will be added to the assembly in the near future.
With the housing printed and heat set inserts installed, the next step was translating the breadboard prototype into a final soldered circuit — no small task. The breadboard proved invaluable here, serving as a clear and direct reference throughout the soldering process. Working component by component, the entire circuit was transferred cleanly enough that the device powered up and worked on the very first attempt.
Response to comments
I want to first responed to some suggested improvements. One thing that came up 3 times actually in the feeback crit was "I think maybe having abigger display would make it seem more interactive but maybe that goes against the point", "I think the display things can be bigger and more clear", and "I think a larger screen could help the user interact with it better." I somewhat agree wiht this feeback and had similar concerns myself but I do think that keeping a very simple small form factor was more advantagous in this project than a larger screen. Moreover, this is always something that I could upgrade by just slightly changin the models dimensions and looking into some larger oled displays. Another comment that I got a couple times was "how to get a novice to learn how to use it." I think this idea is super important and was originally part of my design intent. But with this being a project for me I ended up just kind of tunneling in on ideas I personally wanted to explore. Most of these things though could be solved. One proposed solution I have was rather that using musically harmony as descriptors I could use moods or a more semantic idea that allows for intuitive interaction from a musical novice.
Self critique pertaining to the project.
I am very happy with how this project turned out for a few reasons and there are also some places where I fell short of my goals. This project was like jumping into a pool and not knowing how to swim but somehow enjoying the process of a fighting to stay above the water. Haha! From a hardware standpoint things were not to difficult but I severely underesitmated the amount of coding work I would have to do. So with that being said I think Im really proud of my ability to think about and create a system in the way I wanted to but I feel like becuase I spent so much type in the terminal coding some form concerns and things like the keycaps and the knobs were neglected.
What you learned about your own abilities and/or limitations
I think I learned that I am capable of a lot more than I expoected in this sort of work. I think that If you use your resources wisely a lot of these things can be figured out and it is actually a really rewarding process. I also think that this project opened my eyes to what actually goes into an electronic device of anysort. Understanding that the physical componenets are in most cases ververy surfacelevel as compared to the software that helps those compenetes interact in a complete system.
Next steps.
In terms of next steps there is a few things that I plan on doing. First I would like to refine the code and UI to sort of reduce some cluter and make it even more intuitive to use. I would also like to add more chords and more harmonic options. This would allow rather than just different chord voicings but actual different chord tonalities based upon the genre selected. Also on the form side of things after printing this and seeing the results I would like to make a redesign of the housing to create a bit of a more proportionally refined form. Further more painting sanding and finishing the 3d printed pieces and adding finsihing details such as rubber feat and nice knobs and nicely modeled keycaps. During this project I was constantly inspired by all sorts of music technology side quests, and I think that this project and this course has inspired me to continue to create music technology devices. This summer I would like to create a sort of analog synth setup basically making the sort of thing I made here but actually creating anylog circuits for the sounds.
Overall System Block Diagram. Note: The switch block represents 7 independent discrete inputs, consolidated into a single block for visual clarity.
Full schematic of the circuit used in the device.
// TEENSY CHORD SEQUENCER v13
Chord Sequencer v13
Author: Jack Estabrook
Description:
A 7-button chord sequencer built on Teensy 4.1 with Audio Shield Rev D.
Triggers stereo WAV chord samples from SD card across three playback
modes: live (immediate), sequence (step record), and loop. Includes
drum patterns in 4/4, 3/4, and 5/4 time signatures with kick, snare,
hi-hat, and percussion layers. Tempo and volume are controlled via
potentiometers. A rotary encoder navigates menus on an SSD1306 OLED
display. All audio routing uses the Teensy Audio Library's object-based
patch system with stereo mixers.
Pin Mapping:
- Pin 0: Chord IV button
- Pin 1: Chord V button
- Pin 2: Rotary encoder CLK
- Pin 3: Rotary encoder DT
- Pin 4: Rotary encoder button
- Pin 5: Chord I button
- Pin 6: Chord II button
- Pin 9: Chord III button
- Pin 14 / A0: Tempo potentiometer (wiper)
- Pin 15 / A1: Volume potentiometer (wiper)
- Pin 16: Chord VI button
- Pin 17: Chord VII button
- Pin 18 (SDA): OLED display I2C data
- Pin 19 (SCL): OLED display I2C clock
- Pins 7,8,20,21,23: I2S audio — reserved for Audio Shield
- Pins 10,11,12,13: SPI/SD — reserved for Audio Shield
Libraries:
- Teensy Audio Library — https://www.pjrc.com/teensy/td_libs_Audio.html
- Adafruit SSD1306 — https://github.com/adafruit/Adafruit_SSD1306
- Adafruit GFX — https://github.com/adafruit/Adafruit-GFX-Library
Credits:
- Audio system design assisted by PJRC Audio System Design Tool
- Claude AI
============================================================
// Reserved pins: I2S (audio), SPI/SD, I2C (display). Do not assign user pins to these.
#define I2S_RX 7
#define I2S_TX 8
#define I2S_LRCLK 20
#define I2S_BCLK 21
#define I2S_MCLK 23
#define SD_CS 10
#define SD_MOSI 11
#define SD_MISO 12
#define SD_SCK 13
#define I2C_SDA 18
#define I2C_SCL 19
#define PIN_CONFLICTS_RESERVED(p) ((p)==I2S_RX||(p)==I2S_TX||(p)==I2S_LRCLK||(p)==I2S_BCLK||(p)==I2S_MCLK||(p)==SD_CS||(p)==SD_MOSI||(p)==SD_MISO||(p)==SD_SCK||(p)==I2C_SDA||(p)==I2C_SCL)
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Audio.h>
#include <SD.h>
#include <SPI.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
AudioPlayMemory kickMemory;
AudioPlayMemory snareMemory;
AudioPlayMemory hihatMemory;
AudioPlayMemory perc1Memory;
AudioPlayMemory perc2Memory;
AudioPlayMemory perc3Memory;
AudioSynthWaveformSine metronomeSine;
AudioPlaySdWav chordPlayer1;
AudioPlaySdWav chordPlayer2;
AudioPlaySdWav livePlayer;
AudioMixer4 drumMixerL;
AudioMixer4 drumMixerR;
AudioMixer4 percMixerL;
AudioMixer4 percMixerR;
AudioMixer4 chordMixerL;
AudioMixer4 chordMixerR;
AudioMixer4 masterMixerL;
AudioMixer4 masterMixerR;
AudioOutputI2S audioOutput;
AudioControlSGTL5000 audioShield;
AudioConnection patchDL0(kickMemory, 0, drumMixerL, 0);
AudioConnection patchDL1(snareMemory, 0, drumMixerL, 1);
AudioConnection patchDL2(hihatMemory, 0, drumMixerL, 2);
AudioConnection patchDL3(metronomeSine, 0, drumMixerL, 3);
AudioConnection patchDR0(kickMemory, 0, drumMixerR, 0);
AudioConnection patchDR1(snareMemory, 0, drumMixerR, 1);
AudioConnection patchDR2(hihatMemory, 0, drumMixerR, 2);
AudioConnection patchDR3(metronomeSine, 0, drumMixerR, 3);
AudioConnection patchPL0(perc1Memory, 0, percMixerL, 0);
AudioConnection patchPL1(perc2Memory, 0, percMixerL, 1);
AudioConnection patchPL2(perc3Memory, 0, percMixerL, 2);
AudioConnection patchPR0(perc1Memory, 0, percMixerR, 0);
AudioConnection patchPR1(perc2Memory, 0, percMixerR, 1);
AudioConnection patchPR2(perc3Memory, 0, percMixerR, 2);
AudioConnection patchCL0(chordPlayer1, 0, chordMixerL, 0);
AudioConnection patchCL1(chordPlayer2, 0, chordMixerL, 1);
AudioConnection patchCL2(livePlayer, 0, chordMixerL, 2);
AudioConnection patchCR0(chordPlayer1, 1, chordMixerR, 0);
AudioConnection patchCR1(chordPlayer2, 1, chordMixerR, 1);
AudioConnection patchCR2(livePlayer, 1, chordMixerR, 2);
AudioConnection patchML0(drumMixerL, 0, masterMixerL, 0);
AudioConnection patchML1(chordMixerL, 0, masterMixerL, 1);
AudioConnection patchML2(percMixerL, 0, masterMixerL, 2);
AudioConnection patchMR0(drumMixerR, 0, masterMixerR, 0);
AudioConnection patchMR1(chordMixerR, 0, masterMixerR, 1);
AudioConnection patchMR2(percMixerR, 0, masterMixerR, 2);
AudioConnection patchOut0(masterMixerL, 0, audioOutput, 0);
AudioConnection patchOut1(masterMixerR, 0, audioOutput, 1);
// ============================================================
// PINS — user assignments (compile-time checked against reserved I2S/SPI/I2C pins above)
// ============================================================
const int TEMPO_PIN = A0;
const int VOLUME_PIN = A1;
const int ENC_CLK = 2;
const int ENC_DT = 3;
const int ENC_BTN = 4;
const int CHORD_PINS[7] = {5, 6, 9, 0, 1, 16, 17};
// Switch 1=pin5 2=pin6 3=pin9 4=pin0 5=pin1 6=pin16(A2) 7=pin17(A3)
// Compile-time pin conflict check (use literals so condition is constant on all toolchains)
static_assert(!PIN_CONFLICTS_RESERVED(TEMPO_PIN), "TEMPO_PIN must not be a reserved I2S/SPI/I2C pin");
static_assert(!PIN_CONFLICTS_RESERVED(VOLUME_PIN), "VOLUME_PIN must not be a reserved I2S/SPI/I2C pin");
static_assert(!PIN_CONFLICTS_RESERVED(2), "ENC_CLK must not be a reserved I2S/SPI/I2C pin");
static_assert(!PIN_CONFLICTS_RESERVED(3), "ENC_DT must not be a reserved I2S/SPI/I2C pin");
static_assert(!PIN_CONFLICTS_RESERVED(4), "ENC_BTN must not be a reserved I2S/SPI/I2C pin");
static_assert(!PIN_CONFLICTS_RESERVED(5) && !PIN_CONFLICTS_RESERVED(6) && !PIN_CONFLICTS_RESERVED(9) && !PIN_CONFLICTS_RESERVED(0) && !PIN_CONFLICTS_RESERVED(1) && !PIN_CONFLICTS_RESERVED(16) && !PIN_CONFLICTS_RESERVED(17), "CHORD_PINS must not use reserved I2S/SPI/I2C pins");
enum State {
STATE_STARTUP,
STATE_INSTRUMENT,
STATE_MAIN_MENU,
STATE_TIME_SIG,
STATE_GENRE,
STATE_KEY,
STATE_SCALE_MODE,
STATE_EXTENSION,
STATE_MODE_SELECT,
STATE_CHORD_DURATION,
STATE_SEQUENCE_EDIT,
STATE_PLAYING,
STATE_PAUSED,
STATE_LOOP_LENGTH,
STATE_LOOP_READY,
STATE_LOOP_COUNTIN,
STATE_LOOP_RECORD
};
State currentState = STATE_STARTUP;
void tickDrums();
bool drumsOn = true;
int selectedTimeSig = 0;
int selectedGenre = 0;
int selectedKey = 0;
int selectedScaleMode= 0;
int selectedExt = 0;
int pendingExt = 0;
bool liveMode = true;
int chordDuration = 0; // 0=whole note, 1=half note, 2=quarter note
// Instrument = which chord folder on SD: /chords/EP/, /chords/Rhodes/, /chords/Pad/, /chords/Grand Piano/, /chords/Strings/
const char* INSTRUMENT_NAMES[5] = {"EP", "Rhodes", "Pad", "Grand Piano", "Strings"};
const char* INSTRUMENT_FOLDERS[5] = {"EP", "Rhodes", "Pad", "Grand Piano", "Strings"};
const char* INSTRUMENT_SHORT[5] = {"EP", "Rho", "Pad", "Pno", "Str"}; // short labels for play screen
int selectedInstrument = 0;
struct LoopEvent {
int degree;
unsigned long offset; // µs from loop start (raw timing)
int quantStep; // snapped position: bar * stepsPerBar + drumStep
};
#define MAX_LOOP_EVENTS 128
LoopEvent loopEvents[MAX_LOOP_EVENTS];
int loopEventCount = 0;
bool loopMode = false;
bool loopQuantized = true;
int loopBars = 8;
int loopBar = 0;
int recordBar = 0;
int countInBar = 0;
int lastLoopPlayStep = -1;
unsigned long loopStartTime = 0;
unsigned long metronomeOffTime = 0;
int lastMetronomeBeat = -1;
bool loopEventTriggered[MAX_LOOP_EVENTS];
bool liveOverride = false;
int liveOverrideDegree = 0;
bool chordPlayer1Active = true;
int pendingChordDegree = -1;
unsigned long chordMuteTime = 0;
int chordUnmuteChannel = -1;
char pendingChordPath[32] = ""; // single path for next chord (default or variant), set by caller before playSequenceChord
int pendingChordPlay = -1;
unsigned long pendingChordTime = 0;
const unsigned long CHORD_BUTTON_SETTLE_MS = 25; // delay so switch/SD noise settles before playing; try 20 if still clean
int scheduledChordDegree = -1; // for loop record: play chord at downbeat when user hit early
unsigned long scheduledChordTime = 0;
const char* TIME_SIG_NAMES[3] = {"4/4", "3/4", "5/4"};
const int NUM_MODES = 7;
const char* SCALE_MODE_NAMES[7] = {
"Ionian Maj",
"Dorian min b3 b7",
"Phrygian min b2b3b6b7",
"Lydian Maj #4",
"Mixolydian Maj b7",
"Aeolian min b3 b6 b7",
"Locrian dim b2b3b5b6b7"
};
const char* SCALE_MODE_SHORT[7] = {
"Ion", "Dor", "Phr", "Lyd", "Mix", "Aeo", "Loc"
};
const char* GENRES_4_4[11] = {
"Rock", "Jazz", "Hip Hop", "Trap", "Funk",
"Latin", "Reggae", "Bossa Nova", "Math Rock", "Midwest Emo",
"House"
};
const char* GENRES_3_4[4] = {"Jazz Waltz", "Rock Waltz", "Metal", "Math Rock"};
const char* GENRES_5_4[4] = {"Jazz", "Metal/Prog", "Afrobeat", "Math Rock"};
const char* GENRE_FOLDERS_4_4[11] = {
"rock", "jazz", "hiphop", "trap", "funk",
"latin", "reggae", "bossa", "mathrock", "midwestemo",
"house"
};
const char* GENRE_FOLDERS_3_4[4] = {"jazz", "rock", "metal", "mathrock"};
const char* GENRE_FOLDERS_5_4[4] = {"jazz", "metal", "afrobeat", "mathrock"};
const char* KEY_NAMES[12] = {
"C", "Db", "D", "Eb", "E", "F",
"Gb", "G", "Ab", "A", "Bb", "B"
};
const char* EXT_NAMES[3] = {"Triads", "7ths", "9ths"};
const char* MODE_NAMES[3] = {"Live", "Sequence", "Loop Record"};
const int GENRE_COUNT[3]= {11, 4, 4};
const int MODE_INTERVALS[7][7] = {
{0, 2, 4, 5, 7, 9, 11}, // Ionian
{0, 2, 3, 5, 7, 9, 10}, // Dorian
{0, 1, 3, 5, 7, 8, 10}, // Phrygian
{0, 2, 4, 6, 7, 9, 11}, // Lydian
{0, 2, 4, 5, 7, 9, 10}, // Mixolydian
{0, 2, 3, 5, 7, 8, 10}, // Aeolian
{0, 1, 3, 5, 6, 8, 10} // Locrian
};
// Quality indices: match CHORD_SUFFIX[quality][extension] and MODE_QUALITY[mode][degree]
#define QUALITY_MAJ 0
#define QUALITY_MIN 1
#define QUALITY_DIM 2
#define QUALITY_DOM 3
// Extension indices: 0=triads, 1=7ths, 2=9ths (selectedExt)
#define EXT_TRIAD 0
#define EXT_7TH 1
#define EXT_9TH 2
// 0=Maj, 1=min, 2=dim, 3=Dom (maj triad, dom7/9 extensions)
const int MODE_QUALITY[7][7] = {
{0, 1, 1, 0, 3, 1, 2}, // Ionian
{1, 1, 0, 3, 1, 2, 0}, // Dorian
{1, 0, 3, 1, 2, 0, 1}, // Phrygian
{0, 3, 1, 2, 0, 1, 1}, // Lydian
{3, 1, 2, 0, 1, 1, 0}, // Mixolydian
{1, 2, 0, 1, 1, 0, 3}, // Aeolian
{2, 0, 1, 1, 0, 3, 1} // Locrian
};
// CHORD_SUFFIX[quality][extension]: quality = QUALITY_MAJ/MIN/DIM/DOM, extension = EXT_TRIAD/7TH/9TH
const char* CHORD_SUFFIX[4][3] = {
{"maj", "maj7", "maj9"},
{"min", "min7", "min9"},
{"dim", "m7b5", "m7b5"},
{"maj", "7", "9"}
};
// 600 chords: default (0) + _low (1, avoid - very low), _high (2), _inv (3), _spread (4)
const char* VOICING_SUFFIX[5] = {"", "_low", "_high", "_inv", "_spread"};
int chordSequence[8] = {0, 0, 0, 0, 0, 0, 0, 0};
bool chordStepSet[8] = {false};
int assigningStep = 0;
int liveChordDegree = 0;
int lastSequenceChord = -1;
bool sdCardOK = false;
bool drumsRunning = false;
bool returnToMain = true;
bool fillActive = false;
bool lastBarWasFill = false;
int drumPatternVariant = 0; // 0, 1, or 2 — 1/3 probability each when picking groove
const int FILL_OVERRIDE_PERCENT = 25; // 25% chance per bar; never back-to-back (lastBarWasFill blocks next fill)
const float LIVE_CHORD_GAIN = 0.52f; // live chord (switches) slightly louder over sequence/loop; kept under chord bus headroom
// Per-button debounce state
const unsigned long DEBOUNCE_MS = 30;
bool chordDebounced[7] = {false};
bool chordLastRaw[7] = {false};
unsigned long chordDebounceTime[7] = {0};
int getInterval(int degree) {
return MODE_INTERVALS[selectedScaleMode][degree];
}
int getQuality(int degree) {
return MODE_QUALITY[selectedScaleMode][degree];
}
// Writes path into buf (no heap String). bufLen typically 32 to match cachedChordFile.
void buildChordFilename(int degree, char* buf, size_t bufLen) {
int root = (selectedKey + getInterval(degree)) % 12;
int qual = getQuality(degree);
snprintf(buf, bufLen, "/chords/%s/%s%s.wav",
INSTRUMENT_FOLDERS[selectedInstrument],
KEY_NAMES[root],
CHORD_SUFFIX[qual][selectedExt]);
}
// voicing: 0=default, 1=low(avoid), 2=high, 3=inv, 4=spread. Used for 600-chord variant substitution on playback.
void buildChordFilenameVariant(int degree, int voicing, char* buf, size_t bufLen) {
if (voicing <= 0) {
buildChordFilename(degree, buf, bufLen);
return;
}
int root = (selectedKey + getInterval(degree)) % 12;
int qual = getQuality(degree);
const char* vs = (voicing >= 1 && voicing <= 4) ? VOICING_SUFFIX[voicing] : "";
snprintf(buf, bufLen, "/chords/%s/%s%s%s.wav",
INSTRUMENT_FOLDERS[selectedInstrument],
KEY_NAMES[root],
CHORD_SUFFIX[qual][selectedExt],
vs);
}
String buildChordName(int degree) {
int root = (selectedKey + getInterval(degree)) % 12;
int qual = getQuality(degree);
return String(KEY_NAMES[root]) + String(CHORD_SUFFIX[qual][selectedExt]);
}
String buildDrumPath(const char* instrument) {
String path = "/drums/";
if (selectedTimeSig == 0) {
// House uses same WAV files as Bossa Nova (no separate /drums/house/ folder)
int folderIdx = (selectedGenre == 10) ? 7 : selectedGenre; // 10=House -> 7=Bossa
path += GENRE_FOLDERS_4_4[folderIdx];
} else if (selectedTimeSig == 1) {
path += GENRE_FOLDERS_3_4[selectedGenre];
} else {
path += GENRE_FOLDERS_5_4[selectedGenre];
}
path += "/";
path += instrument;
path += ".wav";
return path;
}
int encLastCLK = HIGH;
int menuIndex = 0;
unsigned long btnPressTime = 0;
bool btnHeld = false;
const int HOLD_TIME = 800;
int readEncoder() {
int clkState = digitalRead(ENC_CLK);
int result = 0;
if (clkState != encLastCLK && clkState == LOW) {
result = (digitalRead(ENC_DT) != clkState) ? 1 : -1;
}
encLastCLK = clkState;
return result;
}
bool readButtonClick() {
int btn = digitalRead(ENC_BTN);
if (btn == LOW && btnPressTime == 0) btnPressTime = millis();
if (btn == HIGH && btnPressTime > 0) {
unsigned long dur = millis() - btnPressTime;
btnPressTime = 0;
btnHeld = false;
if (dur < HOLD_TIME) return true;
}
return false;
}
bool readButtonHold() {
int btn = digitalRead(ENC_BTN);
if (btn == LOW && btnPressTime > 0) {
if (millis() - btnPressTime >= HOLD_TIME && !btnHeld) {
btnHeld = true;
return true;
}
}
if (btn == HIGH) btnHeld = false;
return false;
}
void scrollMenu(int delta, int maxOptions) {
menuIndex = (menuIndex + delta + maxOptions) % maxOptions;
}
int readChordButton() {
unsigned long now = millis();
int newPress = -1;
for (int i = 0; i < 7; i++) {
bool raw = (digitalRead(CHORD_PINS[i]) == LOW);
if (raw != chordLastRaw[i]) {
chordDebounceTime[i] = now;
chordLastRaw[i] = raw;
}
bool wasPressed = chordDebounced[i];
if ((now - chordDebounceTime[i]) >= DEBOUNCE_MS) {
chordDebounced[i] = raw;
}
// Edge detection: fires once when debounced state transitions to pressed
if (chordDebounced[i] && !wasPressed && newPress < 0) {
newPress = i;
}
}
return newPress;
}
int bpm = 120;
const int BPM_SAMPLES = 16;
int bpmReadings[BPM_SAMPLES] = {120,120,120,120,120,120,120,120,
120,120,120,120,120,120,120,120};
int bpmReadIdx = 0;
long bpmSum = 120L * BPM_SAMPLES;
unsigned long lastDisplayTime = 0;
const unsigned long DISPLAY_MS = 200; // 5 fps -- keeps I2C from starving drum timing
char cachedKickPath[32] = "";
char cachedSnarePath[32] = "";
char cachedHihatPath[32] = "";
char cachedPerc1Path[32] = "";
char cachedPerc2Path[32] = "";
char cachedPerc3Path[32] = "";
char cachedChordFile[7][32];
#define MAX_DRUM_SAMPLES 44100
#define MAX_DRUM_WORDS (MAX_DRUM_SAMPLES / 2 + 1)
#define MAX_PERC_SAMPLES 44100
#define MAX_PERC_WORDS (MAX_PERC_SAMPLES / 2 + 1)
DMAMEM uint32_t kickData[MAX_DRUM_WORDS];
DMAMEM uint32_t snareData[MAX_DRUM_WORDS];
DMAMEM uint32_t hihatData[MAX_DRUM_WORDS];
uint32_t perc1Data[MAX_PERC_WORDS];
uint32_t perc2Data[MAX_PERC_WORDS];
uint32_t perc3Data[MAX_PERC_WORDS];
bool hasPerc1 = false, hasPerc2 = false, hasPerc3 = false;
int fillKickPat[20];
int fillSnarePat[20];
int fillHihatPat[20];
int fillPerc1Pat[20];
int fillPerc2Pat[20];
int fillPerc3Pat[20];
int lastVolRaw = -1;
unsigned long lastAnalogTime = 0;
const unsigned long ANALOG_MS = 40;
const int BEATS_PER_BAR[3] = {4, 3, 5};
const int STEPS_PER_BAR[3] = {16, 12, 20};
unsigned long lastDrumStepTime = 0; // microseconds
unsigned long drumInterval = 0; // microseconds
int currentDrumStep = 0;
unsigned long lastChordStepTime = 0;
unsigned long chordInterval = 0;
int currentChordStep = 0;
bool isPlaying = false;
const int KICK_4_4[11][16] = {
{1,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0}, // Rock: 1 & 3
{1,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0}, // Jazz: light 1 & 3
{1,0,0,0, 0,0,0,0, 1,0,0,1, 0,0,0,0}, // Hip Hop: 1, 3, a-of-3
{1,0,0,0, 0,0,0,1, 0,0,0,0, 1,0,0,0}, // Trap: 1, a-of-2, 4 (half-time bounce)
{1,0,0,0, 0,0,1,0, 1,0,0,0, 0,0,0,0}, // Funk: 1, &-of-2, 3
{1,0,0,1, 0,0,1,0, 1,0,0,0, 0,1,0,0}, // Latin: tumbao-inspired
{0,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0}, // Reggae: one-drop on 3
{1,0,0,0, 0,0,1,0, 1,0,0,0, 0,0,0,0}, // Bossa: tumbao 1, &-of-2, 3
{1,0,0,1, 0,0,1,0, 0,1,0,0, 1,0,0,0}, // Math Rock: polyrhythmic
{1,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,1,0}, // Midwest Emo: 1, 3, pickup 16th
{1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0}, // House: four-on-the-floor
};
const int SNARE_4_4[11][16] = {
{0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0}, // Rock: 2 & 4
{0,0,1,0, 0,0,0,1, 0,0,1,0, 0,0,0,0}, // Jazz: ghost notes
{0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0}, // Hip Hop: 2 & 4
{0,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0}, // Trap: half-time on 3
{0,0,0,0, 1,0,1,0, 0,0,0,0, 1,0,0,1}, // Funk: 2, &-of-2, 4, a-of-4
{0,0,1,0, 0,0,0,1, 0,0,1,0, 0,0,0,0}, // Latin: cross-stick
{0,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0}, // Reggae: rimshot on 3 (with kick)
{0,0,0,0, 1,0,0,0, 0,0,1,0, 0,0,0,0}, // Bossa: cross-stick 2, &-of-3
{0,0,0,1, 0,1,0,0, 0,0,0,1, 0,0,0,0}, // Math Rock: angular
{0,0,0,0, 1,0,0,0, 0,1,0,0, 1,0,0,0}, // Midwest Emo: 2, ghost, 4
{0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0}, // House: clap on 2 & 4
};
const int HIHAT_4_4[11][16] = {
{1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0}, // Rock: 8th notes
{1,0,0,1, 0,1,0,0, 1,0,0,1, 0,1,0,0}, // Jazz: swing ride approx
{1,0,0,1, 0,0,1,0, 1,0,0,1, 0,0,1,0}, // Hip Hop: swung
{1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1}, // Trap: all 16ths
{1,1,0,1, 0,1,1,0, 1,1,0,1, 0,1,1,0}, // Funk: syncopated
{1,1,0,1, 0,1,1,0, 1,0,1,1, 0,1,0,1}, // Latin: bell pattern
{0,0,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,0}, // Reggae: offbeat skank
{1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0}, // Bossa: steady 8ths (ride)
{1,0,0,1, 0,1,0,0, 1,0,1,0, 0,0,1,0}, // Math Rock: irregular
{1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0}, // Midwest Emo: 8th notes
{0,1,0,0, 0,1,0,0, 0,1,0,0, 0,1,0,0}, // House A: offbeat 8ths shifted 1/16th earlier so hits land on grid
};
// perc1: Rock=crash, Jazz=ride, HipHop=openHH, Trap=openHH, Funk=openHH,
// Latin=conga, Reggae=rim, Bossa=shaker, MathRock=crash, MidwestEmo=openHH, House=shaker
const int PERC1_4_4[11][16] = {
{1,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Rock: crash accent on beat 1
{1,0,0,1, 0,1,0,0, 1,0,0,1, 0,1,0,0}, // Jazz: ride swing pattern
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,1,0}, // Hip Hop: open hat end-of-bar
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,1,0}, // Trap: open hat accent
{0,0,0,0, 0,0,0,1, 0,0,0,0, 0,0,0,1}, // Funk: open hat upbeat accents
{1,0,0,1, 0,0,1,0, 0,1,0,0, 1,0,0,1}, // Latin: conga tumbao
{0,0,0,0, 0,0,1,0, 0,0,0,0, 0,0,1,0}, // Reggae: rim offbeat accents
{1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1}, // Bossa: steady 16th shaker
{1,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Math Rock: crash on 1
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,1,0}, // Midwest Emo: open hat pickup
{1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1}, // House: 16th shaker texture
};
// perc2: Rock=ride, Jazz=crossStick, HipHop=clap, Trap=clap, Funk=clap,
// Latin=cowbell, Reggae=openHH, Bossa=bell, MathRock=china, MidwestEmo=crash, House=clap
const int PERC2_4_4[11][16] = {
{1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0}, // Rock: ride quarter notes
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,1,0,0}, // Jazz: ghost cross-stick
{0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0}, // Hip Hop: clap layered on 2 & 4
{0,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0}, // Trap: clap on 3 (half-time)
{0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0}, // Funk: clap doubles snare on 2 & 4
{1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0}, // Latin: cowbell cascara
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Reggae: no open hat in main groove
{1,0,0,0, 0,0,1,0, 0,0,1,0, 0,0,1,0}, // Bossa: agogo 1, &2, &3, &4 (smooth pulse, no gap)
{0,0,0,0, 0,0,0,0, 0,1,0,0, 0,0,0,0}, // Math Rock: china accent
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Midwest Emo: crash reserved for accents
{0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0}, // House: clap on 2 & 4
};
// perc3: Rock=openHH, Jazz=splash, HipHop=rim, Trap=perc, Funk=cowbell,
// Latin=shaker, Reggae=tambourine, Bossa=rim, MathRock=ride, MidwestEmo=ride, House=ride
const int PERC3_4_4[11][16] = {
{0,0,0,0, 0,0,0,1, 0,0,0,0, 0,0,0,1}, // Rock: open hat before 2 & 4
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Jazz: splash only on fills
{0,0,0,0, 0,0,0,0, 0,1,0,0, 0,0,0,0}, // Hip Hop: ghost rim
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Trap: 808 perc reserved for variation
{1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0}, // Funk: quarter note cowbell
{1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1}, // Latin: constant shaker texture
{0,0,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,0}, // Reggae: offbeat tambourine
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Bossa: no rim in groove (avoids single-hit jerk)
{0,0,1,0, 0,1,0,0, 0,0,0,0, 1,0,0,1}, // Math Rock: irregular ride
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Midwest Emo: ride reserved for sections
{0,0,0,0, 0,0,1,0, 0,0,0,0, 0,0,1,0}, // House: ride bell on & of 2 and 4 only (not every 8th)
};
const int KICK_3_4[4][12] = {
{1,0,0,0, 0,0,0,0, 0,0,1,0}, // Jazz Waltz: 1, &-of-3
{1,0,0,0, 0,0,0,0, 1,0,0,0}, // Rock Waltz: 1 & 3
{1,0,1,0, 1,0,1,0, 1,0,0,0}, // Metal: double-bass 8ths through beats 1-2
{1,0,0,1, 0,0,1,0, 0,1,0,0}, // Math Rock: angular grouping
};
const int SNARE_3_4[4][12] = {
{0,0,0,0, 0,0,1,0, 0,0,0,0}, // Jazz Waltz: ghost on &-of-2
{0,0,0,0, 1,0,0,0, 0,0,0,0}, // Rock Waltz: backbeat on 2
{0,0,0,0, 1,0,0,0, 0,0,1,0}, // Metal: 2, &-of-3
{0,0,1,0, 0,0,0,0, 1,0,0,1}, // Math Rock: &-of-1, 3, a-of-3
};
const int HIHAT_3_4[4][12] = {
{1,0,0,1, 0,1,0,0, 1,0,0,1}, // Jazz Waltz: swing ride feel
{1,0,1,0, 1,0,1,0, 1,0,1,0}, // Rock Waltz: 8th notes
{1,0,1,0, 1,0,1,0, 1,0,1,0}, // Metal: 8ths on ride
{1,0,0,1, 1,0,0,1, 0,1,0,1}, // Math Rock: irregular
};
// 3/4 perc: Jazz Waltz uses jazz samples, Rock/Metal use rock, Math Rock uses mathrock
const int PERC1_3_4[4][12] = {
{1,0,0,1, 0,1,0,0, 1,0,0,0}, // Jazz Waltz: ride pattern
{0,0,0,0, 0,0,0,0, 0,0,0,0}, // Rock Waltz: crash reserved
{0,0,0,0, 0,0,0,0, 0,0,0,0}, // Metal: crash reserved
{1,0,0,0, 0,0,0,0, 0,0,0,0}, // Math Rock: crash on 1
};
const int PERC2_3_4[4][12] = {
{0,0,0,0, 0,0,0,1, 0,0,0,0}, // Jazz Waltz: ghost cross-stick
{0,0,0,0, 0,0,0,0, 0,0,0,0}, // Rock Waltz: none
{0,0,0,0, 0,0,0,0, 0,0,0,0}, // Metal: none
{0,0,0,0, 0,0,0,0, 0,1,0,0}, // Math Rock: china accent
};
const int PERC3_3_4[4][12] = {
{0,0,0,0, 0,0,0,0, 0,0,0,0}, // Jazz Waltz: splash reserved
{0,0,0,0, 0,0,0,0, 0,0,1,0}, // Rock Waltz: open hat anticipation
{0,0,1,0, 0,1,0,0, 0,0,0,0}, // Metal: ride accents
{0,0,1,0, 0,0,0,0, 1,0,0,0}, // Math Rock: ride irregular
};
const int KICK_5_4[4][20] = {
{1,0,0,0, 0,0,0,1, 0,0,0,0, 0,1,0,0, 0,0,0,0}, // Jazz: 1, a-of-2, e-of-4
{1,0,1,0, 0,1,0,0, 0,0,1,0, 1,0,0,0, 0,0,0,0}, // Metal/Prog: driving
{1,0,0,0, 0,0,1,0, 0,0,1,0, 0,0,0,0, 0,0,1,0}, // Afrobeat: syncopated
{1,0,0,1, 0,0,1,0, 0,0,1,0, 0,0,1,0, 0,1,0,0}, // Math Rock: angular
};
const int SNARE_5_4[4][20] = {
{0,0,0,0, 0,1,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0}, // Jazz: ghost e-of-2, a-of-4
{0,0,0,0, 0,1,0,0, 0,0,0,0, 0,0,1,0, 0,0,0,1}, // Metal/Prog: offbeats
{0,0,1,0, 0,0,0,0, 1,0,0,0, 0,0,0,0, 0,0,1,0}, // Afrobeat: cross-rhythmic
{0,0,0,0, 1,0,0,1, 0,0,0,0, 0,1,0,0, 0,0,1,0}, // Math Rock: irregular
};
const int HIHAT_5_4[4][20] = {
{1,0,1,0, 0,1,0,1, 0,0,1,0, 1,0,0,1, 0,1,0,0}, // Jazz: swing ride
{1,1,1,1, 0,1,1,1, 1,0,1,1, 1,1,0,1, 1,1,1,0}, // Metal/Prog: dense with 5-gaps
{1,1,0,1, 0,1,0,0, 1,1,0,1, 0,0,1,0, 0,1,0,1}, // Afrobeat: complex polyrhythmic
{1,0,0,1, 0,1,0,0, 0,1,0,0, 1,0,1,0, 0,0,1,0}, // Math Rock: irregular
};
// 5/4 perc: Jazz uses jazz samples, Metal uses rock, Afrobeat uses latin, Math Rock uses mathrock
const int PERC1_5_4[4][20] = {
{1,0,0,1, 0,1,0,0, 1,0,0,1, 0,1,0,0, 1,0,0,0}, // Jazz: ride swing
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Metal/Prog: crash reserved
{1,0,0,1, 0,0,1,0, 0,1,0,0, 1,0,0,1, 0,0,1,0}, // Afrobeat: conga pattern
{1,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Math Rock: crash on 1
};
const int PERC2_5_4[4][20] = {
{0,0,0,0, 0,0,0,1, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Jazz: ghost cross-stick
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Metal/Prog: none
{1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0}, // Afrobeat: cowbell
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,1,0,0, 0,0,0,0}, // Math Rock: china accent
};
const int PERC3_5_4[4][20] = {
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Jazz: splash reserved
{0,0,1,0, 0,0,1,0, 0,0,0,0, 0,0,1,0, 0,0,0,0}, // Metal/Prog: ride accents
{1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1}, // Afrobeat: shaker texture
{0,0,1,0, 0,1,0,0, 0,0,0,1, 0,0,0,0, 1,0,0,0}, // Math Rock: ride irregular
};
// B variants: same backbeat as A, only subtle texture (hat/perc) so it feels like same groove, different bar
// Kick and snare always match A — no extra or missing beats that could stutter
const int KICK_4_4_B[11][16] = {
{1,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0}, // Rock: same 1 & 3
{1,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0}, // Jazz: same
{1,0,0,0, 0,0,0,0, 1,0,0,1, 0,0,0,0}, // Hip Hop: same
{1,0,0,0, 0,0,0,1, 0,0,0,0, 1,0,0,0}, // Trap: same
{1,0,0,0, 0,0,1,0, 1,0,0,0, 0,0,0,0}, // Funk: same
{1,0,0,1, 0,0,1,0, 1,0,0,0, 0,1,0,0}, // Latin: same
{0,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0}, // Reggae: same
{1,0,0,0, 0,0,1,0, 1,0,0,0, 0,0,0,0}, // Bossa: same
{1,0,0,1, 0,0,1,0, 0,1,0,0, 1,0,0,0}, // Math Rock: same
{1,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,1,0}, // Midwest Emo: same
{1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0}, // House: same
};
const int SNARE_4_4_B[11][16] = {
{0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0}, // Rock: same 2 & 4
{0,0,1,0, 0,0,0,1, 0,0,1,0, 0,0,0,0}, // Jazz: same
{0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0}, // Hip Hop: same
{0,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0}, // Trap: same
{0,0,0,0, 1,0,1,0, 0,0,0,0, 1,0,0,1}, // Funk: same
{0,0,1,0, 0,0,0,1, 0,0,1,0, 0,0,0,0}, // Latin: same
{0,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0}, // Reggae: same
{0,0,0,0, 1,0,0,0, 0,0,1,0, 0,0,0,0}, // Bossa: same
{0,0,0,1, 0,1,0,0, 0,0,0,1, 0,0,0,0}, // Math Rock: same
{0,0,0,0, 1,0,0,0, 0,1,0,0, 1,0,0,0}, // Midwest Emo: same
{0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0}, // House: same
};
// Hi-hat B: same pattern as A (steady groove), or one 8th dropped/added for feel
const int HIHAT_4_4_B[11][16] = {
{1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0}, // Rock: same 8ths
{1,0,0,1, 0,1,0,0, 1,0,0,1, 0,1,0,0}, // Jazz: same
{1,0,0,1, 0,0,1,0, 1,0,0,1, 0,0,1,0}, // Hip Hop: same
{1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1}, // Trap: same
{1,1,0,1, 0,1,1,0, 1,1,0,1, 0,1,1,0}, // Funk: same
{1,1,0,1, 0,1,1,0, 1,0,1,1, 0,1,0,1}, // Latin: same
{0,0,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,0}, // Reggae: same
{1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0}, // Bossa: same
{1,0,0,1, 0,1,0,0, 1,0,1,0, 0,0,1,0}, // Math Rock: same
{1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0}, // Midwest Emo: same
{1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0}, // House B: straight 8ths (variation)
};
// Perc B: only accent/timbre variation (e.g. open hat one 16th earlier), never extra downbeat hits
const int PERC1_4_4_B[11][16] = {
{1,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Rock: same crash on 1
{1,0,0,1, 0,1,0,0, 1,0,0,1, 0,1,0,0}, // Jazz: same
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,1,0}, // Hip Hop: same
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,1,0}, // Trap: same
{0,0,0,0, 0,0,0,1, 0,0,0,0, 0,0,0,1}, // Funk: same
{1,0,0,1, 0,0,1,0, 0,1,0,0, 1,0,0,1}, // Latin: same
{0,0,0,0, 0,0,1,0, 0,0,0,0, 0,0,1,0}, // Reggae: same
{1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1}, // Bossa: same
{1,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Math Rock: same
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,1,0}, // Midwest Emo: same
{1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1}, // House: same
};
const int PERC2_4_4_B[11][16] = {
{1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0}, // Rock: same ride quarters
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,1,0,0}, // Jazz: same
{0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0}, // Hip Hop: same
{0,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0}, // Trap: same
{0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0}, // Funk: same
{1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0}, // Latin: same
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Reggae: same
{1,0,0,0, 0,0,1,0, 0,0,1,0, 0,0,1,0}, // Bossa: same
{0,0,0,0, 0,0,0,0, 0,1,0,0, 0,0,0,0}, // Math Rock: same
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Midwest Emo: same
{0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0}, // House: same
};
const int PERC3_4_4_B[11][16] = {
{0,0,0,0, 0,0,1,0, 0,0,0,0, 0,0,1,0}, // Rock: open one 16th earlier (pushed), same groove
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Jazz: same
{0,0,0,0, 0,0,0,0, 0,1,0,0, 0,0,0,0}, // Hip Hop: same
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Trap: same
{1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0}, // Funk: same
{1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1}, // Latin: same
{0,0,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,0}, // Reggae: same
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Bossa: same
{0,0,1,0, 0,1,0,0, 0,0,0,0, 1,0,0,1}, // Math Rock: same
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Midwest Emo: same
{0,0,0,0, 0,0,1,0, 0,0,0,0, 0,0,1,0}, // House B: bell &2 &4 only
};
// C variants: third groove option (1/3 probability) — refine per genre (start with Rock)
const int KICK_4_4_C[11][16] = {
{1,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0}, // Rock
{1,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0}, // Jazz
{1,0,0,0, 0,0,0,0, 1,0,0,1, 0,0,0,0}, // Hip Hop
{1,0,0,0, 0,0,0,1, 0,0,0,0, 1,0,0,0}, // Trap
{1,0,0,0, 0,0,1,0, 1,0,0,0, 0,0,0,0}, // Funk
{1,0,0,1, 0,0,1,0, 1,0,0,0, 0,1,0,0}, // Latin
{0,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0}, // Reggae
{1,0,0,0, 0,0,1,0, 1,0,0,0, 0,0,0,0}, // Bossa
{1,0,0,1, 0,0,1,0, 0,1,0,0, 1,0,0,0}, // Math Rock
{1,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,1,0}, // Midwest Emo
{1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0}, // House
};
const int SNARE_4_4_C[11][16] = {
{0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0}, // Rock
{0,0,1,0, 0,0,0,1, 0,0,1,0, 0,0,0,0}, // Jazz
{0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0}, // Hip Hop
{0,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0}, // Trap
{0,0,0,0, 1,0,1,0, 0,0,0,0, 1,0,0,1}, // Funk
{0,0,1,0, 0,0,0,1, 0,0,1,0, 0,0,0,0}, // Latin
{0,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0}, // Reggae
{0,0,0,0, 1,0,0,0, 0,0,1,0, 0,0,0,0}, // Bossa
{0,0,0,1, 0,1,0,0, 0,0,0,1, 0,0,0,0}, // Math Rock
{0,0,0,0, 1,0,0,0, 0,1,0,0, 1,0,0,0}, // Midwest Emo
{0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0}, // House
};
const int HIHAT_4_4_C[11][16] = {
{1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0}, // Rock
{1,0,0,1, 0,1,0,0, 1,0,0,1, 0,1,0,0}, // Jazz
{1,0,0,1, 0,0,1,0, 1,0,0,1, 0,0,1,0}, // Hip Hop
{1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1}, // Trap
{1,1,0,1, 0,1,1,0, 1,1,0,1, 0,1,1,0}, // Funk
{1,1,0,1, 0,1,1,0, 1,0,1,1, 0,1,0,1}, // Latin
{0,0,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,0}, // Reggae
{1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0}, // Bossa
{1,0,0,1, 0,1,0,0, 1,0,1,0, 0,0,1,0}, // Math Rock
{1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0}, // Midwest Emo
{1,1,1,0, 1,1,1,0, 1,1,1,0, 1,1,1,0}, // House C: 16ths with last 16th rest (driving)
};
const int PERC1_4_4_C[11][16] = {
{1,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Rock
{1,0,0,1, 0,1,0,0, 1,0,0,1, 0,1,0,0}, // Jazz
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,1,0}, // Hip Hop
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,1,0}, // Trap
{0,0,0,0, 0,0,0,1, 0,0,0,0, 0,0,0,1}, // Funk
{1,0,0,1, 0,0,1,0, 0,1,0,0, 1,0,0,1}, // Latin
{0,0,0,0, 0,0,1,0, 0,0,0,0, 0,0,1,0}, // Reggae
{1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1}, // Bossa
{1,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Math Rock
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,1,0}, // Midwest Emo
{1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1}, // House
};
const int PERC2_4_4_C[11][16] = {
{1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0}, // Rock
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,1,0,0}, // Jazz
{0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0}, // Hip Hop
{0,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0}, // Trap
{0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0}, // Funk
{1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0}, // Latin
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Reggae
{1,0,0,0, 0,0,1,0, 0,0,1,0, 0,0,1,0}, // Bossa
{0,0,0,0, 0,0,0,0, 0,1,0,0, 0,0,0,0}, // Math Rock
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Midwest Emo
{0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0}, // House
};
const int PERC3_4_4_C[11][16] = {
{0,0,0,0, 0,0,0,1, 0,0,0,0, 0,0,1,0}, // Rock
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Jazz
{0,0,0,0, 0,0,0,0, 0,1,0,0, 0,0,0,0}, // Hip Hop
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Trap
{1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0}, // Funk
{1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1}, // Latin
{0,0,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,0}, // Reggae
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Bossa C: same
{0,0,1,0, 0,1,0,0, 0,0,0,0, 1,0,0,1}, // Math Rock
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Midwest Emo
{0,0,0,0, 0,0,1,0, 0,0,0,0, 0,0,1,0}, // House C: bell &2 &4 only
};
// 3/4 B: same backbeat as A, texture only
const int KICK_3_4_B[4][12] = {
{1,0,0,0, 0,0,0,0, 0,0,1,0}, // Jazz Waltz: same
{1,0,0,0, 0,0,0,0, 1,0,0,0}, // Rock Waltz: same 1 & 3
{1,0,1,0, 1,0,1,0, 1,0,0,0}, // Metal: same
{1,0,0,1, 0,0,1,0, 0,1,0,0}, // Math Rock: same
};
const int SNARE_3_4_B[4][12] = {
{0,0,0,0, 0,0,1,0, 0,0,0,0}, // Jazz Waltz: same
{0,0,0,0, 1,0,0,0, 0,0,0,0}, // Rock Waltz: same backbeat 2
{0,0,0,0, 1,0,0,0, 0,0,1,0}, // Metal: same
{0,0,1,0, 0,0,0,0, 1,0,0,1}, // Math Rock: same
};
const int HIHAT_3_4_B[4][12] = {
{1,0,0,1, 0,1,0,0, 1,0,0,1}, // Jazz Waltz: same
{1,0,1,0, 1,0,1,0, 1,0,1,0}, // Rock Waltz: same
{1,0,1,0, 1,0,1,0, 1,0,1,0}, // Metal: same
{1,0,0,1, 1,0,0,1, 0,1,0,1}, // Math Rock: same
};
const int PERC1_3_4_B[4][12] = {
{1,0,0,1, 0,1,0,0, 1,0,0,0}, // Jazz Waltz: same
{0,0,0,0, 0,0,0,0, 0,0,0,0}, // Rock Waltz: same
{0,0,0,0, 0,0,0,0, 0,0,0,0}, // Metal: same
{1,0,0,0, 0,0,0,0, 0,0,0,0}, // Math Rock: same
};
const int PERC2_3_4_B[4][12] = {
{0,0,0,0, 0,0,0,1, 0,0,0,0}, // Jazz Waltz: same
{0,0,0,0, 0,0,0,0, 0,0,0,0}, // Rock Waltz: same
{0,0,0,0, 0,0,0,0, 0,0,0,0}, // Metal: same
{0,0,0,0, 0,0,0,0, 0,1,0,0}, // Math Rock: same
};
const int PERC3_3_4_B[4][12] = {
{0,0,0,0, 0,0,0,0, 0,0,0,0}, // Jazz Waltz: same
{0,0,0,0, 0,0,0,0, 0,0,1,0}, // Rock Waltz: same
{0,0,1,0, 0,1,0,0, 0,0,0,0}, // Metal: same
{0,0,1,0, 0,0,0,0, 1,0,0,0}, // Math Rock: same
};
// 3/4 C: third groove (1/3 probability)
const int KICK_3_4_C[4][12] = {
{1,0,0,0, 0,0,0,0, 0,0,1,0}, // Jazz Waltz
{1,0,0,0, 0,0,0,0, 1,0,0,0}, // Rock Waltz
{1,0,1,0, 1,0,1,0, 1,0,0,0}, // Metal
{1,0,0,1, 0,0,1,0, 0,1,0,0}, // Math Rock
};
const int SNARE_3_4_C[4][12] = {
{0,0,0,0, 0,0,1,0, 0,0,0,0}, // Jazz Waltz
{0,0,0,0, 1,0,0,0, 0,0,0,0}, // Rock Waltz
{0,0,0,0, 1,0,0,0, 0,0,1,0}, // Metal
{0,0,1,0, 0,0,0,0, 1,0,0,1}, // Math Rock
};
const int HIHAT_3_4_C[4][12] = {
{1,0,0,1, 0,1,0,0, 1,0,0,1}, // Jazz Waltz
{1,0,1,0, 1,0,1,0, 1,0,1,0}, // Rock Waltz
{1,0,1,0, 1,0,1,0, 1,0,1,0}, // Metal
{1,0,0,1, 1,0,0,1, 0,1,0,1}, // Math Rock
};
const int PERC1_3_4_C[4][12] = {
{1,0,0,1, 0,1,0,0, 1,0,0,0}, // Jazz Waltz
{0,0,0,0, 0,0,0,0, 0,0,0,0}, // Rock Waltz
{0,0,0,0, 0,0,0,0, 0,0,0,0}, // Metal
{1,0,0,0, 0,0,0,0, 0,0,0,0}, // Math Rock
};
const int PERC2_3_4_C[4][12] = {
{0,0,0,0, 0,0,0,1, 0,0,0,0}, // Jazz Waltz
{0,0,0,0, 0,0,0,0, 0,0,0,0}, // Rock Waltz
{0,0,0,0, 0,0,0,0, 0,0,0,0}, // Metal
{0,0,0,0, 0,0,0,0, 0,1,0,0}, // Math Rock
};
const int PERC3_3_4_C[4][12] = {
{0,0,0,0, 0,0,0,0, 0,0,0,0}, // Jazz Waltz
{0,0,0,0, 0,0,0,0, 0,0,1,0}, // Rock Waltz
{0,0,1,0, 0,1,0,0, 0,0,0,0}, // Metal
{0,0,1,0, 0,0,0,0, 1,0,0,0}, // Math Rock
};
// 5/4 B: same backbeat as A, texture only
const int KICK_5_4_B[4][20] = {
{1,0,0,0, 0,0,0,1, 0,0,0,0, 0,1,0,0, 0,0,0,0}, // Jazz: same
{1,0,1,0, 0,1,0,0, 0,0,1,0, 1,0,0,0, 0,0,0,0}, // Metal/Prog: same
{1,0,0,0, 0,0,1,0, 0,0,1,0, 0,0,0,0, 0,0,1,0}, // Afrobeat: same
{1,0,0,1, 0,0,1,0, 0,0,1,0, 0,0,1,0, 0,1,0,0}, // Math Rock: same
};
const int SNARE_5_4_B[4][20] = {
{0,0,0,0, 0,1,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0}, // Jazz: same
{0,0,0,0, 0,1,0,0, 0,0,0,0, 0,0,1,0, 0,0,0,1}, // Metal/Prog: same
{0,0,1,0, 0,0,0,0, 1,0,0,0, 0,0,0,0, 0,0,1,0}, // Afrobeat: same
{0,0,0,0, 1,0,0,1, 0,0,0,0, 0,1,0,0, 0,0,1,0}, // Math Rock: same
};
const int HIHAT_5_4_B[4][20] = {
{1,0,1,0, 0,1,0,1, 0,0,1,0, 1,0,0,1, 0,1,0,0}, // Jazz: same
{1,1,1,1, 0,1,1,1, 1,0,1,1, 1,1,0,1, 1,1,1,0}, // Metal/Prog: same
{1,1,0,1, 0,1,0,0, 1,1,0,1, 0,0,1,0, 0,1,0,1}, // Afrobeat: same
{1,0,0,1, 0,1,0,0, 0,1,0,0, 1,0,1,0, 0,0,1,0}, // Math Rock: same
};
const int PERC1_5_4_B[4][20] = {
{1,0,0,1, 0,1,0,0, 1,0,0,1, 0,1,0,0, 1,0,0,0}, // Jazz: same
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Metal/Prog: same
{1,0,0,1, 0,0,1,0, 0,1,0,0, 1,0,0,1, 0,0,1,0}, // Afrobeat: same
{1,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Math Rock: same
};
const int PERC2_5_4_B[4][20] = {
{0,0,0,0, 0,0,0,1, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Jazz: same
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Metal/Prog: same
{1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0}, // Afrobeat: same
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,1,0,0, 0,0,0,0}, // Math Rock: same
};
const int PERC3_5_4_B[4][20] = {
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Jazz: same
{0,0,1,0, 0,0,1,0, 0,0,0,0, 0,0,1,0, 0,0,0,0}, // Metal/Prog: same
{1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1}, // Afrobeat: same
{0,0,1,0, 0,1,0,0, 0,0,0,1, 0,0,0,0, 1,0,0,0}, // Math Rock: same
};
// 5/4 C: third groove (1/3 probability)
const int KICK_5_4_C[4][20] = {
{1,0,0,0, 0,0,0,1, 0,0,0,0, 0,1,0,0, 0,0,0,0}, // Jazz
{1,0,1,0, 0,1,0,0, 0,0,1,0, 1,0,0,0, 0,0,0,0}, // Metal/Prog
{1,0,0,0, 0,0,1,0, 0,0,1,0, 0,0,0,0, 0,0,1,0}, // Afrobeat
{1,0,0,1, 0,0,1,0, 0,0,1,0, 0,0,1,0, 0,1,0,0}, // Math Rock
};
const int SNARE_5_4_C[4][20] = {
{0,0,0,0, 0,1,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0}, // Jazz
{0,0,0,0, 0,1,0,0, 0,0,0,0, 0,0,1,0, 0,0,0,1}, // Metal/Prog
{0,0,1,0, 0,0,0,0, 1,0,0,0, 0,0,0,0, 0,0,1,0}, // Afrobeat
{0,0,0,0, 1,0,0,1, 0,0,0,0, 0,1,0,0, 0,0,1,0}, // Math Rock
};
const int HIHAT_5_4_C[4][20] = {
{1,0,1,0, 0,1,0,1, 0,0,1,0, 1,0,0,1, 0,1,0,0}, // Jazz
{1,1,1,1, 0,1,1,1, 1,0,1,1, 1,1,0,1, 1,1,1,0}, // Metal/Prog
{1,1,0,1, 0,1,0,0, 1,1,0,1, 0,0,1,0, 0,1,0,1}, // Afrobeat
{1,0,0,1, 0,1,0,0, 0,1,0,0, 1,0,1,0, 0,0,1,0}, // Math Rock
};
const int PERC1_5_4_C[4][20] = {
{1,0,0,1, 0,1,0,0, 1,0,0,1, 0,1,0,0, 1,0,0,0}, // Jazz
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Metal/Prog
{1,0,0,1, 0,0,1,0, 0,1,0,0, 1,0,0,1, 0,0,1,0}, // Afrobeat
{1,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Math Rock
};
const int PERC2_5_4_C[4][20] = {
{0,0,0,0, 0,0,0,1, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Jazz
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Metal/Prog
{1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0}, // Afrobeat
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,1,0,0, 0,0,0,0}, // Math Rock
};
const int PERC3_5_4_C[4][20] = {
{0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}, // Jazz
{0,0,1,0, 0,0,1,0, 0,0,0,0, 0,0,1,0, 0,0,0,0}, // Metal/Prog
{1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1}, // Afrobeat
{0,0,1,0, 0,1,0,0, 0,0,0,1, 0,0,0,0, 1,0,0,0}, // Math Rock
};
// Instrument indices for getPattern(). Keeps playDrums() independent of pattern array layout.
#define DRUM_KICK 0
#define DRUM_SNARE 1
#define DRUM_HIHAT 2
#define DRUM_PERC1 3
#define DRUM_PERC2 4
#define DRUM_PERC3 5
// Single access point for pattern data: timeSig 0=4/4, 1=3/4, 2=5/4; variant 0=A, 1=B, 2=C; instrument DRUM_*.
static int getPattern(int timeSig, int genre, int variant, int instrument, int step) {
if (timeSig == 0) {
if (variant == 0) {
switch (instrument) {
case DRUM_KICK: return KICK_4_4[genre][step];
case DRUM_SNARE: return SNARE_4_4[genre][step];
case DRUM_HIHAT: return HIHAT_4_4[genre][step];
case DRUM_PERC1: return PERC1_4_4[genre][step];
case DRUM_PERC2: return PERC2_4_4[genre][step];
case DRUM_PERC3: return PERC3_4_4[genre][step];
}
} else if (variant == 1) {
switch (instrument) {
case DRUM_KICK: return KICK_4_4_B[genre][step];
case DRUM_SNARE: return SNARE_4_4_B[genre][step];
case DRUM_HIHAT: return HIHAT_4_4_B[genre][step];
case DRUM_PERC1: return PERC1_4_4_B[genre][step];
case DRUM_PERC2: return PERC2_4_4_B[genre][step];
case DRUM_PERC3: return PERC3_4_4_B[genre][step];
}
} else {
switch (instrument) {
case DRUM_KICK: return KICK_4_4_C[genre][step];
case DRUM_SNARE: return SNARE_4_4_C[genre][step];
case DRUM_HIHAT: return HIHAT_4_4_C[genre][step];
case DRUM_PERC1: return PERC1_4_4_C[genre][step];
case DRUM_PERC2: return PERC2_4_4_C[genre][step];
case DRUM_PERC3: return PERC3_4_4_C[genre][step];
}
}
} else if (timeSig == 1) {
if (variant == 0) {
switch (instrument) {
case DRUM_KICK: return KICK_3_4[genre][step];
case DRUM_SNARE: return SNARE_3_4[genre][step];
case DRUM_HIHAT: return HIHAT_3_4[genre][step];
case DRUM_PERC1: return PERC1_3_4[genre][step];
case DRUM_PERC2: return PERC2_3_4[genre][step];
case DRUM_PERC3: return PERC3_3_4[genre][step];
}
} else if (variant == 1) {
switch (instrument) {
case DRUM_KICK: return KICK_3_4_B[genre][step];
case DRUM_SNARE: return SNARE_3_4_B[genre][step];
case DRUM_HIHAT: return HIHAT_3_4_B[genre][step];
case DRUM_PERC1: return PERC1_3_4_B[genre][step];
case DRUM_PERC2: return PERC2_3_4_B[genre][step];
case DRUM_PERC3: return PERC3_3_4_B[genre][step];
}
} else {
switch (instrument) {
case DRUM_KICK: return KICK_3_4_C[genre][step];
case DRUM_SNARE: return SNARE_3_4_C[genre][step];
case DRUM_HIHAT: return HIHAT_3_4_C[genre][step];
case DRUM_PERC1: return PERC1_3_4_C[genre][step];
case DRUM_PERC2: return PERC2_3_4_C[genre][step];
case DRUM_PERC3: return PERC3_3_4_C[genre][step];
}
}
} else {
if (variant == 0) {
switch (instrument) {
case DRUM_KICK: return KICK_5_4[genre][step];
case DRUM_SNARE: return SNARE_5_4[genre][step];
case DRUM_HIHAT: return HIHAT_5_4[genre][step];
case DRUM_PERC1: return PERC1_5_4[genre][step];
case DRUM_PERC2: return PERC2_5_4[genre][step];
case DRUM_PERC3: return PERC3_5_4[genre][step];
}
} else if (variant == 1) {
switch (instrument) {
case DRUM_KICK: return KICK_5_4_B[genre][step];
case DRUM_SNARE: return SNARE_5_4_B[genre][step];
case DRUM_HIHAT: return HIHAT_5_4_B[genre][step];
case DRUM_PERC1: return PERC1_5_4_B[genre][step];
case DRUM_PERC2: return PERC2_5_4_B[genre][step];
case DRUM_PERC3: return PERC3_5_4_B[genre][step];
}
} else {
switch (instrument) {
case DRUM_KICK: return KICK_5_4_C[genre][step];
case DRUM_SNARE: return SNARE_5_4_C[genre][step];
case DRUM_HIHAT: return HIHAT_5_4_C[genre][step];
case DRUM_PERC1: return PERC1_5_4_C[genre][step];
case DRUM_PERC2: return PERC2_5_4_C[genre][step];
case DRUM_PERC3: return PERC3_5_4_C[genre][step];
}
}
}
return 0;
}
void showScreen(const char* title, const char** options, int count, int highlighted) {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println(title);
display.drawLine(0, 10, 128, 10, SSD1306_WHITE);
int startIdx = max(0, min(highlighted - 1, count - 4));
for (int i = startIdx; i < min(startIdx + 4, count); i++) {
int y = 14 + (i - startIdx) * 12;
if (i == highlighted) {
display.fillRect(0, y - 1, 128, 11, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
} else {
display.setTextColor(SSD1306_WHITE);
}
display.setCursor(4, y);
display.print(options[i]);
}
display.display();
delay(1); // let OLED finish refresh before next draw, reduces I2C glitches
tickDrums();
}
void drawEye(int lidOpen, int pupilX, int pupilY) {
const int cx = 64, cy = 32, rx = 50, ry = 28;
display.clearDisplay();
if (lidOpen <= 0) {
display.drawLine(cx - rx, cy, cx + rx, cy, SSD1306_WHITE);
display.display();
return;
}
int topOpen = min(lidOpen, ry);
// Eye outline (lemon shape)
for (int x = -rx; x <= rx; x++) {
float nx = (float)x / (float)rx;
int ySpan = (int)(topOpen * sqrt(1.0f - nx * nx));
if (ySpan < 1) ySpan = 1;
display.drawPixel(cx + x, cy - ySpan, SSD1306_WHITE);
display.drawPixel(cx + x, cy + ySpan, SSD1306_WHITE);
}
// Fill the eye white
for (int x = -rx; x <= rx; x++) {
float nx = (float)x / (float)rx;
int ySpan = (int)(topOpen * sqrt(1.0f - nx * nx));
if (ySpan > 1) {
display.drawLine(cx + x, cy - ySpan + 1, cx + x, cy + ySpan - 1, SSD1306_WHITE);
}
}
// Iris
int irisR = min(topOpen - 2, 14);
if (irisR > 3) {
display.fillCircle(cx + pupilX, cy + pupilY, irisR, SSD1306_BLACK);
display.drawCircle(cx + pupilX, cy + pupilY, irisR, SSD1306_WHITE);
}
// Pupil
int pupilR = max(irisR / 2, 2);
if (pupilR > 1) {
display.fillCircle(cx + pupilX, cy + pupilY, pupilR, SSD1306_WHITE);
}
// Highlight
if (irisR > 5) {
display.fillCircle(cx + pupilX - 3, cy + pupilY - 3, 2, SSD1306_BLACK);
}
display.display();
}
void showSplashScreen() {
// Closed eye
drawEye(0, 0, 0);
delay(400);
// Eyelid opening
for (int i = 2; i <= 28; i += 3) {
drawEye(i, 0, 0);
delay(40);
}
// Look around
delay(200);
drawEye(28, -8, 0);
delay(250);
drawEye(28, 8, 0);
delay(250);
drawEye(28, 0, 0);
delay(100);
// Slight squint then wide open
drawEye(20, 0, 0);
delay(100);
drawEye(28, 0, 0);
}
// Returns true if enough time has passed to redraw (5fps cap); updates lastDisplayTime. Use at start of any throttled screen.
static bool shouldRedraw() {
unsigned long now = millis();
if (now - lastDisplayTime < DISPLAY_MS) return false;
lastDisplayTime = now;
return true;
}
void showSequenceEditScreen() {
if (!shouldRedraw()) return;
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print("Step ");
display.print(assigningStep + 1);
display.print("/8: ");
if (chordStepSet[assigningStep])
display.print(buildChordName(chordSequence[assigningStep]));
else
display.print("--");
display.drawLine(0, 10, 128, 10, SSD1306_WHITE);
// Show all 8 steps in two rows of 4
for (int row = 0; row < 2; row++) {
for (int col = 0; col < 4; col++) {
int idx = row * 4 + col;
int x = col * 32;
int y = 14 + row * 18;
if (idx == assigningStep) {
display.fillRect(x, y - 1, 31, 16, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
} else {
display.setTextColor(SSD1306_WHITE);
}
display.setCursor(x + 1, y);
display.print(idx + 1);
display.print(":");
display.setCursor(x + 1, y + 8);
if (chordStepSet[idx])
display.print(buildChordName(chordSequence[idx]));
else
display.print("--");
}
}
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 55);
display.print("Knob=Nav Click=Done");
display.display();
delay(1);
}
// Bottom status bar on play screen: loop mode (4 options) or pause/exit (2 options).
static void renderPlayStatusBar(bool isLoopMode) {
display.drawLine(0, 52, 128, 52, SSD1306_WHITE);
if (isLoopMode) {
const char* labels[4] = {"PAUSE", loopQuantized ? "GRID" : "RAW", "REC", "EXIT"};
int widths[4] = {36, 34, 26, 32};
int xPos[4] = {0, 36, 70, 96};
for (int i = 0; i < 4; i++) {
if (menuIndex == i) {
display.fillRect(xPos[i], 53, widths[i], 11, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
} else {
display.setTextColor(SSD1306_WHITE);
}
int tw = strlen(labels[i]) * 6;
display.setCursor(xPos[i] + (widths[i] - tw) / 2, 55);
display.print(labels[i]);
}
display.setTextColor(SSD1306_WHITE);
} else {
if (menuIndex == 0) {
display.fillRect(0, 53, 63, 11, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
display.setCursor(10, 55);
display.print("PAUSE");
display.setTextColor(SSD1306_WHITE);
display.setCursor(78, 55);
display.print("EXIT");
} else {
display.setCursor(10, 55);
display.print("PAUSE");
display.fillRect(64, 53, 64, 11, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
display.setCursor(78, 55);
display.print("EXIT");
display.setTextColor(SSD1306_WHITE);
}
}
}
void showPlayScreen() {
if (!shouldRedraw()) return;
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print(KEY_NAMES[selectedKey]);
display.print(" ");
display.print(SCALE_MODE_SHORT[selectedScaleMode]);
display.print(" ");
display.print(EXT_NAMES[selectedExt]);
display.drawLine(0, 10, 128, 10, SSD1306_WHITE);
display.setCursor(0, 13);
if (liveMode) {
display.print(buildChordName(liveChordDegree));
display.print(" LIVE");
} else if (liveOverride) {
display.print(buildChordName(liveOverrideDegree));
display.print(" LIVE*");
} else if (loopMode) {
display.print("Bar ");
display.print(loopBar + 1);
display.print("/");
display.print(loopBars);
display.print(" LOOP ");
display.print(loopQuantized ? "GRID" : "RAW");
} else {
display.print("S");
display.print(currentChordStep + 1);
display.print(":");
display.print(buildChordName(chordSequence[currentChordStep]));
display.print(" SEQ");
}
display.setCursor(0, 26);
display.print("BPM:");
display.print(bpm);
display.print(" ");
display.print(TIME_SIG_NAMES[selectedTimeSig]);
if (!liveMode && !loopMode) {
if (chordDuration == 0) display.print(" W");
else if (chordDuration == 1) display.print(" H");
else display.print(" Q");
}
display.print(" ");
display.print(INSTRUMENT_SHORT[selectedInstrument]);
display.setCursor(0, 38);
if (drumsOn) {
if (selectedTimeSig == 0) display.print(GENRES_4_4[selectedGenre]);
else if (selectedTimeSig == 1) display.print(GENRES_3_4[selectedGenre]);
else display.print(GENRES_5_4[selectedGenre]);
} else {
display.print("No Drums");
}
renderPlayStatusBar(loopMode);
display.display();
delay(1);
tickDrums();
}
void playStartupSound() {
String path = "/chords/";
path += INSTRUMENT_FOLDERS[selectedInstrument];
path += "/Gmaj9.wav";
chordPlayer1.stop();
chordPlayer1.play(path.c_str());
}
void cacheChordPaths() {
for (int i = 0; i < 7; i++) {
buildChordFilename(i, cachedChordFile[i], sizeof(cachedChordFile[i]));
}
}
void playSequenceChord(int degree) {
// Sync flag with actual player state so we recover if they ever drift
if (chordPlayer1.isPlaying() && !chordPlayer2.isPlaying())
chordPlayer1Active = true;
else if (chordPlayer2.isPlaying() && !chordPlayer1.isPlaying())
chordPlayer1Active = false;
if (chordPlayer1Active) {
chordMixerL.gain(0, 0.0f);
chordMixerR.gain(0, 0.0f);
chordPlayer1.stop();
chordUnmuteChannel = 0;
} else {
chordMixerL.gain(1, 0.0f);
chordMixerR.gain(1, 0.0f);
chordPlayer2.stop();
chordUnmuteChannel = 1;
}
pendingChordDegree = degree;
chordMuteTime = micros();
}
// Probability (0-100) to substitute default chord with a variant on sequence/loop playback. Genre = drum selection; no drums = even odds.
static int variantSubstituteProbability() {
if (!drumsOn || !drumsRunning) return 18; // no drums: natural ~18% chance
return 12 + (selectedGenre % 11); // with drums: 12-22% by genre
}
// Call instead of playSequenceChord when playing back a sequence or loop. With probability, uses high/inv/spread (avoids low).
void maybePlaySequenceChord(int degree) {
if (degree < 0 || degree > 6) return;
if (random(100) >= variantSubstituteProbability()) {
strcpy(pendingChordPath, cachedChordFile[degree]);
playSequenceChord(degree);
return;
}
// Pick variant: 2=high, 3=inv, 4=spread (avoid 1=low - very low notes don't sound good)
int voicing = 2 + (random(3)); // 2, 3, or 4
buildChordFilenameVariant(degree, voicing, pendingChordPath, sizeof(pendingChordPath));
playSequenceChord(degree);
}
void finishChordTransition() {
if (pendingChordDegree < 0) return;
if (micros() - chordMuteTime < 3500) return;
if (pendingChordDegree > 6) { pendingChordDegree = -1; return; }
if (chordUnmuteChannel < 0) { pendingChordDegree = -1; return; }
if (chordUnmuteChannel == 0) {
chordPlayer1.play(pendingChordPath);
chordMixerL.gain(0, 0.50f);
chordMixerR.gain(0, 0.50f);
} else {
chordPlayer2.play(pendingChordPath);
chordMixerL.gain(1, 0.50f);
chordMixerR.gain(1, 0.50f);
}
chordPlayer1Active = !chordPlayer1Active;
pendingChordDegree = -1;
}
void playLiveChord(int degree) {
livePlayer.play(cachedChordFile[degree]);
}
void generateFill(int stepsPerBar, int fillVariant) {
memset(fillKickPat, 0, sizeof(fillKickPat));
memset(fillSnarePat, 0, sizeof(fillSnarePat));
memset(fillHihatPat, 0, sizeof(fillHihatPat));
memset(fillPerc1Pat, 0, sizeof(fillPerc1Pat));
memset(fillPerc2Pat, 0, sizeof(fillPerc2Pat));
memset(fillPerc3Pat, 0, sizeof(fillPerc3Pat));
// fillVariant 0, 1, or 2 — 1/3 probability each (chosen by caller)
int type = (fillVariant >= 0 && fillVariant <= 2) ? fillVariant : 0;
int half = stepsPerBar / 2;
int quarter = stepsPerBar / 4;
int threeQ = half + quarter;
switch (type) {
case 0: // Snare build: kick anchors, snare ramps 8ths→16ths, resolves with kick
fillKickPat[0] = 1;
if (quarter > 0) fillKickPat[quarter] = 1;
fillKickPat[stepsPerBar - 1] = 1;
for (int i = half; i < threeQ; i += 2) fillSnarePat[i] = 1;
for (int i = threeQ; i < stepsPerBar - 1; i++) fillSnarePat[i] = 1;
for (int i = 0; i < half; i += 2) fillHihatPat[i] = 1;
fillPerc1Pat[0] = 1;
if (half > 0) fillPerc2Pat[half] = 1;
fillPerc3Pat[stepsPerBar - 1] = 1;
break;
case 1: // Gradual snare build: sparse→8ths→16ths
fillKickPat[0] = 1;
fillKickPat[half] = 1;
if (quarter > 0) for (int i = quarter; i < half; i += 4) fillSnarePat[i] = 1;
for (int i = half; i < threeQ; i += 2) fillSnarePat[i] = 1;
for (int i = threeQ; i < stepsPerBar; i++) fillSnarePat[i] = 1;
for (int i = 0; i < quarter; i += 2) fillHihatPat[i] = 1;
fillPerc1Pat[threeQ] = 1;
fillPerc2Pat[stepsPerBar - 1] = 1;
if (stepsPerBar > 4) fillPerc3Pat[stepsPerBar - 3] = 1;
break;
case 2: // Kick-snare walk: alternating with hihat glue, denser at end
for (int i = 0; i < stepsPerBar; i += 2) {
if (i % 4 == 0) fillKickPat[i] = 1;
else fillSnarePat[i] = 1;
}
for (int i = threeQ; i < stepsPerBar; i++) {
if (i % 2 == 0) fillKickPat[i] = 1;
else fillSnarePat[i] = 1;
}
for (int i = 0; i < half; i += 4) fillHihatPat[i] = 1;
fillPerc1Pat[0] = 1;
if (half > 0) fillPerc2Pat[half] = 1;
fillPerc3Pat[stepsPerBar - 1] = 1;
break;
case 3: // Syncopated: offbeat snares from halfway, kick anchors
fillKickPat[0] = 1;
if (quarter > 0) fillKickPat[quarter] = 1;
for (int i = half + 1; i < stepsPerBar; i += 2) fillSnarePat[i] = 1;
fillSnarePat[stepsPerBar - 1] = 1;
for (int i = 0; i < half; i += 2) fillHihatPat[i] = 1;
for (int i = half; i < stepsPerBar; i += 4) fillPerc1Pat[i] = 1;
fillPerc2Pat[stepsPerBar - 1] = 1;
if (threeQ < stepsPerBar) fillPerc3Pat[threeQ] = 1;
break;
case 4: // Descending: snare first half → kick second half, unison landing
for (int i = 0; i < half; i += 2) fillSnarePat[i] = 1;
for (int i = half; i < stepsPerBar; i += 2) fillKickPat[i] = 1;
fillKickPat[stepsPerBar - 1] = 1;
fillSnarePat[stepsPerBar - 1] = 1;
fillPerc1Pat[stepsPerBar - 1] = 1;
if (quarter > 0) fillPerc2Pat[quarter] = 1;
fillPerc3Pat[half] = 1;
break;
case 5: // Sparse/dramatic: space then tension into resolution
fillKickPat[0] = 1;
fillHihatPat[0] = 1;
if (threeQ < stepsPerBar) fillKickPat[threeQ] = 1;
if (quarter > 0) fillSnarePat[quarter] = 1;
if (stepsPerBar > 1) fillSnarePat[stepsPerBar - 2] = 1;
fillSnarePat[stepsPerBar - 1] = 1;
for (int i = half; i < threeQ; i += 2) fillHihatPat[i] = 1;
fillPerc1Pat[0] = 1;
fillPerc2Pat[0] = 1;
fillPerc1Pat[stepsPerBar - 1] = 1;
if (half < stepsPerBar) fillPerc3Pat[half] = 1;
break;
case 6: // House buildup: kick keeps 4otf, hats ramp 8ths→16ths, perc accents
for (int i = 0; i < stepsPerBar; i += 4) fillKickPat[i] = 1;
for (int i = 0; i < half; i += 2) fillHihatPat[i] = 1;
for (int i = half; i < stepsPerBar; i++) fillHihatPat[i] = 1;
fillSnarePat[stepsPerBar - 1] = 1;
if (stepsPerBar > 2) fillSnarePat[stepsPerBar - 3] = 1;
for (int i = 0; i < stepsPerBar; i += 2) fillPerc1Pat[i] = 1;
fillPerc2Pat[stepsPerBar - 1] = 1;
for (int i = 1; i < stepsPerBar; i += 4) fillPerc3Pat[i] = 1;
break;
case 7: // House drop: kick drops out, snare marches, perc fills gap
for (int i = half; i < stepsPerBar; i += 2) fillSnarePat[i] = 1;
for (int i = 0; i < half; i += 2) fillHihatPat[i] = 1;
for (int i = 0; i < half; i += 4) fillPerc3Pat[i] = 1;
fillKickPat[stepsPerBar - 1] = 1;
fillSnarePat[stepsPerBar - 1] = 1;
fillHihatPat[stepsPerBar - 1] = 1;
fillPerc1Pat[stepsPerBar - 1] = 1;
if (half > 0) fillPerc2Pat[half] = 1;
break;
}
}
void startFill() {
fillActive = true;
generateFill(STEPS_PER_BAR[selectedTimeSig], random(3)); // 1/3 each of 3 fill options
}
void endFill() {
fillActive = false;
}
void playDrums() {
int kick = 0, snare = 0, hihat = 0;
int p1 = 0, p2 = 0, p3 = 0;
if (fillActive) {
kick = fillKickPat[currentDrumStep];
snare = fillSnarePat[currentDrumStep];
hihat = fillHihatPat[currentDrumStep];
p1 = fillPerc1Pat[currentDrumStep];
p2 = fillPerc2Pat[currentDrumStep];
p3 = fillPerc3Pat[currentDrumStep];
} else {
kick = getPattern(selectedTimeSig, selectedGenre, drumPatternVariant, DRUM_KICK, currentDrumStep);
snare = getPattern(selectedTimeSig, selectedGenre, drumPatternVariant, DRUM_SNARE, currentDrumStep);
hihat = getPattern(selectedTimeSig, selectedGenre, drumPatternVariant, DRUM_HIHAT, currentDrumStep);
p1 = getPattern(selectedTimeSig, selectedGenre, drumPatternVariant, DRUM_PERC1, currentDrumStep);
p2 = getPattern(selectedTimeSig, selectedGenre, drumPatternVariant, DRUM_PERC2, currentDrumStep);
p3 = getPattern(selectedTimeSig, selectedGenre, drumPatternVariant, DRUM_PERC3, currentDrumStep);
}
// Gains with headroom so combined drums + chords + perc don't clip. Tight random range (≈2%) to avoid perceived volume swells.
if (kick) {
float kv = 0.24f * (0.98f + random(5) * 0.004f);
drumMixerL.gain(0, kv); drumMixerR.gain(0, kv);
kickMemory.play(kickData);
}
if (snare) {
float sv = 0.22f * (0.98f + random(5) * 0.004f);
drumMixerL.gain(1, sv); drumMixerR.gain(1, sv);
snareMemory.play(snareData);
}
if (hihat) {
float hv = 0.085f * (0.98f + random(5) * 0.004f);
if (selectedTimeSig == 0 && selectedGenre == 10) hv *= 0.72f; // House: hi-hat back
drumMixerL.gain(2, hv); drumMixerR.gain(2, hv);
hihatMemory.play(hihatData);
}
if (p1 && hasPerc1) {
float pv = 0.12f * (0.98f + random(5) * 0.004f);
if (selectedTimeSig == 0 && selectedGenre == 10) pv *= 0.72f; // House: shaker back
percMixerL.gain(0, pv); percMixerR.gain(0, pv);
perc1Memory.play(perc1Data);
}
if (p2 && hasPerc2) {
float pv = 0.12f * (0.98f + random(5) * 0.004f);
percMixerL.gain(1, pv); percMixerR.gain(1, pv);
perc2Memory.play(perc2Data);
}
if (p3 && hasPerc3) {
float pv = 0.10f * (0.98f + random(5) * 0.004f);
if (selectedTimeSig == 0 && selectedGenre == 10) pv *= 0.55f; // House: ride bell lower
percMixerL.gain(2, pv); percMixerR.gain(2, pv);
perc3Memory.play(perc3Data);
}
}
bool loadWavToMemory(const char* path, uint32_t* buffer, int maxWords) {
File f = SD.open(path);
if (!f) {
buffer[0] = 0;
Serial.print("WAV load FAILED: ");
Serial.println(path);
return false;
}
uint8_t riff[12];
if (f.read(riff, 12) != 12 || riff[0] != 'R' || riff[1] != 'I' || riff[2] != 'F' || riff[3] != 'F' ||
riff[8] != 'W' || riff[9] != 'A' || riff[10] != 'V' || riff[11] != 'E') {
buffer[0] = 0;
Serial.print("WAV not RIFF/WAVE: ");
Serial.println(path);
f.close();
return false;
}
int numCh = 1, bps = 16;
uint32_t dSz = 0;
bool foundFmt = false, foundData = false;
while (f.available() >= 8) {
uint8_t id[4];
uint32_t chunkLen;
if (f.read(id, 4) != 4 || f.read((uint8_t*)&chunkLen, 4) != 4) break;
if (id[0] == 'f' && id[1] == 'm' && id[2] == 't' && id[3] == ' ') {
uint8_t fmtBuf[16];
if (chunkLen < 16 || f.read(fmtBuf, 16) != 16) { f.close(); return false; }
if (chunkLen > 16) f.seek(f.position() + (chunkLen - 16));
numCh = fmtBuf[2] | (fmtBuf[3] << 8);
bps = fmtBuf[14] | (fmtBuf[15] << 8);
foundFmt = true;
} else if (id[0] == 'd' && id[1] == 'a' && id[2] == 't' && id[3] == 'a') {
dSz = chunkLen;
foundData = true;
break;
} else {
f.seek(f.position() + chunkLen);
}
}
if (!foundFmt || !foundData || numCh < 1 || numCh > 2 || bps != 16) {
buffer[0] = 0;
Serial.print("WAV bad fmt/data or unsupported: ");
Serial.println(path);
f.close();
return false;
}
int totalMono = (int)(dSz / (numCh * (bps / 8)));
int maxSamp = (maxWords - 1) * 2;
if (totalMono > maxSamp) totalMono = maxSamp;
int numWords = (totalMono + 1) / 2;
buffer[0] = numWords | (0x81 << 24);
for (int i = 0; i < totalMono; i++) {
int16_t sample = 0;
if (bps == 16) {
int16_t s;
f.read(&s, 2);
sample = s;
if (numCh == 2) {
int16_t r;
f.read(&r, 2);
sample = (sample / 2) + (r / 2);
}
}
int wordIdx = 1 + i / 2;
if (i % 2 == 0)
buffer[wordIdx] = (uint16_t)sample;
else
buffer[wordIdx] |= ((uint32_t)(uint16_t)sample) << 16;
}
if (totalMono % 2 == 1) {
int lastWord = 1 + totalMono / 2;
buffer[lastWord] &= 0x0000FFFF;
}
// Short fade-in at start (≈0.5 ms) to avoid crack/pop on trigger
const int fadeSamples = 22;
if (totalMono > fadeSamples) {
for (int i = 0; i < fadeSamples; i++) {
float g = (float)i / (float)(fadeSamples - 1);
int wordIdx = 1 + i / 2;
int16_t s;
if (i % 2 == 0)
s = (int16_t)(buffer[wordIdx] & 0xFFFF);
else
s = (int16_t)(buffer[wordIdx] >> 16);
s = (int16_t)((float)s * g);
if (i % 2 == 0)
buffer[wordIdx] = (buffer[wordIdx] & 0xFFFF0000) | ((uint16_t)s);
else
buffer[wordIdx] = (buffer[wordIdx] & 0x0000FFFF) | ((uint32_t)(uint16_t)s << 16);
}
}
f.close();
return true;
}
void cacheDrumPaths() {
buildDrumPath("kick").toCharArray(cachedKickPath, 32);
buildDrumPath("snare").toCharArray(cachedSnarePath, 32);
buildDrumPath("hihat").toCharArray(cachedHihatPath, 32);
buildDrumPath("perc1").toCharArray(cachedPerc1Path, 32);
buildDrumPath("perc2").toCharArray(cachedPerc2Path, 32);
buildDrumPath("perc3").toCharArray(cachedPerc3Path, 32);
loadWavToMemory(cachedKickPath, kickData, MAX_DRUM_WORDS);
loadWavToMemory(cachedSnarePath, snareData, MAX_DRUM_WORDS);
loadWavToMemory(cachedHihatPath, hihatData, MAX_DRUM_WORDS);
hasPerc1 = loadWavToMemory(cachedPerc1Path, perc1Data, MAX_PERC_WORDS);
hasPerc2 = loadWavToMemory(cachedPerc2Path, perc2Data, MAX_PERC_WORDS);
hasPerc3 = loadWavToMemory(cachedPerc3Path, perc3Data, MAX_PERC_WORDS);
}
void startDrums() {
if (!drumsOn) return;
cacheDrumPaths();
drumsRunning = true;
currentDrumStep = 0;
lastBarWasFill = false;
drumPatternVariant = random(3);
lastDrumStepTime = micros();
if (fillActive) endFill();
playDrums();
}
void stopDrums() {
drumsRunning = false;
if (fillActive) endFill();
metronomeSine.amplitude(0.0f);
drumMixerL.gain(3, 0.0f);
drumMixerR.gain(3, 0.0f);
metronomeOffTime = 0;
pendingChordDegree = -1;
}
void startPlaying() {
isPlaying = true;
currentChordStep = 0;
lastChordStepTime = millis();
liveOverride = false;
lastSequenceChord = -1;
pendingChordDegree = -1;
chordUnmuteChannel = -1;
metronomeSine.amplitude(0.0f);
drumMixerL.gain(3, 0.0f);
drumMixerR.gain(3, 0.0f);
metronomeOffTime = 0;
chordMixerL.gain(0, 0.34f); chordMixerR.gain(0, 0.34f);
chordMixerL.gain(1, 0.34f); chordMixerR.gain(1, 0.34f);
chordMixerL.gain(2, LIVE_CHORD_GAIN); chordMixerR.gain(2, LIVE_CHORD_GAIN);
if (drumsOn) {
if (!drumsRunning) {
cacheDrumPaths();
drumsRunning = true;
}
currentDrumStep = 0;
lastBarWasFill = false;
lastDrumStepTime = micros();
if (fillActive) endFill();
playDrums();
}
if (loopMode) {
loopBar = 0;
lastLoopPlayStep = -1;
loopStartTime = micros();
memset(loopEventTriggered, 0, sizeof(loopEventTriggered));
} else if (!liveMode) {
strcpy(pendingChordPath, cachedChordFile[chordSequence[0]]);
playSequenceChord(chordSequence[0]);
}
}
void setup() {
Serial.begin(9600);
delay(500);
Serial.println("=== TEENSY CHORD SEQUENCER v13 ===");
pinMode(TEMPO_PIN, INPUT);
pinMode(VOLUME_PIN, INPUT);
pinMode(ENC_CLK, INPUT_PULLUP);
pinMode(ENC_DT, INPUT_PULLUP);
pinMode(ENC_BTN, INPUT_PULLUP);
AudioMemory(192);
audioShield.enable();
audioShield.volume(1.0); // start at max; VOLUME_PIN can turn down
audioShield.dacVolumeRamp();
audioShield.unmuteHeadphone();
audioShield.unmuteLineout();
audioShield.lineOutLevel(13); // 13 = max line-out for PAM8302A/speaker
// Set chord pin modes AFTER AudioMemory so I2S pins are claimed first
for (int i = 0; i < 7; i++) {
pinMode(CHORD_PINS[i], INPUT_PULLUP);
Serial.print("Chord btn ");
Serial.print(i + 1);
Serial.print(" -> pin ");
Serial.println(CHORD_PINS[i]);
}
initDrumMixer();
memset(kickData, 0, sizeof(kickData));
memset(snareData, 0, sizeof(snareData));
memset(hihatData, 0, sizeof(hihatData));
memset(perc1Data, 0, sizeof(perc1Data));
memset(perc2Data, 0, sizeof(perc2Data));
memset(perc3Data, 0, sizeof(perc3Data));
initChordMixer();
initPercMixer();
initMasterMixer();
delay(500);
Serial.println("Trying BUILTIN_SDCARD...");
sdCardOK = SD.begin(BUILTIN_SDCARD);
if (!sdCardOK) {
Serial.println(" -> FAILED. Trying Audio Shield SD (pin 10)...");
sdCardOK = SD.begin(10);
}
Serial.print("SD card init: ");
Serial.println(sdCardOK ? "OK" : "FAILED - NO AUDIO WILL PLAY");
if (sdCardOK) {
// Check expected folder structure; report first missing path to Serial
const char* chordDirs[] = { "/chords/EP", "/chords/Rhodes", "/chords/Pad", "/chords/Grand Piano", "/chords/Strings" };
const char* drumDirs[] = { "rock", "jazz", "hiphop", "trap", "funk", "latin", "reggae", "bossa", "mathrock", "midwestemo", "house" };
bool ok = true;
for (int i = 0; ok && i < 5; i++) {
if (!SD.exists(chordDirs[i])) {
Serial.print("SD missing: ");
Serial.println(chordDirs[i]);
ok = false;
}
}
for (int i = 0; ok && i < 11; i++) {
String p = String("/drums/") + drumDirs[i];
if (!SD.exists(p.c_str())) {
Serial.print("SD missing: ");
Serial.println(p);
ok = false;
}
}
}
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.clearDisplay();
display.display();
randomSeed(analogRead(A9));
Serial.println("Setup complete.");
goToStartup();
}
// --- Timing sub-handlers: called from tickDrums() at specific moments (order preserved) ---
static void handleBarBoundary(bool* chordDue) {
if (fillActive) { endFill(); lastBarWasFill = true; }
else { lastBarWasFill = false; drumPatternVariant = random(3); }
if (!lastBarWasFill && !loopMode && currentState != STATE_LOOP_COUNTIN && currentState != STATE_LOOP_READY && (random(100) < FILL_OVERRIDE_PERCENT))
startFill();
if (isPlaying && currentState == STATE_PLAYING && !liveMode && !liveOverride && !loopMode) {
currentChordStep = (currentChordStep + 1) % 8;
*chordDue = true;
}
if (currentState == STATE_LOOP_RECORD)
recordBar++;
if (currentState == STATE_LOOP_COUNTIN) {
countInBar++;
if (countInBar >= 1) {
metronomeSine.amplitude(0.0f);
drumMixerL.gain(3, 0.0f);
drumMixerR.gain(3, 0.0f);
metronomeOffTime = 0;
lastMetronomeBeat = -1;
loopStartTime = micros();
recordBar = 0;
currentDrumStep = 0;
goToLoopRecord();
}
}
if (loopMode && currentState == STATE_PLAYING) {
loopBar = (loopBar + 1) % loopBars;
if (loopBar == 0) {
lastLoopPlayStep = -1;
if (!loopQuantized) {
loopStartTime = micros();
memset(loopEventTriggered, 0, sizeof(loopEventTriggered));
}
}
}
}
static void handleChordAdvance(int prevStep, bool* chordDue) {
if (chordDuration == 0 || loopMode || !isPlaying || currentState != STATE_PLAYING || liveMode || liveOverride) return;
if (chordDuration == 1) {
int halfPoint = STEPS_PER_BAR[selectedTimeSig] / 2;
if (currentDrumStep == halfPoint && prevStep == halfPoint - 1) {
currentChordStep = (currentChordStep + 1) % 8;
*chordDue = true;
}
} else if (chordDuration == 2) {
if (currentDrumStep % 4 == 0 && currentDrumStep != 0 && prevStep == currentDrumStep - 1) {
currentChordStep = (currentChordStep + 1) % 8;
*chordDue = true;
}
}
}
static void handleMetronomeTick() {
if (currentState != STATE_LOOP_COUNTIN || countInBar < 0 || currentDrumStep % 4 != 0) return;
int beat = currentDrumStep / 4;
if (beat == lastMetronomeBeat) return;
lastMetronomeBeat = beat;
float freq = (beat == 0) ? 1200.0f : 900.0f;
float vol = (beat == 0) ? 0.22f : 0.16f; // count-in metronome level
metronomeSine.frequency(freq);
metronomeSine.amplitude(1.0f);
drumMixerL.gain(3, vol);
drumMixerR.gain(3, vol);
metronomeOffTime = millis() + 40;
}
static void handleMetronomeOff() {
if (metronomeOffTime == 0 || millis() < metronomeOffTime) return;
metronomeSine.amplitude(0.0f);
drumMixerL.gain(3, 0.0f);
drumMixerR.gain(3, 0.0f);
metronomeOffTime = 0;
}
static void handleQuantizedLoopChords() {
if (!loopMode || !loopQuantized || currentState != STATE_PLAYING) return;
int globalStep = loopBar * STEPS_PER_BAR[selectedTimeSig] + currentDrumStep;
if (globalStep == lastLoopPlayStep) return;
lastLoopPlayStep = globalStep;
int degreeToPlay = -1;
for (int i = 0; i < loopEventCount; i++) {
if (loopEvents[i].quantStep == globalStep)
degreeToPlay = loopEvents[i].degree;
}
if (degreeToPlay >= 0)
maybePlaySequenceChord(degreeToPlay);
}
void tickDrums() {
bool needsTiming = (currentState == STATE_LOOP_COUNTIN || currentState == STATE_LOOP_RECORD);
if (!drumsRunning && !needsTiming) return;
drumInterval = 15000000UL / (unsigned long)bpm; // µs per 16th note
bool chordDue = false;
unsigned long now = micros();
int steps = 0;
while (now - lastDrumStepTime >= drumInterval && steps < 1) {
steps++;
lastDrumStepTime += drumInterval;
int prevStep = currentDrumStep;
currentDrumStep = (currentDrumStep + 1) % STEPS_PER_BAR[selectedTimeSig];
bool barBoundary = (currentDrumStep == 0 && prevStep > 0);
if (barBoundary)
handleBarBoundary(&chordDue);
handleChordAdvance(prevStep, &chordDue);
if (drumsRunning) playDrums();
handleMetronomeTick();
now = micros();
}
// If behind, advance by one interval only (avoids big jump that causes timing glitches)
if (now - lastDrumStepTime >= drumInterval) {
lastDrumStepTime += drumInterval;
}
handleMetronomeOff();
if (chordDue) {
if (selectedExt != pendingExt) { selectedExt = pendingExt; cacheChordPaths(); }
maybePlaySequenceChord(chordSequence[currentChordStep]);
}
handleQuantizedLoopChords();
}
// --- State transition helpers: set currentState + menuIndex (and any state-specific reset) in one place ---
static void goToStartup() { currentState = STATE_STARTUP; menuIndex = 0; }
static void goToInstrument() { currentState = STATE_INSTRUMENT; menuIndex = selectedInstrument; }
static void goToMainMenu() { currentState = STATE_MAIN_MENU; menuIndex = 0; }
static void goToTimeSig(int idx) { currentState = STATE_TIME_SIG; menuIndex = (idx >= 0) ? idx : selectedTimeSig; }
static void goToGenre(int idx) { currentState = STATE_GENRE; menuIndex = (idx >= 0) ? idx : selectedGenre; }
static void goToKey() { currentState = STATE_KEY; menuIndex = selectedKey; }
static void goToScaleMode(int idx) { currentState = STATE_SCALE_MODE; menuIndex = idx; }
static void goToExtension(int idx) { currentState = STATE_EXTENSION; menuIndex = (idx >= 0) ? idx : selectedExt; }
static void goToModeSelect(int idx){ currentState = STATE_MODE_SELECT; menuIndex = idx; }
static void goToChordDuration(int idx) { currentState = STATE_CHORD_DURATION; menuIndex = idx; }
static void goToSequenceEdit() {
currentState = STATE_SEQUENCE_EDIT;
menuIndex = 0;
assigningStep = 0;
lastSequenceChord = -1;
for (int i = 0; i < 8; i++) chordStepSet[i] = false;
}
static void goToPlaying() { currentState = STATE_PLAYING; menuIndex = 0; }
static void goToPaused() { currentState = STATE_PAUSED; menuIndex = 0; }
static void goToLoopLength() { currentState = STATE_LOOP_LENGTH; menuIndex = 0; }
static void goToLoopReady() { currentState = STATE_LOOP_READY; menuIndex = 0; }
static void goToLoopCountin() { currentState = STATE_LOOP_COUNTIN; }
static void goToLoopRecord() { currentState = STATE_LOOP_RECORD; }
// --- Mixer initialization: call from setup(); levels tuned for drums/chords/perc balance ---
static void initDrumMixer() {
// Kick, snare, hihat, metronome (ch 3). Metronome off at startup; tickDrums sets gains per hit.
drumMixerL.gain(0, 0.26); drumMixerR.gain(0, 0.26);
drumMixerL.gain(1, 0.24); drumMixerR.gain(1, 0.24);
drumMixerL.gain(2, 0.085); drumMixerR.gain(2, 0.085);
drumMixerL.gain(3, 0.0); drumMixerR.gain(3, 0.0);
metronomeSine.amplitude(0.0f);
}
static void initChordMixer() {
// Ch 0/1 = sequence players, ch 2 = live, ch 3 = unused (free for 4th voice).
chordMixerL.gain(0, 0.34); chordMixerR.gain(0, 0.34);
chordMixerL.gain(1, 0.34); chordMixerR.gain(1, 0.34);
chordMixerL.gain(2, LIVE_CHORD_GAIN); chordMixerR.gain(2, LIVE_CHORD_GAIN);
chordMixerL.gain(3, 0.0); chordMixerR.gain(3, 0.0);
}
static void initPercMixer() {
// Perc 1/2/3; ch 3 unused.
percMixerL.gain(0, 0.12); percMixerR.gain(0, 0.12);
percMixerL.gain(1, 0.12); percMixerR.gain(1, 0.12);
percMixerL.gain(2, 0.10); percMixerR.gain(2, 0.10);
percMixerL.gain(3, 0.0); percMixerR.gain(3, 0.0);
}
static void initMasterMixer() {
// Master: drums forward, chords slightly back, perc mid. Tune by ear when changing samples; volume pot trims level.
masterMixerL.gain(0, 0.82); masterMixerR.gain(0, 0.82); // drums
masterMixerL.gain(1, 0.44); masterMixerR.gain(1, 0.44); // chords
masterMixerL.gain(2, 0.58); masterMixerR.gain(2, 0.58); // perc
}
// Apply new BPM and rescale drum timing so current step position is preserved (e.g. for pot or tap-tempo).
void applyNewBPM(int newBpm) {
if (drumsRunning) {
unsigned long now_us = micros();
unsigned long oldInt = 15000000UL / (unsigned long)bpm;
unsigned long newInt = 15000000UL / (unsigned long)newBpm;
unsigned long elapsed = now_us - lastDrumStepTime;
if (elapsed < oldInt) {
float frac = (float)elapsed / (float)oldInt;
lastDrumStepTime = now_us - (unsigned long)(frac * (float)newInt);
}
}
bpm = newBpm;
}
void loop() {
finishChordTransition();
tickDrums();
if (pendingChordPlay >= 0 && (millis() - pendingChordTime) >= CHORD_BUTTON_SETTLE_MS) {
playLiveChord(pendingChordPlay);
pendingChordPlay = -1;
}
// Chord timing fallback -- only used when drums are OFF
if (isPlaying && currentState == STATE_PLAYING && !drumsRunning && !loopMode) {
unsigned long barMs = (60000UL / (unsigned long)bpm) * (unsigned long)BEATS_PER_BAR[selectedTimeSig];
if (chordDuration == 2) chordInterval = barMs / BEATS_PER_BAR[selectedTimeSig];
else if (chordDuration == 1) chordInterval = barMs / 2;
else chordInterval = barMs;
unsigned long now = millis();
if (now - lastChordStepTime >= chordInterval) {
lastChordStepTime += chordInterval;
currentChordStep = (currentChordStep + 1) % 8;
if (selectedExt != pendingExt) { selectedExt = pendingExt; cacheChordPaths(); }
if (!liveMode && !liveOverride) maybePlaySequenceChord(chordSequence[currentChordStep]);
}
}
// Raw (unquantized) loop playback
if (loopMode && !loopQuantized && currentState == STATE_PLAYING && loopEventCount > 0) {
unsigned long di = 15000000UL / (unsigned long)bpm;
unsigned long loopDuration = (unsigned long)loopBars * STEPS_PER_BAR[selectedTimeSig] * di;
unsigned long elapsed = micros() - loopStartTime;
if (elapsed >= loopDuration) {
loopStartTime += loopDuration;
memset(loopEventTriggered, 0, sizeof(loopEventTriggered));
elapsed = micros() - loopStartTime;
}
for (int i = 0; i < loopEventCount; i++) {
if (!loopEventTriggered[i] && elapsed >= loopEvents[i].offset) {
maybePlaySequenceChord(loopEvents[i].degree);
loopEventTriggered[i] = true;
}
}
}
// --- Throttled analog reads: BPM + volume every 40ms instead of every loop ---
unsigned long analogNow = millis();
if (analogNow - lastAnalogTime >= ANALOG_MS) {
lastAnalogTime = analogNow;
int rawBpm = map(analogRead(TEMPO_PIN), 0, 1023, 60, 180);
bpmSum -= bpmReadings[bpmReadIdx];
bpmReadings[bpmReadIdx] = rawBpm;
bpmSum += rawBpm;
bpmReadIdx = (bpmReadIdx + 1) % BPM_SAMPLES;
int smoothed = (int)(bpmSum / BPM_SAMPLES);
if (abs(smoothed - bpm) >= 2)
applyNewBPM(smoothed);
int volRaw = analogRead(VOLUME_PIN);
if (abs(volRaw - lastVolRaw) > 12) {
lastVolRaw = volRaw;
float vol = max(0.05f, volRaw / 1023.0f);
audioShield.volume(vol);
}
}
// --- Read inputs ---
int encDelta = readEncoder();
bool clicked = readButtonClick();
bool held = readButtonHold();
int chordPressed= readChordButton();
// --- State machine (display & UI) ---
switch (currentState) {
case STATE_STARTUP: {
static bool splashDone = false;
if (!splashDone) {
showSplashScreen();
splashDone = true;
}
if (clicked || encDelta != 0) {
if (sdCardOK) playStartupSound();
splashDone = false;
delay(1500);
goToInstrument();
}
break;
}
case STATE_INSTRUMENT: {
showScreen("Instrument", INSTRUMENT_NAMES, 5, menuIndex);
scrollMenu(encDelta, 5);
if (clicked) {
selectedInstrument = menuIndex;
cacheChordPaths(); // refresh paths for new instrument (EP or Rhodes)
goToMainMenu();
}
break;
}
case STATE_MAIN_MENU: {
// Instrument first so it's easy to find and change
static const char* mainOpts[4] = {"Instrument", "Chords", "Drums", "Play"};
showScreen("Menu", mainOpts, 4, menuIndex);
scrollMenu(encDelta, 4);
if (clicked) {
returnToMain = true;
if (menuIndex == 0) {
goToInstrument();
} else if (menuIndex == 1) {
goToKey();
} else if (menuIndex == 2) {
goToTimeSig(-1);
} else {
goToModeSelect(0);
}
}
break;
}
case STATE_TIME_SIG:
showScreen("Time Signature", TIME_SIG_NAMES, 3, menuIndex);
scrollMenu(encDelta, 3);
if (clicked) {
selectedTimeSig = menuIndex;
selectedGenre = 0;
goToGenre(0);
}
break;
case STATE_GENRE: {
const char** gl = (selectedTimeSig == 0) ? GENRES_4_4 : (selectedTimeSig == 1) ? GENRES_3_4 : GENRES_5_4;
int gc = GENRE_COUNT[selectedTimeSig];
const char* genreList[12];
for (int i = 0; i < gc; i++) genreList[i] = gl[i];
genreList[gc] = "No Drums";
int totalOpts = gc + 1;
showScreen("Genre", genreList, totalOpts, menuIndex);
scrollMenu(encDelta, totalOpts);
if (clicked) {
if (menuIndex == gc) {
drumsOn = false;
if (drumsRunning) stopDrums();
} else {
drumsOn = true;
selectedGenre = menuIndex;
}
if (returnToMain) {
goToMainMenu();
} else {
if (drumsOn) startDrums();
startPlaying();
goToPlaying();
}
}
break;
}
case STATE_KEY:
showScreen("Key", KEY_NAMES, 12, menuIndex);
scrollMenu(encDelta, 12);
if (clicked) { selectedKey = menuIndex; goToScaleMode(0); }
break;
case STATE_SCALE_MODE:
showScreen("Scale", SCALE_MODE_NAMES, NUM_MODES, menuIndex);
scrollMenu(encDelta, NUM_MODES);
if (clicked) { selectedScaleMode = menuIndex; goToExtension(0); }
break;
case STATE_EXTENSION:
showScreen("Extension", EXT_NAMES, 3, menuIndex);
scrollMenu(encDelta, 3);
if (clicked) {
selectedExt = menuIndex;
pendingExt = selectedExt;
cacheChordPaths();
if (returnToMain) {
goToMainMenu();
} else {
if (drumsOn) startDrums();
startPlaying();
goToPlaying();
}
}
break;
case STATE_MODE_SELECT:
showScreen("Play Mode", MODE_NAMES, 3, menuIndex);
scrollMenu(encDelta, 3);
if (clicked) {
cacheChordPaths();
if (menuIndex == 0) {
liveMode = true; loopMode = false;
startPlaying(); goToPlaying();
} else if (menuIndex == 1) {
liveMode = false; loopMode = false;
for (int i = 0; i < 8; i++) chordStepSet[i] = false;
goToChordDuration(0);
} else {
liveMode = false; loopMode = true;
loopEventCount = 0;
recordBar = 0;
goToLoopLength();
}
}
break;
case STATE_CHORD_DURATION: {
static const char* durOpts[3] = {"Whole Note", "Half Note", "Quarter Note"};
showScreen("Chord Length", durOpts, 3, menuIndex);
scrollMenu(encDelta, 3);
if (clicked) {
chordDuration = menuIndex;
if (chordStepSet[0]) {
startPlaying();
goToPlaying();
} else {
goToSequenceEdit();
if (drumsOn && !drumsRunning) startDrums(); // reference groove while picking chords
}
}
break;
}
case STATE_SEQUENCE_EDIT:
showSequenceEditScreen();
if (encDelta != 0) {
assigningStep = constrain(assigningStep + encDelta, 0, 7);
}
if (chordPressed >= 0) {
chordSequence[assigningStep] = chordPressed;
chordStepSet[assigningStep] = true;
pendingChordPlay = chordPressed;
pendingChordTime = millis();
if (assigningStep < 7) {
assigningStep++;
}
}
// Encoder click = done editing, fill unset steps with degree 0
if (clicked || held) {
for (int i = 0; i < 8; i++) {
if (!chordStepSet[i]) { chordSequence[i] = 0; chordStepSet[i] = true; }
}
// Reset chord playback so sequence starts clean (no live preview bleed or stale state)
livePlayer.stop();
chordPlayer1.stop();
chordPlayer2.stop();
chordMixerL.gain(0, 0.34f);
chordMixerR.gain(0, 0.34f);
chordMixerL.gain(1, 0.34f);
chordMixerR.gain(1, 0.34f);
pendingChordDegree = -1;
chordUnmuteChannel = -1;
chordPlayer1Active = true;
if (drumsOn && !drumsRunning) startDrums();
startPlaying();
goToPlaying();
}
break;
case STATE_PLAYING:
showPlayScreen();
scrollMenu(encDelta, loopMode ? 4 : 2);
if (chordPressed >= 0) {
liveChordDegree = chordPressed;
pendingChordPlay = chordPressed;
pendingChordTime = millis();
}
if (held) { isPlaying = false; liveOverride = false; stopDrums(); goToPaused(); }
if (clicked) {
if (loopMode) {
if (menuIndex == 0) { isPlaying = false; liveOverride = false; stopDrums(); goToPaused(); }
else if (menuIndex == 1) {
loopQuantized = !loopQuantized;
if (!loopQuantized) {
loopStartTime = micros();
memset(loopEventTriggered, 0, sizeof(loopEventTriggered));
}
lastLoopPlayStep = -1;
}
else if (menuIndex == 2) {
loopEventCount = 0;
recordBar = 0;
goToLoopLength();
}
else { isPlaying = false; liveOverride = false; stopDrums(); goToMainMenu(); }
} else {
if (menuIndex == 0) { isPlaying = false; liveOverride = false; stopDrums(); goToPaused(); }
else { isPlaying = false; liveOverride = false; stopDrums(); goToMainMenu(); }
}
}
break;
case STATE_LOOP_LENGTH: {
static const char* loopLenOpts[2] = {"4 Bars", "8 Bars"};
showScreen("Loop Length", loopLenOpts, 2, menuIndex);
scrollMenu(encDelta, 2);
if (clicked) {
loopBars = (menuIndex == 0) ? 4 : 8;
if (!drumsRunning) startDrums();
goToLoopReady();
}
break;
}
case STATE_LOOP_READY: {
unsigned long now = millis();
if (now - lastDisplayTime >= DISPLAY_MS) {
lastDisplayTime = now;
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print("Loop Record");
display.drawLine(0, 10, 128, 10, SSD1306_WHITE);
display.setTextSize(2);
display.setCursor(10, 20);
display.print("READY");
display.setTextSize(1);
display.setCursor(0, 48);
display.print("BPM:");
display.print(bpm);
display.print(" ");
display.print(TIME_SIG_NAMES[selectedTimeSig]);
display.setCursor(0, 56);
display.print("Click = Record");
display.display();
}
if (chordPressed >= 0) {
pendingChordPlay = chordPressed;
pendingChordTime = millis();
}
if (clicked) {
if (fillActive) endFill();
lastBarWasFill = false;
countInBar = -1;
lastMetronomeBeat = -1;
goToLoopCountin();
}
break;
}
case STATE_LOOP_COUNTIN: {
unsigned long now = millis();
if (countInBar >= 0 && now - lastDisplayTime >= 50) {
int bpb = BEATS_PER_BAR[selectedTimeSig];
int currentBeat = currentDrumStep / 4 + 1;
if (currentBeat > bpb) currentBeat = bpb;
lastDisplayTime = now;
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print("Count In ");
display.print(TIME_SIG_NAMES[selectedTimeSig]);
display.drawLine(0, 10, 128, 10, SSD1306_WHITE);
display.setTextSize(4);
display.setCursor(20, 18);
display.print(currentBeat);
display.setTextSize(2);
display.setCursor(80, 28);
display.print("/");
display.print(bpb);
display.display();
}
break;
}
case STATE_LOOP_RECORD: {
unsigned long now = millis();
if (now - lastDisplayTime >= DISPLAY_MS) {
lastDisplayTime = now;
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print("REC ");
display.print(min(recordBar + 1, loopBars));
display.print("/");
display.print(loopBars);
display.setTextSize(1);
display.drawLine(0, 18, 128, 18, SSD1306_WHITE);
display.setCursor(0, 24);
display.print("BPM:");
display.print(bpm);
display.print(" ");
display.print(TIME_SIG_NAMES[selectedTimeSig]);
int recBeat = currentDrumStep / 4 + 1;
int recBpb = BEATS_PER_BAR[selectedTimeSig];
if (recBeat > recBpb) recBeat = recBpb;
display.print(" Beat ");
display.print(recBeat);
display.setCursor(0, 40);
if (loopEventCount > 0)
display.print(buildChordName(loopEvents[loopEventCount - 1].degree));
else
display.print("Play chords...");
display.setCursor(0, 54);
display.print(loopEventCount);
display.print(" events");
if (loopEventCount >= MAX_LOOP_EVENTS)
display.print(" FULL!");
display.display();
}
if (chordPressed >= 0 && loopEventCount < MAX_LOOP_EVENTS) {
unsigned long off = micros() - loopStartTime;
unsigned long di = 15000000UL / (unsigned long)bpm;
int nearest = (int)((off + di / 2) / di);
int totalSteps = loopBars * STEPS_PER_BAR[selectedTimeSig];
if (nearest >= totalSteps) nearest = totalSteps - 1;
// Capture early hit: only at bar boundaries. If user hits on "4" or just before the 1, snap to downbeat
int stepsPerBar = STEPS_PER_BAR[selectedTimeSig];
const unsigned long earlyWindowUs = 400000UL; // 400 ms before bar line (~most of beat 4)
bool snappedToDownbeat = false;
if (nearest < totalSteps - 1) {
int nextStep = nearest + 1;
if (nextStep % stepsPerBar == 0) { // next step is a bar line
unsigned long nextStepStart = (unsigned long)nextStep * di;
if (off >= nextStepStart - earlyWindowUs) {
nearest = nextStep;
snappedToDownbeat = true;
}
}
}
loopEvents[loopEventCount].degree = chordPressed;
loopEvents[loopEventCount].offset = off;
loopEvents[loopEventCount].quantStep = nearest;
loopEventCount++;
if (snappedToDownbeat) {
scheduledChordDegree = chordPressed;
scheduledChordTime = loopStartTime + (unsigned long)nearest * di;
} else {
pendingChordPlay = chordPressed;
pendingChordTime = millis();
}
}
if (scheduledChordDegree >= 0 && micros() >= scheduledChordTime) {
playLiveChord(scheduledChordDegree);
scheduledChordDegree = -1;
}
if (recordBar >= loopBars) {
scheduledChordDegree = -1;
loopQuantized = true;
startPlaying();
goToPlaying();
}
break;
}
case STATE_PAUSED: {
const char* pauseOpts[12];
int pauseIdx[12];
int pauseCount = 0;
pauseOpts[pauseCount] = "> Resume"; pauseIdx[pauseCount++] = 11;
pauseOpts[pauseCount] = "Drums on/off"; pauseIdx[pauseCount++] = 0;
pauseOpts[pauseCount] = "Time Sig"; pauseIdx[pauseCount++] = 1;
if (drumsOn) { pauseOpts[pauseCount] = "Genre"; pauseIdx[pauseCount++] = 2; }
pauseOpts[pauseCount] = "Key"; pauseIdx[pauseCount++] = 3;
pauseOpts[pauseCount] = "Scale"; pauseIdx[pauseCount++] = 4;
pauseOpts[pauseCount] = "Extension"; pauseIdx[pauseCount++] = 5;
pauseOpts[pauseCount] = "Mode"; pauseIdx[pauseCount++] = 6;
pauseOpts[pauseCount] = "Instrument"; pauseIdx[pauseCount++] = 12;
if (!liveMode && !loopMode) { pauseOpts[pauseCount] = "Edit Sequence"; pauseIdx[pauseCount++] = 7; }
if (!liveMode && !loopMode) { pauseOpts[pauseCount] = "Chord Length"; pauseIdx[pauseCount++] = 8; }
if (loopMode) {
pauseOpts[pauseCount] = loopQuantized ? "Loop: On Grid" : "Loop: Raw";
pauseIdx[pauseCount++] = 10;
pauseOpts[pauseCount] = "Re-record Loop"; pauseIdx[pauseCount++] = 9;
}
showScreen("Paused", pauseOpts, pauseCount, menuIndex);
scrollMenu(encDelta, pauseCount);
if (clicked) {
returnToMain = false;
int action = pauseIdx[menuIndex];
switch (action) {
case 0: drumsOn = !drumsOn; if (drumsOn) goToTimeSig(0); break;
case 1: goToTimeSig(-1); break;
case 2: goToGenre(-1); break;
case 3: goToKey(); break;
case 4: goToScaleMode(selectedScaleMode); break;
case 5: goToExtension(-1); break;
case 6: goToModeSelect(loopMode ? 2 : (liveMode ? 0 : 1)); break;
case 12: goToInstrument(); break;
case 7:
goToSequenceEdit();
if (drumsOn && !drumsRunning) startDrums(); // reference groove while picking chords
break;
case 8:
goToChordDuration(chordDuration);
break;
case 9:
loopEventCount = 0;
recordBar = 0;
if (!drumsRunning) startDrums();
goToLoopReady();
break;
case 10:
loopQuantized = !loopQuantized;
if (!loopQuantized) {
loopStartTime = micros();
memset(loopEventTriggered, 0, sizeof(loopEventTriggered));
}
lastLoopPlayStep = -1;
if (!drumsRunning) startDrums(); startPlaying();
goToPlaying();
break;
case 11:
if (drumsOn && !drumsRunning) startDrums();
startPlaying();
goToPlaying();
break;
}
}
if (held) { if (drumsOn && !drumsRunning) startDrums(); startPlaying(); goToPlaying(); }
break;
}
}
}