gibber.core.lib
Version:
small set of core objects and functions for gibber
896 lines (759 loc) • 27.6 kB
JavaScript
const patternWrapper = function( Gibber ) {
"use strict"
// hack to pass Gibberish to pattern generator from within worklet processor
let Gibberish
if( Gibber.Gibberish === undefined ) {
Gibberish = Gibber.Audio !== undefined ? Gibber.Audio.Gibberish : Gibber
}else{
Gibberish = Gibber.Gibberish
}
let PatternProto = Object.create( function(){} )
// this prototype is somewhat limited, as we want to be able to add
// .seq() methods to everything. This means that every pattern needs its own
// copy of each method. One alternative would be to use a more comprehensive
// prototype and then place proxies on each method of each pattern that access
// object-specific sequencers... but just making copies of all functions is certainly
// simpler.
Object.assign( PatternProto, {
type:'pattern',
DNR: -987654321,
concat( _pattern ) { this.values = this.values.concat( _pattern.values ) },
//toString() { return this.values.toString() },
//valueOf() { return this.values },
__message( key, args ) {
if( this.category === 'audio' ) {
if( Gibberish.mode === 'processor' )
Gibberish.processor.messages.push( this.id, key, args )
}
},
getLength() {
let l
if( this.start <= this.end ) {
l = this.end - this.start + 1
}else{
l = this.values.length + this.end - this.start + 1
}
return l
},
runFilters( val, idx ) {
let args = [ val, 1, idx ] // 1 is phaseModifier
for( let filter of this.filters ) {
const __args = args
try {
args = filter( args, this )
} catch( e ) {
console.error( e )
console.log( 'filter problem?' )
console.log( `removing bad filter from pattern: ${filter.toString()}` )
const idx = this.filters.indexOf( filter )
this.filters.splice( idx, 1 )
args = __args
}
}
// XXX why is this one off from the worklet-side pattern id?
this.__message( 'update.value', args.override === undefined ? args[0] : args.override )
this.__message( 'update.currentIndex', args[2] )
if( this.isGen === true ) {
this.__message( 'waveformPoint', args.override === undefined ? args[0] : args.override )
}
return args
},
checkForUpdateFunction( name, ...args ) {
if( this.__delayAnnotations === true ) {
setTimeout( ()=> {
if( this.listeners[ name ] ) {
this.listeners[ name ].apply( this, args )
}else if( Pattern.listeners[ name ] ) {
Pattern.listeners[ name ].apply( this, args )
}
}, 5 )
}else{
if( this.listeners[ name ] ) {
this.listeners[ name ].apply( this, args )
}else if( Pattern.listeners[ name ] ) {
Pattern.listeners[ name ].apply( this, args )
}
}
},
// used when _onchange has not been assigned to individual patterns
_onchange() {},
// XXX it would be nice if filters used a similar style to fx chains...
// pattern.filters.add( ) etc.
addFilter( filter, name=null ) {
if( name !== null ) {
filter.__name = name
const idx = this.filters.findIndex( f => f.__name === name )
this.filters.splice( idx, 1 )
}
this.filters.push( filter )
return this
},
removeFilter( idx=null ) {
if( idx !== null ) {
this.filters.splice( idx, 1 )
}else{
this.filters.length = 0
}
return this
},
inspect() {
if( Gibberish.mode === 'processor' )
console.table({ values:this.values.toString(), 'number of filters':this.filters.length, phase:this.phase })
},
render( cat='Audio' ) {
this.category = cat
if( typeof Gibber[ cat ] === 'object' && typeof Gibber[ cat ].patternRender === 'function' ) {
Gibber[ cat ].patternRender( this )
}
if( typeof this.onrender === 'function' ) {
this.onrender()
}
},
copy() {
const p = Pattern( ...this.values )
//this.filters.forEach( f => p.addFilter( f ) )
p.start = this.start
p.end = this.end
p.phase = this.phase
return p
},
__methodNames: [
'rotate','switch','invert','flip',
'transpose','reverse','shuffle','scale',
'store', 'range', 'set', 'freeze', 'thaw', 'double'
]
})
const Pattern = function( ...args ) {
//console.log( 'args[0]:', args[0].isPattern, args[0] )
//if( typeof args[0] === 'function' && args[0] !== null && args[0].isPattern === true ) {
// console.log( 'got pattern' )
// return args[0]
//}
const isFunction = args.length === 1 && typeof args[0] === 'function',
isGen = typeof args[0] === 'object' && args[0].__isGen
//if( isGen === true ) {
// // must have a priority or it screws us codegen for analysis
// args[0].priority = 0
// Gibberish.analyzers.push( args[0] )
// Gibberish.dirty( Gibberish.analyzers )
//}
const fnc = function() {
let len = fnc.getLength(),
idx, val, args
if( len === 1 ) {
idx = 0
}else{
idx = fnc.phase > -1 ? Math.floor( fnc.start + (fnc.phase % len ) ) : Math.floor( fnc.end + (fnc.phase % len ) )
}
if( isFunction ) {
val = fnc.values[ 0 ]()
args = fnc.runFilters( val, idx )
val = args[0]
} else if( isGen === true ) {
val = fnc.values[ 0 ].callback.out[0]
args = fnc.runFilters( val, idx )
val = args[0]
}else{
val = fnc.values[ Math.floor( idx % fnc.values.length ) ]
args = fnc.runFilters( val, idx )
fnc.phase += fnc.stepSize * args[ 1 ]
val = args[ 0 ]
}
// check to see if value is a function, and if so evaluate it
//if( typeof val === 'function' ) {
//val = val()
//}
/*else if ( Array.isArray( val ) ) {
// if val is an Array, loop through array and evaluate any functions found there. TODO: IS THIS SMART?
for( let i = 0; i < val.length; i++ ){
if( typeof val[ i ] === 'function' ) {
val[ i ] = val[ i ]()
}
}
}
*/
//XXX is this needed? Adding it interferes with Steps
//if( val === fnc.DNR ) val = null
return val
}
let out
const DNR = -987654321
Object.assign( fnc, {
category:'audio',
start : 0,
end : 0,
phase : 0,
values : args,
isPattern: true,
__frozen:false,
// wrap annotation update in setTimeout( func, 0 )
__delayAnnotations:false,
//values : typeof arguments[0] !== 'string' || arguments.length > 1 ? Array.prototype.slice.call( arguments, 0 ) : arguments[0].split(''),
original : null,
storage : [],
stepSize : 1,
integersOnly : false,
filters : [],
__listeners: [],
onchange : null,
isop:true,
repeats: {},
isGen,
freeze( shouldFreezeTheory = true ) {
fnc.__frozen = true
},
thaw() {
fnc.__frozen = false
Gibber.Theory.thaw()
},
setSeq( seq ) {
this.seq = seq
},
range(...args) {
if( this.__rendered !== undefined && this.__rendered !== this ) {
this.__rendered.range( ...args )
return this
}
if( !fnc.__frozen ) {
let start, end
if( Array.isArray( arguments[0] ) ) {
start = arguments[0][0]
end = arguments[0][1]
}else{
start = arguments[0]
end = arguments[1]
}
if( start < end ) {
fnc.start = start
fnc.end = end
}else{
fnc.start = end
fnc.end = start
}
this.checkForUpdateFunction( 'range', fnc )
}
return fnc
},
// doubles length of the array by duplicating values
double(...args) {
if( this.__rendered !== undefined && this.__rendered !== this ) {
this.__rendered.double(...args)
return this
}
if( !fnc.__frozen ) {
fnc.values.push( ...fnc.values )
fnc.end = fnc.values.length - 1
// if( fnc.end > fnc.values.length - 1 ) {
// fnc.end = fnc.values.length - 1
// }else if( fnc.end < )
if( Gibberish.mode === 'processor' ) {
fnc.__message( 'values', fnc.values )
fnc.__message( '_onchange', true )
}
fnc._onchange()
}
return fnc
},
set(...args) {
if( this.__rendered !== undefined && this.__rendered !== this ) {
this.__rendered.set(...args)
return this
}
if( !fnc.__frozen ) {
let args = Array.isArray( arguments[ 0 ] ) ? arguments[ 0 ] : arguments
fnc.values.length = 0
const tmp = []
for( let i = 0; i < args.length; i++ ) {
const val = args[i].isPattern === true ? args[ i ].original.slice(0) : args[ i ]
tmp.push( val )
}
fnc.values = tmp
fnc.end = fnc.values.length - 1
// if( fnc.end > fnc.values.length - 1 ) {
// fnc.end = fnc.values.length - 1
// }else if( fnc.end < )
if( Gibberish.mode === 'processor' ) {
fnc.__message( 'values', fnc.values )
fnc.__message( '_onchange', true )
}
fnc._onchange( 'set', fnc.values ) //args )
}
return fnc
},
reverse() {
if( this.__rendered !== undefined && this.__rendered !== this ) {
this.__rendered.reverse()
return this
}
if( !fnc.__frozen ) {
let array = fnc.values,
left = null,
right = null,
length = array.length,
temporary;
for ( left = 0, right = length - 1; left < right; left += 1, right -= 1 ) {
temporary = array[ left ]
array[ left ] = array[ right ]
array[ right ] = temporary;
}
if( Gibberish.mode === 'processor' ) {
fnc.__message( 'values', array )
fnc.__message( '_onchange', true )
}
fnc._onchange( 'reverse', null )
}
return fnc
},
// humanize: function( randomMin, randomMax ) {
// let lastAmt = 0
//
// for( let i = 0; i < this.filters.length; i++ ) {
// if( this.filters[ i ].humanize ) {
// lastAmt = this.filters[ i ].lastAmt
// this.filters.splice( i, 1 )
// break;
// }
// }
//
// let filter = function( args ) {
// console.log( filter.lastAmt, args[0])
// args[ 0 ] -= filter.lastAmt
// filter.lastAmt = Gibber.Clock.time( Gibber.Utilities.rndi( randomMin, randomMax ) )
//
// console.log( "LA", filter.lastAmt )
// args[0] += filter.lastAmt
//
// return args
// }
// filter.lastAmt = lastAmt
// filter.humanize = true
//
// this.filters.push( filter )
//
// return this
// },
// TODO how do we make this run in the audio thread?
// syn.note.seq( [0,1,2,3].rnd( 1/16,2, 1/3,3 )
repeat( ...args ) {
if( this.__rendered !== undefined && this.__rendered !== this ) {
this.__rendered.repeat(...args)
return this
}
for( let i = 0; i < arguments.length; i +=2 ) {
fnc.repeats[ arguments[ i ] ] = {
phase: 0,
target: arguments[ i + 1 ]
}
}
let repeating = false, repeatValue = null, repeatIndex = null
let filter = function( args ) {
let value = args[ 0 ], phaseModifier = args[ 1 ], output = args
//console.log( args, counts )
if( repeating === false && fnc.repeats[ value ] ) {
repeating = true
repeatValue = value
repeatIndex = args[2]
}
if( repeating === true ) {
if( fnc.repeats[ repeatValue ].phase !== fnc.repeats[ repeatValue ].target ) {
output[ 0 ] = repeatValue
output[ 1 ] = 0
output[ 2 ] = repeatIndex
//[ val, 1, idx ]
fnc.repeats[ repeatValue ].phase++
}else{
fnc.repeats[ repeatValue ].phase = 0
output[ 1 ] = 1
if( value !== repeatValue ) {
repeating = false
}else{
fnc.repeats[ repeatValue ].phase++
}
}
}
return output
}
fnc.filters.push( filter )
return fnc
},
reset() {
if( this.__rendered !== undefined && this.__rendered !== this ) {
this.__rendered.reset()
return this
}
if( !fnc.__frozen ) {
// XXX replace with some type of standard deep copy
if( Array.isArray( fnc.original[0] ) ) {
const arr = []
for( let i = 0; i < fnc.original.length; i++ ) {
const chord = fnc.original[ i ]
arr[ i ] = []
for( let j = 0; j < chord.length; j++ ) {
arr[ i ][ j ] = chord[ j ]
}
}
fnc.values = arr
}else{
fnc.values = fnc.original.slice(0)
}
//fnc.set( fnc.original.slice( 0 ) );
if( Gibberish.mode === 'processor' ) {
fnc.__message( 'values', fnc.values )
fnc.__message( '_onchange', true )
}
fnc._onchange( 'reset', null )
}
return fnc
},
store( pos ) { fnc.storage[ pos || fnc.storage.length ] = fnc.values.slice( 0 ); return fnc; },
transpose( amt ) {
if( this.__rendered !== undefined && this.__rendered !== this ) {
this.__rendered.transpose( amt )
return this
}
if( !fnc.__frozen ) {
for( let i = 0; i < fnc.values.length; i++ ) {
let val = fnc.values[ i ]
if( Array.isArray( val ) ) {
for( let j = 0; j < val.length; j++ ) {
if( typeof val[ j ] === 'number' && val !== -987654321 ) {
val[ j ] = fnc.integersOnly ? Math.round( val[ j ] + amt ) : val[ j ] + amt
}
}
}else{
if( typeof val === 'number' && val !== -987654321 ) {
fnc.values[ i ] = fnc.integersOnly ? Math.round( fnc.values[ i ] + amt ) : fnc.values[ i ] + amt
}
}
}
if( Gibberish.mode === 'processor' ) {
fnc.__message( 'values', fnc.values )
fnc.__message( '_onchange', ['transpose', amt] )
}
//fnc._onchange( 'transpose', amt )
}
return fnc
},
shuffle() {
if( this.__rendered !== undefined && this.__rendered !== this ) {
this.__rendered.shuffle( )
return this
}
if( !fnc.__frozen ) {
Gibber.Utility.shuffle( fnc.values )
fnc._onchange( 'shuffule', null )
}
return fnc
},
scale( amt ) {
if( this.__rendered !== undefined && this.__rendered !== this ) {
this.__rendered.scale( amt )
return this
}
if( !fnc.__frozen ) {
fnc.values.map( (val, idx, array) => {
if( Array.isArray( val ) ) {
array[ idx ] = val.map( inside => {
if( typeof inside === 'number' && inside !== DNR) {
return fnc.integersOnly ? Math.round( inside * amt ) : inside * amt
} else {
return inside
}
})
}else{
if( typeof val === 'number' && val !== DNR ) {
array[ idx ] = fnc.integersOnly ? Math.round( val * amt ) : val * amt
}
}
})
if( Gibberish.mode === 'processor' ) {
fnc.__message( 'values', fnc.values )
fnc.__message( '_onchange', true )
}
fnc._onchange( 'scale', amt )
}
return fnc
},
flip() {
if( this.__rendered !== undefined && this.__rendered !== this ) {
this.__rendered.flip( )
return this
}
if( !fnc.__frozen ) {
let start = [],
ordered = null
ordered = fnc.values.filter( function(elem) {
let shouldPush = start.indexOf( elem ) === -1
if( shouldPush ) start.push( elem )
return shouldPush
})
ordered = ordered.sort( function( a,b ){ return a - b } )
for( let i = 0; i < fnc.values.length; i++ ) {
let pos = ordered.indexOf( fnc.values[ i ] )
fnc.values[ i ] = ordered[ ordered.length - pos - 1 ]
}
if( Gibberish.mode === 'processor' ) {
fnc.__message( 'values', fnc.values )
fnc.__message( '_onchange', true )
}
fnc._onchange( 'flip', null )
}
return fnc
},
invert() {
if( this.__rendered !== undefined && this.__rendered !== this ) {
this.__rendered.invert( )
return this
}
if( !fnc.__frozen ) {
// first non-rest value
let prime0 = fnc.values.find( v=> v !== DNR )
for( let i = 1; i < fnc.values.length; i++ ) {
if( typeof fnc.values[ i ] === 'number' && fnc.values[i] !== DNR ) {
let inverse = prime0 + (prime0 - fnc.values[ i ])
fnc.values[ i ] = inverse
}
}
if( Gibberish.mode === 'processor' ) {
fnc.__message( 'values', fnc.values )
fnc.__message( '_onchange', true )
}
fnc._onchange( 'invert', null )
}
return fnc
},
switch( to ) {
if( this.__rendered !== undefined && this.__rendered !== this ) {
this.__rendered.switch( to )
return this
}
if( !fnc.__frozen ) {
if( fnc.storage[ to ] ) {
fnc.values = fnc.storage[ to ].slice( 0 )
}
fnc._onchange( 'switch', to )
}
return fnc
},
rotate( amt ) {
if( this.__rendered !== undefined && this.__rendered !== this ) {
this.__rendered.rotate( amt )
return this
}
if( !fnc.__frozen ) {
if( amt > 0 ) {
while( amt > 0 ) {
let end = fnc.values.pop()
fnc.values.unshift( end )
amt--
}
}else if( amt < 0 ) {
while( amt < 0 ) {
let begin = fnc.values.shift()
fnc.values.push( begin )
amt++
}
}
if( Gibberish.mode === 'processor' ) {
fnc.__message( 'values', fnc.values )
fnc.__message( '_onchange', true )
}
fnc._onchange( 'rotate', amt )
}
return fnc
},
clear() {
if( Gibberish.mode === 'worklet' ) {
for( let key of PatternProto.__methodNames ) {
for( let i = 0; i < out[ key ].sequencers.length; i++ ) {
// this can most certainly be optimized, but I had real problems
// getting this clearing to work, perhaps related to proxy behaviors?
const __seq = Gibber.Seq.sequencers.find( s => s.id === out[ key ][ i ].id )
if( __seq !== undefined ) {
Gibber.Audio.Gibberish.worklet.port.postMessage({ address:'method', object:__seq.id, name:'stop', args:[] })
__seq.stop()
__seq.clear()
const idx = Gibber.Seq.sequencers.indexOf( __seq )
Gibber.Seq.sequencers.splice( idx, 1 )
__seq.target[ __seq.key ][0].stop()
}
}
}
}else{
// genish-based patterns are connected as analyzers so that they
// don't have to feed into a bus to get rendered. we must remove them
// from the analysis array to finalize clearing.
if( out.isGen === true ) {
const idx = Gibberish.analyzers.indexOf( args[0] )
if( idx !== -1 ) {
Gibberish.analyzers.splice( idx, 1 )
Gibberish.analyzers.dirty = true
}
}
}
}
})
if( Gibberish.mode === 'worklet' ) {
fnc.id = Gibberish.utilities.getUID()
if( isGen === true ) {
fnc.waveformPoint = val => {
// accounts for annoying edge case where wave pattern is
// inlined to a call to .seq
// XXX fix in parsing or somehow figure out how to only do this once
if( fnc.widget !== undefined ) fnc.values[0].widget = fnc.widget
if( fnc.values[0].widget !== undefined ) {
// convert samples to beats
if( fnc.__patternType === 'timings' ) {
val = Gibber.Clock.stob( val )
}
fnc.values[0].widget.values[ fnc.values[0].widget.values.length - 1 ] = { value:val }
}
}
}
}
//fnc.filters.pattern = fnc
// can I resotre this without making the object non-serializable?
//fnc.retrograde = fnc.reverse.bind( fnc )
fnc.end = fnc.values.length - 1
/*
fnc.filters.test = function() { console.log( 'test' ) }
fnc.filters.add = function( f ) {
this.filters.push( f )
}.bind(fnc)
fnc.filters.remove = function( idx ) {
console.log( 'filter:', this )
this.filters.splice( idx, 1 )
}.bind( fnc )
fnc.filters.inspect = function() {
console.log( `there are currenly ${this.length} filters on this pattern` )
}.bind(fnc)
*/
if( Array.isArray( fnc.values[0] ) ) {
const arr = []
for( let i = 0; i < fnc.values.length; i++ ) {
const chord = fnc.values[ i ]
arr[ i ] = []
for( let j = 0; j < chord.length; j++ ) {
arr[ i ][ j ] = chord[ j ]
}
}
fnc.original = arr
}else{
fnc.original = fnc.values.slice(0)
}
fnc.storage[ 0 ] = fnc.original.slice( 0 )
fnc.integersOnly = fnc.values.every( function( n ) { return n === +n && n === (n|0); })
fnc.listeners = {}
fnc.sequences = {}
if( Gibberish.mode === 'worklet' ) {
for( let key of PatternProto.__methodNames ) {
Gibber.addSequencing( fnc,key,2,undefined )
}
//for( let key of PatternProto.__methodNames ) {
// fnc.sequencers[ key ] = Gibber.Core !== undefined
// ? Gibber.Core.addSequencing( fnc, key, 2 )
// : Gibber.addSequencing( fnc,key,2 )
//}
fnc.sequences.reset = Gibber.addSequencing( fnc, 'reset', 1 )
}
// TODO: Gibber.createProxyProperties( fnc, { 'stepSize':0, 'start':0, 'end':0 })
fnc.__proto__ = PatternProto
let pn = ''
Object.defineProperty( fnc, 'patternName', {
get() { return pn },
set(__pn) {
pn = __pn
}
})
fnc.render = function( mode='audio' ) {
if( mode === 'audio' ) {
// 'isPattern' is a hack to force pattern initialization arguments to be submitted as
// a list, instead of in a property dictionary. When 'isPattern' is true, gibberish
// looks for an 'inputs' property and then passes its value (assumed to be an array)
// using the spread operator to the constructor.
out = Gibberish.Proxy( 'pattern', { inputs:fnc.values, isPattern:true, filters:fnc.filters, id:fnc.id }, fnc )
if( isGen === true ) {
// must have a priority or it screws up codegen for analysis
args[0].priority = 0
Gibberish.analyzers.push( args[0] )
Gibberish.dirty( Gibberish.analyzers )
}
}
if( args.filters ) {
args.filters.forEach( f => out.addFilter( f ) )
}else if( typeof args[0] === 'object' && args[0].filters ) {
args[0].filters.forEach( f => out.addFilter( f ) )
}
Pattern.children.push( out )
if( fnc.onrender ) fnc.onrender( out )
fnc.__rendered = out
return out
}
if( Gibberish.mode === 'processor' ) return fnc.render()
return fnc
}
Pattern.listeners = {}
Pattern.children = []
Pattern.__isFrozen = false
Pattern.freeze = function( shouldFreezeTheory = true ) {
Pattern.children.forEach( p => p.freeze() )
if( shouldFreezeTheory === true ) {
Gibber.Theory.freeze()
Pattern.__isFrozen = true
}
}
Pattern.thaw = ()=> {
Pattern.children.forEach( p => p.thaw() )
if( Pattern.__isFrozen === true ) {
Gibber.Theory.thaw()
Pattern.__isFrozen = false
}
}
Pattern.export = function( obj ) {
obj.freeze = Pattern.freeze
obj.thaw = Pattern.thaw
}
Pattern.listeners.range = function( fnc ) {
//if( !Notation.isRunning ) return
if( Gibberish.mode === 'processor' ) return
// TODO: don't use Gibber.currentTrack, store the object in the pattern
let rangeStart = fnc.markers[ fnc.start ].find(),
rangeEnd = fnc.markers[ fnc.end ].find()
if( !fnc.range.init ) {
fnc.range.init = true
var ptrnStart = fnc.markers[ 0 ].find(),
ptrnEnd = fnc.markers[ fnc.markers.length - 1 ].find()
//fnc.column.editor.markText( ptrnStart.from, ptrnEnd.to, { className:'rangeOutside' })
Gibber.Environment.editor.markText( ptrnStart.from, ptrnEnd.to, { className:'rangeOutside' })//className:'pattern-update-range-outside' })
if( !Pattern.listeners.range.initialzied ) Pattern.listeners.range.init()
}
if( fnc.range.mark ) fnc.range.mark.clear()
//fnc.range.mark = fnc.column.editor.markText( rangeStart.from, rangeEnd.to, { className:'rangeInside' })
// TODO: Dont use GE.codemirror... how else do I get this? stored in pattern is created?
fnc.range.mark = Gibber.Environment.editor.markText( rangeStart.from, rangeEnd.to, { className:'rangeInside' })
}
Pattern.listeners.range.init = function() {
//$.injectCSS({
// '.rangeOutside': {
// 'color':'#666 !important'
// },
// '.rangeInside': {
// 'color':'rgba(102, 153, 221, 1) !important'
// }
//})
Pattern.listeners.range.initialized = true
}
//Pattern.prototype = PatternProto*/
return Pattern
}
// helper function to pass the pattern constructor to the gibberish worklet processor.
patternWrapper.transfer = function( Audio, constructorString ) {
if( Audio.Gibberish !== undefined && Audio.Gibberish.mode === 'worklet' ) {
Audio.Gibberish.worklet.port.postMessage({
address:'addConstructor',
name:'Pattern',
constructorString
})
}
}
module.exports = patternWrapper