import AudioTimeEngine from '../core/AudioTimeEngine';
function optOrDef(opt, def) {
if (opt !== undefined)
return opt;
return def;
}
/**
* Granular synthesis TimeEngine implementing the scheduled interface.
* The grain position (grain onset or center time in the audio buffer) is
* optionally determined by the engine's currentPosition attribute.
*
* Example that shows a `GranularEngine` (with a few parameter controls) driven
* by a `Scheduler` and a `PlayControl`:
* {@link https://rawgit.com/wavesjs/waves-audio/master/examples/granular-engine/index.html}
*
* @extends AudioTimeEngine
* @example
* import * as audio from 'waves-audio';
* const scheduler = audio.getScheduler();
* const granularEngine = new audio.GranularEngine();
*
* scheduler.add(granularEngine);
*
*
* @param {Object} options={} - Parameters
* @param {AudioBuffer} [options.buffer=null] - Audio buffer
* @param {Number} [options.periodAbs=0.01] - Absolute grain period in sec
* @param {Number} [options.periodRel=0] - Grain period relative to absolute
* duration
* @param {Number} [options.periodVar=0] - Amout of random grain period
* variation relative to grain period
* @param {Number} [options.periodMin=0.001] - Minimum grain period
* @param {Number} [options.position=0] - Grain position (onset time in audio
* buffer) in sec
* @param {Number} [options.positionVar=0.003] - Amout of random grain position
* variation in sec
* @param {Number} [options.durationAbs=0.1] - Absolute grain duration in sec
* @param {Number} [options.durationRel=0] - Grain duration relative to grain
* period (overlap)
* @param {Number} [options.attackAbs=0] - Absolute attack time in sec
* @param {Number} [options.attackRel=0.5] - Attack time relative to grain duration
* @param {String} [options.attackShape='lin'] - Shape of attack
* @param {Number} [options.releaseAbs=0] - Absolute release time in sec
* @param {Number} [options.releaseRel=0.5] - Release time relative to grain duration
* @param {Number} [options.releaseShape='lin'] - Shape of release
* @param {String} [options.expRampOffset=0.0001] - Offset (start/end value)
* for exponential attack/release
* @param {Number} [options.resampling=0] - Grain resampling in cent
* @param {Number} [options.resamplingVar=0] - Amout of random resampling variation in cent
* @param {Number} [options.gain=1] - Linear gain factor
* @param {Boolean} [options.centered=true] - Whether the grain position refers
* to the center of the grain (or the beginning)
* @param {Boolean} [options.cyclic=false] - Whether the audio buffer and grain
* position are considered as cyclic
* @param {Number} [options.wrapAroundExtension=0] - Portion at the end of the
* audio buffer that has been copied from the beginning to assure cyclic behavior
*/
class GranularEngine extends AudioTimeEngine {
constructor(options = {}) {
super(options.audioContext);
/**
* Audio buffer
*
* @type {AudioBuffer}
* @name buffer
* @default null
* @memberof GranularEngine
* @instance
*/
this.buffer = optOrDef(options.buffer, null);
/**
* Absolute grain period in sec
*
* @type {Number}
* @name periodAbs
* @default 0.01
* @memberof GranularEngine
* @instance
*/
this.periodAbs = optOrDef(options.periodAbs, 0.01);
/**
* Grain period relative to absolute duration
*
* @type {Number}
* @name periodRel
* @default 0
* @memberof GranularEngine
* @instance
*/
this.periodRel = optOrDef(options.periodRel, 0);
/**
* Amout of random grain period variation relative to grain period
*
* @type {Number}
* @name periodVar
* @default 0
* @memberof GranularEngine
* @instance
*/
this.periodVar = optOrDef(options.periodVar, 0);
/**
* Minimum grain period
*
* @type {Number}
* @name periodMin
* @default 0.001
* @memberof GranularEngine
* @instance
*/
this.periodMin = optOrDef(options.periodMin, 0.001);
/**
* Grain position (onset time in audio buffer) in sec
*
* @type {Number}
* @name position
* @default 0
* @memberof GranularEngine
* @instance
*/
this.position = optOrDef(options.position, 0);
/**
* Amout of random grain position variation in sec
*
* @type {Number}
* @name positionVar
* @default 0.003
* @memberof GranularEngine
* @instance
*/
this.positionVar = optOrDef(options.positionVar, 0.003);
/**
* Absolute grain duration in sec
*
* @type {Number}
* @name durationAbs
* @default 0.1
* @memberof GranularEngine
* @instance
*/
this.durationAbs = optOrDef(options.durationAbs, 0.1); // absolute grain duration
/**
* Grain duration relative to grain period (overlap)
*
* @type {Number}
* @name durationRel
* @default 0
* @memberof GranularEngine
* @instance
*/
this.durationRel = optOrDef(options.durationRel, 0);
/**
* Absolute attack time in sec
*
* @type {Number}
* @name attackAbs
* @default 0
* @memberof GranularEngine
* @instance
*/
this.attackAbs = optOrDef(options.attackAbs, 0);
/**
* Attack time relative to grain duration
*
* @type {Number}
* @name attackRel
* @default 0.5
* @memberof GranularEngine
* @instance
*/
this.attackRel = optOrDef(options.attackRel, 0.5);
/**
* Shape of attack ('lin' for linear ramp, 'exp' for exponential ramp)
*
* @type {String}
* @name attackShape
* @default 'lin'
* @memberof GranularEngine
* @instance
*/
this.attackShape = optOrDef(options.attackShape, 'lin');
/**
* Absolute release time in sec
*
* @type {Number}
* @name releaseAbs
* @default 0
* @memberof GranularEngine
* @instance
*/
this.releaseAbs = optOrDef(options.releaseAbs, 0);
/**
* Release time relative to grain duration
*
* @type {Number}
* @name releaseRel
* @default 0.5
* @memberof GranularEngine
* @instance
*/
this.releaseRel = optOrDef(options.releaseRel, 0.5);
/**
* Shape of release ('lin' for linear ramp, 'exp' for exponential ramp)
*
* @type {String}
* @name releaseShape
* @default 'lin'
* @memberof GranularEngine
* @instance
*/
this.releaseShape = optOrDef(options.releaseShape, 'lin');
/**
* Offset (start/end value) for exponential attack/release
*
* @type {Number}
* @name expRampOffset
* @default 0.0001
* @memberof GranularEngine
* @instance
*/
this.expRampOffset = optOrDef(options.expRampOffset, 0.0001);
/**
* Grain resampling in cent
*
* @type {Number}
* @name resampling
* @default 0
* @memberof GranularEngine
* @instance
*/
this.resampling = optOrDef(options.resampling, 0);
/**
* Amout of random resampling variation in cent
*
* @type {Number}
* @name resamplingVar
* @default 0
* @memberof GranularEngine
* @instance
*/
this.resamplingVar = optOrDef(options.resamplingVar, 0);
/**
* Linear gain factor
*
* @type {Number}
* @name gain
* @default 1
* @memberof GranularEngine
* @instance
*/
this.gain = optOrDef(options.gain, 1);
/**
* Whether the grain position refers to the center of the grain (or the beginning)
*
* @type {Boolean}
* @name centered
* @default true
* @memberof GranularEngine
* @instance
*/
this.centered = optOrDef(options.centered, true);
/**
* Whether the audio buffer and grain position are considered as cyclic
*
* @type {Boolean}
* @name cyclic
* @default false
* @memberof GranularEngine
* @instance
*/
this.cyclic = optOrDef(options.cyclic, false);
/**
* Portion at the end of the audio buffer that has been copied from the
* beginning to assure cyclic behavior
*
* @type {Number}
* @name wrapAroundExtension
* @default 0
* @memberof GranularEngine
* @instance
*/
this.wrapAroundExtension = optOrDef(options.wrapAroundExtension, 0);
this.outputNode = this.audioContext.createGain();
}
/**
* Get buffer duration (excluding wrapAroundExtension)
*
* @type {Number}
* @name bufferDuration
* @memberof GranularEngine
* @instance
* @readonly
*/
get bufferDuration() {
if (this.buffer) {
var bufferDuration = this.buffer.duration;
if (this.wrapAroundExtension)
bufferDuration -= this.wrapAroundExtension;
return bufferDuration;
}
return 0;
}
/**
* Current position
*
* @type {Number}
* @name currentPosition
* @memberof GranularEngine
* @instance
* @readonly
*/
get currentPosition() {
var master = this.master;
if (master && master.currentPosition !== undefined)
return master.currentPosition;
return this.position;
}
advanceTime(time) {
time = Math.max(time, this.audioContext.currentTime);
return time + this.trigger(time);
}
/**
* Trigger a grain. This function can be called at any time (whether the
* engine is scheduled or not) to generate a single grain according to the
* current grain parameters.
*
* @param {Number} time - grain synthesis audio time
* @return {Number} - period to next grain
*/
trigger(time) {
var audioContext = this.audioContext;
var grainTime = time || audioContext.currentTime;
var grainPeriod = this.periodAbs;
var grainPosition = this.currentPosition;
var grainDuration = this.durationAbs;
if (this.buffer) {
var resamplingRate = 1.0;
// calculate resampling
if (this.resampling !== 0 || this.resamplingVar > 0) {
var randomResampling = (Math.random() - 0.5) * 2.0 * this.resamplingVar;
resamplingRate = Math.pow(2.0, (this.resampling + randomResampling) / 1200.0);
}
grainPeriod += this.periodRel * grainDuration;
grainDuration += this.durationRel * grainPeriod;
// grain period randon variation
if (this.periodVar > 0.0)
grainPeriod += 2.0 * (Math.random() - 0.5) * this.periodVar * grainPeriod;
// center grain
if (this.centered)
grainPosition -= 0.5 * grainDuration;
// randomize grain position
if (this.positionVar > 0)
grainPosition += (2.0 * Math.random() - 1) * this.positionVar;
var bufferDuration = this.bufferDuration;
// wrap or clip grain position and duration into buffer duration
if (grainPosition < 0 || grainPosition >= bufferDuration) {
if (this.cyclic) {
var cycles = grainPosition / bufferDuration;
grainPosition = (cycles - Math.floor(cycles)) * bufferDuration;
if (grainPosition + grainDuration > this.buffer.duration)
grainDuration = this.buffer.duration - grainPosition;
} else {
if (grainPosition < 0) {
grainTime -= grainPosition;
grainDuration += grainPosition;
grainPosition = 0;
}
if (grainPosition + grainDuration > bufferDuration)
grainDuration = bufferDuration - grainPosition;
}
}
// make grain
if (this.gain > 0 && grainDuration >= 0.001) {
// make grain envelope
var envelope = audioContext.createGain();
var attack = this.attackAbs + this.attackRel * grainDuration;
var release = this.releaseAbs + this.releaseRel * grainDuration;
if (attack + release > grainDuration) {
var factor = grainDuration / (attack + release);
attack *= factor;
release *= factor;
}
var attackEndTime = grainTime + attack;
var grainEndTime = grainTime + grainDuration / resamplingRate;
var releaseStartTime = grainEndTime - release;
envelope.gain.value = 0;
if (this.attackShape === 'lin') {
envelope.gain.setValueAtTime(0.0, grainTime);
envelope.gain.linearRampToValueAtTime(this.gain, attackEndTime);
} else {
envelope.gain.setValueAtTime(this.expRampOffset, grainTime);
envelope.gain.exponentialRampToValueAtTime(this.gain, attackEndTime);
}
if (releaseStartTime > attackEndTime)
envelope.gain.setValueAtTime(this.gain, releaseStartTime);
if (this.releaseShape === 'lin') {
envelope.gain.linearRampToValueAtTime(0.0, grainEndTime);
} else {
envelope.gain.exponentialRampToValueAtTime(this.expRampOffset, grainEndTime);
}
envelope.connect(this.outputNode);
// make source
var source = audioContext.createBufferSource();
source.buffer = this.buffer;
source.playbackRate.value = resamplingRate;
source.connect(envelope);
source.start(grainTime, grainPosition);
source.stop(grainEndTime);
}
}
return Math.max(this.periodMin, grainPeriod);
}
}
export default GranularEngine;