gibberish-dsp
Version:
Gibberish is designed to be an optimized API for audio synthesis using per-sample techniques.
224 lines (187 loc) • 6.75 kB
JavaScript
const g = require( 'genish.js' ),
instrument = require( './instrument.js' )
module.exports = function( Gibberish ) {
const proto = Object.create( instrument )
const memo = {}
Object.assign( proto, {
note( rate ) {
this.rate = rate
if( rate > 0 ) {
this.__trigger()
}else{
this.__phase__.value = this.end * (this.data.buffer.length - 1)
}
},
trigger( volume ) {
if( volume !== undefined ) this.gain = volume
if( Gibberish.mode === 'processor' ) {
// if we're playing the sample forwards...
if( Gibberish.memory.heap[ this.__rateStorage__.memory.values.idx ] > 0 ) {
this.__trigger()
}else{
this.__phase__.value = this.end * (this.data.buffer.length - 1)
}
}
},
})
const Sampler = inputProps => {
const syn = Object.create( proto )
const props = Object.assign( { onload:null }, Sampler.defaults, inputProps )
syn.isStereo = props.isStereo !== undefined ? props.isStereo : false
const start = g.in( 'start' ), end = g.in( 'end' ),
bufferLength = g.in( 'bufferLength' ),
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', 'Sampler'],
properties: JSON.stringify(props),
id: syn.id
}
Gibberish.worklet.ugens.set( syn.id, syn )
Gibberish.worklet.port.postMessage( syn.__meta__ )
}
syn.__createGraph = function() {
syn.__bang__ = g.bang()
syn.__trigger = syn.__bang__.trigger
syn.__phase__ = g.counter(
rate,
g.mul(start,bufferLength),
g.mul( end, bufferLength ),
syn.__bang__,
shouldLoop,
{ shouldWrap:false, initialValue:9999999 }
)
syn.__rateStorage__ = rateStorage
rateStorage[0] = rate
// XXX we added our recorded 'rate' param and then effectively subtract it,
// so that its presence in the graph will force genish to actually record the
// rate as the input. this is extremely hacky... there should be a way to record
// value without having to include it in the graph!
syn.graph = g.add( g.mul(
g.ifelse(
g.and( g.gte( syn.__phase__, g.mul(start,bufferLength) ), g.lt( syn.__phase__, g.mul(end,bufferLength) ) ),
g.peek(
syn.data,
syn.__phase__,
{ mode:'samples' }
),
0
),
g.mul( g.mul( loudness, triggerLoudness ), g.in('gain') )
), rateStorage[0], g.mul( rateStorage[0], -1 ) )
if( syn.panVoices === true ) {
const panner = g.pan( syn.graph, syn.graph, g.in( 'pan' ) )
syn.graph = [ panner.left, panner.right ]
}
}
const onload = (buffer,filename) => {
if( buffer === undefined ) return
if( Gibberish.mode === 'worklet' ) {
//const memIdx = memo[ filename ].idx !== undefined ? memo[ filename ].idx : Gibberish.memory.alloc( syn.data.memory.values.length, true )
const memIdx = Gibberish.memory.alloc( buffer.length, true )
//memo[ filename ].idx = memIdx
Gibberish.worklet.port.postMessage({
address:'copy',
id: syn.id,
idx: memIdx,
buffer
})
}else if ( Gibberish.mode === 'processor' ) {
syn.data.buffer = buffer
syn.data.memory.values.length = syn.data.dim = buffer.length
syn.__redoGraph()
}
if( typeof syn.onload === 'function' ){
syn.onload( buffer || syn.data.buffer )
}
if( syn.bufferLength === -999999999 && syn.data.buffer !== undefined ) syn.bufferLength = syn.data.buffer.length - 1
}
//if( props.filename ) {
syn.loadFile = function( filename ) {
//if( memo[ filename ] === undefined ) {
if( Gibberish.mode !== 'processor' ) {
syn.data = g.data( 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( syn.data instanceof Promise ) {
syn.data.then( d => {
syn.data = d
memo[ filename ] = syn.data
onload( d.buffer, filename )
})
}else{
// using a cached data buffer, no need
// for asynchronous loading.
memo[ filename ] = syn.data
onload( syn.data.buffer, filename )
}
}else{
syn.data = g.data( new Float32Array(), 1, { onload, filename })
//memo[ filename ] = syn.data
}
//}else{
// syn.data = memo[ filename ]
// console.log( 'memo data:', syn.data )
// onload( syn.data.buffer, filename )
//}
}
syn.loadBuffer = function( buffer ) {
if( Gibberish.mode === 'processor' ) {
syn.data.buffer = buffer
syn.data.memory.values.length = syn.data.dim = buffer.length
syn.__redoGraph()
}
}
if( props.filename !== undefined ) {
syn.loadFile( props.filename )
}else{
syn.data = g.data( new Float32Array() )
}
if( syn.data !== undefined ) {
syn.data.onload = onload
syn.__createGraph()
}
const out = Gibberish.factory(
syn,
syn.graph,
['instruments','sampler'],
props
)
return out
}
Sampler.defaults = {
gain: 1,
pan: .5,
rate: 1,
panVoices:false,
loops: 0,
start:0,
end:1,
bufferLength:-999999999,
loudness:1,
__triggerLoudness:1
}
const envCheckFactory = function( voice, _poly ) {
const envCheck = () => {
const phase = Gibberish.memory.heap[ voice.__phase__.memory.value.idx ]
if( ( voice.rate > 0 && phase > voice.end ) || ( voice.rate < 0 && phase < 0 ) ) {
_poly.disconnectUgen.call( _poly, voice )
voice.isConnected = false
}else{
Gibberish.blockCallbacks.push( envCheck )
}
}
return envCheck
}
const PolySampler = Gibberish.PolyTemplate( Sampler, ['rate','pan','gain','start','end','loops','bufferLength','__triggerLoudness','loudness'], envCheckFactory )
return [ Sampler, PolySampler ]
}