gibberish-dsp
Version:
Gibberish is designed to be an optimized API for audio synthesis using per-sample techniques.
326 lines (271 loc) • 10 kB
JavaScript
const g = require( 'genish.js' ),
instrument = require( './instrument.js' )
const genish = g
module.exports = function( Gibberish ) {
const proto = Object.create( instrument )
const memo = {}
Object.assign( proto, {
pickFile( sample ) {
this.currentSample = sample
},
pick( __idx ) {
const idx = Math.floor( __idx )
const keys = Object.keys( this.samplers )
const key = keys[ idx ]
this.currentSample = key
},
pickplay( __idx ) {
const idx = Math.floor( __idx )
const keys = Object.keys( this.samplers )
const key = keys[ idx ]
this.currentSample = key
return this.trigger()
},
note( rate ) {
//this.rate = rate
return this.trigger( null, rate )
},
setpan( num=0, value=.5 ) {
if( Gibberish.mode === 'processor' ) {
const voice = this.voices[ num ]
// set voice buffer length
//g.gen.memory.heap.set( [ value ], voice.pan.memory.values.idx )
voice.pan = value
}
},
setrate( num=0, value=1 ) {
if( Gibberish.mode === 'processor' ) {
const voice = this.voices[ num ]
// set voice buffer length
//g.gen.memory.heap.set( [ value ], voice.rate.memory.values.idx )
voice.rate = value
}
},
trigger( volume=null, rate=null ) {
'no jsdsp'
if( volume !== null ) this.__triggerLoudness = volume
let voice = null
if( Gibberish.mode === 'processor' ) {
const sampler = this.samplers[ this.currentSample ]
// if sample isn't loaded...
if( sampler === undefined ) return
voice = this.__getVoice__()
// set voice buffer length
g.gen.memory.heap[ voice.bufferLength.memory.values.idx ] = sampler.dataLength
// set voice data index
g.gen.memory.heap[ voice.bufferLoc.memory.values.idx ] = sampler.dataIdx
// assume voice plays forward if no rate is provided
// global rate for sampler can still be used to reverse
voice.rate = rate !== null ? rate : 1
// determine direction voice will play at by checking sign
// of voice.rate and sampler.rate. If both are the same,
// then the direction will be forward, as they are multiplied
// ... two positives or two negatives will both create a
// positive value
// assume positive value if a modulation is applied to rate
const samplerRate = typeof this.rate === 'object' ? 1 : this.rate
const dir = Math.sign( voice.rate ) === Math.sign( samplerRate ) ? 1 : 0
if( dir === 1 ) {
// trigger the bang assigned to the reset property of the
// counter object representing phase for the voice
voice.trigger()
}else{
// reset the value of the phase counter to the
// end of the sample for reverse playback
voice.phase.value = sampler.dataLength - 1
}
}
return voice
},
__getVoice__() {
return this.voices[ this.voiceCount++ % this.voices.length ]
},
})
const Sampler = inputProps => {
const syn = Object.create( proto )
const props = Object.assign( { onload:null, voiceCount:0, files:[] }, Sampler.defaults, inputProps )
syn.isStereo = props.isStereo !== undefined ? props.isStereo : false
const start = g.in( 'start' ), end = g.in( 'end' ),
rate = g.in( 'rate' ), shouldLoop = g.in( 'loops' ),
loudness = g.in( 'loudness' ),
triggerLoudness = g.in( '__triggerLoudness' ),
// rate storage is used to determine whether we're playing
// the sample forward or in reverse, for use in the 'trigger' method.
rateStorage = g.data([0], 1, { meta:true })
Object.assign( syn, props )
if( Gibberish.mode === 'worklet' ) {
syn.__meta__ = {
address:'add',
name: ['instruments', 'Multisampler'],
properties: JSON.stringify(props),
id: syn.id
}
Gibberish.worklet.ugens.set( syn.id, syn )
Gibberish.worklet.port.postMessage( syn.__meta__ )
}
const voices = []
for( let i = 0; i < syn.maxVoices; i++ ) {
'use jsdsp'
const voice = {
bufferLength: g.data( [1], 1, { meta:true }),
bufferLoc: g.data( [1], 1, { meta:true }),
bang: g.bang(),
// XXX how do I change this from main thread?
__pan: g.data( [.5], 1, { meta:true }),
__rate: g.data( [1], 1, { meta:true }),
__shouldLoop: g.data( [1], 1, { meta:true }),
__loudness: g.data( [1], 1, { meta:true }),
get loudness() {
return g.gen.memory.heap[ this.__loudness.memory.values.idx ]
},
set loudness( v ) {
g.gen.memory.heap[ this.__loudness.memory.values.idx ] = v
},
set pan(v) {
g.gen.memory.heap[ this.__pan.memory.values.idx ] = v
},
set rate(v) {
g.gen.memory.heap[ this.__rate.memory.values.idx ] = v
},
get rate() {
return g.gen.memory.heap[ this.__rate.memory.values.idx ]
},
}
voice.phase = g.counter(
rate * voice.__rate[0],
start * voice.bufferLength[0],
end * voice.bufferLength[0],
voice.bang,
shouldLoop,
{ shouldWrap:false, initialValue:9999999 }
)
voice.trigger = voice.bang.trigger
voice.graph = g.ifelse(
// if phase is greater than start and less than end...
g.and(
g.gte( voice.phase, start * voice.bufferLength[0] ),
g.lt( voice.phase, end * voice.bufferLength[0] )
),
// ...read data
voice.peek = g.peekDyn(
voice.bufferLoc[0],
voice.bufferLength[0],
voice.phase,
{ mode:'samples' }
),
// ...else return 0
0
)
* loudness
* voice.__loudness[0]
const pan = g.pan( voice.graph, voice.graph, voice.__pan[0] )
voice.graph = [ pan.left, pan.right ]
voices.push( voice )
}
// load in sample data
const samplers = {}
// bound to individual sampler objects in loadSample function
syn.loadBuffer = function( buffer, onload ) {
// main thread: when sample is loaded, copy it over message port
// processor thread: onload is called via messageport handler, and
// passed in the new buffer to be copied.
if( Gibberish.mode === 'worklet' ) {
const memIdx = Gibberish.memory.alloc( this.data.buffer.length, true )
Gibberish.worklet.port.postMessage({
address:'copy_multi',
id: syn.id,
buffer: this.data.buffer,
filename: this.filename
})
if( typeof onload === 'function' ) onload( this, buffer )
}else if( Gibberish.mode === 'processor' ) {
this.data.buffer = buffer
// set data memory spec before issuing memory request
this.dataLength = this.data.memory.values.length = this.data.dim = this.data.buffer.length
// request memory to copy the bufer over
g.gen.requestMemory( this.data.memory, false )
g.gen.memory.heap.set( this.data.buffer, this.data.memory.values.idx )
// set location of buffer (does not work)
this.dataIdx = this.data.memory.values.idx
syn.currentSample = this.filename
}
}
syn.loadSample = function( filename, __onload, buffer=null ) {
'use jsdsp'
const sampler = samplers[ filename ] = {
dataLength: null,
dataIdx: null,
buffer: null,
filename
}
const onload = syn.loadBuffer.bind( sampler )
// passing a filename to data will cause it to be loaded in the main thread
// onload will then be called to pass the buffer over the messageport. In the
// processor thread, make a placeholder until data is available.
if( Gibberish.mode === 'worklet' ) {
sampler.data = g.data( buffer !== null ? buffer : filename, 1, { onload })
// check to see if a promise is returned; a valid
// data object is only return if the file has been
// previously loaded and the corresponding buffer has
// been cached.
if( sampler.data instanceof Promise ) {
sampler.data.then( d => {
sampler.data = d
memo[ filename ] = sampler.data
onload( sampler, __onload )
})
}else{
// using a cached data buffer, no need
// for asynchronous loading.
memo[ filename ] = sampler
onload( sampler, __onload )
}
}else{
sampler.data = g.data( new Float32Array(), 1, { onload, filename })
sampler.data.onload = onload
}
}
props.files.forEach( filename => syn.loadSample( filename ) )
syn.__createGraph = function() {
'use jsdsp'
const graphs = voices.map( voice => voice.graph )
const left = g.add( ...voices.map( voice => voice.graph[0] ) )
const right = g.add( ...voices.map( voice => voice.graph[1] ) )
const gain = g.in( 'gain' )
syn.graph = [ left * gain, right * gain ]
if( syn.panVoices === true ) {
const panner = g.pan( syn.graph[0], syn.graph[1], g.in( 'pan' ) )
syn.graph = [ panner.left, panner.right ]
}
}
syn.__createGraph()
const out = Gibberish.factory(
syn,
syn.graph,
['instruments','multisampler'],
props
)
Gibberish.preventProxy = true
Gibberish.proxyEnabled = false
out.voices = voices
out.samplers = samplers
Gibberish.proxyEnabled = true
Gibberish.preventProxy = false
return out
}
Sampler.defaults = {
gain: 1,
pan: .5,
rate: 1,
panVoices:false,
shouldLoop:false,
loops: 0,
start:0,
end:1,
bufferLength:-999999999,
loudness:1,
maxVoices:5,
__triggerLoudness:1
}
return Sampler
}