Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
This is an unbelievably simple system I've made with basic vJASS (First vJASS spell!!) as I currently don't know advanced vJASS (structs and stuff) but it wasn't needed, as the system is waaaaay too simple, but I think useful. Might not be approved, as it is too simple, but for some Map-Makers that don't want to waste filesize on Music files they can attach the soundtrack to the map and let the player install it.
The is the External Music System. A system where you give it the .mp3 files in your Warcraft III folder (you give the system the path) and it plays the music for you one by one, I tried to make it as Configurable as possible, like you can choose all .mp3 prefixes, the choice whether the 0 before 1 should be there or not (like "Track-01" and "Track-1"), playing the music random, etc. The user can fully configure the system, and of course the .mp3 files!
Fully MUI and GUI friendly. Demo map includes whatever you need to know, and a way to use the system is under the Category "Demo related".
Requirements: JNGP
Local Files Enabled
Why JNGP?
Because this is coded in vJASS and also you need to have local files enabled. Tick it from the Grimoire extension of JNGP (thanks Emm-A-!).
Disclaimer:
Changing the .mp3 files in the middle of when system is ran is NOT supported.
Only works with .mp3 files. .wav file formats and others are basically avoided.
Remember, if anyone uses this and finds a problem with it, he has the Responsibility to report the problem here (using the debug messages).
Special thanks to Zwiebelchen for his helps.
MS Configuration
Events
Map initialization
Conditions
Actions
-------- ===== --------
-------- Configuration --------
-------- ===== --------
-------- The Timeout for the system. --------
-------- The real timeout will be calculated by how many times(MS_Timeout) checks will happen in a SINGLE SECOND. So the system now currently works every 0.5 seconds. --------
-------- The bigger the variable is, the more percise the system gets. But with bigger values than like 40 or 50 you'll have some minor problems on Battle.net. --------
Set MS_Timeout = 20.00
-------- When set to true, "Test-01" will be used as a sound and "Test-1" won't be. Vica Versa as well. --------
Set MS_Use0 = True
-------- The Variable would look for a known number of tracks, so it will loop until then for like 3 times. --------
-------- Set it to 0 so it would look for all available tracks. (recommended) --------
Set MS_ForLoop = 0
-------- When true, it may choose random tracks from the folder (the one played earlier might be played :p). --------
Set MS_PlayingIsRandom = False
-------- ===== --------
-------- End of Configuration --------
-------- ===== --------
Sound - Clear the music list
Sound - Stop music Immediately
MS How to use
Events
Conditions
Actions
-------- ---- --------
-------- Just note that for these functions to run you'll need to call StartSoundtrack at least once. --------
-------- ---- --------
-------- Use this code to start your soundtrack. Most important function. --------
-------- The first parameter is the folder path and the second parameter is the .mp3 file prefix. --------
-------- So it's like this: Warcraft3Folder\MyFolderPath\MyFilePrefix-01.mp3 --------
-------- Here's how to get the number of tracks a player has: --------
Set TempPlayer = Player 1 (Red)
Custom script: set udg_DemoTrackNumber = EMSGetTrackNum(udg_TempPlayer)
-------- Variable Demo_TrackNumber now has the value. --------
-------- ---- --------
-------- Listen carefully to this one. --------
-------- This will get the number of tracks the player has "locally". The integer won't cause desyncs (dont' worry) but the results may be different for each player. --------
Custom script: set udg_DemoTrackNumber = EMSGetTrackNumLocal()
-------- Variable Demo_TrackNumber now has the value. --------
-------- ---- --------
-------- Gets the current Track playing. It's local just like GetTrackNumLocal(). --------
Custom script: set udg_DemoTrackNumber = EMSGetCurrentTrack()
-------- Variable Demo_TrackNumber now has the value. --------
JASS:
library EMS /* v.1.5 */ initializer Init /*
******************************************************************************************
*
* EMS by Arad MNK
*
******************************************************************************************
*
* EMS
*
* EMS is the External Music System which is a GUI-Friendly system. You Can
* use this system both with JASS and GUI (other things are supported too!)
* You can control the music playing from other external folders with this system.
*
* Notes:
* - Changing the .mp3 files in the middle of when system is on is NOT supported.
* - Only works with .mp3 files. .wav file formats and others are basically avoided.
*
******************************************************************************************
*/
//! novjass
//* API
function StartSoundtrack takes string path, string filePrefix returns nothing
//* - Most important Function. Initializes the system with any GUI event, with the according folder and file prefix.
function EMSSetVolume takes player p, integer vol returns nothing
//* - Set a player's music volume to an integer.
function EMSPlayCustomTrack takes integer track returns boolean
//* - Play a custom track while the system is running.
function EMSGetTrackNum takes player p returns integer
//* - The number of tracks player p has.
function EMSGetTrackNumLocal takes nothing returns integer
//* - The number of tracks the player has. It's a local integer.
function EMSGetCurrentTrack takes nothing returns integer
//* - Current track running. Local integer.
//! endnovjass
/*
*
*
******************************************************************************************
*
* Credits
* Zwiebelchen - He helped me at some critical points. Also the main idea drives from him.
*
******************************************************************************************
*/
globals
private integer array TrackNum // Global how many tracks you have.
private integer TrackNumLocal // Local how many tracks you have.
private sound array Track // The sound handles of tracks.
private integer CurrTrackNum // Current track playing number, i.e MusicFolder\Test-CurrTrackNum.mp3
private boolean Allowed = true // Should the system run or it's not allowed?
private string SoundtrackFilePrefix = "NULL" // These two NULLs are used in
private string SoundtrackPath = "NULL" // the function Message.
private timer SystemTimer = CreateTimer()
private boolean FirstCall = false
endglobals
//********************************************//
//*************/Message Function/*************//
//********************************************//
private function Message takes string s returns nothing //Debug messages
debug call DisplayTextToPlayer(Player(0), 0, 0, "|c00FFFF00EMS:|r " + s + "\n|c00FF0000ID:|r " + SoundtrackPath +" " + SoundtrackFilePrefix)
endfunction
//*******************************//
//*************/API/*************//
//*******************************//
function EMSSetVolume takes player p, integer vol returns nothing
local integer i = 1
if FirstCall then
loop
exitwhen i == TrackNum[GetPlayerId(p)] + 1
if GetLocalPlayer() == p then
call SetSoundVolume(Track[i], vol)
endif
set i = i + 1
endloop
call Message("New Volume: " + I2S(vol))
else
call Message("System is not initialized for the first time yet...")
endif
endfunction
function EMSPlayCustomTrack takes integer track returns boolean //The boolean means that the track exists or not
local boolean b = track <= TrackNumLocal and FirstCall
if b then
set Allowed = false //Does not allow the core to detect the stopped sound and restart
call StopSound(Track[CurrTrackNum], false, false)
call Message("Custom Track Playing: " + I2S(track))
set CurrTrackNum = track - 1
call StartSound(Track[CurrTrackNum])
set Allowed = true
else
call Message("Invalid Track.")
endif
return b
endfunction
function EMSGetTrackNum takes player p returns integer
if FirstCall then
return TrackNum[GetPlayerId(p)]
else
call Message("System is not initialized for the first time yet...")
return 0
endif
endfunction
function EMSGetTrackNumLocal takes nothing returns integer
if FirstCall then
return TrackNumLocal
else
call Message("System is not initialized for the first time yet...")
return 0
endif
endfunction
function EMSGetCurrentTrack takes nothing returns integer
if FirstCall then
return CurrTrackNum
else
call Message("System is not initialized for the first time yet...")
return 0
endif
endfunction
//*********************************************************//
//*************/Functions used by the system./*************//
//*********************************************************//
private function CreateTrackLocation takes integer i returns string
local string s
local string smsg
if udg_MS_Use0 and i < 10 then
set s = "0"
else
set s = ""
endif
set smsg = SoundtrackPath + "\\" + SoundtrackFilePrefix + "-" + s + I2S(i) + ".mp3"
call Message("Track path generated: " + smsg)
set s = null
return smsg
endfunction
private function NumOfTracksLocal takes nothing returns integer
local integer i = 1
local integer i2 = 1
loop
exitwhen i != i2
if GetSoundFileDuration(CreateTrackLocation(i)) > 0 then
set i2 = i2 + 1
endif
set i = i + 1
endloop
return i2 - 1
endfunction
private function NumOfTracksLimitLocal takes integer limit returns integer
local integer i = 1
local integer i2 = 1
loop
exitwhen i == udg_MS_ForLoop or i != i2
if GetSoundFileDuration(CreateTrackLocation(i)) > 0 then
set i2 = i2 + 1
endif
set i = i + 1
endloop
return i2 - 1
endfunction
private function NumOfTracks takes player p returns integer
local integer i = 1
local integer i2 = 1
loop
exitwhen i != i2
if GetLocalPlayer() == p then
if GetSoundFileDuration(CreateTrackLocation(i)) > 0 then
set i2 = i2 + 1
endif
endif
set i = i + 1
endloop
return i2 - 1
endfunction
private function NumOfTracksLimit takes player p, integer limit returns integer
local integer i = 1
local integer i2 = 1
loop
exitwhen i == udg_MS_ForLoop or i != i2
if GetLocalPlayer() == p then
if GetSoundFileDuration(CreateTrackLocation(i)) > 0 then
set i2 = i2 + 1
endif
endif
set i = i + 1
endloop
return i2 - 1
endfunction
private function Core takes nothing returns nothing
if not(GetSoundIsLoading(Track[CurrTrackNum]) or GetSoundIsPlaying(Track[CurrTrackNum])) and Allowed then
call StopSound(Track[CurrTrackNum], false, false)
if udg_MS_PlayingIsRandom then
set CurrTrackNum = GetRandomInt(1, TrackNumLocal)
call Message("Random Track: " + I2S(CurrTrackNum))
else
if CurrTrackNum != TrackNumLocal + 1 then
set CurrTrackNum = CurrTrackNum + 1
elseif CurrTrackNum == TrackNumLocal + 1 then
set CurrTrackNum = 1 //start from beginning
endif
call Message("Next Track: " + I2S(CurrTrackNum))
endif
call StartSound(Track[CurrTrackNum])
endif
endfunction
private function Init takes nothing returns nothing
call Message("Initialized.\nDebug mode is on.")
endfunction
//****************************************************************************//
//*************/Most Important user function, starts the system./*************//
//****************************************************************************//
function StartSoundtrack takes string path, string filePrefix returns nothing
local integer i = 1
set Allowed = false
set SoundtrackPath = path
set SoundtrackFilePrefix = filePrefix
if FirstCall then
call Message("Not first call, Stopping music.")
call Message("Track stopped: " + I2S(CurrTrackNum))
call StopSound(Track[CurrTrackNum], false, false) //Stopping the music playing if called when a soundtrack is on
loop
exitwhen i == TrackNumLocal
call KillSoundWhenDone(Track[i])
call Message("Track destroyed. Track Number:" + I2S(i))
set i = i + 1
endloop
else
set FirstCall = true
call Message("First call.")
endif
set CurrTrackNum = 1
set i = 1
call Message("Starting soundtrack: " + path + "\\" + filePrefix)
if udg_MS_ForLoop == 0 then
set TrackNumLocal = NumOfTracksLocal()
elseif udg_MS_ForLoop > 0 then
set TrackNumLocal = NumOfTracksLimitLocal(udg_MS_ForLoop)
endif
loop
exitwhen i == TrackNumLocal
set Track[i] = CreateSound(CreateTrackLocation(i), false, false, false, 12700, 12700, "")
call Message("Track created. Track Number:" + I2S(i))
set i = i + 1
endloop
call StartSound(Track[1])
call TimerStart(SystemTimer, 1 / udg_MS_Timeout, true, function Core)
set Allowed = true
endfunction
endlibrary
Keywords:
Music, music, arad mnk, arad, mnk, m, n, k, external, system, external music, external music system, bleh, filesize, orpg, sound, mp3
16:57, 8th Feb 2016
Tank-Commander: A small system with a lot of use - great for saving map memory space and a good edition to the section. Code looks good from where I'm sat and the API fully explains the system. Well done.
Notes (for users)...
16:57, 8th Feb 2016 Tank-Commander: A small system with a lot of use - great for saving map memory space and a good edition to the section. Code looks good from where I'm sat and the API fully explains the system. Well done.
Notes (for users): using it does somewhat make dling from the maps section (with links to tracks for maps) rather necessary (dling a map through bnet won't give the appropriate track)
Actually, the main idea came from him
He told us that Warcraft III reads folders like .mpq archives and therefore finding a music in the .mpq archive is the same as finding a music in a custom folder!
EDIT: There's something wrong with the system. Some weird mistakes (thread crashes!!) by me. Don't review this while I fix them! Done. Ready.
EDIT 2: Another shameful update. Removed BJDebugMsg("hh")!!
Why not use a timer instead of call TriggerRegisterTimerEvent(EMS_Trig, (1 / udg_MS_Timeout), true). Read more about TimerStart
Why not use initializer so you don't have to force users to call InitEMS()?
Try adding a check in your loop for picking all players like PLAYER_SLOT_STATE_PLAYING and MAP_CONTROL_USER
You could store (1 / udg_MS_Timeout) to a variable to avoid re-calculating them every loop.
Why are you not indenting your local variables?
Feel free to use the and boolean operator instead of putting two if ... endif. You also might wanna use elseif for more readability so you don't have to indent again.
Do you really have to put a "by Arad MNK" in the name?
Why not use a timer instead of call TriggerRegisterTimerEvent(EMS_Trig, (1 / udg_MS_Timeout), true). Read more about TimerStart
Why not use initializer so you don't have to force users to call InitEMS()?
Try adding a check in your loop for picking all players like PLAYER_SLOT_STATE_PLAYING and MAP_CONTROL_USER
You could store (1 / udg_MS_Timeout) to a variable to avoid re-calculating them every loop.
Why are you not indenting your local variables?
Feel free to use the and boolean operator instead of putting two if ... endif. You also might wanna use elseif for more readability so you don't have to indent again.
Do you really have to put a "by Arad MNK" in the name?
Timers are going to be removed in the system, they stop when a player pauses the game, fucking the timing. I'll just use a periodic timer, and figure it out.
And I'm the master of TimerStart already, used it in my cinematic system for my Cinematic 1000 times.
This is going to be GUI friendly, so that the user would use it with any GUI event.
Well, I gotta check for all AVAILABLE people in all slots... meh. Will do it by forces.
OK. Will try. (In the new system it no longer exists 2 times)
Why should I? the readability is still fine.
If you're talking about the GetLocalPlayer() part then I'm not sure if it will still localize.
- I mean using TimerStart instead of creating a trigger with a periodic timer event.
- I don't see a disadvantage of using initializer. It prevents users from manually calling InitEMS.
- I mean just adding a condition in the beginning of the loop so it won't execute on empty slots or computer players.
- Ok if you're comfortable with that indention scheme. It was merely a suggestion because it's best to have uniform coding style. Read more here
- I think it will work fine with and though I'm not 100% sure.
I think /* */ is not a valid type for comment blocks in the standard editor. ( untested )
Only JPNG probably supports /*
The normal editor does only support //
Just saw the system in coded in vJass so the upper comment is pointless.
//just a linebreak without a use, readability improvement
Just a line without use to kill readability... Just leave a whitespace.
Comments should be informative or really great jokes.
That'll be easy. It'll be a check, and an update. Will be done once I implement the debug mode.
Hey, next update will include debug mode and more control over the music. The update after that will be the "Soundtracks" update. Using different musics for different situations (dungeons, villages and etc for example).
I get the debug messages spammed "Next Track: 1", "Next Track: 2", and no sound.
Also there exists a debug mode already, under JASSHelper settings. In code you then
would need to write debug in front of the command/operation.
JASS:
loop
call DisplayTimedTextToPlayer(Player(i), 0, 0, 60, s)
set i = i + 1
exitwhen i == 12
endloop
-> BJDebugMsg could be used instead, but actually why does it print messages to all players if a player changes sound for example?
The indentation is massed up at certain places. Always use 1x tab for indenting.
For example:
JASS:
private function Core takes nothing returns nothing
if not(GetSoundIsLoading(Track[CurrTrackNum]) or GetSoundIsPlaying(Track[CurrTrackNum])) and Allowed then
call StopSound(Track[CurrTrackNum], false, false)
if udg_MS_PlayingIsRandom then
set CurrTrackNum = GetRandomInt(1, TrackNumLocal)
call Message("Random Track: " + I2S(CurrTrackNum))
else
if CurrTrackNum != TrackNumLocal + 1 then
set CurrTrackNum = CurrTrackNum + 1
elseif CurrTrackNum == TrackNumLocal + 1 then
set CurrTrackNum = 1 //start from beginning
endif
call Message("Next Track: " + I2S(CurrTrackNum))
endif
call StartSound(Track[CurrTrackNum])
endif
endfunction
shoudl look like:
JASS:
private function Core takes nothing returns nothing
if not(GetSoundIsLoading(Track[CurrTrackNum]) or GetSoundIsPlaying(Track[CurrTrackNum])) and Allowed then
call StopSound(Track[CurrTrackNum], false, false)
if udg_MS_PlayingIsRandom then
set CurrTrackNum = GetRandomInt(1, TrackNumLocal)
call Message("Random Track: " + I2S(CurrTrackNum))
else
if CurrTrackNum != TrackNumLocal + 1 then
set CurrTrackNum = CurrTrackNum + 1
elseif CurrTrackNum == TrackNumLocal + 1 then
set CurrTrackNum = 1 //start from beginning
endif
call Message("Next Track: " + I2S(CurrTrackNum))
endif
call StartSound(Track[CurrTrackNum])
endif
endfunction
And a function's body should always be indented. Doesn't matter if it's declaring and initialisizing locals.
It should follow JPAG
Can you add some more documention for the functions, please?
I'm not very sure a function like "MuteAtInit" is really needed.
I mean the user has fully access to mute/unmute commands,
so if necessariy he can normaly take usage of them instead of
calling the wrapper that might be anyways only used once at all.
Not playing sounds: Check your sound options in Warcraft III options. When you see the messages, you MUST hear the music/sound. They are SOUNDS. Don't look for music mutation or etc.
Debug mode: Tell me moar please! Never heard of that ;p Adding a debug before the function, the call, the what?
Indentation: I'll try to follow JPAG. Will be done as soon as possible.
great job on your system. This is excellent for saving filesize. It would be great, if you uploaded a spin-off snippet, which would just enable someone to load a sound handle from a specified folder without playing it, so we could use this sound in another sound system
struct test
method loadSoundFromFolder takes string path returns sound
return CreateSound(path, false, false, false, 12700, 12700, "")
endmethod
//Example:
private static method onInit takes nothing returns nothing
call StartSound(.loadSoundFromFolder("Music\\MahTest.mp3"))
endmethod
endstruct
//First time using structs! Not sure that I'm using it correctly in onInit :p
Save it to a variable and done. Use it. Just note it may be different for different players (handles are different) that's why this is a system, the main point is to control the localizations and not to cause desyncs. Else the whole thing is the code forementioned.
struct test
method loadSoundFromFolder takes string path returns sound
return CreateSound(path, false, false, false, 12700, 12700, "")
endmethod
//Example:
private static method onInit takes nothing returns nothing
call StartSound(.loadSoundFromFolder("Music\\MahTest.mp3"))
endmethod
endstruct
//First time using structs! Not sure that I'm using it correctly in onInit :p
Save it to a variable and done. Use it. Just note it may be different for different players (handles are different) that's why this is a system, the main point is to control the localizations and not to cause desyncs. Else the whole thing is the code forementioned.
thanks a lot for posting this This is so great, because I use a lot of custom music, and I didn't know, the possibility of outsourcing was there.
This actually brought me to think about directly creating the sound from folders into variables for each player in that map I am talking about.
It uses a custom music system with timers for each player, and anyway sound is only played locally, so it won't desync, right? I mean of course, every player would have to save the music folder and subfolders at the specified location ...
Anyway, your system looks great!
(sry I dont want to spam, but I thought, this might be interresting for others as well)
Oh, if you don't want to change tracks you can use this to work with musics not sounds:
JASS:
function PlayExMusic takes string path, player forPlayer returns nothing
if GetLocalPlayer() == forPlayer
if GetSoundFileDuration(path) >= 0
//music exists
call StopMusic(false) //Stops the currently playing music
call PlayMusic(path)
endif
endif
endfunction
No desyncs, all safe. Changing tracks is a bit more complicated and requires timers when using musics, but with sounds there is no need for timers (system base). Timers are stopped when the player pauses the game.
Using this system allows you to change tracks safely using sounds with a lot of configurabilities. No need for your own system (I mean I've already created it).
If you want to change between tracks, use this system. Else, use the easy snippet up here. This snippet is used for variabling external .mp3 files.
Oh, if you don't want to change tracks you can use this to work with musics not sounds:
JASS:
function PlayExMusic takes string path, player forPlayer returns nothing
if GetLocalPlayer() == forPlayer
if GetSoundFileDuration(path) >= 0
//music exists
call StopMusic(false) //Stops the currently playing music
call PlayMusic(path)
endif
endif
endfunction
No desyncs, all safe. Changing tracks is a bit more complicated and requires timers when using musics, but with sounds there is no need for timers (system base). Timers are stopped when the player pauses the game.
Using this system allows you to change tracks safely using sounds with a lot of configurabilities. No need for your own system (I mean I've already created it).
If you want to change between tracks, use this system. Else, use the easy snippet up here. This snippet is used for variabling external .mp3 files.
so to clarify this, in my map I am using a highly artificial mp3 sound system with timers for each player (sry its not music, I am also using mp3 sounds), so that music will be looping. It has to react very individually to all kinds of situations (seasons, events, dayNight etc.) and play local sound for each player according to its individual situation.
I didn't see an easy way to implement your system in exchange for that.
If I used your system and wanted to play a certain track at a specific situation, I could find it via the track number of the system, right? E.g. test-01.mp3 would be that specific sound I needed so I could find it in the system via its track number 1 and so on? Also, I didnt see, whether your system checks, if a requested sound is already played (which is very important for me that it is able to).
Also I would have to adapt your system anyway, so I am currently thinking of a good way or if I will be able to at all (of course I will credit you even if I will only use your idea (and Zwiebelchen as well) )
I'm still trying to figure out keywords for the API... Else I woulda upload it today
The demo will include what you want.
Soundtrack update will be released either tomorrow or the day after (EMS v1.5).
When released, with different events you can use:
JASS:
//register the event you want, GUI friendly as well
call StartSoundtrack("myFolder", "myFilePrefix") //stops the music, changes the folder with the .mp3 files, and starts playing
The demo event will be a peseant that with moving it you'd change track folders (specific songs you need).
EDIT: I had some problems with thread crashes. It'll be released tomorrow, if my time allows that.
EDIT 2: BRUH. Thread crash problem fixed, now when starting another soundtrack the other soundtrack plays the next track, making 2 tracks play at once. (You'll feel like a DJ)
EDIT 3: FINALLY! Managed to find the issue. Preparing to upload.
Unfortunately the system causes havy lag every time I enter one of these regions with the worker in your demo map ... Other than that it is really cool, thanks for creating this system. If I find some time, I will think about this more
Note that the sounds can't be preloaded as they are created at call. This might be the reason for your lag. Maybe I'll add a new function to be able to preload sounds.
Note that the sounds can't be preloaded as they are created at call. This might be the reason for your lag. Maybe I'll add a new function to be able to preload sounds.
I think this would be important .. I dont really get why my computer has problems with that as it runs wither 3 on max details but lags with that create native here. Might be some stupid special problem and not generally related to your function ..
Hey by testing the CreateSound native like in your system, I just figured out, that you have to enable Warcraft to be able to use Local Files. I didnt read this here or I didnt see it, and as I have 2 computers with 2 editors I have been searching for almost 2 days now ...
So I suggest you add this important tip to your description instruction using regedit.exe (or simply tick the "enable local files" in grimoire extensions of JNGP.
@Lag: this laggy problem dissappeared when I changed some thing in my system, so besides a little first lag, there is no problem anymore
This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
By continuing to use this site, you are consenting to our use of cookies.