UNPKG

genish.js

Version:
384 lines (295 loc) 11.2 kB
'use strict' 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