Closed dy closed 5 months ago
Hi, thank you for the suggestion but that would add too much to the size of zzfx. Part of the goal with this lib is to keep it as small as possible. Not only would it increase the size but also require 6 more parameters.
However you can use something like BiquadFilterNode, ConvolverNode, or use other code to process the result of zzfx.
I actually had in mind just simple biquad formula, that's around 5-10 loc.
y = b0*x + b1*x1 + b2*x2 - a1*y1 - a2*y2;
too much wires with WAA.
I'm not sure I understand, why not use this?
https://developer.mozilla.org/en-US/docs/Web/API/BiquadFilterNode
just following the spirit of the package - least Web Audio API. Compiled WAA can end up in bigger size than the actual biquad math
I don't a good understanding of how to implement the filters here. Would be interesting in seeing what you envision the code looking like. If it was possible to do that small I'd like to include it somehow!
In a simple bpf case it's roughly this (adapted from audio-biquad):
...
const zzfx = // play sound
(
// parameters
volume = 1, randomness = .05, frequency = 220, attack = 0, sustain = 0, release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0, pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0, bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0,
+ filterType=0, filterFreq = 220, filterQ = 1
)=>
{
// init parameters
let PI2 = Math.PI*2,
sign = v => v>0?1:-1,
startSlide = slide *= 500 * PI2 / zzfxR / zzfxR,
startFrequency = frequency *= (1 + randomness*2*Math.random() - randomness)
* PI2 / zzfxR,
b=[], t=0, tm=0, i=0, j=1, r=0, c=0, s=0, f, length, buffer, source,
+ x = 0, y = 0, x2 =0, x1 = 0, y2 = 0, y1 = 0;
+ const [b0,b1,b2,a1,a2] = filters[filterType](filterFreq * 2 / zzfxR, filterQ) // filterType === 3 is BPF type (example)
// scale by sample rate
attack = attack * zzfxR + 9; // minimum attack to prevent pop
decay *= zzfxR;
sustain *= zzfxR;
release *= zzfxR;
delay *= zzfxR;
deltaSlide *= 500 * PI2 / zzfxR**3;
modulation *= PI2 / zzfxR;
pitchJump *= PI2 / zzfxR;
pitchJumpTime *= zzfxR;
repeatTime = repeatTime * zzfxR | 0;
// generate waveform
for(length = attack + decay + sustain + release + delay | 0;
i < length; b[i++] = s)
{
if (!(++c%(bitCrush*100|0))) // bit crush
{
s = shape? shape>1? shape>2? shape>3? // wave shape
Math.sin((t%PI2)**3) : // 4 noise
Math.max(Math.min(Math.tan(t),1),-1): // 3 tan
1-(2*t/PI2%2+2)%2: // 2 saw
1-4*Math.abs(Math.round(t/PI2)-t/PI2): // 1 triangle
Math.sin(t); // 0 sin
s = (repeatTime ?
1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
: 1) *
sign(s)*(Math.abs(s)**shapeCurve) * // curve 0=square, 2=pointy
volume * zzfxV * ( // envelope
i < attack ? i/attack : // attack
i < attack + decay ? // decay
1-((i-attack)/decay)*(1-sustainVolume) : // decay falloff
i < attack + decay + sustain ? // sustain
sustainVolume : // sustain volume
i < length - delay ? // release
(length - i - delay)/release * // release falloff
sustainVolume : // release volume
0); // post release
s = delay ? s/2 + (delay > i ? 0 : // delay
(i<length-delay? 1 : (length-i)/delay) * // release delay
b[i-delay|0]/2) : s; // sample delay
+ // biquad filter
+ if (filterType) x = s, y = b0*x + b1*x1 + b2*x2 - a1*y1 - a2*y2, s = y, x2 = x1, x1 = x, y2 = y1, y1 = y;
}
f = (frequency += slide += deltaSlide) * // frequency
Math.cos(modulation*tm++); // modulation
t += f - f*noise*(1 - (Math.sin(i)+1)*1e9%2); // noise
if (j && ++j > pitchJumpTime) // pitch jump
{
frequency += pitchJump; // apply pitch jump
startFrequency += pitchJump; // also apply to start
j = 0; // reset pitch jump time
}
if (repeatTime && !(++r % repeatTime)) // repeat
{
frequency = startFrequency; // reset frequency
slide = startSlide; // reset slide
j = j || 1; // reset pitch jump time
}
}
...
+ // get biquad filter params [b0, b1, b2, a1, a2]
+ const filters = [,
+ () => [], // lpf
+ () => [], // hpf
+ (f, Q, w0 = Math.PI * f, α=Math.sin(w0) / (2 * Q), k = Math.cos(w0)) => f > 0 && f < 1 ? Q > 0 ? nc(α, 0, -α, 1 + α, -2*k, 1-α) : nc(1, 0, 0, 1, 0, 0) : nc(0, 0, 0, 1, 0, 0) // bpf
+ ]
+ // normalize coeffs
+ const nc = (b0, b1, b2, a0, a1, a2, a0i=1/a0) => [b0 * a0i, b1 * a0i, b2 * a0i, a1 * a0i, a2 * a0i]
(sorry got no chance to debug yet, tested only expanded version)
That is pretty cool. Doesn't seem like too much code (assuming it works), though considering how small zzfx is, this would probably add 10-20%. Also need to figure out lpf and hpf.
I wonder if it would be simpler/smaller to just implement lpf and hpf. Then maybe it just requires a single extra parameter. If 0 there is no filter. If a positive number it does a high pass using that value. If negative it does a low pass using the absolute value as frequency.
Cool! I'd inverse though - for positive it does LP (cutoff frequency), for negative - HP (intuition like array.slice(-5)) - slices from the end. Maybe there's even shorter formulas for just LP/HP, not generic biquad.
Yeah that's what I am thinking. I will look into it. We will leave it open for now.
Will leave it here Audio EQ Cookbook, biquad-coefs
@KilledByAPixel I wonder if I should've reversed negative filter values - it doesn't exactly follows array logic, at 0 it has discontinuity so that when values go negative mb it would make more sense to cut off frequency from the end of spectrum
@dy yes i was also thinking of how to fix the continuity issue. it's not too late to change, i am still working on the big update.
what would be the end of the spectrum in this case, the Nyquist frequency?
it is all checked in now. From my testing it feels good the way you have it set. Because for low cutoff you typically want a value of around 2000hz or less, but the upper theoretical cutoff would be 10x that.
audionodes provides adsr for filtering effects: Enabling simple filters would make a big deal for recreating realistic sounds with zzfx.
wafxr has that too: https://andyhall.github.io/wafxr/