mirror of
https://github.com/esphome/esphome.git
synced 2026-10-17 19:52:50 +00:00
[audio_dac] Add ES9038Q2M Katana platform (#15017)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
pre-commit-ci-lite[bot]
J. Nick Koston
J. Nick Koston
parent
48a0a41cce
commit
d8ca4a9460
@@ -183,6 +183,7 @@ esphome/components/es7243e/* @kbx81
|
||||
esphome/components/es8156/* @kbx81
|
||||
esphome/components/es8311/* @kahrendt @kroimon
|
||||
esphome/components/es8388/* @P4uLT
|
||||
esphome/components/es9038q2m_katana/* @GioMez
|
||||
esphome/components/esp32/* @esphome/core
|
||||
esphome/components/esp32_ble/* @bdraco @jesserockz @Rapsssito
|
||||
esphome/components/esp32_ble_client/* @bdraco @jesserockz
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
CODEOWNERS = ["@GioMez"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "es9038q2m_katana"
|
||||
@@ -0,0 +1,92 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import audio_dac, i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_SAMPLE_RATE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CONF_DEEMPHASIS = "deemphasis"
|
||||
CONF_DOP = "dop"
|
||||
CONF_FILTER_SHAPE = "filter_shape"
|
||||
|
||||
# Fields of the Katana format register: channel layout, word length and rate code.
|
||||
FORMAT_STEREO = 0x80
|
||||
BITS_PER_SAMPLE_CODES = {16: 0x10, 24: 0x20, 32: 0x30}
|
||||
SAMPLE_RATE_CODES = {
|
||||
44100: 0x04,
|
||||
48000: 0x05,
|
||||
88200: 0x06,
|
||||
96000: 0x07,
|
||||
176400: 0x08,
|
||||
192000: 0x09,
|
||||
352800: 0x0A,
|
||||
384000: 0x0B,
|
||||
}
|
||||
|
||||
es9038q2m_katana_ns = cg.esphome_ns.namespace("es9038q2m_katana")
|
||||
ES9038Q2MKatana = es9038q2m_katana_ns.class_(
|
||||
"ES9038Q2MKatana",
|
||||
audio_dac.AudioDac,
|
||||
cg.Component,
|
||||
i2c.I2CDevice,
|
||||
)
|
||||
|
||||
FilterShape = es9038q2m_katana_ns.enum("FilterShape")
|
||||
FILTER_SHAPES = {
|
||||
"linear_phase_fast": FilterShape.FILTER_SHAPE_LINEAR_PHASE_FAST,
|
||||
"linear_phase_slow": FilterShape.FILTER_SHAPE_LINEAR_PHASE_SLOW,
|
||||
"minimum_phase_fast": FilterShape.FILTER_SHAPE_MIN_PHASE_FAST,
|
||||
"minimum_phase_slow": FilterShape.FILTER_SHAPE_MIN_PHASE_SLOW,
|
||||
"apodizing": FilterShape.FILTER_SHAPE_APODIZING,
|
||||
"hybrid": FilterShape.FILTER_SHAPE_HYBRID,
|
||||
"brick_wall": FilterShape.FILTER_SHAPE_BRICK_WALL,
|
||||
}
|
||||
|
||||
DeemphasisMode = es9038q2m_katana_ns.enum("DeemphasisMode")
|
||||
DEEMPHASIS_MODES = {
|
||||
"bypass": DeemphasisMode.DEEMPHASIS_BYPASS,
|
||||
"32khz": DeemphasisMode.DEEMPHASIS_32KHZ,
|
||||
"44_1khz": DeemphasisMode.DEEMPHASIS_44_1KHZ,
|
||||
"48khz": DeemphasisMode.DEEMPHASIS_48KHZ,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ES9038Q2MKatana),
|
||||
cv.Optional(CONF_FILTER_SHAPE, default="apodizing"): cv.enum(
|
||||
FILTER_SHAPES, lower=True
|
||||
),
|
||||
cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All(
|
||||
cv.float_with_unit("bits", "bit"),
|
||||
cv.one_of(*BITS_PER_SAMPLE_CODES, int=True),
|
||||
),
|
||||
cv.Optional(CONF_SAMPLE_RATE, default=48000): cv.one_of(
|
||||
*SAMPLE_RATE_CODES, int=True
|
||||
),
|
||||
cv.Optional(CONF_DEEMPHASIS, default="bypass"): cv.enum(
|
||||
DEEMPHASIS_MODES, lower=True
|
||||
),
|
||||
# Only switches the control logic; the source must already send DoP frames.
|
||||
cv.Optional(CONF_DOP, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x30))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
cg.add(var.set_filter_shape(config[CONF_FILTER_SHAPE]))
|
||||
cg.add(
|
||||
var.set_format(
|
||||
FORMAT_STEREO
|
||||
| BITS_PER_SAMPLE_CODES[config[CONF_BITS_PER_SAMPLE]]
|
||||
| SAMPLE_RATE_CODES[config[CONF_SAMPLE_RATE]]
|
||||
)
|
||||
)
|
||||
cg.add(var.set_deemphasis_mode(config[CONF_DEEMPHASIS]))
|
||||
cg.add(var.set_dop_enabled(config[CONF_DOP]))
|
||||
@@ -0,0 +1,176 @@
|
||||
#include "es9038q2m_katana.h"
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::es9038q2m_katana {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "es9038q2m_katana");
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t KATANA_CHIP_ID = 0x30;
|
||||
|
||||
constexpr uint8_t KATANA_REG_CHIP_ID = 0x00;
|
||||
constexpr uint8_t KATANA_REG_RESET = 0x01;
|
||||
constexpr uint8_t KATANA_REG_VOLUME_1 = 0x02;
|
||||
constexpr uint8_t KATANA_REG_VOLUME_2 = 0x03;
|
||||
constexpr uint8_t KATANA_REG_MUTE = 0x04;
|
||||
constexpr uint8_t KATANA_REG_DSP_PROGRAM = 0x05;
|
||||
constexpr uint8_t KATANA_REG_DEEMPHASIS = 0x06;
|
||||
constexpr uint8_t KATANA_REG_DOP = 0x07;
|
||||
constexpr uint8_t KATANA_REG_FORMAT = 0x08;
|
||||
constexpr uint8_t KATANA_REG_COMMAND = 0x09;
|
||||
constexpr uint8_t KATANA_REG_MUTE_STREAM = 0x0A;
|
||||
|
||||
constexpr float KATANA_VOLUME_MIDPOINT = 0.5f;
|
||||
constexpr float KATANA_VOLUME_MIDPOINT_STEPS = 50.0f; // -25 dB at 50% volume.
|
||||
// Cubic amplitude gain: -20 * log10(volume^3) / 0.5 attenuation steps.
|
||||
constexpr float KATANA_VOLUME_CUBIC_SCALE = 120.0f;
|
||||
constexpr uint8_t KATANA_VOLUME_FULL_ATTENUATION = 255;
|
||||
|
||||
// The control logic ignores writes for this long after a reset.
|
||||
constexpr uint32_t RESET_SETTLE_DELAY_MS = 10;
|
||||
|
||||
const LogString *filter_shape_to_str(FilterShape shape) {
|
||||
switch (shape) {
|
||||
case FILTER_SHAPE_LINEAR_PHASE_FAST:
|
||||
return LOG_STR("linear_phase_fast");
|
||||
case FILTER_SHAPE_LINEAR_PHASE_SLOW:
|
||||
return LOG_STR("linear_phase_slow");
|
||||
case FILTER_SHAPE_MIN_PHASE_FAST:
|
||||
return LOG_STR("minimum_phase_fast");
|
||||
case FILTER_SHAPE_MIN_PHASE_SLOW:
|
||||
return LOG_STR("minimum_phase_slow");
|
||||
case FILTER_SHAPE_APODIZING:
|
||||
return LOG_STR("apodizing");
|
||||
case FILTER_SHAPE_HYBRID:
|
||||
return LOG_STR("hybrid");
|
||||
default:
|
||||
return LOG_STR("brick_wall");
|
||||
}
|
||||
}
|
||||
|
||||
const LogString *deemphasis_to_str(DeemphasisMode mode) {
|
||||
switch (mode) {
|
||||
case DEEMPHASIS_32KHZ:
|
||||
return LOG_STR("32khz");
|
||||
case DEEMPHASIS_44_1KHZ:
|
||||
return LOG_STR("44_1khz");
|
||||
case DEEMPHASIS_48KHZ:
|
||||
return LOG_STR("48khz");
|
||||
default:
|
||||
return LOG_STR("bypass");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ES9038Q2MKatana::setup() {
|
||||
uint8_t chip_id = 0;
|
||||
if (!this->read_byte(KATANA_REG_CHIP_ID, &chip_id)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (chip_id != KATANA_CHIP_ID) {
|
||||
ESP_LOGE(TAG, "Unexpected chip ID 0x%02X", chip_id);
|
||||
this->mark_failed(LOG_STR("Unexpected chip ID"));
|
||||
return;
|
||||
}
|
||||
if (!this->write_byte(KATANA_REG_RESET, 0x01)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
delay(RESET_SETTLE_DELAY_MS);
|
||||
|
||||
// MUTE_STREAM is the gate the Linux driver closes between streams; ESPHome has no such
|
||||
// notion, so it stays open and mute is only ever driven through MUTE.
|
||||
if (!this->write_byte(KATANA_REG_FORMAT, this->format_) || !this->write_byte(KATANA_REG_COMMAND, 0x00) ||
|
||||
!this->write_byte(KATANA_REG_DSP_PROGRAM, this->filter_shape_) ||
|
||||
!this->write_byte(KATANA_REG_DEEMPHASIS, this->deemphasis_mode_) ||
|
||||
!this->write_byte(KATANA_REG_DOP, this->dop_enabled_ ? 1 : 0) || !this->write_volume_() ||
|
||||
!this->write_byte(KATANA_REG_MUTE, this->is_muted_ ? 1 : 0) || !this->write_byte(KATANA_REG_MUTE_STREAM, 0x00)) {
|
||||
this->mark_failed(LOG_STR("Failed to configure the control logic"));
|
||||
}
|
||||
}
|
||||
|
||||
void ES9038Q2MKatana::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"ES9038Q2M Katana:\n"
|
||||
" Filter shape: %s\n"
|
||||
" De-emphasis: %s\n"
|
||||
" Format register: 0x%02X\n"
|
||||
" DoP: %s",
|
||||
LOG_STR_ARG(filter_shape_to_str(this->filter_shape_)),
|
||||
LOG_STR_ARG(deemphasis_to_str(this->deemphasis_mode_)), this->format_, ONOFF(this->dop_enabled_));
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed())
|
||||
return;
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
for (uint8_t reg = KATANA_REG_CHIP_ID; reg <= KATANA_REG_MUTE_STREAM; reg++) {
|
||||
uint8_t value = 0;
|
||||
if (this->read_byte(reg, &value)) {
|
||||
ESP_LOGV(TAG, " Register 0x%02X: 0x%02X", reg, value);
|
||||
} else {
|
||||
ESP_LOGV(TAG, " Register 0x%02X: read failed", reg);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Both channel registers follow ESPHome's single master volume.
|
||||
bool ES9038Q2MKatana::write_volume_() {
|
||||
return this->write_byte(KATANA_REG_VOLUME_1, this->volume_reg_) &&
|
||||
this->write_byte(KATANA_REG_VOLUME_2, this->volume_reg_);
|
||||
}
|
||||
|
||||
// Records the outcome of a runtime write; the cached state only changes on success.
|
||||
bool ES9038Q2MKatana::check_write_(bool ok) {
|
||||
if (ok) {
|
||||
this->status_clear_warning();
|
||||
} else {
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool ES9038Q2MKatana::set_volume(float volume) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
volume = clamp(volume, 0.0f, 1.0f);
|
||||
const uint8_t previous = this->volume_reg_;
|
||||
float attenuation = KATANA_VOLUME_FULL_ATTENUATION;
|
||||
if (volume >= KATANA_VOLUME_MIDPOINT) {
|
||||
attenuation = KATANA_VOLUME_MIDPOINT_STEPS * (1.0f - volume) / (1.0f - KATANA_VOLUME_MIDPOINT);
|
||||
} else if (volume > 0.0f) {
|
||||
attenuation = KATANA_VOLUME_MIDPOINT_STEPS - KATANA_VOLUME_CUBIC_SCALE * log10f(volume / KATANA_VOLUME_MIDPOINT);
|
||||
}
|
||||
this->volume_reg_ =
|
||||
static_cast<uint8_t>(lroundf(clamp(attenuation, 0.0f, static_cast<float>(KATANA_VOLUME_FULL_ATTENUATION))));
|
||||
if (this->check_write_(this->write_volume_()))
|
||||
return true;
|
||||
this->volume_reg_ = previous;
|
||||
return false;
|
||||
}
|
||||
|
||||
float ES9038Q2MKatana::volume() {
|
||||
if (this->volume_reg_ == KATANA_VOLUME_FULL_ATTENUATION)
|
||||
return 0.0f;
|
||||
if (this->volume_reg_ <= KATANA_VOLUME_MIDPOINT_STEPS)
|
||||
return 1.0f - this->volume_reg_ * (1.0f - KATANA_VOLUME_MIDPOINT) / KATANA_VOLUME_MIDPOINT_STEPS;
|
||||
return KATANA_VOLUME_MIDPOINT *
|
||||
powf(10.0f, (KATANA_VOLUME_MIDPOINT_STEPS - this->volume_reg_) / KATANA_VOLUME_CUBIC_SCALE);
|
||||
}
|
||||
|
||||
bool ES9038Q2MKatana::set_mute_state_(bool mute_state) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
if (!this->check_write_(this->write_byte(KATANA_REG_MUTE, mute_state ? 1 : 0)))
|
||||
return false;
|
||||
this->is_muted_ = mute_state;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::es9038q2m_katana
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/audio_dac/audio_dac.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::es9038q2m_katana {
|
||||
|
||||
// DSP program numbers of the Katana control interface; there is no program 5.
|
||||
enum FilterShape : uint8_t {
|
||||
FILTER_SHAPE_LINEAR_PHASE_FAST = 0,
|
||||
FILTER_SHAPE_LINEAR_PHASE_SLOW = 1,
|
||||
FILTER_SHAPE_MIN_PHASE_FAST = 2,
|
||||
FILTER_SHAPE_MIN_PHASE_SLOW = 3,
|
||||
FILTER_SHAPE_APODIZING = 4,
|
||||
FILTER_SHAPE_HYBRID = 6,
|
||||
FILTER_SHAPE_BRICK_WALL = 7,
|
||||
};
|
||||
|
||||
enum DeemphasisMode : uint8_t {
|
||||
DEEMPHASIS_BYPASS = 0,
|
||||
DEEMPHASIS_32KHZ = 1,
|
||||
DEEMPHASIS_44_1KHZ = 2,
|
||||
DEEMPHASIS_48KHZ = 3,
|
||||
};
|
||||
|
||||
// Boards that expose an ES9038Q2M through the Allo Katana control logic at I2C address 0x30.
|
||||
// Ported from the Linux driver sound/soc/bcm/allo-katana-codec.c; the DAC's own registers
|
||||
// are never written, the control logic applies the configuration itself.
|
||||
class ES9038Q2MKatana : public audio_dac::AudioDac, public Component, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
// Cubic below 50%, then 0.5 dB per 1% above, meeting at -25 dB.
|
||||
// Zero selects full attenuation (-127.5 dB).
|
||||
bool set_volume(float volume) override;
|
||||
float volume() override;
|
||||
bool set_mute_off() override { return this->set_mute_state_(false); }
|
||||
bool set_mute_on() override { return this->set_mute_state_(true); }
|
||||
bool is_muted() override { return this->is_muted_; }
|
||||
|
||||
void set_filter_shape(FilterShape shape) { this->filter_shape_ = shape; }
|
||||
void set_format(uint8_t format) { this->format_ = format; }
|
||||
void set_deemphasis_mode(DeemphasisMode mode) { this->deemphasis_mode_ = mode; }
|
||||
void set_dop_enabled(bool enabled) { this->dop_enabled_ = enabled; }
|
||||
|
||||
protected:
|
||||
bool set_mute_state_(bool mute_state);
|
||||
bool write_volume_();
|
||||
bool check_write_(bool ok);
|
||||
|
||||
FilterShape filter_shape_{FILTER_SHAPE_APODIZING};
|
||||
DeemphasisMode deemphasis_mode_{DEEMPHASIS_BYPASS};
|
||||
uint8_t format_{0};
|
||||
// Attenuation in 0.5 dB steps: 0 is loudest, 255 quietest. Starts 20 dB down.
|
||||
uint8_t volume_reg_{0x28};
|
||||
bool dop_enabled_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::es9038q2m_katana
|
||||
@@ -0,0 +1,16 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- audio_dac.mute_off:
|
||||
- audio_dac.mute_on:
|
||||
- audio_dac.set_volume:
|
||||
volume: 50%
|
||||
|
||||
audio_dac:
|
||||
- platform: es9038q2m_katana
|
||||
i2c_id: i2c_bus
|
||||
filter_shape: hybrid
|
||||
bits_per_sample: 24bit
|
||||
sample_rate: 96000
|
||||
deemphasis: 48khz
|
||||
dop: true
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
es9038q2m_katana: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
es9038q2m_katana: !include common.yaml
|
||||
Reference in New Issue
Block a user