addon/mixins/connectable.js
import Ember from 'ember';
import { Connection } from 'ember-audio';
/**
* Provides classes that are capable of interacting with the Web Audio API's
* AudioContext.
*
* @public
* @module Audio
*/
const {
A,
on,
get,
set,
observer,
Mixin
} = Ember;
/**
* A mixin that allows an object to create AudioNodes and connect them together.
* Depends on `audioContext` being available on the consuming object.
*
* @public
* @class Connectable
* @todo figure out how to augment Ember.MutableArray so that the connections
* array can have methods like addConnection, removeConnection, addFilter, disableNode
*/
export default Mixin.create({
/**
* An array of Connection instances. Determines which AudioNode instances are
* connected to one-another and the order in which they are connected. Starts
* as `null` but set to an array on `init` via the
* {{#crossLink "Connectable/_initConnections:method"}}{{/crossLink}} method.
*
* @public
* @property connections
* @type {Ember.MutableArray}
*/
connections: null,
/**
* returns a connection's AudioNode from the connections array by the
* connection's `name`.
*
* @public
* @method getNodeFrom
*
* @param {string} name The name of the AudioNode that should be returned.
*
* @return {AudioNode} The requested AudioNode.
*/
getNodeFrom(name) {
const connection = this.getConnection(name);
if (connection) {
return get(connection, 'node');
}
},
/**
* returns a connection from the connections array by it's name
*
* @public
* @method getConnection
*
* @param {string} name The name of the AudioNode that should be returned.
*
* @return {Connection} The requested Connection.
*/
getConnection(name) {
return this.get('connections').findBy('name', name);
},
/**
* Find's a connection in the connections array by it's `name` and removes it.
*
* @param {string} name The name of the connection that should be removed.
*
* @public
* @method removeConnection
*/
removeConnection(name) {
this.get('connections').removeObject(this.getConnection(name));
},
/**
* Updates an AudioNode's property in real time by setting the property on the
* connectable object (which affects the next play) and also sets it on it's
* corresponding `connection.node` (which affects the audio in real time).
*
* If a connection is pulling a property from a connectable object via
* `onPlaySetAttrsOnNode[index].relativePath`, this method will update that
* property directly on the Connection's `node` as well as on the connectable
* object.
*
* Important to note that this only works for properties that are set directly
* on a connectable object and then proxied/set on a node via
* `onPlaySetAttrsOnNode`.
*
* @example
* // With `set`
* const osc = audioService.createOscillator({ frequency: 440 });
* osc.play(); // playing at 440Hz
* osc.set('frequency', 1000); // still playing at 440Hz
* osc.stop();
* osc.play(); // now playing at 1000Hz
*
* @example
* // With `update`
* const osc = audioService.createOscillator({ frequency: 440 });
* osc.play(); // playing at 440Hz
* osc.update('frequency', 1000); // playing at 1000Hz
*
* @public
* @method update
*
* @param key {string} The name/path to a property on an Oscillator instance
* that should be updated.
*
* @param value {string} The value that the property should be set to.
*/
update(key, value) {
this.get('connections').map((connection) => {
connection.get('onPlaySetAttrsOnNode').map((attr) => {
const path = get(attr, 'relativePath');
if (key === path) {
const pathChunks = path.split('.');
connection.get('node')[pathChunks.pop()].value = value;
}
});
});
this.set(key, value);
},
/**
* Initializes default connections on Sound instantiation. Runs `on('init')`.
*
* @protected
* @method _initConnections
*/
_initConnections: on('init', function() {
const bufferSource = Connection.create({
name: 'audioSource',
createdOnPlay: true,
source: 'audioContext',
createCommand: 'createBufferSource',
onPlaySetAttrsOnNode: [
{
attrNameOnNode: 'buffer',
relativePath: 'audioBuffer'
}
]
});
const gain = Connection.create({
name: 'gain',
source: 'audioContext',
createCommand: 'createGain'
});
const panner = Connection.create({
name: 'panner',
source: 'audioContext',
createCommand: 'createStereoPanner'
});
const destination = Connection.create({
name: 'destination',
path: 'audioContext.destination'
});
this.set('connections', A([ bufferSource, gain, panner, destination ]));
}),
/*
* Note about _watchConnectionChanges:
* Yeah yeah yeah.. observers are bad. Making the connections array a computed
* property doesn't work very well because complete control over when it
* recalculates is needed.
*/
/**
* Observes the connections array and runs wireConnections each time it
* changes.
*
* @private
* @method _watchConnectionChanges
*/
_watchConnectionChanges: observer('connections.[]', function() {
this.wireConnections();
}),
/**
* Gets the array of Connection instances from the connections array and
* returns the same array, having created any AudioNode instances that needed
* to be created, and having connected the AudioNode instances to one another
* in the order in which they were present in the connections array.
*
* @protected
* @method wireConnections
*
* @return {array|Connection} Array of Connection instances collected from the
* connections array, created, connected, and ready to play.
*/
wireConnections() {
const createNode = this._createNode.bind(this);
const setAttrsOnNode = this._setAttrsOnNode.bind(this);
const wireConnection = this._wireConnection;
const connections = this.get('connections');
connections.map(createNode).map(setAttrsOnNode).map(wireConnection);
},
/**
* Creates an AudioNode instance for a Connection instance and sets it on it's
* `node` property. Unless the Connection instance's `createdOnPlay` property
* is true, does nothing if the AudioNode instance has already been created.
*
* Also sets any properties from a connection's `onPlaySetAttrsOnNode` array
* on the node.
*
* @private
* @method _createNode
*
* @param {Connection} connection A Connection instance that should have it's
* node created (if needed).
*
* @return {Connection} The input Connection instance after having it's node
* created.
*/
_createNode(connection) {
const { path, name, createdOnPlay, source, createCommand, node } = connection;
if (node && !createdOnPlay) {
// The node is already created and doesn't need to be created again
return connection;
} else if (path) {
connection.node = this.get(path);
} else if (createCommand && source) {
connection.node = this.get(source)[createCommand]();
} else if (!connection.node) {
console.error('ember-audio:', `The ${name} connection is not configured correctly. Please fix this connection.`);
return;
}
return connection;
},
/**
* Gets a Connection instance's `onPlaySetAttrsOnNode` and sets them on it's
* node.
*
* @private
* @method _setAttrsOnNode
*
* @param {Connection} connection The Connection instance that needs it's
* node's attrs set.
*
* @return {Connection} The input Connection instance after having it's nodes
* attrs set.
*/
_setAttrsOnNode(connection) {
const currentTime = this.get('audioContext.currentTime');
connection.get('onPlaySetAttrsOnNode').map((attr) => {
const { attrNameOnNode, relativePath, value } = attr;
const attrValue = relativePath ? this.get(relativePath) || value : value;
if (connection.node && attrNameOnNode && attrValue) {
set(connection.node, attrNameOnNode, attrValue);
}
});
connection.get('exponentialRampToValuesAtTime').map((opts) => {
const time = currentTime + opts.endTime;
connection.node[opts.key].exponentialRampToValueAtTime(opts.value, time);
});
connection.get('linearRampToValuesAtTime').map((opts) => {
const time = currentTime + opts.endTime;
connection.node[opts.key].linearRampToValueAtTime(opts.value, time);
});
connection.get('setValuesAtTime').map((opts) => {
const time = currentTime + opts.startTime;
connection.node[opts.key].setValueAtTime(opts.value, time);
});
connection.get('startingValues').map((opts) => {
connection.node[opts.key].setValueAtTime(opts.value, currentTime);
});
return connection;
},
/**
* Meant to be passed to a Array.prototype.map function. Connects a Connection
* instance's node to the next Connection instance's node.
*
* @private
* @method _wireConnection
*
* @param {Connection} connection The current Connection instance in the
* iteration.
*
* @param {number} idx The index of the current iteration.
*
* @param {array|Connection} connections The original array of connections.
*
* @return {Connection} The input Connection instance after having it's node
* connected to the next Connection instance's node.
*/
_wireConnection(connection, idx, connections) {
const nextIdx = idx + 1;
const currentNode = connection;
if (nextIdx < connections.length) {
const nextNode = connections[nextIdx];
// Assign nextConnection back to connections array.
// Since we're working one step ahead, we don't want
// each connection created twice
connections[nextIdx] = nextNode;
// Make the connection from current to next
currentNode.node.connect(nextNode.node);
}
return currentNode;
}
});