Sound refactor and improvements (#12764)

This commit is contained in:
DS 2023-06-16 20:15:21 +02:00 committed by GitHub
parent 8e1af25738
commit edcbfa31c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 2802 additions and 1211 deletions

@ -83,6 +83,8 @@ SmallJoker:
DS: DS:
games/devtest/mods/soundstuff/textures/soundstuff_bigfoot.png games/devtest/mods/soundstuff/textures/soundstuff_bigfoot.png
games/devtest/mods/soundstuff/textures/soundstuff_jukebox.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 games/devtest/mods/testtools/textures/testtools_branding_iron.png
License of Minetest source code License of Minetest source code

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

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

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

@ -754,9 +754,9 @@ Call these functions only at load time!
* `minetest.sound_play(spec, parameters)`: returns a handle * `minetest.sound_play(spec, parameters)`: returns a handle
* `spec` is a `SimpleSoundSpec` * `spec` is a `SimpleSoundSpec`
* `parameters` is a sound parameter table * `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` * `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` * `handle` is a handle returned by `minetest.sound_play`
* `step` determines how fast a sound will fade. * `step` determines how fast a sound will fade.
Negative step will lower the sound volume, positive step will increase Negative step will lower the sound volume, positive step will increase

@ -994,16 +994,21 @@ Only Ogg Vorbis files are supported.
For positional playing of sounds, only single-channel (mono) files are For positional playing of sounds, only single-channel (mono) files are
supported. Otherwise OpenAL will play them non-positionally. 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: the mod name "`foomod`", a sound could be called:
foomod_foosound.ogg foomod_foosound.ogg
Sounds are referred to by their name with a dot, a single digit and the Sound group
file extension stripped out. When a sound is played, the actual sound file -----------
is chosen randomly from the matching sounds.
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:
`<sound-group name>[.<single digit>].ogg`
When a sound-group is played, one the files in the group is chosen at random.
Sound files can only be referred to by their sound-group name.
Example: When playing the sound `foomod_foosound`, the sound is chosen randomly
from the available ones of the following files: from the available ones of the following files:
* `foomod_foosound.ogg` * `foomod_foosound.ogg`
@ -1012,62 +1017,10 @@ from the available ones of the following files:
* (...) * (...)
* `foomod_foosound.9.ogg` * `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 = <an ObjectRef>,
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` `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. This is either a string or a table.
In string form, you just specify the sound name or 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: Table form has the following fields:
* `name`: Sound name * `name`:
* `gain`: Volume (`1.0` = 100%) Sound-group name.
* `pitch`: Pitch (`1.0` = 100%) 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: Examples:
@ -1090,10 +1057,105 @@ Examples:
* `{name = "default_place_node", gain = 0.5}`: 50% volume * `{name = "default_place_node", gain = 0.5}`: 50% volume
* `{name = "default_place_node", gain = 0.9, pitch = 1.1}`: 90% volume, 110% pitch * `{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 = <an ObjectRef>,
-- Attach the sound to an object.
-- Can't be used together with `pos`.
to_player = name,
-- Only play for this player.
-- Can't be used together with `exclude_player`.
exclude_player = name,
-- Don't play sound for this player.
-- Can't be used together with `to_player`.
max_hear_distance = 32,
-- Only play for players that are at most this far away when the sound
-- starts playing.
-- Needs `pos` or `object` to be set.
-- `32` is the default.
}
```
Examples:
```lua
-- Play locationless on all clients
{
gain = 1.0, -- default
fade = 0.0, -- default
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 = <an ObjectRef>,
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_damage`: Played when the local player takes damage (gain = 0.5)
* `player_falling_damage`: Played when the local player takes * `player_falling_damage`: Played when the local player takes
@ -8804,7 +8866,10 @@ Used by `minetest.register_node`.
footstep = <SimpleSoundSpec>, footstep = <SimpleSoundSpec>,
-- If walkable, played when object walks on it. If node is -- If walkable, played when object walks on it. If node is
-- climbable or a liquid, played when object moves through it -- climbable or a liquid, played when object moves through it.
-- Sound is played at the base of the object's collision-box.
-- Gain is multiplied by `0.6`.
-- For local player, it's played position-less, with normal gain.
dig = <SimpleSoundSpec> or "__group", dig = <SimpleSoundSpec> or "__group",
-- While digging node. -- While digging node.

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

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

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

@ -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,
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

@ -1,8 +1,9 @@
set(sound_SRCS "") set(sound_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/sound.cpp)
if(USE_SOUND) if(USE_SOUND)
set(sound_SRCS ${sound_SRCS} 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 set(SOUND_INCLUDE_DIRS
${OPENAL_INCLUDE_DIR} ${OPENAL_INCLUDE_DIR}
${VORBIS_INCLUDE_DIR} ${VORBIS_INCLUDE_DIR}

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

@ -374,6 +374,11 @@ Client::~Client()
if (m_mod_storage_database) if (m_mod_storage_database)
m_mod_storage_database->endSave(); m_mod_storage_database->endSave();
delete m_mod_storage_database; 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) 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) { 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; u16 object_id = m_sounds_to_object.second;
ClientActiveObject *cao = m_env.getActiveObject(object_id); ClientActiveObject *cao = m_env.getActiveObject(object_id);
if (!cao) if (!cao)
continue; 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) { if(m_removed_sounds_check_timer >= 2.32) {
m_removed_sounds_check_timer = 0; m_removed_sounds_check_timer = 0;
// Find removed sounds and clear references to them // Find removed sounds and clear references to them
std::vector<sound_handle_t> removed_client_ids = m_sound->pollRemovedSounds();
std::vector<s32> removed_server_ids; std::vector<s32> removed_server_ids;
for (std::unordered_map<s32, int>::iterator i = m_sounds_server_to_client.begin(); for (sound_handle_t client_id : removed_client_ids) {
i != m_sounds_server_to_client.end();) { auto client_to_server_id_it = m_sounds_client_to_server.find(client_id);
s32 server_id = i->first; if (client_to_server_id_it == m_sounds_client_to_server.end())
int client_id = i->second; continue;
++i; s32 server_id = client_to_server_id_it->second;
if(!m_sound->soundExists(client_id)) { 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_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); removed_server_ids.push_back(server_id);
} }
m_sounds_to_objects.erase(client_id);
} }
// Sync to server // Sync to server
if(!removed_server_ids.empty()) { if (!removed_server_ids.empty()) {
sendRemovedSounds(removed_server_ids); sendRemovedSounds(removed_server_ids);
} }
} }
@ -800,9 +808,13 @@ bool Client::loadMedia(const std::string &data, const std::string &filename,
}; };
name = removeStringEnd(filename, sound_ext); name = removeStringEnd(filename, sound_ext);
if (!name.empty()) { if (!name.empty()) {
TRACESTREAM(<< "Client: Attempting to load sound " TRACESTREAM(<< "Client: Attempting to load sound file \""
<< "file \"" << filename << "\"" << std::endl); << filename << "\"" << std::endl);
return m_sound->loadSoundData(name, data); 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[] = { const char *model_ext[] = {
@ -1205,7 +1217,7 @@ void Client::sendGotBlocks(const std::vector<v3s16> &blocks)
Send(&pkt); Send(&pkt);
} }
void Client::sendRemovedSounds(std::vector<s32> &soundList) void Client::sendRemovedSounds(const std::vector<s32> &soundList)
{ {
size_t server_ids = soundList.size(); size_t server_ids = soundList.size();
assert(server_ids <= 0xFFFF); assert(server_ids <= 0xFFFF);

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

@ -47,7 +47,8 @@ public:
virtual bool getCollisionBox(aabb3f *toset) const { return false; } virtual bool getCollisionBox(aabb3f *toset) const { return false; }
virtual bool getSelectionBox(aabb3f *toset) const { return false; } virtual bool getSelectionBox(aabb3f *toset) const { return false; }
virtual bool collideWithObjects() 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 virtual scene::ISceneNode *getSceneNode() const
{ return NULL; } { return NULL; }
virtual scene::IAnimatedMeshSceneNode *getAnimatedMeshSceneNode() const virtual scene::IAnimatedMeshSceneNode *getAnimatedMeshSceneNode() const

@ -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), v3s16 node_below_pos = floatToInt(foot_pos + v3f(0.0f, -0.5f, 0.0f),
1.0f); 1.0f);
MapNode n = m_env->getMap().getNode(node_below_pos); 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 // Reduce footstep gain, as non-local-player footsteps are
// somehow louder. // somehow louder.
spec.gain *= 0.6f; 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));
} }
} }
} }

@ -162,7 +162,9 @@ public:
virtual bool getSelectionBox(aabb3f *toset) const; 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; } inline const v3f &getRotation() const { return m_rotation; }

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

97
src/client/sound.cpp Normal file

@ -0,0 +1,97 @@
/*
Minetest
Copyright (C) 2023 DS
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "sound.h"
#include "filesys.h"
#include "log.h"
#include "porting.h"
#include "util/numeric.h"
#include <algorithm>
#include <string>
#include <vector>
std::vector<std::string> SoundFallbackPathProvider::
getLocalFallbackPathsForSoundname(const std::string &name)
{
std::vector<std::string> paths;
// only try each name once
if (m_done_names.count(name))
return paths;
m_done_names.insert(name);
addThePaths(name, paths);
// remove duplicates
std::sort(paths.begin(), paths.end());
auto end = std::unique(paths.begin(), paths.end());
paths.erase(end, paths.end());
return paths;
}
void SoundFallbackPathProvider::addAllAlternatives(const std::string &common,
std::vector<std::string> &paths)
{
paths.reserve(paths.size() + 11);
for (auto &&ext : {".ogg", ".0.ogg", ".1.ogg", ".2.ogg", ".3.ogg", ".4.ogg",
".5.ogg", ".6.ogg", ".7.ogg", ".8.ogg", ".9.ogg", }) {
paths.push_back(common + ext);
}
}
void SoundFallbackPathProvider::addThePaths(const std::string &name,
std::vector<std::string> &paths)
{
addAllAlternatives(porting::path_share + DIR_DELIM + "sounds" + DIR_DELIM + name, paths);
addAllAlternatives(porting::path_user + DIR_DELIM + "sounds" + DIR_DELIM + name, paths);
}
void ISoundManager::reportRemovedSound(sound_handle_t id)
{
if (id <= 0)
return;
freeId(id);
m_removed_sounds.push_back(id);
}
sound_handle_t ISoundManager::allocateId(u32 num_owners)
{
while (m_occupied_ids.find(m_next_id) != m_occupied_ids.end()
|| m_next_id == SOUND_HANDLE_T_MAX) {
m_next_id = static_cast<sound_handle_t>(
myrand() % static_cast<u32>(SOUND_HANDLE_T_MAX - 1) + 1);
}
sound_handle_t id = m_next_id++;
m_occupied_ids.emplace(id, num_owners);
return id;
}
void ISoundManager::freeId(sound_handle_t id, u32 num_owners)
{
auto it = m_occupied_ids.find(id);
if (it == m_occupied_ids.end())
return;
if (it->second <= num_owners)
m_occupied_ids.erase(it);
else
it->second -= num_owners;
}

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

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

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

File diff suppressed because it is too large Load Diff

@ -0,0 +1,613 @@
/*
Minetest
Copyright (C) 2022 DS
Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
OpenAL support based on work by:
Copyright (C) 2011 Sebastian 'Bahamada' Rühl
Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com>
Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; ifnot, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#pragma once
#include "log.h"
#include "porting.h"
#include "sound_openal.h"
#include "util/basic_macros.h"
#if defined(_WIN32)
#include <al.h>
#include <alc.h>
//#include <alext.h>
#elif defined(__APPLE__)
#define OPENAL_DEPRECATED
#include <OpenAL/al.h>
#include <OpenAL/alc.h>
//#include <OpenAL/alext.h>
#else
#include <AL/al.h>
#include <AL/alc.h>
#include <AL/alext.h>
#endif
#include <vorbis/vorbisfile.h>
#include <optional>
#include <unordered_map>
#include <utility>
#include <vector>
/*
*
* The coordinate space for sounds (sound-space):
* ----------------------------------------------
*
* * The functions from ISoundManager (see sound.h) take spatial vectors in node-space.
* * All other `v3f`s here are, if not told otherwise, in sound-space, which is
* defined as node-space mirrored along the x-axis.
* (This is needed because OpenAL uses a right-handed coordinate system.)
* * Use `swap_handedness()` to convert between those two coordinate spaces.
*
*
* How sounds are loaded:
* ----------------------
*
* * Step 1:
* `loadSoundFile` or `loadSoundFile` is called. This adds an unopen sound with
* the given name to `m_sound_datas_unopen`.
* Unopen / lazy sounds (`ISoundDataUnopen`) are ogg-vorbis files that we did not yet
* start to decode. (Decoding an unopen sound does not fail under normal circumstances
* (because we check whether the file exists at least), if it does fail anyways,
* we should notify the user.)
* * Step 2:
* `addSoundToGroup` is called, to add the name from step 1 to a group. If the
* group does not yet exist, a new one is created. A group can later be played.
* (The mapping is stored in `m_sound_groups`.)
* * Step 3:
* `playSound` or `playSoundAt` is called.
* * Step 3.1:
* If the group with the name `spec.name` does not exist, and `spec.use_local_fallback`
* is true, a new group is created using the user's sound-pack.
* * Step 3.2:
* We choose one random sound name from the given group.
* * Step 3.3:
* We open the sound (see `openSingleSound`).
* If the sound is already open (in `m_sound_datas_open`), we take that one.
* Otherwise we open it by calling `ISoundDataUnopen::open`. We choose (by
* sound length), whether it's a single-buffer (`SoundDataOpenBuffer`) or
* streamed (`SoundDataOpenStream`) sound.
* Single-buffer sounds are always completely loaded. Streamed sounds can be
* partially loaded.
* The sound is erased from `m_sound_datas_unopen` and added to `m_sound_datas_open`.
* Open sounds are kept forever.
* * Step 3.4:
* We create the new `PlayingSound`. It has a `shared_ptr` to its open sound.
* If the open sound is streaming, the playing sound needs to be stepped using
* `PlayingSound::stepStream` for enqueuing buffers. For this purpose, the sound
* is added to `m_sounds_streaming` (as `weak_ptr`).
* If the sound is fading, it is added to `m_sounds_fading` for regular fade-stepping.
* The sound is also added to `m_sounds_playing`, so that one can access it
* via its sound handle.
* * Step 4:
* Streaming sounds are updated. For details see [Streaming of sounds].
* * Step 5:
* At deinitialization, we can just let the destructors do their work.
* Sound sources are deleted (and with this also stopped) by ~PlayingSound.
* Buffers can't be deleted while sound sources using them exist, because
* PlayingSound has a shared_ptr to its ISoundData.
*
*
* Streaming of sounds:
* --------------------
*
* In each "bigstep", all streamed sounds are stepStream()ed. This means a
* sound can be stepped at any point in time in the bigstep's interval.
*
* In the worst case, a sound is stepped at the start of one bigstep and in the
* end of the next bigstep. So between two stepStream()-calls lie at most
* 2 * STREAM_BIGSTEP_TIME seconds.
* As there are always 2 sound buffers enqueued, at least one untouched full buffer
* is still available after the first stepStream().
* If we take a MIN_STREAM_BUFFER_LENGTH > 2 * STREAM_BIGSTEP_TIME, we can hence
* not run into an empty queue.
*
* The MIN_STREAM_BUFFER_LENGTH needs to be a little bigger because of dtime jitter,
* other sounds that may have taken long to stepStream(), and sounds being played
* faster due to Doppler effect.
*
*/
// constants
// in seconds
constexpr f32 REMOVE_DEAD_SOUNDS_INTERVAL = 2.0f;
// maximum length in seconds that a sound can have without being streamed
constexpr f32 SOUND_DURATION_MAX_SINGLE = 3.0f;
// minimum time in seconds of a single buffer in a streamed sound
constexpr f32 MIN_STREAM_BUFFER_LENGTH = 1.0f;
// duration in seconds of one bigstep
constexpr f32 STREAM_BIGSTEP_TIME = 0.3f;
static_assert(MIN_STREAM_BUFFER_LENGTH > STREAM_BIGSTEP_TIME * 2.0f,
"See [Streaming of sounds].");
static_assert(SOUND_DURATION_MAX_SINGLE >= MIN_STREAM_BUFFER_LENGTH * 2.0f,
"There's no benefit in streaming if we can't queue more than 2 buffers.");
/**
* RAII wrapper for openal sound buffers.
*/
struct RAIIALSoundBuffer final
{
RAIIALSoundBuffer() noexcept = default;
explicit RAIIALSoundBuffer(ALuint buffer) noexcept : m_buffer(buffer) {};
~RAIIALSoundBuffer() noexcept { reset(0); }
DISABLE_CLASS_COPY(RAIIALSoundBuffer)
RAIIALSoundBuffer(RAIIALSoundBuffer &&other) noexcept : m_buffer(other.release()) {}
RAIIALSoundBuffer &operator=(RAIIALSoundBuffer &&other) noexcept;
ALuint get() noexcept { return m_buffer; }
ALuint release() noexcept { return std::exchange(m_buffer, 0); }
void reset(ALuint buf) noexcept;
static RAIIALSoundBuffer generate() noexcept;
private:
// According to openal specification:
// > Deleting buffer name 0 is a legal NOP.
//
// and:
// > [...] the NULL buffer (i.e., 0) which can always be queued.
ALuint m_buffer = 0;
};
/**
* For vorbisfile to read from our buffer instead of from a file.
*/
struct OggVorbisBufferSource {
std::string buf;
size_t cur_offset = 0;
static size_t read_func(void *ptr, size_t size, size_t nmemb, void *datasource) noexcept;
static int seek_func(void *datasource, ogg_int64_t offset, int whence) noexcept;
static int close_func(void *datasource) noexcept;
static long tell_func(void *datasource) noexcept;
static const ov_callbacks s_ov_callbacks;
};
/**
* Metadata of an Ogg-Vorbis file, used for decoding.
* We query this information once and store it in this struct.
*/
struct OggFileDecodeInfo {
std::string name_for_logging;
bool is_stereo;
ALenum format; // AL_FORMAT_MONO16 or AL_FORMAT_STEREO16
size_t bytes_per_sample;
ALsizei freq;
ALuint length_samples = 0;
f32 length_seconds = 0.0f;
};
/**
* RAII wrapper for OggVorbis_File.
*/
struct RAIIOggFile {
bool m_needs_clear = false;
OggVorbis_File m_file;
RAIIOggFile() = default;
DISABLE_CLASS_COPY(RAIIOggFile)
~RAIIOggFile() noexcept
{
if (m_needs_clear)
ov_clear(&m_file);
}
OggVorbis_File *get() { return &m_file; }
std::optional<OggFileDecodeInfo> getDecodeInfo(const std::string &filename_for_logging);
/**
* Main function for loading ogg vorbis sounds.
* Loads exactly the specified interval of PCM-data, and creates an OpenAL
* buffer with it.
*
* @param decode_info Cached meta information of the file.
* @param pcm_start First sample in the interval.
* @param pcm_end One after last sample of the interval (=> exclusive).
* @return An AL sound buffer, or a 0-buffer on failure.
*/
RAIIALSoundBuffer loadBuffer(const OggFileDecodeInfo &decode_info, ALuint pcm_start,
ALuint pcm_end);
};
/**
* Class for the openal device and context
*/
class SoundManagerSingleton
{
public:
struct AlcDeviceDeleter {
void operator()(ALCdevice *p)
{
alcCloseDevice(p);
}
};
struct AlcContextDeleter {
void operator()(ALCcontext *p)
{
alcMakeContextCurrent(nullptr);
alcDestroyContext(p);
}
};
using unique_ptr_alcdevice = std::unique_ptr<ALCdevice, AlcDeviceDeleter>;
using unique_ptr_alccontext = std::unique_ptr<ALCcontext, AlcContextDeleter>;
unique_ptr_alcdevice m_device;
unique_ptr_alccontext m_context;
public:
bool init();
~SoundManagerSingleton();
};
/**
* Stores sound pcm data buffers.
*/
struct ISoundDataOpen
{
OggFileDecodeInfo m_decode_info;
explicit ISoundDataOpen(const OggFileDecodeInfo &decode_info) :
m_decode_info(decode_info) {}
virtual ~ISoundDataOpen() = default;
/**
* Iff the data is streaming, there is more than one buffer.
* @return Whether it's streaming data.
*/
virtual bool isStreaming() const noexcept = 0;
/**
* Load a buffer containing data starting at the given offset. Or just get it
* if it was already loaded.
*
* This function returns multiple values:
* * `buffer`: The OpenAL buffer.
* * `buffer_end`: The offset (in the file) where `buffer` ends (exclusive).
* * `offset_in_buffer`: Offset relative to `buffer`'s start where the requested
* `offset` is.
* `offset_in_buffer == 0` is guaranteed if some loaded buffer ends at
* `offset`.
*
* @param offset The start of the buffer.
* @return `{buffer, buffer_end, offset_in_buffer}` or `{0, sound_data_end, 0}`
* if `offset` is invalid.
*/
virtual std::tuple<ALuint, ALuint, ALuint> getOrLoadBufferAt(ALuint offset) = 0;
static std::shared_ptr<ISoundDataOpen> fromOggFile(std::unique_ptr<RAIIOggFile> oggfile,
const std::string &filename_for_logging);
};
/**
* Will be opened lazily when first used.
*/
struct ISoundDataUnopen
{
virtual ~ISoundDataUnopen() = default;
// Note: The ISoundDataUnopen is moved (see &&). It is not meant to be kept
// after opening.
virtual std::shared_ptr<ISoundDataOpen> open(const std::string &sound_name) && = 0;
};
/**
* Sound file is in a memory buffer.
*/
struct SoundDataUnopenBuffer final : ISoundDataUnopen
{
std::string m_buffer;
explicit SoundDataUnopenBuffer(std::string &&buffer) : m_buffer(std::move(buffer)) {}
std::shared_ptr<ISoundDataOpen> open(const std::string &sound_name) && override;
};
/**
* Sound file is in file system.
*/
struct SoundDataUnopenFile final : ISoundDataUnopen
{
std::string m_path;
explicit SoundDataUnopenFile(const std::string &path) : m_path(path) {}
std::shared_ptr<ISoundDataOpen> open(const std::string &sound_name) && override;
};
/**
* Non-streaming opened sound data.
* All data is completely loaded in one buffer.
*/
struct SoundDataOpenBuffer final : ISoundDataOpen
{
RAIIALSoundBuffer m_buffer;
SoundDataOpenBuffer(std::unique_ptr<RAIIOggFile> oggfile,
const OggFileDecodeInfo &decode_info);
bool isStreaming() const noexcept override { return false; }
std::tuple<ALuint, ALuint, ALuint> getOrLoadBufferAt(ALuint offset) override
{
if (offset >= m_decode_info.length_samples)
return {0, m_decode_info.length_samples, 0};
return {m_buffer.get(), m_decode_info.length_samples, offset};
}
};
/**
* Streaming opened sound data.
*
* Uses a sorted list of contiguous sound data regions (`ContiguousBuffers`s) for
* efficient seeking.
*/
struct SoundDataOpenStream final : ISoundDataOpen
{
/**
* An OpenAL buffer that goes until `m_end` (exclusive).
*/
struct SoundBufferUntil final
{
ALuint m_end;
RAIIALSoundBuffer m_buffer;
};
/**
* A sorted non-empty vector of contiguous buffers.
* The start (inclusive) of each buffer is the end of its predecessor, or
* `m_start` for the first buffer.
*/
struct ContiguousBuffers final
{
ALuint m_start;
std::vector<SoundBufferUntil> m_buffers;
};
std::unique_ptr<RAIIOggFile> m_oggfile;
// A sorted vector of non-overlapping, non-contiguous `ContiguousBuffers`s.
std::vector<ContiguousBuffers> m_bufferss;
SoundDataOpenStream(std::unique_ptr<RAIIOggFile> oggfile,
const OggFileDecodeInfo &decode_info);
bool isStreaming() const noexcept override { return true; }
std::tuple<ALuint, ALuint, ALuint> getOrLoadBufferAt(ALuint offset) override;
private:
// offset must be before after_it's m_start and after (after_it-1)'s last m_end
// new buffer will be inserted into m_bufferss before after_it
// returns same as getOrLoadBufferAt
std::tuple<ALuint, ALuint, ALuint> loadBufferAt(ALuint offset,
std::vector<ContiguousBuffers>::iterator after_it);
};
/**
* A sound that is currently played.
* Can be streaming.
* Can be fading.
*/
class PlayingSound final
{
struct FadeState {
f32 step;
f32 target_gain;
};
ALuint m_source_id;
std::shared_ptr<ISoundDataOpen> m_data;
ALuint m_next_sample_pos = 0;
bool m_looping;
bool m_is_positional;
bool m_stopped_means_dead = true;
std::optional<FadeState> m_fade_state = std::nullopt;
public:
PlayingSound(ALuint source_id, std::shared_ptr<ISoundDataOpen> data, bool loop,
f32 volume, f32 pitch, f32 start_time,
const std::optional<std::pair<v3f, v3f>> &pos_vel_opt);
~PlayingSound() noexcept
{
alDeleteSources(1, &m_source_id);
}
DISABLE_CLASS_COPY(PlayingSound)
// return false means streaming finished
bool stepStream();
// retruns true if it wasn't fading already
bool fade(f32 step, f32 target_gain) noexcept;
// returns true if more fade is needed later
bool doFade(f32 dtime) noexcept;
void updatePosVel(const v3f &pos, const v3f &vel) noexcept;
void setGain(f32 gain) noexcept;
f32 getGain() noexcept;
void setPitch(f32 pitch) noexcept { alSourcef(m_source_id, AL_PITCH, pitch); }
bool isStreaming() const noexcept { return m_data->isStreaming(); }
void play() noexcept { alSourcePlay(m_source_id); }
// returns one of AL_INITIAL, AL_PLAYING, AL_PAUSED, AL_STOPPED
ALint getState() noexcept
{
ALint state;
alGetSourcei(m_source_id, AL_SOURCE_STATE, &state);
return state;
}
bool isDead() noexcept
{
// streaming sounds can (but should not) stop because the queue runs empty
return m_stopped_means_dead && getState() == AL_STOPPED;
}
void pause() noexcept
{
// this is a NOP if state != AL_PLAYING
alSourcePause(m_source_id);
}
void resume() noexcept
{
if (getState() == AL_PAUSED)
play();
}
};
/*
* The public ISoundManager interface
*/
class OpenALSoundManager final : public ISoundManager
{
private:
std::unique_ptr<SoundFallbackPathProvider> m_fallback_path_provider;
ALCdevice *m_device;
ALCcontext *m_context;
// time in seconds until which removeDeadSounds will be called again
f32 m_time_until_dead_removal = REMOVE_DEAD_SOUNDS_INTERVAL;
// loaded sounds
std::unordered_map<std::string, std::unique_ptr<ISoundDataUnopen>> m_sound_datas_unopen;
std::unordered_map<std::string, std::shared_ptr<ISoundDataOpen>> m_sound_datas_open;
// sound groups
std::unordered_map<std::string, std::vector<std::string>> m_sound_groups;
// currently playing sounds
std::unordered_map<sound_handle_t, std::shared_ptr<PlayingSound>> m_sounds_playing;
// streamed sounds
std::vector<std::weak_ptr<PlayingSound>> m_sounds_streaming_current_bigstep;
std::vector<std::weak_ptr<PlayingSound>> m_sounds_streaming_next_bigstep;
// time left until current bigstep finishes
f32 m_stream_timer = STREAM_BIGSTEP_TIME;
std::vector<std::weak_ptr<PlayingSound>> m_sounds_fading;
// if true, all sounds will be directly paused after creation
bool m_is_paused = false;
private:
void stepStreams(f32 dtime);
void doFades(f32 dtime);
/**
* Gives the open sound for a loaded sound.
* Opens the sound if currently unopened.
*
* @param sound_name Name of the sound.
* @return The open sound.
*/
std::shared_ptr<ISoundDataOpen> openSingleSound(const std::string &sound_name);
/**
* Gets a random sound name from a group.
*
* @param group_name The name of the sound group.
* @return The name of a sound in the group, or "" on failure. Getting the
* sound with `openSingleSound` directly afterwards will not fail.
*/
std::string getLoadedSoundNameFromGroup(const std::string &group_name);
/**
* Same as `getLoadedSoundNameFromGroup`, but if sound does not exist, try to
* load from local files.
*/
std::string getOrLoadLoadedSoundNameFromGroup(const std::string &group_name);
std::shared_ptr<PlayingSound> createPlayingSound(const std::string &sound_name,
bool loop, f32 volume, f32 pitch, f32 start_time,
const std::optional<std::pair<v3f, v3f>> &pos_vel_opt);
void playSoundGeneric(sound_handle_t id, const std::string &group_name, bool loop,
f32 volume, f32 fade, f32 pitch, bool use_local_fallback, f32 start_time,
const std::optional<std::pair<v3f, v3f>> &pos_vel_opt);
/**
* Deletes sounds that are dead (=finished).
*
* @return Number of removed sounds.
*/
int removeDeadSounds();
public:
OpenALSoundManager(SoundManagerSingleton *smg,
std::unique_ptr<SoundFallbackPathProvider> fallback_path_provider);
~OpenALSoundManager() override;
DISABLE_CLASS_COPY(OpenALSoundManager)
/* Interface */
void step(f32 dtime) override;
void pauseAll() override;
void resumeAll() override;
void updateListener(const v3f &pos_, const v3f &vel_, const v3f &at_, const v3f &up_) override;
void setListenerGain(f32 gain) override;
bool loadSoundFile(const std::string &name, const std::string &filepath) override;
bool loadSoundData(const std::string &name, std::string &&filedata) override;
void addSoundToGroup(const std::string &sound_name, const std::string &group_name) override;
void playSound(sound_handle_t id, const SoundSpec &spec) override;
void playSoundAt(sound_handle_t id, const SoundSpec &spec, const v3f &pos_,
const v3f &vel_) override;
void stopSound(sound_handle_t sound) override;
void fadeSound(sound_handle_t soundid, f32 step, f32 target_gain) override;
void updateSoundPosVel(sound_handle_t sound, const v3f &pos_, const v3f &vel_) override;
};

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

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

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

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

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

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

@ -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 new fields for TOCLIENT_SET_LIGHTING and TOCLIENT_SET_SKY
Send forgotten TweenedParameter properties Send forgotten TweenedParameter properties
[scheduled bump for 5.7.0] [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) #define LATEST_PROTOCOL_VERSION_STRING TOSTRING(LATEST_PROTOCOL_VERSION)
// Server's supported network protocol range // Server's supported network protocol range
@ -454,15 +457,18 @@ enum ToClientCommand
TOCLIENT_PLAY_SOUND = 0x3f, TOCLIENT_PLAY_SOUND = 0x3f,
/* /*
s32 sound_id s32 server_id
u16 len u16 len
u8[len] sound name u8[len] sound name
s32 gain*1000 f32 gain
u8 type (0=local, 1=positional, 2=object) u8 type (SoundLocation: 0=local, 1=positional, 2=object)
s32[3] pos_nodes*10000 v3f pos_nodes (in BS-space)
u16 object_id u16 object_id
u8 loop (bool) u8 loop (bool)
f32 fade
f32 pitch
u8 ephemeral (bool) u8 ephemeral (bool)
f32 start_time (in seconds)
*/ */
TOCLIENT_STOP_SOUND = 0x40, TOCLIENT_STOP_SOUND = 0x40,

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

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

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

@ -104,10 +104,10 @@ void read_item_definition(lua_State* L, int index,
if (!lua_isnil(L, -1)) { if (!lua_isnil(L, -1)) {
luaL_checktype(L, -1, LUA_TTABLE); luaL_checktype(L, -1, LUA_TTABLE);
lua_getfield(L, -1, "place"); lua_getfield(L, -1, "place");
read_soundspec(L, -1, def.sound_place); read_simplesoundspec(L, -1, def.sound_place);
lua_pop(L, 1); lua_pop(L, 1);
lua_getfield(L, -1, "place_failed"); 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);
} }
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)) { if (!lua_isnil(L, -1)) {
luaL_checktype(L, -1, LUA_TTABLE); luaL_checktype(L, -1, LUA_TTABLE);
lua_getfield(L, -1, "punch_use"); 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_pop(L, 1);
lua_getfield(L, -1, "punch_use_air"); 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);
} }
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); push_groups(L, i.groups);
lua_setfield(L, -2, "groups"); lua_setfield(L, -2, "groups");
push_soundspec(L, i.sound_place); push_simplesoundspec(L, i.sound_place);
lua_setfield(L, -2, "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_setfield(L, -2, "sound_place_failed");
lua_pushstring(L, i.node_placement_prediction.c_str()); lua_pushstring(L, i.node_placement_prediction.c_str());
lua_setfield(L, -2, "node_placement_prediction"); 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"); lua_getfield(L, index, "sounds");
if(lua_istable(L, -1)){ if(lua_istable(L, -1)){
lua_getfield(L, -1, "footstep"); lua_getfield(L, -1, "footstep");
read_soundspec(L, -1, f.sound_footstep); read_simplesoundspec(L, -1, f.sound_footstep);
lua_pop(L, 1); lua_pop(L, 1);
lua_getfield(L, -1, "dig"); lua_getfield(L, -1, "dig");
read_soundspec(L, -1, f.sound_dig); read_simplesoundspec(L, -1, f.sound_dig);
lua_pop(L, 1); lua_pop(L, 1);
lua_getfield(L, -1, "dug"); 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);
} }
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); push_nodebox(L, c.collision_box);
lua_setfield(L, -2, "collision_box"); lua_setfield(L, -2, "collision_box");
lua_newtable(L); lua_newtable(L);
push_soundspec(L, c.sound_footstep); push_simplesoundspec(L, c.sound_footstep);
lua_setfield(L, -2, "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"); 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, "sound_dug");
lua_setfield(L, -2, "sounds"); lua_setfield(L, -2, "sounds");
lua_pushboolean(L, c.legacy_facedir_simple); lua_pushboolean(L, c.legacy_facedir_simple);
@ -1067,10 +1067,11 @@ void read_server_sound_params(lua_State *L, int index,
if(index < 0) if(index < 0)
index = lua_gettop(L) + 1 + index; index = lua_gettop(L) + 1 + index;
if(lua_istable(L, index)){ if (lua_istable(L, index)) {
// Functional overlap: this may modify SimpleSoundSpec contents // Functional overlap: this may modify SimpleSoundSpec contents
getfloatfield(L, index, "fade", params.spec.fade); getfloatfield(L, index, "fade", params.spec.fade);
getfloatfield(L, index, "pitch", params.spec.pitch); getfloatfield(L, index, "pitch", params.spec.pitch);
getfloatfield(L, index, "start_time", params.spec.start_time);
getboolfield(L, index, "loop", params.spec.loop); getboolfield(L, index, "loop", params.spec.loop);
getfloatfield(L, index, "gain", params.gain); 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) if(index < 0)
index = lua_gettop(L) + 1 + index; 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_createtable(L, 0, 3);
lua_pushstring(L, spec.name.c_str()); lua_pushstring(L, spec.name.c_str());

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

@ -28,10 +28,11 @@ set(common_SCRIPT_LUA_API_SRCS
set(client_SCRIPT_LUA_API_SRCS set(client_SCRIPT_LUA_API_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/l_camera.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_camera.cpp
${CMAKE_CURRENT_SOURCE_DIR}/l_client.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_localplayer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/l_mainmenu.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_minimap.cpp
${CMAKE_CURRENT_SOURCE_DIR}/l_particles_local.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_particles_local.cpp
${CMAKE_CURRENT_SOURCE_DIR}/l_sound.cpp
${CMAKE_CURRENT_SOURCE_DIR}/l_storage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_storage.cpp
PARENT_SCOPE) PARENT_SCOPE)

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

@ -78,15 +78,6 @@ private:
// get_meta(pos) // get_meta(pos)
static int l_get_meta(lua_State *L); 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() // get_server_info()
static int l_get_server_info(lua_State *L); static int l_get_server_info(lua_State *L);

@ -0,0 +1,150 @@
/*
Minetest
Copyright (C) 2023 DS
Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
Copyright (C) 2017 nerzhul, Loic Blot <loic.blot@unix-experience.fr>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "l_client_sound.h"
#include "l_internal.h"
#include "common/c_content.h"
#include "common/c_converter.h"
#include "client/client.h"
#include "client/sound.h"
/* ModApiClientSound */
// sound_play(spec, parameters)
int ModApiClientSound::l_sound_play(lua_State *L)
{
ISoundManager *sound_manager = getClient(L)->getSoundManager();
SoundSpec spec;
read_simplesoundspec(L, 1, spec);
SoundLocation type = SoundLocation::Local;
float gain = 1.0f;
v3f position;
if (lua_istable(L, 2)) {
getfloatfield(L, 2, "gain", gain);
getfloatfield(L, 2, "pitch", spec.pitch);
getboolfield(L, 2, "loop", spec.loop);
lua_getfield(L, 2, "pos");
if (!lua_isnil(L, -1)) {
position = read_v3f(L, -1);
type = SoundLocation::Position;
lua_pop(L, 1);
}
}
spec.gain *= gain;
sound_handle_t handle = sound_manager->allocateId(2);
if (type == SoundLocation::Local)
sound_manager->playSound(handle, spec);
else
sound_manager->playSoundAt(handle, spec, position, v3f(0.0f));
ClientSoundHandle::create(L, handle);
return 1;
}
void ModApiClientSound::Initialize(lua_State *L, int top)
{
API_FCT(sound_play);
}
/* ClientSoundHandle */
ClientSoundHandle *ClientSoundHandle::checkobject(lua_State *L, int narg)
{
luaL_checktype(L, narg, LUA_TUSERDATA);
void *ud = luaL_checkudata(L, narg, className);
if (!ud)
luaL_typerror(L, narg, className);
return *(ClientSoundHandle**)ud; // unbox pointer
}
int ClientSoundHandle::gc_object(lua_State *L)
{
ClientSoundHandle *o = *(ClientSoundHandle **)(lua_touserdata(L, 1));
if (getClient(L) && getClient(L)->getSoundManager())
getClient(L)->getSoundManager()->freeId(o->m_handle);
delete o;
return 0;
}
// :stop()
int ClientSoundHandle::l_stop(lua_State *L)
{
ClientSoundHandle *o = checkobject(L, 1);
getClient(L)->getSoundManager()->stopSound(o->m_handle);
return 0;
}
// :fade(step, gain)
int ClientSoundHandle::l_fade(lua_State *L)
{
ClientSoundHandle *o = checkobject(L, 1);
float step = readParam<float>(L, 2);
float gain = readParam<float>(L, 3);
getClient(L)->getSoundManager()->fadeSound(o->m_handle, step, gain);
return 0;
}
void ClientSoundHandle::create(lua_State *L, sound_handle_t handle)
{
ClientSoundHandle *o = new ClientSoundHandle(handle);
*(void **)(lua_newuserdata(L, sizeof(void *))) = o;
luaL_getmetatable(L, className);
lua_setmetatable(L, -2);
}
void ClientSoundHandle::Register(lua_State *L)
{
lua_newtable(L);
int methodtable = lua_gettop(L);
luaL_newmetatable(L, className);
int metatable = lua_gettop(L);
lua_pushliteral(L, "__metatable");
lua_pushvalue(L, methodtable);
lua_settable(L, metatable); // hide metatable from Lua getmetatable()
lua_pushliteral(L, "__index");
lua_pushvalue(L, methodtable);
lua_settable(L, metatable);
lua_pushliteral(L, "__gc");
lua_pushcfunction(L, gc_object);
lua_settable(L, metatable);
lua_pop(L, 1); // drop metatable
luaL_register(L, nullptr, methods); // fill methodtable
lua_pop(L, 1); // drop methodtable
}
const char ClientSoundHandle::className[] = "ClientSoundHandle";
const luaL_Reg ClientSoundHandle::methods[] = {
luamethod(ClientSoundHandle, stop),
luamethod(ClientSoundHandle, fade),
{0,0}
};

@ -0,0 +1,66 @@
/*
Minetest
Copyright (C) 2023 DS
Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
Copyright (C) 2017 nerzhul, Loic Blot <loic.blot@unix-experience.fr>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#pragma once
#include "lua_api/l_base.h"
#include "util/basic_macros.h"
using sound_handle_t = int;
class ModApiClientSound : public ModApiBase
{
private:
// sound_play(spec, parameters)
static int l_sound_play(lua_State *L);
public:
static void Initialize(lua_State *L, int top);
};
class ClientSoundHandle final : public ModApiBase
{
private:
sound_handle_t m_handle;
static const char className[];
static const luaL_Reg methods[];
ClientSoundHandle(sound_handle_t handle) : m_handle(handle) {}
DISABLE_CLASS_COPY(ClientSoundHandle)
static ClientSoundHandle *checkobject(lua_State *L, int narg);
static int gc_object(lua_State *L);
// :stop()
static int l_stop(lua_State *L);
// :fade(step, gain)
static int l_fade(lua_State *L);
public:
~ClientSoundHandle() = default;
static void create(lua_State *L, sound_handle_t handle);
static void Register(lua_State *L);
};

@ -0,0 +1,116 @@
/*
Minetest
Copyright (C) 2023 DS
Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
Copyright (C) 2017 nerzhul, Loic Blot <loic.blot@unix-experience.fr>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "l_mainmenu_sound.h"
#include "l_internal.h"
#include "common/c_content.h"
#include "gui/guiEngine.h"
/* ModApiMainMenuSound */
// sound_play(spec, loop)
int ModApiMainMenuSound::l_sound_play(lua_State *L)
{
SoundSpec spec;
read_simplesoundspec(L, 1, spec);
spec.loop = readParam<bool>(L, 2);
ISoundManager &sound_manager = *getGuiEngine(L)->m_sound_manager;
sound_handle_t handle = sound_manager.allocateId(2);
sound_manager.playSound(handle, spec);
MainMenuSoundHandle::create(L, handle);
return 1;
}
void ModApiMainMenuSound::Initialize(lua_State *L, int top)
{
API_FCT(sound_play);
}
/* MainMenuSoundHandle */
MainMenuSoundHandle *MainMenuSoundHandle::checkobject(lua_State *L, int narg)
{
luaL_checktype(L, narg, LUA_TUSERDATA);
void *ud = luaL_checkudata(L, narg, className);
if (!ud)
luaL_typerror(L, narg, className);
return *(MainMenuSoundHandle**)ud; // unbox pointer
}
int MainMenuSoundHandle::gc_object(lua_State *L)
{
MainMenuSoundHandle *o = *(MainMenuSoundHandle **)(lua_touserdata(L, 1));
if (getGuiEngine(L) && getGuiEngine(L)->m_sound_manager)
getGuiEngine(L)->m_sound_manager->freeId(o->m_handle);
delete o;
return 0;
}
// :stop()
int MainMenuSoundHandle::l_stop(lua_State *L)
{
MainMenuSoundHandle *o = checkobject(L, 1);
getGuiEngine(L)->m_sound_manager->stopSound(o->m_handle);
return 0;
}
void MainMenuSoundHandle::create(lua_State *L, sound_handle_t handle)
{
MainMenuSoundHandle *o = new MainMenuSoundHandle(handle);
*(void **)(lua_newuserdata(L, sizeof(void *))) = o;
luaL_getmetatable(L, className);
lua_setmetatable(L, -2);
}
void MainMenuSoundHandle::Register(lua_State *L)
{
lua_newtable(L);
int methodtable = lua_gettop(L);
luaL_newmetatable(L, className);
int metatable = lua_gettop(L);
lua_pushliteral(L, "__metatable");
lua_pushvalue(L, methodtable);
lua_settable(L, metatable); // hide metatable from Lua getmetatable()
lua_pushliteral(L, "__index");
lua_pushvalue(L, methodtable);
lua_settable(L, metatable);
lua_pushliteral(L, "__gc");
lua_pushcfunction(L, gc_object);
lua_settable(L, metatable);
lua_pop(L, 1); // drop metatable
luaL_register(L, nullptr, methods); // fill methodtable
lua_pop(L, 1); // drop methodtable
}
const char MainMenuSoundHandle::className[] = "MainMenuSoundHandle";
const luaL_Reg MainMenuSoundHandle::methods[] = {
luamethod(MainMenuSoundHandle, stop),
{0,0}
};

@ -1,5 +1,6 @@
/* /*
Minetest Minetest
Copyright (C) 2023 DS
Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com> Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
Copyright (C) 2017 nerzhul, Loic Blot <loic.blot@unix-experience.fr> Copyright (C) 2017 nerzhul, Loic Blot <loic.blot@unix-experience.fr>
@ -21,13 +22,42 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#pragma once #pragma once
#include "lua_api/l_base.h" #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: private:
// sound_play(spec, loop)
static int l_sound_play(lua_State *L); static int l_sound_play(lua_State *L);
static int l_sound_stop(lua_State *L);
public: public:
static void Initialize(lua_State *L, int top); 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);
};

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

@ -1,53 +0,0 @@
/*
Minetest
Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
Copyright (C) 2017 nerzhul, Loic Blot <loic.blot@unix-experience.fr>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "l_sound.h"
#include "l_internal.h"
#include "common/c_content.h"
#include "gui/guiEngine.h"
int ModApiSound::l_sound_play(lua_State *L)
{
SimpleSoundSpec spec;
read_soundspec(L, 1, spec);
spec.loop = readParam<bool>(L, 2);
s32 handle = getGuiEngine(L)->playSound(spec);
lua_pushinteger(L, handle);
return 1;
}
int ModApiSound::l_sound_stop(lua_State *L)
{
u32 handle = luaL_checkinteger(L, 1);
getGuiEngine(L)->stopSound(handle);
return 1;
}
void ModApiSound::Initialize(lua_State *L, int top)
{
API_FCT(sound_play);
API_FCT(sound_stop);
}

@ -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_modchannels.h"
#include "lua_api/l_particles_local.h" #include "lua_api/l_particles_local.h"
#include "lua_api/l_storage.h" #include "lua_api/l_storage.h"
#include "lua_api/l_sound.h"
#include "lua_api/l_util.h" #include "lua_api/l_util.h"
#include "lua_api/l_item.h" #include "lua_api/l_item.h"
#include "lua_api/l_nodemeta.h" #include "lua_api/l_nodemeta.h"
#include "lua_api/l_localplayer.h" #include "lua_api/l_localplayer.h"
#include "lua_api/l_camera.h" #include "lua_api/l_camera.h"
#include "lua_api/l_settings.h" #include "lua_api/l_settings.h"
#include "lua_api/l_client_sound.h"
ClientScripting::ClientScripting(Client *client): ClientScripting::ClientScripting(Client *client):
ScriptApiBase(ScriptingType::Client) ScriptApiBase(ScriptingType::Client)
@ -75,6 +75,7 @@ void ClientScripting::InitializeModApi(lua_State *L, int top)
LuaCamera::Register(L); LuaCamera::Register(L);
ModChannelRef::Register(L); ModChannelRef::Register(L);
LuaSettings::Register(L); LuaSettings::Register(L);
ClientSoundHandle::Register(L);
ModApiUtil::InitializeClient(L, top); ModApiUtil::InitializeClient(L, top);
ModApiClient::Initialize(L, top); ModApiClient::Initialize(L, top);
@ -83,6 +84,7 @@ void ClientScripting::InitializeModApi(lua_State *L, int top)
ModApiEnvMod::InitializeClient(L, top); ModApiEnvMod::InitializeClient(L, top);
ModApiChannels::Initialize(L, top); ModApiChannels::Initialize(L, top);
ModApiParticlesLocal::Initialize(L, top); ModApiParticlesLocal::Initialize(L, top);
ModApiClientSound::Initialize(L, top);
} }
void ClientScripting::on_client_ready(LocalPlayer *localplayer) void ClientScripting::on_client_ready(LocalPlayer *localplayer)

@ -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_base.h"
#include "lua_api/l_http.h" #include "lua_api/l_http.h"
#include "lua_api/l_mainmenu.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_util.h"
#include "lua_api/l_settings.h" #include "lua_api/l_settings.h"
#include "log.h" #include "log.h"
@ -66,7 +66,7 @@ void MainMenuScripting::initializeModApi(lua_State *L, int top)
// Initialize mod API modules // Initialize mod API modules
ModApiMainMenu::Initialize(L, top); ModApiMainMenu::Initialize(L, top);
ModApiUtil::Initialize(L, top); ModApiUtil::Initialize(L, top);
ModApiSound::Initialize(L, top); ModApiMainMenuSound::Initialize(L, top);
ModApiHttp::Initialize(L, top); ModApiHttp::Initialize(L, top);
asyncEngine.registerStateInitializer(registerLuaClasses); asyncEngine.registerStateInitializer(registerLuaClasses);
@ -83,6 +83,7 @@ void MainMenuScripting::initializeModApi(lua_State *L, int top)
void MainMenuScripting::registerLuaClasses(lua_State *L, int top) void MainMenuScripting::registerLuaClasses(lua_State *L, int top)
{ {
LuaSettings::Register(L); LuaSettings::Register(L);
MainMenuSoundHandle::Register(L);
} }
/******************************************************************************/ /******************************************************************************/

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

@ -62,7 +62,7 @@ struct RollbackAction;
class EmergeManager; class EmergeManager;
class ServerScripting; class ServerScripting;
class ServerEnvironment; class ServerEnvironment;
struct SimpleSoundSpec; struct SoundSpec;
struct CloudParams; struct CloudParams;
struct SkyboxParams; struct SkyboxParams;
struct SunParams; 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 struct ServerPlayingSound
{ {
SoundLocation type = SoundLocation::Local; SoundLocation type = SoundLocation::Local;
@ -111,7 +111,7 @@ struct ServerPlayingSound
v3f getPos(ServerEnvironment *env, bool *pos_exists) const; v3f getPos(ServerEnvironment *env, bool *pos_exists) const;
SimpleSoundSpec spec; SoundSpec spec;
std::unordered_set<session_t> clients; // peer ids std::unordered_set<session_t> clients; // peer ids
}; };

@ -24,20 +24,29 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "util/serialize.h" #include "util/serialize.h"
#include "irrlichttypes_bloated.h" #include "irrlichttypes_bloated.h"
// This class describes the basic sound information for playback. /**
// Positional handling is done separately. * Describes the sound information for playback.
* Positional handling is done separately.
struct SimpleSoundSpec *
* `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, SoundSpec(const std::string &name = "", float gain = 1.0f,
bool loop = false, float fade = 0.0f, float pitch = 1.0f) : bool loop = false, float fade = 0.0f, float pitch = 1.0f,
name(name), gain(gain), fade(fade), pitch(pitch), loop(loop) 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(); } 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); os << serializeString16(name);
writeF32(os, gain); writeF32(os, gain);
@ -45,7 +54,10 @@ struct SimpleSoundSpec
writeF32(os, fade); 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); name = deSerializeString16(is);
gain = readF32(is); gain = readF32(is);
@ -53,11 +65,16 @@ struct SimpleSoundSpec
fade = readF32(is); fade = readF32(is);
} }
// Name of the sound-group
std::string name; std::string name;
float gain = 1.0f; float gain = 1.0f;
float fade = 0.0f; float fade = 0.0f;
float pitch = 1.0f; float pitch = 1.0f;
float start_time = 0.0f;
bool loop = false; 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;
}; };