0

I'm trying to get fft amplitudes to match the input time domain signals amplitude scale.

I've tried so many 'answers' to this problem - none of which seem to work for me. So I've written a program to experiment.

This takes a section of audio (from a file or - you can use additative synthesis to create a wave).

I then give the option of padding the signal with zeros (choose number and number of steps to analyse).

I don't have a site to post images of the plots I've created - but if you cut and post the program at the bottom (python 3.x) you'll quickly see them for yourselves!

A small sub-section of the program is below.

The pertinent section of code (plot_pad_stats() below) is:

    for p in np.arange(start,end,step):
        s1 = pad(s,int(p))

        ftp = np.fft.fft(s1,norm=norma)
        ft = norm_fft(s1,norm)

        if real:
            ftp = ftp.real
            ft = ft.real
        else:
            ftp = np.sqrt(ftp.real**2 + ftp.imag**2)
            ft = np.sqrt(ft.real**2 + ft.imag**2)
        if absolute:
            ftp = abs(ftp)      
            ft = abs(ft)      
        if active:
            ftp = [(f+abs(f))/2 for f in ftp]
            ft = [(f+abs(f))/2 for f in ft]

        minft[p1] = min(ftp)
        maxft[p1] = max(ftp)
        meanft[p1] = np.mean(ftp)
        lenft[p1] = int(p)

        minnft[p1] = min(ft)
        maxnft[p1] = max(ft)
        meannft[p1] = np.mean(ft)
        lennft[p1] = int(p)

I suspect something to do with phase - but have no idea how to get an fft amplitude scale to match the time domain signal being analysed...

Not only is the change as padding is added interesting, but the change as the signal goes from a single sine wave to more complex waves!

Any help gratefully appreciated to:- tony.fdlr@gmail.com

arrays = plot_pad_stats: just call with no args to plot 440hz signal full amplitude

use

   additativeSignal(hza=[1],aa=None,ap=[0],n=88200,sr=44100,verbose=True):
    '''
    hza = herz array
    aa = amplitude array
    ap = phase array
    n = number samples to return
    sr = sampling frequency
    verbose = sanity check
    '''
to create additative synthesised signals eg
    signal = additativeSignal([440,880,1320],[0.6,0.3,0.2],[0,0,0])
    signal = additativeSignal([440,550,600],[0.6,0.5,0.3],[0,50,100])

then

    plot_pad_stats(signal)

use

    plot_signal(s,start,end) to plot the signal (or a section thereof)

Note = phase values are in samples not degrees or radians!

OR for more fun:

sawtooth wave:

        f = 100
        a = 0.8
        ph = 0
        noFqs = 20

        signal = additativeSignal(
         [f * i for i in range(1,noFqs+1)],
         [am/i for i in range(1,noFqs+1)],
         [0 for i in range(1,noFqs+1)])   

        plot_signal(signal[0:1000])

        arrs = plot_pad_stats(signal,maxpad=1000,steps=1000,absolute=False)

Play with the other args to your heart's content!

Be aware it can take a little time to plot!

Here's the code for you to play with (python 3.x)

import numpy as np
import matplotlib.pyplot as plt 

def pad(s,n):
    '''
    pad signal to make length n
    '''
    ls = len(s)
    #print('Pad request: sig len: {}, new len: {}'.format(ls,n))
    if n <= ls:
        return s
    s2 = np.zeros(n)
    try:
        s2[:ls] = s
    except:
        #print('Pad exception: sig len: {}, new len: {}'.format(ls,n))            
        return s
    return s2

def norm_fft(s,norm=True):
    '''
    See plot_pad_stats below for explanation    
    '''
    if norm:
        norma = 'ortho'
    else:
        norma = None
    n = len(s)

    ft = np.fft.fft(s,norm=norma) / n
    ft = ft[range(int(n / 2))]

    return ft

def additativeSignal(hza=[1],aa=None,ap=[0],n=88200,sr=44100,verbose=True):
    '''
    hza = herz array
    aa = amplitude array
    ap = phase array
    n = number samples to return
    sr = sampling frequency
    verbose = sanity check
    '''
    xa = np.arange(0,n)
    s = np.zeros(n)
    a = 0
    apl = len(ap)
    if apl < len(hza):
        apl = np.zeros(len(hza))
    if not (isinstance(aa,(np.ndarray,list))):
        aa = np.ones(len(hza))
    if not (isinstance(ap,(np.ndarray,list))):
        aa = np.ones(len(hza))
    for hz in hza:
        amp=aa[a]
        phase = ap[a]
        if verbose:
            print("freq %d, amp = %f, phase: %d" % (hz,amp,phase))
        if hz != 0:
            s += (np.sin(2*np.pi*(xa+phase)/(sr/hz))*amp)
        a += 1
    return s

def plot_pad_stats(s=None,
                  sr=44100,
                  minpad = 0,
                  maxpad = 300,
                  steps = 300,
                  real = True,
                  norm = None,
                  absolute = True,
                  active = False
                  ):
    '''
    s = signal (audio etc)
    sr = sample rate
    minpad - minimum number of zeroed samples to append
    maxpad - maximum number of zeroed samples to append
    steps - number of ffts to calculate
    real - use only real values otherwise take sqrt(real**2 + imag**2)
    norm - np.fft.fft norm arg
    absolute - make fft results absolute (ie >0)
    active - standard activation function - any negative values changed to zero
    '''

    if norm:
        norma = 'ortho'
    else:
        norma = None
    minpad = int(minpad)

    # Create 1 sec 4450 herz full volume audio signal at 44100 hz sampling frequency
    if type(s) == type(None):
        s = additativeSignal([440],[1],[0],44100,44100,True)

    start = len(s) + minpad
    end = start + maxpad
    prange = maxpad

    #Arrays of fft results:

    # length fft
    lenft = [0 for p in range(steps)]
    # max fft val
    maxft = [0 for p in range(steps)]
    # min fft val
    minft = [0 for p in range(steps)]
    # mean fft val
    meanft = [0 for p in range(steps)]

    # same arrays using the above normalised function
    lennft = [0 for p in range(steps)]
    maxnft = [0 for p in range(steps)]
    minnft = [0 for p in range(steps)]
    meannft = [0 for p in range(steps)]

    p1 = 0
    step = prange/steps
    print('fft len sig {} step {}'.format(len(s),step))

    for p in np.arange(start,end,step):
        #print('fft {} len sig {} padded len {}'.format(p1,len(s),int(p))) 
        s1 = pad(s,int(p))

        ftp = np.fft.fft(s1,norm=norma)
        ft = norm_fft(s1,norm)

        if real:
            ftp = ftp.real
            ft = ft.real
        else:
            ftp = np.sqrt(ftp.real**2 + ftp.imag**2)
            ft = np.sqrt(ft.real**2 + ft.imag**2)
        if absolute:
            ftp = abs(ftp)      
            ft = abs(ft)      
        if active:
            ftp = [(f+abs(f))/2 for f in ftp]
            ft = [(f+abs(f))/2 for f in ft]

        minft[p1] = min(ftp)
        maxft[p1] = max(ftp)
        meanft[p1] = np.mean(ftp)
        lenft[p1] = int(p)

        minnft[p1] = min(ft)
        maxnft[p1] = max(ft)
        meannft[p1] = np.mean(ft)
        lennft[p1] = int(p)

        p1 += 1
        #:22.8f
    plt.subplot(211)
    plt.title("FFT stats. Sig Len {} with {} - {} appended zeros".format(len(s),minpad,maxpad+minpad))
    plt.xlabel("signal + pad length {} - {}".format(start,end))
    plt.ylabel("max {:f} - {:f} and\nmin {:f} - {:f}\nand mean {:f} - {:f}\nffta  values (abs/real)".format(min(maxft),max(maxft),min(minft),max(minft),min(meanft),max(meanft)))
    plt.plot(lenft,maxft,'bo')
    plt.plot(lenft,minft,'r+')
    plt.plot(lenft,meanft,'g*')

    plt.subplot(212)
    plt.title("\nNormalised FFT stats. Sig Len {} with {} - {} appended zeros".format(len(s),minpad,maxpad+minpad))
    plt.xlabel("signal + pad length\n{} - {}".format(start,end))
    plt.ylabel("max {:22.8f} - {:22.8f} and\nmin {:22.8f} - {:22.8f}\nand mean {:22.8f} - {:22.8f}\nffta  values (abs/real)".format(min(maxnft),max(maxnft),min(minnft),max(minnft),min(meannft),max(meannft)))
    plt.plot(lennft,maxnft,'bo')
    plt.plot(lennft,minnft,'r+')
    plt.plot(lennft,meannft,'g*')

    plt.show()

    return maxft,minft,lenft

def plot_signal(s,start=0,end=None):
    plt.title("Signal")
    plt.xlabel("samples")
    plt.ylabel("amplitude")
    if end == None:
        plt.plot(s)
    else:
        plt.plot(s[start:end])
    plt.show()

Regards

Tony

  • take a look at https://stackoverflow.com/questions/55430065/how-to-calculate-the-energy-per-bin-in-a-dft/55431279#55431279 .... it gives the formula to parse output of the fft call ... at bottom of that answer is a link to pseudocode for further details ... to give yourself confidence your process is working transform the time domain signal into frequency domain ( the fft call ) then perform an inverse fft call to return back the same data in the time domain ... this output time domain will approximately match your input time domain if your process is accurate – Scott Stensland Nov 11 '19 at 23:07

1 Answers1

0

If I understand you right, you want to see the proper amplitude of a time signal in the spectral domain. In the example below I'm adding two sine waves of amplitudes 1 and 0.5. In the spectrum to the right you can see that the amplitudes appear to be halved, which is due to the complex FFT splitting the power between negative and positive frequencies.

import numpy as np
import matplotlib.pyplot as p
%matplotlib inline

T=3 # secs
d=0.04 # secs
n=int(T/d)
print(n)
t=np.arange(0,T,d)  
fr=1 # Hz
y1= np.sin(2*np.pi*fr*t) 
y2= 1/2*np.sin(2*np.pi*3*fr*t+0.5) 
y=y1+y2 
f=np.fft.fftshift(np.fft.fft(y))
freq=np.fft.fftshift(np.fft.fftfreq(n,d))
p.figure(figsize=(12,5))
p.subplot(121)
p.plot(t,y1,'.-',color='red', lw=0.5, ms=1)  
p.plot(t,y2,'.-',color='blue', lw=0.5,ms=1) 
p.plot(t,y,'.-',color='green', lw=4, ms=4, alpha=0.3) 
p.xlabel('time (sec)')
p.ylabel('amplitude (Volt)')
p.subplot(122)
p.plot(freq,np.abs(f)/n)
p.xlabel(' freq (Hz)')
p.ylabel('amplitude (Volt)');

enter image description here

roadrunner66
  • 7,772
  • 4
  • 32
  • 38