Skip to content

Commit 9dcaf23

Browse files
committed
Initial 48t 3/2 downsample filter
1 parent 20e9f56 commit 9dcaf23

12 files changed

Lines changed: 402 additions & 2 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@
4141
doc/_build/pdf/*.pdf
4242
**/doc/pdf/*.pdf
4343
**/.vscode/*
44+
**/autogen/**
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2023-2026 XMOS LIMITED.
2+
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
3+
4+
#define FUNCTION_NAME push_s32_24t
5+
6+
#define NSTACKWORDS (0)
7+
8+
#define state r0
9+
#define new_samp r1
10+
#define indx r2
11+
#define _32 r3
12+
13+
.text
14+
.issue_mode dual
15+
.globl FUNCTION_NAME;
16+
.type FUNCTION_NAME,@function
17+
.align 16
18+
.cc_top FUNCTION_NAME.function,FUNCTION_NAME
19+
20+
FUNCTION_NAME: // (int32_t * state, int32_t new_samp)
21+
dualentsp NSTACKWORDS
22+
#if (defined(__XS3A__)) // Only available for XS3 with VPU
23+
24+
// r4 - r10 are not used here
25+
26+
// Setting up the vpu and a poiter to the state[15]
27+
{ ldc _32, 32 ; ldc r11, 0 }
28+
{ ldc indx, 15 ; vsetc r11 }
29+
{ ldaw r11, state[indx] ; } // r11 -> st[15 - 23]
30+
31+
#undef indx
32+
#define buff r2
33+
34+
{ add buff, r11, 4 ; vldr r11[0] } // buff -> st[16 - 24] // vR has st[15 - 23]
35+
{ sub r11, r11, _32 ; vstr buff[0] } // r11 -> st[7 - 15] // buff has vR
36+
37+
{ add buff, r11, 4 ; vldr r11[0] } // buff -> st[8 - 16] // vR has st[7 - 15]
38+
{ sub r11, r11, _32 ; vstr buff[0] } // r11 -> st[-1 - 7] // buff has vR
39+
40+
{ add buff, r11, 4 ; vldr r11[0] } // buff -> st[0 - 8] // vR has st[-1 - 7]
41+
{ ; vstr buff[0] } // buff has vR
42+
43+
// put new_samp in state[0]
44+
{ ; stw new_samp, state[0] }
45+
46+
#endif // Only available for XS3 with VPU
47+
retsp NSTACKWORDS
48+
49+
.cc_bottom FUNCTION_NAME.function;
50+
.set FUNCTION_NAME.nstackwords,NSTACKWORDS; .global FUNCTION_NAME.nstackwords;
51+
.set FUNCTION_NAME.maxcores,1; .global FUNCTION_NAME.maxcores;
52+
.set FUNCTION_NAME.maxtimers,0; .global FUNCTION_NAME.maxtimers;
53+
.set FUNCTION_NAME.maxchanends,0; .global FUNCTION_NAME.maxchanends;
54+
55+
#undef FUNCTION_NAME
56+

lib_src/src/fixed_factor_vpu_voice/src_low_level.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@
1414
*/
1515
int32_t conv_s32_24t(const int32_t * samples, const int32_t * coef);
1616

17+
/**
18+
* @brief Perforns VPU-optimised ring buffer shift for s32 type integers
19+
*
20+
* @param state State that keep previous samples
21+
* @param new_samp New sample to put in the state
22+
* @note Both state and coef has to have 24 values int32_t in them
23+
* @note Both state and coef have to be 8 bit aligned
24+
*/
25+
void push_s32_24t(int32_t * state, int32_t new_samp);
26+
1727
/**
1828
* @brief Perforns VPU-optimised FIR filtering for s32 type integers
1929
*

lib_src/src/fixed_factor_vpu_voice/src_poly.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,29 @@ static inline void src_rat_3_2_96t_us(int32_t samp_in[2], int32_t samp_out[3], c
151151

152152
/**@}*/ // END: addtogroup src_rat_3_2_96t_us
153153

154+
155+
/**
156+
* \addtogroup src_rat_2_3_48t_ds src_rat_2_3_48t_ds
157+
*
158+
* The public API for using SRC.
159+
* @{
160+
*/
161+
162+
/**
163+
* @brief Performs VPU-optimised 48 taps polyphase rational factor 2/3 downsampling
164+
*
165+
* @param samp_in Values to be downsampled
166+
* @param samp_out Downsampled output
167+
* @param coefs_ds Two-phase FIR coefficients array with [2][24] dimensions
168+
* @param state_ds FIR state array with 48 elements in it
169+
*/
170+
static inline void src_rat_2_3_48t_ds(int32_t samp_in[3], int32_t samp_out[2], const int32_t coefs_ds[2][24], int32_t state_ds[24])
171+
{
172+
push_s32_24t(state_ds, samp_in[0]);
173+
samp_out[0] = fir_s32_24t(state_ds, coefs_ds[0], samp_in[1]) * 2;
174+
samp_out[1] = fir_s32_24t(state_ds, coefs_ds[1], samp_in[2]) * 2;
175+
}
176+
177+
/**@}*/ // END: addtogroup src_rat_2_3_48t_ds
178+
154179
#endif // _SRC_POLY_VPU_H_
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2023-2026 XMOS LIMITED.
2+
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
3+
4+
/*********************************/
5+
/* AUTOGENERATED. DO NOT MODIFY! */
6+
/*********************************/
7+
8+
// Use src_rat_fir_gen.py script to regenerate this file
9+
// python src_rat_fir_gen.py -gc True -nt 48
10+
11+
#include "src_rat_fir_48t_ds_coefs.h"
12+
#include <stdint.h>
13+
14+
/** q30 coefficients to use for the 48 -> 32 kHz polyphase rational factor 48t downsampling */
15+
const int32_t ALIGNMENT(8) src_rat_fir_48t_ds_coefs[SRC_RAT_FIR_48T_DS_NUM_PHASES][SRC_RAT_FIR_48T_DS_TAPS_PER_PHASE] = {
16+
{
17+
-836279, -1511853, 4633855, -3331393, -5205286, 14094115,
18+
-9447157, -13998500, 38329634, -27797034, -47620793, 226309753,
19+
342129244, 66484670, -71415051, 23608611, 15631718, -23045498,
20+
8619065, 5719490, -8365728, 2961045, 1793971, -2267583,
21+
},
22+
{
23+
-2267583, 1793971, 2961045, -8365728, 5719490, 8619065,
24+
-23045498, 15631718, 23608611, -71415051, 66484670, 342129244,
25+
226309753, -47620793, -27797034, 38329634, -13998500, -9447157,
26+
14094115, -5205286, -3331393, 4633855, -1511853, -836279,
27+
},
28+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2023-2026 XMOS LIMITED.
2+
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
3+
4+
/*********************************/
5+
/* AUTOGENERATED. DO NOT MODIFY! */
6+
/*********************************/
7+
8+
// Use src_rat_fir_gen.py script to regenerate this file
9+
// python src_rat_fir_gen.py -gc True -nt 48
10+
11+
#ifndef _SRC_RAT_48T_DS_COEFS_H_
12+
#define _SRC_RAT_48T_DS_COEFS_H_
13+
14+
#include <stdint.h>
15+
16+
#ifndef ALIGNMENT
17+
# ifdef __xcore__
18+
# define ALIGNMENT(N) __attribute__((aligned (N)))
19+
# else
20+
# define ALIGNMENT(N)
21+
# endif
22+
#endif
23+
24+
#define SRC_RAT_FIR_48T_DS_NUM_TAPS (48)
25+
#define SRC_RAT_FIR_48T_DS_NUM_PHASES (2)
26+
#define SRC_RAT_FIR_48T_DS_TAPS_PER_PHASE (24)
27+
28+
/** q30 coefficients to use for the 48 -> 32 kHz polyphase rational factor 48t downsampling */
29+
extern const int32_t src_rat_fir_48t_ds_coefs[SRC_RAT_FIR_48T_DS_NUM_PHASES][SRC_RAT_FIR_48T_DS_TAPS_PER_PHASE];
30+
31+
#endif // _SRC_RAT_48T_DS_COEFS_H_

python/fixed_factor_vpu_voice/src_rat_fir_gen.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,102 @@ def generate_c_file(output_path, mixed_taps_ds, mixed_taps_us, total_num_taps =
220220
'coefs_ds':coefs_ds,
221221
'coefs_us':coefs_us})
222222

223+
def gen_coefs_48t_ds():
224+
"""
225+
Generate 2-phase polyphase DS coefficients for 48t (24 taps/phase) 48->32 kHz SRC.
226+
Uses the same filter design as gen_coefs but skips the 96t-specific passband check.
227+
228+
Returns:
229+
taps[48] in float
230+
poly_ds[2][24] in float
231+
poly_ds_int[2][24] in int32
232+
"""
233+
total_num_taps_ds = 48
234+
num_phases_ds = 2
235+
lpf = signal.firwin2(total_num_taps_ds, [0, 15000, 17000, 0.5 * fs], [1, 1, 0, 0],
236+
window=("kaiser", 3.2), fs=fs)
237+
poly_ds, poly_ds_int = mix_coefs(lpf, num_phases_ds)
238+
return lpf, poly_ds, poly_ds_int
239+
240+
241+
def generate_48t_ds_header_file(output_path, filename=None):
242+
header_template = """\
243+
// Copyright 2023-2026 XMOS LIMITED.
244+
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
245+
246+
/*********************************/
247+
/* AUTOGENERATED. DO NOT MODIFY! */
248+
/*********************************/
249+
250+
// Use src_rat_fir_gen.py script to regenerate this file
251+
// python src_rat_fir_gen.py -gc True -nt 48
252+
253+
#ifndef _SRC_RAT_48T_DS_COEFS_H_
254+
#define _SRC_RAT_48T_DS_COEFS_H_
255+
256+
#include <stdint.h>
257+
258+
#ifndef ALIGNMENT
259+
# ifdef __xcore__
260+
# define ALIGNMENT(N) __attribute__((aligned (N)))
261+
# else
262+
# define ALIGNMENT(N)
263+
# endif
264+
#endif
265+
266+
#define SRC_RAT_FIR_48T_DS_NUM_TAPS (48)
267+
#define SRC_RAT_FIR_48T_DS_NUM_PHASES (2)
268+
#define SRC_RAT_FIR_48T_DS_TAPS_PER_PHASE (24)
269+
270+
/** q30 coefficients to use for the 48 -> 32 kHz polyphase rational factor 48t downsampling */
271+
extern const int32_t src_rat_fir_48t_ds_coefs[SRC_RAT_FIR_48T_DS_NUM_PHASES][SRC_RAT_FIR_48T_DS_TAPS_PER_PHASE];
272+
273+
#endif // _SRC_RAT_48T_DS_COEFS_H_
274+
"""
275+
if filename is None:
276+
filename = "src_rat_fir_48t_ds_coefs.h"
277+
header_path = Path(output_path) / filename
278+
with open(header_path, "w") as header_file:
279+
header_file.write(header_template)
280+
281+
282+
def generate_48t_ds_c_file(output_path, mixed_taps_ds_48t):
283+
tph_ds = 24
284+
num_phases_ds = 2
285+
c_template = """\
286+
// Copyright 2023-2026 XMOS LIMITED.
287+
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
288+
289+
/*********************************/
290+
/* AUTOGENERATED. DO NOT MODIFY! */
291+
/*********************************/
292+
293+
// Use src_rat_fir_gen.py script to regenerate this file
294+
// python src_rat_fir_gen.py -gc True -nt 48
295+
296+
#include "src_rat_fir_48t_ds_coefs.h"
297+
#include <stdint.h>
298+
299+
/** q30 coefficients to use for the 48 -> 32 kHz polyphase rational factor 48t downsampling */
300+
const int32_t ALIGNMENT(8) src_rat_fir_48t_ds_coefs[SRC_RAT_FIR_48T_DS_NUM_PHASES][SRC_RAT_FIR_48T_DS_TAPS_PER_PHASE] = {
301+
%(coefs_ds)s
302+
};
303+
"""
304+
coefs_ds = ''
305+
for phase in range(num_phases_ds):
306+
coefs_ds += ' {\n '
307+
for tap in range(tph_ds):
308+
coefs_ds += ' ' + str(mixed_taps_ds_48t[phase][tap]).rjust(12) + ','
309+
if (((tap + 1) % 6) == 0):
310+
coefs_ds += '\n '
311+
coefs_ds += '},\n'
312+
313+
filename = "src_rat_fir_48t_ds_coefs.c"
314+
c_path = Path(output_path) / filename
315+
with open(c_path, "w") as c_file:
316+
c_file.write(c_template % {'coefs_ds': coefs_ds})
317+
318+
223319
if __name__ == "__main__":
224320
parser = argparse.ArgumentParser("Generate FIR coefficiens for a 48 - 32 kHz polyphase SRC")
225321
parser.add_argument('--output_dir','-o', help='output path for filter files')

tests/sim_tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ add_subdirectory(unity_gain_voice_test)
1212
add_subdirectory(us3_voice_test)
1313
add_subdirectory(vpu_ff3_test)
1414
add_subdirectory(vpu_rat_test)
15+
add_subdirectory(vpu_rat_48t_test)

tests/sim_tests/test_voice_vpu_rat.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,46 @@ def test_src_vpu_rat_prepare():
149149
build_c(poly_ds_int, poly_us_int)
150150

151151

152+
def build_c_48t(poly_ds_48t_int):
153+
coeffs_path = Path(__file__).resolve().parent / "vpu_rat_48t_test" / "autogen"
154+
coeffs_path.mkdir(exist_ok=True, parents=True)
155+
gf.generate_48t_ds_header_file(coeffs_path)
156+
gf.generate_48t_ds_c_file(coeffs_path, poly_ds_48t_int)
157+
# Note: The autogenerated files are not compiled as part of the application. The copies of these committed in lib_src are compiled instead
158+
build_firmware_xcommon_cmake(Path(__file__).parent / "vpu_rat_48t_test")
159+
160+
161+
def run_c_48t(fc_ex):
162+
file_path = Path(__file__).parent
163+
testname = "vpu_rat_48t_test"
164+
xe = file_path / testname / "bin" / f"{testname}.xe"
165+
app = f"xsim {xe}"
166+
subprocess.check_output(app.split())
167+
168+
sig_bin = Path("sig_c_32k.bin")
169+
assert sig_bin.is_file(), "could not find output bin"
170+
sig32k_int = np.fromfile(sig_bin, dtype=np.int32)
171+
172+
thdn, freq = THDN_and_freq(sig32k_int.astype(np.float64), 32000)
173+
print(f"C 32k THDN: {thdn}, fc: {freq}")
174+
assert_thdn_and_fc(thdn, freq, -60, fc_ex)
175+
176+
sig_bin = Path("sig_c_48k.bin")
177+
assert sig_bin.is_file(), "could not find output bin"
178+
sig48k_int = np.fromfile(sig_bin, dtype=np.int32)
179+
180+
thdn, freq = THDN_and_freq(sig48k_int.astype(np.float64), 48000)
181+
print(f"C 48k THDN: {thdn}, fc: {freq}")
182+
assert_thdn_and_fc(thdn, freq, -50, fc_ex)
183+
184+
185+
@pytest.mark.prepare
186+
def test_src_vpu_rat_48t_prepare():
187+
print(f"Preparing rat 48t test")
188+
_, _, poly_ds_48t_int = gf.gen_coefs_48t_ds() # 2 phases x 24 taps, uses push_s32_24t and fir_s32_24t
189+
build_c_48t(poly_ds_48t_int)
190+
191+
152192
@pytest.mark.parametrize(
153193
"test_freq", [
154194
100, 14000
@@ -166,3 +206,36 @@ def test_src_vpu_rat(test_freq):
166206
sig32k = downsample(sig_fl, taps, poly_ds, test_freq)
167207
upsample(sig32k, taps, poly_us, test_freq)
168208
run_c(test_freq)
209+
210+
211+
@pytest.mark.parametrize(
212+
"test_freq", [
213+
100, 14000
214+
]
215+
)
216+
@pytest.mark.main
217+
def test_src_vpu_rat_48t(test_freq):
218+
"""Test src_rat_2_3_48t_ds (exercises push_s32_24t and fir_s32_24t).
219+
Downsamples 48k->32k using 48t DS filter; upsamples back 32k->48k using existing 96t US filter.
220+
"""
221+
print(f"Testing 48t rat DS {test_freq} Hz sinewave")
222+
total_num_taps_us = 8 * 2 * 3 * 2 # 96 taps for US (reuse existing 96t design)
223+
taps_ds, poly_ds_48t, poly_ds_48t_int = gf.gen_coefs_48t_ds() # 2 phases x 24 taps per phase
224+
taps_us, _, _, poly_us, poly_us_int = gf.gen_coefs(total_num_taps_us)
225+
226+
working_dir = Path(tempfile.mkdtemp(prefix="test_src_vpu_rat_48t", dir=Path(__file__).parent))
227+
with tmp_dir(working_dir):
228+
sig_fl, sig_int = get_sig(test_freq)
229+
230+
# Python-side downsampling check with bounds appropriate for a 48t filter
231+
sig32k = np.zeros(len(sig_fl) * 2 // 3)
232+
buff = np.zeros(len(sig_fl) * 2)
233+
buff[0::2] = sig_fl
234+
buff = signal.convolve(buff, taps_ds, "same", "direct") * 2
235+
sig32k = buff[0::3]
236+
thdn, freq = THDN_and_freq(sig32k.astype(np.float64), 32000)
237+
print(f"PY 32k THDN: {thdn}, fc: {freq}")
238+
assert_thdn_and_fc(thdn, freq, -60, test_freq)
239+
240+
upsample(sig32k, taps_us, poly_us, test_freq)
241+
run_c_48t(test_freq)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
cmake_minimum_required(VERSION 3.21)
2+
include($ENV{XMOS_CMAKE_PATH}/xcommon.cmake)
3+
4+
if(NOT BUILD_NATIVE)
5+
project(vpu_rat_48t_test)
6+
7+
set(APP_HW_TARGET XK-EVK-XU316)
8+
9+
set(APP_PCA_ENABLE ON)
10+
11+
set(APP_COMPILER_FLAGS "-g"
12+
"-O3"
13+
"-mno-dual-issue"
14+
)
15+
16+
include(${CMAKE_CURRENT_LIST_DIR}/../../../examples/deps.cmake)
17+
18+
set(XMOS_SANDBOX_DIR ${CMAKE_CURRENT_LIST_DIR}/../../../../)
19+
20+
XMOS_REGISTER_APP()
21+
endif()

0 commit comments

Comments
 (0)