LASP 1.0
Library for Acoustic Signal Processing
Loading...
Searching...
No Matches
tools.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3"""
4Author: T. Hekman, C. Jansen, J.A. de Jong - ASCEE V.O.F.
5
6Smooth data in the frequency domain.
7
8TODO: This function is rather slow as it uses [for loops] in Python. Speed up.
9NOTE: function requires lin frequency spaced input data
10TODO: accept input data that is not lin spaced in frequency
11TODO: it makes more sense to output data that is log spaced in frequency
12TODO: Make SmoothSpectralData() multi-dimensional array aware.
13TODO: Smoothing does not work due to complex numbers. Is it reasonable to
14smooth complex data? If so, the data could be split in magnitude and phase.
15"""
16
17__all__ = ['SmoothingType', 'smoothSpectralData', 'SmoothingWidth']
18
19from enum import Enum, unique
20import copy
21import numpy as np
22from numpy import log2, pi, sin
23
24
25@unique
26class SmoothingWidth(Enum):
27 none = (0, 'No smoothing')
28 one = (1, '1/1st octave smoothing')
29 two = (2, '1/2th octave smoothing')
30 three = (3, '1/3rd octave smoothing')
31 six = (6, '1/6th octave smoothing')
32 twelve = (12, '1/12th octave smoothing')
33 twfo = (24, '1/24th octave smoothing')
34 ftei = (48, '1/48th octave smoothing')
35 hundred = (100, '1/100th octave smoothing') # useful for removing 'grass'
36
37 @staticmethod
38 def fillComboBox(cb):
39 """
40 Fill Windows to a combobox
41
42 Args:
43 cb: QComboBox to fill
44 """
45 cb.clear()
46 for w in list(SmoothingWidth):
47 cb.addItem(w.value[1], w)
48 cb.setCurrentIndex(0)
49
50 @staticmethod
51 def getCurrent(cb):
52 return list(SmoothingWidth)[cb.currentIndex()]
53
54
56 levels = 'l', 'Levels' # [dB]
57 # tf = 'tf', 'Transfer function',
58 ps = 'ps', '(Auto) powers'
59
60
61# TO DO: check if everything is correct
62# TO DO: add possibility to insert data that is not lin spaced in frequency
63# Integrated Hann window
64
65def intHann(x1, x2):
66 """
67 Calculate integral of (part of) Hann window.
68 If the args are vectors, the return value will match those.
69
70 Args:
71 x1: lower bound [-0.5, 0.5]
72 x2: upper bound [-0.5, 0.5]
73 Return:
74 Integral of Hann window between x1 and x2
75 """
76 x1 = np.clip(x1, -0.5, 0.5)
77 x2 = np.clip(x2, -0.5, 0.5)
78 return (sin(2*pi*x2) - sin(2*pi*x1))/(2*pi) + (x2-x1)
79
80
81def smoothCalcMatrix(freq, sw: SmoothingWidth):
82 """
83 Args:
84 freq: array of frequencies of data points [Hz] - equally spaced
85 sw: SmoothingWidth
86
87 Returns:
88 freq: array frequencies of data points [Hz]
89 Q: matrix to smooth, power: {fsm} = [Q] * {fraw}
90
91 Warning: this method does not work on levels (dB)
92
93 According to Tylka_JAES_SmoothingWeights.pdf
94 "A Generalized Method for Fractional-Octave Smoothing of Transfer Functions
95 that Preserves Log-Frequency Symmetry"
96 https://doi.org/10.17743/jaes.2016.0053
97 par 1.3
98 eq. 16
99 """
100 # Settings
101 Noct = sw.value[0]
102 assert Noct > 0, "'Noct' must be absolute positive"
103 assert np.isclose(freq[-1]-freq[-2], freq[1]-freq[0]), "Input data must "\
104 "have a linear frequency spacing"
105 if Noct < 1:
106 raise Warning('Check if \'Noct\' is entered correctly')
107
108 # Initialize
109 L = len(freq)
110 Q = np.zeros(shape=(L, L), dtype=np.float16) # float16: keep size small
111 Q[0, 0] = 1 # in case first point is skipped
112 x0 = 1 if freq[0] == 0 else 0 # skip first data point if zero frequency
113
114 Noct /= 2 # corr. factor: window @ -3dB at Noct bounds (Noct/1.5 for -6dB)
115 ifreq = freq/(freq[1]-freq[0]) # frequency, normalized to step=1
116 ifreq = np.array(ifreq.astype(int))
117 ifreqMin = ifreq[x0] # min. freq, normalized to step=1
118 ifreqMax = ifreq[L-1] # max. freq, normalized to step=1
119 sfact = 2**((1/Noct)/2) # bounds are this factor from the center freq
120
121 kpmin = np.floor(ifreq/sfact).astype(int) # min freq of window
122 kpmax = np.ceil(ifreq*sfact).astype(int) # max freq of window
123
124 for ff in range(x0, L): # loop over input freq
125 # Find window bounds and actual smoothing width
126 # Keep window symmetrical if one side is truncated
127 if kpmin[ff] < ifreqMin:
128 kpmin[ff] = ifreqMin
129 kpmax[ff] = np.ceil(ifreq[ff]**2/ifreqMin) # decrease smooth width
130 if np.isclose(kpmin[ff], kpmax[ff]):
131 kpmax[ff] += 1
132 NoctAct = 1/log2(kpmax[ff]/kpmin[ff])
133 elif kpmax[ff] > ifreqMax:
134 kpmin[ff] = np.floor(ifreq[ff]**2/ifreqMax) # decrease smoothwidth
135 kpmax[ff] = ifreqMax
136 if np.isclose(kpmin[ff], kpmax[ff]):
137 kpmin[ff] -= 1
138 NoctAct = 1/log2(kpmax[ff]/kpmin[ff])
139 else:
140 NoctAct = Noct
141
142 kp = np.arange(kpmin[ff], kpmax[ff]+1) # freqs of window
143 Phi1 = log2((kp - 0.5)/ifreq[ff]) * NoctAct # integr. bounds Hann wind
144 Phi2 = log2((kp + 0.5)/ifreq[ff]) * NoctAct
145 W = intHann(Phi1, Phi2) # smoothing window
146 Q[ff, kpmin[ff]-ifreq[0]:kpmax[ff]-ifreq[0]+1] = W
147
148 # Normalize to conserve input power
149 Qpower = np.sum(Q, axis=0)
150 Q = Q / Qpower[np.newaxis, :]
151
152 return Q
153
154
155def smoothSpectralData(freq, M, sw: SmoothingWidth,
156 st: SmoothingType = SmoothingType.levels):
157 """
158 Apply fractional octave smoothing to data in the frequency domain.
159
160 Args:
161 freq: array of frequencies of data points [Hz] - equally spaced
162 M: array of data, either power or dB
163 the smoothing type `st`, the smoothing is applied.
164 sw: smoothing width
165 st: smoothing type = data type of input data
166
167 Returns:
168 freq : array frequencies of data points [Hz]
169 Msm : float smoothed magnitude of data points
170 """
171 # Safety
172 MM = copy.deepcopy(M)
173 Noct = sw.value[0]
174 assert len(MM) > 0, "Smoothing function: input array is empty" # not sure if this works
175 assert Noct > 0, "'Noct' must be absolute positive"
176 if Noct < 1:
177 raise Warning('Check if \'Noct\' is entered correctly')
178 assert len(freq) == len(MM), "f and M should have equal length"
179
180 if st == SmoothingType.ps:
181 assert np.min(MM) >= 0, 'Power spectrum values cannot be negative'
182 if st == SmoothingType.levels and isinstance(MM.dtype, complex):
183 raise RuntimeError('Decibel input should be real-valued')
184
185 # Convert to power
186 if st == SmoothingType.levels:
187 P = 10**(MM/10)
188 elif st == SmoothingType.ps:
189 P = MM
190 else:
191 raise RuntimeError(f"Incorrect SmoothingType: {st}")
192
193 # P is power while smoothing. x are indices of P.
194 Psm = np.zeros_like(P) # Smoothed power - to be calculated
195 if freq[0] == 0:
196 Psm[0] = P[0] # Reuse old value in case first data..
197 # ..point is skipped. Not plotted any way.
198
199 # Re-use smoothing matrix Q if available. Otherwise, calculate.
200 # Store in dict 'Qdict'
201 nfft = int(2*(len(freq)-1))
202 key = f"nfft{nfft}_Noct{Noct}" # matrix name
203
204 if 'Qdict' not in globals(): # Guarantee Qdict exists
205 global Qdict
206 Qdict = {}
207
208 if key in Qdict:
209 Q = Qdict[key]
210 else:
211 Q = smoothCalcMatrix(freq, sw)
212 Qdict[key] = Q
213
214 # Apply smoothing
215 Psm = np.matmul(Q, P)
216
217 # Convert to original format
218 if st == SmoothingType.levels:
219 Psm = 10*np.log10(Psm)
220
221 return Psm
222
223
224# %% Test
225if __name__ == "__main__":
226 """ Test function for evaluation and debugging
227
228 Note: make a distinction between lin and log spaced (in frequency) data
229 points. They should be treated and weighted differently.
230 """
231 import matplotlib.pyplot as plt
232 # Initialize
233 Noct = 2 # Noct = 6 for 1/6 oct. smoothing
234
235 # Create dummy data set 1: noise
236 fmin = 1e3 # [Hz] min freq
237 fmax = 24e3 # [Hz] max freq
238 Ndata = 200 # number of data points
239 freq = np.linspace(fmin, fmax, Ndata) # frequency points
240 # freq = np.hstack((0, freq))
241 M = abs(0.4*np.random.normal(size=(len(freq),)))+0.01 #
242 M = 20*np.log10(M)
243
244# # Create dummy data set 2: dirac delta
245# fmin = 3e3 # [Hz] min freq
246# fmax = 24e3 # [Hz] max freq
247# Ndata = 200 # number of data points
248# freq = np.linspace(fmin, fmax, Ndata) # frequency points
249# M = 0 * abs(1+0.4*np.random.normal(size=(Ndata,))) + 0.01 #
250# M[int(100)] = 1
251# M = 20*np.log10(M)
252
253 # Apply function
254 class sw:
255 value = [Noct]
256 st = SmoothingType.levels # so data is given in dB
257 # st = SmoothingType.ps # so data is given in power
258
259 # Smooth
260 Msm = smoothSpectralData(freq, M, sw, st)
261 fsm = freq
262
263
264 # Plot - lin frequency
265 plt.figure()
266 plt.plot(freq, M, '.b')
267 plt.plot(fsm, Msm, 'r')
268 plt.xlabel('f (Hz)')
269 plt.ylabel('magnitude')
270 plt.xlim([100, fmax])
271 plt.title('lin frequency')
272 plt.legend(['Raw', 'Smooth'])
273
274 # Plot - log frequency
275 plt.figure()
276 plt.semilogx(freq, M, '.b')
277 plt.semilogx(fsm, Msm, 'r')
278 plt.xlabel('f (Hz)')
279 plt.ylabel('magnitude')
280 plt.xlim([100, fmax])
281 plt.title('log frequency')
282 plt.legend(['Raw', 'Smooth 1'])
fillComboBox(cb)
Fill Windows to a combobox.
Definition tools.py:38
smoothSpectralData(freq, M, SmoothingWidth sw, SmoothingType st=SmoothingType.levels)
Apply fractional octave smoothing to data in the frequency domain.
Definition tools.py:156
intHann(x1, x2)
Calculate integral of (part of) Hann window.
Definition tools.py:65
smoothCalcMatrix(freq, SmoothingWidth sw)
Args: freq: array of frequencies of data points [Hz] - equally spaced sw: SmoothingWidth.
Definition tools.py:81