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

[vJASS] creating a custom pseudo-midi-engine?

Status
Not open for further replies.

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
So ... you guys all know the pitch tool that is present in the sound editor of WC3. As I was experimenting around with it, I had this weird idea about creating a music library, which lets you input strings based on midi-to-text conversions and uses them to play music without having to actually import mp3s.

it could work like this:
Instead of having to import a whole music file, all you need are custom sounds, for example, the sound of a guitar string being played on a base note. Then, you create internal copies of that sound using the pitch shifting tool. Of course, some testing here is needed, to find the right pitch shifting values to keep those "notes" in tune.

Then, you could have the library working as a musical interface:
The sounds, run by a timer and the strings based on the midi data, play and stop the single notes according to the input string, actually playing music all together (kinda like card tapes of barrel organs).

In the end, all you would need to create an infinite amount of music for your map are:
One or two different sounds per instrument (Guitars, Pianos, drumsounds).
A midi editing tool that allows to export the midi into a txt.

So ... anyone thinks this could be a useful idea?
Any ideas how to handle it? Maybe someone volunteers to create this lib? I would do it myself, but I currently don't have the time to do that. All I can say is that this could (or could not, depending on how accurate the pitch shifting tool works) be awesome, if it works.

What about you awesome vJass pros out there? Nes, Mag, ...?
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
I'm not sure if this would work due to holding notes for extended periods. You'd have to export something like the C note into a small midi file, important that into a map, then play the sound multiple times to see if you can hold the note. At this point, it's just easier to write the music in another program and import the song as a sound into wc3 >.>. Midi files are tiny after all.
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
I'm not sure if this would work due to holding notes for extended periods.
Well, this could be a limitation then. But you could always just import a long note sound, as you can stop the sound whenever you want. Let's say a sound length of 5 or 6 seconds would be sufficient for most uses. Most acoustic instruments have a decaying sound anyway, so the maximum length of a single note is limited anyway.

You'd have to export something like the C note into a small midi file, important that into a map, then play the sound multiple times to see if you can hold the note.
You can import and use midis to WC3, but that comes with two disadvantages:
The setting for midi support of wc3 is by default disabled. You need to edit some game files to allow for 'real' midi support.
Also, midi sounds usually sound really really bad. I was more thinking about using short wavs or mp3s for the note sounds.

At this point, it's just easier to write the music in another program and import the song as a sound into wc3 >.>. Midi files are tiny after all.
As I said, midis don't work by default. This is the whole reason of this idea: create music with adjustable sounds without having to import 5 megabytes of mp3s.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
Ok... the next problem is that a string can only hold like 2000 chars or something. For every note, you'd need the pitch + the length (2 values). You would then have to store sequences of these values in large strings into an array. Interpreting these values is easy, but writing the music then becomes extremely difficult. What if you want to have 6 different sounds playing at once? You'd need to run 6 different scores at the same time. Really, you'd need an editor that converted the notes into a string. Most music editors like Sibelius and Finale will output the music in an XML format. From there, you could have a program that goes over the XML and outputs a JASS string representation of the scores.

Go learn c++ and how the XML stuff for the music programs >: p.
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
Well, this is what I was thinking. As a common program, for example, guitar pro 5 allows the output of the midi into an ascii file.
It contains all neccesary information: tune, notes, length of notes and even some dynamic informations (bracket notes and hammer on notes have less volume)
Also, this allows for multiple notes at the same time.

However, writing an interpretator for musicXML would be another way to go, but would probably be overkill. Sticking to the ascii tab format could be the easiest way to handle it.

This is an example of how Guitar Pro converts files to ascii:
Code:
                  Trains                     


                               Porcupine Tree
Acoustic Capo V (Capo 5)



     S S S S S S S  S  
E||----0-0-0-0-0------|
B||----3-3-3-3-3------|
G||----2-2-0-2-0------|
D||--0---0-0-0-0-0h-2-|
A||--------------0h-3-|
E||-------------------|


          >                            
  S S E   E.   S  S  S  S  S S S S  S  
---------(0)---0-----0--0--0-0-0------|
----3-----3----3-----3--3--3-3-0------|
----0-0---0----0--0--2--2--0-2-0-0----|
--L---2---2----2--0--0--0--0-0-0-0h-2-|
--L---3---3------(0)---(0)-------0h-2-|
---------------------------------0----|

Using a tab format could be a very elegant solution, as it already provides highly compressed strings. You'd need 7 strings for one track: the length information (S = 1/16th note, E = 1/8th note, E. = 3/16th note, etc.) and 6 note information strings.

The more I think about it, the more I think this could actually work.

You simply input every ascii line (you need to line them up manually first, as they are not continous but use tabstops) into the 7 data strings per track, provide track data to the system (instrument sound, volume, bpm) and it loops through the string one by one depending on the bpm input, starting and stopping the notes with every period.
 
Last edited:

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
Made a quick test now, using a recorded E note, pitchbending it with the soundeditor, using a chromatic tuner and it turns out to work well. All the notes are almost perfectly in tune. A is a tiny bit off. I think it can be corrected by starting off with a lower note as the base, as the higher you pitch, the smaller the gaps between the pitch digits are.

I will try creating a "chord" with those notes now and see how it works.

EDIT: Seems like you can get even better than that by using SetSoundPitch(). Awesome stuff! Really need to write this system as soon as I got time and SoundTools allows for setting pitches and volume of sounds.
 

Attachments

  • test.jpg
    test.jpg
    80.4 KB · Views: 142
Last edited:
If a string can hold 2000 chars, you would instead use a string array and a counter.
When you push data, increase the counter by 1 for every char, and if counter == 2000, set it to 0 and increase some index you're using in the Table, then continue pushing data. This wouldn't need a loop of course, at first, you would compare the size of the string you want to push with 2000 - currentSize. If it's greater, then you would push as much data as you can, skip to the next Table, and push again. If it's less, just push :p I will update SoundTools to give users more control over the sound, and I'll have to write a SoundPlayback object instead of the returning the native sound. Of course, the native sound will be accessible through the SoundPlayback struct instance :P
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
Hmm, using single-character strings? ... would that even be useful? I mean: There is no problem looping through a long string by using substrings (especially as you always know where your last note was, you only need to loop through the next spaceholder and dynamic information characters like "-"), especially, as the step sizes per loop are very small. Long strings wouldn't need any conversion, as a direct input of the ASCII lines created by for example Guitar Pro would be sufficient.
Usually, music progresses in long intervals anyway. It's not like with graphical effects where you would need 32 fps. The fastest interval you would need here are 1/64th notes on a fixed maximum bpm (which would be at around 180 bpm).
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
I didn't it mean it that way, I meant that you would have arrays of 2000 char strings :p
Sure ... that's what I intend to do. I still need some more testing on which information are needed, how long the sounds need to be, the best base note to perform the conversions from, etc.

I think the best quality would be achieved to start out from a center octaved base note and pitchbend up and down to both sides, so that the amount of pitching requires stays at a minimum. This way, the sound quality should be better.

Let's see how this turns out at the end. In theory, this stuff should not be that hard to do, it just requires a lot of testing, finding the right pitch value settings, etc..

I was wondering if it is possible to change the pitch of a sound while it is played (for example, to create glissando, vibrato and bending effects)
 
Level 16
Joined
Mar 3, 2006
Messages
1,564
Bump, tested.

If you set the pitch of a sound while it's playing, it will stop playing.

Are you certain of this Mag, I was testing this as well so I used a music as a sound then played it then I added a trigger to add a fraction to the pitch every second and I found the sound to be playing faster but it didn't stop.

Perhaps you made different test. I don't know but that what happened when I was testing.
 
Bump, tested.

If you set the pitch of a sound while it's playing, it will stop playing.

In any case, whether it stops the sound or not, you can always restart/recreate the sound and use:
JASS:
native SetSoundPlayPosition         takes sound soundHandle, integer millisecs returns nothing

Good luck zwieb, sounds like a cool project.
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
The main problem is the lack of sound channels in WarCraft III. With a complex theme running it would leave very few sound channels for actual game play.
Hmm ... how many sound channels does WC3 support? I suppose its 16 or 32? In that case, this could really be a problem, as the main purpose of a system like that would usually be soundtrack kind of music without vocals. Which could be hard to do with only 16 sound channels (A 6 string chord note would take up 6 sound channels). With 32, it could work, I think.
Someone got exact information how many channels WC3 has?
 

Dr Super Good

Spell Reviewer
Level 63
Joined
Jan 18, 2005
Messages
27,180
With 32, it could work, I think.
Someone got exact information how many channels WC3 has?
Test it. Make short sounds (such as banshee dying) and then a long sound such as a pissed sound. Keep increasing the number of short sounds played until you can no longer hear the long sound. This will be the maximum usable channel number.

Some channels might be used for ambient noise and remember that footsteps also make sounds.
 
That's a good idea Purge :D
We'd just have to track the sound's play position.
We don't need an exact number though, something with an error range with of +/- 0.03125 would be fine ;)

I guess a sound play position tracker needs to be coded too :D

We could either use a simple linked list or a good ol' binary heap.

I might have to make all the data in SoundTools readonly O-o
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
Well, there are two different types of sound channels in Warcraft III:

category channels and actual sound channels.

category channels are not what you call sound channels when coding software.

category channels are like "music", "general", "ambient", "fight", etc. ... used to determine some general attributes of the sounds (like interface sounds not being able to be used as 3d sounds and being played always in front of the camera at the same volume).
Basicly, this doesn't make much difference. I bet it was something used in earlier builds of warcraft and became obsolete later on.

Sound channels, however, are different. Sound channels are something that are required for your soundcard to actually process the sound. Every sound played (no matter if by WC3 or any other software or OS) needs to have a channel ID so that your soundcard can compute it and turn it into an actual audio signal. As far as I know, most softwares and games are limited to like 32 sound channels. If you play more than 32 sounds at the same time, either an old one will stop playing or the new one won't play at all.
It really depends on the games engine, though, how many soundchannels it can take.

I bet it's 32, as this is the usual standard for games, but I could be wrong. WC3 is rather old, then again. 16 might also be possible. I really need to test that before start coding this. With less than 32 channels, the idea can be graveyarded immediately. :(
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
You can merge some sounds if needed.
A C+E sound for example.
Hmm, but that would destroy the whole purpose of this system, though.
The idea was to create an infinite amount of music/soundtracks for maps by using as few imported sounds as possible. If I put chords into presets, the amount of sounds required doubles, etc. - also, additional user input would be needed, as then you would need to redesign your midi tabs.

I want this system to work with a 100% unmodified tab exports from common programs, so that it's easy to use.
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
Started the testing now.
This is what I used:

JASS:
library Music initializer init uses SoundTools

globals
    integer count = 0
    sound array s
endglobals

function StartFun takes nothing returns nothing
    local integer i = 0
    local integer played = 1
    set s[count] = RunSoundEx(NewSound("Sound\\Music\\mp3Music\\Doom.mp3", 66000, false, false), 5, 100, 1)
    set count = count + 1
    loop
        set i = i + 1
        exitwhen i > count
        if GetSoundIsPlaying(s[i]) then
            set played = played + 1
        endif
    endloop
    call BJDebugMsg("Number of Sounds playing: "+I2S(played))
endfunction

function fun takes nothing returns nothing
    call TimerStart(CreateTimer(), 1, true, function StartFun)
endfunction

//===========================================================================
function init takes nothing returns nothing
    local trigger t = CreateTrigger(  )
    call TriggerRegisterPlayerEvent(t, Player(0), EVENT_PLAYER_END_CINEMATIC)
    call TriggerAddAction( t, function fun )
endfunction

endlibrary

It runs only 4 instances of the sound before the next created instances are simply not played (GetSoundIsPlaying() btw seems to work perfectly, nice find!).
I choosed the doom music track because you can easily tell when it starts due to its beginning sound. Let's see what I can do by adjusting the soundchannel manually.
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
Okay, did some further testing:

it seems that there are two limitations in WC3 sounds:
(1) you are limited to 4 instances of the same sound running at the same time (means: it checks for the filename, changing pitch or volume doesn't help)
(2) you are limited to only 16 instances of sounds, no matter which file is played

All sounds that are created or started afterwards will simply not play and also return GetSoundIsPlaying() == false.
Also, calling GetSoundIsPlaying() at the moment you run the sound will return a false negative, so this native is only useful after a delay.

I also observed that those 16 sounds played won't block other sounds that are not initiated by triggers at all, like interface buttons, battle sounds, etc..

Also, setting the sound channel of a sound manually, does absolutely nothing. I bet its an unused variable.

It also doesn't make any difference wether the sound is .wav or .mp3.

I tried some workarounds, but nothing seemed to work. All in all I can say, that this is probably the death of this idea, as both limitations screw it up badly. :(

conclusion:
WC3 seems to use 32 sound channels, but only 16 of them are reserved to the StartSound() native.
JASS:
library Music initializer init uses SoundTools

globals
    integer count = 0
    integer secondcount = 0
    sound array s
endglobals

function StartFun takes nothing returns nothing
    local integer i = 0
    local integer played = 1
    if ModuloInteger(count, 4) == 0 then
        set secondcount = secondcount+1
    endif
    if secondcount == 1 then
        set s[count] = RunSoundEx(NewSound("Sound\\Music\\mp3Music\\Doom.mp3", 66000, false, false), 5, 100, 1)
    elseif secondcount == 2 then
        set s[count] = RunSoundEx(NewSound("Sound\\Music\\mp3Music\\IllidansTheme.mp3", 66000, false, false), 5, 100, 1)
    elseif secondcount == 3 then
        set s[count] = RunSoundEx(NewSound("Sound\\Music\\mp3Music\\LichKingTheme.mp3", 66000, false, false), 5, 100, 1)
    elseif secondcount == 4 then
        set s[count] = RunSoundEx(NewSound("Sound\\Music\\mp3Music\\PursuitTheme.mp3", 66000, false, false), 5, 100, 1)
    elseif secondcount == 5 then
        set s[count] = RunSoundEx(NewSound("Sound\\Music\\mp3Music\\SadMystery.mp3", 66000, false, false), 5, 100, 1)
    elseif secondcount == 5 then
        set s[count] = RunSoundEx(NewSound("Sound\\Music\\mp3Music\\TragicConfrontation.mp3", 66000, false, false), 5, 100, 1)
    elseif secondcount == 6 then
        set s[count] = RunSoundEx(NewSound("Sound\\Music\\mp3Music\\Undead1.mp3", 66000, false, false), 5, 100, 1)
    elseif secondcount == 7 then
        set s[count] = RunSoundEx(NewSound("Sound\\Music\\mp3Music\\Undead2.mp3", 66000, false, false), 5, 100, 1)
    elseif secondcount == 8 then
        set s[count] = RunSoundEx(NewSound("Sound\\Music\\mp3Music\\Undead3.mp3", 66000, false, false), 5, 100, 1)
    endif
    set count = count + 1
    loop
        set i = i + 1
        exitwhen i > count
        if GetSoundIsPlaying(s[i]) then
            set played = played + 1
        endif
    endloop
    call BJDebugMsg("Number of Sounds playing: "+I2S(played))
endfunction

function fun takes nothing returns nothing
    call TimerStart(CreateTimer(), 1, true, function StartFun)
endfunction

//===========================================================================
function init takes nothing returns nothing
    local trigger t = CreateTrigger(  )
    call TriggerRegisterPlayerEvent(t, Player(0), EVENT_PLAYER_END_CINEMATIC)
    call TriggerAddAction( t, function fun )
endfunction

endlibrary
 
(1) you are limited to 4 instances of the same sound running at the same time (means: it checks for the filename, changing pitch or volume doesn't help)

This has workarounds (import the same sound with a different file name)

(2) you are limited to only 16 instances of sounds, no matter which file is played

That kills it :C
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
That kills it :C
I think I will still give it a try, even with those limitations. People might want to make some slight modifications to music tracks or maybe compose music tracks on their own, knowing about the limit and learn how to deal with it, so its not completely rubbish.

Also, for things like battle music, where you basicly need drum patterns and some dark synthesizer sounds, it could still be useful. Or some simple string ensemble styled overworld themes. I don't know. Lets still remember that back in the days of the SNES, people managed to create awesome soundtracks with very limited sound capabilities.
 
I guess you're right =)

We should really work on this :D

edit

I don't have my World Editor available, but here's some code I was able to put together on the spot ;P

JASS:
globals
    private constant integer MAX_CHARS = 2047 // I don't really know the exact number
endglobals

globals
    private SoundType C
    private SoundType E
endglobals

struct SoundType extends array
    Sound file
endstruct

struct SoundFile extends array
    private static integer stack = 0

    private static Table array data
    private static integer array index
    private static integer array count

    static method create takes nothing returns thistype
        set stack = stack + 1
        set data[stack] = Table.create()
        return stack
    endmethod

    method write takes string s returns nothing
        local integer i = StringLength(s)

        if count[this] + i <= MAX_CHARS then
            set data[this][index[this]] = data[this][index[this]] + s
            set count[this] = count[this] + i
        else
            set data[this][index[this]] = data[this][index[this]] + SubString(s, 0, MAX_CHARS - count[this])
            set index[this] = index[this] + 1
            set data[this][index[this]] = SubString(s, MAX_CHARS - count[this], i)
            set count[this] = i - MAX_CHARS + count[this]
        endif
    endmethod

    method read takes nothing returns SoundType
        // ...
    endmethod
endstruct

private module Init
    private static method onInit takes nothing returns nothing
        set C = SoundType(1)
        set E = SoundType(2)
        set C.file = NewSound("C.mp3", 3969, false, false)
        set E.file = NewSound("E.mp3", 3969, false, false)
    endmethod
endmodule
private struct InitS extends array
    implement Init
endstruct
 
Last edited:

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
Currently writing this, but I'm stuck on the key translation function.

Is there a way to turn a single char into its ascii id?
Like A being 64 or something? I need that to determine the base of the tab to find out how high the note is.

This is what I have so far:
JASS:
library TabReader initializer init uses SoundTools

globals
    //configurables
    constant integer PARTS_PER_TRACK = 4
    constant integer TABLINES_PER_TRACK = 6
    
    
    
    private constant integer PARTSTIMESLINES = PARTS_PER_TRACK*TABLINES_PER_TRACK
endglobals

private function init takes nothing returns nothing
    //setup the key translation table
endfunction

private function TranslateKey takes string s returns integer
    //first 3 chars of tab strings are tab keys
    local string key = SubString(s, 0, 3)
    return 0
endfunction

struct Track
    private string array accent[PARTS_PER_TRACK]
    private string array length[PARTS_PER_TRACK]
    private string array tab[PARTSTIMESLINES]
    
    private integer array checklength[PARTS_PER_TRACK]
    private integer accentcount = 0
    private integer lengthcount = 0
    private integer array tabcount[TABLINES_PER_TRACK]
    
    private integer array key[TABLINES_PER_TRACK]
    
    method pushTabAccent takes string s returns nothing
        if accentcount < PARTS_PER_TRACK then
            if checklength[accentcount] == 0 then
                set checklength[accentcount] = StringLength(s)
            elseif checklength[accentcount] == StringLength(s) then
                set accent[accentcount] = s
                set accentcount = accentcount + 1
            else
                call BJDebugMsg("ERROR: Length of track input does not match!")
            endif
        endif
    endmethod
    
    method pushTabLength takes string s returns nothing
        if lengthcount < PARTS_PER_TRACK then
            if checklength[lengthcount] == 0 then
                set checklength[lengthcount] = StringLength(s)
            elseif checklength[lengthcount] == StringLength(s) then
                set length[lengthcount] = s
                set lengthcount = lengthcount + 1
            else
                call BJDebugMsg("ERROR: Length of track input does not match!")
            endif
        endif
    endmethod
    
    method pushTabNotes takes integer line, string s returns nothing
        if line < TABLINES_PER_TRACK then
            if tabcount[line] < PARTS_PER_TRACK then
                if checklength[tabcount[line]] == 0 then
                    set checklength[tabcount[line]] = StringLength(s)
                elseif checklength[tabcount[line]] == StringLength(s) then
                    set tab[line*TABLINES_PER_TRACK+tabcount[line]] = s
                    //in case input is first input, store the key of the tabline
                    if tabcount[line] == 0 then
                        set key[line] = TranslateKey(s)
                    endif
                    set tabcount[line] = tabcount[line] + 1
                else
                    call BJDebugMsg("ERROR: Length of track input does not match!")
                endif
            endif
        endif
    endmethod
    
    private method read takes integer pos returns nothing
    
    endmethod
endstruct

struct Song

    string name
    real bpm

    static method create takes string name, real bpm returns thistype
        local thistype this = thistype.allocate()
        set this.name = name
        set this.bpm = bpm
        return this
    endmethod
    
    method addTrack takes Track t returns nothing
    endmethod
    
    method play takes nothing returns nothing
    endmethod
    
    method stop takes nothing returns nothing
    endmethod
    
endstruct


endlibrary
 
Last edited:

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
Exactly what I was looking for. Nice.


EDIT:
it's slowly taking shape ...

EDIT2:
Done with the peripherals. Only thing left is the actual read method.

JASS:
library TabReader uses SoundTools, TimerUtils

globals
    //configurables
    constant integer PARTS_PER_TRACK = 4 //max number of strings per tabline, as strings are limited to 2000 characters
    constant integer TABLINES_PER_TRACK = 6 //max number of tablines per track
    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
    constant integer SOUNDS_PER_INSTRUMENT = 4 //max number of sound files that can be assigned to one instrument
    constant integer MIN_INTERVAL = 64 //the minimum note length value. 64 equals 1/64th note. The minimum note can not be punctured or played with triplets - setting it lower will increase performance
                                       //only put 16, 32 or 64 here.
    //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 integer MIN_BPM = 40
    private constant integer MAX_BPM = 180
    private constant integer PARTSTIMESLINES = PARTS_PER_TRACK*TABLINES_PER_TRACK
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 = 0
    private integer array note[SOUNDS_PER_INSTRUMENT]
    private Sound array s[SOUNDS_PER_INSTRUMENT]

    static method create takes string key, integer duration, string filepath returns thistype
        //duration in milliseconds, key like: "C#2"
        local thistype this = thistype.allocate()
        set s[0] = NewSound(filepath, duration, false, false)
        set note[0] = TranslateKey(key)
        return this
    endmethod
    
    method add takes string key, integer duration, string filepath returns nothing
        //allows to add up more sounds to your instrument
        //for pitching, the system will always try to find the closest registered note sound from those
        //Also, this allows to avoid the 4-of-the-same sound instance limit, i.e. to allow 6 string chords
        //or when using the instrument for playing chords on multiple tracks
        //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
        //duration in milliseconds, key like: "C#2"
        set count = count+1
        if count < SOUNDS_PER_INSTRUMENT then
            set s[count] = NewSound(filepath, duration, false, false)
            set note[count] = TranslateKey(key)
        endif
    endmethod
endstruct

struct Track
    integer volume
    
    private Instrument instrument
    
    private string array accent[PARTS_PER_TRACK]
    private string array length[PARTS_PER_TRACK]
    private string array tab[PARTSTIMESLINES]
    
    private integer array checklength[PARTS_PER_TRACK]
    private integer accentcount = 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 vol = 127
        elseif vol < 0 then
            set vol = 0
        else
            set volume = vol
        endif
        set instrument = inst
        return this
    endmethod
    
    method pushTabAccent takes string s returns nothing
        if accentcount < PARTS_PER_TRACK then
            if checklength[accentcount] == 0 then
                set checklength[accentcount] = StringLength(s)
            elseif checklength[accentcount] == StringLength(s) then
                set accent[accentcount] = s
                set accentcount = accentcount + 1
            else
                call BJDebugMsg("ERROR: Length of track input does not match!")
            endif
        endif
    endmethod
    
    method pushTabLength takes string s returns nothing
        if lengthcount < PARTS_PER_TRACK then
            if checklength[lengthcount] == 0 then
                set checklength[lengthcount] = StringLength(s)
            elseif checklength[lengthcount] == StringLength(s) then
                set length[lengthcount] = s
                set lengthcount = lengthcount + 1
            else
                call BJDebugMsg("ERROR: Length of track input does not match!")
            endif
        endif
    endmethod
    
    method pushTabNotes takes integer line, string s returns nothing
        if line < TABLINES_PER_TRACK then
            if tabcount[line] < PARTS_PER_TRACK then
                if checklength[tabcount[line]] == 0 then
                    set checklength[tabcount[line]] = StringLength(s)
                elseif checklength[tabcount[line]] == StringLength(s) then
                    set tab[line*TABLINES_PER_TRACK+tabcount[line]] = s
                    //in case input is first input, store the key of the tabline
                    if tabcount[line] == 0 then
                        set key[line] = TranslateKey(s)
                    endif
                    set tabcount[line] = tabcount[line] + 1
                else
                    call BJDebugMsg("ERROR: Length of track input does not match!")
                endif
            endif
        endif
    endmethod
    
    method read takes integer pos returns nothing
        
    endmethod
endstruct

struct Song

    private string name
    private real bpm
    
    private boolean playing
    private timer reader
    private integer array position[12]
    private integer trackcount
    private Track array t[TRACKS_PER_SONG]
    
    static method create takes string nam, real speed returns thistype
        local thistype this = thistype.allocate()
        set name = nam
        if speed < MIN_BPM then
            set bpm = MIN_BPM
        elseif speed > MAX_BPM then
            set bpm = MAX_BPM
        else
            set bpm = speed
        endif
        set trackcount = 0
        set reader = NewTimer()
        set playing = false
        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
    
    private static method periodic takes nothing returns nothing
        local thistype this = GetTimerData(GetExpiredTimer())
        local integer i = 0
        local integer j = 0
        loop
            exitwhen i > 11
            if position[i] > 0 then //only for players playing the song
                loop
                    exitwhen j >= trackcount
                    call t[j].read(position[i])
                    set j = j+1
                endloop
            endif
            set i = i+1
        endloop
    endmethod
    
    method play takes player for returns nothing
        set position[GetPlayerId(for)] = 3 //first chars are key information and bar lines, so we can cut them out
        if not playing then
            set playing = true
            call SetTimerData(reader, this)
            call TimerStart(reader, 1/((bpm/240.)*MIN_INTERVAL), true, function thistype.periodic)
        endif
    endmethod
    
    method stop takes player for returns nothing
        local integer i = 0
        set position[GetPlayerId(for)] = 0
        if playing then
            loop
                exitwhen i > 11
                if position[i] != 0 then
                    return
                endif
                set i = i + 1
            endloop
        endif
        //halt timer when nobody is listening
        set playing = false
        call PauseTimer(reader)
    endmethod
    
endstruct


endlibrary
 
Last edited:

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
When you're done with that, I can optimize it for you :D
Hah, go ahead. I didn't really care for efficiency yet, as I wanted to concentrate on actually writing the read algorythm, which is pretty complicated, as it must be highly flexible and failure proof to weird input.
Especially triplet notes cause a lot of headache, as I need to multiply the smallest stepsize by 3 because of that, which basicly kills the possibility to support 1/64th notes (180 bpm on 1/64th triplets = overkill timer intervals of less than 0.01s). But then again, who the fuck uses 1/64ths anyway? So I simply got rid of them... :>
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
I have an idea, to save on string space, you could convert -- to _
__ to &,
etc...

You could even write an application that converts the sheets of music data to the format that you would be reading :D
Well, the idea behind this system was, that you do not need to alter the ascii output of Guitar Pro at all and can simply put it into the script functions.

If I would alter the tab layout, it would require even more work from the user and I don't want that.
 
Status
Not open for further replies.
Top