From 9dcae8460983df69945f0408750c07225c7d5fcf Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Tue, 21 Jul 2020 18:12:59 -0700 Subject: [PATCH] Remove SoundManager dependency 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. --- .eslintrc.js | 2 +- .gitignore | 1 + CONTRIBUTING.md | 2 +- index.template.html | 5 +- js/client-chat.js | 16 +- js/client-ladder.js | 8 +- js/client.js | 8 +- js/replay-embed.template.js | 9 +- preactalpha.template.html | 2 +- replays/js/replay.js | 9 +- replays/theme/wrapper.inc.template.php | 5 +- replays/warstory.php | 2 +- src/battle-animations.ts | 204 +------------------------ src/battle-sound.ts | 184 ++++++++++++++++++++++ src/globals.d.ts | 1 - testclient-beta.html | 2 +- testclient.html | 5 +- 17 files changed, 210 insertions(+), 255 deletions(-) create mode 100644 src/battle-sound.ts 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 3c1f3d3c6..6f8ea1f50 100644 --- a/js/client-chat.js +++ b/js/client-chat.js @@ -1275,9 +1275,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; @@ -1382,9 +1380,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; @@ -1392,9 +1388,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; @@ -1651,9 +1645,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 2c8f2ae7c..69613fe7c 100644 --- a/js/client.js +++ b/js/client.js @@ -215,7 +215,7 @@ function toId() { getActionPHP: function () { var ret = '/~~' + Config.server.id + '/action.php'; if (Config.testclient) { - ret = 'https://' + Config.origindomain + ret; + ret = 'https://' + Config.routes.client + ret; } return (this.getActionPHP = function () { return ret; @@ -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 acd7875c9..2f958ea0a 100644 --- a/js/replay-embed.template.js +++ b/js/replay-embed.template.js @@ -21,7 +21,7 @@ requireScript('https://play.pokemonshowdown.com/js/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 @@ - + -