public void SetRelease(double releaseMs) { ReleaseMs = releaseMs; double dbDecayPerSample = -60 / (ReleaseMs / 1000.0 * Fs); fastDecay = Utils.Db2Gain(dbDecayPerSample); }
public EnvelopeFollower(double fs, double releaseMs) { Fs = fs; double ts = 1.0 / fs; inputFilter = new Butterworth(fs); inputFilter.Parameters[Butterworth.P_ORDER] = 2; inputFilter.Parameters[Butterworth.P_CUTOFF_HZ] = InputFilterCutoff; inputFilter.Update(); var emaAlpha = Utils.ComputeLpAlpha(EmaFc, ts); double slowDbDecayPerSample = -60 / (3000 / 1000.0 * fs); slowDecay = Utils.Db2Gain(slowDbDecayPerSample); SetRelease(releaseMs); sma = new Sma((int)(fs * SmaPeriodSeconds)); ema = new Ema(emaAlpha); movementLatch = new EmaLatch(0.005, 0.2); // frequency dependent, but not really that critical... triggerCounterTimeoutSamples = (int)(fs * TimeoutPeriodSeconds); holdAlpha = Utils.ComputeLpAlpha(HoldSmootherFc, ts); }
public void Run() { /*var pm = new PlotModel(); * var xs = Enumerable.Range(0, 1000).Select(x => x / 1000.0).Select(x => -100 + x * 100).ToArray(); * var ys1 = xs.Select(x => Compress(x, ThresholdDb, UpperSlope, 3, true)).ToArray(); * var ys2 = xs.Select(x => Compress(x, ThresholdDb + 6, LowerSlope, 3, true)).ToArray(); * * pm.AddLine(xs, ys1); * pm.AddLine(xs, ys2); * pm.Show(); */ var pm = new PlotModel(); pm.Axes.Add(new LinearAxis { Position = AxisPosition.Left, Key = "L" }); pm.Axes.Add(new LinearAxis { Position = AxisPosition.Right, Key = "R" }); var follower = new EnvelopeFollower(48000, 20); var expander = new Expander(); var slewLimiter = new SlewLimiter(48000); follower.SetRelease(ReleaseMs); slewLimiter.UpdateDb60(2.0, ReleaseMs); expander.Update(ThresholdDb, ReductionDb, UpperSlope); var signalValues = new List <double>(); var followerValues = new List <double>(); var gainValues = new List <double>(); var rand = new Random(); Func <double> random = () => 2 * rand.NextDouble() - 1; double decay = 1.0; for (int i = 0; i < 20000; i++) { var x = (Math.Sin(i / fs * 2 * Math.PI * 300) + random() * 0.4) * decay; decay *= 0.9998; if (i < 1000) { x = random() * 0.001; } else if (i > 12000) { x = random() * 0.001; } signalValues.Add(x); follower.ProcessEnvelope(x); var env = follower.GetOutput(); followerValues.Add(env); expander.Expand(Utils.Gain2Db(env)); var gainDb = expander.GetOutput(); gainDb = slewLimiter.Process(gainDb); var gain = Utils.Db2Gain(gainDb); gainValues.Add(gain); } var signalLine = pm.AddLine(signalValues); signalLine.Title = "Signal"; signalLine.YAxisKey = "R"; //pm.AddLine(followerValues.Select(Utils.Gain2Db)).Title = "followerValues"; pm.AddLine(gainValues.Select(Utils.Gain2Db)).Title = "gainValues"; pm.Show(); }
public void ProcessEnvelope(double val) { double combinedFiltered; double decay; // 1. Rectify the input signal val = Math.Abs(val); // 2. Band pass filter to ~ 100hz - 2Khz //lpValue = lpAlpha * val + (1 - lpAlpha) * lpValue; var lpValue = inputFilter.Process(val); //hpSignal = hipassAlpha * lpSignal + (1 - hipassAlpha) * hpSignal // rectify the lpValue again, because the resonance in the filter can cause a tiny bit of ringing and cause the values to go negative again lpValue = Math.Abs(lpValue); var mainInput = lpValue; // 3. Compute the EMA and SMA of the band-filtered signal. Also compute the per-sample dB decay baed on the SMA var emaValue = ema.Update(mainInput); var smaValue = sma.Update(mainInput); // 4. use a latching low-pass classifier to determine if signal strength is generally increasing or decreasing. // This removes spike from the signal where the SMA may move in the opposite direction for a short period var movementValue = movementLatch.Update(sma.GetDbDecayPerSample() > 0); // 5. If the movement is going up, prefer the faster moving EMA signal if it's above the SMA // If the movement is going down, prefer the faster moving EMA signal if it's below the SMA // otherwise, use SMA if (movementValue > 0) // going up { combinedFiltered = emaValue > smaValue ? emaValue : smaValue; } else // going down { combinedFiltered = emaValue < smaValue ? emaValue : smaValue; } // 6. use a hold mechanism to store the peak if (combinedFiltered > hold) { hold = combinedFiltered; lastTriggerCounter = 0; } // 7. Choosing the decay speed // Under normal conditions, use the decay from the SMA, scaled by a fudge factor to make it slightly faster decaying. // The reason for this is so that we gently bump into the peaks of the signal once in a while. // If the hold mechanism hasn't been triggered for a specific timeout, then the current hold value is too high, and we need to rapidly decay downwards. // Use the fastDecay (based on the user- specified release value) as a slew limited value if (lastTriggerCounter > triggerCounterTimeoutSamples) { decay = fastDecay; } else { decay = Utils.Db2Gain(sma.GetDbDecayPerSample() * 1.2); // 1.2 is fudge factor to make the follower decay slightly faster than actual signal, so we gently bump into the peaks } // 7.5 Limit the decay speed in the general range of slowDecay...fastDecay, the slow decay is currently a fixed 3 seconds to -60dB value if (decay > slowDecay) { decay = slowDecay; } if (decay < fastDecay) { decay = fastDecay; } hold = hold * decay; // 8. Filter the resulting hold signal to retrieve a smooth envelope. // Currently using 4x 1 pole lowpass, should replace with a proper 4th order butterworth h1 = holdAlpha * hold + (1 - holdAlpha) * h1; h2 = holdAlpha * h1 + (1 - holdAlpha) * h2; h3 = holdAlpha * h2 + (1 - holdAlpha) * h3; h4 = holdAlpha * h3 + (1 - holdAlpha) * h4; holdFiltered = h4; lastTriggerCounter++; }