You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1236 lines
28 KiB

(function(root, factory) {
if (typeof module === "object" && typeof module.exports === "object") {
module.exports = factory();
} else {
root.jsfx = factory();
}
}(this, function() {
'use strict';
var chr = String.fromCharCode;
var TAU = +Math.PI * 2;
var bitsPerSample = 16 | 0;
var numChannels = 1 | 0;
var sin = Math.sin;
var pow = Math.pow;
var abs = Math.abs;
var EPSILON = 0.000001;
var jsfx = {};
var AudioContext = window.AudioContext || window.webkitAudioContext;
jsfx.SampleRate = 0 | 0;
jsfx.Sec = 0 | 0;
jsfx.SetSampleRate = function(sampleRate) {
jsfx.SampleRate = sampleRate | 0;
jsfx.Sec = sampleRate | 0;
};
jsfx.SetSampleRate(getDefaultSampleRate());
// MAIN API
// Creates a new Audio object based on the params
// params can be a params generating function or the actual parameters
jsfx.Sound = function(params) {
var processor = new Processor(params, jsfx.DefaultModules);
var block = createFloatArray(processor.getSamplesLeft());
processor.generate(block);
return CreateAudio(block);
};
// Same as Sounds, but avoids locking the browser for too long
// in case you have a large amount of sounds to generate
jsfx.Sounds = function(library, ondone, onprogress) {
var audio = {};
var player = {};
player._audio = audio;
var toLoad = [];
// create playing functions
map_object(library, function(_, name) {
player[name] = function() {
if (typeof audio[name] !== "undefined") {
audio[name].currentTime = 0.0;
audio[name].play();
}
};
toLoad.push(name);
});
var loaded = 0,
total = toLoad.length;
function next() {
if (toLoad.length == 0) {
ondone && ondone(sounds);
return;
}
var name = toLoad.shift();
audio[name] = jsfx.Sound(library[name]);
loaded++;
onprogress && onprogress(name, loaded, total);
window.setTimeout(next, 30);
}
next();
return player;
}
// SoundsImmediate takes a named set of params, and generates multiple
// sound objects at once.
jsfx.SoundsImmediate = function(library) {
var audio = {};
var player = {};
player._audio = audio;
map_object(library, function(params, name) {
audio[name] = jsfx.Sound(params);
player[name] = function() {
if (typeof audio[name] !== "undefined") {
audio[name].currentTime = 0.0;
audio[name].play();
}
};
})
return player;
};
// FloatBuffer creates a FloatArray filled with audio
jsfx.FloatBuffer = function(params, modules) {
var processor = new Processor(params, jsfx.DefaultModules);
var block = createFloatArray(processor.getSamplesLeft());
processor.generate(block);
return block;
};
if (typeof AudioContext !== "undefined") {
// Node creates a new AudioContext ScriptProcessor that outputs the
// sound. It will automatically disconnect, unless otherwise specified.
jsfx.Node = function(audioContext, params, modules, bufferSize, stayConnected) {
var node = audioContext.createScriptProcessor(bufferSize, 0, 1);
var gen = new Processor(params, modules || jsfx.DefaultModules);
node.onaudioprocess = function(ev) {
var block = ev.outputBuffer.getChannelData(0);
gen.generate(block);
if (!stayConnected && gen.finished) {
// we need to do an async disconnect, otherwise Chrome may
// glitch
setTimeout(function() {
node.disconnect();
}, 30);
}
}
return node;
}
// AudioBuffer creates a buffer filled with the proper audio
// This is useful, when you want to use AudioContext.BufferSource
jsfx.AudioBuffer = function(audioContext, params, modules) {
var processor = new Processor(params, modules || jsfx.DefaultModules);
var buffer = audioContext.createBuffer(numChannels, processor.getSamplesLeft(), jsfx.SampleRate);
var block = buffer.getChannelData(0);
processor.generate(block);
return buffer;
};
// Live creates an managed AudioContext for playing.
// This is useful, when you want to use procedurally generated sounds.
jsfx.Live = function(library, modules, BufferSize) {
//TODO: add limit for number of notes played at the same time
BufferSize = BufferSize || 2048;
var player = {};
var context = new AudioContext();
var volume = context.createGain();
volume.connect(context.destination);
player._context = context;
player._volume = volume;
map_object(library, function(params, name) {
player[name] = function() {
var node = jsfx.Node(context, params, modules, BufferSize);
node.connect(volume);
};
});
player._close = function() {
context.close();
};
player._play = function(params) {
var node = jsfx.Node(context, params, modules, BufferSize);
node.connect(volume);
};
return player;
}
} else {
jsfx.Live = jsfx.Sounds;
}
// SOUND GENERATION
jsfx.Module = {};
// generators
jsfx.G = {};
var stage = jsfx.stage = {
PhaseSpeed: 0,
PhaseSpeedMod: 10,
Generator: 20,
SampleMod: 30,
Volume: 40
};
function byStage(a, b) {
return a.stage - b.stage;
}
jsfx.InitDefaultParams = InitDefaultParams;
function InitDefaultParams(params, modules) {
// setup modules
for (var i = 0; i < modules.length; i += 1) {
var M = modules[i];
var P = params[M.name] || {};
// add missing parameters
map_object(M.params, function(def, name) {
if (typeof P[name] === 'undefined') {
P[name] = def.D;
}
});
params[M.name] = P;
}
}
// Generates a stateful sound effect processor
// params can be a function that creates a parameter set
jsfx.Processor = Processor;
function Processor(params, modules) {
params = params || {};
modules = modules || jsfx.DefaultModules;
if (typeof params === 'function') {
params = params();
} else {
params = JSON.parse(JSON.stringify(params))
}
this.finished = false;
this.state = {
SampleRate: params.SampleRate || jsfx.SampleRate
};
// sort modules
modules = modules.slice();
modules.sort(byStage)
this.modules = modules;
// init missing params
InitDefaultParams(params, modules);
// setup modules
for (var i = 0; i < this.modules.length; i += 1) {
var M = this.modules[i];
this.modules[i].setup(this.state, params[M.name]);
}
}
Processor.prototype = {
//TODO: see whether this can be converted to a module
generate: function(block) {
for (var i = 0 | 0; i < block.length; i += 1) {
block[i] = 0;
}
if (this.finished) {
return;
}
var $ = this.state,
N = block.length | 0;
for (var i = 0; i < this.modules.length; i += 1) {
var M = this.modules[i];
var n = M.process($, block.subarray(0, N)) | 0;
N = Math.min(N, n);
}
if (N < block.length) {
this.finished = true;
}
for (var i = N; i < block.length; i++) {
block[i] = 0;
}
},
getSamplesLeft: function() {
var samples = 0;
for (var i = 0; i < this.state.envelopes.length; i += 1) {
samples += this.state.envelopes[i].N;
}
if (samples === 0) {
samples = 3 * this.state.SampleRate;
}
return samples;
}
};
// Frequency
jsfx.Module.Frequency = {
name: 'Frequency',
params: {
/* beautify preserve:start */
Start: { L:30, H:1800, D:440 },
Min: { L:30, H:1800, D:30 },
Max: { L:30, H:1800, D:1800 },
Slide: { L:-1, H:1, D:0 },
DeltaSlide: { L:-1, H:1, D:0 },
RepeatSpeed: { L:0, H: 3.0, D: 0 },
ChangeAmount: { L:-12, H:12, D:0 },
ChangeSpeed : { L: 0, H:1, D:0 }
/* beautify preserve:end */
},
stage: stage.PhaseSpeed,
setup: function($, P) {
var SR = $.SampleRate;
$.phaseParams = P;
$.phaseSpeed = P.Start * TAU / SR;
$.phaseSpeedMax = P.Max * TAU / SR;
$.phaseSpeedMin = P.Min * TAU / SR;
$.phaseSpeedMin = Math.min($.phaseSpeedMin, $.phaseSpeed);
$.phaseSpeedMax = Math.max($.phaseSpeedMax, $.phaseSpeed);
$.phaseSlide = 1.0 + pow(P.Slide, 3.0) * 64.0 / SR;
$.phaseDeltaSlide = pow(P.DeltaSlide, 3.0) / (SR * 1000);
$.repeatTime = 0;
$.repeatLimit = Infinity;
if (P.RepeatSpeed > 0) {
$.repeatLimit = P.RepeatSpeed * SR;
}
$.arpeggiatorTime = 0;
$.arpeggiatorLimit = P.ChangeSpeed * SR;
if (P.ChangeAmount == 0) {
$.arpeggiatorLimit = Infinity;
}
$.arpeggiatorMod = 1 + P.ChangeAmount / 12.0;
},
process: function($, block) {
var speed = +$.phaseSpeed,
min = +$.phaseSpeedMin,
max = +$.phaseSpeedMax,
slide = +$.phaseSlide,
deltaSlide = +$.phaseDeltaSlide;
var repeatTime = $.repeatTime,
repeatLimit = $.repeatLimit;
var arpTime = $.arpeggiatorTime,
arpLimit = $.arpeggiatorLimit,
arpMod = $.arpeggiatorMod;
for (var i = 0; i < block.length; i++) {
slide += deltaSlide;
speed *= slide;
speed = speed < min ? min : speed > max ? max : speed;
if (repeatTime > repeatLimit) {
this.setup($, $.phaseParams);
return i + this.process($, block.subarray(i)) - 1;
}
repeatTime++;
if (arpTime > arpLimit) {
speed *= arpMod;
arpTime = 0;
arpLimit = Infinity;
}
arpTime++;
block[i] += speed;
}
$.repeatTime = repeatTime;
$.arpeggiatorTime = arpTime;
$.arpeggiatorLimit = arpLimit;
$.phaseSpeed = speed;
$.phaseSlide = slide;
return block.length;
}
};
// Vibrato
jsfx.Module.Vibrato = {
name: 'Vibrato',
params: {
/* beautify preserve:start */
Depth: {L: 0, H:1, D:0},
DepthSlide: {L:-1, H:1, D:0},
Frequency: {L: 0.01, H:48, D:0},
FrequencySlide: {L: -1.00, H: 1, D:0}
/* beautify preserve:end */
},
stage: stage.PhaseSpeedMod,
setup: function($, P) {
var SR = $.SampleRate;
$.vibratoPhase = 0;
$.vibratoDepth = P.Depth;
$.vibratoPhaseSpeed = P.Frequency * TAU / SR;
$.vibratoPhaseSpeedSlide = 1.0 + pow(P.FrequencySlide, 3.0) * 3.0 / SR;
$.vibratoDepthSlide = P.DepthSlide / SR;
},
process: function($, block) {
var phase = +$.vibratoPhase,
depth = +$.vibratoDepth,
speed = +$.vibratoPhaseSpeed,
slide = +$.vibratoPhaseSpeedSlide,
depthSlide = +$.vibratoDepthSlide;
if ((depth == 0) && (depthSlide <= 0)) {
return block.length;
}
for (var i = 0; i < block.length; i++) {
phase += speed;
if (phase > TAU) {
phase -= TAU
};
block[i] += block[i] * sin(phase) * depth;
speed *= slide;
depth += depthSlide;
depth = clamp1(depth);
}
$.vibratoPhase = phase;
$.vibratoDepth = depth;
$.vibratoPhaseSpeed = speed;
return block.length;
}
};
// Generator
jsfx.Module.Generator = {
name: 'Generator',
params: {
/* beautify preserve:start */
// C = choose
Func: {C: jsfx.G, D:'square'},
A: {L: 0, H: 1, D: 0},
B: {L: 0, H: 1, D: 0},
ASlide: {L: -1, H: 1, D: 0},
BSlide: {L: -1, H: 1, D: 0}
/* beautify preserve:end */
},
stage: stage.Generator,
setup: function($, P) {
$.generatorPhase = 0;
if (typeof P.Func === 'string') {
$.generator = jsfx.G[P.Func];
} else {
$.generator = P.Func;
}
if (typeof $.generator === 'object') {
$.generator = $.generator.create();
}
assert(typeof $.generator === 'function', 'generator must be a function')
$.generatorA = P.A;
$.generatorASlide = P.ASlide;
$.generatorB = P.B;
$.generatorBSlide = P.BSlide;
},
process: function($, block) {
return $.generator($, block);
}
};
// Karplus Strong algorithm for string sound
var GuitarBufferSize = 1 << 16;
jsfx.Module.Guitar = {
name: 'Guitar',
params: {
/* beautify preserve:start */
A: {L:0.0, H:1.0, D: 1},
B: {L:0.0, H:1.0, D: 1},
C: {L:0.0, H:1.0, D: 1}
/* beautify preserve:end */
},
stage: stage.Generator,
setup: function($, P) {
$.guitarA = P.A;
$.guitarB = P.B;
$.guitarC = P.C;
$.guitarBuffer = createFloatArray(GuitarBufferSize);
$.guitarHead = 0;
var B = $.guitarBuffer;
for (var i = 0; i < B.length; i++) {
B[i] = Math.random() * 2 - 1;
}
},
process: function($, block) {
var BS = GuitarBufferSize,
BM = BS - 1;
var A = +$.guitarA,
B = +$.guitarB,
C = +$.guitarC;
var T = A + B + C;
var h = $.guitarHead;
var buffer = $.guitarBuffer;
for (var i = 0; i < block.length; i++) {
// buffer size
var n = (TAU / block[i]) | 0;
n = n > BS ? BS : n;
// tail
var t = ((h - n) + BS) & BM;
buffer[h] =
(buffer[(t - 0 + BS) & BM] * A +
buffer[(t - 1 + BS) & BM] * B +
buffer[(t - 2 + BS) & BM] * C) / T;
block[i] = buffer[h];
h = (h + 1) & BM;
}
$.guitarHead = h;
return block.length;
}
}
// Low/High-Pass Filter
jsfx.Module.Filter = {
name: 'Filter',
params: {
/* beautify preserve:start */
LP: {L: 0, H:1, D:1},
LPSlide: {L:-1, H:1, D:0},
LPResonance: {L: 0, H:1, D:0},
HP: {L: 0, H:1, D:0},
HPSlide: {L:-1, H:1, D:0}
/* beautify preserve:end */
},
stage: stage.SampleMod + 0,
setup: function($, P) {
$.FilterEnabled = (P.HP > EPSILON) || (P.LP < 1 - EPSILON);
$.LPEnabled = P.LP < 1 - EPSILON;
$.LP = pow(P.LP, 3.0) / 10;
$.LPSlide = 1.0 + P.LPSlide * 100 / $.SampleRate;
$.LPPos = 0;
$.LPPosSlide = 0;
$.LPDamping = 5.0 / (1.0 + pow(P.LPResonance, 2) * 20) * (0.01 + P.LP);
$.LPDamping = 1.0 - Math.min($.LPDamping, 0.8);
$.HP = pow(P.HP, 2.0) / 10;
$.HPPos = 0;
$.HPSlide = 1.0 + P.HPSlide * 100 / $.SampleRate;
},
enabled: function($) {
return $.FilterEnabled;
},
process: function($, block) {
if (!this.enabled($)) {
return block.length;
}
var lp = +$.LP;
var lpPos = +$.LPPos;
var lpPosSlide = +$.LPPosSlide;
var lpSlide = +$.LPSlide;
var lpDamping = +$.LPDamping;
var lpEnabled = +$.LPEnabled;
var hp = +$.HP;
var hpPos = +$.HPPos;
var hpSlide = +$.HPSlide;
for (var i = 0; i < block.length; i++) {
if ((hp > EPSILON) || (hp < -EPSILON)) {
hp *= hpSlide;
hp = hp < EPSILON ? EPSILON : hp > 0.1 ? 0.1 : hp;
}
var lpPos_ = lpPos;
lp *= lpSlide;
lp = lp < 0 ? lp = 0 : lp > 0.1 ? 0.1 : lp;
var sample = block[i];
if (lpEnabled) {
lpPosSlide += (sample - lpPos) * lp;
lpPosSlide *= lpDamping;
} else {
lpPos = sample;
lpPosSlide = 0;
}
lpPos += lpPosSlide;
hpPos += lpPos - lpPos_;
hpPos *= 1.0 - hp;
block[i] = hpPos;
}
$.LPPos = lpPos;
$.LPPosSlide = lpPosSlide;
$.LP = lp;
$.HP = hp;
$.HPPos = hpPos;
return block.length;
}
};
// Phaser Effect
var PhaserBufferSize = 1 << 10;
jsfx.Module.Phaser = {
name: 'Phaser',
params: {
/* beautify preserve:start */
Offset: {L:-1, H:1, D:0},
Sweep: {L:-1, H:1, D:0}
/* beautify preserve:end */
},
stage: stage.SampleMod + 1,
setup: function($, P) {
$.phaserBuffer = createFloatArray(PhaserBufferSize);
$.phaserPos = 0;
$.phaserOffset = pow(P.Offset, 2.0) * (PhaserBufferSize - 4);
$.phaserOffsetSlide = pow(P.Sweep, 3.0) * 4000 / $.SampleRate;
},
enabled: function($) {
return (abs($.phaserOffsetSlide) > EPSILON) ||
(abs($.phaserOffset) > EPSILON);
},
process: function($, block) {
if (!this.enabled($)) {
return block.length;
}
var BS = PhaserBufferSize,
BM = BS - 1;
var buffer = $.phaserBuffer,
pos = $.phaserPos | 0,
offset = +$.phaserOffset,
offsetSlide = +$.phaserOffsetSlide;
for (var i = 0; i < block.length; i++) {
offset += offsetSlide;
//TODO: check whether this is correct
if (offset < 0) {
offset = -offset;
offsetSlide = -offsetSlide;
}
if (offset > BM) {
offset = BM;
offsetSlide = 0;
}
buffer[pos] = block[i];
var p = (pos - (offset | 0) + BS) & BM;
block[i] += buffer[p];
pos = ((pos + 1) & BM) | 0;
}
$.phaserPos = pos;
$.phaserOffset = offset;
return block.length;
}
};
// Volume dynamic control with Attack-Sustain-Decay
// ATTACK | 0 - Volume + Punch
// SUSTAIN | Volume + Punch - Volume
// DECAY | Volume - 0
jsfx.Module.Volume = {
name: 'Volume',
params: {
/* beautify preserve:start */
Master: { L: 0, H: 1, D: 0.5 },
Attack: { L: 0.001, H: 1, D: 0.01 },
Sustain: { L: 0, H: 2, D: 0.3 },
Punch: { L: 0, H: 3, D: 1.0 },
Decay: { L: 0.001, H: 2, D: 1.0 }
/* beautify preserve:end */
},
stage: stage.Volume,
setup: function($, P) {
var SR = $.SampleRate;
var V = P.Master;
var VP = V * (1 + P.Punch);
$.envelopes = [
// S = start volume, E = end volume, N = duration in samples
{
S: 0,
E: V,
N: (P.Attack * SR) | 0
}, // Attack
{
S: VP,
E: V,
N: (P.Sustain * SR) | 0
}, // Sustain
{
S: V,
E: 0,
N: (P.Decay * SR) | 0
} // Decay
];
// G = volume gradient
for (var i = 0; i < $.envelopes.length; i += 1) {
var e = $.envelopes[i];
e.G = (e.E - e.S) / e.N;
}
},
process: function($, block) {
var i = 0;
while (($.envelopes.length > 0) && (i < block.length)) {
var E = $.envelopes[0];
var vol = E.S,
grad = E.G;
var N = Math.min(block.length - i, E.N) | 0;
var end = (i + N) | 0;
for (; i < end; i += 1) {
block[i] *= vol;
vol += grad;
vol = clamp(vol, 0, 10);
}
E.S = vol;
E.N -= N;
if (E.N <= 0) {
$.envelopes.shift();
}
}
return i;
}
};
// PRESETS
jsfx.DefaultModules = [
jsfx.Module.Frequency,
jsfx.Module.Vibrato,
jsfx.Module.Generator,
jsfx.Module.Filter,
jsfx.Module.Phaser,
jsfx.Module.Volume
];
jsfx.DefaultModules.sort(byStage);
jsfx.EmptyParams = EmptyParams;
function EmptyParams() {
return map_object(jsfx.Module, function() {
return {}
});
}
jsfx._RemoveEmptyParams = RemoveEmptyParams;
function RemoveEmptyParams(params) {
for (var name in params) {
if (Object_keys(params[name]).length == 0) {
delete params[name];
}
}
};
jsfx.Preset = {
Reset: function() {
return EmptyParams();
},
Coin: function() {
var p = EmptyParams();
p.Frequency.Start = runif(880, 660);
p.Volume.Sustain = runif(0.1);
p.Volume.Decay = runif(0.4, 0.1);
p.Volume.Punch = runif(0.3, 0.3);
if (runif() < 0.5) {
p.Frequency.ChangeSpeed = runif(0.15, 0.1);
p.Frequency.ChangeAmount = runif(8, 4);
}
RemoveEmptyParams(p);
return p;
},
Laser: function() {
var p = EmptyParams();
p.Generator.Func = rchoose(['square', 'saw', 'sine']);
if (runif() < 0.33) {
p.Frequency.Start = runif(880, 440);
p.Frequency.Min = runif(0.1);
p.Frequency.Slide = runif(0.3, -0.8);
} else {
p.Frequency.Start = runif(1200, 440);
p.Frequency.Min = p.Frequency.Start - runif(880, 440);
if (p.Frequency.Min < 110) {
p.Frequency.Min = 110;
}
p.Frequency.Slide = runif(0.3, -1);
}
if (runif() < 0.5) {
p.Generator.A = runif(0.5);
p.Generator.ASlide = runif(0.2);
} else {
p.Generator.A = runif(0.5, 0.4);
p.Generator.ASlide = runif(0.7);
}
p.Volume.Sustain = runif(0.2, 0.1);
p.Volume.Decay = runif(0.4);
if (runif() < 0.5) {
p.Volume.Punch = runif(0.3);
}
if (runif() < 0.33) {
p.Phaser.Offset = runif(0.2);
p.Phaser.Sweep = runif(0.2);
}
if (runif() < 0.5) {
p.Filter.HP = runif(0.3);
}
RemoveEmptyParams(p);
return p;
},
Explosion: function() {
var p = EmptyParams();
p.Generator.Func = 'noise';
if (runif() < 0.5) {
p.Frequency.Start = runif(440, 40);
p.Frequency.Slide = runif(0.4, -0.1);
} else {
p.Frequency.Start = runif(1600, 220);
p.Frequency.Slide = runif(-0.2, -0.2);
}
if (runif() < 0.2) {
p.Frequency.Slide = 0;
}
if (runif() < 0.3) {
p.Frequency.RepeatSpeed = runif(0.5, 0.3);
}
p.Volume.Sustain = runif(0.3, 0.1);
p.Volume.Decay = runif(0.5);
p.Volume.Punch = runif(0.6, 0.2);
if (runif() < 0.5) {
p.Phaser.Offset = runif(0.9, -0.3);
p.Phaser.Sweep = runif(-0.3);
}
if (runif() < 0.33) {
p.Frequency.ChangeSpeed = runif(0.3, 0.6);
p.Frequency.ChangeAmount = runif(24, -12);
}
RemoveEmptyParams(p);
return p;
},
Powerup: function() {
var p = EmptyParams();
if (runif() < 0.5) {
p.Generator.Func = 'saw';
} else {
p.Generator.A = runif(0.6);
}
p.Frequency.Start = runif(220, 440);
if (runif() < 0.5) {
p.Frequency.Slide = runif(0.5, 0.2);
p.Frequency.RepeatSpeed = runif(0.4, 0.4);
} else {
p.Frequency.Slide = runif(0.2, 0.05);
if (runif() < 0.5) {
p.Vibrato.Depth = runif(0.6, 0.1);
p.Vibrato.Frequency = runif(30, 10);
}
}
p.Volume.Sustain = runif(0.4);
p.Volume.Decay = runif(0.4, 0.1);
RemoveEmptyParams(p);
return p;
},
Hit: function() {
var p = EmptyParams();
p.Generator.Func = rchoose(['square', 'saw', 'noise']);
p.Generator.A = runif(0.6);
p.Generator.ASlide = runif(1, -0.5);
p.Frequency.Start = runif(880, 220);
p.Frequency.Slide = -runif(0.4, 0.3);
p.Volume.Sustain = runif(0.1);
p.Volume.Decay = runif(0.2, 0.1);
if (runif() < 0.5) {
p.Filter.HP = runif(0.3);
}
RemoveEmptyParams(p);
return p;
},
Jump: function() {
var p = EmptyParams();
p.Generator.Func = 'square';
p.Generator.A = runif(0.6);
p.Frequency.Start = runif(330, 330);
p.Frequency.Slide = runif(0.4, 0.2);
p.Volume.Sustain = runif(0.3, 0.1);
p.Volume.Decay = runif(0.2, 0.1);
if (runif() < 0.5) {
p.Filter.HP = runif(0.3);
}
if (runif() < 0.3) {
p.Filter.LP = runif(-0.6, 1);
}
RemoveEmptyParams(p);
return p;
},
Select: function() {
var p = EmptyParams();
p.Generator.Func = rchoose(['square', 'saw']);
p.Generator.A = runif(0.6);
p.Frequency.Start = runif(660, 220);
p.Volume.Sustain = runif(0.1, 0.1);
p.Volume.Decay = runif(0.2);
p.Filter.HP = 0.2;
RemoveEmptyParams(p);
return p;
},
Lucky: function() {
var p = EmptyParams();
map_object(p, function(out, moduleName) {
var defs = jsfx.Module[moduleName].params;
map_object(defs, function(def, name) {
if (def.C) {
var values = Object_keys(def.C);
out[name] = values[(values.length * Math.random()) | 0];
} else {
out[name] = Math.random() * (def.H - def.L) + def.L;
}
});
});
p.Volume.Master = 0.4;
p.Filter = {}; // disable filter, as it usually will clip everything
RemoveEmptyParams(p);
return p;
}
};
// GENERATORS
// uniform noise
jsfx.G.unoise = newGenerator("sample = Math.random();");
// sine wave
jsfx.G.sine = newGenerator("sample = Math.sin(phase);");
// saw wave
jsfx.G.saw = newGenerator("sample = 2*(phase/TAU - ((phase/TAU + 0.5)|0));");
// triangle wave
jsfx.G.triangle = newGenerator("sample = Math.abs(4 * ((phase/TAU - 0.25)%1) - 2) - 1;");
// square wave
jsfx.G.square = newGenerator("var s = Math.sin(phase); sample = s > A ? 1.0 : s < A ? -1.0 : A;");
// simple synth
jsfx.G.synth = newGenerator("sample = Math.sin(phase) + .5*Math.sin(phase/2) + .3*Math.sin(phase/4);");
// STATEFUL
var __noiseLast = 0;
jsfx.G.noise = newGenerator("if(phase % TAU < 4){__noiseLast = Math.random() * 2 - 1;} sample = __noiseLast;");
// Karplus-Strong string
jsfx.G.string = {
create: function() {
var BS = 1 << 16;
var BM = BS - 1;
var buffer = createFloatArray(BS);
for (var i = 0; i < buffer.length; i++) {
buffer[i] = Math.random() * 2 - 1;
}
var head = 0;
return function($, block) {
var TAU = Math.PI * 2;
var A = +$.generatorA,
ASlide = +$.generatorASlide,
B = +$.generatorB,
BSlide = +$.generatorBSlide;
var buf = buffer;
for (var i = 0; i < block.length; i++) {
var phaseSpeed = block[i];
var n = (TAU / phaseSpeed) | 0;
A += ASlide;
B += BSlide;
A = A < 0 ? 0 : A > 1 ? 1 : A;
B = B < 0 ? 0 : B > 1 ? 1 : B;
var t = ((head - n) + BS) & BM;
var sample = (
buf[(t - 0 + BS) & BM] * 1 +
buf[(t - 1 + BS) & BM] * A +
buf[(t - 2 + BS) & BM] * B) / (1 + A + B);
buf[head] = sample;
block[i] = buf[head];
head = (head + 1) & BM;
}
$.generatorA = A;
$.generatorB = B;
return block.length;
}
}
};
// Generates samples using given frequency and generator
function newGenerator(line) {
return new Function("$", "block", "" +
"var TAU = Math.PI * 2;\n" +
"var sample;\n" +
"var phase = +$.generatorPhase,\n" +
" A = +$.generatorA, ASlide = +$.generatorASlide,\n" +
" B = +$.generatorB, BSlide = +$.generatorBSlide;\n" +
"\n" +
"for(var i = 0; i < block.length; i++){\n" +
" var phaseSpeed = block[i];\n" +
" phase += phaseSpeed;\n" +
" if(phase > TAU){ phase -= TAU };\n" +
" A += ASlide; B += BSlide;\n" +
" A = A < 0 ? 0 : A > 1 ? 1 : A;\n" +
" B = B < 0 ? 0 : B > 1 ? 1 : B;\n" +
line +
" block[i] = sample;\n" +
"}\n" +
"\n" +
"$.generatorPhase = phase;\n" +
"$.generatorA = A;\n" +
"$.generatorB = B;\n" +
"return block.length;\n" +
"");
}
// WAVE SUPPORT
// Creates an Wave byte array from audio data [-1.0 .. 1.0]
jsfx.CreateWave = CreateWave;
function CreateWave(data) {
if (typeof Float32Array !== "undefined") {
assert(data instanceof Float32Array, 'data must be an Float32Array');
}
var blockAlign = numChannels * bitsPerSample >> 3;
var byteRate = jsfx.SampleRate * blockAlign;
var output = createByteArray(8 + 36 + data.length * 2);
var p = 0;
// emits string to output
function S(value) {
for (var i = 0; i < value.length; i += 1) {
output[p] = value.charCodeAt(i);
p++;
}
}
// emits integer value to output
function V(value, nBytes) {
if (nBytes <= 0) {
return;
}
output[p] = value & 0xFF;
p++;
V(value >> 8, nBytes - 1);
}
/* beautify preserve:start */
S('RIFF'); V(36 + data.length * 2, 4);
S('WAVEfmt '); V(16, 4); V(1, 2);
V(numChannels, 2); V(jsfx.SampleRate, 4);
V(byteRate, 4); V(blockAlign, 2); V(bitsPerSample, 2);
S('data'); V(data.length * 2, 4);
CopyFToU8(output.subarray(p), data);
/* beautify preserve:end */
return output;
};
// Creates an Audio element from audio data [-1.0 .. 1.0]
jsfx.CreateAudio = CreateAudio;
function CreateAudio(data) {
var wave = CreateWave(data);
return new Audio('data:audio/wav;base64,' + U8ToB64(wave));
};
jsfx.DownloadAsFile = function(audio) {
assert(audio instanceof Audio, 'input must be an Audio object');
document.location.href = audio.src;
};
// HELPERS
jsfx.Util = {};
// Copies array of Floats to a Uint8Array with 16bits per sample
jsfx.Util.CopyFToU8 = CopyFToU8;
function CopyFToU8(into, floats) {
assert(into.length / 2 == floats.length,
'the target buffer must be twice as large as the iinput');
var k = 0;
for (var i = 0; i < floats.length; i++) {
var v = +floats[i];
var a = (v * 0x7FFF) | 0;
a = a < -0x8000 ? -0x8000 : 0x7FFF < a ? 0x7FFF : a;
a += a < 0 ? 0x10000 : 0;
into[k] = a & 0xFF;
k++;
into[k] = a >> 8;
k++;
}
}
// Encodes Uint8Array with base64
jsfx.Util.U8ToB64 = U8ToB64;
function U8ToB64(data) {
var CHUNK = 0x8000;
var result = '';
for (var start = 0; start < data.length; start += CHUNK) {
var end = Math.min(start + CHUNK, data.length);
result += String.fromCharCode.apply(null, data.subarray(start, end));
}
return btoa(result);
}
// uses AudioContext sampleRate or 44100;
function getDefaultSampleRate() {
if (typeof AudioContext !== 'undefined') {
return (new AudioContext()).sampleRate;
}
return 44100;
}
// for checking pre/post conditions
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
function clamp(v, min, max) {
v = +v;
min = +min;
max = +max;
if (v < min) {
return +min;
}
if (v > max) {
return +max;
}
return +v;
}
function clamp1(v) {
v = +v;
if (v < +0.0) {
return +0.0;
}
if (v > +1.0) {
return +1.0;
}
return +v;
}
function map_object(obj, fn) {
var r = {};
for (var name in obj) {
if (obj.hasOwnProperty(name)) {
r[name] = fn(obj[name], name);
}
}
return r;
}
// uniform random
function runif(scale, offset) {
var a = Math.random();
if (scale !== undefined)
a *= scale;
if (offset !== undefined)
a += offset;
return a;
}
function rchoose(gens) {
return gens[(gens.length * Math.random()) | 0];
}
function Object_keys(obj) {
var r = [];
for (var name in obj) {
r.push(name);
}
return r;
}
jsfx._createFloatArray = createFloatArray;
function createFloatArray(N) {
if (typeof Float32Array === "undefined") {
var r = new Array(N);
for (var i = 0; i < r.length; i++) {
r[i] = 0.0;
}
}
return new Float32Array(N);
}
function createByteArray(N) {
if (typeof Uint8Array === "undefined") {
var r = new Array(N);
for (var i = 0; i < r.length; i++) {
r[i] = 0 | 0;
}
}
return new Uint8Array(N);
}
return jsfx;
}));