init
This commit is contained in:
2
lib_audio_dsp/test/pipeline/python/__init__.py
Normal file
2
lib_audio_dsp/test/pipeline/python/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# Copyright 2024-2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
125
lib_audio_dsp/test/pipeline/python/audio_helpers.py
Normal file
125
lib_audio_dsp/test/pipeline/python/audio_helpers.py
Normal file
@@ -0,0 +1,125 @@
|
||||
# Copyright 2024-2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
|
||||
"""
|
||||
Helper functions for creating and validating audio files
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import scipy
|
||||
import matplotlib.pyplot as plt
|
||||
from pathlib import Path
|
||||
|
||||
from audio_dsp.dsp import generic
|
||||
|
||||
def read_wav(path):
|
||||
rate, data = scipy.io.wavfile.read(path)
|
||||
if data.ndim == 1:
|
||||
data = np.expand_dims(data, axis=1)
|
||||
return rate, data
|
||||
|
||||
def write_wav(path, fs, data):
|
||||
return scipy.io.wavfile.write(path, fs, data)
|
||||
|
||||
def read_and_truncate(path, f_bits=generic.Q_SIG):
|
||||
"""Read wav and truncate the least significant fractional bits"""
|
||||
rate, data = read_wav(path)
|
||||
|
||||
if data.dtype != np.int32:
|
||||
raise TypeError(f"wav data type {data.dtype} not supported")
|
||||
|
||||
mask = ~int(2**(31 - f_bits) - 1)
|
||||
print("mask ", mask)
|
||||
return rate, data & mask
|
||||
|
||||
def correlate_and_diff(output_file, input_file, out_ch_start_end, in_ch_start_end, skip_seconds_start, skip_seconds_end, tol, corr_plot_file=None, verbose=False):
|
||||
rate_out, data_out = scipy.io.wavfile.read(output_file)
|
||||
rate_in, data_in = scipy.io.wavfile.read(input_file)
|
||||
|
||||
if data_out.ndim == 1:
|
||||
data_out = data_out.reshape(len(data_out), 1)
|
||||
|
||||
if data_in.ndim == 1:
|
||||
data_in = data_in.reshape(len(data_in), 1)
|
||||
|
||||
if rate_out != rate_in:
|
||||
assert False, f"input and output file rates are not equal. input rate {rate_in}, output rate {rate_out}"
|
||||
|
||||
|
||||
if data_in.dtype != np.int32:
|
||||
if data_in.dtype == np.int16:
|
||||
data_in = np.array(data_in, dtype=np.int32)
|
||||
data_in = data_in * (2**16)
|
||||
else:
|
||||
assert False, "Unsupported data_in.dtype {data_in.dtype}"
|
||||
|
||||
if data_out.dtype != np.int32:
|
||||
if data_out.dtype == np.int16:
|
||||
data_out = np.array(data_out, dtype=np.int32)
|
||||
else:
|
||||
assert False, "Unsupported data_out.dtype {data_out.dtype}"
|
||||
|
||||
assert out_ch_start_end[1]-out_ch_start_end[0] == in_ch_start_end[1]-in_ch_start_end[0], "input and output files have different channel nos."
|
||||
|
||||
|
||||
skip_samples_start = int(rate_out * skip_seconds_start)
|
||||
skip_samples_end = int(rate_out * skip_seconds_end)
|
||||
data_in = data_in[:,in_ch_start_end[0]:in_ch_start_end[1]+1]
|
||||
data_out = data_out[:,out_ch_start_end[0]:out_ch_start_end[1]+1]
|
||||
|
||||
small_len = min(len(data_in), len(data_out), 64000)
|
||||
data_in_small = data_in[skip_samples_start : small_len+skip_samples_start, :].astype(np.float64)
|
||||
data_out_small = data_out[skip_samples_start : small_len+skip_samples_start, :].astype(np.float64)
|
||||
|
||||
corr = scipy.signal.correlate(data_in_small[:, 0], data_out_small[:, 0], "full")
|
||||
delay = (corr.shape[0] // 2) - np.argmax(corr)
|
||||
print(f"delay = {delay}")
|
||||
|
||||
if corr_plot_file != None:
|
||||
plt.plot(corr)
|
||||
plt.savefig(corr_plot_file)
|
||||
plt.clf()
|
||||
delay_orig = delay
|
||||
|
||||
data_size = min(data_in.shape[0], data_out.shape[0])
|
||||
data_size -= skip_samples_end
|
||||
|
||||
print(f"compare {data_size - skip_samples_start} samples")
|
||||
print(data_in.shape)
|
||||
print(data_out.shape)
|
||||
print(delay)
|
||||
|
||||
num_channels = out_ch_start_end[1]-out_ch_start_end[0]+1
|
||||
all_close = True
|
||||
max_diff = []
|
||||
for ch in range(num_channels):
|
||||
print(f"comparing ch {ch}")
|
||||
close = np.isclose(
|
||||
data_in[skip_samples_start : data_size - delay, ch],
|
||||
data_out[skip_samples_start + delay : data_size, ch],
|
||||
atol=tol,
|
||||
)
|
||||
print(f"ch {ch}, close = {np.all(close)}")
|
||||
|
||||
if verbose:
|
||||
int_max_idxs = np.argwhere(close[:] == False)
|
||||
print("shape = ", int_max_idxs.shape)
|
||||
print(int_max_idxs)
|
||||
if np.all(close) == False:
|
||||
if int_max_idxs[0] != 0:
|
||||
count = 0
|
||||
for i in int_max_idxs:
|
||||
if count < 100: # Print first 100 values that were not close
|
||||
print(i, data_in[skip_samples_start+i, ch], data_out[skip_samples_start + delay + i, ch])
|
||||
count += 1
|
||||
|
||||
diff = np.abs((data_in[skip_samples_start : data_size - delay, ch]) - (data_out[skip_samples_start + delay : data_size, ch]))
|
||||
max_diff.append(np.amax(diff))
|
||||
print(f"max diff value is {max_diff[-1]}")
|
||||
all_close = all_close & np.all(close)
|
||||
|
||||
print(f"all_close: {np.all(all_close)}")
|
||||
return all_close, max(max_diff), delay_orig
|
||||
|
||||
|
||||
|
||||
41
lib_audio_dsp/test/pipeline/python/build_utils.py
Normal file
41
lib_audio_dsp/test/pipeline/python/build_utils.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# Copyright 2024-2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""
|
||||
Utility functions for building and running the application within
|
||||
the jupyter notebook
|
||||
"""
|
||||
import subprocess
|
||||
import ipywidgets as widgets
|
||||
from IPython import display
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import os
|
||||
|
||||
BUILD_LOCK = "test_pipeline_build.lock"
|
||||
|
||||
def build(source_dir, build_dir, target):
|
||||
"""
|
||||
Attempt to build and xrun the application
|
||||
"""
|
||||
print("Build and run - output will be in the terminal if it is not displayed below\r")
|
||||
cache = build_dir / "CMakeCache.txt"
|
||||
print("Configuring...\r")
|
||||
if cache.exists():
|
||||
# Generator is already known by cmake
|
||||
ret = subprocess.run([*(f"cmake -S {source_dir} -B {build_dir}".split())])
|
||||
else:
|
||||
# need to configure, default to Ninja because its better
|
||||
generator = "Ninja" if shutil.which("ninja") else "Unix Makefiles"
|
||||
ret = subprocess.run([*(f"cmake -S {source_dir} -B {build_dir} -G".split()), generator])
|
||||
if ret.returncode:
|
||||
print("Configuring failed, check log for details\r")
|
||||
assert(0)
|
||||
|
||||
print("Compiling...\r")
|
||||
if os.name == "nt":
|
||||
ret = subprocess.run(f"cmake --build {build_dir} --target {target}".split())
|
||||
else:
|
||||
ret = subprocess.run(f"cmake --build {build_dir} --target {target} -j".split())
|
||||
if ret.returncode:
|
||||
print("ERROR: Building failed, check log for details\r")
|
||||
assert(0)
|
||||
130
lib_audio_dsp/test/pipeline/python/run_pipeline_xcoreai.py
Normal file
130
lib_audio_dsp/test/pipeline/python/run_pipeline_xcoreai.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# Copyright 2022-2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
import xscope_fileio
|
||||
import argparse
|
||||
import shutil
|
||||
import subprocess
|
||||
import scipy.io.wavfile
|
||||
import pathlib
|
||||
from filelock import FileLock
|
||||
import time
|
||||
import subprocess
|
||||
|
||||
FORCE_ADAPTER_ID = None
|
||||
|
||||
def get_adapter_id():
|
||||
# check the --adapter-id option
|
||||
if FORCE_ADAPTER_ID is not None:
|
||||
return FORCE_ADAPTER_ID
|
||||
|
||||
try:
|
||||
xrun_out = subprocess.check_output(['xrun', '-l'], text=True, stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print('Error: %s' % e.output)
|
||||
assert False
|
||||
|
||||
xrun_out = xrun_out.split('\n')
|
||||
# Check that the first 4 lines of xrun_out match the expected lines
|
||||
expected_header = ["", "Available XMOS Devices", "----------------------", ""]
|
||||
if len(xrun_out) < len(expected_header):
|
||||
raise RuntimeError(
|
||||
f"Error: xrun output:\n{xrun_out}\n"
|
||||
f"does not contain expected header:\n{expected_header}"
|
||||
)
|
||||
|
||||
header_match = True
|
||||
for i, expected_line in enumerate(expected_header):
|
||||
if xrun_out[i] != expected_line:
|
||||
header_match = False
|
||||
|
||||
if not header_match:
|
||||
raise RuntimeError(
|
||||
f"Error: xrun output header:\n{xrun_out[:4]}\n"
|
||||
f"does not match expected header:\n{expected_header}"
|
||||
)
|
||||
|
||||
try:
|
||||
if "No Available Devices Found" in xrun_out[4]:
|
||||
raise RuntimeError(f"Error: No available devices found\n")
|
||||
return
|
||||
except IndexError:
|
||||
raise RuntimeError(f"Error: xrun output is too short:\n{xrun_out}\n")
|
||||
|
||||
for line in xrun_out[6:]:
|
||||
if line.strip():
|
||||
adapterID = line[26:34].strip()
|
||||
status = line[34:].strip()
|
||||
else:
|
||||
continue
|
||||
print("adapter_id = ",adapterID)
|
||||
return adapterID
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("xe", nargs='?',
|
||||
help=".xe file to run")
|
||||
parser.add_argument('-i', '--input', type=pathlib.Path, required=True, help="input wav file")
|
||||
parser.add_argument('-o', '--output', type=pathlib.Path, required=True, help="output wav file")
|
||||
parser.add_argument('-n', '--num-out-channels', type=str, help="Number of channels in the output file. If unspecified, set to be same as the input number of channels",
|
||||
default=None)
|
||||
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
def run(xe, input_file, output_file, num_out_channels, pipeline_stages=1, return_stdout=False):
|
||||
"""
|
||||
Run an xe on an xcore with fileio enabled.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
xe : str
|
||||
file to run
|
||||
input_file : str
|
||||
wav file to play
|
||||
output_file : str
|
||||
output wav file
|
||||
num_out_channels : int
|
||||
number of channels the pipeline will output.
|
||||
pipeline_stages : int
|
||||
Number of stages in the pipeline, the frame delay from the input to the output. This
|
||||
is used to synchronise inputs and outputs. Set to 1 to get unmodified output from the
|
||||
pipeline.
|
||||
return_stdout : bool
|
||||
if true the process output will be returned
|
||||
"""
|
||||
# Create the cmd line string
|
||||
args = f"-i {input_file} -o {output_file} -n {num_out_channels} -t {pipeline_stages}"
|
||||
|
||||
with FileLock("run_pipeline.lock"):
|
||||
with open("args.txt", "w") as fp:
|
||||
fp.write(args)
|
||||
|
||||
adapter_id = get_adapter_id()
|
||||
print("Running on adapter_id ",adapter_id)
|
||||
|
||||
if return_stdout == False:
|
||||
xscope_fileio.run_on_target(adapter_id, xe)
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
with open("stdout.txt", "w+") as ff:
|
||||
xscope_fileio.run_on_target(adapter_id, xe, stdout=ff)
|
||||
ff.seek(0)
|
||||
stdout = ff.readlines()
|
||||
return stdout
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = parse_arguments()
|
||||
assert args.xe is not None, "Specify vaild .xe file"
|
||||
print(f"args.input = {args.input}")
|
||||
|
||||
if(args.num_out_channels == None):
|
||||
rate, data = scipy.io.wavfile.read(args.input)
|
||||
if data.ndim == 1:
|
||||
args.num_out_channels = 1
|
||||
else:
|
||||
args.num_out_channels = data.shape[1]
|
||||
|
||||
print(f"num_out_channels = {args.num_out_channels}")
|
||||
run(args.xe, args.input, args.output, args.num_out_channels)
|
||||
|
||||
Reference in New Issue
Block a user