2
$\begingroup$

I'm trying to obtain the envelope of an audio signal by using the Hilbert method. My code generates the analytical signal in the same way as scipy.signal Hilbert() function does (I basically copied the scipy source code in troubleshooting this issue). As can be seen from the pictures below the envelope is not obtained and it seems there is a lot of periodic signal left after the envelope extraction.

The audio signal and the extracted envelope

The same signal zoomed in to give a better view

I've not used the Hilbert method before so maybe this is an inherent limitation of the method? Yet all the pictures I've found on the internet show a really neat envelope extraction, why wouldn't it work in this case?

I've tried scipy's hilbert function and it gives identical results.

The code used to generate these images:

'''

import numpy as np
import matplotlib.pyplot as plt
import wave
#functions to read wav files

def read(filename):
    global raw_bin
    global res
    global channels

    file=wave.open(filename,mode="rb")
    samplerate=file.getframerate()
    length=file.getnframes()
    channels=file.getnchannels()
    res=file.getsampwidth()
    bits=res*8
    raw_bin=file.readframes(length)

    data=list(map(forward_conv,range(length//channels)))

    return samplerate,bits,data

def forward_conv(i):
    global raw_bin
    global res
    global channels

    i=i*res*channels
    current=int.from_bytes(raw_bin[i:i+res],"little")
    
    if current >= (2**(res*8)//2):
        current -= 2**(res*8)
    
    return current


filename = 'bas.wav'

#read wav file
samplerate, bits, data = read(filename)

#ensure length of data is a power of 2
bits = int(np.log2(len(data)))
data = data[:2**bits]

#remove DC bias
mean = np.mean(data)
data = [x - mean for x in data]

def Analytical_signal(data):
    fft = np.fft.fft(data)

    N = len(data)

    for i in range(1, N//2):
        fft[i] *= 2

    for i in range(N//2 + 1, N):
        fft[i] *= 0

    return np.fft.ifft(fft)


analyticData = Analytical_signal(data)

plt.plot(data, label='data')
plt.plot(np.abs(analyticData), label='envelope')

plt.legend()

'''

$\endgroup$
1
  • 2
    $\begingroup$ Not an expert on the topic but I believe that Hilbert transform is not a good choice for envelope extraction with complex (more than one frequency component) signals. It works quite well with monochromatic signals but it fails easily when this is not the case. $\endgroup$
    – ZaellixA
    Commented Aug 19, 2022 at 19:10

1 Answer 1

4
$\begingroup$

If your only interest is amplitude then Hilbert is a choice, good or bad, but it will need to be designed with the transition width proportional to the lowest frequency. E.g. for HiFi audio, the lowest frequency is 20 Hz, so that's the transition width that you'll need. but you also have a highest frequency of interest, and that will be 20 kHz, which means the sampling frequency needs to be greater than double that upper limit. For a typical 44.1 kHz you'll end up with an enormous order for the filter.

E.g. for a Kaiser window with 60 dB attenuation I get a 7995th order. If I increase the transition width to 100 Hz and reduce the attenuation to 40 dB, I get a more "manageable" 985th order. And here is a kickdrum + cymbal (black), ran through this Hilbert transformer (blue), and how the "envelope" turns out (red):

audio Hilbert

The input has been delayed to match the Hilbert transformer's order, that's why it doesn't show as starting from zero. You may as well just use the abs() value. What you probably saw in other's examples was the already filtered version. For example, the same magnitude, ran through a 100 Hz, 4th order Bessel lowpass:

lowpass filtered

$\endgroup$

Not the answer you're looking for? Browse other questions tagged or ask your own question.