[mixer] Support any bit depth audio (#16524)

This commit is contained in:
Kevin Ahrendt
2026-05-24 15:32:43 -04:00
committed by GitHub
parent 750d52741a
commit c17c4478ac
4 changed files with 65 additions and 236 deletions
+16 -13
View File
@@ -44,20 +44,10 @@ SOURCE_SPEAKER_SCHEMA = speaker.SPEAKER_SCHEMA.extend(
cv.positive_time_period_milliseconds,
cv.one_of(CONF_NEVER, lower=True),
),
cv.Optional(CONF_BITS_PER_SAMPLE, default=16): cv.int_range(16, 16),
}
)
def _set_stream_limits(config):
audio.set_stream_limits(
min_bits_per_sample=16,
max_bits_per_sample=16,
)(config)
return config
def _validate_source_speaker(config):
fconf = fv.full_config.get()
@@ -67,15 +57,25 @@ def _validate_source_speaker(config):
output_speaker_id = fconf.get_config_for_path(path)
config[CONF_OUTPUT_SPEAKER] = output_speaker_id
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)
audio.final_validate_audio_schema(
"mixer",
audio_device=CONF_OUTPUT_SPEAKER,
sample_rate=config.get(CONF_SAMPLE_RATE),
)(config)
return config
def _validate_output_speaker(config):
audio.final_validate_audio_schema(
"mixer",
audio_device=CONF_OUTPUT_SPEAKER,
bits_per_sample=config.get(CONF_BITS_PER_SAMPLE),
channels=config.get(CONF_NUM_CHANNELS),
sample_rate=config.get(CONF_SAMPLE_RATE),
)(config)
return config
@@ -89,8 +89,8 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_SOURCE_SPEAKERS): cv.All(
cv.ensure_list(SOURCE_SPEAKER_SCHEMA),
cv.Length(min=2, max=8),
[_set_stream_limits],
),
cv.Optional(CONF_BITS_PER_SAMPLE): cv.one_of(8, 16, 24, 32, int=True),
cv.Optional(CONF_NUM_CHANNELS): cv.int_range(min=1, max=2),
cv.Optional(CONF_QUEUE_MODE, default=False): cv.boolean,
cv.Optional(CONF_TASK_STACK_IN_PSRAM, default=False): cv.boolean,
@@ -100,13 +100,15 @@ CONFIG_SCHEMA = cv.All(
)
FINAL_VALIDATE_SCHEMA = cv.All(
inherit_property_from(CONF_BITS_PER_SAMPLE, CONF_OUTPUT_SPEAKER),
inherit_property_from(CONF_NUM_CHANNELS, CONF_OUTPUT_SPEAKER),
cv.Schema(
{
cv.Optional(CONF_SOURCE_SPEAKERS): [_validate_source_speaker],
},
extra=cv.ALLOW_EXTRA,
),
inherit_property_from(CONF_NUM_CHANNELS, CONF_OUTPUT_SPEAKER),
_validate_output_speaker,
)
@@ -116,6 +118,7 @@ async def to_code(config):
spkr = await cg.get_variable(config[CONF_OUTPUT_SPEAKER])
cg.add(var.set_output_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
cg.add(var.set_output_channels(config[CONF_NUM_CHANNELS]))
cg.add(var.set_output_speaker(spkr))
cg.add(var.set_queue_mode(config[CONF_QUEUE_MODE]))
@@ -7,8 +7,10 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <mixer.h> // esp-audio-libs
#include <pcm_convert.h> // esp-audio-libs
#include <algorithm>
#include <array>
#include <cstring>
namespace esphome::mixer_speaker {
@@ -22,19 +24,8 @@ static const uint32_t MIXER_AUTO_STOP_DEBOUNCE_MS = 200;
static const size_t TASK_STACK_SIZE = 4096;
static const int16_t MAX_AUDIO_SAMPLE_VALUE = INT16_MAX;
static const int16_t MIN_AUDIO_SAMPLE_VALUE = INT16_MIN;
static const char *const TAG = "speaker_mixer";
// Gives the Q15 fixed point scaling factor to reduce by 0 dB, 1dB, ..., 50 dB
// dB to PCM scaling factor formula: floating_point_scale_factor = 2^(-db/6.014)
// float to Q15 fixed point formula: q15_scale_factor = floating_point_scale_factor * 2^(15)
static const std::array<int16_t, 51> DECIBEL_REDUCTION_TABLE = {
32767, 29201, 26022, 23189, 20665, 18415, 16410, 14624, 13032, 11613, 10349, 9222, 8218, 7324, 6527, 5816, 5183,
4619, 4116, 3668, 3269, 2913, 2596, 2313, 2061, 1837, 1637, 1459, 1300, 1158, 1032, 920, 820, 731,
651, 580, 517, 461, 411, 366, 326, 291, 259, 231, 206, 183, 163, 146, 130, 116, 103};
// Event bits for SourceSpeaker command processing
enum SourceSpeakerEventBits : uint32_t {
SOURCE_SPEAKER_COMMAND_START = (1 << 0),
@@ -315,97 +306,17 @@ size_t SourceSpeaker::process_data_from_source(std::shared_ptr<audio::RingBuffer
uint32_t samples_to_duck = this->audio_stream_info_.bytes_to_samples(bytes_read);
if (samples_to_duck > 0) {
int16_t *current_buffer = reinterpret_cast<int16_t *>(audio_source->mutable_data());
duck_samples(current_buffer, samples_to_duck, &this->current_ducking_db_reduction_,
&this->ducking_transition_samples_remaining_, this->samples_per_ducking_step_,
this->db_change_per_ducking_step_);
esp_audio_libs::ducking::apply(audio_source->mutable_data(),
static_cast<uint8_t>(this->audio_stream_info_.get_bits_per_sample() / 8),
samples_to_duck, this->ducking_state_);
}
return bytes_read;
}
void SourceSpeaker::apply_ducking(uint8_t decibel_reduction, uint32_t duration) {
if (this->target_ducking_db_reduction_ != decibel_reduction) {
// Start transition from the previous target (which becomes the new current level)
this->current_ducking_db_reduction_ = this->target_ducking_db_reduction_;
this->target_ducking_db_reduction_ = decibel_reduction;
// Calculate the number of intermediate dB steps for the transition timing.
// Subtract 1 because the first step is taken immediately after this calculation.
uint8_t total_ducking_steps = 0;
if (this->target_ducking_db_reduction_ > this->current_ducking_db_reduction_) {
// The dB reduction level is increasing (which results in quieter audio)
total_ducking_steps = this->target_ducking_db_reduction_ - this->current_ducking_db_reduction_ - 1;
this->db_change_per_ducking_step_ = 1;
} else {
// The dB reduction level is decreasing (which results in louder audio)
total_ducking_steps = this->current_ducking_db_reduction_ - this->target_ducking_db_reduction_ - 1;
this->db_change_per_ducking_step_ = -1;
}
if ((duration > 0) && (total_ducking_steps > 0)) {
this->ducking_transition_samples_remaining_ = this->audio_stream_info_.ms_to_samples(duration);
this->samples_per_ducking_step_ = this->ducking_transition_samples_remaining_ / total_ducking_steps;
this->ducking_transition_samples_remaining_ =
this->samples_per_ducking_step_ * total_ducking_steps; // adjust for integer division rounding
this->current_ducking_db_reduction_ += this->db_change_per_ducking_step_;
} else {
this->ducking_transition_samples_remaining_ = 0;
this->current_ducking_db_reduction_ = this->target_ducking_db_reduction_;
}
}
}
void SourceSpeaker::duck_samples(int16_t *input_buffer, uint32_t input_samples_to_duck,
int8_t *current_ducking_db_reduction, uint32_t *ducking_transition_samples_remaining,
uint32_t samples_per_ducking_step, int8_t db_change_per_ducking_step) {
if (*ducking_transition_samples_remaining > 0) {
// Ducking level is still transitioning
// Takes the ceiling of input_samples_to_duck/samples_per_ducking_step
uint32_t ducking_steps_in_batch =
input_samples_to_duck / samples_per_ducking_step + (input_samples_to_duck % samples_per_ducking_step != 0);
for (uint32_t i = 0; i < ducking_steps_in_batch; ++i) {
uint32_t samples_left_in_step = *ducking_transition_samples_remaining % samples_per_ducking_step;
if (samples_left_in_step == 0) {
samples_left_in_step = samples_per_ducking_step;
}
uint32_t samples_to_duck = std::min(input_samples_to_duck, samples_left_in_step);
samples_to_duck = std::min(samples_to_duck, *ducking_transition_samples_remaining);
// Ensure we only point to valid index in the Q15 scaling factor table
uint8_t safe_db_reduction_index =
clamp<uint8_t>(*current_ducking_db_reduction, 0, DECIBEL_REDUCTION_TABLE.size() - 1);
int16_t q15_scale_factor = DECIBEL_REDUCTION_TABLE[safe_db_reduction_index];
audio::scale_audio_samples(input_buffer, input_buffer, q15_scale_factor, samples_to_duck);
if (samples_left_in_step - samples_to_duck == 0) {
// After scaling the current samples, we are ready to transition to the next step
*current_ducking_db_reduction += db_change_per_ducking_step;
}
input_buffer += samples_to_duck;
*ducking_transition_samples_remaining -= samples_to_duck;
input_samples_to_duck -= samples_to_duck;
}
}
if ((*current_ducking_db_reduction > 0) && (input_samples_to_duck > 0)) {
// Audio is ducked, but its not in the middle of a transition step
uint8_t safe_db_reduction_index =
clamp<uint8_t>(*current_ducking_db_reduction, 0, DECIBEL_REDUCTION_TABLE.size() - 1);
int16_t q15_scale_factor = DECIBEL_REDUCTION_TABLE[safe_db_reduction_index];
audio::scale_audio_samples(input_buffer, input_buffer, q15_scale_factor, input_samples_to_duck);
}
const uint32_t transition_samples = duration > 0 ? this->audio_stream_info_.ms_to_samples(duration) : 0;
esp_audio_libs::ducking::set_target(this->ducking_state_, decibel_reduction, transition_samples);
}
void SourceSpeaker::enter_stopping_state_() {
@@ -417,8 +328,9 @@ void SourceSpeaker::enter_stopping_state_() {
void MixerSpeaker::dump_config() {
ESP_LOGCONFIG(TAG,
"Speaker Mixer:\n"
" Number of output channels: %u",
this->output_channels_);
" Number of output channels: %" PRIu8 "\n"
" Output bits per sample: %" PRIu8,
this->output_channels_, this->output_bits_per_sample_);
}
void MixerSpeaker::setup() {
@@ -512,13 +424,8 @@ void MixerSpeaker::loop() {
esp_err_t MixerSpeaker::start(audio::AudioStreamInfo &stream_info) {
if (!this->audio_stream_info_.has_value()) {
if (stream_info.get_bits_per_sample() != 16) {
// Audio streams that don't have 16 bits per sample are not supported
return ESP_ERR_NOT_SUPPORTED;
}
this->audio_stream_info_ = audio::AudioStreamInfo(stream_info.get_bits_per_sample(), this->output_channels_,
stream_info.get_sample_rate());
this->audio_stream_info_ =
audio::AudioStreamInfo(this->output_bits_per_sample_, this->output_channels_, stream_info.get_sample_rate());
this->output_speaker_->set_audio_stream_info(this->audio_stream_info_.value());
} else {
if (!this->queue_mode_ && (stream_info.get_sample_rate() != this->audio_stream_info_.value().get_sample_rate())) {
@@ -542,57 +449,6 @@ esp_err_t MixerSpeaker::start(audio::AudioStreamInfo &stream_info) {
return ESP_OK;
}
void MixerSpeaker::copy_frames(const int16_t *input_buffer, audio::AudioStreamInfo input_stream_info,
int16_t *output_buffer, audio::AudioStreamInfo output_stream_info,
uint32_t frames_to_transfer) {
uint8_t input_channels = input_stream_info.get_channels();
uint8_t output_channels = output_stream_info.get_channels();
const uint8_t max_input_channel_index = input_channels - 1;
if (input_channels == output_channels) {
size_t bytes_to_copy = input_stream_info.frames_to_bytes(frames_to_transfer);
memcpy(output_buffer, input_buffer, bytes_to_copy);
return;
}
for (uint32_t frame_index = 0; frame_index < frames_to_transfer; ++frame_index) {
for (uint8_t output_channel_index = 0; output_channel_index < output_channels; ++output_channel_index) {
uint8_t input_channel_index = std::min(output_channel_index, max_input_channel_index);
output_buffer[output_channels * frame_index + output_channel_index] =
input_buffer[input_channels * frame_index + input_channel_index];
}
}
}
void MixerSpeaker::mix_audio_samples(const int16_t *primary_buffer, audio::AudioStreamInfo primary_stream_info,
const int16_t *secondary_buffer, audio::AudioStreamInfo secondary_stream_info,
int16_t *output_buffer, audio::AudioStreamInfo output_stream_info,
uint32_t frames_to_mix) {
const uint8_t primary_channels = primary_stream_info.get_channels();
const uint8_t secondary_channels = secondary_stream_info.get_channels();
const uint8_t output_channels = output_stream_info.get_channels();
const uint8_t max_primary_channel_index = primary_channels - 1;
const uint8_t max_secondary_channel_index = secondary_channels - 1;
for (uint32_t frames_index = 0; frames_index < frames_to_mix; ++frames_index) {
for (uint8_t output_channel_index = 0; output_channel_index < output_channels; ++output_channel_index) {
const uint32_t secondary_channel_index = std::min(output_channel_index, max_secondary_channel_index);
const int32_t secondary_sample = secondary_buffer[frames_index * secondary_channels + secondary_channel_index];
const uint32_t primary_channel_index = std::min(output_channel_index, max_primary_channel_index);
const int32_t primary_sample =
static_cast<int32_t>(primary_buffer[frames_index * primary_channels + primary_channel_index]);
const int32_t added_sample = secondary_sample + primary_sample;
output_buffer[frames_index * output_channels + output_channel_index] =
static_cast<int16_t>(clamp<int32_t>(added_sample, MIN_AUDIO_SAMPLE_VALUE, MAX_AUDIO_SAMPLE_VALUE));
}
}
}
// NOLINTBEGIN(bugprone-unchecked-optional-access) -- audio_stream_info_ always set before this task is created
void MixerSpeaker::audio_mixer_task(void *params) {
MixerSpeaker *this_mixer = static_cast<MixerSpeaker *>(params);
@@ -662,6 +518,10 @@ void MixerSpeaker::audio_mixer_task(void *params) {
uint32_t frames_to_mix = output_frames_free;
const audio::AudioStreamInfo &output_info = this_mixer->audio_stream_info_.value();
const uint8_t output_bps = output_info.get_bits_per_sample() / 8;
const uint8_t output_channels = output_info.get_channels();
if ((audio_sources_with_data.size() == 1) || this_mixer->queue_mode_) {
// Only one speaker has audio data, just copy samples over
@@ -669,14 +529,15 @@ void MixerSpeaker::audio_mixer_task(void *params) {
if (active_stream_info.get_sample_rate() ==
this_mixer->output_speaker_->get_audio_stream_info().get_sample_rate()) {
// Speaker's sample rate matches the output speaker's, copy directly
// Speaker's sample rate matches the output speaker's, convert directly into the output buffer
const uint32_t frames_available_in_buffer =
active_stream_info.bytes_to_frames(audio_sources_with_data[0]->available());
frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer);
copy_frames(reinterpret_cast<const int16_t *>(audio_sources_with_data[0]->data()), active_stream_info,
reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()),
this_mixer->audio_stream_info_.value(), frames_to_mix);
esp_audio_libs::pcm_convert::copy_frames(
audio_sources_with_data[0]->data(), output_transfer_buffer->get_buffer_end(),
static_cast<uint8_t>(active_stream_info.get_bits_per_sample() / 8), active_stream_info.get_channels(),
output_bps, output_channels, frames_to_mix);
// Set playback delay for newly contributing source
if (!speakers_with_data[0]->has_contributed_.load(std::memory_order_acquire)) {
@@ -690,8 +551,7 @@ void MixerSpeaker::audio_mixer_task(void *params) {
audio_sources_with_data[0]->consume(active_stream_info.frames_to_bytes(frames_to_mix));
// Update output transfer buffer length and pipeline frame count
output_transfer_buffer->increase_buffer_length(
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix));
output_transfer_buffer->increase_buffer_length(output_info.frames_to_bytes(frames_to_mix));
this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release);
} else {
// Speaker's stream info doesn't match the output speaker's, so it's a new source speaker
@@ -703,7 +563,7 @@ void MixerSpeaker::audio_mixer_task(void *params) {
} else {
// Speaker has finished writing the current audio, update the stream information and restart the speaker
this_mixer->audio_stream_info_ =
audio::AudioStreamInfo(active_stream_info.get_bits_per_sample(), this_mixer->output_channels_,
audio::AudioStreamInfo(this_mixer->output_bits_per_sample_, this_mixer->output_channels_,
active_stream_info.get_sample_rate());
this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value());
this_mixer->output_speaker_->start();
@@ -719,21 +579,22 @@ void MixerSpeaker::audio_mixer_task(void *params) {
speakers_with_data[i]->get_audio_stream_info().bytes_to_frames(audio_sources_with_data[i]->available());
frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer);
}
const int16_t *primary_buffer = reinterpret_cast<const int16_t *>(audio_sources_with_data[0]->data());
const uint8_t *primary_buffer = audio_sources_with_data[0]->data();
audio::AudioStreamInfo primary_stream_info = speakers_with_data[0]->get_audio_stream_info();
// Mix two streams together
// Mix two streams together at a time, accumulating into the output buffer.
for (size_t i = 1; i < audio_sources_with_data.size(); ++i) {
mix_audio_samples(primary_buffer, primary_stream_info,
reinterpret_cast<const int16_t *>(audio_sources_with_data[i]->data()),
speakers_with_data[i]->get_audio_stream_info(),
reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()),
this_mixer->audio_stream_info_.value(), frames_to_mix);
esp_audio_libs::mixer::mix_frames(
primary_buffer, static_cast<uint8_t>(primary_stream_info.get_bits_per_sample() / 8),
primary_stream_info.get_channels(), audio_sources_with_data[i]->data(),
static_cast<uint8_t>(speakers_with_data[i]->get_audio_stream_info().get_bits_per_sample() / 8),
speakers_with_data[i]->get_audio_stream_info().get_channels(), output_transfer_buffer->get_buffer_end(),
output_bps, output_channels, frames_to_mix);
if (i != audio_sources_with_data.size() - 1) {
// Need to mix more streams together, point primary buffer and stream info to the already mixed output
primary_buffer = reinterpret_cast<const int16_t *>(output_transfer_buffer->get_buffer_end());
primary_stream_info = this_mixer->audio_stream_info_.value();
primary_buffer = output_transfer_buffer->get_buffer_end();
primary_stream_info = output_info;
}
}
@@ -754,8 +615,7 @@ void MixerSpeaker::audio_mixer_task(void *params) {
}
// Update output transfer buffer length and pipeline frame count (once, not per source)
output_transfer_buffer->increase_buffer_length(
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix));
output_transfer_buffer->increase_buffer_length(output_info.frames_to_bytes(frames_to_mix));
this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release);
}
}
@@ -11,6 +11,8 @@
#include "esphome/core/helpers.h"
#include "esphome/core/static_task.h"
#include <ducking.h> // esp-audio-libs
#include <freertos/event_groups.h>
#include <atomic>
@@ -22,7 +24,8 @@ namespace esphome::mixer_speaker {
* - Source speaker commands are signaled via event group bits and processed in its loop function to ensure thread
* safety
* - Directly handles pausing at the SourceSpeaker level; pause state is not passed through to the output speaker.
* - Audio sent to the SourceSpeaker must have 16 bits per sample.
* - Audio sent to the SourceSpeaker can have 8, 16, 24, or 32 bits per sample. Each source is converted to the output
* speaker's bit depth as it is mixed (or copied) into the output buffer.
* - Audio sent to the SourceSpeaker can have any number of channels. They are duplicated or ignored as needed to match
* the number of channels required for the output speaker.
* - In queue mode, the audio sent to the SourceSpeakers can have different sample rates.
@@ -93,19 +96,6 @@ class SourceSpeaker : public speaker::Speaker, public Component {
void enter_stopping_state_();
void send_command_(uint32_t command_bit, bool wake_loop = false);
/// @brief Ducks audio samples by a specified amount. When changing the ducking amount, it can transition gradually
/// over a specified amount of samples.
/// @param input_buffer buffer with audio samples to be ducked in place
/// @param input_samples_to_duck number of samples to process in ``input_buffer``
/// @param current_ducking_db_reduction pointer to the current dB reduction
/// @param ducking_transition_samples_remaining pointer to the total number of samples left before the
/// transition is finished
/// @param samples_per_ducking_step total number of samples per ducking step for the transition
/// @param db_change_per_ducking_step the change in dB reduction per step
static void duck_samples(int16_t *input_buffer, uint32_t input_samples_to_duck, int8_t *current_ducking_db_reduction,
uint32_t *ducking_transition_samples_remaining, uint32_t samples_per_ducking_step,
int8_t db_change_per_ducking_step);
MixerSpeaker *parent_;
std::shared_ptr<audio::RingBufferAudioSource> audio_source_;
@@ -118,11 +108,7 @@ class SourceSpeaker : public speaker::Speaker, public Component {
bool pause_state_{false};
int8_t target_ducking_db_reduction_{0};
int8_t current_ducking_db_reduction_{0};
int8_t db_change_per_ducking_step_{1};
uint32_t ducking_transition_samples_remaining_{0};
uint32_t samples_per_ducking_step_{0};
esp_audio_libs::ducking::DuckingState ducking_state_{};
std::atomic<uint32_t> pending_playback_frames_{0};
std::atomic<uint32_t> playback_delay_frames_{0}; // Frames in output pipeline when this source started contributing
@@ -143,12 +129,14 @@ class MixerSpeaker : public Component {
/// @brief Starts the mixer task. Called by a source speaker giving the current audio stream information
/// @param stream_info The calling source speaker's audio stream information
/// @return ESP_ERR_NOT_SUPPORTED if the incoming stream is incompatible due to unsupported bits per sample
/// ESP_ERR_INVALID_ARG if the incoming stream is incompatible to be mixed with the other input audio stream
/// @return ESP_ERR_INVALID_ARG if the incoming stream is incompatible to be mixed with the other input audio stream
/// ESP_OK if the incoming stream is compatible and the mixer task starts
esp_err_t start(audio::AudioStreamInfo &stream_info);
void set_output_channels(uint8_t output_channels) { this->output_channels_ = output_channels; }
void set_output_bits_per_sample(uint8_t output_bits_per_sample) {
this->output_bits_per_sample_ = output_bits_per_sample;
}
void set_output_speaker(speaker::Speaker *speaker) { this->output_speaker_ = speaker; }
void set_queue_mode(bool queue_mode) { this->queue_mode_ = queue_mode; }
void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; }
@@ -159,33 +147,6 @@ class MixerSpeaker : public Component {
uint32_t get_frames_in_pipeline() const { return this->frames_in_pipeline_.load(std::memory_order_acquire); }
protected:
/// @brief Copies audio frames from the input buffer to the output buffer taking into account the number of channels
/// in each stream. If the output stream has more channels, the input samples are duplicated. If the output stream has
/// less channels, the extra channel input samples are dropped.
/// @param input_buffer
/// @param input_stream_info
/// @param output_buffer
/// @param output_stream_info
/// @param frames_to_transfer number of frames (consisting of a sample for each channel) to copy from the input buffer
static void copy_frames(const int16_t *input_buffer, audio::AudioStreamInfo input_stream_info, int16_t *output_buffer,
audio::AudioStreamInfo output_stream_info, uint32_t frames_to_transfer);
/// @brief Mixes the primary and secondary streams taking into account the number of channels in each stream. Primary
/// and secondary samples are duplicated or dropped as necessary to ensure the output stream has the configured number
/// of channels. Output samples are clamped to the corresponding int16 min or max values if the mixed sample
/// overflows.
/// @param primary_buffer samples buffer for the primary stream
/// @param primary_stream_info stream info for the primary stream
/// @param secondary_buffer samples buffer for secondary stream
/// @param secondary_stream_info stream info for the secondary stream
/// @param output_buffer buffer for the mixed samples
/// @param output_stream_info stream info for the output buffer
/// @param frames_to_mix number of frames in the primary and secondary buffers to mix together
static void mix_audio_samples(const int16_t *primary_buffer, audio::AudioStreamInfo primary_stream_info,
const int16_t *secondary_buffer, audio::AudioStreamInfo secondary_stream_info,
int16_t *output_buffer, audio::AudioStreamInfo output_stream_info,
uint32_t frames_to_mix);
static void audio_mixer_task(void *params);
EventGroupHandle_t event_group_{nullptr};
@@ -193,6 +154,7 @@ class MixerSpeaker : public Component {
FixedVector<SourceSpeaker *> source_speakers_;
speaker::Speaker *output_speaker_{nullptr};
uint8_t output_bits_per_sample_;
uint8_t output_channels_;
bool queue_mode_;
bool task_stack_in_psram_{false};
+4
View File
@@ -16,8 +16,12 @@ speaker:
id: speaker_id
dac_type: external
i2s_dout_pin: ${dout_pin}
bits_per_sample: 32bit
channel: stereo
- platform: mixer
output_speaker: speaker_id
bits_per_sample: 32
num_channels: 2
source_speakers:
- id: source_speaker_1_id
- id: source_speaker_2_id