• 🏆 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!

[System] TabReader (Music Interpreter)

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
What is this?
Basicly, it's a guitar tab interpreter system for Warcraft III, allowing to import very few highly compressed sounds and generate music with them, kind of like a MIDI interpreter software. It was designed to be used in combination with Guitar Pro, but is also compatible to any other midi interpreter software that allows exporting midis or files as a standardized guitar tab.

How to use it?
Best would be to check the demo map. There is also a quick tutorial at the end of this post.

JASS:
/*
=======================================================================================

    TAB READER v1.1                 Created by Zwiebelchen
    
=======================================================================================

    Scans Guitar Pro Sheets and turns them into audible music - without importing whole music files.
    
        Inspired by Midi-Readers
        
        
    Check Hiveworkshop Forums for a Tutorial on how to use it.

        
        SUPPORTS:
            - Tabs extracted by Guitar Pro (tested with GP5, but should also work with other versions)
            - Notes up to an interval of 1/32, such as triplets and dotted notes
            - Accented notes (>) and ghost notes (bracket notes)
            - Tied notes (L)
            - Percussion Tabs
            - Customizable tunings (Drop D, Bass tunings, etc.)
                ...and more to come.
                
        LIMITATIONS:
            - Warcraft III comes with some limitations when using sounds. This system is build to try to overcome most of those limitations, but for some, this is simply not possible and
              needs the user to react according to them:
                (1) sounds of the same filepath can only be played up to 4 times at the same time
                (2) sounds of the same filepath must have a delay of 0.1 seconds inbetween them to be played correctly
                (3) only 16 sounds (no matter which filepath) can be played at the same time
            - rules (1) and (2) can be bypassed by adding more sound files to your instrument. Check the API on how to do so.
            - rule (3) can not be avoided, however. Its up to the user to create music that doesnt play too many sounds at the same time
                NOTE: fading sounds also take up one of those 16 slots. If you got a lot of trouble with this limitation, set your fadeout rates higher.
                      Check your song for instruments that dont need fading at all (most likely rhythm guitars or basses) and set the fadeout rate to 12700,
                      to be allowed to play more sounds at the same time
            examples:
            - in order to allow for chords (starting the same sound multiple times on different pitches), you need to register more sounds to your instrument.
            - the system uses a trick to bypass the 0.1 second delay rule on very fast passages. However, this means that the last sound might be cut off early, so if very fast passages sound weird
              you should consider adding one more sound to your instrument.
        
        
        
        API:


            Struct Song:
                [static].create(string name, real bpm, real duration) returns thistype
                    name: allows to put in a name string in case you need something to be displayed on screen
                    bpm: the speed of the song in beats per minute (= quarters per minute)
                    duration: length of the song in seconds; information required for looping
                    
                .addTrack(Track tr)
                    tr: the Track struct you want to assign to the song. You can change the maximum number of tracks possible in the system globals
                    
                .play(player for)
                    for: player the song shall be played for.
                    
                .stop(player for)
                    for: player the song shall be stopped for.
                    
                .isPlaying(player for) returns boolean
                    for: player you want to check wether the song is playing or not.
                
                .name         [readonly string]
                    Allows to get the name of the song
                    
                    
            Struct Track:
                [static].create(Instrument inst, integer vol, integer fadeout) returns thistype
                    inst: assigns an instrument to the Track. Only one instrument can be assigned to the same trick, but you can assign one instrument to multiple tracks if you wish (see LIMITATIONS!)
                    vol: the volume of your track between 0 and 127. The accent informations are multiplied by that number to get the real volume of a note.
                         Ordinary notes use volume*0.85, ghost notes use volume*0.5, accented notes use volume*1
                    fadeout: Sets the fadeout rate of the instrument sounds (0-12700).
                             This setting is important for defining the style of your instrument. If your fadeout rate is very slow (the lower the number, the slower your fade), the sounds will
                             get blurred and overlap each other, even without using tied notes (L). This is nice for Piano type sounds with high percussive momentum and high reverb.
                             If your fadeout rate is very high, you can achieve a staccato kind of sound, suited for rhythm instruments. Also, if your fadeout is faster,
                             the instrument will consume less sound slots, which is good if you got trouble with the sound limit of Warcraft III.
                             (see LIMITATIONS for more information)
                                    
                                    recommended settings:        instant cutoff (staccato):      no cutoff (auto tied notes):
                                        fadeout: 15-30               fadeout: 12700                  fadeout: 0
                         
                [static].createPercussion(integer vol) returns thistype
                    creates a percussion track. Remember that having more than one percussion track usually doesnt make much sense, so better put all your stuff in one track to improve performance.
                    vol: the volume of your track between 0 and 127. The accent informations are multiplied by that number to get the real volume of a note.
                         Ordinary notes use volume*0.85, ghost notes use volume*0.5, accented notes use volume*1
                         
                [static].registerPercussionSound(integer which, string path)   
                    registers a sound to all percussive tracks. You can only put one sound per ID.
                    which: the midi-ID of your percussion sound (see table at the end of this documentation!)
                    path: filepath of your sound
                         
                .pushTabLength(string s) [required!]
                    pushes the ascii string for note length information into your track. If you push multiple times, you are able to divide your track into multiple track parts,
                    in case you reach the maximum string length. The length strings must be pushed in the order of playback. All strings of the same track part
                    must have the same length. Dont worry: The system will display a warning you if you did something wrong here.
                    
                .pushTabTriplet(string s) [optional]
                    pushes the ascii string for triplet information into your track. Follows the same rules as .pushTabLength().
                    If only one later track part of your track has triplet information, you still need to push the empty triplet part strings that come first, even if they only contain spacebars.
                    They still need to have the same string length or you will receive a warning.
                    
                .pushTabAccent(string s) [optional]
                    pushes the ascii string for accent information into your track. Follows the same rules as .pushTabLength().
                    If only one later track part of your track has accent information, you still need to push the empty accent part strings that come first, even if they only contain spacebars.
                    They still need to have the same string length or you will receive a warning.
                    
                .pushTabNotes[integer line, string s) [optional]
                    line: determines which line of the track your string is. Guitars usually have lines 0 to 5, with 0 being the highest and 5 being the lowest string.
                    pushes the ascii string for note values into your track. Follows the same rules as .pushTabLength(). In case a whole line is empty (for example, if your tab only using the
                    thinest two strings), you can leave it out. So only put those lines here that actually contain information. However, if you put a line, you will need to push it for all track parts,
                    even if it only contains empty lines. Otherwise you will receive a warning.
                    The first push per line needs the tune information of your tab line. ASCII outputs of guitar pro usually dont display the octave number here, so you need to add that.
                    In this case, dont forget to add an extra spacebar on the first track part of your length, triplet and accent information lines to match the increased length.
                    Standard guitar tuning is: E3, A3, D4, G4, B4, E5. Standard bass tuning is: E2, A2, D3, G3.
                                       
                    If you did everything right, the length information should always be on the second digit of your note. If it is on the first digit, you need to put in a spacebar.
                    HINT: use indendation to improve readability!
                    
                    example:
                    .pushTabTriplet(        "       |-3-|-3-|  ")
                    .pushTabLength(         "       Q   Q   Q  ")
                    .pushTabNotes(0,        "E5||--10----------")
                    .pushTabNotes(1,        "B4||--------------")
                    .pushTabNotes(2,        "G4||--------------")
                    .pushTabNotes(3,        "D4||--------------")
                    .pushTabNotes(4,        "A3||---0---3---2--")
                    .pushTabNotes(5,        "E3||--------------")
                    
                    For percussion Tabs, there is no alteration needed, as there is no tuning information. Simply leave everything as it is.
                    
                .volume [integer vol]
                    allows to change the volume of your track at any time.
                

            Struct Instrument:
                [static].create(string key, string filepath) returns thistype
                    key: the key of the sound you imported, used for determining pitch values of other notes
                         must contain: base note (A, B, C, D, E, F, G), half note information (# or b) and octave number (1 to 9).
                         example: "C#3" or "D4" - "E3" being the standard tune of the thickest string on guitars, "E5" being the standard tune of the thinnest string.
                         remember: in music theory, octave numbers switch at the C note, not at the note A!
                    filepath: the path of your sound file.
                    
                .add(string key, string filepath)
                    See create method. Allows to add more sounds to one instrument, to allow for chords and multiple notes at the same time (see LIMITATIONS!)
                    You can change the maximum number of sounds allowed per instrument in globals.

------------------------------------------------------------------------------------------------------------------------
                    
        PERCUSSION ID TABLE:
        
             27 High Q
             28 Slap
             29 Scratch Push
             30 Scratch Pull
             31 Sticks
             32 Square Click
             33 Metronome Click
             34 Metronome Bell
             35 Bass Drum 2
             36 Bass Drum 1
             37 Side Stick/Rimshot
             38 Snare Drum 1
             39 Hand Clap
             40 Snare Drum 2
             41 Low Tom 2
             42 Closed Hi-hat
             43 Low Tom 1
             44 Pedal Hi-hat
             45 Mid Tom 2
             46 Open Hi-hat
             47 Mid Tom 1
             48 High Tom 2
             49 Crash Cymbal 1
             50 High Tom 1
             51 Ride Cymbal 1
             52 Chinese Cymbal
             53 Ride Bell
             54 Tambourine
             55 Splash Cymbal
             56 Cowbell
             57 Crash Cymbal 2
             58 Vibra Slap
             59 Ride Cymbal 2
             60 High Bongo
             61 Low Bongo
             62 Mute High Conga
             63 Open High Conga
             64 Low Conga
             65 High Timbale
             66 Low Timbale
             67 High Agogô
             68 Low Agogô
             69 Cabasa
             70 Maracas
             71 Short Whistle
             72 Long Whistle
             73 Short Güiro
             74 Long Güiro
             75 Claves
             76 High Wood Block
             77 Low Wood Block
             78 Mute Cuíca
             79 Open Cuíca
             80 Mute Triangle
             81 Open Triangle
             82 Shaker
             83 Jingle Bell
             84 Bell Tree
             85 Castinets
             86 Mude Surdo
             87 Open Surdo

======================================================================================================

*/
library TabReader uses TimerUtils

globals
    //configurables
    private constant integer FILE_TIME_OFFSET = 50   //when saving sound files, most programs add a very small break before the sounds to avoid clipping noise.
                                                  //This constant determines when the actual sound starts in your sound files to avoid delay [ms] (default: 50)
    private constant integer PARTS_PER_TRACK = 5 //max number of strings per tabline, as strings are limited to 2000 characters
    private constant integer TABLINES_PER_TRACK = 6 //max number of tablines per track
    private constant integer PARTSTIMESLINES = 30 //must be set to PARTS_PER_TRACK*TABLINES_PER_TRACK (due to struct limitations, this can not be automated)
    private constant integer TRACKS_PER_SONG = 12 //max number of tracks per song; remember that you are limited to 16 sounds playing at the same time
    private constant integer SOUNDS_PER_INSTRUMENT = 6 //max number of sound files that can be assigned to one instrument
    private constant integer SOUNDSTIMESFOUR = 24 //must be set to SOUNDS_PER_INSTRUMENT*4 (due to struct limitations, this can not be automated)
    private constant boolean ALLOW_32TH = true //Allows the use of 1/32th notes. Those notes can still be dotted or played as triplets.
                                       //set this to FALSE to limit the system to 1/16th notes, resulting in higher performance.
    //end of configurables
    //-------------------------------------
    
       
    private constant real PBASE = 1.059465 //base used for determining pitch values
                                           //pitch value = PBASE^n
                                           //with n = number of half steps to desired note
    private constant real IGNORE_SAME_INTERVAL = 0.1 //the interval in which Warcraft III ignores StartSound() calls of the same filepath
    private constant integer MIN_BPM = 40
    private constant integer MAX_BPM = 180
    private sound array drumsounds[61] //in midi standard, there are 61 different drum sounds
endglobals

private function CharToKey takes string char returns integer
    if char == "C" then
        return 0
    elseif char == "D" then
        return 2
    elseif char == "E" then
        return 4
    elseif char == "F" then
        return 5
    elseif char == "G" then
        return 7
    elseif char == "A" then
        return 9
    elseif char == "B" then
        return 11
    endif
    return (-1)
endfunction

private function TranslateKey takes string s returns integer
    //first 3 chars of tab strings are tab keys
    //C0 being the lowest note possible, B9 being the highest
    local string temp = SubString(s, 0, 1)
    local integer octave = 0
    local integer key = CharToKey(temp)
    set temp = SubString(s, 1, 2)
    if temp == "#" then //sharp keys
        set key = key+1
        set temp = SubString(s, 2, 3)
        set octave = S2I(temp)
        if octave != 0 or temp == "0" then //octave number
            set key = key+octave*12
        endif
    elseif temp == "b" then //flat keys for compat
        set key = key-1
        set temp = SubString(s, 2, 3)
        set octave = S2I(temp)
        if octave != 0 or temp == "0" then //octave number
            set key = key+octave*12
        endif
    else
        set octave = S2I(temp)
        if octave != 0 or temp == "0" then //octave number
            set key = key+octave*12
        endif
    endif
    if key < 0 then
        return 0
    elseif key > 119 then
        return 119
    endif
    return key
endfunction

struct Instrument
    private integer count
    private integer fOut
    private integer array note[SOUNDS_PER_INSTRUMENT]
    private sound array obj[SOUNDSTIMESFOUR]
    private real array lp[SOUNDSTIMESFOUR]
    private real array creationtime[SOUNDSTIMESFOUR]
    real array stopWhen[SOUNDSTIMESFOUR]
          
    static method create takes string key, string filepath, integer fadeout returns thistype
        //key like: "C#2" or "D4"
        local thistype this = thistype.allocate()
        set this.fOut = fadeout
        //create sound objects to allow playing the same sound up to 4 times (this is the internal hardcoded limitation of warcraft III, so we won't need more than that)
        set this.obj[0] = CreateSound(filepath, false, false, false, 10, fOut, "CombatSoundsEAX")
        set this.obj[1] = CreateSound(filepath, false, false, false, 10, fOut, "CombatSoundsEAX")
        set this.obj[2] = CreateSound(filepath, false, false, false, 10, fOut, "CombatSoundsEAX")
        set this.obj[3] = CreateSound(filepath, false, false, false, 10, fOut, "CombatSoundsEAX")
        set this.lp[0] = 1
        set this.lp[1] = 1
        set this.lp[2] = 1
        set this.lp[3] = 1
        set this.note[0] = TranslateKey(key)
        set this.count = 1
        return this
    endmethod
    
    method add takes string key, string path returns nothing
        //allows to add up more sounds with a different filepath to your instrument
        //there are 4 rules you need to know about this:
        //*1 for pitching, the system will always try to find the closest registered note sound from those first
        //*2 you HAVE TO add one new filepath for every note STARTED at the same time. For example, if your song involves 3-string chords, you need to import 2 more sound files to make it work (chord notes)
        //*3 the same file can be run up to 4 times simoultanously, if the notes that triggered it are not started at the same time (overlapping notes, i.e. for guitar or piano arpeggios).
        //you can also use this to add a little diversity to the sounds, like adding extra treble to high sounds, to simulate strumming of thinner strings
        //key like: "C#2" or "D4"
        if count < SOUNDS_PER_INSTRUMENT then
            //create sound objects to allow playing the same sound up to 4 times (this is the internal hardcoded limitation of warcraft III, so we won't need more than that)
            set obj[count*4+0] = CreateSound(path, false, false, false, 10, fOut, "CombatSoundsEAX")
            set obj[count*4+1] = CreateSound(path, false, false, false, 10, fOut, "CombatSoundsEAX")
            set obj[count*4+2] = CreateSound(path, false, false, false, 10, fOut, "CombatSoundsEAX")
            set obj[count*4+3] = CreateSound(path, false, false, false, 10, fOut, "CombatSoundsEAX")
            set lp[count*4+0] = 1
            set lp[count*4+1] = 1
            set lp[count*4+2] = 1
            set lp[count*4+3] = 1
            set note[count] = TranslateKey(key)
            set count = count+1
        endif
    endmethod
    
    private method setPitch takes integer i, real pitch returns nothing
        //due to the bugged pitch native, we need this workaround snippet in order to make it work
        if GetSoundIsPlaying(obj[i]) or GetSoundIsLoading(obj[i]) then
            call SetSoundPitch(obj[i], 1/lp[i])
            call SetSoundPitch(obj[i], pitch)
            set lp[i] = pitch
        else
            if pitch == 1 then
                call SetSoundPitch(obj[i], 1.0001)
                set lp[i] = 1.0001
            else
                call SetSoundPitch(obj[i], pitch)
                set lp[i] = pitch
            endif
        endif
    endmethod
    
    method resetTimestamps takes nothing returns nothing
        local integer i = 0
        loop
            exitwhen i >= (count*4)
            set creationtime[i] = 0
            if obj[i] != null then
                if GetSoundIsPlaying(obj[i]) or GetSoundIsLoading(obj[i]) then
                    call StopSound(obj[i], false, true)
                endif
            endif
            set i=i+1
        endloop
    endmethod
    
    method stopNote takes integer objkey, real timestamp returns nothing
        if objkey >= 0 then
            if stopWhen[objkey] <= timestamp then
                if fOut == 12700 then //non fading sound
                    call StopSound(obj[objkey], false, false)
                else
                    call StopSound(obj[objkey], false, true)
                endif
            endif
        endif
    endmethod
    
    method playNote takes integer key, integer vol, real timestamp returns integer //this function is called inside a local block for performance reasons, so do not create/alter/destroy handles
        local integer dist = 1000
        local integer newdist
        local integer closest = 0
        local integer closesub = 0
        local integer oldest = 0
        local integer oldsub = 0
        local real oldtime = 0
        local integer i = 0
        local integer j
        local sound new = null
        local sound old = null
        //these loops seem to be overkill, but as the exits are very early (>3), it's not as performance hungry as it might look
        loop
            exitwhen i >= count
            set newdist = IAbsBJ(note[i]-key)
            set j = 0
            loop //check wether the sound's filepath is valid to be played, as Warcraft III doesn't allow playing the same filepath twice in a short interval
                exitwhen j>3
                if timestamp-creationtime[i*4+j] < IGNORE_SAME_INTERVAL then
                    exitwhen true //sound is not valid, so check next filepath
                endif
                set j = j+1
            endloop
            if j>3 then //sound is valid, so we can perform additional checks
                if newdist < dist then //first check if the sound is closer to the desired key than the last one found
                    set j = 0
                    loop //now check for an unused sound
                        exitwhen j>3
                        if not (GetSoundIsPlaying(obj[i*4+j]) or GetSoundIsLoading(obj[i*4+j])) then //found an unused sound, so make this sound and filepath the new reference
                            set dist = newdist
                            set closest = i
                            set closesub = j
                            set new = obj[i*4+j]
                            exitwhen true
                        else //keep the playing sound in mind in case we can find no other sound
                            if creationtime[i*4+j] < oldtime or old == null then
                                set oldest = i
                                set oldsub = j
                                set old = obj[i*4+j]
                                set oldtime = creationtime[i*4+j]
                            endif
                        endif
                        set j = j+1
                    endloop
                endif
            else
                if i == 0 then //store the first not valid sound in case we can find nothing else
                    if oldtime == 0 then
                        loop
                            exitwhen j>3
                            if GetSoundIsPlaying(obj[i*4+j]) or GetSoundIsLoading(obj[i*4+j]) then //find a USED sound, as only those can be played again even between the 0.1 interval and keep it as a reserve
                                set oldest = i
                                set oldsub = j
                                set oldtime = creationtime[i*4+j]
                                //don't put the sound object into the old variable yet, as it is part of the improved reserve condition - this one is only for extreme emergencies
                                exitwhen true
                            endif
                            set j = j+1
                        endloop
                    endif
                endif
            endif
            set i = i + 1
        endloop
        if new != null then
            set dist = key-note[closest]
            call setPitch(closest*4+closesub, Pow(PBASE, dist))
            call SetSoundVolume(new, vol)
            call StartSound(new)
            call SetSoundPlayPosition(new, FILE_TIME_OFFSET)
            set creationtime[closest*4+closesub] = timestamp
            set old = null
            set new = null
            return (closest*4+closesub)
        else //no valid sound could be found, so abuse a playing sound
            if old == null then //not even a reserve has been found, so use the emergency reserve
                set old = obj[oldest*4+oldsub]
            endif
            set dist = key-note[oldest]
            call setPitch(oldest*4+oldsub, Pow(PBASE, dist))
            call SetSoundVolume(old, vol)
            call StartSound(old) //overwrites stop command in case the sound was currently fading out
            call SetSoundPlayPosition(old, FILE_TIME_OFFSET)
            set creationtime[oldest*4+oldsub] = timestamp
            set old = null
            return (oldest*4+oldsub)
        endif
    endmethod
endstruct

struct Track
    integer volume
    
    private Instrument instrument
    
    private boolean perc
    private string array accent[PARTS_PER_TRACK]
    private string array triplet[PARTS_PER_TRACK]
    private string array length[PARTS_PER_TRACK]
    private string array tab[PARTSTIMESLINES]
    private integer array s[TABLINES_PER_TRACK]
    private real ttn = 0
    private real tracktimestamp
    private integer position = 0
    private boolean plays = false
    
    private integer array checklength[PARTS_PER_TRACK]
    private integer accentcount = 0
    private integer tripletcount = 0
    private integer lengthcount = 0
    private integer array tabcount[TABLINES_PER_TRACK]
    
    private integer array key[TABLINES_PER_TRACK]
    
    static method create takes Instrument inst, integer vol returns thistype
        local thistype this = thistype.allocate()
        if vol > 127 then
            set this.volume = 127
        elseif vol < 0 then
            set this.volume = 0
        else
            set this.volume = vol
        endif
        set this.instrument = inst
        set this.perc = false
        return this
    endmethod
    
    static method createPercussion takes integer vol returns thistype
        local thistype this = thistype.allocate()
        if vol > 127 then
            set this.volume = 127
        elseif vol < 0 then
            set this.volume = 0
        else
            set this.volume = vol
        endif
        set this.perc = true
        return this
    endmethod
    
    static method registerPercussionSound takes integer which, string path returns nothing
        if which >= 27 and which <= 87 then
            if drumsounds[which-27] != null then
                debug call BJDebugMsg("ERROR: Percussion Sound ID already assigned.")
            endif
            set drumsounds[which-27]  = CreateSound(path, false, false, false, 12700, 12700, "CombatSoundsEAX")
        else
            debug call BJDebugMsg("ERROR: Invalid percussion ID.")
        endif
    endmethod
    
    private static method playPercussion takes integer which, integer volume returns nothing
        local integer i
        if which >= 27 and which <= 87 then
            set i = which-27
            call StartSound(drumsounds[i])
            call SetSoundPlayPosition(drumsounds[i], FILE_TIME_OFFSET)
            call SetSoundVolume(drumsounds[i], volume)
        endif
    endmethod
    
    method pushTabAccent takes string st returns nothing
        if accentcount < PARTS_PER_TRACK then
            if StringLength(st) == 0 then
                debug call BJDebugMsg("ERROR: Wrong line input!")
                return
            endif
            if checklength[accentcount] == StringLength(st) or checklength[accentcount] == 0 then
                set checklength[accentcount] = StringLength(st)
                set accent[accentcount] = st
                set accentcount = accentcount + 1
            else
                debug call BJDebugMsg("ERROR: Wrong line input!")
            endif
        endif
    endmethod
    
    method pushTabTriplet takes string st returns nothing
        if tripletcount < PARTS_PER_TRACK then
            if StringLength(st) == 0 then
                debug call BJDebugMsg("ERROR: Wrong line input!")
                return
            endif
            if checklength[tripletcount] == StringLength(st) or checklength[tripletcount] == 0 then
                set checklength[tripletcount] = StringLength(st)
                set triplet[tripletcount] = st
                set tripletcount = tripletcount + 1
            else
                debug call BJDebugMsg("ERROR: Wrong line input!")
            endif
        endif
    endmethod
    
    method pushTabLength takes string st returns nothing
        if lengthcount < PARTS_PER_TRACK then
            if StringLength(st) == 0 then
                debug call BJDebugMsg("ERROR: Wrong line input!")
                return
            endif
            if checklength[lengthcount] == StringLength(st) or checklength[lengthcount] == 0 then
                set checklength[lengthcount] = StringLength(st)
                set length[lengthcount] = st
                set lengthcount = lengthcount + 1
            else
                debug call BJDebugMsg("ERROR: Wrong line input!")
            endif
        endif
    endmethod
    
    method pushTabNotes takes integer line, string st returns nothing
        if line < TABLINES_PER_TRACK then
            if StringLength(st) == 0 then
                debug call BJDebugMsg("ERROR: Wrong line input!")
                return
            endif
            if tabcount[line] < PARTS_PER_TRACK then
                if checklength[tabcount[line]] == StringLength(st) or checklength[tabcount[line]] == 0 then
                    set checklength[tabcount[line]] = StringLength(st)
                    set tab[line*PARTS_PER_TRACK+tabcount[line]] = st
                    //in case input is first input, store the key of the tabline
                    if perc then
                        set key[line] = 0
                    else
                        if tabcount[line] == 0 then
                            set key[line] = TranslateKey(st)
                        endif
                    endif
                    set tabcount[line] = tabcount[line] + 1
                endif
            else
                debug call BJDebugMsg("ERROR: Wrong line input!")
            endif
        endif
    endmethod
    
    method stopAll takes nothing returns nothing
        if not perc then
            call instrument.resetTimestamps()
        endif
    endmethod
    
    method read takes real interval, boolean reset returns nothing
        local integer i = 0
        local integer j
        local integer part
        local integer k
        local integer m
        local real volfactor
        local string temp
        set tracktimestamp = tracktimestamp+interval //does not interfere with other players, as the read function is already within a local block at this point
        if reset then
            set position = 3
            set ttn = 0
            set plays = true
            set tracktimestamp = 1
            loop
                exitwhen i >= TABLINES_PER_TRACK
                set s[i]=-1
                set i=i+1
            endloop
            if not perc then
                call instrument.resetTimestamps()
            endif
        else
            set ttn = ttn-interval
        endif
        if ttn <= 0 and plays then //only check for new information when last note ended
            set j = 0
            set part = R2I(position/2048)//determines the string part we are looking at
            set i = position-part*2048
            set k = StringLength(length[part])-2 
            set m = 0
            set volfactor = 0.85 //standard volume factor
            loop
                set i=i+1
                set temp = SubString(length[part],i,i+1)
                if temp != " " and temp != "." then
                    if temp == "T" then //1/32th
                        static if ALLOW_32TH then
                            set ttn = interval*3
                        else
                            return 0
                        endif
                    elseif temp == "S" then //1/16th
                        set ttn = interval*6
                    elseif temp == "E" then //1/8th
                        set ttn = interval*12
                    elseif temp == "Q" then //quarter
                        set ttn = interval*24
                    elseif temp == "H" then //half
                        set ttn = interval*48
                    elseif temp == "W" then //whole
                        set ttn = interval*96
                    else
                        set ttn = 0
                    endif
                    static if not ALLOW_32TH then
                        set ttn = ttn/2
                    endif
                    if SubString(length[part],i+1,i+2) == "." then //dotted note
                        set ttn = ttn*1.5
                    endif
                    if tripletcount > 0 then
                        if part < tripletcount then
                            if SubString(triplet[part],i,i+1) != " " then
                                set ttn = ttn*0.666 //triplet note
                            endif
                        endif
                    endif
                    set ttn = ttn-0.001 //in case of rounding errors, subtract a very small amount of ttn to allow for the <= 0 check to return true
                    
                    //now that we have the length information, we need to check for accent information
                    if accentcount > 0 then
                        if part < accentcount then
                            if SubString(accent[part],i,i+1) == ">" then
                                set volfactor = 1 //accentuation mark applies to the notes of all tablines, so doing this before scanning the notes is correct
                            endif
                        endif
                    endif
                    
                    //now we can finally scan for the actual notes
                    loop
                        exitwhen j >= TABLINES_PER_TRACK
                        if tabcount[j] <= 0 then //in case there is no more tab on this line (like 4 or 5 string basses)
                            exitwhen true
                        endif
                        set temp = SubString(tab[j*PARTS_PER_TRACK+part],i,i+1)
                        if temp != "-" then
                            //there is information on this tabline
                            set m = S2I(temp)
                            if m > 0 or temp == "0" then
                                if not perc then
                                    call instrument.stopNote(s[j], tracktimestamp)
                                    set s[j] = -1
                                endif
                                //check previous Substring for 2-digit notes and add tabline key for true note value
                                set m = m+10*S2I(SubString(tab[j*PARTS_PER_TRACK+part],i-1,i))+key[j]
                                if SubString(tab[j*PARTS_PER_TRACK+part],i+1,i+2) == ")" then //ghost note
                                    set volfactor = volfactor*0.5
                                endif
                                if perc then
                                    call playPercussion(m, R2I(I2R(volume)*volfactor))
                                else
                                    set s[j] = instrument.playNote(m, R2I(I2R(volume)*volfactor), tracktimestamp)
                                    set instrument.stopWhen[s[j]] = tracktimestamp+ttn
                                endif
                            elseif temp == "L" then //only keep the sound going when the note is tied (L)
                                if not perc then
                                    set instrument.stopWhen[s[j]] = tracktimestamp+ttn
                                endif
                            else
                                if not perc then
                                    call instrument.stopNote(s[j], tracktimestamp)
                                    set s[j] = -1
                                endif
                            endif
                        else
                            if not perc then
                                call instrument.stopNote(s[j], tracktimestamp)
                                set s[j] = -1
                            endif
                        endif
                        set j = j+1
                    endloop
                    exitwhen true
                endif
                if i>=k then //reached end of part and did not play anything yet
                    set part = part+1
                    if part < lengthcount then //not the last part of the track so ...
                        set position = part*2048 //... go to next part ...
                        call read(interval, false) //... instantly!
                        return
                    else
                        set j = 0
                        loop
                            exitwhen j >= TABLINES_PER_TRACK
                            set s[j] = -1
                            set plays = false
                            call stopAll()
                            set j = j+1
                        endloop
                    endif
                    exitwhen true
                endif
            endloop
            set position = i+part*2048
        endif
    endmethod
endstruct

struct Song

    readonly string name
    private real interval
    private real duration
    private real timestamp
    private real array endtime[12]
    private boolean array looping[12]
    private boolean array playing[12]
    private boolean isrepeating
    private timer reader
    private integer trackcount
    private Track array t[TRACKS_PER_SONG]
    
    static method create takes string nam, real bpm, real dur returns thistype
        local thistype this = thistype.allocate()
        set this.name = nam
        set this.duration = dur
        if bpm < MIN_BPM then
            set this.interval = 1/(MIN_BPM*0.2)
        elseif bpm > MAX_BPM then
            set this.interval = 1/(MAX_BPM*0.2)
        else
            set this.interval = 1/(bpm*0.2) //bpm/60*8*3 
        endif
        static if ALLOW_32TH then
            set this.interval = this.interval/2
        endif
        set this.trackcount = 0
        set this.reader = NewTimer()
        set this.timestamp = 1
        return this
    endmethod
    
    method addTrack takes Track tr returns nothing
        if trackcount < TRACKS_PER_SONG then
            set t[trackcount] = tr
            set trackcount = trackcount + 1
        endif
    endmethod
    
    method stop takes player for returns nothing
        local integer i = 0
        local integer j = 0
        local boolean b = true
        set playing[GetPlayerId(for)] = false
        loop
            exitwhen i > 11
            if playing[i] then
                set b = false
            else
                //stop all sounds locally
                if GetLocalPlayer() == Player(i) then
                    set j = 0
                    loop
                        exitwhen j >= trackcount
                        call t[j].stopAll()
                        set j=j+1
                    endloop
                endif
            endif
            set i = i + 1
        endloop
        if b then
            //halt timer when nobody is listening
            call PauseTimer(reader)
            set isrepeating = false
        endif
    endmethod
    
    private method reset takes nothing returns nothing
        local integer j = 0
        loop
            exitwhen j >= trackcount
            call t[j].read(interval, true)
            set j = j+1
        endloop
    endmethod
    
    private static method periodic takes nothing returns nothing
        local thistype this = GetTimerData(GetExpiredTimer())
        local integer i = 0
        local integer j = 0
        set timestamp = timestamp+interval
        loop
            exitwhen i > 11
            if playing[i] then
                if timestamp > endtime[i] then
                    if looping[i] then
                        set endtime[i]=timestamp+duration
                        if GetLocalPlayer() == Player(i) then
                            call reset()
                        endif
                    else
                        call stop(Player(i))
                    endif
                endif
            endif
            set i = i + 1
        endloop
        if playing[GetPlayerId(GetLocalPlayer())] then //everything after this is local
            set j = 0
            loop
                exitwhen j >= trackcount
                call t[j].read(interval, false)
                set j = j+1
            endloop
        endif
    endmethod
    
    method play takes player for, boolean loopit returns nothing
        set playing[GetPlayerId(for)] = true
        set endtime[GetPlayerId(for)] = timestamp+duration
        set looping[GetPlayerId(for)] = loopit
        if GetLocalPlayer() == for then
            call reset()
        endif
        if not isrepeating then
            call SetTimerData(reader, this)
            call TimerStart(reader, interval, true, function thistype.periodic)
            set isrepeating = true
        endif
    endmethod
    
    method isPlaying takes player for returns boolean
        return playing[GetPlayerId(for)]
    endmethod
    
endstruct
    
endlibrary


---------------------------------

Tutorial on how to import tabs to this system (for Guitar Pro users):

Sorry that I could not provide you with screenshots of the english version of Guitar Pro. But I think the screens might still be helpful.

(1) First, open your midi or .gp file with Guitar Pro. You can download a free trial version of Guitar Pro 6 here:
http://www.guitar-pro.com/en/index.php?pg=download

(2) Select the track you want to extract.

(3) In Guitar Pro 5, click on "Tools", then "Fill up bars with breaks".
attachment.php


(4) Click on your track, check the tuning information for the octave numbers.
attachment.php


(5) Go to "Data" and "Export" and select "Export as Ascii"
attachment.php


(6) A window opens. Set the number of columns to 999 --> 1.
Check your guitar tab for triplet (|-3-| or 3) and accent (>) information. --> 2
If your tab contains those extra lines, make sure to copy them.
attachment.php


(7) Copy and paste everything into a text converted trigger in your WC3 trigger editor.

(8) Create Instruments and import sounds by using the provided struct methods (check example), then create your Track, add your instrument to it. --> 1
Add extra sounds to your instruments if chord notes are needed. --> 2
In case your tab provides triplet information, make sure to copy and paste them too. --> 3
Make sure everything is on the same line: The length information, the triplet information, the last digit of your note value. --> 4
Make sure to push a line of spacebars to other track parts even if there are no triplets on this part. If you use triplets just for one part, you need to put it over every part of this track aswell. --> 5
Adjust the tuning information of the first part by adding the octave number (see step 4). Make sure to add a spacebar to your length, accent and triplet line for this part, to make up for the extra character. --> 6
113691d1336247473-tabreader-music-interpreter-tut6.jpg


(9) Add the track to your Song. Repeat this for all tracks.

(10) Use the play method to play your song. Enjoy.
 

Attachments

  • Tut2.jpg
    Tut2.jpg
    96.1 KB · Views: 1,458
  • Tut3.jpg
    Tut3.jpg
    106.6 KB · Views: 1,135
  • Tut4.jpg
    Tut4.jpg
    91.4 KB · Views: 1,185
  • Tut5.jpg
    Tut5.jpg
    166.5 KB · Views: 1,295
  • Tut6.jpg
    Tut6.jpg
    324.2 KB · Views: 1,156
  • TabReaderv11.w3m
    1.7 MB · Views: 401
Last edited:

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
Well, some knowledge about music does really help here, but in the end, it all boils down to copy and paste, if the user got Guitar Pro, so it's not that hard to use.

I will write a tutorial on it now.

If you post it in the Spells section, this /will/ get a DC.
But then I am not allowed to have a post about it here and people will not notice it existing. :/
I had the same problem with my aggro system ZTS, which is an absolutely amazing system, but only has like 35 comments on the spells section.
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
Yes! I really want to make Dubstep with this :D

Could this support vocals?

edit
I realized it could, but that would lag way too much :p

edit
Since Vocals are too complex for the user to pull off, they should be an extension to this, or built-in :eek:
You can always sample vocals and then play them by using dummy notes. I see no reason why this should lag at all. However, importing whole vocal passages would make the system useless, as then you can also import the whole mp3 anyway. ;D
Works fine for repeating voice samples, though.


PS: Finished the Percussion reader implementation now. Will upload as soon as I got an example ready.
 
Level 16
Joined
Aug 7, 2009
Messages
1,403
Yes! I really want to make Dubstep with this :D

You couldn't really do that; according to Zwiebelchen's tests, changing the pitch of an already playing sound will cause bugs, hence achieving the real Dubstep wooble bass is impossible. That's why I said you in VM that I'll do house with it (actually it'll be some easier Minimal Techno), that'll work perfectly with the system.
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
You couldn't really do that; according to Zwiebelchen's tests, changing the pitch of an already playing sound will cause bugs, hence achieving the real Dubstep wooble bass is impossible. That's why I said you in VM that I'll do house with it (actually it'll be some easier Minimal Techno), that'll work perfectly with the system.
That's not true. I fixed that bug with an almost magical workaround. The system has no more flaw now, except for a minor bug that sometimes causes sounds to be cut off earlier than they should due to double references, but I will fix that ASAP. There should be no problem with any kind of music with this. Only stuff that could really be hard is extremely fast metal (160 bpm and more) - but even that should be possible.
Also, 1.1 version (which is going to be released tonight) will also come with some tweaks within the system to improve sound quality of extremely fast passages (32' note triplets).
 
Last edited:
JASS:
private function CharToKey takes string char returns integer
    if char == "C" then
        return 0
    elseif char == "D" then
        return 2
    elseif char == "E" then
        return 4
    elseif char == "F" then
        return 5
    elseif char == "G" then
        return 7
    elseif char == "A" then
        return 9
    elseif char == "B" then
        return 11
    endif
    return (-1)
endfunction

->

JASS:
globals
    private key tbK
    private Table tb = tbK
endglobals

private function CharToKey takes string char returns integer
    return tb[StringHash(char)] - 1
endfunction

private module Init
    private static method onInit takes nothing returns nothing
        set tb[StringHash("C")] = 1  // 0  + 1
        set tb[StringHash("D")] = 3  // 2  + 1
        set tb[StringHash("E")] = 5  // 4  + 1
        set tb[StringHash("F")] = 6  // 5  + 1
        set tb[StringHash("G")] = 8  // 7  + 1
        set tb[StringHash("A")] = 10 // 9  + 1
        set tb[StringHash("B")] = 12 // 11 + 1
    endmethod
endmodule
private struct Inits extends array
    implement Init
endstruct

Of course, then you'd have to make Table a requirement, but an extra hashtable shouldn't be a problem :p
You could hash everything so you wouldn't need functions to interpret pieces of the music sheets :D
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
Please watch the language ;)

Also, debug messages please add "debug" keyword on those lines.

I will see if we can get someone to highlight this in the HiveWorkshop monthly news batch, or maybe I will sticky this after I approve it to be the first DC of its kind.
Done that. What do you mean by watch the language?
I know this is not ... the style of a learned programmer. I'm mostly self-taught. But in the end, it works fine, so I think it should be alright. If someone wants to optimize it, go ahead - but I don't think there is a lot to be optimized here.

Of course, then you'd have to make Table a requirement, but an extra hashtable shouldn't be a problem :p
You could hash everything so you wouldn't need functions to interpret pieces of the music sheets :D
Hmm ... doesn't really matter, if you ask me. The key conversion is done only once when the track is initialized and like that I can drop the table requirement.


UPDATE!

v1.1
- Now added a percussion tab reader to the system (and added a demo song for that :) ).
- Now allows for setting the fadeout rates of individual instruments instead of a global constant (I realized that this is an important setting for determining the sound of your instrument)
- Fixed a bug that sometimes caused sounds to fade before they should
- Added a time offset global constant to get rid of the nasty delay most recording softwares add at the beginning of sound files
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
The "debug" keyword before BJDebugMsg is not an optimization, it is simply a disabling of the debug message if the user is not running in debug mode. You wouldn't want to display those in a public-released game, for example.

By "watch the language" I meant to not use swear words in your post.

I don't think there is a lot to be optimized here.

I agree. Any room for optimizing might increase performance by 5-10% which is not worth it at all. I am putting less and less time into studying how efficient something is because efficiency-spotting is not healthy in 99% of cases.
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
The "debug" keyword before BJDebugMsg is not an optimization, it is simply a disabling of the debug message if the user is not running in debug mode. You wouldn't want to display those in a public-released game, for example.
Added that. Though I think it could be dangerous to do so (as lots of people, including me, never used the debug mode). The system is very sensitive to wrong input data. I hope I won't get thousands of PMs "why doesn't it work for me?" that I have to answer by "maybe you forgot to add a spacebar to your strings." :/

By "watch the language" I meant to not use swear words in your post.
Fixed :)

I agree. Any room for optimizing might increase performance by 5-10% which is not worth it at all. I am putting less and less time into studying how efficient something is because efficiency-spotting is not healthy in 99% of cases.
Well, it's mostly local code anyway. So the optimizations that could be done boil down to restructuring some stuff, rearranging conditions and logic and stuff. I don't think it will make a huge difference.

Maybe some textmacros would help for the registry functions of tabs. I copy&pasted the same method three times. :/
Then again, I'm not that good with textmacros and don't know what is possible and how.
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
Textmacros do the same CnP job that you'd do; so in the end, when it's compiled the amount of code remains the same. It only shortens the time you nead to write and the for others to read your code.
Yeah I know that. I was more thinking of instead of using the methods to get that user input, I could switch to a macro that allows c'n'ping the tab ascii directly without having to add those unneccesary function calls before.
Not sure wether this is possible or not.

So that this:
JASS:
                    .pushTabTriplet(        "       |-3-|-3-|  ")
                    .pushTabLength(         "       Q   Q   Q  ")
                    .pushTabNotes(0,        "E5||--10----------")
                    .pushTabNotes(1,        "B4||--------------")
                    .pushTabNotes(2,        "G4||--------------")
                    .pushTabNotes(3,        "D4||--------------")
                    .pushTabNotes(4,        "A3||---0---3---2--")
                    .pushTabNotes(5,        "E3||--------------")

becomes this:
JASS:
RegisterTab(
       |-3-|-3-|  
       Q   Q   Q  
E5||--10----------
B4||--------------
G4||--------------
D4||--------------
A3||---0---3---2--
E3||--------------
)
Something like that ... can anyone give me an advice on wether this is possibly with textmacros?
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
Well, the only other optimization you could do is changing the read function from a recursive to one to a regular one and use a loop instead.

In High-Level programming languages, this barely makes a difference, but in Jass, the difference is noticeable because function calls are unusually costy.
Not quite sure what you mean. Read is not recursive from my point of view and also I don't understand how a loop could help, as there is nothing to loop in there. Maybe give an example so I can understand what you mean by that?
 
JASS:
@method read takes real interval, boolean reset returns nothing@
        local integer i = 0
        local integer j
        local integer part
        local integer k
        local integer m
        local real volfactor
        local string temp
        set tracktimestamp = tracktimestamp+interval //does not interfere with other players, as the read function is already within a local block at this point
        if reset then
            set position = 3
            set ttn = 0
            set plays = true
            set tracktimestamp = 1
            loop
                exitwhen i >= TABLINES_PER_TRACK
                set s[i]=-1
                set i=i+1
            endloop
            if not perc then
                call instrument.resetTimestamps()
            endif
        else
            set ttn = ttn-interval
        endif
        if ttn <= 0 and plays then //only check for new information when last note ended
            set j = 0
            set part = R2I(position/2048)//determines the string part we are looking at
            set i = position-part*2048
            set k = StringLength(length[part])-2 
            set m = 0
            set volfactor = 0.85 //standard volume factor
            loop
                set i=i+1
                set temp = SubString(length[part],i,i+1)
                if temp != " " and temp != "." then
                    if temp == "T" then //1/32th
                        static if ALLOW_32TH then
                            set ttn = interval*3
                        else
                            return 0
                        endif
                    elseif temp == "S" then //1/16th
                        set ttn = interval*6
                    elseif temp == "E" then //1/8th
                        set ttn = interval*12
                    elseif temp == "Q" then //quarter
                        set ttn = interval*24
                    elseif temp == "H" then //half
                        set ttn = interval*48
                    elseif temp == "W" then //whole
                        set ttn = interval*96
                    else
                        set ttn = 0
                    endif
                    static if not ALLOW_32TH then
                        set ttn = ttn/2
                    endif
                    if SubString(length[part],i+1,i+2) == "." then //dotted note
                        set ttn = ttn*1.5
                    endif
                    if tripletcount > 0 then
                        if part < tripletcount then
                            if SubString(triplet[part],i,i+1) != " " then
                                set ttn = ttn*0.666 //triplet note
                            endif
                        endif
                    endif
                    set ttn = ttn-0.001 //in case of rounding errors, subtract a very small amount of ttn to allow for the <= 0 check to return true
                    
                    //now that we have the length information, we need to check for accent information
                    if accentcount > 0 then
                        if part < accentcount then
                            if SubString(accent[part],i,i+1) == ">" then
                                set volfactor = 1 //accentuation mark applies to the notes of all tablines, so doing this before scanning the notes is correct
                            endif
                        endif
                    endif
                    
                    //now we can finally scan for the actual notes
                    loop
                        exitwhen j >= TABLINES_PER_TRACK
                        if tabcount[j] <= 0 then //in case there is no more tab on this line (like 4 or 5 string basses)
                            exitwhen true
                        endif
                        set temp = SubString(tab[j*PARTS_PER_TRACK+part],i,i+1)
                        if temp != "-" then
                            //there is information on this tabline
                            set m = S2I(temp)
                            if m > 0 or temp == "0" then
                                if not perc then
                                    call instrument.stopNote(s[j], tracktimestamp)
                                    set s[j] = -1
                                endif
                                //check previous Substring for 2-digit notes and add tabline key for true note value
                                set m = m+10*S2I(SubString(tab[j*PARTS_PER_TRACK+part],i-1,i))+key[j]
                                if SubString(tab[j*PARTS_PER_TRACK+part],i+1,i+2) == ")" then //ghost note
                                    set volfactor = volfactor*0.5
                                endif
                                if perc then
                                    call playPercussion(m, R2I(I2R(volume)*volfactor))
                                else
                                    set s[j] = instrument.playNote(m, R2I(I2R(volume)*volfactor), tracktimestamp)
                                    set instrument.stopWhen[s[j]] = tracktimestamp+ttn
                                endif
                            elseif temp == "L" then //only keep the sound going when the note is tied (L)
                                if not perc then
                                    set instrument.stopWhen[s[j]] = tracktimestamp+ttn
                                endif
                            else
                                if not perc then
                                    call instrument.stopNote(s[j], tracktimestamp)
                                    set s[j] = -1
                                endif
                            endif
                        else
                            if not perc then
                                call instrument.stopNote(s[j], tracktimestamp)
                                set s[j] = -1
                            endif
                        endif
                        set j = j+1
                    endloop
                    exitwhen true
                endif
                if i>=k then //reached end of part and did not play anything yet
                    set part = part+1
                    if part < lengthcount then //not the last part of the track so ...
                        set position = part*2048 //... go to next part ...
                        @call read(interval, false)@ //... instantly!
                        return
                    else
                        set j = 0
                        loop
                            exitwhen j >= TABLINES_PER_TRACK
                            set s[j] = -1
                            set plays = false
                            call stopAll()
                            set j = j+1
                        endloop
                    endif
                    exitwhen true
                endif
            endloop
            set position = i+part*2048
        endif
    endmethod

It would be faster if you are to loop instead of call the function again is what I mean :eek:
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
JASS:
@method read takes real interval, boolean reset returns nothing@
        local integer i = 0
        local integer j
        local integer part
        local integer k
        local integer m
        local real volfactor
        local string temp
        set tracktimestamp = tracktimestamp+interval //does not interfere with other players, as the read function is already within a local block at this point
        if reset then
            set position = 3
            set ttn = 0
            set plays = true
            set tracktimestamp = 1
            loop
                exitwhen i >= TABLINES_PER_TRACK
                set s[i]=-1
                set i=i+1
            endloop
            if not perc then
                call instrument.resetTimestamps()
            endif
        else
            set ttn = ttn-interval
        endif
        if ttn <= 0 and plays then //only check for new information when last note ended
            set j = 0
            set part = R2I(position/2048)//determines the string part we are looking at
            set i = position-part*2048
            set k = StringLength(length[part])-2 
            set m = 0
            set volfactor = 0.85 //standard volume factor
            loop
                set i=i+1
                set temp = SubString(length[part],i,i+1)
                if temp != " " and temp != "." then
                    if temp == "T" then //1/32th
                        static if ALLOW_32TH then
                            set ttn = interval*3
                        else
                            return 0
                        endif
                    elseif temp == "S" then //1/16th
                        set ttn = interval*6
                    elseif temp == "E" then //1/8th
                        set ttn = interval*12
                    elseif temp == "Q" then //quarter
                        set ttn = interval*24
                    elseif temp == "H" then //half
                        set ttn = interval*48
                    elseif temp == "W" then //whole
                        set ttn = interval*96
                    else
                        set ttn = 0
                    endif
                    static if not ALLOW_32TH then
                        set ttn = ttn/2
                    endif
                    if SubString(length[part],i+1,i+2) == "." then //dotted note
                        set ttn = ttn*1.5
                    endif
                    if tripletcount > 0 then
                        if part < tripletcount then
                            if SubString(triplet[part],i,i+1) != " " then
                                set ttn = ttn*0.666 //triplet note
                            endif
                        endif
                    endif
                    set ttn = ttn-0.001 //in case of rounding errors, subtract a very small amount of ttn to allow for the <= 0 check to return true
                    
                    //now that we have the length information, we need to check for accent information
                    if accentcount > 0 then
                        if part < accentcount then
                            if SubString(accent[part],i,i+1) == ">" then
                                set volfactor = 1 //accentuation mark applies to the notes of all tablines, so doing this before scanning the notes is correct
                            endif
                        endif
                    endif
                    
                    //now we can finally scan for the actual notes
                    loop
                        exitwhen j >= TABLINES_PER_TRACK
                        if tabcount[j] <= 0 then //in case there is no more tab on this line (like 4 or 5 string basses)
                            exitwhen true
                        endif
                        set temp = SubString(tab[j*PARTS_PER_TRACK+part],i,i+1)
                        if temp != "-" then
                            //there is information on this tabline
                            set m = S2I(temp)
                            if m > 0 or temp == "0" then
                                if not perc then
                                    call instrument.stopNote(s[j], tracktimestamp)
                                    set s[j] = -1
                                endif
                                //check previous Substring for 2-digit notes and add tabline key for true note value
                                set m = m+10*S2I(SubString(tab[j*PARTS_PER_TRACK+part],i-1,i))+key[j]
                                if SubString(tab[j*PARTS_PER_TRACK+part],i+1,i+2) == ")" then //ghost note
                                    set volfactor = volfactor*0.5
                                endif
                                if perc then
                                    call playPercussion(m, R2I(I2R(volume)*volfactor))
                                else
                                    set s[j] = instrument.playNote(m, R2I(I2R(volume)*volfactor), tracktimestamp)
                                    set instrument.stopWhen[s[j]] = tracktimestamp+ttn
                                endif
                            elseif temp == "L" then //only keep the sound going when the note is tied (L)
                                if not perc then
                                    set instrument.stopWhen[s[j]] = tracktimestamp+ttn
                                endif
                            else
                                if not perc then
                                    call instrument.stopNote(s[j], tracktimestamp)
                                    set s[j] = -1
                                endif
                            endif
                        else
                            if not perc then
                                call instrument.stopNote(s[j], tracktimestamp)
                                set s[j] = -1
                            endif
                        endif
                        set j = j+1
                    endloop
                    exitwhen true
                endif
                if i>=k then //reached end of part and did not play anything yet
                    set part = part+1
                    if part < lengthcount then //not the last part of the track so ...
                        set position = part*2048 //... go to next part ...
                        @call read(interval, false)@ //... instantly!
                        return
                    else
                        set j = 0
                        loop
                            exitwhen j >= TABLINES_PER_TRACK
                            set s[j] = -1
                            set plays = false
                            call stopAll()
                            set j = j+1
                        endloop
                    endif
                    exitwhen true
                endif
            endloop
            set position = i+part*2048
        endif
    endmethod

It would be faster if you are to loop instead of call the function again is what I mean :eek:
This only happens when the end of the tab is reached. I think a loop would make it slower in this case, as this only happens very rarely and checking for the exitwhen condition everytime such as increasing the increment would probably take more power.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
We all did for a long time. One of the reasons warcraft 3 modding is difficult though - lack of standards. Most people want to use World Editor or Jass NewGen Pack, JNGP is not updated with the latest JassHelper so it needs further updates, other enthusiasts want to use cJass, Cohadar's JassHelper is a bit better than Vexorian's JassHelper making it the better choice, so all in all we have a mess.
 
Top