--- implements tools for quickly playing audio. -- @module play_sound local sqrt = math.sqrt --- Guns4d soundspec -- -- simple specification for playing a sound in relation to an action, acts as a layer of minetest.play_sound. -- ATTENTION: read lua_api.md for more parameters! all parameters from there are valid unless otherwise stated here, these are auxillary. -- @table guns4d_soundspec -- @compact -- @field min_hear_distance `float` this is useful if you wish to play a sound which has a "far" sound, such as distant gunshots. incompatible `with to_player` -- @field sounds `table` a @{misc_helpers.weighted_randoms| weighted_randoms table} for randomly selecting sounds. The output will overwrite the `sound` field. -- @field to_player `objRef` 4dguns changes `to_player` so it only plays positionless audio (as it is only intended for first person audio). If set to string "from_player" and player present -- @field player `objRef` this is so to_player being set to "from_player". It's to be set to the player which fired the weapon. -- @field delay `float` delays the playing of the sound -- @field attenuation_rate `float` the rate of dropoff for a sound. I figure this is a bit more intuitive then jacking the gain up super high for every sound... Set the default in config. -- @field split_audio_by_perspective `bool` [GUN CLASS SPECIFIC] tells the gun wether to split into third and first person (positionless) audio and adjust gain. -- @field third_person_gain_multiplier `float` [GUN CLASS SPECIFIC] replaces the constant/config value "third_person_gain_multiplier/THIRD_PERSON_GAIN_MULTIPLIER". -- @example -- soundspec = { -- sounds = { --weighted randoms -- fire_fp = .5. -- fire_fp_2 = .2. -- fire_fp_3 = .3 -- }, -- pitch = { -- min = .6, -- max = 1 -- }, -- gain = 1, --format for pitch and gain is interchangable. -- min_hear_distance = 20, --this is for distant gunshots, for example. Entirely optional. Cannot be used with to_player -- exclude_player -- to_player -- --when present it automatically plays positionless audio, as this is for first person effects. -- } -- local function handle_min_max(tbl) return tbl.min+(math.random()*(tbl.max-tbl.min)) end local sound_handles = {} local function play_sound(sound, soundspec, handle, i) if soundspec.delay then minetest.after(soundspec.delay, function() if sound_handles[handle] ~= false then sound_handles[handle][i] = minetest.sound_play(sound, soundspec, soundspec.ephemeral) end end) else sound_handles[handle][i] = minetest.sound_play(sound, soundspec) end end --- allows you to play one or more sounds with more complex features, so sounds can be easily coded for guns without the need for functions. -- -- WARNING: this function modifies the tables passed to it, use `Guns4d.table.shallow_copy()` or `table.copy` for inputted soundspecs -- @example -- { -- to_player = "singeplayer", -- min_distance = 100, --soundspec_to_play1 & soundspec_to_play2 will share this field as it is in the higher level table (as well as the above field) -- soundspec_to_play1, --a sound parameter table -- soundspec_to_play2 -- } -- @tparam table soundspecs_list a list a list of soundspecs optionally accompanied with fields to be used in all of them. -- @treturn integer guns4d sound handle, used by stop_sounds & get_sounds function Guns4d.play_sounds(soundspecs_list) --print(dump(soundspecs_list)) --support a list of sounds to play if not soundspecs_list[1] then --turn into iteratable format. soundspecs_list = {soundspecs_list} end local applied = {} --all fields that aren't numbers will be copied over, allowing you to set fields across all sounds (i.e. pos, target player.), if already present it will remain the same. for field, v in pairs(soundspecs_list) do if type(field) ~= "number" then for _, spec in ipairs(soundspecs_list) do if not spec[field] then spec[field] = v end end soundspecs_list[field] = nil --so it isn't iterated end end local handle = #sound_handles+1 --determine the sound handle before playing sound_handles[handle] = {} --local handle_object = sound_handles[handle] for arg, soundspec in pairs(soundspecs_list) do if soundspec.to_player == "from_player" then soundspec.to_player = soundspec.player:get_player_name() end --setter of sound may not have access to this info, so add a method to use it. assert(not (soundspec.to_player and soundspec.min_distance), "in argument '"..tostring(arg).."' `min_distance` and `to_player` are incompatible parameters.") local sound = soundspec.sound for i, v in pairs(soundspec) do if type(v) == "table" and v.min then soundspec[i]=handle_min_max(v) end end if type(sound) == "table" then sound = Guns4d.math.weighted_randoms(sound) end assert(sound, "no sound provided") if not leef.paths.media_paths[(sound or "[NIL]")..".ogg"] then minetest.log("error", "no sound by the name `"..leef.paths.media_paths[(sound or "[NIL]")..".ogg"].."`") end local exclude_player_ref = soundspec.exclude_player if type(soundspec.exclude_player)=="string" then exclude_player_ref = minetest.get_player_by_name(soundspec.exclude_player) elseif soundspec.exclude_player then exclude_player_ref = soundspec.exclude_player soundspec.exclude_player = exclude_player_ref:get_player_name() end --print(dump(soundspecs_list), i) if soundspec.to_player then soundspec.pos = nil end --play sound for all players outside min hear distance local original_gain = soundspec.gain or 1 local attenuation_rate = soundspec.attenuation_rate or Guns4d.config.default_audio_attenuation_rate local player_list = ((not soundspec.to_player) and minetest.get_connected_players()) or {minetest.get_player_by_name(soundspec.to_player)} for _, player in pairs(player_list) do soundspec.sound = nil local pos = player:get_pos() local dist = 0 if soundspec.pos then dist = sqrt( sqrt((pos.x-(soundspec.pos.x))^2+(pos.y-soundspec.pos.y)^2)^2 + (pos.z-soundspec.pos.z)^2) end if ((not soundspec.max_hear_distance) or (dist <= soundspec.max_hear_distance)) and ((not soundspec.min_hear_distance) or (dist > soundspec.min_hear_distance)) and (player~=exclude_player_ref) then soundspec.exclude_player = nil --not needed anyway because we can just not play it for this player. soundspec.to_player = player:get_player_name() soundspec.gain = original_gain/(Guns4d.math.clamp((dist-(soundspec.min_hear_distance or 0))*attenuation_rate, 1, math.huge)^2) --so i found out the hard way that it doesn't fucking reduce volume by distance if there's a to_player. Kind of pisses me off. play_sound(sound, soundspec, handle, arg) end end end return handle end --- gets a list of currently playing Minetest sound handles from the Guns4d sound handle. Modification of table highly discouraged. -- @tparam integer handle a sound handle as returned by play_sounds -- @treturn table a list of sound handles as returned by minetest.sound_play() function Guns4d.get_sounds(handle) return sound_handles[handle] end --- stops a list of sounds -- @tparam integer|table handle a guns4d sound handle OR list of minetest sound handles to stop -- @treturn bool returns true if successful. function Guns4d.stop_sounds(handle) local handle_list = (type(handle) == "table" and handle) or sound_handles[handle] if not handle_list then return false end if type(handle) == "number" then sound_handles[handle] = false --indicate to not play any delayed noises. end for i, v in pairs(handle_list) do minetest.sound_stop(v) end return true end