diff --git a/src/extensions/scratch3_music/index.js b/src/extensions/scratch3_music/index.js index 4450a69e8c4..8d73067b31e 100644 --- a/src/extensions/scratch3_music/index.js +++ b/src/extensions/scratch3_music/index.js @@ -52,18 +52,25 @@ class Scratch3MusicBlocks { this._concurrencyCounter = 0; /** - * An array of audio buffers, one for each drum sound. + * An array of sound players, one for each drum sound. * @type {Array} * @private */ - this._drumBuffers = []; + this._drumPlayers = []; /** - * An array of arrays of audio buffers. Each instrument has one or more audio buffers. + * An array of arrays of sound players. Each instrument has one or more audio players. * @type {Array[]} * @private */ - this._instrumentBufferArrays = []; + this._instrumentPlayerArrays = []; + + /** + * An array of arrays of sound players. Each instrument mya have an audio player for each playable note. + * @type {Array[]} + * @private + */ + this._instrumentPlayerNoteArrays = []; /** * An array of audio bufferSourceNodes. Each time you play an instrument or drum sound, @@ -87,14 +94,15 @@ class Scratch3MusicBlocks { const loadingPromises = []; this.DRUM_INFO.forEach((drumInfo, index) => { const filePath = `drums/${drumInfo.fileName}`; - const promise = this._storeSound(filePath, index, this._drumBuffers); + const promise = this._storeSound(filePath, index, this._drumPlayers); loadingPromises.push(promise); }); this.INSTRUMENT_INFO.forEach((instrumentInfo, instrumentIndex) => { - this._instrumentBufferArrays[instrumentIndex] = []; + this._instrumentPlayerArrays[instrumentIndex] = []; + this._instrumentPlayerNoteArrays[instrumentIndex] = []; instrumentInfo.samples.forEach((sample, noteIndex) => { const filePath = `instruments/${instrumentInfo.dirName}/${sample}`; - const promise = this._storeSound(filePath, noteIndex, this._instrumentBufferArrays[instrumentIndex]); + const promise = this._storeSound(filePath, noteIndex, this._instrumentPlayerArrays[instrumentIndex]); loadingPromises.push(promise); }); }); @@ -104,22 +112,22 @@ class Scratch3MusicBlocks { } /** - * Decode a sound and store the buffer in an array. + * Decode a sound and store the player in an array. * @param {string} filePath - the audio file name. - * @param {number} index - the index at which to store the audio buffer. - * @param {array} bufferArray - the array of buffers in which to store it. + * @param {number} index - the index at which to store the audio player. + * @param {array} playerArray - the array of players in which to store it. * @return {Promise} - a promise which will resolve once the sound has been stored. */ - _storeSound (filePath, index, bufferArray) { + _storeSound (filePath, index, playerArray) { const fullPath = `${filePath}.mp3`; if (!assetData[fullPath]) return; - // The sound buffer has already been downloaded via the manifest file required above. + // The sound player has already been downloaded via the manifest file required above. const soundBuffer = assetData[fullPath]; - return this._decodeSound(soundBuffer).then(buffer => { - bufferArray[index] = buffer; + return this._decodeSound(soundBuffer).then(player => { + playerArray[index] = player; }); } @@ -129,24 +137,14 @@ class Scratch3MusicBlocks { * @return {Promise} - a promise which will resolve once the sound has decoded. */ _decodeSound (soundBuffer) { - const context = this.runtime.audioEngine && this.runtime.audioEngine.audioContext; + const engine = this.runtime.audioEngine; - if (!context) { + if (!engine) { return Promise.reject(new Error('No Audio Context Detected')); } // Check for newer promise-based API - if (context.decodeAudioData.length === 1) { - return context.decodeAudioData(soundBuffer); - } else { // eslint-disable-line no-else-return - // Fall back to callback API - return new Promise((resolve, reject) => - context.decodeAudioData(soundBuffer, - buffer => resolve(buffer), - error => reject(error) - ) - ); - } + return engine.decodeSoundPlayer({data: {buffer: soundBuffer}}); } /** @@ -774,26 +772,34 @@ class Scratch3MusicBlocks { */ _playDrumNum (util, drumNum) { if (util.runtime.audioEngine === null) return; - if (util.target.audioPlayer === null) return; + if (util.target.sprite.soundBank === null) return; // If we're playing too many sounds, do not play the drum sound. if (this._concurrencyCounter > Scratch3MusicBlocks.CONCURRENCY_LIMIT) { return; } - const outputNode = util.target.audioPlayer.getInputNode(); - const context = util.runtime.audioEngine.audioContext; - const bufferSource = context.createBufferSource(); - bufferSource.buffer = this._drumBuffers[drumNum]; - bufferSource.connect(outputNode); - bufferSource.start(); - const bufferSourceIndex = this._bufferSources.length; - this._bufferSources.push(bufferSource); + const player = this._drumPlayers[drumNum]; + + if (typeof player === 'undefined') return; + + if (player.isPlaying) { + // Take the internal player state and create a new player with it. + // `.play` does this internally but then instructs the sound to + // stop. + player.take(); + } + + const engine = util.runtime.audioEngine; + const chain = engine.createEffectChain(); + chain.setEffectsFromTarget(util.target); + player.connect(chain); this._concurrencyCounter++; - bufferSource.onended = () => { + player.once('stop', () => { this._concurrencyCounter--; - delete this._bufferSources[bufferSourceIndex]; - }; + }); + + player.play(); } /** @@ -852,7 +858,7 @@ class Scratch3MusicBlocks { */ _playNote (util, note, durationSec) { if (util.runtime.audioEngine === null) return; - if (util.target.audioPlayer === null) return; + if (util.target.sprite.soundBank === null) return; // If we're playing too many sounds, do not play the note. if (this._concurrencyCounter > Scratch3MusicBlocks.CONCURRENCY_LIMIT) { @@ -867,28 +873,37 @@ class Scratch3MusicBlocks { const sampleIndex = this._selectSampleIndexForNote(note, sampleArray); // If the audio sample has not loaded yet, bail out - if (typeof this._instrumentBufferArrays[inst] === 'undefined') return; - if (typeof this._instrumentBufferArrays[inst][sampleIndex] === 'undefined') return; + if (typeof this._instrumentPlayerArrays[inst] === 'undefined') return; + if (typeof this._instrumentPlayerArrays[inst][sampleIndex] === 'undefined') return; - // Create the audio buffer to play the note, and set its pitch - const context = util.runtime.audioEngine.audioContext; - const bufferSource = context.createBufferSource(); + // Fetch the sound player to play the note. + const engine = util.runtime.audioEngine; - const bufferSourceIndex = this._bufferSources.length; - this._bufferSources.push(bufferSource); + if (!this._instrumentPlayerNoteArrays[inst][note]) { + this._instrumentPlayerNoteArrays[inst][note] = this._instrumentPlayerArrays[inst][sampleIndex].take(); + } - bufferSource.buffer = this._instrumentBufferArrays[inst][sampleIndex]; - const sampleNote = sampleArray[sampleIndex]; - bufferSource.playbackRate.value = this._ratioForPitchInterval(note - sampleNote); + const player = this._instrumentPlayerNoteArrays[inst][note]; - // Create a gain node for this note, and connect it to the sprite's audioPlayer. - const gainNode = context.createGain(); - bufferSource.connect(gainNode); - const outputNode = util.target.audioPlayer.getInputNode(); - gainNode.connect(outputNode); + if (player.isPlaying) { + // Take the internal player state and create a new player with it. + // `.play` does this internally but then instructs the sound to + // stop. + player.take(); + } - // Start playing the note - bufferSource.start(); + const chain = engine.createEffectChain(); + chain.setEffectsFromTarget(util.target); + + // Set its pitch. + const sampleNote = sampleArray[sampleIndex]; + const notePitchInterval = this._ratioForPitchInterval(note - sampleNote); + + // Create a gain node for this note, and connect it to the sprite's + // simulated effectChain. + const context = engine.audioContext; + const releaseGain = context.createGain(); + releaseGain.connect(chain.getInputNode()); // Schedule the release of the note, ramping its gain down to zero, // and then stopping the sound. @@ -898,16 +913,24 @@ class Scratch3MusicBlocks { } const releaseStart = context.currentTime + durationSec; const releaseEnd = releaseStart + releaseDuration; - gainNode.gain.setValueAtTime(1, releaseStart); - gainNode.gain.linearRampToValueAtTime(0.0001, releaseEnd); - bufferSource.stop(releaseEnd); + releaseGain.gain.setValueAtTime(1, releaseStart); + releaseGain.gain.linearRampToValueAtTime(0.0001, releaseEnd); - // Update the concurrency counter this._concurrencyCounter++; - bufferSource.onended = () => { + player.once('stop', () => { this._concurrencyCounter--; - delete this._bufferSources[bufferSourceIndex]; - }; + }); + + // Start playing the note + player.play(); + // Connect the player to the gain node. + player.connect({getInputNode () { + return releaseGain; + }}); + // Set playback now after play creates the outputNode. + player.outputNode.playbackRate.value = notePitchInterval; + // Schedule playback to stop. + player.outputNode.stop(releaseEnd); } /**