Skip to content

fix(firmware): audio logic does not use all of the available buffers - #7689

Merged
pfeerick merged 2 commits into
mainfrom
philmoz/fix-audio-buffering
Sep 16, 2026
Merged

pfeerick merged 2 commits into
mainfrom
philmoz/fix-audio-buffering

Conversation

@philmoz

@philmoz philmoz commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

The audio logic uses a circular buffer array to handle writing and reading data to the audio hardware.

The code would not allow all of the available buffers to be filled as it marked the buffers as full when there was still one empty buffer available.

A fix for this was applied to 2.11 in #7472 (plus #7685).

This PR applies to fixed logic for 2.12 and 3.0.
It also removes some unused code.

@philmoz philmoz added this to the 3.0 milestone Aug 21, 2026
@philmoz philmoz added bug 🪲 Something isn't working firmware (fw) General radio firmware issue, not colorlcd or B&W specific backport/2.12 To be backported to a 2.12 release also. labels Aug 21, 2026
@pfeerick

Copy link
Copy Markdown
Member

@richardclli Can you follow this one up, as you did mention in #7472 you were going to check main also ;)

@raphaelcoeffic

Copy link
Copy Markdown
Member

This PR reclaims the wasted slot by adding bufferFull, which is now written by both sides (audioPushBuffer sets it true, freeNextFilledBuffer sets it false).

I would strongly prefer an implementation that preserves the property of textbook SPSC: single writer for each variable.
Normally this is done by using free running counters and using power-of-2 buffer sizes to automatically reduce the index whenever needed.

The same technique can be used slightly generalised for any N by computing the modulo on index usage rather than only counter advances.

Something like this:

private:
  volatile uint8_t readIdx;   // free-running in [0, 2*AUDIO_BUFFER_COUNT)
  volatile uint8_t writeIdx;

  inline uint8_t nextIndex(uint8_t idx) const {
    return (idx >= 2 * AUDIO_BUFFER_COUNT - 1 ? 0 : idx + 1);
  }
  inline uint8_t slot(uint8_t idx) const {
    return idx >= AUDIO_BUFFER_COUNT ? idx - AUDIO_BUFFER_COUNT : idx;
  }
  uint8_t used() const {
    return writeIdx >= readIdx ? writeIdx - readIdx
                               : writeIdx + 2 * AUDIO_BUFFER_COUNT - readIdx;
  }
  bool full()  const { return used() == AUDIO_BUFFER_COUNT; }
  bool empty() const { return readIdx == writeIdx; }

public:
  AudioBuffer* getEmptyBuffer() const {
    return full() ? nullptr : &audioBuffers[slot(writeIdx)];
  }
  void audioPushBuffer() { writeIdx = nextIndex(writeIdx); }
  void freeNextFilledBuffer() { readIdx = nextIndex(readIdx); }
  const AudioBuffer* getNextFilledBuffer() {
    return empty() ? nullptr : &audioBuffers[slot(readIdx)];
  }

@pfeerick pfeerick added the sound 🔉 Related to generated or played audio/sound label Aug 25, 2026
@philmoz

philmoz commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

@pfeerick the code here was copied from #7472 in 2.11 so your changes will probably be needed there as well.

@pfeerick pfeerick added the backport/2.11 To be backported to a 2.11 release also. label Aug 30, 2026
philmoz and others added 2 commits September 16, 2026 03:54
…IFOs

raphaelcoeffic pointed out in review that the bufferFull flag added here
is written by both producer (audioPushBuffer) and consumer
(freeNextFilledBuffer), breaking the textbook SPSC property that each
mutable field has exactly one writer (writeIdx only by the producer,
readIdx only by the consumer). That property is what makes this queue
obviously correct without reasoning about cross-context timing.

Switch AudioBufferFifo to his suggested fix: free-running counters over
[0, 2*AUDIO_BUFFER_COUNT), folded down to the actual buffer slot -- the
standard generalization of the power-of-2-mask ring buffer trick to
arbitrary N. No extra state, no wasted slot, no dual-writer field.

Apply the same technique to AudioFragmentFifo, which had the identical
wasted-slot sentinel issue and already used the power-of-2-mask pattern
(AUDIO_QUEUE_LENGTH), so the fix generalizes it rather than introducing
a new idiom.

A couple of hardening follow-ups from an independent review pass:
- used() now snapshots each volatile into a local before computing, so
  the result is provably self-consistent instead of relying on a race
  analysis of the compiler's re-reads across the ternary.
- static_asserts guard the two assumptions the free-running-counter math
  depends on: AUDIO_QUEUE_LENGTH being a power of 2, and 2*AUDIO_BUFFER_COUNT
  fitting in uint8_t.
- The DEBUG_AUDIO printAudioVars() output now also prints the folded slot
  alongside each raw free-running index, for easier debugging.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@pfeerick
pfeerick force-pushed the philmoz/fix-audio-buffering branch from 1aa4203 to 3c7ec15 Compare September 16, 2026 03:56
@pfeerick

Copy link
Copy Markdown
Member

Seems ok on ST16, TX16SMK3, T14 - around 15 bootups, for each to test power on sounding from cold, SF playing track and sounds, wile also overlapping trim sounds, only some to-be-expected what sort of sound like wav audio micro-stutters/clipping due higher priority trim beeps being played.

@pfeerick pfeerick modified the milestones: 3.0, 2.12.5, 2.11.8 Sep 16, 2026
@pfeerick
pfeerick merged commit e2968ac into main Sep 16, 2026
48 checks passed
@pfeerick
pfeerick deleted the philmoz/fix-audio-buffering branch September 16, 2026 05:12
@pfeerick pfeerick mentioned this pull request Sep 24, 2026
21 tasks done
pfeerick added a commit that referenced this pull request Oct 2, 2026
…7689)

Co-authored-by: Peter Feerick <5500713+pfeerick@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit e2968ac)
@pfeerick pfeerick mentioned this pull request Oct 2, 2026
47 of 49 tasks
pfeerick pushed a commit that referenced this pull request Oct 7, 2026
…7689)

Co-authored-by: Peter Feerick <5500713+pfeerick@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

2.11 port: 2.11 already fixed the AudioBufferFifo full/empty ambiguity
its own way in #7472 (a0cd30f, a
volatile bufferFull flag) plus #7685
(53199fe, used() when full). That
flag is written by both the producer (audioPushBuffer) and the consumer
(freeNextFilledBuffer), so an interrupt between the index update and
the flag update can leave it wrong. This port replaces it with
upstream's free-running counters, so each side only writes its own
index, and also brings in the AudioFragmentFifo fix
(all AUDIO_QUEUE_LENGTH slots usable), the static_asserts and the CLI
output.

Conflicts in the AudioBufferFifo class were resolved to the upstream
version, and the bufferFull member and initialiser were removed. The
cli.cpp debug output prints full() instead of bufferFull, as upstream
does. The DAC underrun handling from #7472 is kept unchanged.

filledAtleast() is kept, although upstream removes it as unused. 2.11's
simulator audio callback (targets/simu/simpgmspace.cpp fillAudioBuffer)
still uses it as a pre-buffer threshold. used() keeps returning
AUDIO_BUFFER_COUNT when full, as it did after #7685.

(cherry picked from commit e2968ac)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport/2.11 To be backported to a 2.11 release also. backport/2.12 To be backported to a 2.12 release also. bug 🪲 Something isn't working firmware (fw) General radio firmware issue, not colorlcd or B&W specific sound 🔉 Related to generated or played audio/sound

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants