gibberish-dsp
Version:
Gibberish is designed to be an optimized API for audio synthesis using per-sample techniques.
505 lines (403 loc) • 15.6 kB
JavaScript
let MemoryHelper = require( 'memory-helper' ),
genish = require( 'genish.js' )
let Gibberish = {
blockCallbacks: [], // called every block
dirtyUgens: [],
callbackUgens: [],
callbackNames: [],
analyzers: [],
graphIsDirty: false,
ugens: {},
debug: false,
id: -1,
preventProxy:false,
proxyEnabled: true,
output: null,
memory : null, // 20 minutes by default?
factory: null,
genish,
scheduler: require( './scheduling/scheduler.js' ),
//workletProcessorLoader: require( './workletProcessor.js' ),
workletProcessor: null,
memoed: {},
mode:'scriptProcessor',
prototypes: {
ugen: null,//require('./ugen.js'),
instrument: require( './instruments/instrument.js' ),
effect: require( './fx/effect.js' ),
analyzer: require( './analysis/analyzer.js' )
},
mixins: {
polyinstrument: require( './instruments/polyMixin.js' )
},
workletPath: './gibberish_worklet.js',
init( memAmount, ctx, mode='worklet' ) {
let numBytes = isNaN( memAmount ) ? 20 * 60 * 44100 : memAmount
// regardless of whether or not gibberish is using worklets,
// we still want genish to output vanilla js functions instead
// of audio worklet classes; these functions will be called
// from within the gibberish audioworklet processor node.
this.genish.gen.mode = 'scriptProcessor'
this.memory = MemoryHelper.create( numBytes, Float64Array )
this.mode = mode
const startup = this.utilities.createWorklet
this.scheduler.init( this )
this.analyzers.dirty = false
if( this.mode === 'worklet' ) {
const p = new Promise( (resolve, reject ) => {
const pp = new Promise( (__resolve, __reject ) => {
this.utilities.createContext( ctx, startup.bind( this.utilities ), __resolve )
}).then( ()=> {
Gibberish.preventProxy = true
Gibberish.load()
Gibberish.preventProxy = false
Gibberish.output = this.Bus2()
// Gibberish.output needs to be assign so that ugens can
// connect to it by default. There's no other way to assign it
// outside of evaling code at this point.
Gibberish.worklet.port.postMessage({
address:'eval',
code:`Gibberish.output = this.ugens.get(${Gibberish.output.id});`
})
resolve()
})
})
return p
}else if( this.mode === 'processor' ) {
Gibberish.load()
}
},
load() {
this.factory = require( './factory.js' )( this )
this.Panner = require( './misc/panner.js' )( this )
this.PolyTemplate = require( './instruments/polytemplate.js' )( this )
this.oscillators = require( './oscillators/oscillators.js' )( this )
this.filters = require( './filters/filters.js' )( this )
this.binops = require( './misc/binops.js' )( this )
this.monops = require( './misc/monops.js' )( this )
this.Bus = require( './misc/bus.js' )( this )
this.Bus2 = require( './misc/bus2.js' )( this )
this.instruments = require( './instruments/instruments.js' )( this )
this.fx = require( './fx/effects.js' )( this )
this.Sequencer = require( './scheduling/sequencer.js' )( this )
this.Sequencer2 = require( './scheduling/seq2.js' )( this )
this.Tidal = require( './scheduling/tidal.js' )( this )
this.envelopes = require( './envelopes/envelopes.js' )( this )
this.analysis = require( './analysis/analyzers.js' )( this )
this.time = require( './misc/time.js' )( this )
this.Proxy = require( './workletProxy.js' )( this )
},
export( target, shouldExportGenish=false ) {
if( target === undefined ) throw Error('You must define a target object for Gibberish to export variables to.')
if( shouldExportGenish ) this.genish.export( target )
this.instruments.export( target )
this.fx.export( target )
this.filters.export( target )
this.oscillators.export( target )
this.binops.export( target )
this.monops.export( target )
this.envelopes.export( target )
this.analysis.export( target )
target.Sequencer = this.Sequencer
target.Sequencer2 = this.Sequencer2
target.Bus = this.Bus
target.Bus2 = this.Bus2
target.Scheduler = this.scheduler
target.Tidal = this.Tidal
this.time.export( target )
this.utilities.export( target )
},
printcb() {
Gibberish.worklet.port.postMessage({ address:'callback' })
},
printobj( obj ) {
Gibberish.worklet.port.postMessage({ address:'print', object:obj.id })
},
send( msg ){
Gibberish.worklet.port.postMessage( msg )
},
dirty( ugen ) {
if( ugen === this.analyzers ) {
this.graphIsDirty = true
this.analyzers.dirty = true
} else {
this.dirtyUgens.push( ugen )
this.graphIsDirty = true
if( this.memoed[ ugen.ugenName ] ) {
delete this.memoed[ ugen.ugenName ]
}
}
},
clear() {
// do not delete the gain and the pan of the master bus
this.output.inputs.splice( 0, this.output.inputs.length - 2 )
//this.output.inputNames.length = 0
this.analyzers.length = 0
this.scheduler.clear()
this.dirty( this.output )
if( this.mode === 'worklet' ) {
this.worklet.port.postMessage({
address:'method',
object:this.id,
name:'clear',
args:[]
})
}
// clear memory... XXX should this be a MemoryHelper function?
//this.memory.heap.fill(0)
//this.memory.list = {}
Gibberish.genish.gen.removeAllListeners('memory init')
Gibberish.genish.gen.histories.clear()
//Gibberish.output = this.Bus2()
},
// used to sort analysis ugens by priority.
// higher priorities mean lower ordering in the array,
// which means they will run first in the callback function.
// by defult, analysis ugens are assigned a priority of 0 in the
// analysis prototype.
analysisCompare( a,b ) {
return (isNaN(b.priority) ? 0 : b.priority) - (isNaN(a.priority) ? 0: a.priority )
},
generateCallback() {
if( this.mode === 'worklet' ) {
Gibberish.callback = function() { return 0 }
Gibberish.callback.out = []
return Gibberish.callback
}
let uid = 0,
callbackBody, lastLine, analysis=''
this.memoed = {}
callbackBody = this.processGraph( this.output )
lastLine = callbackBody[ callbackBody.length - 1]
callbackBody.unshift( "\t'use strict'" )
this.analyzers
.sort( this.analysisCompare )
.forEach( v=> {
const analysisBlock = Gibberish.processUgen( v )
//if( Gibberish.mode === 'processor' ) {
// console.log( 'analysis:', analysisBlock, v )
//}
let analysisLine
if( typeof analysisBlock === 'object' ) {
analysisLine = analysisBlock.pop()
analysisBlock.forEach( v => {
callbackBody.splice( callbackBody.length - 1, 0, v )
})
}else{
analysisLine = analysisBlock
}
callbackBody.push( analysisLine )
})
this.analyzers.forEach( v => {
if( this.callbackUgens.indexOf( v.callback ) === -1 )
this.callbackUgens.push( v.callback )
})
this.callbackNames = this.callbackUgens.map( v => v.ugenName )
callbackBody.push( '\n\treturn ' + lastLine.split( '=' )[0].split( ' ' )[1] )
if( this.debug === true ) console.log( 'callback:\n', callbackBody.join('\n') )
this.callbackNames.push( 'mem' )
this.callbackUgens.push( this.memory.heap )
this.callback = Function( ...this.callbackNames, callbackBody.join( '\n' ) )//.bind( null, ...this.callbackUgens )
this.callback.out = []
if( this.oncallback ) this.oncallback( this.callback )
return this.callback
},
processGraph( output ) {
this.callbackUgens.length = 0
this.callbackNames.length = 0
this.callbackUgens.push( output.callback )
let body = this.processUgen( output )
this.dirtyUgens.length = 0
this.graphIsDirty = false
return body
},
proxyReplace( obj ) {
if( typeof obj === 'object' && obj !== null ) {
if( obj.id !== undefined ) {
const __obj = Gibberish.processor.ugens.get( obj.id )
//console.log( 'retrieved:', __obj.name )
//if( obj.prop !== undefined ) console.log( 'got a ssd.out', obj )
return obj.prop !== undefined ? __obj[ obj.prop ] : __obj
}else if( obj.isFunc === true ) {
let func = eval( '(' + obj.value + ')' )
//console.log( 'replacing function:', func )
return func
}
}
return obj
},
processUgen( ugen, block ) {
if( block === undefined ) block = []
if( ugen === undefined ) return block
let dirtyIdx = Gibberish.dirtyUgens.indexOf( ugen )
let memo = Gibberish.memoed[ ugen.ugenName ]
if( memo !== undefined ) {
return memo
} else if( ugen === true || ugen === false ) {
throw "Why is ugen a boolean? [true] or [false]";
} else if( ugen.block === undefined || dirtyIndex !== -1 ) {
// weird edge case with analysis (follow) ugen
if( ugen.id === undefined ) {
ugen.id = ugen.__properties__.overrideid
}
let line = `\tconst v_${ugen.id} = `
if( !ugen.isop ) line += `${ugen.ugenName}( `
// must get array so we can keep track of length for comma insertion
const keys = ugen.isop === true || ugen.type === 'bus'
? Object.keys( ugen.inputs )
: [...ugen.inputNames ]
line = ugen.isop === true
? Gibberish.__processBinop( ugen, line, block, keys )
: Gibberish.__processNonBinop( ugen, line, block, keys )
line = Gibberish.__addLineEnding( line, ugen, keys )
block.push( line )
Gibberish.memoed[ ugen.ugenName ] = `v_${ugen.id}`
if( dirtyIdx !== -1 ) {
Gibberish.dirtyUgens.splice( dirtyIdx, 1 )
}
}else if( ugen.block ) {
return ugen.block
}
return block
},
__processBinop( ugen, line, block, keys ) {
//__getInputString( line, input, block, key, ugen ) {
const isLeftStereo = Gibberish.__isStereo( ugen.inputs[0] ),
isRightStereo = Gibberish.__isStereo( ugen.inputs[1] ),
left = Gibberish.__getInputString( line, ugen.inputs[0], block, '0', keys ),
right= Gibberish.__getInputString( line, ugen.inputs[1], block, '1', keys ),
op = ugen.op
let graph, out
if( isLeftStereo === true && isRightStereo === false ) {
line += `[ ${left}[0] ${op} ${right}, ${left}[1] ${op} ${right} ]`
//graph = [ g.add( args[0].graph[0], args[1] ), g.add( args[0].graph[1], args[1] )]
}else if( isLeftStereo === false && isRightStereo === true ) {
//graph = [ g.add( args[0], args[1].graph[0] ), g.add( args[0], args[1].graph[1] )]
line += `[ ${left} ${op} ${right}[0], ${left} ${op} ${right}[1] ]`
}else if( isLeftStereo === true && isRightStereo === true ) {
//graph = [ g.add( args[0].graph[0], args[1].graph[0] ), g.add( args[0].graph[1], args[1].graph[1] )]
line += `[ ${left}[0] ${op} ${right}[0], ${left}[1] ${op} ${right}[1] ]`
}else{
// XXX important, must re-assign when calling processNonBinop
line = Gibberish.__processNonBinop( ugen, line, block, keys )
}
return line
},
__processNonBinop( ugen, line, block, keys ) {
for( let i = 0; i < keys.length; i++ ) {
let key = keys[ i ]
// binop.inputs is actual values, not just property names
let input
if( ugen.isop || ugen.type ==='bus' ) {
input = ugen.inputs[ key ]
}else{
input = ugen[ key ]
}
if( input !== undefined ) {
input = Gibberish.__getBypassedInput( input )
line += Gibberish.__getInputString( line, input, block, key, ugen )
line = Gibberish.__addSeparator( line, input, ugen, i < keys.length - 1 )
}
}
return line
},
// determine if a ugen is stereo
__isStereo( ugen ) {
let isStereo = false
if( ugen === undefined || ugen === null ) return false
if( ugen.isStereo === true ) return true
if( ugen.isop === true ) {
return Gibberish.__isStereo( ugen.inputs[0] ) || Gibberish.__isStereo( ugen.inputs[1] )
}
return isStereo
},
// if an effect is bypassed, get next one in chain (or output destination)
__getBypassedInput( input ) {
if( input.bypass === true ) {
// loop through inputs of chain until one is found
// that is not being bypassed
let found = false
while( input.input !== 'undefined' && found === false ) {
if( typeof input.input.bypass !== 'undefined' ) {
input = input.input
if( input.bypass === false ) found = true
}else{
input = input.input
found = true
}
}
}
return input
},
// get a string representing a ugen for insertion into callback.
// if a ugen contains other ugens, trigger codegen for those ugens as well.
__getInputString( line, input, block, key, ugen ) {
let value = ''
if( typeof input === 'number' ) {
if( isNaN(key) ) {
value += `mem[${ugen.__addresses__[ key ]}]`//input
}else{
value += input
}
} else if( typeof input === 'boolean' ) {
value += '' + input
}else{
//console.log( 'key:', key, 'input:', ugen.inputs, ugen.inputs[ key ] )
// XXX not sure why this has to be here, but somehow non-processed objects
// that only contain id numbers are being passed here...
if( input !== undefined ) {
if( Gibberish.mode === 'processor' ) {
if( input.ugenName === undefined && input.id !== undefined ) {
if( ugen === undefined ) {
input = Gibberish.processor.ugens.get( input.id )
}else{
if( ugen.type !== 'seq' ) {
input = Gibberish.processor.ugens.get( input.id )
}
}
}
}
Gibberish.processUgen( input, block )
if( !input.isop ) {
// check is needed so that graphs with ssds that refer to themselves
// don't add the ssd in more than once
if( Gibberish.callbackUgens.indexOf( input.callback ) === -1 ) {
Gibberish.callbackUgens.push( input.callback )
}
}
value += `v_${input.id}`
input.__varname = value
}
}
return value
},
// add separators for function calls and handle binops (mono only)
__addSeparator( line, input, ugen, isNotEndOfLine ) {
if( isNotEndOfLine === true ) {
if( ugen.isop === true ) {
if( ugen.op === '*' || ugen.op === '/' ) {
if( input !== 1 ) {
line += ' ' + ugen.op + ' '
}else{
line = line.slice( 0, -1 * (''+input).length )
}
}else{
line += ' ' + ugen.op + ' '
}
}else{
line += ', '
}
}
return line
},
// add memory to end of function calls and close parenthesis
__addLineEnding( line, ugen, keys ) {
if( (ugen.type === 'bus' && keys.length > 0) ) line += ', '
if( !ugen.isop && ugen.type !== 'seq' ) line += 'mem'
line += ugen.isop ? '' : ' )'
return line
},
}
Gibberish.prototypes.Ugen = Gibberish.prototypes.ugen = require( './ugen.js' )( Gibberish )
Gibberish.utilities = require( './utilities.js' )( Gibberish )
module.exports = Gibberish