UNPKG

sfxr.js

Version:

8-bit sound effects generator based on sfxr

1,025 lines (1,024 loc) 35.1 kB
/** * SFXR.ts - 8-bit sound generator for HTML5 (TypeScript port) * Original: Based on the C++ sfxr by Tomas Pettersson * JavaScript port: Copyleft 2011 by Thomas Vian * TypeScript modernization: Complete rewrite with modern ES6+ features and proper typing * * Public Domain */ // Import dependencies import { RiffWave } from './riffwave'; // Wave type constants export const SQUARE = 0; export const SAWTOOTH = 1; export const SINE = 2; export const NOISE = 3; // Master volume control let masterVolume = 1; const OVERSAMPLING = 8; // Base58 encoding alphabet const b58alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; // Parameter order definition const paramsOrder = [ "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" ]; // Signed parameters list const paramsSigned = [ "p_freq_ramp", "p_freq_dramp", "p_arp_mod", "p_duty_ramp", "p_pha_offset", "p_pha_ramp", "p_lpf_ramp", "p_hpf_ramp" ]; // Helper functions const sqr = (x) => x * x; const cube = (x) => x * x * x; const sign = (x) => x < 0 ? -1 : 1; const log = (x, b) => Math.log(x) / Math.log(b); const pow = Math.pow; const frnd = (range) => Math.random() * range; const rndr = (from, to) => Math.random() * (to - from) + from; const rnd = (max) => Math.floor(Math.random() * (max + 1)); // IEEE 754 floating point conversion functions const assembleFloat = (sign, exponent, mantissa) => { return (sign << 31) | (exponent << 23) | (mantissa); }; const floatToNumber = (flt) => { if (isNaN(flt)) { return assembleFloat(0, 0xFF, 0x1337); } const sign = (flt < 0) ? 1 : 0; flt = Math.abs(flt); if (flt === 0.0) { return assembleFloat(sign, 0, 0); } const exponent = Math.floor(Math.log(flt) / Math.LN2); if (exponent > 127 || exponent < -126) { return assembleFloat(sign, 0xFF, 0); } const mantissa = flt / Math.pow(2, exponent); return assembleFloat(sign, exponent + 127, (mantissa * Math.pow(2, 23)) & 0x7FFFFF); }; const numberToFloat = (bytes) => { const sign = (bytes & 0x80000000) ? -1 : 1; const exponent = ((bytes >> 23) & 0xFF) - 127; let 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; significand /= (1 << 22); } else { significand = (significand | (1 << 23)) / (1 << 23); } return sign * significand * Math.pow(2, exponent); }; // Modernized Params class export class Params { constructor() { this.oldParams = true; this.wave_type = SQUARE; // Envelope parameters this.p_env_attack = 0; this.p_env_sustain = 0.3; this.p_env_punch = 0; this.p_env_decay = 0.4; // Frequency parameters this.p_base_freq = 0.3; this.p_freq_limit = 0; this.p_freq_ramp = 0; this.p_freq_dramp = 0; // Vibrato parameters this.p_vib_strength = 0; this.p_vib_speed = 0; // Arpeggio parameters this.p_arp_mod = 0; this.p_arp_speed = 0; // Square wave duty cycle parameters this.p_duty = 0; this.p_duty_ramp = 0; // Repeat parameters this.p_repeat_speed = 0; // Phaser parameters this.p_pha_offset = 0; this.p_pha_ramp = 0; // Filter parameters this.p_lpf_freq = 1; this.p_lpf_ramp = 0; this.p_lpf_resonance = 0; this.p_hpf_freq = 0; this.p_hpf_ramp = 0; // Sampling parameters this.sound_vol = 0.5; this.sample_rate = 44100; this.sample_size = 8; } // Base58 encoding toB58() { const convert = []; for (const p of paramsOrder) { if (p === "wave_type") { convert.push(this[p]); } else if (p.startsWith("p_")) { const val = floatToNumber(this[p]); convert.push(0xff & val); convert.push(0xff & (val >> 8)); convert.push(0xff & (val >> 16)); convert.push(0xff & (val >> 24)); } } return this.base58Encode(convert); } base58Encode(data) { const d = []; let s = ""; let i, j = 0, c, n; for (const idx in data) { i = Number(idx); j = 0; c = data[i]; s += c || s.length ^ i ? "" : "1"; while (j in d || c) { n = d[j]; n = n ? n * 256 + c : c; c = Math.floor(n / 58); d[j] = n % 58; j++; } } while (j-- > 0) { s += b58alphabet[d[j]]; } return s; } // Base58 decoding fromB58(b58encoded) { this.fromJSON(b58decode(b58encoded)); return this; } // Set parameters from JSON object fromJSON(struct) { for (const p in struct) { if (struct.hasOwnProperty(p)) { this[p] = struct[p]; } } return this; } // Preset generation methods pickupCoin() { 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; } laserShoot() { 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); } this.p_hpf_freq = frnd(0.3); return this; } explosion() { 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; } powerUp() { 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; } hitHurt() { 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; } jump() { 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; } blipSelect() { 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; } synth() { 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; } tone() { 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; } click() { 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; } random() { 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; } mutate() { const mutateParam = (value) => value + frnd(0.1) - 0.05; if (rnd(1)) this.p_base_freq = mutateParam(this.p_base_freq); if (rnd(1)) this.p_freq_ramp = mutateParam(this.p_freq_ramp); if (rnd(1)) this.p_freq_dramp = mutateParam(this.p_freq_dramp); if (rnd(1)) this.p_duty = mutateParam(this.p_duty); if (rnd(1)) this.p_duty_ramp = mutateParam(this.p_duty_ramp); if (rnd(1)) this.p_vib_strength = mutateParam(this.p_vib_strength); if (rnd(1)) this.p_vib_speed = mutateParam(this.p_vib_speed); if (rnd(1)) this.p_env_attack = mutateParam(this.p_env_attack); if (rnd(1)) this.p_env_sustain = mutateParam(this.p_env_sustain); if (rnd(1)) this.p_env_decay = mutateParam(this.p_env_decay); if (rnd(1)) this.p_env_punch = mutateParam(this.p_env_punch); if (rnd(1)) this.p_lpf_resonance = mutateParam(this.p_lpf_resonance); if (rnd(1)) this.p_lpf_freq = mutateParam(this.p_lpf_freq); if (rnd(1)) this.p_lpf_ramp = mutateParam(this.p_lpf_ramp); if (rnd(1)) this.p_hpf_freq = mutateParam(this.p_hpf_freq); if (rnd(1)) this.p_hpf_ramp = mutateParam(this.p_hpf_ramp); if (rnd(1)) this.p_pha_offset = mutateParam(this.p_pha_offset); if (rnd(1)) this.p_pha_ramp = mutateParam(this.p_pha_ramp); if (rnd(1)) this.p_repeat_speed = mutateParam(this.p_repeat_speed); if (rnd(1)) this.p_arp_speed = mutateParam(this.p_arp_speed); if (rnd(1)) this.p_arp_mod = mutateParam(this.p_arp_mod); return this; } } // Base58 decoding function const b58decode = (b58encoded) => { const decoded = base58DecodeRaw(b58encoded, b58alphabet); const result = {}; for (let pi = 0; pi < paramsOrder.length; pi++) { const p = paramsOrder[pi]; const offset = (pi - 1) * 4 + 1; if (p === "wave_type") { result[p] = decoded[0]; } else { const val = (decoded[offset] | (decoded[offset + 1] << 8) | (decoded[offset + 2] << 16) | (decoded[offset + 3] << 24)); result[p] = numberToFloat(val); } } return result; }; const base58DecodeRaw = (s, alphabet) => { const d = []; const b = []; let i, j = 0, c, n; for (i = 0; i < s.length; i++) { j = 0; c = alphabet.indexOf(s[i]); if (c < 0) throw new Error('Invalid base58 character'); if (c || b.length !== i) { // Continue processing } else { b.push(0); } while (j in d || c) { n = d[j] || 0; n = n ? n * 58 + c : c; c = n >> 8; d[j] = n % 256; j++; } } while (j-- > 0) { b.push(d[j]); } return new Uint8Array(b); }; // Modernized SoundEffect class export class SoundEffect { constructor(params) { this.elapsedSinceRepeat = 0; this.period = 0; this.periodMax = 0; this.enableFrequencyCutoff = false; this.periodMult = 0; this.periodMultSlide = 0; this.dutyCycle = 0; this.dutyCycleSlide = 0; this.arpeggioMultiplier = 0; this.arpeggioTime = 0; this.waveShape = SQUARE; // Filter variables this.fltw = 0; this.enableLowPassFilter = false; this.fltw_d = 0; this.fltdmp = 0; this.flthp = 0; this.flthp_d = 0; // Vibrato variables this.vibratoSpeed = 0; this.vibratoAmplitude = 0; // Envelope variables this.envelopeLength = []; this.envelopePunch = 0; // Phaser variables this.flangerOffset = 0; this.flangerOffsetSlide = 0; // Repeat variables this.repeatTime = 0; // Volume and sample rate this.gain = 0; this.sampleRate = 44100; this.bitsPerChannel = 8; if (typeof params === "string") { const p = new Params(); if (params.startsWith("#")) { params = params.slice(1); } params = p.fromB58(params); } this.init(params); } init(ps) { this.parameters = ps; this.initForRepeat(); // Wave shape this.waveShape = ps.wave_type; // Filters 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; // Phaser 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; } initForRepeat() { const 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; } } getRawBuffer() { let fltp = 0; let fltdp = 0; let fltphp = 0; const noiseBuffer = new Array(32); for (let i = 0; i < 32; ++i) { noiseBuffer[i] = Math.random() * 2 - 1; } let envelopeStage = 0; let envelopeElapsed = 0; let vibratoPhase = 0; let phase = 0; let ipp = 0; const flangerBuffer = new Array(1024); flangerBuffer.fill(0); let numClipped = 0; const buffer = []; const normalized = []; let sampleSum = 0; let numSummed = 0; const summands = Math.floor(44100 / this.sampleRate); for (let t = 0;; ++t) { // Repeat handling if (this.repeatTime !== 0 && ++this.elapsedSinceRepeat >= this.repeatTime) { this.initForRepeat(); } // Arpeggio handling if (this.arpeggioTime !== 0 && t >= this.arpeggioTime) { this.arpeggioTime = 0; this.period *= this.arpeggioMultiplier; } // Frequency sliding this.periodMult += this.periodMultSlide; this.period *= this.periodMult; if (this.period > this.periodMax) { this.period = this.periodMax; if (this.enableFrequencyCutoff) { break; } } // Vibrato let rfperiod = this.period; if (this.vibratoAmplitude > 0) { vibratoPhase += this.vibratoSpeed; rfperiod = this.period * (1 + Math.sin(vibratoPhase) * this.vibratoAmplitude); } let 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; } } let envVol; const envf = envelopeElapsed / this.envelopeLength[envelopeStage]; if (envelopeStage === 0) { // Attack envVol = envf; } else if (envelopeStage === 1) { // Sustain envVol = 1 + (1 - envf) * 2 * this.envelopePunch; } else { // Decay envVol = 1 - envf; } // Phaser step this.flangerOffset += this.flangerOffsetSlide; let 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 let sample = 0; for (let si = 0; si < OVERSAMPLING; ++si) { let subSample = 0; phase++; if (phase >= iperiod) { phase %= iperiod; if (this.waveShape === NOISE) { for (let i = 0; i < 32; ++i) { noiseBuffer[i] = Math.random() * 2 - 1; } } } // Base waveform const fp = phase / iperiod; if (this.waveShape === SQUARE) { if (fp < this.dutyCycle) { subSample = 0.5; } else { subSample = -0.5; } } else if (this.waveShape === SAWTOOTH) { if (fp < this.dutyCycle) { subSample = -1 + 2 * fp / this.dutyCycle; } else { subSample = 1 - 2 * (fp - this.dutyCycle) / (1 - this.dutyCycle); } } else if (this.waveShape === SINE) { subSample = Math.sin(fp * 2 * Math.PI); } else if (this.waveShape === NOISE) { subSample = noiseBuffer[Math.floor(phase * 32 / iperiod)]; } else { throw new Error(`ERROR: Bad wave type: ${this.waveShape}`); } // Low-pass filter const 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 += (subSample - fltp) * this.fltw; fltdp -= fltdp * this.fltdmp; } else { fltp = subSample; fltdp = 0; } fltp += fltdp; // High-pass filter fltphp += fltp - pp; fltphp -= fltphp * this.flthp; subSample = fltphp; // Phaser flangerBuffer[ipp & 1023] = subSample; subSample += flangerBuffer[(ipp - iphase + 1024) & 1023]; ipp = (ipp + 1) & 1023; // Final accumulation and envelope application sample += subSample * envVol; } // Accumulate samples appropriately for sample rate sampleSum += sample; if (++numSummed >= summands) { numSummed = 0; sample = sampleSum / summands; sampleSum = 0; } else { continue; } sample = sample / OVERSAMPLING * masterVolume; sample *= this.gain; // Store raw normalized float samples normalized.push(sample); if (this.bitsPerChannel === 8) { // Rescale [-1, 1) to [0, 256) sample = Math.floor((sample + 1) * 128); if (sample > 255) { sample = 255; ++numClipped; } else if (sample < 0) { sample = 0; ++numClipped; } buffer.push(sample); } else { // Rescale [-1, 1) to [-32768, 32768) sample = Math.floor(sample * (1 << 15)); if (sample >= (1 << 15)) { sample = (1 << 15) - 1; ++numClipped; } else if (sample < -(1 << 15)) { sample = -(1 << 15); ++numClipped; } buffer.push(sample & 0xFF); buffer.push((sample >> 8) & 0xFF); } } return { buffer, normalized, clipped: numClipped }; } generate() { const rendered = this.getRawBuffer(); const 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 = getAudioFunction(wave); return wave; } } // Audio context management let audioContext = null; const getAudioFunction = (wave) => { return () => { // Check for programmatic audio if (!audioContext) { if (typeof AudioContext !== 'undefined') { audioContext = new AudioContext(); } else if (typeof window.webkitAudioContext !== 'undefined') { audioContext = new window.webkitAudioContext(); } } if (audioContext && wave.buffer) { const buff = audioContext.createBuffer(1, wave.buffer.length, wave.header.sampleRate); const nowBuffering = buff.getChannelData(0); for (let i = 0; i < wave.buffer.length; i++) { nowBuffering[i] = wave.buffer[i]; } let volume = 1.0; const audioSource = { channels: [], setVolume: (v) => { volume = v; return audioSource; }, play: () => { const proc = audioContext.createBufferSource(); proc.buffer = buff; const gainNode = audioContext.createGain(); gainNode.gain.value = volume; gainNode.connect(audioContext.destination); proc.connect(gainNode); if (proc.start) { proc.start(); } else if (proc.noteOn) { proc.noteOn(0); } audioSource.channels.push(proc); return proc; } }; return audioSource; } else { const audio = new Audio(); audio.src = wave.dataURI; return audio; } }; }; // Simplified namespace functional API export const sfxr = { toBuffer: (synthdef) => { return new SoundEffect(synthdef).getRawBuffer().buffer; }, toWebAudio: (synthdef, audiocontext) => { const sfx = new SoundEffect(synthdef); const buffer = sfx.getRawBuffer().normalized; if (audiocontext) { const buff = audiocontext.createBuffer(1, buffer.length, sfx.sampleRate); const nowBuffering = buff.getChannelData(0); for (let i = 0; i < buffer.length; i++) { nowBuffering[i] = buffer[i]; } const proc = audiocontext.createBufferSource(); proc.buffer = buff; return proc; } }, toWave: (synthdef) => { return new SoundEffect(synthdef).generate(); }, toAudio: (synthdef) => { return sfxr.toWave(synthdef).getAudio(); }, play: (synthdef) => { return sfxr.toAudio(synthdef).play(); }, b58decode, b58encode: (synthdef) => { const p = new Params(); p.fromJSON(synthdef); return p.toB58(); }, generate: (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](); } }; // Slider transformation functions export const sliders = { p_env_attack: (v) => v * v * 100000.0, p_env_sustain: (v) => v * v * 100000.0, p_env_punch: (v) => v, p_env_decay: (v) => v * v * 100000.0, p_base_freq: (v) => 8 * 44100 * (v * v + 0.001) / 100, p_freq_limit: (v) => 8 * 44100 * (v * v + 0.001) / 100, p_freq_ramp: (v) => 1.0 - Math.pow(v, 3.0) * 0.01, p_freq_dramp: (v) => -Math.pow(v, 3.0) * 0.000001, p_vib_speed: (v) => Math.pow(v, 2.0) * 0.01, p_vib_strength: (v) => v * 0.5, p_arp_mod: (v) => v >= 0 ? 1.0 - Math.pow(v, 2) * 0.9 : 1.0 + Math.pow(v, 2) * 10, p_arp_speed: (v) => (v === 1.0) ? 0 : Math.floor(Math.pow(1.0 - v, 2.0) * 20000 + 32), p_duty: (v) => 0.5 - v * 0.5, p_duty_ramp: (v) => -v * 0.00005, p_repeat_speed: (v) => (v === 0) ? 0 : Math.floor(Math.pow(1 - v, 2) * 20000) + 32, p_pha_offset: (v) => (v < 0 ? -1 : 1) * Math.pow(v, 2) * 1020, p_pha_ramp: (v) => (v < 0 ? -1 : 1) * Math.pow(v, 2), p_lpf_freq: (v) => Math.pow(v, 3) * 0.1, p_lpf_ramp: (v) => 1.0 + v * 0.0001, p_lpf_resonance: (v) => 5.0 / (1.0 + Math.pow(v, 2) * 20), p_hpf_freq: (v) => Math.pow(v, 2) * 0.1, p_hpf_ramp: (v) => 1.0 + v * 0.0003, sound_vol: (v) => Math.exp(v) - 1 }; // Inverse slider transformation functions export const slidersInverse = { p_env_attack: (v) => Math.sqrt(v / 100000.0), p_env_sustain: (v) => Math.sqrt(v / 100000.0), p_env_punch: (v) => v, p_env_decay: (v) => Math.sqrt(v / 100000.0), p_base_freq: (v) => Math.sqrt(v * 100 / 8 / 44100 - 0.001), p_freq_limit: (v) => Math.sqrt(v * 100 / 8 / 44100 - 0.001), p_freq_ramp: (v) => Math.cbrt((1.0 - v) / 0.01), p_freq_dramp: (v) => Math.cbrt(v / -0.000001), p_vib_speed: (v) => Math.sqrt(v / 0.01), p_vib_strength: (v) => v / 0.5, p_arp_mod: (v) => v < 1 ? Math.sqrt((1.0 - v) / 0.9) : -Math.sqrt((v - 1.0) / 10.0), p_arp_speed: (v) => (v === 0) ? 1.0 : (1.0 - Math.sqrt((v - (v < 100 ? 30 : 32)) / 20000)), p_duty: (v) => (v - 0.5) / -0.5, p_duty_ramp: (v) => v / -0.00005, p_repeat_speed: (v) => v === 0 ? 0 : -(Math.sqrt((v - 32) / 20000) - 1.0), p_pha_offset: (v) => (v < 0 ? -1 : 1) * Math.sqrt(Math.abs(v) / 1020), p_pha_ramp: (v) => (v < 0 ? -1 : 1) * Math.sqrt(Math.abs(v)), p_lpf_freq: (v) => Math.cbrt(v / 0.1), p_lpf_ramp: (v) => (v - 1.0) / 0.0001, p_lpf_resonance: (v) => Math.sqrt((1.0 / (v / 5.0) - 1) / 20), p_hpf_freq: (v) => Math.sqrt(v / 0.1), p_hpf_ramp: (v) => (v - 1.0) / 0.0003, sound_vol: (v) => Math.log(v + 1) }; // Export constants and types export { masterVolume, paramsOrder, paramsSigned }; // Function to set master volume export const setMasterVolume = (volume) => { masterVolume = Math.max(0, Math.min(1, volume)); }; // Function to get master volume export const getMasterVolume = () => masterVolume; // Default export export default { sfxr, Params, SoundEffect, SQUARE, SAWTOOTH, SINE, NOISE, sliders, slidersInverse, setMasterVolume, getMasterVolume };