Apologies for the delay, it took me too long to realise that adding an equaliser on a esp32-c3 simply isn’t going to work due to the cpu constraints.
Here’s the code: GitHub - clogboy/EspWebradioPlayer: Webradio player based on esp32 supermini MCU
Anyone willing to give it a try on a more capable MCU should use this code:
#include "AudioModule.h"
// Single URL holder for dynamic changes
static const char* dynamicURL[1] = { "" };
AudioModule::AudioModule(const char* ssid, const char* password)
: wifiSSID(ssid), wifiPassword(password), playing(false), currentVolume(0.05),
sleepTimerActive(false), sleepEndTime(0), sleepFadeStart(0), sleepStartVolume(0),
eqBass(0), eqMid(0), eqTreble(0) {
urlStream = new URLStream(wifiSSID, wifiPassword);
source = new AudioSourceURL(*urlStream, dynamicURL, "audio/mp3");
i2s = new I2SStream();
// Create equalizer - it wraps the I2S output
equalizer = new Equalizer3Bands(*i2s);
// Configure equalizer with default settings (unity gain = 1.0)
// MONO for single DAC!
auto& eq_cfg = equalizer->config();
eq_cfg.channels = 1; // MONO - single DAC
eq_cfg.bits_per_sample = 16;
eq_cfg.sample_rate = 44100;
eq_cfg.gain_low = 1.0; // Bass (1.0 = unity, no change)
eq_cfg.gain_medium = 1.0; // Mid
eq_cfg.gain_high = 1.0; // Treble
decoder = new MP3DecoderHelix();
// Player outputs to equalizer, which outputs to I2S
player = new AudioPlayer(*source, *equalizer, *decoder);
}
bool AudioModule::begin() {
Serial.println("Setting up audio...");
// I2S setup
auto cfg = i2s->defaultConfig(TX_MODE);
cfg.pin_bck = I2S_BCLK_PIN;
cfg.pin_ws = I2S_LRCK_PIN;
cfg.pin_data = I2S_DATA_PIN;
cfg.channels = 1; // MONO for single DAC
cfg.sample_rate = 44100;
cfg.bits_per_sample = 16;
cfg.buffer_count = 8; // Increase buffer count
cfg.buffer_size = 512; // Increase buffer size
Serial.print("I2S Config - Channels: ");
Serial.print(cfg.channels);
Serial.print(", Sample Rate: ");
Serial.print(cfg.sample_rate);
Serial.print(", Bits: ");
Serial.println(cfg.bits_per_sample);
if (!i2s->begin(cfg)) {
Serial.println("I2S init failed");
return false;
}
Serial.println("I2S: OK");
// Initialize equalizer
if (!equalizer->begin()) {
Serial.println("Equalizer init failed");
return false;
}
Serial.println("Equalizer: OK");
// Player setup
if (!player->begin()) {
Serial.println("Player init failed");
return false;
}
Serial.println("Player: OK");
// Start paused - wait for user to select a station
playing = false;
player->setVolume(currentVolume);
Serial.println("Audio ready (paused - select station to play)");
return true;
}
void AudioModule::process() {
if (playing) {
player->copy();
}
// Handle sleep timer
if (sleepTimerActive) {
processSleepTimer();
}
}
void AudioModule::play() {
playing = true;
Serial.println("Audio: playing");
}
void AudioModule::pause() {
playing = false;
Serial.println("Audio: paused");
}
bool AudioModule::isPlaying() {
return playing;
}
void AudioModule::setVolume(float vol) {
if (vol < 0.0) vol = 0.0;
if (vol > 1.0) vol = 1.0;
currentVolume = vol;
player->setVolume(currentVolume);
Serial.print("Volume: ");
Serial.println(currentVolume);
}
float AudioModule::getVolume() {
return currentVolume;
}
bool AudioModule::setURL(const char* url) {
if (!url || strlen(url) == 0) {
Serial.println("setURL: Invalid URL");
return false;
}
Serial.print("Changing stream to: ");
Serial.println(url);
currentURL = String(url);
// Stop current playback
bool wasPlaying = playing;
playing = false;
delay(200); // Give audio thread time to stop
// Clean up old objects (in reverse order of creation)
if (player) { delete player; player = nullptr; }
if (decoder) { delete decoder; decoder = nullptr; }
if (equalizer) { delete equalizer; equalizer = nullptr; }
if (source) { delete source; source = nullptr; }
if (i2s) { delete i2s; i2s = nullptr; }
if (urlStream) { delete urlStream; urlStream = nullptr; }
// Recreate with new URL
dynamicURL[0] = currentURL.c_str();
urlStream = new URLStream(wifiSSID, wifiPassword);
source = new AudioSourceURL(*urlStream, dynamicURL, "audio/mp3");
i2s = new I2SStream();
// Recreate equalizer
equalizer = new Equalizer3Bands(*i2s);
// Restore EQ settings
float bassGain = pow(10.0, eqBass / 20.0);
float midGain = pow(10.0, eqMid / 20.0);
float trebleGain = pow(10.0, eqTreble / 20.0);
auto& eq_cfg = equalizer->config();
eq_cfg.channels = 1; // MONO for single DAC
eq_cfg.bits_per_sample = 16;
eq_cfg.sample_rate = 44100;
eq_cfg.gain_low = bassGain;
eq_cfg.gain_medium = midGain;
eq_cfg.gain_high = trebleGain;
decoder = new MP3DecoderHelix();
player = new AudioPlayer(*source, *equalizer, *decoder);
// Reinitialize
auto cfg = i2s->defaultConfig(TX_MODE);
cfg.pin_bck = I2S_BCLK_PIN;
cfg.pin_ws = I2S_LRCK_PIN;
cfg.pin_data = I2S_DATA_PIN;
cfg.channels = 1; // MONO for single DAC
cfg.sample_rate = 44100;
cfg.bits_per_sample = 16;
if (!i2s->begin(cfg)) {
Serial.println("I2S reinit failed");
return false;
}
if (!equalizer->begin()) {
Serial.println("Equalizer reinit failed");
return false;
}
if (!player->begin()) {
Serial.println("Player reinit failed");
return false;
}
player->setVolume(currentVolume);
// Resume if was playing
if (wasPlaying) {
playing = true;
}
Serial.println("Stream changed successfully");
return true;
}
String AudioModule::getCurrentURL() {
return currentURL;
}
void AudioModule::setEQ(int bass, int mid, int treble) {
// Clamp values to -12 to +12 dB
eqBass = constrain(bass, -12, 12);
eqMid = constrain(mid, -12, 12);
eqTreble = constrain(treble, -12, 12);
// Convert dB to gain factor: gain = 10^(dB/20)
// Gain range: 0.0 to 2.0 (0.0 = silence, 1.0 = unity, 2.0 = +6dB)
float bassGain = pow(10.0, eqBass / 20.0);
float midGain = pow(10.0, eqMid / 20.0);
float trebleGain = pow(10.0, eqTreble / 20.0);
// Update equalizer config directly
auto& eq_cfg = equalizer->config();
eq_cfg.gain_low = bassGain;
eq_cfg.gain_medium = midGain;
eq_cfg.gain_high = trebleGain;
// Apply changes by calling begin()
equalizer->begin();
Serial.print("EQ set - Bass: ");
Serial.print(eqBass);
Serial.print("dB (");
Serial.print(bassGain, 2);
Serial.print("), Mid: ");
Serial.print(eqMid);
Serial.print("dB (");
Serial.print(midGain, 2);
Serial.print("), Treble: ");
Serial.print(eqTreble);
Serial.print("dB (");
Serial.print(trebleGain, 2);
Serial.println(")");
}
void AudioModule::getEQ(int &bass, int &mid, int &treble) {
bass = eqBass;
mid = eqMid;
treble = eqTreble;
}
void AudioModule::setSleepTimer(unsigned long durationMinutes) {
unsigned long durationMs = durationMinutes * 60 * 1000;
unsigned long fadeMs = 2 * 60 * 1000; // 2 minute fade
sleepEndTime = millis() + durationMs;
sleepFadeStart = sleepEndTime - fadeMs;
sleepStartVolume = currentVolume;
sleepTimerActive = true;
Serial.print("Sleep timer set for ");
Serial.print(durationMinutes);
Serial.println(" minutes");
}
void AudioModule::cancelSleepTimer() {
sleepTimerActive = false;
Serial.println("Sleep timer cancelled");
}
bool AudioModule::hasSleepTimer() {
return sleepTimerActive;
}
unsigned long AudioModule::getSleepTimeRemaining() {
if (!sleepTimerActive) return 0;
unsigned long now = millis();
if (now >= sleepEndTime) return 0;
return (sleepEndTime - now) / 1000; // return seconds
}
void AudioModule::processSleepTimer() {
unsigned long now = millis();
// Time's up - pause
if (now >= sleepEndTime) {
pause();
cancelSleepTimer();
Serial.println("Sleep timer ended - paused");
return;
}
// In fade period - gradually reduce volume
if (now >= sleepFadeStart) {
unsigned long fadeElapsed = now - sleepFadeStart;
unsigned long fadeDuration = sleepEndTime - sleepFadeStart;
float fadeProgress = (float)fadeElapsed / fadeDuration;
float newVolume = sleepStartVolume * (1.0 - fadeProgress);
player->setVolume(newVolume);
}
}