diff --git a/src/client/Response.cxx b/src/client/Response.cxx
index e427f30f4..75f0694a8 100644
--- a/src/client/Response.cxx
+++ b/src/client/Response.cxx
@@ -53,13 +53,13 @@ Response::VFmt(fmt::string_view format_str, fmt::format_args args) noexcept
 }
 
 bool
-Response::WriteBinary(ConstBuffer<void> payload) noexcept
+Response::WriteBinary(std::span<const std::byte> payload) noexcept
 {
-	assert(payload.size <= client.binary_limit);
+	assert(payload.size() <= client.binary_limit);
 
 	return
-		Fmt("binary: {}\n", payload.size) &&
-		Write(payload.data, payload.size) &&
+		Fmt("binary: {}\n", payload.size()) &&
+		Write(payload.data(), payload.size()) &&
 		Write("\n");
 }
 
diff --git a/src/client/Response.hxx b/src/client/Response.hxx
index 7955d07c1..c77bfe136 100644
--- a/src/client/Response.hxx
+++ b/src/client/Response.hxx
@@ -28,8 +28,8 @@
 #endif
 
 #include <cstddef>
+#include <span>
 
-template<typename T> struct ConstBuffer;
 class Client;
 class TagMask;
 
@@ -99,7 +99,7 @@ public:
 	 *
 	 * @return true on success
 	 */
-	bool WriteBinary(ConstBuffer<void> payload) noexcept;
+	bool WriteBinary(std::span<const std::byte> payload) noexcept;
 
 	void Error(enum ack code, const char *msg) noexcept;
 
diff --git a/src/decoder/DecoderBuffer.cxx b/src/decoder/DecoderBuffer.cxx
index 4f1d54b40..c74079c63 100644
--- a/src/decoder/DecoderBuffer.cxx
+++ b/src/decoder/DecoderBuffer.cxx
@@ -29,7 +29,7 @@ DecoderBuffer::Fill()
 		return false;
 
 	size_t nbytes = decoder_read(client, is,
-				     w.data, w.size);
+				     w.data(), w.size());
 	if (nbytes == 0)
 		/* end of file, I/O error or decoder command
 		   received */
@@ -39,16 +39,16 @@ DecoderBuffer::Fill()
 	return true;
 }
 
-ConstBuffer<void>
+std::span<const std::byte>
 DecoderBuffer::Need(size_t min_size)
 {
 	while (true) {
 		const auto r = Read();
-		if (r.size >= min_size)
+		if (r.size() >= min_size)
 			return r;
 
 		if (!Fill())
-			return nullptr;
+			return {};
 	}
 }
 
@@ -56,13 +56,13 @@ bool
 DecoderBuffer::Skip(size_t nbytes)
 {
 	const auto r = buffer.Read();
-	if (r.size >= nbytes) {
+	if (r.size() >= nbytes) {
 		buffer.Consume(nbytes);
 		return true;
 	}
 
 	buffer.Clear();
-	nbytes -= r.size;
+	nbytes -= r.size();
 
 	return decoder_skip(client, is, nbytes);
 }
diff --git a/src/decoder/DecoderBuffer.hxx b/src/decoder/DecoderBuffer.hxx
index 7b72d8a00..6b55e4642 100644
--- a/src/decoder/DecoderBuffer.hxx
+++ b/src/decoder/DecoderBuffer.hxx
@@ -21,10 +21,6 @@
 #define MPD_DECODER_BUFFER_HXX
 
 #include "util/DynamicFifoBuffer.hxx"
-#include "util/ConstBuffer.hxx"
-
-#include <cstddef>
-#include <cstdint>
 
 class DecoderClient;
 class InputStream;
@@ -38,7 +34,7 @@ class DecoderBuffer {
 	DecoderClient *const client;
 	InputStream &is;
 
-	DynamicFifoBuffer<uint8_t> buffer;
+	DynamicFifoBuffer<std::byte> buffer;
 
 public:
 	/**
@@ -83,16 +79,15 @@ public:
 	 * you have to call Consume() to do that.  The returned buffer
 	 * becomes invalid after a Fill() or a Consume() call.
 	 */
-	ConstBuffer<void> Read() const noexcept {
-		auto r = buffer.Read();
-		return { r.data, r.size };
+	std::span<const std::byte> Read() const noexcept {
+		return buffer.Read();
 	}
 
 	/**
 	 * Wait until this number of bytes are available.  Returns nullptr on
 	 * error.
 	 */
-	ConstBuffer<void> Need(size_t min_size);
+	std::span<const std::byte> Need(size_t min_size);
 
 	/**
 	 * Consume (delete, invalidate) a part of the buffer.  The
diff --git a/src/decoder/plugins/FaadDecoderPlugin.cxx b/src/decoder/plugins/FaadDecoderPlugin.cxx
index 33aa380ad..0a4eb7530 100644
--- a/src/decoder/plugins/FaadDecoderPlugin.cxx
+++ b/src/decoder/plugins/FaadDecoderPlugin.cxx
@@ -93,7 +93,7 @@ adts_find_frame(DecoderBuffer &buffer)
 			continue;
 		}
 
-		if (buffer.Need(frame_length).IsNull()) {
+		if (buffer.Need(frame_length).empty()) {
 			/* not enough data; discard this frame to
 			   prevent a possible buffer overflow */
 			buffer.Clear();
diff --git a/src/decoder/plugins/HybridDsdDecoderPlugin.cxx b/src/decoder/plugins/HybridDsdDecoderPlugin.cxx
index 4a39235db..c11b9a290 100644
--- a/src/decoder/plugins/HybridDsdDecoderPlugin.cxx
+++ b/src/decoder/plugins/HybridDsdDecoderPlugin.cxx
@@ -237,11 +237,11 @@ HybridDsdDecode(DecoderClient &client, InputStream &input)
 		auto w = buffer.Write();
 		if (!w.empty()) {
 			if (remaining_bytes < (1<<30ULL) &&
-			    w.size > size_t(remaining_bytes))
-				w.size = remaining_bytes;
+			    w.size() > size_t(remaining_bytes))
+				w = w.first(remaining_bytes);
 
 			const size_t nbytes = client.Read(input,
-							  w.data, w.size);
+							  w.data(), w.size());
 			if (nbytes == 0)
 				return;
 
@@ -251,9 +251,9 @@ HybridDsdDecode(DecoderClient &client, InputStream &input)
 
 		/* submit the buffer to our client */
 		auto r = buffer.Read();
-		auto n_frames = r.size / frame_size;
+		auto n_frames = r.size() / frame_size;
 		if (n_frames > 0) {
-			cmd = client.SubmitData(input, r.data,
+			cmd = client.SubmitData(input, r.data(),
 						n_frames * frame_size,
 						kbit_rate);
 			buffer.Consume(n_frames * frame_size);
diff --git a/src/decoder/plugins/PcmDecoderPlugin.cxx b/src/decoder/plugins/PcmDecoderPlugin.cxx
index 6fae29a1c..bd4dea739 100644
--- a/src/decoder/plugins/PcmDecoderPlugin.cxx
+++ b/src/decoder/plugins/PcmDecoderPlugin.cxx
@@ -51,7 +51,7 @@ FillBuffer(DecoderClient &client, InputStream &is, B &buffer)
 	if (w.empty())
 		return true;
 
-	size_t nbytes = decoder_read(client, is, w.data, w.size);
+	size_t nbytes = decoder_read(client, is, w.data(), w.size());
 	if (nbytes == 0 && is.LockIsEOF())
 		return false;
 
@@ -188,25 +188,28 @@ pcm_stream_decode(DecoderClient &client, InputStream &is)
 		/* round down to the nearest frame size, because we
 		   must not pass partial frames to
 		   DecoderClient::SubmitData() */
-		r.size -= r.size % in_frame_size;
-		buffer.Consume(r.size);
+		r = r.first(r.size() - r.size() % in_frame_size);
+		buffer.Consume(r.size());
 
 		if (reverse_endian)
 			/* make sure we deliver samples in host byte order */
-			reverse_bytes_16((uint16_t *)r.data,
-					 (uint16_t *)r.data,
-					 (uint16_t *)(r.data + r.size));
+			reverse_bytes_16((uint16_t *)r.data(),
+					 (uint16_t *)r.data(),
+					 (uint16_t *)(r.data() + r.size()));
 		else if (l24) {
 			/* convert big-endian packed 24 bit
 			   (audio/L24) to native-endian 24 bit (in 32
 			   bit integers) */
-			pcm_unpack_24be(unpack_buffer, r.begin(), r.end());
-			r.data = (uint8_t *)&unpack_buffer[0];
-			r.size = (r.size / 3) * 4;
+			pcm_unpack_24be(unpack_buffer,
+					r.data(), r.data() + r.size());
+			r = {
+				(uint8_t *)&unpack_buffer[0],
+				(r.size() / 3) * 4,
+			};
 		}
 
 		cmd = !r.empty()
-			? client.SubmitData(is, r.data, r.size, 0)
+			? client.SubmitData(is, r.data(), r.size(), 0)
 			: client.GetCommand();
 		if (cmd == DecoderCommand::SEEK) {
 			uint64_t frame = client.GetSeekFrame();
diff --git a/src/encoder/plugins/WaveEncoderPlugin.cxx b/src/encoder/plugins/WaveEncoderPlugin.cxx
index b224b11e9..7cb8635da 100644
--- a/src/encoder/plugins/WaveEncoderPlugin.cxx
+++ b/src/encoder/plugins/WaveEncoderPlugin.cxx
@@ -131,8 +131,8 @@ WaveEncoder::WaveEncoder(AudioFormat &audio_format) noexcept
 	}
 
 	auto range = buffer.Write();
-	assert(range.size >= sizeof(WaveHeader));
-	auto *header = (WaveHeader *)range.data;
+	assert(range.size() >= sizeof(WaveHeader));
+	auto *header = (WaveHeader *)(void *)range.data();
 
 	/* create PCM wave header in initial buffer */
 	*header = MakeWaveHeader(audio_format.channels,
diff --git a/src/event/BufferedSocket.cxx b/src/event/BufferedSocket.cxx
index 53877da5f..dc6deceee 100644
--- a/src/event/BufferedSocket.cxx
+++ b/src/event/BufferedSocket.cxx
@@ -54,7 +54,7 @@ BufferedSocket::ReadToBuffer() noexcept
 	const auto buffer = input.Write();
 	assert(!buffer.empty());
 
-	const auto nbytes = DirectRead(buffer.data, buffer.size);
+	const auto nbytes = DirectRead(buffer.data(), buffer.size());
 	if (nbytes > 0)
 		input.Append(nbytes);
 
@@ -73,7 +73,7 @@ BufferedSocket::ResumeInput() noexcept
 			return true;
 		}
 
-		const auto result = OnSocketInput(buffer.data, buffer.size);
+		const auto result = OnSocketInput(buffer.data(), buffer.size());
 		switch (result) {
 		case InputResult::MORE:
 			if (input.IsFull()) {
diff --git a/src/event/FullyBufferedSocket.cxx b/src/event/FullyBufferedSocket.cxx
index b8bfdf5f9..159f0df3b 100644
--- a/src/event/FullyBufferedSocket.cxx
+++ b/src/event/FullyBufferedSocket.cxx
@@ -58,7 +58,7 @@ FullyBufferedSocket::Flush() noexcept
 		return true;
 	}
 
-	auto nbytes = DirectWrite(data.data, data.size);
+	auto nbytes = DirectWrite(data.data(), data.size());
 	if (gcc_unlikely(nbytes <= 0))
 		return nbytes == 0;
 
@@ -82,7 +82,7 @@ FullyBufferedSocket::Write(const void *data, size_t length) noexcept
 
 	const bool was_empty = output.empty();
 
-	if (!output.Append(data, length)) {
+	if (!output.Append({(const std::byte *)data, length})) {
 		OnSocketError(std::make_exception_ptr(std::runtime_error("Output buffer is full")));
 		return false;
 	}
diff --git a/src/input/TextInputStream.cxx b/src/input/TextInputStream.cxx
index 0198ef1e5..d6f3e8796 100644
--- a/src/input/TextInputStream.cxx
+++ b/src/input/TextInputStream.cxx
@@ -39,13 +39,13 @@ TextInputStream::ReadLine()
 
 	while (true) {
 		auto dest = buffer.Write();
-		if (dest.size < 2) {
+		if (dest.size() < 2) {
 			/* line too long: terminate the current
 			   line */
 
 			assert(!dest.empty());
 			dest[0] = 0;
-			line = buffer.Read().data;
+			line = buffer.Read().data();
 			buffer.Clear();
 			return line;
 		}
@@ -53,9 +53,9 @@ TextInputStream::ReadLine()
 		/* reserve one byte for the null terminator if the
 		   last line is not terminated by a newline
 		   character */
-		--dest.size;
+		dest = dest.first(dest.size() - 1);
 
-		size_t nbytes = is->LockRead(dest.data, dest.size);
+		size_t nbytes = is->LockRead(dest.data(), dest.size());
 
 		buffer.Append(nbytes);
 
@@ -75,7 +75,7 @@ TextInputStream::ReadLine()
 			buffer.Clear();
 			return r.empty()
 				? nullptr
-				: r.data;
+				: r.data();
 		}
 	}
 }
diff --git a/src/io/BufferedOutputStream.cxx b/src/io/BufferedOutputStream.cxx
index e16e537b1..f30827259 100644
--- a/src/io/BufferedOutputStream.cxx
+++ b/src/io/BufferedOutputStream.cxx
@@ -44,10 +44,10 @@ bool
 BufferedOutputStream::AppendToBuffer(const void *data, std::size_t size) noexcept
 {
 	auto r = buffer.Write();
-	if (r.size < size)
+	if (r.size() < size)
 		return false;
 
-	memcpy(r.data, data, size);
+	memcpy(r.data(), data, size);
 	buffer.Append(size);
 	return true;
 }
@@ -88,10 +88,10 @@ BufferedOutputStream::Format(const char *fmt, ...)
 	/* format into the buffer */
 	std::va_list ap;
 	va_start(ap, fmt);
-	std::size_t size = vsnprintf((char *)r.data, r.size, fmt, ap);
+	std::size_t size = vsnprintf((char *)r.data(), r.size(), fmt, ap);
 	va_end(ap);
 
-	if (gcc_unlikely(size >= r.size)) {
+	if (gcc_unlikely(size >= r.size())) {
 		/* buffer was not large enough; flush it and try
 		   again */
 
@@ -99,20 +99,20 @@ BufferedOutputStream::Format(const char *fmt, ...)
 
 		r = buffer.Write();
 
-		if (gcc_unlikely(size >= r.size)) {
+		if (gcc_unlikely(size >= r.size())) {
 			/* still not enough space: grow the buffer and
 			   try again */
-			r.size = size + 1;
-			r.data = buffer.Write(r.size);
+			++size;
+			r = {buffer.Write(size), size};
 		}
 
 		/* format into the new buffer */
 		va_start(ap, fmt);
-		size = vsnprintf((char *)r.data, r.size, fmt, ap);
+		size = vsnprintf((char *)r.data(), r.size(), fmt, ap);
 		va_end(ap);
 
 		/* this time, it must fit */
-		assert(size < r.size);
+		assert(size < r.size());
 	}
 
 	buffer.Append(size);
@@ -140,7 +140,7 @@ BufferedOutputStream::WriteWideToUTF8(const wchar_t *src,
 	}
 
 	int length = WideCharToMultiByte(CP_UTF8, 0, src, src_length,
-					 (char *)r.data, r.size,
+					 (char *)r.data(), r.size(),
 					 nullptr, nullptr);
 	if (length <= 0) {
 		const auto error = GetLastError();
@@ -173,6 +173,6 @@ BufferedOutputStream::Flush()
 	if (r.empty())
 		return;
 
-	os.Write(r.data, r.size);
-	buffer.Consume(r.size);
+	os.Write(r.data(), r.size());
+	buffer.Consume(r.size());
 }
diff --git a/src/io/BufferedReader.cxx b/src/io/BufferedReader.cxx
index 95f48e73e..e8b7dd669 100644
--- a/src/io/BufferedReader.cxx
+++ b/src/io/BufferedReader.cxx
@@ -51,7 +51,7 @@ BufferedReader::Fill(bool need_more)
 		assert(!w.empty());
 	}
 
-	std::size_t nbytes = reader.Read(w.data, w.size);
+	std::size_t nbytes = reader.Read(w.data(), w.size());
 	if (nbytes == 0) {
 		eof = true;
 		return !need_more;
@@ -122,7 +122,7 @@ BufferedReader::ReadLine()
 	/* terminate the last line */
 	w[0] = 0;
 
-	char *line = buffer.Read().data;
+	char *line = buffer.Read().data();
 	buffer.Clear();
 	++line_number;
 	return line;
diff --git a/src/io/BufferedReader.hxx b/src/io/BufferedReader.hxx
index 035ac8d8e..33f9facc0 100644
--- a/src/io/BufferedReader.hxx
+++ b/src/io/BufferedReader.hxx
@@ -65,7 +65,7 @@ public:
 
 	[[gnu::pure]]
 	std::span<std::byte> Read() const noexcept {
-		return buffer.Read().ToVoid();
+		return std::as_writable_bytes(buffer.Read());
 	}
 
 	/**
diff --git a/src/lib/zlib/GunzipReader.cxx b/src/lib/zlib/GunzipReader.cxx
index eddc3d162..1b7988bd6 100644
--- a/src/lib/zlib/GunzipReader.cxx
+++ b/src/lib/zlib/GunzipReader.cxx
@@ -50,7 +50,7 @@ GunzipReader::FillBuffer()
 	auto w = buffer.Write();
 	assert(!w.empty());
 
-	std::size_t nbytes = next.Read(w.data, w.size);
+	std::size_t nbytes = next.Read(w.data(), w.size());
 	if (nbytes == 0)
 		return false;
 
@@ -78,8 +78,8 @@ GunzipReader::Read(void *data, std::size_t size)
 				flush = Z_FINISH;
 		}
 
-		z.next_in = r.data;
-		z.avail_in = r.size;
+		z.next_in = r.data();
+		z.avail_in = r.size();
 
 		int result = inflate(&z, flush);
 		if (result == Z_STREAM_END) {
@@ -88,7 +88,7 @@ GunzipReader::Read(void *data, std::size_t size)
 		} else if (result != Z_OK)
 			throw ZlibError(result);
 
-		buffer.Consume(r.size - z.avail_in);
+		buffer.Consume(r.size() - z.avail_in);
 
 		if (z.avail_out < size)
 			return size - z.avail_out;
diff --git a/src/util/DynamicFifoBuffer.hxx b/src/util/DynamicFifoBuffer.hxx
index 6ca0d029f..6fa529574 100644
--- a/src/util/DynamicFifoBuffer.hxx
+++ b/src/util/DynamicFifoBuffer.hxx
@@ -103,7 +103,7 @@ public:
 	 */
 	pointer Write(size_type n) noexcept {
 		WantWrite(n);
-		return Write().data;
+		return Write().data();
 	}
 
 	/**
diff --git a/src/util/ForeignFifoBuffer.hxx b/src/util/ForeignFifoBuffer.hxx
index 65f6bce94..9445267b7 100644
--- a/src/util/ForeignFifoBuffer.hxx
+++ b/src/util/ForeignFifoBuffer.hxx
@@ -1,5 +1,5 @@
 /*
- * Copyright 2003-2019 Max Kellermann <max.kellermann@gmail.com>
+ * Copyright 2003-2022 Max Kellermann <max.kellermann@gmail.com>
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -27,14 +27,12 @@
  * OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-#ifndef FOREIGN_FIFO_BUFFER_HXX
-#define FOREIGN_FIFO_BUFFER_HXX
-
-#include "WritableBuffer.hxx"
+#pragma once
 
 #include <algorithm>
 #include <cassert>
 #include <cstddef>
+#include <span>
 #include <utility>
 
 /**
@@ -50,7 +48,7 @@ template<typename T>
 class ForeignFifoBuffer {
 public:
 	using size_type = std::size_t;
-	using Range = WritableBuffer<T>;
+	using Range = std::span<T>;
 	using pointer = typename Range::pointer;
 	using const_pointer = typename Range::const_pointer;
 
@@ -211,9 +209,9 @@ public:
 
 	size_type Read(pointer p, size_type n) noexcept {
 		auto range = Read();
-		if (n > range.size)
-			n = range.size;
-		std::copy_n(range.data, n, p);
+		if (n > range.size())
+			n = range.size();
+		std::copy_n(range.data(), n, p);
 		Consume(n);
 		return n;
 	}
@@ -227,7 +225,7 @@ public:
 		auto r = src.Read();
 		auto w = Write();
 
-		if (w.size < r.size && head > 0) {
+		if (w.size() < r.size() && head > 0) {
 			/* if the source contains more data than we
 			   can append at the tail, try to make more
 			   room by shifting the head to 0 */
@@ -235,9 +233,9 @@ public:
 			w = Write();
 		}
 
-		const auto n = std::min(r.size, w.size);
+		const auto n = std::min(r.size(), w.size());
 
-		std::move(r.data, r.data + n, w.data);
+		std::move(r.data(), r.data() + n, w.data());
 		Append(n);
 		src.Consume(n);
 		return n;
@@ -258,5 +256,3 @@ protected:
 		head = 0;
 	}
 };
-
-#endif
diff --git a/src/util/PeakBuffer.cxx b/src/util/PeakBuffer.cxx
index 6af9df266..97dbccfbf 100644
--- a/src/util/PeakBuffer.cxx
+++ b/src/util/PeakBuffer.cxx
@@ -23,8 +23,6 @@
 #include <algorithm>
 #include <cassert>
 
-#include <string.h>
-
 PeakBuffer::~PeakBuffer() noexcept
 {
 	delete normal_buffer;
@@ -38,22 +36,22 @@ PeakBuffer::empty() const noexcept
 		(peak_buffer == nullptr || peak_buffer->empty());
 }
 
-WritableBuffer<void>
+std::span<std::byte>
 PeakBuffer::Read() const noexcept
 {
 	if (normal_buffer != nullptr) {
 		const auto p = normal_buffer->Read();
 		if (!p.empty())
-			return p.ToVoid();
+			return p;
 	}
 
 	if (peak_buffer != nullptr) {
 		const auto p = peak_buffer->Read();
 		if (!p.empty())
-			return p.ToVoid();
+			return p;
 	}
 
-	return nullptr;
+	return {};
 }
 
 void
@@ -77,10 +75,9 @@ PeakBuffer::Consume(std::size_t length) noexcept
 
 static std::size_t
 AppendTo(DynamicFifoBuffer<std::byte> &buffer,
-	 const void *data, size_t length) noexcept
+	 std::span<const std::byte> src) noexcept
 {
-	assert(data != nullptr);
-	assert(length > 0);
+	assert(!src.empty());
 
 	std::size_t total = 0;
 
@@ -89,37 +86,35 @@ AppendTo(DynamicFifoBuffer<std::byte> &buffer,
 		if (p.empty())
 			break;
 
-		const std::size_t nbytes = std::min(length, p.size);
-		memcpy(p.data, data, nbytes);
+		const std::size_t nbytes = std::min(src.size(), p.size());
+		std::copy_n(src.begin(), nbytes, p.begin());
 		buffer.Append(nbytes);
 
-		data = (const std::byte *)data + nbytes;
-		length -= nbytes;
+		src = src.subspan(nbytes);
 		total += nbytes;
-	} while (length > 0);
+	} while (!src.empty());
 
 	return total;
 }
 
 bool
-PeakBuffer::Append(const void *data, std::size_t length)
+PeakBuffer::Append(std::span<const std::byte> src)
 {
-	if (length == 0)
+	if (src.empty())
 		return true;
 
 	if (peak_buffer != nullptr && !peak_buffer->empty()) {
-		std::size_t nbytes = AppendTo(*peak_buffer, data, length);
-		return nbytes == length;
+		std::size_t nbytes = AppendTo(*peak_buffer, src);
+		return nbytes == src.size();
 	}
 
 	if (normal_buffer == nullptr)
 		normal_buffer = new DynamicFifoBuffer<std::byte>(normal_size);
 
-	std::size_t nbytes = AppendTo(*normal_buffer, data, length);
+	std::size_t nbytes = AppendTo(*normal_buffer, src);
 	if (nbytes > 0) {
-		data = (const std::byte *)data + nbytes;
-		length -= nbytes;
-		if (length == 0)
+		src = src.subspan(nbytes);
+		if (src.empty())
 			return true;
 	}
 
@@ -130,6 +125,6 @@ PeakBuffer::Append(const void *data, std::size_t length)
 			return false;
 	}
 
-	nbytes = AppendTo(*peak_buffer, data, length);
-	return nbytes == length;
+	nbytes = AppendTo(*peak_buffer, src);
+	return nbytes == src.size();
 }
diff --git a/src/util/PeakBuffer.hxx b/src/util/PeakBuffer.hxx
index ba87da91c..e21bd6b9f 100644
--- a/src/util/PeakBuffer.hxx
+++ b/src/util/PeakBuffer.hxx
@@ -21,8 +21,8 @@
 #define MPD_PEAK_BUFFER_HXX
 
 #include <cstddef>
+#include <span>
 
-template<typename T> struct WritableBuffer;
 template<typename T> class DynamicFifoBuffer;
 
 /**
@@ -61,11 +61,11 @@ public:
 	bool empty() const noexcept;
 
 	[[gnu::pure]]
-	WritableBuffer<void> Read() const noexcept;
+	std::span<std::byte> Read() const noexcept;
 
 	void Consume(std::size_t length) noexcept;
 
-	bool Append(const void *data, std::size_t length);
+	bool Append(std::span<const std::byte> src);
 };
 
 #endif
diff --git a/src/util/StaticFifoBuffer.hxx b/src/util/StaticFifoBuffer.hxx
index 6b7f6d005..b0e8b8054 100644
--- a/src/util/StaticFifoBuffer.hxx
+++ b/src/util/StaticFifoBuffer.hxx
@@ -1,5 +1,5 @@
 /*
- * Copyright 2003-2019 Max Kellermann <max.kellermann@gmail.com>
+ * Copyright 2003-2022 Max Kellermann <max.kellermann@gmail.com>
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -27,14 +27,12 @@
  * OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-#ifndef STATIC_FIFO_BUFFER_HXX
-#define STATIC_FIFO_BUFFER_HXX
-
-#include "WritableBuffer.hxx"
+#pragma once
 
 #include <algorithm>
 #include <cassert>
 #include <cstddef>
+#include <span>
 #include <utility>
 
 /**
@@ -46,7 +44,7 @@ template<class T, size_t size>
 class StaticFifoBuffer {
 public:
 	using size_type = std::size_t;
-	using Range = WritableBuffer<T>;
+	using Range = std::span<T>;
 
 protected:
 	size_type head = 0, tail = 0;
@@ -132,5 +130,3 @@ public:
 		head += n;
 	}
 };
-
-#endif
diff --git a/src/util/TextFile.hxx b/src/util/TextFile.hxx
index 1867f7774..95733a6be 100644
--- a/src/util/TextFile.hxx
+++ b/src/util/TextFile.hxx
@@ -37,16 +37,16 @@ char *
 ReadBufferedLine(B &buffer)
 {
 	auto r = buffer.Read();
-	char *newline = reinterpret_cast<char*>(std::memchr(r.data, '\n', r.size));
+	char *newline = reinterpret_cast<char*>(std::memchr(r.data(), '\n', r.size()));
 	if (newline == nullptr)
 		return nullptr;
 
-	buffer.Consume(newline + 1 - r.data);
+	buffer.Consume(newline + 1 - r.data());
 
-	if (newline > r.data && newline[-1] == '\r')
+	if (newline > r.data() && newline[-1] == '\r')
 		--newline;
 	*newline = 0;
-	return r.data;
+	return r.data();
 }
 
 #endif
diff --git a/test/run_convert.cxx b/test/run_convert.cxx
index b7a687c03..b78d159b4 100644
--- a/test/run_convert.cxx
+++ b/test/run_convert.cxx
@@ -116,7 +116,7 @@ RunConvert(PcmConvert &convert, size_t in_frame_size,
 			const auto dest = buffer.Write();
 			assert(!dest.empty());
 
-			ssize_t nbytes = in_fd.Read(dest.data, dest.size);
+			ssize_t nbytes = in_fd.Read(dest.data(), dest.size());
 			if (nbytes <= 0)
 				break;
 
@@ -126,13 +126,13 @@ RunConvert(PcmConvert &convert, size_t in_frame_size,
 		auto src = buffer.Read();
 		assert(!src.empty());
 
-		src.size -= src.size % in_frame_size;
+		src = src.first(src.size() - src.size() % in_frame_size);
 		if (src.empty())
 			continue;
 
-		buffer.Consume(src.size);
+		buffer.Consume(src.size());
 
-		auto output = convert.Convert({src.data, src.size});
+		auto output = convert.Convert({src.data(), src.size()});
 		out_fd.FullWrite(output.data, output.size);
 	}
 
diff --git a/test/run_output.cxx b/test/run_output.cxx
index bc6782c32..e67ec25d3 100644
--- a/test/run_output.cxx
+++ b/test/run_output.cxx
@@ -141,7 +141,7 @@ RunOutput(AudioOutput &ao, AudioFormat audio_format,
 			const auto dest = buffer.Write();
 			assert(!dest.empty());
 
-			ssize_t nbytes = in_fd.Read(dest.data, dest.size);
+			ssize_t nbytes = in_fd.Read(dest.data(), dest.size());
 			if (nbytes <= 0)
 				break;
 
@@ -151,13 +151,13 @@ RunOutput(AudioOutput &ao, AudioFormat audio_format,
 		auto src = buffer.Read();
 		assert(!src.empty());
 
-		src.size -= src.size % in_frame_size;
+		src = src.first(src.size() - src.size() % in_frame_size);
 		if (src.empty())
 			continue;
 
-		size_t consumed = ao.Play(src.data, src.size);
+		size_t consumed = ao.Play(src.data(), src.size());
 
-		assert(consumed <= src.size);
+		assert(consumed <= src.size());
 		assert(consumed % in_frame_size == 0);
 
 		buffer.Consume(consumed);