/*
ESP32 Sound Board “Brain” — ESP-IDF I2S version (no <I2S.h> dependency)
- 12 debounced buttons triggering short audio beeps via I2S amp (e.g., MAX98357A)
- 16x2 I2C LCD status display
- Notes:
* Buttons to GND (active LOW). Use external 10k pull-ups on GPIO35/36/39.
* Bootstrap pins (0/2/12/15) must idle at safe levels; don’t hold buttons during reset.
* LCD moved to SDA=22, SCL=23 (since your map uses GPIO21 for a button).
*/
#include <Arduino.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include "driver/i2s.h"
#include <math.h>
// ------------------------- User-configurable: Verify these -------------------------
const int BUTTON_PINS[12] = {
12, // Button 1
13, // Button 2
15, // Button 3 (BOOT-strap pin)
2, // Button 4 (assumed; BOOT-strap pin)
17, // Button 5
21, // Button 6 (default I2C SDA; we're moving I2C off this to 22/23)
35, // Button 7 (input-only; needs external pull-up)
36, // Button 8 (input-only; needs external pull-up)
39, // Button 9 (input-only; needs external pull-up)
0, // Button 10 (assumed; BOOT-strap pin)
3, // Button 11 (UART0 RX)
4 // Button 12
};
// I2C pins and LCD
const int I2C_SDA = 22;
const int I2C_SCL = 23;
const uint8_t LCD_ADDR = 0x27; // try 0x3F if blank
LiquidCrystal_I2C lcd(LCD_ADDR, 16, 2);
// I2S pins for MAX98357A-style amplifier
struct {
int BCLK = 26; // Bit clock
int LRCK = 25; // Word select (LRCLK/WS)
int DIN = 27; // Data out to amp
} I2S_PINS;
// Audio settings
const uint32_t SAMPLE_RATE = 22050;
const int BITS_PER_SAMPLE = 16;
const int BEEP_MS = 120;
const float BEEP_FREQS[12] = {
440.0, 466.16, 493.88, 523.25,
554.37, 587.33, 622.25, 659.25,
698.46, 739.99, 783.99, 830.61
};
// Debounce
const unsigned long DEBOUNCE_MS = 25;
// ------------------------- Internals -------------------------
unsigned long lastChange[12] = {0};
bool lastStableState[12];
bool lastReadState[12];
bool isInputOnly(int pin) { return (pin >= 34 && pin <= 39); }
// ------------------------- I2S (ESP-IDF driver) -------------------------
bool setupI2S() {
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0)
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
#else
.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),
#endif
.intr_alloc_flags = 0,
.dma_buf_count = 6,
.dma_buf_len = 256,
.use_apll = false,
.tx_desc_auto_clear = true,
.fixed_mclk = 0
};
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_PINS.BCLK,
.ws_io_num = I2S_PINS.LRCK,
.data_out_num = I2S_PINS.DIN,
.data_in_num = I2S_PIN_NO_CHANGE
};
if (i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL) != ESP_OK) return false;
if (i2s_set_pin(I2S_NUM_0, &pin_config) != ESP_OK) return false;
if (i2s_set_clk(I2S_NUM_0, SAMPLE_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_STEREO) != ESP_OK) return false;
return true;
}
// Generate and play a short sine beep on both channels
void playBeep(float freqHz, int ms) {
const uint32_t totalSamples = (uint32_t)((SAMPLE_RATE * ms) / 1000.0f);
const float twoPiFOverFs = 2.0f * 3.14159265f * freqHz / SAMPLE_RATE;
for (uint32_t n = 0; n < totalSamples; n++) {
float s = sinf(twoPiFOverFs * n);
int16_t sample = (int16_t)(s * 3000); // modest amplitude
// Stereo frame: Left then Right (16-bit each)
size_t bw;
i2s_write(I2S_NUM_0, (const void*)&sample, sizeof(sample), &bw, portMAX_DELAY);
i2s_write(I2S_NUM_0, (const void*)&sample, sizeof(sample), &bw, portMAX_DELAY);
}
}
// ------------------------- Buttons/LCD -------------------------
void setupButtons() {
for (int i = 0; i < 12; i++) {
int pin = BUTTON_PINS[i];
if (isInputOnly(pin)) {
pinMode(pin, INPUT); // needs external pull-up
} else {
pinMode(pin, INPUT_PULLUP);
}
bool r = digitalRead(pin);
lastStableState[i] = r;
lastReadState[i] = r;
lastChange[i] = millis();
}
}
bool edgePressed(int idx, bool newStable) {
// Active LOW: pressed when HIGH -> LOW
return (lastStableState[idx] == HIGH && newStable == LOW);
}
void reportButtonOnLCD(int idx) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Button ");
lcd.print(idx + 1);
lcd.print(" pressed");
lcd.setCursor(0, 1);
lcd.print("Beep ");
lcd.print((int)BEEP_FREQS[idx]);
lcd.print(" Hz");
}
// ------------------------- Arduino lifecycle -------------------------
void setup() {
Serial.begin(115200);
delay(100);
Wire.begin(I2C_SDA, I2C_SCL);
lcd.init();
lcd.backlight();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Sound Board v1");
lcd.setCursor(0, 1);
lcd.print("Init...");
setupButtons();
if (!setupI2S()) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("I2S init failed");
lcd.setCursor(0, 1);
lcd.print("Check wiring/pins");
} else {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Ready...");
}
}
void loop() {
unsigned long now = millis();
for (int i = 0; i < 12; i++) {
int pin = BUTTON_PINS[i];
bool r = digitalRead(pin);
if (r != lastReadState[i]) {
lastReadState[i] = r;
lastChange[i] = now;
}
if ((now - lastChange[i]) >= DEBOUNCE_MS) {
if (r != lastStableState[i]) {
bool wasPressed = edgePressed(i, r);
lastStableState[i] = r;
if (wasPressed) {
reportButtonOnLCD(i);
playBeep(BEEP_FREQS[i], BEEP_MS);
delay(60);
}
}
}
}
}