Skip to content

Commit

Permalink
[i2s_audio] Add more options to speakers and microphones (esphome#7306)
Browse files Browse the repository at this point in the history
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
  • Loading branch information
pyos and jesserockz authored Sep 11, 2024
1 parent dbecade commit 04248b6
Show file tree
Hide file tree
Showing 18 changed files with 136 additions and 81 deletions.
24 changes: 22 additions & 2 deletions esphome/components/i2s_audio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
CONF_PRIMARY = "primary"
CONF_SECONDARY = "secondary"

CONF_USE_APLL = "use_apll"
CONF_BITS_PER_SAMPLE = "bits_per_sample"
CONF_BITS_PER_CHANNEL = "bits_per_channel"
CONF_MONO = "mono"
CONF_LEFT = "left"
CONF_RIGHT = "right"
CONF_STEREO = "stereo"
Expand Down Expand Up @@ -58,6 +62,7 @@

i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t")
I2S_CHANNELS = {
CONF_MONO: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ALL_LEFT,
CONF_LEFT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT,
CONF_RIGHT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT,
CONF_STEREO: i2s_channel_fmt_t.I2S_CHANNEL_FMT_RIGHT_LEFT,
Expand All @@ -67,17 +72,25 @@
I2S_BITS_PER_SAMPLE = {
8: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_8BIT,
16: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_16BIT,
24: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_24BIT,
32: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_32BIT,
}

INTERNAL_ADC_VARIANTS = [VARIANT_ESP32]
PDM_VARIANTS = [VARIANT_ESP32, VARIANT_ESP32S3]
i2s_bits_per_chan_t = cg.global_ns.enum("i2s_bits_per_chan_t")
I2S_BITS_PER_CHANNEL = {
"default": i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_DEFAULT,
8: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_8BIT,
16: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_16BIT,
24: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_24BIT,
32: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_32BIT,
}

_validate_bits = cv.float_with_unit("bits", "bit")


def i2s_audio_component_schema(
class_: MockObjClass,
*,
default_sample_rate: int,
default_channel: str,
default_bits_per_sample: str,
Expand All @@ -96,6 +109,11 @@ def i2s_audio_component_schema(
cv.Optional(CONF_I2S_MODE, default=CONF_PRIMARY): cv.enum(
I2S_MODE_OPTIONS, lower=True
),
cv.Optional(CONF_USE_APLL, default=False): cv.boolean,
cv.Optional(CONF_BITS_PER_CHANNEL, default="default"): cv.All(
cv.Any(cv.float_with_unit("bits", "bit"), "default"),
cv.enum(I2S_BITS_PER_CHANNEL),
),
}
)

Expand All @@ -107,6 +125,8 @@ async def register_i2s_audio_component(var, config):
cg.add(var.set_channel(config[CONF_CHANNEL]))
cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE]))
cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
cg.add(var.set_bits_per_channel(config[CONF_BITS_PER_CHANNEL]))
cg.add(var.set_use_apll(config[CONF_USE_APLL]))


CONFIG_SCHEMA = cv.Schema(
Expand Down
4 changes: 4 additions & 0 deletions esphome/components/i2s_audio/i2s_audio.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ class I2SAudioBase : public Parented<I2SAudioComponent> {
void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; }
void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; }
void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; }
void set_bits_per_channel(i2s_bits_per_chan_t bits_per_channel) { this->bits_per_channel_ = bits_per_channel; }
void set_use_apll(uint32_t use_apll) { this->use_apll_ = use_apll; }

protected:
i2s_mode_t i2s_mode_{};
i2s_channel_fmt_t channel_;
uint32_t sample_rate_;
i2s_bits_per_sample_t bits_per_sample_;
i2s_bits_per_chan_t bits_per_channel_;
bool use_apll_;
};

class I2SAudioIn : public I2SAudioBase {};
Expand Down
12 changes: 8 additions & 4 deletions esphome/components/i2s_audio/media_player/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
I2SAudioOut,
CONF_I2S_AUDIO_ID,
CONF_I2S_DOUT_PIN,
CONF_LEFT,
CONF_RIGHT,
CONF_MONO,
CONF_STEREO,
)

CODEOWNERS = ["@jesserockz"]
Expand All @@ -30,12 +34,12 @@
CONF_I2S_COMM_FMT = "i2s_comm_fmt"

INTERNAL_DAC_OPTIONS = {
"left": i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN,
"right": i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN,
"stereo": i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN,
CONF_LEFT: i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN,
CONF_RIGHT: i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN,
CONF_STEREO: i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN,
}

EXTERNAL_DAC_OPTIONS = ["mono", "stereo"]
EXTERNAL_DAC_OPTIONS = [CONF_MONO, CONF_STEREO]

NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ enum I2SState : uint8_t {
I2S_STATE_STOPPING,
};

class I2SAudioMediaPlayer : public Component, public media_player::MediaPlayer, public I2SAudioOut {
class I2SAudioMediaPlayer : public Component, public Parented<I2SAudioComponent>, public media_player::MediaPlayer {
public:
void setup() override;
float get_setup_priority() const override { return esphome::setup_priority::LATE; }
Expand Down
19 changes: 11 additions & 8 deletions esphome/components/i2s_audio/microphone/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
from .. import (
CONF_I2S_DIN_PIN,
CONF_RIGHT,
INTERNAL_ADC_VARIANTS,
PDM_VARIANTS,
I2SAudioIn,
i2s_audio_component_schema,
i2s_audio_ns,
Expand All @@ -23,12 +21,13 @@
CONF_ADC_TYPE = "adc_type"
CONF_PDM = "pdm"

CONF_USE_APLL = "use_apll"

I2SAudioMicrophone = i2s_audio_ns.class_(
"I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component
)

INTERNAL_ADC_VARIANTS = [esp32.const.VARIANT_ESP32]
PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3]


def validate_esp32_variant(config):
variant = esp32.get_esp32_variant()
Expand All @@ -45,9 +44,15 @@ def validate_esp32_variant(config):


BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend(
i2s_audio_component_schema(I2SAudioMicrophone, 16000, CONF_RIGHT, "32bit")
i2s_audio_component_schema(
I2SAudioMicrophone,
default_sample_rate=16000,
default_channel=CONF_RIGHT,
default_bits_per_sample="32bit",
)
).extend(cv.COMPONENT_SCHEMA)


CONFIG_SCHEMA = cv.All(
cv.typed_schema(
{
Expand All @@ -59,8 +64,7 @@ def validate_esp32_variant(config):
"external": BASE_SCHEMA.extend(
{
cv.Required(CONF_I2S_DIN_PIN): pins.internal_gpio_input_pin_number,
cv.Required(CONF_PDM): cv.boolean,
cv.Optional(CONF_USE_APLL, default=False): cv.boolean,
cv.Optional(CONF_PDM, default=False): cv.boolean,
}
),
},
Expand All @@ -84,4 +88,3 @@ async def to_code(config):
else:
cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN]))
cg.add(var.set_pdm(config[CONF_PDM]))
cg.add(var.set_use_apll(config[CONF_USE_APLL]))
33 changes: 18 additions & 15 deletions esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ void I2SAudioMicrophone::start_() {
.tx_desc_auto_clear = false,
.fixed_mclk = 0,
.mclk_multiple = I2S_MCLK_MULTIPLE_256,
.bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT,
.bits_per_chan = this->bits_per_channel_,
};

esp_err_t err;
Expand Down Expand Up @@ -167,21 +167,24 @@ size_t I2SAudioMicrophone::read(int16_t *buf, size_t len) {
return 0;
}
this->status_clear_warning();
if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_16BIT) {
return bytes_read;
} else if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_32BIT) {
std::vector<int16_t> samples;
size_t samples_read = bytes_read / sizeof(int32_t);
samples.resize(samples_read);
for (size_t i = 0; i < samples_read; i++) {
int32_t temp = reinterpret_cast<int32_t *>(buf)[i] >> 14;
samples[i] = clamp<int16_t>(temp, INT16_MIN, INT16_MAX);
// ESP-IDF I2S implementation right-extends 8-bit data to 16 bits,
// and 24-bit data to 32 bits.
switch (this->bits_per_sample_) {
case I2S_BITS_PER_SAMPLE_8BIT:
case I2S_BITS_PER_SAMPLE_16BIT:
return bytes_read;
case I2S_BITS_PER_SAMPLE_24BIT:
case I2S_BITS_PER_SAMPLE_32BIT: {
size_t samples_read = bytes_read / sizeof(int32_t);
for (size_t i = 0; i < samples_read; i++) {
int32_t temp = reinterpret_cast<int32_t *>(buf)[i] >> 14;
buf[i] = clamp<int16_t>(temp, INT16_MIN, INT16_MAX);
}
return samples_read * sizeof(int16_t);
}
memcpy(buf, samples.data(), samples_read * sizeof(int16_t));
return samples_read * sizeof(int16_t);
} else {
ESP_LOGE(TAG, "Unsupported bits per sample: %d", this->bits_per_sample_);
return 0;
default:
ESP_LOGE(TAG, "Unsupported bits per sample: %d", this->bits_per_sample_);
return 0;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
}
#endif

void set_use_apll(uint32_t use_apll) { this->use_apll_ = use_apll; }

protected:
void start_();
void stop_();
Expand All @@ -44,8 +42,6 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
#endif
bool pdm_{false};

bool use_apll_;

HighFrequencyLoopRequester high_freq_;
};

Expand Down
32 changes: 26 additions & 6 deletions esphome/components/i2s_audio/speaker/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
import esphome.codegen as cg
from esphome.components import esp32, speaker
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID
from esphome.const import CONF_CHANNEL, CONF_ID, CONF_MODE, CONF_TIMEOUT

from .. import (
CONF_I2S_DOUT_PIN,
CONF_LEFT,
CONF_MONO,
CONF_RIGHT,
CONF_STEREO,
I2SAudioOut,
Expand All @@ -32,7 +33,6 @@
CONF_STEREO: i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN,
}


NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2]


Expand All @@ -45,14 +45,33 @@ def validate_esp32_variant(config):
return config


BASE_SCHEMA = speaker.SPEAKER_SCHEMA.extend(
i2s_audio_component_schema(I2SAudioSpeaker, 16000, "stereo", "16bit")
).extend(cv.COMPONENT_SCHEMA)
BASE_SCHEMA = (
speaker.SPEAKER_SCHEMA.extend(
i2s_audio_component_schema(
I2SAudioSpeaker,
default_sample_rate=16000,
default_channel=CONF_MONO,
default_bits_per_sample="16bit",
)
)
.extend(
{
cv.Optional(
CONF_TIMEOUT, default="100ms"
): cv.positive_time_period_milliseconds,
}
)
.extend(cv.COMPONENT_SCHEMA)
)

CONFIG_SCHEMA = cv.All(
cv.typed_schema(
{
"internal": BASE_SCHEMA,
"internal": BASE_SCHEMA.extend(
{
cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True),
}
),
"external": BASE_SCHEMA.extend(
{
cv.Required(
Expand All @@ -77,3 +96,4 @@ async def to_code(config):
cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL]))
else:
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
cg.add(var.set_timeout(config[CONF_TIMEOUT]))
Loading

0 comments on commit 04248b6

Please sign in to comment.