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);
        }
예제 #3
0
        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++;
        }