mirror of
https://github.com/esphome/esphome.git
synced 2026-09-18 10:38:38 +00:00
[i2s_audio] Ramp software volume changes (#19302)
This commit is contained in:
@@ -14,17 +14,19 @@
|
||||
|
||||
#include "esp_timer.h"
|
||||
|
||||
// esp-audio-libs
|
||||
#include <gain.h>
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::i2s_audio {
|
||||
|
||||
static const char *const TAG = "i2s_audio.speaker";
|
||||
|
||||
// Software volume control maps the user-facing [0.0, 1.0] range to a Q31 scale factor.
|
||||
// Volumes in (0.0, 1.0) map linearly to a dB reduction in [-49.0, 0.0] dB.
|
||||
// Software volume control maps the user-facing (0.0, 1.0) range linearly to a dB reduction in
|
||||
// [-49.0, 0.0] dB; 0.0 is silence.
|
||||
static constexpr float SOFTWARE_VOLUME_MIN_DB = -49.0f;
|
||||
|
||||
// Rate at which the software gain moves toward a new target.
|
||||
static constexpr uint32_t GAIN_RAMP_MS_PER_DB = 1;
|
||||
|
||||
void I2SAudioSpeakerBase::setup() {
|
||||
this->event_group_ = xEventGroupCreate();
|
||||
|
||||
@@ -34,9 +36,10 @@ void I2SAudioSpeakerBase::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize volume control. When audio_dac is configured, this sets the DAC volume.
|
||||
// Initialize volume control. When audio_dac is configured, this sets the DAC volume and mute state.
|
||||
// When no audio_dac is configured, this initializes software volume control.
|
||||
this->set_volume(this->volume_);
|
||||
this->set_mute_state(this->mute_state_);
|
||||
}
|
||||
|
||||
void I2SAudioSpeakerBase::dump_config() {
|
||||
@@ -136,6 +139,10 @@ void I2SAudioSpeakerBase::loop() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Seed the ramp at the live target so this run adopts it instantly rather than fading to it
|
||||
// from wherever the previous run left off. Posted here, not in the task: the ramp's mailbox
|
||||
// allows one writer, and that is the main loop.
|
||||
this->post_software_gain_(0);
|
||||
xTaskCreate(I2SAudioSpeakerBase::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
|
||||
&this->speaker_task_handle_);
|
||||
|
||||
@@ -153,50 +160,31 @@ void I2SAudioSpeakerBase::loop() {
|
||||
}
|
||||
|
||||
void I2SAudioSpeakerBase::set_volume(float volume) {
|
||||
this->volume_ = volume;
|
||||
#ifdef USE_AUDIO_DAC
|
||||
if (this->audio_dac_ != nullptr) {
|
||||
if (volume > 0.0f) {
|
||||
this->audio_dac_->set_mute_off();
|
||||
}
|
||||
this->audio_dac_->set_volume(volume);
|
||||
} else
|
||||
#endif // USE_AUDIO_DAC
|
||||
{
|
||||
// Fallback to software volume control by using a Q31 fixed point scaling factor.
|
||||
// At maximum volume (1.0), set to INT32_MAX to bypass volume processing entirely
|
||||
// and avoid any floating-point precision issues that could cause slight volume reduction.
|
||||
if (volume >= 1.0f) {
|
||||
this->q31_volume_factor_ = INT32_MAX;
|
||||
} else if (volume <= 0.0f) {
|
||||
this->q31_volume_factor_ = 0;
|
||||
} else {
|
||||
this->q31_volume_factor_ =
|
||||
esp_audio_libs::gain::db_to_q31(remap<float, float>(volume, 0.0f, 1.0f, SOFTWARE_VOLUME_MIN_DB, 0.0f));
|
||||
}
|
||||
}
|
||||
speaker::Speaker::set_volume(volume);
|
||||
this->post_software_gain_(this->audio_stream_info_.ms_to_samples(GAIN_RAMP_MS_PER_DB));
|
||||
}
|
||||
|
||||
void I2SAudioSpeakerBase::set_mute_state(bool mute_state) {
|
||||
this->mute_state_ = mute_state;
|
||||
speaker::Speaker::set_mute_state(mute_state);
|
||||
this->post_software_gain_(this->audio_stream_info_.ms_to_samples(GAIN_RAMP_MS_PER_DB));
|
||||
}
|
||||
|
||||
void I2SAudioSpeakerBase::post_software_gain_(uint32_t rate_samples) {
|
||||
#ifdef USE_AUDIO_DAC
|
||||
if (this->audio_dac_) {
|
||||
if (mute_state) {
|
||||
this->audio_dac_->set_mute_on();
|
||||
} else {
|
||||
this->audio_dac_->set_mute_off();
|
||||
}
|
||||
} else
|
||||
#endif // USE_AUDIO_DAC
|
||||
{
|
||||
if (mute_state) {
|
||||
// Fallback to software volume control and scale by 0
|
||||
this->q31_volume_factor_ = 0;
|
||||
} else {
|
||||
// Revert to previous volume when unmuting
|
||||
this->set_volume(this->volume_);
|
||||
}
|
||||
if (this->audio_dac_ != nullptr) {
|
||||
return; // Hardware volume; the ramp stays at unity
|
||||
}
|
||||
#endif // USE_AUDIO_DAC
|
||||
// Software volume control. The ramp treats 0 dB as unity and skips processing there.
|
||||
float target_db;
|
||||
if (this->mute_state_ || this->volume_ <= 0.0f) {
|
||||
target_db = -INFINITY;
|
||||
} else if (this->volume_ >= 1.0f) {
|
||||
target_db = 0.0f;
|
||||
} else {
|
||||
target_db = remap<float, float>(this->volume_, 0.0f, 1.0f, SOFTWARE_VOLUME_MIN_DB, 0.0f);
|
||||
}
|
||||
this->gain_ramp_.set_target_db_at_rate(target_db, rate_samples);
|
||||
}
|
||||
|
||||
size_t I2SAudioSpeakerBase::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) {
|
||||
@@ -355,14 +343,14 @@ bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s
|
||||
}
|
||||
|
||||
void I2SAudioSpeakerBase::apply_software_volume_(uint8_t *data, size_t bytes_read) {
|
||||
if (this->q31_volume_factor_ == INT32_MAX) {
|
||||
return; // Max volume, no processing needed
|
||||
#ifdef USE_AUDIO_DAC
|
||||
if (this->audio_dac_ != nullptr) {
|
||||
return; // Hardware volume; the ramp is never targeted
|
||||
}
|
||||
|
||||
#endif // USE_AUDIO_DAC
|
||||
const size_t bytes_per_sample = this->current_stream_info_.samples_to_bytes(1);
|
||||
const uint32_t len = bytes_read / bytes_per_sample;
|
||||
|
||||
esp_audio_libs::gain::apply(data, data, this->q31_volume_factor_, len, bytes_per_sample);
|
||||
this->gain_ramp_.process(data, static_cast<uint8_t>(bytes_per_sample),
|
||||
this->current_stream_info_.bytes_to_samples(bytes_read));
|
||||
}
|
||||
|
||||
void I2SAudioSpeakerBase::swap_esp32_mono_samples_(uint8_t *data, size_t bytes_read) {
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <gain.h> // esp-audio-libs
|
||||
|
||||
namespace esphome::i2s_audio {
|
||||
|
||||
// Shared constants used by both standard and SPDIF speaker implementations
|
||||
@@ -77,19 +79,23 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
|
||||
|
||||
bool has_buffered_data() const override;
|
||||
|
||||
/// @brief Sets the volume of the speaker. Uses the speaker's configured audio dac component. If unavailble, it is
|
||||
/// implemented as a software volume control. Overrides the default setter to convert the floating point volume to a
|
||||
/// Q15 fixed-point factor.
|
||||
/// @brief Sets the volume of the speaker. Uses the speaker's configured audio dac component. If unavailable, it is
|
||||
/// implemented as a software volume control. Overrides the default setter to convert the volume to a dB target for
|
||||
/// the gain ramp.
|
||||
/// @param volume between 0.0 and 1.0
|
||||
void set_volume(float volume) override;
|
||||
|
||||
/// @brief Mutes or unmute the speaker. Uses the speaker's configured audio dac component. If unavailble, it is
|
||||
/// implemented as a software volume control. Overrides the default setter to convert the floating point volume to a
|
||||
/// Q15 fixed-point factor.
|
||||
/// @brief Mutes or unmutes the speaker. Uses the speaker's configured audio dac component. If unavailable, it is
|
||||
/// implemented as a software volume control. Overrides the default setter to post the mute state to the gain ramp.
|
||||
/// @param mute_state true for muting, false for unmuting
|
||||
void set_mute_state(bool mute_state) override;
|
||||
|
||||
protected:
|
||||
/// @brief Posts the ramp target derived from the current volume and mute state. No-op when an audio dac owns
|
||||
/// volume. Main loop only.
|
||||
/// @param rate_samples Samples the ramp takes per dB of change; 0 adopts the target at once
|
||||
void post_software_gain_(uint32_t rate_samples);
|
||||
|
||||
/// @brief FreeRTOS task entry point. Casts params to I2SAudioSpeakerBase and calls run_speaker_task_().
|
||||
/// @param params I2SAudioSpeakerBase component pointer
|
||||
static void speaker_task(void *params);
|
||||
@@ -128,7 +134,8 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
|
||||
/// @brief Called in loop() when the task has stopped. Override for mode-specific cleanup.
|
||||
virtual void on_task_stopped() {}
|
||||
|
||||
/// @brief Apply software volume control using Q15 fixed-point scaling.
|
||||
/// @brief Apply software volume control by running the samples through the gain ramp. Called from the
|
||||
/// speaker task only.
|
||||
/// @param data Pointer to audio sample data (modified in place)
|
||||
/// @param bytes_read Number of bytes of audio data
|
||||
void apply_software_volume_(uint8_t *data, size_t bytes_read);
|
||||
@@ -155,7 +162,9 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
|
||||
|
||||
bool pause_state_{false};
|
||||
|
||||
int32_t q31_volume_factor_{INT32_MAX};
|
||||
// Smooths software gain changes. The main loop posts targets, the speaker task processes;
|
||||
// GainRamp's mailbox makes that safe. The main loop is the only poster.
|
||||
esp_audio_libs::gain::GainRamp gain_ramp_;
|
||||
|
||||
audio::AudioStreamInfo current_stream_info_; // Format of the audio in the ring buffer (the I2S input)
|
||||
// Format actually clocked out of the I2S peripheral. Same channel count and sample rate as
|
||||
|
||||
Reference in New Issue
Block a user