Community project

3D Loud Sound Generator

ESP32
Photo of 3D Loud Sound Generator
Generated with AI

Rob Fia

Published October 24, 2026

This project builds a customizable alarm clock with a loud speaker output, touchscreen interface, and real-time clock. The ESP32 drives an ILI9341 touchscreen for settings and display, uses a DS3231 real-time clock module to keep accurate time, and outputs sound through a MAX98357A I2S amplifier connected to an 8Ω speaker. Push buttons provide tactile control alongside the touch interface.

The guide includes a complete wiring diagram showing all connections between the ESP32, TFT display, audio amplifier, RTC module, and buttons, a full parts list with component specifications, and firmware with support for multiple alarm sounds, volume control, and persistent settings storage. Assembly steps walk through safe connection procedures and clock initialization to get the alarm functioning.

Wiring diagram

Wiring diagram for 3D Loud Sound Generator

Gather all the parts

QtyComponent
1

ILI9341 TFT Touchscreen

240x320 SPI TFT display using the ILI9341 LCD controller with an XPT2046 resistive touch controller sharing the SPI bus

1

Resistor

220 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Adafruit MAX98357A breakout powered at 5 V

VIN accepts 2.5–5.5 V per manufacturer; inputs accept 3.3 V logic. USB 5 V is a supply input, not an output from this module.

1

8Ω Speaker

8 ohm, 3 W

Generic small 8Ω 0.5-3W loudspeaker (~28mm typical). Pair with an I2S amp (MAX98357A) or class-D amp (TPA3116D2) for usable volume; do not drive directly from a GPIO pin. Audio output for music/voice playback.

1

Adafruit DS3231 breakout powered at 3.3 V

Manufacturer supply range 2.3–5.5 V. Powered at 3.3 V here; onboard SDA/SCL pull-ups go to VIN, so bus logic is 3.3 V.

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

Assemble it in 5 steps

1. Choose the matching parts

Use an ESP32 DevKit v1, a 3.3 V ILI9341 SPI screen with the listed pins, an Adafruit MAX98357A breakout, an 8 ohm 3 W speaker, the Adafruit DS3231 breakout, three buttons and one 220 ohm resistor. This build uses buttons, not touch input.

2. Connect the screen with power disconnected

Connect VCC to 3V3 and GND to GND. SCK goes to GPIO18 (clock), MOSI to GPIO23 (display data), MISO to GPIO19 (return data), TFT_CS to GPIO4 (select), TFT_DC to GPIO16 (commands), and TFT_RST to GPIO17 (reset). Tie TOUCH_CS to 3V3 to leave touch inactive. Connect TFT_BL to 3V3 through the 220 ohm resistor.

  • Use a screen rated for 3.3 V logic. Never connect its signal pins to 5 V.

3. Connect the louder sound output

Connect amplifier VIN to the board 5V pin and GND to GND. BCLK goes to GPIO26 (audio timing), LRC to GPIO25 (sample timing), and DIN to GPIO27 (sound data). Connect speaker POS only to SPK+ and NEG only to SPK-. Leave SD and GAIN unconnected on the Adafruit breakout for their default operation.

  • Neither speaker terminal may connect to ground. Start at volume 1 and keep the speaker away from ears. Use an 8 ohm 3 W speaker
  • not a 0.5 W version.

4. Connect the clock and buttons

Power the Adafruit DS3231 VIN from 3V3 so its signal pull-ups are safe. Connect GND to GND, SDA to GPIO21 (clock data), and SCL to GPIO22 (clock timing). Connect MODE SIGNAL to GPIO32, PLUS SIGNAL to GPIO33, and TEST SIGNAL to GPIO13. Connect each button GND to GND. On four-legged buttons use contacts that join only when pressed.

5. Power and set the clock

Use one regulated 5 V USB supply rated at least 1 A through the ESP32 USB socket. Do not add a second power source. Press MODE to choose the clock hour, clock minute, alarm hour, alarm minute, alarm enabled, sound or volume. PLUS increments the selected setting. TEST plays the selected sound; any button stops it. Alarms stop automatically after 30 seconds. Return MODE to Normal when finished. Alarm settings are saved; without a backup battery the time must be reset after power loss. The initial time comes from the firmware build and must be corrected to local time.

  • Mount the speaker behind a grille in a rigid enclosure for fuller sound. Actual loudness depends on the speaker and enclosure; it cannot be compared reliably with the pictured unit.

Review all connections

1. Connections between "screen" and "ESP32"

FunctionscreenESP32
powerVCC3V3
groundGNDGND
spiMOSIGPIO 23
spiMISOGPIO 19
spiSCKGPIO 18
digitalTFT_CSGPIO 4
digitalTFT_DCGPIO 16
digitalTFT_RSTGPIO 17
powerTOUCH_CS3V3
digitalTFT_BL → Resistor P2EXT

2. Connections between "backlight_resistor" and "ESP32"

Functionbacklight_resistorESP32
powerP13V3

3. Connections between "amp" and "ESP32"

FunctionampESP32
powerVIN5V
groundGNDGND
dataBCLKGPIO 26
dataLRCGPIO 25
dataDINGPIO 27
dataSPK+ → 8Ω Speaker POSEXT
dataSPK- → 8Ω Speaker NEGEXT

4. Connections between "rtc" and "ESP32"

FunctionrtcESP32
powerVIN3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22

5. Connections between "mode" and "ESP32"

FunctionmodeESP32
digitalSIGNALGPIO 32
groundGNDGND

6. Connections between "plus" and "ESP32"

FunctionplusESP32
digitalSIGNALGPIO 33
groundGNDGND

7. Connections between "test" and "ESP32"

FunctiontestESP32
digitalSIGNALGPIO 13
groundGNDGND

Deploy the firmware

#include <Arduino.h>
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <RTClib.h>
#include <Preferences.h>
#include <driver/i2s.h>
#include <math.h>

Adafruit_ILI9341 tft(4,16,17);
RTC_DS3231 rtc;
Preferences prefs;
const int buttons[]={32,33,13};
const char* sounds[]={"Digital bell","Space pulse","Rising siren","Triple beep"};
const char* fields[]={"Normal","Clock hour","Clock minute","Alarm hour","Alarm minute","Alarm enabled","Sound","Volume"};
int field=0, ah=7, am=0, soundId=0, volume=2;
bool enabled=false, rtcOK=false, audioOK=false, ringing=false, dirty=true;
uint32_t ringStart=0, sampleIndex=0, lastPoll=0;
int lastMinute=-1; uint32_t lastAlarm=0;
bool stable[3]={true,true,true}, rawOld[3]={true,true,true};
uint32_t changed[3]={0,0,0};
uint32_t fallbackEpoch=0, fallbackMillis=0;
DateTime clockNow(){return rtcOK?rtc.now():DateTime(fallbackEpoch+(millis()-fallbackMillis)/1000);}
void setClock(DateTime d){if(rtcOK)rtc.adjust(d); fallbackEpoch=d.unixtime();fallbackMillis=millis();}
void save(){prefs.putInt("ah",ah);prefs.putInt("am",am);prefs.putInt("sound",soundId);prefs.putInt("vol",volume);prefs.putBool("on",enabled);}
void startSound(){if(!audioOK)return;ringing=true;ringStart=millis();sampleIndex=0;dirty=true;}
void stopSound(){ringing=false;dirty=true;}
void digit(int x,int y,int n){
 const uint8_t masks[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};
 const int sx[]={8,43,43,8,0,0,8}, sy[]={0,8,55,100,55,8,50};
 for(int s=0;s<7;s++){
  int w=(s==0||s==3||s==6)?35:9,h=(s==0||s==3||s==6)?9:43;
  if(masks[n]&(1<<s)){
   for(int d=6;d>0;d--)tft.fillRoundRect(x+sx[s]+d,y+sy[s]+d,w,h,3,0x0190);
   tft.fillRoundRect(x+sx[s],y+sy[s],w,h,3,ILI9341_CYAN);
   tft.drawFastHLine(x+sx[s]+2,y+sy[s]+1,w-4,ILI9341_WHITE);
  }else tft.fillRoundRect(x+sx[s],y+sy[s],w,h,3,0x1082);
 }
}
void draw(DateTime now){
 tft.fillScreen(0x0841);tft.setTextWrap(false);tft.setTextSize(2);tft.setTextColor(ILI9341_CYAN);
 tft.setCursor(12,8);tft.print("DIMENSION / DIGITAL");
 digit(17,43,now.hour()/10);digit(78,43,now.hour()%10);
 digit(175,43,now.minute()/10);digit(236,43,now.minute()%10);
 tft.fillCircle(155,78,4,ILI9341_WHITE);tft.fillCircle(155,120,4,ILI9341_WHITE);
 char line[48];snprintf(line,sizeof(line),"Alarm %02d:%02d %s  Vol %d/5",ah,am,enabled?"ON":"OFF",volume);
 tft.setTextSize(1);tft.setTextColor(ILI9341_WHITE);tft.setCursor(12,171);tft.print(line);
 tft.setCursor(12,187);tft.print(sounds[soundId]);
 tft.setTextColor(ILI9341_YELLOW);tft.setCursor(12,203);
 if(!rtcOK)tft.print("RTC missing: check wires");
 else if(!audioOK)tft.print("Audio initialization failed");
 else if(ringing)tft.print("ALARM! Press any button to stop");
 else {tft.print("Edit: ");tft.print(fields[field]);}
 tft.setTextColor(0xBDF7);tft.setCursor(12,225);tft.print("MODE: next   +: change   TEST: listen");
 dirty=false;
}
void press(int b){
 if(ringing){stopSound();return;}
 if(b==0){field=(field+1)%8;dirty=true;return;}
 if(b==2){startSound();return;}
 DateTime n=clockNow();
 switch(field){
 case 1:setClock(DateTime(n.year(),n.month(),n.day(),(n.hour()+1)%24,n.minute(),0));break;
 case 2:setClock(DateTime(n.year(),n.month(),n.day(),n.hour(),(n.minute()+1)%60,0));break;
 case 3:ah=(ah+1)%24;break;case 4:am=(am+1)%60;break;
 case 5:enabled=!enabled;break;case 6:soundId=(soundId+1)%4;break;
 default:volume=volume%5+1;break;
 }save();dirty=true;
}
void audio(){
 if(!audioOK){delay(4);return;}
 int16_t out[256];
 for(int i=0;i<128;i++){
  float v=0;
  if(ringing){
   float t=sampleIndex/16000.0f, f=880, env=1;
   if(soundId==0){float p=fmodf(t,1.0f);env=expf(-5*p);v=(sinf(2*PI*660*t)+0.4f*sinf(2*PI*1320*t))*env/1.4f;}
   else {if(soundId==1){f=700+350*((sampleIndex/4000)%3);env=fmodf(t,0.25f)<0.18f?1:0;}
    if(soundId==2)f=600+700*(0.5f-0.5f*cosf(2*PI*t));
    if(soundId==3)env=fmodf(t,1.4f)<0.75f && fmodf(t,0.25f)<0.13f?1:0;
    v=sinf(2*PI*f*t)*env;}
   sampleIndex++;
  }
  // Conservative digital gain: start at volume 1 before trying higher levels.
  int16_t s=(int16_t)(v*volume*2400);out[2*i]=s;out[2*i+1]=s;
 }
 size_t written;i2s_write(I2S_NUM_0,out,sizeof(out),&written,portMAX_DELAY);
}
void setup(){
 Serial.begin(115200);for(int p:buttons)pinMode(p,INPUT_PULLUP);
 prefs.begin("clock",false);ah=prefs.getInt("ah",7);am=prefs.getInt("am",0);soundId=prefs.getInt("sound",0);volume=prefs.getInt("vol",2);enabled=prefs.getBool("on",false);
 Wire.begin(21,22);rtcOK=rtc.begin();DateTime initial(F(__DATE__),F(__TIME__));
 fallbackEpoch=initial.unixtime();fallbackMillis=millis();if(rtcOK&&rtc.lostPower())rtc.adjust(initial);
 SPI.begin(18,19,23,4);tft.begin();tft.setRotation(1);
 i2s_config_t cfg={};cfg.mode=(i2s_mode_t)(I2S_MODE_MASTER|I2S_MODE_TX);cfg.sample_rate=16000;cfg.bits_per_sample=I2S_BITS_PER_SAMPLE_16BIT;cfg.channel_format=I2S_CHANNEL_FMT_RIGHT_LEFT;cfg.communication_format=I2S_COMM_FORMAT_STAND_I2S;cfg.intr_alloc_flags=0;cfg.dma_buf_count=4;cfg.dma_buf_len=128;cfg.use_apll=false;cfg.tx_desc_auto_clear=true;
 i2s_pin_config_t pins={};pins.bck_io_num=26;pins.ws_io_num=25;pins.data_out_num=27;pins.data_in_num=I2S_PIN_NO_CHANGE;
 audioOK=i2s_driver_install(I2S_NUM_0,&cfg,0,nullptr)==ESP_OK;
 if(audioOK)audioOK=i2s_set_pin(I2S_NUM_0,&pins)==ESP_OK;
}
void loop(){
 for(int i=0;i<3;i++){bool r=digitalRead(buttons[i]);if(r!=rawOld[i]){rawOld[i]=r;changed[i]=millis();}if(millis()-changed[i]>35&&r!=stable[i]){stable[i]=r;if(!r)press(i);}}
 if(millis()-lastPoll>=200){lastPoll=millis();DateTime n=clockNow();if(n.minute()!=lastMinute){lastMinute=n.minute();dirty=true;}
 uint32_t minuteKey=n.unixtime()/60;
 if(enabled&&n.hour()==ah&&n.minute()==am&&lastAlarm!=minuteKey){lastAlarm=minuteKey;startSound();}
 if(ringing&&millis()-ringStart>30000)stopSound();if(dirty)draw(n);}
 audio();
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik