From 5ac6bd48d954675f462edbed939ada06ae1b559d Mon Sep 17 00:00:00 2001 From: Aronai Sieyes Date: Mon, 31 May 2021 19:13:52 -0400 Subject: [PATCH] Ports /tg/ instruments --- code/__defines/instruments.dm | 24 ++ code/__defines/misc.dm | 2 + code/__defines/sound.dm | 76 ++-- code/__defines/subsystems.dm | 7 +- .../subsystems/processing/instruments.dm | 56 +++ code/controllers/subsystems/sounds.dm | 135 ++++++ code/datums/looping_sounds/_looping_sound.dm | 2 +- code/game/area/Space Station 13 areas.dm | 24 +- code/game/atoms.dm | 3 + code/game/objects/items/devices/violin.dm | 41 -- .../objects/items/weapons/gift_wrappaper.dm | 2 +- code/game/objects/objs.dm | 3 - code/game/objects/structures/musician.dm | 366 ---------------- code/game/sound.dm | 15 +- .../instrument_data/_instrument_data.dm | 113 +++++ .../instrument_data/_instrument_key.dm | 31 ++ .../instruments/instrument_data/brass.dm | 26 ++ .../instrument_data/chromatic_percussion.dm | 31 ++ .../instruments/instrument_data/fun.dm | 25 ++ .../instruments/instrument_data/guitar.dm | 36 ++ .../instruments/instrument_data/hardcoded.dm | 86 ++++ .../instruments/instrument_data/organ.dm | 43 ++ .../instruments/instrument_data/piano.dm | 56 +++ .../instrument_data/synth_tones.dm | 19 + code/modules/instruments/items.dm | 309 +++++++++++++ code/modules/instruments/songs/_song.dm | 405 ++++++++++++++++++ code/modules/instruments/songs/editor.dm | 251 +++++++++++ code/modules/instruments/songs/play_legacy.dm | 91 ++++ .../instruments/songs/play_synthesized.dm | 150 +++++++ code/modules/instruments/stationary.dm | 59 +++ code/modules/mob/living/living.dm | 10 +- icons/mob/items/lefthand_horns.dmi | Bin 0 -> 470 bytes icons/mob/items/lefthand_instruments.dmi | Bin 0 -> 10530 bytes icons/mob/items/righthand_horns.dmi | Bin 0 -> 472 bytes icons/mob/items/righthand_instruments.dmi | Bin 0 -> 11396 bytes icons/obj/musician.dmi | Bin 4574 -> 23693 bytes sound/weapons/banjoslap.ogg | Bin 0 -> 19084 bytes sound/weapons/stringsmash.ogg | Bin 0 -> 17235 bytes vorestation.dme | 23 +- 39 files changed, 2042 insertions(+), 478 deletions(-) create mode 100644 code/__defines/instruments.dm create mode 100644 code/controllers/subsystems/processing/instruments.dm create mode 100644 code/controllers/subsystems/sounds.dm delete mode 100644 code/game/objects/items/devices/violin.dm delete mode 100644 code/game/objects/structures/musician.dm create mode 100644 code/modules/instruments/instrument_data/_instrument_data.dm create mode 100644 code/modules/instruments/instrument_data/_instrument_key.dm create mode 100644 code/modules/instruments/instrument_data/brass.dm create mode 100644 code/modules/instruments/instrument_data/chromatic_percussion.dm create mode 100644 code/modules/instruments/instrument_data/fun.dm create mode 100644 code/modules/instruments/instrument_data/guitar.dm create mode 100644 code/modules/instruments/instrument_data/hardcoded.dm create mode 100644 code/modules/instruments/instrument_data/organ.dm create mode 100644 code/modules/instruments/instrument_data/piano.dm create mode 100644 code/modules/instruments/instrument_data/synth_tones.dm create mode 100644 code/modules/instruments/items.dm create mode 100644 code/modules/instruments/songs/_song.dm create mode 100644 code/modules/instruments/songs/editor.dm create mode 100644 code/modules/instruments/songs/play_legacy.dm create mode 100644 code/modules/instruments/songs/play_synthesized.dm create mode 100644 code/modules/instruments/stationary.dm create mode 100644 icons/mob/items/lefthand_horns.dmi create mode 100644 icons/mob/items/lefthand_instruments.dmi create mode 100644 icons/mob/items/righthand_horns.dmi create mode 100644 icons/mob/items/righthand_instruments.dmi create mode 100644 sound/weapons/banjoslap.ogg create mode 100644 sound/weapons/stringsmash.ogg diff --git a/code/__defines/instruments.dm b/code/__defines/instruments.dm new file mode 100644 index 00000000000..fa09eee0dd7 --- /dev/null +++ b/code/__defines/instruments.dm @@ -0,0 +1,24 @@ +#define INSTRUMENT_MIN_OCTAVE 1 +#define INSTRUMENT_MAX_OCTAVE 9 +#define INSTRUMENT_MIN_KEY 0 +#define INSTRUMENT_MAX_KEY 127 + +/// Max number of playing notes per instrument. +#define CHANNELS_PER_INSTRUMENT 128 + +/// Maximum length a note should ever go for +#define INSTRUMENT_MAX_TOTAL_SUSTAIN (5 SECONDS) + +/// These are per decisecond. +#define INSTRUMENT_EXP_FALLOFF_MIN 1.025 //100/(1.025^50) calculated for [INSTRUMENT_MIN_SUSTAIN_DROPOFF] to be 30. +#define INSTRUMENT_EXP_FALLOFF_MAX 10 + +/// Minimum volume for when the sound is considered dead. +#define INSTRUMENT_MIN_SUSTAIN_DROPOFF 0 + +#define SUSTAIN_LINEAR 1 +#define SUSTAIN_EXPONENTIAL 2 + +// /datum/instrument instrument_flags +#define INSTRUMENT_LEGACY (1<<0) //Legacy instrument. Implies INSTRUMENT_DO_NOT_AUTOSAMPLE +#define INSTRUMENT_DO_NOT_AUTOSAMPLE (1<<1) //Do not automatically sample diff --git a/code/__defines/misc.dm b/code/__defines/misc.dm index 143eaceb37c..824155f93eb 100644 --- a/code/__defines/misc.dm +++ b/code/__defines/misc.dm @@ -423,6 +423,7 @@ var/global/list/##LIST_NAME = list();\ #define VOLUME_CHANNEL_ALARMS "Alarms" #define VOLUME_CHANNEL_VORE "Vore" #define VOLUME_CHANNEL_DOORS "Doors" +#define VOLUME_CHANNEL_INSTRUMENTS "Instruments" // Make sure you update this or clients won't be able to adjust the channel GLOBAL_LIST_INIT(all_volume_channels, list( @@ -431,6 +432,7 @@ GLOBAL_LIST_INIT(all_volume_channels, list( VOLUME_CHANNEL_ALARMS, VOLUME_CHANNEL_VORE, VOLUME_CHANNEL_DOORS, + VOLUME_CHANNEL_INSTRUMENTS )) #define APPEARANCECHANGER_CHANGED_RACE "Race" diff --git a/code/__defines/sound.dm b/code/__defines/sound.dm index 7be3e688571..ea8ae8922ac 100644 --- a/code/__defines/sound.dm +++ b/code/__defines/sound.dm @@ -18,43 +18,47 @@ #define SOUND_MINIMUM_PRESSURE 10 #define FALLOFF_SOUNDS 0.5 -//Sound environment defines. Reverb preset for sounds played in an area, see sound datum reference for more. -#define GENERIC 0 -#define PADDED_CELL 1 -#define ROOM 2 -#define BATHROOM 3 -#define LIVINGROOM 4 -#define STONEROOM 5 -#define AUDITORIUM 6 -#define CONCERT_HALL 7 -#define CAVE 8 -#define ARENA 9 -#define HANGAR 10 -#define CARPETED_HALLWAY 11 -#define HALLWAY 12 -#define STONE_CORRIDOR 13 -#define ALLEY 14 -#define FOREST 15 -#define CITY 16 -#define MOUNTAINS 17 -#define QUARRY 18 -#define PLAIN 19 -#define PARKING_LOT 20 -#define SEWER_PIPE 21 -#define UNDERWATER 22 -#define DRUGGED 23 -#define DIZZY 24 -#define PSYCHOTIC 25 +#define MAX_INSTRUMENT_CHANNELS (128 * 6) -#define STANDARD_STATION STONEROOM -#define LARGE_ENCLOSED HANGAR -#define SMALL_ENCLOSED BATHROOM -#define TUNNEL_ENCLOSED CAVE -#define LARGE_SOFTFLOOR CARPETED_HALLWAY -#define MEDIUM_SOFTFLOOR LIVINGROOM -#define SMALL_SOFTFLOOR ROOM -#define ASTEROID CAVE -#define SPACE UNDERWATER +//default byond sound environments +#define SOUND_ENVIRONMENT_NONE -1 +#define SOUND_ENVIRONMENT_GENERIC 0 +#define SOUND_ENVIRONMENT_PADDED_CELL 1 +#define SOUND_ENVIRONMENT_ROOM 2 +#define SOUND_ENVIRONMENT_BATHROOM 3 +#define SOUND_ENVIRONMENT_LIVINGROOM 4 +#define SOUND_ENVIRONMENT_STONEROOM 5 +#define SOUND_ENVIRONMENT_AUDITORIUM 6 +#define SOUND_ENVIRONMENT_CONCERT_HALL 7 +#define SOUND_ENVIRONMENT_CAVE 8 +#define SOUND_ENVIRONMENT_ARENA 9 +#define SOUND_ENVIRONMENT_HANGAR 10 +#define SOUND_ENVIRONMENT_CARPETED_HALLWAY 11 +#define SOUND_ENVIRONMENT_HALLWAY 12 +#define SOUND_ENVIRONMENT_STONE_CORRIDOR 13 +#define SOUND_ENVIRONMENT_ALLEY 14 +#define SOUND_ENVIRONMENT_FOREST 15 +#define SOUND_ENVIRONMENT_CITY 16 +#define SOUND_ENVIRONMENT_MOUNTAINS 17 +#define SOUND_ENVIRONMENT_QUARRY 18 +#define SOUND_ENVIRONMENT_PLAIN 19 +#define SOUND_ENVIRONMENT_PARKING_LOT 20 +#define SOUND_ENVIRONMENT_SEWER_PIPE 21 +#define SOUND_ENVIRONMENT_UNDERWATER 22 +#define SOUND_ENVIRONMENT_DRUGGED 23 +#define SOUND_ENVIRONMENT_DIZZY 24 +#define SOUND_ENVIRONMENT_PSYCHOTIC 25 +//If we ever make custom ones add them here + +#define STANDARD_STATION SOUND_ENVIRONMENT_STONEROOM +#define LARGE_ENCLOSED SOUND_ENVIRONMENT_HANGAR +#define SMALL_ENCLOSED SOUND_ENVIRONMENT_BATHROOM +#define TUNNEL_ENCLOSED SOUND_ENVIRONMENT_CAVE +#define LARGE_SOFTFLOOR SOUND_ENVIRONMENT_CARPETED_HALLWAY +#define MEDIUM_SOFTFLOOR SOUND_ENVIRONMENT_LIVINGROOM +#define SMALL_SOFTFLOOR SOUND_ENVIRONMENT_ROOM +#define ASTEROID SOUND_ENVIRONMENT_CAVE +#define SPACE SOUND_ENVIRONMENT_UNDERWATER // Ambience presets. // All you need to do to make an area play one of these is set their ambience var to one of these lists. diff --git a/code/__defines/subsystems.dm b/code/__defines/subsystems.dm index 1365584f13d..e85b1d1c988 100644 --- a/code/__defines/subsystems.dm +++ b/code/__defines/subsystems.dm @@ -57,6 +57,8 @@ var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_G #define INIT_ORDER_CHEMISTRY 35 #define INIT_ORDER_SKYBOX 30 #define INIT_ORDER_MAPPING 25 +#define INIT_ORDER_SOUNDS 23 +#define INIT_ORDER_INSTRUMENTS 22 #define INIT_ORDER_DECALS 20 #define INIT_ORDER_PLANTS 19 // Must initialize before atoms. #define INIT_ORDER_PLANETS 18 @@ -91,8 +93,9 @@ var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_G #define FIRE_PRIORITY_SUPPLY 5 #define FIRE_PRIORITY_NIGHTSHIFT 5 #define FIRE_PRIORITY_PLANTS 5 -#define FIRE_PRIORITY_ORBIT 8 -#define FIRE_PRIORITY_VOTE 9 +#define FIRE_PRIORITY_ORBIT 7 +#define FIRE_PRIORITY_VOTE 8 +#define FIRE_PRIORITY_INSTRUMENTS 9 #define FIRE_PRIORITY_AI 10 #define FIRE_PRIORITY_GARBAGE 15 #define FIRE_PRIORITY_ALARM 20 diff --git a/code/controllers/subsystems/processing/instruments.dm b/code/controllers/subsystems/processing/instruments.dm new file mode 100644 index 00000000000..ee0fd1ea009 --- /dev/null +++ b/code/controllers/subsystems/processing/instruments.dm @@ -0,0 +1,56 @@ +PROCESSING_SUBSYSTEM_DEF(instruments) + name = "Instruments" + wait = 0.5 + init_order = INIT_ORDER_INSTRUMENTS + flags = SS_KEEP_TIMING + priority = FIRE_PRIORITY_INSTRUMENTS + /// List of all instrument data, associative id = datum + var/static/list/datum/instrument/instrument_data = list() + /// List of all song datums. + var/static/list/datum/song/songs = list() + /// Max lines in songs + var/static/musician_maxlines = 600 + /// Max characters per line in songs + var/static/musician_maxlinechars = 300 + /// Deciseconds between hearchecks. Too high and instruments seem to lag when people are moving around in terms of who can hear it. Too low and the server lags from this. + var/static/musician_hearcheck_mindelay = 5 + /// Maximum instrument channels total instruments are allowed to use. This is so you don't have instruments deadlocking all sound channels. + var/static/max_instrument_channels = MAX_INSTRUMENT_CHANNELS + /// Current number of channels allocated for instruments + var/static/current_instrument_channels = 0 + /// Single cached list for synthesizer instrument ids, so you don't have to have a new list with every synthesizer. + var/static/list/synthesizer_instrument_ids + +/datum/controller/subsystem/processing/instruments/Initialize() + initialize_instrument_data() + synthesizer_instrument_ids = get_allowed_instrument_ids() + return ..() + +/datum/controller/subsystem/processing/instruments/proc/on_song_new(datum/song/S) + songs += S + +/datum/controller/subsystem/processing/instruments/proc/on_song_del(datum/song/S) + songs -= S + +/datum/controller/subsystem/processing/instruments/proc/initialize_instrument_data() + for(var/path in subtypesof(/datum/instrument)) + var/datum/instrument/I = path + if(initial(I.abstract_type) == path) + continue + I = new path + I.Initialize() + if(!I.id) + qdel(I) + continue + instrument_data[I.id] = I + CHECK_TICK + +/datum/controller/subsystem/processing/instruments/proc/get_instrument(id_or_path) + return instrument_data["[id_or_path]"] + +/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) + if(!isnull(.)) + current_instrument_channels++ diff --git a/code/controllers/subsystems/sounds.dm b/code/controllers/subsystems/sounds.dm new file mode 100644 index 00000000000..7d94a3d21d7 --- /dev/null +++ b/code/controllers/subsystems/sounds.dm @@ -0,0 +1,135 @@ +#define DATUMLESS "NO_DATUM" + +SUBSYSTEM_DEF(sounds) + name = "Sounds" + flags = SS_NO_FIRE + init_order = INIT_ORDER_SOUNDS + 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 + +/datum/controller/subsystem/sounds/Initialize() + setup_available_channels() + return ..() + +/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) + +/// 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 != TRUE) // datum channel + using_channels_by_datum[using] -= channel + if(!length(using_channels_by_datum[using])) + using_channels_by_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) + using_channels_by_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_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(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(!.) + return FALSE + var/text_channel = num2text(.) + using_channels[text_channel] = D + LAZYINITLIST(using_channels_by_datum[D]) + using_channels_by_datum[D] += . + +/** + * 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 + +#undef DATUMLESS diff --git a/code/datums/looping_sounds/_looping_sound.dm b/code/datums/looping_sounds/_looping_sound.dm index b16f9ddc351..be6ccd4fa54 100644 --- a/code/datums/looping_sounds/_looping_sound.dm +++ b/code/datums/looping_sounds/_looping_sound.dm @@ -80,7 +80,7 @@ var/list/atoms_cache = output_atoms var/sound/S = sound(soundfile) if(direct) - S.channel = open_sound_channel() + S.channel = SSsounds.random_available_channel() S.volume = volume for(var/i in 1 to atoms_cache.len) var/atom/thing = atoms_cache[i] diff --git a/code/game/area/Space Station 13 areas.dm b/code/game/area/Space Station 13 areas.dm index 3626cafb94f..0dc7d42040c 100755 --- a/code/game/area/Space Station 13 areas.dm +++ b/code/game/area/Space Station 13 areas.dm @@ -330,7 +330,7 @@ NOTE: there are two lists of areas in the end of this file: centcom and station icon_state = "thunder" requires_power = 0 dynamic_lighting = 0 - sound_env = ARENA + sound_env = SOUND_ENVIRONMENT_ARENA flags = AREA_FLAG_IS_NOT_PERSISTENT /area/tdome/tdome1 @@ -1323,28 +1323,28 @@ NOTE: there are two lists of areas in the end of this file: centcom and station /area/holodeck/source_emptycourt name = "\improper Holodeck - Empty Court" - sound_env = ARENA + sound_env = SOUND_ENVIRONMENT_ARENA /area/holodeck/source_boxingcourt name = "\improper Holodeck - Boxing Court" - sound_env = ARENA + sound_env = SOUND_ENVIRONMENT_ARENA /area/holodeck/source_basketball name = "\improper Holodeck - Basketball Court" - sound_env = ARENA + sound_env = SOUND_ENVIRONMENT_ARENA /area/holodeck/source_thunderdomecourt name = "\improper Holodeck - Thunderdome Court" requires_power = 0 - sound_env = ARENA + sound_env = SOUND_ENVIRONMENT_ARENA /area/holodeck/source_courtroom name = "\improper Holodeck - Courtroom" - sound_env = AUDITORIUM + sound_env = SOUND_ENVIRONMENT_AUDITORIUM /area/holodeck/source_beach name = "\improper Holodeck - Beach" - sound_env = PLAIN + sound_env = SOUND_ENVIRONMENT_PLAIN /area/holodeck/source_burntest name = "\improper Holodeck - Atmospheric Burn Test" @@ -1354,23 +1354,23 @@ NOTE: there are two lists of areas in the end of this file: centcom and station /area/holodeck/source_meetinghall name = "\improper Holodeck - Meeting Hall" - sound_env = AUDITORIUM + sound_env = SOUND_ENVIRONMENT_AUDITORIUM /area/holodeck/source_theatre name = "\improper Holodeck - Theatre" - sound_env = CONCERT_HALL + sound_env = SOUND_ENVIRONMENT_CONCERT_HALL /area/holodeck/source_picnicarea name = "\improper Holodeck - Picnic Area" - sound_env = PLAIN + sound_env = SOUND_ENVIRONMENT_PLAIN /area/holodeck/source_snowfield name = "\improper Holodeck - Snow Field" - sound_env = FOREST + sound_env = SOUND_ENVIRONMENT_FOREST /area/holodeck/source_desert name = "\improper Holodeck - Desert" - sound_env = PLAIN + sound_env = SOUND_ENVIRONMENT_PLAIN /area/holodeck/source_space name = "\improper Holodeck - Space" diff --git a/code/game/atoms.dm b/code/game/atoms.dm index fc27bb9f4e0..daf2596ced3 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -669,3 +669,6 @@ /atom/proc/get_visible_gender() return gender + +/atom/proc/interact(mob/user) + return diff --git a/code/game/objects/items/devices/violin.dm b/code/game/objects/items/devices/violin.dm deleted file mode 100644 index 5dc215cf919..00000000000 --- a/code/game/objects/items/devices/violin.dm +++ /dev/null @@ -1,41 +0,0 @@ -//copy pasta of the space piano, don't hurt me -Pete -/obj/item/device/instrument - name = "generic instrument" - var/datum/song/handheld/song - var/instrumentId = "generic" - var/instrumentExt = "mid" - icon = 'icons/obj/musician.dmi' - force = 10 - -/obj/item/device/instrument/New() - ..() - song = new(instrumentId, src) - song.instrumentExt = instrumentExt - -/obj/item/device/instrument/Destroy() - qdel(song) - song = null - ..() - -/obj/item/device/instrument/attack_self(mob/user as mob) - if(!user.IsAdvancedToolUser()) - to_chat(user, "You don't have the dexterity to do this!") - return 1 - interact(user) - -/obj/item/device/instrument/interact(mob/user as mob) - if(!user) - return - - if(user.incapacitated() || user.lying) - return - - user.set_machine(src) - song.interact(user) - -/obj/item/device/instrument/violin - name = "space violin" - desc = "A wooden musical instrument with four strings and a bow. \"The devil went down to space, he was looking for an assistant to grief.\"" - icon_state = "violin" - attack_verb = list("smashed") - instrumentId = "violin" diff --git a/code/game/objects/items/weapons/gift_wrappaper.dm b/code/game/objects/items/weapons/gift_wrappaper.dm index cd48a4a78a7..e4d96a0c79a 100644 --- a/code/game/objects/items/weapons/gift_wrappaper.dm +++ b/code/game/objects/items/weapons/gift_wrappaper.dm @@ -104,7 +104,7 @@ /obj/item/weapon/reagent_containers/food/snacks/grown/ambrosiadeus, /obj/item/weapon/reagent_containers/food/snacks/grown/ambrosiavulgaris, /obj/item/device/paicard, - /obj/item/device/instrument/violin, + /obj/item/instrument/violin, /obj/item/weapon/storage/belt/utility/full, /obj/item/clothing/accessory/tie/horrible) diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm index 046958a915c..2432e79008a 100644 --- a/code/game/objects/objs.dm +++ b/code/game/objects/objs.dm @@ -120,9 +120,6 @@ tgui_interact(user) ..() -/obj/proc/interact(mob/user) - return - /mob/proc/unset_machine() machine?.remove_visual(src) src.machine = null diff --git a/code/game/objects/structures/musician.dm b/code/game/objects/structures/musician.dm deleted file mode 100644 index fa9221f4c39..00000000000 --- a/code/game/objects/structures/musician.dm +++ /dev/null @@ -1,366 +0,0 @@ -//This file was auto-corrected by findeclaration.exe on 25.5.2012 20:42:32 - -#define MUSICIAN_HEARCHECK_MINDELAY 4 -#define INSTRUMENT_MAX_LINE_LENGTH 50 -#define INSTRUMENT_MAX_LINE_NUMBER 300 - -/datum/song - var/name = "Untitled" - var/list/lines = new() - var/tempo = 5 // delay between notes - - var/playing = 0 // if we're playing - var/help = 0 // if help is open - var/edit = 1 // if we're in editing mode - var/repeat = 0 // number of times remaining to repeat - var/max_repeats = 10 // maximum times we can repeat - - var/instrumentDir = "piano" // the folder with the sounds - var/instrumentExt = "ogg" // the file extension - var/obj/instrumentObj = null // the associated obj playing the sound - var/last_hearcheck = 0 - var/list/hearing_mobs - -/datum/song/New(dir, obj, ext = "ogg") - instrumentDir = dir - instrumentObj = obj - instrumentExt = ext - -/datum/song/Destroy() - instrumentObj = null - return ..() - - -// note is a number from 1-7 for A-G -// acc is either "b", "n", or "#" -// oct is 1-8 (or 9 for C) -/datum/song/proc/playnote(note, acc as text, oct) - // handle accidental -> B<>C of E<>F - if(acc == "b" && (note == 3 || note == 6)) // C or F - if(note == 3) - oct-- - note-- - acc = "n" - else if(acc == "#" && (note == 2 || note == 5)) // B or E - if(note == 2) - oct++ - note++ - acc = "n" - else if(acc == "#" && (note == 7)) //G# - note = 1 - acc = "b" - else if(acc == "#") // mass convert all sharps to flats, octave jump already handled - acc = "b" - note++ - - // check octave, C is allowed to go to 9 - if(oct < 1 || (note == 3 ? oct > 9 : oct > 8)) - return - - // now generate name - var/soundfile = "sound/instruments/[instrumentDir]/[ascii2text(note+64)][acc][oct].[instrumentExt]" - soundfile = file(soundfile) - // make sure the note exists - if(!fexists(soundfile)) - return - // and play - var/turf/source = get_turf(instrumentObj) - if((world.time - MUSICIAN_HEARCHECK_MINDELAY) > last_hearcheck) - LAZYCLEARLIST(hearing_mobs) - for(var/mob/M in hearers(15, source)) - if(!M.client || !(M.is_preference_enabled(/datum/client_preference/instrument_toggle))) - continue - LAZYSET(hearing_mobs, M, TRUE) - last_hearcheck = world.time - var/sound/music_played = sound(soundfile) - for(var/i in hearing_mobs) - var/mob/M = i - M.playsound_local(source, null, 100, falloff = 0.5, S = music_played) - -/datum/song/proc/updateDialog(mob/user) - instrumentObj.updateDialog() // assumes it's an object in world, override if otherwise - -/datum/song/proc/shouldStopPlaying(mob/user) - if(instrumentObj) - if(!instrumentObj.Adjacent(user) || user.stat) - return 1 - return !instrumentObj.anchored // add special cases to stop in subclasses - else - return 1 - -/datum/song/proc/playsong(mob/user) - while(repeat >= 0) - var/cur_oct[7] - var/cur_acc[7] - for(var/i = 1 to 7) - cur_oct[i] = 3 - cur_acc[i] = "n" - - for(var/line in lines) - for(var/beat in splittext(lowertext(line), ",")) - var/list/notes = splittext(beat, "/") - for(var/note in splittext(notes[1], "-")) - if(!playing || shouldStopPlaying(user))//If the instrument is playing, or special case - playing = 0 - return - if(length(note) == 0) - continue - var/cur_note = text2ascii(note) - 96 - if(cur_note < 1 || cur_note > 7) - continue - for(var/i=2 to length(note)) - var/ni = copytext(note,i,i+1) - if(!text2num(ni)) - if(ni == "#" || ni == "b" || ni == "n") - cur_acc[cur_note] = ni - else if(ni == "s") - cur_acc[cur_note] = "#" // so shift is never required - else - cur_oct[cur_note] = text2num(ni) - playnote(cur_note, cur_acc[cur_note], cur_oct[cur_note]) - if(notes.len >= 2 && text2num(notes[2])) - sleep(sanitize_tempo(tempo / text2num(notes[2]))) - else - sleep(tempo) - repeat-- - playing = 0 - repeat = 0 - updateDialog(user) - -/datum/song/proc/interact(mob/user) - var/dat = "" - if(lines.len > 0) - dat += "

Playback

" - if(!playing) - dat += {"Play Stop

- Repeat Song: - [repeat > 0 ? "--" : "--"] - [repeat] times - [repeat < max_repeats ? "++" : "++"] -
"} - else - dat += {"Play Stop
- Repeats left: [repeat]
"} - if(!edit) - dat += "
Show Editor
" - else - var/bpm = round(600 / tempo) - dat += {"

Editing

- Hide Editor - Start a New Song - Import a Song

- Tempo: - [bpm] BPM +

"} - var/linecount = 0 - for(var/line in lines) - linecount += 1 - dat += "Line [linecount]: Edit X [line]
" - dat += "Add Line

" - if(help) - dat += {"Hide Help
- Lines are a series of chords, separated by commas (,), each with notes seperated by hyphens (-).
- Every note in a chord will play together, with chord timed by the tempo.
-
- Notes are played by the names of the note, and optionally, the accidental, and/or the octave number.
- By default, every note is natural and in octave 3. Defining otherwise is remembered for each note.
- Example: C,D,E,F,G,A,B will play a C major scale.
- After a note has an accidental placed, it will be remembered: C,C4,C,C3 is C3,C4,C4,C3
- Chords can be played simply by seperating each note with a hyphon: A-C#,Cn-E,E-G#,Gn-B
- A pause may be denoted by an empty chord: C,E,,C,G
- To make a chord be a different time, end it with /x, where the chord length will be length
- defined by tempo / x: C,G/2,E/4
- Combined, an example is: E-E4/4,F#/2,G#/8,B/8,E3-E4/4 -
- Lines may be up to 50 characters.
- A song may only contain up to 50 lines.
- "} - else - dat += "Show Help
" - var/datum/browser/popup = new(user, "instrument", instrumentObj.name, 700, 500) - popup.set_content(dat) - popup.set_title_image(user.browse_rsc_icon(instrumentObj.icon, instrumentObj.icon_state)) - popup.open() - -/datum/song/Topic(href, href_list) - if(!instrumentObj.Adjacent(usr) || usr.stat) - usr << browse(null, "window=instrument") - usr.unset_machine() - return - instrumentObj.add_fingerprint(usr) - if(href_list["newsong"]) - lines = new() - tempo = sanitize_tempo(5) // default 120 BPM - name = "" - else if(href_list["import"]) - var/t = "" - do - t = html_encode(input(usr, "Please paste the entire song, formatted:", text("[]", name), t) as message) - if(!in_range(instrumentObj, usr)) - return - if(length(t) >= INSTRUMENT_MAX_LINE_LENGTH*INSTRUMENT_MAX_LINE_NUMBER) - var/cont = input(usr, "Your message is too long! Would you like to continue editing it?", "", "yes") in list("yes", "no") - if(cont == "no") - break - while(length(t) > INSTRUMENT_MAX_LINE_LENGTH*INSTRUMENT_MAX_LINE_NUMBER) - //split into lines - spawn() - lines = splittext(t, "\n") - if(copytext(lines[1],1,6) == "BPM: ") - tempo = sanitize_tempo(600 / text2num(copytext(lines[1],6))) - lines.Cut(1,2) - else - tempo = sanitize_tempo(5) // default 120 BPM - if(lines.len > INSTRUMENT_MAX_LINE_NUMBER) - to_chat(usr, "Too many lines!") - lines.Cut(INSTRUMENT_MAX_LINE_NUMBER+1) - var/linenum = 1 - for(var/l in lines) - if(length(l) > INSTRUMENT_MAX_LINE_LENGTH) - to_chat(usr, "Line [linenum] too long!") - lines.Remove(l) - else - linenum++ - updateDialog(usr) // make sure updates when complete - else if(href_list["help"]) - help = text2num(href_list["help"]) - 1 - else if(href_list["edit"]) - edit = text2num(href_list["edit"]) - 1 - if(href_list["repeat"]) //Changing this from a toggle to a number of repeats to avoid infinite loops. - if(playing) - return //So that people cant keep adding to repeat. If the do it intentionally, it could result in the server crashing. - repeat += round(text2num(href_list["repeat"])) - if(repeat < 0) - repeat = 0 - if(repeat > max_repeats) - repeat = max_repeats - else if(href_list["tempo"]) - tempo = sanitize_tempo(tempo + text2num(href_list["tempo"])) - else if(href_list["play"]) - playing = 1 - spawn() - playsong(usr) - else if(href_list["newline"]) - var/newline = html_encode(input("Enter your line: ", instrumentObj.name) as text|null) - if(!newline || !in_range(instrumentObj, usr)) - return - if(lines.len > INSTRUMENT_MAX_LINE_NUMBER) - return - if(length(newline) > INSTRUMENT_MAX_LINE_LENGTH) - newline = copytext(newline, 1, INSTRUMENT_MAX_LINE_LENGTH) - lines.Add(newline) - else if(href_list["deleteline"]) - var/num = round(text2num(href_list["deleteline"])) - if(num > lines.len || num < 1) - return - lines.Cut(num, num+1) - else if(href_list["modifyline"]) - var/num = round(text2num(href_list["modifyline"]),1) - var/content = html_encode(input("Enter your line: ", instrumentObj.name, lines[num]) as text|null) - if(!content || !in_range(instrumentObj, usr)) - return - if(length(content) > INSTRUMENT_MAX_LINE_LENGTH) - content = copytext(content, 1, INSTRUMENT_MAX_LINE_LENGTH) - if(num > lines.len || num < 1) - return - lines[num] = content - else if(href_list["stop"]) - playing = 0 - updateDialog(usr) - return - -/datum/song/proc/sanitize_tempo(new_tempo) - new_tempo = abs(new_tempo) - return max(round(new_tempo, world.tick_lag), world.tick_lag) - -// subclass for handheld instruments, like violin -/datum/song/handheld - -/datum/song/handheld/updateDialog(mob/user) - instrumentObj.interact(user) - -/datum/song/handheld/shouldStopPlaying() - if(instrumentObj) - return !isliving(instrumentObj.loc) - else - return 1 - -////////////////////////////////////////////////////////////////////////// -/obj/structure/device/piano - name = "space piano" - desc = "This is a space piano; just like a regular piano, but always in tune! Even if the musician isn't." - icon = 'icons/obj/musician.dmi' - icon_state = "piano" - anchored = 1 - density = 1 - var/datum/song/song - -/obj/structure/device/piano/minimoog - name = "space minimoog" - icon_state = "minimoog" - desc = "This is a minimoog; just like a space piano, but more spacey!" - -/obj/structure/device/piano/New() - ..() - song = new("piano", src) - - if(prob(50)) - name = "space minimoog" - desc = "This is a minimoog, like a space piano, but more spacey!" - icon_state = "minimoog" - else - name = "space piano" - desc = "This is a space piano, like a regular piano, but always in tune! Even if the musician isn't." - icon_state = "piano" - -/obj/structure/device/piano/Destroy() - qdel(song) - song = null - ..() - -/obj/structure/device/piano/verb/rotate_clockwise() - set name = "Rotate Piano Clockwise" - set category = "Object" - set src in oview(1) - - if(ismouse(usr)) - return - if(!usr || !isturf(usr.loc) || usr.stat || usr.restrained()) - return - if (isobserver(usr) && !config.ghost_interaction) - return - src.set_dir(turn(src.dir, 270)) - -/obj/structure/device/piano/attack_hand(mob/user) - if(!user.IsAdvancedToolUser()) - to_chat(user, "You don't have the dexterity to do this!") - return 1 - interact(user) - -/obj/structure/device/piano/interact(mob/user) - if(!user || !anchored) - return - - user.set_machine(src) - song.interact(user) - -/obj/structure/device/piano/attackby(obj/item/O as obj, mob/user as mob) - if(O.is_wrench()) - if(anchored) - playsound(src, O.usesound, 50, 1) - to_chat(user, "You begin to loosen \the [src]'s casters...") - if (do_after(user, 40 * O.toolspeed)) - user.visible_message( \ - "[user] loosens \the [src]'s casters.", \ - "You have loosened \the [src]. Now it can be pulled somewhere else.", \ - "You hear ratchet.") - src.anchored = 0 - else - playsound(src, O.usesound, 50, 1) - to_chat(user, "You begin to tighten \the [src] to the floor...") - if (do_after(user, 20 * O.toolspeed)) - user.visible_message( \ - "[user] tightens \the [src]'s casters.", \ - "You have tightened \the [src]'s casters. Now it can be played again.", \ - "You hear ratchet.") - src.anchored = 1 - else - ..() diff --git a/code/game/sound.dm b/code/game/sound.dm index 5b1214c6751..61fc50368ca 100644 --- a/code/game/sound.dm +++ b/code/game/sound.dm @@ -9,7 +9,7 @@ var/area/area_source = turf_source.loc //allocate a channel if necessary now so its the same for everyone - channel = channel || open_sound_channel() + channel = channel || SSsounds.random_available_channel() // Looping through the player list has the added bonus of working for mobs inside containers var/sound/S = sound(get_sfx(soundin)) @@ -43,7 +43,7 @@ S = sound(get_sfx(soundin)) S.wait = 0 //No queue - S.channel = channel || open_sound_channel() + S.channel = channel || SSsounds.random_available_channel() // I'm not sure if you can modify S.volume, but I'd rather not try to find out what // horrible things lurk in BYOND's internals, so we're just gonna do vol *= @@ -109,15 +109,14 @@ var/mob/MO = M MO.playsound_local(get_turf(MO), sound, volume, vary, pressure_affected = FALSE) -/proc/open_sound_channel() - var/static/next_channel = 1 //loop through the available 1024 - (the ones we reserve) channels and pray that its not still being used - . = ++next_channel - if(next_channel > CHANNEL_HIGHEST_AVAILABLE) - next_channel = 1 - /mob/proc/stop_sound_channel(chan) src << sound(null, repeat = 0, wait = 0, channel = chan) +/mob/proc/set_sound_channel_volume(channel, volume) + var/sound/S = sound(null, FALSE, FALSE, channel, volume) + S.status = SOUND_UPDATE + src << S + /proc/get_rand_frequency() return rand(32000, 55000) //Frequency stuff only works with 45kbps oggs. diff --git a/code/modules/instruments/instrument_data/_instrument_data.dm b/code/modules/instruments/instrument_data/_instrument_data.dm new file mode 100644 index 00000000000..39d16e499f2 --- /dev/null +++ b/code/modules/instruments/instrument_data/_instrument_data.dm @@ -0,0 +1,113 @@ +/** + * Get all non admin_only instruments as a list of text ids. + */ +/proc/get_allowed_instrument_ids() + . = list() + for(var/id in SSinstruments.instrument_data) + var/datum/instrument/I = SSinstruments.instrument_data[id] + if(!I.admin_only) + . += I.id + +/** + * # Instrument Datums + * + * Instrument datums hold the data for any given instrument, as well as data on how to play it and what bounds there are to playing it. + * + * The datums themselves are kept in SSinstruments in a list by their unique ID. The reason it uses ID instead of typepath is to support the runtime creation of instruments. + * Since songs cache them while playing, there isn't realistic issues regarding performance from accessing. + */ +/datum/instrument + /// Name of the instrument + var/name = "Generic instrument" + /// Uniquely identifies this instrument so runtime changes are possible as opposed to paths. If this is unset, things will use path instead. + var/id + /// Category + var/category = "Unsorted" + /// Used for categorization subtypes + var/abstract_type = /datum/instrument + /// Write here however many samples, follow this syntax: "%note num%"='%sample file%' eg. "27"='synthesizer/e2.ogg'. Key must never be lower than 0 and higher than 127 + var/list/real_samples + /// assoc list key = /datum/instrument_key. do not fill this yourself! + var/list/samples + /// See __DEFINES/flags/instruments.dm + var/instrument_flags = NONE + /// For legacy instruments, the path to our notes + var/legacy_instrument_path + /// For legacy instruments, our file extension + var/legacy_instrument_ext + /// What songs are using us + var/list/datum/song/songs_using = list() + /// Don't touch this + var/static/HIGHEST_KEY = 127 + /// Don't touch this x2 + var/static/LOWEST_KEY = 0 + /// Oh no - For truly troll instruments. + var/admin_only = FALSE + /// Volume multiplier. Synthesized instruments are quite loud and I don't like to cut off potential detail via editing. (someone correct me if this isn't a thing) + var/volume_multiplier = 0.33 + +/datum/instrument/New() + if(isnull(id)) + id = "[type]" + +/** + * Initializes the instrument, calculating its samples if necessary. + */ +/datum/instrument/proc/Initialize() + if(instrument_flags & (INSTRUMENT_LEGACY | INSTRUMENT_DO_NOT_AUTOSAMPLE)) + return + calculate_samples() + +/** + * Checks if this instrument is ready to play. + */ +/datum/instrument/proc/ready() + if(instrument_flags & INSTRUMENT_LEGACY) + return legacy_instrument_path && legacy_instrument_ext + else if(instrument_flags & INSTRUMENT_DO_NOT_AUTOSAMPLE) + return length(samples) + return (length(samples) >= 128) + +/datum/instrument/Destroy() + SSinstruments.instrument_data -= id + for(var/i in songs_using) + var/datum/song/S = i + S.set_instrument(null) + real_samples = null + samples = null + songs_using = null + return ..() + +/** + * For synthesized instruments, this is how the instrument generates the "keys" that a [/datum/song] uses to play notes. + * Calculating them on the fly would be unperformant, so we do it during init and keep it all cached in a list. + */ +/datum/instrument/proc/calculate_samples() + if(!length(real_samples)) + CRASH("No real samples defined for [id] [type] on calculate_samples() call.") + var/list/real_keys = list() + samples = list() + for(var/key in real_samples) + real_keys += text2num(key) + sortTim(real_keys, /proc/cmp_numeric_asc, associative = FALSE) + + for(var/i in 1 to (length(real_keys) - 1)) + var/from_key = real_keys[i] + var/to_key = real_keys[i+1] + var/sample1 = real_samples[num2text(from_key)] + var/sample2 = real_samples[num2text(to_key)] + var/pivot = FLOOR((from_key + to_key) / 2, 1) //original code was a round but I replaced it because that's effectively a floor, thanks Baystation! who knows what was intended. + for(var/key in from_key to pivot) + samples[num2text(key)] = new /datum/instrument_key(sample1, key, key - from_key) + for(var/key in (pivot + 1) to to_key) + samples[num2text(key)] = new /datum/instrument_key(sample2, key, key - to_key) + + // Fill in 0 to first key and last key to 127 + var/first_key = real_keys[1] + var/last_key = real_keys[length(real_keys)] + var/first_sample = real_samples[num2text(first_key)] + var/last_sample = real_samples[num2text(last_key)] + for(var/key in LOWEST_KEY to (first_key - 1)) + samples[num2text(key)] = new /datum/instrument_key(first_sample, key, key - first_key) + for(var/key in last_key to HIGHEST_KEY) + samples[num2text(key)] = new /datum/instrument_key(last_sample, key, key - last_key) diff --git a/code/modules/instruments/instrument_data/_instrument_key.dm b/code/modules/instruments/instrument_data/_instrument_key.dm new file mode 100644 index 00000000000..6038b7b76b0 --- /dev/null +++ b/code/modules/instruments/instrument_data/_instrument_key.dm @@ -0,0 +1,31 @@ +/** + * Instrument key datums contain everything needed to know how to play a specific + * note of an instrument.* + */ +/datum/instrument_key + /// The numerical key of what this is, from 1 to 127 on a standard piano keyboard. + var/key + /// The actual sample file that will be loaded when playing. + var/sample + /// The frequency to play the sample to get our desired note. + var/frequency + /// Deviation up/down from the pivot point that uses its sample. Used to calculate frequency. + var/deviation + +/datum/instrument_key/New(sample = src.sample, key = src.key, deviation = src.deviation, frequency = src.frequency) + src.sample = sample + src.key = key + src.deviation = deviation + src.frequency = frequency + if(!frequency && deviation) + calculate() + +/** + * Calculates and stores our deviation. + */ +/datum/instrument_key/proc/calculate() + if(!deviation) + CRASH("Invalid calculate call: No deviation or sample in instrument_key") + #define KEY_TWELTH (1/12) + frequency = 2 ** (KEY_TWELTH * deviation) + #undef KEY_TWELTH diff --git a/code/modules/instruments/instrument_data/brass.dm b/code/modules/instruments/instrument_data/brass.dm new file mode 100644 index 00000000000..7f8f103831f --- /dev/null +++ b/code/modules/instruments/instrument_data/brass.dm @@ -0,0 +1,26 @@ +/datum/instrument/brass + name = "Generic brass instrument" + category = "Brass" + abstract_type = /datum/instrument/brass + +/datum/instrument/brass/crisis_section + name = "Crisis Brass Section" + id = "crbrass" + real_samples = list("36"='sound/instruments/synthesis_samples/brass/crisis_brass/c2.ogg', + "48"='sound/instruments/synthesis_samples/brass/crisis_brass/c3.ogg', + "60"='sound/instruments/synthesis_samples/brass/crisis_brass/c4.ogg', + "72"='sound/instruments/synthesis_samples/brass/crisis_brass/c5.ogg') + +/datum/instrument/brass/crisis_trombone + name = "Crisis Trombone" + id = "crtrombone" + real_samples = list("36"='sound/instruments/synthesis_samples/brass/crisis_trombone/c2.ogg', + "48"='sound/instruments/synthesis_samples/brass/crisis_trombone/c3.ogg', + "60"='sound/instruments/synthesis_samples/brass/crisis_trombone/c4.ogg', + "72"='sound/instruments/synthesis_samples/brass/crisis_trombone/c5.ogg') + +/datum/instrument/brass/crisis_trumpet + name = "Crisis Trumpet" + id = "crtrumpet" + real_samples = list("60"='sound/instruments/synthesis_samples/brass/crisis_trumpet/c4.ogg', + "72"='sound/instruments/synthesis_samples/brass/crisis_trumpet/c5.ogg') diff --git a/code/modules/instruments/instrument_data/chromatic_percussion.dm b/code/modules/instruments/instrument_data/chromatic_percussion.dm new file mode 100644 index 00000000000..cafa9e31edb --- /dev/null +++ b/code/modules/instruments/instrument_data/chromatic_percussion.dm @@ -0,0 +1,31 @@ +/datum/instrument/chromatic + name = "Generic chromatic percussion instrument" + category = "Chromatic percussion" + abstract_type = /datum/instrument/chromatic + +/datum/instrument/chromatic/vibraphone1 + name = "Crisis Vibraphone" + id = "crvibr" + real_samples = list("36"='sound/instruments/synthesis_samples/chromatic/vibraphone1/c2.ogg', + "48"='sound/instruments/synthesis_samples/chromatic/vibraphone1/c3.ogg', + "60"='sound/instruments/synthesis_samples/chromatic/vibraphone1/c4.ogg', + "72"='sound/instruments/synthesis_samples/chromatic/vibraphone1/c5.ogg') + +/datum/instrument/chromatic/musicbox1 + name = "SGM Music Box" + id = "sgmmbox" + real_samples = list("36"='sound/instruments/synthesis_samples/chromatic/sgmbox/c2.ogg', + "48"='sound/instruments/synthesis_samples/chromatic/sgmbox/c3.ogg', + "60"='sound/instruments/synthesis_samples/chromatic/sgmbox/c4.ogg', + "72"='sound/instruments/synthesis_samples/chromatic/sgmbox/c5.ogg') + +/datum/instrument/chromatic/fluid_celeste + name = "FluidR3 Celeste" + id = "r3celeste" + real_samples = list("36"='sound/instruments/synthesis_samples/chromatic/fluid_celeste/c2.ogg', + "48"='sound/instruments/synthesis_samples/chromatic/fluid_celeste/c3.ogg', + "60"='sound/instruments/synthesis_samples/chromatic/fluid_celeste/c4.ogg', + "72"='sound/instruments/synthesis_samples/chromatic/fluid_celeste/c5.ogg', + "84"='sound/instruments/synthesis_samples/chromatic/fluid_celeste/c6.ogg', + "96"='sound/instruments/synthesis_samples/chromatic/fluid_celeste/c7.ogg', + "108"='sound/instruments/synthesis_samples/chromatic/fluid_celeste/c8.ogg') diff --git a/code/modules/instruments/instrument_data/fun.dm b/code/modules/instruments/instrument_data/fun.dm new file mode 100644 index 00000000000..790abe46473 --- /dev/null +++ b/code/modules/instruments/instrument_data/fun.dm @@ -0,0 +1,25 @@ +/datum/instrument/fun + name = "Generic Fun Instrument" + category = "Fun" + abstract_type = /datum/instrument/fun + +/datum/instrument/fun/honk + name = "!!HONK!!" + id = "honk" + real_samples = list("74"='sound/items/bikehorn.ogg') // Cluwne Heaven + +/datum/instrument/fun/signal + name = "Ping" + id = "ping" + real_samples = list("79"='sound/machines/ping.ogg') + +/datum/instrument/fun/chime + name = "Chime" + id = "chime" + real_samples = list("79"='sound/machines/chime.ogg') + +/datum/instrument/fun/mothscream + name = "Moth Scream" + id = "mothscream" + real_samples = list("60"='sound/voice/moth/scream_moth.ogg') + admin_only = TRUE diff --git a/code/modules/instruments/instrument_data/guitar.dm b/code/modules/instruments/instrument_data/guitar.dm new file mode 100644 index 00000000000..be7cfbe467b --- /dev/null +++ b/code/modules/instruments/instrument_data/guitar.dm @@ -0,0 +1,36 @@ +/datum/instrument/guitar + name = "Generic guitar-like instrument" + category = "Guitar" + abstract_type = /datum/instrument/guitar + +/datum/instrument/guitar/steel_crisis + name = "Crisis Steel String Guitar" + id = "csteelgt" + real_samples = list("36"='sound/instruments/synthesis_samples/guitar/crisis_steel/c2.ogg', + "48"='sound/instruments/synthesis_samples/guitar/crisis_steel/c3.ogg', + "60"='sound/instruments/synthesis_samples/guitar/crisis_steel/c4.ogg', + "72"='sound/instruments/synthesis_samples/guitar/crisis_steel/c5.ogg') + +/datum/instrument/guitar/nylon_crisis + name = "Crisis Nylon String Guitar" + id = "cnylongt" + real_samples = list("36"='sound/instruments/synthesis_samples/guitar/crisis_nylon/c2.ogg', + "48"='sound/instruments/synthesis_samples/guitar/crisis_nylon/c3.ogg', + "60"='sound/instruments/synthesis_samples/guitar/crisis_nylon/c4.ogg', + "72"='sound/instruments/synthesis_samples/guitar/crisis_nylon/c5.ogg') + +/datum/instrument/guitar/clean_crisis + name = "Crisis Clean Guitar" + id = "ccleangt" + real_samples = list("36"='sound/instruments/synthesis_samples/guitar/crisis_clean/c2.ogg', + "48"='sound/instruments/synthesis_samples/guitar/crisis_clean/c3.ogg', + "60"='sound/instruments/synthesis_samples/guitar/crisis_clean/c4.ogg', + "72"='sound/instruments/synthesis_samples/guitar/crisis_clean/c5.ogg') + +/datum/instrument/guitar/muted_crisis + name = "Crisis Muted Guitar" + id = "cmutedgt" + real_samples = list("36"='sound/instruments/synthesis_samples/guitar/crisis_muted/c2.ogg', + "48"='sound/instruments/synthesis_samples/guitar/crisis_muted/c3.ogg', + "60"='sound/instruments/synthesis_samples/guitar/crisis_muted/c4.ogg', + "72"='sound/instruments/synthesis_samples/guitar/crisis_muted/c5.ogg') diff --git a/code/modules/instruments/instrument_data/hardcoded.dm b/code/modules/instruments/instrument_data/hardcoded.dm new file mode 100644 index 00000000000..c770f9569e3 --- /dev/null +++ b/code/modules/instruments/instrument_data/hardcoded.dm @@ -0,0 +1,86 @@ +//THESE ARE HARDCODED INSTRUMENT SAMPLES. +//SONGS WILL BE AUTOMATICALLY SWITCHED TO LEGACY MODE IF THEY USE THIS KIND OF INSTRUMENT! +//I'd prefer these stayed. They sound different from the mechanical synthesis of synthed instruments, and I quite like them that way. It's not legacy, it's hardcoded, old style. - kevinz000 +/datum/instrument/hardcoded + abstract_type = /datum/instrument/hardcoded + category = "Non-Synthesized" + instrument_flags = INSTRUMENT_LEGACY + volume_multiplier = 1 //not as loud as synth'd + +/datum/instrument/hardcoded/accordion + name = "Accordion" + id = "accordion" + legacy_instrument_ext = "mid" + legacy_instrument_path = "accordion" + +/datum/instrument/hardcoded/bikehorn + name = "Bike Horn" + id = "bikehorn" + legacy_instrument_ext = "ogg" + legacy_instrument_path = "bikehorn" + +/datum/instrument/hardcoded/eguitar + name = "Electric Guitar" + id = "eguitar" + legacy_instrument_ext = "ogg" + legacy_instrument_path = "eguitar" + +/datum/instrument/hardcoded/glockenspiel + name = "Glockenspiel" + id = "glockenspiel" + legacy_instrument_ext = "mid" + legacy_instrument_path = "glockenspiel" + +/datum/instrument/hardcoded/guitar + name = "Guitar" + id = "guitar" + legacy_instrument_ext = "ogg" + legacy_instrument_path = "guitar" + +/datum/instrument/hardcoded/harmonica + name = "Harmonica" + id = "harmonica" + legacy_instrument_ext = "mid" + legacy_instrument_path = "harmonica" + +/datum/instrument/hardcoded/piano + name = "Piano" + id = "piano" + legacy_instrument_ext = "ogg" + legacy_instrument_path = "piano" + +/datum/instrument/hardcoded/recorder + name = "Recorder" + id = "recorder" + legacy_instrument_ext = "mid" + legacy_instrument_path = "recorder" + +/datum/instrument/hardcoded/saxophone + name = "Saxophone" + id = "saxophone" + legacy_instrument_ext = "mid" + legacy_instrument_path = "saxophone" + +/datum/instrument/hardcoded/trombone + name = "Trombone" + id = "trombone" + legacy_instrument_ext = "mid" + legacy_instrument_path = "trombone" + +/datum/instrument/hardcoded/violin + name = "Violin" + id = "violin" + legacy_instrument_ext = "mid" + legacy_instrument_path = "violin" + +/datum/instrument/hardcoded/xylophone + name = "Xylophone" + id = "xylophone" + legacy_instrument_ext = "mid" + legacy_instrument_path = "xylophone" + +/datum/instrument/hardcoded/banjo + name = "Banjo" + id = "banjo" + legacy_instrument_ext = "ogg" + legacy_instrument_path = "banjo" diff --git a/code/modules/instruments/instrument_data/organ.dm b/code/modules/instruments/instrument_data/organ.dm new file mode 100644 index 00000000000..25da7409980 --- /dev/null +++ b/code/modules/instruments/instrument_data/organ.dm @@ -0,0 +1,43 @@ +/datum/instrument/organ + name = "Generic organ" + category = "Organ" + abstract_type = /datum/instrument/organ + +/datum/instrument/organ/crisis_church + name = "Crisis Church Organ" + id = "crichugan" + real_samples = list("36"='sound/instruments/synthesis_samples/organ/crisis_church/c2.ogg', + "48"='sound/instruments/synthesis_samples/organ/crisis_church/c3.ogg', + "60"='sound/instruments/synthesis_samples/organ/crisis_church/c4.ogg', + "72"='sound/instruments/synthesis_samples/organ/crisis_church/c5.ogg') + +/datum/instrument/organ/crisis_hammond + name = "Crisis Hammond Organ" + id = "crihamgan" + real_samples = list("36"='sound/instruments/synthesis_samples/organ/crisis_hammond/c2.ogg', + "48"='sound/instruments/synthesis_samples/organ/crisis_hammond/c3.ogg', + "60"='sound/instruments/synthesis_samples/organ/crisis_hammond/c4.ogg', + "72"='sound/instruments/synthesis_samples/organ/crisis_hammond/c5.ogg') + +/datum/instrument/organ/crisis_accordian + name = "Crisis Accordian" + id = "crack" + real_samples = list("36"='sound/instruments/synthesis_samples/organ/crisis_accordian/c2.ogg', + "48"='sound/instruments/synthesis_samples/organ/crisis_accordian/c3.ogg', + "60"='sound/instruments/synthesis_samples/organ/crisis_accordian/c4.ogg', + "72"='sound/instruments/synthesis_samples/organ/crisis_accordian/c5.ogg') + +/datum/instrument/organ/crisis_harmonica + name = "Crisis Harmonica" + id = "crharmony" + real_samples = list("48"='sound/instruments/synthesis_samples/organ/crisis_harmonica/c3.ogg', + "60"='sound/instruments/synthesis_samples/organ/crisis_harmonica/c4.ogg', + "72"='sound/instruments/synthesis_samples/organ/crisis_harmonica/c5.ogg') + +/datum/instrument/organ/crisis_tango_accordian + name = "Crisis Tango Accordian" + id = "crtango" + real_samples = list("36"='sound/instruments/synthesis_samples/organ/crisis_tangaccordian/c2.ogg', + "48"='sound/instruments/synthesis_samples/organ/crisis_tangaccordian/c3.ogg', + "60"='sound/instruments/synthesis_samples/organ/crisis_tangaccordian/c4.ogg', + "72"='sound/instruments/synthesis_samples/organ/crisis_tangaccordian/c5.ogg') diff --git a/code/modules/instruments/instrument_data/piano.dm b/code/modules/instruments/instrument_data/piano.dm new file mode 100644 index 00000000000..fdd2f6e9382 --- /dev/null +++ b/code/modules/instruments/instrument_data/piano.dm @@ -0,0 +1,56 @@ +/datum/instrument/piano + name = "Generic piano" + category = "Piano" + abstract_type = /datum/instrument/piano + +/datum/instrument/piano/fluid_piano + name = "FluidR3 Grand Piano" + id = "r3grand" + real_samples = list("36"='sound/instruments/synthesis_samples/piano/fluid_piano/c2.ogg', + "48"='sound/instruments/synthesis_samples/piano/fluid_piano/c3.ogg', + "60"='sound/instruments/synthesis_samples/piano/fluid_piano/c4.ogg', + "72"='sound/instruments/synthesis_samples/piano/fluid_piano/c5.ogg', + "84"='sound/instruments/synthesis_samples/piano/fluid_piano/c6.ogg', + "96"='sound/instruments/synthesis_samples/piano/fluid_piano/c7.ogg', + "108"='sound/instruments/synthesis_samples/piano/fluid_piano/c8.ogg') + +/datum/instrument/piano/fluid_harpsichord + name = "FluidR3 Harpsichord" + id = "r3harpsi" + real_samples = list("36"='sound/instruments/synthesis_samples/piano/fluid_harpsi/c2.ogg', + "48"='sound/instruments/synthesis_samples/piano/fluid_harpsi/c3.ogg', + "60"='sound/instruments/synthesis_samples/piano/fluid_harpsi/c4.ogg', + "72"='sound/instruments/synthesis_samples/piano/fluid_harpsi/c5.ogg', + "84"='sound/instruments/synthesis_samples/piano/fluid_harpsi/c6.ogg', + "96"='sound/instruments/synthesis_samples/piano/fluid_harpsi/c7.ogg', + "108"='sound/instruments/synthesis_samples/piano/fluid_harpsi/c8.ogg') + +/datum/instrument/piano/crisis_harpsichord + name = "Crisis Harpsichord" + id = "crharpsi" + real_samples = list("36"='sound/instruments/synthesis_samples/piano/crisis_harpsichord/c2.ogg', + "48"='sound/instruments/synthesis_samples/piano/crisis_harpsichord/c3.ogg', + "60"='sound/instruments/synthesis_samples/piano/crisis_harpsichord/c4.ogg', + "72"='sound/instruments/synthesis_samples/piano/crisis_harpsichord/c5.ogg') + +/datum/instrument/piano/crisis_grandpiano_uni + name = "Crisis Grand Piano One" + id = "crgrand1" + real_samples = list("36"='sound/instruments/synthesis_samples/piano/crisis_grand_piano/c2.ogg', + "48"='sound/instruments/synthesis_samples/piano/crisis_grand_piano/c3.ogg', + "60"='sound/instruments/synthesis_samples/piano/crisis_grand_piano/c4.ogg', + "72"='sound/instruments/synthesis_samples/piano/crisis_grand_piano/c5.ogg', + "84"='sound/instruments/synthesis_samples/piano/crisis_grand_piano/c6.ogg', + "96"='sound/instruments/synthesis_samples/piano/crisis_grand_piano/c7.ogg', + "108"='sound/instruments/synthesis_samples/piano/crisis_grand_piano/c8.ogg') + +/datum/instrument/piano/crisis_brightpiano_uni + name = "Crisis Bright Piano One" + id = "crbright1" + real_samples = list("36"='sound/instruments/synthesis_samples/piano/crisis_bright_piano/c2.ogg', + "48"='sound/instruments/synthesis_samples/piano/crisis_bright_piano/c3.ogg', + "60"='sound/instruments/synthesis_samples/piano/crisis_bright_piano/c4.ogg', + "72"='sound/instruments/synthesis_samples/piano/crisis_bright_piano/c5.ogg', + "84"='sound/instruments/synthesis_samples/piano/crisis_bright_piano/c6.ogg', + "96"='sound/instruments/synthesis_samples/piano/crisis_bright_piano/c7.ogg', + "108"='sound/instruments/synthesis_samples/piano/crisis_bright_piano/c8.ogg') diff --git a/code/modules/instruments/instrument_data/synth_tones.dm b/code/modules/instruments/instrument_data/synth_tones.dm new file mode 100644 index 00000000000..9ad9250f40d --- /dev/null +++ b/code/modules/instruments/instrument_data/synth_tones.dm @@ -0,0 +1,19 @@ +/datum/instrument/tones + name = "Ideal tone" + category = "Tones" + abstract_type = /datum/instrument/tones + +/datum/instrument/tones/square_wave + name = "Ideal square wave" + id = "square" + real_samples = list("81"='sound/instruments/synthesis_samples/tones/Square.ogg') + +/datum/instrument/tones/sine_wave + name = "Ideal sine wave" + id = "sine" + real_samples = list("81"='sound/instruments/synthesis_samples/tones/Sine.ogg') + +/datum/instrument/tones/saw_wave + name = "Ideal sawtooth wave" + id = "saw" + real_samples = list("81"='sound/instruments/synthesis_samples/tones/Sawtooth.ogg') diff --git a/code/modules/instruments/items.dm b/code/modules/instruments/items.dm new file mode 100644 index 00000000000..7318fe78981 --- /dev/null +++ b/code/modules/instruments/items.dm @@ -0,0 +1,309 @@ +//copy pasta of the space piano, don't hurt me -Pete +/obj/item/instrument + name = "generic instrument" + force = 10 + health = 100 + //resistance_flags = FLAMMABLE + icon = 'icons/obj/musician.dmi' + item_icons = list( + slot_l_hand_str = 'icons/mob/items/lefthand_instruments.dmi', + slot_r_hand_str = 'icons/mob/items/righthand_instruments.dmi', + ) + + /// Our song datum. + var/datum/song/handheld/song + /// Our allowed list of instrument ids. This is nulled on initialize. + var/list/allowed_instrument_ids + /// How far away our song datum can be heard. + var/instrument_range = 15 + +/obj/item/instrument/Initialize(mapload) + . = ..() + song = new(src, allowed_instrument_ids, instrument_range) + allowed_instrument_ids = null //We don't need this clogging memory after it's used. + +/obj/item/instrument/Destroy() + QDEL_NULL(song) + return ..() + +/obj/item/instrument/proc/should_stop_playing(mob/user) + return user.incapacitated() || !((loc == user) || (isturf(loc) && Adjacent(user))) // sorry, no more TK playing. + +/obj/item/instrument/suicide_act(mob/user) + var/datum/gender/T = gender_datums[user.get_visible_gender()] + user.visible_message("[user] begins to play 'Gloomy Sunday'! It looks like [T.hes] trying to commit suicide!") + return (BRUTELOSS) + +/obj/item/instrument/attack_self(mob/user) + if(!user.IsAdvancedToolUser()) + to_chat(user, "You don't have the dexterity to do this!") + return TRUE + interact(user) + +/obj/item/instrument/interact(mob/living/user) + if(!isliving(user) || user.incapacitated()) + return + + user.set_machine(src) + song.interact(user) + +/obj/item/instrument/violin + name = "space violin" + desc = "A wooden musical instrument with four strings and a bow. \"The devil went down to space, he was looking for an assistant to grief.\"" + icon_state = "violin" + hitsound = "swing_hit" + allowed_instrument_ids = "violin" + +/obj/item/instrument/violin/golden + name = "golden violin" + desc = "A golden musical instrument with four strings and a bow. \"The devil went down to space, he was looking for an assistant to grief.\"" + icon_state = "golden_violin" + +/obj/item/instrument/piano_synth + name = "synthesizer" + desc = "An advanced electronic synthesizer that can be used as various instruments." + icon_state = "synth" + allowed_instrument_ids = "piano" + +/obj/item/instrument/piano_synth/Initialize(mapload) + . = ..() + song.allowed_instrument_ids = SSinstruments.synthesizer_instrument_ids + +/* I'll come back to you... +/obj/item/instrument/piano_synth/headphones + name = "headphones" + desc = "Unce unce unce unce. Boop!" + icon = 'icons/obj/clothing/accessories.dmi' + lefthand_file = 'icons/mob/inhands/clothing_lefthand.dmi' + righthand_file = 'icons/mob/inhands/clothing_righthand.dmi' + icon_state = "headphones" + inhand_icon_state = "headphones" + slot_flags = ITEM_SLOT_EARS | ITEM_SLOT_HEAD + force = 0 + w_class = WEIGHT_CLASS_SMALL + custom_price = PAYCHECK_ASSISTANT * 2.5 + instrument_range = 1 + +/obj/item/instrument/piano_synth/headphones/ComponentInitialize() + . = ..() + AddElement(/datum/element/update_icon_updates_onmob) + RegisterSignal(src, COMSIG_SONG_START, .proc/start_playing) + RegisterSignal(src, COMSIG_SONG_END, .proc/stop_playing) + +/** + * Called by a component signal when our song starts playing. + */ +/obj/item/instrument/piano_synth/headphones/proc/start_playing() + SIGNAL_HANDLER + icon_state = "[initial(icon_state)]_on" + update_appearance() + +/** + * Called by a component signal when our song stops playing. + */ +/obj/item/instrument/piano_synth/headphones/proc/stop_playing() + SIGNAL_HANDLER + icon_state = "[initial(icon_state)]" + update_appearance() + +/obj/item/instrument/piano_synth/headphones/spacepods + name = "\improper Nanotrasen space pods" + desc = "Flex your money, AND ignore what everyone else says, all at once!" + icon_state = "spacepods" + inhand_icon_state = "spacepods" + slot_flags = ITEM_SLOT_EARS + strip_delay = 100 //air pods don't fall out + instrument_range = 0 //you're paying for quality here + custom_premium_price = PAYCHECK_ASSISTANT * 36 //Save up 5 shifts worth of pay just to lose it down a drainpipe on the sidewalk +*/ + +/obj/item/instrument/banjo + name = "banjo" + desc = "A 'Mura' brand banjo. It's pretty much just a drum with a neck and strings." + icon_state = "banjo" + attack_verb = list("scruggs-styled", "hum-diggitied", "shin-dug", "clawhammered") + hitsound = 'sound/weapons/banjoslap.ogg' + allowed_instrument_ids = "banjo" + +/obj/item/instrument/guitar + name = "guitar" + desc = "It's made of wood and has bronze strings." + icon_state = "guitar" + attack_verb = list("played metal on", "serenaded", "crashed", "smashed") + hitsound = 'sound/weapons/stringsmash.ogg' + allowed_instrument_ids = list("guitar","csteelgt","cnylongt", "ccleangt", "cmutedgt") + +/obj/item/instrument/eguitar + name = "electric guitar" + desc = "Makes all your shredding needs possible." + icon_state = "eguitar" + force = 12 + attack_verb = list("played metal on", "shreded", "crashed", "smashed") + hitsound = 'sound/weapons/stringsmash.ogg' + allowed_instrument_ids = "eguitar" + +/obj/item/instrument/glockenspiel + name = "glockenspiel" + desc = "Smooth metal bars perfect for any marching band." + icon_state = "glockenspiel" + allowed_instrument_ids = list("glockenspiel","crvibr", "sgmmbox", "r3celeste") + +/obj/item/instrument/accordion + name = "accordion" + desc = "Pun-Pun not included." + icon_state = "accordion" + allowed_instrument_ids = list("crack", "crtango", "accordion") + +/obj/item/instrument/trumpet + name = "trumpet" + desc = "To announce the arrival of the king!" + icon_state = "trumpet" + allowed_instrument_ids = "crtrumpet" + +/obj/item/instrument/trumpet/spectral + name = "spectral trumpet" + desc = "Things are about to get spooky!" + icon_state = "spectral_trumpet" + force = 0 + attack_verb = list("played", "jazzed", "trumpeted", "mourned", "dooted", "spooked") + +/* +/obj/item/instrument/trumpet/spectral/Initialize() + . = ..() + AddComponent(/datum/component/spooky) +*/ +/obj/item/instrument/trumpet/spectral/attack(mob/living/carbon/C, mob/user) + playsound (src, 'sound/instruments/trombone/En4.mid', 100,1,-1) + ..() + +/obj/item/instrument/saxophone + name = "saxophone" + desc = "This soothing sound will be sure to leave your audience in tears." + icon_state = "saxophone" + allowed_instrument_ids = "saxophone" + +/obj/item/instrument/saxophone/spectral + name = "spectral saxophone" + desc = "This spooky sound will be sure to leave mortals in bones." + icon_state = "saxophone" + force = 0 + attack_verb = list("played", "jazzed", "saxed", "mourned", "dooted", "spooked") + +/* +/obj/item/instrument/saxophone/spectral/Initialize() + . = ..() + AddComponent(/datum/component/spooky) +*/ + +/obj/item/instrument/saxophone/spectral/attack(mob/living/carbon/C, mob/user) + playsound (src, 'sound/instruments/saxophone/En4.mid', 100,1,-1) + ..() + +/obj/item/instrument/trombone + name = "trombone" + desc = "How can any pool table ever hope to compete?" + icon_state = "trombone" + allowed_instrument_ids = list("crtrombone", "crbrass", "trombone") + +/obj/item/instrument/trombone/spectral + name = "spectral trombone" + desc = "A skeleton's favorite instrument. Apply directly on the mortals." + icon_state = "trombone" + force = 0 + attack_verb = list("played", "jazzed", "tromboneed", "mourned", "dooted", "spooked") + +/* +/obj/item/instrument/trombone/spectral/Initialize() + . = ..() + AddComponent(/datum/component/spooky) +*/ + +/obj/item/instrument/trombone/spectral/attack(mob/living/carbon/C, mob/user) + playsound (src, 'sound/instruments/trombone/Cn4.mid', 100,1,-1) + ..() + +/obj/item/instrument/recorder + name = "recorder" + desc = "Just like in school, playing ability and all." + force = 5 + icon_state = "recorder" + allowed_instrument_ids = "recorder" + +/obj/item/instrument/harmonica + name = "harmonica" + desc = "For when you get a bad case of the space blues." + icon_state = "harmonica" + allowed_instrument_ids = list("crharmony", "harmonica") + slot_flags = SLOT_MASK + force = 5 + w_class = ITEMSIZE_SMALL +/* + actions_types = list(/datum/action/item_action/instrument) + +/obj/item/instrument/harmonica/proc/handle_speech(datum/source, list/speech_args) + SIGNAL_HANDLER + if(song.playing && ismob(loc)) + to_chat(loc, "You stop playing the harmonica to talk...") + song.playing = FALSE + +/obj/item/instrument/harmonica/equipped(mob/M, slot) + . = ..() + RegisterSignal(M, COMSIG_MOB_SAY, .proc/handle_speech) + +/obj/item/instrument/harmonica/dropped(mob/M) + . = ..() + UnregisterSignal(M, COMSIG_MOB_SAY) +*/ +/obj/item/instrument/bikehorn + name = "gilded bike horn" + desc = "An exquisitely decorated bike horn, capable of honking in a variety of notes." + icon_state = "bike_horn" + item_icons = list( + slot_l_hand_str = 'icons/mob/items/lefthand_horns.dmi', + slot_r_hand_str = 'icons/mob/items/righthand_horns.dmi', + ) + allowed_instrument_ids = list("bikehorn", "honk") + attack_verb = list("beautifully honked") + w_class = ITEMSIZE_SMALL + force = 0 + throw_speed = 3 + throw_range = 15 + hitsound = 'sound/items/bikehorn.ogg' +/* +/obj/item/choice_beacon/music + name = "instrument delivery beacon" + desc = "Summon your tool of art." + icon_state = "gangtool-red" + +/obj/item/choice_beacon/music/generate_display_names() + var/static/list/instruments + if(!instruments) + instruments = list() + var/list/templist = list(/obj/item/instrument/violin, + /obj/item/instrument/piano_synth, + /obj/item/instrument/banjo, + /obj/item/instrument/guitar, + /obj/item/instrument/eguitar, + /obj/item/instrument/glockenspiel, + /obj/item/instrument/accordion, + /obj/item/instrument/trumpet, + /obj/item/instrument/saxophone, + /obj/item/instrument/trombone, + /obj/item/instrument/recorder, + /obj/item/instrument/harmonica, + /obj/item/instrument/piano_synth/headphones + ) + for(var/V in templist) + var/atom/A = V + instruments[initial(A.name)] = A + return instruments +*/ +/obj/item/instrument/musicalmoth + name = "musical moth" + desc = "Despite its popularity, this controversial musical toy was eventually banned due to its unethically sampled sounds of moths screaming in agony." + icon_state = "mothsician" + allowed_instrument_ids = "mothscream" + attack_verb = list("fluttered", "flaped") + w_class = ITEMSIZE_SMALL + force = 0 + hitsound = 'sound/voice/moth/scream_moth.ogg' diff --git a/code/modules/instruments/songs/_song.dm b/code/modules/instruments/songs/_song.dm new file mode 100644 index 00000000000..dc4ccfe8b3a --- /dev/null +++ b/code/modules/instruments/songs/_song.dm @@ -0,0 +1,405 @@ +#define MUSICIAN_HEARCHECK_MINDELAY 4 +#define MUSIC_MAXLINES 1000 +#define MUSIC_MAXLINECHARS 300 + +/** + * # Song datum + * + * These are the actual backend behind instruments. + * They attach to an atom and provide the editor + playback functionality. + */ +/datum/song + /// Name of the song + var/name = "Untitled" + + /// The atom we're attached to/playing from + var/atom/parent + + /// Our song lines + var/list/lines + + /// delay between notes in deciseconds + var/tempo = 5 + + /// How far we can be heard + var/instrument_range = 15 + + /// Are we currently playing? + var/playing = FALSE + + /// Are we currently editing? + var/editing = TRUE + /// Is the help screen open? + var/help = FALSE + + /// Repeats left + var/repeat = 0 + /// Maximum times we can repeat + var/max_repeats = 10 + + /// Our volume + var/volume = 35 + /// Max volume + var/max_volume = 75 + /// Min volume - This is so someone doesn't decide it's funny to set it to 0 and play invisible songs. + var/min_volume = 1 + + /// What instruments our built in picker can use. The picker won't show unless this is longer than one. + var/list/allowed_instrument_ids = list("r3grand") + + //////////// Cached instrument variables ///////////// + /// Instrument we are currently using + var/datum/instrument/using_instrument + /// Cached legacy ext for legacy instruments + var/cached_legacy_ext + /// Cached legacy dir for legacy instruments + var/cached_legacy_dir + /// Cached list of samples, referenced directly from the instrument for synthesized instruments + var/list/cached_samples + /// Are we operating in legacy mode (so if the instrument is a legacy instrument) + var/legacy = FALSE + ////////////////////////////////////////////////////// + + /////////////////// Playing variables //////////////// + /** + * Build by compile_chords() + * Must be rebuilt on instrument switch. + * Compilation happens when we start playing and is cleared after we finish playing. + * Format: list of chord lists, with chordlists having (key1, key2, key3, tempodiv) + */ + var/list/compiled_chords + /// Current section of a long chord we're on, so we don't need to make a billion chords, one for every unit ticklag. + var/elapsed_delay + /// Amount of delay to wait before playing the next chord + var/delay_by + /// Current chord we're on. + var/current_chord + /// Channel as text = current volume percentage but it's 0 to 100 instead of 0 to 1. + var/list/channels_playing = list() + /// List of channels that aren't being used, as text. This is to prevent unnecessary freeing and reallocations from SSsounds/SSinstruments. + var/list/channels_idle = list() + /// Person playing us + var/mob/user_playing + ////////////////////////////////////////////////////// + + /// Last world.time we checked for who can hear us + var/last_hearcheck = 0 + /// The list of mobs that can hear us + var/list/hearing_mobs + /// If this is enabled, some things won't be strictly cleared when they usually are (liked compiled_chords on play stop) + var/debug_mode = FALSE + /// Max sound channels to occupy + var/max_sound_channels = CHANNELS_PER_INSTRUMENT + /// Current channels, so we can save a length() call. + var/using_sound_channels = 0 + /// Last channel to play. text. + var/last_channel_played + /// Should we not decay our last played note? + var/full_sustain_held_note = TRUE + + /////////////////////// DO NOT TOUCH THESE /////////////////// + var/octave_min = INSTRUMENT_MIN_OCTAVE + var/octave_max = INSTRUMENT_MAX_OCTAVE + var/key_min = INSTRUMENT_MIN_KEY + var/key_max = INSTRUMENT_MAX_KEY + var/static/list/note_offset_lookup = list(9, 11, 0, 2, 4, 5, 7) + var/static/list/accent_lookup = list("b" = -1, "s" = 1, "#" = 1, "n" = 0) + ////////////////////////////////////////////////////////////// + + ///////////// !!FUN!! - Only works in synthesized mode! ///////////////// + /// Note numbers to shift. + var/note_shift = 0 + var/note_shift_min = -100 + var/note_shift_max = 100 + var/can_noteshift = TRUE + /// The kind of sustain we're using + var/sustain_mode = SUSTAIN_LINEAR + /// When a note is considered dead if it is below this in volume + var/sustain_dropoff_volume = 0 + /// Total duration of linear sustain for 100 volume note to get to SUSTAIN_DROPOFF + var/sustain_linear_duration = 5 + /// Exponential sustain dropoff rate per decisecond + var/sustain_exponential_dropoff = 1.4 + ////////// DO NOT DIRECTLY SET THESE! + /// Do not directly set, use update_sustain() + var/cached_linear_dropoff = 10 + /// Do not directly set, use update_sustain() + var/cached_exponential_dropoff = 1.045 + ///////////////////////////////////////////////////////////////////////// + +/datum/song/New(atom/parent, list/instrument_ids, new_range) + SSinstruments.on_song_new(src) + lines = list() + tempo = sanitize_tempo(tempo) + src.parent = parent + if(instrument_ids) + allowed_instrument_ids = islist(instrument_ids)? instrument_ids : list(instrument_ids) + if(length(allowed_instrument_ids)) + set_instrument(allowed_instrument_ids[1]) + hearing_mobs = list() + volume = clamp(volume, min_volume, max_volume) + update_sustain() + if(new_range) + instrument_range = new_range + +/datum/song/Destroy() + stop_playing() + SSinstruments.on_song_del(src) + lines = null + if(using_instrument) + using_instrument.songs_using -= src + using_instrument = null + allowed_instrument_ids = null + parent = null + return ..() + +/** + * Checks and stores which mobs can hear us. Terminates sounds for mobs that leave our range. + */ +/datum/song/proc/do_hearcheck() + last_hearcheck = world.time + var/list/old = hearing_mobs.Copy() + hearing_mobs.len = 0 + var/turf/source = get_turf(parent) + var/list/in_range = get_mobs_and_objs_in_view_fast(source, instrument_range) + for(var/mob/M in in_range["mobs"]) + hearing_mobs[M] = get_dist(M, source) + var/list/exited = old - hearing_mobs + for(var/i in exited) + terminate_sound_mob(i) + +/** + * Sets our instrument, caching anything necessary for faster accessing. Accepts an ID, typepath, or instantiated instrument datum. + */ +/datum/song/proc/set_instrument(datum/instrument/I) + terminate_all_sounds() + var/old_legacy + if(using_instrument) + using_instrument.songs_using -= src + old_legacy = (using_instrument.instrument_flags & INSTRUMENT_LEGACY) + using_instrument = null + cached_samples = null + cached_legacy_ext = null + cached_legacy_dir = null + legacy = null + if(istext(I) || ispath(I)) + I = SSinstruments.instrument_data[I] + if(istype(I)) + using_instrument = I + I.songs_using += src + var/instrument_legacy = (I.instrument_flags & INSTRUMENT_LEGACY) + if(instrument_legacy) + cached_legacy_ext = I.legacy_instrument_ext + cached_legacy_dir = I.legacy_instrument_path + legacy = TRUE + else + cached_samples = I.samples + legacy = FALSE + if(isnull(old_legacy) || (old_legacy != instrument_legacy)) + if(playing) + compile_chords() + +/** + * Attempts to start playing our song. + */ +/datum/song/proc/start_playing(mob/user) + if(playing) + return + if(!using_instrument?.ready()) + to_chat(user, "An error has occured with [src]. Please reset the instrument.") + return + compile_chords() + if(!length(compiled_chords)) + to_chat(user, "Song is empty.") + return + playing = TRUE + updateDialog(user_playing) + //we can not afford to runtime, since we are going to be doing sound channel reservations and if we runtime it means we have a channel allocation leak. + //wrap the rest of the stuff to ensure stop_playing() is called. + do_hearcheck() + SEND_SIGNAL(parent, COMSIG_SONG_START) + elapsed_delay = 0 + delay_by = 0 + current_chord = 1 + user_playing = user + START_PROCESSING(SSinstruments, src) + +/** + * Stops playing, terminating all sounds if in synthesized mode. Clears hearing_mobs. + */ +/datum/song/proc/stop_playing() + if(!playing) + return + playing = FALSE + if(!debug_mode) + compiled_chords = null + STOP_PROCESSING(SSinstruments, src) + SEND_SIGNAL(parent, COMSIG_SONG_END) + terminate_all_sounds(TRUE) + hearing_mobs.len = 0 + user_playing = null + +/** + * Processes our song. + */ +/datum/song/proc/process_song(wait) + if(!length(compiled_chords) || should_stop_playing(user_playing)) + stop_playing() + return + var/list/chord = compiled_chords[current_chord] + if(++elapsed_delay >= delay_by) + play_chord(chord) + elapsed_delay = 0 + delay_by = tempodiv_to_delay(chord[length(chord)]) + current_chord++ + if(current_chord > length(compiled_chords)) + if(repeat) + repeat-- + current_chord = 1 + return + else + stop_playing() + return + +/** + * Converts a tempodiv to ticks to elapse before playing the next chord, taking into account our tempo. + */ +/datum/song/proc/tempodiv_to_delay(tempodiv) + if(!tempodiv) + tempodiv = 1 // no division by 0. some song converters tend to use 0 for when it wants to have no div, for whatever reason. + return max(1, round((tempo/tempodiv) / world.tick_lag, 1)) + +/** + * Compiles chords. + */ +/datum/song/proc/compile_chords() + legacy ? compile_legacy() : compile_synthesized() + +/** + * Plays a chord. + */ +/datum/song/proc/play_chord(list/chord) + // last value is timing information + for(var/i in 1 to (length(chord) - 1)) + legacy? playkey_legacy(chord[i][1], chord[i][2], chord[i][3], user_playing) : playkey_synth(chord[i], user_playing) + +/** + * Checks if we should halt playback. + */ +/datum/song/proc/should_stop_playing(mob/user) + return QDELETED(parent) || !using_instrument || !playing + +/** + * Sanitizes tempo to a value that makes sense and fits the current world.tick_lag. + */ +/datum/song/proc/sanitize_tempo(new_tempo) + new_tempo = abs(new_tempo) + return clamp(round(new_tempo, world.tick_lag), world.tick_lag, 5 SECONDS) + +/** + * Gets our beats per minute based on our tempo. + */ +/datum/song/proc/get_bpm() + return 600 / tempo + +/** + * Sets our tempo from a beats-per-minute, sanitizing it to a valid number first. + */ +/datum/song/proc/set_bpm(bpm) + tempo = sanitize_tempo(600 / bpm) + +/** + * Updates the window for our users. Override down the line. + */ +/datum/song/proc/updateDialog(mob/user) + interact(user) + +/datum/song/process(wait) + if(!playing) + return PROCESS_KILL + // it's expected this ticks at every world.tick_lag. if it lags, do not attempt to catch up. + process_song(world.tick_lag) + process_decay(world.tick_lag) + +/** + * Updates our cached linear/exponential falloff stuff, saving calculations down the line. + */ +/datum/song/proc/update_sustain() + // Exponential is easy + cached_exponential_dropoff = sustain_exponential_dropoff + // Linear, not so much, since it's a target duration from 100 volume rather than an exponential rate. + var/target_duration = sustain_linear_duration + var/volume_diff = max(0, 100 - sustain_dropoff_volume) + var/volume_decrease_per_decisecond = volume_diff / target_duration + cached_linear_dropoff = volume_decrease_per_decisecond + +/** + * Setter for setting output volume. + */ +/datum/song/proc/set_volume(volume) + src.volume = clamp(volume, max(0, min_volume), min(100, max_volume)) + update_sustain() + updateDialog() + +/** + * Setter for setting how low the volume has to get before a note is considered "dead" and dropped + */ +/datum/song/proc/set_dropoff_volume(volume) + sustain_dropoff_volume = clamp(volume, INSTRUMENT_MIN_SUSTAIN_DROPOFF, 100) + update_sustain() + updateDialog() + +/** + * Setter for setting exponential falloff factor. + */ +/datum/song/proc/set_exponential_drop_rate(drop) + sustain_exponential_dropoff = clamp(drop, INSTRUMENT_EXP_FALLOFF_MIN, INSTRUMENT_EXP_FALLOFF_MAX) + update_sustain() + updateDialog() + +/** + * Setter for setting linear falloff duration. + */ +/datum/song/proc/set_linear_falloff_duration(duration) + sustain_linear_duration = clamp(duration, 0.1, INSTRUMENT_MAX_TOTAL_SUSTAIN) + update_sustain() + updateDialog() + +/datum/song/vv_edit_var(var_name, var_value) + . = ..() + if(.) + switch(var_name) + if(NAMEOF(src, volume)) + set_volume(var_value) + if(NAMEOF(src, sustain_dropoff_volume)) + set_dropoff_volume(var_value) + if(NAMEOF(src, sustain_exponential_dropoff)) + set_exponential_drop_rate(var_value) + if(NAMEOF(src, sustain_linear_duration)) + set_linear_falloff_duration(var_value) + +// subtype for handheld instruments, like violin +/datum/song/handheld + +/datum/song/handheld/updateDialog(mob/user) + parent.interact(user || usr) + +/datum/song/handheld/should_stop_playing(mob/user) + . = ..() + if(.) + return TRUE + var/obj/item/instrument/I = parent + return I.should_stop_playing(user) + +// subtype for stationary structures, like pianos +/datum/song/stationary + +/datum/song/stationary/updateDialog(mob/user) + parent.interact(user || usr) + +/datum/song/stationary/should_stop_playing(mob/user) + . = ..() + if(.) + return TRUE + var/obj/structure/musician/M = parent + return M.should_stop_playing(user) diff --git a/code/modules/instruments/songs/editor.dm b/code/modules/instruments/songs/editor.dm new file mode 100644 index 00000000000..9435ed8163a --- /dev/null +++ b/code/modules/instruments/songs/editor.dm @@ -0,0 +1,251 @@ +/** + * Returns the HTML for the status UI for this song datum. + */ +/datum/song/proc/instrument_status_ui() + . = list() + . += "
" + . += "Current instrument: " + if(!using_instrument) + . += "No instrument loaded!
" + else + . += "[using_instrument.name]
" + . += "Playback Settings:
" + if(can_noteshift) + . += "Note Shift/Note Transpose: [note_shift] keys / [round(note_shift / 12, 0.01)] octaves
" + var/smt + var/modetext = "" + switch(sustain_mode) + if(SUSTAIN_LINEAR) + smt = "Linear" + modetext = "Linear Sustain Duration: [sustain_linear_duration / 10] seconds
" + if(SUSTAIN_EXPONENTIAL) + smt = "Exponential" + modetext = "Exponential Falloff Factor: [sustain_exponential_dropoff]% per decisecond
" + . += "Sustain Mode: [smt]
" + . += modetext + . += using_instrument?.ready()? "Status: Ready
" : "Status: !Instrument Definition Error!
" + . += "Instrument Type: [legacy? "Legacy" : "Synthesized"]
" + . += "Volume: [volume]
" + . += "Volume Dropoff Threshold: [sustain_dropoff_volume]
" + . += "Sustain indefinitely last held note: [full_sustain_held_note? "Enabled" : "Disabled"].
" + . += "
" + +/datum/song/proc/interact(mob/user) + var/list/dat = list() + + dat += instrument_status_ui() + + if(lines.len > 0) + dat += "

Playback

" + if(!playing) + dat += "Play Stop

" + dat += "Repeat Song: " + dat += repeat > 0 ? "--" : "--" + dat += " [repeat] times " + dat += repeat < max_repeats ? "++" : "++" + dat += "
" + else + dat += "Play Stop
" + dat += "Repeats left: [repeat]
" + if(!editing) + dat += "
Show Editor
" + else + dat += "

Editing

" + dat += "Hide Editor" + dat += " Start a New Song" + dat += " Import a Song

" + var/bpm = round(600 / tempo) + dat += "Tempo: - [bpm] BPM +

" + var/linecount = 0 + for(var/line in lines) + linecount += 1 + dat += "Line [linecount]: Edit X [line]
" + dat += "Add Line

" + if(help) + dat += "Hide Help
" + dat += {" + Lines are a series of chords, separated by commas (,), each with notes separated by hyphens (-).
+ Every note in a chord will play together, with chord timed by the tempo.
+
+ Notes are played by the names of the note, and optionally, the accidental, and/or the octave number.
+ By default, every note is natural and in octave 3. Defining otherwise is remembered for each note.
+ Example: C,D,E,F,G,A,B will play a C major scale.
+ After a note has an accidental placed, it will be remembered: C,C4,C,C3 is C3,C4,C4,C3
+ Chords can be played simply by seperating each note with a hyphon: A-C#,Cn-E,E-G#,Gn-B
+ A pause may be denoted by an empty chord: C,E,,C,G
+ To make a chord be a different time, end it with /x, where the chord length will be length
+ defined by tempo / x: C,G/2,E/4
+ Combined, an example is: E-E4/4,F#/2,G#/8,B/8,E3-E4/4 +
+ Lines may be up to [MUSIC_MAXLINECHARS] characters.
+ A song may only contain up to [MUSIC_MAXLINES] lines.
+ "} + else + dat += "Show Help
" + + var/datum/browser/popup = new(user, "instrument", parent?.name || "instrument", 700, 500) + popup.set_content(dat.Join("")) + popup.open() + +/** + * Parses a song the user has input into lines and stores them. + */ +/datum/song/proc/ParseSong(text) + set waitfor = FALSE + //split into lines + lines = splittext(text, "\n") + if(lines.len) + var/bpm_string = "BPM: " + if(findtext(lines[1], bpm_string, 1, length(bpm_string) + 1)) + var/divisor = text2num(copytext(lines[1], length(bpm_string) + 1)) || 120 // default + tempo = sanitize_tempo(600 / round(divisor, 1)) + lines.Cut(1, 2) + else + tempo = sanitize_tempo(5) // default 120 BPM + if(lines.len > MUSIC_MAXLINES) + to_chat(usr, "Too many lines!") + lines.Cut(MUSIC_MAXLINES + 1) + var/linenum = 1 + for(var/l in lines) + if(length_char(l) > MUSIC_MAXLINECHARS) + to_chat(usr, "Line [linenum] too long!") + lines.Remove(l) + else + linenum++ + updateDialog(usr) // make sure updates when complete + +/datum/song/Topic(href, href_list) + if(!parent.CanUseTopic(usr)) + usr << browse(null, "window=instrument") + usr.unset_machine() + return + + parent.add_fingerprint(usr) + + if(href_list["newsong"]) + lines = new() + tempo = sanitize_tempo(5) // default 120 BPM + name = "" + + else if(href_list["import"]) + var/t = "" + do + t = html_encode(input(usr, "Please paste the entire song, formatted:", text("[]", name), t) as message) + if(!in_range(parent, usr)) + return + + if(length_char(t) >= MUSIC_MAXLINES * MUSIC_MAXLINECHARS) + var/cont = input(usr, "Your message is too long! Would you like to continue editing it?", "", "yes") in list("yes", "no") + if(cont == "no") + break + while(length_char(t) > MUSIC_MAXLINES * MUSIC_MAXLINECHARS) + ParseSong(t) + + else if(href_list["help"]) + help = text2num(href_list["help"]) - 1 + + else if(href_list["edit"]) + editing = text2num(href_list["edit"]) - 1 + + if(href_list["repeat"]) //Changing this from a toggle to a number of repeats to avoid infinite loops. + if(playing) + return //So that people cant keep adding to repeat. If the do it intentionally, it could result in the server crashing. + repeat += round(text2num(href_list["repeat"])) + if(repeat < 0) + repeat = 0 + if(repeat > max_repeats) + repeat = max_repeats + + else if(href_list["tempo"]) + tempo = sanitize_tempo(tempo + text2num(href_list["tempo"])) + + else if(href_list["play"]) + INVOKE_ASYNC(src, .proc/start_playing, usr) + + else if(href_list["newline"]) + var/newline = html_encode(input("Enter your line: ", parent.name) as text|null) + if(!newline || !in_range(parent, usr)) + return + if(lines.len > MUSIC_MAXLINES) + return + if(length(newline) > MUSIC_MAXLINECHARS) + newline = copytext(newline, 1, MUSIC_MAXLINECHARS) + lines.Add(newline) + + else if(href_list["deleteline"]) + var/num = round(text2num(href_list["deleteline"])) + if(num > lines.len || num < 1) + return + lines.Cut(num, num+1) + + else if(href_list["modifyline"]) + var/num = round(text2num(href_list["modifyline"]),1) + var/content = stripped_input(usr, "Enter your line: ", parent.name, lines[num], MUSIC_MAXLINECHARS) + if(!content || !in_range(parent, usr)) + return + if(num > lines.len || num < 1) + return + lines[num] = content + + else if(href_list["stop"]) + stop_playing() + + else if(href_list["setlinearfalloff"]) + var/amount = input(usr, "Set linear sustain duration in seconds", "Linear Sustain Duration") as null|num + if(!isnull(amount)) + set_linear_falloff_duration(round(amount * 10, world.tick_lag)) + + else if(href_list["setexpfalloff"]) + var/amount = input(usr, "Set exponential sustain factor", "Exponential sustain factor") as null|num + if(!isnull(amount)) + set_exponential_drop_rate(round(amount, 0.00001)) + + else if(href_list["setvolume"]) + var/amount = input(usr, "Set volume", "Volume") as null|num + if(!isnull(amount)) + set_volume(round(amount, 1)) + + else if(href_list["setdropoffvolume"]) + var/amount = input(usr, "Set dropoff threshold", "Dropoff Threshold Volume") as null|num + if(!isnull(amount)) + set_dropoff_volume(round(amount, 0.01)) + + else if(href_list["switchinstrument"]) + if(!length(allowed_instrument_ids)) + return + else if(length(allowed_instrument_ids) == 1) + set_instrument(allowed_instrument_ids[1]) + return + var/list/categories = list() + for(var/i in allowed_instrument_ids) + var/datum/instrument/I = SSinstruments.get_instrument(i) + if(I) + LAZYSET(categories[I.category || "ERROR CATEGORY"], I.name, I.id) + var/cat = input(usr, "Select Category", "Instrument Category") as null|anything in categories + if(!cat) + return + var/list/instruments = categories[cat] + var/choice = input(usr, "Select Instrument", "Instrument Selection") as null|anything in instruments + if(!choice) + return + choice = instruments[choice] //get id + if(choice) + set_instrument(choice) + + else if(href_list["setnoteshift"]) + var/amount = input(usr, "Set note shift", "Note Shift") as null|num + if(!isnull(amount)) + note_shift = clamp(amount, note_shift_min, note_shift_max) + + else if(href_list["setsustainmode"]) + var/choice = input(usr, "Choose a sustain mode", "Sustain Mode") as null|anything in list("Linear", "Exponential") + switch(choice) + if("Linear") + sustain_mode = SUSTAIN_LINEAR + if("Exponential") + sustain_mode = SUSTAIN_EXPONENTIAL + + else if(href_list["togglesustainhold"]) + full_sustain_held_note = !full_sustain_held_note + + updateDialog() diff --git a/code/modules/instruments/songs/play_legacy.dm b/code/modules/instruments/songs/play_legacy.dm new file mode 100644 index 00000000000..e8666f78674 --- /dev/null +++ b/code/modules/instruments/songs/play_legacy.dm @@ -0,0 +1,91 @@ +/** + * Compiles our lines into "chords" with filenames for legacy playback. This makes there have to be a bit of lag at the beginning of the song, but repeats will not have to parse it again, and overall playback won't be impacted by as much lag. + */ +/datum/song/proc/compile_legacy() + if(!length(src.lines)) + return + var/list/lines = src.lines //cache for hyepr speed! + compiled_chords = list() + var/list/octaves = list(3, 3, 3, 3, 3, 3, 3) + var/list/accents = list("n", "n", "n", "n", "n", "n", "n") + for(var/line in lines) + var/list/chords = splittext(lowertext(line), ",") + for(var/chord in chords) + var/list/compiled_chord = list() + var/tempodiv = 1 + var/list/notes_tempodiv = splittext(chord, "/") + var/len = length(notes_tempodiv) + if(len >= 2) + tempodiv = text2num(notes_tempodiv[2]) + if(len) //some dunkass is going to do ,,,, to make 3 rests instead of ,/1 because there's no standardization so let's be prepared for that. + var/list/notes = splittext(notes_tempodiv[1], "-") + for(var/note in notes) + if(length(note) == 0) + continue + // 1-7, A-G + var/key = text2ascii(note) - 96 + if((key < 1) || (key > 7)) + continue + for(var/i in 2 to length(note)) + var/oct_acc = copytext(note, i, i + 1) + var/num = text2num(oct_acc) + if(!num) //it's an accidental + accents[key] = oct_acc //if they misspelled it/fucked up that's on them lmao, no safety checks. + else //octave + octaves[key] = clamp(num, octave_min, octave_max) + compiled_chord[++compiled_chord.len] = list(key, accents[key], octaves[key]) + compiled_chord += tempodiv //this goes last + if(length(compiled_chord)) + compiled_chords[++compiled_chords.len] = compiled_chord + +/** + * Proc to play a legacy note. Just plays the sound to hearing mobs (and does hearcheck if necessary), no fancy channel/sustain/management. + * + * Arguments: + * * note is a number from 1-7 for A-G + * * acc is either "b", "n", or "#" + * * oct is 1-8 (or 9 for C) + */ +/datum/song/proc/playkey_legacy(note, acc as text, oct, mob/user) + // handle accidental -> B<>C of E<>F + if(acc == "b" && (note == 3 || note == 6)) // C or F + if(note == 3) + oct-- + note-- + acc = "n" + else if(acc == "#" && (note == 2 || note == 5)) // B or E + if(note == 2) + oct++ + note++ + acc = "n" + else if(acc == "#" && (note == 7)) //G# + note = 1 + acc = "b" + else if(acc == "#") // mass convert all sharps to flats, octave jump already handled + acc = "b" + note++ + + // check octave, C is allowed to go to 9 + if(oct < 1 || (note == 3 ? oct > 9 : oct > 8)) + return + + // now generate name + var/soundfile = "sound/instruments/[cached_legacy_dir]/[ascii2text(note+64)][acc][oct].[cached_legacy_ext]" + soundfile = file(soundfile) + // make sure the note exists + if(!fexists(soundfile)) + return + // and play + var/turf/source = get_turf(parent) + if((world.time - MUSICIAN_HEARCHECK_MINDELAY) > last_hearcheck) + do_hearcheck() + var/sound/music_played = sound(soundfile) + for(var/i in hearing_mobs) + var/mob/M = i + /* Would be nice + if(user && HAS_TRAIT(user, TRAIT_MUSICIAN) && isliving(M)) + var/mob/living/L = M + L.apply_status_effect(STATUS_EFFECT_GOOD_MUSIC) + */ + M.playsound_local(source, null, volume * using_instrument.volume_multiplier, S = music_played, preference = /datum/client_preference/instrument_toggle, volume_channel = VOLUME_CHANNEL_INSTRUMENTS) + // 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 new file mode 100644 index 00000000000..dae69c27822 --- /dev/null +++ b/code/modules/instruments/songs/play_synthesized.dm @@ -0,0 +1,150 @@ +/** + * Compiles our lines into "chords" with numbers. This makes there have to be a bit of lag at the beginning of the song, but repeats will not have to parse it again, and overall playback won't be impacted by as much lag. + */ +/datum/song/proc/compile_synthesized() + if(!length(src.lines)) + return + var/list/lines = src.lines //cache for hyepr speed! + compiled_chords = list() + var/list/octaves = list(3, 3, 3, 3, 3, 3, 3) + var/list/accents = list("n", "n", "n", "n", "n", "n", "n") + for(var/line in lines) + var/list/chords = splittext(lowertext(line), ",") + for(var/chord in chords) + var/list/compiled_chord = list() + var/tempodiv = 1 + var/list/notes_tempodiv = splittext(chord, "/") + var/len = length(notes_tempodiv) + if(len >= 2) + tempodiv = text2num(notes_tempodiv[2]) + if(len) //some dunkass is going to do ,,,, to make 3 rests instead of ,/1 because there's no standardization so let's be prepared for that. + var/list/notes = splittext(notes_tempodiv[1], "-") + for(var/note in notes) + if(length(note) == 0) + continue + // 1-7, A-G + var/key = text2ascii(note) - 96 + if((key < 1) || (key > 7)) + continue + for(var/i in 2 to length(note)) + var/oct_acc = copytext(note, i, i + 1) + var/num = text2num(oct_acc) + if(!num) //it's an accidental + accents[key] = oct_acc //if they misspelled it/fucked up that's on them lmao, no safety checks. + else //octave + octaves[key] = clamp(num, octave_min, octave_max) + compiled_chord += clamp((note_offset_lookup[key] + octaves[key] * 12 + accent_lookup[accents[key]]), key_min, key_max) + compiled_chord += tempodiv //this goes last + if(length(compiled_chord)) + compiled_chords[++compiled_chords.len] = compiled_chord + +/** + * Plays a specific numerical key from our instrument to anyone who can hear us. + * Does a hearing check if enough time has passed. + */ +/datum/song/proc/playkey_synth(key, mob/user) + if(can_noteshift) + key = clamp(key + note_shift, key_min, key_max) + if((world.time - MUSICIAN_HEARCHECK_MINDELAY) > last_hearcheck) + do_hearcheck() + var/datum/instrument_key/K = using_instrument.samples[num2text(key)] //See how fucking easy it is to make a number text? You don't need a complicated 9 line proc! + //Should probably add channel limiters here at some point but I don't care right now. + var/channel = pop_channel() + if(isnull(channel)) + return FALSE + . = TRUE + var/sound/copy = sound(K.sample) + var/volume = src.volume * using_instrument.volume_multiplier + copy.frequency = K.frequency + copy.volume = volume + var/channel_text = num2text(channel) + channels_playing[channel_text] = 100 + last_channel_played = channel_text + for(var/i in hearing_mobs) + var/mob/M = i + /* Maybe someday + if(user && HAS_TRAIT(user, TRAIT_MUSICIAN) && isliving(M)) + var/mob/living/L = M + L.apply_status_effect(STATUS_EFFECT_GOOD_MUSIC) + */ + // Jeez + M.playsound_local( + turf_source = get_turf(parent), + soundin = null, + vol = volume, + vary = FALSE, + frequency = K.frequency, + falloff = null, + is_global = null, + channel = channel, + pressure_affected = null, + S = copy, + preference = /datum/client_preference/instrument_toggle, + volume_channel = VOLUME_CHANNEL_INSTRUMENTS) + // Could do environment and echo later but not for now + +/** + * Stops all sounds we are "responsible" for. Only works in synthesized mode. + */ +/datum/song/proc/terminate_all_sounds(clear_channels = TRUE) + for(var/i in hearing_mobs) + terminate_sound_mob(i) + if(clear_channels) + channels_playing.len = 0 + channels_idle.len = 0 + SSinstruments.current_instrument_channels -= using_sound_channels + using_sound_channels = 0 + SSsounds.free_datum_channels(src) + +/** + * Stops all sounds we are responsible for in a given person. Only works in synthesized mode. + */ +/datum/song/proc/terminate_sound_mob(mob/M) + for(var/channel in channels_playing) + M.stop_sound_channel(text2num(channel)) + +/** + * Pops a channel we have reserved so we don't have to release and re-request them from SSsounds every time we play a note. This is faster. + */ +/datum/song/proc/pop_channel() + if(length(channels_idle)) //just pop one off of here if we have one available + . = text2num(channels_idle[1]) + channels_idle.Cut(1,2) + return + if(using_sound_channels >= max_sound_channels) + return + . = SSinstruments.reserve_instrument_channel(src) + if(!isnull(.)) + using_sound_channels++ + +/** + * Decays our channels and updates their volumes to mobs who can hear us. + * + * Arguments: + * * wait_ds - the deciseconds we should decay by. This is to compensate for any lag, as otherwise songs would get pretty nasty during high time dilation. + */ +/datum/song/proc/process_decay(wait_ds) + var/linear_dropoff = cached_linear_dropoff * wait_ds + var/exponential_dropoff = cached_exponential_dropoff ** wait_ds + for(var/channel in channels_playing) + if(full_sustain_held_note && (channel == last_channel_played)) + continue + var/current_volume = channels_playing[channel] + switch(sustain_mode) + if(SUSTAIN_LINEAR) + current_volume -= linear_dropoff + if(SUSTAIN_EXPONENTIAL) + current_volume /= exponential_dropoff + channels_playing[channel] = current_volume + var/dead = current_volume <= sustain_dropoff_volume + var/channelnumber = text2num(channel) + if(dead) + channels_playing -= channel + channels_idle += channel + for(var/i in hearing_mobs) + var/mob/M = i + M.stop_sound_channel(channelnumber) + else + for(var/i in hearing_mobs) + var/mob/M = i + M.set_sound_channel_volume(channelnumber, (current_volume * 0.01) * volume * using_instrument.volume_multiplier) diff --git a/code/modules/instruments/stationary.dm b/code/modules/instruments/stationary.dm new file mode 100644 index 00000000000..62e9d500dca --- /dev/null +++ b/code/modules/instruments/stationary.dm @@ -0,0 +1,59 @@ +/obj/structure/musician + name = "Not A Piano" + desc = "Something broke, contact coderbus." + var/can_play_unanchored = FALSE + var/list/allowed_instrument_ids = list("r3grand","r3harpsi","crharpsi","crgrand1","crbright1", "crichugan", "crihamgan","piano") + var/datum/song/song + +/obj/structure/musician/Initialize(mapload) + . = ..() + song = new(src, allowed_instrument_ids) + allowed_instrument_ids = null + +/obj/structure/musician/Destroy() + QDEL_NULL(song) + return ..() + +/obj/structure/musician/attack_hand(mob/M) + if(!M.IsAdvancedToolUser()) + return + + interact(M) + +/obj/structure/musician/proc/should_stop_playing(mob/user) + if(!(anchored || can_play_unanchored)) + return TRUE + if(!user) + return FALSE + return !CanUseTopic(user) + +/obj/structure/musician/interact(mob/user) + . = ..() + song.interact(user) + +/* +/obj/structure/musician/wrench_act(mob/living/user, obj/item/I) + default_unfasten_wrench(user, I, 40) + return TRUE +*/ + +/obj/structure/musician/piano + name = "space minimoog" + icon = 'icons/obj/musician.dmi' + icon_state = "minimoog" + anchored = TRUE + density = TRUE + +/obj/structure/musician/piano/unanchored + anchored = FALSE + +/obj/structure/musician/piano/Initialize(mapload) + . = ..() + if(prob(50) && icon_state == initial(icon_state)) + name = "space minimoog" + desc = "This is a minimoog, like a space piano, but more spacey!" + icon_state = "minimoog" + else + name = "space piano" + desc = "This is a space piano, like a regular piano, but always in tune! Even if the musician isn't." + icon_state = "piano" diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 2fd4b34acd6..1951d370c6e 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -1059,15 +1059,15 @@ /mob/living/get_sound_env(var/pressure_factor) if (hallucination) - return PSYCHOTIC + return SOUND_ENVIRONMENT_PSYCHOTIC else if (druggy) - return DRUGGED + return SOUND_ENVIRONMENT_DRUGGED else if (drowsyness) - return DIZZY + return SOUND_ENVIRONMENT_DIZZY else if (confused) - return DIZZY + return SOUND_ENVIRONMENT_DIZZY else if (sleeping) - return UNDERWATER + return SOUND_ENVIRONMENT_UNDERWATER else return ..() diff --git a/icons/mob/items/lefthand_horns.dmi b/icons/mob/items/lefthand_horns.dmi new file mode 100644 index 0000000000000000000000000000000000000000..af8a5e7d22ee5298734cb2e69265e826d82248ec GIT binary patch literal 470 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 literal 0 HcmV?d00001 diff --git a/icons/mob/items/righthand_horns.dmi b/icons/mob/items/righthand_horns.dmi new file mode 100644 index 0000000000000000000000000000000000000000..8d0be6d0aab360464c4e4138dd04b1477188d1f5 GIT binary patch literal 472 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%0DsvnU?v&th*>5OvPob3RAM{jGyw>P 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 HZUYwrM3c=l literal 0 HcmV?d00001 diff --git a/sound/weapons/stringsmash.ogg b/sound/weapons/stringsmash.ogg new file mode 100644 index 0000000000000000000000000000000000000000..1c3e8971dcc722d259ba6b90722b58ea64517091 GIT binary patch literal 17235 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