Browse Source

Sound refactor and improvements (#12764)

DS 10 months ago
parent
commit
edcbfa31c9
52 changed files with 2790 additions and 1199 deletions
  1. 2 0
      LICENSE.txt
  2. 11 0
      builtin/client/misc.lua
  3. 2 0
      builtin/mainmenu/init.lua
  4. 6 0
      builtin/mainmenu/misc.lua
  5. 2 2
      doc/client_lua_api.md
  6. 115 50
      doc/lua_api.md
  7. 1 1
      doc/menu_lua_api.md
  8. 1 0
      games/devtest/mods/soundstuff/init.lua
  9. 23 15
      games/devtest/mods/soundstuff/jukebox.lua
  10. 31 0
      games/devtest/mods/soundstuff/racecar.lua
  11. BIN
      games/devtest/mods/soundstuff/sounds/soundstuff_sinus.ogg
  12. BIN
      games/devtest/mods/soundstuff/textures/soundstuff_racecar.png
  13. 3 2
      src/client/CMakeLists.txt
  14. 0 1
      src/client/camera.cpp
  15. 27 15
      src/client/client.cpp
  16. 6 4
      src/client/client.h
  17. 2 1
      src/client/clientobject.h
  18. 3 2
      src/client/content_cao.cpp
  19. 3 1
      src/client/content_cao.h
  20. 34 71
      src/client/game.cpp
  21. 97 0
      src/client/sound.cpp
  22. 149 53
      src/client/sound.h
  23. 4 696
      src/client/sound_openal.cpp
  24. 5 4
      src/client/sound_openal.h
  25. 1125 0
      src/client/sound_openal_internal.cpp
  26. 613 0
      src/client/sound_openal_internal.h
  27. 13 37
      src/gui/guiEngine.cpp
  28. 7 26
      src/gui/guiEngine.h
  29. 4 4
      src/gui/guiFormSpecMenu.cpp
  30. 12 12
      src/itemdef.cpp
  31. 3 3
      src/itemdef.h
  32. 35 18
      src/network/clientpackethandler.cpp
  33. 11 5
      src/network/networkprotocol.h
  34. 9 9
      src/nodedef.cpp
  35. 4 4
      src/nodedef.h
  36. 3 1
      src/player.h
  37. 16 15
      src/script/common/c_content.cpp
  38. 5 5
      src/script/common/c_content.h
  39. 2 1
      src/script/lua_api/CMakeLists.txt
  40. 0 60
      src/script/lua_api/l_client.cpp
  41. 0 9
      src/script/lua_api/l_client.h
  42. 150 0
      src/script/lua_api/l_client_sound.cpp
  43. 66 0
      src/script/lua_api/l_client_sound.h
  44. 116 0
      src/script/lua_api/l_mainmenu_sound.cpp
  45. 32 2
      src/script/lua_api/l_mainmenu_sound.h
  46. 1 1
      src/script/lua_api/l_server.cpp
  47. 0 53
      src/script/lua_api/l_sound.cpp
  48. 3 1
      src/script/scripting_client.cpp
  49. 3 2
      src/script/scripting_mainmenu.cpp
  50. 1 1
      src/server.cpp
  51. 3 3
      src/server.h
  52. 26 9
      src/sound.h

+ 2 - 0
LICENSE.txt

@@ -83,6 +83,8 @@ SmallJoker:
 DS:
   games/devtest/mods/soundstuff/textures/soundstuff_bigfoot.png
   games/devtest/mods/soundstuff/textures/soundstuff_jukebox.png
+  games/devtest/mods/soundstuff/textures/soundstuff_racecar.png
+  games/devtest/mods/soundstuff/sounds/soundstuff_sinus.ogg
   games/devtest/mods/testtools/textures/testtools_branding_iron.png
 
 License of Minetest source code

+ 11 - 0
builtin/client/misc.lua

@@ -5,3 +5,14 @@ function core.setting_get_pos(name)
 	end
 	return core.string_to_pos(value)
 end
+
+
+-- old non-method sound functions
+
+function core.sound_stop(handle, ...)
+	return handle:stop(...)
+end
+
+function core.sound_fade(handle, ...)
+	return handle:fade(...)
+end

+ 2 - 0
builtin/mainmenu/init.lua

@@ -28,6 +28,8 @@ local basepath = core.get_builtin_path()
 defaulttexturedir = core.get_texturepath_share() .. DIR_DELIM .. "base" ..
 					DIR_DELIM .. "pack" .. DIR_DELIM
 
+dofile(menupath .. DIR_DELIM .. "misc.lua")
+
 dofile(basepath .. "common" .. DIR_DELIM .. "filterlist.lua")
 dofile(basepath .. "fstk" .. DIR_DELIM .. "buttonbar.lua")
 dofile(basepath .. "fstk" .. DIR_DELIM .. "dialog.lua")

+ 6 - 0
builtin/mainmenu/misc.lua

@@ -0,0 +1,6 @@
+
+-- old non-method sound function
+
+function core.sound_stop(handle, ...)
+	return handle:stop(...)
+end

+ 2 - 2
doc/client_lua_api.md

@@ -754,9 +754,9 @@ Call these functions only at load time!
 * `minetest.sound_play(spec, parameters)`: returns a handle
     * `spec` is a `SimpleSoundSpec`
     * `parameters` is a sound parameter table
-* `minetest.sound_stop(handle)`
+* `handle:stop()` or `minetest.sound_stop(handle)`
     * `handle` is a handle returned by `minetest.sound_play`
-* `minetest.sound_fade(handle, step, gain)`
+* `handle:fade(step, gain)` or `minetest.sound_fade(handle, step, gain)`
     * `handle` is a handle returned by `minetest.sound_play`
     * `step` determines how fast a sound will fade.
       Negative step will lower the sound volume, positive step will increase

+ 115 - 50
doc/lua_api.md

@@ -994,16 +994,21 @@ Only Ogg Vorbis files are supported.
 For positional playing of sounds, only single-channel (mono) files are
 supported. Otherwise OpenAL will play them non-positionally.
 
-Mods should generally prefix their sounds with `modname_`, e.g. given
+Mods should generally prefix their sound files with `modname_`, e.g. given
 the mod name "`foomod`", a sound could be called:
 
     foomod_foosound.ogg
 
-Sounds are referred to by their name with a dot, a single digit and the
-file extension stripped out. When a sound is played, the actual sound file
-is chosen randomly from the matching sounds.
+Sound group
+-----------
+
+A sound group is the set of all sound files, whose filenames are of the following
+format:
+`<sound-group name>[.<single digit>].ogg`
+When a sound-group is played, one the files in the group is chosen at random.
+Sound files can only be referred to by their sound-group name.
 
-When playing the sound `foomod_foosound`, the sound is chosen randomly
+Example: When playing the sound `foomod_foosound`, the sound is chosen randomly
 from the available ones of the following files:
 
 * `foomod_foosound.ogg`
@@ -1012,20 +1017,111 @@ from the available ones of the following files:
 * (...)
 * `foomod_foosound.9.ogg`
 
-Examples of sound parameter tables:
+`SimpleSoundSpec`
+-----------------
+
+Specifies a sound name, gain (=volume), pitch and fade.
+This is either a string or a table.
+
+In string form, you just specify the sound name or
+the empty string for no sound.
+
+Table form has the following fields:
+
+* `name`:
+  Sound-group name.
+  If == `""`, no sound is played.
+* `gain`:
+  Volume (`1.0` = 100%), must be non-negative.
+  At the end, OpenAL clamps sound gain to a maximum of `1.0`. By setting gain for
+  a positional sound higher than `1.0`, one can increase the radius inside which
+  maximal gain is reached.
+  Furthermore, gain of positional sounds doesn't increase inside a 1 node radius.
+  The gain given here describes the gain at a distance of 3 nodes.
+* `pitch`:
+  Applies a pitch-shift to the sound.
+  Each factor of `2.0` results in a pitch-shift of +12 semitones.
+  Must be positive.
+* `fade`:
+  If > `0.0`, the sound is faded in, with this value in gain per second, until
+  `gain` is reached.
+
+`gain`, `pitch` and `fade` are optional and default to `1.0`, `1.0` and `0.0`.
+
+Examples:
+
+* `""`: No sound
+* `{}`: No sound
+* `"default_place_node"`: Play e.g. `default_place_node.ogg`
+* `{name = "default_place_node"}`: Same as above
+* `{name = "default_place_node", gain = 0.5}`: 50% volume
+* `{name = "default_place_node", gain = 0.9, pitch = 1.1}`: 90% volume, 110% pitch
+
+Sound parameter table
+---------------------
+
+Table used to specify how a sound is played:
+
+```lua
+{
+    gain = 1.0,
+    -- Scales the gain specified in `SimpleSoundSpec`.
+
+    pitch = 1.0,
+    -- Overwrites the pitch specified in `SimpleSoundSpec`.
+
+    fade = 0.0,
+    -- Overwrites the fade specified in `SimpleSoundSpec`.
+
+    start_time = 0.0,
+    -- Start with a time-offset into the sound.
+    -- The behavior is as if the sound was already playing for this many seconds.
+    -- Negative values are relative to the sound's length, so the sound reaches
+    -- its end in `-start_time` seconds.
+    -- It is unspecified what happens if `loop` is false and `start_time` is
+    -- smaller than minus the sound's length.
+
+    loop = false,
+    -- If true, sound is played in a loop.
+
+    pos = {x = 1, y = 2, z = 3},
+    -- Play sound at a position.
+    -- Can't be used together with `object`.
+
+    object = <an ObjectRef>,
+    -- Attach the sound to an object.
+    -- Can't be used together with `pos`.
+
+    to_player = name,
+    -- Only play for this player.
+    -- Can't be used together with `exclude_player`.
+
+    exclude_player = name,
+    -- Don't play sound for this player.
+    -- Can't be used together with `to_player`.
+
+    max_hear_distance = 32,
+    -- Only play for players that are at most this far away when the sound
+    -- starts playing.
+    -- Needs `pos` or `object` to be set.
+    -- `32` is the default.
+}
+```
+
+Examples:
 
 ```lua
 -- Play locationless on all clients
 {
     gain = 1.0,   -- default
-    fade = 0.0,   -- default, change to a value > 0 to fade the sound in
+    fade = 0.0,   -- default
     pitch = 1.0,  -- default
 }
 -- Play locationless to one player
 {
     to_player = name,
     gain = 1.0,   -- default
-    fade = 0.0,   -- default, change to a value > 0 to fade the sound in
+    fade = 0.0,   -- default
     pitch = 1.0,  -- default
 }
 -- Play locationless to one player, looped
@@ -1034,17 +1130,18 @@ Examples of sound parameter tables:
     gain = 1.0,  -- default
     loop = true,
 }
--- Play at a location
+-- Play at a location, start the sound at offset 5 seconds
 {
     pos = {x = 1, y = 2, z = 3},
     gain = 1.0,  -- default
-    max_hear_distance = 32,  -- default, uses a Euclidean metric
+    max_hear_distance = 32,  -- default
+    start_time = 5.0,
 }
 -- Play connected to an object, looped
 {
     object = <an ObjectRef>,
     gain = 1.0,  -- default
-    max_hear_distance = 32,  -- default, uses a Euclidean metric
+    max_hear_distance = 32,  -- default
     loop = true,
 }
 -- Play at a location, heard by anyone *but* the given player
@@ -1055,45 +1152,10 @@ Examples of sound parameter tables:
 }
 ```
 
-Looped sounds must either be connected to an object or played locationless to
-one player using `to_player = name`.
-
-A positional sound will only be heard by players that are within
-`max_hear_distance` of the sound position, at the start of the sound.
-
-`exclude_player = name` can be applied to locationless, positional and object-
-bound sounds to exclude a single player from hearing them.
-
-`SimpleSoundSpec`
------------------
-
-Specifies a sound name, gain (=volume) and pitch.
-This is either a string or a table.
-
-In string form, you just specify the sound name or
-the empty string for no sound.
-
-Table form has the following fields:
-
-* `name`: Sound name
-* `gain`: Volume (`1.0` = 100%)
-* `pitch`: Pitch (`1.0` = 100%)
-
-`gain` and `pitch` are optional and default to `1.0`.
-
-Examples:
-
-* `""`: No sound
-* `{}`: No sound
-* `"default_place_node"`: Play e.g. `default_place_node.ogg`
-* `{name = "default_place_node"}`: Same as above
-* `{name = "default_place_node", gain = 0.5}`: 50% volume
-* `{name = "default_place_node", gain = 0.9, pitch = 1.1}`: 90% volume, 110% pitch
-
-Special sound files
--------------------
+Special sound-groups
+--------------------
 
-These sound files are played back by the engine if provided.
+These sound-groups are played back by the engine if provided.
 
  * `player_damage`: Played when the local player takes damage (gain = 0.5)
  * `player_falling_damage`: Played when the local player takes
@@ -8804,7 +8866,10 @@ Used by `minetest.register_node`.
 
         footstep = <SimpleSoundSpec>,
         -- If walkable, played when object walks on it. If node is
-        -- climbable or a liquid, played when object moves through it
+        -- climbable or a liquid, played when object moves through it.
+        -- Sound is played at the base of the object's collision-box.
+        -- Gain is multiplied by `0.6`.
+        -- For local player, it's played position-less, with normal gain.
 
         dig = <SimpleSoundSpec> or "__group",
         -- While digging node.

+ 1 - 1
doc/menu_lua_api.md

@@ -81,7 +81,7 @@ Filesystem
 * `core.sound_play(spec, looped)` -> handle
   * `spec` = `SimpleSoundSpec` (see `lua_api.md`)
   * `looped` = bool
-* `core.sound_stop(handle)`
+* `handle:stop()` or `core.sound_stop(handle)`
 * `core.get_video_drivers()`
   * get list of video drivers supported by engine (not all modes are guaranteed to work)
   * returns list of available video drivers' settings name and 'friendly' display name

+ 1 - 0
games/devtest/mods/soundstuff/init.lua

@@ -3,3 +3,4 @@ local path = minetest.get_modpath("soundstuff") .. "/"
 dofile(path .. "sound_event_items.lua")
 dofile(path .. "jukebox.lua")
 dofile(path .. "bigfoot.lua")
+dofile(path .. "racecar.lua")

+ 23 - 15
games/devtest/mods/soundstuff/jukebox.lua

@@ -14,6 +14,7 @@ local meta_keys = {
 	"sparam.gain",
 	"sparam.pitch",
 	"sparam.fade",
+	"sparam.start_time",
 	"sparam.loop",
 	"sparam.pos",
 	"sparam.object",
@@ -39,6 +40,7 @@ local function get_all_metadata(meta)
 			gain              = meta:get_string("sparam.gain"),
 			pitch             = meta:get_string("sparam.pitch"),
 			fade              = meta:get_string("sparam.fade"),
+			start_time       = meta:get_string("sparam.start_time"),
 			loop              = meta:get_string("sparam.loop"),
 			pos               = meta:get_string("sparam.pos"),
 			object            = meta:get_string("sparam.object"),
@@ -86,7 +88,7 @@ local function show_formspec(pos, player)
 
 	fs_add([[
 		formspec_version[6]
-		size[14,11]
+		size[14,12]
 	]])
 
 	-- SimpleSoundSpec
@@ -110,23 +112,25 @@ local function show_formspec(pos, player)
 	-- sound parameter table
 	fs_add(string.format([[
 		container[5.5,0.5]
-		box[-0.1,-0.1;4.2,10.2;#EBEBEB20]
+		box[-0.1,-0.1;4.2,10.7;#EBEBEB20]
 		style[*;font=mono,bold]
 		label[0,0.25;sound parameter table]
 		style[*;font=mono]
 		field[0.00,1;1,0.75;sparam.gain;gain;%s]
 		field[1.25,1;1,0.75;sparam.pitch;pitch;%s]
 		field[2.50,1;1,0.75;sparam.fade;fade;%s]
-		field[0,2.25;4,0.75;sparam.loop;loop;%s]
-		field[0,3.50;4,0.75;sparam.pos;pos;%s]
-		field[0,4.75;4,0.75;sparam.object;object;%s]
-		field[0,6.00;4,0.75;sparam.to_player;to_player;%s]
-		field[0,7.25;4,0.75;sparam.exclude_player;exclude_player;%s]
-		field[0,8.50;4,0.75;sparam.max_hear_distance;max_hear_distance;%s]
+		field[0,2.25;4,0.75;sparam.start_time;start_time;%s]
+		field[0,3.50;4,0.75;sparam.loop;loop;%s]
+		field[0,4.75;4,0.75;sparam.pos;pos;%s]
+		field[0,6.00;4,0.75;sparam.object;object;%s]
+		field[0,7.25;4,0.75;sparam.to_player;to_player;%s]
+		field[0,8.50;4,0.75;sparam.exclude_player;exclude_player;%s]
+		field[0,9.75;4,0.75;sparam.max_hear_distance;max_hear_distance;%s]
 		container_end[]
 		field_close_on_enter[sparam.gain;false]
 		field_close_on_enter[sparam.pitch;false]
 		field_close_on_enter[sparam.fade;false]
+		field_close_on_enter[sparam.start_time;false]
 		field_close_on_enter[sparam.loop;false]
 		field_close_on_enter[sparam.pos;false]
 		field_close_on_enter[sparam.object;false]
@@ -134,9 +138,10 @@ local function show_formspec(pos, player)
 		field_close_on_enter[sparam.exclude_player;false]
 		field_close_on_enter[sparam.max_hear_distance;false]
 		tooltip[sparam.object;Get a name with the Branding Iron.]
-	]], F(md.sparam.gain), F(md.sparam.pitch), F(md.sparam.fade), F(md.sparam.loop),
-			F(md.sparam.pos), F(md.sparam.object), F(md.sparam.to_player),
-			F(md.sparam.exclude_player), F(md.sparam.max_hear_distance)))
+	]], F(md.sparam.gain), F(md.sparam.pitch), F(md.sparam.fade),
+			F(md.sparam.start_time), F(md.sparam.loop), F(md.sparam.pos),
+			F(md.sparam.object), F(md.sparam.to_player), F(md.sparam.exclude_player),
+			F(md.sparam.max_hear_distance)))
 
 	-- fade
 	fs_add(string.format([[
@@ -187,7 +192,7 @@ local function show_formspec(pos, player)
 
 	-- save and quit button
 	fs_add([[
-		button_exit[10.75,10;3,0.75;btn_save_quit;Save & Quit]
+		button_exit[10.75,11;3,0.75;btn_save_quit;Save & Quit]
 	]])
 
 	minetest.show_formspec(player:get_player_name(), "soundstuff:jukebox@"..pos:to_string(),
@@ -210,6 +215,7 @@ minetest.register_node("soundstuff:jukebox", {
 		meta:set_string("sparam.gain", "")
 		meta:set_string("sparam.pitch", "")
 		meta:set_string("sparam.fade", "")
+		meta:set_string("sparam.start_time", "")
 		meta:set_string("sparam.loop", "")
 		meta:set_string("sparam.pos", pos:to_string())
 		meta:set_string("sparam.object", "")
@@ -267,6 +273,7 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
 				gain  = tonumber(md.sparam.gain),
 				pitch = tonumber(md.sparam.pitch),
 				fade  = tonumber(md.sparam.fade),
+				start_time = tonumber(md.sparam.start_time),
 				loop = minetest.is_yes(md.sparam.loop),
 				pos = vector.from_string(md.sparam.pos),
 				object = testtools.get_branded_object(md.sparam.object),
@@ -280,10 +287,11 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
 					"[soundstuff:jukebox] Playing sound: minetest.sound_play(%s, %s, %s)",
 					string.format("{name=\"%s\", gain=%s, pitch=%s, fade=%s}",
 							sss.name, sss.gain, sss.pitch, sss.fade),
-					string.format("{gain=%s, pitch=%s, fade=%s, loop=%s, pos=%s, "
+					string.format("{gain=%s, pitch=%s, fade=%s, start_time=%s, loop=%s, pos=%s, "
 						.."object=%s, to_player=\"%s\", exclude_player=\"%s\", max_hear_distance=%s}",
-							sparam.gain, sparam.pitch, sparam.fade, sparam.loop, sparam.pos,
-							sparam.object and "<objref>", sparam.to_player, sparam.exclude_player,
+							sparam.gain, sparam.pitch, sparam.fade, sparam.start_time,
+							sparam.loop, sparam.pos, sparam.object and "<objref>",
+							sparam.to_player, sparam.exclude_player,
 							sparam.max_hear_distance),
 					tostring(ephemeral)))
 

+ 31 - 0
games/devtest/mods/soundstuff/racecar.lua

@@ -0,0 +1,31 @@
+
+local drive_speed = 20
+local drive_distance = 30
+
+minetest.register_entity("soundstuff:racecar", {
+	initial_properties = {
+        physical = false,
+        collisionbox = {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5},
+        selectionbox = {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5},
+        visual = "upright_sprite",
+        visual_size = {x = 1, y = 1, z = 1},
+        textures = {"soundstuff_racecar.png", "soundstuff_racecar.png^[transformFX"},
+        static_save = false,
+    },
+
+	on_activate = function(self, _staticdata, _dtime_s)
+		self.min_x = self.object:get_pos().x - drive_distance * 0.5
+		self.max_x = self.min_x + drive_distance
+		self.vel = vector.new(drive_speed, 0, 0)
+	end,
+
+	on_step = function(self, _dtime, _moveresult)
+		local pos = self.object:get_pos()
+		if pos.x < self.min_x then
+			self.vel = vector.new(drive_speed, 0, 0)
+		elseif pos.x > self.max_x then
+			self.vel = vector.new(-drive_speed, 0, 0)
+		end
+		self.object:set_velocity(self.vel)
+	end,
+})

BIN
games/devtest/mods/soundstuff/sounds/soundstuff_sinus.ogg


BIN
games/devtest/mods/soundstuff/textures/soundstuff_racecar.png


+ 3 - 2
src/client/CMakeLists.txt

@@ -1,8 +1,9 @@
-set(sound_SRCS "")
+set(sound_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/sound.cpp)
 
 if(USE_SOUND)
 	set(sound_SRCS ${sound_SRCS}
-		${CMAKE_CURRENT_SOURCE_DIR}/sound_openal.cpp)
+		${CMAKE_CURRENT_SOURCE_DIR}/sound_openal.cpp
+		${CMAKE_CURRENT_SOURCE_DIR}/sound_openal_internal.cpp)
 	set(SOUND_INCLUDE_DIRS
 		${OPENAL_INCLUDE_DIR}
 		${VORBIS_INCLUDE_DIR}

+ 0 - 1
src/client/camera.cpp

@@ -30,7 +30,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "settings.h"
 #include "wieldmesh.h"
 #include "noise.h"         // easeCurve
-#include "sound.h"
 #include "mtevent.h"
 #include "nodedef.h"
 #include "util/numeric.h"

+ 27 - 15
src/client/client.cpp

@@ -374,6 +374,11 @@ Client::~Client()
 	if (m_mod_storage_database)
 		m_mod_storage_database->endSave();
 	delete m_mod_storage_database;
+
+	// Free sound ids
+	for (auto &csp : m_sounds_client_to_server)
+		m_sound->freeId(csp.first);
+	m_sounds_client_to_server.clear();
 }
 
 void Client::connect(Address address, bool is_local_server)
@@ -703,12 +708,13 @@ void Client::step(float dtime)
 	*/
 	{
 		for (auto &m_sounds_to_object : m_sounds_to_objects) {
-			int client_id = m_sounds_to_object.first;
+			sound_handle_t client_id = m_sounds_to_object.first;
 			u16 object_id = m_sounds_to_object.second;
 			ClientActiveObject *cao = m_env.getActiveObject(object_id);
 			if (!cao)
 				continue;
-			m_sound->updateSoundPosition(client_id, cao->getPosition());
+			m_sound->updateSoundPosVel(client_id, cao->getPosition() * (1.0f/BS),
+					cao->getVelocity() * (1.0f/BS));
 		}
 	}
 
@@ -719,22 +725,24 @@ void Client::step(float dtime)
 	if(m_removed_sounds_check_timer >= 2.32) {
 		m_removed_sounds_check_timer = 0;
 		// Find removed sounds and clear references to them
+		std::vector<sound_handle_t> removed_client_ids = m_sound->pollRemovedSounds();
 		std::vector<s32> removed_server_ids;
-		for (std::unordered_map<s32, int>::iterator i = m_sounds_server_to_client.begin();
-				i != m_sounds_server_to_client.end();) {
-			s32 server_id = i->first;
-			int client_id = i->second;
-			++i;
-			if(!m_sound->soundExists(client_id)) {
+		for (sound_handle_t client_id : removed_client_ids) {
+			auto client_to_server_id_it = m_sounds_client_to_server.find(client_id);
+			if (client_to_server_id_it == m_sounds_client_to_server.end())
+				continue;
+			s32 server_id = client_to_server_id_it->second;
+			m_sound->freeId(client_id);
+			m_sounds_client_to_server.erase(client_to_server_id_it);
+			if (server_id != -1) {
 				m_sounds_server_to_client.erase(server_id);
-				m_sounds_client_to_server.erase(client_id);
-				m_sounds_to_objects.erase(client_id);
 				removed_server_ids.push_back(server_id);
 			}
+			m_sounds_to_objects.erase(client_id);
 		}
 
 		// Sync to server
-		if(!removed_server_ids.empty()) {
+		if (!removed_server_ids.empty()) {
 			sendRemovedSounds(removed_server_ids);
 		}
 	}
@@ -800,9 +808,13 @@ bool Client::loadMedia(const std::string &data, const std::string &filename,
 	};
 	name = removeStringEnd(filename, sound_ext);
 	if (!name.empty()) {
-		TRACESTREAM(<< "Client: Attempting to load sound "
-			<< "file \"" << filename << "\"" << std::endl);
-		return m_sound->loadSoundData(name, data);
+		TRACESTREAM(<< "Client: Attempting to load sound file \""
+				<< filename << "\"" << std::endl);
+		if (!m_sound->loadSoundData(filename, std::string(data)))
+			return false;
+		// "name[.num].ogg" is in group "name"
+		m_sound->addSoundToGroup(filename, name);
+		return true;
 	}
 
 	const char *model_ext[] = {
@@ -1205,7 +1217,7 @@ void Client::sendGotBlocks(const std::vector<v3s16> &blocks)
 	Send(&pkt);
 }
 
-void Client::sendRemovedSounds(std::vector<s32> &soundList)
+void Client::sendRemovedSounds(const std::vector<s32> &soundList)
 {
 	size_t server_ids = soundList.size();
 	assert(server_ids <= 0xFFFF);

+ 6 - 4
src/client/client.h

@@ -70,6 +70,7 @@ class NetworkPacket;
 namespace con {
 class Connection;
 }
+using sound_handle_t = int;
 
 enum LocalClientState {
 	LC_Created,
@@ -468,7 +469,7 @@ private:
 	void startAuth(AuthMechanism chosen_auth_mechanism);
 	void sendDeletedBlocks(std::vector<v3s16> &blocks);
 	void sendGotBlocks(const std::vector<v3s16> &blocks);
-	void sendRemovedSounds(std::vector<s32> &soundList);
+	void sendRemovedSounds(const std::vector<s32> &soundList);
 
 	bool canSendChatMessage() const;
 
@@ -564,11 +565,12 @@ private:
 	// Sounds
 	float m_removed_sounds_check_timer = 0.0f;
 	// Mapping from server sound ids to our sound ids
-	std::unordered_map<s32, int> m_sounds_server_to_client;
+	std::unordered_map<s32, sound_handle_t> m_sounds_server_to_client;
 	// And the other way!
-	std::unordered_map<int, s32> m_sounds_client_to_server;
+	// This takes ownership for the sound handles.
+	std::unordered_map<sound_handle_t, s32> m_sounds_client_to_server;
 	// Relation of client id to object id
-	std::unordered_map<int, u16> m_sounds_to_objects;
+	std::unordered_map<sound_handle_t, u16> m_sounds_to_objects;
 
 	// Privileges
 	std::unordered_set<std::string> m_privileges;

+ 2 - 1
src/client/clientobject.h

@@ -47,7 +47,8 @@ public:
 	virtual bool getCollisionBox(aabb3f *toset) const { return false; }
 	virtual bool getSelectionBox(aabb3f *toset) const { return false; }
 	virtual bool collideWithObjects() const { return false; }
-	virtual const v3f getPosition() const { return v3f(0.0f); }
+	virtual const v3f getPosition() const { return v3f(0.0f); } // in BS-space
+	virtual const v3f getVelocity() const { return v3f(0.0f); } // in BS-space
 	virtual scene::ISceneNode *getSceneNode() const
 	{ return NULL; }
 	virtual scene::IAnimatedMeshSceneNode *getAnimatedMeshSceneNode() const

+ 3 - 2
src/client/content_cao.cpp

@@ -1179,11 +1179,12 @@ void GenericCAO::step(float dtime, ClientEnvironment *env)
 				v3s16 node_below_pos = floatToInt(foot_pos + v3f(0.0f, -0.5f, 0.0f),
 						1.0f);
 				MapNode n = m_env->getMap().getNode(node_below_pos);
-				SimpleSoundSpec spec = ndef->get(n).sound_footstep;
+				SoundSpec spec = ndef->get(n).sound_footstep;
 				// Reduce footstep gain, as non-local-player footsteps are
 				// somehow louder.
 				spec.gain *= 0.6f;
-				m_client->sound()->playSoundAt(spec, foot_pos * BS);
+				// The footstep-sound doesn't travel with the object. => vel=0
+				m_client->sound()->playSoundAt(0, spec, foot_pos, v3f(0.0f));
 			}
 		}
 	}

+ 3 - 1
src/client/content_cao.h

@@ -162,7 +162,9 @@ public:
 
 	virtual bool getSelectionBox(aabb3f *toset) const;
 
-	const v3f getPosition() const;
+	const v3f getPosition() const override final;
+
+	const v3f getVelocity() const override final { return m_velocity; }
 
 	inline const v3f &getRotation() const { return m_rotation; }
 

+ 34 - 71
src/client/game.cpp

@@ -260,31 +260,25 @@ class SoundMaker
 	const NodeDefManager *m_ndef;
 
 public:
-	bool makes_footstep_sound;
-	float m_player_step_timer;
-	float m_player_jump_timer;
+	bool makes_footstep_sound = true;
+	float m_player_step_timer = 0.0f;
+	float m_player_jump_timer = 0.0f;
 
-	SimpleSoundSpec m_player_step_sound;
-	SimpleSoundSpec m_player_leftpunch_sound;
+	SoundSpec m_player_step_sound;
+	SoundSpec m_player_leftpunch_sound;
 	// Second sound made on left punch, currently used for item 'use' sound
-	SimpleSoundSpec m_player_leftpunch_sound2;
-	SimpleSoundSpec m_player_rightpunch_sound;
-
-	SoundMaker(ISoundManager *sound, const NodeDefManager *ndef):
-		m_sound(sound),
-		m_ndef(ndef),
-		makes_footstep_sound(true),
-		m_player_step_timer(0.0f),
-		m_player_jump_timer(0.0f)
-	{
-	}
+	SoundSpec m_player_leftpunch_sound2;
+	SoundSpec m_player_rightpunch_sound;
+
+	SoundMaker(ISoundManager *sound, const NodeDefManager *ndef) :
+		m_sound(sound), m_ndef(ndef) {}
 
 	void playPlayerStep()
 	{
 		if (m_player_step_timer <= 0 && m_player_step_sound.exists()) {
 			m_player_step_timer = 0.03;
 			if (makes_footstep_sound)
-				m_sound->playSound(m_player_step_sound);
+				m_sound->playSound(0, m_player_step_sound);
 		}
 	}
 
@@ -292,7 +286,7 @@ public:
 	{
 		if (m_player_jump_timer <= 0.0f) {
 			m_player_jump_timer = 0.2f;
-			m_sound->playSound(SimpleSoundSpec("player_jump", 0.5f));
+			m_sound->playSound(0, SoundSpec("player_jump", 0.5f));
 		}
 	}
 
@@ -317,33 +311,33 @@ public:
 	static void cameraPunchLeft(MtEvent *e, void *data)
 	{
 		SoundMaker *sm = (SoundMaker *)data;
-		sm->m_sound->playSound(sm->m_player_leftpunch_sound);
-		sm->m_sound->playSound(sm->m_player_leftpunch_sound2);
+		sm->m_sound->playSound(0, sm->m_player_leftpunch_sound);
+		sm->m_sound->playSound(0, sm->m_player_leftpunch_sound2);
 	}
 
 	static void cameraPunchRight(MtEvent *e, void *data)
 	{
 		SoundMaker *sm = (SoundMaker *)data;
-		sm->m_sound->playSound(sm->m_player_rightpunch_sound);
+		sm->m_sound->playSound(0, sm->m_player_rightpunch_sound);
 	}
 
 	static void nodeDug(MtEvent *e, void *data)
 	{
 		SoundMaker *sm = (SoundMaker *)data;
 		NodeDugEvent *nde = (NodeDugEvent *)e;
-		sm->m_sound->playSound(sm->m_ndef->get(nde->n).sound_dug);
+		sm->m_sound->playSound(0, sm->m_ndef->get(nde->n).sound_dug);
 	}
 
 	static void playerDamage(MtEvent *e, void *data)
 	{
 		SoundMaker *sm = (SoundMaker *)data;
-		sm->m_sound->playSound(SimpleSoundSpec("player_damage", 0.5));
+		sm->m_sound->playSound(0, SoundSpec("player_damage", 0.5));
 	}
 
 	static void playerFallingDamage(MtEvent *e, void *data)
 	{
 		SoundMaker *sm = (SoundMaker *)data;
-		sm->m_sound->playSound(SimpleSoundSpec("player_falling_damage", 0.5));
+		sm->m_sound->playSound(0, SoundSpec("player_falling_damage", 0.5));
 	}
 
 	void registerReceiver(MtEventManager *mgr)
@@ -365,42 +359,6 @@ public:
 	}
 };
 
-// Locally stored sounds don't need to be preloaded because of this
-class GameOnDemandSoundFetcher: public OnDemandSoundFetcher
-{
-	std::set<std::string> m_fetched;
-private:
-	void paths_insert(std::set<std::string> &dst_paths,
-		const std::string &base,
-		const std::string &name)
-	{
-		dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".ogg");
-		dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".0.ogg");
-		dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".1.ogg");
-		dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".2.ogg");
-		dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".3.ogg");
-		dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".4.ogg");
-		dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".5.ogg");
-		dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".6.ogg");
-		dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".7.ogg");
-		dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".8.ogg");
-		dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".9.ogg");
-	}
-public:
-	void fetchSounds(const std::string &name,
-		std::set<std::string> &dst_paths,
-		std::set<std::string> &dst_datas)
-	{
-		if (m_fetched.count(name))
-			return;
-
-		m_fetched.insert(name);
-
-		paths_insert(dst_paths, porting::path_share, name);
-		paths_insert(dst_paths, porting::path_user,  name);
-	}
-};
-
 
 typedef s32 SamplerLayer_t;
 
@@ -936,7 +894,6 @@ private:
 	IWritableItemDefManager *itemdef_manager = nullptr;
 	NodeDefManager *nodedef_manager = nullptr;
 
-	GameOnDemandSoundFetcher soundfetcher; // useful when testing
 	std::unique_ptr<ISoundManager> sound_manager;
 	SoundMaker *soundmaker = nullptr;
 
@@ -1278,10 +1235,13 @@ void Game::run()
 			if (m_is_paused)
 				dtime = 0.0f;
 
-			if (!was_paused && m_is_paused)
+			if (!was_paused && m_is_paused) {
 				pauseAnimation();
-			else if (was_paused && !m_is_paused)
+				sound_manager->pauseAll();
+			} else if (was_paused && !m_is_paused) {
 				resumeAnimation();
+				sound_manager->resumeAll();
+			}
 		}
 
 		if (!m_is_paused)
@@ -1397,11 +1357,13 @@ bool Game::initSound()
 #if USE_SOUND
 	if (g_settings->getBool("enable_sound") && g_sound_manager_singleton.get()) {
 		infostream << "Attempting to use OpenAL audio" << std::endl;
-		sound_manager.reset(createOpenALSoundManager(g_sound_manager_singleton.get(), &soundfetcher));
+		sound_manager = createOpenALSoundManager(g_sound_manager_singleton.get(),
+				std::make_unique<SoundFallbackPathProvider>());
 		if (!sound_manager)
 			infostream << "Failed to initialize OpenAL audio" << std::endl;
-	} else
+	} else {
 		infostream << "Sound disabled." << std::endl;
+	}
 #endif
 
 	if (!sound_manager) {
@@ -3194,10 +3156,13 @@ void Game::updateCamera(f32 dtime)
 void Game::updateSound(f32 dtime)
 {
 	// Update sound listener
+	LocalPlayer *player = client->getEnv().getLocalPlayer();
+	ClientActiveObject *parent = player->getParent();
 	v3s16 camera_offset = camera->getOffset();
 	sound_manager->updateListener(
-			camera->getCameraNode()->getPosition() + intToFloat(camera_offset, BS),
-			v3f(0, 0, 0), // velocity
+			(1.0f/BS) * camera->getCameraNode()->getPosition()
+					+ intToFloat(camera_offset, 1.0f),
+			(1.0f/BS) * (parent ? parent->getVelocity() : player->getSpeed()),
 			camera->getDirection(),
 			camera->getCameraNode()->getUpVector());
 
@@ -3215,8 +3180,6 @@ void Game::updateSound(f32 dtime)
 		}
 	}
 
-	LocalPlayer *player = client->getEnv().getLocalPlayer();
-
 	// Tell the sound maker whether to make footstep sounds
 	soundmaker->makes_footstep_sound = player->makes_footstep_sound;
 
@@ -3332,7 +3295,7 @@ void Game::processPlayerInteraction(f32 dtime, bool show_hud)
 
 	runData.punching = false;
 
-	soundmaker->m_player_leftpunch_sound = SimpleSoundSpec();
+	soundmaker->m_player_leftpunch_sound = SoundSpec();
 	soundmaker->m_player_leftpunch_sound2 = pointed.type != POINTEDTHING_NOTHING ?
 		selected_def.sound_use : selected_def.sound_use_air;
 
@@ -3530,7 +3493,7 @@ void Game::handlePointingAtNode(const PointedThing &pointed,
 		// Placing animation (always shown for feedback)
 		camera->setDigging(1);
 
-		soundmaker->m_player_rightpunch_sound = SimpleSoundSpec();
+		soundmaker->m_player_rightpunch_sound = SoundSpec();
 
 		// If the wielded item has node placement prediction,
 		// make that happen

+ 97 - 0
src/client/sound.cpp

@@ -0,0 +1,97 @@
+/*
+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; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "sound.h"
+
+#include "filesys.h"
+#include "log.h"
+#include "porting.h"
+#include "util/numeric.h"
+#include <algorithm>
+#include <string>
+#include <vector>
+
+std::vector<std::string> SoundFallbackPathProvider::
+		getLocalFallbackPathsForSoundname(const std::string &name)
+{
+	std::vector<std::string> paths;
+
+	// only try each name once
+	if (m_done_names.count(name))
+		return paths;
+	m_done_names.insert(name);
+
+	addThePaths(name, paths);
+
+	// remove duplicates
+	std::sort(paths.begin(), paths.end());
+	auto end = std::unique(paths.begin(), paths.end());
+	paths.erase(end, paths.end());
+
+	return paths;
+}
+
+void SoundFallbackPathProvider::addAllAlternatives(const std::string &common,
+		std::vector<std::string> &paths)
+{
+	paths.reserve(paths.size() + 11);
+	for (auto &&ext : {".ogg", ".0.ogg", ".1.ogg", ".2.ogg", ".3.ogg", ".4.ogg",
+			".5.ogg", ".6.ogg", ".7.ogg", ".8.ogg", ".9.ogg", }) {
+		paths.push_back(common + ext);
+	}
+}
+
+void SoundFallbackPathProvider::addThePaths(const std::string &name,
+		std::vector<std::string> &paths)
+{
+	addAllAlternatives(porting::path_share + DIR_DELIM + "sounds" + DIR_DELIM + name, paths);
+	addAllAlternatives(porting::path_user + DIR_DELIM + "sounds" + DIR_DELIM + name, paths);
+}
+
+void ISoundManager::reportRemovedSound(sound_handle_t id)
+{
+	if (id <= 0)
+		return;
+
+	freeId(id);
+	m_removed_sounds.push_back(id);
+}
+
+sound_handle_t ISoundManager::allocateId(u32 num_owners)
+{
+	while (m_occupied_ids.find(m_next_id) != m_occupied_ids.end()
+			|| m_next_id == SOUND_HANDLE_T_MAX) {
+		m_next_id = static_cast<sound_handle_t>(
+				myrand() % static_cast<u32>(SOUND_HANDLE_T_MAX - 1) + 1);
+	}
+	sound_handle_t id = m_next_id++;
+	m_occupied_ids.emplace(id, num_owners);
+	return id;
+}
+
+void ISoundManager::freeId(sound_handle_t id, u32 num_owners)
+{
+	auto it = m_occupied_ids.find(id);
+	if (it == m_occupied_ids.end())
+		return;
+	if (it->second <= num_owners)
+		m_occupied_ids.erase(it);
+	else
+		it->second -= num_owners;
+}

+ 149 - 53
src/client/sound.h

@@ -19,72 +19,168 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 
 #pragma once
 
-#include <set>
-#include <string>
 #include "irr_v3d.h"
-#include "../sound.h"
+#include <limits>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
 
-class OnDemandSoundFetcher
+struct SoundSpec;
+
+class SoundFallbackPathProvider
 {
 public:
-	virtual void fetchSounds(const std::string &name,
-			std::set<std::string> &dst_paths,
-			std::set<std::string> &dst_datas) = 0;
+	virtual ~SoundFallbackPathProvider() = default;
+	std::vector<std::string> getLocalFallbackPathsForSoundname(const std::string &name);
+protected:
+	virtual void addThePaths(const std::string &name, std::vector<std::string> &paths);
+	// adds <common>.ogg, <common>.1.ogg, ..., <common>.9.ogg to paths
+	void addAllAlternatives(const std::string &common, std::vector<std::string> &paths);
+private:
+	std::unordered_set<std::string> m_done_names;
 };
 
+
+/**
+ * IDs for playing sounds.
+ * 0 is for sounds that are never modified after creation.
+ * Negative numbers are invalid.
+ * Positive numbers are allocated via allocateId and are manually reference-counted.
+ */
+using sound_handle_t = int;
+
+constexpr sound_handle_t SOUND_HANDLE_T_MAX = std::numeric_limits<sound_handle_t>::max();
+
 class ISoundManager
 {
+private:
+	std::unordered_map<sound_handle_t, u32> m_occupied_ids;
+	sound_handle_t m_next_id = 1;
+	std::vector<sound_handle_t> m_removed_sounds;
+
+protected:
+	void reportRemovedSound(sound_handle_t id);
+
 public:
 	virtual ~ISoundManager() = default;
 
-	// Multiple sounds can be loaded per name; when played, the sound
-	// should be chosen randomly from alternatives
-	// Return value determines success/failure
-	virtual bool loadSoundFile(
-			const std::string &name, const std::string &filepath) = 0;
-	virtual bool loadSoundData(
-			const std::string &name, const std::string &filedata) = 0;
-
-	virtual void updateListener(
-			const v3f &pos, const v3f &vel, const v3f &at, const v3f &up) = 0;
-	virtual void setListenerGain(float gain) = 0;
-
-	// playSound functions return -1 on failure, otherwise a handle to the
-	// sound. If name=="", call should be ignored without error.
-	virtual int playSound(const SimpleSoundSpec &spec) = 0;
-	virtual int playSoundAt(const SimpleSoundSpec &spec, const v3f &pos) = 0;
-	virtual void stopSound(int sound) = 0;
-	virtual bool soundExists(int sound) = 0;
-	virtual void updateSoundPosition(int sound, v3f pos) = 0;
-	virtual bool updateSoundGain(int id, float gain) = 0;
-	virtual float getSoundGain(int id) = 0;
-	virtual void step(float dtime) = 0;
-	virtual void fadeSound(int sound, float step, float gain) = 0;
+	/**
+	 * Removes finished sounds, steps streamed sounds, and does similar tasks.
+	 * Should not be called while paused.
+	 * @param dtime In seconds.
+	 */
+	virtual void step(f32 dtime) = 0;
+	/**
+	 * Pause all sound playback.
+	 */
+	virtual void pauseAll() = 0;
+	/**
+	 * Resume sound playback after pause.
+	 */
+	virtual void resumeAll() = 0;
+
+	/**
+	 * @param pos In node-space.
+	 * @param vel In node-space.
+	 * @param at Vector in node-space pointing forwards.
+	 * @param up Vector in node-space pointing upwards, orthogonal to `at`.
+	 */
+	virtual void updateListener(const v3f &pos, const v3f &vel, const v3f &at,
+			const v3f &up) = 0;
+	virtual void setListenerGain(f32 gain) = 0;
+
+	/**
+	 * Adds a sound to load from a file (only OggVorbis).
+	 * @param name The name of the sound. Must be unique, otherwise call fails.
+	 * @param filepath The path for
+	 * @return true on success, false on failure (ie. sound was already added or
+	 *         file does not exist).
+	 */
+	virtual bool loadSoundFile(const std::string &name, const std::string &filepath) = 0;
+	/**
+	 * Same as `loadSoundFile`, but reads the OggVorbis file from memory.
+	 */
+	virtual bool loadSoundData(const std::string &name, std::string &&filedata) = 0;
+	/**
+	 * Adds sound with name sound_name to group `group_name`. Creates the group
+	 * if non-existent.
+	 * @param sound_name The name of the sound, as used in `loadSoundData`.
+	 * @param group_name The name of the sound group.
+	 */
+	virtual void addSoundToGroup(const std::string &sound_name,
+			const std::string &group_name) = 0;
+
+	/**
+	 * Plays a random sound from a sound group (position-less).
+	 * @param id Id for new sound. Move semantics apply if id > 0.
+	 */
+	virtual void playSound(sound_handle_t id, const SoundSpec &spec) = 0;
+	/**
+	 * Same as `playSound`, but at a position.
+	 * @param pos In node-space.
+	 * @param vel In node-space.
+	 */
+	virtual void playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos,
+			const v3f &vel) = 0;
+	/**
+	 * Request the sound to be stopped.
+	 * The id should be freed afterwards.
+	 */
+	virtual void stopSound(sound_handle_t sound) = 0;
+	virtual void fadeSound(sound_handle_t sound, f32 step, f32 target_gain) = 0;
+	/**
+	 * Update position and velocity of positional sound.
+	 * @param pos In node-space.
+	 * @param vel In node-space.
+	 */
+	virtual void updateSoundPosVel(sound_handle_t sound, const v3f &pos,
+			const v3f &vel) = 0;
+
+	/**
+	 * Get and reset the list of sounds that were stopped.
+	 */
+	std::vector<sound_handle_t> pollRemovedSounds()
+	{
+		std::vector<sound_handle_t> ret;
+		std::swap(m_removed_sounds, ret);
+		return ret;
+	}
+
+	/**
+	 * Returns a positive id.
+	 * The id will be returned again until freeId is called.
+	 * @param num_owners Owner-counter for id. Set this to 2, if you want to play
+	 *                   a sound and store the id also otherwhere.
+	 */
+	sound_handle_t allocateId(u32 num_owners);
+
+	/**
+	 * Free an id allocated via allocateId.
+	 * @param num_owners How much the owner-counter should be decreased. Id can
+	 *                   be reused when counter reaches 0.
+	 */
+	void freeId(sound_handle_t id, u32 num_owners = 1);
 };
 
-class DummySoundManager : public ISoundManager
+class DummySoundManager final : public ISoundManager
 {
 public:
-	virtual bool loadSoundFile(const std::string &name, const std::string &filepath)
-	{
-		return true;
-	}
-	virtual bool loadSoundData(const std::string &name, const std::string &filedata)
-	{
-		return true;
-	}
-	void updateListener(const v3f &pos, const v3f &vel, const v3f &at, const v3f &up)
-	{
-	}
-	void setListenerGain(float gain) {}
-
-	int playSound(const SimpleSoundSpec &spec) { return -1; }
-	int playSoundAt(const SimpleSoundSpec &spec, const v3f &pos) { return -1; }
-	void stopSound(int sound) {}
-	bool soundExists(int sound) { return false; }
-	void updateSoundPosition(int sound, v3f pos) {}
-	bool updateSoundGain(int id, float gain) { return false; }
-	float getSoundGain(int id) { return 0; }
-	void step(float dtime) {}
-	void fadeSound(int sound, float step, float gain) {}
+	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 { return true; }
+	bool loadSoundData(const std::string &name, std::string &&filedata) override { return true; }
+	void addSoundToGroup(const std::string &sound_name, const std::string &group_name) override {};
+
+	void playSound(sound_handle_t id, const SoundSpec &spec) override { reportRemovedSound(id); }
+	void playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos,
+			const v3f &vel) override { reportRemovedSound(id); }
+	void stopSound(sound_handle_t sound) override {}
+	void fadeSound(sound_handle_t sound, f32 step, f32 target_gain) override {}
+	void updateSoundPosVel(sound_handle_t sound, const v3f &pos, const v3f &vel) override {}
 };

+ 4 - 696
src/client/sound_openal.cpp

@@ -22,703 +22,10 @@ with this program; ifnot, write to the Free Software Foundation, Inc.,
 */
 
 #include "sound_openal.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 <cmath>
-#include <vorbis/vorbisfile.h>
-#include <cassert>
-#include "log.h"
-#include "util/numeric.h" // myrand()
-#include "porting.h"
-#include <vector>
-#include <fstream>
-#include <unordered_map>
-#include <unordered_set>
-
-#define BUFFER_SIZE 30000
+#include "sound_openal_internal.h"
 
 std::shared_ptr<SoundManagerSingleton> g_sound_manager_singleton;
 
-typedef std::unique_ptr<ALCdevice, void (*)(ALCdevice *p)> unique_ptr_alcdevice;
-typedef std::unique_ptr<ALCcontext, void(*)(ALCcontext *p)> unique_ptr_alccontext;
-
-static void delete_alcdevice(ALCdevice *p)
-{
-	if (p)
-		alcCloseDevice(p);
-}
-
-static void delete_alccontext(ALCcontext *p)
-{
-	if (p) {
-		alcMakeContextCurrent(nullptr);
-		alcDestroyContext(p);
-	}
-}
-
-static const char *alErrorString(ALenum err)
-{
-	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_error(ALenum err, const char *desc)
-{
-	if(err == AL_NO_ERROR)
-		return err;
-	warningstream<<desc<<": "<<alErrorString(err)<<std::endl;
-	return err;
-}
-
-void f3_set(ALfloat *f3, v3f v)
-{
-	f3[0] = v.X;
-	f3[1] = v.Y;
-	f3[2] = v.Z;
-}
-
-struct SoundBuffer
-{
-	ALenum format;
-	ALsizei freq;
-	ALuint buffer_id;
-	std::vector<char> buffer;
-};
-
-SoundBuffer *load_opened_ogg_file(OggVorbis_File *oggFile,
-		const std::string &filename_for_logging)
-{
-	int endian = 0; // 0 for Little-Endian, 1 for Big-Endian
-	int bitStream;
-	long bytes;
-	char array[BUFFER_SIZE]; // Local fixed size array
-	vorbis_info *pInfo;
-
-	SoundBuffer *snd = new SoundBuffer;
-
-	// Get some information about the OGG file
-	pInfo = ov_info(oggFile, -1);
-
-	// Check the number of channels... always use 16-bit samples
-	if(pInfo->channels == 1)
-		snd->format = AL_FORMAT_MONO16;
-	else
-		snd->format = AL_FORMAT_STEREO16;
-
-	// The frequency of the sampling rate
-	snd->freq = pInfo->rate;
-
-	// Keep reading until all is read
-	do
-	{
-		// Read up to a buffer's worth of decoded sound data
-		bytes = ov_read(oggFile, array, BUFFER_SIZE, endian, 2, 1, &bitStream);
-
-		if(bytes < 0)
-		{
-			ov_clear(oggFile);
-			infostream << "Audio: Error decoding "
-				<< filename_for_logging << std::endl;
-			delete snd;
-			return nullptr;
-		}
-
-		// Append to end of buffer
-		snd->buffer.insert(snd->buffer.end(), array, array + bytes);
-	} while (bytes > 0);
-
-	alGenBuffers(1, &snd->buffer_id);
-	alBufferData(snd->buffer_id, snd->format,
-			&(snd->buffer[0]), snd->buffer.size(),
-			snd->freq);
-
-	ALenum error = alGetError();
-
-	if(error != AL_NO_ERROR){
-		infostream << "Audio: OpenAL error: " << alErrorString(error)
-				<< "preparing sound buffer" << std::endl;
-	}
-
-	//infostream << "Audio file "
-	//	<< filename_for_logging << " loaded" << std::endl;
-
-	// Clean up!
-	ov_clear(oggFile);
-
-	return snd;
-}
-
-SoundBuffer *load_ogg_from_file(const std::string &path)
-{
-	OggVorbis_File oggFile;
-
-	// Try opening the given file.
-	// This requires libvorbis >= 1.3.2, as
-	// previous versions expect a non-const char *
-	if (ov_fopen(path.c_str(), &oggFile) != 0) {
-		infostream << "Audio: Error opening " << path
-			<< " for decoding" << std::endl;
-		return nullptr;
-	}
-
-	return load_opened_ogg_file(&oggFile, path);
-}
-
-struct BufferSource {
-	const char *buf;
-	size_t cur_offset;
-	size_t len;
-};
-
-size_t buffer_sound_read_func(void *ptr, size_t size, size_t nmemb, void *datasource)
-{
-	BufferSource *s = (BufferSource *)datasource;
-	size_t copied_size = MYMIN(s->len - s->cur_offset, size);
-	memcpy(ptr, s->buf + s->cur_offset, copied_size);
-	s->cur_offset += copied_size;
-	return copied_size;
-}
-
-int buffer_sound_seek_func(void *datasource, ogg_int64_t offset, int whence)
-{
-	BufferSource *s = (BufferSource *)datasource;
-	if (whence == SEEK_SET) {
-		if (offset < 0 || (size_t)MYMAX(offset, 0) >= s->len) {
-			// 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->len) {
-			// offset out of bounds
-			return -1;
-		}
-		s->cur_offset += offset;
-		return 0;
-	}
-	// invalid whence param (SEEK_END doesn't have to be supported)
-	return -1;
-}
-
-long BufferSourceell_func(void *datasource)
-{
-	BufferSource *s = (BufferSource *)datasource;
-	return s->cur_offset;
-}
-
-static ov_callbacks g_buffer_ov_callbacks = {
-	&buffer_sound_read_func,
-	&buffer_sound_seek_func,
-	nullptr,
-	&BufferSourceell_func
-};
-
-SoundBuffer *load_ogg_from_buffer(const std::string &buf, const std::string &id_for_log)
-{
-	OggVorbis_File oggFile;
-
-	BufferSource s;
-	s.buf = buf.c_str();
-	s.cur_offset = 0;
-	s.len = buf.size();
-
-	if (ov_open_callbacks(&s, &oggFile, nullptr, 0, g_buffer_ov_callbacks) != 0) {
-		infostream << "Audio: Error opening " << id_for_log
-			<< " for decoding" << std::endl;
-		return nullptr;
-	}
-
-	return load_opened_ogg_file(&oggFile, id_for_log);
-}
-
-struct PlayingSound
-{
-	ALuint source_id;
-	bool loop;
-};
-
-class SoundManagerSingleton
-{
-public:
-	unique_ptr_alcdevice  m_device;
-	unique_ptr_alccontext m_context;
-public:
-	SoundManagerSingleton() :
-		m_device(nullptr, delete_alcdevice),
-		m_context(nullptr, delete_alccontext)
-	{
-	}
-
-	bool init()
-	{
-		if (!(m_device = unique_ptr_alcdevice(alcOpenDevice(nullptr), delete_alcdevice))) {
-			errorstream << "Audio: Global Initialization: Failed to open device" << std::endl;
-			return false;
-		}
-
-		if (!(m_context = unique_ptr_alccontext(
-				alcCreateContext(m_device.get(), nullptr), delete_alccontext))) {
-			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);
-
-		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()
-	{
-		infostream << "Audio: Global Deinitialized." << std::endl;
-	}
-};
-
-class OpenALSoundManager: public ISoundManager
-{
-private:
-	OnDemandSoundFetcher *m_fetcher;
-	ALCdevice *m_device;
-	ALCcontext *m_context;
-	u16 m_last_used_id = 0; // only access within getFreeId() !
-	std::unordered_map<std::string, std::vector<SoundBuffer*>> m_buffers;
-	std::unordered_map<int, PlayingSound*> m_sounds_playing;
-	struct FadeState {
-		FadeState() = default;
-
-		FadeState(float step, float current_gain, float target_gain):
-			step(step),
-			current_gain(current_gain),
-			target_gain(target_gain) {}
-		float step;
-		float current_gain;
-		float target_gain;
-	};
-
-	std::unordered_map<int, FadeState> m_sounds_fading;
-public:
-	OpenALSoundManager(SoundManagerSingleton *smg, OnDemandSoundFetcher *fetcher):
-		m_fetcher(fetcher),
-		m_device(smg->m_device.get()),
-		m_context(smg->m_context.get())
-	{
-		infostream << "Audio: Initialized: OpenAL " << std::endl;
-	}
-
-	~OpenALSoundManager()
-	{
-		infostream << "Audio: Deinitializing..." << std::endl;
-
-		std::unordered_set<int> source_del_list;
-
-		for (const auto &sp : m_sounds_playing)
-			source_del_list.insert(sp.first);
-
-		for (const auto &id : source_del_list)
-			deleteSound(id);
-
-		for (auto &buffer : m_buffers) {
-			for (SoundBuffer *sb : buffer.second) {
-				alDeleteBuffers(1, &sb->buffer_id);
-
-				ALenum error = alGetError();
-				if (error != AL_NO_ERROR) {
-					warningstream << "Audio: Failed to free stream for "
-						<< buffer.first << ": " << alErrorString(error) << std::endl;
-				}
-
-				delete sb;
-			}
-			buffer.second.clear();
-		}
-		m_buffers.clear();
-
-		infostream << "Audio: Deinitialized." << std::endl;
-	}
-
-	u16 getFreeId()
-	{
-		u16 startid = m_last_used_id;
-		while (!isFreeId(++m_last_used_id)) {
-			if (m_last_used_id == startid)
-				return 0;
-		}
-
-		return m_last_used_id;
-	}
-
-	inline bool isFreeId(int id) const
-	{
-		return id > 0 && m_sounds_playing.find(id) == m_sounds_playing.end();
-	}
-
-	void step(float dtime)
-	{
-		doFades(dtime);
-	}
-
-	void addBuffer(const std::string &name, SoundBuffer *buf)
-	{
-		std::unordered_map<std::string, std::vector<SoundBuffer*>>::iterator i =
-				m_buffers.find(name);
-		if(i != m_buffers.end()){
-			i->second.push_back(buf);
-			return;
-		}
-		std::vector<SoundBuffer*> bufs;
-		bufs.push_back(buf);
-		m_buffers[name] = std::move(bufs);
-	}
-
-	SoundBuffer* getBuffer(const std::string &name)
-	{
-		std::unordered_map<std::string, std::vector<SoundBuffer*>>::iterator i =
-				m_buffers.find(name);
-		if(i == m_buffers.end())
-			return nullptr;
-		std::vector<SoundBuffer*> &bufs = i->second;
-		int j = myrand() % bufs.size();
-		return bufs[j];
-	}
-
-	PlayingSound* createPlayingSound(SoundBuffer *buf, bool loop,
-			float volume, float pitch)
-	{
-		infostream << "OpenALSoundManager: Creating playing sound" << std::endl;
-		assert(buf);
-		PlayingSound *sound = new PlayingSound;
-		assert(sound);
-		warn_if_error(alGetError(), "before createPlayingSound");
-		alGenSources(1, &sound->source_id);
-		alSourcei(sound->source_id, AL_BUFFER, buf->buffer_id);
-		alSourcei(sound->source_id, AL_SOURCE_RELATIVE, true);
-		alSource3f(sound->source_id, AL_POSITION, 0, 0, 0);
-		alSource3f(sound->source_id, AL_VELOCITY, 0, 0, 0);
-		alSourcei(sound->source_id, AL_LOOPING, loop ? AL_TRUE : AL_FALSE);
-		volume = std::fmax(0.0f, volume);
-		alSourcef(sound->source_id, AL_GAIN, volume);
-		alSourcef(sound->source_id, AL_PITCH, pitch);
-		alSourcePlay(sound->source_id);
-		warn_if_error(alGetError(), "createPlayingSound");
-		return sound;
-	}
-
-	PlayingSound* createPlayingSoundAt(SoundBuffer *buf, bool loop,
-			float volume, v3f pos, float pitch)
-	{
-		infostream << "OpenALSoundManager: Creating positional playing sound"
-				<< std::endl;
-		assert(buf);
-		PlayingSound *sound = new PlayingSound;
-
-		warn_if_error(alGetError(), "before createPlayingSoundAt");
-		alGenSources(1, &sound->source_id);
-		alSourcei(sound->source_id, AL_BUFFER, buf->buffer_id);
-		alSourcei(sound->source_id, AL_SOURCE_RELATIVE, false);
-		alSource3f(sound->source_id, AL_POSITION, pos.X, pos.Y, pos.Z);
-		alSource3f(sound->source_id, AL_VELOCITY, 0, 0, 0);
-		// Use alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED) and set reference
-		// distance to clamp gain at <1 node distance, to avoid excessive
-		// volume when closer
-		alSourcef(sound->source_id, AL_REFERENCE_DISTANCE, 10.0f);
-		alSourcei(sound->source_id, AL_LOOPING, loop ? AL_TRUE : AL_FALSE);
-		// Multiply by 3 to compensate for reducing AL_REFERENCE_DISTANCE from
-		// the previous value of 30 to the new value of 10
-		volume = std::fmax(0.0f, volume * 3.0f);
-		alSourcef(sound->source_id, AL_GAIN, volume);
-		alSourcef(sound->source_id, AL_PITCH, pitch);
-		alSourcePlay(sound->source_id);
-		warn_if_error(alGetError(), "createPlayingSoundAt");
-		return sound;
-	}
-
-	int playSoundRaw(SoundBuffer *buf, bool loop, float volume, float pitch)
-	{
-		assert(buf);
-		PlayingSound *sound = createPlayingSound(buf, loop, volume, pitch);
-		if (!sound)
-			return -1;
-
-		int handle = getFreeId();
-		m_sounds_playing[handle] = sound;
-		return handle;
-	}
-
-	void deleteSound(int id)
-	{
-		auto i = m_sounds_playing.find(id);
-		if(i == m_sounds_playing.end())
-			return;
-		PlayingSound *sound = i->second;
-
-		alDeleteSources(1, &sound->source_id);
-
-		delete sound;
-		m_sounds_playing.erase(id);
-	}
-
-	/* If buffer does not exist, consult the fetcher */
-	SoundBuffer* getFetchBuffer(const std::string &name)
-	{
-		SoundBuffer *buf = getBuffer(name);
-		if(buf)
-			return buf;
-		if(!m_fetcher)
-			return nullptr;
-		std::set<std::string> paths;
-		std::set<std::string> datas;
-		m_fetcher->fetchSounds(name, paths, datas);
-		for (const std::string &path : paths) {
-			loadSoundFile(name, path);
-		}
-		for (const std::string &data : datas) {
-			loadSoundData(name, data);
-		}
-		return getBuffer(name);
-	}
-
-	// Remove stopped sounds
-	void maintain()
-	{
-		if (!m_sounds_playing.empty()) {
-			verbosestream << "OpenALSoundManager::maintain(): "
-					<< m_sounds_playing.size() <<" playing sounds, "
-					<< m_buffers.size() <<" sound names loaded"<<std::endl;
-		}
-		std::unordered_set<int> del_list;
-		for (const auto &sp : m_sounds_playing) {
-			int id = sp.first;
-			PlayingSound *sound = sp.second;
-			// If not playing, remove it
-			{
-				ALint state;
-				alGetSourcei(sound->source_id, AL_SOURCE_STATE, &state);
-				if(state != AL_PLAYING){
-					del_list.insert(id);
-				}
-			}
-		}
-		if(!del_list.empty())
-			verbosestream<<"OpenALSoundManager::maintain(): deleting "
-					<<del_list.size()<<" playing sounds"<<std::endl;
-		for (int i : del_list) {
-			deleteSound(i);
-		}
-	}
-
-	/* Interface */
-
-	bool loadSoundFile(const std::string &name,
-			const std::string &filepath)
-	{
-		SoundBuffer *buf = load_ogg_from_file(filepath);
-		if (buf)
-			addBuffer(name, buf);
-		return !!buf;
-	}
-
-	bool loadSoundData(const std::string &name,
-			const std::string &filedata)
-	{
-		SoundBuffer *buf = load_ogg_from_buffer(filedata, name);
-		if (buf)
-			addBuffer(name, buf);
-		return !!buf;
-	}
-
-	void updateListener(const v3f &pos, const v3f &vel, const v3f &at, const v3f &up)
-	{
-		alListener3f(AL_POSITION, pos.X, pos.Y, pos.Z);
-		alListener3f(AL_VELOCITY, vel.X, vel.Y, vel.Z);
-		ALfloat f[6];
-		f3_set(f, at);
-		f3_set(f+3, -up);
-		alListenerfv(AL_ORIENTATION, f);
-		warn_if_error(alGetError(), "updateListener");
-	}
-
-	void setListenerGain(float gain)
-	{
-		alListenerf(AL_GAIN, gain);
-	}
-
-	int playSound(const SimpleSoundSpec &spec)
-	{
-		maintain();
-		if (spec.name.empty())
-			return 0;
-		SoundBuffer *buf = getFetchBuffer(spec.name);
-		if(!buf){
-			infostream << "OpenALSoundManager: \"" << spec.name << "\" not found."
-					<< std::endl;
-			return -1;
-		}
-
-		int handle = -1;
-		if (spec.fade > 0) {
-			handle = playSoundRaw(buf, spec.loop, 0.0f, spec.pitch);
-			fadeSound(handle, spec.fade, spec.gain);
-		} else {
-			handle = playSoundRaw(buf, spec.loop, spec.gain, spec.pitch);
-		}
-		return handle;
-	}
-
-	int playSoundAt(const SimpleSoundSpec &spec, const v3f &pos)
-	{
-		maintain();
-		if (spec.name.empty())
-			return 0;
-		SoundBuffer *buf = getFetchBuffer(spec.name);
-		if (!buf) {
-			infostream << "OpenALSoundManager: \"" << spec.name << "\" not found."
-					<< std::endl;
-			return -1;
-		}
-
-		PlayingSound *sound = createPlayingSoundAt(buf, spec.loop, spec.gain, pos, spec.pitch);
-		if (!sound)
-			return -1;
-		int handle = getFreeId();
-		m_sounds_playing[handle] = sound;
-		return handle;
-	}
-
-	void stopSound(int sound)
-	{
-		maintain();
-		deleteSound(sound);
-	}
-
-	void fadeSound(int soundid, float step, float gain)
-	{
-		// Ignore the command if step isn't valid.
-		if (step == 0 || soundid < 0)
-			return;
-
-		float current_gain = getSoundGain(soundid);
-		step = gain - current_gain > 0 ? abs(step) : -abs(step);
-		if (m_sounds_fading.find(soundid) != m_sounds_fading.end()) {
-			auto current_fade = m_sounds_fading[soundid];
-			// Do not replace the fade if it's equivalent.
-			if (current_fade.target_gain == gain && current_fade.step == step)
-				return;
-			m_sounds_fading.erase(soundid);
-		}
-		gain = rangelim(gain, 0, 1);
-		m_sounds_fading[soundid] = FadeState(step, current_gain, gain);
-	}
-
-	void doFades(float dtime)
-	{
-		for (auto i = m_sounds_fading.begin(); i != m_sounds_fading.end();) {
-			FadeState& fade = i->second;
-			assert(fade.step != 0);
-			fade.current_gain += (fade.step * dtime);
-
-			if (fade.step < 0.f)
-				fade.current_gain = std::max(fade.current_gain, fade.target_gain);
-			else
-				fade.current_gain = std::min(fade.current_gain, fade.target_gain);
-
-			if (fade.current_gain <= 0.f)
-				stopSound(i->first);
-			else
-				updateSoundGain(i->first, fade.current_gain);
-
-			// The increment must happen during the erase call, or else it'll segfault.
-			if (fade.current_gain == fade.target_gain)
-				m_sounds_fading.erase(i++);
-			else
-				i++;
-		}
-	}
-
-	bool soundExists(int sound)
-	{
-		maintain();
-		return (m_sounds_playing.count(sound) != 0);
-	}
-
-	void updateSoundPosition(int id, v3f pos)
-	{
-		auto i = m_sounds_playing.find(id);
-		if (i == m_sounds_playing.end())
-			return;
-		PlayingSound *sound = i->second;
-
-		alSourcei(sound->source_id, AL_SOURCE_RELATIVE, false);
-		alSource3f(sound->source_id, AL_POSITION, pos.X, pos.Y, pos.Z);
-		alSource3f(sound->source_id, AL_VELOCITY, 0.0f, 0.0f, 0.0f);
-		alSourcef(sound->source_id, AL_REFERENCE_DISTANCE, 10.0f);
-	}
-
-	bool updateSoundGain(int id, float gain)
-	{
-		auto i = m_sounds_playing.find(id);
-		if (i == m_sounds_playing.end())
-			return false;
-
-		PlayingSound *sound = i->second;
-		alSourcef(sound->source_id, AL_GAIN, gain);
-		return true;
-	}
-
-	float getSoundGain(int id)
-	{
-		auto i = m_sounds_playing.find(id);
-		if (i == m_sounds_playing.end())
-			return 0;
-
-		PlayingSound *sound = i->second;
-		ALfloat gain;
-		alGetSourcef(sound->source_id, AL_GAIN, &gain);
-		return gain;
-	}
-};
-
 std::shared_ptr<SoundManagerSingleton> createSoundManagerSingleton()
 {
 	auto smg = std::make_shared<SoundManagerSingleton>();
@@ -728,7 +35,8 @@ std::shared_ptr<SoundManagerSingleton> createSoundManagerSingleton()
 	return smg;
 }
 
-ISoundManager *createOpenALSoundManager(SoundManagerSingleton *smg, OnDemandSoundFetcher *fetcher)
+std::unique_ptr<ISoundManager> createOpenALSoundManager(SoundManagerSingleton *smg,
+		std::unique_ptr<SoundFallbackPathProvider> fallback_path_provider)
 {
-	return new OpenALSoundManager(smg, fetcher);
+	return std::make_unique<OpenALSoundManager>(smg, std::move(fallback_path_provider));
 };

+ 5 - 4
src/client/sound_openal.h

@@ -19,13 +19,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 
 #pragma once
 
-#include <memory>
-
 #include "sound.h"
 
+#include <memory>
+
 class SoundManagerSingleton;
 extern std::shared_ptr<SoundManagerSingleton> g_sound_manager_singleton;
 
 std::shared_ptr<SoundManagerSingleton> createSoundManagerSingleton();
-ISoundManager *createOpenALSoundManager(
-		SoundManagerSingleton *smg, OnDemandSoundFetcher *fetcher);
+std::unique_ptr<ISoundManager> createOpenALSoundManager(
+		SoundManagerSingleton *smg,
+		std::unique_ptr<SoundFallbackPathProvider> fallback_path_provider);

+ 1125 - 0
src/client/sound_openal_internal.cpp

@@ -0,0 +1,1125 @@
+/*
+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 "../sound.h"
+#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
+	int num_issued_sounds = std::ceil(m_sounds_streaming_current_bigstep.size()
+			* dtime / m_stream_timer);
+
+	for (; num_issued_sounds > 0; --num_issued_sounds) {
+		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 chosen_sound_name;
+
+	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)
+			break;
+
+		// 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 chosen_sound_name;
+}
+
+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)
+{
+	if (id == 0)
+		id = allocateId(1);
+
+	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) :
+	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;
+
+	// remember for lazy loading
+	m_sound_datas_unopen.emplace(name, std::make_unique<SoundDataUnopenFile>(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;
+
+	// remember for lazy loading
+	m_sound_datas_unopen.emplace(name, std::make_unique<SoundDataUnopenBuffer>(std::move(filedata)));
+	return true;
+}
+
+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);
+}

+ 613 - 0
src/client/sound_openal_internal.h

@@ -0,0 +1,613 @@
+/*
+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 "util/basic_macros.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 <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;
+
+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 public ISoundManager interface
+ */
+
+class OpenALSoundManager final : public ISoundManager
+{
+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;
+
+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)
+
+	/* 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;
+};

+ 13 - 37
src/gui/guiEngine.cpp

@@ -32,7 +32,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "guiMainMenu.h"
 #include "sound.h"
 #include "client/sound_openal.h"
-#include "client/clouds.h"
 #include "httpfetch.h"
 #include "log.h"
 #include "client/fontengine.h"
@@ -97,28 +96,15 @@ video::ITexture *MenuTextureSource::getTexture(const std::string &name, u32 *id)
 /******************************************************************************/
 /** MenuMusicFetcher                                                          */
 /******************************************************************************/
-void MenuMusicFetcher::fetchSounds(const std::string &name,
-			std::set<std::string> &dst_paths,
-			std::set<std::string> &dst_datas)
+void MenuMusicFetcher::addThePaths(const std::string &name,
+		std::vector<std::string> &paths)
 {
-	if(m_fetched.count(name))
-		return;
-	m_fetched.insert(name);
-	std::vector<fs::DirListNode> list;
-	// Reusable local function
-	auto add_paths = [&dst_paths](const std::string name, const std::string base = "") {
-		dst_paths.insert(base + name + ".ogg");
-		for (int i = 0; i < 10; i++)
-			dst_paths.insert(base + name + "." + itos(i) + ".ogg");
-	};
 	// Allow full paths
 	if (name.find(DIR_DELIM_CHAR) != std::string::npos) {
-		add_paths(name);
+		addAllAlternatives(name, paths);
 	} else {
-		std::string share_prefix = porting::path_share + DIR_DELIM;
-		add_paths(name, share_prefix + "sounds" + DIR_DELIM);
-		std::string user_prefix = porting::path_user + DIR_DELIM;
-		add_paths(name, user_prefix + "sounds" + DIR_DELIM);
+		addAllAlternatives(porting::path_share + DIR_DELIM + "sounds" + DIR_DELIM + name, paths);
+		addAllAlternatives(porting::path_user + DIR_DELIM + "sounds" + DIR_DELIM + name, paths);
 	}
 }
 
@@ -151,8 +137,10 @@ GUIEngine::GUIEngine(JoystickController *joystick,
 
 	// create soundmanager
 #if USE_SOUND
-	if (g_settings->getBool("enable_sound") && g_sound_manager_singleton.get())
-		m_sound_manager.reset(createOpenALSoundManager(g_sound_manager_singleton.get(), &m_soundfetcher));
+	if (g_settings->getBool("enable_sound") && g_sound_manager_singleton.get()) {
+		m_sound_manager = createOpenALSoundManager(g_sound_manager_singleton.get(),
+				std::make_unique<MenuMusicFetcher>());
+	}
 #endif
 	if (!m_sound_manager)
 		m_sound_manager = std::make_unique<DummySoundManager>();
@@ -318,11 +306,12 @@ void GUIEngine::run()
 /******************************************************************************/
 GUIEngine::~GUIEngine()
 {
-	m_sound_manager.reset();
-
-	infostream<<"GUIEngine: Deinitializing scripting"<<std::endl;
+	// deinitialize script first. gc destructors might depend on other stuff
+	infostream << "GUIEngine: Deinitializing scripting" << std::endl;
 	m_script.reset();
 
+	m_sound_manager.reset();
+
 	m_irr_toplefttext->setText(L"");
 
 	//clean up texture pointers
@@ -608,16 +597,3 @@ void GUIEngine::updateTopLeftTextSize()
 	m_irr_toplefttext = gui::StaticText::add(m_rendering_engine->get_gui_env(),
 			m_toplefttext, rect, false, true, 0, -1);
 }
-
-/******************************************************************************/
-s32 GUIEngine::playSound(const SimpleSoundSpec &spec)
-{
-	s32 handle = m_sound_manager->playSound(spec);
-	return handle;
-}
-
-/******************************************************************************/
-void GUIEngine::stopSound(s32 handle)
-{
-	m_sound_manager->stopSound(handle);
-}

+ 7 - 26
src/gui/guiEngine.h

@@ -115,30 +115,20 @@ private:
 	std::vector<video::ITexture*> m_to_delete;
 };
 
-/** GUIEngine specific implementation of OnDemandSoundFetcher */
-class MenuMusicFetcher: public OnDemandSoundFetcher
+/** GUIEngine specific implementation of SoundFallbackPathProvider */
+class MenuMusicFetcher final : public SoundFallbackPathProvider
 {
-public:
-	/**
-	 * get sound file paths according to sound name
-	 * @param name sound name
-	 * @param dst_paths receives possible paths to sound files
-	 * @param dst_datas receives binary sound data (not used here)
-	 */
-	void fetchSounds(const std::string &name,
-			std::set<std::string> &dst_paths,
-			std::set<std::string> &dst_datas);
-
-private:
-	/** set of fetched sound names */
-	std::set<std::string> m_fetched;
+protected:
+	void addThePaths(const std::string &name,
+			std::vector<std::string> &paths) override;
 };
 
 /** implementation of main menu based uppon formspecs */
 class GUIEngine {
 	/** grant ModApiMainMenu access to private members */
 	friend class ModApiMainMenu;
-	friend class ModApiSound;
+	friend class ModApiMainMenuSound;
+	friend class MainMenuSoundHandle;
 
 public:
 	/**
@@ -197,8 +187,6 @@ private:
 	MainMenuData                         *m_data = nullptr;
 	/** texture source */
 	std::unique_ptr<ISimpleTextureSource> m_texture_source;
-	/** sound fetcher, used by sound manager*/
-	MenuMusicFetcher                      m_soundfetcher{};
 	/** sound manager*/
 	std::unique_ptr<ISoundManager>        m_sound_manager;
 
@@ -296,11 +284,4 @@ private:
 	bool        m_clouds_enabled = true;
 	/** data used to draw clouds */
 	clouddata   m_cloud;
-
-	/** start playing a sound and return handle */
-	s32 playSound(const SimpleSoundSpec &spec);
-	/** stop playing a sound started with playSound() */
-	void stopSound(s32 handle);
-
-
 };

+ 4 - 4
src/gui/guiFormSpecMenu.cpp

@@ -4816,7 +4816,7 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
 				if ((s.ftype == f_TabHeader) &&
 						(s.fid == event.GUIEvent.Caller->getID())) {
 					if (!s.sound.empty() && m_sound_manager)
-						m_sound_manager->playSound(SimpleSoundSpec(s.sound, 1.0f));
+						m_sound_manager->playSound(0, SoundSpec(s.sound, 1.0f));
 					s.send = true;
 					acceptInput();
 					s.send = false;
@@ -4861,7 +4861,7 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
 
 				if (s.ftype == f_Button || s.ftype == f_CheckBox) {
 					if (!s.sound.empty() && m_sound_manager)
-						m_sound_manager->playSound(SimpleSoundSpec(s.sound, 1.0f));
+						m_sound_manager->playSound(0, SoundSpec(s.sound, 1.0f));
 
 					s.send = true;
 					if (s.is_exit) {
@@ -4886,7 +4886,7 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
 						}
 					}
 					if (!s.sound.empty() && m_sound_manager)
-						m_sound_manager->playSound(SimpleSoundSpec(s.sound, 1.0f));
+						m_sound_manager->playSound(0, SoundSpec(s.sound, 1.0f));
 					s.send = true;
 					acceptInput(quit_mode_no);
 
@@ -4904,7 +4904,7 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
 					s.fdefault.clear();
 				} else if (s.ftype == f_Unknown || s.ftype == f_HyperText) {
 					if (!s.sound.empty() && m_sound_manager)
-						m_sound_manager->playSound(SimpleSoundSpec(s.sound, 1.0f));
+						m_sound_manager->playSound(0, SoundSpec(s.sound, 1.0f));
 					s.send = true;
 					acceptInput();
 					s.send = false;

+ 12 - 12
src/itemdef.cpp

@@ -117,10 +117,10 @@ void ItemDefinition::reset()
 	delete tool_capabilities;
 	tool_capabilities = NULL;
 	groups.clear();
-	sound_place = SimpleSoundSpec();
-	sound_place_failed = SimpleSoundSpec();
-	sound_use = SimpleSoundSpec();
-	sound_use_air = SimpleSoundSpec();
+	sound_place = SoundSpec();
+	sound_place_failed = SoundSpec();
+	sound_use = SoundSpec();
+	sound_use_air = SoundSpec();
 	range = -1;
 	node_placement_prediction.clear();
 	place_param2 = 0;
@@ -158,8 +158,8 @@ void ItemDefinition::serialize(std::ostream &os, u16 protocol_version) const
 	os << serializeString16(node_placement_prediction);
 
 	// Version from ContentFeatures::serialize to keep in sync
-	sound_place.serialize(os, protocol_version);
-	sound_place_failed.serialize(os, protocol_version);
+	sound_place.serializeSimple(os, protocol_version);
+	sound_place_failed.serializeSimple(os, protocol_version);
 
 	writeF32(os, range);
 	os << serializeString16(palette_image);
@@ -171,8 +171,8 @@ void ItemDefinition::serialize(std::ostream &os, u16 protocol_version) const
 
 	os << place_param2;
 
-	sound_use.serialize(os, protocol_version);
-	sound_use_air.serialize(os, protocol_version);
+	sound_use.serializeSimple(os, protocol_version);
+	sound_use_air.serializeSimple(os, protocol_version);
 }
 
 void ItemDefinition::deSerialize(std::istream &is, u16 protocol_version)
@@ -212,8 +212,8 @@ void ItemDefinition::deSerialize(std::istream &is, u16 protocol_version)
 
 	node_placement_prediction = deSerializeString16(is);
 
-	sound_place.deSerialize(is, protocol_version);
-	sound_place_failed.deSerialize(is, protocol_version);
+	sound_place.deSerializeSimple(is, protocol_version);
+	sound_place_failed.deSerializeSimple(is, protocol_version);
 
 	range = readF32(is);
 	palette_image = deSerializeString16(is);
@@ -228,8 +228,8 @@ void ItemDefinition::deSerialize(std::istream &is, u16 protocol_version)
 
 		place_param2 = readU8(is); // 0 if missing
 
-		sound_use.deSerialize(is, protocol_version);
-		sound_use_air.deSerialize(is, protocol_version);
+		sound_use.deSerializeSimple(is, protocol_version);
+		sound_use_air.deSerializeSimple(is, protocol_version);
 	} catch(SerializationError &e) {};
 }
 

+ 3 - 3
src/itemdef.h

@@ -78,9 +78,9 @@ struct ItemDefinition
 	// May be NULL. If non-NULL, deleted by destructor
 	ToolCapabilities *tool_capabilities;
 	ItemGroupList groups;
-	SimpleSoundSpec sound_place;
-	SimpleSoundSpec sound_place_failed;
-	SimpleSoundSpec sound_use, sound_use_air;
+	SoundSpec sound_place;
+	SoundSpec sound_place_failed;
+	SoundSpec sound_use, sound_use_air;
 	f32 range;
 
 	// Client shall immediately place this node when player places the item.

+ 35 - 18
src/network/clientpackethandler.cpp

@@ -805,57 +805,74 @@ void Client::handleCommand_ItemDef(NetworkPacket* pkt)
 void Client::handleCommand_PlaySound(NetworkPacket* pkt)
 {
 	/*
-		[0] u32 server_id
+		[0] s32 server_id
 		[4] u16 name length
 		[6] char name[len]
 		[ 6 + len] f32 gain
-		[10 + len] u8 type
-		[11 + len] (f32 * 3) pos
+		[10 + len] u8 type (SoundLocation)
+		[11 + len] v3f pos (in BS-space)
 		[23 + len] u16 object_id
 		[25 + len] bool loop
 		[26 + len] f32 fade
 		[30 + len] f32 pitch
 		[34 + len] bool ephemeral
+		[35 + len] f32 start_time (in seconds)
 	*/
 
 	s32 server_id;
 
-	SimpleSoundSpec spec;
-	SoundLocation type; // 0=local, 1=positional, 2=object
+	SoundSpec spec;
+	SoundLocation type;
 	v3f pos;
 	u16 object_id;
 	bool ephemeral = false;
 
 	*pkt >> server_id >> spec.name >> spec.gain >> (u8 &)type >> pos >> object_id >> spec.loop;
+	pos *= 1.0f/BS;
 
 	try {
 		*pkt >> spec.fade;
 		*pkt >> spec.pitch;
 		*pkt >> ephemeral;
+		*pkt >> spec.start_time;
 	} catch (PacketError &e) {};
 
+	// Generate a new id
+	sound_handle_t client_id = (ephemeral && object_id == 0) ? 0 : m_sound->allocateId(2);
+
 	// Start playing
-	int client_id = -1;
 	switch(type) {
 	case SoundLocation::Local:
-		client_id = m_sound->playSound(spec);
+		m_sound->playSound(client_id, spec);
 		break;
 	case SoundLocation::Position:
-		client_id = m_sound->playSoundAt(spec, pos);
+		m_sound->playSoundAt(client_id, spec, pos, v3f(0.0f));
 		break;
-	case SoundLocation::Object:
-		{
-			ClientActiveObject *cao = m_env.getActiveObject(object_id);
-			if (cao)
-				pos = cao->getPosition();
-			client_id = m_sound->playSoundAt(spec, pos);
-			break;
+	case SoundLocation::Object: {
+		ClientActiveObject *cao = m_env.getActiveObject(object_id);
+		v3f vel(0.0f);
+		if (cao) {
+			pos = cao->getPosition() * (1.0f/BS);
+			vel = cao->getVelocity() * (1.0f/BS);
 		}
+		m_sound->playSoundAt(client_id, spec, pos, vel);
+		break;
+	}
+	default:
+		// Unknown SoundLocation, instantly remove sound
+		if (client_id != 0)
+			m_sound->freeId(client_id, 2);
+		if (!ephemeral)
+			sendRemovedSounds({server_id});
+		return;
 	}
 
-	if (client_id != -1) {
-		// for ephemeral sounds, server_id is not meaningful
-		if (!ephemeral) {
+	if (client_id != 0) {
+		// Note: m_sounds_client_to_server takes 1 ownership
+		// For ephemeral sounds, server_id is not meaningful
+		if (ephemeral) {
+			m_sounds_client_to_server[client_id] = -1;
+		} else {
 			m_sounds_server_to_client[server_id] = client_id;
 			m_sounds_client_to_server[client_id] = server_id;
 		}

+ 11 - 5
src/network/networkprotocol.h

@@ -215,9 +215,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 		new fields for TOCLIENT_SET_LIGHTING and TOCLIENT_SET_SKY
 		Send forgotten TweenedParameter properties
 		[scheduled bump for 5.7.0]
+	PROTOCOL VERSION 43:
+		"start_time" added to TOCLIENT_PLAY_SOUND
+		[scheduled bump for 5.8.0]
 */
 
-#define LATEST_PROTOCOL_VERSION 42
+#define LATEST_PROTOCOL_VERSION 43
 #define LATEST_PROTOCOL_VERSION_STRING TOSTRING(LATEST_PROTOCOL_VERSION)
 
 // Server's supported network protocol range
@@ -454,15 +457,18 @@ enum ToClientCommand
 
 	TOCLIENT_PLAY_SOUND = 0x3f,
 	/*
-		s32 sound_id
+		s32 server_id
 		u16 len
 		u8[len] sound name
-		s32 gain*1000
-		u8 type (0=local, 1=positional, 2=object)
-		s32[3] pos_nodes*10000
+		f32 gain
+		u8 type (SoundLocation: 0=local, 1=positional, 2=object)
+		v3f pos_nodes (in BS-space)
 		u16 object_id
 		u8 loop (bool)
+		f32 fade
+		f32 pitch
 		u8 ephemeral (bool)
+		f32 start_time (in seconds)
 	*/
 
 	TOCLIENT_STOP_SOUND = 0x40,

+ 9 - 9
src/nodedef.cpp

@@ -403,9 +403,9 @@ void ContentFeatures::reset()
 	waving = 0;
 	legacy_facedir_simple = false;
 	legacy_wallmounted = false;
-	sound_footstep = SimpleSoundSpec();
-	sound_dig = SimpleSoundSpec("__group");
-	sound_dug = SimpleSoundSpec();
+	sound_footstep = SoundSpec();
+	sound_dig = SoundSpec("__group");
+	sound_dug = SoundSpec();
 	connects_to.clear();
 	connects_to_ids.clear();
 	connect_sides = 0;
@@ -529,9 +529,9 @@ void ContentFeatures::serialize(std::ostream &os, u16 protocol_version) const
 	collision_box.serialize(os, protocol_version);
 
 	// sound
-	sound_footstep.serialize(os, protocol_version);
-	sound_dig.serialize(os, protocol_version);
-	sound_dug.serialize(os, protocol_version);
+	sound_footstep.serializeSimple(os, protocol_version);
+	sound_dig.serializeSimple(os, protocol_version);
+	sound_dug.serializeSimple(os, protocol_version);
 
 	// legacy
 	writeU8(os, legacy_facedir_simple);
@@ -626,9 +626,9 @@ void ContentFeatures::deSerialize(std::istream &is, u16 protocol_version)
 	collision_box.deSerialize(is);
 
 	// sounds
-	sound_footstep.deSerialize(is, protocol_version);
-	sound_dig.deSerialize(is, protocol_version);
-	sound_dug.deSerialize(is, protocol_version);
+	sound_footstep.deSerializeSimple(is, protocol_version);
+	sound_dig.deSerializeSimple(is, protocol_version);
+	sound_dug.deSerializeSimple(is, protocol_version);
 
 	// read legacy properties
 	legacy_facedir_simple = readU8(is);

+ 4 - 4
src/nodedef.h

@@ -31,7 +31,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 class Client;
 #endif
 #include "itemgroup.h"
-#include "sound.h" // SimpleSoundSpec
+#include "sound.h" // SoundSpec
 #include "constants.h" // BS
 #include "texture_override.h" // TextureOverride
 #include "tileanimation.h"
@@ -434,9 +434,9 @@ struct ContentFeatures
 
 	// --- SOUND PROPERTIES ---
 
-	SimpleSoundSpec sound_footstep;
-	SimpleSoundSpec sound_dig;
-	SimpleSoundSpec sound_dug;
+	SoundSpec sound_footstep;
+	SoundSpec sound_dig;
+	SoundSpec sound_dug;
 
 	// --- LEGACY ---
 

+ 3 - 1
src/player.h

@@ -146,11 +146,13 @@ public:
 			std::vector<CollisionInfo> *collision_info)
 	{}
 
+	// in BS-space
 	v3f getSpeed() const
 	{
 		return m_speed;
 	}
 
+	// in BS-space
 	void setSpeed(v3f speed)
 	{
 		m_speed = speed;
@@ -223,7 +225,7 @@ public:
 
 protected:
 	char m_name[PLAYERNAME_SIZE];
-	v3f m_speed;
+	v3f m_speed; // velocity; in BS-space
 	u16 m_wield_index = 0;
 	PlayerFovSpec m_fov_override_spec = { 0.0f, false, 0.0f };
 

+ 16 - 15
src/script/common/c_content.cpp

@@ -104,10 +104,10 @@ void read_item_definition(lua_State* L, int index,
 	if (!lua_isnil(L, -1)) {
 		luaL_checktype(L, -1, LUA_TTABLE);
 		lua_getfield(L, -1, "place");
-		read_soundspec(L, -1, def.sound_place);
+		read_simplesoundspec(L, -1, def.sound_place);
 		lua_pop(L, 1);
 		lua_getfield(L, -1, "place_failed");
-		read_soundspec(L, -1, def.sound_place_failed);
+		read_simplesoundspec(L, -1, def.sound_place_failed);
 		lua_pop(L, 1);
 	}
 	lua_pop(L, 1);
@@ -117,10 +117,10 @@ void read_item_definition(lua_State* L, int index,
 	if (!lua_isnil(L, -1)) {
 		luaL_checktype(L, -1, LUA_TTABLE);
 		lua_getfield(L, -1, "punch_use");
-		read_soundspec(L, -1, def.sound_use);
+		read_simplesoundspec(L, -1, def.sound_use);
 		lua_pop(L, 1);
 		lua_getfield(L, -1, "punch_use_air");
-		read_soundspec(L, -1, def.sound_use_air);
+		read_simplesoundspec(L, -1, def.sound_use_air);
 		lua_pop(L, 1);
 	}
 	lua_pop(L, 1);
@@ -187,9 +187,9 @@ void push_item_definition_full(lua_State *L, const ItemDefinition &i)
 	}
 	push_groups(L, i.groups);
 	lua_setfield(L, -2, "groups");
-	push_soundspec(L, i.sound_place);
+	push_simplesoundspec(L, i.sound_place);
 	lua_setfield(L, -2, "sound_place");
-	push_soundspec(L, i.sound_place_failed);
+	push_simplesoundspec(L, i.sound_place_failed);
 	lua_setfield(L, -2, "sound_place_failed");
 	lua_pushstring(L, i.node_placement_prediction.c_str());
 	lua_setfield(L, -2, "node_placement_prediction");
@@ -821,13 +821,13 @@ void read_content_features(lua_State *L, ContentFeatures &f, int index)
 	lua_getfield(L, index, "sounds");
 	if(lua_istable(L, -1)){
 		lua_getfield(L, -1, "footstep");
-		read_soundspec(L, -1, f.sound_footstep);
+		read_simplesoundspec(L, -1, f.sound_footstep);
 		lua_pop(L, 1);
 		lua_getfield(L, -1, "dig");
-		read_soundspec(L, -1, f.sound_dig);
+		read_simplesoundspec(L, -1, f.sound_dig);
 		lua_pop(L, 1);
 		lua_getfield(L, -1, "dug");
-		read_soundspec(L, -1, f.sound_dug);
+		read_simplesoundspec(L, -1, f.sound_dug);
 		lua_pop(L, 1);
 	}
 	lua_pop(L, 1);
@@ -965,11 +965,11 @@ void push_content_features(lua_State *L, const ContentFeatures &c)
 	push_nodebox(L, c.collision_box);
 	lua_setfield(L, -2, "collision_box");
 	lua_newtable(L);
-	push_soundspec(L, c.sound_footstep);
+	push_simplesoundspec(L, c.sound_footstep);
 	lua_setfield(L, -2, "sound_footstep");
-	push_soundspec(L, c.sound_dig);
+	push_simplesoundspec(L, c.sound_dig);
 	lua_setfield(L, -2, "sound_dig");
-	push_soundspec(L, c.sound_dug);
+	push_simplesoundspec(L, c.sound_dug);
 	lua_setfield(L, -2, "sound_dug");
 	lua_setfield(L, -2, "sounds");
 	lua_pushboolean(L, c.legacy_facedir_simple);
@@ -1067,10 +1067,11 @@ void read_server_sound_params(lua_State *L, int index,
 	if(index < 0)
 		index = lua_gettop(L) + 1 + index;
 
-	if(lua_istable(L, index)){
+	if (lua_istable(L, index)) {
 		// Functional overlap: this may modify SimpleSoundSpec contents
 		getfloatfield(L, index, "fade", params.spec.fade);
 		getfloatfield(L, index, "pitch", params.spec.pitch);
+		getfloatfield(L, index, "start_time", params.spec.start_time);
 		getboolfield(L, index, "loop", params.spec.loop);
 
 		getfloatfield(L, index, "gain", params.gain);
@@ -1101,7 +1102,7 @@ void read_server_sound_params(lua_State *L, int index,
 }
 
 /******************************************************************************/
-void read_soundspec(lua_State *L, int index, SimpleSoundSpec &spec)
+void read_simplesoundspec(lua_State *L, int index, SoundSpec &spec)
 {
 	if(index < 0)
 		index = lua_gettop(L) + 1 + index;
@@ -1118,7 +1119,7 @@ void read_soundspec(lua_State *L, int index, SimpleSoundSpec &spec)
 	}
 }
 
-void push_soundspec(lua_State *L, const SimpleSoundSpec &spec)
+void push_simplesoundspec(lua_State *L, const SoundSpec &spec)
 {
 	lua_createtable(L, 0, 3);
 	lua_pushstring(L, spec.name.c_str());

+ 5 - 5
src/script/common/c_content.h

@@ -53,7 +53,7 @@ struct ItemStack;
 struct ItemDefinition;
 struct ToolCapabilities;
 struct ObjectProperties;
-struct SimpleSoundSpec;
+struct SoundSpec;
 struct ServerPlayingSound;
 class Inventory;
 class InventoryList;
@@ -87,8 +87,8 @@ void               push_palette              (lua_State *L,
 TileDef            read_tiledef              (lua_State *L, int index,
                                               u8 drawtype, bool special);
 
-void               read_soundspec            (lua_State *L, int index,
-                                              SimpleSoundSpec &spec);
+void               read_simplesoundspec      (lua_State *L, int index,
+                                              SoundSpec &spec);
 NodeBox            read_nodebox              (lua_State *L, int index);
 
 void               read_server_sound_params  (lua_State *L, int index,
@@ -167,8 +167,8 @@ std::vector<ItemStack> read_items            (lua_State *L,
                                               int index,
                                               IGameDef* gdef);
 
-void               push_soundspec            (lua_State *L,
-                                              const SimpleSoundSpec &spec);
+void               push_simplesoundspec      (lua_State *L,
+                                              const SoundSpec &spec);
 
 bool               string_to_enum            (const EnumString *spec,
                                               int &result,

+ 2 - 1
src/script/lua_api/CMakeLists.txt

@@ -28,10 +28,11 @@ set(common_SCRIPT_LUA_API_SRCS
 set(client_SCRIPT_LUA_API_SRCS
 	${CMAKE_CURRENT_SOURCE_DIR}/l_camera.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/l_client.cpp
+	${CMAKE_CURRENT_SOURCE_DIR}/l_client_sound.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/l_localplayer.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/l_mainmenu.cpp
+	${CMAKE_CURRENT_SOURCE_DIR}/l_mainmenu_sound.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/l_minimap.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/l_particles_local.cpp
-	${CMAKE_CURRENT_SOURCE_DIR}/l_sound.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/l_storage.cpp
 	PARENT_SCOPE)

+ 0 - 60
src/script/lua_api/l_client.cpp

@@ -260,63 +260,6 @@ int ModApiClient::l_get_meta(lua_State *L)
 	return 1;
 }
 
-// sound_play(spec, parameters)
-int ModApiClient::l_sound_play(lua_State *L)
-{
-	ISoundManager *sound = getClient(L)->getSoundManager();
-
-	SimpleSoundSpec spec;
-	read_soundspec(L, 1, spec);
-
-	SoundLocation type = SoundLocation::Local;
-	float gain = 1.0f;
-	v3f position;
-
-	if (lua_istable(L, 2)) {
-		getfloatfield(L, 2, "gain", gain);
-		getfloatfield(L, 2, "pitch", spec.pitch);
-		getboolfield(L, 2, "loop", spec.loop);
-
-		lua_getfield(L, 2, "pos");
-		if (!lua_isnil(L, -1)) {
-			position = read_v3f(L, -1) * BS;
-			type = SoundLocation::Position;
-			lua_pop(L, 1);
-		}
-	}
-
-	spec.gain *= gain;
-
-	s32 handle;
-	if (type == SoundLocation::Local)
-		handle = sound->playSound(spec);
-	else
-		handle = sound->playSoundAt(spec, position);
-
-	lua_pushinteger(L, handle);
-	return 1;
-}
-
-// sound_stop(handle)
-int ModApiClient::l_sound_stop(lua_State *L)
-{
-	s32 handle = luaL_checkinteger(L, 1);
-
-	getClient(L)->getSoundManager()->stopSound(handle);
-
-	return 0;
-}
-
-// sound_fade(handle, step, gain)
-int ModApiClient::l_sound_fade(lua_State *L)
-{
-	s32 handle = luaL_checkinteger(L, 1);
-	float step = readParam<float>(L, 2);
-	float gain = readParam<float>(L, 3);
-	getClient(L)->getSoundManager()->fadeSound(handle, step, gain);
-	return 0;
-}
-
 // get_server_info()
 int ModApiClient::l_get_server_info(lua_State *L)
 {
@@ -433,9 +376,6 @@ void ModApiClient::Initialize(lua_State *L, int top)
 	API_FCT(get_node_or_nil);
 	API_FCT(disconnect);
 	API_FCT(get_meta);
-	API_FCT(sound_play);
-	API_FCT(sound_stop);
-	API_FCT(sound_fade);
 	API_FCT(get_server_info);
 	API_FCT(get_item_def);
 	API_FCT(get_node_def);

+ 0 - 9
src/script/lua_api/l_client.h

@@ -78,15 +78,6 @@ private:
 	// get_meta(pos)
 	static int l_get_meta(lua_State *L);
 
-	// sound_play(spec, parameters)
-	static int l_sound_play(lua_State *L);
-
-	// sound_stop(handle)
-	static int l_sound_stop(lua_State *L);
-
-	// sound_fade(handle, step, gain)
-	static int l_sound_fade(lua_State *L);
-
 	// get_server_info()
 	static int l_get_server_info(lua_State *L);
 

+ 150 - 0
src/script/lua_api/l_client_sound.cpp

@@ -0,0 +1,150 @@
+/*
+Minetest
+Copyright (C) 2023 DS
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+Copyright (C) 2017 nerzhul, Loic Blot <loic.blot@unix-experience.fr>
+
+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; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "l_client_sound.h"
+#include "l_internal.h"
+#include "common/c_content.h"
+#include "common/c_converter.h"
+#include "client/client.h"
+#include "client/sound.h"
+
+/* ModApiClientSound */
+
+// sound_play(spec, parameters)
+int ModApiClientSound::l_sound_play(lua_State *L)
+{
+	ISoundManager *sound_manager = getClient(L)->getSoundManager();
+
+	SoundSpec spec;
+	read_simplesoundspec(L, 1, spec);
+
+	SoundLocation type = SoundLocation::Local;
+	float gain = 1.0f;
+	v3f position;
+
+	if (lua_istable(L, 2)) {
+		getfloatfield(L, 2, "gain", gain);
+		getfloatfield(L, 2, "pitch", spec.pitch);
+		getboolfield(L, 2, "loop", spec.loop);
+
+		lua_getfield(L, 2, "pos");
+		if (!lua_isnil(L, -1)) {
+			position = read_v3f(L, -1);
+			type = SoundLocation::Position;
+			lua_pop(L, 1);
+		}
+	}
+
+	spec.gain *= gain;
+
+	sound_handle_t handle = sound_manager->allocateId(2);
+
+	if (type == SoundLocation::Local)
+		sound_manager->playSound(handle, spec);
+	else
+		sound_manager->playSoundAt(handle, spec, position, v3f(0.0f));
+
+	ClientSoundHandle::create(L, handle);
+	return 1;
+}
+
+void ModApiClientSound::Initialize(lua_State *L, int top)
+{
+	API_FCT(sound_play);
+}
+
+/* ClientSoundHandle */
+
+ClientSoundHandle *ClientSoundHandle::checkobject(lua_State *L, int narg)
+{
+	luaL_checktype(L, narg, LUA_TUSERDATA);
+	void *ud = luaL_checkudata(L, narg, className);
+	if (!ud)
+		luaL_typerror(L, narg, className);
+	return *(ClientSoundHandle**)ud; // unbox pointer
+}
+
+int ClientSoundHandle::gc_object(lua_State *L)
+{
+	ClientSoundHandle *o = *(ClientSoundHandle **)(lua_touserdata(L, 1));
+	if (getClient(L) && getClient(L)->getSoundManager())
+		getClient(L)->getSoundManager()->freeId(o->m_handle);
+	delete o;
+	return 0;
+}
+
+// :stop()
+int ClientSoundHandle::l_stop(lua_State *L)
+{
+	ClientSoundHandle *o = checkobject(L, 1);
+	getClient(L)->getSoundManager()->stopSound(o->m_handle);
+	return 0;
+}
+
+// :fade(step, gain)
+int ClientSoundHandle::l_fade(lua_State *L)
+{
+	ClientSoundHandle *o = checkobject(L, 1);
+	float step = readParam<float>(L, 2);
+	float gain = readParam<float>(L, 3);
+	getClient(L)->getSoundManager()->fadeSound(o->m_handle, step, gain);
+	return 0;
+}
+
+void ClientSoundHandle::create(lua_State *L, sound_handle_t handle)
+{
+	ClientSoundHandle *o = new ClientSoundHandle(handle);
+	*(void **)(lua_newuserdata(L, sizeof(void *))) = o;
+	luaL_getmetatable(L, className);
+	lua_setmetatable(L, -2);
+}
+
+void ClientSoundHandle::Register(lua_State *L)
+{
+	lua_newtable(L);
+	int methodtable = lua_gettop(L);
+	luaL_newmetatable(L, className);
+	int metatable = lua_gettop(L);
+
+	lua_pushliteral(L, "__metatable");
+	lua_pushvalue(L, methodtable);
+	lua_settable(L, metatable);  // hide metatable from Lua getmetatable()
+
+	lua_pushliteral(L, "__index");
+	lua_pushvalue(L, methodtable);
+	lua_settable(L, metatable);
+
+	lua_pushliteral(L, "__gc");
+	lua_pushcfunction(L, gc_object);
+	lua_settable(L, metatable);
+
+	lua_pop(L, 1);  // drop metatable
+
+	luaL_register(L, nullptr, methods);  // fill methodtable
+	lua_pop(L, 1);  // drop methodtable
+}
+
+const char ClientSoundHandle::className[] = "ClientSoundHandle";
+const luaL_Reg ClientSoundHandle::methods[] = {
+	luamethod(ClientSoundHandle, stop),
+	luamethod(ClientSoundHandle, fade),
+	{0,0}
+};

+ 66 - 0
src/script/lua_api/l_client_sound.h

@@ -0,0 +1,66 @@
+/*
+Minetest
+Copyright (C) 2023 DS
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+Copyright (C) 2017 nerzhul, Loic Blot <loic.blot@unix-experience.fr>
+
+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; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#pragma once
+
+#include "lua_api/l_base.h"
+#include "util/basic_macros.h"
+
+using sound_handle_t = int;
+
+class ModApiClientSound : public ModApiBase
+{
+private:
+	// sound_play(spec, parameters)
+	static int l_sound_play(lua_State *L);
+
+public:
+	static void Initialize(lua_State *L, int top);
+};
+
+class ClientSoundHandle final : public ModApiBase
+{
+private:
+	sound_handle_t m_handle;
+
+	static const char className[];
+	static const luaL_Reg methods[];
+
+	ClientSoundHandle(sound_handle_t handle) : m_handle(handle) {}
+
+	DISABLE_CLASS_COPY(ClientSoundHandle)
+
+	static ClientSoundHandle *checkobject(lua_State *L, int narg);
+
+	static int gc_object(lua_State *L);
+
+	// :stop()
+	static int l_stop(lua_State *L);
+
+	// :fade(step, gain)
+	static int l_fade(lua_State *L);
+
+public:
+	~ClientSoundHandle() = default;
+
+	static void create(lua_State *L, sound_handle_t handle);
+	static void Register(lua_State *L);
+};

+ 116 - 0
src/script/lua_api/l_mainmenu_sound.cpp

@@ -0,0 +1,116 @@
+/*
+Minetest
+Copyright (C) 2023 DS
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+Copyright (C) 2017 nerzhul, Loic Blot <loic.blot@unix-experience.fr>
+
+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; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "l_mainmenu_sound.h"
+#include "l_internal.h"
+#include "common/c_content.h"
+#include "gui/guiEngine.h"
+
+/* ModApiMainMenuSound */
+
+// sound_play(spec, loop)
+int ModApiMainMenuSound::l_sound_play(lua_State *L)
+{
+	SoundSpec spec;
+	read_simplesoundspec(L, 1, spec);
+	spec.loop = readParam<bool>(L, 2);
+
+	ISoundManager &sound_manager = *getGuiEngine(L)->m_sound_manager;
+
+	sound_handle_t handle = sound_manager.allocateId(2);
+	sound_manager.playSound(handle, spec);
+
+	MainMenuSoundHandle::create(L, handle);
+
+	return 1;
+}
+
+void ModApiMainMenuSound::Initialize(lua_State *L, int top)
+{
+	API_FCT(sound_play);
+}
+
+/* MainMenuSoundHandle */
+
+MainMenuSoundHandle *MainMenuSoundHandle::checkobject(lua_State *L, int narg)
+{
+	luaL_checktype(L, narg, LUA_TUSERDATA);
+	void *ud = luaL_checkudata(L, narg, className);
+	if (!ud)
+		luaL_typerror(L, narg, className);
+	return *(MainMenuSoundHandle**)ud; // unbox pointer
+}
+
+int MainMenuSoundHandle::gc_object(lua_State *L)
+{
+	MainMenuSoundHandle *o = *(MainMenuSoundHandle **)(lua_touserdata(L, 1));
+	if (getGuiEngine(L) && getGuiEngine(L)->m_sound_manager)
+		getGuiEngine(L)->m_sound_manager->freeId(o->m_handle);
+	delete o;
+	return 0;
+}
+
+// :stop()
+int MainMenuSoundHandle::l_stop(lua_State *L)
+{
+	MainMenuSoundHandle *o = checkobject(L, 1);
+	getGuiEngine(L)->m_sound_manager->stopSound(o->m_handle);
+	return 0;
+}
+
+void MainMenuSoundHandle::create(lua_State *L, sound_handle_t handle)
+{
+	MainMenuSoundHandle *o = new MainMenuSoundHandle(handle);
+	*(void **)(lua_newuserdata(L, sizeof(void *))) = o;
+	luaL_getmetatable(L, className);
+	lua_setmetatable(L, -2);
+}
+
+void MainMenuSoundHandle::Register(lua_State *L)
+{
+	lua_newtable(L);
+	int methodtable = lua_gettop(L);
+	luaL_newmetatable(L, className);
+	int metatable = lua_gettop(L);
+
+	lua_pushliteral(L, "__metatable");
+	lua_pushvalue(L, methodtable);
+	lua_settable(L, metatable);  // hide metatable from Lua getmetatable()
+
+	lua_pushliteral(L, "__index");
+	lua_pushvalue(L, methodtable);
+	lua_settable(L, metatable);
+
+	lua_pushliteral(L, "__gc");
+	lua_pushcfunction(L, gc_object);
+	lua_settable(L, metatable);
+
+	lua_pop(L, 1);  // drop metatable
+
+	luaL_register(L, nullptr, methods);  // fill methodtable
+	lua_pop(L, 1);  // drop methodtable
+}
+
+const char MainMenuSoundHandle::className[] = "MainMenuSoundHandle";
+const luaL_Reg MainMenuSoundHandle::methods[] = {
+	luamethod(MainMenuSoundHandle, stop),
+	{0,0}
+};

+ 32 - 2
src/script/lua_api/l_sound.h → src/script/lua_api/l_mainmenu_sound.h

@@ -1,5 +1,6 @@
 /*
 Minetest
+Copyright (C) 2023 DS
 Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
 Copyright (C) 2017 nerzhul, Loic Blot <loic.blot@unix-experience.fr>
 
@@ -21,13 +22,42 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #pragma once
 
 #include "lua_api/l_base.h"
+#include "util/basic_macros.h"
 
-class ModApiSound : public ModApiBase
+using sound_handle_t = int;
+
+class ModApiMainMenuSound : public ModApiBase
 {
 private:
+	// sound_play(spec, loop)
 	static int l_sound_play(lua_State *L);
-	static int l_sound_stop(lua_State *L);
 
 public:
 	static void Initialize(lua_State *L, int top);
 };
+
+class MainMenuSoundHandle final : public ModApiBase
+{
+private:
+	sound_handle_t m_handle;
+
+	static const char className[];
+	static const luaL_Reg methods[];
+
+	MainMenuSoundHandle(sound_handle_t handle) : m_handle(handle) {}
+
+	DISABLE_CLASS_COPY(MainMenuSoundHandle)
+
+	static MainMenuSoundHandle *checkobject(lua_State *L, int narg);
+
+	static int gc_object(lua_State *L);
+
+	// :stop()
+	static int l_stop(lua_State *L);
+
+public:
+	~MainMenuSoundHandle() = default;
+
+	static void create(lua_State *L, sound_handle_t handle);
+	static void Register(lua_State *L);
+};

+ 1 - 1
src/script/lua_api/l_server.cpp

@@ -503,7 +503,7 @@ int ModApiServer::l_sound_play(lua_State *L)
 {
 	NO_MAP_LOCK_REQUIRED;
 	ServerPlayingSound params;
-	read_soundspec(L, 1, params.spec);
+	read_simplesoundspec(L, 1, params.spec);
 	read_server_sound_params(L, 2, params);
 	bool ephemeral = lua_gettop(L) > 2 && readParam<bool>(L, 3);
 	if (ephemeral) {

+ 0 - 53
src/script/lua_api/l_sound.cpp

@@ -1,53 +0,0 @@
-/*
-Minetest
-Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
-Copyright (C) 2017 nerzhul, Loic Blot <loic.blot@unix-experience.fr>
-
-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; if not, write to the Free Software Foundation, Inc.,
-51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-*/
-
-#include "l_sound.h"
-#include "l_internal.h"
-#include "common/c_content.h"
-#include "gui/guiEngine.h"
-
-
-int ModApiSound::l_sound_play(lua_State *L)
-{
-	SimpleSoundSpec spec;
-	read_soundspec(L, 1, spec);
-	spec.loop = readParam<bool>(L, 2);
-
-	s32 handle = getGuiEngine(L)->playSound(spec);
-
-	lua_pushinteger(L, handle);
-
-	return 1;
-}
-
-int ModApiSound::l_sound_stop(lua_State *L)
-{
-	u32 handle = luaL_checkinteger(L, 1);
-
-	getGuiEngine(L)->stopSound(handle);
-
-	return 1;
-}
-
-void ModApiSound::Initialize(lua_State *L, int top)
-{
-	API_FCT(sound_play);
-	API_FCT(sound_stop);
-}

+ 3 - 1
src/script/scripting_client.cpp

@@ -29,13 +29,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "lua_api/l_modchannels.h"
 #include "lua_api/l_particles_local.h"
 #include "lua_api/l_storage.h"
-#include "lua_api/l_sound.h"
 #include "lua_api/l_util.h"
 #include "lua_api/l_item.h"
 #include "lua_api/l_nodemeta.h"
 #include "lua_api/l_localplayer.h"
 #include "lua_api/l_camera.h"
 #include "lua_api/l_settings.h"
+#include "lua_api/l_client_sound.h"
 
 ClientScripting::ClientScripting(Client *client):
 	ScriptApiBase(ScriptingType::Client)
@@ -75,6 +75,7 @@ void ClientScripting::InitializeModApi(lua_State *L, int top)
 	LuaCamera::Register(L);
 	ModChannelRef::Register(L);
 	LuaSettings::Register(L);
+	ClientSoundHandle::Register(L);
 
 	ModApiUtil::InitializeClient(L, top);
 	ModApiClient::Initialize(L, top);
@@ -83,6 +84,7 @@ void ClientScripting::InitializeModApi(lua_State *L, int top)
 	ModApiEnvMod::InitializeClient(L, top);
 	ModApiChannels::Initialize(L, top);
 	ModApiParticlesLocal::Initialize(L, top);
+	ModApiClientSound::Initialize(L, top);
 }
 
 void ClientScripting::on_client_ready(LocalPlayer *localplayer)

+ 3 - 2
src/script/scripting_mainmenu.cpp

@@ -23,7 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "lua_api/l_base.h"
 #include "lua_api/l_http.h"
 #include "lua_api/l_mainmenu.h"
-#include "lua_api/l_sound.h"
+#include "lua_api/l_mainmenu_sound.h"
 #include "lua_api/l_util.h"
 #include "lua_api/l_settings.h"
 #include "log.h"
@@ -66,7 +66,7 @@ void MainMenuScripting::initializeModApi(lua_State *L, int top)
 	// Initialize mod API modules
 	ModApiMainMenu::Initialize(L, top);
 	ModApiUtil::Initialize(L, top);
-	ModApiSound::Initialize(L, top);
+	ModApiMainMenuSound::Initialize(L, top);
 	ModApiHttp::Initialize(L, top);
 
 	asyncEngine.registerStateInitializer(registerLuaClasses);
@@ -83,6 +83,7 @@ void MainMenuScripting::initializeModApi(lua_State *L, int top)
 void MainMenuScripting::registerLuaClasses(lua_State *L, int top)
 {
 	LuaSettings::Register(L);
+	MainMenuSoundHandle::Register(L);
 }
 
 /******************************************************************************/

+ 1 - 1
src/server.cpp

@@ -2231,7 +2231,7 @@ s32 Server::playSound(ServerPlayingSound &params, bool ephemeral)
 	pkt << id << params.spec.name << gain
 			<< (u8) params.type << pos << params.object
 			<< params.spec.loop << params.spec.fade << params.spec.pitch
-			<< ephemeral;
+			<< ephemeral << params.spec.start_time;
 
 	bool as_reliable = !ephemeral;
 

+ 3 - 3
src/server.h

@@ -62,7 +62,7 @@ struct RollbackAction;
 class EmergeManager;
 class ServerScripting;
 class ServerEnvironment;
-struct SimpleSoundSpec;
+struct SoundSpec;
 struct CloudParams;
 struct SkyboxParams;
 struct SunParams;
@@ -97,7 +97,7 @@ struct MediaInfo
 	}
 };
 
-// Combines the pure sound (SimpleSoundSpec) with positional information
+// Combines the pure sound (SoundSpec) with positional information
 struct ServerPlayingSound
 {
 	SoundLocation type = SoundLocation::Local;
@@ -111,7 +111,7 @@ struct ServerPlayingSound
 
 	v3f getPos(ServerEnvironment *env, bool *pos_exists) const;
 
-	SimpleSoundSpec spec;
+	SoundSpec spec;
 
 	std::unordered_set<session_t> clients; // peer ids
 };

+ 26 - 9
src/sound.h

@@ -24,20 +24,29 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "util/serialize.h"
 #include "irrlichttypes_bloated.h"
 
-// This class describes the basic sound information for playback.
-// Positional handling is done separately.
-
-struct SimpleSoundSpec
+/**
+ * Describes the sound information for playback.
+ * Positional handling is done separately.
+ *
+ * `SimpleSoundSpec`, as used by modding, is a `SoundSpec` with only name, fain,
+ * pitch and fade.
+*/
+struct SoundSpec
 {
-	SimpleSoundSpec(const std::string &name = "", float gain = 1.0f,
-			bool loop = false, float fade = 0.0f, float pitch = 1.0f) :
-			name(name), gain(gain), fade(fade), pitch(pitch), loop(loop)
+	SoundSpec(const std::string &name = "", float gain = 1.0f,
+			bool loop = false, float fade = 0.0f, float pitch = 1.0f,
+			float start_time = 0.0f) :
+			name(name), gain(gain), fade(fade), pitch(pitch), start_time(start_time),
+			loop(loop)
 	{
 	}
 
 	bool exists() const { return !name.empty(); }
 
-	void serialize(std::ostream &os, u16 protocol_version) const
+	/**
+	 * Serialize a `SimpleSoundSpec`.
+	 */
+	void serializeSimple(std::ostream &os, u16 protocol_version) const
 	{
 		os << serializeString16(name);
 		writeF32(os, gain);
@@ -45,7 +54,10 @@ struct SimpleSoundSpec
 		writeF32(os, fade);
 	}
 
-	void deSerialize(std::istream &is, u16 protocol_version)
+	/**
+	 * Deserialize a `SimpleSoundSpec`.
+	 */
+	void deSerializeSimple(std::istream &is, u16 protocol_version)
 	{
 		name = deSerializeString16(is);
 		gain = readF32(is);
@@ -53,11 +65,16 @@ struct SimpleSoundSpec
 		fade = readF32(is);
 	}
 
+	// Name of the sound-group
 	std::string name;
 	float gain = 1.0f;
 	float fade = 0.0f;
 	float pitch = 1.0f;
+	float start_time = 0.0f;
 	bool loop = false;
+	// If true, a local fallback (ie. from the user's sound pack) is used if the
+	// sound-group does not exist.
+	bool use_local_fallback = true;
 };