mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-23 05:00:55 +01:00
## About The Pull Request This pull request implements sound tokens, based on the implementation by Kapu at https://github.com/DaedalusDock/daedalusdock/pull/1379 I've taken that implementation and extended it to looping_sounds, allowing us to have spatial audio that updates much more nicely when players walk closer. This system also lets players that walk into range after the sound has started playing to also hear the sound, before if you missed the sound, you would never hear it. This is particularly noticeable with long sounds (Which is lame!) **VIDEOS**: griddle: https://streamable.com/v7ayqm engineering / SM: https://streamable.com/5lr1oj The system essentially sends the sound to every client, and changes the volume based on distance to the sound. There are some performance concerns here, so we will need to testmerge this and see how impactful it is. It is in a subsystem though, so there is some limit to the amount of cpu it will use. Do keep this in mind when watching the videos; it will probably not be AS responsive as in there. I've also changed how intense our fall-off is, making sounds lose their volume slower. Before taking one step away from a 50 volume sound would make it 18 volume, this is way too big of a drop and it became particularly jarring with the changes in this PR. Some sounds might be too long range now, but I think we should just reduce the max range of those sounds as we were basically playing them at <5 volume after 10 tiles, at which point we should maybe just reduce the range a bit since at those volumes theyre really not audible. **OLD FALLOFF;** note the MASSIVE drop from 1 tile to 2 tiles <img width="1416" height="897" alt="image" src="https://github.com/user-attachments/assets/805b270e-6aa5-4e84-868e-39585966f0a4" /> **NEW FALLOFF:** <img width="1418" height="905" alt="image" src="https://github.com/user-attachments/assets/eabbd592-ae9c-4690-8945-fbd0b4f6614f" /> Also tweaked a few looping sounds (deep-fryer in particular got some clean-up, as its mid-length was way too short for the sounds and it had a double-definition) ## Why It's Good For The Game This system allows us to have a richer audio experience, I really don't like how choppy our looping sounds are, especially in some areas where you hear them a lot (kitchen and engineering) ## Changelog 🆑 CabinetOnFire, Kapu sound: Changed our falloff to be less intense, as it was punching volume down way too fast. refactor: Implements a system for spatial audio to improve our looping sounds /🆑 --------- Co-authored-by: mrmanlikesbt <99309552+mrmanlikesbt@users.noreply.github.com> Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
281 lines
10 KiB
Plaintext
281 lines
10 KiB
Plaintext
#define DATUMLESS "NO_DATUM"
|
|
|
|
SUBSYSTEM_DEF(sounds)
|
|
name = "Sounds"
|
|
ss_flags = SS_NO_FIRE
|
|
init_stage = INITSTAGE_EARLY
|
|
var/static/using_channels_max = CHANNEL_HIGHEST_AVAILABLE //BYOND max channels
|
|
/// Amount of channels to reserve for random usage rather than reservations being allowed to reserve all channels. Also a nice safeguard for when someone screws up.
|
|
var/static/random_channels_min = 50
|
|
|
|
// Hey uh these two needs to be initialized fast because the whole "things get deleted before init" thing.
|
|
/// Assoc list, `"[channel]" =` either the datum using it or TRUE for an unsafe-reserved (datumless reservation) channel
|
|
var/list/using_channels
|
|
/// Assoc list datum = list(channel1, channel2, ...) for what channels something reserved.
|
|
var/list/using_channels_by_datum
|
|
// Special datastructure for fast channel management
|
|
/// List of all channels as numbers
|
|
var/list/channel_list
|
|
/// Associative list of all reserved channels associated to their position. `"[channel_number]" =` index as number
|
|
var/list/reserved_channels
|
|
/// lower iteration position - Incremented and looped to get "random" sound channels for normal sounds. The channel at this index is returned when asking for a random channel.
|
|
var/channel_random_low
|
|
/// higher reserve position - decremented and incremented to reserve sound channels, anything above this is reserved. The channel at this index is the highest unreserved channel.
|
|
var/channel_reserve_high
|
|
|
|
/// All valid sound files in the sound directory
|
|
var/list/all_sounds
|
|
|
|
// || Sound caching ||
|
|
/// k:v list of file_path : length
|
|
VAR_PRIVATE/list/sound_lengths
|
|
/// A list of sounds to cache upon initialize.
|
|
VAR_PRIVATE/list/sounds_to_precache = list()
|
|
/// Any errors from precaching.
|
|
VAR_PRIVATE/list/precache_errors = list()
|
|
|
|
// Comments from https://github.com/DaedalusDock/daedalusdock We love Francinum.
|
|
/// A list of sound formats that work in byond. Indexed for direct accesing rather then loop itteration or usage of `in`
|
|
var/static/list/byond_sound_formats = list(
|
|
"mid" = TRUE, //Midi, 8.3 File Name
|
|
"midi" = TRUE, //Midi, Long File Name
|
|
"mod" = TRUE, //Module, Original Amiga Tracker format
|
|
"it" = TRUE, //Impulse Tracker Module format
|
|
"s3m" = TRUE, //ScreamTracker 3 Module
|
|
"xm" = TRUE, //FastTracker 2 Module
|
|
"oxm" = TRUE, //FastTracker 2 (Vorbis Compressed Samples)
|
|
"wav" = TRUE, //Waveform Audio File Format, A (R)IFF-class format, and Microsoft's choice in the 80s sound format pissing match.
|
|
"ogg" = TRUE, //OGG Audio Container, Usually contains Vorbis-compressed Audio
|
|
//"raw" = TRUE, //On the tin, byond purports to support raw, uncompressed PCM Audio. I actually have no fucking idea how FMOD actually handles these.
|
|
//since they completely lack all information. As a confusion based anti-footgun, I'm just going to wire this to FALSE for now. It's here though.
|
|
"wma" = TRUE, //Windows Media Audio container
|
|
"aiff" = TRUE, //Audio Interchange File Format, Apple's side of the 80s sound format pissing match. It's also (R)IFF in a trenchcoat.
|
|
"mp3" = TRUE //MPeg Layer 3 Container (And usually, Codec.)
|
|
)
|
|
|
|
/// File types we can sniff the duration from using rustg.
|
|
var/static/list/safe_formats = list(
|
|
"ogg" = TRUE,
|
|
"mp3" = TRUE
|
|
)
|
|
|
|
// Currently set to private as I would prefer you use byond_sound_formats but you can unprivate it if you have a valid use!
|
|
// Put more common extensions first to speed this up a bit (So only ogg and mp3 lol.)
|
|
/// Similar to byond_sound_formats, a list of sound formats that work in byond.
|
|
VAR_PRIVATE/static/list/byond_sound_extensions = list(
|
|
".ogg",
|
|
".mp3",
|
|
".mid",
|
|
".midi",
|
|
".mod",
|
|
".it",
|
|
".s3m",
|
|
".xm",
|
|
".oxm",
|
|
".wav",
|
|
//".raw", See byond_sound_formats
|
|
".wma",
|
|
".aiff"
|
|
)
|
|
|
|
/datum/controller/subsystem/sounds/Initialize()
|
|
setup_available_channels()
|
|
find_all_available_sounds()
|
|
init_sound_keys()
|
|
|
|
if(!(RUST_G))
|
|
to_chat(world, span_boldnotice("Sounds subsystem: No rust_g detected."))
|
|
return ..()
|
|
|
|
// Precache ambience sounds
|
|
for(var/key in GLOB.ambience_assoc)
|
|
sounds_to_precache |= GLOB.ambience_assoc[key]
|
|
|
|
precache_sounds()
|
|
|
|
return SS_INIT_SUCCESS
|
|
|
|
/datum/controller/subsystem/sounds/proc/setup_available_channels()
|
|
channel_list = list()
|
|
reserved_channels = list()
|
|
using_channels = list()
|
|
using_channels_by_datum = list()
|
|
for(var/i in 1 to using_channels_max)
|
|
channel_list += i
|
|
channel_random_low = 1
|
|
channel_reserve_high = length(channel_list)
|
|
|
|
/datum/controller/subsystem/sounds/proc/find_all_available_sounds()
|
|
all_sounds = list()
|
|
all_sounds = pathwalk("sound/", byond_sound_extensions)
|
|
|
|
/// Removes a channel from using list.
|
|
/datum/controller/subsystem/sounds/proc/free_sound_channel(channel)
|
|
var/text_channel = num2text(channel)
|
|
var/using = using_channels[text_channel]
|
|
using_channels -= text_channel
|
|
if(using != DATUMLESS) // datum channel
|
|
using_channels_by_datum[using] -= channel
|
|
if(!length(using_channels_by_datum[using]))
|
|
stop_tracking_datum(using)
|
|
free_channel(channel)
|
|
|
|
/// Frees all the channels a datum is using.
|
|
/datum/controller/subsystem/sounds/proc/free_datum_channels(datum/D)
|
|
var/list/L = using_channels_by_datum[D]
|
|
if(!L)
|
|
return
|
|
for(var/channel in L)
|
|
using_channels -= num2text(channel)
|
|
free_channel(channel)
|
|
stop_tracking_datum(D)
|
|
|
|
/// Frees all datumless channels
|
|
/datum/controller/subsystem/sounds/proc/free_datumless_channels()
|
|
free_datum_channels(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
|
|
var/text_channel = num2text(.)
|
|
using_channels[text_channel] = DATUMLESS
|
|
LAZYINITLIST(using_channels_by_datum[DATUMLESS])
|
|
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_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()
|
|
if(!.)
|
|
CRASH("No more sound channels can be reserved")
|
|
var/text_channel = num2text(.)
|
|
using_channels[text_channel] = D
|
|
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.
|
|
*/
|
|
/datum/controller/subsystem/sounds/proc/reserve_channel()
|
|
PRIVATE_PROC(TRUE)
|
|
if(channel_reserve_high <= random_channels_min) // out of channels
|
|
return
|
|
var/channel = channel_list[channel_reserve_high]
|
|
reserved_channels[num2text(channel)] = channel_reserve_high--
|
|
return channel
|
|
|
|
/**
|
|
* Frees a channel and updates the datastructure. Private proc.
|
|
*/
|
|
/datum/controller/subsystem/sounds/proc/free_channel(number)
|
|
PRIVATE_PROC(TRUE)
|
|
var/text_channel = num2text(number)
|
|
var/index = reserved_channels[text_channel]
|
|
if(!index)
|
|
CRASH("Attempted to (internally) free a channel that wasn't reserved.")
|
|
reserved_channels -= text_channel
|
|
// push reserve index up, which makes it now on a channel that is reserved
|
|
channel_reserve_high++
|
|
// swap the reserved channel wtih the unreserved channel so the reserve index is now on an unoccupied channel and the freed channel is next to be used.
|
|
channel_list.Swap(channel_reserve_high, index)
|
|
// now, an existing reserved channel will likely (exception: unreserving last reserved channel) be at index
|
|
// get it, and update position.
|
|
var/text_reserved = num2text(channel_list[index])
|
|
if(!reserved_channels[text_reserved]) //if it isn't already reserved make sure we don't accidently mistakenly put it on reserved list!
|
|
return
|
|
reserved_channels[text_reserved] = index
|
|
|
|
/// Random available channel, returns text.
|
|
/datum/controller/subsystem/sounds/proc/random_available_channel_text()
|
|
if(channel_random_low > channel_reserve_high)
|
|
channel_random_low = 1
|
|
. = "[channel_list[channel_random_low++]]"
|
|
|
|
/// Random available channel, returns number
|
|
/datum/controller/subsystem/sounds/proc/random_available_channel()
|
|
if(channel_random_low > channel_reserve_high)
|
|
channel_random_low = 1
|
|
. = channel_list[channel_random_low++]
|
|
|
|
/// How many channels we have left.
|
|
/datum/controller/subsystem/sounds/proc/available_channels_left()
|
|
return length(channel_list) - random_channels_min
|
|
|
|
/datum/controller/subsystem/sounds/proc/precache_sounds()
|
|
if(!length(sounds_to_precache))
|
|
return
|
|
|
|
var/list/lengths = rustg_sound_length_list(sounds_to_precache)
|
|
precache_errors = lengths[RUSTG_SOUNDLEN_ERRORS]
|
|
sound_lengths = lengths[RUSTG_SOUNDLEN_SUCCESSES]
|
|
for(var/sound_path in sound_lengths)
|
|
sound_lengths[sound_path] = text2num(sound_lengths[sound_path])
|
|
|
|
sounds_to_precache = null
|
|
|
|
/// Cache a list of sound lengths.
|
|
/datum/controller/subsystem/sounds/proc/cache_sounds(list/paths)
|
|
var/list/reconstructed = list()
|
|
reconstructed.len = length(paths)
|
|
|
|
for(var/i in 1 to length(paths))
|
|
reconstructed[i] = "[paths[i]]"
|
|
|
|
var/list/out = rustg_sound_length_list(paths)
|
|
var/list/successes = out[RUSTG_SOUNDLEN_SUCCESSES]
|
|
for(var/sound_path in successes)
|
|
sound_lengths[sound_path] = text2num(successes[sound_path])
|
|
|
|
/// Cache and return a single sound.
|
|
/datum/controller/subsystem/sounds/proc/get_sound_length(file_path)
|
|
. = 0
|
|
if(!istext(file_path))
|
|
if(!isfile(file_path))
|
|
CRASH("rustg_sound_length error: Passed non-text object")
|
|
|
|
if(length("[file_path]")) // Runtime generated RSC references stringify into 0-length strings.
|
|
file_path = "[file_path]"
|
|
else
|
|
CRASH("rustg_sound_length does not support non-static file refs.")
|
|
|
|
var/cached_length = sound_lengths[file_path]
|
|
if(!isnull(cached_length))
|
|
return cached_length
|
|
|
|
var/ret = RUSTG_CALL(RUST_G, "sound_len")(file_path)
|
|
var/as_num = text2num(ret)
|
|
if(isnull(ret))
|
|
. = 0
|
|
CRASH("rustg_sound_length error: [ret]")
|
|
|
|
sound_lengths[file_path] = as_num
|
|
return as_num
|
|
|
|
/datum/controller/subsystem/sounds/proc/init_sound_keys()
|
|
for(var/datum/sound_effect/sfx as anything in subtypesof(/datum/sound_effect))
|
|
// this is for the assoc subtype
|
|
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
|