• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[vJASS] Sound

Level 13
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
 

Attachments

  • silly-sound-demo.w3x
    27.3 KB · Views: 149
It's nice that this uses only 1 hashtable whereas SoundUtils uses 4. Are there any other benefits to this over SoundUtils? I don't think the naming convention fits the (v)JASS style but I suppose it's the style you want to use.

You also probably shouldn't rely on a bug for timer attachment. Your system already has a hashtable, use that.
 
Top