mirror of
https://github.com/Aurorastation/Aurora.3.git
synced 2025-12-23 08:31:57 +00:00
264 lines
9.7 KiB
Plaintext
264 lines
9.7 KiB
Plaintext
var/singleton/sound_player/sound_player = new()
|
|
|
|
/*
|
|
A sound player/manager for looping 3D sound effects.
|
|
Due to how the BYOND sound engine works a sound datum must be played on a specific channel for updates to work properly.
|
|
If a channel is not assigned it will just result in a new sound effect playing, even if re-using the same datum instance.
|
|
We also use the channel to play a null-sound on Stop(), just in case BYOND clients don't like having a large nuber, albeit stopped, looping sounds.
|
|
As such there is a maximum limit of 1024 sound sources, with further limitations due to some channels already being potentially in use.
|
|
However, multiple sources may share the same sound_id and there is a best-effort attempt to play the closest source where possible.
|
|
The line above is currently a lie. Will probably just have to enforce moderately short sound ranges.
|
|
*/
|
|
|
|
/singleton/sound_player
|
|
var/list/taken_channels // taken_channels and source_id_uses can be merged into one but would then require a meta-object to store the different values I desire.
|
|
var/list/sound_tokens_by_sound_id
|
|
|
|
/singleton/sound_player/New()
|
|
..()
|
|
taken_channels = list()
|
|
sound_tokens_by_sound_id = list()
|
|
|
|
|
|
//This can be called if either we're doing whole sound setup ourselves or it will be as part of from-file sound setup
|
|
/singleton/sound_player/proc/PlaySoundDatum(atom/source, sound_id, sound/sound, range, prefer_mute, sound_type = ASFX_AMBIENCE)
|
|
var/token_type = isnum(sound.environment) ? /datum/sound_token : /datum/sound_token/static_environment
|
|
return new token_type(source, sound_id, sound, range, prefer_mute, sound_type)
|
|
|
|
/singleton/sound_player/proc/PlayLoopingSound(atom/source, sound_id, sound, volume, range, falloff = 1, echo, frequency, prefer_mute, sound_type = ASFX_AMBIENCE)
|
|
var/sound/S = istype(sound, /sound) ? sound : new(sound)
|
|
S.environment = 0 // Ensures a 3D effect even if x/y offset happens to be 0 the first time it's played
|
|
S.volume = volume
|
|
S.falloff = falloff
|
|
S.echo = echo
|
|
S.frequency = frequency
|
|
S.repeat = TRUE
|
|
|
|
return PlaySoundDatum(source, sound_id, S, range, prefer_mute, sound_type)
|
|
|
|
/singleton/sound_player/proc/PrivStopSound(datum/sound_token/sound_token)
|
|
var/channel = sound_token.sound.channel
|
|
var/sound_id = sound_token.sound_id
|
|
|
|
var/sound_tokens = sound_tokens_by_sound_id[sound_id]
|
|
if(!(sound_token in sound_tokens))
|
|
return
|
|
sound_tokens -= sound_token
|
|
if(length(sound_tokens))
|
|
return
|
|
|
|
sound_channels.ReleaseChannel(channel)
|
|
taken_channels -= sound_id
|
|
sound_tokens_by_sound_id -= sound_id
|
|
|
|
/singleton/sound_player/proc/PrivGetChannel(datum/sound_token/sound_token)
|
|
var/sound_id = sound_token.sound_id
|
|
|
|
. = taken_channels[sound_id] // Does this sound_id already have an assigned channel?
|
|
if(!.) // If not, request a new one.
|
|
. = sound_channels.RequestChannel(sound_id)
|
|
if(!.) // Oh no, still no channel. Abort
|
|
return
|
|
taken_channels[sound_id] = .
|
|
|
|
var/sound_tokens = sound_tokens_by_sound_id[sound_id]
|
|
if(!sound_tokens)
|
|
sound_tokens = list()
|
|
sound_tokens_by_sound_id[sound_id] = sound_tokens
|
|
sound_tokens += sound_token
|
|
|
|
#define SOUND_STOPPED FLAG(15)
|
|
|
|
/*
|
|
Outwardly this is a merely a toke/little helper that a user utilize to adjust sounds as desired (and possible).
|
|
In reality this is where the heavy-lifting happens.
|
|
*/
|
|
/datum/sound_token
|
|
var/atom/source // Where the sound originates from
|
|
var/list/listeners // Assoc: Atoms hearing this sound, and their sound datum
|
|
var/range // How many turfs away the sound will stop playing completely
|
|
var/prefer_mute // If sound should be muted instead of stopped when mob moves out of range. In the general case this should be avoided because listeners will remain tracked.
|
|
var/sound/sound // Sound datum, holds most sound relevant data
|
|
var/sound_id // The associated sound id, used for cleanup
|
|
var/status = 0 // Paused, muted, running? Global for all listeners
|
|
var/listener_status// Paused, muted, running? Specific for the given listener.
|
|
var/sound_type // Type of sound this token is handling, it's set by default (arbitrarily) as ambience, gets set on init with the actual one if specified.
|
|
|
|
|
|
/datum/sound_token/New(atom/source, sound_id, sound/sound, range = 4, prefer_mute = FALSE, sound_type = ASFX_AMBIENCE)
|
|
..()
|
|
if(!istype(source))
|
|
CRASH("Invalid sound source: [log_info_line(source)]")
|
|
if(!istype(sound))
|
|
CRASH("Invalid sound: [log_info_line(sound)]")
|
|
if(sound.repeat && !sound_id)
|
|
CRASH("No sound id given")
|
|
if(!PrivIsValidEnvironment(sound.environment))
|
|
CRASH("Invalid sound environment: [log_info_line(sound.environment)]")
|
|
|
|
src.prefer_mute = prefer_mute
|
|
src.range = range
|
|
src.source = source
|
|
src.sound = sound
|
|
src.sound_id = sound_id
|
|
src.sound_type = sound_type
|
|
|
|
if(sound.repeat) // Non-looping sounds may not reserve a sound channel due to the risk of not hearing when someone forgets to stop the token
|
|
var/channel = sound_player.PrivGetChannel(src) //Attempt to find a channel
|
|
if(!isnum(channel))
|
|
CRASH("All available sound channels are in active use.")
|
|
sound.channel = channel
|
|
else
|
|
sound.channel = 0
|
|
|
|
listeners = list()
|
|
listener_status = list()
|
|
|
|
destroyed_event.register(source, src, TYPE_PROC_REF(/datum, qdel_self))
|
|
|
|
PrivLocateListeners()
|
|
START_PROCESSING(SSprocessing, src)
|
|
|
|
/datum/sound_token/Destroy()
|
|
Stop()
|
|
. = ..()
|
|
|
|
/datum/sound_token/proc/SetVolume(new_volume)
|
|
new_volume = clamp(new_volume, 0, 100)
|
|
if(sound.volume == new_volume)
|
|
return
|
|
sound.volume = new_volume
|
|
PrivUpdateListeners()
|
|
|
|
/datum/sound_token/proc/Mute()
|
|
PrivUpdateStatus(status|SOUND_MUTE)
|
|
|
|
/datum/sound_token/proc/Unmute()
|
|
PrivUpdateStatus(status & ~SOUND_MUTE)
|
|
|
|
/datum/sound_token/proc/Pause()
|
|
PrivUpdateStatus(status|SOUND_PAUSED)
|
|
|
|
// Normally called Resume but I don't want to give people false hope about being unable to un-stop a sound
|
|
/datum/sound_token/proc/Unpause()
|
|
PrivUpdateStatus(status & ~SOUND_PAUSED)
|
|
|
|
/datum/sound_token/proc/Stop()
|
|
if(status & SOUND_STOPPED)
|
|
return
|
|
status |= SOUND_STOPPED
|
|
|
|
var/sound/null_sound = new(channel = sound.channel)
|
|
for(var/listener in listeners)
|
|
PrivRemoveListener(listener, null_sound)
|
|
listeners = null
|
|
listener_status = null
|
|
|
|
destroyed_event.unregister(source, src, TYPE_PROC_REF(/datum, qdel_self))
|
|
source = null
|
|
|
|
sound_player.PrivStopSound(src)
|
|
STOP_PROCESSING(SSprocessing, src)
|
|
|
|
/datum/sound_token/process()
|
|
PrivLocateListeners()
|
|
|
|
/datum/sound_token/proc/PrivLocateListeners()
|
|
if(status & SOUND_STOPPED)
|
|
return
|
|
|
|
var/current_listeners = SSspatial_grid.orthogonal_range_search(source, SPATIAL_GRID_CONTENTS_TYPE_HEARING, range)
|
|
var/new_listeners = current_listeners - listeners
|
|
|
|
if(!prefer_mute)
|
|
var/former_listeners = listeners - current_listeners
|
|
for(var/listener in former_listeners)
|
|
PrivRemoveListener(listener)
|
|
|
|
for(var/mob/listener in new_listeners)
|
|
if((listener.client?.prefs.sfx_toggles & sound_type) && (!isdeaf(listener))) //Only give the sound token if the listener has the preference for the type of token active, and is not deaf
|
|
PrivAddListener(listener)
|
|
|
|
for(var/mob/listener in current_listeners)
|
|
PrivUpdateListenerLoc(listener)
|
|
|
|
/datum/sound_token/proc/PrivUpdateStatus(new_status)
|
|
// Once stopped, always stopped. Go ask the player to play the sound again.
|
|
if(status & SOUND_STOPPED)
|
|
return
|
|
if(new_status == status)
|
|
return
|
|
status = new_status
|
|
PrivUpdateListeners()
|
|
|
|
/datum/sound_token/proc/PrivAddListener(atom/listener)
|
|
if(listener in listeners)
|
|
return
|
|
|
|
listeners += listener
|
|
|
|
moved_event.register(listener, src, PROC_REF(PrivUpdateListenerLoc))
|
|
destroyed_event.register(listener, src, PROC_REF(PrivRemoveListener))
|
|
|
|
PrivUpdateListenerLoc(listener, FALSE)
|
|
|
|
/datum/sound_token/proc/PrivRemoveListener(atom/listener, sound/null_sound)
|
|
null_sound = null_sound || new(channel = sound.channel)
|
|
sound_to(listener, null_sound)
|
|
moved_event.unregister(listener, src, PROC_REF(PrivUpdateListenerLoc))
|
|
destroyed_event.unregister(listener, src, PROC_REF(PrivRemoveListener))
|
|
listeners -= listener
|
|
|
|
/datum/sound_token/proc/PrivUpdateListenerLoc(atom/listener, update_sound = TRUE)
|
|
var/turf/source_turf = get_turf(source)
|
|
var/turf/listener_turf = get_turf(listener)
|
|
|
|
if (!source_turf || !listener_turf)
|
|
return
|
|
|
|
var/distance = get_dist(source_turf, listener_turf)
|
|
if(!listener_turf || (distance > range))
|
|
if(prefer_mute)
|
|
listener_status[listener] |= SOUND_MUTE
|
|
else
|
|
PrivRemoveListener(listener)
|
|
return
|
|
else if(prefer_mute)
|
|
listener_status[listener] &= ~SOUND_MUTE
|
|
|
|
sound.x = source_turf.x - listener_turf.x
|
|
sound.z = source_turf.y - listener_turf.y
|
|
sound.y = 1
|
|
// Far as I can tell from testing, sound priority just doesn't work.
|
|
// Sounds happily steal channels from each other no matter what.
|
|
sound.priority = clamp(255 - distance, 0, 255)
|
|
PrivUpdateListener(listener, update_sound)
|
|
|
|
/datum/sound_token/proc/PrivUpdateListeners()
|
|
for(var/listener in listeners)
|
|
PrivUpdateListener(listener)
|
|
|
|
/datum/sound_token/proc/PrivUpdateListener(mob/listener, update_sound = TRUE)
|
|
sound.environment = PrivGetEnvironment(listener)
|
|
sound.status = status|listener_status[listener]
|
|
if(update_sound)
|
|
sound.status |= SOUND_UPDATE
|
|
if((listener.client?.prefs.sfx_toggles & sound_type) && (!isdeaf(listener))) //Send the sound only if the preference for the type of sound is set and is not deaf, otherwise remove the listener.
|
|
sound_to(listener, sound)
|
|
else
|
|
PrivRemoveListener(listener)
|
|
|
|
/datum/sound_token/proc/PrivGetEnvironment(listener)
|
|
var/area/A = get_area(listener)
|
|
return A && PrivIsValidEnvironment(A.sound_env) ? A.sound_env : sound.environment
|
|
|
|
/datum/sound_token/proc/PrivIsValidEnvironment(environment)
|
|
if(islist(environment) && length(environment) != 23)
|
|
return FALSE
|
|
if(!isnum(environment) || environment < 0 || environment > 25)
|
|
return FALSE
|
|
return TRUE
|
|
|
|
/datum/sound_token/static_environment/PrivGetEnvironment()
|
|
return sound.environment
|