- Joined
- Nov 7, 2014
- Messages
- 571
Sound - a simplish way of playing sounds
JASS:
library Sound
//! novjass
Sound - a simplish way of playing sounds
General notes:
Playing sounds is somewhat tricky because there are limitations (for good reasons probably,
see this thread post: http://www.hiveworkshop.com/threads/play-sound-consecutively.270026/#post-2734432).
One has to use different sound handles that have been initialized with the same file name (but only up to 4) in order
to play the same sound more than once at the same time.
Credits:
Zwiebelchen - testing/reasearching about the workings and quirks of playing sounds in WC3.
Rising_Dusk - SoundUtils (Sound is based on it): http://www.wc3c.net/showthread.php?t=107433
Toadcop - TcX/SndX (inspiration): http://tcx.xgm.guru/downloads#tcx
LeP - jassdoc: https://github.com/lep/jassdoc/blob/master/sound.j
API:
//
// Defining sounds
//
// params:
// string file_name - the name of the sound file (e.g: "Abilities\\Spells\\Other\\Doom\\DoomTarget.wav")
// real duration - the duration of the sound file in seconds (e.g: 2.261)
// integer fade_in_rate - the fade in rate (e.g: 10)
// integer fade_in_rate - the fade out rate (e.g: 10)
//
// integer sound_flags - flags that combine a few properties about a sound:
// Sound.FLAGS_NONE - no flags
// Sound.FLAGS_IS_LOOPING - if set, the sound will repeat after finish playing until manually stopped/killed
// Sound.FLAGS_IS_3D - this flags must be set in order to play a sound at some xyz point or attach it to a unit
// Sound.FLAGS_STOP_WHEN_OUT_OF_RANGE
// Sound.FLAGS_ALL - FLAGS_IS_LOOPING + FLAGS_IS_3D + FLAGS_STOP_WHEN_OUT_OF_RANGE
//
// string eax_setting - (e.g: "SpellsEAX")
//
// returns - a sound ref that is used (see Sound.get) to play the sound
//
// Hint: you can use the Sound Editor (F5), right clicking on a sound and selecting "Use as Sound", then make compile time error
// and find the "InitSounds" function and there you can find some autogenerated jass from which you can copy
// most of the parameters:
//
// function InitSounds takes nothing returns nothing
// set gg_snd_DoomTarget=CreateSound("Abilities\\Spells\\Other\\Doom\\DoomTarget.wav", false, true, true, 10, 10, "SpellsEAX")
// call SetSoundParamsFromLabel(gg_snd_DoomTarget, "DoomTarget")
// call SetSoundDuration(gg_snd_DoomTarget, 2261)
// endfnction
//
// From the above we can define the sound (note: the boolean paramters order for CreateSound is: is-looping, is-3d, stop-when-out-of-range)
//
// globals
// integer DOOM_TARGET
// endglobals
//
// set DOOM_TARGET = Sound.define("Abilities\\Spells\\Other\\Doom\\DoomTarget.wav", 2.261, 10, 10, Sound.FLAGS_IS_3D + Sound.FLAGS_STOP_WHEN_OUT_OF_RANGE, "SpellsEAX")
// // same as
// set DOOM_TARGET = Sound.define("Abilities\\Spells\\Other\\Doom\\DoomTarget.wav", 2.261, 10, 10, Sound.FLAGS_ALL - Sound.FLAGS_IS_LOOPING, "SpellsEAX")
//
static method define takes string file_name, real duration, integer fade_in_rate, integer fade_out_rate, integer sound_flags, string eax_setting returns integer
//
// Selecting a sound to be played
//
// ref - a sound references that was returned from a Sound.define call
//
static method get takes integer ref returns Sound
//
// Configuring a sound
//
// Sets the volume of the sound. Range: 0 .. 100
//
method volume takes integer v returns thistype
// Sets the cutoff distance for the sound. (NOTE: only for 3D sounds, Sound.FLAGS_IS_3D must be set)
method cutoff takes real c returns thistype
// Attaches the sound to the unit. (only for 3D sounds)
method attach_to_unit takes unit u returns thistype
// Sets the positon of the sound. (only for 3D sounds)
method set_xyz takes real x, real y, real z returns thistype
// Plays the sound from position start for a duration of duration.
// start and duration are in seconds. (only for non-3D sounds, Sound.FLAGS_IS_3D must NOT be set)
method seek takes real start, real duration returns thistype
// Sets the channel. Range: 0 .. 11
method channel takes integer c returns thistype
// From jassdoc:
// "Tones the pitch of the sound, default value is 1. Increasing it you get the chipmunk
// version and the sound becomes shorter, when decremented the sound becomes low-pitched and longer."
//
method pitch takes real p returns thistype
// (only for 3D sounds)
method distances takes real min, real max returns thistype
// The first three parameters are the those of SetSoundConeAngles,
// the second three are those of SetSoundConeOrientation. (only for 3D sounds)
method cone takes real inside, real outside, integer outside_volume, real orientation_x, real orientation_y, real orientation_z returns thistype
// (only for 3D sounds)
method velocity takes real x, real y, real z returns thistype
// Sets whether the sound fades or not.
// integer fade
// Sound.NO_FADE_OUT
// Sound.FADE_OUT
//
method fade takes integer fade returns thistype
//
// Playing/Killing a sound
//
// NOTE: in order to play 3D sounds one of the methods attach_to_unit or set_xyz must be called.
// Plays the sound for all players.
method play takes nothing returns thistype
// Plays the sound for Player(p). Range: 0 .. 11
method play_for takes integer p returns thistype
// Stops playing the sound. This method is called automatically for non-looping sounds.
// You have to manually call this method for sounds that have the flag Sound.FLAGS_IS_LOOPING set.
//
method kill takes integer fade_type returns nothing
// Method chaining:
// Sound.get(DOOM_TARGET).set_xyz(GetUnitX(u), GetUnitY(u), 80.0).cutoff(3000.0).play()
//! endnovjass
private struct Timer extends array
readonly static integer max_count = 0
private static Timer head = 0
private Timer next
private timer t
integer data
real timeout
private boolean is_started
static method start takes integer user_data, real timeout, code callback returns Timer
local Timer this
if head != 0 then
set this = head
set head = head.next
else
set max_count = max_count + 1
set this = max_count
set this.t = CreateTimer()
endif
set this.next = 0
set this.data = user_data
set this.timeout = timeout
set this.is_started = true
call TimerStart(this.t, this, false, null)
call PauseTimer(this.t)
call TimerStart(this.t, timeout, false, callback)
return this
endmethod
method stop takes nothing returns nothing
if this != 0 and this.is_started then
set this.is_started = false
call TimerStart(this.t, 0.0, false, null)
set this.next = head
set head = this
endif
endmethod
static method get_expired_data takes nothing returns integer
local Timer t = Timer( R2I(TimerGetRemaining(GetExpiredTimer()) + 0.5) )
local integer data = t.data
call t.stop()
return data
endmethod
endstruct
struct Sound
// can't have more than this many sounds with the same file name playing
// at the same time (jass limitation?)
//
private static constant integer MAX_SOUNDS = 4
private static hashtable ht = InitHashtable()
private static integer new_ref = 0
private static boolean is_new_ref
private static method snd_ref_from takes integer sound_flags, string file_name returns integer
local integer hash = StringHash(file_name)
local integer result = LoadInteger(ht, sound_flags, hash)
set is_new_ref = false
if result == 0 then
set is_new_ref = true
set new_ref = new_ref + 1
set result = new_ref
call SaveInteger(ht, sound_flags, hash, result)
endif
return result
endmethod
// sound ref fields
//
private thistype fl_head
private integer instance_count
private thistype q_head
private thistype q_tail
private method remove_from_queue takes nothing returns nothing
local thistype ref = this.ref
if ref.q_head != 0 then
set ref.q_head = ref.q_head.q_next
if ref.q_head == 0 then
set ref.q_tail = 0
endif
endif
endmethod
private method add_to_queue takes nothing returns nothing
local thistype ref = this.ref
set this.q_next = 0
if ref.q_tail != 0 then
set ref.q_tail.q_next = this
endif
set ref.q_tail = this
if ref.q_head == 0 then
set ref.q_head = this
endif
endmethod
private method add_to_free_list takes nothing returns nothing
local thistype ref = this.ref
set this.fl_next = ref.fl_head
set ref.fl_head = this
endmethod
private static method get_from_free_list takes thistype ref returns thistype
local thistype snd
local thistype snd_new
if ref.fl_head == 0 then
return 0
endif
set snd = ref.fl_head
set ref.fl_head = ref.fl_head.fl_next
if ref.instance_count < MAX_SOUNDS and ref.fl_head == 0 then
set snd_new = allocate()
set snd_new.ref = ref
set snd_new.s = CreateSound(ref.file_name, ref.is_looping, ref.is_3d, ref.stop_when_out_of_range, ref.fade_in_rate, ref.fade_out_rate, ref.eax_setting)
call snd_new.add_to_free_list()
set ref.instance_count = ref.instance_count + 1
endif
set snd.use_count = 1
return snd
endmethod
static constant integer /*Sound.*/NO_FADE_OUT = 0
static constant integer /*Sound.*/FADE_OUT = 1
method kill takes integer fade_type returns nothing
if this.is_playing then
set this.use_count = this.use_count - 1
if this.use_count <= 0 then
set this.is_playing = false
call StopSound(this.s, /*kill-when-done:*/ false, /*fade-out:*/ fade_type == FADE_OUT)
call this.tmr.stop()
call this.remove_from_queue()
call this.add_to_free_list()
endif
endif
endmethod
static constant integer /*Sound.*/FLAGS_NONE = 0
static constant integer /*Sound.*/FLAGS_IS_LOOPING = 1
static constant integer /*Sound.*/FLAGS_IS_3D = 2
static constant integer /*Sound.*/FLAGS_STOP_WHEN_OUT_OF_RANGE = 4
static constant integer /*Sound.*/FLAGS_ALL = FLAGS_IS_LOOPING + FLAGS_IS_3D + FLAGS_STOP_WHEN_OUT_OF_RANGE
readonly string file_name
readonly real duration // in seconds
readonly integer fade_in_rate
readonly integer fade_out_rate
readonly boolean is_looping
readonly boolean is_3d
readonly boolean stop_when_out_of_range
readonly string eax_setting
// snd fields
//
private thistype fl_next
private thistype q_next
readonly thistype ref
private sound s
private boolean is_playing
// readonly integer played_for
private Timer tmr
private integer use_count
readonly integer m_volume // 0 .. 100
readonly real m_cutoff
readonly real start_offset
readonly real end_offset
readonly integer m_channel
readonly real m_pitch
readonly real min_dist
readonly real max_dist
readonly real ca_inside
readonly real ca_outside
readonly integer ca_outside_volume
readonly real co_x
readonly real co_y
readonly real co_z
readonly real vx
readonly real vy
readonly real vz
readonly integer m_fade
static method define takes string file_name, real duration, integer fade_in_rate, integer fade_out_rate, integer sound_flags, string eax_setting returns integer
local thistype ref = thistype(snd_ref_from(sound_flags, file_name))
local thistype snd
if is_new_ref then
set ref.file_name = file_name
set ref.duration = duration
set ref.fade_in_rate = fade_in_rate
set ref.fade_out_rate = fade_out_rate
if sound_flags >= FLAGS_STOP_WHEN_OUT_OF_RANGE then
set ref.stop_when_out_of_range = true
set sound_flags = sound_flags - FLAGS_STOP_WHEN_OUT_OF_RANGE
else
set ref.stop_when_out_of_range = false
endif
if sound_flags >= FLAGS_IS_3D then
set ref.is_3d = true
set sound_flags = sound_flags - FLAGS_IS_3D
else
set ref.is_3d = false
endif
set ref.is_looping = sound_flags >= FLAGS_IS_LOOPING
set ref.eax_setting = eax_setting
set snd = allocate()
set snd.ref = ref
set snd.s = CreateSound(ref.file_name, ref.is_looping, ref.is_3d, ref.stop_when_out_of_range, ref.fade_in_rate, ref.fade_out_rate, ref.eax_setting)
call snd.add_to_free_list()
set ref.instance_count = 1
endif
return ref
endmethod
static if DEBUG_MODE then
private static method panic takes string s returns nothing
call BJDebugMsg("|cffFF0000" + "thistype" + " error: " + s + "|r")
if 1 / 0 == 1 then // we report the error and stop the script from running any further
endif
endmethod
endif
static method get takes thistype ref returns thistype
local thistype snd
static if DEBUG_MODE then
if not (1 <= ref and ref <= new_ref) then
call panic(I2S(ref) + " is not a valid Sound ref")
endif
endif
set snd = get_from_free_list(ref)
if snd == 0 then
set snd = ref.q_head
call snd.remove_from_queue()
call snd.add_to_queue()
call StopSound(snd.s, /*kill-when-done:*/ false, /*fade-out:*/ snd.m_fade == FADE_OUT)
set snd.use_count = snd.use_count + 1
else
call snd.add_to_queue()
endif
call snd.tmr.stop()
// these seem like good defaults
set snd.m_volume = 100
if snd.ref.is_3d then
set snd.m_cutoff = 3000.0
endif
set snd.start_offset = 0.0
set snd.end_offset = ref.duration
set snd.m_channel = GetRandomInt(0, 11)
set snd.m_pitch = 1.001
set snd.min_dist = 600.0
set snd.max_dist = 100000.0
set snd.ca_inside = 0.0
set snd.ca_outside = 0.0
set snd.ca_outside_volume = 0
set snd.co_x = 0.0
set snd.co_y = 0.0
set snd.co_z = 0.0
set snd.vx = 0.0
set snd.vy = 0.0
set snd.vz = 0.0
set snd.m_fade = FADE_OUT
return snd
endmethod
method volume takes integer v returns thistype
set this.m_volume = v
return this
endmethod
//! textmacro Sound_3D_CHECK takes METHOD_NAME
static if DEBUG_MODE then
if not this.ref.is_3d then
call panic("attempt to call method $METHOD_NAME$ for Sound(" + I2S(this) + "), with file name '" + this.ref.file_name + "', but sound is not 3D")
endif
endif
//! endtextmacro
method cutoff takes real c returns thistype
//! runtextmacro Sound_3D_CHECK("cutoff")
set this.m_cutoff = c
return this
endmethod
method attach_to_unit takes unit u returns thistype
//! runtextmacro Sound_3D_CHECK("attach_to_unit")
call AttachSoundToUnit(this.s, u)
return this
endmethod
method set_xyz takes real x, real y, real z returns thistype
//! runtextmacro Sound_3D_CHECK("set_xyz")
call SetSoundPosition(this.s, x, y, z)
return this
endmethod
method seek takes real start, real duration returns thistype
local real max_dur = this.ref.duration
if start < 0.0 or start > max_dur then
set start = 0.0
endif
set this.start_offset = start
if duration < 0.0 or duration > max_dur then
set this.end_offset = this.start_offset + max_dur - this.start_offset
else
set this.end_offset = this.start_offset + duration
if this.end_offset > max_dur then
set this.end_offset = max_dur
endif
endif
return this
endmethod
method channel takes integer c returns thistype
set this.m_channel = c
return this
endmethod
method pitch takes real p returns thistype
set this.m_pitch = p + 0.001
return this
endmethod
method distances takes real min, real max returns thistype
//! runtextmacro Sound_3D_CHECK("distances")
set this.min_dist = min
set this.max_dist = max
return this
endmethod
method cone takes real inside, real outside, integer outside_volume, real ox, real oy, real oz returns thistype
//! runtextmacro Sound_3D_CHECK("cone")
set this.ca_inside = inside
set this.ca_outside = outside
set this.ca_outside_volume = outside_volume
set this.co_x = ox
set this.co_y = oy
set this.co_z = oz
return this
endmethod
method velocity takes real x, real y, real z returns thistype
//! runtextmacro Sound_3D_CHECK("velocity")
set this.vx = x
set this.vy = y
set this.vz = z
return this
endmethod
method fade takes integer f returns thistype
set this.m_fade = f
return this
endmethod
private method prepare takes nothing returns nothing
local sound s = this.s
call SetSoundVolume(s, R2I(this.m_volume / 100.0 * 127.0))
call SetSoundChannel(s, this.m_channel)
call SetSoundPitch(s, this.m_pitch)
if this.ref.is_3d then
call SetSoundDistanceCutoff(s, this.m_cutoff)
call SetSoundDistances(s, this.min_dist, this.max_dist)
call SetSoundConeAngles(s, this.ca_inside, this.ca_outside, this.ca_outside_volume)
call SetSoundConeOrientation(s, this.co_x, this.co_y, this.co_z)
call SetSoundVelocity(s, this.vx, this.vy, this.vz)
endif
endmethod
private method start takes integer p returns nothing
local boolean is_3d = this.ref.is_3d
local real pitch = this.m_pitch
local integer pos
set this.is_playing = true
// set this.played_for = p
if p == /*all-players:*/ -1 then
call StartSound(s)
// SetSoundPlayPosition doesn't seem to work with 3d sounds,
// the GUI note for "Sound - Play Sound From Offset" has a note on this
// as well: "This should not be used on 3D sounds"
//
if not is_3d and this.start_offset != 0.0 then
set pos = R2I(this.start_offset * 1000.0)
call SetSoundPlayPosition(s, pos)
endif
elseif GetLocalPlayer() == Player(p) then
call StartSound(s)
if not is_3d and this.start_offset != 0.0 then
set pos = R2I(this.start_offset * 1000.0)
call SetSoundPlayPosition(s, pos)
endif
endif
endmethod
private static method kill_when_done takes nothing returns nothing
local thistype snd = Timer.get_expired_data()
call snd.kill(snd.m_fade)
endmethod
private method play_internal takes integer p returns thistype
local real dur
call this.prepare()
call start(p)
if not this.ref.is_looping then
set dur = (this.end_offset - this.start_offset) / this.m_pitch
set this.tmr = Timer.start(this, dur, function thistype.kill_when_done)
endif
return this
endmethod
method play takes nothing returns thistype
return this.play_internal(/*all-players:*/ -1)
endmethod
method play_for takes integer p returns thistype
return this.play_internal(p)
endmethod
endstruct
endlibrary