Skip to content

Fix Pulseaudio buffering#220

Open
LaurentChardon wants to merge 3 commits into
pguyot:masterfrom
LaurentChardon:pulseaudio-buffering-pr
Open

Fix Pulseaudio buffering#220
LaurentChardon wants to merge 3 commits into
pguyot:masterfrom
LaurentChardon:pulseaudio-buffering-pr

Conversation

@LaurentChardon

@LaurentChardon LaurentChardon commented Jul 10, 2026

Copy link
Copy Markdown

Long sounds were choppy. This fixes it.
See commit messages for details.
I accidentally pushed these changes to another PR that I had open, but they are separate. I did take into account the AI code review from that initial PR though, and addressed them.

Summary by CodeRabbit

  • Bug Fixes
    • Improved PulseAudio audio output by switching to buffered/queued delivery, reducing underflows and dropped audio during sustained playback.
    • Enhanced start/stop/shutdown coordination to make interruptions less likely.
  • Performance
    • Audio delivery is now better synchronized with PulseAudio’s write requests for smoother, more consistent playback.
  • Technical Updates
    • Improved internal buffering behavior with support for peeking and discarding data without advancing output prematurely.

Greptile Summary

This PR changes PulseAudio output to buffer audio locally before writing it to the stream. The main changes are:

  • A circular output buffer for queued audio samples.
  • A PulseAudio write callback that drains queued output.
  • Pending interrupt tracking for refill requests.
  • Cleanup updates for stream callbacks, stream refs, and context refs.
  • New Peek and Discard helpers on TCircleBuffer.

Confidence Score: 4/5

This is close, but one cleanup path should be fixed before merging.

  • Partial PulseAudio construction can still leave the context alive after teardown.
  • The main buffering and callback changes are otherwise scoped to the output path.

Emulator/Sound/TPulseAudioSoundManager.cpp

Important Files Changed

Filename Overview
Emulator/Sound/TPulseAudioSoundManager.cpp Adds queued PulseAudio output, write-callback draining, refill request tracking, and cleanup changes.
Emulator/Sound/TPulseAudioSoundManager.h Adds declarations and state for queued output and pending refill requests.
K/Misc/TCircleBuffer.cpp Adds non-consuming buffer reads and explicit discard support.
K/Misc/TCircleBuffer.h Exposes the new circular-buffer helper methods.

Reviews (2): Last reviewed commit: "Make PulseAudio queued writes transactio..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Laurent Chardon added 2 commits July 10, 2026 07:50
Long Newton sounds were choppy because the PulseAudio backend wrote directly from the Newton sound callback and could not reliably handle cases where the stream could not accept a full Newton buffer immediately.

Replace the direct-write path with a local ring buffer drained by PulseAudio's write callback. This keeps queued sound data intact across partial writable-space windows.

Throttle output interrupt requests so audio refill does not starve emulator input, and unregister PulseAudio callbacks during teardown to avoid callback use after shutdown.
Initialize PulseAudio pointer members before setup so constructor error paths can safely check them.

Unref PulseAudio stream and context during normal teardown after disconnecting them.

Use std::min instead of the non-portable MIN macro when sizing queued PulseAudio writes.
@LaurentChardon LaurentChardon changed the title Pulseaudio buffering pr Fix Pulseaudio buffering Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

TPulseAudioSoundManager now buffers generated audio in a circular buffer and delivers it through PulseAudio write callbacks. Output state and interrupt requests use mutex synchronization, while startup, shutdown, underflow handling, and stream teardown coordinate with the threaded mainloop.

Changes

PulseAudio queued output

Layer / File(s) Summary
Output state and buffer contract
Emulator/Sound/TPulseAudioSoundManager.h, Emulator/Sound/TPulseAudioSoundManager.cpp, K/Misc/TCircleBuffer.h, K/Misc/TCircleBuffer.cpp
The manager adds circular-buffer and pending-request state, initializes PulseAudio members, registers the stream write callback, and adds non-consuming peek/discard operations to TCircleBuffer.
Queued audio write flow
Emulator/Sound/TPulseAudioSoundManager.cpp
ScheduleOutput buffers audio, and callback-driven writes consume buffered bytes through pa_stream_begin_write and pa_stream_write, requesting more output when needed.
Synchronized lifecycle and shutdown
Emulator/Sound/TPulseAudioSoundManager.cpp
Start, stop, status, underflow, interrupt, connection failure, and destructor paths synchronize state and coordinate PulseAudio mainloop teardown.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Emulator
  participant TPulseAudioSoundManager
  participant TCircleBuffer
  participant PulseAudio
  Emulator->>TPulseAudioSoundManager: ScheduleOutput audio
  TPulseAudioSoundManager->>TCircleBuffer: buffer audio bytes
  PulseAudio->>TPulseAudioSoundManager: write callback with requested bytes
  TPulseAudioSoundManager->>TCircleBuffer: peek and discard queued bytes
  TPulseAudioSoundManager->>PulseAudio: pa_stream_write
  PulseAudio->>TPulseAudioSoundManager: underflow notification
  TPulseAudioSoundManager->>Emulator: RequestOutputInterrupt
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: fixing PulseAudio buffering behavior.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread Emulator/Sound/TPulseAudioSoundManager.cpp
Comment on lines +375 to +378
mDataMutex->Lock();
mOutputIsRunning = false;
mOutputRequestPending = false;
mDataMutex->Unlock();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stop Path Lock Inversion

StopOutput() now takes mDataMutex while it already holds the PulseAudio mainloop lock, but producer paths take the same locks in the opposite order. A zero-size output buffer can enter this path while another output call is waiting on the mainloop lock, which can deadlock the stop/drain sequence.

Comment thread Emulator/Sound/TPulseAudioSoundManager.cpp
Comment on lines 246 to +252
// disconnect the stream
pa_stream_disconnect(mOutputStream);
pa_context_disconnect(mPAContext);
// pa_threaded_mainloop_stop
pa_stream_unref(mOutputStream);
if (mPAContext)
{
pa_context_disconnect(mPAContext);
pa_context_unref(mPAContext);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Context Cleanup Depends On Stream

The new context unref is nested under if (mOutputStream). If construction creates mPAContext but fails before a stream is assigned, destruction skips pa_context_disconnect() and pa_context_unref(), leaving the PulseAudio context alive after the manager is torn down.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Emulator/Sound/TPulseAudioSoundManager.cpp (1)

375-390: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

StopOutput leaves queued samples behind. pa_stream_drain() only waits for PulseAudio’s server-side buffer; mOutputBuffer is never cleared, so leftover audio can play after stop or on the next start. Clear or discard the local queue before draining, or switch to flush/cork semantics if this is meant to abort playback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Emulator/Sound/TPulseAudioSoundManager.cpp` around lines 375 - 390, Update
StopOutput around mOutputBuffer and the pa_stream_drain call so stopping
playback also discards any locally queued samples; clear the queue while holding
mDataMutex before initiating the PulseAudio operation, or use flush/cork
semantics if StopOutput is intended to abort playback rather than drain it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Emulator/Sound/TPulseAudioSoundManager.cpp`:
- Around line 518-543: Make the output-buffer write in the sound callback
transactional: update the logic around mOutputBuffer->Consume() and
pa_stream_write() so queued bytes are not permanently removed before a
successful PulseAudio write. Check pa_stream_write()’s return value, restore or
otherwise retain consumed data when it fails, and route both
pa_stream_begin_write() and pa_stream_write() failures through the existing
recovery/request-output handling.

---

Outside diff comments:
In `@Emulator/Sound/TPulseAudioSoundManager.cpp`:
- Around line 375-390: Update StopOutput around mOutputBuffer and the
pa_stream_drain call so stopping playback also discards any locally queued
samples; clear the queue while holding mDataMutex before initiating the
PulseAudio operation, or use flush/cork semantics if StopOutput is intended to
abort playback rather than drain it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 44f6c605-40b8-4c1c-bdd9-efd85c496360

📥 Commits

Reviewing files that changed from the base of the PR and between f5544a0 and 35f5995.

📒 Files selected for processing (2)
  • Emulator/Sound/TPulseAudioSoundManager.cpp
  • Emulator/Sound/TPulseAudioSoundManager.h

Comment thread Emulator/Sound/TPulseAudioSoundManager.cpp Outdated
Follow-up to the PulseAudio buffering fix for choppy long sounds.

The write callback copied data out of the Newton-side queue with Consume() before checking whether PulseAudio accepted the write. If pa_stream_write() failed, those queued bytes were lost.

Add Peek() and Discard() to TCircleBuffer and use them so the PulseAudio callback only advances the output queue after a successful pa_stream_write(). Request more output on begin/write failure through the existing recovery path.
Comment on lines +249 to +253
if (mPAContext)
{
pa_context_disconnect(mPAContext);
pa_context_unref(mPAContext);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Context cleanup remains scoped When pa_context_new() succeeds but stream creation fails before mOutputStream is assigned, the destructor skips this block entirely. That leaves the PulseAudio context connected and referenced after the manager is destroyed. Move the context disconnect and unref out of the stream-only branch so partial construction is cleaned up correctly.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
K/Misc/TCircleBuffer.cpp (1)

238-315: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Refactor Consume to use Peek + Discard to eliminate duplication and fix a pre-existing bug.

Peek and Discard are correct and well-implemented. However, Peek is nearly a line-for-line copy of Consume's copy logic. Consume could be refactored as Peek + Discard, which would:

  1. Eliminate the duplication between Peek and Consume.
  2. Fix a pre-existing bug in Consume (lines 215 and 230 use inAmount instead of the clamped amount), which Peek correctly avoids.
♻️ Proposed refactor of Consume
 KUIntPtr
 TCircleBuffer::Consume(void* outBuffer, KUIntPtr inAmount)
 {
-	KUIntPtr amount = inAmount;
-
-	// Copy data.
-	if (mConsumerCrsr <= mProducerCrsr)
-	{
-		// ---C123P---
-		KUIntPtr max = mProducerCrsr - mConsumerCrsr;
-		if (amount > max)
-		{
-			amount = max;
-		}
-
-		(void) ::memcpy(
-			outBuffer,
-			(const void*) (mBuffer + mConsumerCrsr),
-			amount);
-
-		mConsumerCrsr += amount;
-	} else
-	{
-		// 456P---C123
-		KUIntPtr max = mBufferSize + mProducerCrsr - mConsumerCrsr;
-		if (amount > max)
-		{
-			amount = max;
-		}
-
-		if (amount + mConsumerCrsr > mBufferSize)
-		{
-			KUIntPtr toEnd = mBufferSize - mConsumerCrsr;
-			KUIntPtr fromBeginning = inAmount - toEnd;
-			(void) ::memcpy(
-				outBuffer,
-				(const void*) (mBuffer + mConsumerCrsr),
-				toEnd);
-			(void) ::memcpy(
-				((KUInt8*) outBuffer) + toEnd,
-				(const void*) mBuffer,
-				fromBeginning);
-			mConsumerCrsr = fromBeginning;
-		} else
-		{
-			(void) ::memcpy(
-				outBuffer,
-				(const void*) (mBuffer + mConsumerCrsr),
-				inAmount);
-			mConsumerCrsr += inAmount;
-		}
-	}
-
-	return amount;
+	KUIntPtr copied = Peek(outBuffer, inAmount);
+	Discard(copied);
+	return copied;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@K/Misc/TCircleBuffer.cpp` around lines 238 - 315, Refactor
TCircleBuffer::Consume to delegate copying and cursor advancement to the
existing TCircleBuffer::Peek and TCircleBuffer::Discard methods. Replace its
duplicated copy logic with a Peek(outBuffer, inAmount) call, pass the returned
clamped byte count to Discard, and return that count; do not use the original
inAmount when advancing or copying.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@K/Misc/TCircleBuffer.cpp`:
- Around line 238-315: Refactor TCircleBuffer::Consume to delegate copying and
cursor advancement to the existing TCircleBuffer::Peek and
TCircleBuffer::Discard methods. Replace its duplicated copy logic with a
Peek(outBuffer, inAmount) call, pass the returned clamped byte count to Discard,
and return that count; do not use the original inAmount when advancing or
copying.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e19efa58-76d0-4b94-90e4-26e4574ee4c7

📥 Commits

Reviewing files that changed from the base of the PR and between 35f5995 and 15154c8.

📒 Files selected for processing (3)
  • Emulator/Sound/TPulseAudioSoundManager.cpp
  • K/Misc/TCircleBuffer.cpp
  • K/Misc/TCircleBuffer.h
🚧 Files skipped from review as they are similar to previous changes (1)
  • Emulator/Sound/TPulseAudioSoundManager.cpp

@MatthiasWM

Copy link
Copy Markdown
Collaborator

Thanks for setting up this PR. I'll try to get my Linux environment running so I can check the PR and eventually merge it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants