01/03/24 03:43:06 C:\FFT Python 3.10\FFT_numpy_pyplot.py
   1 
#=======================================================================
   2 
# Direct and inverse FFT examples (including frequency analysis,
   3 
# spectrogram generation and "brutal" band pass filtering)
   4 
#
   5 
# Cesare Brizio, 3 January 2024
   6 
#
   7 
# Based on several examples, no creative merit on my part except
   8 
# reconciling a few variable / array names among the examples.
   9 
#
  10 
#=======================================================================
  11 
import numpy as np
  12 
from matplotlib import pyplot as plt
  13 
from scipy.io import wavfile
  14 
from scipy.fft import fft, fftfreq
  15 
from scipy.fft import rfft, rfftfreq
  16 
from scipy.fft import irfft
  17 
from scipy import signal
  18 
 
  19 
 
  20 
SAMPLE_RATE = 44100  #Samples per second
  21 
DURATION = 5  #Seconds
  22 
FREQUENCY_TEST = 2 #Hertz
  23 
 
  24 
def generate_sine_wave(freq, sample_rate, duration):
  25 
    x = np.linspace(0, duration, sample_rate * duration, endpoint=False)
  26 
    frequencies = x * freq
  27 
    #2pi because np.sin takes radians
  28 
    y = np.sin((2 * np.pi) * frequencies)
  29 
    return x, y
  30 
 
  31 
 
  32 
#======================================================
  33 
#======================================================
  34 
#
  35 
# GENERATE AND PLOT A SINE WAVE
  36 
#
  37 
#======================================================
  38 
#======================================================
  39 
 
  40 
#Generate a 2 hertz sine wave that lasts for 5 seconds
  41 
x, y = generate_sine_wave(FREQUENCY_TEST, SAMPLE_RATE, DURATION)
  42 
plt.rcParams['figure.figsize'] = [12, 7]
  43 
plt.plot(x, y)
  44 
# displaying the title
  45 
plt.title("2 Hz sine Wave generated by generate_sine_wave()\nSample rate = "+str(SAMPLE_RATE)+" Hz, Duration = "+str(DURATION)+" sec\nClose window for next step",
  46 
          fontsize=14,
  47 
          fontweight="bold",
  48 
          color="green")
  49 
plt.ylabel('SIN value')
  50 
plt.xlabel('sec')
  51 
plt.show()
  52 
 
  53 
 
  54 
#======================================================
  55 
#======================================================
  56 
#
  57 
# GENERATE, NORMALIZE, PLOT AND SAVE AS A WAVEFILE 
  58 
# THE SUM OF TWO SINE WAVES (WILL BE BAND-STOP-FILTERED
  59 
# IN SUBSEQUENT STEPS)
  60 
#
  61 
#======================================================
  62 
#======================================================
  63 
FREQUENCY_GOOD = 400
  64 
FREQUENCY_NOISE = 4000
  65 
_, nice_tone = generate_sine_wave(FREQUENCY_GOOD, SAMPLE_RATE, DURATION)
  66 
_, noise_tone = generate_sine_wave(FREQUENCY_NOISE, SAMPLE_RATE, DURATION)
  67 
noise_tone = noise_tone * 0.3
  68 
 
  69 
mixed_tone = nice_tone + noise_tone
  70 
 
  71 
normalized_tone = np.int16((mixed_tone / mixed_tone.max()) * 32767)
  72 
 
  73 
plt.rcParams['figure.figsize'] = [12, 7]
  74 
LIST_SLICE = 1000
  75 
DURATION_LIST_SLICE = round((1/SAMPLE_RATE)*LIST_SLICE,3)
  76 
FREQ_GOOD_CYCLES_PER_SLICE = round(DURATION_LIST_SLICE*FREQUENCY_GOOD,3)
  77 
plt.plot(normalized_tone[:LIST_SLICE])
  78 
# displaying the title
  79 
plt.title("Normalized tone from the sum of 2 sine waves (will be saved as mysinewave.wav)\n\u00ABGood\u00BB = "+str(FREQUENCY_GOOD)+" Hz, \u00ABNoise\u00BB = "+str(FREQUENCY_NOISE)+" Hz, Duration = first "+str(LIST_SLICE)+" entries at a sample rate of "+str(SAMPLE_RATE)+" Hz\nClose window for next step",
  80 
          fontsize=14,
  81 
          fontweight="bold",
  82 
          color="green")
  83 
plt.ylabel('Samples')
  84 
plt.xlabel('Entries at a sample rate of '+str(SAMPLE_RATE)+' Hz - 1000 entries = '+str(DURATION_LIST_SLICE)+' sec - 1000 entries = '+str(FREQ_GOOD_CYCLES_PER_SLICE)+' cycles at '+str(FREQUENCY_GOOD)+' Hz')
  85 
plt.show()
  86 
 
  87 
#Remember SAMPLE_RATE = 44100 Hz is our playback rate
  88 
wavfile.write("C:\mysinewave.wav", SAMPLE_RATE, normalized_tone)
  89 
 
  90 
 
  91 
#======================================================
  92 
#======================================================
  93 
#
  94 
# PERFORM AND DISPLAY DIRECT FFT (FROM TIME DOMAIN TO
  95 
# FREQUENCY DOMAIN) OF THE NORMALIZED COMBINATION
  96 
# OF TWO SINE WAVES (WILL BE BAND-STOP-FILTERED
  97 
# IN SUBSEQUENT STEPS). THE FFT WILL INCLUDE THE ENTIRE
  98 
# COMPLEX INPUT, INCLUDING NEGATIVE FREQUENCIES
  99 
#
 100 
#======================================================
 101 
#======================================================
 102 
 
 103 
#Number of samples in normalized_tone
 104 
N = SAMPLE_RATE * DURATION
 105 
 
 106 
yf = fft(normalized_tone)
 107 
xf = fftfreq(N, 1 / SAMPLE_RATE)
 108 
 
 109 
plt.rcParams['figure.figsize'] = [12, 7]
 110 
plt.plot(xf, np.abs(yf))
 111 
# displaying the title
 112 
plt.title("Direct FFT of the normalized tone\nFull-Range ( -"+str(SAMPLE_RATE/2)+" Hz to +"+str(SAMPLE_RATE/2)+" Hz) Pressure-Frequency Analysis\nClose window for next step",
 113 
          fontsize=14,
 114 
          fontweight="bold",
 115 
          color="green")
 116 
plt.ylabel('FFT value (sound pressure)')
 117 
plt.xlabel('Frequency')
 118 
plt.show()
 119 
 
 120 
 
 121 
 
 122 
#======================================================
 123 
#======================================================
 124 
#
 125 
# PERFORM AND DISPLAY DIRECT FFT (FROM TIME DOMAIN TO
 126 
# FREQUENCY DOMAIN) OF THE NORMALIZED COMBINATION
 127 
# OF TWO SINE WAVES (WILL BE BAND-STOP-FILTERED
 128 
# IN SUBSEQUENT STEPS). THE FFT WILL INCLUDE ONLY
 129 
# REAL INPUT (EXCLUDING NEGATIVE FREQUENCIES)
 130 
#
 131 
#======================================================
 132 
#======================================================
 133 
 
 134 
#Note the extra 'r' at the front
 135 
yf = rfft(normalized_tone)
 136 
xf = rfftfreq(N, 1 / SAMPLE_RATE)
 137 
plt.rcParams['figure.figsize'] = [12, 7]
 138 
plt.plot(xf, np.abs(yf))
 139 
# displaying the title
 140 
plt.title("Direct FFT of the normalized tone (REAL INPUT, does not compute the negative frequency terms)\nRange (0 Hz to +"+str(SAMPLE_RATE/2)+" Hz) Pressure-Frequency Analysis\nClose window for next step",
 141 
          fontsize=14,
 142 
          fontweight="bold",
 143 
          color="green")
 144 
plt.ylabel('FFT value (sound pressure)')
 145 
plt.xlabel('Frequency')
 146 
plt.show()
 147 
 
 148 
 
 149 
#======================================================
 150 
#======================================================
 151 
#
 152 
# READ THE WAVEFILE AND, USING THE WELCH() FUNCTION 
 153 
# FROM THE SIGNAL PACKAGE, PLOT THE POWER SPECTRUM
 154 
# IN dBFS.
 155 
# Welch’s method computes an estimate of the power
 156 
# spectral density by dividing the data into
 157 
# overlapping segments, computing a modified
 158 
# periodogram for each segment and averaging the
 159 
# periodograms.
 160 
#
 161 
#======================================================
 162 
#======================================================
 163 
 
 164 
segment_size = 512
 165 
 
 166 
fs, x = wavfile.read('C:\mysinewave.wav')
 167 
x = x / 32768.0  # scale signal to [-1.0 .. 1.0]
 168 
 
 169 
noverlap = segment_size / 2
 170 
f, Pxx = signal.welch(x,                        # signal
 171 
                      fs=fs,                    # sample rate
 172 
                      nperseg=segment_size,     # segment size
 173 
                      window='hann',            # window type to use e.g.hamming or hann
 174 
                      nfft=segment_size,        # num. of samples in FFT
 175 
                      detrend=False,            # remove DC part
 176 
                      scaling='spectrum',       # return power spectrum [V^2]
 177 
                      noverlap=noverlap)        # overlap between segments
 178 
 
 179 
# set 0 dB to energy of sine wave with maximum amplitude
 180 
ref = (1/np.sqrt(2)**2)   # simply 0.5 ;)
 181 
p = 10 * np.log10(Pxx/ref)
 182 
 
 183 
plt.rcParams['figure.figsize'] = [12, 7]
 184 
 
 185 
fill_to = -150 * (np.ones_like(p))  # anything below -150dB is irrelevant
 186 
plt.fill_between(f, p, fill_to )
 187 
plt.xlim([f[2], f[-1]])
 188 
plt.ylim([-150, 6])
 189 
# plt.xscale('log')   # uncomment if you want log scale on x-axis
 190 
plt.grid(True)
 191 
# displaying the title
 192 
plt.title("Direct FFT of the normalized tone (REAL INPUT, does not compute the negative frequency terms)\nRange (0 Hz to +"+str(SAMPLE_RATE/2)+" Hz) Pressure-Frequency Analysis\nClose window for next step",
 193 
          fontsize=14,
 194 
          fontweight="bold",
 195 
          color="green")
 196 
plt.xlabel('Frequency, Hz')
 197 
plt.ylabel('Power spectrum, dBFS')
 198 
plt.show()
 199 
 
 200 
 
 201 
 
 202 
#======================================================
 203 
#======================================================
 204 
#
 205 
# READ THE WAVEFILE AND, USING THE SPECTROGRAM()
 206 
# FUNCTION FROM THE SIGNAL PACKAGE, PLOT THE
 207 
# TIME/FREQUENCY SPECTROGRAM.
 208 
#
 209 
#======================================================
 210 
#======================================================
 211 
 
 212 
f, t, Sxx = signal.spectrogram(x, SAMPLE_RATE)
 213 
plt.pcolormesh(t, f, Sxx, shading='nearest') # shading may be flat, nearest, gouraud or auto
 214 
plt.rcParams['figure.figsize'] = [12, 7]
 215 
# displaying the title
 216 
plt.title("Spectrogram of the normalized tone (REAL INPUT, does not compute the negative frequency terms)\nRange (0 Hz to +"+str(SAMPLE_RATE/2)+" Hz) Pressure-Frequency Analysis\nClose window for next step",
 217 
          fontsize=14,
 218 
          fontweight="bold",
 219 
          color="green")
 220 
plt.ylabel('Frequency [Hz]')
 221 
plt.xlabel('Time [sec]')
 222 
plt.show()
 223 
 
 224 
 
 225 
#======================================================
 226 
#======================================================
 227 
#
 228 
# BAND-STOP FILTERING (BRUTAL, NO WINDOWING FUNCTION)
 229 
#
 230 
# Above, scipy.fft.rfftfreq() was used to return the
 231 
# Discrete Fourier Transform sample frequencies.
 232 
# The xf float array, returned by scipy.fft.rfftfreq()
 233 
# contains the frequency bin centers in cycles per unit
 234 
# of the sample spacing (with zero at the start). For
 235 
# instance, if the sample spacing is in seconds, then
 236 
# the frequency unit is cycles/second.
 237 
#
 238 
# The yf float array was returned by scipy.fft.rfft(),
 239 
# a function that computes the 1-D n-point discrete
 240 
# Fourier Transform (DFT) for real input.
 241 
#
 242 
# By setting to zero the yf values corresponding to
 243 
# a given xf bin, we are brutally silencing the range
 244 
# of frequencies subsumed by that bin.
 245 
#
 246 
# A more sophisticated approach to filtering, based
 247 
# on windowing functions such as the one exemplified
 248 
# above. A brutal surrogate of the windowing function
 249 
# is setting to zero also a few bins adjacent to the
 250 
# target bin.
 251 
#
 252 
#======================================================
 253 
#======================================================
 254 
 
 255 
#The maximum frequency is half the sample rate
 256 
points_per_freq = len(xf) / (SAMPLE_RATE / 2)
 257 
 
 258 
#Our target (noise) frequency is 4000 Hz
 259 
target_idx = int(points_per_freq * 4000)
 260 
#Also a small range of adjacent bins is set to zero
 261 
yf[target_idx - 1 : target_idx + 2] = 0
 262 
 
 263 
plt.rcParams['figure.figsize'] = [12, 7]
 264 
plt.plot(xf, np.abs(yf))
 265 
# displaying the title
 266 
plt.title("Effects of the \u00ABbrutal\u00BB band stop filtering of the normalized tone \nRange (0 Hz to +"+str(SAMPLE_RATE/2)+" Hz) Pressure-Frequency Analysis\nClose window for next step",
 267 
          fontsize=14,
 268 
          fontweight="bold",
 269 
          color="green")
 270 
plt.ylabel('FFT value (sound pressure)')
 271 
plt.xlabel('Frequency')
 272 
plt.show()
 273 
 
 274 
 
 275 
#======================================================
 276 
#======================================================
 277 
#
 278 
# RESTORE THE SINE WAVE (NOW NOT INCLUDING NOISE) BY
 279 
# PERFORMING AN INVERSE FFT (REAL VALUES ONLY), DISPLAY
 280 
# THE CLEAN WAVE AND SAVE IT AS A WAVEFILE.
 281 
#
 282 
#======================================================
 283 
#======================================================
 284 
 
 285 
new_sig = irfft(yf)
 286 
 
 287 
plt.rcParams['figure.figsize'] = [12, 7]
 288 
plt.plot(new_sig[:LIST_SLICE])
 289 
# displaying the title
 290 
plt.title("Normalized tone after the band-stop filtering of the noise wave (will be saved as clean.wav)\nDuration = first "+str(LIST_SLICE)+" entries at a sample rate of "+str(SAMPLE_RATE)+" Hz\nClose window to end program",
 291 
          fontsize=14,
 292 
          fontweight="bold",
 293 
          color="green")
 294 
plt.ylabel('Samples')
 295 
plt.xlabel('Entries at a sample rate of '+str(SAMPLE_RATE)+' Hz - 1000 entries = '+str(DURATION_LIST_SLICE)+' sec - 1000 entries = '+str(FREQ_GOOD_CYCLES_PER_SLICE)+' cycles at '+str(FREQUENCY_GOOD)+' Hz')
 296 
plt.show()
 297 
 
 298 
norm_new_sig = np.int16(new_sig * (32767 / new_sig.max()))
 299 
 
 300 
 
 301 
wavfile.write("C:\clean.wav", SAMPLE_RATE, norm_new_sig)