Skip to content

Commit

Permalink
[i2s_audio] Add more options to speakers and microphones
Browse files Browse the repository at this point in the history
Now I2S speakers also have the following options that already
exist for microphones:

 * `i2s_mode` (primary/secondary; default: primary)
 * `use_apll` (true/false, default: false)
 * `channel` (left/right/mono/stereo, default: mono)
 * `sample_rate` (default: 16000)
 * `bits_per_sample` (default: 16bit)

Both speakers and microphones also have:

 * `bits_per_channel` (default: equal to `bits_per_sample`)

`channel` has new values:

 * stereo: input/output data is interpreted as a sequence of
   (right, left) pairs (this option works for both speakers
   and microphones);
 * mono: for speakers, this is the old behavior - each sample
   from the input is sent to both left and right channels.

Also, speakers have an extra option:

 * `timeout` (default: 100ms) - how long the background tasks
   should wait before releasing the bus if the buffer is empty.
  • Loading branch information
pyos committed Aug 18, 2024
1 parent 56aa587 commit 764a85c
Show file tree
Hide file tree
Showing 18 changed files with 209 additions and 148 deletions.
71 changes: 70 additions & 1 deletion esphome/components/i2s_audio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import esphome.codegen as cg

from esphome import pins
from esphome.const import CONF_ID
from esphome.const import CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE
from esphome.components.esp32 import get_esp32_variant
from esphome.components.esp32.const import (
VARIANT_ESP32,
Expand All @@ -29,6 +29,14 @@
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"

i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio")
I2SAudioComponent = i2s_audio_ns.class_("I2SAudioComponent", cg.Component)
I2SAudioIn = i2s_audio_ns.class_("I2SAudioIn", cg.Parented.template(I2SAudioComponent))
Expand All @@ -42,6 +50,30 @@
CONF_SECONDARY: i2s_mode_t.I2S_MODE_SLAVE, # NOLINT
}

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,
}

i2s_bits_per_sample_t = cg.global_ns.enum("i2s_bits_per_sample_t")
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,
}

i2s_bits_per_chan_t = cg.global_ns.enum("i2s_bits_per_chan_t")
I2S_BITS_PER_CHANNEL = {
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,
}

# https://github.com/espressif/esp-idf/blob/master/components/soc/{variant}/include/soc/soc_caps.h
I2S_PORTS = {
VARIANT_ESP32: 2,
Expand All @@ -60,6 +92,27 @@
)


def i2s_component_schema(base_schema, klass, default_channel, default_bits_per_sample):
return base_schema.extend(
{
cv.GenerateID(): cv.declare_id(klass),
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
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_CHANNEL, default=default_channel): cv.enum(I2S_CHANNELS),
cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1),
cv.Optional(CONF_BITS_PER_SAMPLE, default=default_bits_per_sample): cv.All(
cv.float_with_unit("bits", "bit"), cv.enum(I2S_BITS_PER_SAMPLE)
),
cv.Optional(CONF_BITS_PER_CHANNEL): cv.All(
cv.float_with_unit("bits", "bit"), cv.enum(I2S_BITS_PER_CHANNEL)
),
}
).extend(cv.COMPONENT_SCHEMA)


def _final_validate(_):
i2s_audio_configs = fv.full_config.get()[CONF_I2S_AUDIO]
variant = get_esp32_variant()
Expand All @@ -83,3 +136,19 @@ async def to_code(config):
cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN]))
if CONF_I2S_MCLK_PIN in config:
cg.add(var.set_mclk_pin(config[CONF_I2S_MCLK_PIN]))


async def register_i2s_component(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_I2S_AUDIO_ID])
cg.add(var.set_i2s_mode(config[CONF_I2S_MODE]))
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]))
bits_per_channel = config.get(
CONF_BITS_PER_CHANNEL, i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_DEFAULT
)
cg.add(var.set_bits_per_channel(bits_per_channel))
cg.add(var.set_use_apll(config[CONF_USE_APLL]))
return var
22 changes: 20 additions & 2 deletions esphome/components/i2s_audio/i2s_audio.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,27 @@ namespace i2s_audio {

class I2SAudioComponent;

class I2SAudioIn : public Parented<I2SAudioComponent> {};
class I2SAudioInOut : public Component, public Parented<I2SAudioComponent> {
public:
void set_i2s_mode(i2s_mode_t mode) { this->i2s_mode_ = mode; }
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 I2SAudioInOut {};

class I2SAudioOut : public Parented<I2SAudioComponent> {};
class I2SAudioOut : public I2SAudioInOut {};

class I2SAudioComponent : public Component {
public:
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
66 changes: 15 additions & 51 deletions esphome/components/i2s_audio/microphone/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,17 @@
import esphome.codegen as cg

from esphome import pins
from esphome.const import CONF_CHANNEL, CONF_ID, CONF_NUMBER, CONF_SAMPLE_RATE
from esphome.const import CONF_NUMBER
from esphome.components import microphone, esp32
from esphome.components.adc import ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, validate_adc_pin

from .. import (
CONF_I2S_MODE,
CONF_PRIMARY,
I2S_MODE_OPTIONS,
i2s_audio_ns,
I2SAudioComponent,
I2SAudioIn,
CONF_I2S_AUDIO_ID,
CONF_I2S_DIN_PIN,
CONF_RIGHT,
I2SAudioIn,
i2s_audio_ns,
i2s_component_schema,
register_i2s_component,
)

CODEOWNERS = ["@jesserockz"]
Expand All @@ -23,29 +21,14 @@
CONF_ADC_PIN = "adc_pin"
CONF_ADC_TYPE = "adc_type"
CONF_PDM = "pdm"
CONF_BITS_PER_SAMPLE = "bits_per_sample"
CONF_USE_APLL = "use_apll"

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

i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t")
CHANNELS = {
"left": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT,
"right": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT,
}
i2s_bits_per_sample_t = cg.global_ns.enum("i2s_bits_per_sample_t")
BITS_PER_SAMPLE = {
16: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_16BIT,
32: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_32BIT,
}

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

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


def validate_esp32_variant(config):
variant = esp32.get_esp32_variant()
Expand All @@ -61,21 +44,12 @@ def validate_esp32_variant(config):
raise NotImplementedError


BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(I2SAudioMicrophone),
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
cv.Optional(CONF_CHANNEL, default="right"): cv.enum(CHANNELS),
cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1),
cv.Optional(CONF_BITS_PER_SAMPLE, default="32bit"): cv.All(
_validate_bits, cv.enum(BITS_PER_SAMPLE)
),
cv.Optional(CONF_USE_APLL, default=False): cv.boolean,
cv.Optional(CONF_I2S_MODE, default=CONF_PRIMARY): cv.enum(
I2S_MODE_OPTIONS, lower=True
),
}
).extend(cv.COMPONENT_SCHEMA)
BASE_SCHEMA = i2s_component_schema(
microphone.MICROPHONE_SCHEMA,
I2SAudioMicrophone,
default_channel=CONF_RIGHT,
default_bits_per_sample="32bit",
)

CONFIG_SCHEMA = cv.All(
cv.typed_schema(
Expand All @@ -88,7 +62,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_PDM, default=False): cv.boolean,
}
),
},
Expand All @@ -99,10 +73,8 @@ def validate_esp32_variant(config):


async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

await cg.register_parented(var, config[CONF_I2S_AUDIO_ID])
var = await register_i2s_component(config)
await microphone.register_microphone(var, config)

if config[CONF_ADC_TYPE] == "internal":
variant = esp32.get_esp32_variant()
Expand All @@ -112,11 +84,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_i2s_mode(config[CONF_I2S_MODE]))
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_use_apll(config[CONF_USE_APLL]))

await microphone.register_microphone(var, config)
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
14 changes: 1 addition & 13 deletions esphome/components/i2s_audio/microphone/i2s_audio_microphone.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
namespace esphome {
namespace i2s_audio {

class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, public Component {
class I2SAudioMicrophone : public microphone::Microphone, public I2SAudioIn {
public:
void setup() override;
void start() override;
Expand All @@ -30,13 +30,6 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
}
#endif

void set_i2s_mode(i2s_mode_t mode) { this->i2s_mode_ = mode; }

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_use_apll(uint32_t use_apll) { this->use_apll_ = use_apll; }

protected:
void start_();
void stop_();
Expand All @@ -48,11 +41,6 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
bool adc_{false};
#endif
bool pdm_{false};
i2s_mode_t i2s_mode_{};
i2s_channel_fmt_t channel_;
uint32_t sample_rate_;
i2s_bits_per_sample_t bits_per_sample_;
bool use_apll_;

HighFrequencyLoopRequester high_freq_;
};
Expand Down
Loading

0 comments on commit 764a85c

Please sign in to comment.