diff --git a/plugins/knee_clipper.jsfx b/plugins/knee_clipper.jsfx index abdaad6..8d5438d 100644 --- a/plugins/knee_clipper.jsfx +++ b/plugins/knee_clipper.jsfx @@ -31,7 +31,7 @@ // // While the knee is set to 0 dB, this is just a standard hard clipper, like all // the other hard clippers out there. As soon as a knee value above 0 dB is set, -// two thresholds will be set around the ceiling level, one below it, one above. +// two thresholds are created around the ceiling level, one below it, one above. // Each threshold is 1/2 the knee dB away from the ceiling, this creates a "soft // zone" around the ceiling with a start-to-end range of the set knee dB. // @@ -42,7 +42,16 @@ // to fit below the ceiling, even if the signal originally shot past the ceiling, // within the confines of the [ceiling,upper knee threshold] range. This imparts // a little bit of soft clipping saturation, and preverves the life in very loud -// transients significantly better than other, simple clipping methods. +// transients significantly better than other, more trivial clipping methods. +// +// The "super soft" option decides how the part of the signal within the knee is +// rescaled. With the "super soft" option disabled, the rescaling is more linear +// which means the transition into hard clipping occurs later but faster, so the +// signal can become slightly louder before the hard distortion kicks in, but at +// the cost of being harsher and much more immediate. With "super soft" enabled, +// rescaling inside the knee uses cosine interpolation to transition between the +// unclipped and clipped states more gradually. This sounds less harsh but comes +// at the cost of some output volume. // // Any signal exceeding the upper threshold of the knee will just be chopped off // by a regular hard clipper. @@ -63,21 +72,64 @@ // If you think clipping your mix any more would only make it worse, bypass your // clipper and give this one a try. ;) // +// In the metering section, the ceiling is displayed as a red line while no knee +// is set. Anything above the red line will be hard-clipped. As soon as there is +// a knee present, the ceiling turns orange, because it now no longer represents +// the point of hard clipping. With knee enabled, the yellow marker line left of +// the ceiling line visualizes the onset of the knee. Signal exceeding that line +// is gently compacted until it hits the upper limit of the knee range, which is +// displayed as the red marker line to the right of the ceiling. Anything beyond +// the red line will be hard-clipped, even with the knee, even with "super soft" +// mode enabled. +// +// The green bar in the metering section reflects the incoming audio signal of a +// plugin input channel. The blue bar underneath it reflects the outgoing signal +// of its related plugin output channel. As long the bars are just green or blue, +// the signal is still as pure as it ever was, and in no way clipped or filtered +// or otherwise affected. When the green bar turns yellow the signal has entered +// the knee range, so the clipper is starting to softly compact its levels. From +// hereon, soft saturation will increasingly be introduced. When one of the bars +// (green or yellow, depending on knee amount) turns red, it means the signal is +// beyond the compactable range and has to be hard-clipped. Anything that is red +// has gone for good. (This doesn't have to be bad, though. So don't let it stop +// you from pushing it some more.) +// +// The effect of the Boost slider is reflected in the metering section, since it +// directly affects how the clipping stage is driven. Changes to the Trim slider +// however are NOT reflected in the metering, primarily because it has no effect +// on the clipping stage, but also since it would make the display unnecessarily +// complex or confusing. Nothing above the ceiling leaves this plugin. And there +// are always level meters on its sides. :) +// +// Last but not least - this supports multi-channel audio. I/O and metering will +// automatically adjust to the number of channels the track you slam this on has. +// // author: chokehold // url: https://github.com/chkhld/jsfx/ // tags: processing gain amplitude clipper distortion saturation // desc: Knee Clipper -slider1:dBGain=0<-12,12,0.01>Gain [dB] -slider2:dBCeil=0<-24,0,0.01>Ceiling [dBfs] -slider3:dBKnee=12<0,24,0.1>Knee [dB] -slider4:dBTrim=0<-12,12,0.01>Trim [dB] +slider1:beSoft=1<0,1,{No,Yes}>Super soft +slider2 +slider3:dBCeil=0<-24,0,0.01>Ceiling [dBfs] +slider4:dBKnee=12<0,24,0.1>Knee [dB] +slider5 +slider6:dBGain=0<-12,12,0.01>Boost [dB] +slider7 +slider8:dBTrim=0<-12,12,0.01>Trim [dB] // By not having any in_pin and out_pin assignments, this plugin will // automatically adapt to the number of channels of the track. -@init +@init // ----------------------------------------------------------------------- + + // Convenience variable to save CPU cycles during processing + halfPi = $pi * 0.5; + + // Used to buffer input/output samples for GUI interaction + bufferIn = 10000; + bufferOut = 20000; // Converts dB values to float gain factors function dBToGain (decibels) (pow(10, decibels * 0.05)); @@ -89,18 +141,39 @@ slider4:dBTrim=0<-12,12,0.01>Trim [dB] // Converts float gain factors to dB values function gainTodB (float) (log(max(FLOAT_FLOOR, abs(float))) * M_LN10_20); + // VALUE INTERPOLATION ------------------------------------------------------- + // + // Return interpolated value at position [0,1] between + // two not necessarily related input values. + // + // Implemented after Paul Bourke and Lewis Van Winkle + // http://paulbourke.net/miscellaneous/interpolation/ + // https://codeplea.com/simple-interpolation + // + // Basic linear interpolation + // Constant speed + function linearInterpolation (value1, value2, position) + ( + (value1 * (1.0 - position) + value2 * position); + ); + // + // Cosine interpolation between two values + // Slow start, slow stop + function cosineInterpolation (value1, value2, position) + ( + linearInterpolation(value1, value2, (-cos($PI * position) * 0.5) + 0.5); + ); + // HARD CLIPPING ------------------------------------------------------------- // // Most basic "if larger than ceiling, set to ceiling" clamping // function hardClip (sample) (max(-ceiling, min(ceiling, sample))); - // The main processing function with gains, knee and clipping - function process (sample) local (dBKey, ratio, scale, reduction, gr) + // KNEE BASED CLIPPING ------------------------------------------------------- + // + function process (sample) local (dBKey, scale, reduction, gr) ( - // Apply pre-clip gain to input sample - sample *= gain; - // Only process this bit if knee is > 0 dB useKnee ? ( @@ -113,41 +186,78 @@ slider4:dBTrim=0<-12,12,0.01>Trim [dB] // How far inside the knee is the key signal [0,1] ratio = (dBKey - kneeLower) / dBKnee; - // Turn the "inside the knee" ratio into a scaling factor - scale = ratio * (ratio * 0.25); - - // Calculate appropriate amount of dB gain reduction - reduction = scale * (kneeLower - dBKey); - - // Turn gain reduction dB value into a float factor - gr = dBToGain(reduction); - - // Apply the gain reduction to the sample - sample *= gr; + // If the "super soft" option is set + beSoft ? + ( + // Scale the "how far inside" ratio into range [0,1/2π] + scaled = ratio * halfPi; + + // Run the [0,1/2π] ratio through sin, this rescales it into [0,1] + sined = sin(ratio * halfPi); // [0,1] + + // Use the now differently scaled [0,1] ratio as the position inside the + // target range of [kneeLower,ceiling] and interpolate an output value + squashed = cosineInterpolation(kneeLowerGain, ceiling, sined); + + // Output value has positive polarity, so apply input sample's polarity + sample = squashed * sign(sample); + ) + : // If regular "knee clipping" is desired + ( + // Turn the "inside the knee" ratio into a scaling factor + scale = ratio * (ratio * 0.25); + + // Calculate appropriate amount of dB gain reduction + reduction = scale * (kneeLower - dBKey); + + // Turn gain reduction dB value into a float factor + gr = dBToGain(reduction); + + // Apply the gain reduction to the sample + sample *= gr; + ); ); ); // Apply hard clipping to the sample, no matter what happened before. // Any sample inside the knee is now below the ceiling, and the ones // still above the ceiling need to be hard clipped, so this is OK. - sample = hardClip(sample); - - // Return the sample with output volume trim applied - sample * trim; + // Once clipped, return the sample back out. + hardClip(sample); ); + + // CONVENIENCE VARIABLES ----------------------------------------------------- + // + // The speed (dB per second) at which the meters return to 0 (well... -144) + falloff = dBToGain(-36 / srate); + // + // Used in @gfx to check if size has changed + lastWidth = -1; + lastHeight = -1; + // + // Various buffered dB values as float gain factors + dB_Neg48 = dBToGain(-48); + dB_Neg36 = dBToGain(-36); + dB_Neg24 = dBToGain(-24); + dB_Neg12 = dBToGain(-12); + dB_Neg06 = dBToGain(-6); + dB_Neg03 = dBToGain(-3); + dB_Zero0 = dBToGain(0); + dB_Pos03 = dBToGain(+3); + dB_Pos06 = dBToGain(+6); + dB_Pos12 = dBToGain(+12); + dB_Pos24 = dBToGain(+24); + dB_Pos36 = dBToGain(+36); + dB_Pos48 = dBToGain(+48); -@slider +@slider // --------------------------------------------------------------------- - // Turn the Gain slider's dB value into a float gain factor + // Turn the slider dB values into a float gain factors gain = dBToGain(dBGain); - - // Turn the Ceiling slider's dBfs value into a float gain factor ceiling = dBToGain(dBCeil); - - // Turn the Trim slider's dB value into a float gain factor trim = dBToGain(dBTrim); - // Flag to only process clipping knee if a knee width is set + // Flag to only process the knee during clipping if a knee width is set useKnee = (dBKnee > 0.0); // Determine how far above and below the ceiling the knee ranges @@ -155,16 +265,463 @@ slider4:dBTrim=0<-12,12,0.01>Trim [dB] kneeUpper = dBCeil + kneeWidth; kneeLower = dBCeil - kneeWidth; + // Convenience variables to save CPU cycles during per-sample processing + kneeLowerGain = dBToGain(kneeLower); + kneeUpperGain = dBToGain(kneeUpper); + // + dB_InvCeil = dBToGain(-dBCeil); + dB_InvKneeLower = dBToGain(-kneeLower); + dB_InvKneeUpper = dBToGain(-kneeUpper); + // + // Transparency values; I got tired of changing repetitively + alphaMeterBars = 0.75; + alphaMarkerHard = 0.8; + alphaMarkerSoft = 0.4; + alphaMarkerText = 0.8; + +@gfx 0 150 // ------------------------------------------------------------------ + + // Reset the canvas to black + gfx_dest = -1; + gfx_clear = 0; + gfx_a = 1.0; + + // RECALCULATE DIMENSIONS AND RELATIONS -------------------------------------- + // + // Only really needs to happen if screen size changes + (lastWidth != gfx_w) || (lastHeight != gfx_h) ? + ( + // Work out various sizes and heights and relations + labels_h = 20; // Height of the dB/line marker label area + pad_h = (gfx_h - 2*labels_h) / 20; // Black spacer between meter bars + laneH = (gfx_h - 2*labels_h - pad_h) / num_ch; // Height for 1 channel i.e. 2 lines + lineH = (laneH / 2) - pad_h; // Height for 1 line inside a channel + + // Calculate Decibel marker positions + gfx_quarter = gfx_w / 4; // 0dB is at 3/4*gfx_w, this is the remainder to the right + // + // These are gfx_x values for various elments. + X_0dB = gfx_quarter * 3; // 1/4*3 = 3/4 = 0 dB position at 3/4*gfx_w + X_Neg48 = X_0dB * dB_Neg48; + X_Neg36 = X_0dB * dB_Neg36; + X_Neg24 = X_0dB * dB_Neg24; + X_Neg12 = X_0dB * dB_Neg12; + X_Neg06 = X_0dB * dB_Neg06; + X_Neg03 = X_0dB * dB_Neg03; + X_Pos03 = X_0dB + gfx_quarter * (1-dB_Neg03); + X_Pos06 = X_0dB + gfx_quarter * (1-dB_Neg06); + X_Pos12 = X_0dB + gfx_quarter * (1-dB_Neg12); + X_Pos24 = x_0dB + gfx_quarter * (1-dB_Neg24); + X_Pos36 = x_0dB + gfx_quarter * (1-dB_Neg36); + + // Update size flags to not calculate again next cycle + lastWidth = gfx_w; + lastHeight = gfx_h; + ); + + // CEILING AND KNEE MARKER POSITIONS ----------------------------------------- + // + // Can't be offloaded to the one-time calculation above, because these will + // change without the size of the UI changing. + // + // Ceiling + dBCeil <= 0 ? X_Ceil = X_0dB * ceiling; + dBCeil > 0 ? X_Ceil = X_0dB + gfx_quarter * (1-dB_InvCeil); + // + // Knee Lower + kneeLower <= 0 ? X_KneeLower = X_0dB * kneeLowerGain; + kneeLower > 0 ? X_KneeLower = X_0dB + gfx_quarter * (1-dB_InvKneeLower); + // + // Knee Upper + kneeUpper <= 0 ? X_KneeUpper = X_0dB * kneeUpperGain; + kneeUpper > 0 ? X_KneeUpper = X_0dB + gfx_quarter * (1-dB_InvKneeUpper); + // + // The X position of where various labels should theoretically go + X_LabelKneeLower = max(0, X_KneeLower - lbl_w); + X_LabelKneeUpper = min(gfx_w-lbl_w, X_KneeUpper + 8); + X_LabelCeiling = min(gfx_w-lbl_w, X_Ceil + 8); + // + // If knee lower dB only has 1 int digit, compensate spacing + kneeLower <= -10 ? X_LabelKneeLower -= 8; + // + // Evaluate if the Ceiling dB label would overlap either of the Knee dB labels + lblCollision = useKnee && ((X_LabelCeiling <= X_LabelKneeLower + lbl_w) || + (X_LabelCeiling + lbl_w >= X_LabelKneeUpper)); + + // VERTICAL dB MARKER LINES -------------------------------------------------- + // + // For -48 to -3 dBfs + gfx_set(1,1,1,0.35); // Dark gray + // + // -48 dBfs + gfx_x = X_Neg48; + gfx_y = 0; + gfx_lineto(gfx_x,gfx_h,1); + // + // -36 dBfs + gfx_x = X_Neg36; + gfx_y = 0; + gfx_lineto(gfx_x,gfx_h,1); + // + // -24 dBfs + gfx_x = X_Neg24; + gfx_y = 0; + gfx_lineto(gfx_x,gfx_h,1); + // + // -12 dBfs + gfx_x = X_Neg12; + gfx_y = 0; + gfx_lineto(gfx_x,gfx_h,1); + // + // -6 dBfs + gfx_x = X_Neg06; + gfx_y = 0; + gfx_lineto(gfx_x,gfx_h,1); + // + // -3 dBfs + gfx_x = X_Neg03; + gfx_y = 0; + gfx_lineto(gfx_x,gfx_h,1); + // + // Somewhat lighter, only for 0 dBfs -------------- 0 dBfs + gfx_set(1,1,1,0.6); // Gray + // + // 0 dBfs + gfx_x = X_0dB; + gfx_y = 0; + gfx_lineto(gfx_x,gfx_h,1); + // + // For +3 to +36 dBfs + gfx_set(1,1,1,0.35); // Dark gray + // + // +3 dBfs + gfx_x = X_Pos03; + gfx_y = 0; + gfx_lineto(gfx_x,gfx_h,1); + // + // +6 dBfs + gfx_x = X_Pos06; + gfx_y = 0; + gfx_lineto(gfx_x,gfx_h,1); + // + // +12 dBfs + gfx_x = X_Pos12; + gfx_y = 0; + gfx_lineto(gfx_x,gfx_h,1); + // + // +24 dBfs + gfx_x = X_Pos24; + gfx_y = 0; + gfx_lineto(gfx_x,gfx_h,1); + // + // +36 dBfs + gfx_x = X_Pos36; + gfx_y = 0; + gfx_lineto(gfx_x,gfx_h,1); + + // TEXT dB MARKER LABELS ----------------------------------------------------- + // + lbl_w = lbl_h = 0; + gfx_measurestr("-48", lbl_w, lbl_h); + // + // For -36 to -3 dBfs + gfx_set(1,1,1,0.35); // Dark gray + // + // TOP LABELS ------------------------ + // + gfx_y = 4; + gfx_setfont(0); + // + gfx_x = X_Neg36 + 2; + gfx_drawstr("-36"); + // + gfx_x = X_Neg24 + 8; + gfx_drawstr("-24"); + // + gfx_x = X_Neg12 + 8; + gfx_drawstr("-12"); + // + gfx_x = X_Neg06 + 8; + gfx_drawstr("-6"); + // + gfx_x = X_Neg03 + 8; + gfx_drawstr("-3"); + // + gfx_x = X_0dB + 8; + gfx_drawstr("0"); + // + gfx_x = X_Pos03 + 8; + gfx_drawstr("+3"); + // + gfx_x = X_Pos06 + 8; + gfx_drawstr("+6"); + // + gfx_x = X_Pos12 + 4; + gfx_drawstr("+12"); + // + // BOTTOM LABELS ----------------------- + // + gfx_y = gfx_h - labels_h + lbl_h; + gfx_setfont(0); + // + gfx_x = X_Neg36 + 2; + gfx_drawstr("-36"); + // + gfx_x = X_Neg24 + 8; + gfx_drawstr("-24"); + // + gfx_x = X_Neg12 + 8; + gfx_drawstr("-12"); + // + gfx_x = X_Neg06 + 8; + gfx_drawstr("-6"); + // + gfx_x = X_Neg03 + 8; + gfx_drawstr("-3"); + // + gfx_x = X_0dB + 8; + gfx_drawstr("0"); + // + gfx_x = X_Pos03 + 8; + gfx_drawstr("+3"); + // + gfx_x = X_Pos06 + 8; + gfx_drawstr("+6"); + // + gfx_x = X_Pos12 + 4; + gfx_drawstr("+12"); + + // CEILING MARKER (always displayed) ----------------------------------------- + // + // Line marker + gfx_x = X_Ceil; + gfx_y = 0; + gfx_set(1,useKnee*0.5,0,useKnee ? alphaMarkerSoft : alphaMarkerHard); // Red (no knee) or Orange (knee) + gfx_rectto(gfx_x+3,gfx_h); + // + // Marker label ------------ TOP + gfx_x = X_LabelCeiling; + gfx_y = lbl_h + 6; + gfx_y -= lblCollision * (lbl_h+2); // Adjustment for label collision + gfx_set(1,useKnee*0.5,0,alphaMarkerText); // Red (no knee) or Orange (knee) + dBCeil > 0.0 ? gfx_drawstr("+"); + gfx_printf("%02.2f", dBCeil); + // + // Marker label ------------ BOTTOM + gfx_x = X_LabelCeiling; + gfx_y = gfx_h - labels_h - 2; + gfx_y += lblCollision * (lbl_h+2); // Adjustment for label collision + dBCeil > 0.0 ? gfx_drawstr("+"); + gfx_printf("%02.2f", dBCeil); + + // LEVEL METERING LANES ------------------------------------------------------ + // + // Every input and output signal is displayed in its own separate level metering lane. + // Each lane consists of two level bars, each bar has a blank/black pad underneath it. + // + // Cycle through all plugin channels + lane = 0; + while (lane < num_ch) + ( + // SAMPLE PREPARATION -------------- + // + // The processing block always writes up-to-date input and output samples into a buffer. + // + // Fetch the buffered input/output samples and restrict them to a useful range + splIn = max(dB_Neg48, min(dB_Pos48, bufferIn[lane] )); + splOut = max(dB_Neg48, min(dB_Pos48, bufferOut[lane])); + // + // Signed dB values of the buffered and restricted input/output samples + dBSplIn = gainTodB(splIn); + dBSplOut = gainTodB(splOut); + + // LANE DRAWING -------------------- + // + // The Y position for this channel's metering lane + lane_y = labels_h + lane * laneH; + // + // Only draw level metering bars for this lane if one of the channel's samples actually + // contains signal. This removes metering for a channel if no signal is present. + (splIn > dB_Neg48) || (splOut > dB_Neg48) ? + ( + // Right-most X position for the full horizontal scale of the input sample + dBSplIn <= 0 ? X_SampleIn = X_0dB * splIn; + dBSplIn > 0 ? X_SampleIn = X_0dB + gfx_quarter * (1-dBToGain(-dBSplIn)); + + // Right-most X position for the full horizontal scale of the output sample + dBSplOut<= 0 ? X_SampleOut= X_0dB * splOut; + dBSplOut > 0 ? X_SampleOut= X_0dB + gfx_quarter * (1-dBToGain(-dBSplOut)); + + // INPUT METER (start to knee lower) --------------------------- + gfx_y = pad_h + lane_y; + gfx_x = 0; + gfx_set(0,0.75,0,alphaMeterBars); // Green + gfx_rectto(min(X_KneeLower+1, X_SampleIn), gfx_y+lineH); + // + // KNEE METER (knee lower to knee upper) ----------------------- + useKnee ? + ( + X_KneeMeter = max(X_KneeLower, min(X_KneeUpper+1, X_SampleIn)); + + gfx_y = pad_h + lane_y; + gfx_x = X_KneeLower; + gfx_set(1,0.8,0,alphaMeterBars); // Orange Yellow + gfx_rectto(X_KneeMeter, gfx_y+lineH); + ); + // + // CLIPPING METER (everything above knee upper) ---------------- + (dBSplIn > kneeUpper) ? + ( + X_ClipMeter = max(X_KneeUpper, min(gfx_w, X_SampleIn)); + + gfx_y = pad_h + lane_y; + gfx_x = X_KneeUpper; + gfx_set(1,0,0,alphaMeterBars); // Red + gfx_rectto(X_ClipMeter, gfx_y+lineH); + ); + // + // OUTPUT METER (start to knee lower) -------------------------- + gfx_y = pad_h + lane_y + pad_h + lineH; + gfx_x = 0; + gfx_set(0,0.7,1,alphaMeterBars); // Blue + gfx_rectto(min(X_KneeLower+1, X_SampleOut), gfx_y+lineH); + // + // OUTPUT KNEE METER (knee lower to ceiling) ------------------- + useKnee && (dBSplOut >= kneeLower) ? + ( + gfx_y = pad_h + lane_y + pad_h + lineH; + gfx_x = X_KneeLower; + gfx_set(1,0.8,0,alphaMeterBars); // Orange Yellow + gfx_rectto(X_SampleOut + (X_SampleOut == X_Ceil), gfx_y+lineH); + ); + ); + + // CHANNEL I/O LABELS ------------------------------------------------------ + // + // Get dimensions for current string category and set colour to transparent black + gfx_measurestr("Out 64", lbl_w, lbl_h); + gfx_set(0,0,0,0.5); // Black + // + // If the channel number only has 1 digit, add a padding character for even spacing + laneBuffer = (lane < 10) ? " " : ""; + // + // CHANNEL INPUT LABEL ------------ + gfx_y = pad_h + lane_y + (lineH / 2) - (lbl_h / 2); + gfx_x = pad_h; + gfx_drawstr(laneBuffer); + gfx_drawnumber(lane+1, 0); + gfx_drawstr(" In"); + // + // CHANNEL OUTPUT LABEL ------------ + gfx_y = pad_h + lane_y + pad_h + lineH + (lineH / 2) - (lbl_h / 2); + gfx_x = pad_h; + gfx_drawstr(laneBuffer); + gfx_drawnumber(lane+1, 0); + gfx_drawstr(" Out"); + + // Advance to next channel metering lane + lane += 1; + ); + + // KNEE MARKER LINES AND LABELS ---------------------------------------------- + // + // Get dimensions for current string category + gfx_measurestr("-12.34", lbl_w, lbl_h); + // + // Only draw knee lines and labels if a knee is actually set + useKnee ? + ( + // KNEE LOWER -------------------------------- + // + // Line marker + gfx_x = X_KneeLower; + gfx_y = 0; + gfx_set(1,1,0,alphaMarkerHard); // Yellow + gfx_rectto(gfx_x+3,gfx_h); + // + // Marker label ------------ TOP + gfx_x = X_LabelKneeLower; + gfx_y = lbl_h + 6; + gfx_set(1,1,0,alphaMarkerText); // Yellow + kneeLower > 0.0 ? gfx_drawstr("+"); + gfx_printf("%02.2f", kneeLower); + // + // Marker label ------------ BOTTOM + gfx_x = X_LabelKneeLower; + gfx_y = gfx_h - labels_h - 2; + kneeLower > 0.0 ? gfx_drawstr("+"); + gfx_printf("%02.2f", kneeLower); + + // KNEE UPPER -------------------------------- + // + // Line marker + gfx_x = X_KneeUpper; + gfx_y = 0; + gfx_set(1,0,0,alphaMarkerHard); // Red + gfx_rectto(gfx_x+3,gfx_h); + // + // Marker label ------------ TOP + gfx_x = X_LAbelKneeUpper; + gfx_y = lbl_h + 6; + gfx_set(1,0,0,alphaMarkerText); // Red + kneeUpper > 0.0 ? gfx_drawstr("+"); + gfx_printf("%02.2f", kneeUpper); + + // Marker label ------------ BOTTOM + gfx_x = X_LAbelKneeUpper; + gfx_y = gfx_h - labels_h - 2; + kneeUpper > 0.0 ? gfx_drawstr("+"); + gfx_printf("%02.2f", kneeUpper); + ); + @sample + // Safety mechanism to force @gfx recalculation (once) if UI size was changed + (gfx_w != lastWidth) || (gfx_h != lastHeight) ? + ( + lastWidth = -1; + lastHeight = -1; + ); + // Set to 0 to start loop at first input channel channel = 0; // Loop through all of this plugin's input channels while (channel < num_ch) ( - // Apply processing to the current channel's sample - spl(channel) = process(spl(channel)); + // Locally buffer input sample because spl() addressing is slow + inputSample = spl(channel); + + // Apply Boost gain to input sample + inputSample *= gain; + + // Write absolute value of the gained input sample to the input buffer for this channel, + // also factor-in falloff speed for a smoother @gfx display. + bufferIn[channel] = max(bufferIn[channel]*falloff, abs(inputSample)); + + // Process the gained input sample (Note: the one in buffer has abs() and falloff) + inputSample = process(inputSample); + + // Write the processed output sample to the output buffer for this channel, + // also factor-in falloff speed for a smoother @gfx display. + bufferOut[channel] = max(bufferOut[channel]*falloff, abs(inputSample)); + + // OUTPUT TRIM GAIN ---------------- + // + // Output Trim gain is applied AFTER storing the processed sample in the buffer for @gfx. + // This creates a discrepancy between the values shown in the metering section and actual + // output values. Since this processor contains a hard-clipping section, nothing can pass + // to the outputs above the set ceiling level, so Ceiling dB symbolizes the loudest level + // that could ever come out of this plugin. Levels below the ceiling line are monitorable + // in the meter bars, and nothing comes out above the ceiling line. When using Trim gain, + // it is very much possible to shoot the clipped signal louder to the outputs than should + // be done. So in addition to the plugin's metering, always keep an eye on the I/O meters + // on the sides of the interface. + // + inputSample *= trim; + + // Finally, write the gained, clipped and trimmed output sample to this channel's output + spl(channel) = inputSample; // Increment counter to next channel channel += 1;