From 7bf9dba15b0d57380aa8f2625fe676ca215e3c67 Mon Sep 17 00:00:00 2001 From: !Ryan <100989385+softedco@users.noreply.github.com> Date: Thu, 5 Jan 2023 06:41:26 +0600 Subject: [PATCH] Add sound extension (#100) Co-authored-by: GarboMuffin --- extensions/sound.js | 187 ++++++++++++++++++++++++++++++++++++++++++++ images/README.md | 3 + images/sound.svg | 1 + website/index.ejs | 7 ++ website/meow.mp3 | Bin 0 -> 37420 bytes 5 files changed, 198 insertions(+) create mode 100644 extensions/sound.js create mode 100644 images/sound.svg create mode 100644 website/meow.mp3 diff --git a/extensions/sound.js b/extensions/sound.js new file mode 100644 index 0000000000..31d00cbe14 --- /dev/null +++ b/extensions/sound.js @@ -0,0 +1,187 @@ +(Scratch => { + 'use strict'; + + const audioEngine = Scratch.vm.runtime.audioEngine; + + const fetchAsArrayBufferWithTimeout = (url) => new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + let timeout = setTimeout(() => { + xhr.abort(); + throw new Error('Timed out'); + }, 5000); + xhr.onload = () => { + clearTimeout(timeout); + if (xhr.status === 200) { + resolve(xhr.response); + } else { + reject(new Error(`HTTP error ${xhr.status} while fetching ${url}`)); + } + }; + xhr.onerror = () => { + clearTimeout(timeout); + reject(new Error(`Failed to request ${url}`)); + }; + xhr.responseType = 'arraybuffer'; + xhr.open('GET', url); + xhr.send(); + }); + + /** + * @type {Map} + */ + const soundPlayerCache = new Map(); + + /** + * @param {string} url + * @returns {Promise} + */ + const decodeSoundPlayer = async (url) => { + const cached = soundPlayerCache.get(url); + if (cached) { + if (cached.sound) { + return cached.sound; + } + throw cached.error; + } + + try { + const arrayBuffer = await fetchAsArrayBufferWithTimeout(url); + const soundPlayer = await audioEngine.decodeSoundPlayer({ + data: { + buffer: arrayBuffer + } + }); + soundPlayerCache.set(url, { + sound: soundPlayer, + error: null + }); + return soundPlayer; + } catch (e) { + soundPlayerCache.set(url, { + sound: null, + error: e + }); + throw e; + } + }; + + /** + * @param {string} url + * @param {VM.Target} target + * @returns {Promise} true if the sound could be played, false if the sound could not be decoded + */ + const playWithAudioEngine = async (url, target) => { + const soundBank = target.sprite.soundBank; + + /** @type {AudioEngine.SoundPlayer} */ + let soundPlayer; + try { + const originalSoundPlayer = await decodeSoundPlayer(url); + // @ts-expect-error + soundPlayer = originalSoundPlayer.take(); + } catch (e) { + console.warn('Could not fetch audio; falling back to primitive approach', e); + return false; + } + + soundBank.addSoundPlayer(soundPlayer); + await soundBank.playSound(target, soundPlayer.id); + + delete soundBank.soundPlayers[soundPlayer.id]; + // @ts-expect-error + soundBank.playerTargets.delete(soundPlayer.id); + // @ts-expect-error + soundBank.soundEffects.delete(soundPlayer.id); + + return true; + }; + + /** + * @param {string} url + * @param {VM.Target} target + * @returns {Promise} + */ + const playWithAudioElement = (url, target) => new Promise((resolve, reject) => { + // Unfortunately, we can't play all sounds with the audio engine. + // For these sounds, fall back to a primitive