Skip to content

Commit f074125

Browse files
authored
Merge pull request #375 from tlecomte/fixFftSmooth
fix(Spectrum): copy data in harmonic product estimation
2 parents 1292acf + 3103ec6 commit f074125

File tree

3 files changed

+83
-29
lines changed

3 files changed

+83
-29
lines changed

friture/spectrum.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
# along with Friture. If not, see <http://www.gnu.org/licenses/>.
1919

2020
from PyQt5.QtCore import QObject
21-
from numpy import log10, argmax, zeros, arange, floor, float64
21+
from numpy import log10, argmax, zeros, arange, floor, float64, ones
2222
from friture.audioproc import audioproc # audio processing class
2323
from friture.spectrum_settings import (Spectrum_Settings_Dialog, # settings dialog
2424
DEFAULT_FFT_SIZE,
@@ -105,11 +105,21 @@ def harmonic_product_spectrum(self, sp):
105105
# for pitch detection are good.
106106
product_count = 3
107107

108-
# Downsample and multiply
109108
harmonic_length = sp.shape[0] // product_count
110-
res = sp[:harmonic_length]
111-
for i in range(2, product_count + 1):
112-
res *= sp[::i][:harmonic_length]
109+
110+
# common case: product_count == 3 — multiply the three slices
111+
# directly. This avoids allocating a temporary ones array and still
112+
# doesn't make copies of the original `sp` beyond NumPy's routine
113+
# temporary results for the elementwise multiplication.
114+
if product_count == 3:
115+
# Downsample and multiply
116+
res = (sp[:harmonic_length]
117+
* sp[::2][:harmonic_length]
118+
* sp[::3][:harmonic_length])
119+
else:
120+
res = ones(harmonic_length, dtype=sp.dtype)
121+
for i in range(1, product_count + 1):
122+
res *= sp[::i][:harmonic_length]
113123
return res
114124

115125
def handle_new_data(self, floatdata):
@@ -191,7 +201,14 @@ def setresponsetime(self, response_time):
191201
self.response_time = response_time
192202

193203
# an exponential smoothing filter is a simple IIR filter
194-
# s_i = alpha*x_i + (1-alpha)*s_{i-1}
204+
# y_i = a*x_i + (1-a)*y_{i-1}
205+
#
206+
# we can unroll the recurrence a bit:
207+
# y_{i+1} = a*x_{i+1} + (1-a)*y_i
208+
# = a*x_{i+1} + (1-a)*a*x_i + (1-a)^2*y_{i-1}
209+
# y_{i+2} = a*x_{i+2} + (1-a)*y_{i+1}
210+
# = a*x_{i+2} + (1-a)*a*x_{i+1} + (1-a)^2*a*x_i + (1-a)^3*y_{i-1}
211+
# ...
195212
# we compute alpha so that the N most recent samples represent 100*w percent of the output
196213
w = 0.65
197214
delta_n = self.fft_size * (1. - self.overlap)

friture/spectrum_settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ def responsetimechanged(self, index):
190190
response_time = 1.
191191
elif index == 4:
192192
response_time = 5.
193-
self.logger.info("responsetimechanged slot %d %d", index, response_time)
193+
self.logger.info("responsetimechanged slot %d %f", index, response_time)
194194
self.view_model.setresponsetime(response_time)
195195

196196
# method

friture_extensions/exp_smoothing_conv.pyx

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,48 +15,85 @@ def pyx_exp_smoothed_value(np.ndarray[dtype_t, ndim=1] kernel, dtype_t alpha, np
1515
cdef Py_ssize_t Nk = kernel.shape[0]
1616
cdef Py_ssize_t i
1717
cdef dtype_t value, conv
18+
cdef double a = (1.-alpha)**N
1819

1920
# as we disable the cython bounds checking, do it by ourselves
2021
# it is absolutely needed as the kernel is not of infinite size !!
2122
if N > Nk:
2223
N = Nk
24+
a = 0.
2325

2426
if N == 0:
2527
value = previous
26-
else:
27-
conv = 0.
28-
29-
for i in range(0, N):
30-
conv = conv + kernel[Nk - N + i]*data[i]
31-
32-
value = alpha * conv + previous*(1.-alpha)**N
28+
29+
conv = 0.
30+
31+
for i in range(0, N):
32+
conv = conv + kernel[Nk - N + i]*data[i]
33+
34+
value = alpha * conv + previous*a
3335

3436
return value
3537

38+
3639
def pyx_exp_smoothed_value_numpy(np.ndarray[dtype_t, ndim=1] kernel, dtype_t alpha, np.ndarray[dtype_t, ndim=2] data, np.ndarray[dtype_t, ndim=1] previous):
37-
cdef Py_ssize_t N = data.shape[1]
40+
"""
41+
Compute exponential filtering of data with given kernel and alpha.
42+
The filter is applied on the 2nd axis (axis=1), independently for each row (axis=0).
43+
44+
This is equivalent to applying the following IIR filter on each row of data:
45+
y_i = alpha*x_i + (1-alpha)*y_{i-1}
46+
where x_i is the input data, y_i the filtered data, and y_{i-1} the previous filtered value.
47+
48+
We can unroll the recurrence a bit:
49+
y_{i+1} = a*x_{i+1} + (1-a)*y_i
50+
= a*x_{i+1} + (1-a)*a*x_i + (1-a)^2*y_{i-1}
51+
y_{i+2} = a*x_{i+2} + (1-a)*y_{i+1}
52+
= a*x_{i+2} + (1-a)*a*x_{i+1} + (1-a)^2*a*x_i + (1-a)^3*y_{i-1}
53+
...
54+
55+
This unrolling can be implemented with a convolution of a precomputed kernel.
56+
57+
By using a precomputed kernel, this function is optimized to process a large number of samples at once.
58+
59+
Parameters:
60+
-----------
61+
kernel : 1D array
62+
the pre-computed convolution kernel for the exponential smoothing
63+
alpha : float
64+
the exponential smoothing factor, between 0 and 1
65+
data : 2D array
66+
the input data to filter
67+
previous : 1D array
68+
the previous filtered value
69+
Returns:
70+
--------
71+
1D array
72+
the filtered value"""
3873
cdef Py_ssize_t Nf = data.shape[0]
74+
cdef Py_ssize_t Nt = data.shape[1]
3975
cdef Py_ssize_t Nk = kernel.shape[0]
4076
cdef Py_ssize_t i, j
4177
cdef np.ndarray[dtype_t, ndim=1] value, conv
42-
cdef double a = (1.-alpha)**N
78+
cdef double a = (1.-alpha)**Nt
4379

4480
# as we disable the cython bounds checking, do it by ourselves
4581
# it is absolutely needed as the kernel is not of infinite size !!
46-
if N > Nk:
47-
N = Nk
82+
if Nt > Nk:
83+
Nt = Nk
84+
a = 0.
4885

49-
if N == 0:
86+
if Nt == 0:
5087
return previous
51-
else:
52-
conv = np.zeros(Nf)
53-
value = np.zeros(Nf)
54-
55-
for i in range(0, N):
56-
for j in range(Nf):
57-
conv[j] = conv[j] + kernel[Nk - N + i]*data[j, i]
58-
88+
89+
conv = np.zeros(Nf, dtype=dtype)
90+
value = np.zeros(Nf, dtype=dtype)
91+
92+
for i in range(0, Nt):
5993
for j in range(Nf):
60-
value[j] = alpha * conv[j] + previous[j]*a
94+
conv[j] = conv[j] + kernel[Nk - Nt + i]*data[j, i]
6195

62-
return value
96+
for j in range(Nf):
97+
value[j] = alpha * conv[j] + previous[j]*a
98+
99+
return value

0 commit comments

Comments
 (0)