├── LICENSE ├── README.md └── pfdkf.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Shimin Zhang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PFDKF 2 | partition block based frequency domain Kalman filter 3 | 4 | Reference paper: STATE-SPACE ARCHITECTURE OF THE PARTITIONED-BLOCK-BASED ACOUSTIC ECHO CONTROLLER 5 | 6 | ## Note 7 | The `filter` and `update` API refer to https://github.com/ewan-xu/pyaec. 8 | 9 | If you use it in your research or anything else, please cite the implementation source (https://github.com/echocatzh/PFDKF/) 10 | 11 | ## TODO (if I'm free): 12 | - update the unconstrained version. 13 | - corresponding torch-kalman impl. 14 | - data-driven based pfdkf 15 | 16 | ## Any bugs? 17 | Discussion is welcome, which will help me continue to improve this repo. 18 | -------------------------------------------------------------------------------- /pfdkf.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Shimin Zhang 2 | # Version: 1.0 3 | # 4 | # reference paper: "STATE-SPACE ARCHITECTURE OF THE PARTITIONED-BLOCK-BASED 5 | # ACOUSTIC ECHO CONTROLLER" 6 | # ============================================================================= 7 | 8 | """ Partitioned-Block-Based Frequency Domain Kalman Filter """ 9 | 10 | import numpy as np 11 | from numpy.fft import rfft as fft 12 | from numpy.fft import irfft as ifft 13 | 14 | 15 | class PFDKF: 16 | def __init__(self, N, M, A=0.999, P_initial=1, keep_m_gate=0.5, res=True): 17 | """Initial state of partitioned block based frequency domain kalman filter 18 | 19 | Args: 20 | N (int): Num of blocks. 21 | M (int): Filter length in one block. 22 | A (float, optional): Diag coeff, if more nonlinear components, 23 | can be set to 0.99. Defaults to 0.999. 24 | P_initial (int, optional): About the begining covergence. 25 | Defaults to 10. 26 | keep_m_gate (float, optional): When more linear, 27 | can be set to 0.2 or less. Defaults to 0.5. 28 | """ 29 | # M = 2*V 30 | self.N = N 31 | self.M = M 32 | self.A = A 33 | self.A2 = A**2 34 | self.m_smooth_factor = keep_m_gate 35 | self.res = res 36 | 37 | self.x = np.zeros(shape=(2*self.M), dtype=np.float32) 38 | self.m = np.zeros(shape=(self.M + 1), dtype=np.float32) 39 | self.P = np.full((self.N, self.M + 1), P_initial) 40 | self.X = np.zeros((self.N, self.M + 1), dtype=complex) 41 | self.H = np.zeros((self.N, self.M + 1), dtype=complex) 42 | self.mu = np.zeros((self.N, self.M + 1), dtype=complex) 43 | self.half_window = np.concatenate(([1]*self.M, [0]*self.M)) 44 | 45 | def filt(self, x, d): 46 | assert(len(x) == self.M) 47 | self.x = np.concatenate([self.x[self.M:], x]) 48 | X = fft(self.x) 49 | self.X[1:] = self.X[:-1] 50 | self.X[0] = X 51 | Y = np.sum(self.H*self.X, axis=0) 52 | y = ifft(Y).real[self.M:] 53 | e = d-y 54 | 55 | e_fft = np.concatenate( 56 | (np.zeros(shape=(self.M,), dtype=np.float32), e)) 57 | self.E = fft(e_fft) 58 | self.m = self.m_smooth_factor * self.m + \ 59 | (1-self.m_smooth_factor) * np.abs(self.E)**2 60 | R = np.sum(self.X*self.P*self.X.conj(), 0) + 2*self.m/self.N 61 | self.mu = self.P / (R + 1e-10) 62 | if self.res: 63 | W = 1 - np.sum(self.mu*np.abs(self.X)**2, 0) 64 | E_res = W*self.E 65 | e = ifft(E_res).real[self.M:].real 66 | y = d-e 67 | return e, y 68 | 69 | def update(self): 70 | G = self.mu*self.X.conj() 71 | self.P = self.A2*(1 - 0.5*G*self.X)*self.P + \ 72 | (1-self.A2)*np.abs(self.H)**2 73 | self.H = self.A*(self.H + fft(self.half_window*(ifft(self.E*G).real))) 74 | 75 | 76 | def pfdkf(x, d, N=10, M=256, A=0.999, P_initial=1, keep_m_gate=0.1): 77 | ft = PFDKF(N, M, A, P_initial, keep_m_gate) 78 | num_block = min(len(x), len(d)) // M 79 | 80 | e = np.zeros(num_block*M) 81 | y = np.zeros(num_block*M) 82 | for n in range(num_block): 83 | x_n = x[n*M:(n+1)*M] 84 | d_n = d[n*M:(n+1)*M] 85 | e_n, y_n = ft.filt(x_n, d_n) 86 | ft.update() 87 | e[n*M:(n+1)*M] = e_n 88 | y[n*M:(n+1)*M] = y_n 89 | return e, y 90 | 91 | 92 | if __name__ == "__main__": 93 | import soundfile as sf 94 | mic, sr = sf.read("d.wav") 95 | ref, sr = sf.read("u.wav") 96 | # for linear senario: 97 | # error, echo = pfdkf(ref, mic, A=0.999, keep_m_gate=0.2) 98 | # for non-linear senario: 99 | error, echo = pfdkf(ref, mic, A=0.999, keep_m_gate=0.5) 100 | sf.write("out_kalman.wav", error, sr) 101 | sf.write("out_echo.wav", echo, sr) 102 | --------------------------------------------------------------------------------