Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

scratch-audio

Package Overview
Dependencies
Maintainers
1
Versions
432
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

scratch-audio - npm Package Compare versions

Comparing version 0.1.0-prerelease.1527803318 to 0.1.0-prerelease.1528210666

src/AudioEngine.js

906

dist.js

@@ -444,2 +444,514 @@ module.exports =

/***/ "./src/AudioEngine.js":
/*!****************************!*\
!*** ./src/AudioEngine.js ***!
\****************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var StartAudioContext = __webpack_require__(/*! startaudiocontext */ "startaudiocontext");
var AudioContext = __webpack_require__(/*! audio-context */ "audio-context");
var log = __webpack_require__(/*! ./log */ "./src/log.js");
var uid = __webpack_require__(/*! ./uid */ "./src/uid.js");
var ADPCMSoundDecoder = __webpack_require__(/*! ./ADPCMSoundDecoder */ "./src/ADPCMSoundDecoder.js");
var AudioPlayer = __webpack_require__(/*! ./AudioPlayer */ "./src/AudioPlayer.js");
var Loudness = __webpack_require__(/*! ./Loudness */ "./src/Loudness.js");
/**
* Wrapper to ensure that audioContext.decodeAudioData is a promise
* @param {object} audioContext The current AudioContext
* @param {ArrayBuffer} buffer Audio data buffer to decode
* @return {Promise} A promise that resolves to the decoded audio
*/
var decodeAudioData = function decodeAudioData(audioContext, buffer) {
// Check for newer promise-based API
if (audioContext.decodeAudioData.length === 1) {
return audioContext.decodeAudioData(buffer);
}
// Fall back to callback API
return new Promise(function (resolve, reject) {
audioContext.decodeAudioData(buffer, function (decodedAudio) {
return resolve(decodedAudio);
}, function (error) {
return reject(error);
});
});
};
/**
* There is a single instance of the AudioEngine. It handles global audio
* properties and effects, loads all the audio buffers for sounds belonging to
* sprites.
*/
var AudioEngine = function () {
function AudioEngine() {
_classCallCheck(this, AudioEngine);
/**
* AudioContext to play and manipulate sounds with a graph of source
* and effect nodes.
* @type {AudioContext}
*/
this.audioContext = new AudioContext();
StartAudioContext(this.audioContext);
/**
* Master GainNode that all sounds plays through. Changing this node
* will change the volume for all sounds.
* @type {GainNode}
*/
this.input = this.audioContext.createGain();
this.input.connect(this.audioContext.destination);
/**
* a map of soundIds to audio buffers, holding sounds for all sprites
* @type {Object<String, ArrayBuffer>}
*/
this.audioBuffers = {};
/**
* A Loudness detector.
* @type {Loudness}
*/
this.loudness = null;
}
/**
* Names of the audio effects.
* @enum {string}
*/
_createClass(AudioEngine, [{
key: 'decodeSound',
/**
* Decode a sound, decompressing it into audio samples.
* Store a reference to it the sound in the audioBuffers dictionary, indexed by soundId
* @param {object} sound - an object containing audio data and metadata for a sound
* @property {Buffer} data - sound data loaded from scratch-storage.
* @returns {?Promise} - a promise which will resolve to the soundId if decoded and stored.
*/
value: function decodeSound(sound) {
var _this = this;
// Make a copy of the buffer because decoding detaches the original buffer
var bufferCopy1 = sound.data.buffer.slice(0);
var soundId = uid();
// Partially apply updateSoundBuffer function with the current
// soundId so that it's ready to be used on successfully decoded audio
var addDecodedAudio = this.updateSoundBuffer.bind(this, soundId);
// Attempt to decode the sound using the browser's native audio data decoder
// If that fails, attempt to decode as ADPCM
return decodeAudioData(this.audioContext, bufferCopy1).then(addDecodedAudio, function () {
// The audio context failed to parse the sound data
// we gave it, so try to decode as 'adpcm'
// First we need to create another copy of our original data
var bufferCopy2 = sound.data.buffer.slice(0);
// Try decoding as adpcm
return new ADPCMSoundDecoder(_this.audioContext).decode(bufferCopy2).then(addDecodedAudio, function (error) {
log.warn('audio data could not be decoded', error);
});
});
}
/**
* Retrieve the audio buffer as held in memory for a given sound id.
* @param {!string} soundId - the id of the sound buffer to get
* @return {AudioBuffer} the buffer corresponding to the given sound id.
*/
}, {
key: 'getSoundBuffer',
value: function getSoundBuffer(soundId) {
return this.audioBuffers[soundId];
}
/**
* Add or update the in-memory audio buffer to a new one by soundId.
* @param {!string} soundId - the id of the sound buffer to update.
* @param {AudioBuffer} newBuffer - the new buffer to swap in.
* @return {string} The uid of the sound that was updated or added
*/
}, {
key: 'updateSoundBuffer',
value: function updateSoundBuffer(soundId, newBuffer) {
this.audioBuffers[soundId] = newBuffer;
return soundId;
}
/**
* An older version of the AudioEngine had this function to load all sounds
* This is a stub to provide a warning when it is called
* @todo remove this
*/
}, {
key: 'loadSounds',
value: function loadSounds() {
log.warn('The loadSounds function is no longer available. Please use Scratch Storage.');
}
/**
* Get the current loudness of sound received by the microphone.
* Sound is measured in RMS and smoothed.
* @return {number} loudness scaled 0 to 100
*/
}, {
key: 'getLoudness',
value: function getLoudness() {
// The microphone has not been set up, so try to connect to it
if (!this.loudness) {
this.loudness = new Loudness(this.audioContext);
}
return this.loudness.getLoudness();
}
/**
* Create an AudioPlayer. Each sprite or clone has an AudioPlayer.
* It includes a reference to the AudioEngine so it can use global
* functionality such as playing notes.
* @return {AudioPlayer} new AudioPlayer instance
*/
}, {
key: 'createPlayer',
value: function createPlayer() {
return new AudioPlayer(this);
}
}, {
key: 'EFFECT_NAMES',
get: function get() {
return {
pitch: 'pitch',
pan: 'pan'
};
}
/**
* A short duration, for use as a time constant for exponential audio parameter transitions.
* See:
* https://developer.mozilla.org/en-US/docs/Web/API/AudioParam/setTargetAtTime
* @const {number}
*/
}, {
key: 'DECAY_TIME',
get: function get() {
return 0.001;
}
}]);
return AudioEngine;
}();
module.exports = AudioEngine;
/***/ }),
/***/ "./src/AudioPlayer.js":
/*!****************************!*\
!*** ./src/AudioPlayer.js ***!
\****************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var PitchEffect = __webpack_require__(/*! ./effects/PitchEffect */ "./src/effects/PitchEffect.js");
var PanEffect = __webpack_require__(/*! ./effects/PanEffect */ "./src/effects/PanEffect.js");
var SoundPlayer = __webpack_require__(/*! ./SoundPlayer */ "./src/SoundPlayer.js");
var AudioPlayer = function () {
/**
* Each sprite or clone has an audio player
* the audio player handles sound playback, volume, and the sprite-specific audio effects:
* pitch and pan
* @param {AudioEngine} audioEngine AudioEngine for player
* @constructor
*/
function AudioPlayer(audioEngine) {
_classCallCheck(this, AudioPlayer);
this.audioEngine = audioEngine;
// Create the audio effects
this.pitchEffect = new PitchEffect();
this.panEffect = new PanEffect(this.audioEngine);
// Chain the audio effects together
// effectsNode -> panEffect -> audioEngine.input
this.effectsNode = this.audioEngine.audioContext.createGain();
this.effectsNode.connect(this.panEffect.input);
this.panEffect.connect(this.audioEngine.input);
// reset effects to their default parameters
this.clearEffects();
// sound players that are currently playing, indexed by the sound's soundId
this.activeSoundPlayers = {};
}
/**
* Get this sprite's input node, so that other objects can route sound through it.
* @return {AudioNode} the AudioNode for this sprite's input
*/
_createClass(AudioPlayer, [{
key: 'getInputNode',
value: function getInputNode() {
return this.effectsNode;
}
/**
* Play a sound
* @param {string} soundId - the soundId id of a sound file
* @return {Promise} a Promise that resolves when the sound finishes playing
*/
}, {
key: 'playSound',
value: function playSound(soundId) {
// if this sound is not in the audio engine, return
if (!this.audioEngine.audioBuffers[soundId]) {
return;
}
// if this sprite or clone is already playing this sound, stop it first
if (this.activeSoundPlayers[soundId]) {
this.activeSoundPlayers[soundId].stop();
}
// create a new soundplayer to play the sound
var player = new SoundPlayer(this.audioEngine.audioContext);
player.setBuffer(this.audioEngine.audioBuffers[soundId]);
player.connect(this.effectsNode);
this.pitchEffect.updatePlayer(player);
player.start();
// add it to the list of active sound players
this.activeSoundPlayers[soundId] = player;
// remove sounds that are not playing from the active sound players array
for (var id in this.activeSoundPlayers) {
if (this.activeSoundPlayers.hasOwnProperty(id)) {
if (!this.activeSoundPlayers[id].isPlaying) {
delete this.activeSoundPlayers[id];
}
}
}
return player.finished();
}
/**
* Stop all sounds that are playing
*/
}, {
key: 'stopAllSounds',
value: function stopAllSounds() {
// stop all active sound players
for (var soundId in this.activeSoundPlayers) {
this.activeSoundPlayers[soundId].stop();
}
}
/**
* Set an audio effect to a value
* @param {string} effect - the name of the effect
* @param {number} value - the value to set the effect to
*/
}, {
key: 'setEffect',
value: function setEffect(effect, value) {
switch (effect) {
case this.audioEngine.EFFECT_NAMES.pitch:
this.pitchEffect.set(value, this.activeSoundPlayers);
break;
case this.audioEngine.EFFECT_NAMES.pan:
this.panEffect.set(value);
break;
}
}
/**
* Clear all audio effects
*/
}, {
key: 'clearEffects',
value: function clearEffects() {
this.panEffect.set(0);
this.pitchEffect.set(0, this.activeSoundPlayers);
if (this.audioEngine === null) return;
this.effectsNode.gain.setTargetAtTime(1.0, 0, this.audioEngine.DECAY_TIME);
}
/**
* Set the volume for sounds played by this AudioPlayer
* @param {number} value - the volume in range 0-100
*/
}, {
key: 'setVolume',
value: function setVolume(value) {
if (this.audioEngine === null) return;
this.effectsNode.gain.setTargetAtTime(value / 100, 0, this.audioEngine.DECAY_TIME);
}
/**
* Clean up and disconnect audio nodes.
*/
}, {
key: 'dispose',
value: function dispose() {
this.panEffect.dispose();
this.effectsNode.disconnect();
}
}]);
return AudioPlayer;
}();
module.exports = AudioPlayer;
/***/ }),
/***/ "./src/Loudness.js":
/*!*************************!*\
!*** ./src/Loudness.js ***!
\*************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var log = __webpack_require__(/*! ./log */ "./src/log.js");
var Loudness = function () {
/**
* Instrument and detect a loudness value from a local microphone.
* @param {AudioContext} audioContext - context to create nodes from for
* detecting loudness
* @constructor
*/
function Loudness(audioContext) {
_classCallCheck(this, Loudness);
/**
* AudioContext the mic will connect to and provide analysis of
* @type {AudioContext}
*/
this.audioContext = audioContext;
/**
* Are we connecting to the mic yet?
* @type {Boolean}
*/
this.connectingToMic = false;
/**
* microphone, for measuring loudness, with a level meter analyzer
* @type {MediaStreamSourceNode}
*/
this.mic = null;
}
/**
* Get the current loudness of sound received by the microphone.
* Sound is measured in RMS and smoothed.
* Some code adapted from Tone.js: https://github.com/Tonejs/Tone.js
* @return {number} loudness scaled 0 to 100
*/
_createClass(Loudness, [{
key: 'getLoudness',
value: function getLoudness() {
var _this = this;
// The microphone has not been set up, so try to connect to it
if (!this.mic && !this.connectingToMic) {
this.connectingToMic = true; // prevent multiple connection attempts
navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) {
_this.audioStream = stream;
_this.mic = _this.audioContext.createMediaStreamSource(stream);
_this.analyser = _this.audioContext.createAnalyser();
_this.mic.connect(_this.analyser);
_this.micDataArray = new Float32Array(_this.analyser.fftSize);
}).catch(function (err) {
log.warn(err);
});
}
// If the microphone is set up and active, measure the loudness
if (this.mic && this.audioStream.active) {
this.analyser.getFloatTimeDomainData(this.micDataArray);
var sum = 0;
// compute the RMS of the sound
for (var i = 0; i < this.micDataArray.length; i++) {
sum += Math.pow(this.micDataArray[i], 2);
}
var rms = Math.sqrt(sum / this.micDataArray.length);
// smooth the value, if it is descending
if (this._lastValue) {
rms = Math.max(rms, this._lastValue * 0.6);
}
this._lastValue = rms;
// Scale the measurement so it's more sensitive to quieter sounds
rms *= 1.63;
rms = Math.sqrt(rms);
// Scale it up to 0-100 and round
rms = Math.round(rms * 100);
// Prevent it from going above 100
rms = Math.min(rms, 100);
return rms;
}
// if there is no microphone input, return -1
return -1;
}
}]);
return Loudness;
}();
module.exports = Loudness;
/***/ }),
/***/ "./src/SoundPlayer.js":

@@ -807,396 +1319,10 @@ /*!****************************!*\

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var StartAudioContext = __webpack_require__(/*! startaudiocontext */ "startaudiocontext");
var AudioContext = __webpack_require__(/*! audio-context */ "audio-context");
var log = __webpack_require__(/*! ./log */ "./src/log.js");
var uid = __webpack_require__(/*! ./uid */ "./src/uid.js");
var PitchEffect = __webpack_require__(/*! ./effects/PitchEffect */ "./src/effects/PitchEffect.js");
var PanEffect = __webpack_require__(/*! ./effects/PanEffect */ "./src/effects/PanEffect.js");
var SoundPlayer = __webpack_require__(/*! ./SoundPlayer */ "./src/SoundPlayer.js");
var ADPCMSoundDecoder = __webpack_require__(/*! ./ADPCMSoundDecoder */ "./src/ADPCMSoundDecoder.js");
/**
* @fileOverview Scratch Audio is divided into a single AudioEngine,
* that handles global functionality, and AudioPlayers, belonging to individual sprites and clones.
* @fileOverview Scratch Audio is divided into a single AudioEngine, that
* handles global functionality, and AudioPlayers, belonging to individual
* sprites and clones.
*/
/**
* Wrapper to ensure that audioContext.decodeAudioData is a promise
* @param {object} audioContext The current AudioContext
* @param {ArrayBuffer} buffer Audio data buffer to decode
* @return {Promise} A promise that resolves to the decoded audio
*/
var decodeAudioData = function decodeAudioData(audioContext, buffer) {
// Check for newer promise-based API
if (audioContext.decodeAudioData.length === 1) {
return audioContext.decodeAudioData(buffer);
}
// Fall back to callback API
return new Promise(function (resolve, reject) {
audioContext.decodeAudioData(buffer, function (decodedAudio) {
return resolve(decodedAudio);
}, function (error) {
return reject(error);
});
});
};
var AudioEngine = __webpack_require__(/*! ./AudioEngine */ "./src/AudioEngine.js");
var AudioPlayer = function () {
/**
* Each sprite or clone has an audio player
* the audio player handles sound playback, volume, and the sprite-specific audio effects:
* pitch and pan
* @param {AudioEngine} audioEngine AudioEngine for player
* @constructor
*/
function AudioPlayer(audioEngine) {
_classCallCheck(this, AudioPlayer);
this.audioEngine = audioEngine;
// Create the audio effects
this.pitchEffect = new PitchEffect();
this.panEffect = new PanEffect(this.audioEngine);
// Chain the audio effects together
// effectsNode -> panEffect -> audioEngine.input
this.effectsNode = this.audioEngine.audioContext.createGain();
this.effectsNode.connect(this.panEffect.input);
this.panEffect.connect(this.audioEngine.input);
// reset effects to their default parameters
this.clearEffects();
// sound players that are currently playing, indexed by the sound's soundId
this.activeSoundPlayers = {};
}
/**
* Get this sprite's input node, so that other objects can route sound through it.
* @return {AudioNode} the AudioNode for this sprite's input
*/
_createClass(AudioPlayer, [{
key: 'getInputNode',
value: function getInputNode() {
return this.effectsNode;
}
/**
* Play a sound
* @param {string} soundId - the soundId id of a sound file
* @return {Promise} a Promise that resolves when the sound finishes playing
*/
}, {
key: 'playSound',
value: function playSound(soundId) {
// if this sound is not in the audio engine, return
if (!this.audioEngine.audioBuffers[soundId]) {
return;
}
// if this sprite or clone is already playing this sound, stop it first
if (this.activeSoundPlayers[soundId]) {
this.activeSoundPlayers[soundId].stop();
}
// create a new soundplayer to play the sound
var player = new SoundPlayer(this.audioEngine.audioContext);
player.setBuffer(this.audioEngine.audioBuffers[soundId]);
player.connect(this.effectsNode);
this.pitchEffect.updatePlayer(player);
player.start();
// add it to the list of active sound players
this.activeSoundPlayers[soundId] = player;
// remove sounds that are not playing from the active sound players array
for (var id in this.activeSoundPlayers) {
if (this.activeSoundPlayers.hasOwnProperty(id)) {
if (!this.activeSoundPlayers[id].isPlaying) {
delete this.activeSoundPlayers[id];
}
}
}
return player.finished();
}
/**
* Stop all sounds that are playing
*/
}, {
key: 'stopAllSounds',
value: function stopAllSounds() {
// stop all active sound players
for (var soundId in this.activeSoundPlayers) {
this.activeSoundPlayers[soundId].stop();
}
}
/**
* Set an audio effect to a value
* @param {string} effect - the name of the effect
* @param {number} value - the value to set the effect to
*/
}, {
key: 'setEffect',
value: function setEffect(effect, value) {
switch (effect) {
case this.audioEngine.EFFECT_NAMES.pitch:
this.pitchEffect.set(value, this.activeSoundPlayers);
break;
case this.audioEngine.EFFECT_NAMES.pan:
this.panEffect.set(value);
break;
}
}
/**
* Clear all audio effects
*/
}, {
key: 'clearEffects',
value: function clearEffects() {
this.panEffect.set(0);
this.pitchEffect.set(0, this.activeSoundPlayers);
if (this.audioEngine === null) return;
this.effectsNode.gain.setTargetAtTime(1.0, 0, this.audioEngine.DECAY_TIME);
}
/**
* Set the volume for sounds played by this AudioPlayer
* @param {number} value - the volume in range 0-100
*/
}, {
key: 'setVolume',
value: function setVolume(value) {
if (this.audioEngine === null) return;
this.effectsNode.gain.setTargetAtTime(value / 100, 0, this.audioEngine.DECAY_TIME);
}
/**
* Clean up and disconnect audio nodes.
*/
}, {
key: 'dispose',
value: function dispose() {
this.panEffect.dispose();
this.effectsNode.disconnect();
}
}]);
return AudioPlayer;
}();
/**
* There is a single instance of the AudioEngine. It handles global audio properties and effects,
* loads all the audio buffers for sounds belonging to sprites.
*/
var AudioEngine = function () {
function AudioEngine() {
_classCallCheck(this, AudioEngine);
this.audioContext = new AudioContext();
StartAudioContext(this.audioContext);
this.input = this.audioContext.createGain();
this.input.connect(this.audioContext.destination);
// a map of soundIds to audio buffers, holding sounds for all sprites
this.audioBuffers = {};
// microphone, for measuring loudness, with a level meter analyzer
this.mic = null;
}
/**
* Names of the audio effects.
* @enum {string}
*/
_createClass(AudioEngine, [{
key: 'decodeSound',
/**
* Decode a sound, decompressing it into audio samples.
* Store a reference to it the sound in the audioBuffers dictionary, indexed by soundId
* @param {object} sound - an object containing audio data and metadata for a sound
* @property {Buffer} data - sound data loaded from scratch-storage.
* @returns {?Promise} - a promise which will resolve to the soundId if decoded and stored.
*/
value: function decodeSound(sound) {
var _this = this;
// Make a copy of the buffer because decoding detaches the original buffer
var bufferCopy1 = sound.data.buffer.slice(0);
var soundId = uid();
// Partially apply updateSoundBuffer function with the current
// soundId so that it's ready to be used on successfully decoded audio
var addDecodedAudio = this.updateSoundBuffer.bind(this, soundId);
// Attempt to decode the sound using the browser's native audio data decoder
// If that fails, attempt to decode as ADPCM
return decodeAudioData(this.audioContext, bufferCopy1).then(addDecodedAudio, function () {
// The audio context failed to parse the sound data
// we gave it, so try to decode as 'adpcm'
// First we need to create another copy of our original data
var bufferCopy2 = sound.data.buffer.slice(0);
// Try decoding as adpcm
return new ADPCMSoundDecoder(_this.audioContext).decode(bufferCopy2).then(addDecodedAudio, function (error) {
log.warn('audio data could not be decoded', error);
});
});
}
/**
* Retrieve the audio buffer as held in memory for a given sound id.
* @param {!string} soundId - the id of the sound buffer to get
* @return {AudioBuffer} the buffer corresponding to the given sound id.
*/
}, {
key: 'getSoundBuffer',
value: function getSoundBuffer(soundId) {
return this.audioBuffers[soundId];
}
/**
* Add or update the in-memory audio buffer to a new one by soundId.
* @param {!string} soundId - the id of the sound buffer to update.
* @param {AudioBuffer} newBuffer - the new buffer to swap in.
* @return {string} The uid of the sound that was updated or added
*/
}, {
key: 'updateSoundBuffer',
value: function updateSoundBuffer(soundId, newBuffer) {
this.audioBuffers[soundId] = newBuffer;
return soundId;
}
/**
* An older version of the AudioEngine had this function to load all sounds
* This is a stub to provide a warning when it is called
* @todo remove this
*/
}, {
key: 'loadSounds',
value: function loadSounds() {
log.warn('The loadSounds function is no longer available. Please use Scratch Storage.');
}
/**
* Get the current loudness of sound received by the microphone.
* Sound is measured in RMS and smoothed.
* Some code adapted from Tone.js: https://github.com/Tonejs/Tone.js
* @return {number} loudness scaled 0 to 100
*/
}, {
key: 'getLoudness',
value: function getLoudness() {
var _this2 = this;
// The microphone has not been set up, so try to connect to it
if (!this.mic && !this.connectingToMic) {
this.connectingToMic = true; // prevent multiple connection attempts
navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) {
_this2.audioStream = stream;
_this2.mic = _this2.audioContext.createMediaStreamSource(stream);
_this2.analyser = _this2.audioContext.createAnalyser();
_this2.mic.connect(_this2.analyser);
_this2.micDataArray = new Float32Array(_this2.analyser.fftSize);
}).catch(function (err) {
log.warn(err);
});
}
// If the microphone is set up and active, measure the loudness
if (this.mic && this.audioStream.active) {
this.analyser.getFloatTimeDomainData(this.micDataArray);
var sum = 0;
// compute the RMS of the sound
for (var i = 0; i < this.micDataArray.length; i++) {
sum += Math.pow(this.micDataArray[i], 2);
}
var rms = Math.sqrt(sum / this.micDataArray.length);
// smooth the value, if it is descending
if (this._lastValue) {
rms = Math.max(rms, this._lastValue * 0.6);
}
this._lastValue = rms;
// Scale the measurement so it's more sensitive to quieter sounds
rms *= 1.63;
rms = Math.sqrt(rms);
// Scale it up to 0-100 and round
rms = Math.round(rms * 100);
// Prevent it from going above 100
rms = Math.min(rms, 100);
return rms;
}
// if there is no microphone input, return -1
return -1;
}
/**
* Create an AudioPlayer. Each sprite or clone has an AudioPlayer.
* It includes a reference to the AudioEngine so it can use global
* functionality such as playing notes.
* @return {AudioPlayer} new AudioPlayer instance
*/
}, {
key: 'createPlayer',
value: function createPlayer() {
return new AudioPlayer(this);
}
}, {
key: 'EFFECT_NAMES',
get: function get() {
return {
pitch: 'pitch',
pan: 'pan'
};
}
/**
* A short duration, for use as a time constant for exponential audio parameter transitions.
* See:
* https://developer.mozilla.org/en-US/docs/Web/API/AudioParam/setTargetAtTime
* @const {number}
*/
}, {
key: 'DECAY_TIME',
get: function get() {
return 0.001;
}
}]);
return AudioEngine;
}();
module.exports = AudioEngine;

@@ -1203,0 +1329,0 @@

{
"name": "scratch-audio",
"version": "0.1.0-prerelease.1527803318",
"version": "0.1.0-prerelease.1528210666",
"description": "audio engine for scratch 3.0",

@@ -5,0 +5,0 @@ "main": "dist.js",

@@ -1,337 +0,9 @@

const StartAudioContext = require('startaudiocontext');
const AudioContext = require('audio-context');
const log = require('./log');
const uid = require('./uid');
const PitchEffect = require('./effects/PitchEffect');
const PanEffect = require('./effects/PanEffect');
const SoundPlayer = require('./SoundPlayer');
const ADPCMSoundDecoder = require('./ADPCMSoundDecoder');
/**
* @fileOverview Scratch Audio is divided into a single AudioEngine,
* that handles global functionality, and AudioPlayers, belonging to individual sprites and clones.
* @fileOverview Scratch Audio is divided into a single AudioEngine, that
* handles global functionality, and AudioPlayers, belonging to individual
* sprites and clones.
*/
/**
* Wrapper to ensure that audioContext.decodeAudioData is a promise
* @param {object} audioContext The current AudioContext
* @param {ArrayBuffer} buffer Audio data buffer to decode
* @return {Promise} A promise that resolves to the decoded audio
*/
const decodeAudioData = function (audioContext, buffer) {
// Check for newer promise-based API
if (audioContext.decodeAudioData.length === 1) {
return audioContext.decodeAudioData(buffer);
}
// Fall back to callback API
return new Promise((resolve, reject) => {
audioContext.decodeAudioData(buffer,
decodedAudio => resolve(decodedAudio),
error => reject(error)
);
});
};
const AudioEngine = require('./AudioEngine');
class AudioPlayer {
/**
* Each sprite or clone has an audio player
* the audio player handles sound playback, volume, and the sprite-specific audio effects:
* pitch and pan
* @param {AudioEngine} audioEngine AudioEngine for player
* @constructor
*/
constructor (audioEngine) {
this.audioEngine = audioEngine;
// Create the audio effects
this.pitchEffect = new PitchEffect();
this.panEffect = new PanEffect(this.audioEngine);
// Chain the audio effects together
// effectsNode -> panEffect -> audioEngine.input
this.effectsNode = this.audioEngine.audioContext.createGain();
this.effectsNode.connect(this.panEffect.input);
this.panEffect.connect(this.audioEngine.input);
// reset effects to their default parameters
this.clearEffects();
// sound players that are currently playing, indexed by the sound's soundId
this.activeSoundPlayers = {};
}
/**
* Get this sprite's input node, so that other objects can route sound through it.
* @return {AudioNode} the AudioNode for this sprite's input
*/
getInputNode () {
return this.effectsNode;
}
/**
* Play a sound
* @param {string} soundId - the soundId id of a sound file
* @return {Promise} a Promise that resolves when the sound finishes playing
*/
playSound (soundId) {
// if this sound is not in the audio engine, return
if (!this.audioEngine.audioBuffers[soundId]) {
return;
}
// if this sprite or clone is already playing this sound, stop it first
if (this.activeSoundPlayers[soundId]) {
this.activeSoundPlayers[soundId].stop();
}
// create a new soundplayer to play the sound
const player = new SoundPlayer(this.audioEngine.audioContext);
player.setBuffer(this.audioEngine.audioBuffers[soundId]);
player.connect(this.effectsNode);
this.pitchEffect.updatePlayer(player);
player.start();
// add it to the list of active sound players
this.activeSoundPlayers[soundId] = player;
// remove sounds that are not playing from the active sound players array
for (const id in this.activeSoundPlayers) {
if (this.activeSoundPlayers.hasOwnProperty(id)) {
if (!this.activeSoundPlayers[id].isPlaying) {
delete this.activeSoundPlayers[id];
}
}
}
return player.finished();
}
/**
* Stop all sounds that are playing
*/
stopAllSounds () {
// stop all active sound players
for (const soundId in this.activeSoundPlayers) {
this.activeSoundPlayers[soundId].stop();
}
}
/**
* Set an audio effect to a value
* @param {string} effect - the name of the effect
* @param {number} value - the value to set the effect to
*/
setEffect (effect, value) {
switch (effect) {
case this.audioEngine.EFFECT_NAMES.pitch:
this.pitchEffect.set(value, this.activeSoundPlayers);
break;
case this.audioEngine.EFFECT_NAMES.pan:
this.panEffect.set(value);
break;
}
}
/**
* Clear all audio effects
*/
clearEffects () {
this.panEffect.set(0);
this.pitchEffect.set(0, this.activeSoundPlayers);
if (this.audioEngine === null) return;
this.effectsNode.gain.setTargetAtTime(1.0, 0, this.audioEngine.DECAY_TIME);
}
/**
* Set the volume for sounds played by this AudioPlayer
* @param {number} value - the volume in range 0-100
*/
setVolume (value) {
if (this.audioEngine === null) return;
this.effectsNode.gain.setTargetAtTime(value / 100, 0, this.audioEngine.DECAY_TIME);
}
/**
* Clean up and disconnect audio nodes.
*/
dispose () {
this.panEffect.dispose();
this.effectsNode.disconnect();
}
}
/**
* There is a single instance of the AudioEngine. It handles global audio properties and effects,
* loads all the audio buffers for sounds belonging to sprites.
*/
class AudioEngine {
constructor () {
this.audioContext = new AudioContext();
StartAudioContext(this.audioContext);
this.input = this.audioContext.createGain();
this.input.connect(this.audioContext.destination);
// a map of soundIds to audio buffers, holding sounds for all sprites
this.audioBuffers = {};
// microphone, for measuring loudness, with a level meter analyzer
this.mic = null;
}
/**
* Names of the audio effects.
* @enum {string}
*/
get EFFECT_NAMES () {
return {
pitch: 'pitch',
pan: 'pan'
};
}
/**
* A short duration, for use as a time constant for exponential audio parameter transitions.
* See:
* https://developer.mozilla.org/en-US/docs/Web/API/AudioParam/setTargetAtTime
* @const {number}
*/
get DECAY_TIME () {
return 0.001;
}
/**
* Decode a sound, decompressing it into audio samples.
* Store a reference to it the sound in the audioBuffers dictionary, indexed by soundId
* @param {object} sound - an object containing audio data and metadata for a sound
* @property {Buffer} data - sound data loaded from scratch-storage.
* @returns {?Promise} - a promise which will resolve to the soundId if decoded and stored.
*/
decodeSound (sound) {
// Make a copy of the buffer because decoding detaches the original buffer
const bufferCopy1 = sound.data.buffer.slice(0);
const soundId = uid();
// Partially apply updateSoundBuffer function with the current
// soundId so that it's ready to be used on successfully decoded audio
const addDecodedAudio = this.updateSoundBuffer.bind(this, soundId);
// Attempt to decode the sound using the browser's native audio data decoder
// If that fails, attempt to decode as ADPCM
return decodeAudioData(this.audioContext, bufferCopy1).then(
addDecodedAudio,
() => {
// The audio context failed to parse the sound data
// we gave it, so try to decode as 'adpcm'
// First we need to create another copy of our original data
const bufferCopy2 = sound.data.buffer.slice(0);
// Try decoding as adpcm
return (new ADPCMSoundDecoder(this.audioContext)).decode(bufferCopy2)
.then(
addDecodedAudio,
error => {
log.warn('audio data could not be decoded', error);
}
);
}
);
}
/**
* Retrieve the audio buffer as held in memory for a given sound id.
* @param {!string} soundId - the id of the sound buffer to get
* @return {AudioBuffer} the buffer corresponding to the given sound id.
*/
getSoundBuffer (soundId) {
return this.audioBuffers[soundId];
}
/**
* Add or update the in-memory audio buffer to a new one by soundId.
* @param {!string} soundId - the id of the sound buffer to update.
* @param {AudioBuffer} newBuffer - the new buffer to swap in.
* @return {string} The uid of the sound that was updated or added
*/
updateSoundBuffer (soundId, newBuffer) {
this.audioBuffers[soundId] = newBuffer;
return soundId;
}
/**
* An older version of the AudioEngine had this function to load all sounds
* This is a stub to provide a warning when it is called
* @todo remove this
*/
loadSounds () {
log.warn('The loadSounds function is no longer available. Please use Scratch Storage.');
}
/**
* Get the current loudness of sound received by the microphone.
* Sound is measured in RMS and smoothed.
* Some code adapted from Tone.js: https://github.com/Tonejs/Tone.js
* @return {number} loudness scaled 0 to 100
*/
getLoudness () {
// The microphone has not been set up, so try to connect to it
if (!this.mic && !this.connectingToMic) {
this.connectingToMic = true; // prevent multiple connection attempts
navigator.mediaDevices.getUserMedia({audio: true}).then(stream => {
this.audioStream = stream;
this.mic = this.audioContext.createMediaStreamSource(stream);
this.analyser = this.audioContext.createAnalyser();
this.mic.connect(this.analyser);
this.micDataArray = new Float32Array(this.analyser.fftSize);
})
.catch(err => {
log.warn(err);
});
}
// If the microphone is set up and active, measure the loudness
if (this.mic && this.audioStream.active) {
this.analyser.getFloatTimeDomainData(this.micDataArray);
let sum = 0;
// compute the RMS of the sound
for (let i = 0; i < this.micDataArray.length; i++){
sum += Math.pow(this.micDataArray[i], 2);
}
let rms = Math.sqrt(sum / this.micDataArray.length);
// smooth the value, if it is descending
if (this._lastValue) {
rms = Math.max(rms, this._lastValue * 0.6);
}
this._lastValue = rms;
// Scale the measurement so it's more sensitive to quieter sounds
rms *= 1.63;
rms = Math.sqrt(rms);
// Scale it up to 0-100 and round
rms = Math.round(rms * 100);
// Prevent it from going above 100
rms = Math.min(rms, 100);
return rms;
}
// if there is no microphone input, return -1
return -1;
}
/**
* Create an AudioPlayer. Each sprite or clone has an AudioPlayer.
* It includes a reference to the AudioEngine so it can use global
* functionality such as playing notes.
* @return {AudioPlayer} new AudioPlayer instance
*/
createPlayer () {
return new AudioPlayer(this);
}
}
module.exports = AudioEngine;

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc