diff --git a/Buildscripts/properties.cmake b/Buildscripts/properties.cmake index ed7eea059..7b58bed49 100644 --- a/Buildscripts/properties.cmake +++ b/Buildscripts/properties.cmake @@ -1,4 +1,11 @@ function(GET_PROPERTY_VALUE PROPERTIES_CONTENT_VAR KEY_NAME RESULT_VAR) + # Optional 4th arg: default value returned when key is missing, instead of erroring. + set(has_default FALSE) + if (${ARGC} GREATER 3) + set(has_default TRUE) + set(default_value "${ARGV3}") + endif () + # Search for the key and its value in the properties content # Supports KEY=VALUE, KEY="VALUE", and optional spaces around = # Use parentheses to capture the value @@ -6,6 +13,8 @@ function(GET_PROPERTY_VALUE PROPERTIES_CONTENT_VAR KEY_NAME RESULT_VAR) # So we look for the key at the beginning of the string or after a newline. if ("${${PROPERTIES_CONTENT_VAR}}" MATCHES "(^|\n)${KEY_NAME}[ \t]*=[ \t]*\"?([^\n\"]*)\"?") set(${RESULT_VAR} "${CMAKE_MATCH_2}" PARENT_SCOPE) + elseif (has_default) + set(${RESULT_VAR} "${default_value}" PARENT_SCOPE) else () message(FATAL_ERROR "Property '${KEY_NAME}' not found in the properties content.") endif () diff --git a/Devices/guition-jc3248w535c/device.properties b/Devices/guition-jc3248w535c/device.properties index 88497b41a..15da6f6eb 100644 --- a/Devices/guition-jc3248w535c/device.properties +++ b/Devices/guition-jc3248w535c/device.properties @@ -19,3 +19,5 @@ display.shape=rectangle display.dpi=165 lvgl.colorDepth=16 + +sdkconfig.CONFIG_CODEC_DUMMY_SUPPORT=y diff --git a/Devices/guition-jc3248w535c/devicetree.yaml b/Devices/guition-jc3248w535c/devicetree.yaml index 7b534b584..a674aac66 100644 --- a/Devices/guition-jc3248w535c/devicetree.yaml +++ b/Devices/guition-jc3248w535c/devicetree.yaml @@ -1,3 +1,5 @@ dependencies: - Platforms/platform-esp32 + - Drivers/dummy-i2s-amp-module + - Drivers/audio-stream-module dts: guition,jc3248w535c.dts diff --git a/Devices/guition-jc3248w535c/guition,jc3248w535c.dts b/Devices/guition-jc3248w535c/guition,jc3248w535c.dts index a3aabdcd9..42d79b28c 100644 --- a/Devices/guition-jc3248w535c/guition,jc3248w535c.dts +++ b/Devices/guition-jc3248w535c/guition,jc3248w535c.dts @@ -10,6 +10,7 @@ #include #include #include +#include / { compatible = "root"; @@ -54,6 +55,11 @@ pin-data-out = <&gpio0 41 GPIO_FLAG_NONE>; }; + speaker0 { + compatible = "nsiway,ns4168"; + i2s = <&i2s0>; + }; + spi0 { compatible = "espressif,esp32-spi"; host = ; diff --git a/Devices/guition-jc8048w550c/device.properties b/Devices/guition-jc8048w550c/device.properties index b0450a3c8..3cebf4d42 100644 --- a/Devices/guition-jc8048w550c/device.properties +++ b/Devices/guition-jc8048w550c/device.properties @@ -19,3 +19,5 @@ display.dpi=187 lvgl.colorDepth=16 lvgl.fontSize=18 + +sdkconfig.CONFIG_CODEC_DUMMY_SUPPORT=y diff --git a/Devices/guition-jc8048w550c/devicetree.yaml b/Devices/guition-jc8048w550c/devicetree.yaml index 07eeef5e7..a4e919e7c 100644 --- a/Devices/guition-jc8048w550c/devicetree.yaml +++ b/Devices/guition-jc8048w550c/devicetree.yaml @@ -1,3 +1,5 @@ dependencies: - Platforms/platform-esp32 + - Drivers/dummy-i2s-amp-module + - Drivers/audio-stream-module dts: guition,jc8048w550c.dts diff --git a/Devices/guition-jc8048w550c/guition,jc8048w550c.dts b/Devices/guition-jc8048w550c/guition,jc8048w550c.dts index 64c063463..78757856b 100644 --- a/Devices/guition-jc8048w550c/guition,jc8048w550c.dts +++ b/Devices/guition-jc8048w550c/guition,jc8048w550c.dts @@ -9,6 +9,7 @@ #include #include #include +#include / { compatible = "root"; @@ -54,6 +55,11 @@ pin-data-out = <&gpio0 17 GPIO_FLAG_NONE>; }; + speaker0 { + compatible = "nsiway,ns4168"; + i2s = <&i2s0>; + }; + spi0 { compatible = "espressif,esp32-spi"; host = ; diff --git a/Devices/lilygo-tdeck-plus/device.properties b/Devices/lilygo-tdeck-plus/device.properties index 462968449..3aaf6fbba 100644 --- a/Devices/lilygo-tdeck-plus/device.properties +++ b/Devices/lilygo-tdeck-plus/device.properties @@ -21,3 +21,5 @@ display.dpi=143 cdn.infoMessage=To put the device into bootloader mode:
1. Press the trackball and then the reset button at the same time,
2. Let go of the reset button, then the trackball.

When this website reports that flashing is finished, you likely have to press the reset button. lvgl.colorDepth=16 + +sdkconfig.CONFIG_CODEC_DUMMY_SUPPORT=y diff --git a/Devices/lilygo-tdeck-plus/devicetree.yaml b/Devices/lilygo-tdeck-plus/devicetree.yaml index 0093181fc..805ca1eaa 100644 --- a/Devices/lilygo-tdeck-plus/devicetree.yaml +++ b/Devices/lilygo-tdeck-plus/devicetree.yaml @@ -3,4 +3,7 @@ dependencies: - Drivers/st7789-module - Drivers/gt911-module - Drivers/lilygo-module + - Drivers/es7210-module + - Drivers/dummy-i2s-amp-module + - Drivers/audio-stream-module dts: lilygo,tdeck-plus.dts diff --git a/Devices/lilygo-tdeck-plus/lilygo,tdeck-plus.dts b/Devices/lilygo-tdeck-plus/lilygo,tdeck-plus.dts index 121c72c81..263503a46 100644 --- a/Devices/lilygo-tdeck-plus/lilygo,tdeck-plus.dts +++ b/Devices/lilygo-tdeck-plus/lilygo,tdeck-plus.dts @@ -16,6 +16,8 @@ #include #include +#include +#include #include #include @@ -64,6 +66,34 @@ pin-click = <&gpio0 0 GPIO_FLAG_NONE>; }; + // i2s0 and i2s1 are declared before i2c0 so i2s1 has started before the es7210 codec (below) references it. + // Speaker I2S (MAX98357A amplifier) + i2s0 { + compatible = "espressif,esp32-i2s"; + port = ; + pin-bclk = <&gpio0 7 GPIO_FLAG_NONE>; + pin-ws = <&gpio0 5 GPIO_FLAG_NONE>; + pin-data-out = <&gpio0 6 GPIO_FLAG_NONE>; + }; + + // Microphone I2S (ES7210), separate port from the speaker + i2s1 { + compatible = "espressif,esp32-i2s"; + port = ; + pin-bclk = <&gpio0 47 GPIO_FLAG_NONE>; + pin-ws = <&gpio0 21 GPIO_FLAG_NONE>; + pin-data-in = <&gpio0 14 GPIO_FLAG_NONE>; + pin-mclk = <&gpio0 48 GPIO_FLAG_NONE>; + }; + + // MAX98357A class-D speaker amplifier. No I2C. Per schematic, SD_MODE/GAIN_SLOT is + // tied via fixed resistors (R21/R22), not driven by an MCU GPIO -- the amp is always + // enabled when powered, so no enable-gpio is wired here. + speaker0 { + compatible = "maxim,max98357a"; + i2s = <&i2s0>; + }; + i2c_internal: i2c0 { compatible = "espressif,esp32-i2c"; port = ; @@ -90,14 +120,18 @@ compatible = "lilygo,tdeck-keyboard-backlight"; reg = <0x55>; }; - }; - i2s0 { - compatible = "espressif,esp32-i2s"; - port = ; - pin-bclk = <&gpio0 7 GPIO_FLAG_NONE>; - pin-ws = <&gpio0 5 GPIO_FLAG_NONE>; - pin-data-out = <&gpio0 6 GPIO_FLAG_NONE>; + // ES7210 microphone ADC. Per schematic all 4 MSM381A3729H9CP mic capsules are + // populated (MIC1-4), and AD0/AD1 are unconnected (default low -> address 0x40). + // Stock LilyGO firmware drives MIC1/MIC2 at 0dB and MIC3/MIC4 at 37.5dB gain -- + // the es7210 driver here applies a single gain to all active mics, so per-pair + // gain differentiation is not yet replicated. + es7210 { + compatible = "everest,es7210"; + reg = <0x40>; + i2s = <&i2s1>; + mic-mask = <15>; + }; }; display_backlight { diff --git a/Devices/m5stack-cardputer/device.properties b/Devices/m5stack-cardputer/device.properties index 3f66c75a4..a59bb26e0 100644 --- a/Devices/m5stack-cardputer/device.properties +++ b/Devices/m5stack-cardputer/device.properties @@ -21,3 +21,5 @@ display.dpi=139 lvgl.colorDepth=16 lvgl.uiDensity=compact + +sdkconfig.CONFIG_CODEC_DUMMY_SUPPORT=y diff --git a/Devices/m5stack-cardputer/devicetree.yaml b/Devices/m5stack-cardputer/devicetree.yaml index 5145267e9..5860a2387 100644 --- a/Devices/m5stack-cardputer/devicetree.yaml +++ b/Devices/m5stack-cardputer/devicetree.yaml @@ -2,4 +2,7 @@ dependencies: - Platforms/platform-esp32 - Drivers/st7789-module - Drivers/m5stack-module + - Drivers/dummy-i2s-amp-module + - Drivers/pdm-mic-module + - Drivers/audio-stream-module dts: m5stack,cardputer.dts diff --git a/Devices/m5stack-cardputer/m5stack,cardputer.dts b/Devices/m5stack-cardputer/m5stack,cardputer.dts index a6030fb69..90e7383e7 100644 --- a/Devices/m5stack-cardputer/m5stack,cardputer.dts +++ b/Devices/m5stack-cardputer/m5stack,cardputer.dts @@ -16,6 +16,8 @@ #include #include +#include +#include // Reference: https://docs.m5stack.com/en/core/Cardputer / { @@ -123,8 +125,17 @@ <&gpio0 7 GPIO_FLAG_NONE>; }; - // Speaker and microphone - // TODO: Microphone I2C init code from https://github.com/m5stack/M5Unified/blob/a6256725481f1bc366655fa48cf03b6095e30ad1/src/M5Unified.cpp#L936 + // Speaker (std TX) and PDM mic (RX) share this single I2S controller. PDM RX is + // hardware-restricted to I2S controller 0 on ESP32 targets, and per schematic the + // mic's PDM CLK and the speaker's LRCLK both land on GPIO43 anyway -- splitting them + // across two controllers (as M5Stack's own reference firmware does) isn't possible + // here since Tactility's GPIO ownership model is exclusive-acquire-only (no pin + // sharing), and two I2S controllers both declaring GPIO43 would have one fail to + // start. Keeping both on one controller means only one acquire of GPIO43 happens; + // audio_codec_data_if_i2s.c's data_set_fmt() already tears down and recreates the + // channel on each codec open(), so opening one direction after the other naturally + // reconfigures the shared port/pin -- speaker and mic still cannot be used + // simultaneously, which matches the hardware reality. i2s0 { compatible = "espressif,esp32-i2s"; port = ; @@ -133,4 +144,15 @@ pin-data-out = <&gpio0 42 GPIO_FLAG_NONE>; pin-data-in = <&gpio0 46 GPIO_FLAG_NONE>; }; + + speaker0 { + compatible = "nsiway,ns4168"; + i2s = <&i2s0>; + }; + + mic0 { + compatible = "generic,spm1423"; + i2s = <&i2s0>; + channels = <1>; + }; }; diff --git a/Devices/m5stack-stackchan/CMakeLists.txt b/Devices/m5stack-stackchan/CMakeLists.txt index 0803166e6..8dc689005 100644 --- a/Devices/m5stack-stackchan/CMakeLists.txt +++ b/Devices/m5stack-stackchan/CMakeLists.txt @@ -3,5 +3,5 @@ file(GLOB_RECURSE SOURCE_FILES Source/*.c*) idf_component_register( SRCS ${SOURCE_FILES} INCLUDE_DIRS "Source" - REQUIRES Tactility esp_lvgl_port ILI934x FT6x36 AXP2101 AW9523 driver vfs fatfs ina226-module py32ioexpander-module + REQUIRES Tactility esp_lvgl_port ILI934x FT6x36 AXP2101 driver vfs fatfs ina226-module py32ioexpander-module ) diff --git a/Devices/m5stack-stackchan/Source/Configuration.cpp b/Devices/m5stack-stackchan/Source/Configuration.cpp index 2faa63b30..9ebed9e7e 100644 --- a/Devices/m5stack-stackchan/Source/Configuration.cpp +++ b/Devices/m5stack-stackchan/Source/Configuration.cpp @@ -2,6 +2,7 @@ #include "devices/Power.h" #include +#include #include #include #include @@ -20,38 +21,45 @@ static const auto* TAG = "StackChan"; // I2C addresses // --------------------------------------------------------------------------- static constexpr uint8_t AXP2101_ADDR = 0x34; -static constexpr uint8_t AW9523B_ADDR = 0x58; -static constexpr uint8_t AW88298_ADDR = 0x36; -static constexpr uint8_t ES7210_ADDR = 0x40; // --------------------------------------------------------------------------- -// AW9523B GPIO expander — same wiring as CoreS3 +// AW9523B GPIO expander pin map — same wiring as CoreS3. +// AW88298 reset (P0_2) is driven directly by the aw88298-module driver itself +// (see m5stack,stackchan.dts pin-reset property), not from here. // --------------------------------------------------------------------------- -// P0 pins: 0=touch reset, 1=bus out enable, 2=AW88298 reset, 4=SD card, 5=USB OTG -// P1 pins: 0=cam reset, 1=LCD reset, 7=boost enable (SY7088) -static constexpr uint8_t AW9523B_CTL_REG = 0x11; // P0 push-pull mode -static constexpr uint8_t AW9523B_P0_REG = 0x02; -static constexpr uint8_t AW9523B_P1_REG = 0x03; - -static bool initGpioExpander(::Device* i2c) { - // P0: touch, bus enable, AW88298 reset, SD card switch - constexpr uint8_t p0 = (1U << 0U) | (1U << 1U) | (1U << 2U) | (1U << 4U); - // P1: LCD reset, boost enable - constexpr uint8_t p1 = (1U << 1U) | (1U << 7U); - - // Set P0 to push-pull mode - if (i2c_controller_register8_set(i2c, AW9523B_ADDR, AW9523B_CTL_REG, 0x10, pdMS_TO_TICKS(1000)) != ERROR_NONE) { - LOG_E(TAG, "AW9523B: Failed to set CTL"); - return false; - } - if (i2c_controller_register8_set(i2c, AW9523B_ADDR, AW9523B_P0_REG, p0, pdMS_TO_TICKS(1000)) != ERROR_NONE) { - LOG_E(TAG, "AW9523B: Failed to set P0"); - return false; - } - if (i2c_controller_register8_set(i2c, AW9523B_ADDR, AW9523B_P1_REG, p1, pdMS_TO_TICKS(1000)) != ERROR_NONE) { - LOG_E(TAG, "AW9523B: Failed to set P1"); - return false; +constexpr auto AW9523B_PIN_TOUCH_RESET = 0; // P0_0 +constexpr auto AW9523B_PIN_BUS_OUT_ENABLE = 1; // P0_1 +constexpr auto AW9523B_PIN_SD_CARD_SWITCH = 4; // P0_4 +constexpr auto AW9523B_PIN_LCD_RESET = 8 + 1; // P1_1 +constexpr auto AW9523B_PIN_BOOST_ENABLE = 8 + 7; // P1_7 (SY7088) + +static bool initGpioExpander(::Device* aw9523b) { + struct PinInit { uint8_t pin; bool level; }; + static constexpr PinInit pins[] = { + { AW9523B_PIN_TOUCH_RESET, true }, + { AW9523B_PIN_BUS_OUT_ENABLE, true }, + { AW9523B_PIN_SD_CARD_SWITCH, true }, + { AW9523B_PIN_LCD_RESET, true }, + { AW9523B_PIN_BOOST_ENABLE, true }, + }; + + for (const auto& pinInit : pins) { + auto* descriptor = gpio_descriptor_acquire(aw9523b, pinInit.pin, GPIO_OWNER_GPIO); + if (descriptor == nullptr) { + LOG_E(TAG, "AW9523B: Failed to acquire pin %u", pinInit.pin); + return false; + } + error_t error = gpio_descriptor_set_flags(descriptor, GPIO_FLAG_DIRECTION_OUTPUT); + if (error == ERROR_NONE) { + error = gpio_descriptor_set_level(descriptor, pinInit.level); + } + gpio_descriptor_release(descriptor); + if (error != ERROR_NONE) { + LOG_E(TAG, "AW9523B: Failed to configure pin %u", pinInit.pin); + return false; + } } + return true; } @@ -80,100 +88,7 @@ static bool initPowerControl(::Device* i2c) { } // --------------------------------------------------------------------------- -// AW88298 speaker amplifier -// Called once in initBoot() at 16kHz. AW88298 default state after hardware reset -// is sufficient for SfxEngine. Sequence mirrors M5Unified _speaker_enabled_cb_cores3(). -// NOTE: 44100Hz (AudioPlayer/MusicPlayer) needs esp_codec_dev integration — see memory notes. -// --------------------------------------------------------------------------- -static bool initSpeaker(::Device* i2c, uint32_t sample_rate_hz) { - // M5Unified rate table: (sample_rate + 1102) / 2205 steps, first entry >= result - static constexpr uint8_t rate_tbl[] = { 4, 5, 6, 8, 10, 11, 15, 20, 22, 44 }; - size_t reg06_idx = 0; - size_t rate = (sample_rate_hz + 1102) / 2205; - while (rate > rate_tbl[reg06_idx] && ++reg06_idx < sizeof(rate_tbl)) {} - if (reg06_idx >= sizeof(rate_tbl)) { - reg06_idx = sizeof(rate_tbl) - 1; // clamp to max supported rate - } - // 0x14C0: M5Unified's upper byte for CoreS3, I2SBCK=0 (BCK 16*2) - const uint16_t reg06 = static_cast(0x14C0U | reg06_idx); - - // Hardware reset AW88298 via AW9523B P0 bit 2: pull LOW (reset), then HIGH (release). - if (i2c_controller_register8_reset_bits(i2c, AW9523B_ADDR, AW9523B_P0_REG, 0b00000100, pdMS_TO_TICKS(100)) != ERROR_NONE) { - LOG_E(TAG, "AW9523B: failed to assert AW88298 reset"); - return false; - } - vTaskDelay(pdMS_TO_TICKS(10)); - if (i2c_controller_register8_set_bits(i2c, AW9523B_ADDR, AW9523B_P0_REG, 0b00000100, pdMS_TO_TICKS(100)) != ERROR_NONE) { - LOG_E(TAG, "AW9523B: failed to release AW88298 reset"); - return false; - } - vTaskDelay(pdMS_TO_TICKS(50)); - - // Exact sequence from M5Unified _speaker_enabled_cb_cores3() — no I2C software reset. - struct { uint8_t reg; uint16_t val; } regs[] = { - { 0x61, 0x0673 }, // boost mode disabled - { 0x04, 0x4040 }, // I2SEN=1, AMPPD=0, PWDN=0 - { 0x05, 0x0008 }, // RMSE=0, HAGCE=0, HDCCE=0, HMUTE=0 - { 0x06, reg06 }, // I2S mode + sample rate - { 0x0C, 0x3064 }, // volume -24dB - }; - for (auto& r : regs) { - if (i2c_controller_register16be_set(i2c, AW88298_ADDR, r.reg, r.val, pdMS_TO_TICKS(100)) != ERROR_NONE) { - LOG_E(TAG, "AW88298: failed reg 0x%02X", r.reg); - return false; - } - } - - LOG_I(TAG, "AW88298 initialized (%luHz, reg06=0x%04X)", (unsigned long)sample_rate_hz, reg06); - return true; -} - -// --------------------------------------------------------------------------- -// ES7210 microphone ADC -// Source: https://github.com/m5stack/M5Unified -// --------------------------------------------------------------------------- -static bool initMicrophone(::Device* i2c) { - static constexpr uint8_t reg_data[] = { - 0x00, 0x41, // RESET_CTL - 0x01, 0x1F, // CLK_ON_OFF (initial) - 0x06, 0x00, // DIGITAL_PDN - 0x07, 0x20, // ADC_OSR - 0x08, 0x10, // MODE_CFG - 0x09, 0x30, // TCT0_CHPINI - 0x0A, 0x30, // TCT1_CHPINI - 0x20, 0x0A, // ADC34_HPF2 - 0x21, 0x2A, // ADC34_HPF1 - 0x22, 0x0A, // ADC12_HPF2 - 0x23, 0x2A, // ADC12_HPF1 - 0x02, 0xC1, - 0x04, 0x01, - 0x05, 0x00, - 0x11, 0x60, - 0x40, 0x42, // ANALOG_SYS - 0x41, 0x70, // MICBIAS12 - 0x42, 0x70, // MICBIAS34 - 0x43, 0x1B, // MIC1_GAIN - 0x44, 0x1B, // MIC2_GAIN - 0x45, 0x00, // MIC3_GAIN - 0x46, 0x00, // MIC4_GAIN - 0x47, 0x00, // MIC1_LP - 0x48, 0x00, // MIC2_LP - 0x49, 0x00, // MIC3_LP - 0x4A, 0x00, // MIC4_LP - 0x4B, 0x00, // MIC12_PDN - 0x4C, 0xFF, // MIC34_PDN - 0x01, 0x14, // CLK_ON_OFF (final) - }; - - if (i2c_controller_write_register_array(i2c, ES7210_ADDR, reg_data, sizeof(reg_data), pdMS_TO_TICKS(1000)) != ERROR_NONE) { - LOG_E(TAG, "ES7210: Failed to set registers"); - return false; - } - return true; -} - -// --------------------------------------------------------------------------- -// initBoot — called before device tree is started +// initBoot — called after devicetree devices are constructed/started // --------------------------------------------------------------------------- static std::shared_ptr axp2101; @@ -183,26 +98,22 @@ bool initBoot() { LOG_E(TAG, "i2c_internal not found"); return false; } + // Boost enable via AXP2101 before GPIO expander init (same as CoreS3) - // AW9523B P1 bit 7 = SY7088 boost enable — set after AXP2101 is configured if (!initPowerControl(i2c)) { LOG_E(TAG, "AXP2101 init failed"); return false; } - if (!initGpioExpander(i2c)) { - LOG_E(TAG, "AW9523B init failed"); + auto* aw9523b = device_find_by_name("aw9523b"); + if (aw9523b == nullptr) { + LOG_E(TAG, "aw9523b not found"); return false; } - // AW88298 default state after reset is sufficient for SfxEngine (16kHz). - // Full 44100Hz support requires esp_codec_dev integration (see memory notes). - if (!initSpeaker(i2c, 16000)) { - LOG_W(TAG, "AW88298 init failed (non-fatal)"); - } - - if (!initMicrophone(i2c)) { - LOG_W(TAG, "ES7210 init failed (non-fatal)"); + if (!initGpioExpander(aw9523b)) { + LOG_E(TAG, "AW9523B init failed"); + return false; } // Boot LED pattern — confirms PY32IOExpander is working. diff --git a/Devices/m5stack-stackchan/devicetree.yaml b/Devices/m5stack-stackchan/devicetree.yaml index d922e6394..ad01bb7b3 100644 --- a/Devices/m5stack-stackchan/devicetree.yaml +++ b/Devices/m5stack-stackchan/devicetree.yaml @@ -1,7 +1,11 @@ dependencies: -- Platforms/platform-esp32 -- Drivers/bmi270-module -- Drivers/bm8563-module -- Drivers/ina226-module -- Drivers/py32ioexpander-module + - Platforms/platform-esp32 + - Drivers/bmi270-module + - Drivers/bm8563-module + - Drivers/ina226-module + - Drivers/py32ioexpander-module + - Drivers/aw9523b-module + - Drivers/aw88298-module + - Drivers/es7210-module + - Drivers/audio-stream-module dts: m5stack,stackchan.dts diff --git a/Devices/m5stack-stackchan/m5stack,stackchan.dts b/Devices/m5stack-stackchan/m5stack,stackchan.dts index 98b678fc7..4571aa0f5 100644 --- a/Devices/m5stack-stackchan/m5stack,stackchan.dts +++ b/Devices/m5stack-stackchan/m5stack,stackchan.dts @@ -13,6 +13,9 @@ #include #include #include +#include +#include +#include #include #include @@ -36,6 +39,17 @@ gpio-count = <49>; }; + // AW88298 speaker + ES7210 microphone + i2s0: i2s0 { + compatible = "espressif,esp32-i2s"; + port = ; + pin-bclk = <&gpio0 34 GPIO_FLAG_NONE>; + pin-ws = <&gpio0 33 GPIO_FLAG_NONE>; + pin-data-out = <&gpio0 13 GPIO_FLAG_NONE>; + pin-data-in = <&gpio0 14 GPIO_FLAG_NONE>; + pin-mclk = <&gpio0 0 GPIO_FLAG_NONE>; + }; + i2c_internal { compatible = "espressif,esp32-i2c"; port = ; @@ -65,10 +79,37 @@ shunt-milliohms = <10>; }; + // AW9523B pin map (same wiring as CoreS3): + // P0_0 = touch reset, P0_1 = bus-out enable, P0_2 = AW88298 reset, + // P0_4 = SD card switch, P1_1 = LCD reset, P1_7 = boost enable (SY7088) + aw9523b: aw9523b { + compatible = "awinic,aw9523b"; + reg = <0x58>; + }; + + aw88298 { + compatible = "awinic,aw88298"; + reg = <0x36>; + i2s = <&i2s0>; + pin-reset = <&aw9523b 2 GPIO_FLAG_NONE>; + }; + + es7210 { + compatible = "everest,es7210"; + reg = <0x40>; + i2s = <&i2s0>; + // Only MIC1/MIC2 are wired to real capsules (see schematic); MIC3/MIC4 + // slots carry AEC_P/AEC_N reference signal, not microphone audio. + mic-mask = <3>; + // M5Unified applies a 4x post-gain "magnification" for this exact mic + // wiring (see M5Unified.cpp _mic_data_cb_cores3): the ES7210's own + // hardware ADC gain (already near-max via the default 90% input volume + // setting, ~34dB of its 0-37.5dB range) still isn't enough for these + // capsules on their own. + input-gain-percent = <400>; + }; + // AXP2101 PMIC @ 0x34 — initialized manually in initBoot() - // AW9523B GPIO expander @ 0x58 — initialized manually in initBoot() (same as CoreS3) - // AW88298 speaker amp @ 0x36 — initialized manually in initBoot() - // ES7210 microphone ADC @ 0x40 — initialized manually in initBoot() // FT6336U capacitive touch @ 0x38 — used by Display driver (FT6x36 library) // TODO: Si12T 3-zone head touch @ 0x68 — INT active-low, 10kΩ pull-up to 3.3V; driver not yet implemented @@ -135,15 +176,4 @@ frequency-khz = <20000>; }; }; - - // AW88298 speaker + ES7210 microphone - i2s0 { - compatible = "espressif,esp32-i2s"; - port = ; - pin-bclk = <&gpio0 34 GPIO_FLAG_NONE>; - pin-ws = <&gpio0 33 GPIO_FLAG_NONE>; - pin-data-out = <&gpio0 13 GPIO_FLAG_NONE>; - pin-data-in = <&gpio0 14 GPIO_FLAG_NONE>; - pin-mclk = <&gpio0 0 GPIO_FLAG_NONE>; - }; }; diff --git a/Devices/m5stack-sticks3/CMakeLists.txt b/Devices/m5stack-sticks3/CMakeLists.txt index a46669714..fbd5c16c6 100644 --- a/Devices/m5stack-sticks3/CMakeLists.txt +++ b/Devices/m5stack-sticks3/CMakeLists.txt @@ -3,5 +3,5 @@ file(GLOB_RECURSE SOURCE_FILES Source/*.c*) idf_component_register( SRCS ${SOURCE_FILES} INCLUDE_DIRS "Source" - REQUIRES Tactility esp_lvgl_port esp_lcd ST7789 PwmBacklight ButtonControl m5pm1-module vfs fatfs + REQUIRES Tactility esp_lvgl_port esp_lcd ST7789 PwmBacklight ButtonControl vfs fatfs m5pm1-module ) diff --git a/Devices/m5stack-sticks3/Source/Configuration.cpp b/Devices/m5stack-sticks3/Source/Configuration.cpp index 6a0fbf159..7a0558770 100644 --- a/Devices/m5stack-sticks3/Source/Configuration.cpp +++ b/Devices/m5stack-sticks3/Source/Configuration.cpp @@ -2,7 +2,6 @@ #include "devices/Power.h" #include -#include #include #include @@ -13,87 +12,25 @@ using namespace tt::hal; static constexpr auto* TAG = "StickS3"; -static constexpr uint8_t ES8311_I2C_ADDR = 0x18; - -static error_t initSound(::Device* i2c_controller) { - // Init data from M5Unified: - // https://github.com/m5stack/M5Unified/blob/master/src/M5Unified.cpp#L454 - static constexpr uint8_t ENABLED_BULK_DATA[] = { - 0x00, 0x80, // 0x00 RESET/ CSM POWER ON - 0x01, 0x3F, // 0x01 CLOCK_MANAGER/ use MCLK pin (external), all clocks on - 0x02, 0x00, // 0x02 CLOCK_MANAGER/ pre_div=1 pre_multi=1 (256x MCLK from ESP32 is correct ratio) - 0x0D, 0x01, // 0x0D SYSTEM/ Power up analog circuitry - 0x12, 0x00, // 0x12 SYSTEM/ power-up DAC - NOT default - 0x13, 0x10, // 0x13 SYSTEM/ Enable output to HP drive - NOT default - 0x32, 0xBF, // 0x32 DAC/ DAC volume (0xBF == ±0 dB ) - 0x37, 0x08, // 0x37 DAC/ Bypass DAC equalizer - NOT default - }; - - error_t error = i2c_controller_write_register_array( - i2c_controller, - ES8311_I2C_ADDR, - ENABLED_BULK_DATA, - sizeof(ENABLED_BULK_DATA), - pdMS_TO_TICKS(1000) - ); - if (error != ERROR_NONE) { - LOG_E(TAG, "Failed to enable ES8311: %s", error_to_string(error)); - return error; - } - - // Enable speaker amp via M5PM1 driver +// Audio codec register programming is owned by the es8311-module driver (registered as an +// AUDIO_CODEC_TYPE device, see m5stack,sticks3.dts). This board still needs to drive the +// AW8737 speaker amplifier's enable line via m5pm1 (PM1_G3), which is board wiring glue +// rather than codec configuration, so it stays here. +static void enableSpeakerAmplifier() { auto* m5pm1 = device_find_by_name("m5pm1"); - if (m5pm1 != nullptr) { - m5pm1_set_speaker_enable(m5pm1, true); - } else { - LOG_W(TAG, "m5pm1 not found — speaker amp not enabled"); + if (m5pm1 == nullptr) { + LOG_W(TAG, "m5pm1 not found -- speaker amp not enabled"); + return; } - return ERROR_NONE; -} - -static error_t initMicrophone(::Device* i2c_controller) { - // Init data from M5Unified: - // https://github.com/m5stack/M5Unified/blob/master/src/M5Unified.cpp#L842 - static constexpr uint8_t ENABLED_BULK_DATA[] = { - 0x00, 0x80, // 0x00 RESET/ CSM POWER ON - 0x01, 0x3F, // 0x01 CLOCK_MANAGER/ use MCLK pin (external), all clocks on - 0x02, 0x00, // 0x02 CLOCK_MANAGER/ pre_div=1 pre_multi=1 (256x MCLK from ESP32 is correct ratio) - 0x0D, 0x01, // 0x0D SYSTEM/ Power up analog circuitry - 0x0E, 0x02, // 0x0E SYSTEM/ : Enable analog PGA, enable ADC modulator - 0x14, 0x10, // ES8311_ADC_REG14 : select Mic1p-Mic1n / PGA GAIN (minimum) - 0x17, 0xFF, // ES8311_ADC_REG17 : ADC_VOLUME (MAXGAIN) // (0xBF == ± 0 dB ) - 0x1C, 0x6A, // ES8311_ADC_REG1C : ADC Equalizer bypass, cancel DC offset in digital domain - }; - - error_t error = i2c_controller_write_register_array( - i2c_controller, - ES8311_I2C_ADDR, - ENABLED_BULK_DATA, - sizeof(ENABLED_BULK_DATA), - pdMS_TO_TICKS(1000) - ); + error_t error = m5pm1_set_speaker_enable(m5pm1, true); if (error != ERROR_NONE) { - LOG_E(TAG, "Failed to enable ES8311: %s", error_to_string(error)); - return error; + LOG_E(TAG, "Failed to enable speaker amplifier: %s", error_to_string(error)); } - - return ERROR_NONE; } bool initBoot() { - auto* i2c_internal = device_find_by_name("i2c_internal"); - check(i2c_internal, "i2c_internal not found"); - - error_t error = initSound(i2c_internal); - if (error != ERROR_NONE) { - LOG_E(TAG, "Failed to enable ES8311 speaker"); - } - - error = initMicrophone(i2c_internal); - if (error != ERROR_NONE) { - LOG_E(TAG, "Failed to enable ES8311 microphone"); - } + enableSpeakerAmplifier(); return driver::pwmbacklight::init(GPIO_NUM_38, 512); } diff --git a/Devices/m5stack-sticks3/devicetree.yaml b/Devices/m5stack-sticks3/devicetree.yaml index 1022f4de9..fed26664f 100644 --- a/Devices/m5stack-sticks3/devicetree.yaml +++ b/Devices/m5stack-sticks3/devicetree.yaml @@ -1,5 +1,7 @@ dependencies: -- Platforms/platform-esp32 -- Drivers/bmi270-module -- Drivers/m5pm1-module + - Platforms/platform-esp32 + - Drivers/bmi270-module + - Drivers/m5pm1-module + - Drivers/es8311-module + - Drivers/audio-stream-module dts: m5stack,sticks3.dts diff --git a/Devices/m5stack-sticks3/m5stack,sticks3.dts b/Devices/m5stack-sticks3/m5stack,sticks3.dts index 289559870..f5a1c17da 100644 --- a/Devices/m5stack-sticks3/m5stack,sticks3.dts +++ b/Devices/m5stack-sticks3/m5stack,sticks3.dts @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -31,6 +32,17 @@ gpio-count = <49>; }; + // Speaker and microphone (ES8311) + i2s0 { + compatible = "espressif,esp32-i2s"; + port = ; + pin-bclk = <&gpio0 17 GPIO_FLAG_NONE>; + pin-ws = <&gpio0 15 GPIO_FLAG_NONE>; + pin-data-out = <&gpio0 14 GPIO_FLAG_NONE>; + pin-data-in = <&gpio0 16 GPIO_FLAG_NONE>; + pin-mclk = <&gpio0 18 GPIO_FLAG_NONE>; + }; + i2c_internal { compatible = "espressif,esp32-i2c"; port = ; @@ -47,6 +59,12 @@ compatible = "bosch,bmi270"; reg = <0x68>; }; + + es8311 { + compatible = "everest,es8311"; + reg = <0x18>; + i2s = <&i2s0>; + }; }; i2c_grove { @@ -69,17 +87,6 @@ }; }; - // Speaker and microphone (ES8311) - i2s0 { - compatible = "espressif,esp32-i2s"; - port = ; - pin-bclk = <&gpio0 17 GPIO_FLAG_NONE>; - pin-ws = <&gpio0 15 GPIO_FLAG_NONE>; - pin-data-out = <&gpio0 14 GPIO_FLAG_NONE>; - pin-data-in = <&gpio0 16 GPIO_FLAG_NONE>; - pin-mclk = <&gpio0 18 GPIO_FLAG_NONE>; - }; - uart_grove: uart1 { compatible = "espressif,esp32-uart"; status = "disabled"; diff --git a/Devices/m5stack-tab5/CMakeLists.txt b/Devices/m5stack-tab5/CMakeLists.txt index 25bc75361..ed36355cf 100644 --- a/Devices/m5stack-tab5/CMakeLists.txt +++ b/Devices/m5stack-tab5/CMakeLists.txt @@ -3,5 +3,5 @@ file(GLOB_RECURSE SOURCE_FILES Source/*.c*) idf_component_register( SRCS ${SOURCE_FILES} INCLUDE_DIRS "Source" - REQUIRES Tactility esp_lvgl_port esp_lcd EspLcdCompat esp_lcd_ili9881c esp_lcd_st7123 esp_lcd_touch_st7123 GT911 PwmBacklight driver esp_driver_i2c vfs fatfs ina226-module sc2356-module + REQUIRES Tactility esp_lvgl_port esp_lcd EspLcdCompat esp_lcd_ili9881c esp_lcd_st7123 esp_lcd_touch_st7123 GT911 PwmBacklight driver esp_driver_i2c vfs fatfs ina226-module ) diff --git a/Devices/m5stack-tab5/Source/Configuration.cpp b/Devices/m5stack-tab5/Source/Configuration.cpp index 4f6c3eabb..561fd198d 100644 --- a/Devices/m5stack-tab5/Source/Configuration.cpp +++ b/Devices/m5stack-tab5/Source/Configuration.cpp @@ -140,57 +140,14 @@ static void initExpander1(::Device* io_expander1) { gpio_descriptor_release(ip2326_charge_enable_pin); } -static error_t initSound(::Device* i2c_controller, ::Device* io_expander0 = nullptr) { - // Init data from M5Unified: - // https://github.com/m5stack/M5Unified/blob/master/src/M5Unified.cpp - static constexpr uint8_t ES8388_I2C_ADDR = 0x10; - static constexpr uint8_t ENABLED_BULK_DATA[] = { - 0, 0x80, // RESET/ CSM POWER ON - 0, 0x00, - 0, 0x00, - 0, 0x0E, - 1, 0x00, - 2, 0x0A, // CHIP POWER: power up all - 3, 0xFF, // ADC POWER: power down all - 4, 0x3C, // DAC POWER: power up and LOUT1/ROUT1/LOUT2/ROUT2 enable - 5, 0x00, // ChipLowPower1 - 6, 0x00, // ChipLowPower2 - 7, 0x7C, // VSEL - 8, 0x00, // set I2S slave mode - // reg9-22 == adc - 23, 0x18, // I2S format (16bit) - 24, 0x00, // I2S MCLK ratio (128) - 25, 0x20, // DAC unmute - 26, 0x00, // LDACVOL 0x00~0xC0 - 27, 0x00, // RDACVOL 0x00~0xC0 - 28, 0x08, // enable digital click free power up and down - 29, 0x00, - 38, 0x00, // DAC CTRL16 - 39, 0xB8, // LEFT Ch MIX - 42, 0xB8, // RIGHTCh MIX - 43, 0x08, // ADC and DAC separate - 45, 0x00, // 0x00=1.5k VREF analog output / 0x10=40kVREF analog output - 46, 0x21, - 47, 0x21, - 48, 0x21, - 49, 0x21 - }; - - error_t error = i2c_controller_write_register_array( - i2c_controller, - ES8388_I2C_ADDR, - ENABLED_BULK_DATA, - sizeof(ENABLED_BULK_DATA), - pdMS_TO_TICKS(1000) - ); - if (error != ERROR_NONE) { - LOG_E(TAG, "Failed to enable ES8388: %s", error_to_string(error)); - return error; - } - +// Audio codec register programming is owned by the es8388-module/es7210-module drivers +// (registered as AUDIO_CODEC_TYPE devices, see m5stack,tab5.dts). This board still needs +// to drive the external speaker amplifier's enable line via io_expander0, which is board +// wiring glue rather than codec configuration, so it stays here. +static error_t enableSpeakerAmplifier(::Device* io_expander0) { auto* speaker_enable_pin = gpio_descriptor_acquire(io_expander0, GPIO_EXP0_PIN_SPEAKER_ENABLE, GPIO_OWNER_GPIO); check(speaker_enable_pin, "Failed to acquire speaker enable pin"); - error = gpio_descriptor_set_level(speaker_enable_pin, true); + error_t error = gpio_descriptor_set_level(speaker_enable_pin, true); gpio_descriptor_release(speaker_enable_pin); if (error != ERROR_NONE) { LOG_E(TAG, "Failed to enable amplifier: %s", error_to_string(error)); @@ -200,56 +157,6 @@ static error_t initSound(::Device* i2c_controller, ::Device* io_expander0 = null return ERROR_NONE; } -static error_t initMicrophone(::Device* i2c_controller) { - // ES7210 quad-channel microphone ADC at 0x40. - // Register sequence from M5Unified (M5Unified.cpp, _microphone_enabled_cb_tab5). - // Configures 4-slot TDM output at 48kHz/16-bit with MIC1+MIC2 active and MICBIAS enabled. - static constexpr uint8_t ES7210_I2C_ADDR = 0x40; - static constexpr uint8_t INIT_DATA[] = { - 0x00, 0xFF, // RESET_CTL: full reset - 0x00, 0x41, // RESET_CTL: release reset, keep CSM active - 0x01, 0x1F, // CLK_ON_OFF: enable all clocks - 0x06, 0x00, // DIGITAL_PDN: power up all digital blocks - 0x07, 0x20, // ADC_OSR: OSR=256 - 0x08, 0x10, // MODE_CFG: I2S slave, TDM mode - 0x09, 0x30, // TCT0_CHPINI: chopper init period - 0x0A, 0x30, // TCT1_CHPINI - 0x20, 0x0A, // ADC34_HPF2 - 0x21, 0x2A, // ADC34_HPF1 - 0x22, 0x0A, // ADC12_HPF2 - 0x23, 0x2A, // ADC12_HPF1 - 0x02, 0xC1, // CLK_CTRL: MCLK from I2S, PLL off - 0x04, 0x01, // SDPOUT_CTL1: TDM output enable - 0x05, 0x00, // SDPOUT_CTL0 - 0x11, 0x60, // DBIAS: adjust reference voltage for P4 - 0x40, 0x42, // ANALOG_SYS: enable analog supply - 0x41, 0x70, // MICBIAS12: enable MICBIAS for MIC1+MIC2 - 0x42, 0x70, // MICBIAS34: enable MICBIAS for MIC3+MIC4 - 0x43, 0x1B, // MIC1_GAIN: +30 dB - 0x44, 0x1B, // MIC2_GAIN: +30 dB - 0x45, 0x00, // MIC3_GAIN: AEC ref, no gain - 0x46, 0x00, // MIC4_GAIN: AEC ref, no gain - 0x47, 0x00, // MIC1_LP - 0x48, 0x00, // MIC2_LP - 0x49, 0x00, // MIC3_LP - 0x4A, 0x00, // MIC4_LP - 0x4B, 0x00, // MIC12_PDN: power up MIC1+MIC2 - 0x4C, 0xFF, // MIC34_PDN: keep MIC3+MIC4 in power-down (AEC ref not needed) - 0x01, 0x14, // CLK_ON_OFF: final clock config - }; - - error_t error = i2c_controller_write_register_array( - i2c_controller, - ES7210_I2C_ADDR, - INIT_DATA, - sizeof(INIT_DATA), - pdMS_TO_TICKS(1000) - ); - if (error != ERROR_NONE) { - LOG_E(TAG, "Failed to init ES7210: %s", error_to_string(error)); - } - return error; -} static esp_clock_output_mapping_handle_t camera_osc_handle = nullptr; @@ -275,9 +182,6 @@ static void initCameraOsc() { } static bool initBoot() { - auto* i2c0 = device_find_by_name("i2c0"); - check(i2c0, "i2c0 not found"); - auto* io_expander0 = device_find_by_name("io_expander0"); check(io_expander0, "io_expander0 not found"); auto* io_expander1 = device_find_by_name("io_expander1"); @@ -287,14 +191,9 @@ static bool initBoot() { initExpander0(io_expander0); initExpander1(io_expander1); - error_t error = initSound(i2c0, io_expander0); - if (error != ERROR_NONE) { - LOG_E(TAG, "Failed to enable ES8388"); - } - - error = initMicrophone(i2c0); + error_t error = enableSpeakerAmplifier(io_expander0); if (error != ERROR_NONE) { - LOG_E(TAG, "Failed to init ES7210"); + LOG_E(TAG, "Failed to enable speaker amplifier"); } return true; diff --git a/Devices/m5stack-tab5/devicetree.yaml b/Devices/m5stack-tab5/devicetree.yaml index 1135e2a39..08ee968fc 100644 --- a/Devices/m5stack-tab5/devicetree.yaml +++ b/Devices/m5stack-tab5/devicetree.yaml @@ -1,8 +1,11 @@ dependencies: -- Platforms/platform-esp32 -- Drivers/pi4ioe5v6408-module -- Drivers/bmi270-module -- Drivers/ina226-module -- Drivers/rx8130ce-module -- Drivers/sc2356-module + - Platforms/platform-esp32 + - Drivers/pi4ioe5v6408-module + - Drivers/bmi270-module + - Drivers/es7210-module + - Drivers/es8388-module + - Drivers/audio-stream-module + - Drivers/ina226-module + - Drivers/rx8130ce-module + - Drivers/sc2356-module dts: m5stack,tab5.dts diff --git a/Devices/m5stack-tab5/m5stack,tab5.dts b/Devices/m5stack-tab5/m5stack,tab5.dts index 170b6ed10..257952b4b 100644 --- a/Devices/m5stack-tab5/m5stack,tab5.dts +++ b/Devices/m5stack-tab5/m5stack,tab5.dts @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include #include #include @@ -35,6 +37,17 @@ gpio-count = <57>; }; + // Declared before i2c0 so it starts before the es8388/es7210 codecs that reference it + i2s0 { + compatible = "espressif,esp32-i2s"; + port = ; + pin-bclk = <&gpio0 27 GPIO_FLAG_NONE>; + pin-ws = <&gpio0 29 GPIO_FLAG_NONE>; + pin-data-out = <&gpio0 26 GPIO_FLAG_NONE>; + pin-data-in = <&gpio0 28 GPIO_FLAG_NONE>; + pin-mclk = <&gpio0 30 GPIO_FLAG_NONE>; + }; + i2c_internal: i2c0 { compatible = "espressif,esp32-i2c-master"; port = ; @@ -57,6 +70,25 @@ reg = <0x68>; }; + es8388 { + compatible = "everest,es8388"; + reg = <0x10>; + i2s = <&i2s0>; + }; + + es7210 { + compatible = "everest,es7210"; + reg = <0x40>; + i2s = <&i2s0>; + // Per schematic: MIC1/MIC2 are the two onboard capsules (U17/U16). + // MIC3 is not a microphone -- MIC3P is tapped from HPOUT_L (AEC_P) as an + // acoustic-echo-cancellation reference, MIC3N goes to AGND. MIC4 is the + // external headset-mic jack input (HPMIC), also not onboard. Select only + // the two real capsules; MIC3/MIC4 would otherwise show up as silent (or + // speaker-echoing) extra channels. + mic-mask = <3>; + }; + rx8130ce { compatible = "epson,rx8130ce"; reg = <0x32>; @@ -107,17 +139,6 @@ on-chip-ldo-chan = <4>; }; - // ES8388 and ES7210 - i2s0 { - compatible = "espressif,esp32-i2s"; - port = ; - pin-bclk = <&gpio0 27 GPIO_FLAG_NONE>; - pin-ws = <&gpio0 29 GPIO_FLAG_NONE>; - pin-data-out = <&gpio0 26 GPIO_FLAG_NONE>; - pin-data-in = <&gpio0 28 GPIO_FLAG_NONE>; - pin-mclk = <&gpio0 30 GPIO_FLAG_NONE>; - }; - usbhost0 { compatible = "espressif,esp32-usbhost"; peripheral-map = <0>; diff --git a/Documentation/README.md b/Documentation/README.md index b92e05374..bf14d175e 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -8,7 +8,10 @@ Tactility is an operating system for the ESP32 microcontroller family. It runs o ### Simulator (Linux/macOS, no ESP-IDF needed) -Simulator is not supported on Windows +> [!IMPORTANT] +> The simulator does **NOT** build or run on native Windows (Win32/PowerShell/cmd). This is +> a hard platform limitation, not a missing tool or PATH issue — do not attempt `cmake -B +> buildsim` on Windows, it will not work. WSL is a separate, Linux environment and is fine. ```bash cmake -B buildsim -G Ninja @@ -27,6 +30,23 @@ idf.py flash monitor # flash and monitor Device IDs are the folder names under `Devices/` (e.g. `lilygo-tdeck`, `m5stack-cores3`, `cyd-2432s028r`). +#### Windows: activating the ESP-IDF environment + +On native Windows, `idf.py` is not on PATH by default — it must be activated per-shell first. +The install script places a PowerShell profile activator per IDF version at +`%IDF_TOOL_PATH%\Microsoft.v.PowerShell_profile.ps1` (path controlled by the +`IDF_TOOL_PATH` environment variable, set to wherever ESP-IDF's tools were installed, e.g. +`C:\Espressif\tools`). Source it before running any `idf.py` command: + +```powershell +. "$env:IDF_TOOL_PATH\Microsoft.v5.5.2.PowerShell_profile.ps1" # match the installed IDF version +Set-Location "" +idf.py build 2>&1 | Select-Object -Last 250 +``` + +This is Windows-specific setup (the main dev works on Linux, where `idf.py` is normally +already on PATH via `export.sh`/`. ./export.sh` or a shell profile). + ### Devicetree A device implementation has a `.dts` file. diff --git a/Drivers/audio-codec-module/CMakeLists.txt b/Drivers/audio-codec-module/CMakeLists.txt new file mode 100644 index 000000000..6f2683c50 --- /dev/null +++ b/Drivers/audio-codec-module/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +file(GLOB_RECURSE SOURCE_FILES "source/*.c*") + +tactility_add_module(audio-codec-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel esp_codec_dev +) diff --git a/Drivers/audio-codec-module/LICENSE-Apache-2.0.md b/Drivers/audio-codec-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/audio-codec-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Drivers/audio-codec-module/README.md b/Drivers/audio-codec-module/README.md new file mode 100644 index 000000000..48822bdca --- /dev/null +++ b/Drivers/audio-codec-module/README.md @@ -0,0 +1,19 @@ +# Audio codec module + +Defines `AUDIO_CODEC_TYPE` and the codec-agnostic `AudioCodecApi` (open/close/ +read/write/volume/mute/native-rate/native-channels/capabilities) that every +audio codec driver in this repo implements (ES8311, ES8388, ES7210, AW88298, +the dummy I2S amps, the PDM mic). This is intentionally low-level: no +resampling or mixing, one codec device per chip. Most app code should go +through `audio_stream` (see `Drivers/audio-stream-module`) instead of talking +to a codec device directly. + +Also provides `audio_codec_adapters.h`: glue that lets a codec driver build an +`esp_codec_dev` control/data/GPIO interface (`audio_codec_ctrl_if_t` / +`audio_codec_data_if_t` / `audio_codec_gpio_if_t`) directly from Tactility's +own I2C controller, I2S controller, and GPIO pin devices, so a codec driver +only ever needs `device_get_parent()`/`device_find_by_name()` plus a handful +of devicetree properties -- it never touches the platform I2C/I2S/GPIO APIs +directly. + +License: [Apache v2.0](LICENSE-Apache-2.0.md) diff --git a/Drivers/audio-codec-module/include/tactility/drivers/audio_codec_adapters.h b/Drivers/audio-codec-module/include/tactility/drivers/audio_codec_adapters.h new file mode 100644 index 000000000..a0df7f309 --- /dev/null +++ b/Drivers/audio-codec-module/include/tactility/drivers/audio_codec_adapters.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include "audio_codec_data_if.h" +#include "audio_codec_ctrl_if.h" +#include "audio_codec_gpio_if.h" + +struct Device; + +/** + * @brief Creates an esp_codec_dev data interface backed by a Tactility I2S controller device. + * @param[in] i2s_controller the I2S controller device to read/write audio frames from/to + * @return the data interface, or NULL on failure. Free with audio_codec_delete_data_if(). + */ +const audio_codec_data_if_t* audio_codec_adapter_new_i2s_data(struct Device* i2s_controller); + +/** + * @brief Creates an esp_codec_dev data interface backed by a Tactility I2S controller device, + * configured for PDM RX mode (e.g. for a PDM MEMS microphone such as the SPM1423). + * @param[in] i2s_controller the I2S controller device to read PDM audio frames from. Must be + * backed by I2S controller 0 -- PDM RX is hardware-restricted to that controller. + * @return the data interface, or NULL on failure. Free with audio_codec_delete_data_if(). + */ +const audio_codec_data_if_t* audio_codec_adapter_new_i2s_pdm_data(struct Device* i2s_controller); + +/** + * @brief Creates an esp_codec_dev control interface backed by a Tactility I2C controller device. + * @param[in] i2c_controller the I2C controller device the codec chip is attached to + * @param[in] address the 7-bit I2C address of the codec chip + * @return the control interface, or NULL on failure. Free with audio_codec_delete_ctrl_if(). + */ +const audio_codec_ctrl_if_t* audio_codec_adapter_new_i2c_ctrl(struct Device* i2c_controller, uint8_t address); + +/** + * @brief Creates an esp_codec_dev GPIO interface backed by Tactility GpioPinSpec descriptors. + * + * The codec chip driver references GPIOs by small integer index (0, 1, ...); this adapter + * maps those indices to GpioPinSpec entries supplied at creation time (e.g. reset/PA-enable lines + * declared in devicetree). Indices outside the supplied array are treated as no-ops. + * + * @param[in] pins array of pin specs, indexed by the small integer GPIO id used by the codec chip driver + * @note The caller must keep `pins` valid for the lifetime of the returned interface (until + * audio_codec_adapter_delete_gpio() is called); the adapter stores the pointer, not a copy. + * @param[in] pin_count number of entries in `pins` + * @note Only a single instance is supported at a time; a second call before deleting the + * first returns NULL. + * @return the GPIO interface, or NULL on failure. Free with audio_codec_adapter_delete_gpio() + * (esp_codec_dev's generic audio_codec_delete_gpio_if() does not release our GPIO descriptors). + */ +const audio_codec_gpio_if_t* audio_codec_adapter_new_gpio(const struct GpioPinSpec* pins, size_t pin_count); + +/** + * @brief Frees a GPIO interface created by audio_codec_adapter_new_gpio(), releasing its GPIO descriptors. + */ +int audio_codec_adapter_delete_gpio(const audio_codec_gpio_if_t* gpio_if); + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/audio-codec-module/source/audio_codec_ctrl_if_i2c.c b/Drivers/audio-codec-module/source/audio_codec_ctrl_if_i2c.c new file mode 100644 index 000000000..ee1b248bf --- /dev/null +++ b/Drivers/audio-codec-module/source/audio_codec_ctrl_if_i2c.c @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +#include +#include + +#define I2C_TIMEOUT_TICKS pdMS_TO_TICKS(100) + +struct I2cCtrlContext { + audio_codec_ctrl_if_t base; + struct Device* i2c_controller; + uint8_t address; + bool is_open; +}; + +static bool ctrl_is_open(const audio_codec_ctrl_if_t* handle) { + const struct I2cCtrlContext* context = (const struct I2cCtrlContext*) handle; + return context->is_open; +} + +static int ctrl_open(const audio_codec_ctrl_if_t* handle, void* config, int config_size) { + (void) config; + (void) config_size; + struct I2cCtrlContext* context = (struct I2cCtrlContext*) handle; + context->is_open = true; + return ESP_CODEC_DEV_OK; +} + +static int ctrl_read_reg(const audio_codec_ctrl_if_t* handle, int reg, int reg_len, void* data, int data_len) { + struct I2cCtrlContext* context = (struct I2cCtrlContext*) handle; + if (!context->is_open || reg_len != 1) { + return ESP_CODEC_DEV_NOT_SUPPORT; + } + if (data_len < 0 || data_len > UINT16_MAX) { + return ESP_CODEC_DEV_NOT_SUPPORT; + } + error_t error = i2c_controller_read_register(context->i2c_controller, context->address, (uint8_t) reg, (uint8_t*) data, (size_t) data_len, I2C_TIMEOUT_TICKS); + return (error == ERROR_NONE) ? ESP_CODEC_DEV_OK : ESP_CODEC_DEV_READ_FAIL; +} + +static int ctrl_write_reg(const audio_codec_ctrl_if_t* handle, int reg, int reg_len, void* data, int data_len) { + struct I2cCtrlContext* context = (struct I2cCtrlContext*) handle; + if (!context->is_open || reg_len != 1) { + return ESP_CODEC_DEV_NOT_SUPPORT; + } + if (data_len < 0 || data_len > UINT16_MAX) { + return ESP_CODEC_DEV_NOT_SUPPORT; + } + error_t error = i2c_controller_write_register(context->i2c_controller, context->address, (uint8_t) reg, (const uint8_t*) data, (uint16_t) data_len, I2C_TIMEOUT_TICKS); + return (error == ERROR_NONE) ? ESP_CODEC_DEV_OK : ESP_CODEC_DEV_WRITE_FAIL; +} + +static int ctrl_close(const audio_codec_ctrl_if_t* handle) { + struct I2cCtrlContext* context = (struct I2cCtrlContext*) handle; + context->is_open = false; + return ESP_CODEC_DEV_OK; +} + +const audio_codec_ctrl_if_t* audio_codec_adapter_new_i2c_ctrl(struct Device* i2c_controller, uint8_t address) { + if (i2c_controller == NULL) { + return NULL; + } + + struct I2cCtrlContext* context = (struct I2cCtrlContext*) calloc(1, sizeof(struct I2cCtrlContext)); + if (context == NULL) { + return NULL; + } + + context->i2c_controller = i2c_controller; + context->address = address; + context->is_open = false; + context->base.open = ctrl_open; + context->base.is_open = ctrl_is_open; + context->base.read_reg = ctrl_read_reg; + context->base.write_reg = ctrl_write_reg; + context->base.close = ctrl_close; + + return &context->base; +} + +// Note: esp_codec_dev already provides audio_codec_delete_ctrl_if() (calls ->close then frees); +// no adapter-specific cleanup is needed, so we don't redefine it here. diff --git a/Drivers/audio-codec-module/source/audio_codec_data_if_i2s.c b/Drivers/audio-codec-module/source/audio_codec_data_if_i2s.c new file mode 100644 index 000000000..583277ee8 --- /dev/null +++ b/Drivers/audio-codec-module/source/audio_codec_data_if_i2s.c @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +#include + +// esp_codec_dev_read/write may issue several of these chunked I2S transfers per call and +// gives us no way to forward the caller's audio_stream timeout through. Keep this short so +// a stalled/underrunning I2S link returns promptly -- callers retry on failure, and a long +// per-chunk timeout here is what made AudioStreamTest's loopback task unkillable (it blocked +// far longer than its read/write timeout and the 2s drain wait in stopLoopback()). +#define I2S_TIMEOUT_TICKS pdMS_TO_TICKS(200) + +struct I2sDataContext { + audio_codec_data_if_t base; + struct Device* i2s_controller; + bool is_open; + bool is_pdm; // true if created via audio_codec_adapter_new_i2s_pdm_data() +}; + +static bool data_is_open(const audio_codec_data_if_t* handle) { + const struct I2sDataContext* context = (const struct I2sDataContext*) handle; + return context->is_open; +} + +static int data_open(const audio_codec_data_if_t* handle, void* data_cfg, int cfg_size) { + (void) data_cfg; + (void) cfg_size; + struct I2sDataContext* context = (struct I2sDataContext*) handle; + context->is_open = true; + return ESP_CODEC_DEV_OK; +} + +static int data_enable(const audio_codec_data_if_t* handle, esp_codec_dev_type_t dev_type, bool enable) { + struct I2sDataContext* context = (struct I2sDataContext*) handle; + + // esp_codec_dev_close() calls this with enable=false but never calls set_fmt() again + // before the next open -- without releasing the channel here, a TDM/PDM RX channel + // (e.g. ES7210, a PDM mic) or a TX channel stays enabled and holding its GPIOs/DMA + // channel indefinitely after the app that opened it closes, starving other I2S/DMA + // users system-wide. Only release the direction this codec actually owns -- a single + // I2S controller can carry an independent TX user (e.g. a speaker amp) and RX user + // (e.g. a PDM mic) at once, and tearing down the whole controller on one closing would + // also kill the other's still-active stream. + if (!enable) { + bool isInput = (dev_type & ESP_CODEC_DEV_TYPE_IN) != 0; + bool isOutput = (dev_type & ESP_CODEC_DEV_TYPE_OUT) != 0; + if (isInput) { + i2s_controller_disable_direction(context->i2s_controller, true); + } + if (isOutput) { + i2s_controller_disable_direction(context->i2s_controller, false); + } + } + + return ESP_CODEC_DEV_OK; +} + +static int data_set_fmt(const audio_codec_data_if_t* handle, esp_codec_dev_type_t dev_type, esp_codec_dev_sample_info_t* fs) { + struct I2sDataContext* context = (struct I2sDataContext*) handle; + if (!context->is_open || fs == NULL) { + return ESP_CODEC_DEV_INVALID_ARG; + } + + // PDM RX is a distinct setup path chosen at adapter-construction time (see + // audio_codec_adapter_new_i2s_pdm_data()), not inferred from fs here -- PDM mics are + // mono/stereo just like standard mode, so there's no sample-info heuristic that could + // tell them apart the way fs->channel > 2 does for TDM. + if (context->is_pdm) { + // ESP-IDF's PDM RX mode only supports 16-bit samples -- silently configuring the + // controller anyway would leave the caller's chosen bit depth misapplied. + if (fs->bits_per_sample != 16) { + return ESP_CODEC_DEV_NOT_SUPPORT; + } + // PDM RX is fixed at 2 slots (mono or stereo) -- anything else doesn't map onto + // I2S_SLOT_MODE_MONO/STEREO and would silently get treated as stereo otherwise. + if (fs->channel != 1 && fs->channel != 2) { + return ESP_CODEC_DEV_NOT_SUPPORT; + } + struct I2sPdmRxConfig pdm_config = { + .sample_rate_hz = fs->sample_rate, + .stereo = fs->channel == 2, + }; + error_t error = i2s_controller_set_rx_pdm_config(context->i2s_controller, &pdm_config); + if (error != ERROR_NONE) { + return ESP_CODEC_DEV_DRV_ERR; + } + return ESP_CODEC_DEV_OK; + } + + // Standard (stereo) mode and TDM mode are alternative setup paths for the same I2S + // port, not sequential steps -- set_config() always allocates+enables a TX/RX channel + // pair, and calling it before set_rx_tdm_config() leaves that pair's TX channel bound + // to the shared BCLK/WS/MCLK pins, so the TDM path's fresh i2s_new_channel() can't + // claim those pins ("occupied by i2s_driver" / "GPIO not usable" at runtime). + if ((dev_type & ESP_CODEC_DEV_TYPE_IN) != 0 && fs->channel > 2) { + struct I2sTdmRxConfig tdm_config = { + .sample_rate_hz = fs->sample_rate, + .mclk_multiple = (fs->mclk_multiple != 0) ? (uint32_t) fs->mclk_multiple : 256, + .bclk_div = 8, + .slot_count = fs->channel, + .bits_per_sample = fs->bits_per_sample, + .slot_bit_width = 0, + }; + error_t error = i2s_controller_set_rx_tdm_config(context->i2s_controller, &tdm_config); + if (error != ERROR_NONE) { + return ESP_CODEC_DEV_DRV_ERR; + } + return ESP_CODEC_DEV_OK; + } + + struct I2sConfig config = { + .communication_format = I2S_FORMAT_STAND_I2S, + .sample_rate = fs->sample_rate, + .bits_per_sample = fs->bits_per_sample, + .channel_left = 0, + .channel_right = (fs->channel > 1) ? 1 : I2S_CHANNEL_NONE, + }; + + if (i2s_controller_set_config(context->i2s_controller, &config) != ERROR_NONE) { + return ESP_CODEC_DEV_DRV_ERR; + } + + return ESP_CODEC_DEV_OK; +} + +static int data_read(const audio_codec_data_if_t* handle, uint8_t* data, int size) { + struct I2sDataContext* context = (struct I2sDataContext*) handle; + if (!context->is_open) { + return ESP_CODEC_DEV_WRONG_STATE; + } + size_t bytes_read = 0; + error_t error = i2s_controller_read(context->i2s_controller, data, (size_t) size, &bytes_read, I2S_TIMEOUT_TICKS); + if (error != ERROR_NONE) { + return ESP_CODEC_DEV_READ_FAIL; + } + return (int) bytes_read; +} + +static int data_write(const audio_codec_data_if_t* handle, uint8_t* data, int size) { + struct I2sDataContext* context = (struct I2sDataContext*) handle; + if (!context->is_open) { + return ESP_CODEC_DEV_WRONG_STATE; + } + size_t bytes_written = 0; + error_t error = i2s_controller_write(context->i2s_controller, data, (size_t) size, &bytes_written, I2S_TIMEOUT_TICKS); + if (error != ERROR_NONE) { + return ESP_CODEC_DEV_WRITE_FAIL; + } + return (int) bytes_written; +} + +static int data_close(const audio_codec_data_if_t* handle) { + struct I2sDataContext* context = (struct I2sDataContext*) handle; + context->is_open = false; + return ESP_CODEC_DEV_OK; +} + +static const audio_codec_data_if_t* new_i2s_data(struct Device* i2s_controller, bool is_pdm) { + if (i2s_controller == NULL) { + return NULL; + } + + struct I2sDataContext* context = (struct I2sDataContext*) calloc(1, sizeof(struct I2sDataContext)); + if (context == NULL) { + return NULL; + } + + context->i2s_controller = i2s_controller; + context->is_open = false; + context->is_pdm = is_pdm; + context->base.open = data_open; + context->base.is_open = data_is_open; + context->base.enable = data_enable; + context->base.set_fmt = data_set_fmt; + context->base.read = data_read; + context->base.write = data_write; + context->base.close = data_close; + + return &context->base; +} + +const audio_codec_data_if_t* audio_codec_adapter_new_i2s_data(struct Device* i2s_controller) { + return new_i2s_data(i2s_controller, false); +} + +const audio_codec_data_if_t* audio_codec_adapter_new_i2s_pdm_data(struct Device* i2s_controller) { + return new_i2s_data(i2s_controller, true); +} + +// Note: esp_codec_dev already provides audio_codec_delete_data_if() (calls ->close then frees); +// no adapter-specific cleanup is needed, so we don't redefine it here. diff --git a/Drivers/audio-codec-module/source/audio_codec_gpio_if.c b/Drivers/audio-codec-module/source/audio_codec_gpio_if.c new file mode 100644 index 000000000..6b9a0b43e --- /dev/null +++ b/Drivers/audio-codec-module/source/audio_codec_gpio_if.c @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include + +#include + +#define TAG "AudioCodecGpio" + +struct GpioAdapterContext { + audio_codec_gpio_if_t base; + const struct GpioPinSpec* pins; + size_t pin_count; + struct GpioDescriptor** descriptors; +}; + +static struct GpioAdapterContext* g_context = NULL; + +static struct GpioDescriptor* acquire_descriptor(int16_t gpio) { + if (g_context == NULL || gpio < 0 || (size_t) gpio >= g_context->pin_count) { + return NULL; + } + + if (g_context->descriptors[gpio] != NULL) { + return g_context->descriptors[gpio]; + } + + const struct GpioPinSpec* spec = &g_context->pins[gpio]; + if (spec->gpio_controller == NULL) { + return NULL; + } + + struct GpioDescriptor* descriptor = gpio_descriptor_acquire(spec->gpio_controller, spec->pin, GPIO_OWNER_GPIO); + if (descriptor != NULL) { + gpio_descriptor_set_flags(descriptor, spec->flags); + g_context->descriptors[gpio] = descriptor; + } + return descriptor; +} + +static int gpio_setup(int16_t gpio, audio_gpio_dir_t dir, audio_gpio_mode_t mode) { + struct GpioDescriptor* descriptor = acquire_descriptor(gpio); + if (descriptor == NULL) { + return ESP_CODEC_DEV_NOT_FOUND; + } + + gpio_flags_t flags = GPIO_FLAG_NONE; + gpio_descriptor_get_flags(descriptor, &flags); + flags &= ~(GPIO_FLAG_DIRECTION_INPUT | GPIO_FLAG_DIRECTION_OUTPUT | GPIO_FLAG_PULL_UP | GPIO_FLAG_PULL_DOWN); + + flags |= (dir == AUDIO_GPIO_DIR_OUT) ? GPIO_FLAG_DIRECTION_OUTPUT : GPIO_FLAG_DIRECTION_INPUT; + if ((mode & AUDIO_GPIO_MODE_PULL_UP) != 0) { + flags |= GPIO_FLAG_PULL_UP; + } + if ((mode & AUDIO_GPIO_MODE_PULL_DOWN) != 0) { + flags |= GPIO_FLAG_PULL_DOWN; + } + + return (gpio_descriptor_set_flags(descriptor, flags) == ERROR_NONE) ? ESP_CODEC_DEV_OK : ESP_CODEC_DEV_DRV_ERR; +} + +static int gpio_set(int16_t gpio, bool high) { + struct GpioDescriptor* descriptor = acquire_descriptor(gpio); + if (descriptor == NULL) { + return ESP_CODEC_DEV_NOT_FOUND; + } + return (gpio_descriptor_set_level(descriptor, high) == ERROR_NONE) ? ESP_CODEC_DEV_OK : ESP_CODEC_DEV_DRV_ERR; +} + +// esp_codec_dev's gpio_if_t::get returns plain bool with no error channel, so a failed +// acquire and a legitimate low reading are indistinguishable to the caller; log so the +// failure is at least visible. +static bool gpio_get(int16_t gpio) { + struct GpioDescriptor* descriptor = acquire_descriptor(gpio); + if (descriptor == NULL) { + LOG_E(TAG, "gpio_get: failed to acquire descriptor for pin %d", gpio); + return false; + } + bool high = false; + gpio_descriptor_get_level(descriptor, &high); + return high; +} + +const audio_codec_gpio_if_t* audio_codec_adapter_new_gpio(const struct GpioPinSpec* pins, size_t pin_count) { + if (pins == NULL || pin_count == 0) { + return NULL; + } + + // esp_codec_dev's gpio_if_t has no per-call user context -- only a single global instance + // is supported (see g_context usage in acquire_descriptor()/gpio_setup/etc). A second + // call would silently replace it underneath whatever codec already holds the first + // pointer, so refuse rather than corrupt that codec's GPIO access. + if (g_context != NULL) { + return NULL; + } + + struct GpioAdapterContext* context = (struct GpioAdapterContext*) calloc(1, sizeof(struct GpioAdapterContext)); + if (context == NULL) { + return NULL; + } + + context->descriptors = (struct GpioDescriptor**) calloc(pin_count, sizeof(struct GpioDescriptor*)); + if (context->descriptors == NULL) { + free(context); + return NULL; + } + + context->pins = pins; + context->pin_count = pin_count; + context->base.setup = gpio_setup; + context->base.set = gpio_set; + context->base.get = gpio_get; + + g_context = context; + + return &context->base; +} + +// Note: esp_codec_dev's generic audio_codec_delete_gpio_if() only frees the struct — it doesn't +// know about our descriptor array, so we provide our own cleanup under a non-colliding name. +int audio_codec_adapter_delete_gpio(const audio_codec_gpio_if_t* gpio_if) { + struct GpioAdapterContext* context = (struct GpioAdapterContext*) gpio_if; + for (size_t i = 0; i < context->pin_count; i++) { + if (context->descriptors[i] != NULL) { + gpio_descriptor_release(context->descriptors[i]); + } + } + free(context->descriptors); + + if (g_context == context) { + g_context = NULL; + } + free(context); + return ESP_CODEC_DEV_OK; +} diff --git a/Drivers/audio-stream-module/CMakeLists.txt b/Drivers/audio-stream-module/CMakeLists.txt new file mode 100644 index 000000000..1a4f5613f --- /dev/null +++ b/Drivers/audio-stream-module/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +file(GLOB_RECURSE SOURCE_FILES "source/*.c*") + +tactility_add_module(audio-stream-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel +) diff --git a/Drivers/audio-stream-module/LICENSE-Apache-2.0.md b/Drivers/audio-stream-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/audio-stream-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Drivers/audio-stream-module/README.md b/Drivers/audio-stream-module/README.md new file mode 100644 index 000000000..35dbb224e --- /dev/null +++ b/Drivers/audio-stream-module/README.md @@ -0,0 +1,22 @@ +# Audio stream module + +Defines `AUDIO_STREAM_TYPE` and `AudioStreamApi`: the high-level, full-duplex +audio API that apps (including ELF side-loaded apps) and `AudioService` should +use instead of talking to an `AUDIO_CODEC_TYPE` device directly. Binds to one +input-capable and/or one output-capable codec device (found automatically at +start; see `Drivers/audio-codec-module`) and adds on top of it: + +- Resampling, so the same app code works at any requested sample rate + regardless of what rate the bound codec natively runs at. +- Shared volume/mute/enable state per direction, with a change callback + (`AudioStreamChangeCallback`) that `AudioService` subscribes to. +- A single always-present device (`audio-stream0`), constructed unconditionally + at module-start time before any devicetree codec exists, and bound lazily on + first use -- so boards with no audio hardware at all still get a harmless, + inert device rather than nothing. + +`open_input`/`open_output` return a stream handle; `read`/`write` on that +handle are blocking and must be called from the caller's own task, never from +the main/LVGL thread. + +License: [Apache v2.0](LICENSE-Apache-2.0.md) diff --git a/Drivers/audio-stream-module/devicetree.yaml b/Drivers/audio-stream-module/devicetree.yaml new file mode 100644 index 000000000..24ce2c08b --- /dev/null +++ b/Drivers/audio-stream-module/devicetree.yaml @@ -0,0 +1,2 @@ +dependencies: +- TactilityKernel diff --git a/Drivers/audio-stream-module/include/audio_stream_module.h b/Drivers/audio-stream-module/include/audio_stream_module.h new file mode 100644 index 000000000..20817a013 --- /dev/null +++ b/Drivers/audio-stream-module/include/audio_stream_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module audio_stream_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/audio-stream-module/source/audio_stream.cpp b/Drivers/audio-stream-module/source/audio_stream.cpp new file mode 100644 index 000000000..2512d686a --- /dev/null +++ b/Drivers/audio-stream-module/source/audio_stream.cpp @@ -0,0 +1,821 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include +#include + +#include +#include + +#define TAG "AudioStream" + +namespace { + +// Linear-interpolation resampler. Cheap and good enough for voice/UI audio; S3/P4 have +// plenty of headroom for this at the rates this subsystem targets (16k/44.1k/48k). +// Operates on interleaved 16-bit PCM, which is what esp_codec_dev / our codec drivers use. +size_t resample_s16(const int16_t* in, size_t in_frames, uint8_t channels, + uint32_t in_rate, uint32_t out_rate, + int16_t* out, size_t out_frame_capacity) { + if (in_rate == out_rate) { + size_t frames = (in_frames < out_frame_capacity) ? in_frames : out_frame_capacity; + std::memcpy(out, in, frames * channels * sizeof(int16_t)); + return frames; + } + + if (in_frames == 0) { + return 0; + } + + double ratio = (double) in_rate / (double) out_rate; + size_t out_frames = 0; + for (; out_frames < out_frame_capacity; out_frames++) { + double src_pos = (double) out_frames * ratio; + size_t src_index = (size_t) src_pos; + if (src_index + 1 >= in_frames) { + if (src_index >= in_frames) { + break; + } + // Last frame: no next sample to interpolate with, repeat it. + for (uint8_t channel = 0; channel < channels; channel++) { + out[out_frames * channels + channel] = in[src_index * channels + channel]; + } + continue; + } + + double frac = src_pos - (double) src_index; + for (uint8_t channel = 0; channel < channels; channel++) { + int16_t a = in[src_index * channels + channel]; + int16_t b = in[(src_index + 1) * channels + channel]; + out[out_frames * channels + channel] = (int16_t) ((double) a + ((double) b - (double) a) * frac); + } + } + + return out_frames; +} + +// Converts between interleaved S16 PCM with different channel counts. +// - channels_out < channels_in: downmix by averaging the first `channels_out` source channels +// plus folding any extra source channels into them round-robin (e.g. 4 -> 1 averages all 4; +// 4 -> 2 averages {0,2} into channel 0 and {1,3} into channel 1). +// - channels_out > channels_in: upmix by repeating source channels round-robin (e.g. mono -> stereo +// duplicates the single channel into both output channels). +// - equal: copies through. +void convert_channels_s16(const int16_t* in, size_t frames, uint8_t channels_in, + int16_t* out, uint8_t channels_out) { + if (channels_in == channels_out) { + std::memcpy(out, in, frames * channels_in * sizeof(int16_t)); + return; + } + + for (size_t frame = 0; frame < frames; frame++) { + const int16_t* in_frame = in + frame * channels_in; + int16_t* out_frame = out + frame * channels_out; + + if (channels_out < channels_in) { + for (uint8_t out_ch = 0; out_ch < channels_out; out_ch++) { + int32_t sum = 0; + uint8_t count = 0; + for (uint8_t in_ch = out_ch; in_ch < channels_in; in_ch += channels_out) { + sum += in_frame[in_ch]; + count++; + } + out_frame[out_ch] = (int16_t) (sum / (int32_t) count); + } + } else { + for (uint8_t out_ch = 0; out_ch < channels_out; out_ch++) { + out_frame[out_ch] = in_frame[out_ch % channels_in]; + } + } + } +} + +struct AudioStreamHandleImpl : AudioStreamHandleData { + AudioCodecDirection direction = AUDIO_CODEC_DIR_BOTH; + struct AudioStreamConfig config = {}; + uint32_t codec_rate = 0; + uint8_t codec_channels = 0; + uint8_t bytes_per_frame = 0; // app-side frame size (config.channels) + uint8_t codec_bytes_per_frame = 0; // codec-side frame size (codec_channels) + float input_gain = 1.0f; // fixed digital gain multiplier, input direction only (see audio_codec_get_input_gain_multiplier) + std::vector codec_buffer; // raw codec-rate/codec-channel PCM, scratch + std::vector convert_buffer; // intermediate scratch for the second conversion stage + + // Lifetime guard: close_stream() can be triggered from a different task than the one + // doing read()/write() (e.g. the Settings UI disabling output while SfxEngine's audio + // task is mid-write). `closing` keeps new I/O calls out, `busy_count` tracks I/O calls + // currently in flight, and `drain_semaphore` lets close_stream() block until they finish + // before freeing the handle. All three are only touched while `AudioStreamData::mutex` + // is held, except for the give/take on drain_semaphore itself. + bool closing = false; + int busy_count = 0; + SemaphoreHandle_t drain_semaphore = nullptr; +}; + +struct AudioStreamData { + Device* input_codec = nullptr; + Device* output_codec = nullptr; + bool input_enabled = true; + bool output_enabled = true; + + // Codecs reject volume/mute/gain calls until their chip is initialized, which only + // happens when a stream of that direction is first opened (audio_codec_open -> + // esp_codec_dev_open -> chip's open()/enable()). A Settings UI must be able to set + // these regardless of whether anything is currently streaming, so we cache the desired + // values here, apply them best-effort immediately, and replay them once the codec opens. + float input_volume = 100.0f; + float output_volume = 100.0f; + bool input_muted = false; + bool output_muted = false; + AudioStreamHandleImpl* open_input = nullptr; + AudioStreamHandleImpl* open_output = nullptr; + // Guards open_input/open_output and the closing/busy_count fields of any handle reachable + // through them, so close (possibly forced by set_enabled) can't race with read/write. + SemaphoreHandle_t mutex = nullptr; + + AudioStreamChangeCallback change_callback = nullptr; + void* change_callback_user_data = nullptr; +}; + +// Reads the registered change callback under the lock, then invokes it outside the lock +// (same lock-then-release-then-act pattern as codec_for_direction). +void notify_change(AudioStreamData* data, Device* device, AudioCodecDirection direction, AudioStreamChange change) { + xSemaphoreTake(data->mutex, portMAX_DELAY); + AudioStreamChangeCallback callback = data->change_callback; + void* user_data = data->change_callback_user_data; + xSemaphoreGive(data->mutex); + + if (callback != nullptr) { + callback(device, direction, change, user_data); + } +} + +#define GET_DATA(device) (static_cast(device_get_driver_data(device))) + +struct CodecSearchContext { + AudioCodecDirection wanted_direction; + Device* exact_match = nullptr; + Device* fallback_match = nullptr; +}; + +bool find_codec_by_direction(Device* device, void* context_ptr) { + auto* context = static_cast(context_ptr); + if (!device_is_ready(device)) { + return true; // continue searching + } + + AudioCodecDirection capabilities = AUDIO_CODEC_DIR_BOTH; + if (audio_codec_get_capabilities(device, &capabilities) != ERROR_NONE) { + return true; + } + + if (capabilities == context->wanted_direction) { + // Dedicated codec for exactly this direction (e.g. an input-only mic ADC) -- + // prefer it over a wider-capability codec and stop looking. + context->exact_match = device; + return false; + } + + if ((capabilities & context->wanted_direction) == context->wanted_direction + && context->fallback_match == nullptr) { + // Supports the wanted direction as part of a wider capability set (e.g. a + // BOTH-capable codec asked for INPUT). Remember it but keep looking -- boards + // commonly pair a BOTH-capable output codec with a separate dedicated input + // ADC, and binding the wrong one makes both directions fight over the same + // physical esp_codec_dev handle (reopen for input reconfigures/breaks output). + context->fallback_match = device; + } + + return true; +} + +Device* find_first_codec_supporting(AudioCodecDirection direction) { + CodecSearchContext context = { .wanted_direction = direction }; + device_for_each_of_type(&AUDIO_CODEC_TYPE, &context, find_codec_by_direction); + return (context.exact_match != nullptr) ? context.exact_match : context.fallback_match; +} + +// The audio-stream device is constructed while modules start, which happens before the +// device tree's codec devices (nested under i2c0) are started. So codecs can't be resolved +// at start_device time — resolve (and cache) them lazily on first use instead, by which +// point the device tree has finished starting. +Device* codec_for_direction(AudioStreamData* data, AudioCodecDirection direction) { + Device** slot = (direction == AUDIO_CODEC_DIR_INPUT) ? &data->input_codec : &data->output_codec; + + // Fast path: already resolved (the common case after first use). + Device* existing = *slot; + if (existing != nullptr) { + return existing; + } + + // Slow path: resolve outside the lock (device_for_each_of_type can be costly), then + // re-check under the mutex before committing -- avoids two threads racing to write + // *slot (and double-logging "Bound ... codec"). + Device* found = find_first_codec_supporting(direction); + + xSemaphoreTake(data->mutex, portMAX_DELAY); + if (*slot == nullptr) { + *slot = found; + if (found != nullptr) { + LOG_I(TAG, "Bound %s codec: %s", (direction == AUDIO_CODEC_DIR_INPUT) ? "input" : "output", found->name); + } + } + Device* result = *slot; + xSemaphoreGive(data->mutex); + return result; +} + +// Marks an I/O operation as in-flight on `handle`, preventing close_stream() from freeing it +// underneath us. Returns false (and does nothing further) if the handle is closing/closed -- +// callers must bail out with an error in that case. Must be paired with io_end(). +bool io_begin(AudioStreamData* data, AudioStreamHandleImpl* handle) { + xSemaphoreTake(data->mutex, portMAX_DELAY); + bool is_input = (handle->direction == AUDIO_CODEC_DIR_INPUT); + AudioStreamHandleImpl* slot_value = is_input ? data->open_input : data->open_output; + if (slot_value != handle || handle->closing) { + xSemaphoreGive(data->mutex); + return false; + } + handle->busy_count++; + xSemaphoreGive(data->mutex); + return true; +} + +void io_end(AudioStreamData* data, AudioStreamHandleImpl* handle) { + xSemaphoreTake(data->mutex, portMAX_DELAY); + handle->busy_count--; + if (handle->closing && handle->busy_count == 0) { + xSemaphoreGive(handle->drain_semaphore); + } + xSemaphoreGive(data->mutex); +} + +// region AudioStreamApi + +error_t open_stream(Device* device, const struct AudioStreamConfig* config, AudioCodecDirection direction, AudioStreamHandle* out_handle) { + if (config->bits_per_sample != 8 && config->bits_per_sample != 16 + && config->bits_per_sample != 24 && config->bits_per_sample != 32) { + // bytes_per_frame/codec_bytes_per_frame below assume a whole number of bytes per + // sample; anything else corrupts every frame-size calculation in read/write_stream. + return ERROR_INVALID_ARGUMENT; + } + + if (config->channels == 0) { + return ERROR_INVALID_ARGUMENT; + } + + auto* data = GET_DATA(device); + if (data == nullptr) { + return ERROR_RESOURCE; + } + + bool is_input = (direction == AUDIO_CODEC_DIR_INPUT); + Device* codec = codec_for_direction(data, direction); + if (codec == nullptr) { + return ERROR_NOT_SUPPORTED; + } + + xSemaphoreTake(data->mutex, portMAX_DELAY); + + if ((is_input && !data->input_enabled) || (!is_input && !data->output_enabled)) { + xSemaphoreGive(data->mutex); + return ERROR_NOT_ALLOWED; + } + + AudioStreamHandleImpl** slot = is_input ? &data->open_input : &data->open_output; + if (*slot != nullptr) { + xSemaphoreGive(data->mutex); + return ERROR_INVALID_STATE; + } + + // Reserve the slot with a placeholder so concurrent opens can't race past the check + // above while we do the (potentially slow) codec open below outside the lock. + auto* reservation = reinterpret_cast(1); + *slot = reservation; + xSemaphoreGive(data->mutex); + + uint32_t codec_rate = 0; + if (audio_codec_get_native_sample_rate(codec, direction, &codec_rate) != ERROR_NONE || codec_rate == 0) { + xSemaphoreTake(data->mutex, portMAX_DELAY); + if (*slot == reservation) { *slot = nullptr; } + xSemaphoreGive(data->mutex); + return ERROR_RESOURCE; + } + + // The codec must be opened with its native channel layout (e.g. 4 for a 4-slot TDM mic + // ADC) -- opening it with the app's requested channel count can silently corrupt the + // stream (ES7210 in TDM mode halves its configured bit depth for <= 2 channels). We + // convert between the codec's layout and the app's requested channel count ourselves. + uint8_t codec_channels = config->channels; + if (audio_codec_get_native_channels(codec, direction, &codec_channels) != ERROR_NONE || codec_channels == 0) { + codec_channels = config->channels; + } + + struct AudioCodecStreamConfig codec_config = { + .sample_rate = codec_rate, + .bits_per_sample = config->bits_per_sample, + .channels = codec_channels, + .direction = direction, + }; + + if (audio_codec_open(codec, &codec_config) != ERROR_NONE) { + LOG_E(TAG, "Failed to open codec for %s", is_input ? "input" : "output"); + xSemaphoreTake(data->mutex, portMAX_DELAY); + if (*slot == reservation) { *slot = nullptr; } + xSemaphoreGive(data->mutex); + return ERROR_RESOURCE; + } + + // The chip is initialized now -- replay any volume/mute settings that were requested + // before this stream existed (set_volume/set_mute cache them but the chip rejects them + // until opened). + if (is_input) { + audio_codec_set_volume(codec, AUDIO_CODEC_DIR_INPUT, data->input_volume); + audio_codec_set_mute(codec, AUDIO_CODEC_DIR_INPUT, data->input_muted); + } else { + audio_codec_set_volume(codec, AUDIO_CODEC_DIR_OUTPUT, data->output_volume); + audio_codec_set_mute(codec, AUDIO_CODEC_DIR_OUTPUT, data->output_muted); + } + + auto* handle = new AudioStreamHandleImpl(); + handle->device = device; + handle->direction = direction; + handle->config = *config; + handle->codec_rate = codec_rate; + handle->codec_channels = codec_channels; + handle->bytes_per_frame = (uint8_t) ((config->bits_per_sample / 8) * config->channels); + handle->codec_bytes_per_frame = (uint8_t) ((config->bits_per_sample / 8) * codec_channels); + if (is_input) { + audio_codec_get_input_gain_multiplier(codec, &handle->input_gain); + } + handle->drain_semaphore = xSemaphoreCreateBinary(); + if (handle->drain_semaphore == nullptr) { + LOG_E(TAG, "Failed to create drain semaphore"); + delete handle; + audio_codec_close(codec); + xSemaphoreTake(data->mutex, portMAX_DELAY); + if (*slot == reservation) { *slot = nullptr; } + xSemaphoreGive(data->mutex); + return ERROR_OUT_OF_MEMORY; + } + + xSemaphoreTake(data->mutex, portMAX_DELAY); + *slot = handle; + xSemaphoreGive(data->mutex); + + *out_handle = handle; + return ERROR_NONE; +} + +error_t open_input(Device* device, const struct AudioStreamConfig* config, AudioStreamHandle* out_handle) { + return open_stream(device, config, AUDIO_CODEC_DIR_INPUT, out_handle); +} + +error_t open_output(Device* device, const struct AudioStreamConfig* config, AudioStreamHandle* out_handle) { + return open_stream(device, config, AUDIO_CODEC_DIR_OUTPUT, out_handle); +} + +error_t read_stream(AudioStreamHandle handle_base, void* out_data, size_t data_size, size_t* bytes_read, TickType_t timeout) { + auto* handle = static_cast(handle_base); + if (handle->direction != AUDIO_CODEC_DIR_INPUT || handle->bytes_per_frame == 0) { + return ERROR_INVALID_STATE; + } + + auto* data = GET_DATA(handle->device); + if (data == nullptr || data->input_codec == nullptr) { + return ERROR_RESOURCE; + } + + size_t requested_frames = data_size / handle->bytes_per_frame; + if (requested_frames == 0) { + if (bytes_read != nullptr) { + *bytes_read = 0; + } + return ERROR_NONE; + } + + if (!io_begin(data, handle)) { + return ERROR_INVALID_STATE; + } + + bool same_rate = (handle->codec_rate == handle->config.sample_rate); + bool same_channels = (handle->codec_channels == handle->config.channels); + + error_t result; + if (same_rate && same_channels) { + size_t codec_bytes_read = 0; + result = audio_codec_read(data->input_codec, out_data, data_size, &codec_bytes_read, timeout); + if (bytes_read != nullptr) { + *bytes_read = codec_bytes_read; + } + } else { + // Read enough codec-rate frames to produce the requested number of output-rate frames. + size_t codec_frames = (size_t) ((double) requested_frames * ((double) handle->codec_rate / (double) handle->config.sample_rate)) + 2; + size_t codec_bytes_needed = codec_frames * handle->codec_bytes_per_frame; + if (handle->codec_buffer.size() < codec_bytes_needed) { + handle->codec_buffer.resize(codec_bytes_needed); + } + + size_t codec_bytes_read = 0; + result = audio_codec_read(data->input_codec, handle->codec_buffer.data(), codec_bytes_needed, &codec_bytes_read, timeout); + if (result == ERROR_NONE) { + size_t codec_frames_read = codec_bytes_read / handle->codec_bytes_per_frame; + const int16_t* rate_input = reinterpret_cast(handle->codec_buffer.data()); + uint8_t rate_input_channels = handle->codec_channels; + size_t rate_input_frames = codec_frames_read; + + // Downmix first (while still at the codec's higher rate -- cheaper) if needed. + if (!same_channels) { + size_t convert_bytes_needed = codec_frames_read * handle->bytes_per_frame; + if (handle->convert_buffer.size() < convert_bytes_needed) { + handle->convert_buffer.resize(convert_bytes_needed); + } + convert_channels_s16(rate_input, codec_frames_read, handle->codec_channels, + reinterpret_cast(handle->convert_buffer.data()), handle->config.channels); + rate_input = reinterpret_cast(handle->convert_buffer.data()); + rate_input_channels = handle->config.channels; + } + + size_t out_frames = resample_s16( + rate_input, rate_input_frames, rate_input_channels, + handle->codec_rate, handle->config.sample_rate, + reinterpret_cast(out_data), requested_frames); + + if (bytes_read != nullptr) { + *bytes_read = out_frames * handle->bytes_per_frame; + } + } + } + + if (result == ERROR_NONE && handle->input_gain != 1.0f && bytes_read != nullptr && *bytes_read > 0) { + auto* samples = reinterpret_cast(out_data); + size_t sample_count = *bytes_read / sizeof(int16_t); + for (size_t i = 0; i < sample_count; i++) { + float boosted = (float) samples[i] * handle->input_gain; + samples[i] = (int16_t) (boosted < -32768.0f ? -32768.0f : boosted > 32767.0f ? 32767.0f : boosted); + } + } + + io_end(data, handle); + return result; +} + +error_t write_stream(AudioStreamHandle handle_base, const void* in_data, size_t data_size, size_t* bytes_written, TickType_t timeout) { + auto* handle = static_cast(handle_base); + if (handle->direction != AUDIO_CODEC_DIR_OUTPUT || handle->bytes_per_frame == 0) { + return ERROR_INVALID_STATE; + } + + auto* data = GET_DATA(handle->device); + if (data == nullptr || data->output_codec == nullptr) { + return ERROR_RESOURCE; + } + + size_t in_frames = data_size / handle->bytes_per_frame; + if (in_frames == 0) { + if (bytes_written != nullptr) { + *bytes_written = 0; + } + return ERROR_NONE; + } + + if (!io_begin(data, handle)) { + return ERROR_INVALID_STATE; + } + + bool same_rate = (handle->codec_rate == handle->config.sample_rate); + bool same_channels = (handle->codec_channels == handle->config.channels); + + error_t result; + if (same_rate && same_channels) { + size_t codec_bytes_written = 0; + result = audio_codec_write(data->output_codec, in_data, data_size, &codec_bytes_written, timeout); + if (bytes_written != nullptr) { + // Report in terms of input bytes consumed, matching codec_bytes_written 1:1 here. + *bytes_written = codec_bytes_written; + } + } else { + const int16_t* rate_input = reinterpret_cast(in_data); + uint8_t rate_input_channels = handle->config.channels; + size_t rate_input_frames = in_frames; + + // Upmix/downmix first (while still at the app's rate -- cheaper if downmixing) if needed. + if (!same_channels) { + size_t convert_bytes_needed = in_frames * handle->codec_bytes_per_frame; + if (handle->convert_buffer.size() < convert_bytes_needed) { + handle->convert_buffer.resize(convert_bytes_needed); + } + convert_channels_s16(rate_input, in_frames, handle->config.channels, + reinterpret_cast(handle->convert_buffer.data()), handle->codec_channels); + rate_input = reinterpret_cast(handle->convert_buffer.data()); + rate_input_channels = handle->codec_channels; + } + + size_t codec_frame_capacity = (size_t) ((double) rate_input_frames * ((double) handle->codec_rate / (double) handle->config.sample_rate)) + 2; + size_t codec_bytes_capacity = codec_frame_capacity * handle->codec_bytes_per_frame; + if (handle->codec_buffer.size() < codec_bytes_capacity) { + handle->codec_buffer.resize(codec_bytes_capacity); + } + + size_t codec_frames; + if (same_rate) { + codec_frames = rate_input_frames; + std::memcpy(handle->codec_buffer.data(), rate_input, rate_input_frames * handle->codec_bytes_per_frame); + } else { + codec_frames = resample_s16( + rate_input, rate_input_frames, rate_input_channels, + handle->config.sample_rate, handle->codec_rate, + reinterpret_cast(handle->codec_buffer.data()), codec_frame_capacity); + } + + size_t codec_bytes_to_write = codec_frames * handle->codec_bytes_per_frame; + size_t codec_bytes_written = 0; + result = audio_codec_write(data->output_codec, handle->codec_buffer.data(), codec_bytes_to_write, &codec_bytes_written, timeout); + if (result == ERROR_NONE && bytes_written != nullptr) { + // The caller provided `data_size` worth of input; we consumed all of it (resampled/converted). + *bytes_written = data_size; + } + } + + io_end(data, handle); + return result; +} + +error_t close_stream(AudioStreamHandle handle_base) { + auto* handle = static_cast(handle_base); + auto* data = GET_DATA(handle->device); + if (data == nullptr) { + return ERROR_RESOURCE; + } + + bool is_input = (handle->direction == AUDIO_CODEC_DIR_INPUT); + Device* codec = is_input ? data->input_codec : data->output_codec; + AudioStreamHandleImpl** slot = is_input ? &data->open_input : &data->open_output; + + xSemaphoreTake(data->mutex, portMAX_DELAY); + if (handle->closing) { + // Already being closed by another caller (e.g. concurrent set_enabled + app close). + xSemaphoreGive(data->mutex); + return ERROR_NONE; + } + handle->closing = true; + if (*slot == handle) { + *slot = nullptr; + } + bool must_drain = (handle->busy_count > 0); + xSemaphoreGive(data->mutex); + + if (must_drain && handle->drain_semaphore != nullptr) { + xSemaphoreTake(handle->drain_semaphore, portMAX_DELAY); + } + + if (codec != nullptr) { + audio_codec_close(codec); + } + + if (handle->drain_semaphore != nullptr) { + vSemaphoreDelete(handle->drain_semaphore); + } + + delete handle; + return ERROR_NONE; +} + +error_t set_volume(Device* device, AudioCodecDirection direction, float volume_percent) { + auto* data = GET_DATA(device); + if (data == nullptr) { + return ERROR_RESOURCE; + } + Device* codec = codec_for_direction(data, direction); + if (codec == nullptr) { + return ERROR_NOT_SUPPORTED; + } + + bool is_input = (direction == AUDIO_CODEC_DIR_INPUT); + xSemaphoreTake(data->mutex, portMAX_DELAY); + if (is_input) { data->input_volume = volume_percent; } else { data->output_volume = volume_percent; } + xSemaphoreGive(data->mutex); + + // The chip rejects this until it's been opened by a stream of this direction; that's + // fine -- the cached value above gets replayed in open_stream() once it is. + audio_codec_set_volume(codec, direction, volume_percent); + notify_change(data, device, direction, AUDIO_STREAM_CHANGE_VOLUME); + return ERROR_NONE; +} + +error_t get_volume(Device* device, AudioCodecDirection direction, float* volume_percent) { + auto* data = GET_DATA(device); + if (data == nullptr) { + return ERROR_RESOURCE; + } + Device* codec = codec_for_direction(data, direction); + if (codec == nullptr) { + return ERROR_NOT_SUPPORTED; + } + + if (audio_codec_get_volume(codec, direction, volume_percent) == ERROR_NONE) { + return ERROR_NONE; + } + + // Codec not open yet -- report the cached/desired value instead. + xSemaphoreTake(data->mutex, portMAX_DELAY); + *volume_percent = (direction == AUDIO_CODEC_DIR_INPUT) ? data->input_volume : data->output_volume; + xSemaphoreGive(data->mutex); + return ERROR_NONE; +} + +error_t set_mute(Device* device, AudioCodecDirection direction, bool muted) { + auto* data = GET_DATA(device); + if (data == nullptr) { + return ERROR_RESOURCE; + } + Device* codec = codec_for_direction(data, direction); + if (codec == nullptr) { + return ERROR_NOT_SUPPORTED; + } + + bool is_input = (direction == AUDIO_CODEC_DIR_INPUT); + xSemaphoreTake(data->mutex, portMAX_DELAY); + if (is_input) { data->input_muted = muted; } else { data->output_muted = muted; } + xSemaphoreGive(data->mutex); + + // As with volume, the chip may reject this until opened; cached value is replayed + // in open_stream(). + audio_codec_set_mute(codec, direction, muted); + notify_change(data, device, direction, AUDIO_STREAM_CHANGE_MUTE); + return ERROR_NONE; +} + +error_t get_mute(Device* device, AudioCodecDirection direction, bool* muted) { + auto* data = GET_DATA(device); + if (data == nullptr) { + return ERROR_RESOURCE; + } + Device* codec = codec_for_direction(data, direction); + if (codec == nullptr) { + return ERROR_NOT_SUPPORTED; + } + + if (audio_codec_get_mute(codec, direction, muted) == ERROR_NONE) { + return ERROR_NONE; + } + + xSemaphoreTake(data->mutex, portMAX_DELAY); + *muted = (direction == AUDIO_CODEC_DIR_INPUT) ? data->input_muted : data->output_muted; + xSemaphoreGive(data->mutex); + return ERROR_NONE; +} + +error_t set_enabled(Device* device, AudioCodecDirection direction, bool enabled) { + auto* data = GET_DATA(device); + if (data == nullptr) { + return ERROR_RESOURCE; + } + + bool is_input = (direction == AUDIO_CODEC_DIR_INPUT); + Device* codec = codec_for_direction(data, direction); + if (codec == nullptr) { + return ERROR_NOT_SUPPORTED; + } + + xSemaphoreTake(data->mutex, portMAX_DELAY); + if (is_input) { + data->input_enabled = enabled; + } else { + data->output_enabled = enabled; + } + + // Capture and clear the slot under the lock so we hand close_stream() a pointer that + // can't simultaneously be torn down by a racing close from the owning app (close_stream + // re-checks `*slot == handle` and no-ops if it's already been cleared/replaced). + AudioStreamHandleImpl* to_close = nullptr; + if (!enabled) { + AudioStreamHandleImpl** slot = is_input ? &data->open_input : &data->open_output; + to_close = *slot; + } + xSemaphoreGive(data->mutex); + + if (to_close != nullptr) { + close_stream(to_close); + } + + notify_change(data, device, direction, AUDIO_STREAM_CHANGE_ENABLED); + return ERROR_NONE; +} + +error_t get_enabled(Device* device, AudioCodecDirection direction, bool* enabled) { + auto* data = GET_DATA(device); + if (data == nullptr) { + return ERROR_RESOURCE; + } + Device* codec = codec_for_direction(data, direction); + if (codec == nullptr) { + return ERROR_NOT_SUPPORTED; + } + xSemaphoreTake(data->mutex, portMAX_DELAY); + *enabled = (direction == AUDIO_CODEC_DIR_INPUT) ? data->input_enabled : data->output_enabled; + xSemaphoreGive(data->mutex); + return ERROR_NONE; +} + +error_t is_supported(Device* device, AudioCodecDirection direction, bool* supported) { + auto* data = GET_DATA(device); + if (data == nullptr) { + return ERROR_RESOURCE; + } + *supported = codec_for_direction(data, direction) != nullptr; + return ERROR_NONE; +} + +error_t set_change_callback(Device* device, AudioStreamChangeCallback callback, void* user_data) { + auto* data = GET_DATA(device); + if (data == nullptr) { + return ERROR_RESOURCE; + } + + xSemaphoreTake(data->mutex, portMAX_DELAY); + data->change_callback = callback; + data->change_callback_user_data = user_data; + xSemaphoreGive(data->mutex); + return ERROR_NONE; +} + +static const struct AudioStreamApi API = { + .open_input = open_input, + .open_output = open_output, + .read = read_stream, + .write = write_stream, + .close = close_stream, + .set_volume = set_volume, + .get_volume = get_volume, + .set_mute = set_mute, + .get_mute = get_mute, + .set_enabled = set_enabled, + .get_enabled = get_enabled, + .is_supported = is_supported, + .set_change_callback = set_change_callback, +}; + +// endregion + +// region Driver lifecycle + +error_t start_device(Device* device) { + auto* data = new AudioStreamData(); + data->mutex = xSemaphoreCreateMutex(); + if (data->mutex == nullptr) { + delete data; + return ERROR_OUT_OF_MEMORY; + } + device_set_driver_data(device, data); + return ERROR_NONE; +} + +error_t stop_device(Device* device) { + auto* data = GET_DATA(device); + if (data == nullptr) { + return ERROR_NONE; + } + + if (data->open_input != nullptr) { + close_stream(data->open_input); + } + if (data->open_output != nullptr) { + close_stream(data->open_output); + } + + device_set_driver_data(device, nullptr); + if (data->mutex != nullptr) { + vSemaphoreDelete(data->mutex); + } + delete data; + return ERROR_NONE; +} + +// endregion + +} // namespace + +extern "C" { + +Driver audio_stream_driver = { + .name = "audio-stream", + .compatible = (const char*[]) { "audio-stream", nullptr }, + .start_device = start_device, + .stop_device = stop_device, + .api = &API, + .device_type = &AUDIO_STREAM_TYPE, + .owner = nullptr, + .internal = nullptr, +}; + +Device audio_stream_device = { + .address = 0, + .name = "audio-stream0", + .config = nullptr, + .parent = nullptr, + .internal = nullptr, +}; + +} diff --git a/Drivers/audio-stream-module/source/module.cpp b/Drivers/audio-stream-module/source/module.cpp new file mode 100644 index 000000000..a0195d7c7 --- /dev/null +++ b/Drivers/audio-stream-module/source/module.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include +#include + +extern "C" { + +extern Driver audio_stream_driver; +extern Device audio_stream_device; + +static error_t start() { + /* We crash when construct fails, because if a single driver fails to construct, + * there is no guarantee that the previously constructed drivers can be destroyed */ + check(driver_construct_add(&audio_stream_driver) == ERROR_NONE); + + /* The audio-stream device is a software aggregate with no devicetree backing — it binds + * to whichever AUDIO_CODEC_TYPE devices are present at start_device time. Constructed + * here (after codec drivers/devices are up) rather than from a .dts node. */ + if (device_construct_add_start(&audio_stream_device, "audio-stream") != ERROR_NONE) { + LOG_W("AudioStream", "No audio codec available; audio-stream0 not started"); + } + + return ERROR_NONE; +} + +static error_t stop() { + if (device_is_added(&audio_stream_device)) { + device_stop(&audio_stream_device); + device_remove(&audio_stream_device); + device_destruct(&audio_stream_device); + } + + /* We crash when destruct fails, because if a single driver fails to destruct, + * there is no guarantee that the previously destroyed drivers can be recovered */ + check(driver_remove_destruct(&audio_stream_driver) == ERROR_NONE); + return ERROR_NONE; +} + +extern const ModuleSymbol audio_stream_module_symbols[]; + +Module audio_stream_module = { + .name = "audio-stream", + .start = start, + .stop = stop, + .symbols = audio_stream_module_symbols, + .internal = nullptr +}; + +} diff --git a/Drivers/audio-stream-module/source/symbols.c b/Drivers/audio-stream-module/source/symbols.c new file mode 100644 index 000000000..22014878f --- /dev/null +++ b/Drivers/audio-stream-module/source/symbols.c @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +const struct ModuleSymbol audio_stream_module_symbols[] = { + MODULE_SYMBOL_TERMINATOR +}; diff --git a/Drivers/aw88298-module/CMakeLists.txt b/Drivers/aw88298-module/CMakeLists.txt new file mode 100644 index 000000000..fa6af21da --- /dev/null +++ b/Drivers/aw88298-module/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +file(GLOB_RECURSE SOURCE_FILES "source/*.c*") + +tactility_add_module(aw88298-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel audio-codec-module esp_codec_dev esp_driver_i2s +) diff --git a/Drivers/aw88298-module/LICENSE-Apache-2.0.md b/Drivers/aw88298-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/aw88298-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Drivers/aw88298-module/README.md b/Drivers/aw88298-module/README.md new file mode 100644 index 000000000..199393701 --- /dev/null +++ b/Drivers/aw88298-module/README.md @@ -0,0 +1,16 @@ +# AW88298 speaker amplifier + +A driver for the `AW88298` smart speaker power amplifier by Awinic, wired as an +output-only `AUDIO_CODEC_TYPE` device. The I2C bus is the device's parent; the I2S +controller carrying audio data is referenced via a devicetree phandle. + +Wraps Espressif's `esp_codec_dev` AW88298 implementation. If a hardware reset +(`pin-reset`) is wired, this driver pulses it itself before opening the codec, +rather than handing it to `esp_codec_dev`'s own reset handling -- the vendor +driver treats a reset pin index of `0` as "no pin wired" and silently skips the +reset, which doesn't match the `audio_codec_adapter_new_gpio()` convention used +elsewhere in this codebase (a single-pin array is always referenced at index 0). + +See https://www.awinic.com/en/productDetail/AW88298 + +License: [Apache v2.0](LICENSE-Apache-2.0.md) diff --git a/Drivers/aw88298-module/bindings/awinic,aw88298.yaml b/Drivers/aw88298-module/bindings/awinic,aw88298.yaml new file mode 100644 index 000000000..1f6186b74 --- /dev/null +++ b/Drivers/aw88298-module/bindings/awinic,aw88298.yaml @@ -0,0 +1,15 @@ +description: Awinic AW88298 smart speaker power amplifier (output-only) + +include: [ "i2c-device.yaml" ] + +compatible: "awinic,aw88298" + +properties: + i2s: + type: phandle + required: true + description: "I2S controller device that carries audio data" + pin-reset: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: "Hardware reset (RSTN) pin. The AW88298's I2C interface does not respond until this pin is released, so if wired, this driver asserts/releases reset itself before opening the codec, ensuring correct ordering relative to other devicetree devices. Omit if reset is tied high in hardware." diff --git a/Drivers/aw88298-module/devicetree.yaml b/Drivers/aw88298-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/aw88298-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/aw88298-module/include/bindings/aw88298.h b/Drivers/aw88298-module/include/bindings/aw88298.h new file mode 100644 index 000000000..b58407829 --- /dev/null +++ b/Drivers/aw88298-module/include/bindings/aw88298.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +DEFINE_DEVICETREE(aw88298, struct Aw88298Config) + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/aw88298-module/include/drivers/aw88298.h b/Drivers/aw88298-module/include/drivers/aw88298.h new file mode 100644 index 000000000..7d57f03f9 --- /dev/null +++ b/Drivers/aw88298-module/include/drivers/aw88298.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include +#include + +struct Device; + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief AW88298 codec device configuration. + * + * The AW88298 is an output-only (speaker power amp) codec. The I2C bus is the + * device's parent (per i2c-device.yaml), while the I2S controller carrying the + * audio data is referenced via a devicetree phandle. + * + * The chip's I2C interface does not respond until its hardware RESET (RSTN) + * line is released, so if that pin is wired (e.g. behind a GPIO expander on + * M5Stack StackChan/CoreS3), this driver asserts and releases it itself before + * opening the codec. This guarantees correct ordering: devicetree GPIO + * controller devices (e.g. the expander driving reset_pin) start in + * declaration order before this device, whereas board Configuration.cpp + * initBoot() callbacks run after ALL devicetree devices have already started + * and so cannot reliably gate a devicetree device's own bring-up. + */ +struct Aw88298Config { + /** I2C address on the bus (typically 0x36) */ + uint8_t address; + /** I2S controller device that carries audio data */ + struct Device* i2s_device; + /** Optional hardware reset (RSTN) pin. GPIO_PIN_SPEC_NONE if tied high in hardware. */ + struct GpioPinSpec reset_pin; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/aw88298-module/source/aw88298.cpp b/Drivers/aw88298-module/source/aw88298.cpp new file mode 100644 index 000000000..c4ccf0050 --- /dev/null +++ b/Drivers/aw88298-module/source/aw88298.cpp @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#define TAG "AW88298" + +namespace { + +// AW88298 native output rate per M5Unified's _speaker_enabled_cb_cores3(); higher rates +// (e.g. 44100Hz) additionally need mclk_multiple = I2S_MCLK_MULTIPLE_384 per aw88298_dac.h. +constexpr uint32_t NATIVE_SAMPLE_RATE = 16000; + +struct Aw88298Data { + const audio_codec_ctrl_if_t* ctrl_if = nullptr; + const audio_codec_data_if_t* data_if = nullptr; + const audio_codec_if_t* codec_if = nullptr; + esp_codec_dev_handle_t codec_device = nullptr; + bool is_open = false; + esp_codec_dev_sample_info_t open_sample_info = {}; +}; + +#define GET_CONFIG(device) (static_cast((device)->config)) +#define GET_DATA(device) (static_cast(device_get_driver_data(device))) + +// region AudioCodecApi + +error_t open(Device* device, const struct AudioCodecStreamConfig* config) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + if (config->direction == AUDIO_CODEC_DIR_INPUT || config->direction == AUDIO_CODEC_DIR_BOTH) { + LOG_E(TAG, "AW88298 is output-only"); + return ERROR_NOT_SUPPORTED; + } + + esp_codec_dev_sample_info_t sample_info = { + .bits_per_sample = config->bits_per_sample, + .channel = config->channels, + .channel_mask = 0, + .sample_rate = config->sample_rate, + .mclk_multiple = (config->sample_rate > 16000) ? I2S_MCLK_MULTIPLE_384 : 0, + }; + + if (data->is_open) { + bool same_config = data->open_sample_info.bits_per_sample == sample_info.bits_per_sample + && data->open_sample_info.channel == sample_info.channel + && data->open_sample_info.sample_rate == sample_info.sample_rate; + return same_config ? ERROR_NONE : ERROR_RESOURCE; + } + + if (esp_codec_dev_open(data->codec_device, &sample_info) != ESP_CODEC_DEV_OK) { + LOG_E(TAG, "Failed to open codec device"); + return ERROR_RESOURCE; + } + + data->is_open = true; + data->open_sample_info = sample_info; + return ERROR_NONE; +} + +error_t close(Device* device) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + if (data->is_open) { + esp_codec_dev_close(data->codec_device); + data->is_open = false; + } + + return ERROR_NONE; +} + +error_t read(Device* device, void* buffer, size_t size, size_t* bytes_read, TickType_t timeout) { + (void) device; + (void) buffer; + (void) size; + (void) bytes_read; + (void) timeout; + return ERROR_NOT_SUPPORTED; +} + +error_t write(Device* device, const void* buffer, size_t size, size_t* bytes_written, TickType_t timeout) { + (void) timeout; + auto* data = GET_DATA(device); + if (!data->is_open) { + return ERROR_RESOURCE; + } + + // esp_codec_dev_write returns the number of bytes written (>= 0) on success, or a negative + // ESP_CODEC_DEV_* error code on failure -- it does NOT return ESP_CODEC_DEV_OK (0) for + // a successful nonzero-length write. + int result = esp_codec_dev_write(data->codec_device, const_cast(buffer), (int) size); + if (result < 0) { + return ERROR_RESOURCE; + } + + *bytes_written = (size_t) result; + return ERROR_NONE; +} + +error_t set_volume(Device* device, AudioCodecDirection direction, float volume_percent) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr || direction != AUDIO_CODEC_DIR_OUTPUT) { + return ERROR_NOT_SUPPORTED; + } + + if (volume_percent < 0.0f || volume_percent > 100.0f) { + return ERROR_RESOURCE; + } + + int volume = (int) std::lround(volume_percent); + return (esp_codec_dev_set_out_vol(data->codec_device, volume) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE; +} + +error_t get_volume(Device* device, AudioCodecDirection direction, float* volume_percent) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr || direction != AUDIO_CODEC_DIR_OUTPUT) { + return ERROR_NOT_SUPPORTED; + } + + int volume = 0; + if (esp_codec_dev_get_out_vol(data->codec_device, &volume) != ESP_CODEC_DEV_OK) { + return ERROR_RESOURCE; + } + *volume_percent = (float) volume; + return ERROR_NONE; +} + +error_t set_mute(Device* device, AudioCodecDirection direction, bool muted) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr || direction != AUDIO_CODEC_DIR_OUTPUT) { + return ERROR_NOT_SUPPORTED; + } + + return (esp_codec_dev_set_out_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE; +} + +error_t get_mute(Device* device, AudioCodecDirection direction, bool* muted) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr || direction != AUDIO_CODEC_DIR_OUTPUT) { + return ERROR_NOT_SUPPORTED; + } + + return (esp_codec_dev_get_out_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE; +} + +error_t get_native_channels(Device* device, AudioCodecDirection direction, uint8_t* channels) { + (void) device; + if (direction != AUDIO_CODEC_DIR_OUTPUT) { + return ERROR_NOT_SUPPORTED; + } + *channels = 2; + return ERROR_NONE; +} + +error_t get_native_sample_rate(Device* device, AudioCodecDirection direction, uint32_t* rate_hz) { + (void) device; + if (direction != AUDIO_CODEC_DIR_OUTPUT) { + return ERROR_NOT_SUPPORTED; + } + *rate_hz = NATIVE_SAMPLE_RATE; + return ERROR_NONE; +} + +error_t get_capabilities(Device* device, AudioCodecDirection* supported_directions) { + (void) device; + *supported_directions = AUDIO_CODEC_DIR_OUTPUT; + return ERROR_NONE; +} + +static const struct AudioCodecApi API = { + .open = open, + .close = close, + .read = read, + .write = write, + .set_volume = set_volume, + .get_volume = get_volume, + .set_mute = set_mute, + .get_mute = get_mute, + .get_native_sample_rate = get_native_sample_rate, + .get_native_channels = get_native_channels, + .get_capabilities = get_capabilities, + .get_input_gain_multiplier = nullptr, +}; + +// endregion + +// region Driver lifecycle + +error_t start_device(Device* device) { + const auto* config = GET_CONFIG(device); + + auto* i2c_controller = device_get_parent(device); + if (i2c_controller == nullptr || device_get_type(i2c_controller) != &I2C_CONTROLLER_TYPE) { + LOG_E(TAG, "Parent is not an I2C controller"); + return ERROR_RESOURCE; + } + + auto* i2s_controller = config->i2s_device; + if (i2s_controller == nullptr || device_get_type(i2s_controller) != &I2S_CONTROLLER_TYPE) { + LOG_E(TAG, "I2S controller device is not valid"); + return ERROR_RESOURCE; + } + + auto* data = new Aw88298Data(); + + data->ctrl_if = audio_codec_adapter_new_i2c_ctrl(i2c_controller, config->address); + data->data_if = audio_codec_adapter_new_i2s_data(i2s_controller); + if (data->ctrl_if == nullptr || data->data_if == nullptr) { + LOG_E(TAG, "Failed to create adapters"); + goto cleanup; + } + + if (data->ctrl_if->open(data->ctrl_if, nullptr, 0) != ESP_CODEC_DEV_OK) { + LOG_E(TAG, "Failed to open control interface"); + goto cleanup; + } + + if (data->data_if->open(data->data_if, nullptr, 0) != ESP_CODEC_DEV_OK) { + LOG_E(TAG, "Failed to open data interface"); + goto cleanup; + } + + // If a reset pin is wired (e.g. behind a GPIO expander), the AW88298's I2C interface + // won't respond until it's released. We pulse it ourselves here rather than handing it + // to aw88298_codec_new() via gpio_if/reset_pin: the vendor driver's internal + // aw88298_reset() treats reset_pin <= 0 as "no pin wired" and no-ops (aw88298.c:197), + // but the audio_codec_adapter_new_gpio() convention (matched by dummy_i2s_amp's + // pa_pin, see Drivers/dummy-i2s-amp-module) is to pass 0 as the array index of a + // single-pin GPioPinSpec array -- so index 0 collides with the vendor's own "disabled" + // sentinel and the reset never actually fires. This runs synchronously as part of this + // device's own start_device(), so it's correctly ordered relative to devicetree bring-up + // (unlike a board Configuration.cpp initBoot() callback, which only runs after ALL + // devicetree devices, including this one, have already started). + if (config->reset_pin.gpio_controller != nullptr) { + auto* reset_descriptor = gpio_descriptor_acquire(config->reset_pin.gpio_controller, config->reset_pin.pin, GPIO_OWNER_GPIO); + if (reset_descriptor == nullptr) { + LOG_E(TAG, "Failed to acquire reset pin descriptor"); + goto cleanup; + } + gpio_descriptor_set_flags(reset_descriptor, GPIO_FLAG_DIRECTION_OUTPUT); + gpio_descriptor_set_level(reset_descriptor, false); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_descriptor_set_level(reset_descriptor, true); + vTaskDelay(pdMS_TO_TICKS(50)); + gpio_descriptor_release(reset_descriptor); + } + + { + aw88298_codec_cfg_t codec_config = {}; + codec_config.ctrl_if = data->ctrl_if; + codec_config.gpio_if = nullptr; + codec_config.reset_pin = -1; + codec_config.hw_gain = {}; + + data->codec_if = aw88298_codec_new(&codec_config); + if (data->codec_if == nullptr) { + LOG_E(TAG, "Failed to create AW88298 codec interface"); + goto cleanup; + } + } + + { + esp_codec_dev_cfg_t dev_config = { + .dev_type = ESP_CODEC_DEV_TYPE_OUT, + .codec_if = data->codec_if, + .data_if = data->data_if, + }; + + data->codec_device = esp_codec_dev_new(&dev_config); + if (data->codec_device == nullptr) { + LOG_E(TAG, "Failed to create codec device"); + goto cleanup; + } + } + + device_set_driver_data(device, data); + return ERROR_NONE; + +cleanup: + // Mirrors stop_device's teardown order -- delete_*_if() routines close their interface + // first, so we don't need separate ->close() calls here. + if (data->codec_device != nullptr) { + esp_codec_dev_delete(data->codec_device); + } + if (data->codec_if != nullptr) { + audio_codec_delete_codec_if(data->codec_if); + } + if (data->data_if != nullptr) { + audio_codec_delete_data_if(data->data_if); + } + if (data->ctrl_if != nullptr) { + audio_codec_delete_ctrl_if(data->ctrl_if); + } + delete data; + return ERROR_RESOURCE; +} + +error_t stop_device(Device* device) { + auto* data = GET_DATA(device); + if (data == nullptr) { + return ERROR_NONE; + } + + if (data->is_open) { + esp_codec_dev_close(data->codec_device); + } + + if (data->codec_device != nullptr) { + esp_codec_dev_delete(data->codec_device); + } + if (data->codec_if != nullptr) { + audio_codec_delete_codec_if(data->codec_if); + } + if (data->data_if != nullptr) { + audio_codec_delete_data_if(data->data_if); + } + if (data->ctrl_if != nullptr) { + audio_codec_delete_ctrl_if(data->ctrl_if); + } + + device_set_driver_data(device, nullptr); + delete data; + return ERROR_NONE; +} + +// endregion + +} // namespace + +extern "C" { + +Driver aw88298_driver = { + .name = "aw88298", + .compatible = (const char*[]) { "awinic,aw88298", nullptr }, + .start_device = start_device, + .stop_device = stop_device, + .api = &API, + .device_type = &AUDIO_CODEC_TYPE, + .owner = nullptr, + .internal = nullptr, +}; + +} diff --git a/Drivers/aw88298-module/source/module.cpp b/Drivers/aw88298-module/source/module.cpp new file mode 100644 index 000000000..c07dcca63 --- /dev/null +++ b/Drivers/aw88298-module/source/module.cpp @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver aw88298_driver; + +static error_t start() { + check(driver_construct_add(&aw88298_driver) == ERROR_NONE); + return ERROR_NONE; +} + +static error_t stop() { + check(driver_remove_destruct(&aw88298_driver) == ERROR_NONE); + return ERROR_NONE; +} + +extern const ModuleSymbol aw88298_module_symbols[]; + +Module aw88298_module = { + .name = "aw88298", + .start = start, + .stop = stop, + .symbols = aw88298_module_symbols, + .internal = nullptr +}; + +} diff --git a/Drivers/aw88298-module/source/symbols.c b/Drivers/aw88298-module/source/symbols.c new file mode 100644 index 000000000..7961c5b1f --- /dev/null +++ b/Drivers/aw88298-module/source/symbols.c @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +const struct ModuleSymbol aw88298_module_symbols[] = { + MODULE_SYMBOL_TERMINATOR +}; diff --git a/Drivers/aw9523b-module/CMakeLists.txt b/Drivers/aw9523b-module/CMakeLists.txt new file mode 100644 index 000000000..6965c7016 --- /dev/null +++ b/Drivers/aw9523b-module/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +file(GLOB_RECURSE SOURCE_FILES "source/*.c*") + +tactility_add_module(aw9523b-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel +) diff --git a/Drivers/aw9523b-module/LICENSE-Apache-2.0.md b/Drivers/aw9523b-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/aw9523b-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Drivers/aw9523b-module/README.md b/Drivers/aw9523b-module/README.md new file mode 100644 index 000000000..20b22b6a8 --- /dev/null +++ b/Drivers/aw9523b-module/README.md @@ -0,0 +1,15 @@ +# AW9523B I/O expander + +A driver for the `AW9523B` 16-bit I2C-bus I/O expander with LED driver, by Awinic. +Exposes both 8-bit ports (P0/P1) as 16 GPIO pins (0-15, port 0 = pins 0-7, port 1 = +pins 8-15) via the `GPIO_CONTROLLER_TYPE` API. P0 is switched to push-pull mode on +start (every known board wiring for this chip expects push-pull output, not the +open-drain default). + +It does not support pull-up/down resistors, high-impedance outputs, or interrupts; +requesting those returns `ERROR_NOT_SUPPORTED`. LED driver functionality is not +implemented. + +See https://www.awinic.com/en/productDetail/AW9523B + +License: [Apache v2.0](LICENSE-Apache-2.0.md) diff --git a/Drivers/aw9523b-module/bindings/awinic,aw9523b.yaml b/Drivers/aw9523b-module/bindings/awinic,aw9523b.yaml new file mode 100644 index 000000000..b86293f58 --- /dev/null +++ b/Drivers/aw9523b-module/bindings/awinic,aw9523b.yaml @@ -0,0 +1,5 @@ +description: Awinic AW9523B 16-bit I2C-bus I/O expander with LED driver + +include: [ "i2c-device.yaml" ] + +compatible: "awinic,aw9523b" diff --git a/Drivers/aw9523b-module/devicetree.yaml b/Drivers/aw9523b-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/aw9523b-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/aw9523b-module/include/aw9523b_module.h b/Drivers/aw9523b-module/include/aw9523b_module.h new file mode 100644 index 000000000..8a948161c --- /dev/null +++ b/Drivers/aw9523b-module/include/aw9523b_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module aw9523b_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/aw9523b-module/include/bindings/aw9523b.h b/Drivers/aw9523b-module/include/bindings/aw9523b.h new file mode 100644 index 000000000..8577d2e65 --- /dev/null +++ b/Drivers/aw9523b-module/include/bindings/aw9523b.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +DEFINE_DEVICETREE(aw9523b, struct Aw9523bConfig) + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/aw9523b-module/include/drivers/aw9523b.h b/Drivers/aw9523b-module/include/drivers/aw9523b.h new file mode 100644 index 000000000..7e1881a15 --- /dev/null +++ b/Drivers/aw9523b-module/include/drivers/aw9523b.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief AW9523B GPIO expander device configuration. + * + * 16 pins total: P0_0..P0_7 map to GpioDescriptor pin numbers 0..7, P1_0..P1_7 + * map to pin numbers 8..15. P0 defaults to open-drain per hardware reset; this + * driver switches P0 to push-pull mode on start (matches every known board's + * usage of this chip, since open-drain P0 cannot drive an active-high output + * without an external pull-up). + */ +struct Aw9523bConfig { + /** I2C address on the bus (typically 0x58) */ + uint8_t address; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/aw9523b-module/source/aw9523b.cpp b/Drivers/aw9523b-module/source/aw9523b.cpp new file mode 100644 index 000000000..a27a106d4 --- /dev/null +++ b/Drivers/aw9523b-module/source/aw9523b.cpp @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include +#include +#include +#include + +#define TAG "AW9523B" + +#define GET_CONFIG(device) (static_cast((device)->config)) + +namespace { + +// Port 0: pins 0..7, Port 1: pins 8..15. +constexpr auto AW9523B_REG_INPUT_P0 = 0x00; +constexpr auto AW9523B_REG_INPUT_P1 = 0x01; +constexpr auto AW9523B_REG_OUTPUT_P0 = 0x02; +constexpr auto AW9523B_REG_OUTPUT_P1 = 0x03; +// Configuration register: 0 = output, 1 = input (inverted vs. most other expanders). +constexpr auto AW9523B_REG_CONFIG_P0 = 0x04; +constexpr auto AW9523B_REG_CONFIG_P1 = 0x05; +// Global Control Register: bit 4 selects P0 drive mode (0 = open-drain default, 1 = push-pull). +constexpr auto AW9523B_REG_GCR = 0x11; +constexpr uint8_t AW9523B_GCR_P0_PUSH_PULL = 1U << 4U; + +constexpr uint8_t portRegister(bool isPort1, uint8_t regPort0, uint8_t regPort1) { + return isPort1 ? regPort1 : regPort0; +} + +} // namespace + +static error_t start(Device* device) { + auto* parent = device_get_parent(device); + if (device_get_type(parent) != &I2C_CONTROLLER_TYPE) { + LOG_E(TAG, "Parent device is not I2C"); + return ERROR_RESOURCE; + } + + auto address = GET_CONFIG(device)->address; + + // P0 defaults to open-drain out of reset, which can't drive an active-high output + // without an external pull-up. Every known board wiring for this chip expects + // push-pull, so switch it unconditionally on start. + if (i2c_controller_register8_set_bits(parent, address, AW9523B_REG_GCR, AW9523B_GCR_P0_PUSH_PULL, portMAX_DELAY) != ERROR_NONE) { + LOG_E(TAG, "Failed to set P0 to push-pull mode"); + return ERROR_RESOURCE; + } + + LOG_I(TAG, "Started AW9523B device %s", device->name); + return gpio_controller_init_descriptors(device, 16, nullptr); +} + +static error_t stop(Device* device) { + check(gpio_controller_deinit_descriptors(device) == ERROR_NONE); + return ERROR_NONE; +} + +extern "C" { + +static error_t set_level(GpioDescriptor* descriptor, bool high) { + auto* device = descriptor->controller; + auto* parent = device_get_parent(device); + auto address = GET_CONFIG(device)->address; + bool isPort1 = descriptor->pin >= 8; + uint8_t bit = 1U << (descriptor->pin % 8U); + uint8_t reg = portRegister(isPort1, AW9523B_REG_OUTPUT_P0, AW9523B_REG_OUTPUT_P1); + + if (high) { + return i2c_controller_register8_set_bits(parent, address, reg, bit, portMAX_DELAY); + } else { + return i2c_controller_register8_reset_bits(parent, address, reg, bit, portMAX_DELAY); + } +} + +static error_t get_level(GpioDescriptor* descriptor, bool* high) { + auto* device = descriptor->controller; + auto* parent = device_get_parent(device); + auto address = GET_CONFIG(device)->address; + bool isPort1 = descriptor->pin >= 8; + uint8_t bit = 1U << (descriptor->pin % 8U); + uint8_t reg = portRegister(isPort1, AW9523B_REG_INPUT_P0, AW9523B_REG_INPUT_P1); + + uint8_t bits; + error_t err = i2c_controller_register8_get(parent, address, reg, &bits, portMAX_DELAY); + if (err != ERROR_NONE) { + return err; + } + + *high = (bits & bit) != 0; + return ERROR_NONE; +} + +static error_t set_flags(GpioDescriptor* descriptor, gpio_flags_t flags) { + auto* device = descriptor->controller; + auto* parent = device_get_parent(device); + auto address = GET_CONFIG(device)->address; + bool isPort1 = descriptor->pin >= 8; + uint8_t bit = 1U << (descriptor->pin % 8U); + uint8_t reg = portRegister(isPort1, AW9523B_REG_CONFIG_P0, AW9523B_REG_CONFIG_P1); + + // Config register is inverted: clear the bit for output, set it for input. + if (flags & GPIO_FLAG_DIRECTION_OUTPUT) { + return i2c_controller_register8_reset_bits(parent, address, reg, bit, portMAX_DELAY); + } else { + return i2c_controller_register8_set_bits(parent, address, reg, bit, portMAX_DELAY); + } +} + +static error_t get_flags(GpioDescriptor* descriptor, gpio_flags_t* flags) { + auto* device = descriptor->controller; + auto* parent = device_get_parent(device); + auto address = GET_CONFIG(device)->address; + bool isPort1 = descriptor->pin >= 8; + uint8_t bit = 1U << (descriptor->pin % 8U); + uint8_t reg = portRegister(isPort1, AW9523B_REG_CONFIG_P0, AW9523B_REG_CONFIG_P1); + + uint8_t val; + error_t err = i2c_controller_register8_get(parent, address, reg, &val, portMAX_DELAY); + if (err != ERROR_NONE) { + return err; + } + + // Config register is inverted: 0 = output, 1 = input. + *flags = (val & bit) ? GPIO_FLAG_DIRECTION_INPUT : GPIO_FLAG_DIRECTION_OUTPUT; + return ERROR_NONE; +} + +static error_t get_native_pin_number(GpioDescriptor* descriptor, void* pin_number) { + (void) descriptor; + (void) pin_number; + return ERROR_NOT_SUPPORTED; +} + +static error_t add_callback(GpioDescriptor* descriptor, void (*callback)(void*), void* arg) { + (void) descriptor; + (void) callback; + (void) arg; + return ERROR_NOT_SUPPORTED; +} + +static error_t remove_callback(GpioDescriptor* descriptor) { + (void) descriptor; + return ERROR_NOT_SUPPORTED; +} + +static error_t enable_interrupt(GpioDescriptor* descriptor) { + (void) descriptor; + return ERROR_NOT_SUPPORTED; +} + +static error_t disable_interrupt(GpioDescriptor* descriptor) { + (void) descriptor; + return ERROR_NOT_SUPPORTED; +} + +const static GpioControllerApi aw9523b_gpio_api = { + .set_level = set_level, + .get_level = get_level, + .set_flags = set_flags, + .get_flags = get_flags, + .get_native_pin_number = get_native_pin_number, + .add_callback = add_callback, + .remove_callback = remove_callback, + .enable_interrupt = enable_interrupt, + .disable_interrupt = disable_interrupt +}; + +Driver aw9523b_driver = { + .name = "aw9523b", + .compatible = (const char*[]) { "awinic,aw9523b", nullptr }, + .start_device = start, + .stop_device = stop, + .api = static_cast(&aw9523b_gpio_api), + .device_type = &GPIO_CONTROLLER_TYPE, + .owner = &aw9523b_module, + .internal = nullptr +}; + +} diff --git a/Drivers/aw9523b-module/source/module.cpp b/Drivers/aw9523b-module/source/module.cpp new file mode 100644 index 000000000..cbb916ed6 --- /dev/null +++ b/Drivers/aw9523b-module/source/module.cpp @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver aw9523b_driver; + +static error_t start() { + check(driver_construct_add(&aw9523b_driver) == ERROR_NONE); + return ERROR_NONE; +} + +static error_t stop() { + check(driver_remove_destruct(&aw9523b_driver) == ERROR_NONE); + return ERROR_NONE; +} + +Module aw9523b_module = { + .name = "aw9523b", + .start = start, + .stop = stop, + .symbols = nullptr, + .internal = nullptr +}; + +} diff --git a/Drivers/dummy-i2s-amp-module/CMakeLists.txt b/Drivers/dummy-i2s-amp-module/CMakeLists.txt new file mode 100644 index 000000000..045b6a244 --- /dev/null +++ b/Drivers/dummy-i2s-amp-module/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +file(GLOB_RECURSE SOURCE_FILES "source/*.c*") + +tactility_add_module(dummy-i2s-amp-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel audio-codec-module esp_codec_dev +) + +# audio_codec_sw_vol.h lives at the esp_codec_dev component root, which its own +# COMPONENT_ADD_INCLUDEDIRS (include interface device/include) does not expose. +if (DEFINED ENV{ESP_IDF_VERSION}) + idf_component_get_property(esp_codec_dev_dir espressif__esp_codec_dev COMPONENT_DIR) + target_include_directories(${COMPONENT_LIB} PRIVATE "${esp_codec_dev_dir}") +endif() diff --git a/Drivers/dummy-i2s-amp-module/LICENSE-Apache-2.0.md b/Drivers/dummy-i2s-amp-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/dummy-i2s-amp-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Drivers/dummy-i2s-amp-module/README.md b/Drivers/dummy-i2s-amp-module/README.md new file mode 100644 index 000000000..6fbeed71e --- /dev/null +++ b/Drivers/dummy-i2s-amp-module/README.md @@ -0,0 +1,16 @@ +# Dummy I2S class-D amplifiers (MAX98357A, NS4168) + +A driver covering any I2S class-D speaker amplifier with no I2C/register +interface -- just an I2S data line plus an optional GPIO enable (SD) pin -- +wired as an output-only `AUDIO_CODEC_TYPE` device. Has no I2C parent; the device +sits standalone in the devicetree and only references the I2S controller by +name. Covers the Maxim MAX98357A and NSIway NS4168. + +Since these amps have no hardware volume/mute registers, volume and mute are +applied in software on the PCM stream via `esp_codec_dev`'s software volume +handler. + +See https://www.analog.com/media/en/technical-documentation/data-sheets/MAX98357A-MAX98357B.pdf +And https://datasheet.lcsc.com/lcsc/2110191830_NSIWAY-NS4168_C965904.pdf + +License: [Apache v2.0](LICENSE-Apache-2.0.md) diff --git a/Drivers/dummy-i2s-amp-module/bindings/maxim,max98357a.yaml b/Drivers/dummy-i2s-amp-module/bindings/maxim,max98357a.yaml new file mode 100644 index 000000000..9fea5ab8d --- /dev/null +++ b/Drivers/dummy-i2s-amp-module/bindings/maxim,max98357a.yaml @@ -0,0 +1,13 @@ +description: Maxim MAX98357A class-D speaker amplifier (I2S input, optional GPIO enable) + +compatible: "maxim,max98357a" + +properties: + i2s: + type: phandle + required: true + description: "I2S controller device that carries audio data" + pin-enable: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: "Amplifier enable (SD) pin. Omit if the amplifier is always enabled." diff --git a/Drivers/dummy-i2s-amp-module/bindings/nsiway,ns4168.yaml b/Drivers/dummy-i2s-amp-module/bindings/nsiway,ns4168.yaml new file mode 100644 index 000000000..f6b04d502 --- /dev/null +++ b/Drivers/dummy-i2s-amp-module/bindings/nsiway,ns4168.yaml @@ -0,0 +1,13 @@ +description: Nsiway NS4168 class-D speaker amplifier (I2S input, optional GPIO enable) + +compatible: "nsiway,ns4168" + +properties: + i2s: + type: phandle + required: true + description: "I2S controller device that carries audio data" + pin-enable: + type: phandles + default: GPIO_PIN_SPEC_NONE + description: "Amplifier enable (SD) pin. Omit if the amplifier is always enabled." diff --git a/Drivers/dummy-i2s-amp-module/devicetree.yaml b/Drivers/dummy-i2s-amp-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/dummy-i2s-amp-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/dummy-i2s-amp-module/include/bindings/dummy_i2s_amp.h b/Drivers/dummy-i2s-amp-module/include/bindings/dummy_i2s_amp.h new file mode 100644 index 000000000..86ececb40 --- /dev/null +++ b/Drivers/dummy-i2s-amp-module/include/bindings/dummy_i2s_amp.h @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// One DEFINE_DEVICETREE per compatible string -- the devicetree compiler derives the +// expected config typedef name from the compatible string's suffix (e.g. "maxim,max98357a" +// -> max98357a_config_dt), not from a name we choose, so each supported chip needs its own +// typedef even though they all share the same underlying config layout. +DEFINE_DEVICETREE(max98357a, struct DummyI2sAmpConfig) +DEFINE_DEVICETREE(ns4168, struct DummyI2sAmpConfig) + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/dummy-i2s-amp-module/include/drivers/dummy_i2s_amp.h b/Drivers/dummy-i2s-amp-module/include/drivers/dummy_i2s_amp.h new file mode 100644 index 000000000..c899fb153 --- /dev/null +++ b/Drivers/dummy-i2s-amp-module/include/drivers/dummy_i2s_amp.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include +#include + +struct Device; + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Dummy class-D amplifier configuration. + * + * Covers any speaker amp with no I2C/register interface -- just I2S data in plus an + * optional GPIO enable (SD) pin -- e.g. MAX98357A, NS4168. There is no I2C parent; the + * device sits standalone in the devicetree and only references the I2S controller by name. + */ +struct DummyI2sAmpConfig { + /** I2S controller device that carries audio data */ + struct Device* i2s_device; + /** Optional amplifier enable pin (SD pin). GPIO_PIN_SPEC_NONE if not wired. */ + struct GpioPinSpec enable_pin; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/dummy-i2s-amp-module/include/dummy_i2s_amp_module.h b/Drivers/dummy-i2s-amp-module/include/dummy_i2s_amp_module.h new file mode 100644 index 000000000..32b4e7989 --- /dev/null +++ b/Drivers/dummy-i2s-amp-module/include/dummy_i2s_amp_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module dummy_i2s_amp_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/dummy-i2s-amp-module/source/dummy_i2s_amp.cpp b/Drivers/dummy-i2s-amp-module/source/dummy_i2s_amp.cpp new file mode 100644 index 000000000..d370c4743 --- /dev/null +++ b/Drivers/dummy-i2s-amp-module/source/dummy_i2s_amp.cpp @@ -0,0 +1,385 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#define TAG "DummyI2sAmp" + +namespace { + +// A dumb I2S class-D amp has no native rate of its own -- it just clocks whatever I2S +// frames arrive. Report a common default; audio_stream resamples to whatever rate it is +// opened with. +constexpr uint32_t NATIVE_SAMPLE_RATE = 44100; + +struct DummyI2sAmpData { + const audio_codec_data_if_t* data_if = nullptr; + const audio_codec_gpio_if_t* gpio_if = nullptr; + const audio_codec_if_t* codec_if = nullptr; + const audio_codec_vol_if_t* vol_if = nullptr; + esp_codec_dev_handle_t codec_device = nullptr; + bool is_open = false; + esp_codec_dev_sample_info_t open_sample_info = {}; + + // esp_codec_dev_set_out_mute() short-circuits on dummy_codec's mute callback (which + // only toggles the PA enable GPIO, a no-op when there's no enable pin) and never + // reaches its own software-volume mute fallback as a result. Track mute state + // ourselves and implement it by zeroing/restoring volume through the working + // esp_codec_dev_set_out_vol() path (confirmed working, unlike mute) instead. + bool muted = false; + int volume_percent = 100; +}; + +#define GET_CONFIG(device) (static_cast((device)->config)) +#define GET_DATA(device) (static_cast(device_get_driver_data(device))) + +// region AudioCodecApi + +error_t open(Device* device, const struct AudioCodecStreamConfig* config) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + if (config->direction != AUDIO_CODEC_DIR_OUTPUT && config->direction != AUDIO_CODEC_DIR_BOTH) { + return ERROR_NOT_SUPPORTED; + } + + esp_codec_dev_sample_info_t sample_info = { + .bits_per_sample = config->bits_per_sample, + .channel = config->channels, + .channel_mask = 0, + .sample_rate = config->sample_rate, + .mclk_multiple = 0, + }; + + if (data->is_open) { + bool same_config = data->open_sample_info.bits_per_sample == sample_info.bits_per_sample + && data->open_sample_info.channel == sample_info.channel + && data->open_sample_info.sample_rate == sample_info.sample_rate; + return same_config ? ERROR_NONE : ERROR_RESOURCE; + } + + if (esp_codec_dev_open(data->codec_device, &sample_info) != ESP_CODEC_DEV_OK) { + LOG_E(TAG, "Failed to open codec device"); + return ERROR_RESOURCE; + } + + data->is_open = true; + data->open_sample_info = sample_info; + return ERROR_NONE; +} + +error_t close(Device* device) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + if (data->is_open) { + esp_codec_dev_close(data->codec_device); + data->is_open = false; + } + + return ERROR_NONE; +} + +error_t read(Device* device, void* buffer, size_t size, size_t* bytes_read, TickType_t timeout) { + (void) device; + (void) buffer; + (void) size; + (void) bytes_read; + (void) timeout; + return ERROR_NOT_SUPPORTED; +} + +error_t write(Device* device, const void* buffer, size_t size, size_t* bytes_written, TickType_t timeout) { + (void) timeout; + auto* data = GET_DATA(device); + if (!data->is_open) { + return ERROR_RESOURCE; + } + + // esp_codec_dev_write returns the number of bytes written (>= 0) on success, or a negative + // ESP_CODEC_DEV_* error code on failure -- it does NOT return ESP_CODEC_DEV_OK (0) for + // a successful nonzero-length write. + int result = esp_codec_dev_write(data->codec_device, const_cast(buffer), (int) size); + if (result < 0) { + return ERROR_RESOURCE; + } + + *bytes_written = (size_t) result; + return ERROR_NONE; +} + +error_t set_volume(Device* device, AudioCodecDirection direction, float volume_percent) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + if (direction != AUDIO_CODEC_DIR_OUTPUT) { + return ERROR_NOT_SUPPORTED; + } + + if (volume_percent < 0.0f || volume_percent > 100.0f) { + return ERROR_RESOURCE; + } + + data->volume_percent = (int) std::lround(volume_percent); + + // Don't push the new volume to hardware while muted -- that would audibly unmute. + // It takes effect once set_mute(false) restores it. + if (data->muted) { + return ERROR_NONE; + } + + return (esp_codec_dev_set_out_vol(data->codec_device, data->volume_percent) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE; +} + +error_t get_volume(Device* device, AudioCodecDirection direction, float* volume_percent) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + if (direction != AUDIO_CODEC_DIR_OUTPUT) { + return ERROR_NOT_SUPPORTED; + } + + *volume_percent = (float) data->volume_percent; + return ERROR_NONE; +} + +error_t set_mute(Device* device, AudioCodecDirection direction, bool muted) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + if (direction != AUDIO_CODEC_DIR_OUTPUT) { + return ERROR_NOT_SUPPORTED; + } + + if (data->muted == muted) { + return ERROR_NONE; + } + + // esp_codec_dev_set_out_mute() is unusable here -- see DummyI2sAmpData::muted comment. + // Implement mute as a volume override through the working esp_codec_dev_set_out_vol() + // path instead: zero the hardware volume while muted, restore the tracked percentage + // on unmute. + int target = muted ? 0 : data->volume_percent; + if (esp_codec_dev_set_out_vol(data->codec_device, target) != ESP_CODEC_DEV_OK) { + return ERROR_RESOURCE; + } + + data->muted = muted; + return ERROR_NONE; +} + +error_t get_mute(Device* device, AudioCodecDirection direction, bool* muted) { + auto* data = GET_DATA(device); + + if (direction != AUDIO_CODEC_DIR_OUTPUT) { + return ERROR_NOT_SUPPORTED; + } + + *muted = data->muted; + return ERROR_NONE; +} + +error_t get_native_channels(Device* device, AudioCodecDirection direction, uint8_t* channels) { + (void) device; + if (direction != AUDIO_CODEC_DIR_OUTPUT) { + return ERROR_NOT_SUPPORTED; + } + *channels = 2; + return ERROR_NONE; +} + +error_t get_native_sample_rate(Device* device, AudioCodecDirection direction, uint32_t* rate_hz) { + (void) device; + if (direction != AUDIO_CODEC_DIR_OUTPUT) { + return ERROR_NOT_SUPPORTED; + } + *rate_hz = NATIVE_SAMPLE_RATE; + return ERROR_NONE; +} + +error_t get_capabilities(Device* device, AudioCodecDirection* supported_directions) { + (void) device; + *supported_directions = AUDIO_CODEC_DIR_OUTPUT; + return ERROR_NONE; +} + +static const struct AudioCodecApi API = { + .open = open, + .close = close, + .read = read, + .write = write, + .set_volume = set_volume, + .get_volume = get_volume, + .set_mute = set_mute, + .get_mute = get_mute, + .get_native_sample_rate = get_native_sample_rate, + .get_native_channels = get_native_channels, + .get_capabilities = get_capabilities, +}; + +// endregion + +// region Driver lifecycle + +error_t start_device(Device* device) { + const auto* config = GET_CONFIG(device); + + auto* i2s_controller = config->i2s_device; + if (i2s_controller == nullptr || device_get_type(i2s_controller) != &I2S_CONTROLLER_TYPE) { + LOG_E(TAG, "I2S controller device is not valid"); + return ERROR_RESOURCE; + } + + auto* data = new DummyI2sAmpData(); + + data->data_if = audio_codec_adapter_new_i2s_data(i2s_controller); + if (data->data_if == nullptr) { + LOG_E(TAG, "Failed to create I2S data adapter"); + goto cleanup; + } + + if (data->data_if->open(data->data_if, nullptr, 0) != ESP_CODEC_DEV_OK) { + LOG_E(TAG, "Failed to open data interface"); + goto cleanup; + } + + { + int16_t pa_pin = -1; + if (config->enable_pin.gpio_controller != nullptr) { + data->gpio_if = audio_codec_adapter_new_gpio(&config->enable_pin, 1); + if (data->gpio_if == nullptr) { + LOG_E(TAG, "Failed to create GPIO adapter for enable pin"); + goto cleanup; + } + pa_pin = 0; + } + + dummy_codec_cfg_t codec_config = { + .gpio_if = data->gpio_if, + .pa_pin = pa_pin, + .pa_reverted = false, + }; + + data->codec_if = dummy_codec_new(&codec_config); + if (data->codec_if == nullptr) { + LOG_E(TAG, "Failed to create dummy codec interface"); + goto cleanup; + } + } + + { + esp_codec_dev_cfg_t dev_config = { + .dev_type = ESP_CODEC_DEV_TYPE_OUT, + .codec_if = data->codec_if, + .data_if = data->data_if, + }; + + data->codec_device = esp_codec_dev_new(&dev_config); + if (data->codec_device == nullptr) { + LOG_E(TAG, "Failed to create codec device"); + goto cleanup; + } + } + + // Dumb I2S amps have no hardware volume/mute registers -- apply volume/mute in + // software on the PCM stream instead. + data->vol_if = audio_codec_new_sw_vol(); + if (data->vol_if == nullptr || esp_codec_dev_set_vol_handler(data->codec_device, data->vol_if) != ESP_CODEC_DEV_OK) { + LOG_E(TAG, "Failed to install software volume handler"); + goto cleanup; + } + + device_set_driver_data(device, data); + return ERROR_NONE; + +cleanup: + // Mirrors stop_device's teardown order -- delete_*_if() routines close their interface + // first, so we don't need separate ->close() calls here. + if (data->codec_device != nullptr) { + esp_codec_dev_delete(data->codec_device); + } + if (data->vol_if != nullptr) { + audio_codec_delete_vol_if(data->vol_if); + } + if (data->codec_if != nullptr) { + audio_codec_delete_codec_if(data->codec_if); + } + if (data->gpio_if != nullptr) { + audio_codec_adapter_delete_gpio(data->gpio_if); + } + if (data->data_if != nullptr) { + audio_codec_delete_data_if(data->data_if); + } + delete data; + return ERROR_RESOURCE; +} + +error_t stop_device(Device* device) { + auto* data = GET_DATA(device); + if (data == nullptr) { + return ERROR_NONE; + } + + if (data->is_open) { + esp_codec_dev_close(data->codec_device); + } + + if (data->codec_device != nullptr) { + esp_codec_dev_delete(data->codec_device); + } + if (data->vol_if != nullptr) { + audio_codec_delete_vol_if(data->vol_if); + } + if (data->codec_if != nullptr) { + audio_codec_delete_codec_if(data->codec_if); + } + if (data->gpio_if != nullptr) { + audio_codec_adapter_delete_gpio(data->gpio_if); + } + if (data->data_if != nullptr) { + audio_codec_delete_data_if(data->data_if); + } + + device_set_driver_data(device, nullptr); + delete data; + return ERROR_NONE; +} + +// endregion + +} // namespace + +extern "C" { + +Driver dummy_i2s_amp_driver = { + .name = "dummy_i2s_amp", + .compatible = (const char*[]) { "maxim,max98357a", "nsiway,ns4168", nullptr }, + .start_device = start_device, + .stop_device = stop_device, + .api = &API, + .device_type = &AUDIO_CODEC_TYPE, + .owner = nullptr, + .internal = nullptr, +}; + +} diff --git a/Drivers/dummy-i2s-amp-module/source/module.cpp b/Drivers/dummy-i2s-amp-module/source/module.cpp new file mode 100644 index 000000000..45a9391ab --- /dev/null +++ b/Drivers/dummy-i2s-amp-module/source/module.cpp @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver dummy_i2s_amp_driver; + +static error_t start() { + check(driver_construct_add(&dummy_i2s_amp_driver) == ERROR_NONE); + return ERROR_NONE; +} + +static error_t stop() { + check(driver_remove_destruct(&dummy_i2s_amp_driver) == ERROR_NONE); + return ERROR_NONE; +} + +extern const ModuleSymbol dummy_i2s_amp_module_symbols[]; + +Module dummy_i2s_amp_module = { + .name = "dummy_i2s_amp", + .start = start, + .stop = stop, + .symbols = dummy_i2s_amp_module_symbols, + .internal = nullptr +}; + +} diff --git a/Drivers/dummy-i2s-amp-module/source/symbols.c b/Drivers/dummy-i2s-amp-module/source/symbols.c new file mode 100644 index 000000000..bae1ebfb7 --- /dev/null +++ b/Drivers/dummy-i2s-amp-module/source/symbols.c @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +const struct ModuleSymbol dummy_i2s_amp_module_symbols[] = { + MODULE_SYMBOL_TERMINATOR +}; diff --git a/Drivers/es7210-module/CMakeLists.txt b/Drivers/es7210-module/CMakeLists.txt new file mode 100644 index 000000000..ba5dddc4d --- /dev/null +++ b/Drivers/es7210-module/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +file(GLOB_RECURSE SOURCE_FILES "source/*.c*") + +tactility_add_module(es7210-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel audio-codec-module esp_codec_dev +) diff --git a/Drivers/es7210-module/LICENSE-Apache-2.0.md b/Drivers/es7210-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/es7210-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Drivers/es7210-module/README.md b/Drivers/es7210-module/README.md new file mode 100644 index 000000000..8613b9769 --- /dev/null +++ b/Drivers/es7210-module/README.md @@ -0,0 +1,18 @@ +# ES7210 microphone ADC + +A driver for the `ES7210` 4-channel microphone ADC by Everest Semiconductor, +wired as an input-only `AUDIO_CODEC_TYPE` device. The I2C bus is the device's +parent; the I2S controller carrying the (4-slot TDM) audio data is referenced by +name. + +Always configures the hardware for the full 4-slot TDM frame regardless of how +many mics are actually wired (`mic-mask`), then demuxes down to just the active +mic slots in software -- `es7210_set_fs()` silently halves the configured bit +depth when asked for 2 or fewer TDM channels, which corrupts audio if the mic +count is passed straight through. + +Wraps Espressif's `esp_codec_dev` ES7210 implementation. + +See https://www.everest-semi.com/pdf/ES7210%20PB.pdf + +License: [Apache v2.0](LICENSE-Apache-2.0.md) diff --git a/Drivers/es7210-module/bindings/everest,es7210.yaml b/Drivers/es7210-module/bindings/everest,es7210.yaml new file mode 100644 index 000000000..86f0e7e38 --- /dev/null +++ b/Drivers/es7210-module/bindings/everest,es7210.yaml @@ -0,0 +1,20 @@ +description: Everest Semiconductor ES7210 4-channel microphone ADC + +include: ["i2c-device.yaml"] + +compatible: "everest,es7210" + +properties: + i2s: + type: phandle + required: true + description: "I2S controller device that carries audio data" + mic-mask: + type: int + required: false + default: 15 + description: "Bitmask of microphones to enable (bit0=MIC1 .. bit3=MIC4); defaults to all four" + input-gain-percent: + type: int + default: 100 + description: "Extra digital gain multiplier applied by audio_stream on top of the ES7210's own 30dB hardware ADC gain, as an integer percentage (100 = 1.0x / no extra boost). devicetree has no float type, hence x100." diff --git a/Drivers/es7210-module/devicetree.yaml b/Drivers/es7210-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/es7210-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/es7210-module/include/bindings/es7210.h b/Drivers/es7210-module/include/bindings/es7210.h new file mode 100644 index 000000000..abb92b542 --- /dev/null +++ b/Drivers/es7210-module/include/bindings/es7210.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +DEFINE_DEVICETREE(es7210, struct Es7210Config) + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/es7210-module/include/drivers/es7210.h b/Drivers/es7210-module/include/drivers/es7210.h new file mode 100644 index 000000000..16f4ab6f8 --- /dev/null +++ b/Drivers/es7210-module/include/drivers/es7210.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include + +struct Device; + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief ES7210 codec device configuration. + * + * The ES7210 is an input-only (microphone ADC) codec. The I2C bus is the + * device's parent (per i2c-device.yaml), while the I2S controller carrying + * the (typically 4-slot TDM) audio data is referenced via a devicetree phandle. + */ +struct Es7210Config { + /** I2C address on the bus (typically 0x40) */ + uint8_t address; + /** I2S controller device that carries audio data */ + struct Device* i2s_device; + /** Bitmask of microphones to enable, e.g. ES7210_SEL_MIC1 | ES7210_SEL_MIC2 | ... */ + uint8_t mic_selected_mask; + /** + * Extra fixed digital gain multiplier applied by audio_stream on top of the ES7210's + * own hardware ADC gain (already at a hard-coded 30dB default out of a 0..37.5dB + * range), as an integer percentage (100 = 1.0x / no extra boost). Small MEMS mic + * capsules can still sound quiet even near max hardware gain; this is for boards where + * 30dB hardware gain alone isn't enough. devicetree has no float property type, hence + * the x100 integer encoding. + */ + uint16_t input_gain_percent; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/es7210-module/include/es7210_module.h b/Drivers/es7210-module/include/es7210_module.h new file mode 100644 index 000000000..8f00edd93 --- /dev/null +++ b/Drivers/es7210-module/include/es7210_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module es7210_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/es7210-module/source/es7210.cpp b/Drivers/es7210-module/source/es7210.cpp new file mode 100644 index 000000000..267622319 --- /dev/null +++ b/Drivers/es7210-module/source/es7210.cpp @@ -0,0 +1,435 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#define TAG "ES7210" + +namespace { + +constexpr uint32_t NATIVE_SAMPLE_RATE = 16000; +constexpr float MAX_INPUT_GAIN_DB = 37.5f; // ES7210 mic gain range is roughly 0..37.5 dB + +struct Es7210Data { + const audio_codec_ctrl_if_t* ctrl_if = nullptr; + const audio_codec_data_if_t* data_if = nullptr; + const audio_codec_if_t* codec_if = nullptr; + esp_codec_dev_handle_t codec_device = nullptr; + bool is_open = false; + uint8_t mic_count = 4; + uint8_t mic_mask = 0x0F; + uint8_t tdm_slot_count = 4; + uint8_t open_bits_per_sample = 16; + uint32_t open_sample_rate = 0; + float input_gain = 1.0f; + std::vector raw_buffer; +}; + +#define GET_CONFIG(device) (static_cast((device)->config)) +#define GET_DATA(device) (static_cast(device_get_driver_data(device))) + +// region AudioCodecApi + +error_t open(Device* device, const struct AudioCodecStreamConfig* config) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + if (config->direction == AUDIO_CODEC_DIR_OUTPUT || config->direction == AUDIO_CODEC_DIR_BOTH) { + LOG_E(TAG, "ES7210 is input-only"); + return ERROR_NOT_SUPPORTED; + } + + if (data->is_open) { + bool same_config = config->bits_per_sample == data->open_bits_per_sample + && config->sample_rate == data->open_sample_rate; + return same_config ? ERROR_NONE : ERROR_RESOURCE; + } + + // Open with tdm_slot_count channels, not the (possibly smaller) active mic count. + // tdm_slot_count is set in start_device() to match whichever framing the vendor + // driver will actually use: the full 4-slot TDM frame when TDM mode engages + // (mic_count >= 3), or exactly mic_count when it doesn't (plain N-channel I2S -- + // see start_device()'s tdm_slot_count comment). Opening with the wrong channel + // count here mismatches the I2S receive layout the codec actually produces, + // corrupting every channel past the first. When TDM *is* engaged, using the full + // slot count (rather than just the active mic count) also sidesteps a separate + // vendor quirk: es7210_set_fs() silently halves the configured bit depth when asked + // for <= 2 channels in TDM mode (channel_mask == 0). We demux down to the active + // mic slots ourselves in read(). + uint8_t hw_channels = data->tdm_slot_count; + + esp_codec_dev_sample_info_t sample_info = { + .bits_per_sample = config->bits_per_sample, + .channel = hw_channels, + .channel_mask = 0, + .sample_rate = config->sample_rate, + .mclk_multiple = 0, + }; + + if (esp_codec_dev_open(data->codec_device, &sample_info) != ESP_CODEC_DEV_OK) { + LOG_E(TAG, "Failed to open codec device"); + return ERROR_RESOURCE; + } + + data->open_bits_per_sample = config->bits_per_sample; + data->open_sample_rate = config->sample_rate; + data->is_open = true; + return ERROR_NONE; +} + +error_t close(Device* device) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + if (data->is_open) { + esp_codec_dev_close(data->codec_device); + data->is_open = false; + } + + return ERROR_NONE; +} + +error_t read(Device* device, void* buffer, size_t size, size_t* bytes_read, TickType_t timeout) { + (void) timeout; + auto* data = GET_DATA(device); + if (!data->is_open) { + return ERROR_RESOURCE; + } + + // The hardware always runs at tdm_slot_count channels (see open() for why); demux down + // to just the active mic slots here so callers see exactly mic_count channels of real + // signal -- never diluted by silent/AEC-reference TDM slots. + if (data->mic_count == data->tdm_slot_count) { + int result = esp_codec_dev_read(data->codec_device, buffer, (int) size); + if (result < 0) { + return ERROR_RESOURCE; + } + *bytes_read = (size_t) result; + return ERROR_NONE; + } + + uint8_t bytes_per_sample = (uint8_t) (data->open_bits_per_sample / 8); + size_t out_frame_bytes = (size_t) data->mic_count * bytes_per_sample; + size_t requested_frames = (out_frame_bytes != 0) ? (size / out_frame_bytes) : 0; + if (requested_frames == 0) { + *bytes_read = 0; + return ERROR_NONE; + } + + size_t raw_frame_bytes = (size_t) data->tdm_slot_count * bytes_per_sample; + size_t raw_bytes_needed = requested_frames * raw_frame_bytes; + if (data->raw_buffer.size() < raw_bytes_needed) { + data->raw_buffer.resize(raw_bytes_needed); + } + + int result = esp_codec_dev_read(data->codec_device, data->raw_buffer.data(), (int) raw_bytes_needed); + if (result < 0) { + return ERROR_RESOURCE; + } + + size_t raw_bytes_read = (size_t) result; + size_t frames_read = raw_bytes_read / raw_frame_bytes; + + // Demux: pick the bytes_per_sample-wide slot for each set bit in mic_mask, in ascending + // slot order, e.g. mic_mask = MIC1|MIC3 -> slots 0 and 2. + uint8_t slot_indices[8]; + uint8_t slot_count = 0; + for (uint8_t slot = 0; slot < data->tdm_slot_count && slot_count < sizeof(slot_indices); slot++) { + if ((data->mic_mask & (uint8_t) (1u << slot)) != 0) { + slot_indices[slot_count++] = slot; + } + } + + auto* out = static_cast(buffer); + const uint8_t* raw = data->raw_buffer.data(); + for (size_t frame = 0; frame < frames_read; frame++) { + const uint8_t* raw_frame = raw + frame * raw_frame_bytes; + uint8_t* out_frame = out + frame * out_frame_bytes; + for (uint8_t ch = 0; ch < slot_count && ch < data->mic_count; ch++) { + std::memcpy(out_frame + (size_t) ch * bytes_per_sample, raw_frame + (size_t) slot_indices[ch] * bytes_per_sample, bytes_per_sample); + } + } + + *bytes_read = frames_read * out_frame_bytes; + return ERROR_NONE; +} + +error_t write(Device* device, const void* buffer, size_t size, size_t* bytes_written, TickType_t timeout) { + (void) device; + (void) buffer; + (void) size; + (void) bytes_written; + (void) timeout; + return ERROR_NOT_SUPPORTED; +} + +error_t set_volume(Device* device, AudioCodecDirection direction, float volume_percent) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr || direction != AUDIO_CODEC_DIR_INPUT) { + return ERROR_NOT_SUPPORTED; + } + + // Map 0..100% linearly onto the gain range. + float db = (volume_percent / 100.0f) * MAX_INPUT_GAIN_DB; + return (esp_codec_dev_set_in_gain(data->codec_device, db) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE; +} + +error_t get_volume(Device* device, AudioCodecDirection direction, float* volume_percent) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr || direction != AUDIO_CODEC_DIR_INPUT) { + return ERROR_NOT_SUPPORTED; + } + + float db = 0.0f; + if (esp_codec_dev_get_in_gain(data->codec_device, &db) != ESP_CODEC_DEV_OK) { + return ERROR_RESOURCE; + } + *volume_percent = (db / MAX_INPUT_GAIN_DB) * 100.0f; + return ERROR_NONE; +} + +error_t set_mute(Device* device, AudioCodecDirection direction, bool muted) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr || direction != AUDIO_CODEC_DIR_INPUT) { + return ERROR_NOT_SUPPORTED; + } + + return (esp_codec_dev_set_in_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE; +} + +error_t get_mute(Device* device, AudioCodecDirection direction, bool* muted) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr || direction != AUDIO_CODEC_DIR_INPUT) { + return ERROR_NOT_SUPPORTED; + } + + return (esp_codec_dev_get_in_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE; +} + +error_t get_native_channels(Device* device, AudioCodecDirection direction, uint8_t* channels) { + auto* data = GET_DATA(device); + if (direction != AUDIO_CODEC_DIR_INPUT) { + return ERROR_NOT_SUPPORTED; + } + *channels = data->mic_count; + return ERROR_NONE; +} + +error_t get_native_sample_rate(Device* device, AudioCodecDirection direction, uint32_t* rate_hz) { + (void) device; + if (direction != AUDIO_CODEC_DIR_INPUT) { + return ERROR_NOT_SUPPORTED; + } + *rate_hz = NATIVE_SAMPLE_RATE; + return ERROR_NONE; +} + +error_t get_capabilities(Device* device, AudioCodecDirection* supported_directions) { + (void) device; + *supported_directions = AUDIO_CODEC_DIR_INPUT; + return ERROR_NONE; +} + +error_t get_input_gain_multiplier(Device* device, float* gain) { + auto* data = GET_DATA(device); + *gain = data->input_gain; + return ERROR_NONE; +} + +static const struct AudioCodecApi API = { + .open = open, + .close = close, + .read = read, + .write = write, + .set_volume = set_volume, + .get_volume = get_volume, + .set_mute = set_mute, + .get_mute = get_mute, + .get_native_sample_rate = get_native_sample_rate, + .get_native_channels = get_native_channels, + .get_capabilities = get_capabilities, + .get_input_gain_multiplier = get_input_gain_multiplier, +}; + +// endregion + +// region Driver lifecycle + +error_t start_device(Device* device) { + const auto* config = GET_CONFIG(device); + + // devicetree's int property type has no min/max constraint support, so a bogus + // negative literal would otherwise silently wrap to 65535 when narrowed to uint16_t. + // Bound it here instead -- 2000 (20x) is already an extreme upper bound for + // compensating a quiet mic capsule on top of the ES7210's own hardware gain. + if (config->input_gain_percent > 2000) { + LOG_E(TAG, "Invalid input_gain_percent %u (must be 0..2000)", config->input_gain_percent); + return ERROR_RESOURCE; + } + + // Same rationale as input_gain_percent: devicetree ints aren't bit-constrained, so + // reject any bit outside the four supported mic slots before it inflates mic_count + // beyond the fixed 4-slot TDM frame that read() demuxes against. + constexpr uint8_t supported_mic_mask = (uint8_t) (ES7210_SEL_MIC1 | ES7210_SEL_MIC2 | ES7210_SEL_MIC3 | ES7210_SEL_MIC4); + if ((config->mic_selected_mask & ~supported_mic_mask) != 0) { + LOG_E(TAG, "Invalid mic_selected_mask 0x%02X (must be subset of 0x%02X)", config->mic_selected_mask, supported_mic_mask); + return ERROR_RESOURCE; + } + + auto* i2c_controller = device_get_parent(device); + if (i2c_controller == nullptr || device_get_type(i2c_controller) != &I2C_CONTROLLER_TYPE) { + LOG_E(TAG, "Parent is not an I2C controller"); + return ERROR_RESOURCE; + } + + auto* i2s_controller = config->i2s_device; + if (i2s_controller == nullptr || device_get_type(i2s_controller) != &I2S_CONTROLLER_TYPE) { + LOG_E(TAG, "I2S controller device is not valid"); + return ERROR_RESOURCE; + } + + auto* data = new Es7210Data(); + + uint8_t mic_mask = (config->mic_selected_mask != 0) + ? config->mic_selected_mask + : (uint8_t) (ES7210_SEL_MIC1 | ES7210_SEL_MIC2 | ES7210_SEL_MIC3 | ES7210_SEL_MIC4); + data->mic_mask = mic_mask; + data->mic_count = (uint8_t) __builtin_popcount(mic_mask); + // Mirrors the vendor driver's own TDM threshold (es7210_is_tdm_mode(): TDM only + // engages when mic_num >= 3) -- below that it runs a plain N-channel I2S frame, not + // a fixed 4-slot TDM frame. Forcing a 4-channel open (see open()'s hw_channels + // comment) against a non-TDM 2-mic config mismatches the I2S receive layout the + // codec is actually producing, corrupting every channel past the first. + data->tdm_slot_count = (data->mic_count >= 3) ? 4 : data->mic_count; + data->input_gain = (float) config->input_gain_percent / 100.0f; + + data->ctrl_if = audio_codec_adapter_new_i2c_ctrl(i2c_controller, config->address); + data->data_if = audio_codec_adapter_new_i2s_data(i2s_controller); + if (data->ctrl_if == nullptr || data->data_if == nullptr) { + LOG_E(TAG, "Failed to create adapters"); + goto cleanup; + } + + if (data->ctrl_if->open(data->ctrl_if, nullptr, 0) != ESP_CODEC_DEV_OK) { + LOG_E(TAG, "Failed to open control interface"); + goto cleanup; + } + + if (data->data_if->open(data->data_if, nullptr, 0) != ESP_CODEC_DEV_OK) { + LOG_E(TAG, "Failed to open data interface"); + goto cleanup; + } + + { + es7210_codec_cfg_t codec_config = {}; + codec_config.ctrl_if = data->ctrl_if; + codec_config.master_mode = false; + codec_config.mic_selected = mic_mask; + codec_config.mclk_src = ES7210_MCLK_FROM_PAD; + codec_config.mclk_div = 0; + + data->codec_if = es7210_codec_new(&codec_config); + if (data->codec_if == nullptr) { + LOG_E(TAG, "Failed to create ES7210 codec interface"); + goto cleanup; + } + } + + { + esp_codec_dev_cfg_t dev_config = { + .dev_type = ESP_CODEC_DEV_TYPE_IN, + .codec_if = data->codec_if, + .data_if = data->data_if, + }; + + data->codec_device = esp_codec_dev_new(&dev_config); + if (data->codec_device == nullptr) { + LOG_E(TAG, "Failed to create codec device"); + goto cleanup; + } + } + + device_set_driver_data(device, data); + return ERROR_NONE; + +cleanup: + // Mirrors stop_device's teardown order -- delete_*_if() routines close their interface + // first, so we don't need separate ->close() calls here. + if (data->codec_device != nullptr) { + esp_codec_dev_delete(data->codec_device); + } + if (data->codec_if != nullptr) { + audio_codec_delete_codec_if(data->codec_if); + } + if (data->data_if != nullptr) { + audio_codec_delete_data_if(data->data_if); + } + if (data->ctrl_if != nullptr) { + audio_codec_delete_ctrl_if(data->ctrl_if); + } + delete data; + return ERROR_RESOURCE; +} + +error_t stop_device(Device* device) { + auto* data = GET_DATA(device); + if (data == nullptr) { + return ERROR_NONE; + } + + if (data->is_open) { + esp_codec_dev_close(data->codec_device); + } + + if (data->codec_device != nullptr) { + esp_codec_dev_delete(data->codec_device); + } + if (data->codec_if != nullptr) { + audio_codec_delete_codec_if(data->codec_if); + } + if (data->data_if != nullptr) { + audio_codec_delete_data_if(data->data_if); + } + if (data->ctrl_if != nullptr) { + audio_codec_delete_ctrl_if(data->ctrl_if); + } + + device_set_driver_data(device, nullptr); + delete data; + return ERROR_NONE; +} + +// endregion + +} // namespace + +extern "C" { + +Driver es7210_driver = { + .name = "es7210", + .compatible = (const char*[]) { "everest,es7210", nullptr }, + .start_device = start_device, + .stop_device = stop_device, + .api = &API, + .device_type = &AUDIO_CODEC_TYPE, + .owner = nullptr, + .internal = nullptr, +}; + +} diff --git a/Drivers/es7210-module/source/module.cpp b/Drivers/es7210-module/source/module.cpp new file mode 100644 index 000000000..a10c4334f --- /dev/null +++ b/Drivers/es7210-module/source/module.cpp @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver es7210_driver; + +static error_t start() { + check(driver_construct_add(&es7210_driver) == ERROR_NONE); + return ERROR_NONE; +} + +static error_t stop() { + check(driver_remove_destruct(&es7210_driver) == ERROR_NONE); + return ERROR_NONE; +} + +extern const ModuleSymbol es7210_module_symbols[]; + +Module es7210_module = { + .name = "es7210", + .start = start, + .stop = stop, + .symbols = es7210_module_symbols, + .internal = nullptr +}; + +} diff --git a/Drivers/es7210-module/source/symbols.c b/Drivers/es7210-module/source/symbols.c new file mode 100644 index 000000000..fd5c44891 --- /dev/null +++ b/Drivers/es7210-module/source/symbols.c @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +const struct ModuleSymbol es7210_module_symbols[] = { + MODULE_SYMBOL_TERMINATOR +}; diff --git a/Drivers/es8311-module/CMakeLists.txt b/Drivers/es8311-module/CMakeLists.txt new file mode 100644 index 000000000..d27920108 --- /dev/null +++ b/Drivers/es8311-module/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +file(GLOB_RECURSE SOURCE_FILES "source/*.c*") + +tactility_add_module(es8311-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel audio-codec-module esp_codec_dev +) diff --git a/Drivers/es8311-module/LICENSE-Apache-2.0.md b/Drivers/es8311-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/es8311-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Drivers/es8311-module/README.md b/Drivers/es8311-module/README.md new file mode 100644 index 000000000..abf2f7a8d --- /dev/null +++ b/Drivers/es8311-module/README.md @@ -0,0 +1,12 @@ +# ES8311 audio codec + +A driver for the `ES8311` audio codec by Everest Semiconductor, wired as a dual +in/out `AUDIO_CODEC_TYPE` device (speaker output + microphone input on the same +chip). The I2C bus is the device's parent; the I2S controller carrying audio data +is referenced via a devicetree phandle. + +Wraps Espressif's `esp_codec_dev` ES8311 implementation. + +See https://www.everest-semi.com/pdf/ES8311%20PB.pdf + +License: [Apache v2.0](LICENSE-Apache-2.0.md) diff --git a/Drivers/es8311-module/bindings/everest,es8311.yaml b/Drivers/es8311-module/bindings/everest,es8311.yaml new file mode 100644 index 000000000..dbcbc3e14 --- /dev/null +++ b/Drivers/es8311-module/bindings/everest,es8311.yaml @@ -0,0 +1,11 @@ +description: Everest Semiconductor ES8311 audio codec (speaker output + mic input, dual mode) + +include: [ "i2c-device.yaml" ] + +compatible: "everest,es8311" + +properties: + i2s: + type: phandle + required: true + description: "I2S controller device that carries audio data" diff --git a/Drivers/es8311-module/devicetree.yaml b/Drivers/es8311-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/es8311-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/es8311-module/include/bindings/es8311.h b/Drivers/es8311-module/include/bindings/es8311.h new file mode 100644 index 000000000..9b3a1434d --- /dev/null +++ b/Drivers/es8311-module/include/bindings/es8311.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +DEFINE_DEVICETREE(es8311, struct Es8311Config) + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/es8311-module/include/drivers/es8311.h b/Drivers/es8311-module/include/drivers/es8311.h new file mode 100644 index 000000000..f51e18275 --- /dev/null +++ b/Drivers/es8311-module/include/drivers/es8311.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include + +struct Device; + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief ES8311 codec device configuration. + * + * The ES8311 is wired as both control (I2C) and audio (I2S) device: the I2C + * bus is the device's parent (per i2c-device.yaml), while the I2S controller + * is referenced via a devicetree phandle. It operates in dual mode + * (speaker output + mic input) on boards such as M5Stack StickS3. + */ +struct Es8311Config { + /** I2C address on the bus (typically 0x18) */ + uint8_t address; + /** I2S controller device that carries audio data */ + struct Device* i2s_device; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/es8311-module/source/es8311.cpp b/Drivers/es8311-module/source/es8311.cpp new file mode 100644 index 000000000..6b3c0ebc9 --- /dev/null +++ b/Drivers/es8311-module/source/es8311.cpp @@ -0,0 +1,400 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#define TAG "ES8311" + +namespace { + +// Volume curve maps 1..100 to roughly -49.5dB..0dB; native ES8311 range is wide enough +// that we expose a flat 0..100 percent scale to callers and let esp_codec_dev convert. +constexpr uint32_t NATIVE_SAMPLE_RATE = 44100; + +struct Es8311Data { + const audio_codec_ctrl_if_t* ctrl_if = nullptr; + const audio_codec_data_if_t* data_if = nullptr; + const audio_codec_gpio_if_t* gpio_if = nullptr; + const audio_codec_if_t* codec_if = nullptr; + esp_codec_dev_handle_t codec_device = nullptr; + bool is_open = false; + AudioCodecDirection open_direction = AUDIO_CODEC_DIR_BOTH; + esp_codec_dev_sample_info_t open_sample_info = {}; +}; + +#define GET_CONFIG(device) (static_cast((device)->config)) +#define GET_DATA(device) (static_cast(device_get_driver_data(device))) + +// region AudioCodecApi + +error_t open(Device* device, const struct AudioCodecStreamConfig* config) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + esp_codec_dev_sample_info_t sample_info = { + .bits_per_sample = config->bits_per_sample, + .channel = config->channels, + .channel_mask = 0, + .sample_rate = config->sample_rate, + .mclk_multiple = 0, + }; + + if (data->is_open) { + // open_direction == BOTH already serves INPUT-only or OUTPUT-only requests on the + // same sample settings -- only an exact direction mismatch (e.g. requesting BOTH + // while opened for INPUT only) needs a reopen. + bool direction_compatible = data->open_direction == config->direction + || data->open_direction == AUDIO_CODEC_DIR_BOTH; + bool same_config = direction_compatible + && data->open_sample_info.bits_per_sample == sample_info.bits_per_sample + && data->open_sample_info.channel == sample_info.channel + && data->open_sample_info.sample_rate == sample_info.sample_rate; + return same_config ? ERROR_NONE : ERROR_RESOURCE; + } + + if (esp_codec_dev_open(data->codec_device, &sample_info) != ESP_CODEC_DEV_OK) { + LOG_E(TAG, "Failed to open codec device"); + return ERROR_RESOURCE; + } + + data->is_open = true; + data->open_direction = config->direction; + data->open_sample_info = sample_info; + return ERROR_NONE; +} + +error_t close(Device* device) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + if (data->is_open) { + esp_codec_dev_close(data->codec_device); + data->is_open = false; + } + + return ERROR_NONE; +} + +error_t read(Device* device, void* buffer, size_t size, size_t* bytes_read, TickType_t timeout) { + (void) timeout; + auto* data = GET_DATA(device); + if (!data->is_open) { + return ERROR_RESOURCE; + } + + // esp_codec_dev_read returns the number of bytes read (>= 0) on success, or a negative + // ESP_CODEC_DEV_* error code on failure -- it does NOT return ESP_CODEC_DEV_OK (0) for + // a successful nonzero-length read. + int result = esp_codec_dev_read(data->codec_device, buffer, (int) size); + if (result < 0) { + return ERROR_RESOURCE; + } + + *bytes_read = (size_t) result; + return ERROR_NONE; +} + +error_t write(Device* device, const void* buffer, size_t size, size_t* bytes_written, TickType_t timeout) { + (void) timeout; + auto* data = GET_DATA(device); + if (!data->is_open) { + return ERROR_RESOURCE; + } + + // esp_codec_dev_write returns the number of bytes written (>= 0) on success, or a negative + // ESP_CODEC_DEV_* error code on failure -- it does NOT return ESP_CODEC_DEV_OK (0) for + // a successful nonzero-length write. + int result = esp_codec_dev_write(data->codec_device, const_cast(buffer), (int) size); + if (result < 0) { + return ERROR_RESOURCE; + } + + *bytes_written = (size_t) result; + return ERROR_NONE; +} + +error_t set_volume(Device* device, AudioCodecDirection direction, float volume_percent) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + if (volume_percent < 0.0f || volume_percent > 100.0f) { + return ERROR_RESOURCE; + } + + if (direction == AUDIO_CODEC_DIR_OUTPUT) { + int volume = (int) std::lround(volume_percent); + return (esp_codec_dev_set_out_vol(data->codec_device, volume) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE; + } + + if (direction == AUDIO_CODEC_DIR_INPUT) { + // ES8311 ADC gain range is roughly 0..24 dB; map 0..100% linearly onto it. + float db = (volume_percent / 100.0f) * 24.0f; + return (esp_codec_dev_set_in_gain(data->codec_device, db) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE; + } + + return ERROR_NOT_SUPPORTED; +} + +error_t get_volume(Device* device, AudioCodecDirection direction, float* volume_percent) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + if (direction == AUDIO_CODEC_DIR_OUTPUT) { + int volume = 0; + if (esp_codec_dev_get_out_vol(data->codec_device, &volume) != ESP_CODEC_DEV_OK) { + return ERROR_RESOURCE; + } + *volume_percent = (float) volume; + return ERROR_NONE; + } + + if (direction == AUDIO_CODEC_DIR_INPUT) { + float db = 0.0f; + if (esp_codec_dev_get_in_gain(data->codec_device, &db) != ESP_CODEC_DEV_OK) { + return ERROR_RESOURCE; + } + *volume_percent = (db / 24.0f) * 100.0f; + return ERROR_NONE; + } + + return ERROR_NOT_SUPPORTED; +} + +error_t set_mute(Device* device, AudioCodecDirection direction, bool muted) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + if (direction == AUDIO_CODEC_DIR_OUTPUT) { + return (esp_codec_dev_set_out_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE; + } + + if (direction == AUDIO_CODEC_DIR_INPUT) { + return (esp_codec_dev_set_in_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE; + } + + return ERROR_NOT_SUPPORTED; +} + +error_t get_mute(Device* device, AudioCodecDirection direction, bool* muted) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + if (direction == AUDIO_CODEC_DIR_OUTPUT) { + return (esp_codec_dev_get_out_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE; + } + + if (direction == AUDIO_CODEC_DIR_INPUT) { + return (esp_codec_dev_get_in_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE; + } + + return ERROR_NOT_SUPPORTED; +} + +error_t get_native_channels(Device* device, AudioCodecDirection direction, uint8_t* channels) { + (void) device; + if (direction != AUDIO_CODEC_DIR_INPUT && direction != AUDIO_CODEC_DIR_OUTPUT) { + return ERROR_NOT_SUPPORTED; + } + *channels = (direction == AUDIO_CODEC_DIR_INPUT) ? 1 : 2; + return ERROR_NONE; +} + +error_t get_native_sample_rate(Device* device, AudioCodecDirection direction, uint32_t* rate_hz) { + (void) device; + (void) direction; + *rate_hz = NATIVE_SAMPLE_RATE; + return ERROR_NONE; +} + +error_t get_capabilities(Device* device, AudioCodecDirection* supported_directions) { + (void) device; + *supported_directions = AUDIO_CODEC_DIR_BOTH; + return ERROR_NONE; +} + +static const struct AudioCodecApi API = { + .open = open, + .close = close, + .read = read, + .write = write, + .set_volume = set_volume, + .get_volume = get_volume, + .set_mute = set_mute, + .get_mute = get_mute, + .get_native_sample_rate = get_native_sample_rate, + .get_native_channels = get_native_channels, + .get_capabilities = get_capabilities, + .get_input_gain_multiplier = nullptr, +}; + +// endregion + +// region Driver lifecycle + +error_t start_device(Device* device) { + const auto* config = GET_CONFIG(device); + + auto* i2c_controller = device_get_parent(device); + if (i2c_controller == nullptr || device_get_type(i2c_controller) != &I2C_CONTROLLER_TYPE) { + LOG_E(TAG, "Parent is not an I2C controller"); + return ERROR_RESOURCE; + } + + auto* i2s_controller = config->i2s_device; + if (i2s_controller == nullptr || device_get_type(i2s_controller) != &I2S_CONTROLLER_TYPE) { + LOG_E(TAG, "I2S controller device is not valid"); + return ERROR_RESOURCE; + } + + auto* data = new Es8311Data(); + + data->ctrl_if = audio_codec_adapter_new_i2c_ctrl(i2c_controller, config->address); + data->data_if = audio_codec_adapter_new_i2s_data(i2s_controller); + if (data->ctrl_if == nullptr || data->data_if == nullptr) { + LOG_E(TAG, "Failed to create adapters"); + goto cleanup; + } + + if (data->ctrl_if->open(data->ctrl_if, nullptr, 0) != ESP_CODEC_DEV_OK) { + LOG_E(TAG, "Failed to open control interface"); + goto cleanup; + } + + if (data->data_if->open(data->data_if, nullptr, 0) != ESP_CODEC_DEV_OK) { + LOG_E(TAG, "Failed to open data interface"); + goto cleanup; + } + + { + es8311_codec_cfg_t codec_config = {}; + codec_config.ctrl_if = data->ctrl_if; + codec_config.gpio_if = nullptr; + codec_config.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH; + codec_config.pa_pin = -1; + codec_config.pa_reverted = false; + codec_config.master_mode = false; + codec_config.use_mclk = true; + codec_config.digital_mic = false; + codec_config.invert_mclk = false; + codec_config.invert_sclk = false; + codec_config.no_dac_ref = false; + codec_config.mclk_div = 0; + + data->codec_if = es8311_codec_new(&codec_config); + if (data->codec_if == nullptr) { + LOG_E(TAG, "Failed to create ES8311 codec interface"); + goto cleanup; + } + } + + { + esp_codec_dev_cfg_t dev_config = { + .dev_type = ESP_CODEC_DEV_TYPE_IN_OUT, + .codec_if = data->codec_if, + .data_if = data->data_if, + }; + + data->codec_device = esp_codec_dev_new(&dev_config); + if (data->codec_device == nullptr) { + LOG_E(TAG, "Failed to create codec device"); + goto cleanup; + } + } + + device_set_driver_data(device, data); + return ERROR_NONE; + +cleanup: + // Mirrors stop_device's teardown order -- delete_*_if() routines close their interface + // first, so we don't need separate ->close() calls here. + if (data->codec_device != nullptr) { + esp_codec_dev_delete(data->codec_device); + } + if (data->codec_if != nullptr) { + audio_codec_delete_codec_if(data->codec_if); + } + if (data->gpio_if != nullptr) { + audio_codec_adapter_delete_gpio(data->gpio_if); + } + if (data->data_if != nullptr) { + audio_codec_delete_data_if(data->data_if); + } + if (data->ctrl_if != nullptr) { + audio_codec_delete_ctrl_if(data->ctrl_if); + } + delete data; + return ERROR_RESOURCE; +} + +error_t stop_device(Device* device) { + auto* data = GET_DATA(device); + if (data == nullptr) { + return ERROR_NONE; + } + + if (data->is_open) { + esp_codec_dev_close(data->codec_device); + } + + if (data->codec_device != nullptr) { + esp_codec_dev_delete(data->codec_device); + } + if (data->codec_if != nullptr) { + audio_codec_delete_codec_if(data->codec_if); + } + if (data->gpio_if != nullptr) { + audio_codec_adapter_delete_gpio(data->gpio_if); + } + if (data->data_if != nullptr) { + audio_codec_delete_data_if(data->data_if); + } + if (data->ctrl_if != nullptr) { + audio_codec_delete_ctrl_if(data->ctrl_if); + } + + device_set_driver_data(device, nullptr); + delete data; + return ERROR_NONE; +} + +// endregion + +} // namespace + +extern "C" { + +Driver es8311_driver = { + .name = "es8311", + .compatible = (const char*[]) { "everest,es8311", nullptr }, + .start_device = start_device, + .stop_device = stop_device, + .api = &API, + .device_type = &AUDIO_CODEC_TYPE, + .owner = nullptr, + .internal = nullptr, +}; + +} diff --git a/Drivers/es8311-module/source/module.cpp b/Drivers/es8311-module/source/module.cpp new file mode 100644 index 000000000..e71eedd07 --- /dev/null +++ b/Drivers/es8311-module/source/module.cpp @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver es8311_driver; + +static error_t start() { + check(driver_construct_add(&es8311_driver) == ERROR_NONE); + return ERROR_NONE; +} + +static error_t stop() { + check(driver_remove_destruct(&es8311_driver) == ERROR_NONE); + return ERROR_NONE; +} + +extern const ModuleSymbol es8311_module_symbols[]; + +Module es8311_module = { + .name = "es8311", + .start = start, + .stop = stop, + .symbols = es8311_module_symbols, + .internal = nullptr +}; + +} diff --git a/Drivers/es8311-module/source/symbols.c b/Drivers/es8311-module/source/symbols.c new file mode 100644 index 000000000..b7b925031 --- /dev/null +++ b/Drivers/es8311-module/source/symbols.c @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +const struct ModuleSymbol es8311_module_symbols[] = { + MODULE_SYMBOL_TERMINATOR +}; diff --git a/Drivers/es8388-module/CMakeLists.txt b/Drivers/es8388-module/CMakeLists.txt new file mode 100644 index 000000000..6001c6f42 --- /dev/null +++ b/Drivers/es8388-module/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +file(GLOB_RECURSE SOURCE_FILES "source/*.c*") + +tactility_add_module(es8388-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel audio-codec-module esp_codec_dev +) diff --git a/Drivers/es8388-module/LICENSE-Apache-2.0.md b/Drivers/es8388-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/es8388-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Drivers/es8388-module/README.md b/Drivers/es8388-module/README.md new file mode 100644 index 000000000..2a4544712 --- /dev/null +++ b/Drivers/es8388-module/README.md @@ -0,0 +1,11 @@ +# ES8388 audio codec + +A driver for the `ES8388` audio codec by Everest Semiconductor, wired as a dual +in/out `AUDIO_CODEC_TYPE` device (speaker output + line/mic input). The I2C bus is +the device's parent; the I2S controller carrying audio data is referenced via a devicetree phandle. + +Wraps Espressif's `esp_codec_dev` ES8388 implementation. + +See https://www.everest-semi.com/pdf/ES8388%20DS.pdf + +License: [Apache v2.0](LICENSE-Apache-2.0.md) diff --git a/Drivers/es8388-module/bindings/everest,es8388.yaml b/Drivers/es8388-module/bindings/everest,es8388.yaml new file mode 100644 index 000000000..730dc21c9 --- /dev/null +++ b/Drivers/es8388-module/bindings/everest,es8388.yaml @@ -0,0 +1,11 @@ +description: Everest Semiconductor ES8388 audio codec (speaker output + line/mic input) + +include: ["i2c-device.yaml"] + +compatible: "everest,es8388" + +properties: + i2s: + type: phandle + required: true + description: "I2S controller device that carries audio data" diff --git a/Drivers/es8388-module/devicetree.yaml b/Drivers/es8388-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/es8388-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/es8388-module/include/bindings/es8388.h b/Drivers/es8388-module/include/bindings/es8388.h new file mode 100644 index 000000000..5ae3aa0d8 --- /dev/null +++ b/Drivers/es8388-module/include/bindings/es8388.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +DEFINE_DEVICETREE(es8388, struct Es8388Config) + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/es8388-module/include/drivers/es8388.h b/Drivers/es8388-module/include/drivers/es8388.h new file mode 100644 index 000000000..ec3a1a266 --- /dev/null +++ b/Drivers/es8388-module/include/drivers/es8388.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include + +struct Device; + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief ES8388 codec device configuration. + * + * The ES8388 is wired as both control (I2C) and audio (I2S) device: the I2C + * bus is the device's parent (per i2c-device.yaml), while the I2S controller + * is referenced via a devicetree phandle. + */ +struct Es8388Config { + /** I2C address on the bus (typically 0x10) */ + uint8_t address; + /** I2S controller device that carries audio data */ + struct Device* i2s_device; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/es8388-module/include/es8388_module.h b/Drivers/es8388-module/include/es8388_module.h new file mode 100644 index 000000000..22900ff03 --- /dev/null +++ b/Drivers/es8388-module/include/es8388_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module es8388_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/es8388-module/source/es8388.cpp b/Drivers/es8388-module/source/es8388.cpp new file mode 100644 index 000000000..20b0f6fc3 --- /dev/null +++ b/Drivers/es8388-module/source/es8388.cpp @@ -0,0 +1,394 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#define TAG "ES8388" + +namespace { + +// Volume curve maps 1..100 to roughly -49.5dB..0dB; native ES8388 range is wide enough +// that we expose a flat 0..100 percent scale to callers and let esp_codec_dev convert. +constexpr uint32_t NATIVE_SAMPLE_RATE = 44100; + +struct Es8388Data { + const audio_codec_ctrl_if_t* ctrl_if = nullptr; + const audio_codec_data_if_t* data_if = nullptr; + const audio_codec_gpio_if_t* gpio_if = nullptr; + const audio_codec_if_t* codec_if = nullptr; + esp_codec_dev_handle_t codec_device = nullptr; + bool is_open = false; + AudioCodecDirection open_direction = AUDIO_CODEC_DIR_BOTH; + esp_codec_dev_sample_info_t open_sample_info = {}; +}; + +#define GET_CONFIG(device) (static_cast((device)->config)) +#define GET_DATA(device) (static_cast(device_get_driver_data(device))) + +// region AudioCodecApi + +error_t open(Device* device, const struct AudioCodecStreamConfig* config) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + esp_codec_dev_sample_info_t sample_info = { + .bits_per_sample = config->bits_per_sample, + .channel = config->channels, + .channel_mask = 0, + .sample_rate = config->sample_rate, + .mclk_multiple = 0, + }; + + if (data->is_open) { + // open_direction == BOTH already serves INPUT-only or OUTPUT-only requests on the + // same sample settings -- only an exact direction mismatch (e.g. requesting BOTH + // while opened for INPUT only) needs a reopen. + bool direction_compatible = data->open_direction == config->direction + || data->open_direction == AUDIO_CODEC_DIR_BOTH; + bool same_config = direction_compatible + && data->open_sample_info.bits_per_sample == sample_info.bits_per_sample + && data->open_sample_info.channel == sample_info.channel + && data->open_sample_info.sample_rate == sample_info.sample_rate; + return same_config ? ERROR_NONE : ERROR_RESOURCE; + } + + if (esp_codec_dev_open(data->codec_device, &sample_info) != ESP_CODEC_DEV_OK) { + LOG_E(TAG, "Failed to open codec device"); + return ERROR_RESOURCE; + } + + data->is_open = true; + data->open_direction = config->direction; + data->open_sample_info = sample_info; + return ERROR_NONE; +} + +error_t close(Device* device) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + if (data->is_open) { + esp_codec_dev_close(data->codec_device); + data->is_open = false; + } + + return ERROR_NONE; +} + +error_t read(Device* device, void* buffer, size_t size, size_t* bytes_read, TickType_t timeout) { + (void) timeout; + auto* data = GET_DATA(device); + if (!data->is_open) { + return ERROR_RESOURCE; + } + + // esp_codec_dev_read returns the number of bytes read (>= 0) on success, or a negative + // ESP_CODEC_DEV_* error code on failure -- it does NOT return ESP_CODEC_DEV_OK (0) for + // a successful nonzero-length read. + int result = esp_codec_dev_read(data->codec_device, buffer, (int) size); + if (result < 0) { + return ERROR_RESOURCE; + } + + *bytes_read = (size_t) result; + return ERROR_NONE; +} + +error_t write(Device* device, const void* buffer, size_t size, size_t* bytes_written, TickType_t timeout) { + (void) timeout; + auto* data = GET_DATA(device); + if (!data->is_open) { + return ERROR_RESOURCE; + } + + // esp_codec_dev_write returns the number of bytes written (>= 0) on success, or a negative + // ESP_CODEC_DEV_* error code on failure -- it does NOT return ESP_CODEC_DEV_OK (0) for + // a successful nonzero-length write. + int result = esp_codec_dev_write(data->codec_device, const_cast(buffer), (int) size); + if (result < 0) { + return ERROR_RESOURCE; + } + + *bytes_written = (size_t) result; + return ERROR_NONE; +} + +error_t set_volume(Device* device, AudioCodecDirection direction, float volume_percent) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + if (volume_percent < 0.0f || volume_percent > 100.0f) { + return ERROR_RESOURCE; + } + + if (direction == AUDIO_CODEC_DIR_OUTPUT) { + int volume = (int) std::lround(volume_percent); + return (esp_codec_dev_set_out_vol(data->codec_device, volume) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE; + } + + if (direction == AUDIO_CODEC_DIR_INPUT) { + // ES8388 ADC gain range is roughly 0..24 dB; map 0..100% linearly onto it. + float db = (volume_percent / 100.0f) * 24.0f; + return (esp_codec_dev_set_in_gain(data->codec_device, db) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE; + } + + return ERROR_NOT_SUPPORTED; +} + +error_t get_volume(Device* device, AudioCodecDirection direction, float* volume_percent) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + if (direction == AUDIO_CODEC_DIR_OUTPUT) { + int volume = 0; + if (esp_codec_dev_get_out_vol(data->codec_device, &volume) != ESP_CODEC_DEV_OK) { + return ERROR_RESOURCE; + } + *volume_percent = (float) volume; + return ERROR_NONE; + } + + if (direction == AUDIO_CODEC_DIR_INPUT) { + float db = 0.0f; + if (esp_codec_dev_get_in_gain(data->codec_device, &db) != ESP_CODEC_DEV_OK) { + return ERROR_RESOURCE; + } + *volume_percent = (db / 24.0f) * 100.0f; + return ERROR_NONE; + } + + return ERROR_NOT_SUPPORTED; +} + +error_t set_mute(Device* device, AudioCodecDirection direction, bool muted) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + if (direction == AUDIO_CODEC_DIR_OUTPUT) { + return (esp_codec_dev_set_out_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE; + } + + if (direction == AUDIO_CODEC_DIR_INPUT) { + return (esp_codec_dev_set_in_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE; + } + + return ERROR_NOT_SUPPORTED; +} + +error_t get_mute(Device* device, AudioCodecDirection direction, bool* muted) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + if (direction == AUDIO_CODEC_DIR_OUTPUT) { + return (esp_codec_dev_get_out_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE; + } + + if (direction == AUDIO_CODEC_DIR_INPUT) { + return (esp_codec_dev_get_in_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE; + } + + return ERROR_NOT_SUPPORTED; +} + +error_t get_native_channels(Device* device, AudioCodecDirection direction, uint8_t* channels) { + (void) device; + if (direction != AUDIO_CODEC_DIR_INPUT && direction != AUDIO_CODEC_DIR_OUTPUT) { + return ERROR_NOT_SUPPORTED; + } + *channels = 2; + return ERROR_NONE; +} + +error_t get_native_sample_rate(Device* device, AudioCodecDirection direction, uint32_t* rate_hz) { + (void) device; + (void) direction; + *rate_hz = NATIVE_SAMPLE_RATE; + return ERROR_NONE; +} + +error_t get_capabilities(Device* device, AudioCodecDirection* supported_directions) { + (void) device; + *supported_directions = AUDIO_CODEC_DIR_BOTH; + return ERROR_NONE; +} + +static const struct AudioCodecApi API = { + .open = open, + .close = close, + .read = read, + .write = write, + .set_volume = set_volume, + .get_volume = get_volume, + .set_mute = set_mute, + .get_mute = get_mute, + .get_native_sample_rate = get_native_sample_rate, + .get_native_channels = get_native_channels, + .get_capabilities = get_capabilities, + .get_input_gain_multiplier = nullptr, +}; + +// endregion + +// region Driver lifecycle + +error_t start_device(Device* device) { + const auto* config = GET_CONFIG(device); + + auto* i2c_controller = device_get_parent(device); + if (i2c_controller == nullptr || device_get_type(i2c_controller) != &I2C_CONTROLLER_TYPE) { + LOG_E(TAG, "Parent is not an I2C controller"); + return ERROR_RESOURCE; + } + + auto* i2s_controller = config->i2s_device; + if (i2s_controller == nullptr || device_get_type(i2s_controller) != &I2S_CONTROLLER_TYPE) { + LOG_E(TAG, "I2S controller device is not valid"); + return ERROR_RESOURCE; + } + + auto* data = new Es8388Data(); + + data->ctrl_if = audio_codec_adapter_new_i2c_ctrl(i2c_controller, config->address); + data->data_if = audio_codec_adapter_new_i2s_data(i2s_controller); + if (data->ctrl_if == nullptr || data->data_if == nullptr) { + LOG_E(TAG, "Failed to create adapters"); + goto cleanup; + } + + if (data->ctrl_if->open(data->ctrl_if, nullptr, 0) != ESP_CODEC_DEV_OK) { + LOG_E(TAG, "Failed to open control interface"); + goto cleanup; + } + + if (data->data_if->open(data->data_if, nullptr, 0) != ESP_CODEC_DEV_OK) { + LOG_E(TAG, "Failed to open data interface"); + goto cleanup; + } + + { + es8388_codec_cfg_t codec_config = {}; + codec_config.ctrl_if = data->ctrl_if; + codec_config.gpio_if = nullptr; + codec_config.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH; + codec_config.master_mode = false; + codec_config.pa_pin = -1; + codec_config.pa_reverted = false; + + data->codec_if = es8388_codec_new(&codec_config); + if (data->codec_if == nullptr) { + LOG_E(TAG, "Failed to create ES8388 codec interface"); + goto cleanup; + } + } + + { + esp_codec_dev_cfg_t dev_config = { + .dev_type = ESP_CODEC_DEV_TYPE_IN_OUT, + .codec_if = data->codec_if, + .data_if = data->data_if, + }; + + data->codec_device = esp_codec_dev_new(&dev_config); + if (data->codec_device == nullptr) { + LOG_E(TAG, "Failed to create codec device"); + goto cleanup; + } + } + + device_set_driver_data(device, data); + return ERROR_NONE; + +cleanup: + // Mirrors stop_device's teardown order -- delete_*_if() routines close their interface + // first, so we don't need separate ->close() calls here. + if (data->codec_device != nullptr) { + esp_codec_dev_delete(data->codec_device); + } + if (data->codec_if != nullptr) { + audio_codec_delete_codec_if(data->codec_if); + } + if (data->gpio_if != nullptr) { + audio_codec_adapter_delete_gpio(data->gpio_if); + } + if (data->data_if != nullptr) { + audio_codec_delete_data_if(data->data_if); + } + if (data->ctrl_if != nullptr) { + audio_codec_delete_ctrl_if(data->ctrl_if); + } + delete data; + return ERROR_RESOURCE; +} + +error_t stop_device(Device* device) { + auto* data = GET_DATA(device); + if (data == nullptr) { + return ERROR_NONE; + } + + if (data->is_open) { + esp_codec_dev_close(data->codec_device); + } + + if (data->codec_device != nullptr) { + esp_codec_dev_delete(data->codec_device); + } + if (data->codec_if != nullptr) { + audio_codec_delete_codec_if(data->codec_if); + } + if (data->gpio_if != nullptr) { + audio_codec_adapter_delete_gpio(data->gpio_if); + } + if (data->data_if != nullptr) { + audio_codec_delete_data_if(data->data_if); + } + if (data->ctrl_if != nullptr) { + audio_codec_delete_ctrl_if(data->ctrl_if); + } + + device_set_driver_data(device, nullptr); + delete data; + return ERROR_NONE; +} + +// endregion + +} // namespace + +extern "C" { + +Driver es8388_driver = { + .name = "es8388", + .compatible = (const char*[]) { "everest,es8388", nullptr }, + .start_device = start_device, + .stop_device = stop_device, + .api = &API, + .device_type = &AUDIO_CODEC_TYPE, + .owner = nullptr, + .internal = nullptr, +}; + +} diff --git a/Drivers/es8388-module/source/module.cpp b/Drivers/es8388-module/source/module.cpp new file mode 100644 index 000000000..20f829f6b --- /dev/null +++ b/Drivers/es8388-module/source/module.cpp @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver es8388_driver; + +static error_t start() { + check(driver_construct_add(&es8388_driver) == ERROR_NONE); + return ERROR_NONE; +} + +static error_t stop() { + check(driver_remove_destruct(&es8388_driver) == ERROR_NONE); + return ERROR_NONE; +} + +extern const ModuleSymbol es8388_module_symbols[]; + +Module es8388_module = { + .name = "es8388", + .start = start, + .stop = stop, + .symbols = es8388_module_symbols, + .internal = nullptr +}; + +} diff --git a/Drivers/es8388-module/source/symbols.c b/Drivers/es8388-module/source/symbols.c new file mode 100644 index 000000000..83417307d --- /dev/null +++ b/Drivers/es8388-module/source/symbols.c @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +const struct ModuleSymbol es8388_module_symbols[] = { + MODULE_SYMBOL_TERMINATOR +}; diff --git a/Drivers/pdm-mic-module/CMakeLists.txt b/Drivers/pdm-mic-module/CMakeLists.txt new file mode 100644 index 000000000..428d36b51 --- /dev/null +++ b/Drivers/pdm-mic-module/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +file(GLOB_RECURSE SOURCE_FILES "source/*.c*") + +tactility_add_module(pdm-mic-module + SRCS ${SOURCE_FILES} + INCLUDE_DIRS include/ + REQUIRES TactilityKernel audio-codec-module esp_codec_dev +) diff --git a/Drivers/pdm-mic-module/LICENSE-Apache-2.0.md b/Drivers/pdm-mic-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Drivers/pdm-mic-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Drivers/pdm-mic-module/README.md b/Drivers/pdm-mic-module/README.md new file mode 100644 index 000000000..cb6601dd6 --- /dev/null +++ b/Drivers/pdm-mic-module/README.md @@ -0,0 +1,11 @@ +# SPM1423 PDM microphone + +A driver for the `SPM1423` PDM MEMS microphone (and compatible PDM mics), wired +as an input-only `AUDIO_CODEC_TYPE` device. Has no I2C/register interface -- just +a PDM data line -- so there is no I2C parent; the device sits standalone in the +devicetree and only references the I2S controller by name. PDM RX is hardware- +restricted to I2S controller 0. + +See https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/datasheet/core/SPM1423HM4H-B_datasheet_en.pdf + +License: [Apache v2.0](LICENSE-Apache-2.0.md) diff --git a/Drivers/pdm-mic-module/bindings/generic,spm1423.yaml b/Drivers/pdm-mic-module/bindings/generic,spm1423.yaml new file mode 100644 index 000000000..2f2ab63f2 --- /dev/null +++ b/Drivers/pdm-mic-module/bindings/generic,spm1423.yaml @@ -0,0 +1,17 @@ +description: Generic PDM MEMS microphone (e.g. SPM1423), I2S PDM RX input + +compatible: "generic,spm1423" + +properties: + i2s: + type: phandle + required: true + description: "I2S controller device that carries audio data" + channels: + type: int + default: 1 + description: "Number of PDM mics wired (1 = mono, 2 = stereo)" + input-gain-percent: + type: int + default: 400 + description: "Digital gain multiplier applied by audio_stream, as an integer percentage (100 = 1.0x, 400 = 4.0x). PDM mics have no hardware gain; default 400 matches M5Stack's own SPM1423 reference firmware (magnification=4)." diff --git a/Drivers/pdm-mic-module/devicetree.yaml b/Drivers/pdm-mic-module/devicetree.yaml new file mode 100644 index 000000000..a07d6f334 --- /dev/null +++ b/Drivers/pdm-mic-module/devicetree.yaml @@ -0,0 +1,3 @@ +dependencies: + - TactilityKernel +bindings: bindings diff --git a/Drivers/pdm-mic-module/include/bindings/pdm_mic.h b/Drivers/pdm-mic-module/include/bindings/pdm_mic.h new file mode 100644 index 000000000..255e9f99a --- /dev/null +++ b/Drivers/pdm-mic-module/include/bindings/pdm_mic.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// One DEFINE_DEVICETREE per compatible string -- see dummy-i2s-amp-module's bindings +// header for why this can't be unified into a single typedef name. +DEFINE_DEVICETREE(spm1423, struct PdmMicConfig) + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/pdm-mic-module/include/drivers/pdm_mic.h b/Drivers/pdm-mic-module/include/drivers/pdm_mic.h new file mode 100644 index 000000000..b85bcc76f --- /dev/null +++ b/Drivers/pdm-mic-module/include/drivers/pdm_mic.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include + +struct Device; + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief PDM MEMS microphone configuration. + * + * Covers any PDM mic with no I2C/register interface -- just I2S PDM RX -- e.g. SPM1423. + * There is no I2C parent; the device sits standalone in the devicetree and only + * references the I2S controller by name. PDM RX is hardware-restricted to I2S + * controller 0 on ESP32 targets, so i2s_device must resolve to that controller. + */ +struct PdmMicConfig { + /** I2S controller device that carries audio data */ + struct Device* i2s_device; + /** Number of PDM mics wired (1 = mono, 2 = stereo) */ + uint8_t channels; + /** + * Fixed digital gain multiplier applied to this mic's samples by audio_stream, as an + * integer percentage (100 = 1.0x / no boost, 400 = 4.0x). PDM mics have no hardware + * gain control, so this is the only way to compensate for a capsule that's quiet at + * the ADC's full-scale output. devicetree has no float property type, hence the + * percent-scaled integer encoding. M5Stack's own SPM1423 reference firmware uses a + * software multiplier of 4 (i.e. 400 here). + */ + uint16_t input_gain_percent; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/pdm-mic-module/include/pdm_mic_module.h b/Drivers/pdm-mic-module/include/pdm_mic_module.h new file mode 100644 index 000000000..9e27515a8 --- /dev/null +++ b/Drivers/pdm-mic-module/include/pdm_mic_module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module pdm_mic_module; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/pdm-mic-module/source/module.cpp b/Drivers/pdm-mic-module/source/module.cpp new file mode 100644 index 000000000..da290481e --- /dev/null +++ b/Drivers/pdm-mic-module/source/module.cpp @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +extern "C" { + +extern Driver pdm_mic_driver; + +static error_t start() { + check(driver_construct_add(&pdm_mic_driver) == ERROR_NONE); + return ERROR_NONE; +} + +static error_t stop() { + check(driver_remove_destruct(&pdm_mic_driver) == ERROR_NONE); + return ERROR_NONE; +} + +extern const ModuleSymbol pdm_mic_module_symbols[]; + +Module pdm_mic_module = { + .name = "pdm_mic", + .start = start, + .stop = stop, + .symbols = pdm_mic_module_symbols, + .internal = nullptr +}; + +} diff --git a/Drivers/pdm-mic-module/source/pdm_mic.cpp b/Drivers/pdm-mic-module/source/pdm_mic.cpp new file mode 100644 index 000000000..088b649b2 --- /dev/null +++ b/Drivers/pdm-mic-module/source/pdm_mic.cpp @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include +#include +#include + +#include +#include + +#define TAG "PdmMic" + +namespace { + +// PDM mics have no fixed native rate of their own -- the PDM RX peripheral generates +// whatever clock the configured sample rate implies. Report a common voice-capture +// default; audio_stream resamples to whatever rate it is opened with. +constexpr uint32_t NATIVE_SAMPLE_RATE = 16000; + +struct PdmMicData { + const audio_codec_data_if_t* data_if = nullptr; + const audio_codec_if_t* codec_if = nullptr; + esp_codec_dev_handle_t codec_device = nullptr; + uint8_t native_channels = 1; + float input_gain = 1.0f; + bool is_open = false; + esp_codec_dev_sample_info_t open_sample_info = {}; +}; + +#define GET_CONFIG(device) (static_cast((device)->config)) +#define GET_DATA(device) (static_cast(device_get_driver_data(device))) + +// region AudioCodecApi + +error_t open(Device* device, const struct AudioCodecStreamConfig* config) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + // Input-only codec (see get_capabilities()/write()) -- BOTH would imply output support + // that write() can't actually provide. + if (config->direction != AUDIO_CODEC_DIR_INPUT) { + return ERROR_NOT_SUPPORTED; + } + + esp_codec_dev_sample_info_t sample_info = { + .bits_per_sample = config->bits_per_sample, + .channel = config->channels, + .channel_mask = 0, + .sample_rate = config->sample_rate, + .mclk_multiple = 0, + }; + + if (data->is_open) { + bool same_config = data->open_sample_info.bits_per_sample == sample_info.bits_per_sample + && data->open_sample_info.channel == sample_info.channel + && data->open_sample_info.sample_rate == sample_info.sample_rate; + return same_config ? ERROR_NONE : ERROR_RESOURCE; + } + + if (esp_codec_dev_open(data->codec_device, &sample_info) != ESP_CODEC_DEV_OK) { + LOG_E(TAG, "Failed to open codec device"); + return ERROR_RESOURCE; + } + + data->is_open = true; + data->open_sample_info = sample_info; + return ERROR_NONE; +} + +error_t close(Device* device) { + auto* data = GET_DATA(device); + if (data->codec_device == nullptr) { + return ERROR_RESOURCE; + } + + if (data->is_open) { + esp_codec_dev_close(data->codec_device); + data->is_open = false; + } + + return ERROR_NONE; +} + +error_t read(Device* device, void* buffer, size_t size, size_t* bytes_read, TickType_t timeout) { + (void) timeout; + auto* data = GET_DATA(device); + if (!data->is_open) { + return ERROR_RESOURCE; + } + + // esp_codec_dev_read returns the number of bytes read (>= 0) on success, or a negative + // ESP_CODEC_DEV_* error code on failure -- it does NOT return ESP_CODEC_DEV_OK (0) for + // a successful nonzero-length read. + int result = esp_codec_dev_read(data->codec_device, buffer, (int) size); + if (result < 0) { + return ERROR_RESOURCE; + } + + *bytes_read = (size_t) result; + return ERROR_NONE; +} + +error_t write(Device* device, const void* buffer, size_t size, size_t* bytes_written, TickType_t timeout) { + (void) device; + (void) buffer; + (void) size; + (void) bytes_written; + (void) timeout; + return ERROR_NOT_SUPPORTED; +} + +// PDM mics have no hardware gain/mute registers, and unlike esp_codec_dev's output path +// (esp_codec_dev_set_out_vol(), which falls back to a software volume handler when the +// codec_if has no set_vol), the input path (esp_codec_dev_set_in_gain()/set_in_mute()) has +// no such fallback -- it returns ESP_CODEC_DEV_NOT_SUPPORT outright when the codec_if's +// set_mic_gain/mute_mic are NULL, which dummy_codec always leaves NULL. So input +// volume/mute genuinely isn't supported through this codec for now; report that honestly +// rather than masking a dead call path. + +error_t set_volume(Device* device, AudioCodecDirection direction, float volume_percent) { + (void) device; + (void) volume_percent; + if (direction != AUDIO_CODEC_DIR_INPUT) { + return ERROR_NOT_SUPPORTED; + } + return ERROR_NOT_SUPPORTED; +} + +error_t get_volume(Device* device, AudioCodecDirection direction, float* volume_percent) { + (void) device; + (void) volume_percent; + if (direction != AUDIO_CODEC_DIR_INPUT) { + return ERROR_NOT_SUPPORTED; + } + return ERROR_NOT_SUPPORTED; +} + +error_t set_mute(Device* device, AudioCodecDirection direction, bool muted) { + (void) device; + (void) muted; + if (direction != AUDIO_CODEC_DIR_INPUT) { + return ERROR_NOT_SUPPORTED; + } + return ERROR_NOT_SUPPORTED; +} + +error_t get_mute(Device* device, AudioCodecDirection direction, bool* muted) { + (void) device; + (void) muted; + if (direction != AUDIO_CODEC_DIR_INPUT) { + return ERROR_NOT_SUPPORTED; + } + return ERROR_NOT_SUPPORTED; +} + +error_t get_native_channels(Device* device, AudioCodecDirection direction, uint8_t* channels) { + auto* data = GET_DATA(device); + if (direction != AUDIO_CODEC_DIR_INPUT) { + return ERROR_NOT_SUPPORTED; + } + *channels = data->native_channels; + return ERROR_NONE; +} + +error_t get_native_sample_rate(Device* device, AudioCodecDirection direction, uint32_t* rate_hz) { + (void) device; + if (direction != AUDIO_CODEC_DIR_INPUT) { + return ERROR_NOT_SUPPORTED; + } + *rate_hz = NATIVE_SAMPLE_RATE; + return ERROR_NONE; +} + +error_t get_capabilities(Device* device, AudioCodecDirection* supported_directions) { + (void) device; + *supported_directions = AUDIO_CODEC_DIR_INPUT; + return ERROR_NONE; +} + +error_t get_input_gain_multiplier(Device* device, float* gain) { + auto* data = GET_DATA(device); + *gain = data->input_gain; + return ERROR_NONE; +} + +static const struct AudioCodecApi API = { + .open = open, + .close = close, + .read = read, + .write = write, + .set_volume = set_volume, + .get_volume = get_volume, + .set_mute = set_mute, + .get_mute = get_mute, + .get_native_sample_rate = get_native_sample_rate, + .get_native_channels = get_native_channels, + .get_capabilities = get_capabilities, + .get_input_gain_multiplier = get_input_gain_multiplier, +}; + +// endregion + +// region Driver lifecycle + +error_t start_device(Device* device) { + const auto* config = GET_CONFIG(device); + + if (config->channels == 0 || config->channels > 2) { + LOG_E(TAG, "Invalid channel count %u (must be 1 or 2)", config->channels); + return ERROR_RESOURCE; + } + + // devicetree's int property type has no min/max constraint support, so a bogus + // negative literal (e.g. "input-gain-percent = <-1>;") would otherwise silently wrap to + // 65535 when narrowed to uint16_t and produce a wildly wrong gain. Bound it here + // instead -- 0 disables boost entirely, 2000 (20x) is already an extreme upper bound + // for compensating a quiet mic capsule. + if (config->input_gain_percent > 2000) { + LOG_E(TAG, "Invalid input_gain_percent %u (must be 0..2000)", config->input_gain_percent); + return ERROR_RESOURCE; + } + + auto* i2s_controller = config->i2s_device; + if (i2s_controller == nullptr || device_get_type(i2s_controller) != &I2S_CONTROLLER_TYPE) { + LOG_E(TAG, "I2S controller device is not valid"); + return ERROR_RESOURCE; + } + + auto* data = new PdmMicData(); + data->native_channels = config->channels; + data->input_gain = (float) config->input_gain_percent / 100.0f; + + data->data_if = audio_codec_adapter_new_i2s_pdm_data(i2s_controller); + if (data->data_if == nullptr) { + LOG_E(TAG, "Failed to create I2S PDM data adapter"); + goto cleanup; + } + + if (data->data_if->open(data->data_if, nullptr, 0) != ESP_CODEC_DEV_OK) { + LOG_E(TAG, "Failed to open data interface"); + goto cleanup; + } + + { + dummy_codec_cfg_t codec_config = { + .gpio_if = nullptr, + .pa_pin = -1, + .pa_reverted = false, + }; + + data->codec_if = dummy_codec_new(&codec_config); + if (data->codec_if == nullptr) { + LOG_E(TAG, "Failed to create dummy codec interface"); + goto cleanup; + } + } + + { + esp_codec_dev_cfg_t dev_config = { + .dev_type = ESP_CODEC_DEV_TYPE_IN, + .codec_if = data->codec_if, + .data_if = data->data_if, + }; + + data->codec_device = esp_codec_dev_new(&dev_config); + if (data->codec_device == nullptr) { + LOG_E(TAG, "Failed to create codec device"); + goto cleanup; + } + } + + device_set_driver_data(device, data); + return ERROR_NONE; + +cleanup: + // Mirrors stop_device's teardown order -- delete_*_if() routines close their interface + // first, so we don't need separate ->close() calls here. + if (data->codec_device != nullptr) { + esp_codec_dev_delete(data->codec_device); + } + if (data->codec_if != nullptr) { + audio_codec_delete_codec_if(data->codec_if); + } + if (data->data_if != nullptr) { + audio_codec_delete_data_if(data->data_if); + } + delete data; + return ERROR_RESOURCE; +} + +error_t stop_device(Device* device) { + auto* data = GET_DATA(device); + if (data == nullptr) { + return ERROR_NONE; + } + + if (data->is_open) { + esp_codec_dev_close(data->codec_device); + } + + if (data->codec_device != nullptr) { + esp_codec_dev_delete(data->codec_device); + } + if (data->codec_if != nullptr) { + audio_codec_delete_codec_if(data->codec_if); + } + if (data->data_if != nullptr) { + audio_codec_delete_data_if(data->data_if); + } + + device_set_driver_data(device, nullptr); + delete data; + return ERROR_NONE; +} + +// endregion + +} // namespace + +extern "C" { + +Driver pdm_mic_driver = { + .name = "pdm_mic", + .compatible = (const char*[]) { "generic,spm1423", nullptr }, + .start_device = start_device, + .stop_device = stop_device, + .api = &API, + .device_type = &AUDIO_CODEC_TYPE, + .owner = nullptr, + .internal = nullptr, +}; + +} diff --git a/Drivers/pdm-mic-module/source/symbols.c b/Drivers/pdm-mic-module/source/symbols.c new file mode 100644 index 000000000..9a4743080 --- /dev/null +++ b/Drivers/pdm-mic-module/source/symbols.c @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +const struct ModuleSymbol pdm_mic_module_symbols[] = { + MODULE_SYMBOL_TERMINATOR +}; diff --git a/Firmware/idf_component.yml b/Firmware/idf_component.yml index 3b5892f0b..8be075bba 100644 --- a/Firmware/idf_component.yml +++ b/Firmware/idf_component.yml @@ -1,4 +1,5 @@ dependencies: + espressif/esp_codec_dev: "1.5.10" espressif/esp_video: version: "2.2.0" rules: diff --git a/Platforms/platform-esp32/CMakeLists.txt b/Platforms/platform-esp32/CMakeLists.txt index 6d419cb1c..3feb77407 100644 --- a/Platforms/platform-esp32/CMakeLists.txt +++ b/Platforms/platform-esp32/CMakeLists.txt @@ -9,4 +9,12 @@ idf_component_register( REQUIRES TactilityKernel driver esp_adc esp_driver_i2c vfs fatfs esp_wifi esp_netif esp_event ) +if (DEFINED ENV{ESP_IDF_VERSION}) + GET_PROPERTY_FILE_CONTENT("${CMAKE_CURRENT_LIST_DIR}/../../sdkconfig" sdkconfig_text) + GET_PROPERTY_VALUE(sdkconfig_text "CONFIG_ESP_HOSTED_ENABLED" esp_host_enabled "n") + if (esp_host_enabled STREQUAL "y") + idf_component_optional_requires(PRIVATE espressif__esp_hosted) + endif () +endif () + idf_component_optional_requires(PRIVATE bt usb espressif__usb_host_hid espressif__usb_host_msc) diff --git a/Platforms/platform-esp32/bindings/espressif,esp32-i2s.yaml b/Platforms/platform-esp32/bindings/espressif,esp32-i2s.yaml index 6406e63e7..20d41c4dc 100644 --- a/Platforms/platform-esp32/bindings/espressif,esp32-i2s.yaml +++ b/Platforms/platform-esp32/bindings/espressif,esp32-i2s.yaml @@ -13,12 +13,12 @@ properties: Depending on the hardware, these values are available: I2S_NUM_0, I2S_NUM_1 pin-bclk: type: phandles - required: true - description: Bit clock pin + default: GPIO_PIN_SPEC_NONE + description: Bit clock pin. Not used in PDM RX mode (e.g. a PDM microphone), where pin-ws carries the PDM clock instead. pin-ws: type: phandles required: true - description: Word (slot) select pin + description: Word (slot) select pin. Doubles as the PDM clock pin in PDM RX mode. pin-data-out: type: phandles default: GPIO_PIN_SPEC_NONE diff --git a/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h index 61ebb57f5..21c45f988 100644 --- a/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h +++ b/Platforms/platform-esp32/private/bluetooth/esp32_ble_internal.h @@ -68,6 +68,26 @@ struct BleCtx { // One-shot timer used to dispatch dispatchDisable off the NimBLE host task. // nimble_port_stop() must not be called from the NimBLE host task itself. esp_timer_handle_t disable_timer; + // Given by host_task() right after nimble_port_run() returns (i.e. the host + // task has fully drained g_eventq_dflt and will process no further NimBLE + // events). dispatch_disable() must wait on this before nimble_port_deinit(), + // otherwise a reset event still executing on the host task can dereference + // timer/callout state that deinit just freed. See on_reset()/dispatch_disable(). + SemaphoreHandle_t host_task_done_sem; + // Posted to NimBLE's own default event queue (nimble_port_get_dflt_eventq()) + // from on_reset() instead of kicking disable_timer directly. ble_hs_reset() + // calls reset_cb (on_reset) synchronously, then unconditionally falls through + // to ble_hs_sync() -> ble_hs_timer_sched(), which touches the global static + // ble_hs_timer with no locking. Firing disable_timer (and thus + // nimble_port_stop() -> ble_hs_stop() -> ble_npl_callout_deinit(&ble_hs_timer)) + // from on_reset() races that in-flight ble_hs_sync() call on the SAME host + // task from a DIFFERENT task (esp_timer task), nulling out ble_hs_timer's + // callout mid-use -> null deref crash in ble_npl_callout_is_active(). Posting + // this event instead defers the giveup until the host task's event loop is + // idle again (FIFO-after the in-flight reset), so by the time disable_timer + // fires, ble_hs_sync() for this reset has already returned and there is no + // concurrent user of ble_hs_timer left on the host task. + struct ble_npl_event giveup_event; // BLE device name (set before or after radio enable; applied in dispatch_enable) char device_name[BLE_DEVICE_NAME_MAX + 1]; diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp index 0c0585219..60e07f92d 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble.cpp @@ -29,6 +29,16 @@ constexpr auto* TAG = "esp32_ble"; #include #include +#if defined(CONFIG_ESP_HOSTED_ENABLED) +#include +// esp_hosted_misc.h lacks its own extern "C" guard, so its declarations get C++ +// name-mangled when included from a .cpp file, causing "undefined reference" at +// link time against the library's plain-C symbols. +extern "C" { +#include +} +#endif + // ble_store_config_init() is not declared in the public header in some IDF versions. extern "C" void ble_store_config_init(void); @@ -446,7 +456,24 @@ static void on_sync() { static void dispatch_disable_timer_cb(void* arg) { BleCtx* ctx = (BleCtx*)arg; + // Matches api_set_radio_enabled()'s locking so the give-up teardown can't run + // concurrently with a user-initiated dispatch_enable()/dispatch_disable() on + // the same ctx (both would otherwise race nimble_port_stop()/deinit()). + xSemaphoreTake(ctx->radio_mutex, portMAX_DELAY); dispatch_disable(ctx); + xSemaphoreGive(ctx->radio_mutex); +} + +// Runs on the NimBLE host task, but only once the event loop is idle again — +// i.e. strictly after the ble_hs_reset()/ble_hs_sync() call that triggered the +// giveup has fully returned (this event was queued behind it on g_eventq_dflt). +// Safe to kick disable_timer here: no in-flight NimBLE call is touching +// ble_hs_timer on this task anymore. See giveup_event's comment in BleCtx. +static void giveup_event_cb(struct ble_npl_event* ev) { + BleCtx* ctx = (BleCtx*)ble_npl_event_get_arg(ev); + if (ctx != nullptr && ctx->disable_timer != nullptr) { + esp_timer_start_once(ctx->disable_timer, 0); + } } static void on_reset(int reason) { @@ -458,11 +485,12 @@ static void on_reset(int reason) { int count = ctx->pending_reset_count.fetch_add(1) + 1; if (count == 3) { LOG_E(TAG, "BT controller unresponsive after 3 resets — giving up"); - // Can't call nimble_port_stop() from the NimBLE host task. - // Fire a one-shot esp_timer (delay=0 → fires on esp_timer task immediately). - if (ctx->disable_timer != nullptr) { - esp_timer_start_once(ctx->disable_timer, 0); - } + // Do NOT fire disable_timer directly from here: ble_hs_reset() (our + // caller) unconditionally falls through to ble_hs_sync() right after + // reset_cb returns, which touches the unlocked global ble_hs_timer. + // Queue the giveup behind that on NimBLE's own event queue instead — + // see giveup_event_cb(). + ble_npl_eventq_put(nimble_port_get_dflt_eventq(), &ctx->giveup_event); } } } @@ -470,6 +498,16 @@ static void on_reset(int reason) { static void host_task(void* param) { LOG_I(TAG, "BLE host task started"); nimble_port_run(); + // Signal that this task will not process any further NimBLE events (g_eventq_dflt + // is fully drained past the stop sentinel) before dispatch_disable() is allowed to + // proceed to nimble_port_deinit(). nimble_port_stop()'s own semaphore only confirms + // the stop event was serviced, not that the host task has stopped dequeuing events — + // a fresh HCI-ack-wait timeout can still queue and run ble_hs_ev_reset in that gap, + // racing against deinit freeing ble_hs_timer's callout (crash: ble_hs_timer_sched -> + // ble_npl_callout_is_active on a freed/zeroed callout). + if (s_ctx != nullptr && s_ctx->host_task_done_sem != nullptr) { + xSemaphoreGive(s_ctx->host_task_done_sem); + } // nimble_port_deinit() is called by dispatch_disable() after nimble_port_stop() returns. nimble_port_freertos_deinit(); } @@ -637,6 +675,31 @@ static void dispatch_enable(BleCtx* ctx) { ble_publish_event(ctx->device, e); } +#if defined(CONFIG_ESP_HOSTED_ENABLED) + // Over esp_hosted (SDIO to a co-processor, e.g. C6), the slave's on-chip BT + // controller is never auto-started at slave boot — it only comes up in + // response to these RPCs. Without them, HCI Reset (the first command NimBLE + // ever sends) is handed to esp_vhci_host_send_packet() on the slave with no + // controller underneath to consume it, so it's silently dropped and NimBLE + // times out (BLE_HS_ETIMEOUT_HCI) forever. Must run before nimble_port_init(). + // esp_hosted_bt_controller_init/enable() both bail out immediately (no retry, + // no blocking) if the SDIO/SPI transport to the co-processor isn't marked up + // yet. On boot, BT can auto-enable before Wi-Fi has driven that bring-up, so + // explicitly (re)connect here and let it block until the slave handshake + // (the "Attempt connection with slave" / "Card init success" sequence) + // completes — esp_hosted_connect_to_slave() is a thin wrapper that's safe to + // call again if the transport is already up. + if (esp_hosted_connect_to_slave() != ESP_OK) { + LOG_W(TAG, "esp_hosted_connect_to_slave failed"); + } + if (esp_hosted_bt_controller_init() != ESP_OK) { + LOG_W(TAG, "esp_hosted_bt_controller_init failed"); + } + if (esp_hosted_bt_controller_enable() != ESP_OK) { + LOG_W(TAG, "esp_hosted_bt_controller_enable failed"); + } +#endif + int rc = nimble_port_init(); if (rc != 0) { LOG_E(TAG, "nimble_port_init failed (rc=%d)", rc); @@ -648,6 +711,10 @@ static void dispatch_enable(BleCtx* ctx) { return; } + // npl_funcs only becomes valid after nimble_port_init() above; must (re-)init + // the giveup event each enable cycle, not once at device-start time. + ble_npl_event_init(&ctx->giveup_event, giveup_event_cb, ctx); + ble_hs_cfg.sync_cb = on_sync; ble_hs_cfg.reset_cb = on_reset; ble_hs_cfg.store_status_cb = ble_store_util_status_rr; @@ -703,6 +770,13 @@ static void dispatch_enable(BleCtx* ctx) { #if defined(CONFIG_ESP_HOSTED_ENABLED) ble_hci_gate_set_active(true); // Open gate: NimBLE transport pool is ready #endif + // Drain any stale "done" signal left over from a previous cycle (e.g. if the + // last dispatch_disable() timed out waiting on it — see xSemaphoreTake below + // with pdMS_TO_TICKS(2000)) so dispatch_disable() only ever consumes the + // completion signal for THIS host task instance. + if (ctx->host_task_done_sem != nullptr) { + xSemaphoreTake(ctx->host_task_done_sem, 0); + } // Start NimBLE host task (on_sync will fire when ready) nimble_port_freertos_init(host_task); } @@ -731,6 +805,18 @@ static void dispatch_disable(BleCtx* ctx) { // controller. Closing the gate before this point starves NimBLE of those events, // causing HCI timeouts and a cascade of resets that crash in ble_hs_timer_sched. nimble_port_stop(); + // nimble_port_stop()'s internal semaphore only confirms the stop sentinel event was + // serviced — not that host_task()'s call to nimble_port_run() has returned. A reset + // event (e.g. from an independent HCI-ack-wait timeout) can still be running on the + // host task in that gap. Wait for host_task() to explicitly signal completion before + // freeing anything NimBLE still references (ble_hs_timer et al in nimble_port_deinit()), + // otherwise ble_hs_timer_sched() on the host task can dereference a freed callout — + // see host_task() for the crash this fixes. + if (ctx->host_task_done_sem != nullptr) { + if (xSemaphoreTake(ctx->host_task_done_sem, pdMS_TO_TICKS(2000)) != pdTRUE) { + LOG_W(TAG, "host task did not signal completion in time"); + } + } #if defined(CONFIG_ESP_HOSTED_ENABLED) // Close the gate NOW — after nimble_port_stop() returns the NimBLE host task has // exited and nimble_port_deinit() is about to zero npl_funcs. Any HCI packet @@ -742,6 +828,16 @@ static void dispatch_disable(BleCtx* ctx) { #endif nimble_port_deinit(); +#if defined(CONFIG_ESP_HOSTED_ENABLED) + // Symmetric with the enable-side esp_hosted_bt_controller_init/enable() calls. + if (esp_hosted_bt_controller_disable() != ESP_OK) { + LOG_W(TAG, "esp_hosted_bt_controller_disable failed"); + } + if (esp_hosted_bt_controller_deinit(true) != ESP_OK) { + LOG_W(TAG, "esp_hosted_bt_controller_deinit failed"); + } +#endif + ctx->spp_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); ctx->spp_active.store(false); ctx->midi_conn_handle.store(BLE_HS_CONN_HANDLE_NONE); @@ -1048,6 +1144,10 @@ static error_t esp32_ble_start_device(struct Device* device) { ctx->scan_mutex = xSemaphoreCreateMutex(); ctx->scan_count = 0; memset(ctx->scan_results, 0, sizeof(ctx->scan_results)); + ctx->host_task_done_sem = xSemaphoreCreateBinary(); + if (ctx->host_task_done_sem == nullptr) { + LOG_E(TAG, "start_device: host_task_done_sem create failed"); + } // Create the disable timer used to dispatch dispatchDisable off the NimBLE host task. esp_timer_create_args_t disable_args = {}; @@ -1085,7 +1185,9 @@ static error_t esp32_ble_stop_device(struct Device* device) { destroy_child_device(ctx->serial_child); if (ctx->radio_state.load() != BT_RADIO_STATE_OFF) { + xSemaphoreTake(ctx->radio_mutex, portMAX_DELAY); dispatch_disable(ctx); + xSemaphoreGive(ctx->radio_mutex); } if (ctx->scan_mutex != nullptr) { @@ -1093,6 +1195,11 @@ static error_t esp32_ble_stop_device(struct Device* device) { ctx->scan_mutex = nullptr; } + if (ctx->host_task_done_sem != nullptr) { + vSemaphoreDelete(ctx->host_task_done_sem); + ctx->host_task_done_sem = nullptr; + } + if (ctx->disable_timer != nullptr) { esp_timer_stop(ctx->disable_timer); esp_timer_delete(ctx->disable_timer); diff --git a/Platforms/platform-esp32/source/drivers/esp32_i2s.cpp b/Platforms/platform-esp32/source/drivers/esp32_i2s.cpp index 879c05e17..54183a214 100644 --- a/Platforms/platform-esp32/source/drivers/esp32_i2s.cpp +++ b/Platforms/platform-esp32/source/drivers/esp32_i2s.cpp @@ -5,6 +5,9 @@ #ifdef SOC_I2S_SUPPORTS_TDM #include #endif +#ifdef SOC_I2S_SUPPORTS_PDM_RX +#include +#endif #include #include @@ -191,6 +194,16 @@ static error_t set_config(Device* device, const struct I2sConfig* config) { auto* internal = GET_DATA(device); auto* dts_config = GET_CONFIG(device); + + // Standard mode always drives BCLK -- unlike PDM RX (which uses pin-ws as its clock + // line), there's no mode where BCLK is legitimately absent here. pin-bclk is optional + // in the devicetree binding only to support PDM-only controllers; reject a missing + // pin here rather than silently building an std_cfg with BCLK=-1. + if (internal->bclk_descriptor == nullptr) { + LOG_E(TAG, "Standard I2S mode requires pin-bclk"); + return ERROR_INVALID_ARGUMENT; + } + lock(internal); cleanup_channel_handles(internal); @@ -290,6 +303,29 @@ static error_t reset(Device* device) { return ERROR_NONE; } +static error_t disable_direction(Device* device, bool is_input) { + if (xPortInIsrContext()) return ERROR_ISR_STATUS; + + LOG_I(TAG, "disable_direction %s (%s)", device->name, is_input ? "rx" : "tx"); + auto* driver_data = GET_DATA(device); + lock(driver_data); + if (is_input) { + if (driver_data->rx_handle) { + i2s_channel_disable(driver_data->rx_handle); + i2s_del_channel(driver_data->rx_handle); + driver_data->rx_handle = nullptr; + } + } else { + if (driver_data->tx_handle) { + i2s_channel_disable(driver_data->tx_handle); + i2s_del_channel(driver_data->tx_handle); + driver_data->tx_handle = nullptr; + } + } + unlock(driver_data); + return ERROR_NONE; +} + #ifdef SOC_I2S_SUPPORTS_TDM static error_t set_rx_tdm_config(Device* device, const struct I2sTdmRxConfig* config) { if (xPortInIsrContext()) return ERROR_ISR_STATUS; @@ -330,6 +366,14 @@ static error_t set_rx_tdm_config(Device* device, const struct I2sTdmRxConfig* co auto* internal = GET_DATA(device); auto* dts_config = GET_CONFIG(device); + + // TDM, like standard mode, always drives BCLK -- reject rather than build a tdm_cfg + // with BCLK=-1 (see set_config() for the matching check/rationale). + if (internal->bclk_descriptor == nullptr) { + LOG_E(TAG, "TDM mode requires pin-bclk"); + return ERROR_INVALID_ARGUMENT; + } + lock(internal); // Tear down only the RX channel; TX stays in standard mode for playback. @@ -414,6 +458,73 @@ static error_t set_rx_tdm_config(Device* device, const struct I2sTdmRxConfig* co } #endif // SOC_I2S_SUPPORTS_TDM +#ifdef SOC_I2S_SUPPORTS_PDM_RX +static error_t set_rx_pdm_config(Device* device, const struct I2sPdmRxConfig* config) { + if (xPortInIsrContext()) return ERROR_ISR_STATUS; + + if (config->sample_rate_hz == 0) { + return ERROR_INVALID_ARGUMENT; + } + + auto* internal = GET_DATA(device); + auto* dts_config = GET_CONFIG(device); + + // PDM RX is hardware-restricted to I2S controller 0 on ESP32 targets. + if (dts_config->port != I2S_NUM_0) { + return ERROR_NOT_SUPPORTED; + } + + lock(internal); + + // Tear down only the RX channel; TX (if any) stays in its own mode for playback. + if (internal->rx_handle) { + i2s_channel_disable(internal->rx_handle); + i2s_del_channel(internal->rx_handle); + internal->rx_handle = nullptr; + } + + i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(dts_config->port, I2S_ROLE_MASTER); + i2s_chan_handle_t new_rx = nullptr; + esp_err_t esp_error = i2s_new_channel(&chan_cfg, nullptr, &new_rx); + if (esp_error != ESP_OK) { + LOG_E(TAG, "PDM: failed to create RX channel: %s", esp_err_to_name(esp_error)); + unlock(internal); + return ERROR_RESOURCE; + } + + gpio_num_t clk_pin = get_native_pin(internal->ws_descriptor); + gpio_num_t data_in_pin = get_native_pin(internal->data_in_descriptor); + + i2s_slot_mode_t slot_mode = config->stereo ? I2S_SLOT_MODE_STEREO : I2S_SLOT_MODE_MONO; + + i2s_pdm_rx_config_t pdm_cfg = { + .clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG(config->sample_rate_hz), + .slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, slot_mode), + .gpio_cfg = { + .clk = clk_pin, + .din = data_in_pin, + .invert_flags = { + .clk_inv = is_pin_inverted(internal->ws_descriptor), + }, + }, + }; + + esp_error = i2s_channel_init_pdm_rx_mode(new_rx, &pdm_cfg); + if (esp_error == ESP_OK) esp_error = i2s_channel_enable(new_rx); + + if (esp_error != ESP_OK) { + LOG_E(TAG, "PDM: failed to init/enable RX channel: %s", esp_err_to_name(esp_error)); + i2s_del_channel(new_rx); + unlock(internal); + return esp_err_to_error(esp_error); + } + + internal->rx_handle = new_rx; + unlock(internal); + return ERROR_NONE; +} +#endif // SOC_I2S_SUPPORTS_PDM_RX + const static I2sControllerApi esp32_i2s_api = { .read = read, .write = write, @@ -425,6 +536,12 @@ const static I2sControllerApi esp32_i2s_api = { #else .set_rx_tdm_config = nullptr, #endif +#ifdef SOC_I2S_SUPPORTS_PDM_RX + .set_rx_pdm_config = set_rx_pdm_config, +#else + .set_rx_pdm_config = nullptr, +#endif + .disable_direction = disable_direction, }; extern struct Module platform_esp32_module; diff --git a/Tactility/Include/Tactility/service/audio/Audio.h b/Tactility/Include/Tactility/service/audio/Audio.h new file mode 100644 index 000000000..5d2f028f7 --- /dev/null +++ b/Tactility/Include/Tactility/service/audio/Audio.h @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include + +namespace tt::service::audio { + +enum class AudioEvent { + InputEnabledChanged, + OutputEnabledChanged, + InputVolumeChanged, + OutputVolumeChanged, + InputMuteChanged, + OutputMuteChanged +}; + +/** @return the audio pubsub that broadcasts AudioEvent objects */ +std::shared_ptr> getPubsub(); + +/** @return true when an AUDIO_STREAM_TYPE device is bound and ready */ +bool isAvailable(); + +/** @return true if a codec supporting input (microphone) is bound, e.g. a device with a speaker but no mic returns false */ +bool isInputAvailable(); + +/** @return true if a codec supporting output (speaker) is bound, e.g. a device with a mic but no speaker returns false */ +bool isOutputAvailable(); + +bool isInputEnabled(); +void setInputEnabled(bool enabled); + +bool isOutputEnabled(); +void setOutputEnabled(bool enabled); + +float getInputVolume(); +void setInputVolume(float percent); + +float getOutputVolume(); +void setOutputVolume(float percent); + +bool isInputMuted(); +void setInputMuted(bool muted); + +bool isOutputMuted(); +void setOutputMuted(bool muted); + +} // namespace tt::service::audio diff --git a/Tactility/Include/Tactility/service/audio/AudioService.h b/Tactility/Include/Tactility/service/audio/AudioService.h new file mode 100644 index 000000000..b979a42d8 --- /dev/null +++ b/Tactility/Include/Tactility/service/audio/AudioService.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include + +struct Device; + +namespace tt::service::audio { + +class AudioService final : public Service { + + Mutex mutex; + Device* streamDevice = nullptr; + std::shared_ptr> pubsub = std::make_shared>(); + std::unique_ptr persistTimer; + bool persistPending = false; + + void findStreamDevice(); + void primeFromSettings() const; + void schedulePersist(); + void persistIfPending(); + void onDriverChanged(enum AudioCodecDirection direction, enum AudioStreamChange change); + static void onDriverChangedCallback(Device* device, enum AudioCodecDirection direction, enum AudioStreamChange change, void* userData); + +public: + + bool onStart(ServiceContext& serviceContext) override; + void onStop(ServiceContext& serviceContext) override; + + bool isAvailable() const; + + /** @return true if a codec supporting input (microphone) is bound for this device */ + bool isInputAvailable() const; + /** @return true if a codec supporting output (speaker) is bound for this device */ + bool isOutputAvailable() const; + + bool isInputEnabled() const; + void setInputEnabled(bool enabled); + + bool isOutputEnabled() const; + void setOutputEnabled(bool enabled); + + float getInputVolume() const; + void setInputVolume(float percent); + + float getOutputVolume() const; + void setOutputVolume(float percent); + + bool isInputMuted() const; + void setInputMuted(bool muted); + + bool isOutputMuted() const; + void setOutputMuted(bool muted); + + std::shared_ptr> getPubsub() const { return pubsub; } +}; + +std::shared_ptr findAudioService(); + +} // namespace tt::service::audio diff --git a/Tactility/Include/Tactility/settings/AudioSettings.h b/Tactility/Include/Tactility/settings/AudioSettings.h new file mode 100644 index 000000000..6edf7af8d --- /dev/null +++ b/Tactility/Include/Tactility/settings/AudioSettings.h @@ -0,0 +1,22 @@ +#pragma once + +namespace tt::settings::audio { + +struct AudioSettings { + bool inputEnabled; + bool outputEnabled; + bool inputMuted; + bool outputMuted; + float inputVolume; // 0..100 + float outputVolume; // 0..100 +}; + +bool load(AudioSettings& settings); + +AudioSettings loadOrGetDefault(); + +AudioSettings getDefault(); + +bool save(const AudioSettings& settings); + +} // namespace tt::settings::audio diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index 66af4d9e9..e9609c0fe 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -19,10 +19,12 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -72,6 +74,7 @@ bool MainDispatcher::dispatch(Function function, TickType_t timeout) const { // region Default services namespace service { // Primary + namespace audio { extern const ServiceManifest manifest; } namespace gps { extern const ServiceManifest manifest; } namespace wifi { extern const ServiceManifest manifest; } #ifdef ESP_PLATFORM @@ -111,6 +114,7 @@ namespace app { namespace appdetails { extern const AppManifest manifest; } namespace applist { extern const AppManifest manifest; } namespace appsettings { extern const AppManifest manifest; } + namespace audiosettings { extern const AppManifest manifest; } namespace boot { extern const AppManifest manifest; } namespace development { extern const AppManifest manifest; } namespace display { extern const AppManifest manifest; } @@ -172,6 +176,9 @@ static void registerInternalApps() { addAppManifest(app::apphubdetails::manifest); addAppManifest(app::applist::manifest); addAppManifest(app::appsettings::manifest); + if (service::audio::isAvailable()) { + addAppManifest(app::audiosettings::manifest); + } if (device_exists_of_type(&DISPLAY_TYPE)) { addAppManifest(app::kerneldisplay::manifest); } else if (hal::hasDevice(hal::Device::Type::Display)) { @@ -308,6 +315,9 @@ static void registerAndStartSecondaryServices() { static void registerAndStartPrimaryServices() { LOG_I(TAG, "Registering and starting primary system services"); + if (device_exists_of_type(&AUDIO_STREAM_TYPE)) { + addService(service::audio::manifest); + } addService(service::gps::manifest); addService(service::wifi::manifest); #ifdef ESP_PLATFORM diff --git a/Tactility/Source/app/audiosettings/AudioSettings.cpp b/Tactility/Source/app/audiosettings/AudioSettings.cpp new file mode 100644 index 000000000..bb32c56bf --- /dev/null +++ b/Tactility/Source/app/audiosettings/AudioSettings.cpp @@ -0,0 +1,220 @@ +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace tt::app::audiosettings { + +class AudioSettingsApp final : public App { + + PubSub::SubscriptionHandle audioSubscription = nullptr; + + lv_obj_t* inputEnabledSwitch = nullptr; + lv_obj_t* inputMuteSwitch = nullptr; + lv_obj_t* inputVolumeSlider = nullptr; + + lv_obj_t* outputEnabledSwitch = nullptr; + lv_obj_t* outputMuteSwitch = nullptr; + lv_obj_t* outputVolumeSlider = nullptr; + + static void onInputEnabledSwitch(lv_event_t* event) { + auto* sw = static_cast(lv_event_get_target(event)); + bool enabled = lv_obj_has_state(sw, LV_STATE_CHECKED); + service::audio::setInputEnabled(enabled); + } + + static void onOutputEnabledSwitch(lv_event_t* event) { + auto* sw = static_cast(lv_event_get_target(event)); + bool enabled = lv_obj_has_state(sw, LV_STATE_CHECKED); + service::audio::setOutputEnabled(enabled); + } + + static void onInputMuteSwitch(lv_event_t* event) { + auto* sw = static_cast(lv_event_get_target(event)); + bool muted = lv_obj_has_state(sw, LV_STATE_CHECKED); + service::audio::setInputMuted(muted); + } + + static void onOutputMuteSwitch(lv_event_t* event) { + auto* sw = static_cast(lv_event_get_target(event)); + bool muted = lv_obj_has_state(sw, LV_STATE_CHECKED); + service::audio::setOutputMuted(muted); + } + + static void onInputVolumeSlider(lv_event_t* event) { + auto* sliderBox = static_cast(lv_event_get_target(event)); + float percent = static_cast(lvgl::sliderbox_get_value(sliderBox)); + service::audio::setInputVolume(percent); + } + + static void onOutputVolumeSlider(lv_event_t* event) { + auto* sliderBox = static_cast(lv_event_get_target(event)); + float percent = static_cast(lvgl::sliderbox_get_value(sliderBox)); + service::audio::setOutputVolume(percent); + } + + static lv_obj_t* createSection(lv_obj_t* parent, const char* title) { + auto* wrapper = lv_obj_create(parent); + lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_hor(wrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT); + + auto* title_label = lv_label_create(wrapper); + lv_label_set_text(title_label, title); + + return wrapper; + } + + static lv_obj_t* createSwitchRow(lv_obj_t* parent, const char* label, lv_event_cb_t cb, void* userData) { + auto* row = lv_obj_create(parent); + lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(row, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT); + + auto* row_label = lv_label_create(row); + lv_label_set_text(row_label, label); + lv_obj_align(row_label, LV_ALIGN_LEFT_MID, 0, 0); + + auto* sw = lv_switch_create(row); + lv_obj_align(sw, LV_ALIGN_RIGHT_MID, 0, 0); + lv_obj_add_event_cb(sw, cb, LV_EVENT_VALUE_CHANGED, userData); + + return sw; + } + + static lv_obj_t* createSliderRow(lv_obj_t* parent, const char* label, int32_t initialValue, lv_event_cb_t cb, void* userData) { + auto* row = lv_obj_create(parent); + lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(row, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT); + + auto* row_label = lv_label_create(row); + lv_label_set_text(row_label, label); + lv_obj_align(row_label, LV_ALIGN_LEFT_MID, 0, 0); + + auto* sliderBox = lvgl::sliderbox_create(row, 0, 100, 10, initialValue); + lv_obj_set_width(sliderBox, LV_PCT(50)); + lv_obj_align(sliderBox, LV_ALIGN_RIGHT_MID, 0, 0); + lvgl::sliderbox_add_value_changed_cb(sliderBox, cb, userData); + + return sliderBox; + } + +public: + + void onShow(AppContext& app, lv_obj_t* parent) override { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); + + lvgl::toolbar_create(parent, app); + + auto* main_wrapper = lv_obj_create(parent); + lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_set_width(main_wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(main_wrapper, 1); + + if (!service::audio::isAvailable()) { + auto* label = lv_label_create(main_wrapper); + lv_label_set_text(label, "No audio hardware available"); + lv_obj_center(label); + return; + } + + // Gated per-direction, not just isAvailable() - a mic-only or speaker-only + // device (e.g. a dedicated input codec with no output codec bound) should + // only show the section it actually has, not a dead section for the other. + if (service::audio::isInputAvailable()) { + auto* input_section = createSection(main_wrapper, "Microphone"); + inputEnabledSwitch = createSwitchRow(input_section, "Enabled", onInputEnabledSwitch, this); + inputMuteSwitch = createSwitchRow(input_section, "Mute", onInputMuteSwitch, this); + inputVolumeSlider = createSliderRow(input_section, "Volume", static_cast(service::audio::getInputVolume()), onInputVolumeSlider, this); + } + + if (service::audio::isOutputAvailable()) { + auto* output_section = createSection(main_wrapper, "Speaker"); + outputEnabledSwitch = createSwitchRow(output_section, "Enabled", onOutputEnabledSwitch, this); + outputMuteSwitch = createSwitchRow(output_section, "Mute", onOutputMuteSwitch, this); + outputVolumeSlider = createSliderRow(output_section, "Volume", static_cast(service::audio::getOutputVolume()), onOutputVolumeSlider, this); + } + + // isAvailable() only reflects that the audio-stream device exists, not that any + // codec is actually bound to it (the stream device is constructed unconditionally + // at module-start time, before devicetree codecs exist, and binds lazily on first + // use) -- so a board with no input or output codec at all reaches here with both + // sections skipped above and would otherwise show an empty page. + if (!service::audio::isInputAvailable() && !service::audio::isOutputAvailable()) { + auto* label = lv_label_create(main_wrapper); + lv_label_set_text(label, "No supported audio controls"); + lv_obj_center(label); + } + + refresh(); + + audioSubscription = service::audio::getPubsub()->subscribe([this](auto) { + if (lvgl::lock(lvgl::defaultLockTime)) { + refresh(); + lvgl::unlock(); + } + }); + } + + void onHide(AppContext& app) override { + if (audioSubscription != nullptr) { + service::audio::getPubsub()->unsubscribe(audioSubscription); + audioSubscription = nullptr; + } + + inputEnabledSwitch = nullptr; + inputMuteSwitch = nullptr; + inputVolumeSlider = nullptr; + outputEnabledSwitch = nullptr; + outputMuteSwitch = nullptr; + outputVolumeSlider = nullptr; + } + + void refresh() const { + if (inputEnabledSwitch) { + if (service::audio::isInputEnabled()) lv_obj_add_state(inputEnabledSwitch, LV_STATE_CHECKED); + else lv_obj_remove_state(inputEnabledSwitch, LV_STATE_CHECKED); + } + if (inputMuteSwitch) { + if (service::audio::isInputMuted()) lv_obj_add_state(inputMuteSwitch, LV_STATE_CHECKED); + else lv_obj_remove_state(inputMuteSwitch, LV_STATE_CHECKED); + } + if (inputVolumeSlider) { + lvgl::sliderbox_set_value(inputVolumeSlider, static_cast(service::audio::getInputVolume()), LV_ANIM_OFF); + } + + if (outputEnabledSwitch) { + if (service::audio::isOutputEnabled()) lv_obj_add_state(outputEnabledSwitch, LV_STATE_CHECKED); + else lv_obj_remove_state(outputEnabledSwitch, LV_STATE_CHECKED); + } + if (outputMuteSwitch) { + if (service::audio::isOutputMuted()) lv_obj_add_state(outputMuteSwitch, LV_STATE_CHECKED); + else lv_obj_remove_state(outputMuteSwitch, LV_STATE_CHECKED); + } + if (outputVolumeSlider) { + lvgl::sliderbox_set_value(outputVolumeSlider, static_cast(service::audio::getOutputVolume()), LV_ANIM_OFF); + } + } +}; + +extern const AppManifest manifest = { + .appId = "AudioSettings", + .appName = "Audio", + .appIcon = LVGL_ICON_SHARED_MUSIC_NOTE, + .appCategory = Category::Settings, + .createApp = create +}; + +} // namespace tt::app::audiosettings diff --git a/Tactility/Source/service/audio/Audio.cpp b/Tactility/Source/service/audio/Audio.cpp new file mode 100644 index 000000000..481b3c4de --- /dev/null +++ b/Tactility/Source/service/audio/Audio.cpp @@ -0,0 +1,107 @@ +#include +#include +#include +#include + +namespace tt::service::audio { + +extern const ServiceManifest manifest; + +// The service is only registered when audio hardware is present (see Tactility.cpp); +// treat "not registered" as "no device bound" rather than asserting via findAudioService(). +static std::shared_ptr tryFindAudioService() { + return findServiceById(manifest.id); +} + +std::shared_ptr> getPubsub() { + if (auto service = tryFindAudioService()) { + return service->getPubsub(); + } + // No audio hardware, service was never registered: hand back an inert pubsub + // that nothing ever publishes to, so callers can subscribe/unsubscribe safely. + static auto fallbackPubsub = std::make_shared>(); + return fallbackPubsub; +} + +bool isAvailable() { + auto service = tryFindAudioService(); + return service != nullptr && service->isAvailable(); +} + +bool isInputAvailable() { + auto service = tryFindAudioService(); + return service != nullptr && service->isInputAvailable(); +} + +bool isOutputAvailable() { + auto service = tryFindAudioService(); + return service != nullptr && service->isOutputAvailable(); +} + +bool isInputEnabled() { + auto service = tryFindAudioService(); + return service != nullptr && service->isInputEnabled(); +} + +void setInputEnabled(bool enabled) { + if (auto service = tryFindAudioService()) { + service->setInputEnabled(enabled); + } +} + +bool isOutputEnabled() { + auto service = tryFindAudioService(); + return service != nullptr && service->isOutputEnabled(); +} + +void setOutputEnabled(bool enabled) { + if (auto service = tryFindAudioService()) { + service->setOutputEnabled(enabled); + } +} + +float getInputVolume() { + auto service = tryFindAudioService(); + return service != nullptr ? service->getInputVolume() : 0.0f; +} + +void setInputVolume(float percent) { + if (auto service = tryFindAudioService()) { + service->setInputVolume(percent); + } +} + +float getOutputVolume() { + auto service = tryFindAudioService(); + return service != nullptr ? service->getOutputVolume() : 0.0f; +} + +void setOutputVolume(float percent) { + if (auto service = tryFindAudioService()) { + service->setOutputVolume(percent); + } +} + +bool isInputMuted() { + auto service = tryFindAudioService(); + return service != nullptr && service->isInputMuted(); +} + +void setInputMuted(bool muted) { + if (auto service = tryFindAudioService()) { + service->setInputMuted(muted); + } +} + +bool isOutputMuted() { + auto service = tryFindAudioService(); + return service != nullptr && service->isOutputMuted(); +} + +void setOutputMuted(bool muted) { + if (auto service = tryFindAudioService()) { + service->setOutputMuted(muted); + } +} + +} // namespace tt::service::audio diff --git a/Tactility/Source/service/audio/AudioService.cpp b/Tactility/Source/service/audio/AudioService.cpp new file mode 100644 index 000000000..b58c3c42e --- /dev/null +++ b/Tactility/Source/service/audio/AudioService.cpp @@ -0,0 +1,336 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include + +namespace tt::service::audio { + +constexpr auto* TAG = "AudioService"; +extern const ServiceManifest manifest; + +constexpr TickType_t PERSIST_INTERVAL_TICKS = pdMS_TO_TICKS(2000); + +void AudioService::findStreamDevice() { + streamDevice = nullptr; + device_for_each_of_type(&AUDIO_STREAM_TYPE, &streamDevice, [](Device* device, void* context) -> bool { + if (device_is_ready(device)) { + *static_cast(context) = device; + return false; + } + return true; + }); +} + +void AudioService::primeFromSettings() const { + if (streamDevice == nullptr) { + return; + } + + auto settings = settings::audio::loadOrGetDefault(); + + audio_stream_set_enabled(streamDevice, AUDIO_CODEC_DIR_INPUT, settings.inputEnabled); + audio_stream_set_enabled(streamDevice, AUDIO_CODEC_DIR_OUTPUT, settings.outputEnabled); + audio_stream_set_mute(streamDevice, AUDIO_CODEC_DIR_INPUT, settings.inputMuted); + audio_stream_set_mute(streamDevice, AUDIO_CODEC_DIR_OUTPUT, settings.outputMuted); + audio_stream_set_volume(streamDevice, AUDIO_CODEC_DIR_INPUT, settings.inputVolume); + audio_stream_set_volume(streamDevice, AUDIO_CODEC_DIR_OUTPUT, settings.outputVolume); +} + +void AudioService::schedulePersist() { + auto lock = mutex.asScopedLock(); + lock.lock(); + persistPending = true; +} + +void AudioService::persistIfPending() { + bool shouldPersist; + Device* device; + { + auto lock = mutex.asScopedLock(); + lock.lock(); + shouldPersist = persistPending; + persistPending = false; + device = streamDevice; + } + + if (!shouldPersist || device == nullptr) { + return; + } + + // audio_stream_get_*() leave their out-param untouched (not zeroed) when the + // direction has no codec bound, returning ERROR_NOT_SUPPORTED instead -- starting + // from a freshly default-constructed (uninitialized) AudioSettings and ignoring + // those return values would persist garbage stack memory for the unsupported side. + // Start from the existing saved settings so an unsupported direction's fields are + // left at their last known-good value instead. + settings::audio::AudioSettings settings = settings::audio::loadOrGetDefault(); + if (audio_stream_get_enabled(device, AUDIO_CODEC_DIR_INPUT, &settings.inputEnabled) == ERROR_NONE + && audio_stream_get_mute(device, AUDIO_CODEC_DIR_INPUT, &settings.inputMuted) == ERROR_NONE) { + audio_stream_get_volume(device, AUDIO_CODEC_DIR_INPUT, &settings.inputVolume); + } + if (audio_stream_get_enabled(device, AUDIO_CODEC_DIR_OUTPUT, &settings.outputEnabled) == ERROR_NONE + && audio_stream_get_mute(device, AUDIO_CODEC_DIR_OUTPUT, &settings.outputMuted) == ERROR_NONE) { + audio_stream_get_volume(device, AUDIO_CODEC_DIR_OUTPUT, &settings.outputVolume); + } + settings::audio::save(settings); +} + +void AudioService::onDriverChangedCallback(Device* device, enum AudioCodecDirection direction, enum AudioStreamChange change, void* userData) { + static_cast(userData)->onDriverChanged(direction, change); +} + +void AudioService::onDriverChanged(enum AudioCodecDirection direction, enum AudioStreamChange change) { + bool isInput = (direction == AUDIO_CODEC_DIR_INPUT); + AudioEvent event; + switch (change) { + case AUDIO_STREAM_CHANGE_VOLUME: + event = isInput ? AudioEvent::InputVolumeChanged : AudioEvent::OutputVolumeChanged; + break; + case AUDIO_STREAM_CHANGE_MUTE: + event = isInput ? AudioEvent::InputMuteChanged : AudioEvent::OutputMuteChanged; + break; + case AUDIO_STREAM_CHANGE_ENABLED: + default: + event = isInput ? AudioEvent::InputEnabledChanged : AudioEvent::OutputEnabledChanged; + break; + } + + // Called synchronously from the set*() caller's call stack, but AFTER it has already + // released mutex (see setInputEnabled() etc.) -- pubsub->publish() below re-enters this + // service (e.g. a subscriber's refresh() calling getOutputVolume()) on the same task, + // which would deadlock on the non-recursive Mutex type if we were still holding it here. + // Set the flag directly rather than via schedulePersist() to avoid taking mutex at all + // in this already-sensitive path. + persistPending = true; + pubsub->publish(event); +} + +bool AudioService::onStart(ServiceContext& serviceContext) { + findStreamDevice(); + + if (streamDevice == nullptr) { + LOG_W(TAG, "No audio stream device found; service is unavailable"); + return true; + } + + primeFromSettings(); + audio_stream_set_change_callback(streamDevice, &onDriverChangedCallback, this); + + persistTimer = std::make_unique(Timer::Type::Periodic, PERSIST_INTERVAL_TICKS, [this] { + persistIfPending(); + }); + persistTimer->start(); + + return true; +} + +void AudioService::onStop(ServiceContext& serviceContext) { + if (persistTimer) { + persistTimer->stop(); + persistTimer.reset(); + } + persistIfPending(); + { + auto lock = mutex.asScopedLock(); + lock.lock(); + if (streamDevice != nullptr) { + audio_stream_set_change_callback(streamDevice, nullptr, nullptr); + } + streamDevice = nullptr; + } +} + +bool AudioService::isAvailable() const { + auto lock = mutex.asScopedLock(); + lock.lock(); + return streamDevice != nullptr; +} + +bool AudioService::isInputAvailable() const { + auto lock = mutex.asScopedLock(); + lock.lock(); + bool supported = false; + if (streamDevice != nullptr) { + audio_stream_is_supported(streamDevice, AUDIO_CODEC_DIR_INPUT, &supported); + } + return supported; +} + +bool AudioService::isOutputAvailable() const { + auto lock = mutex.asScopedLock(); + lock.lock(); + bool supported = false; + if (streamDevice != nullptr) { + audio_stream_is_supported(streamDevice, AUDIO_CODEC_DIR_OUTPUT, &supported); + } + return supported; +} + +bool AudioService::isInputEnabled() const { + auto lock = mutex.asScopedLock(); + lock.lock(); + bool enabled = false; + if (streamDevice != nullptr) { + audio_stream_get_enabled(streamDevice, AUDIO_CODEC_DIR_INPUT, &enabled); + } + return enabled; +} + +void AudioService::setInputEnabled(bool enabled) { + Device* device; + { + auto lock = mutex.asScopedLock(); + lock.lock(); + device = streamDevice; + } + if (device == nullptr) { + return; + } + // Must not hold mutex here: this can synchronously trigger onDriverChanged() -> + // pubsub->publish(), which re-enters this service (e.g. a subscriber's refresh() + // calling isInputEnabled()) on the SAME task/call-stack. mutex is a non-recursive + // FreeRTOS mutex, so holding it across this call deadlocks that re-entrant call. + audio_stream_set_enabled(device, AUDIO_CODEC_DIR_INPUT, enabled); +} + +bool AudioService::isOutputEnabled() const { + auto lock = mutex.asScopedLock(); + lock.lock(); + bool enabled = false; + if (streamDevice != nullptr) { + audio_stream_get_enabled(streamDevice, AUDIO_CODEC_DIR_OUTPUT, &enabled); + } + return enabled; +} + +void AudioService::setOutputEnabled(bool enabled) { + Device* device; + { + auto lock = mutex.asScopedLock(); + lock.lock(); + device = streamDevice; + } + if (device == nullptr) { + return; + } + // See setInputEnabled() for why mutex must be released before this call. + audio_stream_set_enabled(device, AUDIO_CODEC_DIR_OUTPUT, enabled); +} + +float AudioService::getInputVolume() const { + auto lock = mutex.asScopedLock(); + lock.lock(); + float volume = 0.0f; + if (streamDevice != nullptr) { + audio_stream_get_volume(streamDevice, AUDIO_CODEC_DIR_INPUT, &volume); + } + return volume; +} + +void AudioService::setInputVolume(float percent) { + Device* device; + { + auto lock = mutex.asScopedLock(); + lock.lock(); + device = streamDevice; + } + if (device == nullptr) { + return; + } + // See setInputEnabled() for why mutex must be released before this call. + audio_stream_set_volume(device, AUDIO_CODEC_DIR_INPUT, percent); +} + +float AudioService::getOutputVolume() const { + auto lock = mutex.asScopedLock(); + lock.lock(); + float volume = 0.0f; + if (streamDevice != nullptr) { + audio_stream_get_volume(streamDevice, AUDIO_CODEC_DIR_OUTPUT, &volume); + } + return volume; +} + +void AudioService::setOutputVolume(float percent) { + Device* device; + { + auto lock = mutex.asScopedLock(); + lock.lock(); + device = streamDevice; + } + if (device == nullptr) { + return; + } + // See setInputEnabled() for why mutex must be released before this call. + audio_stream_set_volume(device, AUDIO_CODEC_DIR_OUTPUT, percent); +} + +bool AudioService::isInputMuted() const { + auto lock = mutex.asScopedLock(); + lock.lock(); + bool muted = false; + if (streamDevice != nullptr) { + audio_stream_get_mute(streamDevice, AUDIO_CODEC_DIR_INPUT, &muted); + } + return muted; +} + +void AudioService::setInputMuted(bool muted) { + Device* device; + { + auto lock = mutex.asScopedLock(); + lock.lock(); + device = streamDevice; + } + if (device == nullptr) { + return; + } + // See setInputEnabled() for why mutex must be released before this call. + audio_stream_set_mute(device, AUDIO_CODEC_DIR_INPUT, muted); +} + +bool AudioService::isOutputMuted() const { + auto lock = mutex.asScopedLock(); + lock.lock(); + bool muted = false; + if (streamDevice != nullptr) { + audio_stream_get_mute(streamDevice, AUDIO_CODEC_DIR_OUTPUT, &muted); + } + return muted; +} + +void AudioService::setOutputMuted(bool muted) { + Device* device; + { + auto lock = mutex.asScopedLock(); + lock.lock(); + device = streamDevice; + } + if (device == nullptr) { + return; + } + // See setInputEnabled() for why mutex must be released before this call. + audio_stream_set_mute(device, AUDIO_CODEC_DIR_OUTPUT, muted); +} + +// Precondition: AudioService must already be registered and started. Audio.cpp's wrapper +// functions do NOT use this (they must tolerate the service never having been registered +// on devices without audio hardware) - this is for callers that require the service to exist. +std::shared_ptr findAudioService() { + auto service = findServiceById(manifest.id); + assert(service != nullptr); + return std::static_pointer_cast(service); +} + +extern const ServiceManifest manifest = { + .id = "Audio", + .createService = create +}; + +} // namespace tt::service::audio diff --git a/Tactility/Source/settings/AudioSettings.cpp b/Tactility/Source/settings/AudioSettings.cpp new file mode 100644 index 000000000..c2bc30ab8 --- /dev/null +++ b/Tactility/Source/settings/AudioSettings.cpp @@ -0,0 +1,113 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace tt::settings::audio { + +static std::string getSettingsFilePath() { + return getUserDataPath() + "/settings/audio.properties"; +} + +constexpr auto* SETTINGS_KEY_INPUT_ENABLED = "inputEnabled"; +constexpr auto* SETTINGS_KEY_OUTPUT_ENABLED = "outputEnabled"; +constexpr auto* SETTINGS_KEY_INPUT_MUTED = "inputMuted"; +constexpr auto* SETTINGS_KEY_OUTPUT_MUTED = "outputMuted"; +constexpr auto* SETTINGS_KEY_INPUT_VOLUME = "inputVolume"; +constexpr auto* SETTINGS_KEY_OUTPUT_VOLUME = "outputVolume"; + +static bool toBool(const std::map& map, const char* key, bool defaultValue) { + auto entry = map.find(key); + if (entry == map.end()) { + return defaultValue; + } + return (entry->second == "1" || entry->second == "true" || entry->second == "True"); +} + +static float toFloat(const std::map& map, const char* key, float defaultValue) { + auto entry = map.find(key); + if (entry == map.end()) { + return defaultValue; + } + // Volume is documented/consumed as a 0..100 percentage (audio_stream_set_volume); clamp + // here so a hand-edited or corrupted properties file can't push out-of-range values + // through to the codec. + float value = std::strtof(entry->second.c_str(), nullptr); + return std::clamp(value, 0.0f, 100.0f); +} + +static std::string toString(bool value) { + return value ? "1" : "0"; +} + +static std::string toString(float value) { + // std::to_string always emits 6 decimals (e.g. "20.000000"); volume is a 0..100 + // percentage where whole-number precision is all that's meaningful here. + char buffer[16]; + snprintf(buffer, sizeof(buffer), "%.1f", (double) value); + return std::string(buffer); +} + +bool load(AudioSettings& settings) { + auto settings_path = getSettingsFilePath(); + if (!file::isFile(settings_path)) { + return false; + } + + std::map map; + if (!file::loadPropertiesFile(settings_path, map)) { + return false; + } + + settings.inputEnabled = toBool(map, SETTINGS_KEY_INPUT_ENABLED, true); + settings.outputEnabled = toBool(map, SETTINGS_KEY_OUTPUT_ENABLED, true); + settings.inputMuted = toBool(map, SETTINGS_KEY_INPUT_MUTED, false); + settings.outputMuted = toBool(map, SETTINGS_KEY_OUTPUT_MUTED, false); + settings.inputVolume = toFloat(map, SETTINGS_KEY_INPUT_VOLUME, 90.0f); + settings.outputVolume = toFloat(map, SETTINGS_KEY_OUTPUT_VOLUME, 50.0f); + + return true; +} + +AudioSettings getDefault() { + return AudioSettings { + .inputEnabled = true, + .outputEnabled = true, + .inputMuted = false, + .outputMuted = false, + .inputVolume = 90.0f, + .outputVolume = 50.0f + }; +} + +AudioSettings loadOrGetDefault() { + AudioSettings settings; + if (!load(settings)) { + settings = getDefault(); + } + return settings; +} + +bool save(const AudioSettings& settings) { + std::map map; + map[SETTINGS_KEY_INPUT_ENABLED] = toString(settings.inputEnabled); + map[SETTINGS_KEY_OUTPUT_ENABLED] = toString(settings.outputEnabled); + map[SETTINGS_KEY_INPUT_MUTED] = toString(settings.inputMuted); + map[SETTINGS_KEY_OUTPUT_MUTED] = toString(settings.outputMuted); + map[SETTINGS_KEY_INPUT_VOLUME] = toString(settings.inputVolume); + map[SETTINGS_KEY_OUTPUT_VOLUME] = toString(settings.outputVolume); + auto settings_path = getSettingsFilePath(); + if (!file::findOrCreateParentDirectory(settings_path, 0755)) { + return false; + } + return file::savePropertiesFile(settings_path, map); +} + +} // namespace tt::settings::audio diff --git a/TactilityKernel/include/tactility/drivers/audio_codec.h b/TactilityKernel/include/tactility/drivers/audio_codec.h new file mode 100644 index 000000000..83c9bc033 --- /dev/null +++ b/TactilityKernel/include/tactility/drivers/audio_codec.h @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#include +#include + +struct Device; + +/** + * @brief Direction of an audio codec operation or capability. + */ +enum AudioCodecDirection { + AUDIO_CODEC_DIR_INPUT = 1, + AUDIO_CODEC_DIR_OUTPUT = 2, + AUDIO_CODEC_DIR_BOTH = 3, +}; + +/** + * @brief Stream configuration used to open a codec for reading or writing. + */ +struct AudioCodecStreamConfig { + uint32_t sample_rate; + uint8_t bits_per_sample; // 16, 24, 32 + uint8_t channels; + enum AudioCodecDirection direction; +}; + +/** + * @brief API for audio codec drivers (e.g. ES8388, ES7210, AW88298). + * + * Implementations typically wrap a vendor codec library (e.g. esp_codec_dev) and + * the underlying I2S/I2C/GPIO controllers. This API is intentionally low-level and + * codec-agnostic: it does not perform resampling or mixing. Most apps should use + * the higher-level audio_stream API (see audio_stream.h) instead. + */ +struct AudioCodecApi { + /** + * @brief Opens the codec for streaming in the given direction(s). + * @param[in] device the audio codec device + * @param[in] config the stream configuration + * @retval ERROR_NONE on success + * @retval ERROR_NOT_SUPPORTED if the requested direction is not supported + */ + error_t (*open)(struct Device* device, const struct AudioCodecStreamConfig* config); + + /** + * @brief Closes the codec, stopping any active streams. + * @param[in] device the audio codec device + * @retval ERROR_NONE on success + */ + error_t (*close)(struct Device* device); + + /** + * @brief Reads recorded audio data from the codec. Only valid for codecs opened with input direction. + * @param[in] device the audio codec device + * @param[out] data the buffer to store the read data + * @param[in] data_size the number of bytes to read + * @param[out] bytes_read the number of bytes actually read + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE on success + * @retval ERROR_TIMEOUT when the operation timed out + */ + error_t (*read)(struct Device* device, void* data, size_t data_size, size_t* bytes_read, TickType_t timeout); + + /** + * @brief Writes audio data for playback to the codec. Only valid for codecs opened with output direction. + * @param[in] device the audio codec device + * @param[in] data the buffer containing the data to write + * @param[in] data_size the number of bytes to write + * @param[out] bytes_written the number of bytes actually written + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE on success + * @retval ERROR_TIMEOUT when the operation timed out + */ + error_t (*write)(struct Device* device, const void* data, size_t data_size, size_t* bytes_written, TickType_t timeout); + + /** + * @brief Sets the volume for the given direction. + * @param[in] device the audio codec device + * @param[in] direction AUDIO_CODEC_DIR_INPUT or AUDIO_CODEC_DIR_OUTPUT + * @param[in] volume_percent volume in the range 0..100 + * @retval ERROR_NONE on success + * @retval ERROR_NOT_SUPPORTED if the direction is not supported by this codec + */ + error_t (*set_volume)(struct Device* device, enum AudioCodecDirection direction, float volume_percent); + + /** + * @brief Gets the volume for the given direction. + * @param[in] device the audio codec device + * @param[in] direction AUDIO_CODEC_DIR_INPUT or AUDIO_CODEC_DIR_OUTPUT + * @param[out] volume_percent volume in the range 0..100 + * @retval ERROR_NONE on success + * @retval ERROR_NOT_SUPPORTED if the direction is not supported by this codec + */ + error_t (*get_volume)(struct Device* device, enum AudioCodecDirection direction, float* volume_percent); + + /** + * @brief Mutes or unmutes the given direction. + * @param[in] device the audio codec device + * @param[in] direction AUDIO_CODEC_DIR_INPUT or AUDIO_CODEC_DIR_OUTPUT + * @param[in] muted true to mute, false to unmute + * @retval ERROR_NONE on success + * @retval ERROR_NOT_SUPPORTED if the direction is not supported by this codec + */ + error_t (*set_mute)(struct Device* device, enum AudioCodecDirection direction, bool muted); + + /** + * @brief Gets the mute state for the given direction. + * @param[in] device the audio codec device + * @param[in] direction AUDIO_CODEC_DIR_INPUT or AUDIO_CODEC_DIR_OUTPUT + * @param[out] muted true if muted, false otherwise + * @retval ERROR_NONE on success + * @retval ERROR_NOT_SUPPORTED if the direction is not supported by this codec + */ + error_t (*get_mute)(struct Device* device, enum AudioCodecDirection direction, bool* muted); + + /** + * @brief Gets the native sample rate the codec hardware runs at for the given direction. + * @param[in] device the audio codec device + * @param[in] direction AUDIO_CODEC_DIR_INPUT or AUDIO_CODEC_DIR_OUTPUT + * @param[out] rate_hz the native sample rate in Hz + * @retval ERROR_NONE on success + * @retval ERROR_NOT_SUPPORTED if the direction is not supported by this codec + */ + error_t (*get_native_sample_rate)(struct Device* device, enum AudioCodecDirection direction, uint32_t* rate_hz); + + /** + * @brief Gets the number of channels the codec hardware natively expects/produces for the + * given direction (e.g. a 4-slot TDM microphone ADC reports 4 here even if an app only + * wants 1). Callers that need a different channel count must downmix/upmix themselves -- + * opening the codec with a channel count that doesn't match its native layout can produce + * corrupted audio (e.g. ES7210 in TDM mode silently halves its configured bit depth when + * asked for <= 2 channels). + * @param[in] device the audio codec device + * @param[in] direction AUDIO_CODEC_DIR_INPUT or AUDIO_CODEC_DIR_OUTPUT + * @param[out] channels the native channel count + * @retval ERROR_NONE on success + * @retval ERROR_NOT_SUPPORTED if the direction is not supported by this codec + */ + error_t (*get_native_channels)(struct Device* device, enum AudioCodecDirection direction, uint8_t* channels); + + /** + * @brief Gets the directions supported by this codec. + * @param[in] device the audio codec device + * @param[out] supported_directions AUDIO_CODEC_DIR_INPUT, AUDIO_CODEC_DIR_OUTPUT, or AUDIO_CODEC_DIR_BOTH + * @retval ERROR_NONE on success + */ + error_t (*get_capabilities)(struct Device* device, enum AudioCodecDirection* supported_directions); + + /** + * @brief Gets a fixed digital gain multiplier to apply to this codec's input samples, + * on top of whatever audio_codec_set_volume() controls. Compensates for mic capsules + * that are quiet even at full hardware gain (e.g. small PDM/MEMS mics), or codecs with + * no working hardware gain control at all (e.g. a dummy PDM codec). Set from the + * codec's own devicetree config; 1.0 (no boost) if this field is NULL or the codec + * doesn't implement it. + * @param[in] device the audio codec device + * @param[out] gain the multiplier to apply (1.0 = no change) + * @retval ERROR_NONE on success + * @retval ERROR_NOT_SUPPORTED if the direction is not supported by this codec + */ + error_t (*get_input_gain_multiplier)(struct Device* device, float* gain); +}; + +/** @brief See AudioCodecApi::open */ +error_t audio_codec_open(struct Device* device, const struct AudioCodecStreamConfig* config); + +/** @brief See AudioCodecApi::close */ +error_t audio_codec_close(struct Device* device); + +/** @brief See AudioCodecApi::read */ +error_t audio_codec_read(struct Device* device, void* data, size_t data_size, size_t* bytes_read, TickType_t timeout); + +/** @brief See AudioCodecApi::write */ +error_t audio_codec_write(struct Device* device, const void* data, size_t data_size, size_t* bytes_written, TickType_t timeout); + +/** @brief See AudioCodecApi::set_volume */ +error_t audio_codec_set_volume(struct Device* device, enum AudioCodecDirection direction, float volume_percent); + +/** @brief See AudioCodecApi::get_volume */ +error_t audio_codec_get_volume(struct Device* device, enum AudioCodecDirection direction, float* volume_percent); + +/** @brief See AudioCodecApi::set_mute */ +error_t audio_codec_set_mute(struct Device* device, enum AudioCodecDirection direction, bool muted); + +/** @brief See AudioCodecApi::get_mute */ +error_t audio_codec_get_mute(struct Device* device, enum AudioCodecDirection direction, bool* muted); + +/** @brief See AudioCodecApi::get_native_sample_rate */ +error_t audio_codec_get_native_sample_rate(struct Device* device, enum AudioCodecDirection direction, uint32_t* rate_hz); + +/** @brief See AudioCodecApi::get_native_channels */ +error_t audio_codec_get_native_channels(struct Device* device, enum AudioCodecDirection direction, uint8_t* channels); + +/** @brief See AudioCodecApi::get_capabilities */ +error_t audio_codec_get_capabilities(struct Device* device, enum AudioCodecDirection* supported_directions); + +/** + * @brief See AudioCodecApi::get_input_gain_multiplier. Returns 1.0 (no change) if the + * codec doesn't implement this field, rather than ERROR_NOT_SUPPORTED -- callers can treat + * the multiplier as always present. + */ +error_t audio_codec_get_input_gain_multiplier(struct Device* device, float* gain); + +extern const struct DeviceType AUDIO_CODEC_TYPE; + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/include/tactility/drivers/audio_stream.h b/TactilityKernel/include/tactility/drivers/audio_stream.h new file mode 100644 index 000000000..e796b7ea5 --- /dev/null +++ b/TactilityKernel/include/tactility/drivers/audio_stream.h @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#include +#include +#include + +struct Device; + +/** + * @brief Handle to an open audio stream (see audio_stream_open_input/open_output). + * + * The `device` field identifies the owning audio stream device so that + * audio_stream_read/write/close can dispatch to the correct driver; implementations + * embed this as the first member of a larger private struct and may store additional + * state after it. + */ +struct AudioStreamHandleData { + struct Device* device; +}; + +typedef struct AudioStreamHandleData* AudioStreamHandle; + +/** + * @brief Stream configuration requested by the caller. + * + * The sample_rate is the rate the caller wants to read/write at; the audio-stream + * device transparently resamples to/from the bound codec's native rate, so the same + * app code works regardless of which codec hardware is present. + */ +struct AudioStreamConfig { + uint32_t sample_rate; // e.g. 16000, 44100, 48000 + uint8_t bits_per_sample; // 16, 24, 32 + uint8_t channels; +}; + +/** @brief Identifies which cached field changed in an AudioStreamChangeCallback. */ +enum AudioStreamChange { + AUDIO_STREAM_CHANGE_VOLUME, + AUDIO_STREAM_CHANGE_MUTE, + AUDIO_STREAM_CHANGE_ENABLED, +}; + +typedef void (*AudioStreamChangeCallback)(struct Device* device, enum AudioCodecDirection direction, enum AudioStreamChange change, void* user_data); + +/** + * @brief API for the high-level full-duplex audio stream device. + * + * Implementations bind to one input-capable and/or one output-capable AUDIO_CODEC_TYPE + * device, and provide resampling plus shared volume/mute/enable state on top. This is + * the API apps (including ELF side-loaded apps) and AudioService should use — most code + * should not talk to AUDIO_CODEC_TYPE devices directly. + * + * read/write are blocking and must be called from the caller's own task, never from the + * main/LVGL thread. + */ +struct AudioStreamApi { + /** + * @brief Opens an input (recording) stream at the requested configuration. + * @param[in] device the audio stream device + * @param[in] config the requested stream configuration + * @param[out] out_handle receives the opened stream handle + * @retval ERROR_NONE on success + * @retval ERROR_NOT_SUPPORTED if no input-capable codec is bound + * @retval ERROR_INVALID_STATE if an input stream is already open + */ + error_t (*open_input)(struct Device* device, const struct AudioStreamConfig* config, AudioStreamHandle* out_handle); + + /** + * @brief Opens an output (playback) stream at the requested configuration. + * @param[in] device the audio stream device + * @param[in] config the requested stream configuration + * @param[out] out_handle receives the opened stream handle + * @retval ERROR_NONE on success + * @retval ERROR_NOT_SUPPORTED if no output-capable codec is bound + * @retval ERROR_INVALID_STATE if an output stream is already open + */ + error_t (*open_output)(struct Device* device, const struct AudioStreamConfig* config, AudioStreamHandle* out_handle); + + /** + * @brief Reads recorded audio data from an input stream, resampled to the requested rate. + * @param[in] handle the stream handle returned by open_input + * @param[out] data the buffer to store the read data + * @param[in] data_size the number of bytes to read + * @param[out] bytes_read the number of bytes actually read + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE on success + * @retval ERROR_TIMEOUT when the operation timed out + */ + error_t (*read)(AudioStreamHandle handle, void* data, size_t data_size, size_t* bytes_read, TickType_t timeout); + + /** + * @brief Writes audio data for playback to an output stream, resampled from the requested rate. + * @param[in] handle the stream handle returned by open_output + * @param[in] data the buffer containing the data to write + * @param[in] data_size the number of bytes to write + * @param[out] bytes_written the number of bytes actually written + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE on success + * @retval ERROR_TIMEOUT when the operation timed out + */ + error_t (*write)(AudioStreamHandle handle, const void* data, size_t data_size, size_t* bytes_written, TickType_t timeout); + + /** + * @brief Closes a stream opened with open_input or open_output. + * @param[in] handle the stream handle to close + * @retval ERROR_NONE on success + */ + error_t (*close)(AudioStreamHandle handle); + + /** + * @brief Sets the shared volume for the given direction. Visible to all consumers + * (other apps, AudioService, Settings UI) since the state lives on the bound codec. + * @param[in] device the audio stream device + * @param[in] direction AUDIO_CODEC_DIR_INPUT or AUDIO_CODEC_DIR_OUTPUT + * @param[in] volume_percent volume in the range 0..100 + * @retval ERROR_NONE on success + * @retval ERROR_NOT_SUPPORTED if no codec is bound for the given direction + */ + error_t (*set_volume)(struct Device* device, enum AudioCodecDirection direction, float volume_percent); + + /** @brief Gets the shared volume for the given direction. See set_volume. */ + error_t (*get_volume)(struct Device* device, enum AudioCodecDirection direction, float* volume_percent); + + /** @brief Sets the shared mute state for the given direction. See set_volume. */ + error_t (*set_mute)(struct Device* device, enum AudioCodecDirection direction, bool muted); + + /** @brief Gets the shared mute state for the given direction. See set_volume. */ + error_t (*get_mute)(struct Device* device, enum AudioCodecDirection direction, bool* muted); + + /** + * @brief Enables or disables the given direction. A disabled direction cannot be + * opened (open_input/open_output return ERROR_NOT_ALLOWED) and any open stream in + * that direction is closed. Used by Settings UI / AudioService to govern access. + * @param[in] device the audio stream device + * @param[in] direction AUDIO_CODEC_DIR_INPUT or AUDIO_CODEC_DIR_OUTPUT + * @param[in] enabled true to enable, false to disable + * @retval ERROR_NONE on success + * @retval ERROR_NOT_SUPPORTED if no codec is bound for the given direction + */ + error_t (*set_enabled)(struct Device* device, enum AudioCodecDirection direction, bool enabled); + + /** @brief Gets the enabled state for the given direction. See set_enabled. */ + error_t (*get_enabled)(struct Device* device, enum AudioCodecDirection direction, bool* enabled); + + /** + * @brief Checks whether a codec supporting the given direction is bound. + * @param[in] device the audio stream device + * @param[in] direction AUDIO_CODEC_DIR_INPUT or AUDIO_CODEC_DIR_OUTPUT + * @param[out] supported true if a codec for this direction is bound, false otherwise + * @retval ERROR_NONE on success + */ + error_t (*is_supported)(struct Device* device, enum AudioCodecDirection direction, bool* supported); + + /** @brief See audio_stream_set_change_callback. */ + error_t (*set_change_callback)(struct Device* device, AudioStreamChangeCallback callback, void* user_data); +}; + +/** @brief See AudioStreamApi::open_input */ +error_t audio_stream_open_input(struct Device* device, const struct AudioStreamConfig* config, AudioStreamHandle* out_handle); + +/** @brief See AudioStreamApi::open_output */ +error_t audio_stream_open_output(struct Device* device, const struct AudioStreamConfig* config, AudioStreamHandle* out_handle); + +/** @brief See AudioStreamApi::read */ +error_t audio_stream_read(AudioStreamHandle handle, void* data, size_t data_size, size_t* bytes_read, TickType_t timeout); + +/** @brief See AudioStreamApi::write */ +error_t audio_stream_write(AudioStreamHandle handle, const void* data, size_t data_size, size_t* bytes_written, TickType_t timeout); + +/** @brief See AudioStreamApi::close */ +error_t audio_stream_close(AudioStreamHandle handle); + +/** @brief See AudioStreamApi::set_volume */ +error_t audio_stream_set_volume(struct Device* device, enum AudioCodecDirection direction, float volume_percent); + +/** @brief See AudioStreamApi::get_volume */ +error_t audio_stream_get_volume(struct Device* device, enum AudioCodecDirection direction, float* volume_percent); + +/** @brief See AudioStreamApi::set_mute */ +error_t audio_stream_set_mute(struct Device* device, enum AudioCodecDirection direction, bool muted); + +/** @brief See AudioStreamApi::get_mute */ +error_t audio_stream_get_mute(struct Device* device, enum AudioCodecDirection direction, bool* muted); + +/** @brief See AudioStreamApi::set_enabled */ +error_t audio_stream_set_enabled(struct Device* device, enum AudioCodecDirection direction, bool enabled); + +/** @brief See AudioStreamApi::get_enabled */ +error_t audio_stream_get_enabled(struct Device* device, enum AudioCodecDirection direction, bool* enabled); + +/** @brief See AudioStreamApi::is_supported */ +error_t audio_stream_is_supported(struct Device* device, enum AudioCodecDirection direction, bool* supported); + +/** @brief See AudioStreamApi::set_change_callback */ +error_t audio_stream_set_change_callback(struct Device* device, AudioStreamChangeCallback callback, void* user_data); + +extern const struct DeviceType AUDIO_STREAM_TYPE; + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/include/tactility/drivers/i2s_controller.h b/TactilityKernel/include/tactility/drivers/i2s_controller.h index cf9f94e14..71d491a2c 100644 --- a/TactilityKernel/include/tactility/drivers/i2s_controller.h +++ b/TactilityKernel/include/tactility/drivers/i2s_controller.h @@ -40,6 +40,16 @@ struct I2sTdmRxConfig { uint8_t slot_bit_width; // bit width of each TDM slot (0 = auto, matches bits_per_sample) }; +/** + * @brief I2S PDM RX config (e.g. for a PDM MEMS microphone such as the SPM1423). + * Only supported on I2S controller 0 on ESP32 targets -- PDM RX is hardware-restricted + * to that controller. PDM only supports 16-bit samples and mono or stereo. + */ +struct I2sPdmRxConfig { + uint32_t sample_rate_hz; + bool stereo; // false = mono (single PDM mic), true = stereo (two PDM mics) +}; + /** * @brief I2S Config */ @@ -104,13 +114,40 @@ struct I2sControllerApi { /** * @brief Reconfigures the RX channel to TDM mode (e.g. for ES7210). - * Must be called after set_config() which creates the channel handles. + * Does not require set_config() to be called first -- TDM allocates its own RX + * channel handle independently of standard mode (calling set_config() first would + * leave its TX/RX channel pair bound to the shared BCLK/WS/MCLK pins, which then + * blocks this call from claiming them; callers should pick TDM or standard mode + * up front, not call both in sequence). * @param[in] device the I2S controller device * @param[in] config TDM parameters * @retval ERROR_NONE when the operation was successful * @retval ERROR_NOT_SUPPORTED if the driver does not implement TDM */ error_t (*set_rx_tdm_config)(struct Device* device, const struct I2sTdmRxConfig* config); + + /** + * @brief Reconfigures the RX channel to PDM mode (e.g. for a PDM MEMS microphone). + * Does not require set_config() to be called first -- PDM RX allocates its own + * channel handle independently of standard/TDM mode. + * @param[in] device the I2S controller device + * @param[in] config PDM parameters + * @retval ERROR_NONE when the operation was successful + * @retval ERROR_NOT_SUPPORTED if the driver/controller does not support PDM RX + */ + error_t (*set_rx_pdm_config)(struct Device* device, const struct I2sPdmRxConfig* config); + + /** + * @brief Disables and releases only the TX or only the RX channel, leaving the other + * direction (if active) running. Unlike reset(), which tears down both directions -- + * unsuitable when a single controller carries an independent TX user (e.g. a speaker + * amp) and RX user (e.g. a PDM mic) that must be able to close without affecting each + * other. + * @param[in] device the I2S controller device + * @param[in] is_input true to disable the RX channel, false to disable the TX channel + * @retval ERROR_NONE when the operation was successful + */ + error_t (*disable_direction)(struct Device* device, bool is_input); }; /** @@ -160,7 +197,9 @@ error_t i2s_controller_reset(struct Device* device); /** * @brief Reconfigures the RX channel to TDM mode (e.g. for ES7210 4-slot mic ADC). - * Must be called after i2s_controller_set_config() which creates the channel handles. + * Does not require i2s_controller_set_config() to be called first -- see + * I2sControllerApi::set_rx_tdm_config for why standard mode and TDM mode are + * alternative setup paths rather than sequential steps. * @param[in] device the I2S controller device * @param[in] config TDM parameters * @retval ERROR_NONE when the operation was successful @@ -168,6 +207,24 @@ error_t i2s_controller_reset(struct Device* device); */ error_t i2s_controller_set_rx_tdm_config(struct Device* device, const struct I2sTdmRxConfig* config); +/** + * @brief Reconfigures the RX channel to PDM mode (e.g. for a PDM MEMS microphone). + * @param[in] device the I2S controller device + * @param[in] config PDM parameters + * @retval ERROR_NONE when the operation was successful + * @retval ERROR_NOT_SUPPORTED if the driver/controller does not support PDM RX + */ +error_t i2s_controller_set_rx_pdm_config(struct Device* device, const struct I2sPdmRxConfig* config); + +/** + * @brief See I2sControllerApi::disable_direction. Falls back to the full reset() if the + * driver doesn't implement direction-aware teardown. + * @param[in] device the I2S controller device + * @param[in] is_input true to disable the RX channel, false to disable the TX channel + * @retval ERROR_NONE when the operation was successful + */ +error_t i2s_controller_disable_direction(struct Device* device, bool is_input); + extern const struct DeviceType I2S_CONTROLLER_TYPE; #ifdef __cplusplus diff --git a/TactilityKernel/source/drivers/audio_codec.cpp b/TactilityKernel/source/drivers/audio_codec.cpp new file mode 100644 index 000000000..b8bcea9f7 --- /dev/null +++ b/TactilityKernel/source/drivers/audio_codec.cpp @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +#define AUDIO_CODEC_DRIVER_API(driver) ((struct AudioCodecApi*)driver->api) + +extern "C" { + +error_t audio_codec_open(Device* device, const struct AudioCodecStreamConfig* config) { + const auto* driver = device_get_driver(device); + return AUDIO_CODEC_DRIVER_API(driver)->open(device, config); +} + +error_t audio_codec_close(Device* device) { + const auto* driver = device_get_driver(device); + return AUDIO_CODEC_DRIVER_API(driver)->close(device); +} + +error_t audio_codec_read(Device* device, void* data, size_t data_size, size_t* bytes_read, TickType_t timeout) { + const auto* driver = device_get_driver(device); + return AUDIO_CODEC_DRIVER_API(driver)->read(device, data, data_size, bytes_read, timeout); +} + +error_t audio_codec_write(Device* device, const void* data, size_t data_size, size_t* bytes_written, TickType_t timeout) { + const auto* driver = device_get_driver(device); + return AUDIO_CODEC_DRIVER_API(driver)->write(device, data, data_size, bytes_written, timeout); +} + +error_t audio_codec_set_volume(Device* device, AudioCodecDirection direction, float volume_percent) { + const auto* driver = device_get_driver(device); + return AUDIO_CODEC_DRIVER_API(driver)->set_volume(device, direction, volume_percent); +} + +error_t audio_codec_get_volume(Device* device, AudioCodecDirection direction, float* volume_percent) { + const auto* driver = device_get_driver(device); + return AUDIO_CODEC_DRIVER_API(driver)->get_volume(device, direction, volume_percent); +} + +error_t audio_codec_set_mute(Device* device, AudioCodecDirection direction, bool muted) { + const auto* driver = device_get_driver(device); + return AUDIO_CODEC_DRIVER_API(driver)->set_mute(device, direction, muted); +} + +error_t audio_codec_get_mute(Device* device, AudioCodecDirection direction, bool* muted) { + const auto* driver = device_get_driver(device); + return AUDIO_CODEC_DRIVER_API(driver)->get_mute(device, direction, muted); +} + +error_t audio_codec_get_native_sample_rate(Device* device, AudioCodecDirection direction, uint32_t* rate_hz) { + const auto* driver = device_get_driver(device); + return AUDIO_CODEC_DRIVER_API(driver)->get_native_sample_rate(device, direction, rate_hz); +} + +error_t audio_codec_get_native_channels(Device* device, AudioCodecDirection direction, uint8_t* channels) { + const auto* driver = device_get_driver(device); + return AUDIO_CODEC_DRIVER_API(driver)->get_native_channels(device, direction, channels); +} + +error_t audio_codec_get_capabilities(Device* device, AudioCodecDirection* supported_directions) { + const auto* driver = device_get_driver(device); + return AUDIO_CODEC_DRIVER_API(driver)->get_capabilities(device, supported_directions); +} + +error_t audio_codec_get_input_gain_multiplier(Device* device, float* gain) { + const auto* driver = device_get_driver(device); + if (AUDIO_CODEC_DRIVER_API(driver)->get_input_gain_multiplier == nullptr) { + *gain = 1.0f; + return ERROR_NONE; + } + return AUDIO_CODEC_DRIVER_API(driver)->get_input_gain_multiplier(device, gain); +} + +const struct DeviceType AUDIO_CODEC_TYPE { + .name = "audio-codec" +}; + +} diff --git a/TactilityKernel/source/drivers/audio_stream.cpp b/TactilityKernel/source/drivers/audio_stream.cpp new file mode 100644 index 000000000..0964d6b60 --- /dev/null +++ b/TactilityKernel/source/drivers/audio_stream.cpp @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +#define AUDIO_STREAM_DRIVER_API(driver) ((struct AudioStreamApi*)driver->api) + +extern "C" { + +error_t audio_stream_open_input(Device* device, const struct AudioStreamConfig* config, AudioStreamHandle* outHandle) { + const auto* driver = device_get_driver(device); + return AUDIO_STREAM_DRIVER_API(driver)->open_input(device, config, outHandle); +} + +error_t audio_stream_open_output(Device* device, const struct AudioStreamConfig* config, AudioStreamHandle* outHandle) { + const auto* driver = device_get_driver(device); + return AUDIO_STREAM_DRIVER_API(driver)->open_output(device, config, outHandle); +} + +error_t audio_stream_read(AudioStreamHandle handle, void* data, size_t data_size, size_t* bytes_read, TickType_t timeout) { + const auto* driver = device_get_driver(handle->device); + return AUDIO_STREAM_DRIVER_API(driver)->read(handle, data, data_size, bytes_read, timeout); +} + +error_t audio_stream_write(AudioStreamHandle handle, const void* data, size_t data_size, size_t* bytes_written, TickType_t timeout) { + const auto* driver = device_get_driver(handle->device); + return AUDIO_STREAM_DRIVER_API(driver)->write(handle, data, data_size, bytes_written, timeout); +} + +error_t audio_stream_close(AudioStreamHandle handle) { + const auto* driver = device_get_driver(handle->device); + return AUDIO_STREAM_DRIVER_API(driver)->close(handle); +} + +error_t audio_stream_set_volume(Device* device, AudioCodecDirection direction, float volume_percent) { + const auto* driver = device_get_driver(device); + return AUDIO_STREAM_DRIVER_API(driver)->set_volume(device, direction, volume_percent); +} + +error_t audio_stream_get_volume(Device* device, AudioCodecDirection direction, float* volume_percent) { + const auto* driver = device_get_driver(device); + return AUDIO_STREAM_DRIVER_API(driver)->get_volume(device, direction, volume_percent); +} + +error_t audio_stream_set_mute(Device* device, AudioCodecDirection direction, bool muted) { + const auto* driver = device_get_driver(device); + return AUDIO_STREAM_DRIVER_API(driver)->set_mute(device, direction, muted); +} + +error_t audio_stream_get_mute(Device* device, AudioCodecDirection direction, bool* muted) { + const auto* driver = device_get_driver(device); + return AUDIO_STREAM_DRIVER_API(driver)->get_mute(device, direction, muted); +} + +error_t audio_stream_set_enabled(Device* device, AudioCodecDirection direction, bool enabled) { + const auto* driver = device_get_driver(device); + return AUDIO_STREAM_DRIVER_API(driver)->set_enabled(device, direction, enabled); +} + +error_t audio_stream_get_enabled(Device* device, AudioCodecDirection direction, bool* enabled) { + const auto* driver = device_get_driver(device); + return AUDIO_STREAM_DRIVER_API(driver)->get_enabled(device, direction, enabled); +} + +error_t audio_stream_is_supported(Device* device, AudioCodecDirection direction, bool* supported) { + const auto* driver = device_get_driver(device); + return AUDIO_STREAM_DRIVER_API(driver)->is_supported(device, direction, supported); +} + +error_t audio_stream_set_change_callback(Device* device, AudioStreamChangeCallback callback, void* user_data) { + const auto* driver = device_get_driver(device); + return AUDIO_STREAM_DRIVER_API(driver)->set_change_callback(device, callback, user_data); +} + +const struct DeviceType AUDIO_STREAM_TYPE { + .name = "audio-stream" +}; + +} diff --git a/TactilityKernel/source/drivers/i2s_controller.cpp b/TactilityKernel/source/drivers/i2s_controller.cpp index 3942ed47e..5b96f25b8 100644 --- a/TactilityKernel/source/drivers/i2s_controller.cpp +++ b/TactilityKernel/source/drivers/i2s_controller.cpp @@ -38,6 +38,20 @@ error_t i2s_controller_set_rx_tdm_config(struct Device* device, const struct I2s return I2S_DRIVER_API(driver)->set_rx_tdm_config(device, config); } +error_t i2s_controller_set_rx_pdm_config(struct Device* device, const struct I2sPdmRxConfig* config) { + const auto* driver = device_get_driver(device); + if (!I2S_DRIVER_API(driver)->set_rx_pdm_config) return ERROR_NOT_SUPPORTED; + return I2S_DRIVER_API(driver)->set_rx_pdm_config(device, config); +} + +error_t i2s_controller_disable_direction(struct Device* device, bool isInput) { + const auto* driver = device_get_driver(device); + if (!I2S_DRIVER_API(driver)->disable_direction) { + return I2S_DRIVER_API(driver)->reset(device); + } + return I2S_DRIVER_API(driver)->disable_direction(device, isInput); +} + const struct DeviceType I2S_CONTROLLER_TYPE { .name = "i2s-controller" }; diff --git a/TactilityKernel/source/kernel_symbols.c b/TactilityKernel/source/kernel_symbols.c index a84d24b26..1a627c5ac 100644 --- a/TactilityKernel/source/kernel_symbols.c +++ b/TactilityKernel/source/kernel_symbols.c @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include @@ -80,6 +82,32 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(driver_is_compatible), DEFINE_MODULE_SYMBOL(driver_find_compatible), DEFINE_MODULE_SYMBOL(driver_get_device_type), + // drivers/audio_codec + DEFINE_MODULE_SYMBOL(audio_codec_open), + DEFINE_MODULE_SYMBOL(audio_codec_close), + DEFINE_MODULE_SYMBOL(audio_codec_read), + DEFINE_MODULE_SYMBOL(audio_codec_write), + DEFINE_MODULE_SYMBOL(audio_codec_set_volume), + DEFINE_MODULE_SYMBOL(audio_codec_get_volume), + DEFINE_MODULE_SYMBOL(audio_codec_set_mute), + DEFINE_MODULE_SYMBOL(audio_codec_get_mute), + DEFINE_MODULE_SYMBOL(audio_codec_get_native_sample_rate), + DEFINE_MODULE_SYMBOL(audio_codec_get_native_channels), + DEFINE_MODULE_SYMBOL(audio_codec_get_capabilities), + DEFINE_MODULE_SYMBOL(AUDIO_CODEC_TYPE), + // drivers/audio_stream + DEFINE_MODULE_SYMBOL(audio_stream_open_input), + DEFINE_MODULE_SYMBOL(audio_stream_open_output), + DEFINE_MODULE_SYMBOL(audio_stream_read), + DEFINE_MODULE_SYMBOL(audio_stream_write), + DEFINE_MODULE_SYMBOL(audio_stream_close), + DEFINE_MODULE_SYMBOL(audio_stream_set_volume), + DEFINE_MODULE_SYMBOL(audio_stream_get_volume), + DEFINE_MODULE_SYMBOL(audio_stream_set_mute), + DEFINE_MODULE_SYMBOL(audio_stream_get_mute), + DEFINE_MODULE_SYMBOL(audio_stream_set_enabled), + DEFINE_MODULE_SYMBOL(audio_stream_get_enabled), + DEFINE_MODULE_SYMBOL(AUDIO_STREAM_TYPE), // drivers/gpio_controller DEFINE_MODULE_SYMBOL(gpio_descriptor_acquire), DEFINE_MODULE_SYMBOL(gpio_descriptor_release),