Community project

Wireless Volume Amplifier

ESP32
Photo of Wireless Volume Amplifier
Generated with AI

Ethan Loukas

Published October 16, 2026

This wireless volume amplifier lets you control audio levels from across the room using a battery-powered rotary encoder remote. The system consists of a receiver box that sits between an audio source and amplifier, with a PT2258 digital volume controller managing the stereo signal path, while an ESP32-powered knob communicates wirelessly via ESP-NOW to adjust volume and mute.

The guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions for building both the stationary receiver and the portable remote control. You'll flash a single firmware sketch to both ESP32 boards (configured via GPIO pin state), solder the audio jack and I2C logic converter, integrate the rotary encoder with debouncing, and set up safe LiPo battery charging with the bq25185 charger board.

Wiring diagram

Wiring diagram for Wireless Volume Amplifier

Gather all the parts

QtyComponent
1

Panel Mount 1/8" / 3.5mm TRS Audio Jack Connector

Panel-mount 3.5mm TRS (Tip-Ring-Sleeve) stereo audio jack for building enclosures or breakout panels with a standard headphone/line-level audio connection.

1

Panel Mount 1/8" / 3.5mm TRS Audio Jack Connector

Panel-mount 3.5mm TRS (Tip-Ring-Sleeve) stereo audio jack for building enclosures or breakout panels with a standard headphone/line-level audio connection.

1

PT2258 stereo digital volume-control module

A ready-made stereo line-level board that changes left and right audio volume under digital control without driving speakers.

1

4-channel bidirectional I2C logic-level converter

A small ready-made board that safely translates the ESP32’s 3.3 V control signals to the volume board’s 5 V signals.

1

ESP32 DevKit v1 for the wireless knob

The second ESP32 board inside the battery-powered knob remote; it runs the same firmware as the receiver board.

1

KY-040 Rotary Encoder Module

5-pin incremental rotary encoder breakout with integrated momentary push switch. CLK and DT are the quadrature outputs; SW is the built-in push-button output and should not be modelled as a separate Push Button component.

1

LiPo 3.7V 1000mAh Battery

Single-cell LiPo pack, nominal 3.7 V, 1000 mAh. Default rechargeable choice for portable ESP32 / Pico projects. Pair with a TP4056 charger for safe USB recharging.

1

Adafruit bq25185 USB / DC / Solar Charger with 3.3V Buck Board

bq25185 multi-source LiPoly/LiIon charger combined with a 3.3V buck regulator output, letting a project run directly from the board without a separate regulator.

1

5 V boost converter module

A ready-made step-up board that turns the remote battery voltage into the stable 5 V needed by its ESP32 board.

1

10 kΩ resistor

10 kΩ

A small resistor that holds the remote board’s mode input low so the shared firmware identifies it as the wireless knob.

1

10 kΩ resistor

10 kΩ

A small resistor that holds the receiver board’s mode input high so the shared firmware identifies it as the audio-box receiver.

Assemble it in 6 steps

1. Fit the receiver-box parts

Mount aux_input and aux_output in opposite sides of a non-metal enclosure, then mount volume_module, i2c_level_shifter, and the receiver ESP32 board so their USB socket remains reachable. Mark the input jack “SOURCE” and the output jack “SPEAKERS.”

  • Use short shielded audio wire inside the box and keep it away from the ESP32 antenna end.
  • Do not connect a passive speaker to this box: it provides only a safe line-level signal for speakers that already plug into wall power.

2. Wire the stereo audio path

Wire aux_input TIP to volume_module LIN (left audio), aux_input RING to volume_module RIN (right audio), and aux_input SLEEVE to volume_module AGND (audio ground). Then wire volume_module LOUT to aux_output TIP (left audio), ROUT to aux_output RING (right audio), and aux_output SLEEVE to volume_module GND (ground).

  • On a 3.5 mm stereo jack, TIP is left, RING is right, and SLEEVE is the long ground contact.
  • Do not swap the input and output jack labels; a source must feed the INPUT jack and powered speakers must connect to the OUTPUT jack.

3. Wire the receiver control board

Connect volume_module VCC to the receiver board’s 5V rail (power) and GND to GND (ground). Connect i2c_level_shifter HV to 5V (power), LV to 3V3 (power), and GND to GND (ground). Connect its LV1 to GPIO21 (data) and LV2 to GPIO22 (clock); connect HV1 to volume_module SDA (data) and HV2 to volume_module SCL (clock). Connect receiver_mode_resistor from GPIO33 to 3V3 (mode identification).

  • The little level-converter board is required because the ESP32 uses 3.3 V logic but the PT2258 board uses 5 V logic.
  • Make sure 5V and 3V3 are not swapped on the level converter — swapped power can damage the ESP32 or the converter.

4. Build the battery-powered knob

Fit remote_esp32 and rotary_encoder in a second enclosure with the knob shaft through the lid. Connect rotary_encoder VCC to remote_charger 3V3 (power), GND to remote_charger GND (ground), CLK to remote_esp32 GPIO25 (turn signal), DT to GPIO26 (direction signal), and SW to GPIO27 (push signal). Connect remote_mode_resistor from remote_esp32 GPIO33 to remote_charger GND so this board identifies itself as the remote.

  • Use a plastic enclosure or leave clear space around the ESP32 antenna end; metal beside it shortens wireless range.
  • Do not power the KY-040 encoder from 5 V — its signal wires would then be too high for the ESP32 inputs.

5. Connect the remote battery safely

Plug remote_battery into remote_charger BAT (battery positive) and GND (battery negative). Connect remote_boost VIN+ to remote_charger BAT and its GND to remote_charger GND; connect remote_boost VOUT+ to remote_esp32 VIN (5 V power). Keep all negative wires joined through the charger GND connection.

  • The charger board’s USB-C socket charges the battery; the receiver box is powered separately from its own USB cable.
  • Never connect the LiPo battery backward or short its leads — it can overheat, swell, or be permanently damaged.

6. Connect the audio equipment

Use one ordinary 3.5 mm stereo AUX cable from your audio source to the box’s SOURCE jack, and a second AUX cable from the box’s SPEAKERS jack to the line input on the powered speakers. Turn the speakers’ own volume down before first testing, then power the receiver ESP32 from USB.

  • Turning the knob changes volume; pressing the knob mutes or restores the sound.
  • Start with the powered speakers at low volume because a line-level connection can still be unexpectedly loud.

Review all connections

1. Connections between "aux_input" and "ESP32"

Functionaux_inputESP32
analogTIP → PT2258 stereo digital volume-control module LINEXT
analogRING → PT2258 stereo digital volume-control module RINEXT
groundSLEEVE → PT2258 stereo digital volume-control module AGNDEXT

2. Connections between "volume_module" and "ESP32"

Functionvolume_moduleESP32
powerVCC5V
groundGNDGND
i2cSDA → 4-channel bidirectional I2C logic-level converter HV1EXT
i2cSCL → 4-channel bidirectional I2C logic-level converter HV2EXT
analogLOUT → Panel Mount 1/8" / 3.5mm TRS Audio Jack Connector TIPEXT
analogROUT → Panel Mount 1/8" / 3.5mm TRS Audio Jack Connector RINGEXT

3. Connections between "aux_output" and "ESP32"

Functionaux_outputESP32
groundSLEEVE → PT2258 stereo digital volume-control module GNDEXT

4. Connections between "i2c_level_shifter" and "ESP32"

Functioni2c_level_shifterESP32
powerLV3V3
powerHV5V
groundGNDGND
i2cLV1GPIO 21
i2cLV2GPIO 22

5. Connections between "receiver_mode_resistor" and "ESP32"

Functionreceiver_mode_resistorESP32
powerB3V3
digitalAGPIO 33

6. Connections between "remote_battery" and "ESP32"

Functionremote_batteryESP32
power+V → Adafruit bq25185 USB / DC / Solar Charger with 3.3V Buck Board BATEXT
groundGND → Adafruit bq25185 USB / DC / Solar Charger with 3.3V Buck Board GNDEXT

7. Connections between "remote_boost" and "ESP32"

Functionremote_boostESP32
powerVIN+ → Adafruit bq25185 USB / DC / Solar Charger with 3.3V Buck Board BATEXT
groundGND → Adafruit bq25185 USB / DC / Solar Charger with 3.3V Buck Board GNDEXT
powerVOUT+ → ESP32 DevKit v1 for the wireless knob VINEXT

8. Connections between "remote_esp32" and "ESP32"

Functionremote_esp32ESP32
groundGND → Adafruit bq25185 USB / DC / Solar Charger with 3.3V Buck Board GNDEXT

9. Connections between "rotary_encoder" and "ESP32"

Functionrotary_encoderESP32
powerVCC → Adafruit bq25185 USB / DC / Solar Charger with 3.3V Buck Board 3V3EXT
groundGND → Adafruit bq25185 USB / DC / Solar Charger with 3.3V Buck Board GNDEXT
digitalCLK → ESP32 DevKit v1 for the wireless knob GPIO25EXT
digitalDT → ESP32 DevKit v1 for the wireless knob GPIO26EXT
digitalSW → ESP32 DevKit v1 for the wireless knob GPIO27EXT

10. Connections between "remote_mode_resistor" and "ESP32"

Functionremote_mode_resistorESP32
groundB → Adafruit bq25185 USB / DC / Solar Charger with 3.3V Buck Board GNDEXT
digitalA → ESP32 DevKit v1 for the wireless knob GPIO33EXT

Deploy the firmware

#include <Arduino.h>
#include <WiFi.h>
#include <esp_now.h>
#include <Wire.h>

// This one sketch is deployed to both ESP32 DevKit v1 boards.
// GPIO33 high = AUX receiver; GPIO33 low = battery-powered knob remote.
static const uint8_t MODE_PIN = 33;
static const uint8_t I2C_SDA_PIN = 21;
static const uint8_t I2C_SCL_PIN = 22;
static const uint8_t ENCODER_CLK_PIN = 25;
static const uint8_t ENCODER_DT_PIN = 26;
static const uint8_t ENCODER_SW_PIN = 27;

static const uint8_t PT2258_ADDRESS = 0x44; // 7-bit form of the usual 0x88 address.
static const uint8_t BROADCAST_ADDRESS[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
static const uint32_t PACKET_MAGIC = 0x564F4C31; // "VOL1"

enum Command : uint8_t { COMMAND_STEP = 1, COMMAND_TOGGLE_MUTE = 2 };

struct VolumePacket {
  uint32_t magic;
  uint8_t command;
  int8_t step;
  uint16_t sequence;
};

bool isReceiver = false;
uint8_t attenuation = 24; // 12 dB below full line level at startup.
bool muted = false;
int lastEncoderClock = HIGH;
bool lastButton = HIGH;
uint32_t lastEncoderChangeMs = 0;
uint32_t lastButtonChangeMs = 0;
uint16_t sequenceNumber = 0;

void writePt2258(uint8_t amount) {
  // PT2258 master attenuation command: 0 = loudest, 63 = quietest.
  Wire.beginTransmission(PT2258_ADDRESS);
  Wire.write((uint8_t)(0xC0 | (amount & 0x3F)));
  Wire.endTransmission();
}

void applyVolume() {
  writePt2258(muted ? 63 : attenuation);
}

void handleCommand(const VolumePacket &packet) {
  if (packet.magic != PACKET_MAGIC) {
    return;
  }

  if (packet.command == COMMAND_STEP) {
    int next = (int)attenuation - (int)packet.step;
    attenuation = (uint8_t)constrain(next, 0, 63);
    muted = false;
    applyVolume();
  } else if (packet.command == COMMAND_TOGGLE_MUTE) {
    muted = !muted;
    applyVolume();
  }
}

#if ESP_ARDUINO_VERSION_MAJOR >= 3
void onEspNowReceive(const esp_now_recv_info_t *info, const uint8_t *data, int dataLength) {
  (void)info;
  if (dataLength == (int)sizeof(VolumePacket)) {
    VolumePacket packet;
    memcpy(&packet, data, sizeof(packet));
    handleCommand(packet);
  }
}
#else
void onEspNowReceive(const uint8_t *mac, const uint8_t *data, int dataLength) {
  (void)mac;
  if (dataLength == (int)sizeof(VolumePacket)) {
    VolumePacket packet;
    memcpy(&packet, data, sizeof(packet));
    handleCommand(packet);
  }
}
#endif

bool startEspNow() {
  WiFi.mode(WIFI_STA);
  if (esp_now_init() != ESP_OK) {
    return false;
  }

  if (isReceiver) {
    esp_now_register_recv_cb(onEspNowReceive);
  } else {
    esp_now_peer_info_t peer = {};
    memcpy(peer.peer_addr, BROADCAST_ADDRESS, sizeof(BROADCAST_ADDRESS));
    peer.channel = 0;
    peer.encrypt = false;
    if (!esp_now_is_peer_exist(BROADCAST_ADDRESS) && esp_now_add_peer(&peer) != ESP_OK) {
      return false;
    }
  }
  return true;
}

void sendCommand(Command command, int8_t step = 0) {
  VolumePacket packet = {PACKET_MAGIC, (uint8_t)command, step, sequenceNumber++};
  esp_now_send(BROADCAST_ADDRESS, (const uint8_t *)&packet, sizeof(packet));
}

void setupReceiver() {
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
  applyVolume();
  startEspNow();
}

void setupRemote() {
  pinMode(ENCODER_CLK_PIN, INPUT_PULLUP);
  pinMode(ENCODER_DT_PIN, INPUT_PULLUP);
  pinMode(ENCODER_SW_PIN, INPUT_PULLUP);
  lastEncoderClock = digitalRead(ENCODER_CLK_PIN);
  lastButton = digitalRead(ENCODER_SW_PIN);
  startEspNow();
}

void setup() {
  pinMode(MODE_PIN, INPUT);
  delay(10);
  isReceiver = digitalRead(MODE_PIN) == HIGH;
  if (isReceiver) {
    setupReceiver();
  } else {
    setupRemote();
  }
}

void serviceRemote() {
  const uint32_t now = millis();
  int clockNow = digitalRead(ENCODER_CLK_PIN);
  if (clockNow != lastEncoderClock && clockNow == LOW && now - lastEncoderChangeMs >= 3) {
    int8_t step = digitalRead(ENCODER_DT_PIN) == HIGH ? 1 : -1;
    sendCommand(COMMAND_STEP, step);
    lastEncoderChangeMs = now;
  }
  lastEncoderClock = clockNow;

  bool buttonNow = digitalRead(ENCODER_SW_PIN);
  if (lastButton == HIGH && buttonNow == LOW && now - lastButtonChangeMs >= 30) {
    sendCommand(COMMAND_TOGGLE_MUTE);
    lastButtonChangeMs = now;
  }
  lastButton = buttonNow;
}

void loop() {
  if (!isReceiver) {
    serviceRemote();
  }
  delay(2);
}

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