Compare commits

...
Author SHA1 Message Date
Kevin Ahrendt 2f9b38976c [microphone] Test the deprecated call(data) publisher 2026-10-26 10:56:31 -04:00
Kevin Ahrendt af8b53e8c4 [microphone] Test the deprecated vector data callback 2026-10-26 10:55:03 -04:00
Kevin Ahrendt f2c3174cb7 [microphone] Add data callback tests 2026-10-26 10:41:38 -04:00
Kevin Ahrendt f2883123b5 [microphone] Trim comments
The longer comments were too verbose, and the developer docs cover the details.
2026-10-26 10:39:29 -04:00
Kevin Ahrendt 20649644f5 [microphone] Pass each audio buffer as an AudioCapture
Data callbacks receive a microphone::AudioCapture with the audio as a span, its
format, and the capture time of its first frame. Carrying the format with each
buffer removes the need to read get_audio_stream_info() from the audio thread.
Callbacks that take a std::vector still work but are deprecated. Microphone
platforms publish through publish_audio_(); calling data_callbacks_.call(data)
directly is deprecated. on_data still gives x as a std::vector.
2026-10-26 09:56:28 -04:00
14 changed files with 279 additions and 47 deletions

No files matched your search

@@ -270,21 +270,23 @@ void I2SAudioMicrophone::mic_task(void *params) {
{ // Ensures the samples vector is freed when the task stops
const size_t bytes_to_read = this_microphone->audio_stream_info_.ms_to_bytes(READ_DURATION_MS);
// Copied so the format cannot change while the task runs
const audio::AudioStreamInfo stream_info = this_microphone->audio_stream_info_;
const size_t bytes_to_read = stream_info.ms_to_bytes(READ_DURATION_MS);
std::vector<uint8_t> samples;
samples.reserve(bytes_to_read);
xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_RUNNING);
while (!(xEventGroupGetBits(this_microphone->event_group_) & MicrophoneEventGroupBits::COMMAND_STOP)) {
if (this_microphone->data_callbacks_.size() > 0) {
if (this_microphone->has_data_callbacks_()) {
samples.resize(bytes_to_read);
size_t bytes_read = this_microphone->read_(samples.data(), bytes_to_read, 2 * READ_DURATION_MS);
samples.resize(bytes_read);
if (this_microphone->correct_dc_offset_) {
this_microphone->fix_dc_offset_(samples);
}
this_microphone->data_callbacks_.call(samples);
this_microphone->publish_audio_(samples, stream_info);
} else {
vTaskDelay(pdMS_TO_TICKS(READ_DURATION_MS));
}
@@ -132,7 +132,7 @@ void MicroWakeWord::setup() {
return;
}
this->microphone_source_->add_data_callback([this](const std::vector<uint8_t> &data) {
this->microphone_source_->add_data_callback([this](const microphone::AudioCapture &capture) {
if (this->state_ == State::STOPPED) {
return;
}
@@ -141,7 +141,7 @@ void MicroWakeWord::setup() {
// Producer-only write: never touches consumer state. If the buffer is full, ask the inference task
// to drain it - reset() is a consumer operation and must run on the inference task's thread.
// Disable partial writes so audio chunks are either fully accepted or rejected and handled below.
if (temp_ring_buffer->write_without_replacement(data.data(), data.size(), 0, false) == 0) {
if (temp_ring_buffer->write_without_replacement(capture.data.data(), capture.data.size(), 0, false) == 0) {
xEventGroupSetBits(this->event_group_,
EventGroupBits::WARNING_FULL_RING_BUFFER | EventGroupBits::COMMAND_RESET_RING_BUFFER);
}
+13 -4
View File
@@ -12,6 +12,7 @@ from esphome.const import (
CONF_ID,
CONF_MICROPHONE,
CONF_ON_DATA,
CONF_TRIGGER_ID,
)
from esphome.core import CORE
from esphome.coroutine import CoroPriority, coroutine_with_priority
@@ -28,13 +29,19 @@ microphone_ns = cg.esphome_ns.namespace("microphone")
Microphone = microphone_ns.class_("Microphone")
MicrophoneSource = microphone_ns.class_("MicrophoneSource")
DataTrigger = microphone_ns.class_(
"DataTrigger",
automation.Trigger.template(
cg.std_vector.template(cg.uint8).operator("ref").operator("const")
),
)
async def setup_microphone_core_(var: MockObj, config: ConfigType) -> None:
for conf in config.get(CONF_ON_DATA, []):
await automation.build_callback_automation(
var,
"add_data_callback",
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger,
[(cg.std_vector.template(cg.uint8).operator("ref").operator("const"), "x")],
conf,
)
@@ -48,7 +55,9 @@ async def register_microphone(var: MockObj, config: ConfigType) -> None:
MICROPHONE_SCHEMA = cv.Schema.extend(audio.AUDIO_COMPONENT_SCHEMA).extend(
{
cv.Optional(CONF_ON_DATA): automation.validate_automation({}),
cv.Optional(CONF_ON_DATA): automation.validate_automation(
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DataTrigger)}
),
}
)
@@ -0,0 +1,25 @@
#pragma once
#include "esphome/core/automation.h"
#include "microphone.h"
#include <cstdint>
#include <vector>
namespace esphome::microphone {
/// Reuses one vector so `x` stays valid in automations with a delay.
class DataTrigger final : public Trigger<const std::vector<uint8_t> &> {
public:
explicit DataTrigger(Microphone *parent) {
parent->add_data_callback([this](const AudioCapture &capture) {
this->data_.assign(capture.data.begin(), capture.data.end());
this->trigger(this->data_);
});
}
protected:
std::vector<uint8_t> data_;
};
} // namespace esphome::microphone
+69 -8
View File
@@ -4,6 +4,9 @@
#include <cstddef>
#include <cstdint>
#include <span>
#include <type_traits>
#include <utility>
#include <vector>
#include "esphome/core/helpers.h"
@@ -16,18 +19,68 @@ enum State : uint8_t {
STATE_STOPPING,
};
struct AudioCapture {
/// Only valid during the callback
std::span<const uint8_t> data;
/// Format of data
audio::AudioStreamInfo stream_info;
/// esp_timer_get_time() of the first frame, or 0 if unknown
int64_t capture_time_us{0};
};
// Remove before 2027.5.0. A template, so it only warns for the callbacks that use it.
template<typename F>
ESPDEPRECATED("Take const microphone::AudioCapture & in data callbacks instead. Removed in 2027.5.0", "2026.11.0")
constexpr void warn_vector_data_callback() {}
template<typename F> auto as_capture_callback(F &&callback) {
using Fn = std::decay_t<F>;
if constexpr (std::is_invocable_v<const Fn &, const AudioCapture &>) {
return Fn(std::forward<F>(callback));
} else {
// Remove before 2027.5.0
static_assert(std::is_invocable_v<const Fn &, const std::vector<uint8_t> &>,
"A data callback takes (const microphone::AudioCapture &)");
warn_vector_data_callback<Fn>();
return [callback = Fn(std::forward<F>(callback))](const AudioCapture &capture) {
const std::vector<uint8_t> copy(capture.data.begin(), capture.data.end());
callback(copy);
};
}
}
using DataCallbackManagerBase = CallbackManager<void(const AudioCapture &)>;
class DataCallbackManager : public DataCallbackManagerBase {
public:
explicit DataCallbackManager(const audio::AudioStreamInfo *stream_info) : stream_info_(stream_info) {}
using DataCallbackManagerBase::call;
// Remove before 2027.5.0, along with this class (use a plain CallbackManager)
ESPDEPRECATED("Use Microphone::publish_audio_() instead. Removed in 2027.5.0", "2026.11.0")
void call(const std::vector<uint8_t> &data) { this->call(AudioCapture{data, *this->stream_info_, 0}); }
protected:
const audio::AudioStreamInfo *stream_info_;
};
class Microphone {
public:
virtual void start() = 0;
virtual void stop() = 0;
/// Callbacks take (const AudioCapture &) and run on the microphone's task.
template<typename F> void add_data_callback(F &&data_callback) {
this->data_callbacks_.add([this, data_callback](const std::vector<uint8_t> &data) {
if (this->mute_state_) {
data_callback(std::vector<uint8_t>(data.size(), 0));
} else {
data_callback(data);
}
});
this->data_callbacks_.add(
[this, callback = as_capture_callback(std::forward<F>(data_callback))](const AudioCapture &capture) {
if (this->mute_state_) {
const std::vector<uint8_t> zeros(capture.data.size(), 0);
callback(AudioCapture{zeros, capture.stream_info, capture.capture_time_us});
} else {
callback(capture);
}
});
}
bool is_running() const { return this->state_ == STATE_RUNNING; }
@@ -39,12 +92,20 @@ class Microphone {
audio::AudioStreamInfo get_audio_stream_info() { return this->audio_stream_info_; }
protected:
void publish_audio_(std::span<const uint8_t> data, const audio::AudioStreamInfo &stream_info,
int64_t capture_time_us = 0) {
this->data_callbacks_.call(AudioCapture{data, stream_info, capture_time_us});
}
bool has_data_callbacks_() const { return this->data_callbacks_.size() > 0; }
State state_{STATE_STOPPED};
bool mute_state_{false};
audio::AudioStreamInfo audio_stream_info_;
CallbackManager<void(const std::vector<uint8_t> &)> data_callbacks_{};
// Make private when the deprecated call(data) is removed in 2027.5.0
DataCallbackManager data_callbacks_{&this->audio_stream_info_};
};
} // namespace esphome::microphone
@@ -36,8 +36,8 @@ void MicrophoneSource::stop() {
}
}
void MicrophoneSource::process_audio_(const std::vector<uint8_t> &data, std::vector<uint8_t> &filtered_data) {
const audio::AudioStreamInfo source_info = this->mic_->get_audio_stream_info();
void MicrophoneSource::process_audio_(std::span<const uint8_t> data, const audio::AudioStreamInfo &source_info,
std::vector<uint8_t> &filtered_data) {
const uint32_t total_frames = source_info.bytes_to_frames(data.size());
const uint8_t target_bytes_per_sample = (this->bits_per_sample_ + 7) / 8;
@@ -7,6 +7,7 @@
#include <bitset>
#include <cstddef>
#include <cstdint>
#include <span>
#include <vector>
namespace esphome::microphone {
@@ -45,20 +46,25 @@ class MicrophoneSource final {
/// @param channel 0-indexed channel number to enable
void add_channel(uint8_t channel) { this->channels_.set(channel); }
/// Same callbacks as Microphone::add_data_callback; stream_info is the processed format.
template<typename F> void add_data_callback(F &&data_callback) {
this->mic_->add_data_callback([this, data_callback](const std::vector<uint8_t> &data) {
if (this->enabled_ || this->passive_) {
if (this->processed_samples_ == nullptr) {
// Create vector if its unused
this->processed_samples_ = std::make_shared<std::vector<uint8_t>>();
}
this->mic_->add_data_callback(
[this, callback = as_capture_callback(std::forward<F>(data_callback))](const AudioCapture &capture) {
if (this->enabled_ || this->passive_) {
if (this->processed_samples_ == nullptr) {
// Create vector if its unused
this->processed_samples_ = std::make_shared<std::vector<uint8_t>>();
}
// Take temporary ownership of samples vector to avoid deallocation before the callback finishes
std::shared_ptr<std::vector<uint8_t>> output_samples = this->processed_samples_;
this->process_audio_(data, *output_samples);
data_callback(*output_samples);
}
});
// Take temporary ownership of samples vector to avoid deallocation before the callback finishes
std::shared_ptr<std::vector<uint8_t>> output_samples = this->processed_samples_;
this->process_audio_(capture.data, capture.stream_info, *output_samples);
callback(AudioCapture{*output_samples,
audio::AudioStreamInfo(this->bits_per_sample_, this->channels_.count(),
capture.stream_info.get_sample_rate()),
capture.capture_time_us});
}
});
}
void set_gain_factor(int32_t gain_factor) { this->gain_factor_ = clamp<int32_t>(gain_factor, 1, MAX_GAIN_FACTOR); }
@@ -76,7 +82,8 @@ class MicrophoneSource final {
bool is_stopped() const { return !this->is_running(); };
protected:
void process_audio_(const std::vector<uint8_t> &data, std::vector<uint8_t> &filtered_data);
void process_audio_(std::span<const uint8_t> data, const audio::AudioStreamInfo &source_info,
std::vector<uint8_t> &filtered_data);
std::shared_ptr<std::vector<uint8_t>> processed_samples_;
@@ -25,7 +25,7 @@ void ResamplerMicrophone::setup() {
return;
}
this->source_->add_data_callback([this](const std::vector<uint8_t> &data) { this->process_audio_(data); });
this->source_->add_data_callback([this](const microphone::AudioCapture &capture) { this->process_audio_(capture); });
this->disable_loop();
}
@@ -127,8 +127,8 @@ bool ResamplerMicrophone::init_resampler_(const audio::AudioStreamInfo &input_st
return true;
}
void ResamplerMicrophone::process_audio_(const std::vector<uint8_t> &data) {
const audio::AudioStreamInfo input_stream_info = this->source_->get_audio_stream_info();
void ResamplerMicrophone::process_audio_(const microphone::AudioCapture &capture) {
const audio::AudioStreamInfo &input_stream_info = capture.stream_info;
if (input_stream_info != this->input_stream_info_) {
this->init_resampler_(input_stream_info);
}
@@ -137,7 +137,7 @@ void ResamplerMicrophone::process_audio_(const std::vector<uint8_t> &data) {
}
if (this->resampler_ == nullptr) {
this->data_callbacks_.call(data);
this->publish_audio_(capture.data, input_stream_info, capture.capture_time_us);
return;
}
@@ -146,8 +146,8 @@ void ResamplerMicrophone::process_audio_(const std::vector<uint8_t> &data) {
// Both limits match the sizes the resampler's internal buffers were allocated with in init_resampler_
const uint32_t max_output_frames = this->audio_stream_info_.ms_to_frames(BUFFER_DURATION_MS);
const uint8_t *input = data.data();
uint32_t input_frames = input_stream_info.bytes_to_frames(data.size());
const uint8_t *input = capture.data.data();
uint32_t input_frames = input_stream_info.bytes_to_frames(capture.data.size());
while (input_frames > 0) {
// Stays within the reserved capacity, so this never reallocates
this->output_buffer_.resize(this->audio_stream_info_.frames_to_bytes(max_output_frames));
@@ -162,7 +162,7 @@ void ResamplerMicrophone::process_audio_(const std::vector<uint8_t> &data) {
if (results.frames_generated > 0) {
this->output_buffer_.resize(this->audio_stream_info_.frames_to_bytes(results.frames_generated));
this->data_callbacks_.call(this->output_buffer_);
this->publish_audio_(this->output_buffer_, this->audio_stream_info_);
} else if (results.frames_used == 0) {
break; // No progress; drop the rest of the chunk instead of spinning
}
@@ -39,7 +39,7 @@ class ResamplerMicrophone final : public Component, public microphone::Microphon
bool init_resampler_(const audio::AudioStreamInfo &input_stream_info);
/// @brief Resamples a chunk of source audio and passes it to the data callbacks. Source microphone task only.
void process_audio_(const std::vector<uint8_t> &data);
void process_audio_(const microphone::AudioCapture &capture);
microphone::MicrophoneSource *source_;
std::unique_ptr<esp_audio_libs::resampler::Resampler> resampler_;
@@ -28,10 +28,10 @@ void SoundLevelComponent::dump_config() {
}
void SoundLevelComponent::setup() {
this->microphone_source_->add_data_callback([this](const std::vector<uint8_t> &data) {
this->microphone_source_->add_data_callback([this](const microphone::AudioCapture &capture) {
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (temp_ring_buffer != nullptr) {
temp_ring_buffer->write((void *) data.data(), data.size());
temp_ring_buffer->write((void *) capture.data.data(), capture.data.size());
}
});
@@ -161,7 +161,7 @@ void USBAudioMicrophone::mic_task_loop_() {
if (err == ESP_OK && bytes_read > 0) {
this->frame_.resize(bytes_read);
this->data_callbacks_.call(this->frame_);
this->publish_audio_(this->frame_, this->audio_stream_info_);
} else if (err == ESP_ERR_TIMEOUT) {
// The driver already waited READ_TIMEOUT_MS for data; read again.
} else if (err == ESP_ERR_INVALID_STATE) {
@@ -40,19 +40,19 @@ static const uint32_t AUDIO_CHANNEL_STALL_TIMEOUT_MS = 2000;
VoiceAssistant::VoiceAssistant() { global_voice_assistant = this; }
void VoiceAssistant::setup() {
this->mic_source_->add_data_callback([this](const std::vector<uint8_t> &data) {
this->mic_source_->add_data_callback([this](const microphone::AudioCapture &capture) {
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (temp_ring_buffer != nullptr) {
temp_ring_buffer->write((void *) data.data(), data.size());
temp_ring_buffer->write((void *) capture.data.data(), capture.data.size());
}
});
// Second microphone channel
if (this->mic_source2_ != nullptr) {
this->mic_source2_->add_data_callback([this](const std::vector<uint8_t> &data) {
this->mic_source2_->add_data_callback([this](const microphone::AudioCapture &capture) {
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer2_.lock();
if (temp_ring_buffer != nullptr) {
temp_ring_buffer->write((void *) data.data(), data.size());
temp_ring_buffer->write((void *) capture.data.data(), capture.data.size());
}
});
}
+10
View File
@@ -0,0 +1,10 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# MicrophoneSource needs esp-audio-libs, which only builds for the ESP32
manifest.resources = [
resource
for resource in manifest.resources
if resource.resource != "microphone_source.cpp"
]
@@ -0,0 +1,118 @@
#include <gtest/gtest.h>
// The deprecation warning for vector callbacks is reported inside microphone.h. Remove in 2027.5.0.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#include "esphome/components/microphone/automation.h"
#include "esphome/components/microphone/microphone.h"
#pragma GCC diagnostic pop
#include <cstdint>
#include <vector>
namespace esphome::microphone::testing {
class TestMicrophone : public Microphone {
public:
void start() override {}
void stop() override {}
void publish(std::span<const uint8_t> data, const audio::AudioStreamInfo &stream_info, int64_t capture_time_us) {
this->publish_audio_(data, stream_info, capture_time_us);
}
bool has_callbacks() const { return this->has_data_callbacks_(); }
// Remove in 2027.5.0 with the deprecated DataCallbackManager::call(data)
void set_stream_info(const audio::AudioStreamInfo &stream_info) { this->audio_stream_info_ = stream_info; }
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
void publish_deprecated(const std::vector<uint8_t> &data) { this->data_callbacks_.call(data); }
#pragma GCC diagnostic pop
};
class RecordAction : public Action<const std::vector<uint8_t> &> {
public:
explicit RecordAction(std::vector<uint8_t> *out) : out_(out) {}
protected:
void play(const std::vector<uint8_t> &x) override { *this->out_ = x; }
std::vector<uint8_t> *out_;
};
static const audio::AudioStreamInfo STEREO_48K(16, 2, 48000);
static const std::vector<uint8_t> SAMPLES{1, 2, 3, 4, 5, 6, 7, 8};
TEST(MicrophoneDataCallbacks, CallbackGetsDataFormatAndCaptureTime) {
TestMicrophone mic;
EXPECT_FALSE(mic.has_callbacks());
std::vector<uint8_t> data;
audio::AudioStreamInfo stream_info;
int64_t capture_time_us = 0;
mic.add_data_callback([&](const AudioCapture &capture) {
data.assign(capture.data.begin(), capture.data.end());
stream_info = capture.stream_info;
capture_time_us = capture.capture_time_us;
});
EXPECT_TRUE(mic.has_callbacks());
mic.publish(SAMPLES, STEREO_48K, 123456);
EXPECT_EQ(data, SAMPLES);
EXPECT_EQ(stream_info, STEREO_48K);
EXPECT_EQ(capture_time_us, 123456);
}
TEST(MicrophoneDataCallbacks, MutedCallbackGetsZeros) {
TestMicrophone mic;
std::vector<uint8_t> data;
mic.add_data_callback([&](const AudioCapture &capture) { data.assign(capture.data.begin(), capture.data.end()); });
mic.set_mute_state(true);
mic.publish(SAMPLES, STEREO_48K, 0);
EXPECT_EQ(data, std::vector<uint8_t>(SAMPLES.size(), 0));
}
TEST(MicrophoneDataCallbacks, DataTriggerPassesACopy) {
TestMicrophone mic;
DataTrigger trigger(&mic);
std::vector<uint8_t> data;
Automation<const std::vector<uint8_t> &> automation(&trigger);
RecordAction record(&data);
automation.add_action(&record);
std::vector<uint8_t> buffer = SAMPLES;
mic.publish(buffer, STEREO_48K, 0);
EXPECT_EQ(data, SAMPLES);
}
// Remove in 2027.5.0 with the deprecated vector callbacks; nothing else in the tree compiles that adapter
TEST(MicrophoneDataCallbacks, DeprecatedVectorCallbackGetsData) {
TestMicrophone mic;
std::vector<uint8_t> data;
mic.add_data_callback([&](const std::vector<uint8_t> &x) { data = x; });
std::vector<uint8_t> buffer = SAMPLES;
mic.publish(buffer, STEREO_48K, 0);
EXPECT_EQ(data, SAMPLES);
}
// Remove in 2027.5.0 with the deprecated DataCallbackManager::call(data)
TEST(MicrophoneDataCallbacks, DeprecatedCallUsesMicrophoneFormat) {
TestMicrophone mic;
mic.set_stream_info(STEREO_48K);
std::vector<uint8_t> data;
audio::AudioStreamInfo stream_info;
int64_t capture_time_us = -1;
mic.add_data_callback([&](const AudioCapture &capture) {
data.assign(capture.data.begin(), capture.data.end());
stream_info = capture.stream_info;
capture_time_us = capture.capture_time_us;
});
mic.publish_deprecated(SAMPLES);
EXPECT_EQ(data, SAMPLES);
EXPECT_EQ(stream_info, STEREO_48K);
EXPECT_EQ(capture_time_us, 0);
}
} // namespace esphome::microphone::testing