From 78ff108529edd3e00c86f6161f26c42e4563b3a7 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 27 Dec 2024 20:42:51 -0600 Subject: [PATCH] Sound Optimizations and Debug Tools (#88517) ## About The Pull Request I was digging through sound code trying to add a few misc `.ogg`'s when I noticed a horrifying revelation. Sound calculations are broken as fuck. At first I didn't think it was a big deal until I remembered this comment in another PR: ![chrome_6rspeYVig1](https://github.com/user-attachments/assets/04c3fa79-c267-43ae-a77f-85b0e8d3108f) I asked for the profile logs: ``` Profile results (total time) Proc Name Self CPU Total CPU Real Time Overtime Calls ---------------------------------------------------------------------------------------------- --------- --------- --------- --------- --------- /proc/playsound 4.771 26.841 26.902 0.347 153938 /mob/proc/playsound_local 9.511 15.689 15.891 0.724 429391 /mob/living/say 0.125 19.064 19.945 0.000 2550 /mob/living/carbon/updatehealth 0.878 8.305 8.341 0.013 49847 /mob/living/carbon/proc/handle_organs 1.486 9.263 9.300 0.000 70977 /obj/item/organ/internal/on_life 0.415 0.455 0.561 0.000 340312 /mob/living/carbon/Life 0.758 30.085 30.123 0.000 71933 /mob/living/proc/Life 5.509 39.404 39.512 0.000 401951 ``` Yeah, that is _really_ bad. Especially since it is a hot proc that is used half a million times. Optimizing this would help a decent amount. So now you are probably wondering, what exactly is the problem?! Let's take a look. https://www.desmos.com/calculator/sqdfl8ipgf Sounds are being broadcast on a large radius which is typically about ~17 tiles. Since the volume of a sound is reduced by distance, the further away the sound is, the quieter it becomes. You would think, hey that's fine! Sounds become inaudible at the 17 tile distance limit right? Except no, they generally become inaudible about 2/3rds of the way there. (~10 tile range) Sound volume is between 1-100. When the sound volume is lower than 3, it becomes (pretty much) inaudible. This means most of the sounds that are being spammed in game everywhere, you can't even hear!!! To fix this we used two major optimizations: 1. Add a `SOUND_AUDIBLE_VOLUME_MIN` to ignore any sound lower than this volume and calculate an audible distance 2. Use the new audible distance to drastically shorten the tile range of mob listeners using `get_hearers_in_view()` To give you an idea of how much better this is: Sounds that had a range of 17x17 (289 turfs) are now 12x12 (144 turfs). There is also large range machinery like the SM that has a 40x40 (1600 turfs) that is now 30x30 (900 turfs). It's quite an improvement. I also went and added a debugging tool to the admin/debug equipment set that can be found in the debug box. It is a pair of earmuffs that when worn has any sounds sent to the mob via a `to_chat` message displaying the sounds max range, distance from player, volume, and sound name. It is highly recommend to walk while wearing the earmuffs since walking stops your mob from emitting footstep sounds. ## Why It's Good For The Game The speed of sound is now faster than ever. Please don't break the sound barrier. ## Changelog :cl: refactor: Sound has been heavily optimized and will now ignore low volume sounds from far away. admin: Add debugging sound earmuffs to admin equipment inside the debug box. Wear them to determine a sounds max range, distance, volume, and sound name. Highly recommended to walk otherwise you will get spammed with footstep sounds. /:cl: --- code/__DEFINES/sound.dm | 38 +++++++++++++++++++ code/__DEFINES/traits/declarations.dm | 2 + code/_globalvars/traits/_traits.dm | 1 + .../items/storage/boxes/engineering_boxes.dm | 1 + code/game/sound.dm | 26 ++++++++----- code/modules/clothing/ears/_ears.dm | 5 +++ 6 files changed, 64 insertions(+), 9 deletions(-) diff --git a/code/__DEFINES/sound.dm b/code/__DEFINES/sound.dm index a5fbc5131d1..99d0335065b 100644 --- a/code/__DEFINES/sound.dm +++ b/code/__DEFINES/sound.dm @@ -17,6 +17,44 @@ #define MAX_INSTRUMENT_CHANNELS (128 * 6) +/// This is the lowest volume that can be used by playsound otherwise it gets ignored +/// Most sounds around 10 volume can barely be heard. Almost all sounds at 5 volume or below are inaudible +/// This is to prevent sound being spammed at really low volumes due to distance calculations +/// Recommend setting this to anywhere from 10-3 (or 0 to disable any sound minimum volume restrictions) +/// Ex. For a 70 volume sound, 17 tile range, 3 exponent, 2 falloff_distance: +/// Setting SOUND_AUDIBLE_VOLUME_MIN to 0 for the above will result in 17x17 radius (289 turfs) +/// Setting SOUND_AUDIBLE_VOLUME_MIN to 5 for the above will result in 14x14 radius (196 turfs) +/// Setting SOUND_AUDIBLE_VOLUME_MIN to 10 for the above will result in 11x11 radius (121 turfs) +#define SOUND_AUDIBLE_VOLUME_MIN 3 + +/* Calculates the max distance of a sound based on audible volume + * + * Note - you should NEVER pass in a volume that is lower than SOUND_AUDIBLE_VOLUME_MIN otherwise distance will be insanely large (like +250,000) + * + * Arguments: + * * volume: The initial volume of the sound being played + * * max_distance: The range of the sound in tiles (technically not real max distance since the furthest areas gets pruned due to SOUND_AUDIBLE_VOLUME_MIN) + * * falloff_distance: Distance at which falloff begins. Sound is at peak volume (in regards to falloff) aslong as it is in this range. + * * falloff_exponent: Rate of falloff for the audio. Higher means quicker drop to low volume. Should generally be over 1 to indicate a quick dive to 0 rather than a slow dive. + * Returns: The max distance of a sound based on audible volume range + */ +#define CALCULATE_MAX_SOUND_AUDIBLE_DISTANCE(volume, max_distance, falloff_distance, falloff_exponent)\ + floor(((((-(max(max_distance - falloff_distance, 0) ** (1 / falloff_exponent)) / volume) * (SOUND_AUDIBLE_VOLUME_MIN - volume)) ** falloff_exponent) + falloff_distance)) + +/* Calculates the volume of a sound based on distance + * + * https://www.desmos.com/calculator/sqdfl8ipgf + * + * Arguments: + * * volume: The initial volume of the sound being played + * * distance: How far away the sound is in tiles from the source + * * falloff_distance: Distance at which falloff begins. Sound is at peak volume (in regards to falloff) aslong as it is in this range. + * * falloff_exponent: Rate of falloff for the audio. Higher means quicker drop to low volume. Should generally be over 1 to indicate a quick dive to 0 rather than a slow dive. + * Returns: The max distance of a sound based on audible volume range + */ +#define CALCULATE_SOUND_VOLUME(volume, distance, max_distance, falloff_distance, falloff_exponent)\ + ((max(distance - falloff_distance, 0) ** (1 / falloff_exponent)) / ((max(max_distance, distance) - falloff_distance) ** (1 / falloff_exponent)) * volume) + ///Default range of a sound. #define SOUND_RANGE 17 #define MEDIUM_RANGE_SOUND_EXTRARANGE -5 diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm index 54c2c3d80db..c1957c3581a 100644 --- a/code/__DEFINES/traits/declarations.dm +++ b/code/__DEFINES/traits/declarations.dm @@ -921,6 +921,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai // Debug traits /// This object has light debugging tools attached to it #define TRAIT_LIGHTING_DEBUGGED "lighting_debugged" +/// This object has sound debugging tools attached to it +#define TRAIT_SOUND_DEBUGGED "sound_debugged" /// Gives you the Shifty Eyes quirk, rarely making people who examine you think you examined them back even when you didn't #define TRAIT_SHIFTY_EYES "shifty_eyes" diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm index 8aa0ae2e11e..42932244758 100644 --- a/code/_globalvars/traits/_traits.dm +++ b/code/_globalvars/traits/_traits.dm @@ -492,6 +492,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_SNOB" = TRAIT_SNOB, "TRAIT_SOFTSPOKEN" = TRAIT_SOFTSPOKEN, "TRAIT_SOOTHED_THROAT" = TRAIT_SOOTHED_THROAT, + "TRAIT_SOUND_DEBUGGED" = TRAIT_SOUND_DEBUGGED, "TRAIT_SPACEWALK" = TRAIT_SPACEWALK, "TRAIT_SPARRING" = TRAIT_SPARRING, "TRAIT_SPEAKS_CLEARLY" = TRAIT_SPEAKS_CLEARLY, diff --git a/code/game/objects/items/storage/boxes/engineering_boxes.dm b/code/game/objects/items/storage/boxes/engineering_boxes.dm index 09f641ec315..b30aaed9f68 100644 --- a/code/game/objects/items/storage/boxes/engineering_boxes.dm +++ b/code/game/objects/items/storage/boxes/engineering_boxes.dm @@ -82,6 +82,7 @@ /obj/item/storage/box/material=1, /obj/item/uplink/debug=1, /obj/item/uplink/nuclear/debug=1, + /obj/item/clothing/ears/earmuffs/debug = 1, ) generate_items_inside(items_inside,src) diff --git a/code/game/sound.dm b/code/game/sound.dm index f460847bbcd..5f79ca9e3db 100644 --- a/code/game/sound.dm +++ b/code/game/sound.dm @@ -51,6 +51,9 @@ if (!turf_source || !soundin || !vol) return + if(vol < SOUND_AUDIBLE_VOLUME_MIN) // never let sound go below SOUND_AUDIBLE_VOLUME_MIN or bad things will happen + return + //allocate a channel if necessary now so its the same for everyone channel = channel || SSsounds.random_available_channel() @@ -64,8 +67,9 @@ var/turf/above_turf = GET_TURF_ABOVE(turf_source) var/turf/below_turf = GET_TURF_BELOW(turf_source) - if(ignore_walls) + var/audible_distance = CALCULATE_MAX_SOUND_AUDIBLE_DISTANCE(vol, maxdistance, falloff_distance, falloff_exponent) + if(ignore_walls) if(above_turf && istransparentturf(above_turf)) listeners += SSmobs.clients_by_zlevel[above_turf.z] @@ -73,16 +77,16 @@ listeners += SSmobs.clients_by_zlevel[below_turf.z] else //these sounds don't carry through walls - listeners = get_hearers_in_view(maxdistance, turf_source) + listeners = get_hearers_in_view(audible_distance, turf_source) if(above_turf && istransparentturf(above_turf)) - listeners += get_hearers_in_view(maxdistance, above_turf) + listeners += get_hearers_in_view(audible_distance, above_turf) if(below_turf && istransparentturf(turf_source)) - listeners += get_hearers_in_view(maxdistance, below_turf) + listeners += get_hearers_in_view(audible_distance, below_turf) for(var/mob/listening_mob in listeners | SSmobs.dead_players_by_zlevel[source_z])//observers always hear through walls - if(get_dist(listening_mob, turf_source) <= maxdistance) + if(get_dist(listening_mob, turf_source) <= audible_distance) listening_mob.playsound_local(turf_source, soundin, vol, vary, frequency, falloff_exponent, channel, pressure_affected, S, maxdistance, falloff_distance, 1, use_reverb) . += listening_mob @@ -122,15 +126,16 @@ else sound_to_use.frequency = get_rand_frequency() + var/distance = 0 + if(isturf(turf_source)) var/turf/turf_loc = get_turf(src) //sound volume falloff with distance - var/distance = get_dist(turf_loc, turf_source) * distance_multiplier + distance = get_dist(turf_loc, turf_source) * distance_multiplier if(max_distance) //If theres no max_distance we're not a 3D sound, so no falloff. - sound_to_use.volume -= (max(distance - falloff_distance, 0) ** (1 / falloff_exponent)) / ((max(max_distance, distance) - falloff_distance) ** (1 / falloff_exponent)) * sound_to_use.volume - //https://www.desmos.com/calculator/sqdfl8ipgf + sound_to_use.volume -= CALCULATE_SOUND_VOLUME(vol, distance, max_distance, falloff_distance, falloff_exponent) if(pressure_affected) //Atmosphere affects sound @@ -151,7 +156,7 @@ sound_to_use.volume *= pressure_factor //End Atmosphere affecting sound - if(sound_to_use.volume <= 0) + if(sound_to_use.volume < SOUND_AUDIBLE_VOLUME_MIN) return //No sound var/dx = turf_source.x - turf_loc.x // Hearing from the right/left @@ -176,6 +181,9 @@ sound_to_use.echo[3] = 0 //Room setting, 0 means normal reverb sound_to_use.echo[4] = 0 //RoomHF setting, 0 means normal reverb. + if(HAS_TRAIT(src, TRAIT_SOUND_DEBUGGED)) + to_chat(src, span_admin("Max Range-[max_distance] Distance-[distance] Vol-[round(sound_to_use.volume, 0.01)] Sound-[sound_to_use.file]")) + SEND_SOUND(src, sound_to_use) /proc/sound_to_playing_players(soundin, volume = 100, vary = FALSE, frequency = 0, channel = 0, pressure_affected = FALSE, sound/S) diff --git a/code/modules/clothing/ears/_ears.dm b/code/modules/clothing/ears/_ears.dm index 7520e6e769b..c1898c0ab9d 100644 --- a/code/modules/clothing/ears/_ears.dm +++ b/code/modules/clothing/ears/_ears.dm @@ -27,3 +27,8 @@ AddElement(/datum/element/earhealing) AddComponent(/datum/component/wearertargeting/earprotection, list(ITEM_SLOT_EARS)) AddComponent(/datum/component/adjust_fishing_difficulty, -2) + +/obj/item/clothing/ears/earmuffs/debug + name = "debug earmuffs" + desc = "Wearing these sends a chat message for every sound played. Walking to ignore footsteps is highly recommended." + clothing_traits = list(TRAIT_SOUND_DEBUGGED)