genish.js
Version:
384 lines (295 loc) • 11.2 kB
JavaScript
const AWPF = require( './external/audioworklet-polyfill.js' ),
gen = require( './gen.js' ),
data = require( './data.js' )
let isStereo = false
const utilities = {
ctx: null,
buffers: {},
isStereo:false,
clear() {
if( this.workletNode !== undefined ) {
this.workletNode.disconnect()
}else{
this.callback = () => 0
}
this.clear.callbacks.forEach( v => v() )
this.clear.callbacks.length = 0
this.isStereo = false
if( gen.graph !== null ) gen.free( gen.graph )
},
createContext( bufferSize = 2048 ) {
const AC = typeof AudioContext === 'undefined' ? webkitAudioContext : AudioContext
// tell polyfill global object and buffersize
AWPF( window, bufferSize )
const start = () => {
if( typeof AC !== 'undefined' ) {
this.ctx = new AC({ latencyHint:.0125 })
gen.samplerate = this.ctx.sampleRate
if( document && document.documentElement && 'ontouchstart' in document.documentElement ) {
window.removeEventListener( 'touchstart', start )
}else{
window.removeEventListener( 'mousedown', start )
window.removeEventListener( 'keydown', start )
}
const mySource = utilities.ctx.createBufferSource()
mySource.connect( utilities.ctx.destination )
mySource.start()
}
}
if( document && document.documentElement && 'ontouchstart' in document.documentElement ) {
window.addEventListener( 'touchstart', start )
}else{
window.addEventListener( 'mousedown', start )
window.addEventListener( 'keydown', start )
}
return this
},
createScriptProcessor() {
this.node = this.ctx.createScriptProcessor( 1024, 0, 2 )
this.clearFunction = function() { return 0 }
if( typeof this.callback === 'undefined' ) this.callback = this.clearFunction
this.node.onaudioprocess = function( audioProcessingEvent ) {
var outputBuffer = audioProcessingEvent.outputBuffer;
var left = outputBuffer.getChannelData( 0 ),
right= outputBuffer.getChannelData( 1 ),
isStereo = utilities.isStereo
for( var sample = 0; sample < left.length; sample++ ) {
var out = utilities.callback()
if( isStereo === false ) {
left[ sample ] = right[ sample ] = out
}else{
left[ sample ] = out[0]
right[ sample ] = out[1]
}
}
}
this.node.connect( this.ctx.destination )
return this
},
// remove starting stuff and add tabs
prettyPrintCallback( cb ) {
// get rid of "function gen" and start with parenthesis
// const shortendCB = cb.toString().slice(9)
const cbSplit = cb.toString().split('\n')
const cbTrim = cbSplit.slice( 3, -2 )
const cbTabbed = cbTrim.map( v => ' ' + v )
return cbTabbed.join('\n')
},
createParameterDescriptors( cb ) {
// [{name: 'amplitude', defaultValue: 0.25, minValue: 0, maxValue: 1}];
let paramStr = ''
//for( let ugen of cb.params.values() ) {
// paramStr += `{ name:'${ugen.name}', defaultValue:${ugen.value}, minValue:${ugen.min}, maxValue:${ugen.max} },\n `
//}
for( let ugen of cb.params.values() ) {
paramStr += `{ name:'${ugen.name}', automationRate:'k-rate', defaultValue:${ugen.defaultValue}, minValue:${ugen.min}, maxValue:${ugen.max} },\n `
}
return paramStr
},
createParameterDereferences( cb ) {
let str = cb.params.size > 0 ? '\n ' : ''
for( let ugen of cb.params.values() ) {
str += `const ${ugen.name} = parameters.${ugen.name}[0]\n `
}
return str
},
createParameterArguments( cb ) {
let paramList = ''
for( let ugen of cb.params.values() ) {
paramList += ugen.name + '[i],'
}
paramList = paramList.slice( 0, -1 )
return paramList
},
createInputDereferences( cb ) {
let str = cb.inputs.size > 0 ? '\n' : ''
for( let input of cb.inputs.values() ) {
str += `const ${input.name} = inputs[ ${input.inputNumber} ][ ${input.channelNumber} ]\n `
}
return str
},
createInputArguments( cb ) {
let paramList = ''
for( let input of cb.inputs.values() ) {
paramList += input.name + '[i],'
}
paramList = paramList.slice( 0, -1 )
return paramList
},
createFunctionDereferences( cb ) {
let memberString = cb.members.size > 0 ? '\n' : ''
let memo = {}
for( let dict of cb.members.values() ) {
const name = Object.keys( dict )[0],
value = dict[ name ]
if( memo[ name ] !== undefined ) continue
memo[ name ] = true
memberString += ` const ${name} = ${value}\n`
}
return memberString
},
createWorkletProcessor( graph, name, debug, mem=44100*10 ) {
//const mem = MemoryHelper.create( 4096, Float64Array )
const cb = gen.createCallback( graph, mem, debug )
const inputs = cb.inputs
// get all inputs and create appropriate audioparam initializers
const parameterDescriptors = this.createParameterDescriptors( cb )
const parameterDereferences = this.createParameterDereferences( cb )
const paramList = this.createParameterArguments( cb )
const inputDereferences = this.createInputDereferences( cb )
const inputList = this.createInputArguments( cb )
const memberString = this.createFunctionDereferences( cb )
// change output based on number of channels.
const genishOutputLine = cb.isStereo === false
? `left[ i ] = memory[0]`
: `left[ i ] = memory[0];\n\t\tright[ i ] = memory[1]\n`
const prettyCallback = this.prettyPrintCallback( cb )
/***** begin callback code ****/
// note that we have to check to see that memory has been passed
// to the worker before running the callback function, otherwise
// it can be passed too slowly and fail on occassion
const workletCode = `
class ${name}Processor extends AudioWorkletProcessor {
static get parameterDescriptors() {
const params = [
${ parameterDescriptors }
]
return params
}
constructor( options ) {
super( options )
this.port.onmessage = this.handleMessage.bind( this )
this.initialized = false
}
handleMessage( event ) {
if( event.data.key === 'init' ) {
this.memory = event.data.memory
this.initialized = true
}else if( event.data.key === 'set' ) {
this.memory[ event.data.idx ] = event.data.value
}else if( event.data.key === 'get' ) {
this.port.postMessage({ key:'return', idx:event.data.idx, value:this.memory[event.data.idx] })
}
}
process( inputs, outputs, parameters ) {
if( this.initialized === true ) {
const output = outputs[0]
const left = output[ 0 ]
const right = output[ 1 ]
const len = left.length
const memory = this.memory ${parameterDereferences}${inputDereferences}${memberString}
for( let i = 0; i < len; ++i ) {
${prettyCallback}
${genishOutputLine}
}
}
return true
}
}
registerProcessor( '${name}', ${name}Processor)`
/***** end callback code *****/
if( debug === true ) console.log( workletCode )
const url = window.URL.createObjectURL(
new Blob(
[ workletCode ],
{ type: 'text/javascript' }
)
)
return [ url, workletCode, inputs, cb.params, cb.isStereo ]
},
registeredForNodeAssignment: [],
register( ugen ) {
if( this.registeredForNodeAssignment.indexOf( ugen ) === -1 ) {
this.registeredForNodeAssignment.push( ugen )
}
},
playWorklet( graph, name, debug=false, mem=44100 * 60 ) {
utilities.clear()
const [ url, codeString, inputs, params, isStereo ] = utilities.createWorkletProcessor( graph, name, debug, mem )
const nodePromise = new Promise( (resolve,reject) => {
utilities.ctx.audioWorklet.addModule( url ).then( ()=> {
const workletNode = new AudioWorkletNode( utilities.ctx, name, { outputChannelCount:[ isStereo ? 2 : 1 ] })
workletNode.callbacks = {}
workletNode.onmessage = function( event ) {
if( event.data.message === 'return' ) {
workletNode.callbacks[ event.data.idx ]( event.data.value )
delete workletNode.callbacks[ event.data.idx ]
}
}
workletNode.getMemoryValue = function( idx, cb ) {
this.workletCallbacks[ idx ] = cb
this.workletNode.port.postMessage({ key:'get', idx: idx })
}
workletNode.port.postMessage({ key:'init', memory:gen.memory.heap })
utilities.workletNode = workletNode
utilities.registeredForNodeAssignment.forEach( ugen => ugen.node = workletNode )
utilities.registeredForNodeAssignment.length = 0
// assign all params as properties of node for easier reference
for( let dict of inputs.values() ) {
const name = Object.keys( dict )[0]
const param = workletNode.parameters.get( name )
Object.defineProperty( workletNode, name, {
set( v ) {
param.value = v
},
get() {
return param.value
}
})
}
for( let ugen of params.values() ) {
const name = ugen.name
const param = workletNode.parameters.get( name )
ugen.waapi = param
// initialize?
param.value = ugen.defaultValue
Object.defineProperty( workletNode, name, {
set( v ) {
param.value = v
},
get() {
return param.value
}
})
}
if( utilities.console ) utilities.console.setValue( codeString )
workletNode.connect( utilities.ctx.destination )
resolve( workletNode )
})
})
return nodePromise
},
playGraph( graph, debug, mem=44100*10, memType=Float32Array ) {
utilities.clear()
if( debug === undefined ) debug = false
this.isStereo = Array.isArray( graph )
utilities.callback = gen.createCallback( graph, mem, debug, false, memType )
if( utilities.console ) utilities.console.setValue( utilities.callback.toString() )
return utilities.callback
},
loadSample( soundFilePath, data ) {
const isLoaded = utilities.buffers[ soundFilePath ] !== undefined
let req = new XMLHttpRequest()
req.open( 'GET', soundFilePath, true )
req.responseType = 'arraybuffer'
let promise = new Promise( (resolve,reject) => {
if( !isLoaded ) {
req.onload = function() {
var audioData = req.response
utilities.ctx.decodeAudioData( audioData, (buffer) => {
data.buffer = buffer.getChannelData(0)
utilities.buffers[ soundFilePath ] = data.buffer
resolve( data.buffer )
})
}
}else{
setTimeout( ()=> resolve( utilities.buffers[ soundFilePath ] ), 0 )
}
})
if( !isLoaded ) req.send()
return promise
}
}
utilities.clear.callbacks = []
module.exports = utilities