Port Baystruments PR from tgstation

This commit is contained in:
mochi
2020-07-17 15:18:17 +02:00
parent a7e4c8af42
commit 24e8cd66ac
269 changed files with 2253 additions and 717 deletions

View File

@@ -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]"
/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 ..()
/**
* 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/is_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
/**
* 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)

View File

@@ -0,0 +1,33 @@
#define KEY_TWELTH (1/12)
/**
* 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, key, deviation, 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")
frequency = 2 ** (KEY_TWELTH * deviation)
#undef KEY_TWELTH

View File

@@ -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')

View File

@@ -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')

View File

@@ -0,0 +1,19 @@
/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')

View File

@@ -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')

View File

@@ -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"

View File

@@ -0,0 +1,59 @@
//copy pasta of the space piano, don't hurt me -Pete
/obj/item/instrument
name = "generic instrument"
force = 10
max_integrity = 100
resistance_flags = FLAMMABLE
icon = 'icons/obj/musician.dmi'
lefthand_file = 'icons/mob/inhands/equipment/instruments_lefthand.dmi'
righthand_file = 'icons/mob/inhands/equipment/instruments_righthand.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/suicide_act(mob/user)
user.visible_message("<span class='suicide'>[user] begins to play 'Gloomy Sunday'! It looks like [user.p_theyre()] trying to commit suicide!</span>")
return (BRUTELOSS)
/obj/item/instrument/attack_self(mob/user)
if(!user.IsAdvancedToolUser())
to_chat(user, "<span class='warning'>You don't have the dexterity to do this!</span>")
return TRUE
interact(user)
/obj/item/instrument/attack_self(mob/user)
tgui_interact(user)
/obj/item/instrument/tgui_data(mob/user)
return song.tgui_data(user)
/obj/item/instrument/tgui_interact(mob/user)
if(!isliving(user) || user.incapacitated())
return
song.tgui_interact(user)
/obj/item/instrument/tgui_act(action, params)
if(..())
return
return song.tgui_act(action, params)
/**
* Whether the instrument should stop playing
*
* Arguments:
* * user - The user
*/
/obj/item/instrument/proc/should_stop_playing(mob/user)
return !(src in user) || !isliving(user) || user.incapacitated()

View File

@@ -0,0 +1,206 @@
/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"
item_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"
item_state = "golden_violin"
resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF
/obj/item/instrument/piano_synth
name = "synthesizer"
desc = "An advanced electronic synthesizer that can be used as various instruments."
icon_state = "synth"
item_state = "synth"
allowed_instrument_ids = "piano"
/obj/item/instrument/piano_synth/Initialize(mapload)
. = ..()
song.allowed_instrument_ids = SSinstruments.synthesizer_instrument_ids
/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"
item_state = "banjo"
attack_verb = list("scruggs-styled", "hum-diggitied", "shin-digged", "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"
item_state = "guitar"
attack_verb = list("played metal on", "serenaded", "crashed", "smashed")
hitsound = 'sound/weapons/stringsmash.ogg'
allowed_instrument_ids = "guitar"
/obj/item/instrument/eguitar
name = "electric guitar"
desc = "Makes all your shredding needs possible."
icon_state = "eguitar"
item_state = "eguitar"
force = 12
attack_verb = list("played metal on", "shredded", "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"
item_state = "glockenspiel"
allowed_instrument_ids = "glockenspiel"
/obj/item/instrument/accordion
name = "accordion"
desc = "Pun-Pun not included."
icon_state = "accordion"
item_state = "accordion"
allowed_instrument_ids = "accordion"
/obj/item/instrument/trumpet
name = "trumpet"
desc = "To announce the arrival of the king!"
icon_state = "trumpet"
item_state = "trumpet"
allowed_instrument_ids = "trombone"
/obj/item/instrument/trumpet/spectral
name = "spectral trumpet"
desc = "Things are about to get spooky!"
icon_state = "spectral_trumpet"
item_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"
item_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"
item_state = "saxophone"
force = 0
attack_verb = list("played", "jazzed", "saxxed", "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 = "trombone"
item_state = "trombone"
/obj/item/instrument/trombone/spectral
name = "spectral trombone"
desc = "A skeleton's favorite instrument. Apply directly on the mortals."
icon_state = "trombone"
item_state = "trombone"
force = 0
attack_verb = list("played", "jazzed", "tromboned", "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"
item_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"
item_state = "harmonica"
force = 5
w_class = WEIGHT_CLASS_SMALL
allowed_instrument_ids = "harmonica"
/obj/item/instrument/xylophone
name = "xylophone"
desc = "A percussion instrument with a bright tone."
icon_state = "xylophone"
item_state = "xylophone"
allowed_instrument_ids = "bikehorn"
/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_state = "bike_horn"
lefthand_file = 'icons/mob/inhands/items_lefthand.dmi'
righthand_file = 'icons/mob/inhands/items_righthand.dmi'
attack_verb = list("beautifully honks")
w_class = WEIGHT_CLASS_TINY
force = 0
throw_speed = 3
throw_range = 15
hitsound = 'sound/items/bikehorn.ogg'
allowed_instrument_ids = "bikehorn"
// Crafting recipes
/datum/crafting_recipe/violin
name = "Violin"
result = /obj/item/instrument/violin
reqs = list(/obj/item/stack/sheet/wood = 5,
/obj/item/stack/cable_coil = 6,
/obj/item/stack/tape_roll = 5)
tools = list(TOOL_SCREWDRIVER, TOOL_WIRECUTTER)
time = 8 SECONDS
category = CAT_MISC
/datum/crafting_recipe/guitar
name = "Guitar"
result = /obj/item/instrument/guitar
reqs = list(/obj/item/stack/sheet/wood = 5,
/obj/item/stack/cable_coil = 6,
/obj/item/stack/tape_roll = 5)
tools = list(TOOL_SCREWDRIVER, TOOL_WIRECUTTER)
time = 8 SECONDS
category = CAT_MISC
/datum/crafting_recipe/eguitar
name = "Electric Guitar"
result = /obj/item/instrument/eguitar
reqs = list(/obj/item/stack/sheet/metal = 5,
/obj/item/stack/cable_coil = 6,
/obj/item/stack/tape_roll = 5)
tools = list(TOOL_SCREWDRIVER, TOOL_WIRECUTTER)
time = 8 SECONDS
category = CAT_MISC

View File

@@ -0,0 +1,47 @@
/obj/structure/musician
name = "Not A Piano"
desc = "Something broke!"
var/can_play_unanchored = FALSE
var/list/allowed_instrument_ids
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/user)
add_fingerprint(user)
tgui_interact(user)
/obj/structure/musician/tgui_data(mob/user)
return song.tgui_data(user)
/obj/structure/musician/tgui_interact(mob/user)
song.tgui_interact(user)
/obj/structure/musician/tgui_act(action, params)
if(..())
return
return song.tgui_act(action, params)
/obj/structure/musician/wrench_act(mob/living/user, obj/item/I)
default_unfasten_wrench(user, I, 40)
return TRUE
/**
* Whether the instrument should stop playing
*
* Arguments:
* * user - The user
*/
/obj/structure/musician/proc/should_stop_playing(mob/user)
if(!(anchored || can_play_unanchored))
return TRUE
if(!user)
return FALSE
return !CanUseTopic(user, GLOB.physical_state)

View File

@@ -0,0 +1,22 @@
/obj/structure/piano
parent_type = /obj/structure/musician // TODO: Can't edit maps right now due to a freeze, remove and update path when it's done
name = "space minimoog"
icon = 'icons/obj/musician.dmi'
icon_state = "minimoog"
anchored = TRUE
density = TRUE
allowed_instrument_ids = "piano"
/obj/structure/piano/unanchored
anchored = FALSE
/obj/structure/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"

View File

@@ -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 Accordion"
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 Accordion"
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')

View File

@@ -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')

View File

@@ -0,0 +1,401 @@
#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 = 1
/// Maximum times we can repeat
var/max_repeats = 10
/// Our volume
var/volume = 75
/// 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
//////////// 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 of channels that aren't being used, as text. This is to prevent unnecessary freeing and reallocations from SSsounds/SSinstruments.
var/list/channels_idle
/// 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
var/static/list/accent_lookup
//////////////////////////////////////////////////////////////
///////////// !!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
/////////////////////////////////////////////////////////////////////////
var/static/list/valid_files[0] // Cache to avoid running fexists() every time
/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)
channels_playing = list()
channels_idle = list()
if(!note_offset_lookup)
note_offset_lookup = list(9, 11, 0, 2, 4, 5, 7)
accent_lookup = list("b" = -1, "s" = 1, "#" = 1, "n" = 0)
update_sustain()
if(new_range)
instrument_range = new_range
/datum/song/Destroy()
stop_playing()
SSinstruments.on_song_del(src)
lines = null
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)
for(var/mob/M in GLOB.player_list)
var/dist = get_dist(M, source)
if(dist > instrument_range)
continue
hearing_mobs[M] = dist
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?.is_ready())
to_chat(user, "<span class='warning'>An error has occured with [src]. Please reset the instrument.</span>")
return
compile_chords()
if(!length(compiled_chords))
to_chat(user, "<span class='warning'>Song is empty.</span>")
return
playing = TRUE
SStgui.update_uis(parent)
//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
SStgui.update_uis(parent)
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
SStgui.update_uis(parent)
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)
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)
/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()
SStgui.update_uis(parent)
/**
* 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()
SStgui.update_uis(parent)
/**
* 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()
SStgui.update_uis(parent)
/**
* 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()
SStgui.update_uis(parent)
/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/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/should_stop_playing(mob/user)
. = ..()
if(.)
return TRUE
var/obj/structure/musician/M = parent
return M.should_stop_playing(user)

View File

@@ -0,0 +1,183 @@
/datum/song/tgui_data(mob/user)
var/data[0]
// General
data["playing"] = playing
data["repeat"] = repeat
data["maxRepeats"] = max_repeats
data["editing"] = editing
data["lines"] = lines
data["tempo"] = tempo
data["minTempo"] = world.tick_lag
data["maxTempo"] = 5 SECONDS
data["tickLag"] = world.tick_lag
data["help"] = help
// Status
var/list/allowed_instrument_names = list()
for(var/i in allowed_instrument_ids)
var/datum/instrument/I = SSinstruments.get_instrument(i)
if(I)
allowed_instrument_names += I.name
data["allowedInstrumentNames"] = allowed_instrument_names
data["instrumentLoaded"] = !isnull(using_instrument)
if(using_instrument)
data["instrument"] = using_instrument.name
data["canNoteShift"] = can_noteshift
if(can_noteshift)
data["noteShift"] = note_shift
data["noteShiftMin"] = note_shift_min
data["noteShiftMax"] = note_shift_max
data["sustainMode"] = sustain_mode
switch(sustain_mode)
if(SUSTAIN_LINEAR)
data["sustainLinearDuration"] = sustain_linear_duration
if(SUSTAIN_EXPONENTIAL)
data["sustainExponentialDropoff"] = sustain_exponential_dropoff
data["ready"] = using_instrument?.is_ready()
data["legacy"] = legacy
data["volume"] = volume
data["minVolume"] = min_volume
data["maxVolume"] = max_volume
data["sustainDropoffVolume"] = sustain_dropoff_volume
data["sustainHeldNote"] = full_sustain_held_note
return data
/datum/song/tgui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/tgui_state/state = GLOB.tgui_default_state)
ui = SStgui.try_update_ui(user, parent, ui_key, ui, force_open)
if(!ui)
ui = new(user, parent, ui_key, "Instrument", parent?.name || "Instrument", 700, 500)
ui.open()
ui.set_autoupdate(FALSE) // NO!!! Don't auto-update this!!
/datum/song/tgui_act(action, params)
switch(action)
if("newsong")
lines = new()
tempo = sanitize_tempo(5) // default 120 BPM
name = ""
return TRUE
if("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)
parse_song(t)
if("help")
help = !help
return TRUE
if("edit")
editing = !editing
return TRUE
if("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 = clamp(round(text2num(params["new"])), 0, max_repeats)
return TRUE
if("tempo")
tempo = sanitize_tempo(text2num(params["new"]))
return TRUE
if("play")
INVOKE_ASYNC(src, .proc/start_playing, usr)
if("newline")
var/newline = html_encode(input("Enter your line: ", parent.name) as text|null)
if(!newline || !in_range(parent, usr))
return
if(length(lines) > MUSIC_MAXLINES)
return
if(length(newline) > MUSIC_MAXLINECHARS)
newline = copytext(newline, 1, MUSIC_MAXLINECHARS)
lines.Add(newline)
return TRUE
if("deleteline")
var/num = round(text2num(params["line"]))
if(num > length(lines) || num < 1)
return
lines.Cut(num, num + 1)
return TRUE
if("modifyline")
var/num = round(text2num(params["line"]), 1)
var/content = stripped_input(usr, "Enter your line: ", parent.name, lines[num], MUSIC_MAXLINECHARS)
if(!content || !in_range(parent, usr))
return
if(num > length(lines) || num < 1)
return
lines[num] = content
return TRUE
if("stop")
stop_playing()
if("setlinearfalloff")
set_linear_falloff_duration(round(text2num(params["new"]) * 10, world.tick_lag))
return TRUE
if("setexpfalloff")
set_exponential_drop_rate(round(text2num(params["new"]), 0.00001))
return TRUE
if("setvolume")
set_volume(round(text2num(params["new"]), 1))
return TRUE
if("setdropoffvolume")
set_dropoff_volume(round(text2num(params["new"]), 0.01))
return TRUE
if("switchinstrument")
if(!length(allowed_instrument_ids))
return
else if(length(allowed_instrument_ids) == 1)
set_instrument(allowed_instrument_ids[1])
return
var/choice = params["name"]
for(var/i in allowed_instrument_ids)
var/datum/instrument/I = SSinstruments.get_instrument(i)
if(I && I.name == choice)
set_instrument(I)
return TRUE
if("setnoteshift")
note_shift = clamp(round(text2num(params["new"])), note_shift_min, note_shift_max)
return TRUE
if("setsustainmode")
var/static/list/sustain_modes
if(!length(sustain_modes))
sustain_modes = list("Linear" = SUSTAIN_LINEAR, "Exponential" = SUSTAIN_EXPONENTIAL)
var/choice = params["new"]
sustain_mode = sustain_modes[choice] || sustain_mode
return TRUE
if("togglesustainhold")
full_sustain_held_note = !full_sustain_held_note
return TRUE
else
return FALSE
parent.add_fingerprint(usr)
/**
* Parses a song the user has input into lines and stores them.
*/
/datum/song/proc/parse_song(text)
set waitfor = FALSE
//split into lines
lines = splittext(text, "\n")
if(length(lines))
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(length(lines) > 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++
SStgui.update_uis(parent)

View File

@@ -0,0 +1,95 @@
/**
* 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 - number from 1-7 for A-G
* * acc - either "b", "n", or "#"
* * oct - 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/filename = "sound/instruments/[cached_legacy_dir]/[ascii2text(note + 64)][acc][oct].[cached_legacy_ext]"
var/soundfile = file(filename)
// make sure the note exists
var/cached_fexists = valid_files[filename]
if(!isnull(cached_fexists))
if(!cached_fexists)
return
else if(!fexists(soundfile))
valid_files[filename] = FALSE
return
else
valid_files[filename] = TRUE
// 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
if(!(M?.client?.prefs?.toggles & SOUND_INSTRUMENTS))
continue
M.playsound_local(source, null, volume * using_instrument.volume_multiplier, falloff = 5, S = music_played)
// Could do environment and echo later but not for now

View File

@@ -0,0 +1,134 @@
/**
* 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
if(!(M?.client?.prefs?.toggles & SOUND_INSTRUMENTS))
continue
M.playsound_local(get_turf(parent), null, volume, FALSE, K.frequency, INSTRUMENT_DISTANCE_NO_FALLOFF, channel, null, copy, distance_multiplier = INSTRUMENT_DISTANCE_FALLOFF_BUFF)
// 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)
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)

View File

@@ -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')

View File

@@ -296,7 +296,7 @@
return
var/sound/S = sound(mysound)
S.wait = 0 //No queue
S.channel = open_sound_channel()
S.channel = SSsounds.random_available_channel()
S.volume = 50
for(var/mob/M in passengers | pilot)
M << S