Sound
-
Play
-
-
-To save the sound, right- or control-click the following link, select
-"Save As..." and choose a name with a ".wav" extension.
+ Play
+
+ Download:sfx.wav
- sfx.wav
+ Save As:sfx.json
+
@@ -480,36 +553,37 @@ Sound
8 bit
-
-
-
-
+🔗 permalink
+Copy code
+
-
-
-¢ 2011
-Eric Fredricksen
- (Source code)
-
-
- (Port of
- sfxr
-by DrPetter)
-
-
-
-
+
+
â–¼ Serialize
+
â–² Deserialize
+
⇧ Open Save
+
+
+
+ Copy
+ Hide ✕
+
+
diff --git a/jsfxr-pro-screenshot.png b/jsfxr-pro-screenshot.png
new file mode 100644
index 0000000..31d955f
Binary files /dev/null and b/jsfxr-pro-screenshot.png differ
diff --git a/package.json b/package.json
index 86e6b15..a594662 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,30 @@
{
"name": "jsfxr",
- "version": "0.0.1",
- "dependencies": {
- "express": "2.2.0"
- }
+ "version": "1.2.2",
+ "description": "8-bit sound effects generator based on sfxr",
+ "main": "sfxr.js",
+ "exports": [
+ "./sfxr.js",
+ "./riffwave.js"
+ ],
+ "homepage": "https://github.com/chr15m/jsfxr",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/chr15m/jsfxr.git",
+ "web": "https://github.com/chr15m/jsfxr"
+ },
+ "bugs": {
+ "url": "https://github.com/chr15m/jsfxr/issues/"
+ },
+ "keywords": ["sfxr", "jsfxr", "8-bit", "sfx", "sound effects", "synth", "8bit", "retro", "chiptune", "audio", "music", "sound", "game development"],
+ "devDependencies": {
+ "live-server": "1.2.1",
+ "tape": "5.6.1"
+ },
+ "scripts": {
+ "serve": "live-server",
+ "test": "./tests.js"
+ },
+ "files": ["sfxr.js", "riffwave.js", "UNLICENSE", "README.md", "sfxr-to-wav"],
+ "bin": {"sfxr-to-wav": "./sfxr-to-wav"}
}
diff --git a/riffwave.js b/riffwave.js
index 325a6ed..54dfaee 100644
--- a/riffwave.js
+++ b/riffwave.js
@@ -1,22 +1,19 @@
/*
- * RIFFWAVE.js v0.02 - Audio encoder for HTML5 elements.
- * Copyright (C) 2011 Pedro Ladaria
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * version 2 as published by the Free Software Foundation.
- * The full license is available at http://www.gnu.org/licenses/gpl.html
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
+ * RIFFWAVE.js v0.03 - Audio encoder for HTML5 elements.
+ * Copyleft 2011 by Pedro Ladaria
*
+ * Public Domain
*
* Changelog:
*
* 0.01 - First release
* 0.02 - New faster base64 encoding
+ * 0.03 - Support for 16bit samples
+ *
+ * Notes:
+ *
+ * 8 bit data is unsigned: 0..255
+ * 16 bit data is signed: -32,768..32,767
*
*/
@@ -64,7 +61,7 @@ FastBase64.Init();
var RIFFWAVE = function(data) {
- this.data = []; // Byte array containing audio samples
+ this.data = []; // Array containing audio samples
this.wav = []; // Array containing the generated wave file
this.dataURI = ''; // http://en.wikipedia.org/wiki/Data_URI_scheme
@@ -75,18 +72,33 @@ var RIFFWAVE = function(data) {
subChunk1Id : [0x66,0x6d,0x74,0x20], // 12 4 "fmt " = 0x666d7420
subChunk1Size: 16, // 16 4 16 for PCM
audioFormat : 1, // 20 2 PCM = 1
- numChannels : 1, // 22 2 Mono = 1, Stereo = 2, etc.
- sampleRate : 8000, // 24 4 8000, 44100, etc
+ numChannels : 1, // 22 2 Mono = 1, Stereo = 2...
+ sampleRate : 8000, // 24 4 8000, 44100...
byteRate : 0, // 28 4 SampleRate*NumChannels*BitsPerSample/8
blockAlign : 0, // 32 2 NumChannels*BitsPerSample/8
- bitsPerSample: 8, // 34 2 8 bits = 8, 16 bits = 16, etc...
+ bitsPerSample: 8, // 34 2 8 bits = 8, 16 bits = 16
subChunk2Id : [0x64,0x61,0x74,0x61], // 36 4 "data" = 0x64617461
subChunk2Size: 0 // 40 4 data size = NumSamples*NumChannels*BitsPerSample/8
};
- function u32ToArray(i) { return [i&0xFF, (i>>8)&0xFF, (i>>16)&0xFF, (i>>24)&0xFF]; }
-
- function u16ToArray(i) { return [i&0xFF, (i>>8)&0xFF]; }
+ function u32ToArray(i) {
+ return [i&0xFF, (i>>8)&0xFF, (i>>16)&0xFF, (i>>24)&0xFF];
+ }
+
+ function u16ToArray(i) {
+ return [i&0xFF, (i>>8)&0xFF];
+ }
+
+ function split16bitArray(data) {
+ var r = [];
+ var j = 0;
+ var len = data.length;
+ for (var i=0; i>8) & 0xFF;
+ }
+ return r;
+ }
this.Make = function(data) {
if (data instanceof Array) this.data = data;
@@ -117,6 +129,24 @@ var RIFFWAVE = function(data) {
}; // end RIFFWAVE
-
-if (typeof exports != 'undefined') // For node.js
- exports.RIFFWAVE = RIFFWAVE;
+(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([], function(){
+ return (root.RIFFWAVE = factory());
+ });
+ } 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
+ module.exports = (root.RIFFWAVE = factory());
+ } else {
+ root.RIFFWAVE = factory();
+ }
+}(this, function() {
+ // module code here....
+ return RIFFWAVE;
+}));
diff --git a/screenshot.png b/screenshot.png
new file mode 100644
index 0000000..2781389
Binary files /dev/null and b/screenshot.png differ
diff --git a/sfxr-to-wav b/sfxr-to-wav
new file mode 100755
index 0000000..7e3f718
--- /dev/null
+++ b/sfxr-to-wav
@@ -0,0 +1,50 @@
+#!/usr/bin/env node
+
+sfxr = require('./sfxr.js');
+var fs = require('fs');
+
+// defaults
+var outfile = "sfxr-sound.wav";
+
+// set up initial sound parameters object
+var parameters = new sfxr.Params();
+parameters.sound_vol = 0.25;
+parameters.sample_rate = 44100;
+parameters.sample_size = 8;
+
+// parse arguments
+for (a=2; a 0)
- this.frequencyMin = OVERSAMPLING * 441 * (sqr(ps.p_freq_limit) + 0.001);
- else
- this.frequencyMin = 0;
- this.enableFrequencyCutoff = (ps.p_freq_limit > 0);
- this.frequencySlide = 44100 * log(1 - cube(ps.p_freq_ramp) / 100, 0.5);
- this.frequencySlideSlide = -cube(ps.p_freq_dramp) / 1000000 *
- 44100 * pow(2, 44101/44100);
-
- this.vibratoRate = 44100 * 10 / 64 * sqr(ps.p_vib_speed) / 100;
- this.vibratoDepth = ps.p_vib_strength / 2;
-
- this.arpeggioFactor = 1 / ((ps.p_arp_mod >= 0) ?
- 1 - sqr(ps.p_arp_mod) * 0.9 :
- 1 + sqr(ps.p_arp_mod) * 10);
- this.arpeggioDelay = ((ps.p_arp_speed === 1) ? 0 :
- Math.floor(sqr(1 - ps.p_arp_speed) * 20000 + 32) / 44100);
-
- this.dutyCycle = (1 - ps.p_duty) / 2;
- this.dutyCycleSweep = OVERSAMPLING * 44100 * -ps.p_duty_ramp / 20000;
-
- this.retriggerRate = 44100 / ((ps.p_repeat_speed === 0) ? 0 :
- Math.floor(sqr(1 - ps.p_repeat_speed) * 20000) + 32);
-
- this.flangerOffset = sign(ps.p_pha_offset) *
- sqr(ps.p_pha_offset) * 1020 / 44100;
- this.flangerSweep = sign(ps.p_pha_ramp) * sqr(ps.p_pha_ramp);
-
- this.enableLowPassFilter = (ps.p_lpf_freq != 1);
- function flurp(x) { return x / (1-x) }
- this.lowPassFrequency = ps.p_lpf_freq === 1 ? 44100 :
- Math.round(OVERSAMPLING * 44100 * flurp(cube(ps.p_lpf_freq) / 10));
- this.lowPassSweep = pow(1 + ps.p_lpf_ramp / 10000, 44100);
- this.lowPassResonance = 1 - (5 / (1 + sqr(ps.p_lpf_resonance) * 20)) / 9;
-
- this.highPassFrequency = Math.round(OVERSAMPLING * 44100 *
- flurp(sqr(ps.p_hpf_freq) / 10));
- this.highPassSweep = pow(1 + ps.p_hpf_ramp * 0.0003, 44100);
-
- this.gain = 10 * log(sqr(Math.exp(ps.sound_vol) - 1), 10);
-
- this.sampleRate = ps.sample_rate;
- this.sampleSize = ps.sample_size;
-
- return this;
-}
+/*** Core data structure ***/
// Sound generation parameters are on [0,1] unless noted SIGNED & thus
// on [-1,1]
@@ -181,6 +63,13 @@ function Params() {
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;
@@ -194,12 +83,120 @@ 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);
@@ -212,21 +209,6 @@ Params.prototype.pickupCoin = function () {
return this;
}
-
-Knobs.prototype.pickupCoin = function () {
- this.frequency = rndr(568, 2861);
- this.attack = 0;
- this.sustain = frnd(0.227);
- this.decay = rndr(0.227, 0.567);
- this.punch = rndr(0.3, 0.6);
- if (rnd(1)) {
- this.arpeggioFactor = rndr(1.037, 1.479);
- this.arpeggioDelay = rndr(0.042, 0.114);
- }
- return this;
-}
-
-
Params.prototype.laserShoot = function () {
this.wave_type = rnd(2);
if(this.wave_type === SINE && rnd(1))
@@ -265,45 +247,6 @@ Params.prototype.laserShoot = function () {
return this;
}
-
-Knobs.prototype.laserShoot = function () {
- this.shape = rnd(2);
- if(this.shape === SINE && rnd(1))
- this.shape = rnd(1);
- if (rnd(2) === 0) {
- this.frequency = rndr(321, 2861);
- this.frequencyMin = frnd(38.8);
- this.frequencySlide = rndr(-27.3, -174.5);
- } else {
- this.frequency = rndr(321, 3532);
- this.frequencyMin = rndr(144, 2/3 * this.frequency);
- this.frequencySlide = rndr(-2.15, -27.27);
- }
- if (this.shape === SAWTOOTH)
- this.dutyCycle = 0;
- if (rnd(1)) {
- this.dutyCycle = rndr(1/4, 1/2);
- this.dutyCycleSweep = rndr(0, -3.528);
- } else {
- this.dutyCycle = rndr(0.05, 0.3);
- this.dutyCycleSweep = frnd(12.35);
- }
- this.attack = 0;
- this.sustain = rndr(0.02, 0.2);
- this.decay = frnd(0.36);
- if (rnd(1))
- this.punch = frnd(0.3);
- if (rnd(2) === 0) {
- this.flangerOffset = frnd(0.001);
- this.flangerSweep = -frnd(0.04);
- }
- if (rnd(1))
- this.highPassFrequency = frnd(3204);
-
- return this;
-}
-
-
Params.prototype.explosion = function () {
this.wave_type = NOISE;
if (rnd(1)) {
@@ -337,40 +280,6 @@ Params.prototype.explosion = function () {
return this;
}
-
-Knobs.prototype.explosion = function () {
- this.shape = NOISE;
- if (rnd(1)) {
- this.frequency = rndr(4, 224);
- this.frequencySlide = rndr(-0.623, 17.2);
- } else {
- this.frequency = rndr(9, 2318);
- this.frequencySlide = rndr(-5.1, -40.7);
- }
- if (rnd(4) === 0)
- this.frequencySlide = 0;
- if (rnd(2) === 0)
- this.retriggerRate = rndr(4.5, 53);
- this.attack = 0;
- this.sustain = rndr(0.0227, 0.363);
- this.decay = frnd(0.567);
- if (rnd(1)) {
- this.flangerOffset = rndr(-0.0021, 0.0083);
- this.flangerSweep = -frnd(0.09);
- }
- this.punch = 0.2 + frnd(0.6);
- if (rnd(1)) {
- this.vibratoDepth = frnd(0.35);
- this.vibratoRate = frnd(24.8);
- }
- if (rnd(2) === 0) {
- this.arpeggioFactor = rndr(0.135, 2.358);
- this.arpeggioDelay = rndr(0.00526, 0.0733);
- }
- return this;
-}
-
-
Params.prototype.powerUp = function () {
if (rnd(1)) {
this.wave_type = SAWTOOTH;
@@ -396,33 +305,6 @@ Params.prototype.powerUp = function () {
return this;
}
-
-Knobs.prototype.powerUp = function () {
- if (rnd(1)) {
- this.shape = SAWTOOTH;
- this.dutyCycle = 0;
- } else {
- this.dutyCycle = rndr(0.2, 0.5);
- }
- this.frequency = rndr(145, 886);
- if (rnd(1)) {
- this.frequencySlide = rndr(0.636, 79.6);
- this.retriggerRate = rndr(6, 53);
- } else {
- this.frequencySlide = rndr(0.0795, 9.94);
- if (rnd(1)) {
- this.vibratoDepth = frnd(0.35);
- this.vibratoRate = frnd(24.8);
- }
- }
- this.attack = 0;
- this.sustain = frnd(0.363);
- this.decay = rndr(0.023, 0.57);
-
- return this;
-}
-
-
Params.prototype.hitHurt = function () {
this.wave_type = rnd(2);
if (this.wave_type === SINE)
@@ -441,26 +323,6 @@ Params.prototype.hitHurt = function () {
return this;
}
-
-Knobs.prototype.hitHurt = function () {
- this.shape = rnd(2);
- if (this.shape === SINE)
- this.shape = NOISE;
- if (this.shape === SQUARE)
- this.dutyCycle = rndr(0.2, 0.5);
- if (this.shape === SAWTOOTH)
- this.dutyCycle = 0;
- this.frequency = rndr(145, 2261);
- this.frequencySlide = rndr(-17.2, -217.9);
- this.attack = 0;
- this.sustain = frnd(0.023);
- this.decay = rndr(0.023, 0.2);
- if (rnd(1))
- this.highPassFrequency = frnd(3204);
- return this;
-}
-
-
Params.prototype.jump = function () {
this.wave_type = SQUARE;
this.p_duty = frnd(0.6);
@@ -476,23 +338,6 @@ Params.prototype.jump = function () {
return this;
}
-
-Knobs.prototype.jump = function () {
- this.shape = SQUARE;
- this.dutyCycle = rndr(0.2, 0.5);
- this.frequency = rndr(321, 1274);
- this.frequencySlide = rndr(0.64, 17.2);
- this.attack = 0;
- this.sustain = rndr(0.023, 0.36);
- this.decay = rndr(0.023, 0.2);
- if (rnd(1))
- this.highPassFrequency = frnd(3204);
- if (rnd(1))
- this.lowPassFrequency = rndr(2272, 44100);
- return this;
-}
-
-
Params.prototype.blipSelect = function () {
this.wave_type = rnd(1);
if (this.wave_type === SQUARE)
@@ -507,49 +352,55 @@ Params.prototype.blipSelect = function () {
return this;
}
-
-Knobs.prototype.blipSelect = function () {
- this.shape = rnd(1);
- if (this.shape === SQUARE)
- this.dutyCycle = rndr(0.2, 0.5);
- else
- this.dutyCycle = 0;
- this.frequency = rndr(145, 1274);
- this.attack = 0;
- this.sustain = rndr(0.023, 0.09);
- this.decay = frnd(0.09);
- this.highPassFrequency = 353;
+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.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;
+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
@@ -588,93 +439,113 @@ Params.prototype.random = function () {
return this;
}
-
-Knobs.prototype.random = function () {
- if (rnd(1))
- this.frequency = rndr(885.5, 7941.5);
- else
- this.frequency = rndr(3.5, 3532);
- this.frequencySlide = rndr(-633, 639);
- if (this.frequency > 1732 && this.frequencySlide > 5)
- this.frequencySlide = -this.frequencySlide;
- if (this.frequency < 145 && this.frequencySlide < -0.088)
- this.frequencySlide = -this.frequencySlide;
- this.frequencySlideSlide = rndr(-0.88, 0.88);
- this.dutyCycle = frnd(1);
- this.dudyCycleSweep = rndr(-17.64, 17.64);
- this.vibratoDepth = rndr(-0.5, 0.5);
- this.vibratoRate = rndr(0, 69);
- this.attack = cube(frnd(1)) * 2.26;
- this.sustain = sqr(frnd(1)) * 2.26 + 0.09;
- this.decay = frnd(1) * 2.26;
- this.punch = sqr(frnd(1)) * 0.64;
- if (this.attack + this.sustain + this.decay < 0.45) {
- this.sustain += rndr(0.5, 1.25);
- this.decay += rndr(0.5, 1.25);
- }
- this.lowPassResonance = rndr(0.444, 0.97);
- this.lowPassFrequency = frnd(39200);
- this.lowPassSweep = rndr(0.012, 82);
- if (this.lowPassFrequency < 35 && this.lowPassSweep < 0.802)
- this.lowPassSweep = 1 - this.lowPassSweep;
- this.highPassFrequency = 39200 * pow(frnd(1), 5);
- this.highPassSweep = 555718 * pow(rndr(-1, 1), 5);
- this.flangerOffset = 0.023 * cube(frnd(2) - 1);
- this.flangerSweep = cube(frnd(2) - 1);
- this.retriggerRate = frnd(1378);
- this.arpeggioDelay = frnd(1.81);
- this.arpeggioFactor = rndr(0.09, 10);
+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 ***/
-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;
-}
+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;
+ }
+};
-function SoundEffect(ps) {
- if (ps.oldParams)
- this.initFromUI(ps);
- else
- this.init(ps);
+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();
+}
-SoundEffect.prototype.initFromUI = function (ps) {
- //
- // Convert user-facing parameter values to units usable by the sound
- // generator
- //
+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;
+}
- this.initForRepeat = function() {
- this.elapsedSinceRepeat = 0;
+sfxr.b58encode = function(synthdef) {
+ var p = new Params();
+ p.fromJSON(synthdef);
+ return p.toB58();
+}
- 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;
+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]();
+}
- this.dutyCycle = 0.5 - ps.p_duty * 0.5;
- this.dutyCycleSlide = -ps.p_duty_ramp * 0.00005;
+/*** Main entry point ***/
- if (ps.p_arp_mod >= 0)
- this.arpeggioMultiplier = 1 - Math.pow(ps.p_arp_mod, 2) * .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;
+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
@@ -718,77 +589,31 @@ SoundEffect.prototype.initFromUI = function (ps) {
this.sampleRate = ps.sample_rate;
this.bitsPerChannel = ps.sample_size;
-
- for (var i in this) if (typeof this[i] !== 'function') console.log(i, this[i]);
}
+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;
-SoundEffect.prototype.init = function (ps) {
- //
- // Convert user-facing parameter values to units usable by the sound
- // generator
- //
-
- this.initForRepeat = function() {
- this.elapsedSinceRepeat = 0;
-
- this.period = OVERSAMPLING * 44100 / ps.frequency;
- this.periodMax = OVERSAMPLING * 44100 / ps.frequencyMin;
- this.enableFrequencyCutoff = (ps.frequencyMin > 0);
- this.periodMult = Math.pow(.5, ps.frequencySlide / 44100);
- this.periodMultSlide = ps.frequencySlideSlide * Math.pow(2, -44101/44100)
- / 44100;
-
- this.dutyCycle = ps.dutyCycle;
- this.dutyCycleSlide = ps.dutyCycleSweep / (OVERSAMPLING * 44100);
-
- this.arpeggioMultiplier = 1 / ps.arpeggioFactor;
- this.arpeggioTime = ps.arpeggioDelay * 44100;
- }
- this.initForRepeat(); // First time through, this is a bit of a misnomer
-
- // Waveform shape
- this.waveShape = ps.shape;
-
- // Low pass filter
- this.fltw = ps.lowPassFrequency / (OVERSAMPLING * 44100 + ps.lowPassFrequency);
- this.enableLowPassFilter = ps.lowPassFrequency < 44100;
- this.fltw_d = Math.pow(ps.lowPassSweep, 1/44100);
- this.fltdmp = (1 - ps.lowPassResonance) * 9 * (.01 + this.fltw);
-
- // High pass filter
- this.flthp = ps.highPassFrequency / (OVERSAMPLING * 44100 + ps.highPassFrequency);
- this.flthp_d = Math.pow(ps.highPassSweep, 1/44100);
-
- // Vibrato
- this.vibratoSpeed = ps.vibratoRate * 64 / 44100 / 10;
- this.vibratoAmplitude = ps.vibratoDepth;
-
- // Envelope
- this.envelopeLength = [
- Math.floor(ps.attack * 44100),
- Math.floor(ps.sustain * 44100),
- Math.floor(ps.decay * 44100)
- ];
- this.envelopePunch = ps.punch;
-
- // Flanger
- this.flangerOffset = ps.flangerOffset * 44100;
- this.flangerOffsetSlide = ps.flangerSweep;
-
- // Repeat
- this.repeatTime = ps.retriggerRate ? 1 / (44100 * ps.retriggerRate) : 0;
-
- // Gain
- this.gain = Math.sqrt(Math.pow(10, ps.gain/10));
+ this.dutyCycle = 0.5 - ps.p_duty * 0.5;
+ this.dutyCycleSlide = -ps.p_duty_ramp * 0.00005;
- this.sampleRate = ps.sampleRate;
- this.bitsPerChannel = ps.sampleSize;
+ if (ps.p_arp_mod >= 0)
+ this.arpeggioMultiplier = 1 - Math.pow(ps.p_arp_mod, 2) * .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.generate = function () {
+SoundEffect.prototype.getRawBuffer = function () {
var fltp = 0;
var fltdp = 0;
var fltphp = 0;
@@ -811,6 +636,7 @@ SoundEffect.prototype.generate = function () {
var num_clipped = 0;
var buffer = [];
+ var normalized = [];
var sample_sum = 0;
var num_summed = 0;
@@ -953,6 +779,9 @@ SoundEffect.prototype.generate = function () {
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);
@@ -978,46 +807,324 @@ SoundEffect.prototype.generate = function () {
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(buffer);
- wave.clipping = num_clipped;
+ 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= 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 genners = 'pickupCoin,laserShoot,explosion,powerUp,hitHurt,jump,blipSelect,random,tone'.split(',');
-for (var i = 0; i < genners.length; ++i) {
- (function (g) {
- if (!Knobs.prototype[g])
- Knobs.prototype[g] = function () {
- return this.translate(new Params()[g]());
- }
- })(genners[i]);
+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 === .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*.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) / .11; },
-// For node.js
-if (typeof exports !== 'undefined') {
- var RIFFWAVE = require("./riffwave").RIFFWAVE;
- exports.Params = Params;
- exports.Knobs = Knobs;
- exports.SoundEffect = SoundEffect;
- exports.SQUARE = SQUARE;
- exports.SAWTOOTH = SAWTOOTH;
- exports.SINE = SINE;
- exports.NOISE = NOISE;
+ 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 === .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*.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.js");
+ 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
+ }
+ };
+}));
diff --git a/test.js b/test.js
deleted file mode 100644
index 07b9a70..0000000
--- a/test.js
+++ /dev/null
@@ -1,85 +0,0 @@
-// For testing with node.js
-
-var sys = require("util");
-
-var RIFFWAVE = require("./riffwave.js").RIFFWAVE;
-var sfxr = require("./sfxr.js");
-
-function dump(that, title) {
- if (title) console.log('\n' + title + '\n');
- for (var i in that)
- if (typeof that[i] !== 'function')
- console.log(i, that[i]);
-}
-
-function diff(a, b, title) {
- if (title) console.log('\n' + title + '\n');
- for (var i in a) {
- if (a.hasOwnProperty(i) && typeof a[i] !== 'function') {
- if (b.hasOwnProperty(i)) {
- if (a[i] !== b[i])
- console.log('%', i, a[i], b[i])
- } else
- console.log('<', i, a[i]);
- }
- }
- for (var i in b) {
- if (b.hasOwnProperty(i) && !a.hasOwnProperty(i)) {
- console.log('>', i, b[i]);
- }
- }
-}
-
-
-//var sound = new sfxr.SoundEffect((new sfxr.Params()).tone()).generate();
-/*
-var knobs = new sfxr.Knobs({
- shape: sfxr.SAWTOOTH,
- attack: 0.1,
- decay: 0.1,
- sustain: 1,
- frequency: 440,
-});
-var sound = new sfxr.SoundEffect(knobs).generate();
-*/
-
-
-/*
-console.log("\nKNOBS TONE\n")
-var a, b;
-dump(a = new sfxr.SoundEffect(new sfxr.Knobs().tone()))
-
-console.log("\nKNOBS FOR IT\n");
-dump(new sfxr.Knobs().tone());
-
-console.log("\nPARAMS TONE\n")
-dump(b = new sfxr.SoundEffect(new sfxr.Params().tone()));
-
-console.log("\nPARAMS FOR IT\n")
-dump(new sfxr.Params().tone());
-
-console.log('\nDIFF params\n');
-diff(a, b)
-*/
-
-var x = 'random';
-
-var sound = new sfxr.SoundEffect(new sfxr.Knobs()[x]()).generate();
-require("fs").writeFile("./test.wav", new Buffer(sound.wav), 'binary',
- function(err) {
- if(err) {
- sys.puts(err);
- } else {
- sys.puts("test.wav saved!");
- }
- });
-
-var p0 = new sfxr.Params()[x](0);
-var p1 = new sfxr.Params()[x](1);
-var q0 = new sfxr.Knobs().translate(p0);
-var q1 = new sfxr.Knobs().translate(p1);
-dump(p0, 'PARAMS 0');
-dump(p1, 'PARAMS 1');
-dump(q0, 'KNOBS 0');
-dump(q1, 'KNOBS 1');
-diff(q0, q1, 'DIFF KNOBS');
diff --git a/tests.js b/tests.js
new file mode 100755
index 0000000..4491940
--- /dev/null
+++ b/tests.js
@@ -0,0 +1,34 @@
+#!/usr/bin/env node
+
+var test = require("tape");
+var j = require("./sfxr");
+
+test('test converting values from sliders to domain and back again', async function (t) {
+ const keys = Object.keys(j.convert.sliders);
+ for (k in j.convert.sliders) {
+ //for (k in {"p_lpf_freq": true}) {
+ let tolerance = 0.0001;
+ // if (["p_arp_speed", "p_repeat_speed"].indexOf(k) != -1) continue;
+ // special case these because the rounding in the slider conversion destroys information
+ if (["p_arp_speed", "p_repeat_speed"].indexOf(k) != -1) tolerance = 0.004;
+ const start = j.parameters.signed.indexOf(k) == -1 ? 0 : -1;
+ const table = [];
+ for (i=start; i<=1; i+=0.01) {
+ const r1 = j.convert.sliders[k](i);
+ const r2 = j.convert.domain[k](r1);
+ const r3 = j.convert.domain_inverse[k](r2);
+ const r4 = j.convert.sliders_inverse[k](r3);
+ table[i] = {"slider": r1, "slider_inv": j.convert.sliders_inverse[k](r1), "domain": r2, "domain_inv": r3, "round_trip": r4};
+ //console.log(i, r1, j.convert.sliders_inverse[k](r1), r2, r3, r4);
+ var pass = Math.abs(i - r4) < tolerance;
+ if (!pass) {
+ console.table(table);
+ }
+ t.ok(pass, k + " at " + i + " =~ " + r4);
+ if (!pass) {
+ process.exit();
+ }
+ }
+ console.table(table);
+ }
+});
diff --git a/web.js b/web.js
deleted file mode 100644
index f7bad68..0000000
--- a/web.js
+++ /dev/null
@@ -1,78 +0,0 @@
-// Horrible web server from
-// http://thecodinghumanist.com/blog/archives/2011/5/6/serving-static-files-from-node-js
-var http = require('http');
-var fs = require('fs');
-var path = require('path');
-var sfxr = require('./sfxr');
-
-var port = process.env.PORT || 3000;
-
-var cache = {};
-
-http.createServer(function (request, response) {
-
- var filePath = '.' + request.url;
- if (filePath == './')
- filePath = './index.html';
-
- var DYNO = "./sfx.wav?";
-
- if (filePath.indexOf(DYNO) == 0) {
- var query = filePath.substring(DYNO.length);
- query = require("querystring").parse(query);
- var params = new sfxr.Params();
- for (var k in query)
- if (query.hasOwnProperty(k))
- params[k] = query[k];
- var sfx = sfxr.generate(params);
- response.writeHead(200, {
- 'Content-Type': 'audio/wav',
- 'Content-Length': sfx.wav.length
- });
- response.end(new Buffer(sfx.wav), 'binary');
- return;
- }
-
- var extname = path.extname(filePath);
- var contentType = 'text/html';
- switch (extname) {
- case '.js':
- contentType = 'text/javascript';
- break;
- case '.css':
- contentType = 'text/css';
- break;
- case '.png':
- contentType = 'image/png';
- break;
- }
-
- if (cache.hasOwnProperty(filePath)) {
- response.writeHead(200, { 'Content-Type': contentType });
- response.end(cache[filePath], 'utf-8');
- return;
- }
-
- path.exists(filePath, function(exists) {
-
- if (exists) {
- fs.readFile(filePath, function(error, content) {
- if (error) {
- response.writeHead(500);
- response.end('500 Server error');
- }
- else {
- cache[filePath] = content;
- response.writeHead(200, { 'Content-Type': contentType });
- response.end(content, 'utf-8');
- }
- });
- } else {
- response.writeHead(404);
- response.end('404 File not found');
- }
- });
-
-}).listen(port, function() {
- console.log("Listening on " + port);
-});