genish.js
Version:
310 lines (248 loc) • 8.81 kB
JavaScript
'use strict'
/* gen.js
*
* low-level code generation for unit generators
*
*/
const MemoryHelper = require( 'memory-helper' )
const EE = require( 'events' ).EventEmitter
const gen = {
accum:0,
getUID() { return this.accum++ },
debug:false,
samplerate: 44100, // change on audiocontext creation
shouldLocalize: false,
graph:null,
globals:{
windows: {},
},
mode:'worklet',
/* closures
*
* Functions that are included as arguments to master callback. Examples: Math.abs, Math.random etc.
* XXX Should probably be renamed callbackProperties or something similar... closures are no longer used.
*/
closures: new Set(),
params: new Set(),
inputs: new Set(),
parameters: new Set(),
endBlock: new Set(),
histories: new Map(),
memo: {},
//data: {},
/* export
*
* place gen functions into another object for easier reference
*/
export( obj ) {},
addToEndBlock( v ) {
this.endBlock.add( ' ' + v )
},
requestMemory( memorySpec, immutable=false ) {
for( let key in memorySpec ) {
let request = memorySpec[ key ]
//console.log( 'requesting ' + key + ':' , JSON.stringify( request ) )
if( request.length === undefined ) {
console.log( 'undefined length for:', key )
continue
}
request.idx = gen.memory.alloc( request.length, immutable )
}
},
createMemory( amount=4096, type ) {
const mem = MemoryHelper.create( amount, type )
return mem
},
createCallback( ugen, mem, debug = false, shouldInlineMemory=false, memType = Float64Array ) {
let isStereo = Array.isArray( ugen ) && ugen.length > 1,
callback,
channel1, channel2
if( typeof mem === 'number' || mem === undefined ) {
this.memory = this.createMemory( mem, memType )
}else{
this.memory = mem
}
this.outputIdx = this.memory.alloc( 2, true )
this.emit( 'memory init' )
//console.log( 'cb memory:', mem )
this.graph = ugen
this.memo = {}
this.endBlock.clear()
this.closures.clear()
this.inputs.clear()
this.params.clear()
this.globals = { windows:{} }
this.parameters.clear()
this.functionBody = " 'use strict'\n"
if( shouldInlineMemory===false ) {
this.functionBody += this.mode === 'worklet' ?
" var memory = this.memory\n\n" :
" var memory = gen.memory\n\n"
}
// call .gen() on the head of the graph we are generating the callback for
//console.log( 'HEAD', ugen )
for( let i = 0; i < 1 + isStereo; i++ ) {
if( typeof ugen[i] === 'number' ) continue
//let channel = isStereo ? ugen[i].gen() : ugen.gen(),
let channel = isStereo ? this.getInput( ugen[i] ) : this.getInput( ugen ),
body = ''
// if .gen() returns array, add ugen callback (graphOutput[1]) to our output functions body
// and then return name of ugen. If .gen() only generates a number (for really simple graphs)
// just return that number (graphOutput[0]).
body += Array.isArray( channel ) ? channel[1] + '\n' + channel[0] : channel
// split body to inject return keyword on last line
body = body.split('\n')
//if( debug ) console.log( 'functionBody length', body )
// next line is to accommodate memo as graph head
if( body[ body.length -1 ].trim().indexOf('let') > -1 ) { body.push( '\n' ) }
// get index of last line
let lastidx = body.length - 1
// insert return keyword
body[ lastidx ] = ' memory[' + (this.outputIdx + i) + '] = ' + body[ lastidx ] + '\n'
this.functionBody += body.join('\n')
}
this.histories.forEach( value => {
if( value !== null )
value.gen()
})
const returnStatement = isStereo ? ` return [ memory[${this.outputIdx}], memory[${this.outputIdx + 1}] ]` : ` return memory[${this.outputIdx}]`
this.functionBody = this.functionBody.split('\n')
if( this.endBlock.size ) {
this.functionBody = this.functionBody.concat( Array.from( this.endBlock ) )
this.functionBody.push( returnStatement )
}else{
this.functionBody.push( returnStatement )
}
// reassemble function body
this.functionBody = this.functionBody.join('\n')
// we can only dynamically create a named function by dynamically creating another function
// to construct the named function! sheesh...
//
if( shouldInlineMemory === true ) {
this.parameters.add( 'memory' )
}
let paramString = ''
if( this.mode === 'worklet' ) {
for( let name of this.parameters.values() ) {
paramString += name + ','
}
paramString = paramString.slice(0,-1)
}
const separator = this.parameters.size !== 0 && this.inputs.size > 0 ? ', ' : ''
let inputString = ''
if( this.mode === 'worklet' ) {
for( let ugen of this.inputs.values() ) {
inputString += ugen.name + ','
}
inputString = inputString.slice(0,-1)
}
let buildString = this.mode === 'worklet'
? `return function( ${inputString} ${separator} ${paramString} ){ \n${ this.functionBody }\n}`
: `return function gen( ${ [...this.parameters].join(',') } ){ \n${ this.functionBody }\n}`
if( this.debug || debug ) console.log( buildString )
callback = new Function( buildString )()
// assign properties to named function
for( let dict of this.closures.values() ) {
let name = Object.keys( dict )[0],
value = dict[ name ]
callback[ name ] = value
}
for( let dict of this.params.values() ) {
let name = Object.keys( dict )[0],
ugen = dict[ name ]
Object.defineProperty( callback, name, {
configurable: true,
get() { return ugen.value },
set(v){ ugen.value = v }
})
//callback[ name ] = value
}
callback.members = this.closures
callback.data = this.data
callback.params = this.params
callback.inputs = this.inputs
callback.parameters = this.parameters//.slice( 0 )
callback.out = this.memory.heap.subarray( this.outputIdx, this.outputIdx + 2 )
callback.isStereo = isStereo
//if( MemoryHelper.isPrototypeOf( this.memory ) )
callback.memory = this.memory.heap
this.histories.clear()
return callback
},
/* getInputs
*
* Called by each individual ugen when their .gen() method is called to resolve their various inputs.
* If an input is a number, return the number. If
* it is an ugen, call .gen() on the ugen, memoize the result and return the result. If the
* ugen has previously been memoized return the memoized value.
*
*/
getInputs( ugen ) {
return ugen.inputs.map( gen.getInput )
},
getInput( input ) {
let isObject = typeof input === 'object',
processedInput
if( isObject ) { // if input is a ugen...
//console.log( input.name, gen.memo[ input.name ] )
if( gen.memo[ input.name ] ) { // if it has been memoized...
processedInput = gen.memo[ input.name ]
}else if( Array.isArray( input ) ) {
gen.getInput( input[0] )
gen.getInput( input[1] )
}else{ // if not memoized generate code
if( typeof input.gen !== 'function' ) {
console.log( 'no gen found:', input, input.gen )
input = input.graph
}
let code = input.gen()
//if( code.indexOf( 'Object' ) > -1 ) console.log( 'bad input:', input, code )
if( Array.isArray( code ) ) {
if( !gen.shouldLocalize ) {
gen.functionBody += code[1]
}else{
gen.codeName = code[0]
gen.localizedCode.push( code[1] )
}
//console.log( 'after GEN' , this.functionBody )
processedInput = code[0]
}else{
processedInput = code
}
}
}else{ // it input is a number
processedInput = input
}
return processedInput
},
startLocalize() {
this.localizedCode = []
this.shouldLocalize = true
},
endLocalize() {
this.shouldLocalize = false
return [ this.codeName, this.localizedCode.slice(0) ]
},
free( graph ) {
if( Array.isArray( graph ) ) { // stereo ugen
for( let channel of graph ) {
this.free( channel )
}
} else {
if( typeof graph === 'object' ) {
if( graph.memory !== undefined ) {
for( let memoryKey in graph.memory ) {
this.memory.free( graph.memory[ memoryKey ].idx )
}
}
if( Array.isArray( graph.inputs ) ) {
for( let ugen of graph.inputs ) {
this.free( ugen )
}
}
}
}
}
}
gen.__proto__ = new EE()
module.exports = gen