Community project

Linux Audio Mixer Controller

ESP32
Photo of Linux Audio Mixer Controller
Generated with AI

Caleb Taylor (Bloo1147)

Last updated September 12, 2026

This project turns an ESP32 into a USB audio mixer controller that integrates with Linux audio systems. Four motorized faders send MIDI CC messages for volume control on different audio sinks, while individual mute buttons with LED feedback provide per-sink control. A master volume knob adjusts the main output level, and a media control encoder lets you play, pause, and skip tracks without leaving your audio application.

The guide provides a complete wiring diagram showing how to connect the rotary encoders, slide potentiometers, push buttons, and status LEDs to the ESP32's GPIO pins. You'll get a full parts list, step-by-step assembly instructions, and ready-to-flash firmware that handles MIDI CC generation, HID consumer control commands, and debouncing logic for reliable operation.

Wiring diagram

Wiring diagram for Linux Audio Mixer Controller

Gather all the parts

QtyComponent
1

KY-040 Rotary Encoder Module

master volume / mute

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

10 kΩ linear slide potentiometer

10 kΩ linear, 100 mm travel

A hand-moved slider that reports one audio-channel level to the ESP32.

1

10 kΩ linear slide potentiometer

10 kΩ linear, 100 mm travel

A hand-moved slider that reports one audio-channel level to the ESP32.

1

10 kΩ linear slide potentiometer

10 kΩ linear, 100 mm travel

A hand-moved slider that reports one audio-channel level to the ESP32.

1

10 kΩ linear slide potentiometer

10 kΩ linear, 100 mm travel

A hand-moved slider that reports one audio-channel level to the ESP32.

1

KY-040 Rotary Encoder Module

KY-040

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

Push Button

momentary, normally open

Momentary push button switch

1

Push Button

momentary, normally open

Momentary push button switch

1

Push Button

momentary, normally open

Momentary push button switch

1

Push Button

momentary, normally open

Momentary push button switch

1

LED

red, 5 mm

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

LED

red, 5 mm

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

LED

red, 5 mm

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

LED

red, 5 mm

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

Resistor

220 Ω, 1/4 W

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

1

Resistor

220 Ω, 1/4 W

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

1

Resistor

220 Ω, 1/4 W

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

1

Resistor

220 Ω, 1/4 W

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

Assemble it in 8 steps

1. Place the ESP32 and controls

Put the Feather ESP32-S3, four 10 kΩ linear faders, four normally-open momentary buttons, four red LEDs, four 220 Ω resistors, and the two KY-040 encoder modules on the panel. Place each red LED beside its matching fader and mute button.

  • Label each group 1 through 4 before attaching wires.
  • Do not use motorized faders or connect any old motor-power supply in this hand-operated version.

2. Give every fader safe reference power

Connect one outer terminal, named VREF here, on every fader to the Feather 3V3 pin (power). Connect the opposite outer terminal, named GND here, on every fader to a Feather GND pin (ground). Use the same outer-terminal direction for all four faders so moving every slider upward means louder.

  • If one fader works backward, swap only its two outer-terminal wires; leave its center wire alone.
  • Never connect a fader terminal to 5 V, VIN, 10 V, or 12 V — the center terminal could send too much voltage into the Feather.

3. Connect the four slider signals

Connect fader 1 WIPER, its center terminal, to GPIO1 (signal). Connect fader 2 WIPER to GPIO2 (signal). Connect fader 3 WIPER to GPIO3 (signal). Connect fader 4 WIPER to GPIO4 (signal).

  • The center terminal is often physically between the two outer terminals; check the fader drawing if its labels differ.
  • Keep the center-terminal signal wires away from loose power wires, because a short to 5 V can damage an ESP32 input.

4. Wire the four sink-mute buttons

For button 1, connect GND to Feather GND (ground) and SIGNAL to GPIO5 (mute signal for sink 1). For button 2, connect GND to Feather GND (ground) and SIGNAL to GPIO6 (mute signal for sink 2). For button 3, connect GND to Feather GND (ground) and SIGNAL to GPIO7 (mute signal for sink 3). For button 4, connect GND to Feather GND (ground) and SIGNAL to GPIO8 (mute signal for sink 4).

  • Each button has no positive or negative side. A quick press changes that sink between muted and unmuted.
  • Use normally-open momentary buttons; a latching switch would hold the input down and make mute changes unreliable.

5. Wire the four red mute lights

For LED 1, connect GPIO9 to one lead of resistor 1 (control), connect the resistor’s other lead to the LED’s long leg (current limiting), and connect the LED’s short leg or flat-side lead to GND (ground). Repeat: GPIO10 through resistor 2 to LED 2 long leg, GPIO11 through resistor 3 to LED 3 long leg, and GPIO12 through resistor 4 to LED 4 long leg; connect every LED short leg to GND (ground).

  • The red light turns on when its matching mute button puts that sink into the muted state.
  • Every LED needs its own 220 Ω resistor. Do not connect an LED long leg directly to a GPIO — that can damage the LED or the Feather pin.
  • Make sure the LED long and short legs are not reversed; a reversed LED will not light.

6. Wire the master knob

Connect the master KY-040 encoder VCC to Feather 3V3 (power) and GND to Feather GND (ground). Connect CLK to GPIO14 (turn signal), DT to GPIO21 (turn direction signal), and SW to GPIO38 (push-button signal).

  • The knob controls MIDI master volume when turned and sends MIDI master mute while pressed.
  • Make sure VCC and GND are not swapped — swapped power can damage the encoder module.

7. Wire the media knob

Connect the second KY-040 encoder VCC to Feather 3V3 (power) and GND to Feather GND (ground). Connect CLK to GPIO39 (turn signal), DT to GPIO40 (turn direction signal), and SW to GPIO41 (push-button signal).

  • Turning this knob skips to the previous or next track. Pressing it starts or pauses playback.
  • Make sure VCC and GND are not swapped — swapped power can damage the encoder module.

8. Connect the Feather over USB

Connect one data-capable USB cable from the Linux computer to the Feather ESP32-S3 USB connector; this provides both power and the MIDI plus media-control data.

  • The sliders, buttons, and red lights need no separate power supply.
  • Do not feed power into the Feather VIN pin while it is connected to the computer by USB.

Review all connections

1. Connections between "master_encoder" and "ESP32"

Functionmaster_encoderESP32
powerVCC3V3
groundGNDGND
digitalCLKGPIO 14
digitalDTGPIO 21
digitalSWGPIO 38

2. Connections between "fader_1" and "ESP32"

Functionfader_1ESP32
powerVREF3V3
groundGNDGND
analogWIPERGPIO 1

3. Connections between "fader_2" and "ESP32"

Functionfader_2ESP32
powerVREF3V3
groundGNDGND
analogWIPERGPIO 2

4. Connections between "fader_3" and "ESP32"

Functionfader_3ESP32
powerVREF3V3
groundGNDGND
analogWIPERGPIO 3

5. Connections between "fader_4" and "ESP32"

Functionfader_4ESP32
powerVREF3V3
groundGNDGND
analogWIPERGPIO 4

6. Connections between "media_encoder" and "ESP32"

Functionmedia_encoderESP32
powerVCC3V3
groundGNDGND
digitalCLKGPIO 39
digitalDTGPIO 40
digitalSWGPIO 41

7. Connections between "mute_button_1" and "ESP32"

Functionmute_button_1ESP32
groundGNDGND
digitalSIGNALGPIO 5

8. Connections between "mute_button_2" and "ESP32"

Functionmute_button_2ESP32
groundGNDGND
digitalSIGNALGPIO 6

9. Connections between "mute_button_3" and "ESP32"

Functionmute_button_3ESP32
groundGNDGND
digitalSIGNALGPIO 7

10. Connections between "mute_button_4" and "ESP32"

Functionmute_button_4ESP32
groundGNDGND
digitalSIGNALGPIO 8

11. Connections between "mute_led_resistor_1" and "ESP32"

Functionmute_led_resistor_1ESP32
digitalP1GPIO 9
digitalP2LED ANODEEXT

12. Connections between "mute_led_1" and "ESP32"

Functionmute_led_1ESP32
groundGNDGND

13. Connections between "mute_led_resistor_2" and "ESP32"

Functionmute_led_resistor_2ESP32
digitalP1GPIO 10
digitalP2LED ANODEEXT

14. Connections between "mute_led_2" and "ESP32"

Functionmute_led_2ESP32
groundGNDGND

15. Connections between "mute_led_resistor_3" and "ESP32"

Functionmute_led_resistor_3ESP32
digitalP1GPIO 11
digitalP2LED ANODEEXT

16. Connections between "mute_led_3" and "ESP32"

Functionmute_led_3ESP32
groundGNDGND

17. Connections between "mute_led_resistor_4" and "ESP32"

Functionmute_led_resistor_4ESP32
digitalP1GPIO 12
digitalP2LED ANODEEXT

18. Connections between "mute_led_4" and "ESP32"

Functionmute_led_4ESP32
groundGNDGND

Deploy the firmware

#include <Arduino.h>
#include "USB.h"
#include "USBMIDI.h"
#include "USBHIDConsumerControl.h"

// Forward declarations
uint8_t rawToMidi(int raw);
void tapConsumerKey(uint16_t key);
void sendFaderIfMoved(uint8_t index);
void readMasterEncoder();
void readMasterButton();
void readMediaEncoder();
void readMediaButton();
void readSinkMuteButtons();

USBMIDI MIDI;
USBHIDConsumerControl ConsumerControl;

constexpr uint8_t MIDI_CHANNEL = 1;
constexpr uint8_t FADER_CC[4] = {16, 17, 18, 19};
constexpr uint8_t MASTER_CC = 7;
constexpr uint8_t MUTE_CC = 20;
constexpr uint8_t SINK_MUTE_CC[4] = {21, 22, 23, 24};

constexpr uint8_t FADER_PINS[4] = {1, 2, 3, 4};
constexpr uint8_t SINK_MUTE_BUTTON_PINS[4] = {5, 6, 7, 8};
constexpr uint8_t SINK_MUTE_LED_PINS[4] = {9, 10, 11, 12};
constexpr uint8_t ENCODER_CLK_PIN = 14;
constexpr uint8_t ENCODER_DT_PIN = 21;
constexpr uint8_t ENCODER_SW_PIN = 38;
constexpr uint8_t MEDIA_ENCODER_CLK_PIN = 39;
constexpr uint8_t MEDIA_ENCODER_DT_PIN = 40;
constexpr uint8_t MEDIA_ENCODER_SW_PIN = 41;

constexpr int ADC_MIN = 40;
constexpr int ADC_MAX = 4055;
constexpr int FADER_SEND_DEADBAND = 14;
constexpr unsigned long FADER_INTERVAL_MS = 8;
constexpr unsigned long BUTTON_DEBOUNCE_MS = 25;

int lastSentRaw[4] = {-10000, -10000, -10000, -10000};
uint8_t masterValue = 100;
int lastMasterClock = HIGH;
int lastMasterButtonReading = HIGH;
int stableMasterButtonState = HIGH;
unsigned long lastMasterButtonChangeMs = 0;
int lastMediaClock = HIGH;
int lastMediaButtonReading = HIGH;
int stableMediaButtonState = HIGH;
unsigned long lastMediaButtonChangeMs = 0;
int lastSinkMuteButtonReading[4] = {HIGH, HIGH, HIGH, HIGH};
int stableSinkMuteButtonState[4] = {HIGH, HIGH, HIGH, HIGH};
unsigned long lastSinkMuteButtonChangeMs[4] = {0, 0, 0, 0};
bool sinkMuted[4] = {false, false, false, false};
unsigned long lastFaderReadMs = 0;

uint8_t rawToMidi(int raw) {
  return static_cast<uint8_t>(constrain(map(raw, ADC_MIN, ADC_MAX, 0, 127), 0, 127));
}

void tapConsumerKey(uint16_t key) {
  ConsumerControl.press(key);
  ConsumerControl.release();
}

void sendFaderIfMoved(uint8_t index) {
  const int raw = analogRead(FADER_PINS[index]);
  if (abs(raw - lastSentRaw[index]) >= FADER_SEND_DEADBAND) {
    lastSentRaw[index] = raw;
    MIDI.controlChange(FADER_CC[index], rawToMidi(raw), MIDI_CHANNEL);
  }
}

void readMasterEncoder() {
  const int clockNow = digitalRead(ENCODER_CLK_PIN);
  if (clockNow != lastMasterClock && clockNow == LOW) {
    if (digitalRead(ENCODER_DT_PIN) != clockNow) {
      if (masterValue < 127) ++masterValue;
    } else if (masterValue > 0) {
      --masterValue;
    }
    MIDI.controlChange(MASTER_CC, masterValue, MIDI_CHANNEL);
  }
  lastMasterClock = clockNow;
}

void readMasterButton() {
  const int reading = digitalRead(ENCODER_SW_PIN);
  if (reading != lastMasterButtonReading) lastMasterButtonChangeMs = millis();
  if (millis() - lastMasterButtonChangeMs >= BUTTON_DEBOUNCE_MS && reading != stableMasterButtonState) {
    stableMasterButtonState = reading;
    MIDI.controlChange(MUTE_CC, stableMasterButtonState == LOW ? 127 : 0, MIDI_CHANNEL);
  }
  lastMasterButtonReading = reading;
}

void readMediaEncoder() {
  const int clockNow = digitalRead(MEDIA_ENCODER_CLK_PIN);
  if (clockNow != lastMediaClock && clockNow == LOW) {
    if (digitalRead(MEDIA_ENCODER_DT_PIN) != clockNow) {
      tapConsumerKey(CONSUMER_CONTROL_SCAN_NEXT_TRACK);
    } else {
      tapConsumerKey(CONSUMER_CONTROL_SCAN_PREVIOUS_TRACK);
    }
  }
  lastMediaClock = clockNow;
}

void readMediaButton() {
  const int reading = digitalRead(MEDIA_ENCODER_SW_PIN);
  if (reading != lastMediaButtonReading) lastMediaButtonChangeMs = millis();
  if (millis() - lastMediaButtonChangeMs >= BUTTON_DEBOUNCE_MS && reading != stableMediaButtonState) {
    stableMediaButtonState = reading;
    if (stableMediaButtonState == LOW) {
      tapConsumerKey(CONSUMER_CONTROL_PLAY_PAUSE);
    }
  }
  lastMediaButtonReading = reading;
}

void readSinkMuteButtons() {
  for (uint8_t i = 0; i < 4; ++i) {
    const int reading = digitalRead(SINK_MUTE_BUTTON_PINS[i]);
    if (reading != lastSinkMuteButtonReading[i]) lastSinkMuteButtonChangeMs[i] = millis();
    if (millis() - lastSinkMuteButtonChangeMs[i] >= BUTTON_DEBOUNCE_MS && reading != stableSinkMuteButtonState[i]) {
      stableSinkMuteButtonState[i] = reading;
      if (stableSinkMuteButtonState[i] == LOW) {
        sinkMuted[i] = !sinkMuted[i];
        digitalWrite(SINK_MUTE_LED_PINS[i], sinkMuted[i] ? HIGH : LOW);
        MIDI.controlChange(SINK_MUTE_CC[i], sinkMuted[i] ? 127 : 0, MIDI_CHANNEL);
      }
    }
    lastSinkMuteButtonReading[i] = reading;
  }
}

void setup() {
  analogReadResolution(12);
  for (uint8_t i = 0; i < 4; ++i) {
    pinMode(FADER_PINS[i], INPUT);
    pinMode(SINK_MUTE_BUTTON_PINS[i], INPUT_PULLUP);
    pinMode(SINK_MUTE_LED_PINS[i], OUTPUT);
    digitalWrite(SINK_MUTE_LED_PINS[i], LOW);
    lastSinkMuteButtonReading[i] = digitalRead(SINK_MUTE_BUTTON_PINS[i]);
    stableSinkMuteButtonState[i] = lastSinkMuteButtonReading[i];
  }

  pinMode(ENCODER_CLK_PIN, INPUT_PULLUP);
  pinMode(ENCODER_DT_PIN, INPUT_PULLUP);
  pinMode(ENCODER_SW_PIN, INPUT_PULLUP);
  pinMode(MEDIA_ENCODER_CLK_PIN, INPUT_PULLUP);
  pinMode(MEDIA_ENCODER_DT_PIN, INPUT_PULLUP);
  pinMode(MEDIA_ENCODER_SW_PIN, INPUT_PULLUP);

  lastMasterClock = digitalRead(ENCODER_CLK_PIN);
  lastMasterButtonReading = digitalRead(ENCODER_SW_PIN);
  stableMasterButtonState = lastMasterButtonReading;
  lastMediaClock = digitalRead(MEDIA_ENCODER_CLK_PIN);
  lastMediaButtonReading = digitalRead(MEDIA_ENCODER_SW_PIN);
  stableMediaButtonState = lastMediaButtonReading;

  MIDI.begin(MIDI_CHANNEL_OMNI);
  ConsumerControl.begin();
  USB.begin();
}

void loop() {
  readMasterEncoder();
  readMasterButton();
  readMediaEncoder();
  readMediaButton();
  readSinkMuteButtons();

  if (millis() - lastFaderReadMs >= FADER_INTERVAL_MS) {
    lastFaderReadMs = millis();
    for (uint8_t i = 0; i < 4; ++i) sendFaderIfMoved(i);
  }
}

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