diff --git a/dsp/fx/amp.dsp b/dsp/fx/amp.dsp new file mode 100644 index 00000000..11173bd9 --- /dev/null +++ b/dsp/fx/amp.dsp @@ -0,0 +1,209 @@ +# Amp -- two valve stages, a tone stack and a speaker cabinet. +# +# Most of an electric guitar's sound is what happens after the string, +# and `guitar.dsp' stops at the string. This is the rest of the chain, +# in the order an amplifier puts it. +# +# THE INPUT IS TIGHTENED FIRST. A high-pass at 100 Hz in front of the +# gain, because low end driven into a valve turns to mud: an amp that +# keeps the bass out of the preamp and puts it back in the tone stack +# is how a high-gain chord stays a chord. +# +# TWO STAGES, AND THE FIRST IS BIASED. A tanh is symmetric and makes +# odd harmonics only; a valve is not, because it is biased off center, +# and the even harmonics that asymmetry adds are the warmth people mean +# by "tube". So stage one is tanh of the signal plus a bias, less tanh +# of the bias alone, which is zero for silence; a low-pass at +# 6 kHz between the stages takes the fizz out before stage two clips +# it again, symmetrically. `Gain' drives both. +# +# THE TONE STACK is one state-variable filter at 700 Hz, its three +# outputs -- which sum to the input -- weighted by `Bass', `Mid' and +# `Treble'. At 5 a knob is flat and each step either side is about +# 2.4 dB, so all three at 5 is the signal unchanged. It sits after the +# gain, where an amp has it. +# +# THE CABINET is what makes it a guitar amp rather than a fuzz. A +# 12-inch speaker passes nothing much below 80 Hz -- where the box +# resonates, hence the bump -- and nothing above 5 kHz, with a peak +# where the cone breaks up just under that. Two low-passes give the +# 24 dB slope; `Cab' at 0 bypasses it for a direct, harsh box. +# +# Mono in the middle, because an amp is: the two sides are summed in. +# +# AND AN ECHO PEDAL, because a channel carries one effect and a lead +# guitar without its delay is not the sound anybody means. It hangs off +# the cabinet, left at `Delay' and right half again as late, which is +# where the stereo comes from; `Echo' at 0 is off. + +name "Amp"; +author "Misha Nasledov"; +description "A guitar amplifier: a tightened input, two valve stages, a three-band tone stack and a 12-inch cabinet."; +category "Effects"; + + @gain = 5; + @gain.widget = 1; + @gain.min = 0; + @gain.max = 10; + @gain.label = "Gain"; + + @bass = 5; + @bass.widget = 1; + @bass.min = 0; + @bass.max = 10; + @bass.label = "Bass"; + @bass.group = "Tone"; + + @mid = 5; + @mid.widget = 1; + @mid.min = 0; + @mid.max = 10; + @mid.label = "Mid"; + @mid.group = "Tone"; + + @treble = 5; + @treble.widget = 1; + @treble.min = 0; + @treble.max = 10; + @treble.label = "Treble"; + @treble.group = "Tone"; + + @bias = 0.2; + @bias.widget = 1; + @bias.min = 0; + @bias.max = 0.5; + @bias.label = "Bias"; + + @cab = 1; + @cab.widget = 1; + @cab.min = 0; + @cab.max = 1; + @cab.label = "Cab"; + + @echo = 0; + @echo.widget = 1; + @echo.min = 0; + @echo.max = 1; + @echo.label = "Echo"; + @echo.group = "Echo"; + + @delay = 375 ms; + @delay.widget = 1; + @delay.min = 20 ms; + @delay.max = 1000 ms; + @delay.label = "Delay"; + @delay.group = "Echo"; + + @feedback = 0.35; + @feedback.widget = 1; + @feedback.min = 0; + @feedback.max = 0.9; + @feedback.label = "Feedback"; + @feedback.group = "Echo"; + + @level = 1; + @level.widget = 1; + @level.min = 0; + @level.max = 4; + @level.label = "Level"; + +node ionode { + channels = 2; + + in0 = 0; + in1 = 0; + + out0 = out->out + echol->out * @echo; + out1 = out->out + echor->out * @echo; +}; + +node tight filt::svf { + in = (ionode->in0 + ionode->in1) * 0.5; + cutoff = 100; + res = 0; +}; + +node stage1 dist::saturate { + in = tight->out_high * (1 + @gain * 3) + @bias; + factor = 1; +}; + +node rest dist::saturate { + in = @bias; + factor = 1; +}; + +node couple filt::svf { + in = stage1->out - rest->out; + cutoff = 6000; + res = 0; +}; + +node stage2 dist::saturate { + in = couple->out_low; + factor = 1 + @gain * 0.5; +}; + +node stack filt::svf { + in = stage2->out; + cutoff = 700; + res = 0.2; +}; + +node tone math::add { + in0 = stack->out_low * exp2((@bass - 5) * 0.4) + + stack->out_band * exp2((@mid - 5) * 0.4); + in1 = stack->out_high * exp2((@treble - 5) * 0.4); +}; + +# The box: a resonant high-pass at 85 Hz, then two low-passes, the first +# with the cone's breakup peak on it. +node box filt::svf { + in = tone->out; + cutoff = 85; + res = 0.4; +}; + +node cone filt::svf { + in = box->out_high; + cutoff = 4200; + res = 0.5; +}; + +node cone2 filt::svf { + in = cone->out_low; + cutoff = 5000; + res = 0; +}; + +node cabmix mixer::fade { + in0 = tone->out; + in1 = cone2->out_low; + fade = @cab; +}; + +# A driven stage comes out near full scale however hard it was hit, so +# the master is divided by the gain: `Gain' adds dirt, `Level' adds +# volume, and a quiet clean channel stays as loud as it went in. +node out mixer::mul { + in0 = cabmix->out; + in1 = @level / (1 + @gain * 2); +}; + +node echol delay::echo { + in = out->out; + size = 1600 ms; + delay = @delay; + feedback = @feedback; + dry = 0; +}; + +node echor delay::echo { + in = out->out; + size = 1600 ms; + delay = @delay * 1.5; + feedback = @feedback; + dry = 0; +}; + +io ionode; diff --git a/dsp/fx/glue.dsp b/dsp/fx/glue.dsp new file mode 100644 index 00000000..06702a50 --- /dev/null +++ b/dsp/fx/glue.dsp @@ -0,0 +1,197 @@ +# Glue -- the mix bus: EQ, a slow compressor, tape, width and a ceiling. +# +# What a mastering chain does to a finished mix, as one master effect. +# None of it is dramatic and that is the point: each stage is a few dB, +# and together they are the difference between a set of channels and a +# record. +# +# THE LOW CUT comes first, because everything after it reacts to level: +# rumble the ear cannot hear still pumps a compressor. It sits at 20 Hz +# and not higher because a pad's sub-octave and a low kick live just +# above that, and this filter's slope is gentle. +# +# TWO SHELVES, `Low' and `High' in dB. Each is a state-variable filter +# whose low or high output is added back to the signal scaled by the +# gain less one, so 0 dB is the signal untouched. +# +# THE COMPRESSOR IS SLOW AND GENTLE: 2 to 1, a soft knee, 30 ms to let +# the drums' attack through and a quarter second to come back. Fast +# enough to hold the mix together, slow enough that you hear a band +# breathing and not a meter. It is keyed from the sum and applied to +# both sides, so the image does not lean when one side is louder. +# +# TAPE is a tanh scaled back down by its own drive, which is unity for +# a quiet signal and rounds the peaks of a loud one: `Tape' at 1 is +# barely there, at 3 it is a cassette deck in the red. +# +# WIDTH is mid and side: the side signal times `Width', so 1 is as +# mixed, 0 is mono and 1.3 opens the stereo of the pads and the +# reverbs without moving the kick and bass, which sit in the middle. +# +# THE CEILING is the limiter from `fx/limiter.dsp', last, so a piece +# that wears this one in place of that one keeps its peaks under +# `Ceiling' all the same. + +name "Glue"; +author "Misha Nasledov"; +description "A mix-bus chain for a piece: low cut, shelving EQ, a gentle compressor, tape saturation, width and a ceiling."; +category "Effects"; + + @lowcut = 20; + @lowcut.widget = 1; + @lowcut.min = 10; + @lowcut.max = 120; + @lowcut.label = "Low Cut (Hz)"; + @lowcut.group = "EQ"; + + @low = 1.5; + @low.widget = 1; + @low.min = -12; + @low.max = 12; + @low.label = "Low (dB)"; + @low.group = "EQ"; + + @lowf = 110; + @lowf.widget = 1; + @lowf.min = 40; + @lowf.max = 400; + @lowf.label = "Low Freq (Hz)"; + @lowf.group = "EQ"; + + @high = 2; + @high.widget = 1; + @high.min = -12; + @high.max = 12; + @high.label = "High (dB)"; + @high.group = "EQ"; + + @highf = 8000; + @highf.widget = 1; + @highf.min = 2000; + @highf.max = 16000; + @highf.label = "High Freq (Hz)"; + @highf.group = "EQ"; + + @threshold = -16; + @threshold.widget = 1; + @threshold.min = -40; + @threshold.max = 0; + @threshold.label = "Threshold (dB)"; + @threshold.group = "Compressor"; + + @ratio = 2; + @ratio.widget = 1; + @ratio.min = 1; + @ratio.max = 10; + @ratio.label = "Ratio"; + @ratio.group = "Compressor"; + + @attack = 30 ms; + @attack.widget = 1; + @attack.min = 1 ms; + @attack.max = 100 ms; + @attack.label = "Attack"; + @attack.group = "Compressor"; + + @release = 250 ms; + @release.widget = 1; + @release.min = 50 ms; + @release.max = 1000 ms; + @release.label = "Release"; + @release.group = "Compressor"; + + @makeup = 2; + @makeup.widget = 1; + @makeup.min = 0; + @makeup.max = 12; + @makeup.label = "Makeup (dB)"; + @makeup.group = "Compressor"; + + @tape = 1.2; + @tape.widget = 1; + @tape.min = 0.5; + @tape.max = 4; + @tape.label = "Tape"; + + @width = 1.2; + @width.widget = 1; + @width.min = 0; + @width.max = 2; + @width.label = "Width"; + + @ceiling = 0.9; + @ceiling.widget = 1; + @ceiling.min = 0.1; + @ceiling.max = 1; + @ceiling.label = "Ceiling"; + +node ionode { + channels = 2; + + # The engine writes these every window. See fx/echo.dsp. + in0 = 0; + in1 = 0; + + out0 = outl->out; + out1 = outr->out; +}; + +node cutl filt::svf { in = ionode->in0; cutoff = @lowcut; res = 0; }; +node cutr filt::svf { in = ionode->in1; cutoff = @lowcut; res = 0; }; + +node lowgain misc::decibel { db = @low; }; +node highgain misc::decibel { db = @high; }; + +node lshl filt::svf { in = cutl->out_high; cutoff = @lowf; res = 0; }; +node lshr filt::svf { in = cutr->out_high; cutoff = @lowf; res = 0; }; +node hshl filt::svf { in = cutl->out_high; cutoff = @highf; res = 0; }; +node hshr filt::svf { in = cutr->out_high; cutoff = @highf; res = 0; }; + +node eql math::add { + in0 = cutl->out_high + lshl->out_low * (lowgain->out - 1); + in1 = hshl->out_high * (highgain->out - 1); +}; +node eqr math::add { + in0 = cutr->out_high + lshr->out_low * (lowgain->out - 1); + in1 = hshr->out_high * (highgain->out - 1); +}; + +node det dyn::compressor { + in = (eql->out + eqr->out) * 0.5; + threshold = @threshold; + ratio = @ratio; + attack = @attack; + release = @release; + knee = 6; + makeup = 0; +}; + +node squeeze misc::decibel { + db = det->gain + @makeup; +}; + +node tapel dist::saturate { in = eql->out * squeeze->out; factor = @tape; }; +node taper dist::saturate { in = eqr->out * squeeze->out; factor = @tape; }; + +# Mid and side, the side scaled, back to left and right. +node widel math::add { + in0 = (tapel->out + taper->out) * 0.5 / @tape; + in1 = (tapel->out - taper->out) * 0.5 * @width / @tape; +}; +node wider math::add { + in0 = (tapel->out + taper->out) * 0.5 / @tape; + in1 = (taper->out - tapel->out) * 0.5 * @width / @tape; +}; + +node folll env::follower { in = widel->out; falloff = 1.5; }; +node follr env::follower { in = wider->out; falloff = 1.5; }; + +node over math::max { + in0 = max(folll->out, follr->out); + in1 = @ceiling; +}; + +node outl mixer::mul { in0 = widel->out; in1 = @ceiling / over->out; }; +node outr mixer::mul { in0 = wider->out; in1 = @ceiling / over->out; }; + +io ionode; diff --git a/dsp/fx/rotary.dsp b/dsp/fx/rotary.dsp index decc624f..9752f11f 100644 --- a/dsp/fx/rotary.dsp +++ b/dsp/fx/rotary.dsp @@ -44,7 +44,13 @@ # # `fx/chorus.dsp' is the same node doing a different job; see its head. # Put this on an organ channel -- dsp/organ0.dsp is what it was built -# for. +# for, and dsp/tonewheel.dsp is what it sounds best on. +# +# `Drive' is the cabinet's own valve amplifier pushed: a tanh before the +# crossover, scaled back down by its drive so a quiet chord comes +# through at the level it went in and a loud one growls. It acts on the +# chord rather than on each note, which is why it lives here and not in +# the organ, and at 0 the dry signal passes untouched. name "Rotary"; author "Misha Nasledov"; @@ -119,6 +125,12 @@ category "Effects"; @mix.max = 1; @mix.label = "Mix"; + @drive = 0; + @drive.widget = 1; + @drive.min = 0; + @drive.max = 10; + @drive.label = "Drive"; + node ionode { channels = 2; @@ -151,16 +163,30 @@ node drumrate math::add { in1 = drumspin->out * (@drumfast - @drumslow); }; +node satl dist::saturate { in = ionode->in0; factor = 1 + @drive; }; +node satr dist::saturate { in = ionode->in1; factor = 1 + @drive; }; + +node drivel mixer::fade { + in0 = ionode->in0; + in1 = satl->out / (1 + @drive); + fade = min(@drive, 1); +}; +node driver mixer::fade { + in0 = ionode->in1; + in1 = satr->out / (1 + @drive); + fade = min(@drive, 1); +}; + # The crossover. One filter a side, and both of its outputs used: the # high goes round with the horn and the low with the drum. node splitl filt::svf { - in = ionode->in0; + in = drivel->out; cutoff = @xover; res = 0; }; node splitr filt::svf { - in = ionode->in1; + in = driver->out; cutoff = @xover; res = 0; }; diff --git a/dsp/tonewheel.dsp b/dsp/tonewheel.dsp new file mode 100644 index 00000000..d7da74dc --- /dev/null +++ b/dsp/tonewheel.dsp @@ -0,0 +1,268 @@ +# Tonewheel -- nine drawbars, a key click, percussion and the scanner. +# +# A Hammond is additive synthesis done with gears: a sine for every +# drawbar, each at a fixed ratio to the key -- 16' an octave down, 5 1/3' +# a fifth up, 8' the note, then 4', 2 2/3', 2', 1 3/5', 1 1/3' and 1' +# at the 2nd, 3rd, 4th, 5th, 6th and 8th harmonics. The drawbars run 0 +# to 8 and each step is 3 dB, which is why `888000000' is the jazz +# sound and `888888888' is a wall: the scale is a level, not a mix. +# +# THE TOP FOLDS BACK. The highest tonewheel turns at about 5.9 kHz, so +# a footage that would land above it on a high key plays an octave +# lower instead. That is part of why a Hammond is never harsh up top, +# and it keeps the 1' bar on a high note from aliasing here. +# +# THE CLICK IS THE CONTACTS. Nine springs touch nine busbars a moment +# apart, and what comes out is a burst of every frequency at once -- +# a few milliseconds of noise, which is what `Click' sets the level of. +# +# PERCUSSION is a second or third harmonic struck and left to decay, the +# piano-like attack of the B-3 in a rock band. On the instrument it +# fires only when every key was up; voices here are independent, so it +# strikes on every note, which is what a fast passage wants anyway. +# +# THE SCANNER is a delay line swept at about seven times a second: at +# `Chorus' 1 it is all wet (vibrato), at 0.5 the dry is mixed back in +# (chorus), and the beating between the two is the C3 setting +# everybody leaves it on. `Leakage' is the wheels nobody asked for -- +# the faint other drawbars a real console bleeds. +# +# The overdrive and the rotating speaker are the channel's, not the +# voice's, because they act on the chord: put it on `fx/rotary.dsp' and +# turn up its `Drive'. + +name "Tonewheel"; +author "Misha Nasledov"; +description "A drawbar organ: nine tonewheels with foldback, key click, percussion and scanner chorus."; +category "Keys"; + + @db16 = 8; + @db16.widget = 1; + @db16.min = 0; + @db16.max = 8; + @db16.step = 1; + @db16.label = "16'"; + @db16.group = "Drawbars"; + + @db513 = 8; + @db513.widget = 1; + @db513.min = 0; + @db513.max = 8; + @db513.step = 1; + @db513.label = "5 1/3'"; + @db513.group = "Drawbars"; + + @db8 = 8; + @db8.widget = 1; + @db8.min = 0; + @db8.max = 8; + @db8.step = 1; + @db8.label = "8'"; + @db8.group = "Drawbars"; + + @db4 = 0; + @db4.widget = 1; + @db4.min = 0; + @db4.max = 8; + @db4.step = 1; + @db4.label = "4'"; + @db4.group = "Drawbars"; + + @db223 = 0; + @db223.widget = 1; + @db223.min = 0; + @db223.max = 8; + @db223.step = 1; + @db223.label = "2 2/3'"; + @db223.group = "Drawbars"; + + @db2 = 0; + @db2.widget = 1; + @db2.min = 0; + @db2.max = 8; + @db2.step = 1; + @db2.label = "2'"; + @db2.group = "Drawbars"; + + @db135 = 0; + @db135.widget = 1; + @db135.min = 0; + @db135.max = 8; + @db135.step = 1; + @db135.label = "1 3/5'"; + @db135.group = "Drawbars"; + + @db113 = 0; + @db113.widget = 1; + @db113.min = 0; + @db113.max = 8; + @db113.step = 1; + @db113.label = "1 1/3'"; + @db113.group = "Drawbars"; + + @db1 = 0; + @db1.widget = 1; + @db1.min = 0; + @db1.max = 8; + @db1.step = 1; + @db1.label = "1'"; + @db1.group = "Drawbars"; + + @perc = 0; + @perc.widget = 1; + @perc.min = 0; + @perc.max = 1; + @perc.label = "Percussion"; + @perc.group = "Percussion"; + + @percharm = 3; + @percharm.widget = 1; + @percharm.min = 2; + @percharm.max = 3; + @percharm.step = 1; + @percharm.label = "Harmonic"; + @percharm.group = "Percussion"; + + @percdecay = 250 ms; + @percdecay.widget = 1; + @percdecay.min = 50 ms; + @percdecay.max = 1500 ms; + @percdecay.label = "Decay"; + @percdecay.group = "Percussion"; + + @click = 0.3; + @click.widget = 1; + @click.min = 0; + @click.max = 1; + @click.label = "Key Click"; + + @scanner = 0.6 ms; + @scanner.widget = 1; + @scanner.min = 0 ms; + @scanner.max = 1.5 ms; + @scanner.label = "Scanner Depth"; + @scanner.group = "Scanner"; + + @vchorus = 0.5; + @vchorus.widget = 1; + @vchorus.min = 0; + @vchorus.max = 1; + @vchorus.label = "Chorus"; + @vchorus.group = "Scanner"; + + @leak = 0.004; + @leak.widget = 1; + @leak.min = 0; + @leak.max = 0.05; + @leak.label = "Leakage"; + + @r = 90 ms; + @r.widget = 1; + @r.min = 70 ms; + @r.max = 1000 ms; + @r.label = "Release"; + +node ionode { + channels = 2; + out0 = vca->out; + out1 = vca->out; + play = env->play; +}; + +node freq misc::midi2freq { + note = ionode->note; +}; + +# One wheel per drawbar. A footage over the top wheel's 5920 Hz plays an +# octave down: `clamp((x - 5920) * 1000, 0, 1)' is "x is over 5920". +node w16 osc::simple { freq = freq->out * 0.5; waveform = 0; }; +node w513 osc::simple { freq = freq->out * 1.5; waveform = 0; }; +node w8 osc::simple { freq = freq->out; waveform = 0; }; +node w4 osc::simple { + freq = freq->out * 2 / (1 + clamp((freq->out * 2 - 5920) * 1000, 0, 1)); + waveform = 0; +}; +node w223 osc::simple { + freq = freq->out * 3 / (1 + clamp((freq->out * 3 - 5920) * 1000, 0, 1)); + waveform = 0; +}; +node w2 osc::simple { + freq = freq->out * 4 / (1 + clamp((freq->out * 4 - 5920) * 1000, 0, 1)); + waveform = 0; +}; +node w135 osc::simple { + freq = freq->out * 5 / (1 + clamp((freq->out * 5 - 5920) * 1000, 0, 1)); + waveform = 0; +}; +node w113 osc::simple { + freq = freq->out * 6 / (1 + clamp((freq->out * 6 - 5920) * 1000, 0, 1)); + waveform = 0; +}; +node w1 osc::simple { + freq = freq->out * 8 / (1 + clamp((freq->out * 8 - 5920) * 1000, 0, 1)); + waveform = 0; +}; + +# 3 dB a step, and 0 is off: 10^((bar - 8) * 3 / 20), gated by min(bar, 1). +node bars math::add { + in0 = w16->out * (pow(10, (@db16 - 8) * 0.15) * min(@db16, 1) + @leak) + + w513->out * (pow(10, (@db513 - 8) * 0.15) * min(@db513, 1) + @leak) + + w8->out * (pow(10, (@db8 - 8) * 0.15) * min(@db8, 1) + @leak); + in1 = w4->out * (pow(10, (@db4 - 8) * 0.15) * min(@db4, 1) + @leak) + + w223->out * (pow(10, (@db223 - 8) * 0.15) * min(@db223, 1) + @leak) + + w2->out * (pow(10, (@db2 - 8) * 0.15) * min(@db2, 1) + @leak) + + w135->out * (pow(10, (@db135 - 8) * 0.15) * min(@db135, 1) + @leak) + + w113->out * (pow(10, (@db113 - 8) * 0.15) * min(@db113, 1) + @leak) + + w1->out * (pow(10, (@db1 - 8) * 0.15) * min(@db1, 1) + @leak); +}; + +node percwheel osc::simple { + freq = freq->out * @percharm; + waveform = 0; +}; + +node percenv env::ad { + a = 1 ms; + d = @percdecay; +}; + +node noise osc::noise { + color = 0; +}; + +node clicktone filt::svf { + in = noise->out; + cutoff = 3000; + res = 0.2; +}; + +node clickenv env::ad { + a = 0; + d = 6 ms; +}; + +node scanner delay::chorus { + in = bars->out * 0.2 + percwheel->out * percenv->out * @perc * 0.4 + + clicktone->out_band * clickenv->out * @click * 0.5; + rate = 6.9; + depth = @scanner; + delay = 2 ms; + taps = 1; + mix = @vchorus; + phase = 0; +}; + +node env env::adsr { + a = 2 ms; + d = 0; + s = 1; + r = @r; + trigger = ionode->trigger; +}; + +node vca mixer::mul { + in0 = scanner->out; + in1 = env->out; +}; + +io ionode;