Mic: let record() from the release callback wait briefly for the request lock - #372
Merged
Merged
Conversation
…est lock record() called from the buffer release callback returned false whenever the capture task reached the callback while another caller still held the request lock. That is the common case: the publisher notifies the task under the lock, and with a DMA backlog the buffer is filled and released before the publisher gets to unlock. The header promised false only for a full queue or a begin()/end() in progress, so alternating two buffers from the callback silently stopped capturing. The notify stays under the lock: moving it after the unlock would let an end() delete the task between the unlock and the notify. Instead the in-task path waits for the lock a tick at a time, bounded: - it gives up at once when _task_running has been cleared (end() and the sample-rate rebuild both clear it before they wait for the task to exit, which is the only holder that never releases otherwise); - it gives up after a few ticks, since the plain atomic has no priority inheritance and a middle-priority task could keep the holder off the CPU forever; a legitimate holder is a publisher about to unlock, so the bound only matters under that inversion; - both slots taken is decided before touching the lock: only the capture task frees a slot, so from its own callback waiting cannot change that. A busy spin is not an option: on one core the higher-priority capture task would starve the very caller it waits for. The header now lists every reason the callback's record() returns false (full queue, stop in progress, lock held too long, other rate) in both the callback note and record()'s @return, and states the exception to "never block in it" instead of contradicting it.
This was referenced Sep 20, 2026
Merged
lovyan03
added a commit
that referenced
this pull request
Sep 21, 2026
Mic: let record() from the release callback wait briefly for the request lock (cherry picked from commit 258ab46)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Mic.record()called from the buffer release callback returnedfalseeven though the queue had room and nobegin()/end()was in progress, so the "two buffers alternately, re-queued from the callback" pattern described in the header stopped capturing after the first buffers.The publisher notifies the capture task while it still holds the request lock. With a DMA backlog the task fills and releases the buffer before the publisher unlocks, the callback's
record()finds the lock taken, and the in-task path returnedfalseat once.Reported in detail at ainyan03#1 (ESP32-S3 + ES7210, arduino-esp32 3.3.9).
Changes
Mic_Class.inl(_rec_raw()): from the capture task, a taken request lock is waited for a tick at a time instead of failing immediately. The wait ends withfalsewhen_task_runninghas been cleared (anend()or sample-rate rebuild holds the lock while waiting for the task to exit; both clear the flag first), orin_task_lock_wait_ticks): the plain atomic has no priority inheritance, so a middle-priority task could otherwise keep the holder off the CPU indefinitely and the callback would never return.Both slots already taken is decided before touching the lock: only the capture task frees a slot, so waiting from its own callback cannot change that.
end()delete the task between the unlock and the notify.Mic_Class.hpp: the release-callback note andrecord()'s@returnnow list every reason the callback'srecord()returnsfalse(queue full, stop in progress, lock held too long, different rate), and the "never block in it" note states the exception.Public signatures are unchanged. Callers outside the capture task are unaffected.
Behavior note: from the release callback,
record()may now block for up to a few ticks while another task finishes publishing a request. Previously it never blocked there (and failed instead).Verification
Test firmware re-queues each released buffer from the callback and counts
record()failures, using the reporter's reproduction (600 ms of DMA backlog before the first tworecord()calls):Also checked on both:
end()called while the callback keeps re-queueing returns within 20 ms and the task exits; a sample-rate change from another task while the callback re-queues a single buffer completes (the callback'srecord()at the rebuild instant returnsfalseonce, as documented) and capture continues at the new rate.Two rounds of independent adversarial review. The first found that an unbounded per-tick wait could hang the capture task under priority inversion (a middle-priority task starving the lock holder), which led to the bounded wait. CI run on the ainyan03 fork before this PR.