Files
Aurora.3/code/datums/sound_player.dm

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