2
votes

I am trying to get a third-octave frequency spectrum of a time signal.

The time signal is the acoustic pressure of rotational rotor noise which is harmonic. Its fundamental frequency is ff = n * N_b and for that reason, all frequencies should be multiples of ff.

Using fft I get the expected result: Multiples of the fundamental frequency are the relevant frequencies in the spectrum.

To get the third-octave frequency spectrum I wanted to use python acoustics, but the result of the function bandpass_third_octaves is not what I expected.

I expected the peaks from the fft frequency spectrum to be simply shifted to the third-octave centre frequencies with an adjusted amplitude. At least that is what I would like to get.

I think I interpret the output of bandpass_third_octaves incorrectly. Its output is a tuple containing the third-octave frequencies and a list of arrays which are supposed to contain the amplitude values as far as I know.

I currently use the arrays' maximum value as resulting amplitude because it works better than using the sum of it. It is possible that this interpretation is my mistake.

I would appreciate any help. There is no need for me to use python acoustics. Any solution to get the third-octave frequency spectrum would be awesome.

Edit: Using the mean instead of the maximum produces better results, but I am still not completely satisfied with it.

Signal in time domain, frequency domain and supposed third octave spectrum

import matplotlib.pyplot as plt
import numpy as np
from scipy.sparse import spdiags
from scipy.signal import butter, lfilter, freqz, filtfilt, sosfilt

import acoustics.octave
#from acoustics.octave import REFERENCE

import acoustics.bands
from scipy.signal import hilbert
from acoustics.standards.iso_tr_25417_2007 import REFERENCE_PRESSURE
from acoustics.standards.iec_61672_1_2013 import (NOMINAL_OCTAVE_CENTER_FREQUENCIES,
                                                  NOMINAL_THIRD_OCTAVE_CENTER_FREQUENCIES)

try:
    from pyfftw.interfaces.numpy_fft import rfft
except ImportError:
    from numpy.fft import rfft


def bandpass_filter(lowcut, highcut, fs, order=8, output='sos'):
    """Band-pass filter.
    :param lowcut: Lower cut-off frequency
    :param highcut: Upper cut-off frequency
    :param fs: Sample frequency
    :param order: Filter order
    :param output: Output type. {'ba', 'zpk', 'sos'}. Default is 'sos'. See also :func:`scipy.signal.butter`.
    :returns: Returned value depends on `output`.
    A Butterworth filter is used.
    .. seealso:: :func:`scipy.signal.butter`.
    """
    nyq = 0.5 * fs
    low = lowcut / nyq
    high = highcut / nyq
    output = butter(order / 2, [low, high], btype='band', output=output)
    return output



def bandpass(signal, lowcut, highcut, fs, order=8, zero_phase=False):
    """Filter signal with band-pass filter.
    :param signal: Signal
    :param lowcut: Lower cut-off frequency
    :param highcut: Upper cut-off frequency
    :param fs: Sample frequency
    :param order: Filter order
    :param zero_phase: Prevent phase error by filtering in both directions (filtfilt)
    A Butterworth filter is used. Filtering is done with second-order sections.
    .. seealso:: :func:`bandpass_filter` for the filter that is used.
    """
    sos = bandpass_filter(lowcut, highcut, fs, order, output='sos')
    if zero_phase:
        return _sosfiltfilt(sos, signal)
    else:
        return sosfilt(sos, signal)


class Frequencies:
    """
    Object describing frequency bands.
    """

    def __init__(self, center, lower, upper, bandwidth=None):

        self.center = np.asarray(center)
        """
        Center frequencies.
        """

        self.lower = np.asarray(lower)
        """
        Lower frequencies.
        """

        self.upper = np.asarray(upper)
        """
        Upper frequencies.
        """

        self.bandwidth = np.asarray(bandwidth) if bandwidth is not None else np.asarray(self.upper) - np.asarray(
            self.lower)
        """
        Bandwidth.
        """

    def __iter__(self):
        for i in range(len(self.center)):
            yield self[i]

    def __len__(self):
        return len(self.center)

    def __str__(self):
        return str(self.center)

    def __repr__(self):
        return "Frequencies({})".format(str(self.center))

    def angular(self):
        """Angular center frequency in radians per second.
        """
        return 2.0 * np.pi * self.center



class OctaveBand(Frequencies):
    """Fractional-octave band spectrum.
    """

    def __init__(self, center=None, fstart=None, fstop=None, nbands=None, fraction=1,
                 reference=acoustics.octave.REFERENCE):

        if center is not None:
            try:
                nbands = len(center)
            except TypeError:
                center = [center]
            center = np.asarray(center)
            indices = acoustics.octave.index_of_frequency(center, fraction=fraction, ref=reference)
        elif fstart is not None and fstop is not None:
            nstart = acoustics.octave.index_of_frequency(fstart, fraction=fraction, ref=reference)
            nstop = acoustics.octave.index_of_frequency(fstop, fraction=fraction, ref=reference)
            indices = np.arange(nstart, nstop + 1)
        elif fstart is not None and nbands is not None:
            nstart = acoustics.octave.index_of_frequency(fstart, fraction=fraction, ref=reference)
            indices = np.arange(nstart, nstart + nbands)
        elif fstop is not None and nbands is not None:
            nstop = acoustics.octave.index_of_frequency(fstop, fraction=fraction, ref=reference)
            indices = np.arange(nstop - nbands, nstop)
        else:
            raise ValueError("Insufficient parameters. Cannot determine fstart and/or fstop.")

        center = acoustics.octave.exact_center_frequency(None, fraction=fraction, n=indices, ref=reference)
        lower = acoustics.octave.lower_frequency(center, fraction=fraction)
        upper = acoustics.octave.upper_frequency(center, fraction=fraction)
        bandwidth = upper - lower
        nominal = acoustics.octave.nominal_center_frequency(None, fraction, indices)

        super(OctaveBand, self).__init__(center, lower, upper, bandwidth)

        self.fraction = fraction
        """Fraction of fractional-octave filter.
        """

        self.reference = reference
        """Reference center frequency.
        """

        self.nominal = nominal
        """Nominal center frequencies.
        """

    def __getitem__(self, key):
        return type(self)(center=self.center[key], fraction=self.fraction, reference=self.reference)

    def __repr__(self):
        return "OctaveBand({})".format(str(self.center))




def bandpass_frequencies(x, fs, frequencies, order=8, purge=False, zero_phase=False):
    """"Apply bandpass filters for frequencies
    :param x: Instantaneous signal :math:`x(t)`.
    :param fs: Sample frequency.
    :param frequencies: Frequencies. Instance of :class:`Frequencies`.
    :param order: Filter order.
    :param purge: Discard bands of which the upper corner frequency is above the Nyquist frequency.
    :param zero_phase: Prevent phase error by filtering in both directions (filtfilt)
    :returns: Tuple. First element is an instance of :class:`OctaveBand`. The second element an array.
    """
    if purge:
        frequencies = frequencies[frequencies.upper < fs / 2.0]
    return frequencies, np.array(
        [bandpass(x, band.lower, band.upper, fs, order, zero_phase=zero_phase) for band in frequencies])




def bandpass_third_octaves(x, fs, frequencies=NOMINAL_THIRD_OCTAVE_CENTER_FREQUENCIES, order=8, purge=False,
                           zero_phase=False):
    """Apply 1/3-octave bandpass filters.
    :param x: Instantaneous signal :math:`x(t)`.
    :param fs: Sample frequency.
    :param frequencies: Frequencies.
    :param order: Filter order.
    :param purge: Discard bands of which the upper corner frequency is above the Nyquist frequency.
    :param zero_phase: Prevent phase error by filtering in both directions (filtfilt)
    :returns: Tuple. First element is an instance of :class:`OctaveBand`. The second element an array.
    .. seealso:: :func:`octavepass`
    """
    return bandpass_fractional_octaves(x, fs, frequencies, fraction=3, order=order, purge=purge, zero_phase=zero_phase)



def bandpass_fractional_octaves(x, fs, frequencies, fraction=None, order=8, purge=False, zero_phase=False):
    """Apply 1/N-octave bandpass filters.
    :param x: Instantaneous signal :math:`x(t)`.
    :param fs: Sample frequency.
    :param frequencies: Frequencies. Either instance of :class:`OctaveBand`, or array along with fs.
    :param order: Filter order.
    :param purge: Discard bands of which the upper corner frequency is above the Nyquist frequency.
    :param zero_phase: Prevent phase error by filtering in both directions (filtfilt)
    :returns: Tuple. First element is an instance of :class:`OctaveBand`. The second element an array.
    .. seealso:: :func:`octavepass`
    """
    if not isinstance(frequencies, Frequencies):
        frequencies = OctaveBand(center=frequencies, fraction=fraction)
    return bandpass_frequencies(x, fs, frequencies, order=order, purge=purge, zero_phase=zero_phase)



def _sosfiltfilt(sos, x, axis=-1, padtype='odd', padlen=None, method='pad', irlen=None):
    """Filtfilt version using Second Order sections. Code is taken from scipy.signal.filtfilt and adapted to make it work with SOS.
    Note that broadcasting does not work.
    """
    from scipy.signal import sosfilt_zi
    from scipy.signal._arraytools import odd_ext, axis_slice, axis_reverse
    x = np.asarray(x)

    if padlen is None:
        edge = 0
    else:
        edge = padlen

    # x's 'axis' dimension must be bigger than edge.
    if x.shape[axis] <= edge:
        raise ValueError("The length of the input vector x must be at least " "padlen, which is %d." % edge)

    if padtype is not None and edge > 0:
        # Make an extension of length `edge` at each
        # end of the input array.
        if padtype == 'even':
            ext = even_ext(x, edge, axis=axis)
        elif padtype == 'odd':
            ext = odd_ext(x, edge, axis=axis)
        else:
            ext = const_ext(x, edge, axis=axis)
    else:
        ext = x

    # Get the steady state of the filter's step response.
    zi = sosfilt_zi(sos)

    # Reshape zi and create x0 so that zi*x0 broadcasts
    # to the correct value for the 'zi' keyword argument
    # to lfilter.
    #zi_shape = [1] * x.ndim
    #zi_shape[axis] = zi.size
    #zi = np.reshape(zi, zi_shape)
    x0 = axis_slice(ext, stop=1, axis=axis)
    # Forward filter.
    (y, zf) = sosfilt(sos, ext, axis=axis, zi=zi * x0)

    # Backward filter.
    # Create y0 so zi*y0 broadcasts appropriately.
    y0 = axis_slice(y, start=-1, axis=axis)
    (y, zf) = sosfilt(sos, axis_reverse(y, axis=axis), axis=axis, zi=zi * y0)

    # Reverse y.
    y = axis_reverse(y, axis=axis)

    if edge > 0:
        # Slice the actual signal from the extended signal.
        y = axis_slice(y, start=edge, stop=-edge, axis=axis)

    return y


rho = 1.2
a = 340
N_b = 1
R = 1
r_H = 10
A = np.pi*R**2
TA = 287  
M_H = 0.3
w = M_H*a/R
n = w/(2*np.pi)

t = np.linspace(0,0.8,num=40000)
az = t*2*np.pi*n*N_b

sin = np.sin(az)
cos = np.cos(az)


#Thickness Noise

F_H = R/r_H
F_E = 0.00012875807653441588  #Bestimmt für den Propeller aus Paper
T1 = ((3-M_H*sin)*sin)/((1-M_H*sin)**3)
T2 = (M_H*(cos**2))/(10*(1-M_H*sin)**4)
T3 = 50 + 39*(M_H**2) - 45*M_H*sin - 11*(M_H**2)*(sin**2) + 12* (M_H**3) *sin - 18*(M_H**3)*(sin**3)
T_M = ((M_H**3)/12)*(-T1 + T2 * T3)

p_T = 0.5 * rho * a**2 * F_H * F_E * T_M 


#Loading Noise

F_T = (TA/ (rho * a**2))**(3/2) * (1 / (60 * np.sqrt(2) * N_b))
L = 60 + 30 * M_H**2 * cos**2 - 120 * M_H * sin - 30 * M_H**3 * sin * cos**2 + 80 * M_H**2 * sin**2 + 9 * M_H**4 * sin**2 * cos**2 - 20 * M_H**3 * sin**3
L_M = cos * (1 - M_H * sin)**(-3) * L

p_L = 0.5 * rho * a**2 * F_H * F_T * L_M

#Total

p_total = p_T + p_L
plt.figure(1)
plt.plot(t, p_total)
plt.title('Signal in time domain')
plt.xlabel('time [s]')
plt.ylabel('acoustic pressure [Pa]')

#fundamental frequency
ff = n*N_b
print('ff',ff)

#Sampling frequency
T = t[1] - t[0]
f_s = 1/T
print('fs',f_s)

#Trying to get the one third octave frequency spectrum

test = bandpass_third_octaves(p_total, f_s,frequencies=NOMINAL_THIRD_OCTAVE_CENTER_FREQUENCIES,order=8,purge=False,zero_phase = True)
a_l = list()
i = 0
while i < 34:
    a = max(test[1][i])
    a_l.append(a)
    i+=1

f = NOMINAL_THIRD_OCTAVE_CENTER_FREQUENCIES
plt.figure(2)
plt.bar(f, np.abs(a_l))
plt.title('Supposed one third octave spectrum of the time signal')
plt.xlabel('frequency [Hz]')
plt.ylabel('acoustic pressure [Pa]')
plt.xlim(0,100)

#FFT of the time signal p_total

N = p_total.size
f = np.linspace(0, 1/T, N)
f_scaled = f[:N // 2]
p_total -= np.mean(p_total)
fft = np.fft.fft(p_total)
fft_scaled = np.abs(fft)[:N // 2] * 2 / N
plt.figure(3)
plt.bar(f_scaled, fft_scaled)
plt.title('Signal in frequency domain')
plt.xlabel('frequency [Hz]')
plt.ylabel('acoustic pressure [Pa]')
plt.xlim(0,100)
plt.show()
1
You exactly did what I have done! I think because of the low accuracy of the bandpass command this has happened. Another command should be replaced by bandpass.user13014136

1 Answers

0
votes

I think the problem is that the third octave is defined very badly. In every implementation I have seen, the method is to calculate the spectrum and then averaqge over the thirds.

You are doing it more correctly of course by first filtering the octaves and then averaging.