diff --git a/.eslintrc.js b/.eslintrc.js
index ecc88048a..9251d4fb1 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -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,
diff --git a/.gitignore b/.gitignore
index a8e9bbcb1..295ce82cc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d9e7974a6..4f7fb4582 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -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`
diff --git a/index.template.html b/index.template.html
index 6973bba80..8e28e788a 100644
--- a/index.template.html
+++ b/index.template.html
@@ -102,10 +102,7 @@ ga('send', 'pageview');
-
-
+
diff --git a/js/client-chat.js b/js/client-chat.js
index b7bbd1e16..77f9b4862 100644
--- a/js/client-chat.js
+++ b/js/client-chat.js
@@ -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('') >= 0 ? '(spoiler)' : $lastMessage.children().last().text();
diff --git a/js/client-ladder.js b/js/client-ladder.js
index 077108cbb..014a040d3 100644
--- a/js/client-ladder.js
+++ b/js/client-ladder.js
@@ -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;
diff --git a/js/client.js b/js/client.js
index 04974f7fe..69613fe7c 100644
--- a/js/client.js
+++ b/js/client.js
@@ -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
*********************************************************/
diff --git a/js/replay-embed.template.js b/js/replay-embed.template.js
index 3b347c484..699bb088f 100644
--- a/js/replay-embed.template.js
+++ b/js/replay-embed.template.js
@@ -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');
diff --git a/preactalpha.template.html b/preactalpha.template.html
index bd86a2724..d42a93607 100644
--- a/preactalpha.template.html
+++ b/preactalpha.template.html
@@ -70,7 +70,7 @@
-
+
diff --git a/replays/js/replay.js b/replays/js/replay.js
index 34fbc958a..e08112355 100644
--- a/replays/js/replay.js
+++ b/replays/js/replay.js
@@ -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, '');
diff --git a/replays/theme/wrapper.inc.template.php b/replays/theme/wrapper.inc.template.php
index 95e3d3fd5..44603f898 100644
--- a/replays/theme/wrapper.inc.template.php
+++ b/replays/theme/wrapper.inc.template.php
@@ -88,10 +88,7 @@ function ThemeFooterTemplate() {
-
-
+
diff --git a/replays/warstory.php b/replays/warstory.php
index 78b51c0ad..e561367ee 100644
--- a/replays/warstory.php
+++ b/replays/warstory.php
@@ -98,7 +98,7 @@ else if ($_REQUEST['name'])// && $REPLAYS[$_REQUEST['name']])
-
+
diff --git a/src/battle-animations.ts b/src/battle-animations.ts
index d515a23e3..29bad9d4b 100644
--- a/src/battle-animations.ts
+++ b/src/battle-animations.ts
@@ -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;
diff --git a/src/battle-sound.ts b/src/battle-sound.ts
new file mode 100644
index 000000000..d5b7d24df
--- /dev/null
+++ b/src/battle-sound.ts
@@ -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();
+ }
+ });
+}
diff --git a/src/globals.d.ts b/src/globals.d.ts
index e9439b072..f5b1b04c2 100644
--- a/src/globals.d.ts
+++ b/src/globals.d.ts
@@ -3,7 +3,6 @@
// dependencies
///////////////
-declare var soundManager: any;
// Caja
declare var html4: any;
declare var html: any;
diff --git a/testclient-beta.html b/testclient-beta.html
index 19b8fa037..7f9498648 100644
--- a/testclient-beta.html
+++ b/testclient-beta.html
@@ -71,7 +71,7 @@
-
+
diff --git a/testclient.html b/testclient.html
index 18ebeab2b..5ceebb701 100644
--- a/testclient.html
+++ b/testclient.html
@@ -71,11 +71,8 @@
-
+
-