diff --git a/code/__DEFINES/dcs/signals/signals_global.dm b/code/__DEFINES/dcs/signals/signals_global.dm index 01bce2073f6..81c37f60afa 100644 --- a/code/__DEFINES/dcs/signals/signals_global.dm +++ b/code/__DEFINES/dcs/signals/signals_global.dm @@ -108,3 +108,8 @@ /// Global signal whenever a camera network broadcast is started/stopped/updated: (camera_net, is_show_active, announcement) #define COMSIG_GLOB_NETWORK_BROADCAST_UPDATED "!network_broadcast_updated" +///Global signal sent when the player list grows. Called by [mob/add_to_player_list] (mob/player) +#define COMSIG_GLOB_PLAYER_LOGIN "!player_login" + +///Global signal sent when the player list shrinks. Called by [mob/remove_from_player_list] (mob/player) +#define COMSIG_GLOB_PLAYER_LOGOUT "!player_logout" diff --git a/code/__DEFINES/sound.dm b/code/__DEFINES/sound.dm index 1e6a51e23b2..ce7c9cfeac4 100644 --- a/code/__DEFINES/sound.dm +++ b/code/__DEFINES/sound.dm @@ -64,9 +64,9 @@ ///The range deducted from sound range for things that are considered silent / sneaky #define SILENCED_SOUND_EXTRARANGE -11 ///Percentage of sound's range where no falloff is applied -#define SOUND_DEFAULT_FALLOFF_DISTANCE 1 //For a normal sound this would be 1 tile of no falloff +#define SOUND_DEFAULT_FALLOFF_DISTANCE 0 //Disabled because it doesn't actually have a nice effect, it just makes the jump to fall-off more shocking. maybe delete ///The default exponent of sound falloff -#define SOUND_FALLOFF_EXPONENT 6 +#define SOUND_FALLOFF_EXPONENT 2.5 #define SOUND_MINIMUM_PRESSURE 10 diff --git a/code/controllers/subsystem/processing/instruments.dm b/code/controllers/subsystem/processing/instruments.dm index 197c16047b7..a68a8ff77a6 100644 --- a/code/controllers/subsystem/processing/instruments.dm +++ b/code/controllers/subsystem/processing/instruments.dm @@ -52,6 +52,6 @@ PROCESSING_SUBSYSTEM_DEF(instruments) /datum/controller/subsystem/processing/instruments/proc/reserve_instrument_channel(datum/instrument/I) if(current_instrument_channels > max_instrument_channels) return - . = SSsounds.reserve_sound_channel(I) + . = SSsounds.reserve_sound_channel_for_datum(I) if(!isnull(.)) current_instrument_channels++ diff --git a/code/controllers/subsystem/sound_tokens.dm b/code/controllers/subsystem/sound_tokens.dm new file mode 100644 index 00000000000..dc4bd1600c3 --- /dev/null +++ b/code/controllers/subsystem/sound_tokens.dm @@ -0,0 +1,24 @@ +SUBSYSTEM_DEF(sound_tokens) + name = "Sound Tokens" + wait = 1 + ss_flags = SS_TICKER | SS_BACKGROUND | SS_NO_INIT + + + var/list/clients_needing_update = list() + var/list/currentrun = list() + +/datum/controller/subsystem/sound_tokens/fire(resumed) + if(!resumed) + currentrun = clients_needing_update + clients_needing_update = list() + while(length(currentrun)) + var/client/client = currentrun[currentrun.len] + currentrun.len-- + var/mob/owned_mob = client.mob + if(!owned_mob) + continue + for(var/datum/sound_token/token in client.sound_tokens) + token.update_listener(owned_mob) + if(MC_TICK_CHECK) + break + diff --git a/code/controllers/subsystem/sounds.dm b/code/controllers/subsystem/sounds.dm index db3f94a080b..360f8a883e5 100644 --- a/code/controllers/subsystem/sounds.dm +++ b/code/controllers/subsystem/sounds.dm @@ -114,10 +114,10 @@ SUBSYSTEM_DEF(sounds) var/text_channel = num2text(channel) var/using = using_channels[text_channel] using_channels -= text_channel - if(using != TRUE) // datum channel + if(using != DATUMLESS) // datum channel using_channels_by_datum[using] -= channel if(!length(using_channels_by_datum[using])) - using_channels_by_datum -= using + stop_tracking_datum(using) free_channel(channel) /// Frees all the channels a datum is using. @@ -128,14 +128,14 @@ SUBSYSTEM_DEF(sounds) for(var/channel in L) using_channels -= num2text(channel) free_channel(channel) - using_channels_by_datum -= D + stop_tracking_datum(D) /// Frees all datumless channels /datum/controller/subsystem/sounds/proc/free_datumless_channels() free_datum_channels(DATUMLESS) -/// NO AUTOMATIC CLEANUP - If you use this, you better manually free it later! Returns an integer for channel. -/datum/controller/subsystem/sounds/proc/reserve_sound_channel_datumless() +/// Reserve a sound channel. Free it later with free_sound_channel() +/datum/controller/subsystem/sounds/proc/reserve_sound_channel() . = reserve_channel() if(!.) //oh no.. return FALSE @@ -145,7 +145,7 @@ SUBSYSTEM_DEF(sounds) using_channels_by_datum[DATUMLESS] += . /// Reserves a channel for a datum. Automatic cleanup only when the datum is deleted. Returns an integer for channel. -/datum/controller/subsystem/sounds/proc/reserve_sound_channel(datum/D) +/datum/controller/subsystem/sounds/proc/reserve_sound_channel_for_datum(datum/D) if(!D) //i don't like typechecks but someone will fuck it up CRASH("Attempted to reserve sound channel without datum using the managed proc.") .= reserve_channel() @@ -156,6 +156,8 @@ SUBSYSTEM_DEF(sounds) LAZYINITLIST(using_channels_by_datum[D]) using_channels_by_datum[D] += . + RegisterSignal(D, COMSIG_QDELETING, PROC_REF(tracked_datum_deleted)) + /** * Reserves a channel and updates the datastructure. Private proc. */ @@ -260,4 +262,19 @@ SUBSYSTEM_DEF(sounds) if(!isnull(sfx.key)) GLOB.sfx_datum_by_key[sfx.key] = new sfx() + +///Call to free all channels reserved by a datum. +/datum/controller/subsystem/sounds/proc/stop_tracking_datum(datum/D) + PRIVATE_PROC(TRUE) + + using_channels_by_datum -= D + UnregisterSignal(D, COMSIG_QDELETING) + +/// Handles a tracked datum being deleted, automatically freeing the channels. +/datum/controller/subsystem/sounds/proc/tracked_datum_deleted(datum/source) + SIGNAL_HANDLER + PRIVATE_PROC(TRUE) + + free_datum_channels(source) + #undef DATUMLESS diff --git a/code/datums/looping_sounds/_looping_sound.dm b/code/datums/looping_sounds/_looping_sound.dm index 7080033c63f..d4bd2f0ddf9 100644 --- a/code/datums/looping_sounds/_looping_sound.dm +++ b/code/datums/looping_sounds/_looping_sound.dm @@ -67,6 +67,10 @@ var/reserve_random_channel = FALSE //If we reserve a random sound channel, store the channel number here so we can clean it up later. var/reserved_channel + ///Whether this looping sound uses sound tokens. This should only be true for sounds that need to update as the source or listeners move. (Generally long or important sounds like grav-gen) + var/use_sound_tokens = FALSE + ///The sound token instance for this looping sound. + var/datum/sound_token/sound_token_instance /datum/looping_sound/New( _parent, @@ -104,12 +108,12 @@ if(timer_id) return - if(!sound_channel && reserve_random_channel) - sound_channel = SSsounds.reserve_sound_channel_datumless() + if(!use_sound_tokens && !sound_channel && reserve_random_channel) + sound_channel = SSsounds.reserve_sound_channel() reserved_channel = sound_channel - on_start() + /** * The proc to call to stop the sound loop. * @@ -171,6 +175,14 @@ * * volume_override - The volume we want to play the sound at, overriding the `volume` variable. */ /datum/looping_sound/proc/play(soundfile, volume_override) + + if(use_sound_tokens) + if(sound_token_instance) + sound_token_instance.set_volume(volume_override || volume, FALSE) // Don't update, we'll do that after + sound_token_instance.update_sound(soundfile, TRUE) + else + sound_token_instance = new /datum/sound_token(parent, soundfile, SOUND_RANGE + extra_range, volume_override || volume, falloff_exponent, falloff_distance) + return var/sound/sound_to_play = sound(soundfile) sound_to_play.channel = sound_channel || SSsounds.random_available_channel() sound_to_play.volume = volume_override || volume //Use volume as fallback if theres no override @@ -248,6 +260,7 @@ /// Stops sound playing on current channel, if specified /datum/looping_sound/proc/stop_current() + QDEL_NULL(sound_token_instance) if(!sound_channel || !ismob(parent)) return var/mob/mob_parent = parent diff --git a/code/datums/looping_sounds/burning.dm b/code/datums/looping_sounds/burning.dm index 191ae88db89..8673a9ab1de 100644 --- a/code/datums/looping_sounds/burning.dm +++ b/code/datums/looping_sounds/burning.dm @@ -7,3 +7,4 @@ volume = 50 vary = TRUE extra_range = MEDIUM_RANGE_SOUND_EXTRARANGE + use_sound_tokens = TRUE diff --git a/code/datums/looping_sounds/changeling_absorb.dm b/code/datums/looping_sounds/changeling_absorb.dm index 418c2a8dacf..6b28c56ee47 100644 --- a/code/datums/looping_sounds/changeling_absorb.dm +++ b/code/datums/looping_sounds/changeling_absorb.dm @@ -12,3 +12,4 @@ mid_length = 3 SECONDS volume = 80 ignore_walls = FALSE + use_sound_tokens = TRUE diff --git a/code/datums/looping_sounds/choking.dm b/code/datums/looping_sounds/choking.dm index 6d337b1c7d6..bf0222b038e 100644 --- a/code/datums/looping_sounds/choking.dm +++ b/code/datums/looping_sounds/choking.dm @@ -10,3 +10,4 @@ vary = TRUE // Same as above ignore_walls = FALSE + use_sound_tokens = TRUE diff --git a/code/datums/looping_sounds/cyborg.dm b/code/datums/looping_sounds/cyborg.dm index 0a01a4c7116..1b23271ee33 100644 --- a/code/datums/looping_sounds/cyborg.dm +++ b/code/datums/looping_sounds/cyborg.dm @@ -8,3 +8,4 @@ end_sound = 'sound/mobs/non-humanoids/cyborg/wash_end.ogg' vary = TRUE extra_range = 5 + use_sound_tokens = TRUE diff --git a/code/datums/looping_sounds/drip.dm b/code/datums/looping_sounds/drip.dm index 224d7850fc2..2c6d21e273a 100644 --- a/code/datums/looping_sounds/drip.dm +++ b/code/datums/looping_sounds/drip.dm @@ -7,3 +7,4 @@ vary = TRUE ignore_walls = FALSE falloff_distance = 5 + use_sound_tokens = TRUE diff --git a/code/datums/looping_sounds/item_sounds.dm b/code/datums/looping_sounds/item_sounds.dm index e5950566d4f..517dc752944 100644 --- a/code/datums/looping_sounds/item_sounds.dm +++ b/code/datums/looping_sounds/item_sounds.dm @@ -2,16 +2,19 @@ mid_sounds = list('sound/effects/clock_tick.ogg' = 1) mid_length = 0.35 SECONDS volume = 25 + use_sound_tokens = TRUE /datum/looping_sound/reverse_bear_trap_beep mid_sounds = list('sound/machines/beep/beep.ogg' = 1) mid_length = 6 SECONDS volume = 10 + use_sound_tokens = TRUE /datum/looping_sound/siren mid_sounds = list('sound/items/weeoo1.ogg' = 1) mid_length = 1.5 SECONDS volume = 20 + use_sound_tokens = TRUE /datum/looping_sound/tape_recorder_hiss mid_sounds = list('sound/items/taperecorder/taperecorder_hiss_mid.ogg' = 1) @@ -29,6 +32,7 @@ falloff_exponent = 10 falloff_distance = 1 volume = 5 + use_sound_tokens = TRUE /datum/looping_sound/chainsaw start_sound = list('sound/items/weapons/chainsaw_start.ogg' = 1) @@ -39,6 +43,7 @@ end_volume = 35 volume = 40 ignore_walls = FALSE + use_sound_tokens = TRUE /datum/looping_sound/beesmoke mid_sounds = list('sound/items/weapons/beesmoke.ogg' = 1) @@ -59,3 +64,4 @@ end_volume = 15 ignore_walls = FALSE reserve_random_channel = TRUE + use_sound_tokens = TRUE diff --git a/code/datums/looping_sounds/machinery_sounds.dm b/code/datums/looping_sounds/machinery_sounds.dm index 871a5ca7bf9..9ab93ae378e 100644 --- a/code/datums/looping_sounds/machinery_sounds.dm +++ b/code/datums/looping_sounds/machinery_sounds.dm @@ -18,13 +18,15 @@ falloff_exponent = 10 falloff_distance = 5 vary = TRUE + use_sound_tokens = TRUE /datum/looping_sound/destabilized_crystal mid_sounds = list('sound/machines/sm/loops/delamming.ogg') mid_length = 6 SECONDS volume = 55 - extra_range = 15 + extra_range = 35 vary = TRUE + use_sound_tokens = TRUE /datum/looping_sound/hypertorus mid_sounds = list('sound/machines/hypertorus/loops/hypertorus_nominal.ogg') @@ -32,6 +34,7 @@ volume = 55 extra_range = 15 vary = TRUE + use_sound_tokens = TRUE /datum/looping_sound/generator start_sound = 'sound/machines/generator/generator_start.ogg' @@ -52,9 +55,10 @@ 'sound/machines/fryer/deep_fryer_1.ogg', 'sound/machines/fryer/deep_fryer_2.ogg', ) - mid_length = 0.2 SECONDS + mid_length = 1 SECONDS end_sound = 'sound/machines/fryer/deep_fryer_emerge.ogg' volume = 15 + use_sound_tokens = TRUE /datum/looping_sound/clock mid_sounds = list('sound/ambience/misc/ticking_clock.ogg') @@ -66,6 +70,7 @@ mid_sounds = list('sound/machines/grill/grillsizzle.ogg') mid_length = 18 volume = 50 + use_sound_tokens = TRUE /datum/looping_sound/oven start_sound = 'sound/machines/oven/oven_loop_start.ogg' //my immersions @@ -75,14 +80,7 @@ end_sound = 'sound/machines/oven/oven_loop_end.ogg' volume = 100 falloff_exponent = 4 - -/datum/looping_sound/deep_fryer - mid_length = 0.2 SECONDS - mid_sounds = list( - 'sound/machines/fryer/deep_fryer_1.ogg', - 'sound/machines/fryer/deep_fryer_2.ogg', - ) - volume = 30 + use_sound_tokens = TRUE /datum/looping_sound/microwave start_sound = 'sound/machines/microwave/microwave-start.ogg' @@ -94,6 +92,7 @@ mid_length = 1 SECONDS end_sound = 'sound/machines/microwave/microwave-end.ogg' volume = 90 + use_sound_tokens = TRUE /datum/looping_sound/lathe_print mid_sounds = list('sound/machines/lathe/lathe_print.ogg') @@ -103,12 +102,14 @@ ignore_walls = FALSE falloff_distance = 1 mid_length_vary = 1 SECONDS + use_sound_tokens = TRUE /datum/looping_sound/jackpot mid_length = 1.1 SECONDS mid_sounds = list('sound/machines/roulette/roulettejackpot.ogg') volume = 85 vary = TRUE + use_sound_tokens = TRUE /datum/looping_sound/server mid_sounds = list( @@ -140,9 +141,10 @@ end_sound = 'sound/machines/computer/computer_end.ogg' end_volume = 1 SECONDS volume = SOUND_AUDIBLE_VOLUME_MIN - falloff_exponent = 5 //Ultra quiet very fast + falloff_exponent = 4 //Ultra quiet very fast extra_range = -12 - falloff_distance = 1 //Instant falloff after initial tile + falloff_distance = 0 //Instant falloff after initial tile + use_sound_tokens = TRUE /datum/looping_sound/gravgen start_sound = 'sound/machines/gravgen/grav_gen_start.ogg' @@ -157,7 +159,8 @@ vary = TRUE volume = 70 falloff_distance = 5 - falloff_exponent = 20 + falloff_exponent = 10 + use_sound_tokens = TRUE /datum/looping_sound/firealarm mid_sounds = list( @@ -168,16 +171,19 @@ ) mid_length = 2.4 SECONDS volume = 30 + use_sound_tokens = TRUE /datum/looping_sound/gravgen/kinesis volume = 20 falloff_distance = 2 falloff_exponent = 5 + use_sound_tokens = TRUE /datum/looping_sound/boiling mid_sounds = list('sound/effects/bubbles/bubbles2.ogg') mid_length = 7 SECONDS volume = 25 + use_sound_tokens = TRUE /datum/looping_sound/typing mid_sounds = list( @@ -205,7 +211,8 @@ end_sound = 'sound/effects/soup_boil/soup_boil_end.ogg' end_volume = 60 extra_range = MEDIUM_RANGE_SOUND_EXTRARANGE - falloff_exponent = 4 + falloff_exponent = 3 + use_sound_tokens = TRUE /datum/looping_sound/soup/toxic volume = 40 @@ -225,3 +232,4 @@ ) mid_length = 5 SECONDS volume = 50 + use_sound_tokens = TRUE diff --git a/code/datums/looping_sounds/projectiles.dm b/code/datums/looping_sounds/projectiles.dm index ca96df698e9..1c2dc9b19b7 100644 --- a/code/datums/looping_sounds/projectiles.dm +++ b/code/datums/looping_sounds/projectiles.dm @@ -2,3 +2,4 @@ mid_sounds = list('sound/effects/moon_parade_soundloop.ogg' = 1) mid_length = 2 SECONDS volume = 20 + use_sound_tokens = TRUE diff --git a/code/datums/looping_sounds/vents.dm b/code/datums/looping_sounds/vents.dm index 183c337ef98..04e0246a4ee 100644 --- a/code/datums/looping_sounds/vents.dm +++ b/code/datums/looping_sounds/vents.dm @@ -2,5 +2,6 @@ start_sound = 'sound/machines/fan/fan_start.ogg' start_length = 1.5 SECONDS end_sound = 'sound/machines/fan/fan_stop.ogg' - end_sound = 1.5 SECONDS mid_sounds = 'sound/machines/fan/fan_loop.ogg' + mid_length = 1.9 SECONDS + use_sound_tokens = TRUE diff --git a/code/datums/sound_token.dm b/code/datums/sound_token.dm new file mode 100644 index 00000000000..d9ad9903c28 --- /dev/null +++ b/code/datums/sound_token.dm @@ -0,0 +1,284 @@ +// Sound tokens, a datumized handler for spatial sound. +// Uses the spatial grid to track clients in range and add them as listeners +// Updated by the SSsound_tokens subsystem every tick when requested by client so that if the source or listener moves, the sound updates accordingly. +/datum/sound_token + /// The atom playing the sound. + var/atom/source + /// k:v list of mob : sound status + var/list/listeners = list() + + /// Sound maximum range + var/range + /// Sound volume + var/volume + /// Sound falloff + var/falloff_exponent + /// Sound falloff distance + var/falloff_distance + + /// The master copy of the playing sound. + var/sound/sound + /// Null sound for cancelling the sound entirely. + var/sound/null_sound + + /// Status of the playing sound + var/sound_status = NONE + /// The channel being used. + var/sound_channel + /// world.time when the sound started (or when the sound file was last changed). Used to calculate playback offset for new listeners. + var/start_time + /// Duration of the current sound file in deciseconds. Used to wrap offset for looping sounds. + var/sound_duration + /// Cell tracker managing spatial grid cells within range of the source. The wizards say this is the fastest. + var/datum/cell_tracker/cell_tracker + +/datum/sound_token/New(atom/_source, _sound, _range = 10, _volume = 50, _falloff_exponent = SOUND_FALLOFF_EXPONENT, _falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE) + source = _source + RegisterSignal(source, COMSIG_QDELETING, PROC_REF(source_deleted)) + RegisterSignal(source, COMSIG_MOVABLE_MOVED, PROC_REF(source_moved)) + RegisterSignal(source, COMSIG_ENTER_AREA, PROC_REF(on_enter_area)) + + range = _range + volume = _volume + falloff_exponent = _falloff_exponent + falloff_distance = _falloff_distance + + update_sound(_sound) + + null_sound = sound(channel = sound_channel) + + cell_tracker = new /datum/cell_tracker(range, range) + update_tracked_cells() + + RegisterSignal(SSdcs, COMSIG_GLOB_PLAYER_LOGIN, PROC_REF(player_login)) + RegisterSignal(SSdcs, COMSIG_GLOB_PLAYER_LOGOUT, PROC_REF(player_logout)) + +/datum/sound_token/Destroy(force, ...) + for(var/listener in listeners) + remove_listener(listener) + + listeners = null + source = null + return ..() + +///Lets us update the sound to a new one. +/datum/sound_token/proc/update_sound(_sound, start_playing = FALSE) + sound = sound(_sound) + if(!sound_channel) + sound_channel = SSsounds.reserve_sound_channel_for_datum(src) + sound.channel = sound_channel + sound_duration = SSsounds.get_sound_length(_sound) + start_time = REALTIMEOFDAY + if(start_playing) + force_update_all_listeners(FALSE) + +/// Updates the data of a listener, or adds them if they are not present. +/datum/sound_token/proc/add_or_update_listener(mob/listener_mob) + if(isnull(listeners[listener_mob])) + add_listener(listener_mob) + else + update_listener(listener_mob) + +/// Adds a listener to the sound. +/datum/sound_token/proc/add_listener(mob/listener_mob) + if(!isnull(listeners[listener_mob])) + return FALSE + + if(!listener_mob.client || isnewplayer(listener_mob)) + return + + listeners[listener_mob] = NONE + listener_mob.client.sound_tokens += src + RegisterSignal(listener_mob, COMSIG_QDELETING, PROC_REF(listener_deleted)) + RegisterSignals(listener_mob, list(SIGNAL_ADDTRAIT(TRAIT_DEAF), SIGNAL_REMOVETRAIT(TRAIT_DEAF)), PROC_REF(listener_deafness_update)) + update_listener(listener_mob, FALSE) + return TRUE + +/// Remove a listener from the sound. +/datum/sound_token/proc/remove_listener(mob/listener_mob) + + listeners -= listener_mob + + if(listener_mob.client) + listener_mob.client.sound_tokens -= src + + UnregisterSignal(listener_mob, list(COMSIG_QDELETING, SIGNAL_ADDTRAIT(TRAIT_DEAF),SIGNAL_REMOVETRAIT(TRAIT_DEAF))) + SEND_SOUND(listener_mob, null_sound) + +/datum/sound_token/proc/update_listener(mob/listener_mob, update_sound = TRUE) + if(QDELETED(src)) + return + if(isnull(listeners[listener_mob])) + return + + var/turf/source_turf = get_turf(source) + var/turf/listener_turf = get_turf(listener_mob) + + if(!source_turf || !listener_turf) + return + + var/is_muted = listeners[listener_mob] & SOUND_MUTE + var/should_be_muted = FALSE + + if(source_turf.z != listener_turf.z) + should_be_muted = TRUE + + var/distance = get_dist(source_turf, listener_turf) + if(distance > range) + should_be_muted = TRUE + if(should_be_muted && is_muted) + return + + should_be_muted ||= HAS_TRAIT(listener_mob, TRAIT_DEAF) + if(should_be_muted && is_muted) + return + + set_listener_status(listener_mob, should_be_muted ? SOUND_MUTE : NONE) + send_listener_sound(listener_mob, update_sound) + +/datum/sound_token/proc/send_listener_sound(mob/listener_mob, update_sound) + PRIVATE_PROC(TRUE) + + sound.status = SOUND_STREAM|sound_status|listeners[listener_mob] + if(update_sound) + sound.status |= SOUND_UPDATE + else + sound.offset = calculate_offset() + + if(sound.status & SOUND_MUTE) + SEND_SOUND(listener_mob, sound) + return + + if(!listener_mob.playsound_local(get_turf(source), vol = volume, falloff_exponent = falloff_exponent, channel = sound_channel, sound_to_use = sound, max_distance = range, falloff_distance = falloff_distance, use_reverb = TRUE)) + sound.status = SOUND_UPDATE|SOUND_MUTE + SEND_SOUND(listener_mob, sound) + sound.offset = null + +/datum/sound_token/proc/update_all_listeners() + for(var/mob/listener_mob in listeners) + if(listener_mob.client) + SSsound_tokens.clients_needing_update[listener_mob.client] = TRUE + +/datum/sound_token/proc/force_update_all_listeners(update_sound = TRUE) + for(var/mob/listener_mob in listeners) + if(listener_mob.client) + update_listener(listener_mob, update_sound) + +/// Setter for volume +/datum/sound_token/proc/set_volume(new_volume, update_listeners = TRUE) + volume = new_volume + if(update_listeners) + update_all_listeners() + +/// Set the status of a listener. Does not update the sound. +/datum/sound_token/proc/set_listener_status(mob/listener_mob, new_status) + if(isnull(listeners[listener_mob])) + return + + listeners[listener_mob] = new_status + +/// Respond to TRAIT_DEAF addition/removal +/datum/sound_token/proc/listener_deafness_update(atom/movable/source) + SIGNAL_HANDLER + update_listener(source) + +/datum/sound_token/proc/listener_deleted(datum/source) + SIGNAL_HANDLER + remove_listener(source) + +/// Respond to any mob in the world being logged into. Only adds if the mob is within range. +/datum/sound_token/proc/player_login(datum/source, mob/player) + SIGNAL_HANDLER + var/turf/player_turf = get_turf(player) + var/turf/source_turf = get_turf(src.source) + if(!player_turf || !source_turf) + return + if(player_turf.z != source_turf.z) + return + if(get_dist(source_turf, player_turf) > range) + return + add_or_update_listener(player) + +/// Respond to any cliented mob becoming uncliented +/datum/sound_token/proc/player_logout(datum/source, mob/player) + SIGNAL_HANDLER + remove_listener(player) + +/// If the sound source moves, update tracked cells then refresh all listener positions. +/datum/sound_token/proc/source_moved() + SIGNAL_HANDLER + update_tracked_cells() + update_all_listeners() + +/datum/sound_token/proc/source_deleted() + SIGNAL_HANDLER + + qdel(src) + +///Update env when source is entering new area +/datum/sound_token/proc/on_enter_area(datum/source, area/area_to_register) + SIGNAL_HANDLER + set_new_environment(area_to_register.sound_environment || SOUND_ENVIRONMENT_NONE) + +/datum/sound_token/proc/set_new_environment(new_env) + if(sound.environment == new_env) + return + sound.environment = new_env + update_all_listeners() + +///Calculates the offset to give the sound for people who start hearing it mid-play +/datum/sound_token/proc/calculate_offset() + var/elapsed = REALTIMEOFDAY - start_time + var/freq_factor = (sound.frequency || 100) / 100 + var/pitch_factor = (sound.pitch || 100) / 100 + var/offset = elapsed * freq_factor * pitch_factor + return offset + +///Update tracked cells; happens on movement. We need to check if anyone is now out of cell range and kick them out. +/datum/sound_token/proc/update_tracked_cells() + if(!get_turf(source)) + return + + var/list/new_and_old = cell_tracker.recalculate_cells(get_turf(source)) + var/list/datum/spatial_grid_cell/added_cells = new_and_old[1] + var/list/datum/spatial_grid_cell/removed_cells = new_and_old[2] + + for(var/datum/spatial_grid_cell/cell as anything in removed_cells) + UnregisterSignal(cell, list(SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), SPATIAL_GRID_CELL_EXITED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS),)) + + // Remove listeners whose mob is no longer in any remaining member cell + if(removed_cells.len) + for(var/mob/listener_mob as anything in listeners) + var/still_in_range = FALSE + for(var/datum/spatial_grid_cell/cell as anything in cell_tracker.member_cells) + if(listener_mob in cell.client_contents) + still_in_range = TRUE + break + if(!still_in_range) + remove_listener(listener_mob) + + for(var/datum/spatial_grid_cell/cell as anything in added_cells) + RegisterSignal(cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), PROC_REF(on_cell_client_entered)) + RegisterSignal(cell, SPATIAL_GRID_CELL_EXITED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), PROC_REF(on_cell_client_exited)) + for(var/mob/listener_mob as anything in cell.client_contents) + add_or_update_listener(listener_mob) + +/// Signal handler for SPATIAL_GRID_CELL_ENTERED on tracked cells. Adds newly arriving mobs as listeners. +/datum/sound_token/proc/on_cell_client_entered(datum/source, list/entering_mobs) + SIGNAL_HANDLER + + for(var/mob/listener_mob as anything in entering_mobs) + if(!isnull(listeners[listener_mob])) // already added + continue + add_or_update_listener(listener_mob) + +/// Signal handler for SPATIAL_GRID_CELL_EXITED on tracked cells. Removes mobs who have left all member cells. +/datum/sound_token/proc/on_cell_client_exited(datum/source, list/exiting_mobs) + SIGNAL_HANDLER + for(var/mob/listener_mob as anything in exiting_mobs) + var/still_in_range = FALSE + if(SSspatial_grid.get_cell_of(listener_mob) in cell_tracker.member_cells) + still_in_range = TRUE + + if(!still_in_range) + remove_listener(listener_mob) diff --git a/code/game/objects/structures/mystery_box.dm b/code/game/objects/structures/mystery_box.dm index 28700113381..0e153b32813 100644 --- a/code/game/objects/structures/mystery_box.dm +++ b/code/game/objects/structures/mystery_box.dm @@ -189,7 +189,7 @@ GLOBAL_LIST_INIT(mystery_fishing, list( presented_item.vis_flags = VIS_INHERIT_PLANE vis_contents += presented_item presented_item.start_animation(src) - current_sound_channel = SSsounds.reserve_sound_channel(src) + current_sound_channel = SSsounds.reserve_sound_channel_for_datum(src) playsound(src, open_sound, 70, FALSE, channel = current_sound_channel, falloff_exponent = 10) playsound(src, crate_open_sound, 80) if(user.mind) diff --git a/code/game/sound/sound.dm b/code/game/sound/sound.dm index 451ee6d60a4..92b2e6f19e9 100644 --- a/code/game/sound/sound.dm +++ b/code/game/sound/sound.dm @@ -145,7 +145,7 @@ //End Atmosphere affecting sound if(sound_to_use.volume < SOUND_AUDIBLE_VOLUME_MIN) - return //No sound + return FALSE var/dx = turf_source.x - turf_loc.x // Hearing from the right/left sound_to_use.x = dx * distance_multiplier @@ -175,12 +175,13 @@ var/client_volume_modifier = client.prefs.read_preference(volume_preference) sound_to_use.volume *= (client_volume_modifier / 100) if(sound_to_use.volume < SOUND_AUDIBLE_VOLUME_MIN) - return + return FALSE 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) + return TRUE /proc/sound_to_playing_players(soundin, volume = 100, vary = FALSE, frequency = 0, channel = 0, pressure_affected = FALSE, sound/S, datum/preference/numeric/volume/volume_preference = null) if(!S) diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index a49169b9084..84dd50428ef 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -79,6 +79,9 @@ //SOUND STUFF// /////////////// + /// Sound tokens currently playing for this client. Managed by /datum/sound_token and the soundtoken subsystem!! SOUND TOKENS 2026 + var/list/datum/sound_token/sound_tokens = list() + //////////// //SECURITY// //////////// diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 2f0f10646e6..ff0077c7251 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -635,6 +635,8 @@ GLOBAL_LIST_INIT(unrecommended_builds, list( SSambience.remove_ambience_client(src) SSmouse_entered.hovers -= src SSping.currentrun -= src + SSsound_tokens.clients_needing_update -= src + SSsound_tokens.currentrun -= src QDEL_NULL(view_size) QDEL_NULL(void) QDEL_NULL(tooltips) diff --git a/code/modules/flufftext/dreaming.dm b/code/modules/flufftext/dreaming.dm index 73520a1625c..a97054affe0 100644 --- a/code/modules/flufftext/dreaming.dm +++ b/code/modules/flufftext/dreaming.dm @@ -183,7 +183,7 @@ GLOBAL_LIST_INIT(dreams, populate_dream_list()) addtimer(CALLBACK(src, PROC_REF(StopSound), dreamer), 5 SECONDS) /datum/dream/hear_something/proc/ReserveSoundChannel() - reserved_sound_channel = SSsounds.reserve_sound_channel(src) + reserved_sound_channel = SSsounds.reserve_sound_channel_for_datum(src) UnregisterSignal(SSsounds, COMSIG_SUBSYSTEM_POST_INITIALIZE) /datum/dream/hear_something/proc/PlayRandomSound(mob/living/carbon/dreamer) diff --git a/code/modules/instruments/songs/_song.dm b/code/modules/instruments/songs/_song.dm index f0df050b5c4..04786785bde 100644 --- a/code/modules/instruments/songs/_song.dm +++ b/code/modules/instruments/songs/_song.dm @@ -120,6 +120,10 @@ var/cached_exponential_dropoff = 1.045 ///////////////////////////////////////////////////////////////////////// + + ///Rate at which volume goes down to 0. Not controlled in menu. + var/exponential_falloff = 4 + /datum/song/New(atom/parent, list/instrument_ids, new_range) SSinstruments.on_song_new(src) lines = list() diff --git a/code/modules/instruments/songs/play_legacy.dm b/code/modules/instruments/songs/play_legacy.dm index bb60d63c3b5..8513b858217 100644 --- a/code/modules/instruments/songs/play_legacy.dm +++ b/code/modules/instruments/songs/play_legacy.dm @@ -88,5 +88,5 @@ var/pref_volume = M?.client?.prefs.read_preference(/datum/preference/numeric/volume/sound_instruments) if(!pref_volume) continue - M.playsound_local(source, null, volume * using_instrument.volume_multiplier * (pref_volume/100), sound_to_use = music_played) + M.playsound_local(source, null, volume * using_instrument.volume_multiplier * (pref_volume/100), sound_to_use = music_played, falloff_exponent = exponential_falloff) // Could do environment and echo later but not for now diff --git a/code/modules/instruments/songs/play_synthesized.dm b/code/modules/instruments/songs/play_synthesized.dm index 6f9cf2280b2..92ae4d369c5 100644 --- a/code/modules/instruments/songs/play_synthesized.dm +++ b/code/modules/instruments/songs/play_synthesized.dm @@ -67,7 +67,7 @@ var/pref_volume = M?.client?.prefs.read_preference(/datum/preference/numeric/volume/sound_instruments) if(!pref_volume) continue - M.playsound_local(get_turf(parent), null, volume * (pref_volume/100), FALSE, K.frequency, null, channel, null, copy) + M.playsound_local(get_turf(parent), null, volume * (pref_volume/100), FALSE, K.frequency, exponential_falloff, channel, null, copy) // Could do environment and echo later but not for now /** diff --git a/code/modules/mob/mob_lists.dm b/code/modules/mob/mob_lists.dm index f47f316c078..d020f9df0f2 100644 --- a/code/modules/mob/mob_lists.dm +++ b/code/modules/mob/mob_lists.dm @@ -46,21 +46,26 @@ ///Adds the cliented mob reference to the list of all player-mobs, besides to either the of dead or alive player-mob lists, as appropriate. Called on Login(). /mob/proc/add_to_player_list() SHOULD_CALL_PARENT(TRUE) + + GLOB.player_list |= src if(stat == DEAD) add_to_current_dead_players() else add_to_current_living_players() + SEND_GLOBAL_SIGNAL(COMSIG_GLOB_PLAYER_LOGIN, src) ///Removes the mob reference from the list of all player-mobs, besides from either the of dead or alive player-mob lists, as appropriate. Called on Logout(). /mob/proc/remove_from_player_list() SHOULD_CALL_PARENT(TRUE) + GLOB.player_list -= src GLOB.keyloop_list -= src if(stat == DEAD) remove_from_current_dead_players() else remove_from_current_living_players() + SEND_GLOBAL_SIGNAL(COMSIG_GLOB_PLAYER_LOGOUT, src) ///Adds the cliented mob reference to either the list of dead player-mobs or to the list of observers, depending on how they joined the game. diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm index 9d5bfd2a5b6..93c9dce974e 100644 --- a/code/modules/mob/mob_movement.dm +++ b/code/modules/mob/mob_movement.dm @@ -599,3 +599,8 @@ if(new_turf && (istype(new_turf, /turf/cordon/secret) || is_secret_level(new_turf.z)) && !client?.holder) return return ..() + +/mob/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) + . = ..() + if(client?.sound_tokens.len) + SSsound_tokens.clients_needing_update[client] = TRUE diff --git a/tgstation.dme b/tgstation.dme index 7f0a9318305..f310217da6a 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -756,6 +756,7 @@ #include "code\controllers\subsystem\shuttle.dm" #include "code\controllers\subsystem\skills.dm" #include "code\controllers\subsystem\sound_loops.dm" +#include "code\controllers\subsystem\sound_tokens.dm" #include "code\controllers\subsystem\sounds.dm" #include "code\controllers\subsystem\spatial_gridmap.dm" #include "code\controllers\subsystem\speech_controller.dm" @@ -890,6 +891,7 @@ #include "code\datums\ruins.dm" #include "code\datums\saymode.dm" #include "code\datums\signals.dm" +#include "code\datums\sound_token.dm" #include "code\datums\spawners_menu.dm" #include "code\datums\sprite_accessories.dm" #include "code\datums\station_alert.dm"