Files
3d_audio/lib_audio_dsp/test/pipeline/test_pipeline.py

263 lines
9.2 KiB
Python
Raw Normal View History

2025-12-11 09:43:42 +08:00
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
from pathlib import Path
import scipy.io.wavfile
import numpy as np
import subprocess
import pytest
from copy import deepcopy
import re
import subprocess
from filelock import FileLock
import os
import shutil
from audio_dsp.design.pipeline import Pipeline
from audio_dsp.stages.biquad import Biquad
from audio_dsp.stages.cascaded_biquads import CascadedBiquads
from audio_dsp.stages.signal_chain import Bypass
from audio_dsp.stages.limiter import LimiterRMS, LimiterPeak
from audio_dsp.design.pipeline import generate_dsp_main
import audio_dsp.dsp.signal_gen as gen
from audio_dsp.dsp.generic import HEADROOM_BITS
import audio_dsp.dsp.utils as utils
from python import build_utils, run_pipeline_xcoreai, audio_helpers
from stages.add_n import AddN
# Test config
PKG_DIR = Path(__file__).parent
APP_DIR = PKG_DIR
BUILD_DIR = APP_DIR / "build"
num_in_channels = 2
num_out_channels = 2
input_dtype = np.int32
test_duration = 0.1 # in seconds
infile = "test_input.wav"
outfile = "test_output.wav"
Fs = 48000
def gen_build(app_dir, p, target):
with FileLock(build_utils.BUILD_LOCK):
# Autogenerate C code
generate_dsp_main(p, out_dir = BUILD_DIR / "dsp_pipeline_default")
# Build pipeline test executable. This will download xscope_fileio if not present
build_utils.build(APP_DIR, BUILD_DIR, target)
os.makedirs(app_dir / "bin", exist_ok=True)
shutil.copytree(APP_DIR / "bin", app_dir / "bin", dirs_exist_ok=True)
def create_pipeline():
# Create pipeline
p, i = Pipeline.begin(num_in_channels)
bi = p.stage(Biquad, i, label="biquad")
cb = p.stage(CascadedBiquads, bi, label="casc_biquad")
by = p.stage(Bypass, cb, label="byp_1")
lp = p.stage(LimiterPeak, by)
lr = p.stage(LimiterRMS, lp)
p.next_thread() # ch 0 1
by1 = p.stage(Bypass, lr, label="byp_2")
p.set_outputs(by1)
stages = 2
return p, stages
def test_pipeline():
"""
Basic test playing a sine wave through a stage
"""
p, n_stages = create_pipeline()
app_dir = PKG_DIR / "test_pipeline"
os.makedirs(app_dir, exist_ok=True)
target = "default"
gen_build(app_dir, p, target)
outfile_py = Path(outfile).parent / (str(Path(outfile).stem) + '_py.wav')
outfile_c = Path(outfile).parent / (str(Path(outfile).stem) + '_c.wav')
# Generate input
input_sig_py = np.empty((int(Fs*test_duration), num_in_channels), dtype=np.float64)
for i in range(num_in_channels):
# precision of 28 gives Q27 signal
input_sig_py[:, i] = (gen.sin(fs=Fs, length=test_duration, freq=1000, amplitude=0.1, precision=28)).T
if (input_dtype == np.int32) or (input_dtype == np.int16):
int_sig = (np.array(input_sig_py) * utils.Q_max(27)).astype(np.int32)
if input_dtype == np.int32:
qformat = 31
int_sig <<= 4
else:
qformat = 15
int_sig >>= (27-15)
input_sig_c = np.clip(int_sig, np.iinfo(input_dtype).min, np.iinfo(input_dtype).max).astype(input_dtype)
else:
input_sig_c = deepcopy(input_sig_py)
print(input_sig_py)
print(input_sig_c)
scipy.io.wavfile.write(infile, Fs, input_sig_c)
# Run Python
sim_sig = p.executor().process(input_sig_py).data
np.testing.assert_equal(sim_sig, input_sig_py)
sim_sig = utils.float_to_fixed_array(sim_sig, 27) << 4
audio_helpers.write_wav(outfile_py, Fs, sim_sig)
# Run C
xe = app_dir / f"bin/{target}/pipeline_test_{target}.xe"
run_pipeline_xcoreai.run(xe, infile, outfile_c, num_out_channels, n_stages)
# since gen.sin already generates a quantised input, no need to truncate
exp_fs, exp_sig = audio_helpers.read_wav(infile)
# Compare
out_fs, out_sig_py = audio_helpers.read_wav(outfile_py)
out_fs, out_sig_c = audio_helpers.read_wav(outfile_c)
np.testing.assert_equal(out_sig_py, exp_sig[:out_sig_py.shape[0],:]) # Compare Python with input
np.testing.assert_equal(out_sig_c, exp_sig[:out_sig_c.shape[0],:]) # Compare C with input
INT32_MIN = -(2**31)
INT32_MAX = (-INT32_MIN) - 1
@pytest.mark.parametrize("input, add", [(3, 3), # input too insignificant, output is shifted
(-3, 0), # negative small numbers truncate in the negative direction
(INT32_MIN, -3),
(INT32_MAX, 3),
(INT32_MAX, -3)])
def test_pipeline_q27(input, add):
"""
Check that the pipeline operates at q5.27 and outputs saturated Q1.31
This is done by having a stage which adds a constant to its input. Inputing
small values will result in the small value being too insgnificant and shifted out.
and the result is the constant shifted up.
Check for saturation by adding a constant to large values and checking the output hasn't
overflowed
"""
name = f"q27_{input}_{add}.wav"
infile = "in" + name
outfile = "out" + name
n_samps, channels, rate = 1024, 2, 48000
output = ((input >> HEADROOM_BITS) + add) << HEADROOM_BITS
output = min(output, INT32_MAX)
output = max(output, INT32_MIN)
p, i = Pipeline.begin(channels)
addn = p.stage(AddN, i, n=add)
p.set_outputs(addn)
if input == INT32_MAX:
app_str = "max"
elif input == INT32_MIN:
app_str = "min"
else:
app_str = f"{input}"
app_dir = PKG_DIR / f"test_pipeline_q27_{app_str}"
os.makedirs(app_dir, exist_ok=True)
target = "default"
gen_build(app_dir, p, target)
sig = np.multiply(np.ones((n_samps, channels), dtype=np.int32), input, dtype=np.int32)
audio_helpers.write_wav(infile, rate, sig)
xe = app_dir / f"bin/{target}/pipeline_test_{target}.xe"
run_pipeline_xcoreai.run(xe, infile, outfile, num_out_channels, pipeline_stages=1)
expected = np.multiply(np.ones((n_samps, channels), dtype=np.int32), output, dtype=np.int32)
_, out_data = audio_helpers.read_wav(outfile)
np.testing.assert_equal(expected, out_data)
@pytest.mark.group0
def test_complex_pipeline():
"""
Generate a multithreaded pipeline and check the output is as expected
"""
infile = "incomplex.wav"
outfile = "outcomplex.wav"
n_samps, channels, rate = 1024, 2, 48000
p, i = Pipeline.begin(channels) # ch 0 1
# this thread has both channels
a = p.stage(AddN, i, n=1) # +1 +1
a = p.stage(AddN, a, n=1) # +2 +2
a0 = p.stage(AddN, a[:1], n=1) # +3 +2
p.next_thread()
# this thread has channel 1
a1 = p.stage(AddN, a[1:], n=1) # +3 +3
a1 = p.stage(AddN, a1, n=1) # +3 +4
a1 = p.stage(AddN, a1, n=1) # +3 +5
p.next_thread()
# this thread has channel 0
a0 = p.stage(AddN, a0, n=1) # +4 +5
p.next_thread()
# this thread has both channels
a = p.stage(AddN, a0 + a1, n=1) # +5 +6
p.set_outputs(a)
n_stages = 3 # 2 of the 4 threads are parallel
app_dir = PKG_DIR / "test_pipeline_complex"
os.makedirs(app_dir, exist_ok=True)
target = "default"
gen_build(app_dir, p, target)
in_val = 1000
# expected output is +5 on left, +6 on right, in the Q1.27 format
expected = (np.array([[5, 6]]*n_samps) + (in_val >> HEADROOM_BITS)) << HEADROOM_BITS
sig = np.multiply(np.ones((n_samps, channels), dtype=np.int32), in_val, dtype=np.int32)
audio_helpers.write_wav(infile, rate, sig)
xe = APP_DIR / f"bin/{target}/pipeline_test_{target}.xe"
run_pipeline_xcoreai.run(xe, infile, outfile, num_out_channels, n_stages)
_, out_data = audio_helpers.read_wav(outfile)
np.testing.assert_equal(expected, out_data)
@pytest.mark.group0
def test_stage_labels():
"""
Test for the user defined stage labels.
Compares the autogenerated adsp_instance_id.h file to the stage labels dictionary in the resolved pipeline and
confirms that they match.
Also, runs some code on the device to send an internal control command using the define in adsp_instance_id.h
for the instance ID used in the command and checks if control works as expected.
"""
p, n_stages = create_pipeline()
# Autogenerate C code
with FileLock(build_utils.BUILD_LOCK):
generate_dsp_main(p, out_dir = BUILD_DIR / "dsp_pipeline")
target = "default"
# Check if the adsp_instance_id.h file exists and the labels are present in it
label_defines_file = BUILD_DIR / "dsp_pipeline" / "adsp_instance_id_auto.h"
assert label_defines_file.is_file(), f"{label_defines_file} not found"
resolved_pipe = p.resolve_pipeline()
labels_py = resolved_pipe["labels"]
labels_autogen = {}
with open(label_defines_file, "r") as fp:
lines = fp.read().splitlines()
for line in lines:
m = re.match(r"^\#define\s+(\S+)_stage_index\s+\((\d+)\)", line)
if m:
assert(len(m.groups()) == 2)
labels_autogen[m.groups()[0]] = int(m.groups()[1])
assert(labels_py == labels_autogen), f"stage labels in Python pipeline do not match the stage labels in the autogenerated code"