Hi everyone, I have a question about SynthDefs and coding-style.
I’ve noticed that I’ve been adding a control and a GUI spec for practically everything that can be changed in my SynthDef DSP. I also include very detailed comments. I’ve developed my own GUI builder that cycles through the defined controls for each SynthDef or Ndef and creates EZSliders for every control to which I’ve added a custom spec (no spec in the SynthDef means no EZSlider). The slider order in the GUI is the same as the control order in the SynthDef. I can also define control labels in a dictionary that’s passed controlkeys with section strings to the builder. These then appear in the user interface as section labels before the control. I’ve also integrated my own parameter snapshot function, as well as automatic restore parameter values with Ctrl+. to the Pdef in case the GUI was previously opened with values that differed from the SynthDef defaults.
I did all this because I’m still relatively new to sound design (at least that’s how I feel) and believe that I might be able to explore sounds more effectively using the GUI sliders.
The problem now is that this entire process about SynthDef coding slows me down considerably and takes up a lot of time. All specs, all comments, and even the definitions of the variable names, so that the code is reasonably readable and makes sense.
Now I’m thinking about simplifying all of this drastically. So, I won’t use my GUI builder anymore; I’ll just use Ndef and Pdef GUIs for very, very few parameters—only when I really want a slider. Otherwise, I’ll just do sound design directly with code using set or pattern. And I’ll only create controls for parameter values that I’ll actually want to change from the outside later on. Maybe I still don’t quite realize where the line is between what makes sense and what doesn’t?
How do you guys handle this? Is the GUI overrated? When should you really define a control as an argument? Only when you actually need it, or in anticipation of several you might need but aren’t sure about yet? How do you handle comments?
Here’s a synthdef in my old style, and then how I might write it in a simplified way in the future.
Old Version:
(
// SynthDef: Metal-Pipe Strike Test Synthesizer - V0.1.0
SynthDef(\metalpipe, {
arg out=0, amp=1, gate=1;
// Global
var ampOut = \ampOut.kr(1, spec: [0, 2, \lin, 0.01].asSpec);
var pan = \pan.kr(0, spec: [-1, 1, \lin, 0.01].asSpec);
var cfHpGuard = \cfHpGuard.kr(20, spec: [20, 22000, \exp, 1].asSpec);
var cfLpGuard = \cfLpGuard.kr(20000, spec: [20, 22000, \exp, 1].asSpec);
// Sine-Transient: Stick / Tonal Exciter
var ampTransOut = \ampTransOut.kr(0.35, spec: [0, 2, \lin, 0.01].asSpec);
var ampTransRes = \ampTransRes.kr(0.15, spec: [0, 2, \lin, 0.01].asSpec);
var freqTrans = \freqTrans.kr(330, spec: [20, 20000, \exp, 1].asSpec);
var pitchTransHiHz = \pitchTransHiHz.kr(1000, spec: [20, 20000, \exp, 1].asSpec);
var relPitchTrans = \relPitchTrans.kr(0.015, spec: [0.001, 2, \exp, 0.001].asSpec);
var atkAmpTrans = \atkAmpTrans.kr(0.001, spec: [0.001, 1, \exp, 0.001].asSpec);
var relAmpTrans = \relAmpTrans.kr(0.05, spec: [0.01, 2, \exp, 0.01].asSpec);
// Noise-Transient: Stick / Rough Exciter
var ampNoiseOut = \ampNoiseOut.kr(0.5, spec: [0, 4, \lin, 0.01].asSpec);
var ampNoiseRes = \ampNoiseRes.kr(0.3, spec: [0, 4, \lin, 0.01].asSpec);
var mixHashNoise = \mixHashNoise.kr(0.8, spec: [0, 1, \lin, 0.01].asSpec);
var cfHpNoise = \cfHpNoise.kr(100, spec: [20, 20000, \exp, 1].asSpec);
var cfBpNoise = \cfBpNoise.kr(6500, spec: [20, 20000, \exp, 1].asSpec);
var rqBpNoise = \rqBpNoise.kr(0.9, spec: [0.02, 4, \exp, 0.01].asSpec);
var mixBpNoise = \mixBpNoise.kr(0, spec: [0, 1, \lin, 0.01].asSpec);
var driveTanhNoise = \driveTanhNoise.kr(1, spec: [0.1, 20, \exp, 0.01].asSpec);
var atkAmpNoise = \atkAmpNoise.kr(0.001, spec: [0.001, 1, \exp, 0.001].asSpec);
var relAmpNoise = \relAmpNoise.kr(0.1, spec: [0.01, 2, \exp, 0.01].asSpec);
// Noise-Tail: Metallic Dust / Air Texture
var ampTail = \ampTail.kr(0.35, spec: [0, 2, \lin, 0.01].asSpec);
var cfHpTail = \cfHpTail.kr(300, spec: [20, 20000, \exp, 1].asSpec);
var cfPeakEqTail = \cfPeakEqTail.kr(3000, spec: [20, 20000, \exp, 1].asSpec);
var rqPeakEqTail = \rqPeakEqTail.kr(0.5, spec: [0.02, 4, \exp, 0.01].asSpec);
var dbPeakEqTail = \dbPeakEqTail.kr(3, spec: [-24, 24, \lin, 0.1].asSpec);
var delayAmpTail = \delayAmpTail.kr(0.006, spec: [0, 0.1, \lin, 0.001].asSpec);
var atkAmpTail = \atkAmpTail.kr(0.012, spec: [0.001, 1, \exp, 0.001].asSpec);
var relAmpTail = \relAmpTail.kr(0.22, spec: [0.01, 2, \exp, 0.01].asSpec);
// Feedback-Resonator: Hollow Metal Body
var ampResOut = \ampResOut.kr(1.8, spec: [0, 4, \lin, 0.01].asSpec);
var ampFbRes = \ampFbRes.kr(0.98, spec: [0, 0.98, \lin, 0.01].asSpec).clip(0, 0.98);
var timeDelayResMs = \timeDelayResMs.kr(0.8, spec: [0.1, 5, \exp, 0.01].asSpec).clip(0.1, 5);
var cfHpFbRes = \cfHpFbRes.kr(150, spec: [20, 22000, \exp, 1].asSpec);
var cfLpFbRes = \cfLpFbRes.kr(2500, spec: [20, 22000, \exp, 1].asSpec);
var timeDelayMax=0.2;
var envAmpTrans, envPitchTrans, sigTrans, envAmpNoise, sigNoise, sigExc, sigStick, envAmpTail,
sigTail, sigFbIn, sigFbRes, sig;
// Trans (Attack): pitched sine transient with pitch envelope and amplitude envelope.
// Roles, flexible: tonal knock / punch / short attack body.
envAmpTrans = Env.perc(atkAmpTrans, relAmpTrans, curve: -4).ar(0, gate);
envPitchTrans = Env([pitchTransHiHz, freqTrans], [relPitchTrans], curve: -4).ar(0, gate);
sigTrans = SinOsc.ar(envPitchTrans, 0, 1) * envAmpTrans;
// Noise (Attack): HP-filtered White/Hash noise transient with optional BP mix,
// Tanh saturation and amplitude envelope.
// Roles, flexible: stick noise / rim clack / rough metal contact.
envAmpNoise = Env.perc(atkAmpNoise, relAmpNoise, curve: -4).ar(0, gate);
sigNoise = HPF.ar(WhiteNoise.ar(1).blend(Hasher.ar(Sweep.ar), mixHashNoise), cfHpNoise);
sigNoise = sigNoise.blend(BPF.ar(sigNoise, cfBpNoise, rqBpNoise), mixBpNoise);
sigNoise = (sigNoise * driveTanhNoise).tanh * envAmpNoise;
// Stick / Exciter Mix: direct attack output and separate resonator excitation.
// Roles, flexible: balances audible stick hit against the energy sent into the metal body.
sigStick = (sigTrans * ampTransOut) + (sigNoise * ampNoiseOut);
sigExc = (sigTrans * ampTransRes) + (sigNoise * ampNoiseRes);
// Tail: PeakEQ/HP-filtered WhiteNoise with slightly delayed amplitude envelope.
// Roles, flexible: metallic dust / short noisy after-ring / air texture.
envAmpTail = Env([0, 0, 1, 0], [delayAmpTail, atkAmpTail, relAmpTail], curve: -4).ar(0, gate);
sigTail = BPeakEQ.ar(WhiteNoise.ar(1), cfPeakEqTail, rqPeakEqTail, dbPeakEqTail);
sigTail = HPF.ar(sigTail, cfHpTail) * ampTail * envAmpTail;
// Feedback Resonator: short LP/HP-filtered delay feedback loop excited by the stick signal.
// Roles, flexible: hollow tube body / metallic ring / resonant pipe tone.
sigFbIn = LeakDC.ar(LocalIn.ar(1));
sigFbRes = DelayC.ar(sigExc + (sigFbIn * ampFbRes), timeDelayMax, timeDelayResMs * 0.001);
sigFbRes = HPF.ar(LPF.ar(sigFbRes, cfLpFbRes), cfHpFbRes);
LocalOut.ar(sigFbRes);
// Mix: direct stick hit, noisy tail and resonator body.
sig = Mix([sigStick, sigTail, sigFbRes * ampResOut]);
// Stereofield placement and amplitude output stage.
sig = Pan2.ar(sig, pan, amp * ampOut);
// Guard filters, cascaded for a steeper resonance-free rolloff.
sig = HPF.ar(sig, cfHpGuard); sig = HPF.ar(sig, cfHpGuard);
sig = LPF.ar(sig, cfLpGuard); sig = LPF.ar(sig, cfLpGuard);
// Output
Out.ar(out, sig);
// Free the synth when all output channels stay below -70 dB for 100 ms.
// Impulse starts the silence check immediately; .product requires silence on all channels.
FreeSelf.kr(DetectSilence.ar(sig + Impulse.ar(0), amp: -70.dbamp, time: 0.1).product);
}).add;
// Section headers for GUI titles:
~sectionsMetalPipe = IdentityDictionary[
\ampOut -> "Global",
\ampTransOut -> "Sine-Transient: Stick / Tonal Exciter",
\ampNoiseOut -> "Noise-Transient: Stick / Rough Exciter",
\ampTail -> "Noise-Tail: Metallic Dust / Air Texture",
\ampResOut -> "Feedback-Resonator: Hollow Metal Body"
];
// Custom GUI call:
~buildGui.(\metalpipe, ~sectionsMetalPipe, [17, 17]);
Pdef(\metalpipe,
Pbind(
\instrument, \metalpipe,
\dur, 1,
\amp, 1,
\pan, 0,
\sendGate, false
)
).play;
)
Simplified Version:
(
// SynthDef: Metal-Pipe Strike Test Synthesizer - V0.1.0
SynthDef(\metalpipe, {
arg out=0, amp=1, gate=1, ampOut=1, pan=0;
var envAmpTrans, envPitchTrans, sigTrans, envAmpNoise, sigNoise, sigExc, sigStick, envAmpTail,
sigTail, sigFbIn, sigFbRes, sig;
// Sine-Transient
envAmpTrans = Env.perc(0.001, 0.05, curve: -4).ar(0, gate);
envPitchTrans = Env([1000, 330], [0.015], curve: -4).ar(0, gate);
sigTrans = SinOsc.ar(envPitchTrans, 0, 1) * envAmpTrans;
// Noise-Transient
envAmpNoise = Env.perc(0.001, 0.1, curve: -4).ar(0, gate);
sigNoise = HPF.ar(WhiteNoise.ar(1).blend(Hasher.ar(Sweep.ar), 0.8), 100);
sigNoise = sigNoise.blend(BPF.ar(sigNoise, 6500, 0.9), 0);
sigNoise = (sigNoise * 1).tanh * envAmpNoise;
// Stick / Exciter Mix
sigStick = (sigTrans * 0.35) + (sigNoise * 0.5);
sigExc = (sigTrans * 0.15) + (sigNoise * 0.3);
// Noise-Tail
envAmpTail = Env([0, 0, 1, 0], [0.006, 0.012, 0.22], curve: -4).ar(0, gate);
sigTail = BPeakEQ.ar(WhiteNoise.ar(1), 3000, 0.5, 3);
sigTail = HPF.ar(sigTail, 300) * 0.35 * envAmpTail;
// Feedback-Resonator
sigFbIn = LeakDC.ar(LocalIn.ar(1));
sigFbRes = DelayC.ar(sigExc + (sigFbIn * 0.98), 0.2, 0.8 * 0.001);
sigFbRes = HPF.ar(LPF.ar(sigFbRes, 2500), 150);
LocalOut.ar(sigFbRes);
// Mix
sig = Mix([sigStick, sigTail, sigFbRes * 1.8]);
// Stereofield placement
sig = Pan2.ar(sig, pan, amp * ampOut);
// Guard filters
sig = HPF.ar(sig, 20); sig = HPF.ar(sig, 20);
sig = LPF.ar(sig, 20000); sig = LPF.ar(sig, 20000);
// Output
Out.ar(out, sig);
// Synth freeing
FreeSelf.kr(DetectSilence.ar(sig + Impulse.ar(0), amp: -70.dbamp, time: 0.1).product);
}).add;
Pdef(\metalpipe,
Pbind(
\instrument, \metalpipe,
\dur, 1,
\amp, 1,
\pan, 0,
\sendGate, false
)
).play;
)
What do you think about this direction? Does the reduced version make sense, or am I stripping things down too much? Would you keep working closer to the current more complicated style with args and specs, or move toward the reduced style where most values stay directly at their “point of use” in the SynthDef?
P.S. Feel free to use or modify the synth example if it is useful to you.
P.P.S. My English is really bad, is that okay with you guys? I even use a translator sometimes to help (not always), but that doesn’t really make it much better…
P.P.P.S. The SynthDef comments are also translated, not sure if this fits my meaning exactly everything, but it looks not bad at all.