[resampler] Allow resampler to passthrough bits per sample instead of converting (#16892)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Kevin Ahrendt
2026-06-10 18:21:04 -04:00
committed by GitHub
parent cd7e54dbf2
commit 77009cfafe
4 changed files with 63 additions and 20 deletions

View File

@@ -24,6 +24,8 @@ ResamplerSpeaker = resampler_ns.class_(
CONF_TAPS = "taps"
PASSTHROUGH = "passthrough"
def _set_stream_limits(config):
audio.set_stream_limits(
@@ -35,14 +37,21 @@ def _set_stream_limits(config):
def _validate_audio_compatibility(config):
inherit_property_from(CONF_BITS_PER_SAMPLE, CONF_OUTPUT_SPEAKER)(config)
inherit_property_from(CONF_NUM_CHANNELS, CONF_OUTPUT_SPEAKER)(config)
inherit_property_from(CONF_SAMPLE_RATE, CONF_OUTPUT_SPEAKER)(config)
# In passthrough mode the output bits per sample is determined at runtime from the input stream, so there is
# nothing to inherit or validate against the output speaker.
passthrough = config.get(CONF_BITS_PER_SAMPLE) == PASSTHROUGH
if not passthrough:
inherit_property_from(CONF_BITS_PER_SAMPLE, CONF_OUTPUT_SPEAKER)(config)
audio.final_validate_audio_schema(
"source_speaker",
audio_device=CONF_OUTPUT_SPEAKER,
bits_per_sample=config.get(CONF_BITS_PER_SAMPLE),
bits_per_sample=cv.UNDEFINED
if passthrough
else config.get(CONF_BITS_PER_SAMPLE),
channels=config.get(CONF_NUM_CHANNELS),
sample_rate=config.get(CONF_SAMPLE_RATE),
)(config)
@@ -60,6 +69,9 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(ResamplerSpeaker),
cv.Required(CONF_OUTPUT_SPEAKER): cv.use_id(speaker.Speaker),
cv.Optional(CONF_BITS_PER_SAMPLE, default=PASSTHROUGH): cv.Any(
cv.one_of(PASSTHROUGH, lower=True), cv.int_range(8, 32)
),
cv.Optional(
CONF_BUFFER_DURATION, default="100ms"
): cv.positive_time_period_milliseconds,
@@ -90,6 +102,9 @@ async def to_code(config):
cg.add(var.set_task_stack_in_psram(True))
psram.request_external_task_stack()
if config[CONF_BITS_PER_SAMPLE] == PASSTHROUGH:
cg.add(var.set_passthrough_bits_per_sample(True))
else:
cg.add(var.set_target_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
cg.add(var.set_target_sample_rate(config[CONF_SAMPLE_RATE]))

View File

@@ -40,11 +40,19 @@ enum ResamplingEventGroupBits : uint32_t {
};
void ResamplerSpeaker::dump_config() {
if (this->passthrough_bits_per_sample_) {
ESP_LOGCONFIG(TAG,
"Resampler Speaker:\n"
" Target Bits Per Sample: %u\n"
" Target Bits Per Sample: passthrough\n"
" Target Sample Rate: %" PRIu32 " Hz",
this->target_sample_rate_);
} else {
ESP_LOGCONFIG(TAG,
"Resampler Speaker:\n"
" Target Bits Per Sample: %" PRIu8 "\n"
" Target Sample Rate: %" PRIu32 " Hz",
this->target_bits_per_sample_, this->target_sample_rate_);
}
}
void ResamplerSpeaker::setup() {
@@ -253,8 +261,12 @@ void ResamplerSpeaker::send_command_(uint32_t command_bit, bool wake_loop) {
void ResamplerSpeaker::start() { this->send_command_(ResamplingEventGroupBits::COMMAND_START, true); }
esp_err_t ResamplerSpeaker::start_() {
this->target_stream_info_ = audio::AudioStreamInfo(
this->target_bits_per_sample_, this->audio_stream_info_.get_channels(), this->target_sample_rate_);
// In passthrough mode, the output keeps the input's bits per sample so only the sample rate is resampled.
const uint8_t target_bits_per_sample = this->passthrough_bits_per_sample_
? this->audio_stream_info_.get_bits_per_sample()
: this->target_bits_per_sample_;
this->target_stream_info_ = audio::AudioStreamInfo(target_bits_per_sample, this->audio_stream_info_.get_channels(),
this->target_sample_rate_);
this->output_speaker_->set_audio_stream_info(this->target_stream_info_);
this->output_speaker_->start();
@@ -305,7 +317,11 @@ void ResamplerSpeaker::set_volume(float volume) {
}
bool ResamplerSpeaker::requires_resampling_() const {
return (this->audio_stream_info_.get_sample_rate() != this->target_sample_rate_) ||
if (this->audio_stream_info_.get_sample_rate() != this->target_sample_rate_) {
return true;
}
// In passthrough mode the bits per sample always matches the input, so it never forces resampling.
return !this->passthrough_bits_per_sample_ &&
(this->audio_stream_info_.get_bits_per_sample() != this->target_bits_per_sample_);
}

View File

@@ -49,6 +49,12 @@ class ResamplerSpeaker : public Component, public speaker::Speaker {
}
void set_target_sample_rate(uint32_t target_sample_rate) { this->target_sample_rate_ = target_sample_rate; }
/// @brief When enabled, the input bits per sample are passed through to the output speaker unchanged instead of being
/// converted to a fixed target. Only the sample rate is resampled if it differs from the target.
void set_passthrough_bits_per_sample(bool passthrough_bits_per_sample) {
this->passthrough_bits_per_sample_ = passthrough_bits_per_sample;
}
void set_filters(uint16_t filters) { this->filters_ = filters; }
void set_taps(uint16_t taps) { this->taps_ = taps; }
@@ -80,23 +86,24 @@ class ResamplerSpeaker : public Component, public speaker::Speaker {
speaker::Speaker *output_speaker_{nullptr};
bool task_stack_in_psram_{false};
bool waiting_for_output_{false};
StaticTask task_;
audio::AudioStreamInfo target_stream_info_;
uint16_t taps_;
uint16_t filters_;
uint8_t target_bits_per_sample_;
uint32_t target_sample_rate_;
uint64_t callback_remainder_{0};
uint32_t buffer_duration_ms_;
uint32_t state_start_ms_{0};
uint32_t target_sample_rate_;
uint64_t callback_remainder_{0};
uint16_t taps_;
uint16_t filters_;
uint8_t target_bits_per_sample_{0};
bool passthrough_bits_per_sample_{false};
bool task_stack_in_psram_{false};
bool waiting_for_output_{false};
};
} // namespace esphome::resampler

View File

@@ -7,3 +7,8 @@ speaker:
- platform: resampler
id: resampler_speaker_id
output_speaker: resampler_i2s_speaker_id
bits_per_sample: 16
- platform: resampler
id: resampler_speaker_2_id
output_speaker: resampler_speaker_id
bits_per_sample: passthrough