@excaliburjs/plugin-jsfxr
Version:
excalibur-jsfxr provides sound effect generation utilizing a wrapper around jsfxr
1,371 lines (1,210 loc) • 36.1 kB
JavaScript
//import riffwave
import RIFFWAVE from "./riffwave.mjs";
// Wave shapes
var SQUARE = 0;
var SAWTOOTH = 1;
var SINE = 2;
var NOISE = 3;
// Playback volume
var masterVolume = 1;
var OVERSAMPLING = 8;
/*** Core data structure ***/
// Sound generation parameters are on [0,1] unless noted SIGNED & thus
// on [-1,1]
function Params() {
this.oldParams = true; // Note what structure this is
// Wave shape
this.wave_type = SQUARE;
// Envelope
this.p_env_attack = 0; // Attack time
this.p_env_sustain = 0.3; // Sustain time
this.p_env_punch = 0; // Sustain punch
this.p_env_decay = 0.4; // Decay time
// Tone
this.p_base_freq = 0.3; // Start frequency
this.p_freq_limit = 0; // Min frequency cutoff
this.p_freq_ramp = 0; // Slide (SIGNED)
this.p_freq_dramp = 0; // Delta slide (SIGNED)
// Vibrato
this.p_vib_strength = 0; // Vibrato depth
this.p_vib_speed = 0; // Vibrato speed
// Tonal change
this.p_arp_mod = 0; // Change amount (SIGNED)
this.p_arp_speed = 0; // Change speed
// Square wave duty (proportion of time signal is high vs. low)
this.p_duty = 0; // Square duty
this.p_duty_ramp = 0; // Duty sweep (SIGNED)
// Repeat
this.p_repeat_speed = 0; // Repeat speed
// Flanger
this.p_pha_offset = 0; // Flanger offset (SIGNED)
this.p_pha_ramp = 0; // Flanger sweep (SIGNED)
// Low-pass filter
this.p_lpf_freq = 1; // Low-pass filter cutoff
this.p_lpf_ramp = 0; // Low-pass filter cutoff sweep (SIGNED)
this.p_lpf_resonance = 0; // Low-pass filter resonance
// High-pass filter
this.p_hpf_freq = 0; // High-pass filter cutoff
this.p_hpf_ramp = 0; // High-pass filter cutoff sweep (SIGNED)
// Sample parameters
this.sound_vol = 0.5;
this.sample_rate = 44100;
this.sample_size = 8;
}
/*** Helper functions ***/
function sqr(x) {
return x * x;
}
function cube(x) {
return x * x * x;
}
function sign(x) {
return x < 0 ? -1 : 1;
}
function log(x, b) {
return Math.log(x) / Math.log(b);
}
var pow = Math.pow;
function frnd(range) {
return Math.random() * range;
}
function rndr(from, to) {
return Math.random() * (to - from) + from;
}
function rnd(max) {
return Math.floor(Math.random() * (max + 1));
}
/*** Import/export functions ***/
// http://stackoverflow.com/questions/3096646/how-to-convert-a-floating-point-number-to-its-binary-representation-ieee-754-i
function assembleFloat(sign, exponent, mantissa) {
return (sign << 31) | (exponent << 23) | mantissa;
}
function floatToNumber(flt) {
if (isNaN(flt))
// Special case: NaN
return assembleFloat(0, 0xff, 0x1337); // Mantissa is nonzero for NaN
var sign = flt < 0 ? 1 : 0;
flt = Math.abs(flt);
if (flt == 0.0)
// Special case: +-0
return assembleFloat(sign, 0, 0);
var exponent = Math.floor(Math.log(flt) / Math.LN2);
if (exponent > 127 || exponent < -126)
// Special case: +-Infinity (and huge numbers)
return assembleFloat(sign, 0xff, 0); // Mantissa is zero for +-Infinity
var mantissa = flt / Math.pow(2, exponent);
return assembleFloat(sign, exponent + 127, (mantissa * Math.pow(2, 23)) & 0x7fffff);
}
// http://stackoverflow.com/a/16001019
function numberToFloat(bytes) {
var sign = bytes & 0x80000000 ? -1 : 1;
var exponent = ((bytes >> 23) & 0xff) - 127;
var significand = bytes & ~(-1 << 23);
if (exponent == 128) return sign * (significand ? Number.NaN : Number.POSITIVE_INFINITY);
if (exponent == -127) {
if (significand == 0) return sign * 0.0;
exponent = -126;
significand /= 1 << 22;
} else significand = (significand | (1 << 23)) / (1 << 23);
return sign * significand * Math.pow(2, exponent);
}
// export parameter list to URL friendly base58 string
// https://gist.github.com/diafygi/90a3e80ca1c2793220e5/
var b58alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
var params_order = [
"wave_type",
"p_env_attack",
"p_env_sustain",
"p_env_punch",
"p_env_decay",
"p_base_freq",
"p_freq_limit",
"p_freq_ramp",
"p_freq_dramp",
"p_vib_strength",
"p_vib_speed",
"p_arp_mod",
"p_arp_speed",
"p_duty",
"p_duty_ramp",
"p_repeat_speed",
"p_pha_offset",
"p_pha_ramp",
"p_lpf_freq",
"p_lpf_ramp",
"p_lpf_resonance",
"p_hpf_freq",
"p_hpf_ramp",
];
var params_signed = [
"p_freq_ramp",
"p_freq_dramp",
"p_arp_mod",
"p_duty_ramp",
"p_pha_offset",
"p_pha_ramp",
"p_lpf_ramp",
"p_hpf_ramp",
];
Params.prototype.toB58 = function () {
var convert = [];
for (var pi in params_order) {
var p = params_order[pi];
if (p == "wave_type") {
convert.push(this[p]);
} else if (p.indexOf("p_") == 0) {
var val = this[p];
val = floatToNumber(val);
convert.push(0xff & val);
convert.push(0xff & (val >> 8));
convert.push(0xff & (val >> 16));
convert.push(0xff & (val >> 24));
}
}
return (function (B, A) {
var d = [],
s = "",
i,
j,
c,
n;
for (i in B) {
(j = 0), (c = B[i]);
s += c || s.length ^ i ? "" : 1;
while (j in d || c) {
n = d[j];
n = n ? n * 256 + c : c;
c = (n / 58) | 0;
d[j] = n % 58;
j++;
}
}
while (j--) s += A[d[j]];
return s;
})(convert, b58alphabet);
};
Params.prototype.fromB58 = function (b58encoded) {
this.fromJSON(sfxr.b58decode(b58encoded));
return this;
};
Params.prototype.fromJSON = function (struct) {
for (var p in struct) {
if (struct.hasOwnProperty(p)) {
this[p] = struct[p];
}
}
return this;
};
/*** Presets ***/
// These functions roll up random sounds appropriate to various
// typical game events:
Params.prototype.pickupCoin = function () {
this.wave_type = SAWTOOTH;
this.p_base_freq = 0.4 + frnd(0.5);
this.p_env_attack = 0;
this.p_env_sustain = frnd(0.1);
this.p_env_decay = 0.1 + frnd(0.4);
this.p_env_punch = 0.3 + frnd(0.3);
if (rnd(1)) {
this.p_arp_speed = 0.5 + frnd(0.2);
this.p_arp_mod = 0.2 + frnd(0.4);
}
return this;
};
Params.prototype.laserShoot = function () {
this.wave_type = rnd(2);
if (this.wave_type === SINE && rnd(1)) this.wave_type = rnd(1);
if (rnd(2) === 0) {
this.p_base_freq = 0.3 + frnd(0.6);
this.p_freq_limit = frnd(0.1);
this.p_freq_ramp = -0.35 - frnd(0.3);
} else {
this.p_base_freq = 0.5 + frnd(0.5);
this.p_freq_limit = this.p_base_freq - 0.2 - frnd(0.6);
if (this.p_freq_limit < 0.2) this.p_freq_limit = 0.2;
this.p_freq_ramp = -0.15 - frnd(0.2);
}
if (this.wave_type === SAWTOOTH) this.p_duty = 1;
if (rnd(1)) {
this.p_duty = frnd(0.5);
this.p_duty_ramp = frnd(0.2);
} else {
this.p_duty = 0.4 + frnd(0.5);
this.p_duty_ramp = -frnd(0.7);
}
this.p_env_attack = 0;
this.p_env_sustain = 0.1 + frnd(0.2);
this.p_env_decay = frnd(0.4);
if (rnd(1)) this.p_env_punch = frnd(0.3);
if (rnd(2) === 0) {
this.p_pha_offset = frnd(0.2);
this.p_pha_ramp = -frnd(0.2);
}
//if (rnd(1))
this.p_hpf_freq = frnd(0.3);
return this;
};
Params.prototype.explosion = function () {
this.wave_type = NOISE;
if (rnd(1)) {
this.p_base_freq = sqr(0.1 + frnd(0.4));
this.p_freq_ramp = -0.1 + frnd(0.4);
} else {
this.p_base_freq = sqr(0.2 + frnd(0.7));
this.p_freq_ramp = -0.2 - frnd(0.2);
}
if (rnd(4) === 0) this.p_freq_ramp = 0;
if (rnd(2) === 0) this.p_repeat_speed = 0.3 + frnd(0.5);
this.p_env_attack = 0;
this.p_env_sustain = 0.1 + frnd(0.3);
this.p_env_decay = frnd(0.5);
if (rnd(1)) {
this.p_pha_offset = -0.3 + frnd(0.9);
this.p_pha_ramp = -frnd(0.3);
}
this.p_env_punch = 0.2 + frnd(0.6);
if (rnd(1)) {
this.p_vib_strength = frnd(0.7);
this.p_vib_speed = frnd(0.6);
}
if (rnd(2) === 0) {
this.p_arp_speed = 0.6 + frnd(0.3);
this.p_arp_mod = 0.8 - frnd(1.6);
}
return this;
};
Params.prototype.powerUp = function () {
if (rnd(1)) {
this.wave_type = SAWTOOTH;
this.p_duty = 1;
} else {
this.p_duty = frnd(0.6);
}
this.p_base_freq = 0.2 + frnd(0.3);
if (rnd(1)) {
this.p_freq_ramp = 0.1 + frnd(0.4);
this.p_repeat_speed = 0.4 + frnd(0.4);
} else {
this.p_freq_ramp = 0.05 + frnd(0.2);
if (rnd(1)) {
this.p_vib_strength = frnd(0.7);
this.p_vib_speed = frnd(0.6);
}
}
this.p_env_attack = 0;
this.p_env_sustain = frnd(0.4);
this.p_env_decay = 0.1 + frnd(0.4);
return this;
};
Params.prototype.hitHurt = function () {
this.wave_type = rnd(2);
if (this.wave_type === SINE) this.wave_type = NOISE;
if (this.wave_type === SQUARE) this.p_duty = frnd(0.6);
if (this.wave_type === SAWTOOTH) this.p_duty = 1;
this.p_base_freq = 0.2 + frnd(0.6);
this.p_freq_ramp = -0.3 - frnd(0.4);
this.p_env_attack = 0;
this.p_env_sustain = frnd(0.1);
this.p_env_decay = 0.1 + frnd(0.2);
if (rnd(1)) this.p_hpf_freq = frnd(0.3);
return this;
};
Params.prototype.jump = function () {
this.wave_type = SQUARE;
this.p_duty = frnd(0.6);
this.p_base_freq = 0.3 + frnd(0.3);
this.p_freq_ramp = 0.1 + frnd(0.2);
this.p_env_attack = 0;
this.p_env_sustain = 0.1 + frnd(0.3);
this.p_env_decay = 0.1 + frnd(0.2);
if (rnd(1)) this.p_hpf_freq = frnd(0.3);
if (rnd(1)) this.p_lpf_freq = 1 - frnd(0.6);
return this;
};
Params.prototype.blipSelect = function () {
this.wave_type = rnd(1);
if (this.wave_type === SQUARE) this.p_duty = frnd(0.6);
else this.p_duty = 1;
this.p_base_freq = 0.2 + frnd(0.4);
this.p_env_attack = 0;
this.p_env_sustain = 0.1 + frnd(0.1);
this.p_env_decay = frnd(0.2);
this.p_hpf_freq = 0.1;
return this;
};
Params.prototype.synth = function () {
this.wave_type = rnd(1);
this.p_base_freq = [0.2723171360931539, 0.19255692561524382, 0.13615778746815113][rnd(2)];
this.p_env_attack = rnd(4) > 3 ? frnd(0.5) : 0;
this.p_env_sustain = frnd(1);
this.p_env_punch = frnd(1);
this.p_env_decay = frnd(0.9) + 0.1;
this.p_arp_mod = [0, 0, 0, 0, -0.3162, 0.7454, 0.7454][rnd(6)];
this.p_arp_speed = frnd(0.5) + 0.4;
this.p_duty = frnd(1);
this.p_duty_ramp = rnd(2) == 2 ? frnd(1) : 0;
this.p_lpf_freq = [1, 0.9 * frnd(1) * frnd(1) + 0.1][rnd(1)];
this.p_lpf_ramp = rndr(-1, 1);
this.p_lpf_resonance = frnd(1);
this.p_hpf_freq = rnd(3) == 3 ? frnd(1) : 0;
this.p_hpf_ramp = rnd(3) == 3 ? frnd(1) : 0;
return this;
};
Params.prototype.tone = function () {
this.wave_type = SINE;
this.p_base_freq = 0.35173364; // 440 Hz
this.p_env_attack = 0;
this.p_env_sustain = 0.6641; // 1 sec
this.p_env_decay = 0;
this.p_env_punch = 0;
return this;
};
Params.prototype.click = function () {
const base = ["explosion", "hitHurt"][rnd(1)];
this[base]();
if (rnd(1)) {
this.p_freq_ramp = -0.5 + frnd(1.0);
}
if (rnd(1)) {
this.p_env_sustain = (frnd(0.4) + 0.2) * this.p_env_sustain;
this.p_env_decay = (frnd(0.4) + 0.2) * this.p_env_decay;
}
if (rnd(3) == 0) {
this.p_env_attack = frnd(0.3);
}
this.p_base_freq = 1 - frnd(0.25);
this.p_hpf_freq = 1 - frnd(0.1);
return this;
};
Params.prototype.random = function () {
this.wave_type = rnd(3);
if (rnd(1)) this.p_base_freq = cube(frnd(2) - 1) + 0.5;
else this.p_base_freq = sqr(frnd(1));
this.p_freq_limit = 0;
this.p_freq_ramp = Math.pow(frnd(2) - 1, 5);
if (this.p_base_freq > 0.7 && this.p_freq_ramp > 0.2) this.p_freq_ramp = -this.p_freq_ramp;
if (this.p_base_freq < 0.2 && this.p_freq_ramp < -0.05) this.p_freq_ramp = -this.p_freq_ramp;
this.p_freq_dramp = Math.pow(frnd(2) - 1, 3);
this.p_duty = frnd(2) - 1;
this.p_duty_ramp = Math.pow(frnd(2) - 1, 3);
this.p_vib_strength = Math.pow(frnd(2) - 1, 3);
this.p_vib_speed = rndr(-1, 1);
this.p_env_attack = cube(rndr(-1, 1));
this.p_env_sustain = sqr(rndr(-1, 1));
this.p_env_decay = rndr(-1, 1);
this.p_env_punch = Math.pow(frnd(0.8), 2);
if (this.p_env_attack + this.p_env_sustain + this.p_env_decay < 0.2) {
this.p_env_sustain += 0.2 + frnd(0.3);
this.p_env_decay += 0.2 + frnd(0.3);
}
this.p_lpf_resonance = rndr(-1, 1);
this.p_lpf_freq = 1 - Math.pow(frnd(1), 3);
this.p_lpf_ramp = Math.pow(frnd(2) - 1, 3);
if (this.p_lpf_freq < 0.1 && this.p_lpf_ramp < -0.05) this.p_lpf_ramp = -this.p_lpf_ramp;
this.p_hpf_freq = Math.pow(frnd(1), 5);
this.p_hpf_ramp = Math.pow(frnd(2) - 1, 5);
this.p_pha_offset = Math.pow(frnd(2) - 1, 3);
this.p_pha_ramp = Math.pow(frnd(2) - 1, 3);
this.p_repeat_speed = frnd(2) - 1;
this.p_arp_speed = frnd(2) - 1;
this.p_arp_mod = frnd(2) - 1;
return this;
};
Params.prototype.mutate = function () {
if (rnd(1)) this.p_base_freq += frnd(0.1) - 0.05;
if (rnd(1)) this.p_freq_ramp += frnd(0.1) - 0.05;
if (rnd(1)) this.p_freq_dramp += frnd(0.1) - 0.05;
if (rnd(1)) this.p_duty += frnd(0.1) - 0.05;
if (rnd(1)) this.p_duty_ramp += frnd(0.1) - 0.05;
if (rnd(1)) this.p_vib_strength += frnd(0.1) - 0.05;
if (rnd(1)) this.p_vib_speed += frnd(0.1) - 0.05;
if (rnd(1)) this.p_vib_delay += frnd(0.1) - 0.05;
if (rnd(1)) this.p_env_attack += frnd(0.1) - 0.05;
if (rnd(1)) this.p_env_sustain += frnd(0.1) - 0.05;
if (rnd(1)) this.p_env_decay += frnd(0.1) - 0.05;
if (rnd(1)) this.p_env_punch += frnd(0.1) - 0.05;
if (rnd(1)) this.p_lpf_resonance += frnd(0.1) - 0.05;
if (rnd(1)) this.p_lpf_freq += frnd(0.1) - 0.05;
if (rnd(1)) this.p_lpf_ramp += frnd(0.1) - 0.05;
if (rnd(1)) this.p_hpf_freq += frnd(0.1) - 0.05;
if (rnd(1)) this.p_hpf_ramp += frnd(0.1) - 0.05;
if (rnd(1)) this.p_pha_offset += frnd(0.1) - 0.05;
if (rnd(1)) this.p_pha_ramp += frnd(0.1) - 0.05;
if (rnd(1)) this.p_repeat_speed += frnd(0.1) - 0.05;
if (rnd(1)) this.p_arp_speed += frnd(0.1) - 0.05;
if (rnd(1)) this.p_arp_mod += frnd(0.1) - 0.05;
return this;
};
/*** Simpler namespaced functional API ***/
var sfxr;
export default sfxr = {};
sfxr.toBuffer = function (synthdef) {
return new SoundEffect(synthdef).getRawBuffer()["buffer"];
};
sfxr.toWebAudio = function (synthdef, audiocontext) {
var sfx = new SoundEffect(synthdef);
var buffer = sfx.getRawBuffer()["normalized"];
if (audiocontext) {
var buff = audiocontext.createBuffer(1, buffer.length, sfx.sampleRate);
var nowBuffering = buff.getChannelData(0);
for (var i = 0; i < buffer.length; i++) {
nowBuffering[i] = buffer[i];
}
var proc = audiocontext.createBufferSource();
proc.buffer = buff;
return proc;
}
};
sfxr.toWave = function (synthdef) {
return new SoundEffect(synthdef).generate();
};
sfxr.toAudio = function (synthdef) {
return sfxr.toWave(synthdef).getAudio();
};
sfxr.play = function (synthdef) {
return sfxr.toAudio(synthdef).play();
};
sfxr.b58decode = function (b58encoded) {
var decoded = (function (S, A) {
var d = [],
b = [],
i,
j,
c,
n;
for (i in S) {
(j = 0), (c = A.indexOf(S[i]));
if (c < 0) return undefined;
c || b.length ^ i ? i : b.push(0);
while (j in d || c) {
n = d[j];
n = n ? n * 58 + c : c;
c = n >> 8;
d[j] = n % 256;
j++;
}
}
while (j--) b.push(d[j]);
return new Uint8Array(b);
})(b58encoded, b58alphabet);
var result = {};
for (var pi in params_order) {
var p = params_order[pi];
var offset = (pi - 1) * 4 + 1;
if (p == "wave_type") {
result[p] = decoded[0];
} else {
var val = decoded[offset] | (decoded[offset + 1] << 8) | (decoded[offset + 2] << 16) | (decoded[offset + 3] << 24);
result[p] = numberToFloat(val);
}
}
return result;
};
sfxr.b58encode = function (synthdef) {
var p = new Params();
p.fromJSON(synthdef);
return p.toB58();
};
sfxr.generate = function (algorithm, options) {
const p = new Params();
const opts = options || {};
p.sound_vol = opts["sound_vol"] || 0.25;
p.sample_rate = opts["sample_rate"] || 44100;
p.sample_size = opts["sample_size"] || 8;
return p[algorithm]();
};
/*** Main entry point ***/
function SoundEffect(ps) {
if (typeof ps == "string") {
var PARAMS = new Params();
if (ps.indexOf("#") == 0) {
ps = ps.slice(1);
}
ps = PARAMS.fromB58(ps);
}
this.init(ps);
}
SoundEffect.prototype.init = function (ps) {
this.parameters = ps;
this.initForRepeat(); // First time through, this is a bit of a misnomer
// Waveform shape
this.waveShape = parseInt(ps.wave_type);
// Filter
this.fltw = Math.pow(ps.p_lpf_freq, 3) * 0.1;
this.enableLowPassFilter = ps.p_lpf_freq != 1;
this.fltw_d = 1 + ps.p_lpf_ramp * 0.0001;
this.fltdmp = (5 / (1 + Math.pow(ps.p_lpf_resonance, 2) * 20)) * (0.01 + this.fltw);
if (this.fltdmp > 0.8) this.fltdmp = 0.8;
this.flthp = Math.pow(ps.p_hpf_freq, 2) * 0.1;
this.flthp_d = 1 + ps.p_hpf_ramp * 0.0003;
// Vibrato
this.vibratoSpeed = Math.pow(ps.p_vib_speed, 2) * 0.01;
this.vibratoAmplitude = ps.p_vib_strength * 0.5;
// Envelope
this.envelopeLength = [
Math.floor(ps.p_env_attack * ps.p_env_attack * 100000),
Math.floor(ps.p_env_sustain * ps.p_env_sustain * 100000),
Math.floor(ps.p_env_decay * ps.p_env_decay * 100000),
];
this.envelopePunch = ps.p_env_punch;
// Flanger
this.flangerOffset = Math.pow(ps.p_pha_offset, 2) * 1020;
if (ps.p_pha_offset < 0) this.flangerOffset = -this.flangerOffset;
this.flangerOffsetSlide = Math.pow(ps.p_pha_ramp, 2) * 1;
if (ps.p_pha_ramp < 0) this.flangerOffsetSlide = -this.flangerOffsetSlide;
// Repeat
this.repeatTime = Math.floor(Math.pow(1 - ps.p_repeat_speed, 2) * 20000 + 32);
if (ps.p_repeat_speed === 0) this.repeatTime = 0;
this.gain = Math.exp(ps.sound_vol) - 1;
this.sampleRate = ps.sample_rate;
this.bitsPerChannel = ps.sample_size;
};
SoundEffect.prototype.initForRepeat = function () {
var ps = this.parameters;
this.elapsedSinceRepeat = 0;
this.period = 100 / (ps.p_base_freq * ps.p_base_freq + 0.001);
this.periodMax = 100 / (ps.p_freq_limit * ps.p_freq_limit + 0.001);
this.enableFrequencyCutoff = ps.p_freq_limit > 0;
this.periodMult = 1 - Math.pow(ps.p_freq_ramp, 3) * 0.01;
this.periodMultSlide = -Math.pow(ps.p_freq_dramp, 3) * 0.000001;
this.dutyCycle = 0.5 - ps.p_duty * 0.5;
this.dutyCycleSlide = -ps.p_duty_ramp * 0.00005;
if (ps.p_arp_mod >= 0) this.arpeggioMultiplier = 1 - Math.pow(ps.p_arp_mod, 2) * 0.9;
else this.arpeggioMultiplier = 1 + Math.pow(ps.p_arp_mod, 2) * 10;
this.arpeggioTime = Math.floor(Math.pow(1 - ps.p_arp_speed, 2) * 20000 + 32);
if (ps.p_arp_speed === 1) this.arpeggioTime = 0;
};
SoundEffect.prototype.getRawBuffer = function () {
var fltp = 0;
var fltdp = 0;
var fltphp = 0;
var noise_buffer = Array(32);
for (var i = 0; i < 32; ++i) noise_buffer[i] = Math.random() * 2 - 1;
var envelopeStage = 0;
var envelopeElapsed = 0;
var vibratoPhase = 0;
var phase = 0;
var ipp = 0;
var flanger_buffer = Array(1024);
for (var i = 0; i < 1024; ++i) flanger_buffer[i] = 0;
var num_clipped = 0;
var buffer = [];
var normalized = [];
var sample_sum = 0;
var num_summed = 0;
var summands = Math.floor(44100 / this.sampleRate);
for (var t = 0; ; ++t) {
// Repeats
if (this.repeatTime != 0 && ++this.elapsedSinceRepeat >= this.repeatTime) this.initForRepeat();
// Arpeggio (single)
if (this.arpeggioTime != 0 && t >= this.arpeggioTime) {
this.arpeggioTime = 0;
this.period *= this.arpeggioMultiplier;
}
// Frequency slide, and frequency slide slide!
this.periodMult += this.periodMultSlide;
this.period *= this.periodMult;
if (this.period > this.periodMax) {
this.period = this.periodMax;
if (this.enableFrequencyCutoff) break;
}
// Vibrato
var rfperiod = this.period;
if (this.vibratoAmplitude > 0) {
vibratoPhase += this.vibratoSpeed;
rfperiod = this.period * (1 + Math.sin(vibratoPhase) * this.vibratoAmplitude);
}
var iperiod = Math.floor(rfperiod);
if (iperiod < OVERSAMPLING) iperiod = OVERSAMPLING;
// Square wave duty cycle
this.dutyCycle += this.dutyCycleSlide;
if (this.dutyCycle < 0) this.dutyCycle = 0;
if (this.dutyCycle > 0.5) this.dutyCycle = 0.5;
// Volume envelope
if (++envelopeElapsed > this.envelopeLength[envelopeStage]) {
envelopeElapsed = 0;
if (++envelopeStage > 2) break;
}
var env_vol;
var envf = envelopeElapsed / this.envelopeLength[envelopeStage];
if (envelopeStage === 0) {
// Attack
env_vol = envf;
} else if (envelopeStage === 1) {
// Sustain
env_vol = 1 + (1 - envf) * 2 * this.envelopePunch;
} else {
// Decay
env_vol = 1 - envf;
}
// Flanger step
this.flangerOffset += this.flangerOffsetSlide;
var iphase = Math.abs(Math.floor(this.flangerOffset));
if (iphase > 1023) iphase = 1023;
if (this.flthp_d != 0) {
this.flthp *= this.flthp_d;
if (this.flthp < 0.00001) this.flthp = 0.00001;
if (this.flthp > 0.1) this.flthp = 0.1;
}
// 8x oversampling
var sample = 0;
for (var si = 0; si < OVERSAMPLING; ++si) {
var sub_sample = 0;
phase++;
if (phase >= iperiod) {
phase %= iperiod;
if (this.waveShape === NOISE) for (var i = 0; i < 32; ++i) noise_buffer[i] = Math.random() * 2 - 1;
}
// Base waveform
var fp = phase / iperiod;
if (this.waveShape === SQUARE) {
if (fp < this.dutyCycle) sub_sample = 0.5;
else sub_sample = -0.5;
} else if (this.waveShape === SAWTOOTH) {
if (fp < this.dutyCycle) sub_sample = -1 + (2 * fp) / this.dutyCycle;
else sub_sample = 1 - (2 * (fp - this.dutyCycle)) / (1 - this.dutyCycle);
} else if (this.waveShape === SINE) {
sub_sample = Math.sin(fp * 2 * Math.PI);
} else if (this.waveShape === NOISE) {
sub_sample = noise_buffer[Math.floor((phase * 32) / iperiod)];
} else {
throw "ERROR: Bad wave type: " + this.waveShape;
}
// Low-pass filter
var pp = fltp;
this.fltw *= this.fltw_d;
if (this.fltw < 0) this.fltw = 0;
if (this.fltw > 0.1) this.fltw = 0.1;
if (this.enableLowPassFilter) {
fltdp += (sub_sample - fltp) * this.fltw;
fltdp -= fltdp * this.fltdmp;
} else {
fltp = sub_sample;
fltdp = 0;
}
fltp += fltdp;
// High-pass filter
fltphp += fltp - pp;
fltphp -= fltphp * this.flthp;
sub_sample = fltphp;
// Flanger
flanger_buffer[ipp & 1023] = sub_sample;
sub_sample += flanger_buffer[(ipp - iphase + 1024) & 1023];
ipp = (ipp + 1) & 1023;
// final accumulation and envelope application
sample += sub_sample * env_vol;
}
// Accumulate samples appropriately for sample rate
sample_sum += sample;
if (++num_summed >= summands) {
num_summed = 0;
sample = sample_sum / summands;
sample_sum = 0;
} else {
continue;
}
sample = (sample / OVERSAMPLING) * masterVolume;
sample *= this.gain;
// store the original normalized floating point sample
normalized.push(sample);
if (this.bitsPerChannel === 8) {
// Rescale [-1, 1) to [0, 256)
sample = Math.floor((sample + 1) * 128);
if (sample > 255) {
sample = 255;
++num_clipped;
} else if (sample < 0) {
sample = 0;
++num_clipped;
}
buffer.push(sample);
} else {
// Rescale [-1, 1) to [-32768, 32768)
sample = Math.floor(sample * (1 << 15));
if (sample >= 1 << 15) {
sample = (1 << 15) - 1;
++num_clipped;
} else if (sample < -(1 << 15)) {
sample = -(1 << 15);
++num_clipped;
}
buffer.push(sample & 0xff);
buffer.push((sample >> 8) & 0xff);
}
}
return {
buffer: buffer,
normalized: normalized,
clipped: num_clipped,
};
};
SoundEffect.prototype.generate = function () {
var rendered = this.getRawBuffer();
var wave = new RIFFWAVE();
wave.header.sampleRate = this.sampleRate;
wave.header.bitsPerSample = this.bitsPerChannel;
wave.Make(rendered.buffer);
wave.clipping = rendered.clipped;
wave.buffer = rendered.normalized;
wave.getAudio = _sfxr_getAudioFn(wave);
return wave;
};
var _actx = null;
var _sfxr_getAudioFn = function (wave) {
return function () {
// check for procedural audio
var actx = null;
if (!_actx) {
if ("AudioContext" in window) {
_actx = new AudioContext();
} else if ("webkitAudioContext" in window) {
_actx = new webkitAudioContext();
}
}
actx = _actx;
if (actx) {
var buff = actx.createBuffer(1, wave.buffer.length, wave.header.sampleRate);
var nowBuffering = buff.getChannelData(0);
for (var i = 0; i < wave.buffer.length; i++) {
nowBuffering[i] = wave.buffer[i];
}
var volume = 1.0;
var obj = {
channels: [],
setVolume: function (v) {
volume = v;
return obj;
},
play: function () {
var proc = actx.createBufferSource();
proc.buffer = buff;
var gainNode = actx.createGain();
gainNode.gain.value = volume;
gainNode.connect(actx.destination);
proc.connect(gainNode);
if (proc["start"]) {
proc.start();
} else if (proc["noteOn"]) {
proc.noteOn(0);
}
this.channels.push(proc);
return proc;
},
};
return obj;
} else {
var audio = new Audio();
audio.src = wave.dataURI;
return audio;
}
};
};
/*** conversions from slider values, internal, and units ***/
// convert from slider values to internal representation
var sliders = {
p_env_attack: function (v) {
return v * v * 100000.0;
},
p_env_sustain: function (v) {
return v * v * 100000.0;
},
p_env_punch: function (v) {
return v;
},
p_env_decay: function (v) {
return v * v * 100000.0;
},
p_base_freq: function (v) {
return (8 * 44100 * (v * v + 0.001)) / 100;
},
p_freq_limit: function (v) {
return (8 * 44100 * (v * v + 0.001)) / 100;
},
p_freq_ramp: function (v) {
return 1.0 - Math.pow(v, 3.0) * 0.01;
},
p_freq_dramp: function (v) {
return -Math.pow(v, 3.0) * 0.000001;
},
p_vib_speed: function (v) {
return Math.pow(v, 2.0) * 0.01;
},
p_vib_strength: function (v) {
return v * 0.5;
},
p_arp_mod: function (v) {
return v >= 0 ? 1.0 - Math.pow(v, 2) * 0.9 : 1.0 + Math.pow(v, 2) * 10;
},
p_arp_speed: function (v) {
return v === 1.0 ? 0 : Math.floor(Math.pow(1.0 - v, 2.0) * 20000 + 32);
},
p_duty: function (v) {
return 0.5 - v * 0.5;
},
p_duty_ramp: function (v) {
return -v * 0.00005;
},
p_repeat_speed: function (v) {
return v === 0 ? 0 : Math.floor(Math.pow(1 - v, 2) * 20000) + 32;
},
p_pha_offset: function (v) {
return (v < 0 ? -1 : 1) * Math.pow(v, 2) * 1020;
},
p_pha_ramp: function (v) {
return (v < 0 ? -1 : 1) * Math.pow(v, 2);
},
p_lpf_freq: function (v) {
return Math.pow(v, 3) * 0.1;
},
p_lpf_ramp: function (v) {
return 1.0 + v * 0.0001;
},
p_lpf_resonance: function (v) {
return 5.0 / (1.0 + Math.pow(v, 2) * 20);
}, // * (0.01 + fltw);
p_hpf_freq: function (v) {
return Math.pow(v, 2) * 0.1;
},
p_hpf_ramp: function (v) {
return 1.0 + v * 0.0003;
},
sound_vol: function (v) {
return Math.exp(v) - 1;
},
};
var sliders_inverse = {
p_env_attack: function (v) {
return Math.sqrt(v / 100000.0);
},
p_env_sustain: function (v) {
return Math.sqrt(v / 100000.0);
},
p_env_punch: function (v) {
return v;
},
p_env_decay: function (v) {
return Math.sqrt(v / 100000.0);
},
p_base_freq: function (v) {
return Math.sqrt((v * 100) / 8 / 44100 - 0.001);
},
p_freq_limit: function (v) {
return Math.sqrt((v * 100) / 8 / 44100 - 0.001);
},
p_freq_ramp: function (v) {
return Math.cbrt((1.0 - v) / 0.01);
},
p_freq_dramp: function (v) {
return Math.cbrt(v / -0.000001);
},
p_vib_speed: function (v) {
return Math.sqrt(v / 0.01);
},
p_vib_strength: function (v) {
return v / 0.5;
},
p_arp_mod: function (v) {
return v < 1 ? Math.sqrt((1.0 - v) / 0.9) : -Math.sqrt((v - 1.0) / 10.0);
},
p_arp_speed: function (v) {
return v === 0 ? 1.0 : 1.0 - Math.sqrt((v - (v < 100 ? 30 : 32)) / 20000);
},
p_duty: function (v) {
return (v - 0.5) / -0.5;
},
p_duty_ramp: function (v) {
return v / -0.00005;
},
p_repeat_speed: function (v) {
return v === 0 ? 0 : -(Math.sqrt((v - 32) / 20000) - 1.0);
},
p_pha_offset: function (v) {
return (v < 0 ? -1 : 1) * Math.sqrt(Math.abs(v) / 1020);
},
p_pha_ramp: function (v) {
return (v < 0 ? -1 : 1) * Math.sqrt(Math.abs(v));
},
p_lpf_freq: function (v) {
return Math.cbrt(v / 0.1);
},
p_lpf_ramp: function (v) {
return (v - 1.0) / 0.0001;
},
p_lpf_resonance: function (v) {
return Math.sqrt((1.0 / (v / 5.0) - 1) / 20);
},
p_hpf_freq: function (v) {
return Math.sqrt(v / 0.1);
},
p_hpf_ramp: function (v) {
return (v - 1.0) / 0.0003;
},
sound_vol: function (v) {
return Math.log(v + 1);
},
};
// convert from internal representation to domain value without units
var domain = {
p_env_attack: function (v) {
return v / 44100;
},
p_env_sustain: function (v) {
return v / 44100;
},
p_env_punch: function (v) {
return v * 100;
},
p_env_decay: function (v) {
return v / 44100;
},
p_base_freq: function (v) {
return v;
},
p_freq_limit: function (v) {
return v;
},
p_freq_ramp: function (v) {
return (44100 * Math.log(v)) / Math.log(0.5);
},
p_freq_dramp: function (v) {
return (v * 44100) / Math.pow(2, -44101 / 44100);
},
p_vib_speed: function (v) {
return (441000 / 64) * v;
},
p_vib_strength: function (v) {
return v * 100;
},
p_arp_mod: function (v) {
return 1 / v;
},
p_arp_speed: function (v) {
return v / 44100;
},
p_duty: function (v) {
return 100 * v;
},
p_duty_ramp: function (v) {
return 8 * 44100 * v;
},
p_repeat_speed: function (v) {
return v === 0 ? 0 : 44100 / v;
},
p_pha_offset: function (v) {
return (1000 * v) / 44100;
},
p_pha_ramp: function (v) {
return 1000 * v;
},
p_lpf_freq: function (v) {
return v === 0.1 ? 0 : (8 * 44100 * v) / (1 - v);
},
p_lpf_ramp: function (v) {
return Math.pow(v, 44100);
},
p_lpf_resonance: function (v) {
return 100 * (1 - v * 0.11);
},
p_hpf_freq: function (v) {
return (8 * 44100 * v) / (1 - v);
},
p_hpf_ramp: function (v) {
return Math.pow(v, 44100);
},
sound_vol: function (v) {
return (10 * Math.log(v * v)) / Math.log(10);
},
};
var domain_inverse = {
p_env_attack: function (v) {
return v * 44100;
},
p_env_sustain: function (v) {
return v * 44100;
},
p_env_punch: function (v) {
return v / 100;
},
p_env_decay: function (v) {
return v * 44100;
},
p_base_freq: function (v) {
return v;
},
p_freq_limit: function (v) {
return v;
},
p_freq_ramp: function (v) {
return Math.exp((Math.log(0.5) * v) / 44100);
},
p_freq_dramp: function (v) {
return (v * Math.pow(2, -44101 / 44100)) / 44100;
},
p_vib_speed: function (v) {
return (64 / 441000) * v;
},
p_vib_strength: function (v) {
return v / 100;
},
p_arp_mod: function (v) {
return 1 / v;
},
p_arp_speed: function (v) {
return v * 44100;
},
p_duty: function (v) {
return v / 100;
},
p_duty_ramp: function (v) {
return v / (8 * 44100);
},
p_repeat_speed: function (v) {
return v <= 0 ? 0 : v > 1378 ? 32 : 44100 / v;
},
p_pha_offset: function (v) {
return (v / 1000) * 44100;
},
p_pha_ramp: function (v) {
return v / 1000;
},
p_lpf_freq: function (v) {
return v / (v + 8 * 44100);
},
p_lpf_ramp: function (v) {
return Math.pow(v, 1 / 44100);
},
p_lpf_resonance: function (v) {
return (1 - v / 100) / 0.11;
},
p_hpf_freq: function (v) {
return v / (v + 8 * 44100);
},
p_hpf_ramp: function (v) {
return Math.pow(v, 1 / 44100);
},
sound_vol: function (v) {
return Math.sqrt(Math.pow(10, v / 10));
},
};
// convert from internal representation to printable units
var units = {
p_env_attack: function (v) {
return (v / 44100).toPrecision(4) + " sec";
},
p_env_sustain: function (v) {
return (v / 44100).toPrecision(4) + " sec";
},
p_env_punch: function (v) {
return "+" + (v * 100).toPrecision(4) + "%";
},
p_env_decay: function (v) {
return (v / 44100).toPrecision(4) + " sec";
},
p_base_freq: function (v) {
return v.toPrecision(4) + "Hz";
},
p_freq_limit: function (v) {
return v.toPrecision(4) + "Hz";
},
p_freq_ramp: function (v) {
return ((44100 * Math.log(v)) / Math.log(0.5)).toPrecision(4) + " 8va/sec";
},
p_freq_dramp: function (v) {
return ((v * 44100) / Math.pow(2, -44101 / 44100)).toExponential(3) + " 8va/s^2";
},
p_vib_speed: function (v) {
return v === 0 ? "OFF" : ((441000 / 64) * v).toPrecision(4) + " Hz";
},
p_vib_strength: function (v) {
return v === 0 ? "OFF" : "± " + (v * 100).toPrecision(4) + "%";
},
p_arp_mod: function (v) {
return v === 1 ? "OFF" : "x " + (1 / v).toPrecision(4);
},
p_arp_speed: function (v) {
return v === 0 ? "OFF" : (v / 44100).toPrecision(4) + " sec";
},
p_duty: function (v) {
return (100 * v).toPrecision(4) + "%";
},
p_duty_ramp: function (v) {
return (8 * 44100 * v).toPrecision(4) + "%/sec";
},
p_repeat_speed: function (v) {
return v === 0 ? "OFF" : (44100 / v).toPrecision(4) + " Hz";
},
p_pha_offset: function (v) {
return v === 0 ? "OFF" : ((1000 * v) / 44100).toPrecision(4) + " msec";
},
// Not so sure about this:
p_pha_ramp: function (v) {
return v === 0 ? "OFF" : (1000 * v).toPrecision(4) + " msec/sec";
},
p_lpf_freq: function (v) {
return v === 0.1 ? "OFF" : Math.round((8 * 44100 * v) / (1 - v)) + " Hz";
},
p_lpf_ramp: function (v) {
if (v === 1) return "OFF";
return Math.pow(v, 44100).toPrecision(4) + " ^sec";
},
p_lpf_resonance: function (v) {
return (100 * (1 - v * 0.11)).toPrecision(4) + "%";
},
p_hpf_freq: function (v) {
return v === 0 ? "OFF" : Math.round((8 * 44100 * v) / (1 - v)) + " Hz";
},
p_hpf_ramp: function (v) {
if (v === 1) return "OFF";
return Math.pow(v, 44100).toPrecision(4) + " ^sec";
},
sound_vol: function (v) {
v = (10 * Math.log(v * v)) / Math.log(10);
var sign = v >= 0 ? "+" : "";
return sign + v.toPrecision(4) + " dB";
},
};
/*** Plumbing ***/
/* (function (root, factory) {
if (typeof define === "function" && define.amd) {
// Now we're wrapping the factory and assigning the return
// value to the root (window) and returning it as well to
// the AMD loader.
/* define(["./riffwave"], function (RIFFWAVE) {
return (root.jsfxr = factory(RIFFWAVE));
});
} else if (typeof module === "object" && module.exports) {
// I've not encountered a need for this yet, since I haven't
// run into a scenario where plain modules depend on CommonJS
// *and* I happen to be loading in a CJS browser environment
// but I'm including it for the sake of being thorough
//RIFFWAVE = require("./riffwave.mjs");
module.exports = root.jsfxr = factory(RIFFWAVE);
} else {
root.jsfxr = factory(root.RIFFWAVE);
}
})(this, function (RIFFWAVE) {
// module code here....
return {
sfxr: sfxr,
convert: {
sliders: sliders,
domain: domain,
sliders_inverse: sliders_inverse,
domain_inverse: domain_inverse,
units: units,
},
parameters: {
order: params_order,
signed: params_signed,
},
Params: Params,
SoundEffect: SoundEffect,
waveforms: {
SQUARE: SQUARE,
SAWTOOTH: SAWTOOTH,
SINE: SINE,
NOISE: NOISE,
},
};
});
*/