Remove SoundManager dependency (#1563)

All sound stuff is now handled directly by BattleSound, using the
HTML5 audio API.

The main complicated thing we do with sound is loop music with an intro.
This is unfortunately not supported by ANY sound library out there
(I had to manually add support for it myself to soundManager!)

https://github.com/scottschiller/SoundManager2/pull/13

In the end, I don't think the existing libraries out there actually
give us anything I care about.
This commit is contained in:
Guangcong Luo 2020-07-23 12:51:47 -07:00 committed by GitHub
parent 54cbc51f39
commit c1135497e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 209 additions and 254 deletions

View File

@ -27,7 +27,7 @@ module.exports = {
"BattleTextParser": false,
// Generic global variables
"Config": false, "BattleSearch": false, "soundManager": false, "Storage": false, "Dex": false, "DexSearch": false,
"Config": false, "BattleSearch": false, "Storage": false, "Dex": false, "DexSearch": false,
"app": false, "toID": false, "toRoomid": false, "toUserid": false, "toName": false, "PSUtils": false, "MD5": false,
"ChatHistory": false, "Topbar": false, "UserList": false,

1
.gitignore vendored
View File

@ -20,6 +20,7 @@ package-lock.json
/js/battle-choices.js
/js/battle-text-parser.js
/js/battle-dex.js
/js/battle-sound.js
/js/battle-dex-data.js
/js/battle-animations-moves.js
/js/battle-animations.js

View File

@ -29,7 +29,7 @@ PS loads itself in phases:
- `client-connection.ts`
- Connect to server
- Preact
- SoundManager
- BattleSound
- `panel-mainmenu.tsx`
- `panel-rooms.tsx`
- `panels.tsx`

View File

@ -102,10 +102,7 @@ ga('send', 'pageview');
<script src="//play.pokemonshowdown.com/js/lib/jquery-2.1.4.min.js"></script>
<script src="//play.pokemonshowdown.com/js/lib/jquery-cookie.js"></script>
<script src="//play.pokemonshowdown.com/js/lib/autoresize.jquery.min.js?"></script>
<script src="//play.pokemonshowdown.com/js/lib/soundmanager2-nodebug-jsmin.js?"></script>
<script>
soundManager.setup({url: '//play.pokemonshowdown.com/swf/'});
</script>
<script src="//play.pokemonshowdown.com/js/battle-sound.js?"></script>
<script src="//play.pokemonshowdown.com/js/lib/html-css-sanitizer-minified.js?"></script>
<script src="//play.pokemonshowdown.com/js/lib/lodash.core.js?"></script>
<script src="//play.pokemonshowdown.com/js/lib/backbone.js?"></script>

View File

@ -1279,9 +1279,7 @@
if (autoscroll) {
this.$chatFrame.scrollTop(this.$chat.height());
}
if (!app.focused && !Dex.prefs('mute') && Dex.prefs('notifvolume')) {
soundManager.getSoundById('notif').setVolume(Dex.prefs('notifvolume')).play();
}
if (!app.focused) app.playNotificationSound();
},
addRow: function (line) {
var name, name2, silent;
@ -1386,9 +1384,7 @@
case 'notify':
if (row[3] && !this.getHighlight(row[3])) return;
if (!Dex.prefs('mute') && Dex.prefs('notifvolume')) {
soundManager.getSoundById('notif').setVolume(Dex.prefs('notifvolume')).play();
}
app.playNotificationSound();
this.notifyOnce(row[1], row[2], 'highlight');
break;
@ -1396,9 +1392,7 @@
var notifyOnce = row[4] !== '!';
if (!notifyOnce) row[4] = '';
if (row[4] && !this.getHighlight(row[4])) return;
if (!this.notifications && !Dex.prefs('mute') && Dex.prefs('notifvolume')) {
soundManager.getSoundById('notif').setVolume(Dex.prefs('notifvolume')).play();
}
if (!this.notifications) app.playNotificationSound();
this.notify(row[2], row[3], row[1], notifyOnce);
break;
@ -1655,9 +1649,7 @@
}
if (mayNotify && isHighlighted) {
if (!Dex.prefs('mute') && Dex.prefs('notifvolume')) {
soundManager.getSoundById('notif').setVolume(Dex.prefs('notifvolume')).play();
}
app.playNotificationSound();
var $lastMessage = this.$chat.children().last();
var notifyTitle = "Mentioned by " + name + (this.id === 'lobby' ? '' : " in " + this.title);
var notifyText = $lastMessage.html().indexOf('<span class="spoiler">') >= 0 ? '(spoiler)' : $lastMessage.children().last().text();

View File

@ -63,17 +63,13 @@
break;
case 'notify':
if (!Dex.prefs('mute') && Dex.prefs('notifvolume')) {
soundManager.getSoundById('notif').setVolume(Dex.prefs('notifvolume')).play();
}
app.playNotificationSound();
this.notifyOnce(row[1], row.slice(2).join('|'), 'highlight');
break;
case 'tempnotify':
var notifyOnce = row[4] !== '!';
if (!this.notifications && !Dex.prefs('mute') && Dex.prefs('notifvolume')) {
soundManager.getSoundById('notif').setVolume(Dex.prefs('notifvolume')).play();
}
if (!this.notifications) app.playNotificationSound();
this.notify(row[2], row[3], row[1], notifyOnce);
break;

View File

@ -1885,6 +1885,12 @@ function toId() {
Dex.prefs('autojoin', curAutojoin);
},
playNotificationSound: function () {
if (window.BattleSound && !Dex.prefs('mute')) {
BattleSound.playSound('audio/notification.wav', Dex.prefs('notifvolume'));
}
},
/*********************************************************
* Popups
*********************************************************/

View File

@ -21,7 +21,7 @@ requireScript('https://play.pokemonshowdown.com/config/config.js?a7');
requireScript('https://play.pokemonshowdown.com/js/lib/jquery-1.11.0.min.js');
requireScript('https://play.pokemonshowdown.com/js/lib/lodash.compat.js');
requireScript('https://play.pokemonshowdown.com/js/lib/html-sanitizer-minified.js');
requireScript('https://play.pokemonshowdown.com/js/lib/soundmanager2-nodebug-jsmin.js');
requireScript('https://play.pokemonshowdown.com/js/battle-sound.js');
requireScript('https://play.pokemonshowdown.com/js/battledata.js?a7');
requireScript('https://play.pokemonshowdown.com/data/pokedex-mini.js?a7');
requireScript('https://play.pokemonshowdown.com/data/pokedex-mini-bw.js?a7');
@ -67,17 +67,12 @@ var Replays = {
// eslint-disable-next-line no-self-assign
if (rc2) rc2.innerHTML = rc2.innerHTML;
if (window.soundManager && soundManager.ready) this.soundReady();
if (window.HTMLAudioElement) $('.soundchooser, .startsoundchooser').show();
this.reset();
},
"$": function (sel) {
return this.$el.find(sel);
},
soundReady: function () {
if (Replays.isSoundReady) return;
Replays.isSoundReady = true;
$('.soundchooser, .startsoundchooser').show();
},
clickChangeSetting: function (e) {
e.preventDefault();
var $chooser = $(e.currentTarget).closest('.chooser');

View File

@ -70,7 +70,7 @@
<script defer src="/js/battle-log.js?"></script>
<script defer src="/js/panel-chat.js?"></script>
<script defer src="/js/lib/soundmanager2-nodebug-jsmin.js"></script>
<script defer src="/js/battle-sound.js"></script>
<script defer src="/js/lib/jquery-2.1.4.min.js"></script>
<script defer src="/data/graphics.js?"></script>
<script defer src="/data/text.js?"></script>

View File

@ -5,13 +5,6 @@
} */
//setTimeout(function(){updateProgress(true)}, 10000);
if (window.soundManager) {
soundManager.onready(function(){
soundManager.ready = true;
$('.soundchooser, .startsoundchooser').show();
});
}
// Panels
var Topbar = Panels.Topbar.extend({
@ -162,7 +155,7 @@ var ReplayPanel = Panels.StaticPanel.extend({
var rc2 = this.$('.replay-controls-2')[0];
if (rc2) rc2.innerHTML = rc2.innerHTML;
if (window.soundManager && soundManager.ready) this.$('.soundchooser, .startsoundchooser').show();
if (window.HTMLAudioElement) this.$('.soundchooser, .startsoundchooser').show();
},
clickReplayDownloadButton: function (e) {
var filename = (this.battle.tier || 'Battle').replace(/[^A-Za-z0-9]/g, '');

View File

@ -88,10 +88,7 @@ function ThemeFooterTemplate() {
<script src="//play.pokemonshowdown.com/js/lib/jquery-cookie.js"></script>
<script src="//play.pokemonshowdown.com/js/lib/html-sanitizer-minified.js"></script>
<script src="//play.pokemonshowdown.com/js/lib/soundmanager2-nodebug-jsmin.js?"></script>
<script>
soundManager.setup({url: '//play.pokemonshowdown.com/swf/'});
</script>
<script src="//play.pokemonshowdown.com/js/battle-sound.js?"></script>
<script src="//play.pokemonshowdown.com/config/config.js?"></script>
<script src="//play.pokemonshowdown.com/js/battledata.js?"></script>
<script src="//play.pokemonshowdown.com/data/pokedex-mini.js?"></script>

View File

@ -98,7 +98,7 @@ else if ($_REQUEST['name'])// && $REPLAYS[$_REQUEST['name']])
<link rel="stylesheet" href="/style/replayer.css" />
<script src="/js/jquery-1.9.1.min.js"></script>
<script src="/js/jquery-cookie.js"></script>
<script src="/js/soundmanager2.js"></script>
<script src="/js/battle-sound.js"></script>
<script src="/js/battledata.js"></script>
<script src="/data/pokedex-mini.js"></script>
<script src="/data/graphics.js"></script>

View File

@ -30,17 +30,6 @@ This license DOES NOT extend to any other files in this repository.
*/
if (window.soundManager) {
soundManager.setup({url: `https://${Config.routes.client}/swf/`});
if (window.Replays) soundManager.onready(window.Replays.soundReady);
soundManager.onready(() => {
soundManager.createSound({
id: 'notif',
url: `https://${Config.routes.client}/audio/notification.wav`,
});
});
}
class BattleScene {
battle: Battle;
animating = true;
@ -1560,7 +1549,7 @@ class BattleScene {
);
if (nowPlaying) {
if (!this.bgm) this.rollBgm();
this.bgm!.play();
this.bgm!.resume();
} else if (this.bgm) {
this.bgm.pause();
}
@ -2717,197 +2706,6 @@ Object.assign($.easing, {
},
});
interface SMSound {
play(): this;
pause(): this;
stop(): this;
resume(): this;
setVolume(volume: number): this;
setPosition(position: number): this;
onposition(position: number, callback: (this: this) => void): this;
position: number;
readonly paused: boolean;
playState: 0 | 1;
isSoundPlaceholder?: boolean;
}
class BattleBGM {
/**
* May be shared with other BGM objects: every battle has its own BattleBGM
* object, but two battles with the same music will have the same SMSound
* object.
*/
sound: SMSound;
isPlaying = false;
constructor(sound: SMSound) {
this.sound = sound;
}
play() {
if (this.isPlaying) return;
this.isPlaying = true;
if (BattleSound.muted || !BattleSound.bgmVolume) return;
let thisIsFirst = false;
for (const bgm of BattleSound.bgm) {
if (bgm === this) {
thisIsFirst = true;
} else if (bgm.isPlaying) {
if (!thisIsFirst) return;
bgm.sound.pause();
break;
}
}
this.sound.setVolume(BattleSound.bgmVolume);
// SoundManager bugs out if you call .play() while it's already playing
if (!this.sound.playState || this.sound.paused) {
this.sound.play();
}
}
pause() {
this.isPlaying = false;
this.sound.pause();
BattleBGM.update();
}
stop() {
this.isPlaying = false;
this.sound.stop();
}
destroy() {
this.isPlaying = false;
this.sound.stop();
const soundIndex = BattleSound.bgm.indexOf(this);
if (soundIndex >= 0) BattleSound.bgm.splice(soundIndex, 1);
BattleBGM.update();
}
static update() {
for (const bgm of BattleSound.bgm) {
if (bgm.isPlaying) {
if (BattleSound.muted || !BattleSound.bgmVolume) {
bgm.sound.pause();
} else {
bgm.sound.setVolume(BattleSound.bgmVolume);
// SoundManager bugs out if you call .play() while it's already playing
if (!bgm.sound.playState || bgm.sound.paused) {
bgm.sound.play();
}
}
break;
}
}
}
}
const BattleSound = new class {
effectCache: {[url: string]: SMSound} = {};
// bgm
bgmCache: {[url: string]: SMSound} = {};
bgm: BattleBGM[] = [];
// misc
soundPlaceholder: SMSound = {
play() { return this; },
pause() { return this; },
stop() { return this; },
resume() { return this; },
setVolume() { return this; },
onposition() { return this; },
isSoundPlaceholder: true,
} as any;
// options
effectVolume = 50;
bgmVolume = 50;
muted = false;
loadEffect(url: string) {
if (this.effectCache[url] && !this.effectCache[url].isSoundPlaceholder) {
return this.effectCache[url];
}
try {
this.effectCache[url] = soundManager.createSound({
id: url,
url: Dex.resourcePrefix + url,
volume: this.effectVolume,
}) as SMSound;
} catch {}
if (!this.effectCache[url]) {
this.effectCache[url] = this.soundPlaceholder;
}
return this.effectCache[url];
}
playEffect(url: string) {
if (!this.muted) this.loadEffect(url).setVolume(this.effectVolume).play();
}
addBgm(sound: SMSound, replaceBGM?: BattleBGM | null) {
if (replaceBGM) {
replaceBGM.sound.stop();
replaceBGM.sound = sound;
BattleBGM.update();
return replaceBGM;
}
const bgm = new BattleBGM(sound);
this.bgm.push(bgm);
return bgm;
}
/** loopstart and loopend are in milliseconds */
loadBgm(url: string, loopstart: number, loopend: number, replaceBGM?: BattleBGM | null) {
let sound = this.bgmCache[url];
if (sound) {
if (!sound.isSoundPlaceholder) {
return this.addBgm(sound, replaceBGM);
}
}
try {
sound = soundManager.createSound({
id: url,
url: Dex.resourcePrefix + url,
volume: this.bgmVolume,
});
} catch {}
if (!sound) {
// couldn't load
// suppress crash
return this.addBgm(this.bgmCache[url] = this.soundPlaceholder, replaceBGM);
}
sound.onposition(loopend, function () {
this.setPosition(this.position - (loopend - loopstart));
});
this.bgmCache[url] = sound;
return this.addBgm(sound, replaceBGM);
}
// setting
setMute(muted: boolean) {
muted = !!muted;
if (this.muted === muted) return;
this.muted = muted;
BattleBGM.update();
}
loudnessPercentToAmplitudePercent(loudnessPercent: number) {
// 10 dB is perceived as approximately twice as loud
let decibels = 10 * Math.log(loudnessPercent / 100) / Math.log(2);
return Math.pow(10, decibels / 20) * 100;
}
setBgmVolume(bgmVolume: number) {
this.bgmVolume = this.loudnessPercentToAmplitudePercent(bgmVolume);
BattleBGM.update();
}
setEffectVolume(effectVolume: number) {
this.effectVolume = this.loudnessPercentToAmplitudePercent(effectVolume);
}
};
if (typeof PS === 'object') {
PS.prefs.subscribeAndRun(key => {
if (!key || key === 'musicvolume' || key === 'effectvolume' || key === 'mute') {
BattleSound.effectVolume = PS.prefs.effectvolume;
BattleSound.bgmVolume = PS.prefs.musicvolume;
BattleSound.muted = PS.prefs.mute;
BattleBGM.update();
}
});
}
interface AnimData {
anim(scene: BattleScene, args: PokemonSprite[]): void;
prepareAnim?(scene: BattleScene, args: PokemonSprite[]): void;

184
src/battle-sound.ts Normal file
View File

@ -0,0 +1,184 @@
class BattleBGM {
/**
* May be shared with other BGM objects: every battle has its own BattleBGM
* object, but two battles with the same music will have the same HTMLAudioElement
* object.
*/
sound?: HTMLAudioElement;
url: string;
timer: number | undefined = undefined;
loopstart: number;
loopend: number;
/**
* When multiple battles with BGM are open, they will be `isPlaying`, but only the
* first one will be `isActuallyPlaying`. In addition, muting volume or setting
* BGM volume to 0 will set `isActuallyPlaying` to false.
*/
isPlaying = false;
isActuallyPlaying = false;
constructor(url: string, loopstart: number, loopend: number) {
this.url = url;
this.loopstart = loopstart;
this.loopend = loopend;
}
play() {
if (this.sound) this.sound.currentTime = 0;
this.resume();
}
resume() {
this.isPlaying = true;
this.actuallyResume();
}
pause() {
this.isPlaying = false;
this.actuallyPause();
BattleBGM.update();
}
stop() {
this.pause();
if (this.sound) this.sound.currentTime = 0;
}
destroy() {
BattleSound.deleteBgm(this);
this.pause();
}
actuallyResume() {
if (this !== BattleSound.currentBgm()) return;
if (this.isActuallyPlaying) return;
if (!this.sound) this.sound = BattleSound.getSound(this.url);
if (!this.sound) return;
this.isActuallyPlaying = true;
this.sound.volume = BattleSound.bgmVolume / 100;
this.sound.play();
this.updateTime();
}
actuallyPause() {
if (!this.isActuallyPlaying) return;
this.isActuallyPlaying = false;
this.sound!.pause();
this.updateTime();
}
/**
* Handles the hard part of looping the sound
*/
updateTime() {
clearTimeout(this.timer);
this.timer = undefined;
if (this !== BattleSound.currentBgm()) return;
if (!this.sound) return;
const progress = this.sound.currentTime * 1000;
if (progress > this.loopend - 1000) {
this.sound.currentTime -= (this.loopend - this.loopstart) / 1000;
}
this.timer = setTimeout(() => {
this.updateTime();
}, Math.max(this.loopend - progress, 1));
}
static update() {
const current = BattleSound.currentBgm();
for (const bgm of BattleSound.bgm) {
if (bgm.isPlaying) {
if (bgm === current) {
bgm.actuallyResume();
} else {
bgm.actuallyPause();
}
}
}
}
}
const BattleSound = new class {
soundCache: {[url: string]: HTMLAudioElement | undefined} = {};
bgm: BattleBGM[] = [];
// options
effectVolume = 50;
bgmVolume = 50;
muted = false;
getSound(url: string) {
if (!window.HTMLAudioElement) return;
if (this.soundCache[url]) return this.soundCache[url];
try {
const sound = document.createElement('audio');
sound.src = 'https://' + Config.routes.client + '/' + url;
sound.volume = this.effectVolume / 100;
this.soundCache[url] = sound;
return sound;
} catch {}
}
playEffect(url: string) {
this.playSound(url, this.muted ? 0 : this.effectVolume);
}
playSound(url: string, volume: number) {
if (!volume) return;
const effect = this.getSound(url);
if (effect) {
effect.volume = volume / 100;
effect.play();
}
}
/** loopstart and loopend are in milliseconds */
loadBgm(url: string, loopstart: number, loopend: number, replaceBGM?: BattleBGM | null) {
if (replaceBGM) this.deleteBgm(replaceBGM);
const bgm = new BattleBGM(url, loopstart, loopend);
this.bgm.push(bgm);
return bgm;
}
deleteBgm(bgm: BattleBGM) {
const soundIndex = BattleSound.bgm.indexOf(bgm);
if (soundIndex >= 0) BattleSound.bgm.splice(soundIndex, 1);
}
currentBgm() {
if (!this.bgmVolume || this.muted) return false;
for (const bgm of this.bgm) {
if (bgm.isPlaying) return bgm;
}
return null;
}
// setting
setMute(muted: boolean) {
muted = !!muted;
if (this.muted === muted) return;
this.muted = muted;
BattleBGM.update();
}
loudnessPercentToAmplitudePercent(loudnessPercent: number) {
// 10 dB is perceived as approximately twice as loud
let decibels = 10 * Math.log(loudnessPercent / 100) / Math.log(2);
return Math.pow(10, decibels / 20) * 100;
}
setBgmVolume(bgmVolume: number) {
this.bgmVolume = this.loudnessPercentToAmplitudePercent(bgmVolume);
BattleBGM.update();
}
setEffectVolume(effectVolume: number) {
this.effectVolume = this.loudnessPercentToAmplitudePercent(effectVolume);
}
};
if (typeof PS === 'object') {
PS.prefs.subscribeAndRun(key => {
if (!key || key === 'musicvolume' || key === 'effectvolume' || key === 'mute') {
BattleSound.effectVolume = PS.prefs.effectvolume;
BattleSound.bgmVolume = PS.prefs.musicvolume;
BattleSound.muted = PS.prefs.mute;
BattleBGM.update();
}
});
}

1
src/globals.d.ts vendored
View File

@ -3,7 +3,6 @@
// dependencies
///////////////
declare var soundManager: any;
// Caja
declare var html4: any;
declare var html: any;

View File

@ -71,7 +71,7 @@
<script src="js/battle-log.js"></script>
<script src="js/panel-chat.js"></script>
<script src="js/lib/soundmanager2-nodebug-jsmin.js"></script>
<script src="js/battle-sound.js"></script>
<script src="js/lib/jquery-2.1.4.min.js"></script>
<script src="data/graphics.js"></script>
<script src="data/text.js"></script>

View File

@ -71,11 +71,8 @@
<script src="js/lib/jquery-2.1.4.min.js"></script>
<script src="js/lib/jquery-cookie.js"></script>
<script src="js/lib/autoresize.jquery.min.js"></script>
<script src="js/lib/soundmanager2-nodebug-jsmin.js"></script>
<script src="js/battle-sound.js"></script>
<script src="config/testclient-key.js"></script>
<script>
soundManager.setup({url: 'swf/'});
</script>
<script src="js/lib/html-css-sanitizer-minified.js"></script>
<script src="js/lib/lodash.core.js"></script>
<script src="js/lib/backbone.js"></script>