
Sound Manager API v1.2
Updated: Aug 25 2025 Sound Manager API – How to Use The Sound Manager API lets you manage sound effects in your Roll20 game by assigning sounds from your Roll20 jukebox to three categories:
Damage (dmg)
Heal (heal)
Death (death)
The API plays a random sound from the appropriate category when a token’s hit points change. It also stores your sound configuration persistently in a handout so your settings are preserved across API restarts. Installation
Copy the API Code:
Open your Roll20 game, go to the API Scripts section, create a new script, and paste the full API code into the editor. Save the script.
Persistent Storage:
On startup, the API will look for (or create) a handout named "SoundManagerConfig" . This handout stores your sound configuration (sound names and their categories) and the selected bar in JSON format.
Note: Do not delete or modify this handout unless you want to reset your configuration.
Using the API Main Menu Command: !sound-menu
Type this command (as GM) to display the main menu. The menu includes:
List Configured Sounds
Shows all sounds you have added along with options to preview, change their category, or remove them.
Browse Jukebox
Lists all available jukebox tracks so you can add them.
Add Sound Manually
Allows you to type in the exact name of a sound to add it manually.
Change Bar
Opens a menu where you can choose which token bar (bar1, bar2, or bar3) is used for triggering sound effects.
Bar Toggle By default, the API monitors bar1 . To change this:
Open the Main Menu and click Change Bar .
In the Change Bar menu, click one of the buttons for Bar 1 , Bar 2 , or Bar 3 .
The selected bar is stored persistently, and token HP triggers will now use the chosen bar.
Adding Sounds Manually Adding a Sound:
Command: !set-sound <Sound Name>
Example:
!set-sound Thunder Claps
This adds "Thunder Claps" to your configuration with the default category Damage (dmg).
Adding from the Jukebox:
Command: !jukebox-sounds
This displays a list of all available jukebox tracks.
Next to each track, there is an [Add] link.
Clicking that link adds the track to your configuration (with the default category Damage).
Listing & Managing Sounds Listing Configured Sounds:
Command: !list-sounds
This displays a list of all configured sounds. For each sound, you will see:
Sound Name: Clickable (as a button) to preview/play the sound.
Current Category: Shown next to the name.
Category Change Buttons: Three buttons labeled Damage , Heal , and Death to change the category.
Remove Button: A Remove link to delete the sound.
Navigation: At the bottom of the list, you will see navigation buttons: Main , Jukebox , and Refresh (to reload the list).
Changing a Sound’s Category Directly:
Command: !set-sound-cat <Sound Name> <Category>
Valid categories are:
dmg (Damage)
heal (Heal)
death (Death)
Example:
!set-sound-cat Thunder Claps heal
This sets "Thunder Claps" to the Heal category.
Removing a Sound:
Command: !remove-sound <Sound Name>
Example:
!remove-sound Thunder Claps
Previewing a Sound:
Command: !play-sound <Sound Name>
Example:
!play-sound Thunder Claps
This plays the specified sound for preview.
Jukebox Browser Browsing the Jukebox:
Command: !jukebox-sounds
This command displays a list of all available jukebox tracks. For each track:
The track name is rendered as a button that plays the sound directly.
Next to each track is an [Add] link to add the track to your configuration (default category Damage).
Navigation buttons at the bottom let you go to the Main Menu, view the Configured Sounds list, or refresh the list.
Token HP Triggers The API automatically monitors changes to tokens’ HP using the value in the configured bar. Depending on how the HP changes:
Taking Damage:
If HP falls to 0 or below, a random Death sound is played.
If HP decreases (but stays above 0), a random Damage sound is played.
Healing:
If HP increases, a random Heal sound is played.
These triggers work in tandem with damage application commands (like !apply-damage or other systems that modify token HP). Tips & Reminders
Persistent Configuration:
Your configuration is saved in the handout "SoundManagerConfig" . Do not delete this handout unless you wish to reset your settings.
Case Sensitivity:
When managing sounds, the API now performs case-insensitive matching.
Testing:
Use the preview command ( !play-sound ) to test sounds before relying on them in gameplay.
Summary of Commands
Main Menu: !sound-menu
Add Sound Manually: !set-sound <Sound Name>
List Configured Sounds: !list-sounds
Change Sound Category:
Either by clicking the buttons in the list or using !set-sound-cat <Sound Name> <dmg/heal/death>
Remove Sound: !remove-sound <Sound Name>
Preview Sound: !play-sound <Sound Name>
Browse Jukebox: !jukebox-sounds
Add from Jukebox: Click the [Add] link next to a track
Change Bar:
Access via the Main Menu → Change Bar ; then select Bar 1 , Bar 2 , or Bar 3 using !set-bar <number>
/*
Sound Manager API v1.2 (Revised – ApplyDamage observer + no nag message)
Updated: Feb 24 2025
Author: Surok
Changes vs v1.1:
- Hooks directly into ApplyDamage via registerObserver('change', ...) so sounds play on real damage events,
even when no tokens are selected (e.g., Condef/GroupCheck/ApplyDamage pipeline).
- Removed the "Additional Damage Application Listener" that required selected tokens and produced the
"Please select a token to apply damage." message.
- Added a retry loop to attach the ApplyDamage observer if it isn’t ready yet when this script boots.
*/
on('ready', () => {
const configHandoutName = "SoundManagerConfig";
// Normalize sound names: trim, lowercase, and remove any trailing category word.
const normalizeSoundName = (name) => {
let tokens = name.trim().toLowerCase().split(/\s+/);
if(tokens.length > 1 && ["dmg", "heal", "death"].includes(tokens[tokens.length-1])){
tokens.pop();
}
return tokens.join(" ");
};
// ----------------------------
// Configuration Persistence
// ----------------------------
// state.configuredSounds will be an array of objects: { norm, display, category }
const loadConfig = () => {
let handout = findObjs({ type: 'handout', name: configHandoutName })[0];
if(handout){
handout.get('notes', function(notes){
if(!notes || notes.trim() === ""){
state.configuredSounds = [];
state.configuredBar = "bar1";
log("Sound Manager: handout empty; resetting configuration.");
return;
}
try{
let config = JSON.parse(notes);
if(typeof config === "object" && Array.isArray(config.sounds)){
state.configuredSounds = config.sounds;
state.configuredBar = config.bar || "bar1";
} else {
state.configuredSounds = [];
state.configuredBar = "bar1";
}
log("Sound Manager: loaded config.");
} catch(e){
state.configuredSounds = [];
state.configuredBar = "bar1";
log("Sound Manager: error parsing config; using defaults.");
}
});
} else {
state.configuredSounds = [];
state.configuredBar = "bar1";
createObj('handout', { name: configHandoutName, notes: JSON.stringify({sounds: state.configuredSounds, bar: state.configuredBar}) });
log("Sound Manager: no config handout found. Created one with default bar1.");
}
};
const updateConfig = () => {
let config = {
sounds: state.configuredSounds,
bar: state.configuredBar || "bar1"
};
let handout = findObjs({ type: 'handout', name: configHandoutName })[0];
if(handout){
handout.set('notes', JSON.stringify(config));
} else {
createObj('handout', { name: configHandoutName, notes: JSON.stringify(config) });
}
};
// Optional reset command.
on('chat:message', (msg) => {
if(msg.type === 'api' && msg.content === '!reset-sound-config'){
state.configuredSounds = [];
state.configuredBar = "bar1";
updateConfig();
sendChat('Sound Manager', `/w gm Configuration reset.`);
}
});
const ensureConfig = () => {
let handout = findObjs({ type: 'handout', name: configHandoutName })[0];
if(!handout){
state.configuredSounds = state.configuredSounds || [];
state.configuredBar = state.configuredBar || "bar1";
}
};
loadConfig();
// ----------------------------
// Utility Functions
// ----------------------------
const playSound = (trackname, action) => {
let track = findObjs({ type: 'jukeboxtrack', title: trackname })[0];
if(track){
track.set('playing', false);
track.set('softstop', false);
if(action === 'play'){
track.set('playing', true);
}
} else {
sendChat('Sound Manager', `/w gm No track found: ${trackname}`);
log("Sound Manager: No track found: " + trackname);
}
};
const playRandomSoundFromCategory = (category) => {
let matches = (state.configuredSounds || []).filter(entry => entry.category === category);
if(matches.length === 0){
// stay quiet if nothing configured
return;
}
let randomEntry = matches[Math.floor(Math.random() * matches.length)];
playSound(randomEntry.display, 'play');
};
// ----------------------------
// UI Styling Variables (AURA-Inspired)
// ----------------------------
const containerStyle = "border:2px solid black; border-radius:4px; box-shadow:1px 1px 1px #707070; text-align:center; padding:3px 0; margin:0 auto; color:#000; background:-webkit-linear-gradient(-45deg, #a7c7dc 0%, #85b2d3 100%);";
const menuButtonStyle = 'display:block; text-decoration:none; color:white; background-color:#6FAEC7; padding:5px; border-radius:4px; box-shadow:1px 1px 1px #707070; margin-bottom:5px;';
const listStyle = 'list-style:none; padding:0; margin:0; text-align:center;';
const jukeboxListStyle = 'list-style:none; padding:0; margin:0; text-align:left;';
const headerStyle = 'text-align:center; margin:0 0 10px;';
const smallButtonStyle = 'padding-top:1px; text-align:center; font-size:9pt; width:48px; height:14px; border:1px solid black; margin:1px; border-radius:4px; box-shadow:1px 1px 1px #707070; color:white; text-decoration:none; display:inline-block;';
const playButtonStyle = 'padding:2px 4px; text-align:center; font-size:9pt; border:1px solid black; margin:1px; border-radius:4px; box-shadow:1px 1px 1px #707070; color:white; text-decoration:none; display:inline-block; background-color:#6FAEC7;';
const damageButtonStyle = smallButtonStyle + ' background-color:#B22222;';
const healButtonStyle = smallButtonStyle + ' background-color:#32CD32;';
const deathButtonStyle = smallButtonStyle + ' background-color:#000000;';
const navForSounds = '<div style="text-align:center; margin-top:10px;">'
+ '<a href="!sound-menu" style="' + smallButtonStyle + '">Main</a>'
+ '<a href="!jukebox-sounds" style="' + smallButtonStyle + '">Jukebox</a>'
+ '<a href="!list-sounds" style="' + smallButtonStyle + '">Refresh</a>'
+ '</div>';
const navForJukebox = '<div style="text-align:center; margin-top:10px;">'
+ '<a href="!sound-menu" style="' + smallButtonStyle + '">Main</a>'
+ '<a href="!list-sounds" style="' + smallButtonStyle + '">Sounds</a>'
+ '<a href="!jukebox-sounds" style="' + smallButtonStyle + '">Refresh</a>'
+ '</div>';
const navForChangeBar = '<div style="text-align:center; margin-top:10px;">'
+ '<a href="!sound-menu" style="' + smallButtonStyle + '">Main</a>'
+ '</div>';
// ----------------------------
// Main Menu (Whispered to GM)
// ----------------------------
on('chat:message', (msg) => {
ensureConfig();
if(msg.type !== 'api') return;
if(msg.content === '!sound-menu'){
let menu = `<div style="${containerStyle}">`;
menu += `<h3 style="${headerStyle}">Sound Manager Menu</h3>`;
menu += `<ul style="${listStyle}">`;
menu += `<li><a href="!list-sounds" style="${menuButtonStyle}">List Configured Sounds</a></li>`;
menu += `<li><a href="!jukebox-sounds" style="${menuButtonStyle}">Browse Jukebox</a></li>`;
menu += `<li><a href="!set-sound ?{Sound Name}" style="${menuButtonStyle}">Add Sound Manually</a></li>`;
menu += `<li><a href="!change-bar-menu" style="${menuButtonStyle}">Change Bar</a></li>`;
menu += `</ul>`;
menu += `<p style="font-style:italic; margin-top:10px;">New sounds default to Damage.<br>Monitoring: ${state.configuredBar || 'bar1'}</p>`;
menu += `</div>`;
sendChat('Sound Menu', `/w gm ${menu}`);
}
});
// ----------------------------
// Change Bar Menu (Whispered to GM)
// ----------------------------
on('chat:message', (msg) => {
ensureConfig();
if(msg.type !== 'api') return;
if(msg.content === '!change-bar-menu'){
let output = `<div style="${containerStyle}">`;
output += `<h3 style="${headerStyle}">Change Bar</h3>`;
output += `<p>Select which bar to monitor:</p>`;
output += `<a href="!set-bar 1" style="${smallButtonStyle}">Bar 1</a> `;
output += `<a href="!set-bar 2" style="${smallButtonStyle}">Bar 2</a> `;
output += `<a href="!set-bar 3" style="${smallButtonStyle}">Bar 3</a>`;
output += `<br>` + navForChangeBar;
output += `</div>`;
sendChat('Change Bar', `/w gm ${output}`);
}
});
// ----------------------------
// Command to Set Bar: !set-bar <number>
// ----------------------------
on('chat:message', (msg) => {
ensureConfig();
if(msg.type !== 'api') return;
if(msg.content.indexOf('!set-bar') === 0){
let args = msg.content.split(/\s+/);
if(args.length < 2){
sendChat('Sound Manager', `/w gm Usage: !set-bar <1|2|3>`);
return;
}
let barNum = args[1].trim();
if(!["1","2","3"].includes(barNum)){
sendChat('Sound Manager', `/w gm Invalid bar. Choose 1, 2, or 3.`);
return;
}
state.configuredBar = "bar" + barNum;
updateConfig();
sendChat('Sound Manager', `/w gm Now monitoring ${state.configuredBar}.`);
}
});
// ----------------------------
// Manual Add/Update Command: !set-sound
// ----------------------------
on('chat:message', (msg) => {
ensureConfig();
if(msg.type !== 'api') return;
if(msg.content.indexOf('!set-sound') === 0){
let args = msg.content.split(/\s+/);
if(args.length < 2){
sendChat('Sound Manager', `/w gm Usage: !set-sound <Sound Name>`);
return;
}
let soundName = args.slice(1).join(" ").trim();
let norm = normalizeSoundName(soundName);
let exists = (state.configuredSounds || []).find(entry => entry.norm === norm);
if(!exists){
state.configuredSounds.push({ norm: norm, display: soundName, category: "dmg" });
updateConfig();
sendChat('Sound Manager', `/w gm Sound "<b>${soundName}</b>" added with default category Damage.`);
} else {
sendChat('Sound Manager', `/w gm Sound "<b>${exists.display}</b>" is already configured.`);
}
}
});
// ----------------------------
// Remove Command: !remove-sound
// ----------------------------
on('chat:message', (msg) => {
ensureConfig();
if(msg.type !== 'api') return;
if(msg.content.indexOf('!remove-sound') === 0){
let target = msg.content.replace('!remove-sound', '').trim().toLowerCase();
let origLength = (state.configuredSounds || []).length;
state.configuredSounds = (state.configuredSounds || []).filter(entry => entry.norm !== target);
if((state.configuredSounds || []).length < origLength){
updateConfig();
sendChat('Sound Manager', `/w gm Sound "<b>${target}</b>" removed.`);
} else {
sendChat('Sound Manager', `/w gm Sound "<b>${target}</b>" is not configured.`);
}
}
});
// ----------------------------
// Change Sound Category Command: !set-sound-cat
// ----------------------------
on('chat:message', (msg) => {
ensureConfig();
if(msg.type !== 'api') return;
if(msg.content.indexOf('!set-sound-cat') === 0){
let parts = msg.content.split(/\s+/);
if(parts.length < 3){
sendChat('Sound Manager', `/w gm Usage: !set-sound-cat <Sound Name> <Category (dmg/heal/death)>`);
return;
}
let category = parts[parts.length - 1].toLowerCase();
let soundName = parts.slice(1, parts.length - 1).join(" ").trim();
let norm = normalizeSoundName(soundName);
if(!["dmg","heal","death"].includes(category)){
sendChat('Sound Manager', `/w gm Invalid category. Use dmg, heal, or death.`);
return;
}
let updated = false;
state.configuredSounds.forEach(entry => {
if(entry.norm === norm){
entry.category = category;
updated = true;
}
});
if(updated){
updateConfig();
sendChat('Sound Manager', `/w gm Sound "<b>${soundName}</b>" updated to category ${category}.`);
} else {
sendChat('Sound Manager', `/w gm Sound "<b>${soundName}</b>" is not configured.`);
}
}
});
// ----------------------------
// Preview Command: !play-sound (for configured sounds)
// ----------------------------
on('chat:message', (msg) => {
ensureConfig();
if(msg.type !== 'api') return;
if(msg.content.indexOf('!play-sound') === 0){
let target = msg.content.replace('!play-sound', '').trim().toLowerCase();
let found = (state.configuredSounds || []).find(entry => entry.norm === target);
if(found){
playSound(found.display, 'play');
sendChat('Sound Manager', `/w gm Previewing sound: <b>${found.display}</b>`);
} else {
sendChat('Sound Manager', `/w gm Sound "<b>${target}</b>" is not configured.`);
}
}
});
// ----------------------------
// Configured Sounds Menu (Navigation)
// ----------------------------
on('chat:message', (msg) => {
ensureConfig();
if(msg.type !== 'api') return;
if(msg.content === '!list-sounds'){
let output = `<div style="${containerStyle}">`;
output += `<h3 style="${headerStyle}">Configured Sounds</h3>`;
output += `<ul style="${listStyle}">`;
(state.configuredSounds || []).forEach(entry => {
output += `<li style="margin-bottom:5px;">`;
output += `<a href="!play-sound ${entry.display}" style="${playButtonStyle}">${entry.display}</a> (Category: ${entry.category})<br>`;
output += `<a href="!set-sound-cat ${entry.display} dmg" style="${damageButtonStyle}">Damage</a> `;
output += `<a href="!set-sound-cat ${entry.display} heal" style="${healButtonStyle}">Heal</a> `;
output += `<a href="!set-sound-cat ${entry.display} death" style="${deathButtonStyle}">Death</a> `;
output += `<a href="!remove-sound ${entry.display}" style="${smallButtonStyle}">Remove</a>`;
output += `</li>`;
});
output += `</ul>`;
output += navForSounds;
output += `</div>`;
sendChat('Sound Manager', `/w gm ${output}`);
}
});
// ----------------------------
// Jukebox Browser Menu
// ----------------------------
on('chat:message', (msg) => {
ensureConfig();
if(msg.type !== 'api') return;
if(msg.content === '!jukebox-sounds'){
let tracks = findObjs({ type: 'jukeboxtrack' });
if(!tracks || tracks.length === 0){
sendChat('Sound Manager', `/w gm No jukebox tracks found.`);
return;
}
let output = `<div style="${containerStyle}">`;
output += `<h3 style="${headerStyle}">Available Jukebox Sounds</h3>`;
output += `<ul style="${jukeboxListStyle}">`;
tracks.forEach(track => {
let title = track.get('title');
let encodedTitle = encodeURIComponent(title);
output += `<li style="margin-bottom:5px;">`;
output += `<a href="!play-jukebox ${title}" style="${playButtonStyle}">${title}</a> `;
output += `<a href="!add-from-jukebox ${encodedTitle}" style="${smallButtonStyle}">Add</a>`;
output += `</li>`;
});
output += `</ul>`;
output += navForJukebox;
output += `</div>`;
sendChat('Sound Manager', `/w gm ${output}`);
}
});
// ----------------------------
// Command to Add From Jukebox: !add-from-jukebox
// ----------------------------
on('chat:message', (msg) => {
ensureConfig();
if(msg.type !== 'api') return;
if(msg.content.indexOf('!add-from-jukebox') === 0){
let encodedName = msg.content.replace('!add-from-jukebox', '').trim();
if(!encodedName){
sendChat('Sound Manager', `/w gm No track specified.`);
return;
}
let trackName = decodeURIComponent(encodedName);
let norm = normalizeSoundName(trackName);
let exists = (state.configuredSounds || []).find(entry => entry.norm === norm);
if(exists){
sendChat('Sound Manager', `/w gm Sound "<b>${exists.display}</b>" is already configured.`);
} else {
state.configuredSounds.push({ norm: norm, display: trackName, category: "dmg" });
updateConfig();
sendChat('Sound Manager', `/w gm Sound "<b>${trackName}</b>" added with default category Damage.`);
}
}
});
// ----------------------------
// New Command: !play-jukebox
// ----------------------------
on('chat:message', (msg) => {
ensureConfig();
if(msg.type !== 'api') return;
if(msg.content.indexOf('!play-jukebox') === 0){
let trackName = msg.content.replace('!play-jukebox', '').trim();
if(!trackName){
sendChat('Sound Manager', `/w gm Usage: !play-jukebox <Track Name>`);
return;
}
playSound(trackName, 'play');
sendChat('Sound Manager', `/w gm Previewing jukebox track: <b>${trackName}</b>`);
}
});
// ----------------------------
// Token HP Update Listener (No Delay)
// ----------------------------
on("change:graphic", function(obj, prev) {
const bar = state.configuredBar || "bar1";
let currentHp = parseInt(obj.get(`${bar}_value`), 10) || 0;
let previousHp = parseInt(prev[`${bar}_value`], 10) || 0;
if(currentHp !== previousHp){
if(currentHp <= 0){
playRandomSoundFromCategory("death");
} else if(currentHp > previousHp){
playRandomSoundFromCategory("heal");
} else if(currentHp < previousHp){
playRandomSoundFromCategory("dmg");
}
}
});
// ----------------------------
// TokenMod Command Listener (for autoButtons etc.)
// ----------------------------
on('chat:message', (msg) => {
if(msg.type === 'api' && /^!token-mod --set/.test(msg.content)){
let match = msg.content.match(/--set\s+bar\d+_value\|([+-])/);
if(match){
let sign = match[1];
if(sign === '+'){
playRandomSoundFromCategory("heal");
} else if(sign === '-'){
// If autoButtons targets tokens, check each token shortly afterward
if(msg.selected && msg.selected.length > 0) {
msg.selected.forEach(sel => {
let token = getObj('graphic', sel._id);
setTimeout(() => {
if(!token) return;
let hp = parseInt(token.get(`${state.configuredBar || 'bar1'}_value`), 10) || 0;
if(hp <= 0) {
playRandomSoundFromCategory("death");
} else {
playRandomSoundFromCategory("dmg");
}
}, 1);
});
} else {
// Fallback if no tokens are selected:
playRandomSoundFromCategory("dmg");
}
}
}
}
});
// ----------------------------
// ApplyDamage Observer Hook (NEW)
// ----------------------------
const hookApplyDamageForSounds = (retries) => {
if (typeof ApplyDamage !== 'undefined' && ApplyDamage.registerObserver) {
ApplyDamage.registerObserver('change', (token, prev) => {
try {
const bar = state.configuredBar || 'bar1';
const cur = parseInt(token.get(`${bar}_value`), 10) || 0;
const prior = parseInt(prev[`${bar}_value`], 10) || 0;
if (cur === prior) return;
if (cur <= 0 && prior > 0) {
playRandomSoundFromCategory('death');
} else if (cur < prior) {
playRandomSoundFromCategory('dmg');
} else {
playRandomSoundFromCategory('heal');
}
} catch (e) {
log('Sound Manager ApplyDamage observer error: ' + e);
}
});
log('Sound Manager: hooked into ApplyDamage change observer.');
} else {
// Retry a few times in case ApplyDamage isn't ready yet
const n = (retries || 0);
if (n < 20) {
setTimeout(function(){ hookApplyDamageForSounds(n+1); }, 250);
} else {
log('Sound Manager: ApplyDamage not found; observer not attached.');
}
}
};
hookApplyDamageForSounds();
});