This script allows me to create macros or place a token at specific points on a map that allow me to trigger a sound effect or start a background track or playlist without having to navigate to or scroll through the jukebox. It allows me to distinguish between a sound effect that should layer with the other sounds, and a background track that should stop other background tracks when played. For example, let's assume I have three sounds named "amb-combat", "amb-noncombat", and "effect-rain". The track "amb-noncombat" is started on a loop. Later, it begins to rain so I start the track "effect-rain" on a loop as well. Both the rain and the noncombat tracks play simultaneously. When combat starts, the track "amb-combat" is looped as unique. Because the track is flagged as unique, any other track with the same prefix (in this case "amb") will be stopped; all other tracks are unaffected. So when track "amb-combat" is started, the track "amb-noncombat" is stopped, and the track "effect-rain" continues to play. When combat is over, the track "amb-noncombat" is looped as unique, which again stops all other tracks with the same prefix ("amb"). I can also associate the commands with tokens, and use the token attributes as the parameters (see below). I can now place tokens on my map at specific points of interest that allow me to quickly play a track or change the mood. You can start and stop a playlist with this script; however, the playlist will adhere to the volume and repeat settings as they are set within the jukebox. Command !sfx Parameters Parameters are defined as a keyword and a value separated by a colon. The order of parameters is not important. Parameters are not case sensitive, with the exception of the track name. action - (play, loop, stop) defines whether to play a track, loop a track, or stop a track from playing. unique - (true, false) defines if other tracks currently playing that have a matching prefix should be stopped. song - (text, case-sensitive) the track name exactly as it appears in the Jukebox list - (text, case-sensitive) the playlist name exactly as it appears in the Jukebox volume - (integer 0-100) the volume of the track You cannot use the song and list parameter in the same command. Examples !sfx action:loop unique:true song:Combat Music volume:30 !sfx volume:75 song:Screaming action:play !sfx song:Fast Heartbeat action:stop For playlists, only the action and list parameters are used. !sfx list:Tavern Scene action:play !sfx action:stop list:Dungeon Crawl Character Sheet Abilities I created a new character entry, and use the following two ability macros. In my setup, the name of the token is the name of the track. "Bar1" is the action parameter and is set to either "play' or 'loop'. "Bar2" is the unique parameter, and is set to either "true" or "false". "Bar3" is the volume the track should play. PLAY: !sfx song:@{selected|token_name} action:@{selected|bar1} unique:@{selected|bar2} volume:@{selected|bar3} STOP: !sfx song:@{selected|token_name} action:stop Customize There are a few lines near the top of the code that can be easily changed. SfxCtrl.Command - this is the command that calls the script, and should be listed in code as all caps. SfxCtrl.OptionDelimiter - this is a single character that separates the parameter keys from their values. SfxCtrl.PrefixDelimiter - this is a single character that separates a track prefix from the rest of the track name. SfxCtrl.SendAs - the name displayed when the script whispers a response. SfxCtrl.VolumeBar - the field to monitor for dynamic volume changes. SfxCtrl.AllowPlayerControl - this determines if players can give commands Commands as Players When AllowPlayerControl is set to true, non-GM players can use the !SFX command. This will allow your players to use the !SFX command within their own macros. However, there are some limitations. When the !SFX command is given by anyone other than the GM, that player: cannot start or stop a play list. cannot loop a track; any command of action:loop given by a player will be interpreted as action:play instead. cannot stop another track that was started by another player. Code state.SfxControl = state.SfxControl || {};
var SfxCtrl = {};
//customize options
SfxCtrl.Command = '!SFX';
SfxCtrl.OptionDelimiter = ':';
SfxCtrl.PrefixDelimiter = '-';
SfxCtrl.SendAs = 'SFX';
SfxCtrl.VolumeBar = 'bar3_value';
SfxCtrl.AllowPlayerControl = true;
//do not edit below this line
on('chat:message', function(msg) {
//validate command
var param = msg.content.split(' ');
var isGM = playerIsGM(msg.playerid);
if(msg.type != 'api' || param[0].toLowerCase() != SfxCtrl.Command.toLowerCase() || (!isGM && !SfxCtrl.AllowPlayerControl)) return;
var replyTo = '/w "' + msg.who + '" ';
//can't have song: and list: parameters in the same command
var ucaseContent = msg.content.toUpperCase();
if(ucaseContent.indexOf(' SONG:') !== -1 && ucaseContent.indexOf(' LIST:') !== -1) {
sendChat(SfxCtrl.SendAs, replyTo + 'Error: SONG and LIST are exclusive options.');
return;
}
//set default options
var play = true;
var unique = false;
var loop = false;
var prefix = '';
var song = '';
var list = '';
var lasttype = '';
var vol = null;
//define lookup tables
function ReadParam(key) {
params = {
'ACTION': function ReadAction(key) {
actions = {
'PLAY': function() { play=true; loop=false; },
'LOOP': function() { play=true; loop=true; },
'STOP': function() { play=false; loop=false; },
'DEFAULT': function() { errmsg = 'Unknown action: ' + key; },
};
(actions[key.toUpperCase()] || actions['DEFAULT'])();
},
'UNIQUE': function(val) { unique = (val.toLowerCase() === 'true'); },
'SONG': function(val) { song = val; lasttype = 'SONG'; },
'LIST': function(val) { list = val; lasttype = 'LIST'; },
'VOLUME': function(val) { if (!isNaN(val)) { vol = parseInt(Math.max(0,Math.min(100,val))); } },
'DEFAULT': function() { errmsg = 'Unknown parameter: ' + key; },
};
return (params[key.toUpperCase()] || params['DEFAULT']);
}
//loop through each parameter
for (i=1; i<param.length; i++) {
var errmsg;
//split each parameter by the defined delimiter
var opt = param[i].split(SfxCtrl.OptionDelimiter);
switch (opt.length) {
case 1:
//if no delimiter was found, append the parameter to the song or list title
if (lasttype === 'LIST') {
list += ' ' + opt[0];
} else {
song += ' ' + opt[0];
}
break;
case 2:
//if one delimiter was found, filter through lookup tables
ReadParam(opt[0])(opt[1]);
break;
default:
//if more than one delimiter was found, set the error message
errmsg = 'Too many delimiters in parameter: ' + param[i];
break;
}
//if an error msg was set, whisper and return
if (errmsg) {
sendChat(SfxCtrl.SendAs, replyTo + errmsg);
return;
}
}
//switch based on the the title entered
switch(lasttype) {
case 'LIST':
if (!isGM) return;
var ListID;
var folders = JSON.parse(Campaign().get('_jukeboxfolder'));
for (p in folders) {
if(folders[p]['n'] && folders[p]['n'] === list) { ListID = folders[p]['id']; }
}
if (ListID) {
if (play) { playJukeboxPlaylist(ListID); } else { stopJukeboxPlaylist(); }
} else {
sendChat(SfxCtrl.SendAs, replyTo + 'Playlist Not Found: ' + list);
}
return;
break;
default:
if (song == '' || !findObjs({ _type: 'jukeboxtrack', title: song })[0]) {
sendChat(SfxCtrl.SendAs, replyTo + 'Song Not Found: ' + song);
return;
};
//create list of all campaign tracks
var allsongs = findObjs({
_type: 'jukeboxtrack',
});
//if unique, get the song prefix by delimiter as defined
if (unique) {
prefix = song.split(SfxCtrl.PrefixDelimiter);
if (prefix.length == 1) {
prefix = '';
} else {
prefix = prefix[0];
}
}
//loop through each track in campaign jukebox
_.each(allsongs, function(track) {
//get the ID of the last person who played the song, if it's still playing
var isPlaying = track.get('playing') && (!track.get('softstop') || track.get('loop'));
var playedbyid = isPlaying ? (state.SfxControl[track.get('_id')] || '') : '';
//turn off an active track if unique and the prefix matches
if (unique && play && track.get('title') != song && track.get('title').split(SfxCtrl.PrefixDelimiter)[0] == prefix && track.get('playing') === true && (isGM || playedbyid === msg.playerid )) {
track.set({
playing: false,
softstop: false,
});
delete state.SfxControl[track.get('_id')];
}
//if the song titles match, apply the settings
if (track.get('title') == song) {
if (isGM || !isPlaying || (isPlaying && playedbyid === msg.playerid)) {
track.set({
'playing' : play,
'softstop' : false,
'loop' : (isGM) ? loop : false,
'volume' : !isNaN(vol) ? vol : track.get('volume'),
});
if (play) {
state.SfxControl[track.get('_id')] = msg.playerid;
} else {
delete state.SfxControl[track.get('_id')];
}
}
}
});
break;
}
});
on('change:graphic', function(obj, old) {
if(obj.get(SfxCtrl.VolumeBar) !== old[SfxCtrl.VolumeBar] && !isNaN(obj.get(SfxCtrl.VolumeBar))) {
var song = findObjs({
_type: 'jukeboxtrack',
title: obj.get('name'),
playing: true,
})[0];
if (song) {
song.set({
volume: parseInt(Math.min(Math.max(0,obj.get(SfxCtrl.VolumeBar)),100)),
});
}
}
});
on('ready', function() {
log('Script loaded: Music Control for Tokens');
});