gibberish-dsp
Version:
Gibberish is designed to be an optimized API for audio synthesis using per-sample techniques.
370 lines (322 loc) • 13.1 kB
JavaScript
class GibberishProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {}
constructor( options ) {
super( options )
Gibberish = global.Gibberish
Gibberish.ctx = { sampleRate }
Gibberish.genish.hasWorklet = false
Gibberish.preventProxy = true
// TODO: need to figure out how to read memory amount
// perhaps attach initialization to a message instead?
// then we could easily re-init...
Gibberish.init( undefined, undefined, 'processor' )
Gibberish.preventProxy = false
Gibberish.debug = false
Gibberish.processor = this
Gibberish.time = 0
this.port.onmessage = this.handleMessage.bind( this )
this.queue = []
Gibberish.ugens = this.ugens = new Map()
// XXX ridiculous hack to get around processor not having a worklet property
Gibberish.worklet = { ugens: this.ugens, port:this.port }
Gibberish.genish.gen.samplerate = sampleRate
this.ugens.set( Gibberish.id, Gibberish )
this.messages = []
}
replaceProperties( obj ) {
if( Array.isArray( obj ) ) {
const out = []
for( let i = 0; i < obj.length; i++ ){
const prop = obj[ i ]
if( prop === null ) continue
//console.log( 'PROP:', prop )
if( typeof prop === 'object' && prop.id !== undefined ) {
let objCheck = this.ugens.get( prop.id )
if( objCheck !== undefined ) {
out[ i ] = prop.prop !== undefined ? objCheck[ prop.prop ] : objCheck
if( prop.prop !== undefined ) console.log( 'got a ssd.out', prop, objCheck )
}else{
out[ i ]= prop
}
}else{
if( prop === null ) continue
if( typeof prop === 'object' && prop.action === 'wrap' ) {
out[ i ] = prop.value.bind( null, ...this.replaceProperties( prop.args ) )
}else if( Array.isArray( prop ) ) {
out[ i ] = this.replaceProperties( prop )
}else{
out[ i ] = prop
}
}
}
return out
}else{
const properties = obj
for( let key in properties) {
let prop = properties[ key ]
if( typeof prop === 'object' && prop !== null && prop.id !== undefined ) {
let objCheck = this.ugens.get( prop.id )
if( objCheck !== undefined ) {
properties[ key ] = objCheck
}
}else if( Array.isArray( prop ) ) {
properties[ key ] = this.replaceProperties( prop )
}else{
if( typeof prop === 'object' && prop !== null && prop.action === 'wrap' ) {
properties[ key ] = prop.value()
}
}
}
return properties
}
return obj
}
// playback delayed messages and clear the queue
playQueue() {
// must set delay property to false!!! otherwise the message
// will be delayed continually...
this.queue.forEach( m => {
m.data.delay = false;
try {
this.handleMessage( m )
}catch( e ) {
console.error( e )
}
})
this.queue.length = 0
}
handleMessage( event ) {
if( event.data.delay === true ) {
// we want to delay this message for some time in the future,
// for example, when forcing code to execute at the start of the next
// measure. playQueue will trigger all messages in the queue
this.queue.push( event )
return
}
if( event.data.address === 'add' ) {
const rep = event.data
let constructor = Gibberish
let properties = this.replaceProperties( eval( '(' + rep.properties + ')' ) )
//console.log( 'properties:', properties )
let ugen
// if object is not a gibberish ugen...
if( properties.nogibberish ) {
ugen = properties
}else{
for( let i = 0; i < rep.name.length; i++ ) { constructor = constructor[ rep.name[ i ] ] }
properties.id = rep.id
ugen = properties.isop === true || properties.isPattern === true
? constructor( ...properties.inputs )
: constructor( properties )
if( properties.isPattern ) {
for( let key in properties ) {
if( key !== 'input' && key !== 'isPattern' ) {
ugen[ key ] = properties[ key ]
}
}
}
}
if( rep.post ) {
ugen[ rep.post ]()
}
//console.log( 'adding ugen:', ugen.id, ugen, rep )
this.ugens.set( rep.id, ugen )
ugen.id = rep.id
initialized = true
}else if( event.data.address === 'method' ) {
//if( event.data.name === 'clear' )
//console.log( event.data.address, event.data.name, event.data.args, this.ugens )
const dict = event.data
const obj = this.ugens.get( dict.object )
if( obj === undefined || typeof obj[ dict.name ] !== 'function' ) return
// for edge case when serialized functions are being passed to method calls
if( dict.functions === true ) {
obj[ dict.name ]( eval( '(' + dict.args + ')' ) )
}else{
obj[ dict.name ]( ...dict.args.map( Gibberish.proxyReplace ) )
}
}else if( event.data.address === 'property' ) {
// XXX this is the exact same as the 'set' key... ugh.
const dict = event.data
const obj = this.ugens.get( dict.object )
let value = dict.value
if( typeof dict.value === 'object' && dict.value !== null && dict.value.id !== undefined ) {
value = this.ugens.get( dict.value.id )
}
obj[ dict.name ] = value
}else if( event.data.address === 'print' ) {
const dict = event.data
const obj = this.ugens.get( dict.object )
console.log( 'printing:', dict.object, obj )
}else if( event.data.address === 'printProperty' ) {
const dict = event.data
const obj = this.ugens.get( dict.object )
console.log( 'printing:', obj[ dict.name ] )
}else if( event.data.address === 'set' ) {
const dict = event.data
const obj = this.ugens.get( dict.object )
let value = dict.value
if( typeof dict.value === 'object' && dict.value !== null && dict.value.id !== undefined ) {
value = this.ugens.get( dict.value.id )
}
obj[ dict.name ] = value
}else if( event.data.address === 'copy' ) {
const target = this.ugens.get( event.data.id )
if( target === undefined ) {
// this should only occur when a buffer is loaded prior to a delayed instantiation. for example,
// if gibber starts downloading a file, on beat two and is finished by beat three, the next measure
// will not have occurred yet, meaning a delayed sampler instantiation will not yet have occurred.
// in this case, we wait until the next measure boundary.
this.queue.push( event )
}else{
target.data.onload( event.data.buffer )
}
}else if( event.data.address === 'copy_multi' ) {
const target = this.ugens.get( event.data.id )
if( target === undefined ) {
// this should only occur when a buffer is loaded prior to a delayed instantiation. for example,
// if gibber starts downloading a file, on beat two and is finished by beat three, the next measure
// will not have occurred yet, meaning a delayed sampler instantiation will not yet have occurred.
// in this case, we wait until the next measure boundary.
this.queue.push( event )
}else{
const sampler = target.samplers[ event.data.filename ]
if( sampler !== undefined )
sampler.data.onload( event.data.buffer )
else
target.loadSample( event.data.filename, null, event.data.buffer )
}
}else if( event.data.address === 'callback' ) {
console.log( Gibberish.callback.toString() )
}else if( event.data.address === 'addConstructor' ) {
const wrapper = eval( '(' + event.data.constructorString + ')' )
Gibberish[ event.data.name ] = wrapper( Gibberish, Gibberish.genish )
}else if( event.data.address === 'addMethod' ) {
const target = this.ugens.get( event.data.id )
if( target[ event.data.key ] === undefined ) {
target[ event.data.key ] = eval( '(' + event.data.function + ')' )
//console.log( 'adding method:', target, event.data.key )
}
}else if( event.data.address === 'monkeyPatch' ) {
const target = this.ugens.get( event.data.id )
if( target['___'+event.data.key] === undefined ) {
target[ '___' + event.data.key ] = target[ event.data.key ]
target[ event.data.key ] = eval( '(' + event.data.function + ')' )
//console.log( 'monkey patch:', target, event.data.key )
}
}else if( event.data.address === 'dirty' ) {
const obj = this.ugens.get( event.data.id )
Gibberish.dirty( obj )
}else if( event.data.address === 'initialize' ) {
initialized = true
}else if( event.data.address === 'addToProperty' ) {
const dict = event.data
const obj = this.ugens.get( dict.object )
obj[ dict.name ][ dict.key ] = dict.value
}else if( event.data.address === 'addObjectToProperty' ) {
const dict = event.data
const obj = this.ugens.get( dict.object )
obj[ dict.name ][ dict.key ] = this.ugens.get( dict.value )
}else if( event.data.address === 'messages' ) {
console.log( 'messages:', this.messages )
}else if( event.data.address === 'eval' ) {
eval( event.data.code )
}
}
process(inputs, outputs, parameters) {
if( initialized === true ) {
const gibberish = Gibberish
const scheduler = gibberish.scheduler
let callback = this.callback
let ugens = gibberish.callbackUgens
Gibberish.outputs = outputs
this.messages.length = 0
// XXX is there some way to optimize this out?
//if( callback === undefined && gibberish.graphIsDirty === false ) return true
let callbacklength = gibberish.blockCallbacks.length
if( callbacklength !== 0 ) {
for( let i=0; i< callbacklength; i++ ) {
gibberish.blockCallbacks[ i ]()
}
// can't just set length to 0 as callbacks might be added during for loop,
// so splice pre-existing functions
gibberish.blockCallbacks.splice( 0, callbacklength )
}
const output = outputs[ 0 ]
const len = outputs[0][0].length
let phase = 0
for (let i = 0; i < len; ++i) {
// run sequencers, catch errors and remove from queue
try {
phase = scheduler.tick()
} catch(e) {
console.error( e )
scheduler.queue.pop()
phase++
//continue
}
// if sequencing triggers codegen...
if( gibberish.graphIsDirty ) {
const oldCallback = callback
const oldUgens = ugens.slice(0)
const oldNames = gibberish.callbackNames.slice(0)
// generate callback and try to catch errors...
let cb
try{
cb = gibberish.generateCallback()
} catch(e) {
console.log( 'callback error:', e )
// restore callback
// XXX should some type of notification be sent to the main thread?
cb = oldCallback
gibberish.callbackUgens = oldUgens
gibberish.callbackNames = oldNames
gibberish.dirtyUgens.length = 0
gibberish.graphIsDirty = false
} finally {
ugens = gibberish.callbackUgens
this.callback = callback = cb
// tell main thread that new callback has been created
// in case it wants to display it / do something else
this.port.postMessage({ address:'callback', code:cb.toString() })
}
}
//XXX sub real samplerate sheesh
time += 1/sampleRate
if( callback !== undefined ) {
const out = callback.apply( null, ugens )
output[0][ i ] = out[0]
output[1][ i ] = out[1]
}
}
if( ugens.length > 1 ) {
for( let i = 1; i < ugens.length - 1; i++ ) {
const ugen = ugens[ i ]
if( ugen.out !== undefined ) {
//console.log( ugen.ugenName, ugen.out[0], ugen.out[1] )
//this.messages.push( ugen.id, 'output', ugen.out.length === 1 ? ugen.out[ 0 ] : ugen.out )
this.messages.push( ugen.id, 'output', ugen.out[0], ugen.out[1] )
}
}
}
if( this.messages.length > 0 ) {
try{
this.port.postMessage({
address:'state',
messages:this.messages
})
} catch( e ) {
console.groupCollapsed( 'There was an error passing state from the audio thread to the main thread.' )
console.error( e )
console.groupEnd()
}
}
this.port.postMessage({
address:'phase',
value: phase
})
}
// make sure this is always returned or the callback ceases!!!
return true
}
}