-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathsignal_crusher.jsfx
472 lines (420 loc) · 16.3 KB
/
signal_crusher.jsfx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
desc: Signal Crusher
version: 1.8.2
author: chokehold
tags: processing bit depth crusher resampling dither lofi
link: https://github.com/chkhld/jsfx/
screenshot: https://github.com/chkhld/jsfx/blob/main/assets/screenshots/signal_crusher.png
about:
# Signal Crusher
A combination of everything "retro" to degrade a signal.
Resampling and interpolation based reconstruction, with
filtering at various stages. Bit reduction from 24 down
to 0 bits, including the fitting dithering noise to go
with it.
The dithering noise can be attenuated so that it doesn't
become too overwhelming or annoying, and there's an auto-
blanking feature that will turn off the noise while no
signal is currently running through the plugin.
Note that the "Downsampled to" slider is NOT an actual
control, but just a display to let you know what sample
rate the downsampled signal is currently operating at.
It may also be worth mentioning that the downsampling
and reconstruction filters are only active while the
related section is also operative. If downsampling or
reconstruction are set to "off", the filters won't do
anything.
// ----------------------------------------------------------------------------
slider1:down=1<0,4,{Off,Repeat samples,Drop samples,Linear interpolation,Cosine interpolation}> Downsampling
slider2:dnFilt=1<0,2,{Off,Pre,Post}> Downsampling filter
slider3:up=0<0,2,{Off,Linear interpolation,Cosine interpolation}> Reconstruction
slider4:upFilt=0<0,2,{Off,Pre,Post}> Reconstruction filter
slider5:ratio=20<32,1,1> Resampling factor [SR / x]
slider6:outSR=0<0,0,0.01> Downsampled to [Hz]
slider7:bits=10<0,24,0.001> Bit reduction
slider8:dither=100<0,100,0.01> Bit dithering [%]
slider9:blank=1<0,1,{Off,On}> Auto blanking
in_pin:left input
in_pin:right input
out_pin:left output
out_pin:right output
@init
// CLAMPING i.e. hard clipping
function clamp (ceiling)
(
this = max(-ceiling, min(ceiling, this));
);
// SAMPLE RANDOMIZATION
//
// Returns a randomized sample value between [-limit,+limit]
// which can be used as basic white noise.
//
function random (limit) (rand() * 2.0 * limit - limit);
// DITHERING NOISE
//
// Creates unshaped/unfiltered white noise floor just as it
// would occur when reducing a signal's bit depth. This is
// done in a pretty simple way:
//
// - Create white noise at 0-bit level / 0 dBfs first
// - Calculate noise floor level gain at desired bits
// - Use the calculated gain to lower the noise floor
//
// Noise is just simple white noise, nothing special there.
// The gain to level the noise floor would usually be this:
//
// gain = 1 / (1 << bits)
//
// The bit shift will however cast the resulting gain level
// to Integer, meaning it will only result in full numbers,
// not fractions in between. This in return means that the
// noise floor can only sit at fixed levels and at discrete
// steps, i.e. it does not scale down smoothly but jumps.
//
// 1 << 3 = 8
// 1 << 3.1 = 8
// 1 << 3.9 = 8
// 1 << 4 = 16
//
// Since shifting a number 1 bit to the left will basically
// multiply it by two, it's possible to calculate in powers
// of two instead. This removes the restriction of the full
// numbers, i.e. it's possible to smoothly fade the dither
// noise between full bits.
//
// 2 ^ 3 = 8
// 2 ^ 3.2 = 9.18958684
// 2 ^ 3.7 = 12.99603834
// 2 ^ 4 = 16
//
// The resulting value would be used to divide 1 in order
// to get a gain factor to multiply the noise with. Since
// multiplication of some value y with a second value 1/x
// are essentially just dividing y/x, the additional step
// of the multiplication can be omitted.
//
function ditherNoise (envelope)
(
this += envelope * ditherLevel * bitsLevel * random(1);
);
// BIT REDUCTION
//
// Bits, in layman's terms, are "volume precision/range".
// The number of bits in a signal refers to just how much
// precision/range ever single sample value has available.
//
// Bit reduction means taking bits away that samples would
// formerly store their values in.
//
function bitReduce () instance () local ()
(
// If the absolute (=positive) value of this sample is
// lower than the lowest level of detail this bit depth
// can handle, then cruelly make it zero, which equals
// "losing" the sample to silence or the noise floor.
this = (abs(this) < bitsLevel) ? 0 : this;
);
// Rudimentary envelope follower used for auto-blanking.
function envSetup (msAttack, msRelease) instance (envelope, attack, release) local ()
(
attack = pow(0.01, 1.0 / ( msAttack * srate * 0.001 ));
release = pow(0.01, 1.0 / (msRelease * srate * 0.001 ));
);
function envFollow (sample) instance () local (absolute)
(
absolute = abs(sample);
this.envelope = ((absolute > this.envelope) ? this.attack : this.release) * (this.envelope - absolute) + absolute;
this.envelope;
);
// INTERPOLATION - LINEAR
//
// Takes two values and an additional "where in between"
// argument, then figures out what value would lie at
// that specified "in between" position. Pretty simple.
//
function linearInterpolation (y1, y2, mu)
(
(y1 * (1.0 - mu) + y2 * mu);
);
// INTERPOLATION - COSINE
//
// Also takes two values and figures out an "in between"
// value, but uses a bit more refined method to do so.
//
function cosineInterpolation (y1, y2, mu) local (mu2)
(
mu2 = (1 - cos(mu * $PI)) * 0.5;
(y1 * (1 - mu2) + y2 * mu2);
);
// DOWNSAMPLING PROCESS
//
// Downsampling will remove samples from a signal. Where
// there were formerly several samples, only one sample
// remains, which means the audio would get played back
// faster than before and pitched up. But it would also
// quickly run out of samples to play - and then what..?
//
// To let the downsampled audio still play back at its
// correct pitch, the removed samples are replaced with
// something different. This could be repetitions of the
// samples that are actually left in the signal, or just
// blank samples (=zeroes), or maybe they are reproduced
// with interpolation (=taking two samples and figuring
// out values at intermediate positions).
//
// Stuffing the downsampled signal with more bew samples
// will bring it back to the original sample rate again,
// but at reduced precision i.e. sounding degraded.
//
function downSample () instance (counterDS, lastStateDS, thisStateDS)
(
counterDS += 1;
// Whenever the first sample in a chunk comes in, the
// "loop" doesn't have to run through all the checks
// below, because it's "the actual sample" which will
// be played back as it is, no matter what.
//
// When a new cycle starts...
(counterDS > ratio) || (counterDS == 1) ?
(
// Update the "previous sample" memory, this will be
// used for interpolation if selected.
lastStateDS = thisStateDS;
// Update the "current sample" memory, in step 1 of
// a cycle this will just be output without change,
// but in further steps of the cycle this value may
// be used again, e.g. when repeating samples or in
// interpolation calls.
thisStateDS = this;
// Reset the chunk/loop counter to start over at 1.
counterDS = 1;
):
// However, if this sample is not the first in a chunk,
// it will be one of the "dropped" ones that needs to
// be replaced with something different.
(
// If previous samples should be repeated
(down == 1) ?
(
// Make the current sample the value that is still
// stored in the "this sample" memory from step 1.
this = thisStateDS;
);
// If intermediate samples should be dropped
(down == 2) ?
(
// Make the current sample zero
this = 0;
);
// If this sample value should be created by linear
// interpolation between the "last sample" and "this
// sample" memory values
(down == 3) ?
(
// Do just that
this = linearInterpolation(lastStateDS, thisStateDS, counterDS / ratio);
);
// If this sample value should be created by cosine
// interpolation between the "last sample" and "this
// sample" memory values
(down == 4) ?
(
// Do just that
this = cosineInterpolation(lastStateDS, thisStateDS, counterDS / ratio);
);
);
// Finally, if intermediate samples were dropped for
// downsampling, then the signal has become quieter,
// so add some make-up gain back to the signal here.
(down == 2) ? this *= 1.0 + (down == 2) / ratio;
);
// UPSAMPLING PROCESS
//
// Upsampling will take an existing signal and insert new
// sample values between the already existing samples in
// it. Since those values are not currently in the signal,
// interpolation is used to calculate intermediate samples
// by, well, guessing. Mathematically guessing, but still.
//
// This may already be happening at the downsampling stage,
// but if samples are replaced or dropped there, then this
// process will help bring some of them back, i.e. somewhat
// "reconstruct" the original signal. It will still not be
// back to normal or sound like the input, but may sound a
// little better than without reconstruction.
//
function upSample () instance (counterUS, lastStateUS, thisStateUS)
(
counterUS += 1;
// If dealing with the first sample in a chunk, which
// will be passed out without any additional processing
(counterUS > ratio) || (counterUS == 1) ?
(
// This is the "previous sample" memory and used with
// interpolation methods. At this point, the memories
// are shifted, so this will get the value of what's
// currently the "current sample" memory.
lastStateUS = thisStateUS;
// This is the "current sample" memory and used with
// interpolation methods. Since its current value is
// shifted into the "previous sample" memory, replace
// it with the value of the actually incoming sample.
thisStateUS = this;
// Reset the chunk/loop counter to start over at 1.
counterUS = 1;
):
// If any other sample position in a chunk needs to be
// processed, i.e. the ones that were formerly removed
// or altered in the downsampling process
(
// Attempt reconstructing this intermediate sample
// with the selected interpolation method.
(up == 1) ? this = linearInterpolation(lastStateUS, thisStateUS, counterUS / ratio);
(up == 2) ? this = cosineInterpolation(lastStateUS, thisStateUS, counterUS / ratio);
);
);
// Filter used in downsampling and reconstruction stages.
function bwLP (Hz, order, memOffset) instance (a, d1, d2, w0, w1, w2, stack) local (a1, a2, ro4, step, r, ar, ar2, s2, rs2)
(
a = memOffset; d1 = a+order; d2 = d1+order; w0 = d2+order; w1 = w0+order; w2 = w1+order; stack = order;
a1 = tan($PI * (Hz / srate)); a2 = sqr(a1); ro4 = 1.0 / (4.0 * order); step = 0;
while (step < order)
(
r = sin($PI * (2.0 * step + 1.0) * ro4); ar2 = 2.0 * a1 * r;
s2 = a2 + ar2 + 1.0; rs2 = 1.0 / s2; a[step] = a2 * rs2;
d1[step] = 2.0 * (1.0 - a2) * rs2; d2[step] = -(a2 - ar2 + 1.0) * rs2;
step += 1;
);
);
function bwTick (sample) instance (a, d1, d2, w0, w1, w2, stack) local (output, step)
(
output = sample; step = 0;
while (step < stack)
(
w0[step] = d1[step] * w1[step] + d2[step] * w2[step] + output;
output = a[step] * (w0[step] + 2.0 * w1[step] + w2[step]);
w2[step] = w1[step]; w1[step] = w0[step]; step += 1;
);
output;
);
// DC BLOCKING FILTER
//
// Resampling will cause aliasing, meaning frequencies above
// a certain point will start ping-pong reflecting around in
// the frequency spectum. Some frequencies might even become
// apparent in the sub 10 Hz range, worst case even 0 Hz.
//
// A 0 Hertz signal part essentially means the entire signal
// is shifted to the positive or negative by a constant value
// and such an offset is obviously not desirable, at least in
// this case, as it will further distort the signal.
//
function dcBlocker () instance (stateIn, stateOut)
(
stateOut *= 0.99988487;
stateOut += this - stateIn;
stateIn = this;
this = stateOut;
);
// Setting up the auto-blanking envelopes
evnAutoBlankL.envSetup(10, 300);
envAutoBlankR.envSetup(10, 300);
@slider
// The target sample rate after downsampling
outSR = srate / ratio;
// If sliders move, make sure the various downsampling and
// upsampling memories are reset, in order to avoid clicks.
spl0.counterDS = spl0.stateDS = spl0.counterUS = spl0.lastStateUS = spl0.thisStateUS = 0;
spl1.counterDS = spl1.stateDS = spl1.counterUS = spl1.lastStateUS = spl1.thisStateUS = 0;
// The lowest level the currently set bit precision can store.
// Anything beneath this level will be faded to silence/noise.
bitsLevel = 1.0 / (2.0 ^ bits);
// The bitsLevel variable is already the correct level for
// the noise floor of the currently set bit depth at full
// volume. This variable is used to scale the amount of the
// dithering noise that is actually added to the signal.
ditherLevel = dither * 0.01;
// Cutoff frequencies for the resampling filters. These need
// to be restricted to 20 kHz or things tend to go pop.
downFilterCutoff = min(outSR / 2, 20000);
upFilterCutoff = min(outSR / 2, 20000);
// Configuring the downsampling filters
lpDownL.bwLP(downFilterCutoff, 8, 101000);
lpDownR.bwLP(downFilterCutoff, 8, 102000);
// Configuring the upsampling filters
lpUpL.bwLP(upFilterCutoff, 8, 103000);
lpUpR.bwLP(upFilterCutoff, 8, 104000);
@sample
// First off, generate the auto-blanking envelope. If signal
// is present, make the envelope approach 1. If no signal is
// present, make the envelope approach 0. This value is used
// to lower or raise the volume of the dithering noise.
envAutoBlankL.envFollow(spl0 != 0);
envAutoBlankR.envFollow(spl1 != 0);
// If downsampling should happen
(down > 0) ?
(
// If the PRE filter is selected
(dnFilt == 1) ?
(
// Process the filter
spl0 = lpDownL.bwTick(spl0);
spl1 = lpDownR.bwTick(spl1);
);
// Do the actual downsampling (which includes upsampling
// back to project sample rate, necessarily).
spl0.downSample();
spl1.downSample();
// If the POST filter is selected
(dnFilt == 2) ?
(
// Process the filter
spl0 = lpDownL.bwTick(spl0);
spl1 = lpDownR.bwTick(spl1);
);
);
// If reconstruction should happen
(up > 0) ?
(
// If the PRE filter is selected
(upFilt == 1)?
(
// Process the filter
spl0 = lpUpL.bwTick(spl0);
spl1 = lpUpR.bwTick(spl1);
);
// Attempt reconstruction by interpolating in-between samples
spl0.upSample();
spl1.upSample();
// If the POST filter is selected
(upFilt == 2)?
(
// Process the filter
spl0 = lpUpL.bwTick(spl0);
spl1 = lpUpR.bwTick(spl1);
);
);
// If the target bit depth is set lower than 24 bits
(bits < 24) ?
(
// Do the bit depth reduction first, i.e. lose number precision
spl0.bitReduce();
spl1.bitReduce();
// Add dithering noise to the signal, levelled correctly
// for the selected bit depth. If auto-blanking is active,
// pass in the current signal envelope, otherwise just 1.
// If auto-blanking is enabled and the signal drops quiet,
// the dithering noise will also fade to silence. If auto-
// blanking is disabled, the dithering noise is constantly
// audible, even if the signal drops to silence.
spl0.ditherNoise((blank == 1) ? envAutoBlankL.envelope : 1);
spl1.ditherNoise((blank == 1) ? envAutoBlankR.envelope : 1);
);
// Run a simple high-pass filter at a very low center
// frequency (around 10-20 Hertz) to remove DC content
// which would sit below there at ~ 0 Hertz.
spl0.dcBlocker();
spl1.dcBlocker();
// Finally, just because, do hard clipping on the outputs
// to guarantee that no sample beyond -/+ 1.0 sneaks past.
spl0.clamp(1);
spl1.clamp(1);