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 @@ - + -