From bbc64a2eb5ed8a01324379636b5708e317f39087 Mon Sep 17 00:00:00 2001
From: Desour <ds.desour@proton.me>
Date: Thu, 28 Sep 2023 18:20:53 +0200
Subject: [PATCH] Split sound_openal_internal into serval files

---
 src/client/CMakeLists.txt                  |    9 +-
 src/client/sound/al_helpers.cpp            |   53 +
 src/client/sound/al_helpers.h              |  118 ++
 src/client/sound/ogg_file.cpp              |  179 +++
 src/client/sound/ogg_file.h                |   94 ++
 src/client/sound/playing_sound.cpp         |  241 ++++
 src/client/sound/playing_sound.h           |  107 ++
 src/client/sound/proxy_sound_manager.cpp   |  163 +++
 src/client/sound/proxy_sound_manager.h     |   71 +
 src/client/sound/sound_constants.h         |  119 ++
 src/client/sound/sound_data.cpp            |  231 ++++
 src/client/sound/sound_data.h              |  173 +++
 src/client/sound/sound_manager.cpp         |  523 ++++++++
 src/client/sound/sound_manager.h           |  171 +++
 src/client/sound/sound_manager_messages.h  |   80 ++
 src/client/sound/sound_openal.cpp          |    4 +-
 src/client/sound/sound_openal_internal.cpp | 1363 --------------------
 src/client/sound/sound_openal_internal.h   |  750 -----------
 src/client/sound/sound_singleton.cpp       |   69 +
 src/client/sound/sound_singleton.h         |   60 +
 20 files changed, 2463 insertions(+), 2115 deletions(-)
 create mode 100644 src/client/sound/al_helpers.cpp
 create mode 100644 src/client/sound/al_helpers.h
 create mode 100644 src/client/sound/ogg_file.cpp
 create mode 100644 src/client/sound/ogg_file.h
 create mode 100644 src/client/sound/playing_sound.cpp
 create mode 100644 src/client/sound/playing_sound.h
 create mode 100644 src/client/sound/proxy_sound_manager.cpp
 create mode 100644 src/client/sound/proxy_sound_manager.h
 create mode 100644 src/client/sound/sound_constants.h
 create mode 100644 src/client/sound/sound_data.cpp
 create mode 100644 src/client/sound/sound_data.h
 create mode 100644 src/client/sound/sound_manager.cpp
 create mode 100644 src/client/sound/sound_manager.h
 create mode 100644 src/client/sound/sound_manager_messages.h
 delete mode 100644 src/client/sound/sound_openal_internal.cpp
 delete mode 100644 src/client/sound/sound_openal_internal.h
 create mode 100644 src/client/sound/sound_singleton.cpp
 create mode 100644 src/client/sound/sound_singleton.h

diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt
index 2c64f62d2..5ec61795a 100644
--- a/src/client/CMakeLists.txt
+++ b/src/client/CMakeLists.txt
@@ -2,8 +2,15 @@ set(sound_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/sound.cpp)
 
 if(USE_SOUND)
 	set(sound_SRCS ${sound_SRCS}
+		${CMAKE_CURRENT_SOURCE_DIR}/sound/al_helpers.cpp
+		${CMAKE_CURRENT_SOURCE_DIR}/sound/ogg_file.cpp
+		${CMAKE_CURRENT_SOURCE_DIR}/sound/playing_sound.cpp
+		${CMAKE_CURRENT_SOURCE_DIR}/sound/proxy_sound_manager.cpp
+		${CMAKE_CURRENT_SOURCE_DIR}/sound/sound_data.cpp
+		${CMAKE_CURRENT_SOURCE_DIR}/sound/sound_manager.cpp
 		${CMAKE_CURRENT_SOURCE_DIR}/sound/sound_openal.cpp
-		${CMAKE_CURRENT_SOURCE_DIR}/sound/sound_openal_internal.cpp)
+		${CMAKE_CURRENT_SOURCE_DIR}/sound/sound_singleton.cpp
+	)
 	set(SOUND_INCLUDE_DIRS
 		${OPENAL_INCLUDE_DIR}
 		${VORBIS_INCLUDE_DIR}
diff --git a/src/client/sound/al_helpers.cpp b/src/client/sound/al_helpers.cpp
new file mode 100644
index 000000000..3b104a0b9
--- /dev/null
+++ b/src/client/sound/al_helpers.cpp
@@ -0,0 +1,53 @@
+/*
+Minetest
+Copyright (C) 2022 DS
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+OpenAL support based on work by:
+Copyright (C) 2011 Sebastian 'Bahamada' Rühl
+Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com>
+Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; ifnot, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "al_helpers.h"
+
+/*
+ * RAIIALSoundBuffer
+ */
+
+RAIIALSoundBuffer &RAIIALSoundBuffer::operator=(RAIIALSoundBuffer &&other) noexcept
+{
+	if (&other != this)
+		reset(other.release());
+	return *this;
+}
+
+void RAIIALSoundBuffer::reset(ALuint buf) noexcept
+{
+	if (m_buffer != 0) {
+		alDeleteBuffers(1, &m_buffer);
+		warn_if_al_error("Failed to free sound buffer");
+	}
+
+	m_buffer = buf;
+}
+
+RAIIALSoundBuffer RAIIALSoundBuffer::generate() noexcept
+{
+	ALuint buf;
+	alGenBuffers(1, &buf);
+	return RAIIALSoundBuffer(buf);
+}
diff --git a/src/client/sound/al_helpers.h b/src/client/sound/al_helpers.h
new file mode 100644
index 000000000..8e12db673
--- /dev/null
+++ b/src/client/sound/al_helpers.h
@@ -0,0 +1,118 @@
+/*
+Minetest
+Copyright (C) 2022 DS
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+OpenAL support based on work by:
+Copyright (C) 2011 Sebastian 'Bahamada' Rühl
+Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com>
+Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; ifnot, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#pragma once
+
+#include "log.h"
+#include "util/basic_macros.h"
+#include "irr_v3d.h"
+
+#if defined(_WIN32)
+	#include <al.h>
+	#include <alc.h>
+	//#include <alext.h>
+#elif defined(__APPLE__)
+	#define OPENAL_DEPRECATED
+	#include <OpenAL/al.h>
+	#include <OpenAL/alc.h>
+	//#include <OpenAL/alext.h>
+#else
+	#include <AL/al.h>
+	#include <AL/alc.h>
+	#include <AL/alext.h>
+#endif
+
+#include <utility>
+
+inline const char *getAlErrorString(ALenum err) noexcept
+{
+	switch (err) {
+	case AL_NO_ERROR:
+		return "no error";
+	case AL_INVALID_NAME:
+		return "invalid name";
+	case AL_INVALID_ENUM:
+		return "invalid enum";
+	case AL_INVALID_VALUE:
+		return "invalid value";
+	case AL_INVALID_OPERATION:
+		return "invalid operation";
+	case AL_OUT_OF_MEMORY:
+		return "out of memory";
+	default:
+		return "<unknown OpenAL error>";
+	}
+}
+
+inline ALenum warn_if_al_error(const char *desc)
+{
+	ALenum err = alGetError();
+	if (err == AL_NO_ERROR)
+		return err;
+	warningstream << "[OpenAL Error] " << desc << ": " << getAlErrorString(err)
+			<< std::endl;
+	return err;
+}
+
+/**
+ * Transforms vectors from a left-handed coordinate system to a right-handed one
+ * and vice-versa.
+ * (Needed because Minetest uses a left-handed one and OpenAL a right-handed one.)
+ */
+inline v3f swap_handedness(v3f v) noexcept
+{
+	return v3f(-v.X, v.Y, v.Z);
+}
+
+/**
+ * RAII wrapper for openal sound buffers.
+ */
+struct RAIIALSoundBuffer final
+{
+	RAIIALSoundBuffer() noexcept = default;
+	explicit RAIIALSoundBuffer(ALuint buffer) noexcept : m_buffer(buffer) {};
+
+	~RAIIALSoundBuffer() noexcept { reset(0); }
+
+	DISABLE_CLASS_COPY(RAIIALSoundBuffer)
+
+	RAIIALSoundBuffer(RAIIALSoundBuffer &&other) noexcept : m_buffer(other.release()) {}
+	RAIIALSoundBuffer &operator=(RAIIALSoundBuffer &&other) noexcept;
+
+	ALuint get() noexcept { return m_buffer; }
+
+	ALuint release() noexcept { return std::exchange(m_buffer, 0); }
+
+	void reset(ALuint buf) noexcept;
+
+	static RAIIALSoundBuffer generate() noexcept;
+
+private:
+	// According to openal specification:
+	// > Deleting buffer name 0 is a legal NOP.
+	//
+	// and:
+	// > [...] the NULL buffer (i.e., 0) which can always be queued.
+	ALuint m_buffer = 0;
+};
diff --git a/src/client/sound/ogg_file.cpp b/src/client/sound/ogg_file.cpp
new file mode 100644
index 000000000..729cf7d53
--- /dev/null
+++ b/src/client/sound/ogg_file.cpp
@@ -0,0 +1,179 @@
+/*
+Minetest
+Copyright (C) 2022 DS
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+OpenAL support based on work by:
+Copyright (C) 2011 Sebastian 'Bahamada' Rühl
+Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com>
+Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; ifnot, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "ogg_file.h"
+
+#include <cstring> // memcpy
+
+/*
+ * OggVorbisBufferSource struct
+ */
+
+size_t OggVorbisBufferSource::read_func(void *ptr, size_t size, size_t nmemb,
+		void *datasource) noexcept
+{
+	OggVorbisBufferSource *s = (OggVorbisBufferSource *)datasource;
+	size_t copied_size = MYMIN(s->buf.size() - s->cur_offset, size);
+	memcpy(ptr, s->buf.data() + s->cur_offset, copied_size);
+	s->cur_offset += copied_size;
+	return copied_size;
+}
+
+int OggVorbisBufferSource::seek_func(void *datasource, ogg_int64_t offset, int whence) noexcept
+{
+	OggVorbisBufferSource *s = (OggVorbisBufferSource *)datasource;
+	if (whence == SEEK_SET) {
+		if (offset < 0 || (size_t)offset > s->buf.size()) {
+			// offset out of bounds
+			return -1;
+		}
+		s->cur_offset = offset;
+		return 0;
+	} else if (whence == SEEK_CUR) {
+		if ((size_t)MYMIN(-offset, 0) > s->cur_offset
+				|| s->cur_offset + offset > s->buf.size()) {
+			// offset out of bounds
+			return -1;
+		}
+		s->cur_offset += offset;
+		return 0;
+	} else if (whence == SEEK_END) {
+		if (offset > 0 || (size_t)-offset > s->buf.size()) {
+			// offset out of bounds
+			return -1;
+		}
+		s->cur_offset = s->buf.size() - offset;
+		return 0;
+	}
+	return -1;
+}
+
+int OggVorbisBufferSource::close_func(void *datasource) noexcept
+{
+	auto s = reinterpret_cast<OggVorbisBufferSource *>(datasource);
+	delete s;
+	return 0;
+}
+
+long OggVorbisBufferSource::tell_func(void *datasource) noexcept
+{
+	OggVorbisBufferSource *s = (OggVorbisBufferSource *)datasource;
+	return s->cur_offset;
+}
+
+const ov_callbacks OggVorbisBufferSource::s_ov_callbacks = {
+	&OggVorbisBufferSource::read_func,
+	&OggVorbisBufferSource::seek_func,
+	&OggVorbisBufferSource::close_func,
+	&OggVorbisBufferSource::tell_func
+};
+
+/*
+ * RAIIOggFile struct
+ */
+
+std::optional<OggFileDecodeInfo> RAIIOggFile::getDecodeInfo(const std::string &filename_for_logging)
+{
+	OggFileDecodeInfo ret;
+
+	vorbis_info *pInfo = ov_info(&m_file, -1);
+	if (!pInfo)
+		return std::nullopt;
+
+	ret.name_for_logging = filename_for_logging;
+
+	if (pInfo->channels == 1) {
+		ret.is_stereo = false;
+		ret.format = AL_FORMAT_MONO16;
+		ret.bytes_per_sample = 2;
+	} else if (pInfo->channels == 2) {
+		ret.is_stereo = true;
+		ret.format = AL_FORMAT_STEREO16;
+		ret.bytes_per_sample = 4;
+	} else {
+		warningstream << "Audio: Can't decode. Sound is neither mono nor stereo: "
+				<< ret.name_for_logging << std::endl;
+		return std::nullopt;
+	}
+
+	ret.freq = pInfo->rate;
+
+	ret.length_samples = static_cast<ALuint>(ov_pcm_total(&m_file, -1));
+	ret.length_seconds = static_cast<f32>(ov_time_total(&m_file, -1));
+
+	return ret;
+}
+
+RAIIALSoundBuffer RAIIOggFile::loadBuffer(const OggFileDecodeInfo &decode_info,
+		ALuint pcm_start, ALuint pcm_end)
+{
+	constexpr int endian = 0; // 0 for Little-Endian, 1 for Big-Endian
+	constexpr int word_size = 2; // we use s16 samples
+	constexpr int word_signed = 1; // ^
+
+	// seek
+	if (ov_pcm_tell(&m_file) != pcm_start) {
+		if (ov_pcm_seek(&m_file, pcm_start) != 0) {
+			warningstream << "Audio: Error decoding (could not seek) "
+					<< decode_info.name_for_logging << std::endl;
+			return RAIIALSoundBuffer();
+		}
+	}
+
+	const size_t size = static_cast<size_t>(pcm_end - pcm_start)
+			* decode_info.bytes_per_sample;
+
+	std::unique_ptr<char[]> snd_buffer(new char[size]);
+
+	// read size bytes
+	size_t read_count = 0;
+	int bitStream;
+	while (read_count < size) {
+		// Read up to a buffer's worth of decoded sound data
+		long num_bytes = ov_read(&m_file, &snd_buffer[read_count], size - read_count,
+				endian, word_size, word_signed, &bitStream);
+
+		if (num_bytes <= 0) {
+			warningstream << "Audio: Error decoding "
+					<< decode_info.name_for_logging << std::endl;
+			return RAIIALSoundBuffer();
+		}
+
+		read_count += num_bytes;
+	}
+
+	// load buffer to openal
+	RAIIALSoundBuffer snd_buffer_id = RAIIALSoundBuffer::generate();
+	alBufferData(snd_buffer_id.get(), decode_info.format, &(snd_buffer[0]), size,
+			decode_info.freq);
+
+	ALenum error = alGetError();
+	if (error != AL_NO_ERROR) {
+		warningstream << "Audio: OpenAL error: " << getAlErrorString(error)
+				<< "preparing sound buffer for sound \""
+				<< decode_info.name_for_logging << "\"" << std::endl;
+	}
+
+	return snd_buffer_id;
+}
diff --git a/src/client/sound/ogg_file.h b/src/client/sound/ogg_file.h
new file mode 100644
index 000000000..fe41239e0
--- /dev/null
+++ b/src/client/sound/ogg_file.h
@@ -0,0 +1,94 @@
+/*
+Minetest
+Copyright (C) 2022 DS
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+OpenAL support based on work by:
+Copyright (C) 2011 Sebastian 'Bahamada' Rühl
+Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com>
+Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; ifnot, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#pragma once
+
+#include "al_helpers.h"
+#include <vorbis/vorbisfile.h>
+#include <optional>
+#include <string>
+
+/**
+ * For vorbisfile to read from our buffer instead of from a file.
+ */
+struct OggVorbisBufferSource {
+	std::string buf;
+	size_t cur_offset = 0;
+
+	static size_t read_func(void *ptr, size_t size, size_t nmemb, void *datasource) noexcept;
+	static int seek_func(void *datasource, ogg_int64_t offset, int whence) noexcept;
+	static int close_func(void *datasource) noexcept;
+	static long tell_func(void *datasource) noexcept;
+
+	static const ov_callbacks s_ov_callbacks;
+};
+
+/**
+ * Metadata of an Ogg-Vorbis file, used for decoding.
+ * We query this information once and store it in this struct.
+ */
+struct OggFileDecodeInfo {
+	std::string name_for_logging;
+	bool is_stereo;
+	ALenum format; // AL_FORMAT_MONO16 or AL_FORMAT_STEREO16
+	size_t bytes_per_sample;
+	ALsizei freq;
+	ALuint length_samples = 0;
+	f32 length_seconds = 0.0f;
+};
+
+/**
+ * RAII wrapper for OggVorbis_File.
+ */
+struct RAIIOggFile {
+	bool m_needs_clear = false;
+	OggVorbis_File m_file;
+
+	RAIIOggFile() = default;
+
+	DISABLE_CLASS_COPY(RAIIOggFile)
+
+	~RAIIOggFile() noexcept
+	{
+		if (m_needs_clear)
+			ov_clear(&m_file);
+	}
+
+	OggVorbis_File *get() { return &m_file; }
+
+	std::optional<OggFileDecodeInfo> getDecodeInfo(const std::string &filename_for_logging);
+
+	/**
+	 * Main function for loading ogg vorbis sounds.
+	 * Loads exactly the specified interval of PCM-data, and creates an OpenAL
+	 * buffer with it.
+	 *
+	 * @param decode_info Cached meta information of the file.
+	 * @param pcm_start First sample in the interval.
+	 * @param pcm_end One after last sample of the interval (=> exclusive).
+	 * @return An AL sound buffer, or a 0-buffer on failure.
+	 */
+	RAIIALSoundBuffer loadBuffer(const OggFileDecodeInfo &decode_info, ALuint pcm_start,
+			ALuint pcm_end);
+};
diff --git a/src/client/sound/playing_sound.cpp b/src/client/sound/playing_sound.cpp
new file mode 100644
index 000000000..033020e9b
--- /dev/null
+++ b/src/client/sound/playing_sound.cpp
@@ -0,0 +1,241 @@
+/*
+Minetest
+Copyright (C) 2022 DS
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+OpenAL support based on work by:
+Copyright (C) 2011 Sebastian 'Bahamada' Rühl
+Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com>
+Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; ifnot, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "playing_sound.h"
+
+#include "debug.h"
+#include <cassert>
+#include <cmath>
+
+PlayingSound::PlayingSound(ALuint source_id, std::shared_ptr<ISoundDataOpen> data,
+		bool loop, f32 volume, f32 pitch, f32 start_time,
+		const std::optional<std::pair<v3f, v3f>> &pos_vel_opt)
+	: m_source_id(source_id), m_data(std::move(data)), m_looping(loop),
+	m_is_positional(pos_vel_opt.has_value())
+{
+	// Calculate actual start_time (see lua_api.txt for specs)
+	f32 len_seconds = m_data->m_decode_info.length_seconds;
+	f32 len_samples = m_data->m_decode_info.length_samples;
+	if (!m_looping) {
+		if (start_time < 0.0f) {
+			start_time = std::fmax(start_time + len_seconds, 0.0f);
+		} else if (start_time >= len_seconds) {
+			// No sound
+			m_next_sample_pos = len_samples;
+			return;
+		}
+	} else {
+		// Modulo offset to be within looping time
+		start_time = start_time - std::floor(start_time / len_seconds) * len_seconds;
+	}
+
+	// Queue first buffers
+
+	m_next_sample_pos = std::min((start_time / len_seconds) * len_samples, len_samples);
+
+	if (m_looping && m_next_sample_pos == len_samples)
+		m_next_sample_pos = 0;
+
+	if (!m_data->isStreaming()) {
+		// If m_next_sample_pos >= len_samples, buf will be 0, and setting it as
+		// AL_BUFFER is a NOP (source stays AL_UNDETERMINED). => No sound will be
+		// played.
+
+		auto [buf, buf_end, offset_in_buf] = m_data->getOrLoadBufferAt(m_next_sample_pos);
+		m_next_sample_pos = buf_end;
+
+		alSourcei(m_source_id, AL_BUFFER, buf);
+		alSourcei(m_source_id, AL_SAMPLE_OFFSET, offset_in_buf);
+
+		alSourcei(m_source_id, AL_LOOPING, m_looping ? AL_TRUE : AL_FALSE);
+
+		warn_if_al_error("when creating non-streaming sound");
+
+	} else {
+		// Start with 2 buffers
+		ALuint buf_ids[2];
+
+		// If m_next_sample_pos >= len_samples (happens only if not looped), one
+		// or both of buf_ids will be 0. Queuing 0 is a NOP.
+
+		auto [buf0, buf0_end, offset_in_buf0] = m_data->getOrLoadBufferAt(m_next_sample_pos);
+		buf_ids[0] = buf0;
+		m_next_sample_pos = buf0_end;
+
+		if (m_looping && m_next_sample_pos == len_samples)
+			m_next_sample_pos = 0;
+
+		auto [buf1, buf1_end, offset_in_buf1] = m_data->getOrLoadBufferAt(m_next_sample_pos);
+		buf_ids[1] = buf1;
+		m_next_sample_pos = buf1_end;
+		assert(offset_in_buf1 == 0);
+
+		alSourceQueueBuffers(m_source_id, 2, buf_ids);
+		alSourcei(m_source_id, AL_SAMPLE_OFFSET, offset_in_buf0);
+
+		// We can't use AL_LOOPING because more buffers are queued later
+		// looping is therefore done manually
+
+		m_stopped_means_dead = false;
+
+		warn_if_al_error("when creating streaming sound");
+	}
+
+	// Set initial pos, volume, pitch
+	if (m_is_positional) {
+		updatePosVel(pos_vel_opt->first, pos_vel_opt->second);
+	} else {
+		// Make position-less
+		alSourcei(m_source_id, AL_SOURCE_RELATIVE, true);
+		alSource3f(m_source_id, AL_POSITION, 0.0f, 0.0f, 0.0f);
+		alSource3f(m_source_id, AL_VELOCITY, 0.0f, 0.0f, 0.0f);
+		warn_if_al_error("PlayingSound::PlayingSound at making position-less");
+	}
+	setGain(volume);
+	setPitch(pitch);
+}
+
+bool PlayingSound::stepStream()
+{
+	if (isDead())
+		return false;
+
+	// unqueue finished buffers
+	ALint num_unqueued_bufs = 0;
+	alGetSourcei(m_source_id, AL_BUFFERS_PROCESSED, &num_unqueued_bufs);
+	if (num_unqueued_bufs == 0)
+		return true;
+	// We always have 2 buffers enqueued at most
+	SANITY_CHECK(num_unqueued_bufs <= 2);
+	ALuint unqueued_buffer_ids[2];
+	alSourceUnqueueBuffers(m_source_id, num_unqueued_bufs, unqueued_buffer_ids);
+
+	// Fill up again
+	for (ALint i = 0; i < num_unqueued_bufs; ++i) {
+		if (m_next_sample_pos == m_data->m_decode_info.length_samples) {
+			// Reached end
+			if (m_looping) {
+				m_next_sample_pos = 0;
+			} else {
+				m_stopped_means_dead = true;
+				return false;
+			}
+		}
+
+		auto [buf, buf_end, offset_in_buf] = m_data->getOrLoadBufferAt(m_next_sample_pos);
+		m_next_sample_pos = buf_end;
+		assert(offset_in_buf == 0);
+
+		alSourceQueueBuffers(m_source_id, 1, &buf);
+
+		// Start again if queue was empty and resulted in stop
+		if (getState() == AL_STOPPED) {
+			play();
+			warningstream << "PlayingSound::stepStream: Sound queue ran empty for \""
+					<< m_data->m_decode_info.name_for_logging << "\"" << std::endl;
+		}
+	}
+
+	return true;
+}
+
+bool PlayingSound::fade(f32 step, f32 target_gain) noexcept
+{
+	bool already_fading = m_fade_state.has_value();
+
+	target_gain = MYMAX(target_gain, 0.0f); // 0.0f if nan
+	step = target_gain - getGain() > 0.0f ? std::abs(step) : -std::abs(step);
+
+	m_fade_state = FadeState{step, target_gain};
+
+	return !already_fading;
+}
+
+bool PlayingSound::doFade(f32 dtime) noexcept
+{
+	if (!m_fade_state || isDead())
+		return false;
+
+	FadeState &fade = *m_fade_state;
+	assert(fade.step != 0.0f);
+
+	f32 current_gain = getGain();
+	current_gain += fade.step * dtime;
+
+	if (fade.step < 0.0f)
+		current_gain = std::max(current_gain, fade.target_gain);
+	else
+		current_gain = std::min(current_gain, fade.target_gain);
+
+	if (current_gain <= 0.0f) {
+		// stop sound
+		m_stopped_means_dead = true;
+		alSourceStop(m_source_id);
+
+		m_fade_state = std::nullopt;
+		return false;
+	}
+
+	setGain(current_gain);
+
+	if (current_gain == fade.target_gain) {
+		m_fade_state = std::nullopt;
+		return false;
+	} else {
+		return true;
+	}
+}
+
+void PlayingSound::updatePosVel(const v3f &pos, const v3f &vel) noexcept
+{
+	alSourcei(m_source_id, AL_SOURCE_RELATIVE, false);
+	alSource3f(m_source_id, AL_POSITION, pos.X, pos.Y, pos.Z);
+	alSource3f(m_source_id, AL_VELOCITY, vel.X, vel.Y, vel.Z);
+	// Using alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED) and setting reference
+	// distance to clamp gain at <1 node distance avoids excessive volume when
+	// closer.
+	alSourcef(m_source_id, AL_REFERENCE_DISTANCE, 1.0f);
+
+	warn_if_al_error("PlayingSound::updatePosVel");
+}
+
+void PlayingSound::setGain(f32 gain) noexcept
+{
+	// AL_REFERENCE_DISTANCE was once reduced from 3 nodes to 1 node.
+	// We compensate this by multiplying the volume by 3.
+	if (m_is_positional)
+		gain *= 3.0f;
+
+	alSourcef(m_source_id, AL_GAIN, gain);
+}
+
+f32 PlayingSound::getGain() noexcept
+{
+	ALfloat gain;
+	alGetSourcef(m_source_id, AL_GAIN, &gain);
+	// Same as above, but inverse.
+	if (m_is_positional)
+		gain *= 1.0f/3.0f;
+	return gain;
+}
diff --git a/src/client/sound/playing_sound.h b/src/client/sound/playing_sound.h
new file mode 100644
index 000000000..066e1af42
--- /dev/null
+++ b/src/client/sound/playing_sound.h
@@ -0,0 +1,107 @@
+/*
+Minetest
+Copyright (C) 2022 DS
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+OpenAL support based on work by:
+Copyright (C) 2011 Sebastian 'Bahamada' Rühl
+Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com>
+Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; ifnot, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#pragma once
+
+#include "sound_data.h"
+
+/**
+ * A sound that is currently played.
+ * Can be streaming.
+ * Can be fading.
+ */
+class PlayingSound final
+{
+	struct FadeState {
+		f32 step;
+		f32 target_gain;
+	};
+
+	ALuint m_source_id;
+	std::shared_ptr<ISoundDataOpen> m_data;
+	ALuint m_next_sample_pos = 0;
+	bool m_looping;
+	bool m_is_positional;
+	bool m_stopped_means_dead = true;
+	std::optional<FadeState> m_fade_state = std::nullopt;
+
+public:
+	PlayingSound(ALuint source_id, std::shared_ptr<ISoundDataOpen> data, bool loop,
+			f32 volume, f32 pitch, f32 start_time,
+			const std::optional<std::pair<v3f, v3f>> &pos_vel_opt);
+
+	~PlayingSound() noexcept
+	{
+		alDeleteSources(1, &m_source_id);
+	}
+
+	DISABLE_CLASS_COPY(PlayingSound)
+
+	// return false means streaming finished
+	bool stepStream();
+
+	// retruns true if it wasn't fading already
+	bool fade(f32 step, f32 target_gain) noexcept;
+
+	// returns true if more fade is needed later
+	bool doFade(f32 dtime) noexcept;
+
+	void updatePosVel(const v3f &pos, const v3f &vel) noexcept;
+
+	void setGain(f32 gain) noexcept;
+
+	f32 getGain() noexcept;
+
+	void setPitch(f32 pitch) noexcept { alSourcef(m_source_id, AL_PITCH, pitch); }
+
+	bool isStreaming() const noexcept { return m_data->isStreaming(); }
+
+	void play() noexcept { alSourcePlay(m_source_id); }
+
+	// returns one of AL_INITIAL, AL_PLAYING, AL_PAUSED, AL_STOPPED
+	ALint getState() noexcept
+	{
+		ALint state;
+		alGetSourcei(m_source_id, AL_SOURCE_STATE, &state);
+		return state;
+	}
+
+	bool isDead() noexcept
+	{
+		// streaming sounds can (but should not) stop because the queue runs empty
+		return m_stopped_means_dead && getState() == AL_STOPPED;
+	}
+
+	void pause() noexcept
+	{
+		// this is a NOP if state != AL_PLAYING
+		alSourcePause(m_source_id);
+	}
+
+	void resume() noexcept
+	{
+		if (getState() == AL_PAUSED)
+			play();
+	}
+};
diff --git a/src/client/sound/proxy_sound_manager.cpp b/src/client/sound/proxy_sound_manager.cpp
new file mode 100644
index 000000000..810776502
--- /dev/null
+++ b/src/client/sound/proxy_sound_manager.cpp
@@ -0,0 +1,163 @@
+/*
+Minetest
+Copyright (C) 2023 DS
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; ifnot, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "proxy_sound_manager.h"
+
+#include "filesys.h"
+
+ProxySoundManager::MsgResult ProxySoundManager::handleMsg(SoundManagerMsgToProxy &&msg)
+{
+	using namespace sound_manager_messages_to_proxy;
+
+	return std::visit([&](auto &&msg) {
+			using T = std::decay_t<decltype(msg)>;
+
+			if constexpr (std::is_same_v<T, std::monostate>)
+				return MsgResult::Empty;
+			else if constexpr (std::is_same_v<T, ReportRemovedSound>)
+				reportRemovedSound(msg.id);
+			else if constexpr (std::is_same_v<T, Stopped>)
+				return MsgResult::Stopped;
+
+			return MsgResult::Ok;
+		},
+		std::move(msg));
+}
+
+ProxySoundManager::~ProxySoundManager()
+{
+	if (m_sound_manager.isRunning()) {
+		send(sound_manager_messages_to_mgr::PleaseStop{});
+
+		// recv until it stopped
+		auto recv = [&]	{
+			return m_sound_manager.m_queue_to_proxy.pop_frontNoEx();
+		};
+
+		while (true) {
+			if (handleMsg(recv()) == MsgResult::Stopped)
+				break;
+		}
+
+		// join
+		m_sound_manager.stop();
+		SANITY_CHECK(m_sound_manager.wait());
+	}
+}
+
+void ProxySoundManager::step(f32 dtime)
+{
+	auto recv = [&]	{
+		return m_sound_manager.m_queue_to_proxy.pop_frontNoEx(0);
+	};
+
+	while (true) {
+		MsgResult res = handleMsg(recv());
+		if (res == MsgResult::Empty)
+			break;
+		else if (res == MsgResult::Stopped)
+			throw std::runtime_error("OpenALSoundManager stopped unexpectedly");
+	}
+}
+
+void ProxySoundManager::pauseAll()
+{
+	send(sound_manager_messages_to_mgr::PauseAll{});
+}
+
+void ProxySoundManager::resumeAll()
+{
+	send(sound_manager_messages_to_mgr::ResumeAll{});
+}
+
+void ProxySoundManager::updateListener(const v3f &pos_, const v3f &vel_,
+		const v3f &at_, const v3f &up_)
+{
+	send(sound_manager_messages_to_mgr::UpdateListener{pos_, vel_, at_, up_});
+}
+
+void ProxySoundManager::setListenerGain(f32 gain)
+{
+	send(sound_manager_messages_to_mgr::SetListenerGain{gain});
+}
+
+bool ProxySoundManager::loadSoundFile(const std::string &name,
+		const std::string &filepath)
+{
+	// do not add twice
+	if (m_known_sound_names.count(name) != 0)
+		return false;
+
+	// coarse check
+	if (!fs::IsFile(filepath))
+		return false;
+
+	send(sound_manager_messages_to_mgr::LoadSoundFile{name, filepath});
+
+	m_known_sound_names.insert(name);
+	return true;
+}
+
+bool ProxySoundManager::loadSoundData(const std::string &name, std::string &&filedata)
+{
+	// do not add twice
+	if (m_known_sound_names.count(name) != 0)
+		return false;
+
+	send(sound_manager_messages_to_mgr::LoadSoundData{name, std::move(filedata)});
+
+	m_known_sound_names.insert(name);
+	return true;
+}
+
+void ProxySoundManager::addSoundToGroup(const std::string &sound_name,
+		const std::string &group_name)
+{
+	send(sound_manager_messages_to_mgr::AddSoundToGroup{sound_name, group_name});
+}
+
+void ProxySoundManager::playSound(sound_handle_t id, const SoundSpec &spec)
+{
+	if (id == 0)
+		id = allocateId(1);
+	send(sound_manager_messages_to_mgr::PlaySound{id, spec});
+}
+
+void ProxySoundManager::playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos_,
+		const v3f &vel_)
+{
+	if (id == 0)
+		id = allocateId(1);
+	send(sound_manager_messages_to_mgr::PlaySoundAt{id, spec, pos_, vel_});
+}
+
+void ProxySoundManager::stopSound(sound_handle_t sound)
+{
+	send(sound_manager_messages_to_mgr::StopSound{sound});
+}
+
+void ProxySoundManager::fadeSound(sound_handle_t soundid, f32 step, f32 target_gain)
+{
+	send(sound_manager_messages_to_mgr::FadeSound{soundid, step, target_gain});
+}
+
+void ProxySoundManager::updateSoundPosVel(sound_handle_t sound, const v3f &pos_, const v3f &vel_)
+{
+	send(sound_manager_messages_to_mgr::UpdateSoundPosVel{sound, pos_, vel_});
+}
diff --git a/src/client/sound/proxy_sound_manager.h b/src/client/sound/proxy_sound_manager.h
new file mode 100644
index 000000000..4f376d635
--- /dev/null
+++ b/src/client/sound/proxy_sound_manager.h
@@ -0,0 +1,71 @@
+/*
+Minetest
+Copyright (C) 2023 DS
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; ifnot, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#pragma once
+
+#include "sound_manager.h"
+
+/*
+ * The public ISoundManager interface
+ */
+
+class ProxySoundManager final : public ISoundManager
+{
+	OpenALSoundManager m_sound_manager;
+	// sound names from loadSoundData and loadSoundFile
+	std::unordered_set<std::string> m_known_sound_names;
+
+	void send(SoundManagerMsgToMgr msg)
+	{
+		m_sound_manager.m_queue_to_mgr.push_back(std::move(msg));
+	}
+
+	enum class MsgResult { Ok, Empty, Stopped};
+	MsgResult handleMsg(SoundManagerMsgToProxy &&msg);
+
+public:
+	ProxySoundManager(SoundManagerSingleton *smg,
+			std::unique_ptr<SoundFallbackPathProvider> fallback_path_provider) :
+		m_sound_manager(smg, std::move(fallback_path_provider))
+	{
+		m_sound_manager.start();
+	}
+
+	~ProxySoundManager() override;
+
+	/* Interface */
+
+	void step(f32 dtime) override;
+	void pauseAll() override;
+	void resumeAll() override;
+
+	void updateListener(const v3f &pos_, const v3f &vel_, const v3f &at_, const v3f &up_) override;
+	void setListenerGain(f32 gain) override;
+
+	bool loadSoundFile(const std::string &name, const std::string &filepath) override;
+	bool loadSoundData(const std::string &name, std::string &&filedata) override;
+	void addSoundToGroup(const std::string &sound_name, const std::string &group_name) override;
+
+	void playSound(sound_handle_t id, const SoundSpec &spec) override;
+	void playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos_,
+			const v3f &vel_) override;
+	void stopSound(sound_handle_t sound) override;
+	void fadeSound(sound_handle_t soundid, f32 step, f32 target_gain) override;
+	void updateSoundPosVel(sound_handle_t sound, const v3f &pos_, const v3f &vel_) override;
+};
diff --git a/src/client/sound/sound_constants.h b/src/client/sound/sound_constants.h
new file mode 100644
index 000000000..b16f5204f
--- /dev/null
+++ b/src/client/sound/sound_constants.h
@@ -0,0 +1,119 @@
+/*
+Minetest
+Copyright (C) 2022 DS
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; ifnot, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#pragma once
+
+/*
+ *
+ * The coordinate space for sounds (sound-space):
+ * ----------------------------------------------
+ *
+ * * The functions from ISoundManager (see sound.h) take spatial vectors in node-space.
+ * * All other `v3f`s here are, if not told otherwise, in sound-space, which is
+ *   defined as node-space mirrored along the x-axis.
+ *   (This is needed because OpenAL uses a right-handed coordinate system.)
+ * * Use `swap_handedness()` from `al_helpers.h` to convert between those two
+ *   coordinate spaces.
+ *
+ *
+ * How sounds are loaded:
+ * ----------------------
+ *
+ * * Step 1:
+ *   `loadSoundFile` or `loadSoundFile` is called. This adds an unopen sound with
+ *   the given name to `m_sound_datas_unopen`.
+ *   Unopen / lazy sounds (`ISoundDataUnopen`) are ogg-vorbis files that we did not yet
+ *   start to decode. (Decoding an unopen sound does not fail under normal circumstances
+ *   (because we check whether the file exists at least), if it does fail anyways,
+ *   we should notify the user.)
+ * * Step 2:
+ *   `addSoundToGroup` is called, to add the name from step 1 to a group. If the
+ *   group does not yet exist, a new one is created. A group can later be played.
+ *   (The mapping is stored in `m_sound_groups`.)
+ * * Step 3:
+ *   `playSound` or `playSoundAt` is called.
+ *   * Step 3.1:
+ *     If the group with the name `spec.name` does not exist, and `spec.use_local_fallback`
+ *     is true, a new group is created using the user's sound-pack.
+ *   * Step 3.2:
+ *     We choose one random sound name from the given group.
+ *   * Step 3.3:
+ *     We open the sound (see `openSingleSound`).
+ *     If the sound is already open (in `m_sound_datas_open`), we take that one.
+ *     Otherwise we open it by calling `ISoundDataUnopen::open`. We choose (by
+ *     sound length), whether it's a single-buffer (`SoundDataOpenBuffer`) or
+ *     streamed (`SoundDataOpenStream`) sound.
+ *     Single-buffer sounds are always completely loaded. Streamed sounds can be
+ *     partially loaded.
+ *     The sound is erased from `m_sound_datas_unopen` and added to `m_sound_datas_open`.
+ *     Open sounds are kept forever.
+ *   * Step 3.4:
+ *     We create the new `PlayingSound`. It has a `shared_ptr` to its open sound.
+ *     If the open sound is streaming, the playing sound needs to be stepped using
+ *     `PlayingSound::stepStream` for enqueuing buffers. For this purpose, the sound
+ *     is added to `m_sounds_streaming` (as `weak_ptr`).
+ *     If the sound is fading, it is added to `m_sounds_fading` for regular fade-stepping.
+ *     The sound is also added to `m_sounds_playing`, so that one can access it
+ *     via its sound handle.
+ * * Step 4:
+ *     Streaming sounds are updated. For details see [Streaming of sounds].
+ * * Step 5:
+ *     At deinitialization, we can just let the destructors do their work.
+ *     Sound sources are deleted (and with this also stopped) by ~PlayingSound.
+ *     Buffers can't be deleted while sound sources using them exist, because
+ *     PlayingSound has a shared_ptr to its ISoundData.
+ *
+ *
+ * Streaming of sounds:
+ * --------------------
+ *
+ * In each "bigstep", all streamed sounds are stepStream()ed. This means a
+ * sound can be stepped at any point in time in the bigstep's interval.
+ *
+ * In the worst case, a sound is stepped at the start of one bigstep and in the
+ * end of the next bigstep. So between two stepStream()-calls lie at most
+ * 2 * STREAM_BIGSTEP_TIME seconds.
+ * As there are always 2 sound buffers enqueued, at least one untouched full buffer
+ * is still available after the first stepStream().
+ * If we take a MIN_STREAM_BUFFER_LENGTH > 2 * STREAM_BIGSTEP_TIME, we can hence
+ * not run into an empty queue.
+ *
+ * The MIN_STREAM_BUFFER_LENGTH needs to be a little bigger because of dtime jitter,
+ * other sounds that may have taken long to stepStream(), and sounds being played
+ * faster due to Doppler effect.
+ *
+ */
+
+// constants
+
+// in seconds
+constexpr f32 REMOVE_DEAD_SOUNDS_INTERVAL = 2.0f;
+// maximum length in seconds that a sound can have without being streamed
+constexpr f32 SOUND_DURATION_MAX_SINGLE = 3.0f;
+// minimum time in seconds of a single buffer in a streamed sound
+constexpr f32 MIN_STREAM_BUFFER_LENGTH = 1.0f;
+// duration in seconds of one bigstep
+constexpr f32 STREAM_BIGSTEP_TIME = 0.3f;
+// step duration for the OpenALSoundManager thread, in seconds
+constexpr f32 SOUNDTHREAD_DTIME = 0.016f;
+
+static_assert(MIN_STREAM_BUFFER_LENGTH > STREAM_BIGSTEP_TIME * 2.0f,
+		"See [Streaming of sounds].");
+static_assert(SOUND_DURATION_MAX_SINGLE >= MIN_STREAM_BUFFER_LENGTH * 2.0f,
+		"There's no benefit in streaming if we can't queue more than 2 buffers.");
diff --git a/src/client/sound/sound_data.cpp b/src/client/sound/sound_data.cpp
new file mode 100644
index 000000000..258bf8836
--- /dev/null
+++ b/src/client/sound/sound_data.cpp
@@ -0,0 +1,231 @@
+/*
+Minetest
+Copyright (C) 2022 DS
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+OpenAL support based on work by:
+Copyright (C) 2011 Sebastian 'Bahamada' Rühl
+Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com>
+Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; ifnot, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "sound_data.h"
+
+#include "sound_constants.h"
+
+/*
+ * ISoundDataOpen struct
+ */
+
+std::shared_ptr<ISoundDataOpen> ISoundDataOpen::fromOggFile(std::unique_ptr<RAIIOggFile> oggfile,
+		const std::string &filename_for_logging)
+{
+	// Get some information about the OGG file
+	std::optional<OggFileDecodeInfo> decode_info = oggfile->getDecodeInfo(filename_for_logging);
+	if (!decode_info.has_value()) {
+		warningstream << "Audio: Error decoding "
+				<< filename_for_logging << std::endl;
+		return nullptr;
+	}
+
+	// use duration (in seconds) to decide whether to load all at once or to stream
+	if (decode_info->length_seconds <= SOUND_DURATION_MAX_SINGLE) {
+		return std::make_shared<SoundDataOpenBuffer>(std::move(oggfile), *decode_info);
+	} else {
+		return std::make_shared<SoundDataOpenStream>(std::move(oggfile), *decode_info);
+	}
+}
+
+/*
+ * SoundDataUnopenBuffer struct
+ */
+
+std::shared_ptr<ISoundDataOpen> SoundDataUnopenBuffer::open(const std::string &sound_name) &&
+{
+	// load from m_buffer
+
+	auto oggfile = std::make_unique<RAIIOggFile>();
+
+	auto buffer_source = std::make_unique<OggVorbisBufferSource>();
+	buffer_source->buf = std::move(m_buffer);
+
+	oggfile->m_needs_clear = true;
+	if (ov_open_callbacks(buffer_source.release(), oggfile->get(), nullptr, 0,
+			OggVorbisBufferSource::s_ov_callbacks) != 0) {
+		warningstream << "Audio: Error opening " << sound_name << " for decoding"
+				<< std::endl;
+		return nullptr;
+	}
+
+	return ISoundDataOpen::fromOggFile(std::move(oggfile), sound_name);
+}
+
+/*
+ * SoundDataUnopenFile struct
+ */
+
+std::shared_ptr<ISoundDataOpen> SoundDataUnopenFile::open(const std::string &sound_name) &&
+{
+	// load from file at m_path
+
+	auto oggfile = std::make_unique<RAIIOggFile>();
+
+	if (ov_fopen(m_path.c_str(), oggfile->get()) != 0) {
+		warningstream << "Audio: Error opening " << m_path << " for decoding"
+				<< std::endl;
+		return nullptr;
+	}
+	oggfile->m_needs_clear = true;
+
+	return ISoundDataOpen::fromOggFile(std::move(oggfile), sound_name);
+}
+
+/*
+ * SoundDataOpenBuffer struct
+ */
+
+SoundDataOpenBuffer::SoundDataOpenBuffer(std::unique_ptr<RAIIOggFile> oggfile,
+		const OggFileDecodeInfo &decode_info) : ISoundDataOpen(decode_info)
+{
+	m_buffer = oggfile->loadBuffer(m_decode_info, 0, m_decode_info.length_samples);
+	if (m_buffer.get() == 0) {
+		warningstream << "SoundDataOpenBuffer: Failed to load sound \""
+				<< m_decode_info.name_for_logging << "\"" << std::endl;
+		return;
+	}
+}
+
+/*
+ * SoundDataOpenStream struct
+ */
+
+SoundDataOpenStream::SoundDataOpenStream(std::unique_ptr<RAIIOggFile> oggfile,
+		const OggFileDecodeInfo &decode_info) :
+	ISoundDataOpen(decode_info), m_oggfile(std::move(oggfile))
+{
+	// do nothing here. buffers are loaded at getOrLoadBufferAt
+}
+
+std::tuple<ALuint, ALuint, ALuint> SoundDataOpenStream::getOrLoadBufferAt(ALuint offset)
+{
+	if (offset >= m_decode_info.length_samples)
+		return {0, m_decode_info.length_samples, 0};
+
+	// find the right-most ContiguousBuffers, such that `m_start <= offset`
+	// equivalent: the first element from the right such that `!(m_start > offset)`
+	// (from the right, `offset` is a lower bound to the `m_start`s)
+	auto lower_rit = std::lower_bound(m_bufferss.rbegin(), m_bufferss.rend(), offset,
+			[](const ContiguousBuffers &bufs, ALuint offset) {
+				return bufs.m_start > offset;
+			});
+
+	if (lower_rit != m_bufferss.rend()) {
+		std::vector<SoundBufferUntil> &bufs = lower_rit->m_buffers;
+		// find the left-most SoundBufferUntil, such that `m_end > offset`
+		// equivalent: the first element from the left such that `m_end > offset`
+		// (returns first element where comp gives true)
+		auto upper_it = std::upper_bound(bufs.begin(), bufs.end(), offset,
+				[](ALuint offset, const SoundBufferUntil &buf) {
+					return offset < buf.m_end;
+				});
+
+		if (upper_it != bufs.end()) {
+			ALuint start = upper_it == bufs.begin() ? lower_rit->m_start
+					: (upper_it - 1)->m_end;
+			return {upper_it->m_buffer.get(), upper_it->m_end, offset - start};
+		}
+	}
+
+	// no loaded buffer starts before or at `offset`
+	// or no loaded buffer (that starts before or at `offset`) ends after `offset`
+
+	// lower_rit, but not reverse and 1 farther
+	auto after_it = m_bufferss.begin() + (m_bufferss.rend() - lower_rit);
+
+	return loadBufferAt(offset, after_it);
+}
+
+std::tuple<ALuint, ALuint, ALuint> SoundDataOpenStream::loadBufferAt(ALuint offset,
+		std::vector<ContiguousBuffers>::iterator after_it)
+{
+	bool has_before = after_it != m_bufferss.begin();
+	bool has_after = after_it != m_bufferss.end();
+
+	ALuint end_before = has_before ? (after_it - 1)->m_buffers.back().m_end : 0;
+	ALuint start_after = has_after ? after_it->m_start : m_decode_info.length_samples;
+
+	const ALuint min_buf_len_samples = m_decode_info.freq * MIN_STREAM_BUFFER_LENGTH;
+
+	//
+	// 1) Find the actual start and end of the new buffer
+	//
+
+	ALuint new_buf_start = offset;
+	ALuint new_buf_end = offset + min_buf_len_samples;
+
+	// Don't load into next buffer, or past the end
+	if (new_buf_end > start_after) {
+		new_buf_end = start_after;
+		// Also move start (for min buf size) (but not *into* previous buffer)
+		if (new_buf_end - new_buf_start < min_buf_len_samples) {
+			new_buf_start = std::max(
+					end_before,
+					new_buf_end < min_buf_len_samples ? 0
+							: new_buf_end - min_buf_len_samples
+				);
+		}
+	}
+
+	// Widen if space to right or left is smaller than min buf size
+	if (new_buf_start - end_before < min_buf_len_samples)
+		new_buf_start = end_before;
+	if (start_after - new_buf_end < min_buf_len_samples)
+		new_buf_end = start_after;
+
+	//
+	// 2) Load [new_buf_start, new_buf_end)
+	//
+
+	// If it fails, we get a 0-buffer. we store it and won't try loading again
+	RAIIALSoundBuffer new_buf = m_oggfile->loadBuffer(m_decode_info, new_buf_start,
+			new_buf_end);
+
+	//
+	// 3) Insert before after_it
+	//
+
+	// Choose ContiguousBuffers to add the new SoundBufferUntil into:
+	// * `after_it - 1` (=before) if existent and if there's no space between its
+	//   last buffer and the new buffer
+	// * A new ContiguousBuffers otherwise
+	auto it = has_before && new_buf_start == end_before ? after_it - 1
+			: m_bufferss.insert(after_it, ContiguousBuffers{new_buf_start, {}});
+
+	// Add the new SoundBufferUntil
+	size_t new_buf_i = it->m_buffers.size();
+	it->m_buffers.push_back(SoundBufferUntil{new_buf_end, std::move(new_buf)});
+
+	if (has_after && new_buf_end == start_after) {
+		// Merge after into my ContiguousBuffers
+		auto &bufs = it->m_buffers;
+		auto &bufs_after = (it + 1)->m_buffers;
+		bufs.insert(bufs.end(), std::make_move_iterator(bufs_after.begin()),
+				std::make_move_iterator(bufs_after.end()));
+		it = m_bufferss.erase(it + 1) - 1;
+	}
+
+	return {it->m_buffers[new_buf_i].m_buffer.get(), new_buf_end, offset - new_buf_start};
+}
diff --git a/src/client/sound/sound_data.h b/src/client/sound/sound_data.h
new file mode 100644
index 000000000..f2c06b939
--- /dev/null
+++ b/src/client/sound/sound_data.h
@@ -0,0 +1,173 @@
+/*
+Minetest
+Copyright (C) 2022 DS
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+OpenAL support based on work by:
+Copyright (C) 2011 Sebastian 'Bahamada' Rühl
+Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com>
+Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; ifnot, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#pragma once
+
+#include "ogg_file.h"
+#include <memory>
+#include <tuple>
+
+/**
+ * Stores sound pcm data buffers.
+ */
+struct ISoundDataOpen
+{
+	OggFileDecodeInfo m_decode_info;
+
+	explicit ISoundDataOpen(const OggFileDecodeInfo &decode_info) :
+			m_decode_info(decode_info) {}
+
+	virtual ~ISoundDataOpen() = default;
+
+	/**
+	 * Iff the data is streaming, there is more than one buffer.
+	 * @return Whether it's streaming data.
+	 */
+	virtual bool isStreaming() const noexcept = 0;
+
+	/**
+	 * Load a buffer containing data starting at the given offset. Or just get it
+	 * if it was already loaded.
+	 *
+	 * This function returns multiple values:
+	 * * `buffer`: The OpenAL buffer.
+	 * * `buffer_end`: The offset (in the file) where `buffer` ends (exclusive).
+	 * * `offset_in_buffer`: Offset relative to `buffer`'s start where the requested
+	 *       `offset` is.
+	 *       `offset_in_buffer == 0` is guaranteed if some loaded buffer ends at
+	 *       `offset`.
+	 *
+	 * @param offset The start of the buffer.
+	 * @return `{buffer, buffer_end, offset_in_buffer}` or `{0, sound_data_end, 0}`
+	 *         if `offset` is invalid.
+	 */
+	virtual std::tuple<ALuint, ALuint, ALuint> getOrLoadBufferAt(ALuint offset) = 0;
+
+	static std::shared_ptr<ISoundDataOpen> fromOggFile(std::unique_ptr<RAIIOggFile> oggfile,
+		const std::string &filename_for_logging);
+};
+
+/**
+ * Will be opened lazily when first used.
+ */
+struct ISoundDataUnopen
+{
+	virtual ~ISoundDataUnopen() = default;
+
+	// Note: The ISoundDataUnopen is moved (see &&). It is not meant to be kept
+	// after opening.
+	virtual std::shared_ptr<ISoundDataOpen> open(const std::string &sound_name) && = 0;
+};
+
+/**
+ * Sound file is in a memory buffer.
+ */
+struct SoundDataUnopenBuffer final : ISoundDataUnopen
+{
+	std::string m_buffer;
+
+	explicit SoundDataUnopenBuffer(std::string &&buffer) : m_buffer(std::move(buffer)) {}
+
+	std::shared_ptr<ISoundDataOpen> open(const std::string &sound_name) && override;
+};
+
+/**
+ * Sound file is in file system.
+ */
+struct SoundDataUnopenFile final : ISoundDataUnopen
+{
+	std::string m_path;
+
+	explicit SoundDataUnopenFile(const std::string &path) : m_path(path) {}
+
+	std::shared_ptr<ISoundDataOpen> open(const std::string &sound_name) && override;
+};
+
+/**
+ * Non-streaming opened sound data.
+ * All data is completely loaded in one buffer.
+ */
+struct SoundDataOpenBuffer final : ISoundDataOpen
+{
+	RAIIALSoundBuffer m_buffer;
+
+	SoundDataOpenBuffer(std::unique_ptr<RAIIOggFile> oggfile,
+			const OggFileDecodeInfo &decode_info);
+
+	bool isStreaming() const noexcept override { return false; }
+
+	std::tuple<ALuint, ALuint, ALuint> getOrLoadBufferAt(ALuint offset) override
+	{
+		if (offset >= m_decode_info.length_samples)
+			return {0, m_decode_info.length_samples, 0};
+		return {m_buffer.get(), m_decode_info.length_samples, offset};
+	}
+};
+
+/**
+ * Streaming opened sound data.
+ *
+ * Uses a sorted list of contiguous sound data regions (`ContiguousBuffers`s) for
+ * efficient seeking.
+ */
+struct SoundDataOpenStream final : ISoundDataOpen
+{
+	/**
+	 * An OpenAL buffer that goes until `m_end` (exclusive).
+	 */
+	struct SoundBufferUntil final
+	{
+		ALuint m_end;
+		RAIIALSoundBuffer m_buffer;
+	};
+
+	/**
+	 * A sorted non-empty vector of contiguous buffers.
+	 * The start (inclusive) of each buffer is the end of its predecessor, or
+	 * `m_start` for the first buffer.
+	 */
+	struct ContiguousBuffers final
+	{
+		ALuint m_start;
+		std::vector<SoundBufferUntil> m_buffers;
+	};
+
+	std::unique_ptr<RAIIOggFile> m_oggfile;
+	// A sorted vector of non-overlapping, non-contiguous `ContiguousBuffers`s.
+	std::vector<ContiguousBuffers> m_bufferss;
+
+	SoundDataOpenStream(std::unique_ptr<RAIIOggFile> oggfile,
+			const OggFileDecodeInfo &decode_info);
+
+	bool isStreaming() const noexcept override { return true; }
+
+	std::tuple<ALuint, ALuint, ALuint> getOrLoadBufferAt(ALuint offset) override;
+
+private:
+	// offset must be before after_it's m_start and after (after_it-1)'s last m_end
+	// new buffer will be inserted into m_bufferss before after_it
+	// returns same as getOrLoadBufferAt
+	std::tuple<ALuint, ALuint, ALuint> loadBufferAt(ALuint offset,
+			std::vector<ContiguousBuffers>::iterator after_it);
+};
diff --git a/src/client/sound/sound_manager.cpp b/src/client/sound/sound_manager.cpp
new file mode 100644
index 000000000..0791995c4
--- /dev/null
+++ b/src/client/sound/sound_manager.cpp
@@ -0,0 +1,523 @@
+/*
+Minetest
+Copyright (C) 2022 DS
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+OpenAL support based on work by:
+Copyright (C) 2011 Sebastian 'Bahamada' Rühl
+Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com>
+Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; ifnot, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "sound_manager.h"
+
+#include "sound_singleton.h"
+#include "util/numeric.h" // myrand()
+#include "filesys.h"
+#include "porting.h"
+
+void OpenALSoundManager::stepStreams(f32 dtime)
+{
+	// spread work across steps
+	const size_t num_issued_sounds = std::min(
+			m_sounds_streaming_current_bigstep.size(),
+			(size_t)std::ceil(m_sounds_streaming_current_bigstep.size()
+				* dtime / m_stream_timer)
+		);
+
+	for (size_t i = 0; i < num_issued_sounds; ++i) {
+		auto wptr = std::move(m_sounds_streaming_current_bigstep.back());
+		m_sounds_streaming_current_bigstep.pop_back();
+
+		std::shared_ptr<PlayingSound> snd = wptr.lock();
+		if (!snd)
+			continue;
+
+		if (!snd->stepStream())
+			continue;
+
+		// sound still lives and needs more stream-stepping => add to next bigstep
+		m_sounds_streaming_next_bigstep.push_back(std::move(wptr));
+	}
+
+	m_stream_timer -= dtime;
+	if (m_stream_timer <= 0.0f) {
+		m_stream_timer = STREAM_BIGSTEP_TIME;
+		using std::swap;
+		swap(m_sounds_streaming_current_bigstep, m_sounds_streaming_next_bigstep);
+	}
+}
+
+void OpenALSoundManager::doFades(f32 dtime)
+{
+	for (size_t i = 0; i < m_sounds_fading.size();) {
+		std::shared_ptr<PlayingSound> snd = m_sounds_fading[i].lock();
+		if (snd) {
+			if (snd->doFade(dtime)) {
+				// needs more fading later, keep in m_sounds_fading
+				++i;
+				continue;
+			}
+		}
+
+		// sound no longer needs to be faded
+		m_sounds_fading[i] = std::move(m_sounds_fading.back());
+		m_sounds_fading.pop_back();
+		// continue with same i
+	}
+}
+
+std::shared_ptr<ISoundDataOpen> OpenALSoundManager::openSingleSound(const std::string &sound_name)
+{
+	// if already open, nothing to do
+	auto it = m_sound_datas_open.find(sound_name);
+	if (it != m_sound_datas_open.end())
+		return it->second;
+
+	// find unopened data
+	auto it_unopen = m_sound_datas_unopen.find(sound_name);
+	if (it_unopen == m_sound_datas_unopen.end())
+		return nullptr;
+	std::unique_ptr<ISoundDataUnopen> unopn_snd = std::move(it_unopen->second);
+	m_sound_datas_unopen.erase(it_unopen);
+
+	// open
+	std::shared_ptr<ISoundDataOpen> opn_snd = std::move(*unopn_snd).open(sound_name);
+	if (!opn_snd)
+		return nullptr;
+	m_sound_datas_open.emplace(sound_name, opn_snd);
+	return opn_snd;
+}
+
+std::string OpenALSoundManager::getLoadedSoundNameFromGroup(const std::string &group_name)
+{
+	std::string chosen_sound_name = "";
+
+	auto it_groups = m_sound_groups.find(group_name);
+	if (it_groups == m_sound_groups.end())
+		return "";
+
+	std::vector<std::string> &group_sounds = it_groups->second;
+	while (!group_sounds.empty()) {
+		// choose one by random
+		int j = myrand() % group_sounds.size();
+		chosen_sound_name = group_sounds[j];
+
+		// find chosen one
+		std::shared_ptr<ISoundDataOpen> snd = openSingleSound(chosen_sound_name);
+		if (snd)
+			return chosen_sound_name;
+
+		// it doesn't exist
+		// remove it from the group and try again
+		group_sounds[j] = std::move(group_sounds.back());
+		group_sounds.pop_back();
+	}
+
+	return "";
+}
+
+std::string OpenALSoundManager::getOrLoadLoadedSoundNameFromGroup(const std::string &group_name)
+{
+	std::string sound_name = getLoadedSoundNameFromGroup(group_name);
+	if (!sound_name.empty())
+		return sound_name;
+
+	// load
+	std::vector<std::string> paths = m_fallback_path_provider
+			->getLocalFallbackPathsForSoundname(group_name);
+	for (const std::string &path : paths) {
+		if (loadSoundFile(path, path))
+			addSoundToGroup(path, group_name);
+	}
+	return getLoadedSoundNameFromGroup(group_name);
+}
+
+std::shared_ptr<PlayingSound> OpenALSoundManager::createPlayingSound(
+		const std::string &sound_name, bool loop, f32 volume, f32 pitch,
+		f32 start_time, const std::optional<std::pair<v3f, v3f>> &pos_vel_opt)
+{
+	infostream << "OpenALSoundManager: Creating playing sound \"" << sound_name
+			<< "\"" << std::endl;
+	warn_if_al_error("before createPlayingSound");
+
+	std::shared_ptr<ISoundDataOpen> lsnd = openSingleSound(sound_name);
+	if (!lsnd) {
+		// does not happen because of the call to getLoadedSoundNameFromGroup
+		errorstream << "OpenALSoundManager::createPlayingSound: Sound \""
+				<< sound_name << "\" disappeared." << std::endl;
+		return nullptr;
+	}
+
+	if (lsnd->m_decode_info.is_stereo && pos_vel_opt.has_value()) {
+		warningstream << "OpenALSoundManager::createPlayingSound: "
+				<< "Creating positional stereo sound \"" << sound_name << "\"."
+				<< std::endl;
+	}
+
+	ALuint source_id;
+	alGenSources(1, &source_id);
+	if (warn_if_al_error("createPlayingSound (alGenSources)") != AL_NO_ERROR) {
+		// happens ie. if there are too many sources (out of memory)
+		return nullptr;
+	}
+
+	auto sound = std::make_shared<PlayingSound>(source_id, std::move(lsnd), loop,
+			volume, pitch, start_time, pos_vel_opt);
+
+	sound->play();
+	if (m_is_paused)
+		sound->pause();
+	warn_if_al_error("createPlayingSound");
+	return sound;
+}
+
+void OpenALSoundManager::playSoundGeneric(sound_handle_t id, const std::string &group_name,
+		bool loop, f32 volume, f32 fade, f32 pitch, bool use_local_fallback,
+		f32 start_time, const std::optional<std::pair<v3f, v3f>> &pos_vel_opt)
+{
+	assert(id != 0);
+
+	if (group_name.empty()) {
+		reportRemovedSound(id);
+		return;
+	}
+
+	// choose random sound name from group name
+	std::string sound_name = use_local_fallback ?
+			getOrLoadLoadedSoundNameFromGroup(group_name) :
+			getLoadedSoundNameFromGroup(group_name);
+	if (sound_name.empty()) {
+		infostream << "OpenALSoundManager: \"" << group_name << "\" not found."
+				<< std::endl;
+		reportRemovedSound(id);
+		return;
+	}
+
+	volume = std::max(0.0f, volume);
+	f32 target_fade_volume = volume;
+	if (fade > 0.0f)
+		volume = 0.0f;
+
+	if (!(pitch > 0.0f)) {
+		warningstream << "OpenALSoundManager::playSoundGeneric: Illegal pitch value: "
+				<< start_time << std::endl;
+		pitch = 1.0f;
+	}
+
+	if (!std::isfinite(start_time)) {
+		warningstream << "OpenALSoundManager::playSoundGeneric: Illegal start_time value: "
+				<< start_time << std::endl;
+		start_time = 0.0f;
+	}
+
+	// play it
+	std::shared_ptr<PlayingSound> sound = createPlayingSound(sound_name, loop,
+			volume, pitch, start_time, pos_vel_opt);
+	if (!sound) {
+		reportRemovedSound(id);
+		return;
+	}
+
+	// add to streaming sounds if streaming
+	if (sound->isStreaming())
+		m_sounds_streaming_next_bigstep.push_back(sound);
+
+	m_sounds_playing.emplace(id, std::move(sound));
+
+	if (fade > 0.0f)
+		fadeSound(id, fade, target_fade_volume);
+}
+
+int OpenALSoundManager::removeDeadSounds()
+{
+	int num_deleted_sounds = 0;
+
+	for (auto it = m_sounds_playing.begin(); it != m_sounds_playing.end();) {
+		sound_handle_t id = it->first;
+		PlayingSound &sound = *it->second;
+		// If dead, remove it
+		if (sound.isDead()) {
+			it = m_sounds_playing.erase(it);
+			reportRemovedSound(id);
+			++num_deleted_sounds;
+		} else {
+			++it;
+		}
+	}
+
+	return num_deleted_sounds;
+}
+
+OpenALSoundManager::OpenALSoundManager(SoundManagerSingleton *smg,
+		std::unique_ptr<SoundFallbackPathProvider> fallback_path_provider) :
+	Thread("OpenALSoundManager"),
+	m_fallback_path_provider(std::move(fallback_path_provider)),
+	m_device(smg->m_device.get()),
+	m_context(smg->m_context.get())
+{
+	SANITY_CHECK(!!m_fallback_path_provider);
+
+	infostream << "Audio: Initialized: OpenAL " << std::endl;
+}
+
+OpenALSoundManager::~OpenALSoundManager()
+{
+	infostream << "Audio: Deinitializing..." << std::endl;
+}
+
+/* Interface */
+
+void OpenALSoundManager::step(f32 dtime)
+{
+	m_time_until_dead_removal -= dtime;
+	if (m_time_until_dead_removal <= 0.0f) {
+		if (!m_sounds_playing.empty()) {
+			verbosestream << "OpenALSoundManager::step(): "
+					<< m_sounds_playing.size() << " playing sounds, "
+					<< m_sound_datas_unopen.size() << " unopen sounds, "
+					<< m_sound_datas_open.size() << " open sounds and "
+					<< m_sound_groups.size() << " sound groups loaded."
+					<< std::endl;
+		}
+
+		int num_deleted_sounds = removeDeadSounds();
+
+		if (num_deleted_sounds != 0)
+			verbosestream << "OpenALSoundManager::step(): Deleted "
+					<< num_deleted_sounds << " dead playing sounds." << std::endl;
+
+		m_time_until_dead_removal = REMOVE_DEAD_SOUNDS_INTERVAL;
+	}
+
+	doFades(dtime);
+	stepStreams(dtime);
+}
+
+void OpenALSoundManager::pauseAll()
+{
+	for (auto &snd_p : m_sounds_playing) {
+		PlayingSound &snd = *snd_p.second;
+		snd.pause();
+	}
+	m_is_paused = true;
+}
+
+void OpenALSoundManager::resumeAll()
+{
+	for (auto &snd_p : m_sounds_playing) {
+		PlayingSound &snd = *snd_p.second;
+		snd.resume();
+	}
+	m_is_paused = false;
+}
+
+void OpenALSoundManager::updateListener(const v3f &pos_, const v3f &vel_,
+		const v3f &at_, const v3f &up_)
+{
+	v3f pos = swap_handedness(pos_);
+	v3f vel = swap_handedness(vel_);
+	v3f at = swap_handedness(at_);
+	v3f up = swap_handedness(up_);
+	ALfloat orientation[6] = {at.X, at.Y, at.Z, up.X, up.Y, up.Z};
+
+	alListener3f(AL_POSITION, pos.X, pos.Y, pos.Z);
+	alListener3f(AL_VELOCITY, vel.X, vel.Y, vel.Z);
+	alListenerfv(AL_ORIENTATION, orientation);
+	warn_if_al_error("updateListener");
+}
+
+void OpenALSoundManager::setListenerGain(f32 gain)
+{
+	alListenerf(AL_GAIN, gain);
+}
+
+bool OpenALSoundManager::loadSoundFile(const std::string &name, const std::string &filepath)
+{
+	// do not add twice
+	if (m_sound_datas_open.count(name) != 0 || m_sound_datas_unopen.count(name) != 0)
+		return false;
+
+	// coarse check
+	if (!fs::IsFile(filepath))
+		return false;
+
+	loadSoundFileNoCheck(name, filepath);
+	return true;
+}
+
+bool OpenALSoundManager::loadSoundData(const std::string &name, std::string &&filedata)
+{
+	// do not add twice
+	if (m_sound_datas_open.count(name) != 0 || m_sound_datas_unopen.count(name) != 0)
+		return false;
+
+	loadSoundDataNoCheck(name, std::move(filedata));
+	return true;
+}
+
+void OpenALSoundManager::loadSoundFileNoCheck(const std::string &name, const std::string &filepath)
+{
+	// remember for lazy loading
+	m_sound_datas_unopen.emplace(name, std::make_unique<SoundDataUnopenFile>(filepath));
+}
+
+void OpenALSoundManager::loadSoundDataNoCheck(const std::string &name, std::string &&filedata)
+{
+	// remember for lazy loading
+	m_sound_datas_unopen.emplace(name, std::make_unique<SoundDataUnopenBuffer>(std::move(filedata)));
+}
+
+void OpenALSoundManager::addSoundToGroup(const std::string &sound_name, const std::string &group_name)
+{
+	auto it_groups = m_sound_groups.find(group_name);
+	if (it_groups != m_sound_groups.end())
+		it_groups->second.push_back(sound_name);
+	else
+		m_sound_groups.emplace(group_name, std::vector<std::string>{sound_name});
+}
+
+void OpenALSoundManager::playSound(sound_handle_t id, const SoundSpec &spec)
+{
+	return playSoundGeneric(id, spec.name, spec.loop, spec.gain, spec.fade, spec.pitch,
+			spec.use_local_fallback, spec.start_time, std::nullopt);
+}
+
+void OpenALSoundManager::playSoundAt(sound_handle_t id, const SoundSpec &spec,
+		const v3f &pos_, const v3f &vel_)
+{
+	std::optional<std::pair<v3f, v3f>> pos_vel_opt({
+			swap_handedness(pos_),
+			swap_handedness(vel_)
+		});
+
+	return playSoundGeneric(id, spec.name, spec.loop, spec.gain, spec.fade, spec.pitch,
+			spec.use_local_fallback, spec.start_time, pos_vel_opt);
+}
+
+void OpenALSoundManager::stopSound(sound_handle_t sound)
+{
+	m_sounds_playing.erase(sound);
+	reportRemovedSound(sound);
+}
+
+void OpenALSoundManager::fadeSound(sound_handle_t soundid, f32 step, f32 target_gain)
+{
+	// Ignore the command if step isn't valid.
+	if (step == 0.0f)
+		return;
+	auto sound_it = m_sounds_playing.find(soundid);
+	if (sound_it == m_sounds_playing.end())
+		return; // No sound to fade
+	PlayingSound &sound = *sound_it->second;
+	if (sound.fade(step, target_gain))
+		m_sounds_fading.emplace_back(sound_it->second);
+}
+
+void OpenALSoundManager::updateSoundPosVel(sound_handle_t id, const v3f &pos_,
+		const v3f &vel_)
+{
+	v3f pos = swap_handedness(pos_);
+	v3f vel = swap_handedness(vel_);
+
+	auto i = m_sounds_playing.find(id);
+	if (i == m_sounds_playing.end())
+		return;
+	i->second->updatePosVel(pos, vel);
+}
+
+/* Thread stuff */
+
+void *OpenALSoundManager::run()
+{
+	using namespace sound_manager_messages_to_mgr;
+
+	struct MsgVisitor {
+		enum class Result { Ok, Empty, StopRequested };
+
+		OpenALSoundManager &mgr;
+
+		Result operator()(std::monostate &&) {
+			return Result::Empty; }
+
+		Result operator()(PauseAll &&) {
+			mgr.pauseAll(); return Result::Ok; }
+		Result operator()(ResumeAll &&) {
+			mgr.resumeAll(); return Result::Ok; }
+
+		Result operator()(UpdateListener &&msg) {
+			mgr.updateListener(msg.pos_, msg.vel_, msg.at_, msg.up_); return Result::Ok; }
+		Result operator()(SetListenerGain &&msg) {
+			mgr.setListenerGain(msg.gain); return Result::Ok; }
+
+		Result operator()(LoadSoundFile &&msg) {
+			mgr.loadSoundFileNoCheck(msg.name, msg.filepath); return Result::Ok; }
+		Result operator()(LoadSoundData &&msg) {
+			mgr.loadSoundDataNoCheck(msg.name, std::move(msg.filedata)); return Result::Ok; }
+		Result operator()(AddSoundToGroup &&msg) {
+			mgr.addSoundToGroup(msg.sound_name, msg.group_name); return Result::Ok; }
+
+		Result operator()(PlaySound &&msg) {
+			mgr.playSound(msg.id, msg.spec); return Result::Ok; }
+		Result operator()(PlaySoundAt &&msg) {
+			mgr.playSoundAt(msg.id, msg.spec, msg.pos_, msg.vel_); return Result::Ok; }
+		Result operator()(StopSound &&msg) {
+			mgr.stopSound(msg.sound); return Result::Ok; }
+		Result operator()(FadeSound &&msg) {
+			mgr.fadeSound(msg.soundid, msg.step, msg.target_gain); return Result::Ok; }
+		Result operator()(UpdateSoundPosVel &&msg) {
+			mgr.updateSoundPosVel(msg.sound, msg.pos_, msg.vel_); return Result::Ok; }
+
+		Result operator()(PleaseStop &&msg) {
+			return Result::StopRequested; }
+	};
+
+	u64 t_step_start = porting::getTimeMs();
+	while (true) {
+		auto get_time_since_last_step = [&] {
+			return (f32)(porting::getTimeMs() - t_step_start);
+		};
+		auto get_remaining_timeout = [&] {
+			return (s32)((1.0e3f * SOUNDTHREAD_DTIME) - get_time_since_last_step());
+		};
+
+		bool stop_requested = false;
+
+		while (true) {
+			SoundManagerMsgToMgr msg =
+					m_queue_to_mgr.pop_frontNoEx(std::max(get_remaining_timeout(), 0));
+
+			MsgVisitor::Result res = std::visit(MsgVisitor{*this}, std::move(msg));
+
+			if (res == MsgVisitor::Result::Empty && get_remaining_timeout() <= 0) {
+				break; // finished sleeping
+			} else if (res == MsgVisitor::Result::StopRequested) {
+				stop_requested = true;
+				break;
+			}
+		}
+		if (stop_requested)
+			break;
+
+		f32 dtime = get_time_since_last_step();
+		t_step_start = porting::getTimeMs();
+		step(dtime);
+	}
+
+	send(sound_manager_messages_to_proxy::Stopped{});
+
+	return nullptr;
+}
diff --git a/src/client/sound/sound_manager.h b/src/client/sound/sound_manager.h
new file mode 100644
index 000000000..b69bbf870
--- /dev/null
+++ b/src/client/sound/sound_manager.h
@@ -0,0 +1,171 @@
+/*
+Minetest
+Copyright (C) 2022 DS
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+OpenAL support based on work by:
+Copyright (C) 2011 Sebastian 'Bahamada' Rühl
+Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com>
+Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; ifnot, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#pragma once
+
+#include "playing_sound.h"
+#include "sound_constants.h"
+#include "sound_manager_messages.h"
+#include "../sound.h"
+#include "threading/thread.h"
+#include "util/container.h" // MutexedQueue
+
+class SoundManagerSingleton;
+
+/*
+ * The SoundManager thread
+ *
+ * It's not an ISoundManager. It doesn't allocate ids, and doesn't accept id 0.
+ * All sound loading and interaction with OpenAL happens in this thread.
+ * Access from other threads happens via ProxySoundManager.
+ *
+ * See sound_constants.h for more details.
+ */
+
+class OpenALSoundManager final : public Thread
+{
+private:
+	std::unique_ptr<SoundFallbackPathProvider> m_fallback_path_provider;
+
+	ALCdevice *m_device;
+	ALCcontext *m_context;
+
+	// time in seconds until which removeDeadSounds will be called again
+	f32 m_time_until_dead_removal = REMOVE_DEAD_SOUNDS_INTERVAL;
+
+	// loaded sounds
+	std::unordered_map<std::string, std::unique_ptr<ISoundDataUnopen>> m_sound_datas_unopen;
+	std::unordered_map<std::string, std::shared_ptr<ISoundDataOpen>> m_sound_datas_open;
+	// sound groups
+	std::unordered_map<std::string, std::vector<std::string>> m_sound_groups;
+
+	// currently playing sounds
+	std::unordered_map<sound_handle_t, std::shared_ptr<PlayingSound>> m_sounds_playing;
+
+	// streamed sounds
+	std::vector<std::weak_ptr<PlayingSound>> m_sounds_streaming_current_bigstep;
+	std::vector<std::weak_ptr<PlayingSound>> m_sounds_streaming_next_bigstep;
+	// time left until current bigstep finishes
+	f32 m_stream_timer = STREAM_BIGSTEP_TIME;
+
+	std::vector<std::weak_ptr<PlayingSound>> m_sounds_fading;
+
+	// if true, all sounds will be directly paused after creation
+	bool m_is_paused = false;
+
+public:
+	// used for communication with ProxySoundManager
+	MutexedQueue<SoundManagerMsgToMgr> m_queue_to_mgr;
+	MutexedQueue<SoundManagerMsgToProxy> m_queue_to_proxy;
+
+private:
+	void stepStreams(f32 dtime);
+	void doFades(f32 dtime);
+
+	/**
+	 * Gives the open sound for a loaded sound.
+	 * Opens the sound if currently unopened.
+	 *
+	 * @param sound_name Name of the sound.
+	 * @return The open sound.
+	 */
+	std::shared_ptr<ISoundDataOpen> openSingleSound(const std::string &sound_name);
+
+	/**
+	 * Gets a random sound name from a group.
+	 *
+	 * @param group_name The name of the sound group.
+	 * @return The name of a sound in the group, or "" on failure. Getting the
+	 *         sound with `openSingleSound` directly afterwards will not fail.
+	 */
+	std::string getLoadedSoundNameFromGroup(const std::string &group_name);
+
+	/**
+	 * Same as `getLoadedSoundNameFromGroup`, but if sound does not exist, try to
+	 * load from local files.
+	 */
+	std::string getOrLoadLoadedSoundNameFromGroup(const std::string &group_name);
+
+	std::shared_ptr<PlayingSound> createPlayingSound(const std::string &sound_name,
+			bool loop, f32 volume, f32 pitch, f32 start_time,
+			const std::optional<std::pair<v3f, v3f>> &pos_vel_opt);
+
+	void playSoundGeneric(sound_handle_t id, const std::string &group_name, bool loop,
+			f32 volume, f32 fade, f32 pitch, bool use_local_fallback, f32 start_time,
+			const std::optional<std::pair<v3f, v3f>> &pos_vel_opt);
+
+	/**
+	 * Deletes sounds that are dead (=finished).
+	 *
+	 * @return Number of removed sounds.
+	 */
+	int removeDeadSounds();
+
+public:
+	OpenALSoundManager(SoundManagerSingleton *smg,
+			std::unique_ptr<SoundFallbackPathProvider> fallback_path_provider);
+
+	~OpenALSoundManager() override;
+
+	DISABLE_CLASS_COPY(OpenALSoundManager)
+
+private:
+	/* Similar to ISoundManager */
+
+	void step(f32 dtime);
+	void pauseAll();
+	void resumeAll();
+
+	void updateListener(const v3f &pos_, const v3f &vel_, const v3f &at_, const v3f &up_);
+	void setListenerGain(f32 gain);
+
+	bool loadSoundFile(const std::string &name, const std::string &filepath);
+	bool loadSoundData(const std::string &name, std::string &&filedata);
+	void loadSoundFileNoCheck(const std::string &name, const std::string &filepath);
+	void loadSoundDataNoCheck(const std::string &name, std::string &&filedata);
+	void addSoundToGroup(const std::string &sound_name, const std::string &group_name);
+
+	void playSound(sound_handle_t id, const SoundSpec &spec);
+	void playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos_,
+			const v3f &vel_);
+	void stopSound(sound_handle_t sound);
+	void fadeSound(sound_handle_t soundid, f32 step, f32 target_gain);
+	void updateSoundPosVel(sound_handle_t sound, const v3f &pos_, const v3f &vel_);
+
+protected:
+	/* Thread stuff */
+
+	void *run() override;
+
+private:
+	void send(SoundManagerMsgToProxy msg)
+	{
+		m_queue_to_proxy.push_back(std::move(msg));
+	}
+
+	void reportRemovedSound(sound_handle_t id)
+	{
+		send(sound_manager_messages_to_proxy::ReportRemovedSound{id});
+	}
+};
diff --git a/src/client/sound/sound_manager_messages.h b/src/client/sound/sound_manager_messages.h
new file mode 100644
index 000000000..14d254c98
--- /dev/null
+++ b/src/client/sound/sound_manager_messages.h
@@ -0,0 +1,80 @@
+/*
+Minetest
+Copyright (C) 2023 DS
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; ifnot, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#pragma once
+
+#include "../sound.h"
+#include "../../sound.h"
+#include <variant>
+
+namespace sound_manager_messages_to_mgr {
+	struct PauseAll {};
+	struct ResumeAll {};
+
+	struct UpdateListener { v3f pos_; v3f vel_; v3f at_; v3f up_; };
+	struct SetListenerGain { f32 gain; };
+
+	struct LoadSoundFile { std::string name; std::string filepath; };
+	struct LoadSoundData { std::string name; std::string filedata; };
+	struct AddSoundToGroup { std::string sound_name; std::string group_name; };
+
+	struct PlaySound { sound_handle_t id; SoundSpec spec; };
+	struct PlaySoundAt { sound_handle_t id; SoundSpec spec; v3f pos_; v3f vel_; };
+	struct StopSound { sound_handle_t sound; };
+	struct FadeSound { sound_handle_t soundid; f32 step; f32 target_gain; };
+	struct UpdateSoundPosVel { sound_handle_t sound; v3f pos_; v3f vel_; };
+
+	struct PleaseStop {};
+}
+
+using SoundManagerMsgToMgr = std::variant<
+		std::monostate,
+
+		sound_manager_messages_to_mgr::PauseAll,
+		sound_manager_messages_to_mgr::ResumeAll,
+
+		sound_manager_messages_to_mgr::UpdateListener,
+		sound_manager_messages_to_mgr::SetListenerGain,
+
+		sound_manager_messages_to_mgr::LoadSoundFile,
+		sound_manager_messages_to_mgr::LoadSoundData,
+		sound_manager_messages_to_mgr::AddSoundToGroup,
+
+		sound_manager_messages_to_mgr::PlaySound,
+		sound_manager_messages_to_mgr::PlaySoundAt,
+		sound_manager_messages_to_mgr::StopSound,
+		sound_manager_messages_to_mgr::FadeSound,
+		sound_manager_messages_to_mgr::UpdateSoundPosVel,
+
+		sound_manager_messages_to_mgr::PleaseStop
+	>;
+
+namespace sound_manager_messages_to_proxy {
+	struct ReportRemovedSound { sound_handle_t id; };
+
+	struct Stopped {};
+}
+
+using SoundManagerMsgToProxy = std::variant<
+		std::monostate,
+
+		sound_manager_messages_to_proxy::ReportRemovedSound,
+
+		sound_manager_messages_to_proxy::Stopped
+	>;
diff --git a/src/client/sound/sound_openal.cpp b/src/client/sound/sound_openal.cpp
index e8bb6290e..5d5d46a95 100644
--- a/src/client/sound/sound_openal.cpp
+++ b/src/client/sound/sound_openal.cpp
@@ -22,7 +22,9 @@ with this program; ifnot, write to the Free Software Foundation, Inc.,
 */
 
 #include "sound_openal.h"
-#include "sound_openal_internal.h"
+
+#include "sound_singleton.h"
+#include "proxy_sound_manager.h"
 
 std::shared_ptr<SoundManagerSingleton> g_sound_manager_singleton;
 
diff --git a/src/client/sound/sound_openal_internal.cpp b/src/client/sound/sound_openal_internal.cpp
deleted file mode 100644
index 208c33e45..000000000
--- a/src/client/sound/sound_openal_internal.cpp
+++ /dev/null
@@ -1,1363 +0,0 @@
-/*
-Minetest
-Copyright (C) 2022 DS
-Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
-OpenAL support based on work by:
-Copyright (C) 2011 Sebastian 'Bahamada' Rühl
-Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com>
-Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
-
-This program is free software; you can redistribute it and/or modify
-it under the terms of the GNU Lesser General Public License as published by
-the Free Software Foundation; either version 2.1 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Lesser General Public License for more details.
-
-You should have received a copy of the GNU Lesser General Public License along
-with this program; ifnot, write to the Free Software Foundation, Inc.,
-51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-*/
-
-#include "sound_openal_internal.h"
-
-#include "util/numeric.h" // myrand()
-#include "filesys.h"
-#include "settings.h"
-#include <algorithm>
-#include <cmath>
-
-/*
- * Helpers
- */
-
-static const char *getAlErrorString(ALenum err) noexcept
-{
-	switch (err) {
-	case AL_NO_ERROR:
-		return "no error";
-	case AL_INVALID_NAME:
-		return "invalid name";
-	case AL_INVALID_ENUM:
-		return "invalid enum";
-	case AL_INVALID_VALUE:
-		return "invalid value";
-	case AL_INVALID_OPERATION:
-		return "invalid operation";
-	case AL_OUT_OF_MEMORY:
-		return "out of memory";
-	default:
-		return "<unknown OpenAL error>";
-	}
-}
-
-static ALenum warn_if_al_error(const char *desc)
-{
-	ALenum err = alGetError();
-	if (err == AL_NO_ERROR)
-		return err;
-	warningstream << "[OpenAL Error] " << desc << ": " << getAlErrorString(err)
-			<< std::endl;
-	return err;
-}
-
-/**
- * Transforms vectors from a left-handed coordinate system to a right-handed one
- * and vice-versa.
- * (Needed because Minetest uses a left-handed one and OpenAL a right-handed one.)
- */
-static inline v3f swap_handedness(v3f v) noexcept
-{
-	return v3f(-v.X, v.Y, v.Z);
-}
-
-/*
- * RAIIALSoundBuffer struct
- */
-
-RAIIALSoundBuffer &RAIIALSoundBuffer::operator=(RAIIALSoundBuffer &&other) noexcept
-{
-	if (&other != this)
-		reset(other.release());
-	return *this;
-}
-
-void RAIIALSoundBuffer::reset(ALuint buf) noexcept
-{
-	if (m_buffer != 0) {
-		alDeleteBuffers(1, &m_buffer);
-		warn_if_al_error("Failed to free sound buffer");
-	}
-
-	m_buffer = buf;
-}
-
-RAIIALSoundBuffer RAIIALSoundBuffer::generate() noexcept
-{
-	ALuint buf;
-	alGenBuffers(1, &buf);
-	return RAIIALSoundBuffer(buf);
-}
-
-/*
- * OggVorbisBufferSource struct
- */
-
-size_t OggVorbisBufferSource::read_func(void *ptr, size_t size, size_t nmemb,
-		void *datasource) noexcept
-{
-	OggVorbisBufferSource *s = (OggVorbisBufferSource *)datasource;
-	size_t copied_size = MYMIN(s->buf.size() - s->cur_offset, size);
-	memcpy(ptr, s->buf.data() + s->cur_offset, copied_size);
-	s->cur_offset += copied_size;
-	return copied_size;
-}
-
-int OggVorbisBufferSource::seek_func(void *datasource, ogg_int64_t offset, int whence) noexcept
-{
-	OggVorbisBufferSource *s = (OggVorbisBufferSource *)datasource;
-	if (whence == SEEK_SET) {
-		if (offset < 0 || (size_t)offset > s->buf.size()) {
-			// offset out of bounds
-			return -1;
-		}
-		s->cur_offset = offset;
-		return 0;
-	} else if (whence == SEEK_CUR) {
-		if ((size_t)MYMIN(-offset, 0) > s->cur_offset
-				|| s->cur_offset + offset > s->buf.size()) {
-			// offset out of bounds
-			return -1;
-		}
-		s->cur_offset += offset;
-		return 0;
-	} else if (whence == SEEK_END) {
-		if (offset > 0 || (size_t)-offset > s->buf.size()) {
-			// offset out of bounds
-			return -1;
-		}
-		s->cur_offset = s->buf.size() - offset;
-		return 0;
-	}
-	return -1;
-}
-
-int OggVorbisBufferSource::close_func(void *datasource) noexcept
-{
-	auto s = reinterpret_cast<OggVorbisBufferSource *>(datasource);
-	delete s;
-	return 0;
-}
-
-long OggVorbisBufferSource::tell_func(void *datasource) noexcept
-{
-	OggVorbisBufferSource *s = (OggVorbisBufferSource *)datasource;
-	return s->cur_offset;
-}
-
-const ov_callbacks OggVorbisBufferSource::s_ov_callbacks = {
-	&OggVorbisBufferSource::read_func,
-	&OggVorbisBufferSource::seek_func,
-	&OggVorbisBufferSource::close_func,
-	&OggVorbisBufferSource::tell_func
-};
-
-/*
- * RAIIOggFile struct
- */
-
-std::optional<OggFileDecodeInfo> RAIIOggFile::getDecodeInfo(const std::string &filename_for_logging)
-{
-	OggFileDecodeInfo ret;
-
-	vorbis_info *pInfo = ov_info(&m_file, -1);
-	if (!pInfo)
-		return std::nullopt;
-
-	ret.name_for_logging = filename_for_logging;
-
-	if (pInfo->channels == 1) {
-		ret.is_stereo = false;
-		ret.format = AL_FORMAT_MONO16;
-		ret.bytes_per_sample = 2;
-	} else if (pInfo->channels == 2) {
-		ret.is_stereo = true;
-		ret.format = AL_FORMAT_STEREO16;
-		ret.bytes_per_sample = 4;
-	} else {
-		warningstream << "Audio: Can't decode. Sound is neither mono nor stereo: "
-				<< ret.name_for_logging << std::endl;
-		return std::nullopt;
-	}
-
-	ret.freq = pInfo->rate;
-
-	ret.length_samples = static_cast<ALuint>(ov_pcm_total(&m_file, -1));
-	ret.length_seconds = static_cast<f32>(ov_time_total(&m_file, -1));
-
-	return ret;
-}
-
-RAIIALSoundBuffer RAIIOggFile::loadBuffer(const OggFileDecodeInfo &decode_info,
-		ALuint pcm_start, ALuint pcm_end)
-{
-	constexpr int endian = 0; // 0 for Little-Endian, 1 for Big-Endian
-	constexpr int word_size = 2; // we use s16 samples
-	constexpr int word_signed = 1; // ^
-
-	// seek
-	if (ov_pcm_tell(&m_file) != pcm_start) {
-		if (ov_pcm_seek(&m_file, pcm_start) != 0) {
-			warningstream << "Audio: Error decoding (could not seek) "
-					<< decode_info.name_for_logging << std::endl;
-			return RAIIALSoundBuffer();
-		}
-	}
-
-	const size_t size = static_cast<size_t>(pcm_end - pcm_start)
-			* decode_info.bytes_per_sample;
-
-	std::unique_ptr<char[]> snd_buffer(new char[size]);
-
-	// read size bytes
-	size_t read_count = 0;
-	int bitStream;
-	while (read_count < size) {
-		// Read up to a buffer's worth of decoded sound data
-		long num_bytes = ov_read(&m_file, &snd_buffer[read_count], size - read_count,
-				endian, word_size, word_signed, &bitStream);
-
-		if (num_bytes <= 0) {
-			warningstream << "Audio: Error decoding "
-					<< decode_info.name_for_logging << std::endl;
-			return RAIIALSoundBuffer();
-		}
-
-		read_count += num_bytes;
-	}
-
-	// load buffer to openal
-	RAIIALSoundBuffer snd_buffer_id = RAIIALSoundBuffer::generate();
-	alBufferData(snd_buffer_id.get(), decode_info.format, &(snd_buffer[0]), size,
-			decode_info.freq);
-
-	ALenum error = alGetError();
-	if (error != AL_NO_ERROR) {
-		warningstream << "Audio: OpenAL error: " << getAlErrorString(error)
-				<< "preparing sound buffer for sound \""
-				<< decode_info.name_for_logging << "\"" << std::endl;
-	}
-
-	return snd_buffer_id;
-}
-
-/*
- * SoundManagerSingleton class
- */
-
-bool SoundManagerSingleton::init()
-{
-	if (!(m_device = unique_ptr_alcdevice(alcOpenDevice(nullptr)))) {
-		errorstream << "Audio: Global Initialization: Failed to open device" << std::endl;
-		return false;
-	}
-
-	if (!(m_context = unique_ptr_alccontext(alcCreateContext(m_device.get(), nullptr)))) {
-		errorstream << "Audio: Global Initialization: Failed to create context" << std::endl;
-		return false;
-	}
-
-	if (!alcMakeContextCurrent(m_context.get())) {
-		errorstream << "Audio: Global Initialization: Failed to make current context" << std::endl;
-		return false;
-	}
-
-	alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED);
-
-	// Speed of sound in nodes per second
-	// FIXME: This value assumes 1 node sidelength = 1 meter, and "normal" air.
-	//        Ideally this should be mod-controlled.
-	alSpeedOfSound(343.3f);
-
-	// doppler effect turned off for now, for best backwards compatibility
-	alDopplerFactor(0.0f);
-
-	if (alGetError() != AL_NO_ERROR) {
-		errorstream << "Audio: Global Initialization: OpenAL Error " << alGetError() << std::endl;
-		return false;
-	}
-
-	infostream << "Audio: Global Initialized: OpenAL " << alGetString(AL_VERSION)
-		<< ", using " << alcGetString(m_device.get(), ALC_DEVICE_SPECIFIER)
-		<< std::endl;
-
-	return true;
-}
-
-SoundManagerSingleton::~SoundManagerSingleton()
-{
-	infostream << "Audio: Global Deinitialized." << std::endl;
-}
-
-/*
- * ISoundDataOpen struct
- */
-
-std::shared_ptr<ISoundDataOpen> ISoundDataOpen::fromOggFile(std::unique_ptr<RAIIOggFile> oggfile,
-		const std::string &filename_for_logging)
-{
-	// Get some information about the OGG file
-	std::optional<OggFileDecodeInfo> decode_info = oggfile->getDecodeInfo(filename_for_logging);
-	if (!decode_info.has_value()) {
-		warningstream << "Audio: Error decoding "
-				<< filename_for_logging << std::endl;
-		return nullptr;
-	}
-
-	// use duration (in seconds) to decide whether to load all at once or to stream
-	if (decode_info->length_seconds <= SOUND_DURATION_MAX_SINGLE) {
-		return std::make_shared<SoundDataOpenBuffer>(std::move(oggfile), *decode_info);
-	} else {
-		return std::make_shared<SoundDataOpenStream>(std::move(oggfile), *decode_info);
-	}
-}
-
-/*
- * SoundDataUnopenBuffer struct
- */
-
-std::shared_ptr<ISoundDataOpen> SoundDataUnopenBuffer::open(const std::string &sound_name) &&
-{
-	// load from m_buffer
-
-	auto oggfile = std::make_unique<RAIIOggFile>();
-
-	auto buffer_source = std::make_unique<OggVorbisBufferSource>();
-	buffer_source->buf = std::move(m_buffer);
-
-	oggfile->m_needs_clear = true;
-	if (ov_open_callbacks(buffer_source.release(), oggfile->get(), nullptr, 0,
-			OggVorbisBufferSource::s_ov_callbacks) != 0) {
-		warningstream << "Audio: Error opening " << sound_name << " for decoding"
-				<< std::endl;
-		return nullptr;
-	}
-
-	return ISoundDataOpen::fromOggFile(std::move(oggfile), sound_name);
-}
-
-/*
- * SoundDataUnopenFile struct
- */
-
-std::shared_ptr<ISoundDataOpen> SoundDataUnopenFile::open(const std::string &sound_name) &&
-{
-	// load from file at m_path
-
-	auto oggfile = std::make_unique<RAIIOggFile>();
-
-	if (ov_fopen(m_path.c_str(), oggfile->get()) != 0) {
-		warningstream << "Audio: Error opening " << m_path << " for decoding"
-				<< std::endl;
-		return nullptr;
-	}
-	oggfile->m_needs_clear = true;
-
-	return ISoundDataOpen::fromOggFile(std::move(oggfile), sound_name);
-}
-
-/*
- * SoundDataOpenBuffer struct
- */
-
-SoundDataOpenBuffer::SoundDataOpenBuffer(std::unique_ptr<RAIIOggFile> oggfile,
-		const OggFileDecodeInfo &decode_info) : ISoundDataOpen(decode_info)
-{
-	m_buffer = oggfile->loadBuffer(m_decode_info, 0, m_decode_info.length_samples);
-	if (m_buffer.get() == 0) {
-		warningstream << "SoundDataOpenBuffer: Failed to load sound \""
-				<< m_decode_info.name_for_logging << "\"" << std::endl;
-		return;
-	}
-}
-
-/*
- * SoundDataOpenStream struct
- */
-
-SoundDataOpenStream::SoundDataOpenStream(std::unique_ptr<RAIIOggFile> oggfile,
-		const OggFileDecodeInfo &decode_info) :
-	ISoundDataOpen(decode_info), m_oggfile(std::move(oggfile))
-{
-	// do nothing here. buffers are loaded at getOrLoadBufferAt
-}
-
-std::tuple<ALuint, ALuint, ALuint> SoundDataOpenStream::getOrLoadBufferAt(ALuint offset)
-{
-	if (offset >= m_decode_info.length_samples)
-		return {0, m_decode_info.length_samples, 0};
-
-	// find the right-most ContiguousBuffers, such that `m_start <= offset`
-	// equivalent: the first element from the right such that `!(m_start > offset)`
-	// (from the right, `offset` is a lower bound to the `m_start`s)
-	auto lower_rit = std::lower_bound(m_bufferss.rbegin(), m_bufferss.rend(), offset,
-			[](const ContiguousBuffers &bufs, ALuint offset) {
-				return bufs.m_start > offset;
-			});
-
-	if (lower_rit != m_bufferss.rend()) {
-		std::vector<SoundBufferUntil> &bufs = lower_rit->m_buffers;
-		// find the left-most SoundBufferUntil, such that `m_end > offset`
-		// equivalent: the first element from the left such that `m_end > offset`
-		// (returns first element where comp gives true)
-		auto upper_it = std::upper_bound(bufs.begin(), bufs.end(), offset,
-				[](ALuint offset, const SoundBufferUntil &buf) {
-					return offset < buf.m_end;
-				});
-
-		if (upper_it != bufs.end()) {
-			ALuint start = upper_it == bufs.begin() ? lower_rit->m_start
-					: (upper_it - 1)->m_end;
-			return {upper_it->m_buffer.get(), upper_it->m_end, offset - start};
-		}
-	}
-
-	// no loaded buffer starts before or at `offset`
-	// or no loaded buffer (that starts before or at `offset`) ends after `offset`
-
-	// lower_rit, but not reverse and 1 farther
-	auto after_it = m_bufferss.begin() + (m_bufferss.rend() - lower_rit);
-
-	return loadBufferAt(offset, after_it);
-}
-
-std::tuple<ALuint, ALuint, ALuint> SoundDataOpenStream::loadBufferAt(ALuint offset,
-		std::vector<ContiguousBuffers>::iterator after_it)
-{
-	bool has_before = after_it != m_bufferss.begin();
-	bool has_after = after_it != m_bufferss.end();
-
-	ALuint end_before = has_before ? (after_it - 1)->m_buffers.back().m_end : 0;
-	ALuint start_after = has_after ? after_it->m_start : m_decode_info.length_samples;
-
-	const ALuint min_buf_len_samples = m_decode_info.freq * MIN_STREAM_BUFFER_LENGTH;
-
-	//
-	// 1) Find the actual start and end of the new buffer
-	//
-
-	ALuint new_buf_start = offset;
-	ALuint new_buf_end = offset + min_buf_len_samples;
-
-	// Don't load into next buffer, or past the end
-	if (new_buf_end > start_after) {
-		new_buf_end = start_after;
-		// Also move start (for min buf size) (but not *into* previous buffer)
-		if (new_buf_end - new_buf_start < min_buf_len_samples) {
-			new_buf_start = std::max(
-					end_before,
-					new_buf_end < min_buf_len_samples ? 0
-							: new_buf_end - min_buf_len_samples
-				);
-		}
-	}
-
-	// Widen if space to right or left is smaller than min buf size
-	if (new_buf_start - end_before < min_buf_len_samples)
-		new_buf_start = end_before;
-	if (start_after - new_buf_end < min_buf_len_samples)
-		new_buf_end = start_after;
-
-	//
-	// 2) Load [new_buf_start, new_buf_end)
-	//
-
-	// If it fails, we get a 0-buffer. we store it and won't try loading again
-	RAIIALSoundBuffer new_buf = m_oggfile->loadBuffer(m_decode_info, new_buf_start,
-			new_buf_end);
-
-	//
-	// 3) Insert before after_it
-	//
-
-	// Choose ContiguousBuffers to add the new SoundBufferUntil into:
-	// * `after_it - 1` (=before) if existent and if there's no space between its
-	//   last buffer and the new buffer
-	// * A new ContiguousBuffers otherwise
-	auto it = has_before && new_buf_start == end_before ? after_it - 1
-			: m_bufferss.insert(after_it, ContiguousBuffers{new_buf_start, {}});
-
-	// Add the new SoundBufferUntil
-	size_t new_buf_i = it->m_buffers.size();
-	it->m_buffers.push_back(SoundBufferUntil{new_buf_end, std::move(new_buf)});
-
-	if (has_after && new_buf_end == start_after) {
-		// Merge after into my ContiguousBuffers
-		auto &bufs = it->m_buffers;
-		auto &bufs_after = (it + 1)->m_buffers;
-		bufs.insert(bufs.end(), std::make_move_iterator(bufs_after.begin()),
-				std::make_move_iterator(bufs_after.end()));
-		it = m_bufferss.erase(it + 1) - 1;
-	}
-
-	return {it->m_buffers[new_buf_i].m_buffer.get(), new_buf_end, offset - new_buf_start};
-}
-
-/*
- * PlayingSound class
- */
-
-PlayingSound::PlayingSound(ALuint source_id, std::shared_ptr<ISoundDataOpen> data,
-		bool loop, f32 volume, f32 pitch, f32 start_time,
-		const std::optional<std::pair<v3f, v3f>> &pos_vel_opt)
-	: m_source_id(source_id), m_data(std::move(data)), m_looping(loop),
-	m_is_positional(pos_vel_opt.has_value())
-{
-	// Calculate actual start_time (see lua_api.txt for specs)
-	f32 len_seconds = m_data->m_decode_info.length_seconds;
-	f32 len_samples = m_data->m_decode_info.length_samples;
-	if (!m_looping) {
-		if (start_time < 0.0f) {
-			start_time = std::fmax(start_time + len_seconds, 0.0f);
-		} else if (start_time >= len_seconds) {
-			// No sound
-			m_next_sample_pos = len_samples;
-			return;
-		}
-	} else {
-		// Modulo offset to be within looping time
-		start_time = start_time - std::floor(start_time / len_seconds) * len_seconds;
-	}
-
-	// Queue first buffers
-
-	m_next_sample_pos = std::min((start_time / len_seconds) * len_samples, len_samples);
-
-	if (m_looping && m_next_sample_pos == len_samples)
-		m_next_sample_pos = 0;
-
-	if (!m_data->isStreaming()) {
-		// If m_next_sample_pos >= len_samples, buf will be 0, and setting it as
-		// AL_BUFFER is a NOP (source stays AL_UNDETERMINED). => No sound will be
-		// played.
-
-		auto [buf, buf_end, offset_in_buf] = m_data->getOrLoadBufferAt(m_next_sample_pos);
-		m_next_sample_pos = buf_end;
-
-		alSourcei(m_source_id, AL_BUFFER, buf);
-		alSourcei(m_source_id, AL_SAMPLE_OFFSET, offset_in_buf);
-
-		alSourcei(m_source_id, AL_LOOPING, m_looping ? AL_TRUE : AL_FALSE);
-
-		warn_if_al_error("when creating non-streaming sound");
-
-	} else {
-		// Start with 2 buffers
-		ALuint buf_ids[2];
-
-		// If m_next_sample_pos >= len_samples (happens only if not looped), one
-		// or both of buf_ids will be 0. Queuing 0 is a NOP.
-
-		auto [buf0, buf0_end, offset_in_buf0] = m_data->getOrLoadBufferAt(m_next_sample_pos);
-		buf_ids[0] = buf0;
-		m_next_sample_pos = buf0_end;
-
-		if (m_looping && m_next_sample_pos == len_samples)
-			m_next_sample_pos = 0;
-
-		auto [buf1, buf1_end, offset_in_buf1] = m_data->getOrLoadBufferAt(m_next_sample_pos);
-		buf_ids[1] = buf1;
-		m_next_sample_pos = buf1_end;
-		assert(offset_in_buf1 == 0);
-
-		alSourceQueueBuffers(m_source_id, 2, buf_ids);
-		alSourcei(m_source_id, AL_SAMPLE_OFFSET, offset_in_buf0);
-
-		// We can't use AL_LOOPING because more buffers are queued later
-		// looping is therefore done manually
-
-		m_stopped_means_dead = false;
-
-		warn_if_al_error("when creating streaming sound");
-	}
-
-	// Set initial pos, volume, pitch
-	if (m_is_positional) {
-		updatePosVel(pos_vel_opt->first, pos_vel_opt->second);
-	} else {
-		// Make position-less
-		alSourcei(m_source_id, AL_SOURCE_RELATIVE, true);
-		alSource3f(m_source_id, AL_POSITION, 0.0f, 0.0f, 0.0f);
-		alSource3f(m_source_id, AL_VELOCITY, 0.0f, 0.0f, 0.0f);
-		warn_if_al_error("PlayingSound::PlayingSound at making position-less");
-	}
-	setGain(volume);
-	setPitch(pitch);
-}
-
-bool PlayingSound::stepStream()
-{
-	if (isDead())
-		return false;
-
-	// unqueue finished buffers
-	ALint num_unqueued_bufs = 0;
-	alGetSourcei(m_source_id, AL_BUFFERS_PROCESSED, &num_unqueued_bufs);
-	if (num_unqueued_bufs == 0)
-		return true;
-	// We always have 2 buffers enqueued at most
-	SANITY_CHECK(num_unqueued_bufs <= 2);
-	ALuint unqueued_buffer_ids[2];
-	alSourceUnqueueBuffers(m_source_id, num_unqueued_bufs, unqueued_buffer_ids);
-
-	// Fill up again
-	for (ALint i = 0; i < num_unqueued_bufs; ++i) {
-		if (m_next_sample_pos == m_data->m_decode_info.length_samples) {
-			// Reached end
-			if (m_looping) {
-				m_next_sample_pos = 0;
-			} else {
-				m_stopped_means_dead = true;
-				return false;
-			}
-		}
-
-		auto [buf, buf_end, offset_in_buf] = m_data->getOrLoadBufferAt(m_next_sample_pos);
-		m_next_sample_pos = buf_end;
-		assert(offset_in_buf == 0);
-
-		alSourceQueueBuffers(m_source_id, 1, &buf);
-
-		// Start again if queue was empty and resulted in stop
-		if (getState() == AL_STOPPED) {
-			play();
-			warningstream << "PlayingSound::stepStream: Sound queue ran empty for \""
-					<< m_data->m_decode_info.name_for_logging << "\"" << std::endl;
-		}
-	}
-
-	return true;
-}
-
-bool PlayingSound::fade(f32 step, f32 target_gain) noexcept
-{
-	bool already_fading = m_fade_state.has_value();
-
-	target_gain = MYMAX(target_gain, 0.0f); // 0.0f if nan
-	step = target_gain - getGain() > 0.0f ? std::abs(step) : -std::abs(step);
-
-	m_fade_state = FadeState{step, target_gain};
-
-	return !already_fading;
-}
-
-bool PlayingSound::doFade(f32 dtime) noexcept
-{
-	if (!m_fade_state || isDead())
-		return false;
-
-	FadeState &fade = *m_fade_state;
-	assert(fade.step != 0.0f);
-
-	f32 current_gain = getGain();
-	current_gain += fade.step * dtime;
-
-	if (fade.step < 0.0f)
-		current_gain = std::max(current_gain, fade.target_gain);
-	else
-		current_gain = std::min(current_gain, fade.target_gain);
-
-	if (current_gain <= 0.0f) {
-		// stop sound
-		m_stopped_means_dead = true;
-		alSourceStop(m_source_id);
-
-		m_fade_state = std::nullopt;
-		return false;
-	}
-
-	setGain(current_gain);
-
-	if (current_gain == fade.target_gain) {
-		m_fade_state = std::nullopt;
-		return false;
-	} else {
-		return true;
-	}
-}
-
-void PlayingSound::updatePosVel(const v3f &pos, const v3f &vel) noexcept
-{
-	alSourcei(m_source_id, AL_SOURCE_RELATIVE, false);
-	alSource3f(m_source_id, AL_POSITION, pos.X, pos.Y, pos.Z);
-	alSource3f(m_source_id, AL_VELOCITY, vel.X, vel.Y, vel.Z);
-	// Using alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED) and setting reference
-	// distance to clamp gain at <1 node distance avoids excessive volume when
-	// closer.
-	alSourcef(m_source_id, AL_REFERENCE_DISTANCE, 1.0f);
-
-	warn_if_al_error("PlayingSound::updatePosVel");
-}
-
-void PlayingSound::setGain(f32 gain) noexcept
-{
-	// AL_REFERENCE_DISTANCE was once reduced from 3 nodes to 1 node.
-	// We compensate this by multiplying the volume by 3.
-	if (m_is_positional)
-		gain *= 3.0f;
-
-	alSourcef(m_source_id, AL_GAIN, gain);
-}
-
-f32 PlayingSound::getGain() noexcept
-{
-	ALfloat gain;
-	alGetSourcef(m_source_id, AL_GAIN, &gain);
-	// Same as above, but inverse.
-	if (m_is_positional)
-		gain *= 1.0f/3.0f;
-	return gain;
-}
-
-/*
- * OpenALSoundManager class
- */
-
-void OpenALSoundManager::stepStreams(f32 dtime)
-{
-	// spread work across steps
-	const size_t num_issued_sounds = std::min(
-			m_sounds_streaming_current_bigstep.size(),
-			(size_t)std::ceil(m_sounds_streaming_current_bigstep.size()
-				* dtime / m_stream_timer)
-		);
-
-	for (size_t i = 0; i < num_issued_sounds; ++i) {
-		auto wptr = std::move(m_sounds_streaming_current_bigstep.back());
-		m_sounds_streaming_current_bigstep.pop_back();
-
-		std::shared_ptr<PlayingSound> snd = wptr.lock();
-		if (!snd)
-			continue;
-
-		if (!snd->stepStream())
-			continue;
-
-		// sound still lives and needs more stream-stepping => add to next bigstep
-		m_sounds_streaming_next_bigstep.push_back(std::move(wptr));
-	}
-
-	m_stream_timer -= dtime;
-	if (m_stream_timer <= 0.0f) {
-		m_stream_timer = STREAM_BIGSTEP_TIME;
-		using std::swap;
-		swap(m_sounds_streaming_current_bigstep, m_sounds_streaming_next_bigstep);
-	}
-}
-
-void OpenALSoundManager::doFades(f32 dtime)
-{
-	for (size_t i = 0; i < m_sounds_fading.size();) {
-		std::shared_ptr<PlayingSound> snd = m_sounds_fading[i].lock();
-		if (snd) {
-			if (snd->doFade(dtime)) {
-				// needs more fading later, keep in m_sounds_fading
-				++i;
-				continue;
-			}
-		}
-
-		// sound no longer needs to be faded
-		m_sounds_fading[i] = std::move(m_sounds_fading.back());
-		m_sounds_fading.pop_back();
-		// continue with same i
-	}
-}
-
-std::shared_ptr<ISoundDataOpen> OpenALSoundManager::openSingleSound(const std::string &sound_name)
-{
-	// if already open, nothing to do
-	auto it = m_sound_datas_open.find(sound_name);
-	if (it != m_sound_datas_open.end())
-		return it->second;
-
-	// find unopened data
-	auto it_unopen = m_sound_datas_unopen.find(sound_name);
-	if (it_unopen == m_sound_datas_unopen.end())
-		return nullptr;
-	std::unique_ptr<ISoundDataUnopen> unopn_snd = std::move(it_unopen->second);
-	m_sound_datas_unopen.erase(it_unopen);
-
-	// open
-	std::shared_ptr<ISoundDataOpen> opn_snd = std::move(*unopn_snd).open(sound_name);
-	if (!opn_snd)
-		return nullptr;
-	m_sound_datas_open.emplace(sound_name, opn_snd);
-	return opn_snd;
-}
-
-std::string OpenALSoundManager::getLoadedSoundNameFromGroup(const std::string &group_name)
-{
-	std::string chosen_sound_name = "";
-
-	auto it_groups = m_sound_groups.find(group_name);
-	if (it_groups == m_sound_groups.end())
-		return "";
-
-	std::vector<std::string> &group_sounds = it_groups->second;
-	while (!group_sounds.empty()) {
-		// choose one by random
-		int j = myrand() % group_sounds.size();
-		chosen_sound_name = group_sounds[j];
-
-		// find chosen one
-		std::shared_ptr<ISoundDataOpen> snd = openSingleSound(chosen_sound_name);
-		if (snd)
-			return chosen_sound_name;
-
-		// it doesn't exist
-		// remove it from the group and try again
-		group_sounds[j] = std::move(group_sounds.back());
-		group_sounds.pop_back();
-	}
-
-	return "";
-}
-
-std::string OpenALSoundManager::getOrLoadLoadedSoundNameFromGroup(const std::string &group_name)
-{
-	std::string sound_name = getLoadedSoundNameFromGroup(group_name);
-	if (!sound_name.empty())
-		return sound_name;
-
-	// load
-	std::vector<std::string> paths = m_fallback_path_provider
-			->getLocalFallbackPathsForSoundname(group_name);
-	for (const std::string &path : paths) {
-		if (loadSoundFile(path, path))
-			addSoundToGroup(path, group_name);
-	}
-	return getLoadedSoundNameFromGroup(group_name);
-}
-
-std::shared_ptr<PlayingSound> OpenALSoundManager::createPlayingSound(
-		const std::string &sound_name, bool loop, f32 volume, f32 pitch,
-		f32 start_time, const std::optional<std::pair<v3f, v3f>> &pos_vel_opt)
-{
-	infostream << "OpenALSoundManager: Creating playing sound \"" << sound_name
-			<< "\"" << std::endl;
-	warn_if_al_error("before createPlayingSound");
-
-	std::shared_ptr<ISoundDataOpen> lsnd = openSingleSound(sound_name);
-	if (!lsnd) {
-		// does not happen because of the call to getLoadedSoundNameFromGroup
-		errorstream << "OpenALSoundManager::createPlayingSound: Sound \""
-				<< sound_name << "\" disappeared." << std::endl;
-		return nullptr;
-	}
-
-	if (lsnd->m_decode_info.is_stereo && pos_vel_opt.has_value()) {
-		warningstream << "OpenALSoundManager::createPlayingSound: "
-				<< "Creating positional stereo sound \"" << sound_name << "\"."
-				<< std::endl;
-	}
-
-	ALuint source_id;
-	alGenSources(1, &source_id);
-	if (warn_if_al_error("createPlayingSound (alGenSources)") != AL_NO_ERROR) {
-		// happens ie. if there are too many sources (out of memory)
-		return nullptr;
-	}
-
-	auto sound = std::make_shared<PlayingSound>(source_id, std::move(lsnd), loop,
-			volume, pitch, start_time, pos_vel_opt);
-
-	sound->play();
-	if (m_is_paused)
-		sound->pause();
-	warn_if_al_error("createPlayingSound");
-	return sound;
-}
-
-void OpenALSoundManager::playSoundGeneric(sound_handle_t id, const std::string &group_name,
-		bool loop, f32 volume, f32 fade, f32 pitch, bool use_local_fallback,
-		f32 start_time, const std::optional<std::pair<v3f, v3f>> &pos_vel_opt)
-{
-	assert(id != 0);
-
-	if (group_name.empty()) {
-		reportRemovedSound(id);
-		return;
-	}
-
-	// choose random sound name from group name
-	std::string sound_name = use_local_fallback ?
-			getOrLoadLoadedSoundNameFromGroup(group_name) :
-			getLoadedSoundNameFromGroup(group_name);
-	if (sound_name.empty()) {
-		infostream << "OpenALSoundManager: \"" << group_name << "\" not found."
-				<< std::endl;
-		reportRemovedSound(id);
-		return;
-	}
-
-	volume = std::max(0.0f, volume);
-	f32 target_fade_volume = volume;
-	if (fade > 0.0f)
-		volume = 0.0f;
-
-	if (!(pitch > 0.0f)) {
-		warningstream << "OpenALSoundManager::playSoundGeneric: Illegal pitch value: "
-				<< start_time << std::endl;
-		pitch = 1.0f;
-	}
-
-	if (!std::isfinite(start_time)) {
-		warningstream << "OpenALSoundManager::playSoundGeneric: Illegal start_time value: "
-				<< start_time << std::endl;
-		start_time = 0.0f;
-	}
-
-	// play it
-	std::shared_ptr<PlayingSound> sound = createPlayingSound(sound_name, loop,
-			volume, pitch, start_time, pos_vel_opt);
-	if (!sound) {
-		reportRemovedSound(id);
-		return;
-	}
-
-	// add to streaming sounds if streaming
-	if (sound->isStreaming())
-		m_sounds_streaming_next_bigstep.push_back(sound);
-
-	m_sounds_playing.emplace(id, std::move(sound));
-
-	if (fade > 0.0f)
-		fadeSound(id, fade, target_fade_volume);
-}
-
-int OpenALSoundManager::removeDeadSounds()
-{
-	int num_deleted_sounds = 0;
-
-	for (auto it = m_sounds_playing.begin(); it != m_sounds_playing.end();) {
-		sound_handle_t id = it->first;
-		PlayingSound &sound = *it->second;
-		// If dead, remove it
-		if (sound.isDead()) {
-			it = m_sounds_playing.erase(it);
-			reportRemovedSound(id);
-			++num_deleted_sounds;
-		} else {
-			++it;
-		}
-	}
-
-	return num_deleted_sounds;
-}
-
-OpenALSoundManager::OpenALSoundManager(SoundManagerSingleton *smg,
-		std::unique_ptr<SoundFallbackPathProvider> fallback_path_provider) :
-	Thread("OpenALSoundManager"),
-	m_fallback_path_provider(std::move(fallback_path_provider)),
-	m_device(smg->m_device.get()),
-	m_context(smg->m_context.get())
-{
-	SANITY_CHECK(!!m_fallback_path_provider);
-
-	infostream << "Audio: Initialized: OpenAL " << std::endl;
-}
-
-OpenALSoundManager::~OpenALSoundManager()
-{
-	infostream << "Audio: Deinitializing..." << std::endl;
-}
-
-/* Interface */
-
-void OpenALSoundManager::step(f32 dtime)
-{
-	m_time_until_dead_removal -= dtime;
-	if (m_time_until_dead_removal <= 0.0f) {
-		if (!m_sounds_playing.empty()) {
-			verbosestream << "OpenALSoundManager::step(): "
-					<< m_sounds_playing.size() << " playing sounds, "
-					<< m_sound_datas_unopen.size() << " unopen sounds, "
-					<< m_sound_datas_open.size() << " open sounds and "
-					<< m_sound_groups.size() << " sound groups loaded."
-					<< std::endl;
-		}
-
-		int num_deleted_sounds = removeDeadSounds();
-
-		if (num_deleted_sounds != 0)
-			verbosestream << "OpenALSoundManager::step(): Deleted "
-					<< num_deleted_sounds << " dead playing sounds." << std::endl;
-
-		m_time_until_dead_removal = REMOVE_DEAD_SOUNDS_INTERVAL;
-	}
-
-	doFades(dtime);
-	stepStreams(dtime);
-}
-
-void OpenALSoundManager::pauseAll()
-{
-	for (auto &snd_p : m_sounds_playing) {
-		PlayingSound &snd = *snd_p.second;
-		snd.pause();
-	}
-	m_is_paused = true;
-}
-
-void OpenALSoundManager::resumeAll()
-{
-	for (auto &snd_p : m_sounds_playing) {
-		PlayingSound &snd = *snd_p.second;
-		snd.resume();
-	}
-	m_is_paused = false;
-}
-
-void OpenALSoundManager::updateListener(const v3f &pos_, const v3f &vel_,
-		const v3f &at_, const v3f &up_)
-{
-	v3f pos = swap_handedness(pos_);
-	v3f vel = swap_handedness(vel_);
-	v3f at = swap_handedness(at_);
-	v3f up = swap_handedness(up_);
-	ALfloat orientation[6] = {at.X, at.Y, at.Z, up.X, up.Y, up.Z};
-
-	alListener3f(AL_POSITION, pos.X, pos.Y, pos.Z);
-	alListener3f(AL_VELOCITY, vel.X, vel.Y, vel.Z);
-	alListenerfv(AL_ORIENTATION, orientation);
-	warn_if_al_error("updateListener");
-}
-
-void OpenALSoundManager::setListenerGain(f32 gain)
-{
-	alListenerf(AL_GAIN, gain);
-}
-
-bool OpenALSoundManager::loadSoundFile(const std::string &name, const std::string &filepath)
-{
-	// do not add twice
-	if (m_sound_datas_open.count(name) != 0 || m_sound_datas_unopen.count(name) != 0)
-		return false;
-
-	// coarse check
-	if (!fs::IsFile(filepath))
-		return false;
-
-	loadSoundFileNoCheck(name, filepath);
-	return true;
-}
-
-bool OpenALSoundManager::loadSoundData(const std::string &name, std::string &&filedata)
-{
-	// do not add twice
-	if (m_sound_datas_open.count(name) != 0 || m_sound_datas_unopen.count(name) != 0)
-		return false;
-
-	loadSoundDataNoCheck(name, std::move(filedata));
-	return true;
-}
-
-void OpenALSoundManager::loadSoundFileNoCheck(const std::string &name, const std::string &filepath)
-{
-	// remember for lazy loading
-	m_sound_datas_unopen.emplace(name, std::make_unique<SoundDataUnopenFile>(filepath));
-}
-
-void OpenALSoundManager::loadSoundDataNoCheck(const std::string &name, std::string &&filedata)
-{
-	// remember for lazy loading
-	m_sound_datas_unopen.emplace(name, std::make_unique<SoundDataUnopenBuffer>(std::move(filedata)));
-}
-
-void OpenALSoundManager::addSoundToGroup(const std::string &sound_name, const std::string &group_name)
-{
-	auto it_groups = m_sound_groups.find(group_name);
-	if (it_groups != m_sound_groups.end())
-		it_groups->second.push_back(sound_name);
-	else
-		m_sound_groups.emplace(group_name, std::vector<std::string>{sound_name});
-}
-
-void OpenALSoundManager::playSound(sound_handle_t id, const SoundSpec &spec)
-{
-	return playSoundGeneric(id, spec.name, spec.loop, spec.gain, spec.fade, spec.pitch,
-			spec.use_local_fallback, spec.start_time, std::nullopt);
-}
-
-void OpenALSoundManager::playSoundAt(sound_handle_t id, const SoundSpec &spec,
-		const v3f &pos_, const v3f &vel_)
-{
-	std::optional<std::pair<v3f, v3f>> pos_vel_opt({
-			swap_handedness(pos_),
-			swap_handedness(vel_)
-		});
-
-	return playSoundGeneric(id, spec.name, spec.loop, spec.gain, spec.fade, spec.pitch,
-			spec.use_local_fallback, spec.start_time, pos_vel_opt);
-}
-
-void OpenALSoundManager::stopSound(sound_handle_t sound)
-{
-	m_sounds_playing.erase(sound);
-	reportRemovedSound(sound);
-}
-
-void OpenALSoundManager::fadeSound(sound_handle_t soundid, f32 step, f32 target_gain)
-{
-	// Ignore the command if step isn't valid.
-	if (step == 0.0f)
-		return;
-	auto sound_it = m_sounds_playing.find(soundid);
-	if (sound_it == m_sounds_playing.end())
-		return; // No sound to fade
-	PlayingSound &sound = *sound_it->second;
-	if (sound.fade(step, target_gain))
-		m_sounds_fading.emplace_back(sound_it->second);
-}
-
-void OpenALSoundManager::updateSoundPosVel(sound_handle_t id, const v3f &pos_,
-		const v3f &vel_)
-{
-	v3f pos = swap_handedness(pos_);
-	v3f vel = swap_handedness(vel_);
-
-	auto i = m_sounds_playing.find(id);
-	if (i == m_sounds_playing.end())
-		return;
-	i->second->updatePosVel(pos, vel);
-}
-
-void *OpenALSoundManager::run()
-{
-	using namespace sound_manager_messages_to_mgr;
-
-	struct MsgVisitor {
-		enum class Result { Ok, Empty, StopRequested };
-
-		OpenALSoundManager &mgr;
-
-		Result operator()(std::monostate &&) {
-			return Result::Empty; }
-
-		Result operator()(PauseAll &&) {
-			mgr.pauseAll(); return Result::Ok; }
-		Result operator()(ResumeAll &&) {
-			mgr.resumeAll(); return Result::Ok; }
-
-		Result operator()(UpdateListener &&msg) {
-			mgr.updateListener(msg.pos_, msg.vel_, msg.at_, msg.up_); return Result::Ok; }
-		Result operator()(SetListenerGain &&msg) {
-			mgr.setListenerGain(msg.gain); return Result::Ok; }
-
-		Result operator()(LoadSoundFile &&msg) {
-			mgr.loadSoundFileNoCheck(msg.name, msg.filepath); return Result::Ok; }
-		Result operator()(LoadSoundData &&msg) {
-			mgr.loadSoundDataNoCheck(msg.name, std::move(msg.filedata)); return Result::Ok; }
-		Result operator()(AddSoundToGroup &&msg) {
-			mgr.addSoundToGroup(msg.sound_name, msg.group_name); return Result::Ok; }
-
-		Result operator()(PlaySound &&msg) {
-			mgr.playSound(msg.id, msg.spec); return Result::Ok; }
-		Result operator()(PlaySoundAt &&msg) {
-			mgr.playSoundAt(msg.id, msg.spec, msg.pos_, msg.vel_); return Result::Ok; }
-		Result operator()(StopSound &&msg) {
-			mgr.stopSound(msg.sound); return Result::Ok; }
-		Result operator()(FadeSound &&msg) {
-			mgr.fadeSound(msg.soundid, msg.step, msg.target_gain); return Result::Ok; }
-		Result operator()(UpdateSoundPosVel &&msg) {
-			mgr.updateSoundPosVel(msg.sound, msg.pos_, msg.vel_); return Result::Ok; }
-
-		Result operator()(PleaseStop &&msg) {
-			return Result::StopRequested; }
-	};
-
-	u64 t_step_start = porting::getTimeMs();
-	while (true) {
-		auto get_time_since_last_step = [&] {
-			return (f32)(porting::getTimeMs() - t_step_start);
-		};
-		auto get_remaining_timeout = [&] {
-			return (s32)((1.0e3f * PROXYSOUNDMGR_DTIME) - get_time_since_last_step());
-		};
-
-		bool stop_requested = false;
-
-		while (true) {
-			SoundManagerMsgToMgr msg =
-					m_queue_to_mgr.pop_frontNoEx(std::max(get_remaining_timeout(), 0));
-
-			MsgVisitor::Result res = std::visit(MsgVisitor{*this}, std::move(msg));
-
-			if (res == MsgVisitor::Result::Empty && get_remaining_timeout() <= 0) {
-				break; // finished sleeping
-			} else if (res == MsgVisitor::Result::StopRequested) {
-				stop_requested = true;
-				break;
-			}
-		}
-		if (stop_requested)
-			break;
-
-		f32 dtime = get_time_since_last_step();
-		t_step_start = porting::getTimeMs();
-		step(dtime);
-	}
-
-	send(sound_manager_messages_to_proxy::Stopped{});
-
-	return nullptr;
-}
-
-/*
- * ProxySoundManager class
- */
-
-ProxySoundManager::MsgResult ProxySoundManager::handleMsg(SoundManagerMsgToProxy &&msg)
-{
-	using namespace sound_manager_messages_to_proxy;
-
-	return std::visit([&](auto &&msg) {
-			using T = std::decay_t<decltype(msg)>;
-
-			if constexpr (std::is_same_v<T, std::monostate>)
-				return MsgResult::Empty;
-			else if constexpr (std::is_same_v<T, ReportRemovedSound>)
-				reportRemovedSound(msg.id);
-			else if constexpr (std::is_same_v<T, Stopped>)
-				return MsgResult::Stopped;
-
-			return MsgResult::Ok;
-		},
-		std::move(msg));
-}
-
-ProxySoundManager::~ProxySoundManager()
-{
-	if (m_sound_manager.isRunning()) {
-		send(sound_manager_messages_to_mgr::PleaseStop{});
-
-		// recv until it stopped
-		auto recv = [&]	{
-			return m_sound_manager.m_queue_to_proxy.pop_frontNoEx();
-		};
-
-		while (true) {
-			if (handleMsg(recv()) == MsgResult::Stopped)
-				break;
-		}
-
-		// join
-		m_sound_manager.stop();
-		SANITY_CHECK(m_sound_manager.wait());
-	}
-}
-
-void ProxySoundManager::step(f32 dtime)
-{
-	auto recv = [&]	{
-		return m_sound_manager.m_queue_to_proxy.pop_frontNoEx(0);
-	};
-
-	while (true) {
-		MsgResult res = handleMsg(recv());
-		if (res == MsgResult::Empty)
-			break;
-		else if (res == MsgResult::Stopped)
-			throw std::runtime_error("OpenALSoundManager stopped unexpectedly");
-	}
-}
-
-void ProxySoundManager::pauseAll()
-{
-	send(sound_manager_messages_to_mgr::PauseAll{});
-}
-
-void ProxySoundManager::resumeAll()
-{
-	send(sound_manager_messages_to_mgr::ResumeAll{});
-}
-
-void ProxySoundManager::updateListener(const v3f &pos_, const v3f &vel_,
-		const v3f &at_, const v3f &up_)
-{
-	send(sound_manager_messages_to_mgr::UpdateListener{pos_, vel_, at_, up_});
-}
-
-void ProxySoundManager::setListenerGain(f32 gain)
-{
-	send(sound_manager_messages_to_mgr::SetListenerGain{gain});
-}
-
-bool ProxySoundManager::loadSoundFile(const std::string &name,
-		const std::string &filepath)
-{
-	// do not add twice
-	if (m_known_sound_names.count(name) != 0)
-		return false;
-
-	// coarse check
-	if (!fs::IsFile(filepath))
-		return false;
-
-	send(sound_manager_messages_to_mgr::LoadSoundFile{name, filepath});
-
-	m_known_sound_names.insert(name);
-	return true;
-}
-
-bool ProxySoundManager::loadSoundData(const std::string &name, std::string &&filedata)
-{
-	// do not add twice
-	if (m_known_sound_names.count(name) != 0)
-		return false;
-
-	send(sound_manager_messages_to_mgr::LoadSoundData{name, std::move(filedata)});
-
-	m_known_sound_names.insert(name);
-	return true;
-}
-
-void ProxySoundManager::addSoundToGroup(const std::string &sound_name,
-		const std::string &group_name)
-{
-	send(sound_manager_messages_to_mgr::AddSoundToGroup{sound_name, group_name});
-}
-
-void ProxySoundManager::playSound(sound_handle_t id, const SoundSpec &spec)
-{
-	if (id == 0)
-		id = allocateId(1);
-	send(sound_manager_messages_to_mgr::PlaySound{id, spec});
-}
-
-void ProxySoundManager::playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos_,
-		const v3f &vel_)
-{
-	if (id == 0)
-		id = allocateId(1);
-	send(sound_manager_messages_to_mgr::PlaySoundAt{id, spec, pos_, vel_});
-}
-
-void ProxySoundManager::stopSound(sound_handle_t sound)
-{
-	send(sound_manager_messages_to_mgr::StopSound{sound});
-}
-
-void ProxySoundManager::fadeSound(sound_handle_t soundid, f32 step, f32 target_gain)
-{
-	send(sound_manager_messages_to_mgr::FadeSound{soundid, step, target_gain});
-}
-
-void ProxySoundManager::updateSoundPosVel(sound_handle_t sound, const v3f &pos_, const v3f &vel_)
-{
-	send(sound_manager_messages_to_mgr::UpdateSoundPosVel{sound, pos_, vel_});
-}
diff --git a/src/client/sound/sound_openal_internal.h b/src/client/sound/sound_openal_internal.h
deleted file mode 100644
index 19ba411ef..000000000
--- a/src/client/sound/sound_openal_internal.h
+++ /dev/null
@@ -1,750 +0,0 @@
-/*
-Minetest
-Copyright (C) 2022 DS
-Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
-OpenAL support based on work by:
-Copyright (C) 2011 Sebastian 'Bahamada' Rühl
-Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com>
-Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
-
-This program is free software; you can redistribute it and/or modify
-it under the terms of the GNU Lesser General Public License as published by
-the Free Software Foundation; either version 2.1 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Lesser General Public License for more details.
-
-You should have received a copy of the GNU Lesser General Public License along
-with this program; ifnot, write to the Free Software Foundation, Inc.,
-51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-*/
-
-#pragma once
-
-#include "log.h"
-#include "porting.h"
-#include "sound_openal.h"
-#include "../../sound.h"
-#include "threading/thread.h"
-#include "util/basic_macros.h"
-#include "util/container.h"
-
-#if defined(_WIN32)
-	#include <al.h>
-	#include <alc.h>
-	//#include <alext.h>
-#elif defined(__APPLE__)
-	#define OPENAL_DEPRECATED
-	#include <OpenAL/al.h>
-	#include <OpenAL/alc.h>
-	//#include <OpenAL/alext.h>
-#else
-	#include <AL/al.h>
-	#include <AL/alc.h>
-	#include <AL/alext.h>
-#endif
-#include <vorbis/vorbisfile.h>
-
-#include <optional>
-#include <unordered_map>
-#include <utility>
-#include <variant>
-#include <vector>
-
-
-/*
- *
- * The coordinate space for sounds (sound-space):
- * ----------------------------------------------
- *
- * * The functions from ISoundManager (see sound.h) take spatial vectors in node-space.
- * * All other `v3f`s here are, if not told otherwise, in sound-space, which is
- *   defined as node-space mirrored along the x-axis.
- *   (This is needed because OpenAL uses a right-handed coordinate system.)
- * * Use `swap_handedness()` to convert between those two coordinate spaces.
- *
- *
- * How sounds are loaded:
- * ----------------------
- *
- * * Step 1:
- *   `loadSoundFile` or `loadSoundFile` is called. This adds an unopen sound with
- *   the given name to `m_sound_datas_unopen`.
- *   Unopen / lazy sounds (`ISoundDataUnopen`) are ogg-vorbis files that we did not yet
- *   start to decode. (Decoding an unopen sound does not fail under normal circumstances
- *   (because we check whether the file exists at least), if it does fail anyways,
- *   we should notify the user.)
- * * Step 2:
- *   `addSoundToGroup` is called, to add the name from step 1 to a group. If the
- *   group does not yet exist, a new one is created. A group can later be played.
- *   (The mapping is stored in `m_sound_groups`.)
- * * Step 3:
- *   `playSound` or `playSoundAt` is called.
- *   * Step 3.1:
- *     If the group with the name `spec.name` does not exist, and `spec.use_local_fallback`
- *     is true, a new group is created using the user's sound-pack.
- *   * Step 3.2:
- *     We choose one random sound name from the given group.
- *   * Step 3.3:
- *     We open the sound (see `openSingleSound`).
- *     If the sound is already open (in `m_sound_datas_open`), we take that one.
- *     Otherwise we open it by calling `ISoundDataUnopen::open`. We choose (by
- *     sound length), whether it's a single-buffer (`SoundDataOpenBuffer`) or
- *     streamed (`SoundDataOpenStream`) sound.
- *     Single-buffer sounds are always completely loaded. Streamed sounds can be
- *     partially loaded.
- *     The sound is erased from `m_sound_datas_unopen` and added to `m_sound_datas_open`.
- *     Open sounds are kept forever.
- *   * Step 3.4:
- *     We create the new `PlayingSound`. It has a `shared_ptr` to its open sound.
- *     If the open sound is streaming, the playing sound needs to be stepped using
- *     `PlayingSound::stepStream` for enqueuing buffers. For this purpose, the sound
- *     is added to `m_sounds_streaming` (as `weak_ptr`).
- *     If the sound is fading, it is added to `m_sounds_fading` for regular fade-stepping.
- *     The sound is also added to `m_sounds_playing`, so that one can access it
- *     via its sound handle.
- * * Step 4:
- *     Streaming sounds are updated. For details see [Streaming of sounds].
- * * Step 5:
- *     At deinitialization, we can just let the destructors do their work.
- *     Sound sources are deleted (and with this also stopped) by ~PlayingSound.
- *     Buffers can't be deleted while sound sources using them exist, because
- *     PlayingSound has a shared_ptr to its ISoundData.
- *
- *
- * Streaming of sounds:
- * --------------------
- *
- * In each "bigstep", all streamed sounds are stepStream()ed. This means a
- * sound can be stepped at any point in time in the bigstep's interval.
- *
- * In the worst case, a sound is stepped at the start of one bigstep and in the
- * end of the next bigstep. So between two stepStream()-calls lie at most
- * 2 * STREAM_BIGSTEP_TIME seconds.
- * As there are always 2 sound buffers enqueued, at least one untouched full buffer
- * is still available after the first stepStream().
- * If we take a MIN_STREAM_BUFFER_LENGTH > 2 * STREAM_BIGSTEP_TIME, we can hence
- * not run into an empty queue.
- *
- * The MIN_STREAM_BUFFER_LENGTH needs to be a little bigger because of dtime jitter,
- * other sounds that may have taken long to stepStream(), and sounds being played
- * faster due to Doppler effect.
- *
- */
-
-// constants
-
-// in seconds
-constexpr f32 REMOVE_DEAD_SOUNDS_INTERVAL = 2.0f;
-// maximum length in seconds that a sound can have without being streamed
-constexpr f32 SOUND_DURATION_MAX_SINGLE = 3.0f;
-// minimum time in seconds of a single buffer in a streamed sound
-constexpr f32 MIN_STREAM_BUFFER_LENGTH = 1.0f;
-// duration in seconds of one bigstep
-constexpr f32 STREAM_BIGSTEP_TIME = 0.3f;
-// step duration for the ProxySoundManager, in seconds
-constexpr f32 PROXYSOUNDMGR_DTIME = 0.016f;
-
-static_assert(MIN_STREAM_BUFFER_LENGTH > STREAM_BIGSTEP_TIME * 2.0f,
-		"See [Streaming of sounds].");
-static_assert(SOUND_DURATION_MAX_SINGLE >= MIN_STREAM_BUFFER_LENGTH * 2.0f,
-		"There's no benefit in streaming if we can't queue more than 2 buffers.");
-
-
-/**
- * RAII wrapper for openal sound buffers.
- */
-struct RAIIALSoundBuffer final
-{
-	RAIIALSoundBuffer() noexcept = default;
-	explicit RAIIALSoundBuffer(ALuint buffer) noexcept : m_buffer(buffer) {};
-
-	~RAIIALSoundBuffer() noexcept { reset(0); }
-
-	DISABLE_CLASS_COPY(RAIIALSoundBuffer)
-
-	RAIIALSoundBuffer(RAIIALSoundBuffer &&other) noexcept : m_buffer(other.release()) {}
-	RAIIALSoundBuffer &operator=(RAIIALSoundBuffer &&other) noexcept;
-
-	ALuint get() noexcept { return m_buffer; }
-
-	ALuint release() noexcept { return std::exchange(m_buffer, 0); }
-
-	void reset(ALuint buf) noexcept;
-
-	static RAIIALSoundBuffer generate() noexcept;
-
-private:
-	// According to openal specification:
-	// > Deleting buffer name 0 is a legal NOP.
-	//
-	// and:
-	// > [...] the NULL buffer (i.e., 0) which can always be queued.
-	ALuint m_buffer = 0;
-};
-
-/**
- * For vorbisfile to read from our buffer instead of from a file.
- */
-struct OggVorbisBufferSource {
-	std::string buf;
-	size_t cur_offset = 0;
-
-	static size_t read_func(void *ptr, size_t size, size_t nmemb, void *datasource) noexcept;
-	static int seek_func(void *datasource, ogg_int64_t offset, int whence) noexcept;
-	static int close_func(void *datasource) noexcept;
-	static long tell_func(void *datasource) noexcept;
-
-	static const ov_callbacks s_ov_callbacks;
-};
-
-/**
- * Metadata of an Ogg-Vorbis file, used for decoding.
- * We query this information once and store it in this struct.
- */
-struct OggFileDecodeInfo {
-	std::string name_for_logging;
-	bool is_stereo;
-	ALenum format; // AL_FORMAT_MONO16 or AL_FORMAT_STEREO16
-	size_t bytes_per_sample;
-	ALsizei freq;
-	ALuint length_samples = 0;
-	f32 length_seconds = 0.0f;
-};
-
-/**
- * RAII wrapper for OggVorbis_File.
- */
-struct RAIIOggFile {
-	bool m_needs_clear = false;
-	OggVorbis_File m_file;
-
-	RAIIOggFile() = default;
-
-	DISABLE_CLASS_COPY(RAIIOggFile)
-
-	~RAIIOggFile() noexcept
-	{
-		if (m_needs_clear)
-			ov_clear(&m_file);
-	}
-
-	OggVorbis_File *get() { return &m_file; }
-
-	std::optional<OggFileDecodeInfo> getDecodeInfo(const std::string &filename_for_logging);
-
-	/**
-	 * Main function for loading ogg vorbis sounds.
-	 * Loads exactly the specified interval of PCM-data, and creates an OpenAL
-	 * buffer with it.
-	 *
-	 * @param decode_info Cached meta information of the file.
-	 * @param pcm_start First sample in the interval.
-	 * @param pcm_end One after last sample of the interval (=> exclusive).
-	 * @return An AL sound buffer, or a 0-buffer on failure.
-	 */
-	RAIIALSoundBuffer loadBuffer(const OggFileDecodeInfo &decode_info, ALuint pcm_start,
-			ALuint pcm_end);
-};
-
-
-/**
- * Class for the openal device and context
- */
-class SoundManagerSingleton
-{
-public:
-	struct AlcDeviceDeleter {
-		void operator()(ALCdevice *p)
-		{
-			alcCloseDevice(p);
-		}
-	};
-
-	struct AlcContextDeleter {
-		void operator()(ALCcontext *p)
-		{
-			alcMakeContextCurrent(nullptr);
-			alcDestroyContext(p);
-		}
-	};
-
-	using unique_ptr_alcdevice = std::unique_ptr<ALCdevice, AlcDeviceDeleter>;
-	using unique_ptr_alccontext = std::unique_ptr<ALCcontext, AlcContextDeleter>;
-
-	unique_ptr_alcdevice  m_device;
-	unique_ptr_alccontext m_context;
-
-public:
-	bool init();
-
-	~SoundManagerSingleton();
-};
-
-
-/**
- * Stores sound pcm data buffers.
- */
-struct ISoundDataOpen
-{
-	OggFileDecodeInfo m_decode_info;
-
-	explicit ISoundDataOpen(const OggFileDecodeInfo &decode_info) :
-			m_decode_info(decode_info) {}
-
-	virtual ~ISoundDataOpen() = default;
-
-	/**
-	 * Iff the data is streaming, there is more than one buffer.
-	 * @return Whether it's streaming data.
-	 */
-	virtual bool isStreaming() const noexcept = 0;
-
-	/**
-	 * Load a buffer containing data starting at the given offset. Or just get it
-	 * if it was already loaded.
-	 *
-	 * This function returns multiple values:
-	 * * `buffer`: The OpenAL buffer.
-	 * * `buffer_end`: The offset (in the file) where `buffer` ends (exclusive).
-	 * * `offset_in_buffer`: Offset relative to `buffer`'s start where the requested
-	 *       `offset` is.
-	 *       `offset_in_buffer == 0` is guaranteed if some loaded buffer ends at
-	 *       `offset`.
-	 *
-	 * @param offset The start of the buffer.
-	 * @return `{buffer, buffer_end, offset_in_buffer}` or `{0, sound_data_end, 0}`
-	 *         if `offset` is invalid.
-	 */
-	virtual std::tuple<ALuint, ALuint, ALuint> getOrLoadBufferAt(ALuint offset) = 0;
-
-	static std::shared_ptr<ISoundDataOpen> fromOggFile(std::unique_ptr<RAIIOggFile> oggfile,
-		const std::string &filename_for_logging);
-};
-
-/**
- * Will be opened lazily when first used.
- */
-struct ISoundDataUnopen
-{
-	virtual ~ISoundDataUnopen() = default;
-
-	// Note: The ISoundDataUnopen is moved (see &&). It is not meant to be kept
-	// after opening.
-	virtual std::shared_ptr<ISoundDataOpen> open(const std::string &sound_name) && = 0;
-};
-
-/**
- * Sound file is in a memory buffer.
- */
-struct SoundDataUnopenBuffer final : ISoundDataUnopen
-{
-	std::string m_buffer;
-
-	explicit SoundDataUnopenBuffer(std::string &&buffer) : m_buffer(std::move(buffer)) {}
-
-	std::shared_ptr<ISoundDataOpen> open(const std::string &sound_name) && override;
-};
-
-/**
- * Sound file is in file system.
- */
-struct SoundDataUnopenFile final : ISoundDataUnopen
-{
-	std::string m_path;
-
-	explicit SoundDataUnopenFile(const std::string &path) : m_path(path) {}
-
-	std::shared_ptr<ISoundDataOpen> open(const std::string &sound_name) && override;
-};
-
-/**
- * Non-streaming opened sound data.
- * All data is completely loaded in one buffer.
- */
-struct SoundDataOpenBuffer final : ISoundDataOpen
-{
-	RAIIALSoundBuffer m_buffer;
-
-	SoundDataOpenBuffer(std::unique_ptr<RAIIOggFile> oggfile,
-			const OggFileDecodeInfo &decode_info);
-
-	bool isStreaming() const noexcept override { return false; }
-
-	std::tuple<ALuint, ALuint, ALuint> getOrLoadBufferAt(ALuint offset) override
-	{
-		if (offset >= m_decode_info.length_samples)
-			return {0, m_decode_info.length_samples, 0};
-		return {m_buffer.get(), m_decode_info.length_samples, offset};
-	}
-};
-
-/**
- * Streaming opened sound data.
- *
- * Uses a sorted list of contiguous sound data regions (`ContiguousBuffers`s) for
- * efficient seeking.
- */
-struct SoundDataOpenStream final : ISoundDataOpen
-{
-	/**
-	 * An OpenAL buffer that goes until `m_end` (exclusive).
-	 */
-	struct SoundBufferUntil final
-	{
-		ALuint m_end;
-		RAIIALSoundBuffer m_buffer;
-	};
-
-	/**
-	 * A sorted non-empty vector of contiguous buffers.
-	 * The start (inclusive) of each buffer is the end of its predecessor, or
-	 * `m_start` for the first buffer.
-	 */
-	struct ContiguousBuffers final
-	{
-		ALuint m_start;
-		std::vector<SoundBufferUntil> m_buffers;
-	};
-
-	std::unique_ptr<RAIIOggFile> m_oggfile;
-	// A sorted vector of non-overlapping, non-contiguous `ContiguousBuffers`s.
-	std::vector<ContiguousBuffers> m_bufferss;
-
-	SoundDataOpenStream(std::unique_ptr<RAIIOggFile> oggfile,
-			const OggFileDecodeInfo &decode_info);
-
-	bool isStreaming() const noexcept override { return true; }
-
-	std::tuple<ALuint, ALuint, ALuint> getOrLoadBufferAt(ALuint offset) override;
-
-private:
-	// offset must be before after_it's m_start and after (after_it-1)'s last m_end
-	// new buffer will be inserted into m_bufferss before after_it
-	// returns same as getOrLoadBufferAt
-	std::tuple<ALuint, ALuint, ALuint> loadBufferAt(ALuint offset,
-			std::vector<ContiguousBuffers>::iterator after_it);
-};
-
-
-/**
- * A sound that is currently played.
- * Can be streaming.
- * Can be fading.
- */
-class PlayingSound final
-{
-	struct FadeState {
-		f32 step;
-		f32 target_gain;
-	};
-
-	ALuint m_source_id;
-	std::shared_ptr<ISoundDataOpen> m_data;
-	ALuint m_next_sample_pos = 0;
-	bool m_looping;
-	bool m_is_positional;
-	bool m_stopped_means_dead = true;
-	std::optional<FadeState> m_fade_state = std::nullopt;
-
-public:
-	PlayingSound(ALuint source_id, std::shared_ptr<ISoundDataOpen> data, bool loop,
-			f32 volume, f32 pitch, f32 start_time,
-			const std::optional<std::pair<v3f, v3f>> &pos_vel_opt);
-
-	~PlayingSound() noexcept
-	{
-		alDeleteSources(1, &m_source_id);
-	}
-
-	DISABLE_CLASS_COPY(PlayingSound)
-
-	// return false means streaming finished
-	bool stepStream();
-
-	// retruns true if it wasn't fading already
-	bool fade(f32 step, f32 target_gain) noexcept;
-
-	// returns true if more fade is needed later
-	bool doFade(f32 dtime) noexcept;
-
-	void updatePosVel(const v3f &pos, const v3f &vel) noexcept;
-
-	void setGain(f32 gain) noexcept;
-
-	f32 getGain() noexcept;
-
-	void setPitch(f32 pitch) noexcept { alSourcef(m_source_id, AL_PITCH, pitch); }
-
-	bool isStreaming() const noexcept { return m_data->isStreaming(); }
-
-	void play() noexcept { alSourcePlay(m_source_id); }
-
-	// returns one of AL_INITIAL, AL_PLAYING, AL_PAUSED, AL_STOPPED
-	ALint getState() noexcept
-	{
-		ALint state;
-		alGetSourcei(m_source_id, AL_SOURCE_STATE, &state);
-		return state;
-	}
-
-	bool isDead() noexcept
-	{
-		// streaming sounds can (but should not) stop because the queue runs empty
-		return m_stopped_means_dead && getState() == AL_STOPPED;
-	}
-
-	void pause() noexcept
-	{
-		// this is a NOP if state != AL_PLAYING
-		alSourcePause(m_source_id);
-	}
-
-	void resume() noexcept
-	{
-		if (getState() == AL_PAUSED)
-			play();
-	}
-};
-
-
-/*
- * The SoundManager thread
- */
-
-namespace sound_manager_messages_to_mgr {
-	struct PauseAll {};
-	struct ResumeAll {};
-
-	struct UpdateListener { v3f pos_; v3f vel_; v3f at_; v3f up_; };
-	struct SetListenerGain { f32 gain; };
-
-	struct LoadSoundFile { std::string name; std::string filepath; };
-	struct LoadSoundData { std::string name; std::string filedata; };
-	struct AddSoundToGroup { std::string sound_name; std::string group_name; };
-
-	struct PlaySound { sound_handle_t id; SoundSpec spec; };
-	struct PlaySoundAt { sound_handle_t id; SoundSpec spec; v3f pos_; v3f vel_; };
-	struct StopSound { sound_handle_t sound; };
-	struct FadeSound { sound_handle_t soundid; f32 step; f32 target_gain; };
-	struct UpdateSoundPosVel { sound_handle_t sound; v3f pos_; v3f vel_; };
-
-	struct PleaseStop {};
-}
-
-using SoundManagerMsgToMgr = std::variant<
-		std::monostate,
-
-		sound_manager_messages_to_mgr::PauseAll,
-		sound_manager_messages_to_mgr::ResumeAll,
-
-		sound_manager_messages_to_mgr::UpdateListener,
-		sound_manager_messages_to_mgr::SetListenerGain,
-
-		sound_manager_messages_to_mgr::LoadSoundFile,
-		sound_manager_messages_to_mgr::LoadSoundData,
-		sound_manager_messages_to_mgr::AddSoundToGroup,
-
-		sound_manager_messages_to_mgr::PlaySound,
-		sound_manager_messages_to_mgr::PlaySoundAt,
-		sound_manager_messages_to_mgr::StopSound,
-		sound_manager_messages_to_mgr::FadeSound,
-		sound_manager_messages_to_mgr::UpdateSoundPosVel,
-
-		sound_manager_messages_to_mgr::PleaseStop
-	>;
-
-namespace sound_manager_messages_to_proxy {
-	struct ReportRemovedSound { sound_handle_t id; };
-
-	struct Stopped {};
-}
-
-using SoundManagerMsgToProxy = std::variant<
-		std::monostate,
-
-		sound_manager_messages_to_proxy::ReportRemovedSound,
-
-		sound_manager_messages_to_proxy::Stopped
-	>;
-
-// not an ISoundManager. doesn't allocate ids, and doesn't accept id 0
-class OpenALSoundManager final : public Thread
-{
-private:
-	std::unique_ptr<SoundFallbackPathProvider> m_fallback_path_provider;
-
-	ALCdevice *m_device;
-	ALCcontext *m_context;
-
-	// time in seconds until which removeDeadSounds will be called again
-	f32 m_time_until_dead_removal = REMOVE_DEAD_SOUNDS_INTERVAL;
-
-	// loaded sounds
-	std::unordered_map<std::string, std::unique_ptr<ISoundDataUnopen>> m_sound_datas_unopen;
-	std::unordered_map<std::string, std::shared_ptr<ISoundDataOpen>> m_sound_datas_open;
-	// sound groups
-	std::unordered_map<std::string, std::vector<std::string>> m_sound_groups;
-
-	// currently playing sounds
-	std::unordered_map<sound_handle_t, std::shared_ptr<PlayingSound>> m_sounds_playing;
-
-	// streamed sounds
-	std::vector<std::weak_ptr<PlayingSound>> m_sounds_streaming_current_bigstep;
-	std::vector<std::weak_ptr<PlayingSound>> m_sounds_streaming_next_bigstep;
-	// time left until current bigstep finishes
-	f32 m_stream_timer = STREAM_BIGSTEP_TIME;
-
-	std::vector<std::weak_ptr<PlayingSound>> m_sounds_fading;
-
-	// if true, all sounds will be directly paused after creation
-	bool m_is_paused = false;
-
-public:
-	// used for communication with ProxySoundManager
-	MutexedQueue<SoundManagerMsgToMgr> m_queue_to_mgr;
-	MutexedQueue<SoundManagerMsgToProxy> m_queue_to_proxy;
-
-private:
-	void stepStreams(f32 dtime);
-	void doFades(f32 dtime);
-
-	/**
-	 * Gives the open sound for a loaded sound.
-	 * Opens the sound if currently unopened.
-	 *
-	 * @param sound_name Name of the sound.
-	 * @return The open sound.
-	 */
-	std::shared_ptr<ISoundDataOpen> openSingleSound(const std::string &sound_name);
-
-	/**
-	 * Gets a random sound name from a group.
-	 *
-	 * @param group_name The name of the sound group.
-	 * @return The name of a sound in the group, or "" on failure. Getting the
-	 *         sound with `openSingleSound` directly afterwards will not fail.
-	 */
-	std::string getLoadedSoundNameFromGroup(const std::string &group_name);
-
-	/**
-	 * Same as `getLoadedSoundNameFromGroup`, but if sound does not exist, try to
-	 * load from local files.
-	 */
-	std::string getOrLoadLoadedSoundNameFromGroup(const std::string &group_name);
-
-	std::shared_ptr<PlayingSound> createPlayingSound(const std::string &sound_name,
-			bool loop, f32 volume, f32 pitch, f32 start_time,
-			const std::optional<std::pair<v3f, v3f>> &pos_vel_opt);
-
-	void playSoundGeneric(sound_handle_t id, const std::string &group_name, bool loop,
-			f32 volume, f32 fade, f32 pitch, bool use_local_fallback, f32 start_time,
-			const std::optional<std::pair<v3f, v3f>> &pos_vel_opt);
-
-	/**
-	 * Deletes sounds that are dead (=finished).
-	 *
-	 * @return Number of removed sounds.
-	 */
-	int removeDeadSounds();
-
-public:
-	OpenALSoundManager(SoundManagerSingleton *smg,
-			std::unique_ptr<SoundFallbackPathProvider> fallback_path_provider);
-
-	~OpenALSoundManager() override;
-
-	DISABLE_CLASS_COPY(OpenALSoundManager)
-
-private:
-	/* Similar to ISoundManager */
-
-	void step(f32 dtime);
-	void pauseAll();
-	void resumeAll();
-
-	void updateListener(const v3f &pos_, const v3f &vel_, const v3f &at_, const v3f &up_);
-	void setListenerGain(f32 gain);
-
-	bool loadSoundFile(const std::string &name, const std::string &filepath);
-	bool loadSoundData(const std::string &name, std::string &&filedata);
-	void loadSoundFileNoCheck(const std::string &name, const std::string &filepath);
-	void loadSoundDataNoCheck(const std::string &name, std::string &&filedata);
-	void addSoundToGroup(const std::string &sound_name, const std::string &group_name);
-
-	void playSound(sound_handle_t id, const SoundSpec &spec);
-	void playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos_,
-			const v3f &vel_);
-	void stopSound(sound_handle_t sound);
-	void fadeSound(sound_handle_t soundid, f32 step, f32 target_gain);
-	void updateSoundPosVel(sound_handle_t sound, const v3f &pos_, const v3f &vel_);
-
-protected:
-	/* Thread stuff */
-
-	void *run() override;
-
-private:
-	void send(SoundManagerMsgToProxy msg)
-	{
-		m_queue_to_proxy.push_back(std::move(msg));
-	}
-
-	void reportRemovedSound(sound_handle_t id)
-	{
-		send(sound_manager_messages_to_proxy::ReportRemovedSound{id});
-	}
-};
-
-
-/*
- * The public ISoundManager interface
- */
-
-class ProxySoundManager final : public ISoundManager
-{
-	OpenALSoundManager m_sound_manager;
-	// sound names from loadSoundData and loadSoundFile
-	std::unordered_set<std::string> m_known_sound_names;
-
-	void send(SoundManagerMsgToMgr msg)
-	{
-		m_sound_manager.m_queue_to_mgr.push_back(std::move(msg));
-	}
-
-	enum class MsgResult { Ok, Empty, Stopped};
-	MsgResult handleMsg(SoundManagerMsgToProxy &&msg);
-
-public:
-	ProxySoundManager(SoundManagerSingleton *smg,
-			std::unique_ptr<SoundFallbackPathProvider> fallback_path_provider) :
-		m_sound_manager(smg, std::move(fallback_path_provider))
-	{
-		m_sound_manager.start();
-	}
-
-	~ProxySoundManager() override;
-
-	/* Interface */
-
-	void step(f32 dtime) override;
-	void pauseAll() override;
-	void resumeAll() override;
-
-	void updateListener(const v3f &pos_, const v3f &vel_, const v3f &at_, const v3f &up_) override;
-	void setListenerGain(f32 gain) override;
-
-	bool loadSoundFile(const std::string &name, const std::string &filepath) override;
-	bool loadSoundData(const std::string &name, std::string &&filedata) override;
-	void addSoundToGroup(const std::string &sound_name, const std::string &group_name) override;
-
-	void playSound(sound_handle_t id, const SoundSpec &spec) override;
-	void playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos_,
-			const v3f &vel_) override;
-	void stopSound(sound_handle_t sound) override;
-	void fadeSound(sound_handle_t soundid, f32 step, f32 target_gain) override;
-	void updateSoundPosVel(sound_handle_t sound, const v3f &pos_, const v3f &vel_) override;
-};
diff --git a/src/client/sound/sound_singleton.cpp b/src/client/sound/sound_singleton.cpp
new file mode 100644
index 000000000..26264fe76
--- /dev/null
+++ b/src/client/sound/sound_singleton.cpp
@@ -0,0 +1,69 @@
+/*
+Minetest
+Copyright (C) 2022 DS
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+OpenAL support based on work by:
+Copyright (C) 2011 Sebastian 'Bahamada' Rühl
+Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com>
+Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; ifnot, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "sound_singleton.h"
+
+bool SoundManagerSingleton::init()
+{
+	if (!(m_device = unique_ptr_alcdevice(alcOpenDevice(nullptr)))) {
+		errorstream << "Audio: Global Initialization: Failed to open device" << std::endl;
+		return false;
+	}
+
+	if (!(m_context = unique_ptr_alccontext(alcCreateContext(m_device.get(), nullptr)))) {
+		errorstream << "Audio: Global Initialization: Failed to create context" << std::endl;
+		return false;
+	}
+
+	if (!alcMakeContextCurrent(m_context.get())) {
+		errorstream << "Audio: Global Initialization: Failed to make current context" << std::endl;
+		return false;
+	}
+
+	alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED);
+
+	// Speed of sound in nodes per second
+	// FIXME: This value assumes 1 node sidelength = 1 meter, and "normal" air.
+	//        Ideally this should be mod-controlled.
+	alSpeedOfSound(343.3f);
+
+	// doppler effect turned off for now, for best backwards compatibility
+	alDopplerFactor(0.0f);
+
+	if (alGetError() != AL_NO_ERROR) {
+		errorstream << "Audio: Global Initialization: OpenAL Error " << alGetError() << std::endl;
+		return false;
+	}
+
+	infostream << "Audio: Global Initialized: OpenAL " << alGetString(AL_VERSION)
+		<< ", using " << alcGetString(m_device.get(), ALC_DEVICE_SPECIFIER)
+		<< std::endl;
+
+	return true;
+}
+
+SoundManagerSingleton::~SoundManagerSingleton()
+{
+	infostream << "Audio: Global Deinitialized." << std::endl;
+}
diff --git a/src/client/sound/sound_singleton.h b/src/client/sound/sound_singleton.h
new file mode 100644
index 000000000..a3eb2dfa3
--- /dev/null
+++ b/src/client/sound/sound_singleton.h
@@ -0,0 +1,60 @@
+/*
+Minetest
+Copyright (C) 2022 DS
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+OpenAL support based on work by:
+Copyright (C) 2011 Sebastian 'Bahamada' Rühl
+Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com>
+Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; ifnot, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#pragma once
+
+#include "al_helpers.h"
+
+/**
+ * Class for the openal device and context
+ */
+class SoundManagerSingleton
+{
+public:
+	struct AlcDeviceDeleter {
+		void operator()(ALCdevice *p)
+		{
+			alcCloseDevice(p);
+		}
+	};
+
+	struct AlcContextDeleter {
+		void operator()(ALCcontext *p)
+		{
+			alcMakeContextCurrent(nullptr);
+			alcDestroyContext(p);
+		}
+	};
+
+	using unique_ptr_alcdevice = std::unique_ptr<ALCdevice, AlcDeviceDeleter>;
+	using unique_ptr_alccontext = std::unique_ptr<ALCcontext, AlcContextDeleter>;
+
+	unique_ptr_alcdevice  m_device;
+	unique_ptr_alccontext m_context;
+
+public:
+	bool init();
+
+	~SoundManagerSingleton();
+};