diff --git a/src/output/plugins/PipeWireOutputPlugin.cxx b/src/output/plugins/PipeWireOutputPlugin.cxx
index 1c21f75df..03ec88972 100644
--- a/src/output/plugins/PipeWireOutputPlugin.cxx
+++ b/src/output/plugins/PipeWireOutputPlugin.cxx
@@ -28,6 +28,7 @@
 #include "system/Error.hxx"
 #include "util/BitReverse.hxx"
 #include "util/Domain.hxx"
+#include "util/RingBuffer.hxx"
 #include "util/ScopeExit.hxx"
 #include "util/StringCompare.hxx"
 #include "Log.hxx"
@@ -52,8 +53,6 @@
 #pragma GCC diagnostic pop
 #endif
 
-#include <boost/lockfree/spsc_queue.hpp>
-
 #include <algorithm>
 #include <array>
 #include <numeric>
@@ -81,8 +80,8 @@ class PipeWireOutput final : AudioOutput {
 	/**
 	 * This buffer passes PCM data from Play() to Process().
 	 */
-	using RingBuffer = boost::lockfree::spsc_queue<std::byte>;
-	RingBuffer *ring_buffer;
+	using RingBuffer = ::RingBuffer<std::byte>;
+	RingBuffer ring_buffer;
 
 	uint32_t target_id = PW_ID_ANY;
 
@@ -572,9 +571,7 @@ PipeWireOutput::Open(AudioFormat &audio_format)
 	interrupted = false;
 
 	/* allocate a ring buffer of 0.5 seconds */
-	const std::size_t ring_buffer_size =
-		frame_size * (audio_format.sample_rate / 2);
-	ring_buffer = new RingBuffer(ring_buffer_size);
+	ring_buffer = RingBuffer{frame_size * (audio_format.sample_rate / 2)};
 
 	const struct spa_pod *params[1];
 
@@ -626,7 +623,7 @@ PipeWireOutput::Close() noexcept
 		stream = nullptr;
 	}
 
-	delete ring_buffer;
+	ring_buffer = {};
 }
 
 inline void
@@ -774,25 +771,17 @@ PipeWireOutput::Process() noexcept
 	if (dest == nullptr)
 		return;
 
-	std::size_t max_frames = d.maxsize / frame_size;
+	std::size_t chunk_size = frame_size;
 
 #if defined(ENABLE_DSD) && defined(SPA_AUDIO_DSD_FLAG_NONE)
 	if (use_dsd && dsd_interleave > 1) {
 		/* make sure we don't get partial interleave frames */
-		std::size_t interleave_size = frame_size * dsd_interleave;
-		std::size_t available_bytes = ring_buffer->read_available();
-		std::size_t available_interleaves =
-			available_bytes / interleave_size;
-		std::size_t available_frames =
-			available_interleaves * dsd_interleave;
-		if (max_frames > available_frames)
-			max_frames = available_frames;
+		chunk_size *= dsd_interleave;
 	}
 #endif
 
-	const std::size_t max_size = max_frames * frame_size;
-	size_t nbytes = ring_buffer->pop(dest, max_size);
-	assert(nbytes % frame_size == 0);
+	size_t nbytes = ring_buffer.ReadFramesTo({dest, d.maxsize}, chunk_size);
+	assert(nbytes % chunk_size == 0);
 	if (nbytes == 0) {
 		if (drain_requested) {
 			pw_stream_flush(stream, true);
@@ -800,8 +789,9 @@ PipeWireOutput::Process() noexcept
 		}
 
 		/* buffer underrun: generate some silence */
-		PcmSilence({dest, max_size}, sample_format);
-		nbytes = max_size;
+		std::size_t max_chunks = d.maxsize / chunk_size;
+		nbytes = max_chunks * chunk_size;
+		PcmSilence({dest, nbytes}, sample_format);
 
 		LogWarning(pipewire_output_domain, "Decoder is too slow; playing silence to avoid xrun");
 	}
@@ -846,7 +836,7 @@ PipeWireOutput::Play(std::span<const std::byte> src)
 		CheckThrowError();
 
 		std::size_t bytes_written =
-			ring_buffer->push(src.data(), src.size());
+			ring_buffer.WriteFrom(src);
 		if (bytes_written > 0) {
 			drained = false;
 			return bytes_written;
@@ -902,7 +892,7 @@ PipeWireOutput::Cancel() noexcept
 		return;
 
 	/* clear MPD's ring buffer */
-	ring_buffer->reset();
+	ring_buffer.Clear();
 
 	/* clear libpipewire's buffer */
 	pw_stream_flush(stream, false);