From e521d47f7acc13daea4769345ec17dbc77ba1289 Mon Sep 17 00:00:00 2001 From: CST1229 <68464103+CST1229@users.noreply.github.com> Date: Fri, 19 Apr 2024 20:51:12 +0200 Subject: [PATCH] Lily/SoundExpanded: add play sound at and loop begin/end blocks (#1414) ![image](https://github.com/TurboWarp/extensions/assets/68464103/39f82e05-6e28-4d40-b286-96b58479b59d) Adds one *very* highly-requested feature and another probably-less-requested but still useful feature. They work pretty much as you'd expect. They were actually pretty straightforward to implement (other than start sound at requiring either function overriding or function duplication, and I went with the latter). --- extensions/Lily/SoundExpanded.js | 232 ++++++++++++++++++++++++++++++- 1 file changed, 229 insertions(+), 3 deletions(-) diff --git a/extensions/Lily/SoundExpanded.js b/extensions/Lily/SoundExpanded.js index 329de99a85..cafbf734e5 100644 --- a/extensions/Lily/SoundExpanded.js +++ b/extensions/Lily/SoundExpanded.js @@ -24,13 +24,43 @@ opcode: "startLooping", blockType: Scratch.BlockType.COMMAND, text: "start looping [SOUND]", + arguments: { + SOUND: { + type: Scratch.ArgumentType.SOUND, + }, + }, + extensions: ["colours_sounds"], + }, + { + opcode: "startLoopingBegin", + blockType: Scratch.BlockType.COMMAND, + text: "start looping [SOUND] loop start [START] seconds", arguments: { SOUND: { type: Scratch.ArgumentType.SOUND, }, START: { type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, + defaultValue: "2", + }, + }, + extensions: ["colours_sounds"], + }, + { + opcode: "startLoopingBeginEnd", + blockType: Scratch.BlockType.COMMAND, + text: "start looping [SOUND] loop region [START] to [END] seconds", + arguments: { + SOUND: { + type: Scratch.ArgumentType.SOUND, + }, + START: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "2", + }, + END: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "4", }, }, extensions: ["colours_sounds"], @@ -60,6 +90,74 @@ "---", + { + opcode: "playSoundAtAndWait", + blockType: Scratch.BlockType.COMMAND, + text: "play sound [SOUND] from [START] seconds until done", + arguments: { + SOUND: { + type: Scratch.ArgumentType.SOUND, + }, + START: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "2", + }, + }, + extensions: ["colours_sounds"], + }, + { + opcode: "playSoundAt", + blockType: Scratch.BlockType.COMMAND, + text: "start sound [SOUND] from [START] seconds", + arguments: { + SOUND: { + type: Scratch.ArgumentType.SOUND, + }, + START: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "2", + }, + }, + extensions: ["colours_sounds"], + }, + { + opcode: "playSoundToAndWait", + blockType: Scratch.BlockType.COMMAND, + text: "play sound [SOUND] from [START] to [END] seconds until done", + arguments: { + SOUND: { + type: Scratch.ArgumentType.SOUND, + }, + START: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "2", + }, + END: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "4", + }, + }, + extensions: ["colours_sounds"], + }, + { + opcode: "playSoundTo", + blockType: Scratch.BlockType.COMMAND, + text: "start sound [SOUND] from [START] to [END] seconds", + arguments: { + SOUND: { + type: Scratch.ArgumentType.SOUND, + }, + START: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "2", + }, + END: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "4", + }, + }, + extensions: ["colours_sounds"], + }, { opcode: "stopSound", blockType: Scratch.BlockType.COMMAND, @@ -187,8 +285,8 @@ }; } - startLooping(args, util) { - const index = this._getSoundIndex(args.SOUND, util); + _startLooping(util, sound, loopStart, loopEnd) { + const index = this._getSoundIndex(sound, util); if (index < 0) return 0; const target = util.target; const sprite = util.target.sprite; @@ -203,6 +301,30 @@ if (!soundPlayer.outputNode) return; soundPlayer.outputNode.loop = true; + soundPlayer.outputNode.loopStart = loopStart; + soundPlayer.outputNode.loopEnd = loopEnd; + } + + startLooping(args, util) { + this._startLooping(util, args.SOUND, 0, 0); + } + + startLoopingBegin(args, util) { + this._startLooping( + util, + args.SOUND, + Scratch.Cast.toNumber(args.START), + 0 + ); + } + + startLoopingBeginEnd(args, util) { + this._startLooping( + util, + args.SOUND, + Scratch.Cast.toNumber(args.START), + Scratch.Cast.toNumber(args.END) + ); } stopLooping(args, util) { @@ -229,6 +351,110 @@ return soundPlayer.outputNode.loop; } + // https://github.com/scratchfoundation/scratch-vm/blob/7c1187cc1fe1c763ef61598875acd4fc9a0c8c2e/src/blocks/scratch3_sound.js#L164 + _playSoundAt(args, util, storeWaiting) { + const index = this._getSoundIndex(args.SOUND, util); + if (index >= 0) { + const { target } = util; + const { sprite } = target; + const { soundId } = sprite.sounds[index]; + const start = Math.max(Scratch.Cast.toNumber(args.START), 0); + const end = + args.END == undefined ? undefined : Scratch.Cast.toNumber(args.END); + if (sprite.soundBank) { + if (storeWaiting === true) { + // @ts-expect-error not typed + Scratch.vm.runtime.ext_scratch3_sound._addWaitingSound( + target.id, + soundId + ); + } else { + // @ts-expect-error not typed + Scratch.vm.runtime.ext_scratch3_sound._removeWaitingSound( + target.id, + soundId + ); + } + return this._playSoundBankSound( + sprite.soundBank, + target, + soundId, + start, + end + ); + } + } + } + + // https://github.com/scratchfoundation/scratch-audio/blob/6fb4b142a5f3198483e4c4f992fb623d5e9d1ed5/src/SoundBank.js#L89 + _playSoundBankSound(bank, target, soundId, start, end) { + const effects = bank.getSoundEffects(soundId); + const player = bank.getSoundPlayer(soundId); + + if (bank.playerTargets.get(soundId) !== target) { + // make sure to stop the old sound, effectively "forking" the output + // when the target switches before we adjust it's effects + player.stop(); + } + + bank.playerTargets.set(soundId, target); + effects.addSoundPlayer(player); + effects.setEffectsFromTarget(target); + player.connect(effects); + + this._playSoundPlayer(player, start, end); + + return player.finished(); + } + + // https://github.com/scratchfoundation/scratch-audio/blob/6fb4b142a5f3198483e4c4f992fb623d5e9d1ed5/src/SoundPlayer.js#L253 + _playSoundPlayer(player, start, end) { + if (player.isStarting) { + player.emit("stop"); + player.emit("play"); + return; + } + + if (player.isPlaying) { + player.stop(); + } + + if (player.initialized) { + player._createSource(); + } else { + player.initialize(); + } + + if (end === undefined) { + player.outputNode.start(0, start); + } else { + player.outputNode.start(0, start, Math.max(end - start, 0)); + } + + player.isPlaying = true; + + const { currentTime, DECAY_DURATION } = player.audioEngine; + player.startingUntil = currentTime + DECAY_DURATION; + + player.emit("play"); + } + + playSoundAt(args, util) { + this._playSoundAt(args, util); + } + + playSoundAtAndWait(args, util) { + return this._playSoundAt(args, util, true); + } + + playSoundTo(args, util) { + this._playSoundAt(args, util); + } + + playSoundToAndWait(args, util) { + return this._playSoundAt(args, util, true); + } + stopSound(args, util) { const index = this._getSoundIndex(args.SOUND, util); if (index < 0) return 0;