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)
|