audio2d
Version:
Easy to use API to add the power of web audio to your game.
802 lines (690 loc) • 18.2 kB
JavaScript
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var classCallCheck = _classCallCheck;
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);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
var createClass = _createClass;
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
var defineProperty = _defineProperty;
/**
* Provides an interface for adding web audio nodes.
*/
var Nodes = /*#__PURE__*/function () {
/**
* A reference to the AudioContext.
*
* @private
*
* @property {AudioContext}
*/
/**
* @param {AudioContext} ctx A reference to the AudioContext.
*/
function Nodes(ctx) {
classCallCheck(this, Nodes);
defineProperty(this, "_ctx", void 0);
this._ctx = ctx;
}
/**
* Creates a biquadFilter node.
*
* @returns {Node}
*/
createClass(Nodes, [{
key: "biquadFilter",
value: function biquadFilter() {
return {
name: 'biquadFilter',
instance: this._ctx.createBiquadFilter()
};
}
/**
* Creates a gain node.
*
* @returns {Node}
*/
}, {
key: "gain",
value: function gain() {
return {
name: 'gain',
instance: this._ctx.createGain()
};
}
}]);
return Nodes;
}();
/**
* The states that an audio clip can be in.
*/
var AudioClipState;
(function (AudioClipState) {
AudioClipState["STOPPED"] = "STOPPED";
AudioClipState["PLAYING"] = "PLAYING";
AudioClipState["PAUSED"] = "PAUSED";
})(AudioClipState || (AudioClipState = {}));
/**
* An audio clip represents a piece of audio, which is either an audio html element or an audio boffer, as
* a playable clip with extra properties.
*/
var AudioClip = /*#__PURE__*/function () {
/**
* The name of the audio clip.
*
* @private
*
* @property {string}
*/
/**
* A reference to the audio to play.
*
* @private
*
* @property
*/
/**
* The audio buffer source of the clip.
*
* @private
*
* @property {AudioBufferSourceNode}
*/
/**
* A reference to the options for this audio clip.
*
* @private
*
* @property {AudioClipOptions}
*/
/**
* A reference to the gain node for this clip.
*
* @private
*
* @property {GainNode}
*/
/**
* The current state of this clip.
*
* @private
*
* @property {AudioClipState}
*/
/**
* The number of times this clip has been played.
*
* @private
*
* @property
*/
/**
* The time that this clip start being played at.
*
* @private
*
* @property {number}
*/
/**
* When the clip is paused, this will keep track of when it was paused so it can be resumed at that time.
*
* @private
*
* @property {number}
*/
/**
* The current time of the clip.
*
* @private
*
* @property {number}
*/
/**
* The duration of the clip.
*
* @private
*
* @property {number}
*/
/**
* The volume of this audio clip.
*
* @private
*
* @property {number}
*
* @default 100
*/
/**
* Keeps track of the previous volume of the clip.
*
* @private
*
* @property {number}
*/
/**
* A reference to the nodes that have been added for this clip.
*
* @private
*
* @property {Array<Node>}
*/
/**
* A reference to the nodes that have been added in a way that allows them to be retrieved easily.
*
* @private
*
* @property {*}
*/
/**
* Indicates whether this audio clip is played on a loop or not.
*
* @property {boolean}
*
* @default false
*/
/**
* @param {string} name The name of the audio clip.
* @param {AudioBuffer} audio The AudioBuffer that contains the audio of the clip.
* @param {AudioClipOptions} [options] The options passed to this audio clip.
*/
function AudioClip(name, audio, options) {
classCallCheck(this, AudioClip);
defineProperty(this, "_name", void 0);
defineProperty(this, "_audio", void 0);
defineProperty(this, "_source", void 0);
defineProperty(this, "_options", void 0);
defineProperty(this, "_gain", void 0);
defineProperty(this, "_state", AudioClipState.STOPPED);
defineProperty(this, "_timesPlayed", 0);
defineProperty(this, "_timeStartedAt", 0);
defineProperty(this, "_timePausedAt", 0);
defineProperty(this, "_currentTime", 0);
defineProperty(this, "_duration", void 0);
defineProperty(this, "_volume", 100);
defineProperty(this, "_previousVolume", 1);
defineProperty(this, "_nodesref", []);
defineProperty(this, "_nodes", {});
defineProperty(this, "loop", false);
this._name = name;
this._audio = audio;
this._duration = audio.duration;
this._options = options;
this._gain = this._options.ctx.createGain();
this._gain.connect(this._options.ctx.destination);
if (this._options.trigger) this._setupTrigger();
if (!this._options.markers) this._options.markers = [];
}
/**
* Gets the name of the audio clip.
*
* @returns {string}
*/
createClass(AudioClip, [{
key: "addNode",
/**
* Adds a custom node from `app.nodes[nodeName]`.
*
* @param {Node} node The node to add to this clip.
*
* @example
*
* const track = a2d.addAudio('track-1', track1Buffer);
*
* const bf = a2d.nodes.biquadFilter();
* track.addNode(bf);
*/
value: function addNode(node) {
this._nodes[node.name] = node.instance;
this._nodesref.push(node);
if (this._nodesref.length === 1) return;
var latestNode = this._nodesref[this._nodesref.length - 2];
node.instance.connect(latestNode.instance);
}
/**
* Plays this audio clip.
*
* @param {string} marker The name of the marker of the part of the clip to play.
*
* @example
*
* const sfxMarkers = [{ name: 'walk', start: 1500, duration: 1000 }, { name: 'fall': start: 2500, duration: 1500 }];
* const sfx = a2d.addAudio('sfx', sfxBuffer, { markers: sxfMarkers });
*
* // play just the falling sound.
* sfx.play('fall');
*/
}, {
key: "play",
value: function play(marker) {
var offset = this._timePausedAt;
this._source = this._options.ctx.createBufferSource();
this._source.buffer = this._audio;
this._source.loop = this.loop;
this._connectNodes();
this._oncomplete();
if (marker) {
var _this$_options$marker;
var clipMarker = (_this$_options$marker = this._options.markers) === null || _this$_options$marker === void 0 ? void 0 : _this$_options$marker.find(function (m) {
return m.name === marker;
});
if (!clipMarker) return;
this._source.start(0, clipMarker.start / 1000, clipMarker.duration ? clipMarker.duration / 1000 : undefined);
this._resetA2DMarkers(clipMarker);
} else {
this._source.start();
}
this._timeStartedAt = this._options.ctx.currentTime - offset;
this._timePausedAt = 0;
this._state = AudioClipState.PLAYING;
this._timesPlayed++;
}
/**
* Pause the currently playing audio.
*
* @example
*
* const sfx = a2d.addAudio('sfx', sfxBuffer);
* sfx.play();
*
* setTimeout(() => {
* sfx.pause();
* }, 1000);
*/
}, {
key: "pause",
value: function pause() {
var _this$_options$marker2;
var elapsed = this._options.ctx.currentTime - this._timeStartedAt;
this.stop();
this._timePausedAt = elapsed;
(_this$_options$marker2 = this._options.markers) === null || _this$_options$marker2 === void 0 ? void 0 : _this$_options$marker2.push({
name: 'a2d-pause',
start: this._timePausedAt * 1000
});
this._state = AudioClipState.PAUSED;
}
/**
* Resumes playing this clip from when it was paused.
*
* @example
*
* const sfx = a2d.addAudio('sfx', sfxBuffer);
* sfx.play();
* sfx.pause();
*
* setTimeout(() => {
* sfx.resume();
* }, 1000);
*/
}, {
key: "resume",
value: function resume() {
this.play('a2d-pause');
}
/**
* Stops the playback of this audio.
*
* @returns {AudioClip} Returns this for chaining.
*
* @example
*
* const sfx = a2d.addAudio('sfx', sfxBuffer);
*
* sfx.play();
* sfx.stop();
*/
}, {
key: "stop",
value: function stop() {
this._source.disconnect();
this._source = this._options.ctx.createBufferSource();
this._timePausedAt = 0;
this._timeStartedAt = 0;
this._state = AudioClipState.STOPPED;
}
/**
* Seeks to a specific time in the clip.
*
* @param {number} time The time, in milliseconds, to seek to.
*/
}, {
key: "seek",
value: function seek(time) {
var _this$_options$marker3;
if (!time) return;
if (time > this.duration * 1000) {
console.warn('The time to seek to is greater than the duration of the clip.');
return;
}
if (this._state === AudioClipState.PLAYING) this.stop();
(_this$_options$marker3 = this._options.markers) === null || _this$_options$marker3 === void 0 ? void 0 : _this$_options$marker3.push({
name: 'a2d-seek',
start: time
});
this.play('a2d-seek');
}
/**
* Mutes this clip.
*
* @example
*
* const sfx = a2d.addAudio('sfx', sfxBuffer);
*
* sfx.play();
* sfx.mute();
*/
}, {
key: "mute",
value: function mute() {
this._previousVolume = this.volume;
this.volume = 0;
}
/**
* Puts the volume back to the value it was at before the clip was muted.
*
* @example
*
* const sfx = a2d.addAudio('sfx', sfxBuffer);
* sfx.play();
*
* sfx.mute();
* sfx.unmute();
*/
}, {
key: "unmute",
value: function unmute() {
this.volume = this._previousVolume;
}
/**
* Sets up an onclick event on a trigger element if one was provided in the options.
*
* @private
*/
}, {
key: "_setupTrigger",
value: function _setupTrigger() {
var _this = this;
var el = document.querySelector(this._options.trigger);
if (!el) return;
el.addEventListener('click', function () {
return _this.play();
});
}
/**
* Connects the nodes that have been added through `addNode`.
*
* @private
*/
}, {
key: "_connectNodes",
value: function _connectNodes() {
if (this._nodesref.length > 0) {
var firstNode = this._nodesref[0];
var latestNode = this._nodesref[this._nodesref.length - 1];
this._source.connect(latestNode.instance);
firstNode.instance.connect(this._gain);
} else {
this._source.connect(this._gain);
}
}
/**
* Specify what happens when a clip is finished playing.
*
* @private
*/
}, {
key: "_oncomplete",
value: function _oncomplete() {
var _this2 = this;
this._source.onended = function () {
_this2._state = AudioClipState.STOPPED;
_this2._source.onended = null;
};
}
/**
* Resets any markers set internally.
*
* @private
*
* @param {Marker} clipMarker The marker to check if should be removed.
*/
}, {
key: "_resetA2DMarkers",
value: function _resetA2DMarkers(clipMarker) {
if (clipMarker.name.includes('a2d')) {
var _this$_options$marker4;
this._options.markers = (_this$_options$marker4 = this._options.markers) === null || _this$_options$marker4 === void 0 ? void 0 : _this$_options$marker4.filter(function (marker) {
!marker.name.includes('a2d');
});
}
}
}, {
key: "name",
get: function get() {
return this._name;
}
/**
* Gets the current state of the audio clip.
*
* @returns {string}
*/
}, {
key: "state",
get: function get() {
return this._state;
}
/**
* Gets the number of times that this clip has been played.
*
* @returns {number}
*/
}, {
key: "timesPlayed",
get: function get() {
return this._timesPlayed;
}
/**
* Gets the current time of the clip.
*
* @returns {number}
*/
}, {
key: "currentTime",
get: function get() {
if (this._state === AudioClipState.PAUSED) return this._timePausedAt;
if (this._state === AudioClipState.PLAYING) return this._options.ctx.currentTime - this._timeStartedAt;
return 0;
}
/**
* Gets the duration of the clip.
*
* @returns {number}
*/
}, {
key: "duration",
get: function get() {
return this._duration;
}
/**
* Gets the volume of this clip.
*
* @returns {number}
*/
}, {
key: "volume",
get: function get() {
return this._volume;
}
/**
* Sets the volume of this clip.
*
* @param {number} vol The new volume of the clip.
*/
,
set: function set(vol) {
this._volume = vol;
this._gain.gain.setValueAtTime(this._volume / 100, this._options.ctx.currentTime);
}
/**
* Gets the created nodes.
*
* @returns {*}
*/
}, {
key: "nodes",
get: function get() {
return this._nodes;
}
}]);
return AudioClip;
}();
/**
* Audio2D is a web audio helper for adding sound/music to your JavaScript games.
*/
var Audio2D = /*#__PURE__*/function () {
function Audio2D() {
classCallCheck(this, Audio2D);
defineProperty(this, "_ctx", new AudioContext());
defineProperty(this, "_clips", []);
defineProperty(this, "_nodes", new Nodes(this._ctx));
}
createClass(Audio2D, [{
key: "addAudio",
/**
* Adds audio to the media library.
*
* @param {string} name The name of this audio clip used to reference it.
* @param {AudioBuffer} buffer A reference to the audio buffer for this audio.
* @param {Object} [options]
* @param {Array<Marker>} [options.markers] A breakdown of the audio into individual parts that can be used independently.
* @param {string} [options.trigger] An id or classname of a dom element that when clicked will trigger the clip to play.
*
* @example
*
* // Adding an audio clip with no markers.
* const levelUp = a2d.addAudio('level-up', levelUpBuffer);
*
* // Adding an audio clip with markers.
* const sfxMarkers = [
* { name: 'walk', start: 1500, duration: 1000 },
* { name: 'fall': start: 2500, duration: 1500 },
* ];
* const sfx = a2d.addAudio('sfx', sfxBuffer, { markers: sxfMarkers });
*/
value: function addAudio(name, audio) {
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
options.ctx = this._ctx;
var clip = new AudioClip(name, audio, options);
this._clips.push(clip);
return clip;
}
/**
* Gets an audio clip from the media library.
*
* @param {string} name The name of the audio clip to get.
*
* @returns {AudioClip|undefined} Returns the audio clip if its found or undefined otherwise.
*
* @example
*
* a2d.addAudio('track1', buffer);
*
* const clip = a2d.getAudio('track1');
*/
}, {
key: "getAudio",
value: function getAudio(name) {
return this._clips.find(function (clip) {
return clip.name === name;
});
}
/**
* Removes an audio clip from the media library.
*
* @param {string} name The name of the audio clip to remove.
*
* @returns {Audio2D} Returns this for chaining.
*
* @example
*
* a2d.addAudio('track1', buffer);
*
* a2d.removeAudio('track1');
*/
}, {
key: "removeAudio",
value: function removeAudio(name) {
this._clips = this.clips.filter(function (clip) {
return clip.name !== name;
});
return this;
}
/**
* Removes all audio clips from the media library.
*
* @returns {Audio2D} Returns this for chaining.
*
* @example
*
* a2d.addAudio('track1', buffer1);
* a2d.addAudio('track2', buffer2);
*
* a2d.removeAllAudio();
*/
}, {
key: "removeAllAudio",
value: function removeAllAudio() {
this._clips = [];
return this;
}
}, {
key: "clips",
/**
* Returns the created audio clips.
*
* @returns {Array<AudioClip>}
*/
get: function get() {
return this._clips;
}
/**
* Returns the nodes module to use for creating nodes and adding them to clips.
*
* @returns {Nodes}
*/
}, {
key: "nodes",
get: function get() {
return this._nodes;
}
}]);
return Audio2D;
}();
export { Audio2D };