Audio Subsystem

The audio subsystem provides bidirectional audio transport between the vintage telephone handset and a Bluetooth-connected mobile phone. It also generates telephone signaling tones (dial tone, busy signal, etc.).

Architecture Overview

The audio subsystem consists of three main modules:

┌─────────────────────────────────────────────────────────────────────┐
│                        Audio Subsystem                              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐  │
│  │   audio_output   │  │   audio_bridge   │  │      tones       │  │
│  │                  │  │                  │  │                  │  │
│  │ - I2S TX/RX init │  │ - Ring buffers   │  │ - Tone defs      │  │
│  │ - Tone generator │  │ - BT↔Phone tasks │  │ - Frequencies    │  │
│  │ - Audio write    │  │ - HFP callbacks  │  │ - Cadences       │  │
│  └──────────────────┘  └──────────────────┘  └──────────────────┘  │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Module Responsibilities:

  • audio_output - I2S hardware management, tone generation, audio output API

  • audio_bridge - Bluetooth ↔ Phone audio routing via ring buffers

  • tones - Telephone tone definitions (frequencies, cadences)

Audio Data Flow

Bidirectional audio flows between the phone handset and Bluetooth:

Phone → Bluetooth (Transmit Path):

Phone Microphone
      │
      ▼
┌─────────────┐
│  PCM1808    │  External ADC
│    ADC      │  (Analog → Digital)
└─────────────┘
      │ I2S RX (GPIO 35)
      ▼
┌─────────────┐
│ audio_rx_   │  Reads I2S frames
│   task      │  every 20ms
└─────────────┘
      │
      ▼
┌─────────────┐
│ bt_tx_      │  Ring buffer
│ ringbuf     │  (3600 bytes)
└─────────────┘
      │
      ▼
┌─────────────┐
│ HFP outgoing│  Bluetooth callback
│  callback   │  reads from buffer
└─────────────┘
      │
      ▼
Mobile Phone (via Bluetooth)

Bluetooth → Phone (Receive Path):

Mobile Phone (via Bluetooth)
      │
      ▼
┌─────────────┐
│ HFP incoming│  Bluetooth callback
│  callback   │  writes to buffer
└─────────────┘
      │
      ▼
┌─────────────┐
│ bt_rx_      │  Ring buffer
│ ringbuf     │  (3600 bytes)
└─────────────┘
      │
      ▼
┌─────────────┐
│ audio_tx_   │  Reads from buffer,
│   task      │  writes via audio_output
└─────────────┘
      │ (blocked if tone playing)
      ▼
┌─────────────┐
│  PCM5100    │  External DAC
│    DAC      │  (Digital → Analog)
└─────────────┘
      │ I2S TX (GPIO 26)
      ▼
Phone Earpiece/Speaker

I2S Configuration

Both TX and RX channels share a single I2S port with unified configuration.

Note

Codec Choice Rationale: The PCM5100 (DAC) and PCM1808 (ADC) were selected for “wire and go” simplicity - they require no I2C initialization or MCLK signal. While their specs (106 dB / 98 dB SNR) far exceed 8kHz telephony requirements, this trade-off eliminates codec driver complexity entirely. See the circuit documentation for details.

I2S Parameters:

Parameter

Value

Notes

I2S Port

I2S_NUM_0

Single port for both TX/RX

Sample Rate

8000 Hz

Standard telephony rate

Bit Depth

16-bit

Signed PCM samples

Slot Mode

MONO

Single channel for voice

Format

I2S Standard (Philips)

Standard I2S framing

MCLK

Disabled

Not required for external codec

GPIO Pin Assignments:

GPIO

Signal

Description

25

PCM_FSYNC

Word select / frame sync (LRCLK)

5

PCM_CLK_OUT

Bit clock (BCLK)

26

PCM_DOUT

Audio output to DAC (TX)

35

PCM_DIN

Audio input from ADC (RX, input-only pin)

Unified Channel Creation:

ESP-IDF requires both TX and RX channels on the same I2S port to be created in a single call:

// Create both channels together (required by ESP-IDF)
i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle);

// Initialize each with matching configuration
i2s_channel_init_std_mode(tx_handle, &tx_std_cfg);
i2s_channel_init_std_mode(rx_handle, &rx_std_cfg);

Bluetooth HFP Audio Path

The system uses the HCI audio path for Bluetooth HFP audio, enabling software control over audio routing.

Why HCI Path (not PCM)?

Path

How It Works

Trade-offs

HCI

Audio flows through HCI callbacks. Software handles audio frames via ring buffers.

Enables tone mixing, volume control, audio processing. Small CPU overhead.

PCM

Bluetooth controller routes audio directly via I2S DMA. CPU never sees audio.

Zero CPU overhead but no software control. Tones can’t interrupt calls.

The HCI path is configured in sdkconfig.defaults:

CONFIG_BT_HFP_AUDIO_DATA_PATH_HCI=y

HFP Data Callbacks:

Two callbacks handle Bluetooth audio:

// Called when Bluetooth needs audio to send (Phone → BT)
static uint32_t bt_app_hf_client_outgoing_cb(uint8_t *p_buf, uint32_t sz)
{
    // Read from bt_tx_ringbuf (filled by audio_rx_task)
    // Return number of bytes provided
}

// Called when Bluetooth has audio to deliver (BT → Phone)
static void bt_app_hf_client_incoming_cb(const uint8_t *buf, uint32_t sz)
{
    // Write to bt_rx_ringbuf (read by audio_tx_task)
}

Tone Generation

The audio subsystem generates authentic North American telephone tones.

Supported Tones:

Tone

Frequencies

Cadence

Usage

Dial Tone

350 + 440 Hz

Continuous

Phone off-hook, ready to dial

Ringback

440 + 480 Hz

2s on, 4s off

Outgoing call ringing

Busy Signal

480 + 620 Hz

0.5s on/off

Called party busy

Reorder (Fast Busy)

480 + 620 Hz

0.25s on/off

Call cannot be completed

Off-Hook Warning

1400+2060+2450+2600 Hz

0.1s on/off

Phone left off-hook

Call Waiting

440 Hz

0.3s beep

Incoming call during active call

Tone Priority:

Tones have priority over Bluetooth audio. When a tone is playing:

  1. audio_output_tone_active() returns true

  2. audio_output_write() silently drops Bluetooth audio

  3. Tone continues until explicitly stopped

Tone API:

// Start playing a tone (thread-safe)
esp_err_t audio_output_play_tone(tone_type_t tone);

// Stop the current tone
esp_err_t audio_output_stop_tone(void);

// Check if tone is active
bool audio_output_tone_active(void);

Tone Generation Task:

A dedicated FreeRTOS task generates tone samples using sine wave synthesis:

// Dual-frequency tone generation
float sample1 = sinf(phase1);  // First frequency
float sample2 = sinf(phase2);  // Second frequency
float mixed = (sample1 + sample2) / 2.0f;
buffer[i] = (int16_t)(32767.0f * TONE_VOLUME * mixed);

Ring Buffers

Two ring buffers decouple audio tasks from Bluetooth callbacks:

Buffer Configuration:

  • Size: 3600 bytes each (AUDIO_HFP_RINGBUF_SIZE)

  • Type: Byte buffer (RINGBUF_TYPE_BYTEBUF)

  • Capacity: ~11 audio frames (~220ms of audio)

Buffer Roles:

Buffer

Direction

Producer → Consumer

bt_rx_ringbuf

Bluetooth → Phone

HFP incoming callback → audio_tx_task

bt_tx_ringbuf

Phone → Bluetooth

audio_rx_task → HFP outgoing callback

Initialization Sequence

Audio modules are initialized in specific order:

main()
  └─ audio_output_init()     # Creates I2S TX+RX, starts tone task
       └─ audio_bridge_init() # Creates ring buffers, verifies RX handle
            └─ bluetooth_init()
                 └─ bt_app_hf_register_data_callbacks()  # Registers HFP callbacks

Critical Dependencies:

  1. audio_output_init() must complete before audio_bridge_init()

  2. audio_bridge_init() must complete before Bluetooth audio connects

  3. HFP data callbacks must be registered after HFP client initialization

Module Files

audio_output (main/audio/audio_output.c, audio_output.h):

  • Owns I2S TX and RX channel handles

  • Provides audio_output_write() for BT audio passthrough

  • Provides audio_output_get_rx_handle() for audio_bridge

  • Runs tone generation task

audio_bridge (main/audio/audio_bridge.c, audio_bridge.h):

  • Creates and manages ring buffers

  • Runs audio_rx_task (Phone → BT) and audio_tx_task (BT → Phone)

  • Provides ring buffer accessors for HFP callbacks

tones (main/audio/tones.c, tones.h):

  • Defines tone_type_t enum

  • Stores tone frequency/cadence parameters

  • Provides tone_get_definition() accessor

Diagnostic Output

Initialization Logs:

I (1234) audio_output: Initializing audio output subsystem
I (1240) audio_output: Audio I/O initialized (TX: GPIO26, RX: GPIO35)
I (1245) audio_bridge: Initializing audio bridge
I (1250) audio_bridge: Audio bridge initialized (ring buffers ready)

Runtime Logs:

I (5000) audio_output: Playing tone: 0  (DIAL_TONE)
I (8000) audio_output: Playing tone: 10 (TONE_NONE - stopping)
I (9000) audio_bridge: Audio RX task started (Phone → Bluetooth)
I (9001) audio_bridge: Audio TX task started (Bluetooth → Phone)

References

  • Firmware Architecture - Overall firmware architecture

  • State Management - State management system

  • docs/source/solution-design/circuit/audio.rst - Audio circuit design

  • docs/source/implementation/circuit/pin-assignments.rst - GPIO assignments