Monday 10 April 2017

Prototype Declickin Algorithm

Declicking vinyl record clicks is quite straight forward as they as damaged sections of sound. Declicking digital artefacts is quite a different matter. 

This algorithm is the first I have developed or used which gets any place close to working.

What is the challenge.

In digital audio it is quite easy to get a discontinuity in audio which produces a harsh and completely unnatural 'click' sound. Any transition between to amplitudes which is not part of a carefully generated wave will shoot spectra density in all direction even producing the dreaded negative frequencies.

By far the best way to deal with this is not to produce the discontinuities in the first place. This involves very careful algorithm design. However, sometimes a bit of sound is just great and very hard to reproduce and it has a couple of clicks. Maybe I did not notice the clicks and have long since forgotten how the sounds were made but not want to use them. For all these cases, a declicker is the way to go.

How it works.

The code takes the digital signal one sample at a time.  This is mapped onto a new signal. If the absolute difference between a sample and the next sample exceeds a threshold then a triangle of signal is added to the output with the peak of the triangle centred on the 'click'.

This output signal is then between 0 and X and varies according to the click density of the input signal. This is clipped at one.

This filter signal has far too much high frequency to be used as an driver for a filter; to correct for that I use an biquad lowpass filter. The signal is passed through it twice, once forwards and once in reverse. These two are mixed together. This reversal trick removes the group delay issues of using an IIR filter in the normal way.

Finally, the smoothed filter signal inverted (between 1 and 0 rather than 0 and 1). The input signal is passed through a low pass filter (1 KHz). The input signal is split into to and one is low pass filtered at 1KHz and 6db per octave. Now the filtered input is multiplied by the filter signal and the non inverted input is multiplied by the inverted filter.

The result if that in sections of the input where there are clicks, the 1KHz low pass kicks in.

Issues:

 If there is a lot of click the low pass filter coming in and out is noticeable. This is much less of a problem than the original clicks, but it is an artefact. As I said, no clicks in the first place is much better.

I am using a IIR low pass filter for the 1KHz filter. This means the filtered and unfiltered signals will have phase issues. I should go over to a FIR filter but, realistically, that means using a FFT based approach and the cost/complexity might not really be worth it.

The code:


from sython.concurrent import sf_parallel
from com.nerdscentral.audio.core import SFMemoryZone
from sython.utils.Reverberation import reverberate
from sython.utils.Splitter import writeWave
from com.nerdscentral.audio.core import SFData
import new

def main():
    left  = sf.ReadSignal("temp/left_v1_acc")
    right = sf.ReadSignal("temp/right_v1_acc")

    @sf_parallel
    def declick(signal):
        up = SFData.build(signal.getLength())
        down = SFData.build(signal.getLength())
        thresh = 0.02
        old = 0.0
        width = 192 * 2

        # TODO: Convert all this into Java for speed.
        for pos in xrange(signal.getLength()):
            new = signal.getSample(pos)
            diff = old - new
            if abs(diff) > thresh:
                for x in xrange(width):
                    v = 1.0 - (x / width)
                    up.setSample(pos + x, v)
                    v = v + up.getSample(pos -x)
                    if v > 1.0:
                        v = 1.0
                    up.setSample(pos - x, v)
            old = new

        up = sf.FixSize(sf.Mix(
            sf.RBJLowPass(up, 100, 1.0),
            sf.Reverse(sf.RBJLowPass(sf.Reverse(up), 100, 1.0))
        )).realise()

        minV = 0.0
        for pos in xrange(signal.getLength()):
            v = up.getSample(pos)
            if v < minV:
                minV = v
        
        for pos in xrange(signal.getLength()):
            v = up.getSample(pos)
            up.setSample(pos, v - minV)     
        up = sf.FixSize(up)

        for pos in xrange(signal.getLength()):
            down.setSample(pos, 1.0 - up.getSample(pos))

        filt = sf.RBJLowPass(signal, 1000, 1.0)
        filt = sf.Multiply(filt, up)
        nFlt = sf.Multiply(signal, down)
        return sf.FixSize(sf.Mix(filt, nFlt).realise())

    left  = declick(left)
    right = declick(right)
    sf.WriteFile32((left, right),"temp/declicked.wav")


No comments:

Post a Comment