UNPKG

tinymusic

Version:

A simple, lightweight music sequencer in JavaScript using the Web Audio API.

220 lines (186 loc) 6.62 kB
(function ( root, factory ) { if ( typeof define === 'function' && define.amd ) { define( [ 'exports' ], factory ); } else if ( typeof exports === 'object' && typeof exports.nodeName !== 'string' ) { factory( exports ); } else { factory( root.TinyMusic = {} ); } }( this, function ( exports ) { /* * Private stuffz */ var enharmonics = 'B#-C|C#-Db|D|D#-Eb|E-Fb|E#-F|F#-Gb|G|G#-Ab|A|A#-Bb|B-Cb', middleC = 440 * Math.pow( Math.pow( 2, 1 / 12 ), -9 ), numeric = /^[0-9.]+$/, octaveOffset = 4, space = /\s+/, num = /(\d+)/, offsets = {}; // populate the offset lookup (note distance from C, in semitones) enharmonics.split('|').forEach(function( val, i ) { val.split('-').forEach(function( note ) { offsets[ note ] = i; }); }); /* * Note class * * new Note ('A4 q') === 440Hz, quarter note * new Note ('- e') === 0Hz (basically a rest), eigth note * new Note ('A4 es') === 440Hz, dotted eighth note (eighth + sixteenth) * new Note ('A4 0.0125') === 440Hz, 32nd note (or any arbitrary * divisor/multiple of 1 beat) * */ // create a new Note instance from a string function Note( str ) { var couple = str.split( space ); // frequency, in Hz this.frequency = Note.getFrequency( couple[ 0 ] ) || 0; // duration, as a ratio of 1 beat (quarter note = 1, half note = 0.5, etc.) this.duration = Note.getDuration( couple[ 1 ] ) || 0; } // convert a note name (e.g. 'A4') to a frequency (e.g. 440.00) Note.getFrequency = function( name ) { var couple = name.split( num ), distance = offsets[ couple[ 0 ] ], octaveDiff = ( couple[ 1 ] || octaveOffset ) - octaveOffset, freq = middleC * Math.pow( Math.pow( 2, 1 / 12 ), distance ); return freq * Math.pow( 2, octaveDiff ); }; // convert a duration string (e.g. 'q') to a number (e.g. 1) // also accepts numeric strings (e.g '0.125') // and compund durations (e.g. 'es' for dotted-eight or eighth plus sixteenth) Note.getDuration = function( symbol ) { return numeric.test( symbol ) ? parseFloat( symbol ) : symbol.toLowerCase().split('').reduce(function( prev, curr ) { return prev + ( curr === 'w' ? 4 : curr === 'h' ? 2 : curr === 'q' ? 1 : curr === 'e' ? 0.5 : curr === 's' ? 0.25 : 0 ); }, 0 ); }; /* * Sequence class */ // create a new Sequence function Sequence( ac, tempo, arr ) { this.ac = ac || new AudioContext(); this.createFxNodes(); this.tempo = tempo || 120; this.loop = true; this.smoothing = 0; this.staccato = 0; this.notes = []; this.push.apply( this, arr || [] ); } // create gain and EQ nodes, then connect 'em Sequence.prototype.createFxNodes = function() { var eq = [ [ 'bass', 100 ], [ 'mid', 1000 ], [ 'treble', 2500 ] ], prev = this.gain = this.ac.createGain(); eq.forEach(function( config, filter ) { filter = this[ config[ 0 ] ] = this.ac.createBiquadFilter(); filter.type = 'peaking'; filter.frequency.value = config[ 1 ]; prev.connect( prev = filter ); }.bind( this )); prev.connect( this.ac.destination ); return this; }; // accepts Note instances or strings (e.g. 'A4 e') Sequence.prototype.push = function() { Array.prototype.forEach.call( arguments, function( note ) { this.notes.push( note instanceof Note ? note : new Note( note ) ); }.bind( this )); return this; }; // create a custom waveform as opposed to "sawtooth", "triangle", etc Sequence.prototype.createCustomWave = function( real, imag ) { // Allow user to specify only one array and dupe it for imag. if ( !imag ) { imag = real; } // Wave type must be custom to apply period wave. this.waveType = 'custom'; // Reset customWave this.customWave = [ new Float32Array( real ), new Float32Array( imag ) ]; }; // recreate the oscillator node (happens on every play) Sequence.prototype.createOscillator = function() { this.stop(); this.osc = this.ac.createOscillator(); // customWave should be an array of Float32Arrays. The more elements in // each Float32Array, the dirtier (saw-like) the wave is if ( this.customWave ) { this.osc.setPeriodicWave( this.ac.createPeriodicWave.apply( this.ac, this.customWave ) ); } else { this.osc.type = this.waveType || 'square'; } this.osc.connect( this.gain ); return this; }; // schedules this.notes[ index ] to play at the given time // returns an AudioContext timestamp of when the note will *end* Sequence.prototype.scheduleNote = function( index, when ) { var duration = 60 / this.tempo * this.notes[ index ].duration, cutoff = duration * ( 1 - ( this.staccato || 0 ) ); this.setFrequency( this.notes[ index ].frequency, when ); if ( this.smoothing && this.notes[ index ].frequency ) { this.slide( index, when, cutoff ); } this.setFrequency( 0, when + cutoff ); return when + duration; }; // get the next note Sequence.prototype.getNextNote = function( index ) { return this.notes[ index < this.notes.length - 1 ? index + 1 : 0 ]; }; // how long do we wait before beginning the slide? (in seconds) Sequence.prototype.getSlideStartDelay = function( duration ) { return duration - Math.min( duration, 60 / this.tempo * this.smoothing ); }; // slide the note at <index> into the next note at the given time, // and apply staccato effect if needed Sequence.prototype.slide = function( index, when, cutoff ) { var next = this.getNextNote( index ), start = this.getSlideStartDelay( cutoff ); this.setFrequency( this.notes[ index ].frequency, when + start ); this.rampFrequency( next.frequency, when + cutoff ); return this; }; // set frequency at time Sequence.prototype.setFrequency = function( freq, when ) { this.osc.frequency.setValueAtTime( freq, when ); return this; }; // ramp to frequency at time Sequence.prototype.rampFrequency = function( freq, when ) { this.osc.frequency.linearRampToValueAtTime( freq, when ); return this; }; // run through all notes in the sequence and schedule them Sequence.prototype.play = function( when ) { when = typeof when === 'number' ? when : this.ac.currentTime; this.createOscillator(); this.osc.start( when ); this.notes.forEach(function( note, i ) { when = this.scheduleNote( i, when ); }.bind( this )); this.osc.stop( when ); this.osc.onended = this.loop ? this.play.bind( this, when ) : null; return this; }; // stop playback, null out the oscillator, cancel parameter automation Sequence.prototype.stop = function() { if ( this.osc ) { this.osc.onended = null; this.osc.disconnect(); this.osc = null; } return this; }; exports.Note = Note; exports.Sequence = Sequence; }));