Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mirror "Spatial Sound Tracking" [WIP] #369

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions code/__DEFINES/sounds.dm
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,27 @@
#define SOUND_CHANNEL_LOBBY 1023
#define SOUND_CHANNEL_Z 1024

//default byond sound echo list index positions.
//ECHO_DIRECT and ECHO_ROOM are the only two that actually appear to do anything, and represent the dry and wet channels of the environment effects, respectively.
#define ECHO_DIRECT 1
#define ECHO_DIRECTHF 2
#define ECHO_ROOM 3
#define ECHO_ROOMHF 4
#define ECHO_OBSTRUCTION 5
#define ECHO_OBSTRUCTIONLFRATIO 6
#define ECHO_OCCLUSION 7
#define ECHO_OCCLUSIONLFRATIO 8
#define ECHO_OCCLUSIONROOMRATIO 9
#define ECHO_OCCLUSIONDIRECTRATIO 10
#define ECHO_EXCLUSION 11
#define ECHO_EXCLUSIONLFRATIO 12
#define ECHO_OUTSIDEVOLUMEHF 13
#define ECHO_DOPPLERFACTOR 14
#define ECHO_ROLLOFFFACTOR 15
#define ECHO_ROOMROLLOFFFACTOR 16
#define ECHO_AIRABSORPTIONFACTOR 17
#define ECHO_FLAGS 18

//default byond sound environments
#define SOUND_ENVIRONMENT_NONE -1
#define SOUND_ENVIRONMENT_GENERIC 0
Expand Down Expand Up @@ -61,8 +82,16 @@
#define SOUND_ENVIRONMENT_DIZZY 24
#define SOUND_ENVIRONMENT_PSYCHOTIC 25

#define SOUND_ECHO_REVERB_ON list(0, 0, 0, 0, 0, 0.0, 0, 0.25, 1.5, 1.0, 0, 1.0, 0, 0.0, 0.0, 0.0, 1.0, 0)
#define SOUND_ECHO_REVERB_OFF list(0, 0, -10000, -10000, 0, 0.0, 0, 0.25, 1.5, 1.0, 0, 1.0, 0, 0.0, 0.0, 0.0, 1.0, 0) //-10000 to Room & RoomHF makes enviromental reverb effectively inaudible
#define SOUND_ECHO_REVERB_OFF list(null, null, -10000, -10000, null, null, null, null, null, null, null, null, null, null, null, null, null, null) //-10000 to Room & RoomHF makes enviromental reverb effectively inaudible, nulls are interpreted as default values

/// Enviromental sounds are effected by environmental reverb
#define SOUND_ENVIRONMENTAL (1<<4)
/// Sound can be muted by mob deafness
#define SOUND_CAN_DEAFEN (1<<5)
/// Spatial sounds set the sound position relative to the source
#define SOUND_SPATIAL (1<<6)
/// Sound is tracked by soundOutput for updating position, volume, etc
#define SOUND_TRACKED (1<<7)

#define AMBIENCE_SHIP 'sound/ambience/shipambience.ogg'
#define AMBIENCE_JUNGLE 'sound/ambience/ambienceLV624.ogg'
Expand Down
64 changes: 64 additions & 0 deletions code/_globalvars/lists/sounds.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#define SOUND_LENGTHS_FILEPATH "sound/sound_lengths.txt"

/// Assoc list of of all sounds in the codebase, in the form "[sound/path.ext]" = length in seconds
GLOBAL_LIST_INIT(sound_lengths, json_decode(file2text(SOUND_LENGTHS_FILEPATH)))

/*
//the above file was generated by this proc
/client/proc/generate_sound_lengths()
set category = "Debug.Sound"
set name = "Generate Sound Lengths"

var/static/list/sound_list = get_all_sounds()
to_chat(src, text = "[length(sound_list)] sound files found.")

var/list/playing_sounds = list()
for(var/i in 0 to length(sound_list) step 1000)
sound_to(src, sound()) //clear all playing sounds
to_chat(src, text = "Querying sound files [i + 1] to [min(length(sound_list), i + 1000)]...")

for(var/j in 1 to min(length(sound_list) - i, 1000))
var/sound_file = sound_list[j + i]
var/sound/sound = sound(sound_file, FALSE, FALSE, j, 0)
sound.status |= SOUND_STREAM|SOUND_PAUSED|SOUND_MUTE
sound_to(src, sound)

playing_sounds += src.SoundQuery()
to_chat(src, text = "Queried [length(playing_sounds)] out of [length(sound_list)] sounds.")
sound_to(src, sound())

var/list/sound_lengths = list()
for(var/sound/playing_sound as anything in playing_sounds)
if(!ISINRANGE(playing_sound.channel, 1, 1000))
continue
sound_lengths[playing_sound.file] = playing_sound.len
to_chat(src, text = "Parsed [length(sound_lengths)] out of [length(sound_list)] unique sounds.")

var/saved_lengths = file(SOUND_LENGTHS_FILEPATH)
fdel(saved_lengths)
to_file(saved_lengths, "// [length(sound_lengths)] elements generated by [__PROC__] on [time2text(world.realtime)]")
to_file(saved_lengths, json_encode(sound_lengths, JSON_PRETTY_PRINT))
to_chat(src, text = "Wrote [length(sound_lengths)] elements to file: [saved_lengths]")

#undef SOUND_LENGTHS_FILEPATH

/proc/get_all_sounds()
var/static/list/extensions = list("mid", "midi", "mod", "it", "s3m", "xm", "oxm", "wav", "ogg", "mp3", "raw", "wma", "aiff")
var/static/regex/ext_rgx = new("\\.(?:[jointext(extensions, "|")])$", "i")

var/list/dirs = list("sound/")
var/list/file_paths = list()
for(var/i = 1, i <= length(dirs), i++)
var/path = dirs[i]

var/list/filenames = flist(path)
for(var/filename as anything in filenames)
if(findtext(filename, "/", -1)) //found directory, add to search
dirs += "[path][filename]"
continue
if(!findtext(filename, ext_rgx)) //extension check
continue
file_paths += "[path][filename]"

return file_paths
*/
53 changes: 38 additions & 15 deletions code/controllers/subsystem/sound.dm
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,51 @@ SUBSYSTEM_DEF(sound)

for(var/datum/sound_template/run_template in run_queue)
if(!run_hearers) // Initialize for handling next template
run_hearers = run_queue[run_template] // get base hearers
if(run_template.range) // ranging
run_hearers |= SSquadtree.players_in_range(SQUARE(run_template.x, run_template.y, run_template.range * 2), run_template.z)
run_hearers = run_queue[run_template] // get initial hearers
run_template.end_time = REALTIMEOFDAY + GLOB.sound_lengths["[run_template.file]"] SECONDS
if(MC_TICK_CHECK)
return

while(length(run_hearers)) // Output sound to hearers
var/client/C = run_hearers[run_hearers.len]
var/client/hearer = run_hearers[length(run_hearers)]
run_hearers.len--
if(C && C.soundOutput)
C.soundOutput.process_sound(run_template)
hearer?.soundOutput.process_sound(run_template)
if(MC_TICK_CHECK)
return

run_queue.Remove(run_template) // Everyone that had to get this sound got it. Bye, template
run_hearers = null // Reset so we know next one is new

/datum/controller/subsystem/sound/proc/queue(datum/sound_template/template, list/client/hearers, list/datum/interior/extra_interiors)
if(!hearers)
hearers = list()
if(extra_interiors && SSmapping)
for(var/datum/interior/VI in extra_interiors)
if(VI?.ready)
var/list/bounds = VI.get_middle_coords()
if(bounds.len >= 2)
hearers |= SSquadtree.players_in_range(RECT(bounds[1], bounds[2], VI.map_template.width, VI.map_template.height), bounds[3])
for(var/client/client as anything in GLOB.clients)
client.soundOutput.update_tracked_channels()
if(MC_TICK_CHECK)
return

/datum/controller/subsystem/sound/proc/queue(datum/sound_template/template, list/hearers)
LAZYINITLIST(hearers)

if(!CHECK_BITFIELD(template.sound_flags, SOUND_SPATIAL))
template_queue[template] = hearers
return

var/turf/source_turf = get_turf(template.source)
if(SSinterior.in_interior(source_turf)) //from interior, get hearers in interior and nearby to exterior
var/datum/interior/interior = SSinterior.get_interior_by_coords(source_turf.x, source_turf.y, source_turf.z)
var/list/bounds = interior.get_middle_coords()
hearers |= SSquadtree.players_in_range(RECT(bounds[1], bounds[2], interior.map_template.width, interior.map_template.height), bounds[3]) //in interior
if(interior.exterior)
hearers |= SSquadtree.players_in_range(SQUARE(interior.exterior.x, interior.exterior.y, template.range * 2), interior.exterior.z) //nearby to exterior

else //from exterior, get hearers nearby and in nearby interiors
hearers |= SSquadtree.players_in_range(SQUARE(source_turf.x, source_turf.y, template.range * 2), source_turf.z) //nearby
for(var/datum/interior/interior in SSinterior.interiors)
if(!interior.ready)
continue
if(interior.exterior?.z != source_turf.z)
continue
if(get_dist(interior.exterior, source_turf) > template.range)
continue
var/list/bounds = interior.get_middle_coords()
hearers |= SSquadtree.players_in_range(RECT(bounds[1], bounds[2], interior.map_template.width, interior.map_template.height), bounds[3]) //nearby interiors

template_queue[template] = hearers
98 changes: 64 additions & 34 deletions code/datums/soundOutput.dm
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

/// Currently applied environmental reverb.
VAR_PROTECTED/owner_environment = SOUND_ENVIRONMENT_NONE
/// Assoc list of important channels and their assigned template, in the form of: "[channel]" = template
var/list/tracked_channels = list()

/datum/soundOutput/New(client/client)
if(!client)
Expand All @@ -24,40 +26,68 @@
owner = null
return ..()

/datum/soundOutput/proc/process_sound(datum/sound_template/T)
var/sound/S = sound(T.file, T.repeat, T.wait)
S.volume = owner.volume_preferences[T.volume_cat] * T.volume
if(T.channel == 0)
S.channel = get_free_channel()
else
S.channel = T.channel
S.frequency = T.frequency
S.falloff = T.falloff
S.offset = T.offset
S.pitch = T.pitch
S.status = T.status
if(T.x && T.y && T.z)
var/turf/owner_turf = get_turf(owner.mob)
if(owner_turf)
// We're in an interior and sound came from outside
if(SSinterior.in_interior(owner_turf) && owner_turf.z != T.z)
var/datum/interior/VI = SSinterior.get_interior_by_coords(owner_turf.x, owner_turf.y, owner_turf.z)
if(VI && VI.exterior)
var/turf/candidate = get_turf(VI.exterior)
if(candidate.z != T.z)
return // Invalid location
S.falloff /= 2
owner_turf = candidate
S.x = T.x - owner_turf.x
S.y = 0
S.z = T.y - owner_turf.y
S.y += T.y_s_offset
S.x += T.x_s_offset
S.echo = SOUND_ECHO_REVERB_ON //enable environment reverb for positional sounds
if(owner.mob.ear_deaf > 0)
S.status |= SOUND_MUTE

sound_to(owner,S)
/**
* Translates a sound_template into an appropriate sound for the owner and sends it.
*
* Arguments:
* * template - the sound_template
* * update - if truthy updates the existing sound on the template's channel, otherwise overwrites it
*/
/datum/soundOutput/proc/process_sound(datum/sound_template/template, update = FALSE)
var/sound/sound = template.get_sound(update)

sound.volume *= owner.volume_preferences[template.volume_cat]

if(CHECK_BITFIELD(template.sound_flags, SOUND_CAN_DEAFEN) && CHECK_BITFIELD(src.status_flags, EAR_DEAF_MUTE))
ENABLE_BITFIELD(sound.status, SOUND_MUTE)

if(CHECK_BITFIELD(template.sound_flags, SOUND_TRACKED) && !update)
if(GLOB.spatial_sound_tracking && GLOB.sound_lengths["[template.file]"] SECONDS >= GLOB.spatial_sound_tracking_min_length) //debug
tracked_channels["[sound.channel]"] = template

if(!CHECK_BITFIELD(template.sound_flags, SOUND_SPATIAL)) //non-spatial
sound.x = template.x
sound.y = template.y
sound.z = template.z
sound_to(owner, sound)
return

if(QDELETED(template.source))
return

var/turf/owner_turf = get_turf(owner.mob)
var/turf/source_turf = get_turf(template.source)
//soundsys only traverses one "step", so will never send from one interior to another
if(owner_turf.z == source_turf.z) //both in exterior, or both in same interior
sound.x = source_turf.x - owner_turf.x
sound.z = source_turf.y - owner_turf.y
else if(SSinterior.in_interior(owner_turf)) //source in exterior, owner in interior
var/datum/interior/interior = SSinterior.get_interior_by_coords(owner_turf.x, owner_turf.y, owner_turf.z)
sound.falloff *= 0.5
sound.x = source_turf.x - interior.exterior.x
sound.z = source_turf.y - interior.exterior.y
else if(SSinterior.in_interior(source_turf)) //source in interior, owner in exterior
var/datum/interior/interior = SSinterior.get_interior_by_coords(source_turf.x, source_turf.y, source_turf.z)
sound.falloff *= 0.5
sound.x = interior.exterior.x - owner_turf.x
sound.z = interior.exterior.y - owner_turf.y
else //moved to unrelated z while sound was playing, leave it alone
return

sound_to(owner, sound)

/datum/soundOutput/proc/update_tracked_channels()
for(var/i in length(tracked_channels) to 1 step -1)
var/channel = tracked_channels[i]
var/datum/sound_template/template = tracked_channels[channel]
if(REALTIMEOFDAY >= template.end_time)
tracked_channels -= channel
continue
if(!CHECK_BITFIELD(template.sound_flags, SOUND_SPATIAL))
continue
if(template.source == owner.mob)
continue
process_sound(template, update = TRUE)

/datum/soundOutput/proc/update_ambience(area/target_area, ambience_override, force_update = FALSE)
var/status_flags = SOUND_STREAM
Expand Down
Loading
Loading