gibberwocky
Version:
music sequencing for Live and Max/MSP
519 lines (424 loc) • 16.4 kB
JavaScript
'use strict';
const Big = require( 'big.js' )
let seqclosure = function( Gibber ) {
let Theory = Gibber.Theory
let proto = {
DO_NOT_OUTPUT: -987654321,
_seqs: [],
type: 'sequence',
stop() {
proto._seqs.forEach( s => s.stop() )
},
start() {
proto._seqs.forEach( s => s.start() )
},
create( values, timings, key, object = null, priority=0, mode='live' ) {
let seq = Object.create( this )
if( values.isGen ) values = Gibber.WavePattern( values )
if( timings !== undefined && timings.isGen ) timings = Gibber.WavePattern( timings )
if( mode === 'max' ) object.id = object.path
if( timings === undefined ) {
if( object.autorun === undefined ) {
object.autorun = [ seq ]
}else{
object.autorun.push( seq )
}
}
Object.assign( seq, {
phase: 0,
running: false,
offset: 0,
values,
timings,
object,
key,
priority,
trackID:-1,
octave:0,
autorun:[],
mode
})
if( typeof object === 'object' && object !== null ) {
seq.__client = object.__client
}else{
seq.object = { __client : null }
}
seq.autorun.init = false
if( typeof key === 'string' ) {
if( key.indexOf( 'note' ) > -1 || key.indexOf( 'chord' ) > -1 ) {
let __velocity = null
seq.velocity = v => {
if( v !== undefined ) {
__velocity = v
}
let output = __velocity
if( output === null ) {
let track = null
while( track === null ) {
track = seq.object
}
output = track.velocity()
}
return output
}
Gibber.addSequencingToMethod( seq, 'velocity', 1 )
let __duration = null
seq.duration = v => {
if( v !== undefined ) {
__duration = v
}
let output = __duration
if( output === null ) {
let track = null
while( track === null ) {
track = seq.object
}
output = track.duration()
}
return output
}
Gibber.addSequencingToMethod( seq, 'duration', 1 )
}
}
seq.init()
proto._seqs.push( seq )
return seq
},
__noteFilter( args ) {
args[ 0 ] = Theory.Note.convertToMIDI( args[ 0 ] )
if( this.octave !== 0 || this.object.octave() !== 0 ) {
if( this.octave !== 0 )
args[0] += this.octave * 12
else
args[0] += this.object.octave() * 12
}
return args
},
init() {
let valuesPattern, timingsPattern
if( ! Gibber.Pattern.prototype.isPrototypeOf( this.values ) ) {
if( !Array.isArray( this.values ) ) this.values = [ this.values ]
valuesPattern = Gibber.Pattern.apply( null, this.values )
if( this.values.randomFlag ) {
valuesPattern.filters.push( () => {
var idx = Gibber.Utility.rndi( 0, valuesPattern.values.length - 1 )
return [ valuesPattern.values[ idx ], 1, idx ]
})
for( var i = 0; i < this.values.randomArgs.length; i+=2 ) {
valuesPattern.repeat( this.values.randomArgs[ i ], this.values.randomArgs[ i + 1 ] )
}
}
this.values = valuesPattern
}
if( typeof this.key === 'string' ) {
// only apply for 'note' messages, but need to check for longer keys due to
// max/msp addressing
if( this.key.indexOf( 'midinote' ) === -1 && this.key.indexOf( 'note' ) > -1 ) {
if( this.values.filters.findIndex( v => v.type === 'note' ) === -1 ) {
// round the values for transformation to midinotes... XXX what about for Max version?
this.values.filters.push( args => {
if( typeof args[0] !== 'string' ) args[0] = Math.round( args[0] )
return args
})
const noteFilter = this.__noteFilter.bind( this )
noteFilter.type = 'note'
this.values.filters.push( noteFilter )
}
} else if( this.key.indexOf( 'midichord' ) === -1 && this.key.indexOf( 'chord' ) > -1 ) {
this.values.filters.push( args => {
let chord = args[ 0 ], out
if( typeof chord === 'string' ) {
let chordObj = Gibber.Theory.Chord.create( chord )
out = chordObj.notes
}else{
if( typeof chord === 'function' ) chord = chord()
out = chord.map( Gibber.Theory.Note.convertToMIDI )
if( this.octave !== 0 || this.object.octave() !== 0 ) {
out = this.octave !== 0 ? out.map( v => v + ( this.octave * 12 ) ) : out.map( v=> v + ( this.object.octave() * 12 ) )
}
}
args[0] = out
return args
})
}
}
//XXX implement per sequence velocity
//if( this.key === 'note' || this.key === 'midinote' || this.key === 'chord' || this.key === 'midichord' ) {
// this.values.filters.push( args => {
// const velocity = this.velocity()
// if( velocity !== null ) {
// let msg = this.externalMessages[ 'velocity' ]( velocity, this.values.beat + this.values.beatOffset, this.trackID )
// this.values.scheduler.msgs.push( msg )
// }
// return args
// })
//}
// check in case it has no time values and is autotriggered by note / midinote messages
if( this.timings !== undefined ) {
if( ! Gibber.Pattern.prototype.isPrototypeOf( this.timings ) ) {
if( this.timings !== undefined && !Array.isArray( this.timings ) ) this.timings = [ this.timings ]
timingsPattern = Gibber.Pattern.apply( null, this.timings )
timingsPattern.values.initial = this.timings.initial
if( this.timings !== undefined ) {
if( this.timings.randomFlag ) {
timingsPattern.filters.push( ()=> {
var idx = Gibber.Utility.rndi( 0, timingsPattern.values.length - 1)
return [ timingsPattern.values[ idx ], 1, idx ]
})
for( var i = 0; i < this.timings.randomArgs.length; i+=2 ) {
timingsPattern.repeat( this.timings.randomArgs[ i ], this.timings.randomArgs[ i + 1 ] )
}
}
}
this.timings = timingsPattern
}
const proxyFunctionTimings = ( oldPattern, newPattern ) => {
this.timings = newPattern
this.timings.filters = oldPattern.filters.slice( 0 )
newPattern.__listeners.push( proxyFunctionTimings )
}
this.timings.__listeners.push( proxyFunctionTimings )
this.timings.seq = this
this.timings.nextTime = 0
// add delay for timings pattern so that values updates first, this should help avoid glitches
// in annotations. This delay is inserted in the _addPatternFilter function of codeMarkup.js.
this.timings.__delayAnnotations = true
}
const proxyFunctionValues = ( oldPattern, newPattern ) => {
this.values = newPattern
this.values.filters = oldPattern.filters.slice(0)
newPattern.__listeners.push( proxyFunctionValues )
}
this.values.__listeners.push( proxyFunctionValues )
this.values.nextTime = 0
this.values.seq = this
},
externalMessages: {
note( number, beat, trackID, seq ) {
const velocity = seq.velocity()
const duration = seq.duration()
let msg = ''
if( seq.__client === 'max' ) {
msg = `add ${beat} midinote ${trackID} ${number} ${velocity} ${duration}`
}else if( seq.__client === 'live' ){
msg = `${trackID} add ${beat} note ${number} ${velocity} ${duration}`
}else{
msg = [ 0x90 + seq.object.number, number, velocity ]
}
return msg
},
midinote( number, beat, trackID, seq ) {
const velocity = seq.velocity()
const duration = seq.duration()
let msg = ''
if( seq.__client === 'max' ) {
msg = `add ${beat} midinote ${trackID} ${number} ${velocity} ${duration}`
}else if( seq.__client === 'live' ){
msg = `${trackID} add ${beat} note ${number} ${velocity} ${duration}`
}else{
msg = [ 0x90 + seq.object.number, number, velocity ]
}
return msg
},
//duration( value, beat, trackID ) {
// return `${trackID} add ${beat} duration ${value}`
//},
//velocity( value, beat, trackID ) {
// return `${trackID} add ${beat} velocity ${value}`
//},
chord( chord, beat, trackID, seq ) {
//console.log( chord )
let msgs = []
const velocity = seq.velocity()
const duration = seq.duration()
for( let i = 0; i < chord.length; i++ ) {
let msg = ''
const number = chord[ i ]
if( seq.__client === 'max' ) {
msg = `add ${beat} midinote ${trackID} ${number} ${velocity} ${duration}`
}else if( seq.__client === 'live' ){
msg = `${trackID} add ${beat} note ${number} ${velocity} ${duration}`
}else{
msg = [ 0x90 + seq.object.number, number, velocity ]
}
msgs.push( msg )
}
return msgs
},
midichord( chord, beat, trackID, seq ) {
//console.log( chord )
let msgs = []
const velocity = seq.velocity()
const duration = seq.duration()
for( let i = 0; i < chord.length; i++ ) {
let msg
const number = chord[ i ]
if( seq.__client === 'max' ) {
msg = `add ${beat} midinote ${trackID} ${number} ${velocity} ${duration}`
}else if( seq.__client === 'live' ){
msg = `${trackID} add ${beat} note ${number} ${velocity} ${duration}`
}else{
msg = [ 0x90 + seq.object.number, number, velocity ]
}
msgs.push( msg )
}
return msgs
},
cc( number, value, beat, trackID, seq ) {
let msg = ''
if( seq.__client === 'max' ) {
msg = `add ${beat} cc ${trackID} ${number} ${value}`
}else if( seq.__client === 'live' ){
msg = `${trackID} add ${beat} cc ${number} ${value}`
}else{
msg = [ 0xb0 + seq.object.number, number, value ]
}
//console.log( 'cc msg:', msg, seq.__client )
return msg
},
},
start() {
if( this.running ) return
this.running = true
//console.log( 'starting with offset', this.offset )
Gibber.Scheduler.addMessage( this, Big( this.offset ), true, this.priority, this.object.__client )
return this
},
stop( delay = 0) {
if( delay === 0 ) {
this.running = false
}else{
Gibber.Utility.future( ()=> this.running = false, delay )
}
},
once() {
this.values.filters.push( (args,ptrn) => {
if( args[2] === ptrn.values.length - 1 ) {
this.stop()
}
return args
})
return this
},
repeat( numberOfTimes ) {
let count = 0
this.values.filters.push( (args,ptrn) => {
// XXX don't know why this wonky math is necessary
// something with the messages already being in the
// scheduling queue?
if( (args[2]+1) % ptrn.values.length === 0 ) {
if( count++ === numberOfTimes-1 ) this.stop()
}
return args
})
return this
},
clear() {
this.stop()
if( this.timings !== undefined && typeof this.timings.clear === 'function' ) this.timings.clear()
if( typeof this.values.clear === 'function' ) this.values.clear()
},
delay( v ) {
this.offset = v
return this
},
tick( scheduler, beat, beatOffset, priority=0 ) {
if( !this.running ) return
let _beatOffset = parseFloat( beatOffset.toFixed( 6 ) ),
shouldExecute = false
// if sequencer is not on autorun...
if( this.timings !== undefined ) {
this.timings.nextTime = _beatOffset
// pick a new timing and schedule tick
let nextTime = this.timings()
if( typeof nextTime === 'function' ) nextTime = nextTime()
if( typeof nextTime === 'object' ) {
shouldExecute = nextTime.shouldExecute
nextTime = nextTime.time
}else{
shouldExecute = true
}
let bigTime = Big( nextTime )
scheduler.addMessage( this, bigTime, true, this.priority )
}else{
shouldExecute = true
}
if( shouldExecute ) {
this.values.nextTime = _beatOffset
this.values.beat = beat
this.values.beatOffset = _beatOffset
this.values.scheduler = scheduler
let value = this.values()
if( typeof value === 'function' ) value = value()
if( value !== null ) {
// delay messages
if( this.externalMessages[ this.key ] !== undefined ) {
//Gibber.Communication.send( msg )
if( this.autorun.init === true ) {
if( this.key === 'note' || this.key === 'midinote' ) {
if( this.object !== undefined && this.object !== null ) {
if( Array.isArray( this.object.autorun ) ) {
for( let seq of this.object.autorun ) {
seq.tick( scheduler, beat, beatOffset, 1 )
}
}
}
// for per sequence (i.e. tracks[0].note[0]) sequences,
// primarily velocity (could also be duration)
if( Array.isArray( this.autorun ) ) {
for( let seq of this.autorun ) {
seq.tick( scheduler, beat, beatOffset, 1 )
}
}
}
}else{
this.autorun.init = true
}
let msg = this.externalMessages[ this.key ]( value, beat + _beatOffset, this.trackID, this )
if( this.__client !== 'midi' ) {
scheduler.msgs.push( [msg, this.__client ])
}else{
if( this.key === 'note' || this.key === 'midinote' ) {
//this.externalMessages[ this.key ]( value, Gibber.Utility.beatsToMs( _beatOffset ) )
Gibber.MIDI.send( msg, Gibber.Utility.beatsToMs( _beatOffset + 1, Gibber.Scheduler.bpm ), true, this.duration() )
}else{
Gibber.MIDI.send( msg, Gibber.Utility.beatsToMs( _beatOffset + 1, Gibber.Scheduler.bpm ), false )
}
}
} else { // schedule internal method / function call immediately
if( this.object !== undefined && this.key !== undefined ) {
if( typeof this.object[ this.key ] === 'function' ) {
this.object[ this.key ]( value )
}else{
this.object[ this.key ] = value
}
}
}
}
}
//console.log( 'beat', beat )
//this.timings.nextTime = _beatOffset // for scheduling pattern updates
},
}
// create external messages for cc0, cc1, cc2 etc.
for( let i = 0; i < 128; i++ ) {
proto.externalMessages[ 'cc' + i ] = ( value, beat, trackID, seq ) => {
let msg = ''
let number = i
if( seq.__client === 'max' ) {
msg = `add ${beat} cc ${trackID} ${number} ${value}`
}else if( seq.__client === 'live' ){
msg = `${trackID} add ${beat} cc ${number} ${value}`
}else{
msg = [ 0xb0 + seq.object.number, number, value ]
}
return msg
}
}
proto.create = proto.create.bind( proto )
proto.create.DO_NOT_OUTPUT = proto.DO_NOT_OUTPUT
proto.create._seqs = proto._seqs
proto.create.proto = proto
return proto.create
}
module.exports = seqclosure