Compare commits

...
Author SHA1 Message Date
Kevin Ahrendt 1ea163658a [resampler] Include the resampler's position in output capture times
When a call stops because the output is full, the next output can be up to one input frame before (frames taken in - taps / 2), which put block times up to about 100 us late when upsampling from 8 kHz. ART's position gives the exact offset.
2026-10-26 15:08:43 -04:00
Kevin Ahrendt 0d9db7d0de [resampler] Note how held back frames are timed after a source gap 2026-10-26 14:55:20 -04:00
Kevin Ahrendt 781f50225d [resampler] Carry capture times through the resampler microphone
Each output block's capture time comes from its input chunk's time, the input
taken in so far, and the half filter of input the resampler holds back. It is
worked out per chunk, so it cannot drift.
2026-10-26 14:32:28 -04:00
2 changed files with 26 additions and 4 deletions

No files matched your search

@@ -99,8 +99,8 @@ bool ResamplerMicrophone::init_resampler_(const audio::AudioStreamInfo &input_st
const audio::AudioStreamInfo output_stream_info(input_stream_info.get_bits_per_sample(),
input_stream_info.get_channels(), this->target_sample_rate_);
auto resampler = make_unique<esp_audio_libs::resampler::Resampler>(
input_stream_info.ms_to_samples(BUFFER_DURATION_MS), output_stream_info.ms_to_samples(BUFFER_DURATION_MS));
auto resampler = make_unique<PositionResampler>(input_stream_info.ms_to_samples(BUFFER_DURATION_MS),
output_stream_info.ms_to_samples(BUFFER_DURATION_MS));
esp_audio_libs::resampler::ResamplerConfiguration resample_config = {
.source_sample_rate = static_cast<float>(input_stream_info.get_sample_rate()),
@@ -165,6 +165,8 @@ void ResamplerMicrophone::process_audio_(const microphone::AudioCapture &capture
const uint8_t *input = capture.data.data();
uint32_t input_frames = input_stream_info.bytes_to_frames(capture.data.size());
// Input frames of this chunk the resampler has taken in so far
int64_t frames_consumed = 0;
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));
@@ -176,10 +178,22 @@ void ResamplerMicrophone::process_audio_(const microphone::AudioCapture &capture
input += results.frames_used * input_bytes_per_frame;
input_frames -= results.frames_used;
frames_consumed += results.frames_used;
if (results.frames_generated > 0) {
int64_t output_time_us = 0;
if (capture.capture_time_us != 0) {
// The resampler holds back half its taps of input, so its next output is near the input frame that far behind
// the last one taken in; its position gives the exact offset. The block's first output is one block duration
// before that. After a gap in the source, the few frames held back from before it are timed as if there were
// no gap.
const uint32_t input_rate = input_stream_info.get_sample_rate();
output_time_us = capture.capture_time_us + (frames_consumed - this->taps_ / 2) * 1000000 / input_rate +
static_cast<int64_t>(this->resampler_->get_position() * 1000000.0f / input_rate) -
static_cast<int64_t>(results.frames_generated) * 1000000 / this->target_sample_rate_;
}
this->output_buffer_.resize(this->audio_stream_info_.frames_to_bytes(results.frames_generated));
this->publish_audio_(this->output_buffer_, this->audio_stream_info_);
this->publish_audio_(this->output_buffer_, this->audio_stream_info_, output_time_us);
} else if (results.frames_used == 0) {
break; // No progress; drop the rest of the chunk instead of spinning
}
@@ -15,6 +15,14 @@
namespace esphome::resampler {
/// @brief The esp-audio-libs resampler, with its exact position for timestamping output.
class PositionResampler : public esp_audio_libs::resampler::Resampler {
public:
using Resampler::Resampler;
/// @brief Input frames from (last frame taken in - taps / 2) to the next output frame; usually within one frame.
float get_position() { return esp_audio_libs::art_resampler::resampleGetPosition(this->resampler_); }
};
/// @brief Microphone that converts the audio of a source microphone to a different sample rate.
/// The bits per sample and channels are selected by the source's ``MicrophoneSource``; only the sample rate changes.
/// Resampling runs in the source microphone's data callback, so it needs no task or ring buffer of its own.
@@ -42,7 +50,7 @@ class ResamplerMicrophone final : public Component, public microphone::Microphon
void process_audio_(const microphone::AudioCapture &capture);
microphone::MicrophoneSource *source_;
std::unique_ptr<esp_audio_libs::resampler::Resampler> resampler_;
std::unique_ptr<PositionResampler> resampler_;
// Reused for every chunk so resampling does not allocate
std::vector<uint8_t> output_buffer_;