diff --git a/LICENSE.txt b/LICENSE.txt index 55dd03a79..d7316a0bf 100644 --- a/LICENSE.txt +++ b/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 diff --git a/builtin/client/misc.lua b/builtin/client/misc.lua index 80e0f2904..ed2c869ff 100644 --- a/builtin/client/misc.lua +++ b/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 diff --git a/builtin/mainmenu/init.lua b/builtin/mainmenu/init.lua index add94e420..ede90ffa7 100644 --- a/builtin/mainmenu/init.lua +++ b/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") diff --git a/builtin/mainmenu/misc.lua b/builtin/mainmenu/misc.lua new file mode 100644 index 000000000..0677e96a3 --- /dev/null +++ b/builtin/mainmenu/misc.lua @@ -0,0 +1,6 @@ + +-- old non-method sound function + +function core.sound_stop(handle, ...) + return handle:stop(...) +end diff --git a/doc/client_lua_api.md b/doc/client_lua_api.md index 70fae582d..0af102b10 100644 --- a/doc/client_lua_api.md +++ b/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 diff --git a/doc/lua_api.md b/doc/lua_api.md index d1838975e..b2ae1e8d3 100644 --- a/doc/lua_api.md +++ b/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 +----------- -When playing the sound `foomod_foosound`, the sound is chosen randomly +A sound group is the set of all sound files, whose filenames are of the following +format: +`[.].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. + +Example: When playing the sound `foomod_foosound`, the sound is chosen randomly from the available ones of the following files: * `foomod_foosound.ogg` @@ -1012,62 +1017,10 @@ from the available ones of the following files: * (...) * `foomod_foosound.9.ogg` -Examples of sound parameter tables: - -```lua --- Play locationless on all clients -{ - gain = 1.0, -- default - fade = 0.0, -- default, change to a value > 0 to fade the sound in - 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 - pitch = 1.0, -- default -} --- Play locationless to one player, looped -{ - to_player = name, - gain = 1.0, -- default - loop = true, -} --- Play at a location -{ - pos = {x = 1, y = 2, z = 3}, - gain = 1.0, -- default - max_hear_distance = 32, -- default, uses a Euclidean metric -} --- Play connected to an object, looped -{ - object = , - gain = 1.0, -- default - max_hear_distance = 32, -- default, uses a Euclidean metric - loop = true, -} --- Play at a location, heard by anyone *but* the given player -{ - pos = {x = 32, y = 0, z = 100}, - max_hear_distance = 40, - exclude_player = name, -} -``` - -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. +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 @@ -1075,11 +1028,25 @@ 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%) +* `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` and `pitch` are optional and default to `1.0`. +`gain`, `pitch` and `fade` are optional and default to `1.0`, `1.0` and `0.0`. Examples: @@ -1090,10 +1057,105 @@ Examples: * `{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 -------------------- +Sound parameter table +--------------------- -These sound files are played back by the engine if provided. +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 = , + -- 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 + pitch = 1.0, -- default +} +-- Play locationless to one player +{ + to_player = name, + gain = 1.0, -- default + fade = 0.0, -- default + pitch = 1.0, -- default +} +-- Play locationless to one player, looped +{ + to_player = name, + gain = 1.0, -- default + loop = true, +} +-- 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 + start_time = 5.0, +} +-- Play connected to an object, looped +{ + object = , + gain = 1.0, -- default + max_hear_distance = 32, -- default + loop = true, +} +-- Play at a location, heard by anyone *but* the given player +{ + pos = {x = 32, y = 0, z = 100}, + max_hear_distance = 40, + exclude_player = name, +} +``` + +Special sound-groups +-------------------- + +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 = , -- 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 = or "__group", -- While digging node. diff --git a/doc/menu_lua_api.md b/doc/menu_lua_api.md index 480df7027..963306c6d 100644 --- a/doc/menu_lua_api.md +++ b/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 diff --git a/games/devtest/mods/soundstuff/init.lua b/games/devtest/mods/soundstuff/init.lua index b878f82a0..31a871cad 100644 --- a/games/devtest/mods/soundstuff/init.lua +++ b/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") diff --git a/games/devtest/mods/soundstuff/jukebox.lua b/games/devtest/mods/soundstuff/jukebox.lua index ecba5baa1..298697edf 100644 --- a/games/devtest/mods/soundstuff/jukebox.lua +++ b/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 "", sparam.to_player, sparam.exclude_player, + sparam.gain, sparam.pitch, sparam.fade, sparam.start_time, + sparam.loop, sparam.pos, sparam.object and "", + sparam.to_player, sparam.exclude_player, sparam.max_hear_distance), tostring(ephemeral))) diff --git a/games/devtest/mods/soundstuff/racecar.lua b/games/devtest/mods/soundstuff/racecar.lua new file mode 100644 index 000000000..e7eda6d2e --- /dev/null +++ b/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, +}) diff --git a/games/devtest/mods/soundstuff/sounds/soundstuff_sinus.ogg b/games/devtest/mods/soundstuff/sounds/soundstuff_sinus.ogg new file mode 100644 index 000000000..8dbc00a9a Binary files /dev/null and b/games/devtest/mods/soundstuff/sounds/soundstuff_sinus.ogg differ diff --git a/games/devtest/mods/soundstuff/textures/soundstuff_racecar.png b/games/devtest/mods/soundstuff/textures/soundstuff_racecar.png new file mode 100644 index 000000000..8e8ff5ac7 Binary files /dev/null and b/games/devtest/mods/soundstuff/textures/soundstuff_racecar.png differ diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 7b910d027..2e934ba29 100644 --- a/src/client/CMakeLists.txt +++ b/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} diff --git a/src/client/camera.cpp b/src/client/camera.cpp index 2d8648f52..0871f30d5 100644 --- a/src/client/camera.cpp +++ b/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" diff --git a/src/client/client.cpp b/src/client/client.cpp index 5a9112cf0..e6f855cd6 100644 --- a/src/client/client.cpp +++ b/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 removed_client_ids = m_sound->pollRemovedSounds(); std::vector removed_server_ids; - for (std::unordered_map::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 &blocks) Send(&pkt); } -void Client::sendRemovedSounds(std::vector &soundList) +void Client::sendRemovedSounds(const std::vector &soundList) { size_t server_ids = soundList.size(); assert(server_ids <= 0xFFFF); diff --git a/src/client/client.h b/src/client/client.h index eb965b994..1bec65279 100644 --- a/src/client/client.h +++ b/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 &blocks); void sendGotBlocks(const std::vector &blocks); - void sendRemovedSounds(std::vector &soundList); + void sendRemovedSounds(const std::vector &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 m_sounds_server_to_client; + std::unordered_map m_sounds_server_to_client; // And the other way! - std::unordered_map m_sounds_client_to_server; + // This takes ownership for the sound handles. + std::unordered_map m_sounds_client_to_server; // Relation of client id to object id - std::unordered_map m_sounds_to_objects; + std::unordered_map m_sounds_to_objects; // Privileges std::unordered_set m_privileges; diff --git a/src/client/clientobject.h b/src/client/clientobject.h index b192f0dcd..af7a5eb9c 100644 --- a/src/client/clientobject.h +++ b/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 diff --git a/src/client/content_cao.cpp b/src/client/content_cao.cpp index 76a0b63dc..17ff4d150 100644 --- a/src/client/content_cao.cpp +++ b/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)); } } } diff --git a/src/client/content_cao.h b/src/client/content_cao.h index 5a8116c71..69c7cfe08 100644 --- a/src/client/content_cao.h +++ b/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; } diff --git a/src/client/game.cpp b/src/client/game.cpp index 6489d0791..48ec15d91 100644 --- a/src/client/game.cpp +++ b/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; + SoundSpec m_player_leftpunch_sound2; + SoundSpec 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) - { - } + 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 m_fetched; -private: - void paths_insert(std::set &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 &dst_paths, - std::set &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 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()); 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 diff --git a/src/client/sound.cpp b/src/client/sound.cpp new file mode 100644 index 000000000..b4b073242 --- /dev/null +++ b/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 +#include +#include + +std::vector SoundFallbackPathProvider:: + getLocalFallbackPathsForSoundname(const std::string &name) +{ + std::vector 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 &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 &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( + myrand() % static_cast(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; +} diff --git a/src/client/sound.h b/src/client/sound.h index 4de63ec65..ad5dbf649 100644 --- a/src/client/sound.h +++ b/src/client/sound.h @@ -19,72 +19,168 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include -#include #include "irr_v3d.h" -#include "../sound.h" +#include +#include +#include +#include +#include -class OnDemandSoundFetcher +struct SoundSpec; + +class SoundFallbackPathProvider { public: - virtual void fetchSounds(const std::string &name, - std::set &dst_paths, - std::set &dst_datas) = 0; + virtual ~SoundFallbackPathProvider() = default; + std::vector getLocalFallbackPathsForSoundname(const std::string &name); +protected: + virtual void addThePaths(const std::string &name, std::vector &paths); + // adds .ogg, .1.ogg, ..., .9.ogg to paths + void addAllAlternatives(const std::string &common, std::vector &paths); +private: + std::unordered_set 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::max(); + class ISoundManager { +private: + std::unordered_map m_occupied_ids; + sound_handle_t m_next_id = 1; + std::vector 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; + /** + * 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; - virtual void updateListener( - const v3f &pos, const v3f &vel, const v3f &at, const v3f &up) = 0; - virtual void setListenerGain(float gain) = 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; - // 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; + /** + * 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 pollRemovedSounds() + { + std::vector 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) {} + void step(f32 dtime) override {} + void pauseAll() override {} + void resumeAll() override {} - 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 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 {} }; diff --git a/src/client/sound_openal.cpp b/src/client/sound_openal.cpp index e84c62349..9baa0d152 100644 --- a/src/client/sound_openal.cpp +++ b/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 - #include - //#include -#elif defined(__APPLE__) - #define OPENAL_DEPRECATED - #include - #include - //#include -#else - #include - #include - #include -#endif -#include -#include -#include -#include "log.h" -#include "util/numeric.h" // myrand() -#include "porting.h" -#include -#include -#include -#include - -#define BUFFER_SIZE 30000 +#include "sound_openal_internal.h" std::shared_ptr g_sound_manager_singleton; -typedef std::unique_ptr unique_ptr_alcdevice; -typedef std::unique_ptr 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 ""; - } -} - -static ALenum warn_if_error(ALenum err, const char *desc) -{ - if(err == AL_NO_ERROR) - return err; - warningstream< 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> m_buffers; - std::unordered_map 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 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 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>::iterator i = - m_buffers.find(name); - if(i != m_buffers.end()){ - i->second.push_back(buf); - return; - } - std::vector bufs; - bufs.push_back(buf); - m_buffers[name] = std::move(bufs); - } - - SoundBuffer* getBuffer(const std::string &name) - { - std::unordered_map>::iterator i = - m_buffers.find(name); - if(i == m_buffers.end()) - return nullptr; - std::vector &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 paths; - std::set 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"< 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 " - < 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 createSoundManagerSingleton() { auto smg = std::make_shared(); @@ -728,7 +35,8 @@ std::shared_ptr createSoundManagerSingleton() return smg; } -ISoundManager *createOpenALSoundManager(SoundManagerSingleton *smg, OnDemandSoundFetcher *fetcher) +std::unique_ptr createOpenALSoundManager(SoundManagerSingleton *smg, + std::unique_ptr fallback_path_provider) { - return new OpenALSoundManager(smg, fetcher); + return std::make_unique(smg, std::move(fallback_path_provider)); }; diff --git a/src/client/sound_openal.h b/src/client/sound_openal.h index f04ad7cac..50762331b 100644 --- a/src/client/sound_openal.h +++ b/src/client/sound_openal.h @@ -19,13 +19,14 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -#include - #include "sound.h" +#include + class SoundManagerSingleton; extern std::shared_ptr g_sound_manager_singleton; std::shared_ptr createSoundManagerSingleton(); -ISoundManager *createOpenALSoundManager( - SoundManagerSingleton *smg, OnDemandSoundFetcher *fetcher); +std::unique_ptr createOpenALSoundManager( + SoundManagerSingleton *smg, + std::unique_ptr fallback_path_provider); diff --git a/src/client/sound_openal_internal.cpp b/src/client/sound_openal_internal.cpp new file mode 100644 index 000000000..26890a074 --- /dev/null +++ b/src/client/sound_openal_internal.cpp @@ -0,0 +1,1125 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +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 +#include + +/* + * 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 ""; + } +} + +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(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 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(ov_pcm_total(&m_file, -1)); + ret.length_seconds = static_cast(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(pcm_end - pcm_start) + * decode_info.bytes_per_sample; + + std::unique_ptr 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::fromOggFile(std::unique_ptr oggfile, + const std::string &filename_for_logging) +{ + // Get some information about the OGG file + std::optional 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(std::move(oggfile), *decode_info); + } else { + return std::make_shared(std::move(oggfile), *decode_info); + } +} + +/* + * SoundDataUnopenBuffer struct + */ + +std::shared_ptr SoundDataUnopenBuffer::open(const std::string &sound_name) && +{ + // load from m_buffer + + auto oggfile = std::make_unique(); + + auto buffer_source = std::make_unique(); + 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 SoundDataUnopenFile::open(const std::string &sound_name) && +{ + // load from file at m_path + + auto oggfile = std::make_unique(); + + 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 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 oggfile, + const OggFileDecodeInfo &decode_info) : + ISoundDataOpen(decode_info), m_oggfile(std::move(oggfile)) +{ + // do nothing here. buffers are loaded at getOrLoadBufferAt +} + +std::tuple 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 &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 SoundDataOpenStream::loadBufferAt(ALuint offset, + std::vector::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 data, + bool loop, f32 volume, f32 pitch, f32 start_time, + const std::optional> &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 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 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 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 unopn_snd = std::move(it_unopen->second); + m_sound_datas_unopen.erase(it_unopen); + + // open + std::shared_ptr 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 &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 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 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 OpenALSoundManager::createPlayingSound( + const std::string &sound_name, bool loop, f32 volume, f32 pitch, + f32 start_time, const std::optional> &pos_vel_opt) +{ + infostream << "OpenALSoundManager: Creating playing sound \"" << sound_name + << "\"" << std::endl; + warn_if_al_error("before createPlayingSound"); + + std::shared_ptr 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(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> &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 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 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(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(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{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> 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); +} diff --git a/src/client/sound_openal_internal.h b/src/client/sound_openal_internal.h new file mode 100644 index 000000000..7fcb73a34 --- /dev/null +++ b/src/client/sound_openal_internal.h @@ -0,0 +1,613 @@ +/* +Minetest +Copyright (C) 2022 DS +Copyright (C) 2013 celeron55, Perttu Ahola +OpenAL support based on work by: +Copyright (C) 2011 Sebastian 'Bahamada' Rühl +Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits +Copyright (C) 2011 Giuseppe Bilotta + +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 + #include + //#include +#elif defined(__APPLE__) + #define OPENAL_DEPRECATED + #include + #include + //#include +#else + #include + #include + #include +#endif +#include + +#include +#include +#include +#include + + +/* + * + * 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 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; + using unique_ptr_alccontext = std::unique_ptr; + + 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 getOrLoadBufferAt(ALuint offset) = 0; + + static std::shared_ptr fromOggFile(std::unique_ptr 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 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 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 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 oggfile, + const OggFileDecodeInfo &decode_info); + + bool isStreaming() const noexcept override { return false; } + + std::tuple 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 m_buffers; + }; + + std::unique_ptr m_oggfile; + // A sorted vector of non-overlapping, non-contiguous `ContiguousBuffers`s. + std::vector m_bufferss; + + SoundDataOpenStream(std::unique_ptr oggfile, + const OggFileDecodeInfo &decode_info); + + bool isStreaming() const noexcept override { return true; } + + std::tuple 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 loadBufferAt(ALuint offset, + std::vector::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 m_data; + ALuint m_next_sample_pos = 0; + bool m_looping; + bool m_is_positional; + bool m_stopped_means_dead = true; + std::optional m_fade_state = std::nullopt; + +public: + PlayingSound(ALuint source_id, std::shared_ptr data, bool loop, + f32 volume, f32 pitch, f32 start_time, + const std::optional> &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 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> m_sound_datas_unopen; + std::unordered_map> m_sound_datas_open; + // sound groups + std::unordered_map> m_sound_groups; + + // currently playing sounds + std::unordered_map> m_sounds_playing; + + // streamed sounds + std::vector> m_sounds_streaming_current_bigstep; + std::vector> m_sounds_streaming_next_bigstep; + // time left until current bigstep finishes + f32 m_stream_timer = STREAM_BIGSTEP_TIME; + + std::vector> 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 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 createPlayingSound(const std::string &sound_name, + bool loop, f32 volume, f32 pitch, f32 start_time, + const std::optional> &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> &pos_vel_opt); + + /** + * Deletes sounds that are dead (=finished). + * + * @return Number of removed sounds. + */ + int removeDeadSounds(); + +public: + OpenALSoundManager(SoundManagerSingleton *smg, + std::unique_ptr 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; +}; diff --git a/src/gui/guiEngine.cpp b/src/gui/guiEngine.cpp index d08c6e37e..96085ce22 100644 --- a/src/gui/guiEngine.cpp +++ b/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 &dst_paths, - std::set &dst_datas) +void MenuMusicFetcher::addThePaths(const std::string &name, + std::vector &paths) { - if(m_fetched.count(name)) - return; - m_fetched.insert(name); - std::vector 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()); + } #endif if (!m_sound_manager) m_sound_manager = std::make_unique(); @@ -318,11 +306,12 @@ void GUIEngine::run() /******************************************************************************/ GUIEngine::~GUIEngine() { - m_sound_manager.reset(); - - infostream<<"GUIEngine: Deinitializing scripting"<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); -} diff --git a/src/gui/guiEngine.h b/src/gui/guiEngine.h index ae8ed142f..cb8a97942 100644 --- a/src/gui/guiEngine.h +++ b/src/gui/guiEngine.h @@ -115,30 +115,20 @@ private: std::vector 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 &dst_paths, - std::set &dst_datas); - -private: - /** set of fetched sound names */ - std::set m_fetched; +protected: + void addThePaths(const std::string &name, + std::vector &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 m_texture_source; - /** sound fetcher, used by sound manager*/ - MenuMusicFetcher m_soundfetcher{}; /** sound manager*/ std::unique_ptr 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); - - }; diff --git a/src/gui/guiFormSpecMenu.cpp b/src/gui/guiFormSpecMenu.cpp index 9b9783b8a..cea84fd80 100644 --- a/src/gui/guiFormSpecMenu.cpp +++ b/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; diff --git a/src/itemdef.cpp b/src/itemdef.cpp index 571b17a02..ae252c4a0 100644 --- a/src/itemdef.cpp +++ b/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) {}; } diff --git a/src/itemdef.h b/src/itemdef.h index 556e96b4f..bad3a3e24 100644 --- a/src/itemdef.h +++ b/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. diff --git a/src/network/clientpackethandler.cpp b/src/network/clientpackethandler.cpp index 48524553e..bba9c198b 100644 --- a/src/network/clientpackethandler.cpp +++ b/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; } diff --git a/src/network/networkprotocol.h b/src/network/networkprotocol.h index d5bc47711..c9e29cf12 100644 --- a/src/network/networkprotocol.h +++ b/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, diff --git a/src/nodedef.cpp b/src/nodedef.cpp index 383edae36..fef55e7df 100644 --- a/src/nodedef.cpp +++ b/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); diff --git a/src/nodedef.h b/src/nodedef.h index 53f934ec0..05ba10266 100644 --- a/src/nodedef.h +++ b/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 --- diff --git a/src/player.h b/src/player.h index 7c8077d38..9fdaf6ff5 100644 --- a/src/player.h +++ b/src/player.h @@ -146,11 +146,13 @@ public: std::vector *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 }; diff --git a/src/script/common/c_content.cpp b/src/script/common/c_content.cpp index 299975100..89bf609b2 100644 --- a/src/script/common/c_content.cpp +++ b/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()); diff --git a/src/script/common/c_content.h b/src/script/common/c_content.h index 1f8b973b5..af08dfda2 100644 --- a/src/script/common/c_content.h +++ b/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 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, diff --git a/src/script/lua_api/CMakeLists.txt b/src/script/lua_api/CMakeLists.txt index 32f6a2793..d9405e4fe 100644 --- a/src/script/lua_api/CMakeLists.txt +++ b/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) diff --git a/src/script/lua_api/l_client.cpp b/src/script/lua_api/l_client.cpp index b7112f4f4..0f148070b 100644 --- a/src/script/lua_api/l_client.cpp +++ b/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(L, 2); - float gain = readParam(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); diff --git a/src/script/lua_api/l_client.h b/src/script/lua_api/l_client.h index 5dc3efdad..7eb43e913 100644 --- a/src/script/lua_api/l_client.h +++ b/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); diff --git a/src/script/lua_api/l_client_sound.cpp b/src/script/lua_api/l_client_sound.cpp new file mode 100644 index 000000000..6e7717d80 --- /dev/null +++ b/src/script/lua_api/l_client_sound.cpp @@ -0,0 +1,150 @@ +/* +Minetest +Copyright (C) 2023 DS +Copyright (C) 2013 celeron55, Perttu Ahola +Copyright (C) 2017 nerzhul, Loic Blot + +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(L, 2); + float gain = readParam(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} +}; diff --git a/src/script/lua_api/l_client_sound.h b/src/script/lua_api/l_client_sound.h new file mode 100644 index 000000000..c4ebe99c4 --- /dev/null +++ b/src/script/lua_api/l_client_sound.h @@ -0,0 +1,66 @@ +/* +Minetest +Copyright (C) 2023 DS +Copyright (C) 2013 celeron55, Perttu Ahola +Copyright (C) 2017 nerzhul, Loic Blot + +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); +}; diff --git a/src/script/lua_api/l_mainmenu_sound.cpp b/src/script/lua_api/l_mainmenu_sound.cpp new file mode 100644 index 000000000..dce7c7b2f --- /dev/null +++ b/src/script/lua_api/l_mainmenu_sound.cpp @@ -0,0 +1,116 @@ +/* +Minetest +Copyright (C) 2023 DS +Copyright (C) 2013 celeron55, Perttu Ahola +Copyright (C) 2017 nerzhul, Loic Blot + +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(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} +}; diff --git a/src/script/lua_api/l_sound.h b/src/script/lua_api/l_mainmenu_sound.h similarity index 58% rename from src/script/lua_api/l_sound.h rename to src/script/lua_api/l_mainmenu_sound.h index 888a0f30b..a7cedf5b4 100644 --- a/src/script/lua_api/l_sound.h +++ b/src/script/lua_api/l_mainmenu_sound.h @@ -1,5 +1,6 @@ /* Minetest +Copyright (C) 2023 DS Copyright (C) 2013 celeron55, Perttu Ahola Copyright (C) 2017 nerzhul, Loic Blot @@ -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); +}; diff --git a/src/script/lua_api/l_server.cpp b/src/script/lua_api/l_server.cpp index 67916e074..12e5a1a5d 100644 --- a/src/script/lua_api/l_server.cpp +++ b/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(L, 3); if (ephemeral) { diff --git a/src/script/lua_api/l_sound.cpp b/src/script/lua_api/l_sound.cpp deleted file mode 100644 index 934b4a07e..000000000 --- a/src/script/lua_api/l_sound.cpp +++ /dev/null @@ -1,53 +0,0 @@ -/* -Minetest -Copyright (C) 2013 celeron55, Perttu Ahola -Copyright (C) 2017 nerzhul, Loic Blot - -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(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); -} diff --git a/src/script/scripting_client.cpp b/src/script/scripting_client.cpp index be3bdc2c8..6b3f9512d 100644 --- a/src/script/scripting_client.cpp +++ b/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) diff --git a/src/script/scripting_mainmenu.cpp b/src/script/scripting_mainmenu.cpp index 2a0cadb23..d88082b7d 100644 --- a/src/script/scripting_mainmenu.cpp +++ b/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); } /******************************************************************************/ diff --git a/src/server.cpp b/src/server.cpp index e8c3daaac..9e6685909 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -2231,7 +2231,7 @@ s32 Server::playSound(ServerPlayingSound ¶ms, 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; diff --git a/src/server.h b/src/server.h index e8865fbef..75d10b3a5 100644 --- a/src/server.h +++ b/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 clients; // peer ids }; diff --git a/src/sound.h b/src/sound.h index 801c552a9..5a593e6d0 100644 --- a/src/sound.h +++ b/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; };