System TabReader - A Warcraft III Guitar Tab and Music interpreter

Discussion in 'Tutorials and Resources' started by Zwiebelchen, May 5, 2012.

  1. Zwiebelchen

    Zwiebelchen You can change this now in User CP.

    Ratings:
    +60 / 0 / -0
    Download: http://www.hiveworkshop.com/forums/...-tabreader-music-interpreter-tabreaderv11.w3m

    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):
    Code:
    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 [i]Guitar Pro[/i]. You can download a free trial version of Guitar Pro 6 here:
    [url]http://www.guitar-pro.com/en/index.php?pg=download[/url]
    
    (2) Select the track you want to extract.
    
    (3) In Guitar Pro 5, click on "Tools", then "Fill up bars with breaks".
    [img]http://www.hiveworkshop.com/forums/attachment.php?attachmentid=113687&stc=1&d=1336247487[/img]
    
    (4) Click on your track, check the tuning information for the octave numbers.
    [img]http://www.hiveworkshop.com/forums/attachment.php?attachmentid=113688&stc=1&d=1336247487[/img]
    
    (5) Go to "Data" and "Export" and select "Export as Ascii"
    [img]http://www.hiveworkshop.com/forums/attachment.php?attachmentid=113689&stc=1&d=1336247487[/img]
    
    (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.
    [img]http://www.hiveworkshop.com/forums/attachment.php?attachmentid=113690&stc=1&d=1336247487[/img]
    
    (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
    [img]http://www.hiveworkshop.com/forums/attachments/submissions-414/113691d1336247473-tabreader-music-interpreter-tut6.jpg[/img]
    
    (9) Add the track to your Song. Repeat this for all tracks.
    
    (10) Use the play method to play your song. Enjoy.
    
     

Share This Page