# ============================================================================= # GENERIC ESP32-S3 VOIP + VA - Single I2S bus, no hardware codec # ============================================================================= # Full-featured config: VoIP + Media Player + mixer (VA/MWW ready). # For ESP32 boards where mic and speaker share the SAME I2S bus. # # Audio pipeline: # MEMS mic → esp_audio_stack (48kHz) → esp_ae_rate_cvt x3 → esp_afe (AEC+NS+AGC) → mic_main → VA / VoIP TX / MWW # # Media → media_speaker_input (resampler) ────────────┐ # VA TTS → tts_speaker_input (resampler) ──────────────┼── mixer (48kHz) → hw_speaker → class-D amp # VoIP RX → voip_speaker_input (resampler) ─────────────┘ # # HARDWARE (adapt GPIOs to YOUR board): # - Board: Any ESP32-S3 with PSRAM # - Flash: requires an ESP32-S3 layout with an app slot larger than the # default 4 MB OTA slot; 8 MB or 16 MB flash is recommended # - Microphone: Any I2S MEMS mic on DIN pin # - Speaker: Any I2S class-D amp on DOUT pin # - Mic and speaker SHARE BCLK + LRCLK (same I2S bus, full-duplex) # - LED: SK6812/WS2812 RGB (optional) # - ESP32-S3R8/S3R8V: do not use PSRAM pins for I2S/LED; the GPIOs below # are examples only and may need moving on those modules. # # GPIO PINOUT (example, change to match your wiring): # Shared I2S: BCLK=GPIO36, LRCLK=GPIO37, DIN=GPIO35 (mic), DOUT=GPIO7 (speaker) # LED: DATA=GPIO33 # # ============================================================================= # ============================================================================= # PROJECT SETUP # ============================================================================= substitutions: # Hardware-shared identity: dropping firmware-variant suffix keeps build cache # and HA device entity stable across rebuilds. name: generic-s3 friendly_name: Generic S3 ext_components_source: "github://n-IA-hane/esphome-intercom@main" voip_stack_components_source: "github://n-IA-hane/esphome-voip-stack@main" audio_stack_components_source: "github://n-IA-hane/esphome-audio-stack@main" runtime_controller_components_source: "github://n-IA-hane/esphome-runtime-controller@main" assets_base: "https://github.com/n-IA-hane/esphome-intercom/raw/main/" # Microphone ID for package mute switch mic_id: mic_main runtime_controller_led_preset: rgb_single # --------------------------------------------------------------------------- # Pinout: shared I2S bus # --------------------------------------------------------------------------- # BCLK/LRCLK are shared by mic and speaker on this full-duplex bus. i2s_bclk_pin: GPIO36 i2s_lrclk_pin: GPIO37 # --------------------------------------------------------------------------- # Pinout: microphone # --------------------------------------------------------------------------- i2s_din_pin: GPIO35 # --------------------------------------------------------------------------- # Pinout: speaker # --------------------------------------------------------------------------- i2s_dout_pin: GPIO7 # --------------------------------------------------------------------------- # Pinout: optional status LED # --------------------------------------------------------------------------- status_led_pin: GPIO33 status_led_chipset: WS2812 status_led_rgb_order: GRB packages: full_voice_voip_runtime: github://n-IA-hane/esphome-intercom/packages/presets/full_voice_voip_runtime.yaml@main runtime_controller: github://n-IA-hane/esphome-runtime-controller/packages/runtime_controller/full_controller.yaml@main voice_assistant_runtime_controls: github://n-IA-hane/esphome-intercom/packages/voice_assistant/runtime_controls.yaml@main call_settings: github://n-IA-hane/esphome-intercom/packages/voip/call_settings.yaml@main ha_api: github://n-IA-hane/esphome-intercom/packages/voip/ha_api_runtime.yaml@main voice_assistant_local_commands: github://n-IA-hane/esphome-intercom/packages/voice_assistant/local_commands.yaml@main ota_maintenance: github://n-IA-hane/esphome-intercom/packages/ota/full_audio_maintenance.yaml@main voip_transport: github://n-IA-hane/esphome-intercom/packages/voip/transport.yaml@main phonebook_subscribe: github://n-IA-hane/esphome-intercom/packages/voip/phonebook_subscribe.yaml@main speaker_mute: github://n-IA-hane/esphome-intercom/packages/voip/speaker_mute_runtime.yaml@main s3_base: github://n-IA-hane/esphome-intercom/packages/platform/esp32s3_base.yaml@main native_diagnostics: github://n-IA-hane/esphome-intercom/packages/diagnostics/native.yaml@main status_led: github://n-IA-hane/esphome-intercom/packages/led/status_ws2812_single.yaml@main speaker_mixer: github://n-IA-hane/esphome-intercom/packages/audio/shared_mono_mixer_48k.yaml@main speaker_media_player: github://n-IA-hane/esphome-intercom/packages/media_player/runtime_controller_mono_media_player_48k.yaml@main flac_codecs_psram: github://n-IA-hane/esphome-intercom/packages/audio/flac_codecs_psram.yaml@main audio_stack_controls: github://n-IA-hane/esphome-intercom/packages/esp_audio_stack/controls.yaml@main afe_entities: github://n-IA-hane/esphome-intercom/packages/esp_afe/single_mic_entities.yaml@main # ============================================================================= # BOARD AND MEMORY # ============================================================================= # Device identity exposed to ESPHome, HA and the shared VoIP phonebook. esphome: name: ${name} friendly_name: ${friendly_name} platformio_options: board_build.flash_mode: dio # ESP32-S3 target and ESP-IDF tuning for the experimental single-mic AFE profile. esp32: board: esp32-s3-devkitc-1 flash_size: 16MB variant: esp32s3 framework: type: esp-idf advanced: compiler_optimization: PERF sdkconfig_options: # Allow mbedtls to allocate its working buffers in PSRAM. CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC: "y" # Route ESP-IDF stdout (and ESPHome logger when hardware_uart=USB_SERIAL_JTAG) # to the built-in USB-Serial-JTAG peripheral. Required on boards where the # USB-C connector is wired to the S3's native USB-Serial-JTAG (e.g. Freenove, # bare ESP32-S3-DevKitC-1) rather than to an external USB-UART chip. CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG: "y" CONFIG_ESP_CONSOLE_SECONDARY_NONE: "y" # PSRAM gives VA, media, AFE and VoIP buffers room on generic S3 boards. psram: mode: quad speed: 80MHz # Persist user settings without excessive flash writes. preferences: flash_write_interval: 5min # ============================================================================= # EXTERNAL COMPONENTS # ============================================================================= # Project components for full VA/VoIP, runtime controller and experimental AFE. external_components: - source: ${voip_stack_components_source} components: [voip_stack] - source: ${ext_components_source} components: [speaker, voice_assistant] - source: ${runtime_controller_components_source} components: [runtime_controller] - source: ${audio_stack_components_source} components: [esp_audio_stack, esp_afe] # ============================================================================= # CONNECTIVITY # ============================================================================= # Normal Wi-Fi client mode. Power save is disabled for low-latency SIP/RTP and # reliable HA API updates. wifi: ssid: !secret wifi_ssid password: !secret wifi_password power_save_mode: NONE # Home Assistant native API for entities, service actions and phonebook sync. api: # OTA update endpoint used by ESPHome Dashboard and `esphome upload`. ota: - platform: esphome # USB Serial/JTAG logger. Keep project domains visible while this profile is # still marked untested. logger: hardware_uart: USB_SERIAL_JTAG # DEBUG keeps SIP bridge signaling + AEC/AFE/i2s lifecycle visible. # Mute chatty ESPHome built-ins below; project components stay at DEBUG. level: INFO logs: sensor: WARN text_sensor: WARN binary_sensor: WARN switch: WARN number: WARN button: WARN api: WARN api.connection: WARN component: WARN api.service: INFO light: INFO select: INFO speaker: INFO audio: INFO micro_wake_word: INFO voice_assistant: INFO media_player: INFO resampler: INFO mixer_speaker: INFO wifi: INFO json: INFO voip_stack: INFO # ============================================================================= # AUDIO PIPELINE # ============================================================================= esp_audio_stack: id: audio_stack i2s_lrclk_pin: ${i2s_lrclk_pin} i2s_bclk_pin: ${i2s_bclk_pin} i2s_din_pin: ${i2s_din_pin} i2s_dout_pin: ${i2s_dout_pin} sample_rate: 48000 output_sample_rate: 16000 slot_bit_width: 32 correct_dc_offset: true processor_id: afe_processor # No-codec AEC reference. ring_buffer is the Espressif/ADF TYPE2-style # software reference: speaker PCM is staged in a delay-tunable ring before # being fed to AFE. Use previous_frame only for lighter custom builds. aec_reference: previous_frame buffers_in_psram: true esp_afe: id: afe_processor type: fd mode: high_perf mic_num: 1 aec_enabled: true aec_filter_length: 4 aec_nlp_level: normal se_enabled: false ns_enabled: true vad_enabled: false continuous_vad: true agc_enabled: true memory_alloc_mode: more_psram task_core: 1 task_priority: 5 ringbuf_size: 8 feed_task_core: 0 feed_task_priority: 5 feed_task_stack_size: 3072 fetch_task_core: 1 fetch_task_priority: 5 fetch_task_stack_size: 3072 feed_buf_in_psram: false feed_ring_in_psram: true fetch_ring_in_psram: false microphone: - platform: esp_audio_stack id: mic_main esp_audio_stack_id: audio_stack speaker: - platform: esp_audio_stack id: hw_speaker esp_audio_stack_id: audio_stack sample_rate: 48000 bits_per_sample: 16 timeout: 1s # ============================================================================= # MEDIA, ASSIST AND VOIP # ============================================================================= # Generic AFE targets are intended for 8 MB or larger flash layouts, so keep # the full VA timer alarm that the 4 MB AEC-light profile omits. audio_file: - id: timer_finished_sound file: ${assets_base}assets/sounds/timer_finished.flac micro_wake_word: microphone: mic_main models: - model: alexa voice_assistant: microphone: mic_main media_player: speaker_media_player micro_wake_word: mww noise_suppression_level: 2 auto_gain: 31dBFS volume_multiplier: 2.0 voip_stack: task_stacks_in_psram: true # save ~28KB internal heap on S3 by placing task stacks in PSRAM buffers_in_psram: true # VoIP staging buffers in PSRAM; AEC/AFE buffers live in esp_audio_stack audio: # Primary/preferred RTP profile: 48 kHz/10 ms toward the speaker, and a # matching 10 ms TX reframe of the 16 kHz AFE mic for peers that negotiate # ptime 10. 32 kHz/16 ms remains an alternate advertised format. tx_formats: - sample_rate: 16000 pcm_format: s16le channels: 1 frame_ms: 10 rx: sample_rate: 48000 pcm_format: s16le channels: 1 frame_ms: 10 # Additional accepted RX formats for direct ESP-to-ESP calls. These are valid # only because speaker: voip_speaker_input points at the resampler; do # not advertise additional formats when wiring voip_stack directly to a # fixed-rate mixer input. rx_formats: - sample_rate: 32000 pcm_format: s16le channels: 1 frame_ms: 16 - sample_rate: 16000 pcm_format: s16le channels: 1 frame_ms: 10 - sample_rate: 16000 pcm_format: s16le channels: 1 frame_ms: 16 - sample_rate: 16000 pcm_format: s16le channels: 1 frame_ms: 32 microphone: mic_main speaker: voip_speaker_input ringing_timeout: 30s delete_contact_missing_from: updates_number: 1 # ============================================================================= # RUNTIME LOGIC AND ENTITIES # ============================================================================= script: - id: timer_alarm_loop mode: single then: - while: condition: runtime_controller.is_active: id: runtime activity: timer_ringing then: - media_player.play_media: id: speaker_media_player media_url: "audio-file://timer_finished_sound" announcement: true - delay: 6s - id: timer_alarm_auto_stop mode: restart then: - delay: 15min - runtime_controller.event: id: runtime event: timer_stopped # Local BOOT button: short press controls VA/call toggle, double press changes # contact, long press toggles microphone mute. binary_sensor: # BOOT button (GPIO0), multi-click - platform: gpio name: Boot Button id: boot_button pin: number: GPIO0 inverted: true mode: input: true pullup: true on_multi_click: # Single click: call toggle if VoIP call active, otherwise VA start/stop - timing: - ON for at most 500ms - OFF for at least 400ms then: - if: condition: runtime_controller.is_active: id: runtime activity: timer_ringing then: - runtime_controller.event: id: runtime event: timer_stopped else: - if: condition: not: - voip_stack.is_idle: then: - voip_stack.call_toggle: else: - if: condition: voice_assistant.is_running: then: - voice_assistant.stop: else: - voice_assistant.start: # Double click: next contact - timing: - ON for 50ms to 500ms - OFF for at most 300ms - ON for 50ms to 500ms - OFF for at least 400ms then: - voip_stack.next_contact: # Long press (1-3s): toggle mute - timing: - ON for 1s to 3s - OFF for at least 300ms then: - switch.toggle: mute select: - platform: template name: AEC Mode id: aec_mode_select icon: mdi:tune-vertical # Full-experience default: use the Espressif AFE pipeline. Generic single-mic # targets stay on SR modes by default to preserve MWW spectral features while # still using AFE-owned AEC/NS/AGC plumbing. options: - "fd_low_cost" - "fd_high_perf" initial_option: "fd_high_perf" # Non-optimistic: HA reflects the live component mode, not the value the # user clicked. If the switch is rejected (e.g. internal RAM fragmented) # or the component falls back to the previous mode, the select shows the # actual mode rather than a fictitious one. optimistic: false restore_value: false set_action: - if: condition: lambda: 'return id(init_in_progress);' then: - logger.log: "AEC mode select: skipping during boot" else: - if: condition: or: - voice_assistant.is_running: - not: voip_stack.is_idle: then: - logger.log: level: WARN format: "Cannot switch AEC mode during active VA/VoIP" else: - logger.log: format: "Switching AEC mode to %s" args: ['x.c_str()'] - micro_wake_word.stop: - esp_audio_stack.stop_and_wait: audio_stack - wait_until: condition: esp_audio_stack.is_idle: audio_stack timeout: 2s - esp_afe.set_mode: id: afe_processor mode: !lambda 'return x;' - wait_until: condition: lambda: 'return id(afe_processor).is_reconfigure_idle();' timeout: 20s - if: condition: lambda: 'return id(afe_processor).is_reconfigure_idle() && id(afe_processor).get_last_reconfigure_ok();' then: - esp_audio_stack.start: audio_stack - delay: 300ms - if: condition: switch.is_off: mute then: - micro_wake_word.start: else: - logger.log: level: ERROR tag: main format: "AFE mode switch did not complete cleanly; audio restart skipped" # Reflect actual live mode (covers rejection and silent hardware mode). - lambda: |- id(aec_mode_select).publish_state(id(afe_processor).get_mode_name());