Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions docs/AUX_INPUT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# AUX Input on FerroWave

This document describes the AUX input support added to the FerroWave firmware.

## Supported hardware

- AI-Thinker ESP32-A1S Audio Kit v2.2
- ES8388 audio codec
- A female 3.5 mm jack wired to the board AUX IN pins (LINEINL / LINEINR)

## What was changed

The firmware can now switch between two audio sources:

1. **Bluetooth** (default) — the original behaviour. Audio is decoded by the ESP32 and sent to the ES8388 DAC over I2S. The LED ring and the ferrofluid magnet are driven from the decoded audio.
2. **AUX / line-in** — audio from the 3.5 mm jack is routed directly through the ES8388 codec analog bypass to the speakers.

## How to switch source

### Serial commands

Connect to the board at 115200 baud and type:

- `aux` — switch to AUX input
- `bt` — switch back to Bluetooth

### Physical button

An optional external button can be connected between **GPIO 34** and **GND**. A single press toggles between Bluetooth and AUX. The button is debounced in software.

## Known limitation

In AUX mode the audio never reaches the ESP32. The visualizer (LED ring and ferrofluid magnet) is therefore **idle** while AUX is active.

We tried reading the AUX signal from the ES8388 ADC through the ESP32 I2S interface in full-duplex (RXTX) mode and copying it back to the DAC so the visualizer could react to it. On this hardware that produced audible white-noise bursts in the background. The analog bypass is the cleanest solution we found.

If you know a way to get a clean full-duplex AUX passthrough on the ESP32-A1S + ES8388, we would love to hear about it.

## Required libraries

In addition to the libraries already used by FerroWave, you need:

- [arduino-audio-driver](https://github.com/pschatzmann/arduino-audio-driver) — provides the low-level ES8388 functions used for the analog bypass.

## Build settings

No change from the original firmware:

- ESP32 core: 2.0.14
- Partition scheme: Huge App
- Board: ESP32 Dev Module (or AI Thinker ESP32-A1S if available)

## Hardware note: no automatic jack detection

The stock ESP32-A1S v2.2 board does not expose a GPIO connected to the AUX jack insert/removal switch. Source switching is therefore manual (serial command or button) unless you add a separate jack-detect circuit.
76 changes: 73 additions & 3 deletions firmware/FerroWave.ino
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@
arduino-audio-driver: https://github.com/pschatzmann/arduino-audio-driver
Use ESP32 core 2.0.14 and Partition Scheme "Huge App".

The AUX input feature uses the low-level ES8388 driver functions
(es8388.h) to enable an analog line-in bypass. See docs/AUX_INPUT.md
for details and the known limitation with the LED/magnet visualizer.

----------------------------------------------------------------------
BUTTON LAYOUT (this build)
Button 1 (GPIO 36): Magnet mode UP (1-8)
Expand All @@ -87,6 +91,13 @@
Button 4 (GPIO 23): LED mode DOWN (1-10)
Button 5 (GPIO 18): EQ preset UP (8 presets)
Button 6 (GPIO 5): EQ preset DOWN (8 presets)
Button 7 (GPIO 34): AUX / Bluetooth toggle

IMPORTANT: in AUX mode the audio is routed through the ES8388 codec's analog
bypass, so the ESP32 never receives the AUX signal. The LEDs and the magnet
therefore stay idle while AUX is active. We tried copying AUX audio through
the ESP32 so the visualizer could react to it, but on this board that
produced audible white-noise bursts.

SERIAL COMMANDS (unchanged from original)
Magnet: 1-8 | LED: c1-c10
Expand All @@ -99,6 +110,8 @@
#include <AudioTools.h>
#include <BluetoothA2DPSink.h>
#include <AudioTools/AudioLibs/AudioBoardStream.h>
#include <AudioBoard.h>
#include <Driver/es8388/es8388.h>
#include <Adafruit_NeoPixel.h>

// ==== Magnet / PWM ====
Expand Down Expand Up @@ -128,6 +141,8 @@ const int BTN_4 = 23; // KEY4 - LED Mode DOWN
const int BTN_5 = 18; // KEY5 - EQ Preset UP
const int BTN_6 = 5; // KEY6 - EQ Preset DOWN

const int BTN_SOURCE = 34; // Optional external toggle for AUX / Bluetooth

const int NUM_BTNS = 6;
const int BTN_PINS[NUM_BTNS] = { BTN_1, BTN_2, BTN_3, BTN_4, BTN_5, BTN_6 };

Expand Down Expand Up @@ -201,6 +216,7 @@ AudioBoard board = AudioKitEs8388V1;
AudioBoardStream i2s_out(board);
RingBufferStream processing_stream(4096);
MultiOutput duplicator;
audio_tools::NullStream bt_null; // Used to silence Bluetooth while AUX is active
BluetoothA2DPSink a2dp_sink(duplicator);

static const size_t SAMPLE_COUNT = 512;
Expand Down Expand Up @@ -258,23 +274,53 @@ void applyEQ() {
Serial.printf("Volume: %d%%, Bass: %+d, Treble: %+d\n", volume, bassEQ, trebleEQ);
}

// -----------------------------------------------------------------------------
// AUX input support (analog bypass on the ES8388 codec)
// -----------------------------------------------------------------------------
// The ESP32-A1S Audio Kit v2.2 routes the 3.5 mm AUX jack to the ES8388 line
// inputs. We tried reading the AUX signal through the ESP32 I2S interface in
// RXTX mode and copying it back to the DAC so the LED/magnet visualizer could
// also react to AUX audio, but that produced audible white-noise bursts in the
// background. The cleanest solution for this hardware is to let the ES8388
// itself perform an analog line-in bypass:
// AUX jack -> ES8388 ADC mixers -> ES8388 DAC output
// In this mode the audio never enters the ESP32, so the visualizer stays idle
// while AUX is active. Bluetooth mode is unchanged and still drives the LEDs and
// the magnet.
// -----------------------------------------------------------------------------

void switchToAUX() {
if (currentSource == SOURCE_AUX) return;
Serial.println("Switching to AUX input...");

// Keep Bluetooth paired but stop it from writing to the shared codec so the
// AUX bypass has exclusive use of the DAC output.
a2dp_sink.set_output(bt_null);
currentSource = SOURCE_AUX;
board.setInputVolume(80);
Serial.println("AUX input active");

// ES8388 analog bypass: route line-in straight to the headphones/speakers.
es8388_stop(CODEC_MODE_BOTH);
es8388_start(CODEC_MODE_LINE_IN);
es8388_set_voice_volume(100);

Serial.println("AUX input active (analog bypass)");
}

void switchToBluetooth() {
if (currentSource == SOURCE_BLUETOOTH) return;
Serial.println("Switching to Bluetooth...");

// Restore normal I2S DAC mode for Bluetooth audio.
es8388_stop(CODEC_MODE_LINE_IN);
es8388_start(CODEC_MODE_BOTH);
currentSource = SOURCE_BLUETOOTH;
a2dp_sink.set_output(duplicator);

Serial.println("Bluetooth active");
}

bool checkAUXConnected() {
return false; // To be implemented based on your specific hardware
return false; // No jack-detect GPIO on the stock ESP32-A1S v2.2 board.
}

void checkAUXStatus() {
Expand Down Expand Up @@ -395,6 +441,25 @@ void handleButtons() {
}
}

// Optional external toggle button for AUX / Bluetooth.
// Connect a normally-open push-button between BTN_SOURCE and GND.
void handleSourceButton() {
static bool lastState = HIGH;
static unsigned long lastToggleMs = 0;
bool state = digitalRead(BTN_SOURCE);

if (state == LOW && lastState == HIGH &&
(millis() - lastToggleMs) > BTN_DEBOUNCE_MS) {
lastToggleMs = millis();
if (currentSource == SOURCE_BLUETOOTH) {
switchToAUX();
} else {
switchToBluetooth();
}
}
lastState = state;
}

// ==== LED Functions ====

uint32_t HSVtoRGB(float h, float s, float v) {
Expand Down Expand Up @@ -857,6 +922,9 @@ void setup() {
pinMode(BTN_PINS[i], BTN_HAS_PULLUP[i] ? INPUT_PULLUP : INPUT);
}

// Optional source-toggle button (AUX / Bluetooth)
pinMode(BTN_SOURCE, INPUT_PULLUP);

// LED setup
strip.begin();
strip.setBrightness(ledBrightness);
Expand Down Expand Up @@ -905,13 +973,15 @@ void setup() {
Serial.println(F(" [1] Magnet UP [2] Magnet DOWN"));
Serial.println(F(" [3] LED UP [4] LED DOWN"));
Serial.println(F(" [5] EQ UP [6] EQ DOWN"));
Serial.println(F(" [BTN_SOURCE] Toggle AUX / Bluetooth"));
Serial.println(F("==========================================\n"));
Serial.println(F("Ready! Connect via Bluetooth or AUX cable!\n"));
}

void loop() {
handleSerial();
handleButtons();
handleSourceButton();
checkAUXStatus();

size_t avail = processing_stream.available();
Expand Down