mattlima / mooog

Chainable AudioContext nodes
29 stars 3 forks source link

Mooog

Chainable AudioNode API

Version 0.0.8

Important (Jan. 2016)

Several of the problems this library was created to solve are addressed in the HTML5 Audio API dev roadmap, most notably connect() returning the AudioNode instance and clarifications/behavior specs for the various setValue methods of the AudioParam. To stay up-to-date on this stuff, you can follow the spec at https://github.com/WebAudio/web-audio-api. Hopefully Mooog's behavior will not conflict with the future standard, but if it does, I'll be updating Mooog to stay with the spec. The tldr; is don't use this in production (not like you would, though, amiright?).

What is Mooog?

Mooog is inspired by audio mixing boards on the one hand and jQuery chainable syntax on the other. It automatically does a lot of stuff so you don't have to. Mooog's goal is to take some of the tedium out of working with AudioNodes, as well as patching some odd behaviors. With Mooog, instead of writing this:

AudioContext = AudioContext || webkitAudioContext;
var ctxt = new AudioContext();
var osc = ctxt.createOscillator();
var lfo = ctxt.createOscillator();
var gain = ctxt.createGain();
osc.frequency.value = 300;
lfo.type = 'sawtooth';
lfo.frequency.value = 3;
lfo.connect(gain);
gain.gain.value = 40;
gain.connect(osc.frequency);
osc.connect(ctxt.destination);
lfo.start();
osc.start();

...you can write this:

M = new Mooog();
M.node(
    { id:'lfo', node_type:'Oscillator', type:'sawtooth', frequency:3 }
  )
  .start()
  .chain(
    M.node( {id:'gain', node_type:'Gain', gain:40} )
  )
  .chain(
    M.node({id:'osc', node_type:'Oscillator', frequency:300}), 'frequency'
  )
  .start()

Features

Mooog provides a MooogAudioNode object that can wrap one or more AudioNodes. At a minimum, it exposes the methods of the wrapped Node (or the first in its internal chain) so you can talk to them just like the underlying AudioNode. Many of them offer additional functionality. There are also utilities like an ADSR generator as well as functions to generate common waveshaping curves like Chebyshevs and tanh.

All nodes with a buffer property fire a mooog.audioBufferLoaded event when the requested audio asset has finished loading. Using the same file for multiple buffers can cause duplicate requests if the first request hasn't finished loading and the browser hasn't cached it yet. To prevent this, Mooog caches these buffers internally and automatically subscribes the requesting node to the load event for that file path, setting the buffer when the file is ready, so that each audio asset is only requested once.

mooog.audioBufferLoaded is a synthetic event fired on the document element with the file path and loaded AudioBuffer in the detail property, so you can listen for it to make sure your audio files are loaded before you do any playback.

There is also a specialized MooogAudioNode object called Track, which will automatically create panner and gain nodes at the end of its internal chain that can be controlled from a single place and easily create sends to other Tracks. Like the base MooogAudioNode, it automatically routes the end of its internal chain to the destinationNode.

Patches

The Web Audio API is not, and never will be, supported on IE (though it is supported on Edge), which limits its usefulness for general web projects until Edge supplants IE. Even where it is supported, the API still has not matured (AudioWorkers, for example, are not implemented anywhere yet) so Mooog doesn't worry too much about cross-browser compatibility issues. It does, however, implement a patch for the absence of the StereoPannerNode on for the deprecated Audio API, since panning is such a basic audio operation and the Mooog Track object relies on it. Ensuring cross-platform consistency is on the to-do list once the API stabilizes and browser support improves.

StereoPannerNode

Mooog shims the StereoPannerNode on webkit browsers like Safari that don't support it. (Adapted from https://github.com/mohayonao/stereo-panner-node)

Getting started

If you want to jump right in, see the examples. They won't run well on the local filesystem because of CORS restrictions on AJAX audio file loads, so they're also posted on the github project page.

Install via bower

bower install mooog

Initializing Mooog

Mooog sets up a (Webkit)AudioContext object and manages connections to its DestinationNode automatically. It takes an optional configuration object with the following properties:

Creating AudioNodes

Nodes are created via the node() method of the Mooog object, which takes a node definition object with a single required parameter, node_type. You can also give it a string id to reference it later on:

M = new Mooog();
var my_oscillator = M.node( { 
    id: "my_new_node_string_id",
    node_type: "Oscillator"
} );

Here we've assigned the node reference to a variable, but we can also reference an initialized node using its id as a single argument:

M.node('my_new_node_string_id');

The node_type parameter is the name of the AudioNode as found in its create function, i.e. "Gain" because AudioContext.createGain(), "BiquadFilter" because AudioContext.createBiquadFilter(), etc.

Any other parameters you want to set on the AudioNode can be submitted as part of the node definition:

var my_oscillator = M.node( { 
    id: "my_new_node_string_id",
    node_type: "Oscillator",
    frequency: 850,
    type: "sawtooth"
} );

If you don't need to set parameters, there is also a shorthand for creating a node where you specify only the id and the type:

var my_oscillator = M.node( "my_new_node_string_id", "Oscillator" );

Nodes automatically route their output to the DestinationNode of the context. To override this behavior (useful if you're creating LFOs to modulate AudioParams, for example), pass connect_to_destination : false in the node definition object:

var my_oscillator = M.node( { 
    id: "my_new_node_string_id",
    node_type: "Oscillator",
    connect_to_destination: false
} );

Connecting AudioNodes

connect() works just like the native AudioNode method, except it returns the source, so you can chain them to accomplish fan-out more easily:

M.node("my_previously_created_audio_buffer_source")
    .connect( M.node({ id: "my_short_delay", node_type: "Delay", delayTime: 0.2 }) )
    .connect( M.node({ id: "my_long_delay", node_type: "Delay", delayTime: 1.5 }) );

If you want to link nodes in series, you can use chain() instead of connect().

You can also easily initialize chains of nodes in a single Track object. See below.

chain returns the destination node, not the source node. It also automatically disconnects the source from the context's AudioDestinationNode. To chain an AudioParam, use the name of the param as the second argument.

M.node("my_previously_created_audio_buffer_source")
    .chain( M.node({ id: "my_delay", node_type: "Delay", delayTime: 0.5 ) )
    .chain( M.node({ id: "my_reverb", node_type: "Convolver", buffer_source_file: "/my-impulse-response.wav" ) );

disconnect() works like the native function but won't throw an error if the connection doesn't exist. It will output a warning to the console if Mooog was initialized with debug: true.

Working with parameters

The param() getter/setter

AudioNode parameters are a mix of enumerated properties, strings, numbers, and AudioParam objects. Mooog supports setting any of these jQuery-style via the param() getter/setter function.

var osc = M.node('my_oscillator', 'Oscillator');
osc.param('frequency'); // -> returns 440 
osc.param('frequency', 800); // -> returns 800
osc.param('frequency'); // -> returns 800

Like jQuery, multiple parameters can be set in the same param call:

osc.param( {frequency: 800, type: 'sawtooth'} );

Internally, param() actually calls AudioParam.cancelScheduledValues() and then uses AudioParam.setValueAtTime(value, currentTime) by default in order to ensure consistent behavior. Put another way, using param() will always have the desired effect regardless of whether other value changes have been scheduled on that parameter, unlike acting on Audioparam.value directly.

Parameters in time

The AudioParam API provides 5 different methods for scheduling parameter changes. param() can be used to call any of them by adding properties to the object submitted. Here are examples using an oscillator's frequency parameter. Note the use of from_now which causes a call to setValueAtTime at the currentTime if used with linear or exponential ramp functions.

The cancel and at parameters can be used with any of the ramp types.

ADSR envelopes

For convenience, you can create ADSR, ASR, or ADS envelopes with the adsr method of any Node:

MooogAudioNode.adsr( param: mixed, config: object )

param: An AudioParam or the string name of the AudioParam, assumed to be on this. config: Object with the following properties:

A very small number fake_zero is used in place of actual zero if given as the base, a, or s property so that the exponential ramping function doesn't throw an error. fake_zero defaults to 1/65536 but can be configured when Mooog is initialized.

The Track Object

Mooog includes a Track object designed to make working with lots of nodes a little easier. You set up and refer to the Track with a unique string identifier (just like a node), and populate its internal chain of nodes with one or more additional arguments to the creator function:

M.track( 'my_track', node [, node...] );

Each node argument can be a node definition object of the type you'd pass to the Mooog.node() function or an existing node. The Track object routes the last node in its internal chain through a pan/gain stage. Like all nodes, the Track exposes the native methods/properties of the first node in its internal chain, but it also exposes the gain and pan AudioParams of the nodes in the pan/gain stage directly.

Tracks can be used interchangeably with nodes as source or destination objects for methods connect() and chain().

Tracks have a send() function analogous to mixing board sends. Once created, the send (which is a Gain node) is referenced by string id, just like Tracks and other nodes.

/* create the track */
M.track("my_track", M.node({id:"sin", node_type:"Oscillator", type:"sawtooth"}), { id:"fil",node_type:"BiquadFilter" } );

/* Set up a reverb effect track*/
rev = M.track('reverb', { id:"cv", node_type:"Convolver", buffer_source_file:"/some-impulse-response.wav" });

/* Create the send */
M.track('my_track').send('rev_send', rev, 'post');

/* Come back later and change the gain */
M.track('my_track').send('rev_send').param('gain', 0.25);

Creates a send to another Track object.
id: String ID to assign to this send.
destination: The target Track object or ID thereof
pre: Either 'pre' or 'post'. Defaults to config value in the Mooog object.
gain: Initial gain value for the send. Defaults to config value in the Mooog object.

Utilities

Mooog.freq()

A convenience function for converting MIDI notes to equal temperament Hz

PeriodicWave constructors

The native versions of the native (Sine, Sawtooth, Triangle, Square) waveforms are louder than equivalent waveforms created with createPeriodicWave so if your signal path includes both it may be easier to mix them if you use generated versions of the native waveforms:

Mooog.sawtoothPeriodicWave(n)

Calculates and returns a sawtooth PeriodicWave up to the nth partial.

Mooog.squarePeriodicWave(n)

Calculates and returns a square PeriodicWave up to the nth partial.

Mooog.trianglePeriodicWave(n)

Calculates and returns a triangle PeriodicWave up to the nth partial.

Mooog.sinePeriodicWave()

Returns a sine PeriodicWave.

Additional Node-specific details

AudioBufferSource

ChannelMerger and ChannelSplitter

Constructor parameters numberOfInputs or numberOfOutputs can be passed in the configuration object

Convolver

Delay

Gain

Oscillator

ScriptProcessor

Constructor arguments numberOfInputChannels, numberOfOutputChannels and bufferSize can be passed in the configuration object.

WaveShaper

Includes utility functions tanh for hyperbolic tangent and chebyshev for Chebyshev polynomials, generating Float32Array distortion curves. tanh takes a single argument representing the coefficient (higher coefficients equal more aggressive shaping). chebyshev takes a single argument indicating the number of terms to generate (the exponent of the first term).

/* create the waveshaper */
var shaper = M.node("my_waveshaper", M.node({ node_type:"WaveShaper" });

/* Use a tanh waveshaping curve */
shaper.curve = shaper.tanh(2); 

/* Use a 5th-order Chebyshev polynomial waveshaper */
shaper.curve = shaper.chebyshev(5); 

License

The MIT License (MIT)

Copyright (c) 2016 Matthew Lima

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Donations

If you're feeling generous, you can throw me some dosh here.

Version History

Todo:

Contributing

CONTRIBUTING.md