Skip to content

mattlima/mooog

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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:

  • debug: Output debugging messages to the console. Default: false
  • default_gain: Gain objects that are initiated will have their gain automatically set to this value. Default: 0.5
  • default_ramp_type: adsr envelopes will be produced using this type of curve ('linear or 'expo'). Default: 'expo'
  • default_send_type: For sends from Track objects. Default: 'post'
  • periodic_wave_length: The PeriodicWave generator functions calculate up to this many partials. Default: 2048
  • curve_length: The WaveShaper curve generator functions produce Float32Arrays of this length. Default: 65536
  • fake_zero: This number is substituted for zero to prevent errors when zero is passed to an exponential ramp function. Default: 1 / 65536
  • allow_multiple_audiocontexts: Browsers differ in how many AudioContext instances they support. Safari supports one, Chrome supports 6, and Firefox appears to have no limit. Therefore, by default, Mooog reuses the same AudioContext even if you initialize multiple Mooog objects, but you can override that with this option. Default: false

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.

  • Set frequency to 800 immediately. (Use setValueAtTime) osc.param( {frequency: 800} );

  • Set frequency to 800, 4 seconds from now. (Use setValueAtTime) osc.param( {frequency: 800, at: 4} );

  • Ramp frequency linearly to 800, starting after the last scheduled value change (or now, if there isn't one) and arriving 4 seconds from now. (Use linearRampToValueAtTime) osc.param( {frequency: 800, at: 4, ramp: 'linear'} );

  • Ramp frequency linearly to 800, starting now and arriving 4 seconds from now. osc.param( {frequency: 800, at: 4, ramp: 'linear', from_now: true} );

  • Ramp frequency exponentially to 800 over 4 seconds, starting now. (Use exponentialRampToValueAtTime) osc.param( {frequency: 800, at: 4, ramp: 'expo', from_now: true } );

  • Set frequency to asymptotically approach 800, beginning 4 seconds from now. (Use setTargetAtTime) osc.param( {frequency: 800, at: 4, ramp: 'expo', timeConstant: 1.5} );

  • Set frequency to values 300, 550, 900, 800 over a period of 2 seconds.* (Use setValueCurveAtTime) osc.param( {frequency: [300, 550, 900, 800], duration: 2, ramp: 'curve'} ); *The rhythmic irregularity of the frequency progression produced by the setValueCurveAtTime() method is due to the nearest-value interpolation algorithm it uses. The function is meant for much larger arrays of values describing smooth curves.

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:

  • base: The value to start and end with. Defaults to 'zero'
  • times: An array of time values representing the ending time of each of the ADSR stages. The first is relative to the currentTime, and the others are relative to the previous value. The delay stage can be suppressed by passing an array of three elements, in which case the envelope will be an ASR and the s value will be ignored. The release stage can be suppressed by passing an array of 2 elements, in which case the envelope will be an ADS envelope (useful if you're responding to user input or the duration of the note cannot be predetermined.)
  • a: The final value of the parameter at the end of the attack stage. Defaults to 1
  • s: The value of the parameter at the end of the delay stage (or attack stage, if delay is omitted), to be held until the beginning of the release stage. Defaults to 1
  • ramp: 'linear' or 'expo', determines the ramping function to use. Defaults to the default_ramp_type property of the Mooog config object

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

  • Exposes a state property that is either 'stopped' or 'playing'
  • Includes a config object property buffer_source_file indicating the URL of an audio asset from which to create an AudioBuffer and then set the buffer of the underlying AudioNode
  • Automatically regenerates the buffer when the stop() method is used so you can repeatedly stop and start without initializing a new Node.
  • Fires mooog.audioBufferLoaded event when an external audio asset is completely loaded. The file path is available in the detail property.

ChannelMerger and ChannelSplitter

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

Convolver

  • Includes a config object property buffer_source_file indicating the URL of an audio asset (impulse response) from which to create an AudioBuffer and then set the buffer of the underlying AudioNode
  • Fires mooog.audioBufferLoaded event when an external audio asset is completely loaded. The file path is available in the detail property.

Delay

  • Exposes a feedback property that maps to the Gain of a feedback stage. Defaults to zero, and can be set on initialization: Mooog.node( { node_type: 'Delay', feedback: 0.2 } )

Gain

  • Saturation can easily occur in signal chains with multiple paths when they are summed at the output. To alleviate this effect, the Gain object is initialized with gain set to 0.5 instead of 1.0. You can change this default with the default_gain Mooog config option.

Oscillator

  • Exposes a state property that is either 'stopped' or 'playing'
  • Sets an internal gain so you can repeatedly stop and start without initializing a new Node.

This means, of course, that no guarantees about phase can be made for subsequent calls to start(). If you need to control phase on node start() you should use a raw OscillatorNode.

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

  • 0.0.1 : First working version.
  • 0.0.2 : Added ChannelMerger, ChannelSplitter, ScriptProcessor nodes, fixed bug when setting callbacks via the config object.
  • 0.0.3 : Refactor for more rational signatures and easier homebrew Node creation.
  • 0.0.4 : Add shim for StereoPannerNode.
  • 0.0.5 : AudioBufferSourceNode retains onended() function between plays once set.
  • 0.0.6 : fake_zero is only used when the ramp type is 'expo'. Changed behavior of from_now option in param() to use setTimeout in order to correctly pick up param changes scheduled on the same tick.
  • 0.0.7 : Track objects behave just like other Nodes when used as source or target of .connect and .chain
  • 0.0.8 : Nodes with a buffer property fire loaded events. Buffer source load requests are cached to avoid logjam on page load.

Todo:

  • Equalize gain, add error messages for unsupported browsers in examples
  • Optionally use periodic waves for basic oscillator types to minimize volume differences
  • Vary duty cycle on period wave generator
  • Refactor initialization to combine configure_from and zero_node_setup
  • Clean up debug messages
  • Emit loaded event when using buffer_source_file
  • Allow parameter arrays in adsr()

Contributing

CONTRIBUTING.md