This commit is contained in:
Steven Dan
2025-12-11 09:43:42 +08:00
commit d8b2974133
1822 changed files with 280037 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
cmake_minimum_required(VERSION 3.21)
include($ENV{XMOS_CMAKE_PATH}/xcommon.cmake)
project(pipeline_test)
set(APP_HW_TARGET XCORE-AI-EXPLORER)
set(COMMON_APP_COMPILER_FLAGS
-O3
-g
-report
-Wall
-Werror
-fxscope
-DTEST_WAV_XSCOPE=1
)
set(APP_COMPILER_FLAGS_default
${COMMON_APP_COMPILER_FLAGS}
)
set(APP_COMPILER_FLAGS_tuned
${COMMON_APP_COMPILER_FLAGS}
-DSEND_TEST_CONTROL_COMMANDS=1
)
set(APP_DEPENDENT_MODULES lib_audio_dsp "xscope_fileio(v1.3.1)")
set(APP_INCLUDES app_pipeline app_pipeline/file_utils ../../lib_audio_dsp/api/stages/ build/dsp_pipeline_default/ build/ build/app_pipeline.autogen/host/ .)
file(GLOB C_SRC CONFIGURE_DEPENDS RELATIVE ${CMAKE_CURRENT_LIST_DIR} app_pipeline/*.c app_pipeline/*.xc app_pipeline/file_utils/*.c stages/*.c)
list(APPEND APP_C_SRCS
${C_SRC}
)
# Handle the files specific to each build config.
if(EXISTS ${CMAKE_CURRENT_LIST_DIR}/build/dsp_pipeline_default/adsp_generated_auto.c)
set(SOURCE_FILES_default build/dsp_pipeline_default/adsp_generated_auto.c)
list(APPEND APP_C_SRCS build/dsp_pipeline_default/adsp_generated_auto.c)
endif()
if(EXISTS ${CMAKE_CURRENT_LIST_DIR}/build/dsp_pipeline_tuned/adsp_generated_auto.c)
set(SOURCE_FILES_tuned build/dsp_pipeline_tuned/adsp_generated_auto.c)
list(APPEND APP_C_SRCS build/dsp_pipeline_tuned/adsp_generated_auto.c)
endif()
file(GLOB ADSP_ADDITIONAL_STAGE_CONFIG ${CMAKE_CURRENT_LIST_DIR}/stages/*.yaml )
set(XMOS_SANDBOX_DIR ${CMAKE_SOURCE_DIR}/../../..)
XMOS_REGISTER_APP()

View File

View File

@@ -0,0 +1,52 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#include "app_dsp.h"
#include "stdbool.h"
#include "xcore/chanend.h"
#include "xcore/parallel.h"
#include <stages/adsp_pipeline.h>
#include <adsp_generated_auto.h>
#if SEND_TEST_CONTROL_COMMANDS
#include "run_control_cmds.h"
#endif
static adsp_pipeline_t * m_dsp;
// send data to dsp
void app_dsp_source(int32_t** data) {
adsp_pipeline_source(m_dsp, data);
}
// read output
void app_dsp_sink(int32_t** data) {
adsp_pipeline_sink(m_dsp, data);
}
DECLARE_JOB(dsp_control_thread, (chanend_t, module_instance_t*, size_t));
void dsp_control_thread(chanend_t c_control, module_instance_t* modules, size_t num_modules)
{
chan_in_word(c_control);
#if SEND_TEST_CONTROL_COMMANDS
send_control_cmds(m_dsp, c_control);
#endif
chan_out_word(c_control, 0);
}
// do dsp
void app_dsp_main(chanend_t c_control) {
m_dsp = adsp_auto_pipeline_init();
PAR_JOBS(
PJOB(adsp_auto_pipeline_main, (m_dsp)),
PJOB(dsp_control_thread, (c_control, m_dsp->modules, m_dsp->n_modules))
);
}
int app_dsp_frame_size() {
while(!m_dsp);
return m_dsp->input_mux.chan_cfg[0].frame_size;
}

View File

@@ -0,0 +1,28 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#pragma once
#include <xccompat.h>
#include <stdint.h>
#define MAX_CHANNELS (4)
// XC safe wrapper for adsp
void app_dsp_init(void);
// send data to dsp
void app_dsp_source(int32_t** data);
// read output
void app_dsp_sink(int32_t** data);
// do dsp
void app_dsp_main(chanend c_control);
// get frame size
int app_dsp_frame_size();
// TODO control

View File

@@ -0,0 +1,86 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include "fileio_task.h"
static void show_usage()
{
puts(
"args: Specify in a single line in args.txt file.\n\n"
" -i input wav file (eg. input.wav)\n\n"
" -o output wav file (eg. -o output.wav)\n\n"
" -n no. of output channels (eg. -o 2. If not specified, set to be same as input channels)\n\n"
" -h Show this usage message and abort\n\n"
" -t Input/output offset, number of samples to throw out at boot to account for pipeline length\n\n"
);
exit(0);
}
static void parse_one_arg(char *arg_part_1, test_config_t *test_config)
{
char *arg_part_2 = strtok(NULL, " ");
if(strcmp(arg_part_1, "-h") == 0)
{
show_usage();
}
else
{
assert(arg_part_2 != NULL);
}
if(strcmp(arg_part_1, "-n") == 0)
{
test_config->num_output_channels = atoi(arg_part_2);
}
else if(strcmp(arg_part_1, "-t") == 0)
{
test_config->num_discard_frames = atoi(arg_part_2);
}
else if(strcmp(arg_part_1, "-i") == 0)
{
test_config->input_filename = malloc(strlen(arg_part_2)+1);
strcpy(test_config->input_filename, arg_part_2);
}
else if(strcmp(arg_part_1, "-o") == 0)
{
test_config->output_filename = malloc(strlen(arg_part_2)+1);
strcpy(test_config->output_filename, arg_part_2);
}
else
{
printf("Error: Invalid argument %s\n", arg_part_1);
assert(0);
}
}
void parse_args(const char *args_file, test_config_t *test_config)
{
FILE *fp = fopen(args_file, "r");
// Get the length of the line first
int count = 0;
int ch = fgetc(fp);
while((ch != '\n') && (ch != EOF)) {
count += 1;
ch = fgetc(fp);
}
fseek(fp, 0, SEEK_SET);
char *args = malloc((count+1)*sizeof(char));
// Read the full line
assert(fgets(args, count+1, fp) != NULL);
printf("args = %s\n", args);
char *pch;
pch = strtok(args, " ");
while(pch != NULL)
{
parse_one_arg(pch, test_config);
pch = strtok(NULL, " ");
}
}

View File

@@ -0,0 +1,12 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#pragma once
#include <xcore/chanend.h>
#include <xcore/channel.h>
#include <xcore/parallel.h>
#include <stddef.h>
#include "stages/adsp_module.h"
DECLARE_JOB(dsp_control_thread, (chanend_t, module_instance_t **, size_t));
DECLARE_JOB(dsp_thread, (chanend_t, chanend_t, module_instance_t**, size_t));

View File

@@ -0,0 +1,93 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#include "fileio.h"
int file_open(file_t *fp, const char* name, const char *mode) {
#if TEST_WAV_XSCOPE
fp->xscope_file = xscope_open_file(name, (char*)mode);
#else
if(!strcmp(mode, "rb")) {
fp->file = open(name, O_RDONLY);
if(fp->file == -1) {return -1;}
}
else if(!strcmp(mode, "wb")) {
fp->file = open(name, O_WRONLY|O_CREAT, 0644);
if(fp->file == -1) {return -1;}
}
else
{
assert((0) && "invalid file open mode specified. Only 'rb' and 'wb' modes supported");
}
#endif
return 0;
}
void file_seek(file_t *fp, long int offset, int origin) {
#if TEST_WAV_XSCOPE
xscope_fseek(&fp->xscope_file, offset, origin);
#else
lseek(fp->file, offset, origin);
#endif
}
int get_current_file_offset(file_t *fp) {
#if TEST_WAV_XSCOPE
int current_offset = xscope_ftell(&fp->xscope_file);
#else
int current_offset = lseek(fp->file, 0, SEEK_CUR);
#endif
return current_offset;
}
int get_file_size(file_t *fp) {
#if TEST_WAV_XSCOPE
//find the current offset in the file
int current_offset = xscope_ftell(&fp->xscope_file);
//go to the end
xscope_fseek(&fp->xscope_file, 0, SEEK_END);
//get offset which will be file size
int size = xscope_ftell(&fp->xscope_file);
//return back to the original offset
xscope_fseek(&fp->xscope_file, current_offset, SEEK_SET);
#else
//find the current offset in the file
int current_offset = lseek(fp->file, 0, SEEK_CUR);
//get file size
int size = lseek(fp->file, 0, SEEK_END);
//go back to original offset
lseek(fp->file, current_offset, SEEK_SET);
#endif
return size;
}
void file_read(file_t *fp, void *buf, size_t count) {
#if TEST_WAV_XSCOPE
xscope_fread(&fp->xscope_file, (uint8_t*)buf, count);
#else
read(fp->file, buf, count);
#endif
}
void file_write(file_t *fp, void *buf, size_t count) {
#if TEST_WAV_XSCOPE
xscope_fwrite(&fp->xscope_file, (uint8_t*)buf, count);
#else
write(fp->file, buf, count);
#endif
}
void file_close(file_t *fp) {
#if !TEST_WAV_XSCOPE
close(fp->file);
#else
//files are closed by a single call to xscope_close_all_files()
#endif
}
void shutdown_session() {
//Needed for XSCOPE_ID_HOST_QUIT in xscope_close_all_files()
#if TEST_WAV_XSCOPE
xscope_close_all_files();
#endif
}

View File

@@ -0,0 +1,31 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#if TEST_WAV_XSCOPE
#include "xscope_io_device.h"
#endif
#include <fcntl.h>
#include <unistd.h>
typedef union {
int file;
#if TEST_WAV_XSCOPE
xscope_file_t xscope_file;
#endif
}file_t;
int file_open(file_t *fp, const char* name, const char *mode);
void file_read(file_t *fp, void *buf, size_t count);
void file_write(file_t *fp, void *buf, size_t count);
void file_seek(file_t *fp, long int offset, int origin);
void file_close(file_t *fp);
void shutdown_session(); //Needed for XSCOPE_ID_HOST_QUIT in xscope_close_all_files()
int get_current_file_offset(file_t *fp);
int get_file_size(file_t *fp);

View File

@@ -0,0 +1,135 @@
// Copyright 2022-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include "fileio.h"
#include "wav_utils.h"
#define RIFF_SECTION_SIZE (12)
#define FMT_SUBCHUNK_MIN_SIZE (24)
#define EXTENDED_FMT_GUID_SIZE (16)
static const char wav_default_header[WAV_HEADER_BYTES] = {
0x52, 0x49, 0x46, 0x46,
0x00, 0x00, 0x00, 0x00,
0x57, 0x41, 0x56, 0x45,
0x66, 0x6d, 0x74, 0x20,
0x10, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x64, 0x61, 0x74, 0x61,
0x00, 0x00, 0x00, 0x00,
};
int get_wav_header_details(file_t *input_file, wav_header *s, unsigned *header_size){
//Assume file is already open here. First rewind.
file_seek(input_file, 0, SEEK_SET);
//read riff header section (12 bytes)
file_read(input_file, (uint8_t*)(&s->riff_header[0]), RIFF_SECTION_SIZE);
if(memcmp(s->riff_header, "RIFF", sizeof(s->riff_header)) != 0)
{
printf("Error: couldn't find RIFF: 0x%x, 0x%x, 0x%x, 0x%x\n", s->riff_header[0], s->riff_header[1], s->riff_header[2], s->riff_header[3]);
return 1;
}
if(memcmp(s->wave_header, "WAVE", sizeof(s->wave_header)) != 0)
{
printf("couldn't find WAVE:, 0x%x, 0x%x, 0x%x, 0x%x\n", s->wave_header[0], s->wave_header[1], s->wave_header[2], s->wave_header[3]);
return 1;
}
file_read(input_file, (uint8_t*)&s->fmt_header[0], FMT_SUBCHUNK_MIN_SIZE);
if(memcmp(s->fmt_header, "fmt ", sizeof(s->fmt_header)) != 0)
{
printf("Error: couldn't find fmt: 0x%x, 0x%x, 0x%x, 0x%x\n", s->fmt_header[0], s->fmt_header[1], s->fmt_header[2], s->fmt_header[3]);
return 1;
}
unsigned fmt_subchunk_actual_size = s->fmt_chunk_size + sizeof(s->fmt_header) + sizeof(s->fmt_chunk_size); //fmt_chunk_size doesn't include the fmt_header(4) and size(4) bytes
unsigned fmt_subchunk_remaining_size = fmt_subchunk_actual_size - FMT_SUBCHUNK_MIN_SIZE;
if(s->audio_format == (short)0xfffe)
{
//seek to the end of fmt subchunk and rewind 16bytes to the beginning of GUID
file_seek(input_file, fmt_subchunk_remaining_size - EXTENDED_FMT_GUID_SIZE, SEEK_CUR);
//The first 2 bytes of GUID is the audio_format.
file_read(input_file, (uint8_t *)&s->audio_format, sizeof(s->audio_format));
//skip the rest of GUID
file_seek(input_file, EXTENDED_FMT_GUID_SIZE - sizeof(s->audio_format), SEEK_CUR);
}
else
{
//go to the end of fmt subchunk
file_seek(input_file, fmt_subchunk_remaining_size, SEEK_CUR);
}
if(s->audio_format != 1)
{
printf("Error: audio format(%d) is not PCM\n", s->audio_format);
return 1;
}
//read header (4 bytes) for the next subchunk
file_read(input_file, (uint8_t*)&s->data_header[0], sizeof(s->data_header));
//if next subchunk is fact, read subchunk size and skip it
if(memcmp(s->data_header, "fact", sizeof(s->data_header)) == 0)
{
uint32_t chunksize;
file_read(input_file, (uint8_t *)&chunksize, sizeof(s->data_bytes));
file_seek(input_file, chunksize, SEEK_CUR);
file_read(input_file, (uint8_t*)(&s->data_header[0]), sizeof(s->data_header));
}
//only thing expected at this point is the 'data' subchunk. Throw error if not found.
if(memcmp(s->data_header, "data", sizeof(s->data_header)) != 0)
{
printf("Error: couldn't find data: 0x%x, 0x%x, 0x%x, 0x%x\n", s->data_header[0], s->data_header[1], s->data_header[2], s->data_header[3]);
return 1;
}
//read data subchunk size.
file_read(input_file, (uint8_t *)&s->data_bytes, sizeof(s->data_bytes));
*header_size = get_current_file_offset(input_file); //total file size should be header_size + data_bytes
//No need to close file - handled by caller
return 0;
}
int wav_form_header(wav_header *header,
short audio_format,
short num_channels,
int sample_rate,
short bit_depth,
int num_frames){
memcpy((char*)header, wav_default_header, WAV_HEADER_BYTES);
header->audio_format = audio_format;
header->num_channels = num_channels;
header->sample_rate = sample_rate;
header->bit_depth = bit_depth;
header->byte_rate = sample_rate*bit_depth*num_channels/8;
header->sample_alignment = num_channels* (bit_depth/8);
int data_bytes = num_frames * num_channels * (bit_depth/8);
header->data_bytes = data_bytes;
header->wav_size = data_bytes + WAV_HEADER_BYTES - 8;
return 0;
}
unsigned wav_get_num_bytes_per_frame(const wav_header *s){
int bytes_per_sample = s->bit_depth/CHAR_BIT;
return (unsigned)(bytes_per_sample * s->num_channels);
}
int wav_get_num_frames(const wav_header *s){
unsigned bytes_per_frame = wav_get_num_bytes_per_frame(s);
return s->data_bytes / bytes_per_frame;
}
long wav_get_frame_start(const wav_header *s, unsigned frame_number, uint32_t wavheader_size){
return wavheader_size + frame_number * wav_get_num_bytes_per_frame(s);
}

View File

@@ -0,0 +1,44 @@
// Copyright 2022-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#pragma once
#include <stdint.h>
#define WAV_HEADER_BYTES 44
typedef struct wav_header {
// RIFF Header
char riff_header[4]; // Should be "RIFF"
int wav_size; // File size - 8 = data_bytes + WAV_HEADER_BYTES - 8
char wave_header[4]; // Should be "WAVE"
// Format Subsection
char fmt_header[4]; // Should be "fmt "
int fmt_chunk_size; // Size of the rest of this subchunk
short audio_format;
short num_channels;
int sample_rate;
int byte_rate; // sample_rate * num_channels * (bit_depth/8)
short sample_alignment; // num_channels * (bit_depth/8)
short bit_depth; // bits per sample
// Data Subsection
char data_header[4]; // Should be "data"
int data_bytes; // frame count * num_channels * (bit_depth/8)
} wav_header;
int get_wav_header_details(file_t *input_file, wav_header *s, unsigned *header_size);
int wav_form_header(wav_header *header,
short audio_format,
short num_channels,
int sample_rate,
short bit_depth,
int num_frames);
unsigned wav_get_num_bytes_per_frame(const wav_header *s);
int wav_get_num_frames(const wav_header *s);
long wav_get_frame_start(const wav_header *s, unsigned frame_number, uint32_t wavheader_size);

View File

@@ -0,0 +1,234 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <xcore/assert.h>
#include <xcore/channel.h>
#include <xcore/chanend.h>
#include <xcore/select.h>
#include "fileio.h"
#include "wav_utils.h"
#include "app_dsp.h"
#include "fileio_task.h"
#include "adsp_generated_auto.h"
#define FILEREAD_CHUNK_SIZE (1024)
// Token to start control operations
#define START_CONTROL_TOKEN (0x12345678)
/// @brief Read a chunk of data from the input file.
/// Takes care of different bit-depths and reads the data in left justified 32bit format.
/// @param input_file Input file handle
/// @param input Buffer to read into
/// @param input_header_struct wav header structure
static void read_input_frame(file_t *input_file, int32_t *input, wav_header *input_header_struct)
{
unsigned bytes_per_frame = wav_get_num_bytes_per_frame(input_header_struct);
if(input_header_struct->bit_depth == 32)
{
// Read directly to input
file_read (input_file, (uint8_t*)&input[0], bytes_per_frame * FILEREAD_CHUNK_SIZE); // Read in FILEREAD_CHUNK_SIZE chunks otherwise it's really slow
}
else if(input_header_struct->bit_depth == 16)
{
int16_t input16[ FILEREAD_CHUNK_SIZE * MAX_CHANNELS];
file_read (input_file, (uint8_t*)&input16[0], bytes_per_frame * FILEREAD_CHUNK_SIZE);
for(int i=0; i<FILEREAD_CHUNK_SIZE*input_header_struct->num_channels; i++)
{
input[i] = input16[i] << 16;
}
}
else if(input_header_struct->bit_depth == 24)
{
uint8_t input8[FILEREAD_CHUNK_SIZE * MAX_CHANNELS * 3];
uint8_t *inptr = input8;
file_read (input_file, (uint8_t*)&input8[0], bytes_per_frame * FILEREAD_CHUNK_SIZE);
for(int i=0; i<FILEREAD_CHUNK_SIZE; i++)
{
for(int ch=0; ch<input_header_struct->num_channels; ch++)
{
int32_t temp = 0;
for(int b=0; b<3; b++)
{
uint32_t val = (uint32_t)(*inptr++);
temp = temp | ((uint32_t)val << ((b+1)*8));
}
input[i*input_header_struct->num_channels + ch] = temp;
}
}
}
else
{
printf("ERROR: Unsupported bit depth %d\n", input_header_struct->bit_depth);
assert(0);
}
}
static void prepare_input(int32_t** dsp_input, int32_t* data, int n_ch, int frame_size) {
for(int f = 0; f < frame_size; ++f) {
for(int ch=0; ch < n_ch; ++ch) {
dsp_input[ch][f] = *data;
++data;
}
}
}
static void prepare_output(int32_t* data, int32_t** dsp_input, int n_ch, int frame_size) {
for(int f = 0; f < frame_size; ++f) {
for(int ch=0; ch < n_ch; ++ch) {
*data = dsp_input[ch][f];
++data;
}
}
}
/// @brief Task responsible for sending data read from a wav file to the pipeline and writing the output received
/// from the pipeline to another file. File operations are done using xscope_fileio functions.
/// @param c_control Channel used for control operations.
void fileio_task(chanend_t c_control)
{
printf("In test app!\n");
test_config_t test_config = {0};
parse_args("args.txt", &test_config);
printf("After parse_args: Input file %s, Output file %s, num channels %d\n", test_config.input_filename, test_config.output_filename, test_config.num_output_channels);
assert(test_config.input_filename != NULL);
assert(test_config.output_filename != NULL);
file_t input_file, output_file;
int ret = file_open(&input_file, test_config.input_filename, "rb");
assert((!ret) && "Failed to open file");
ret = file_open(&output_file, test_config.output_filename, "wb");
assert((!ret) && "Failed to open file");
wav_header input_header_struct, output_header_struct;
unsigned input_header_size;
if(get_wav_header_details(&input_file, &input_header_struct, &input_header_size) != 0){
printf("error in get_wav_header_details()\n");
_Exit(1);
}
if(test_config.num_output_channels == 0)
{
test_config.num_output_channels = input_header_struct.num_channels;
}
assert(input_header_struct.num_channels <= MAX_CHANNELS);
assert(test_config.num_output_channels <= MAX_CHANNELS);
unsigned frame_count = wav_get_num_frames(&input_header_struct);
// Calculate number of frames in the wav file
unsigned block_count = frame_count / (FILEREAD_CHUNK_SIZE);
xassert(0 != block_count);
wav_form_header(&output_header_struct,
input_header_struct.audio_format,
test_config.num_output_channels,
input_header_struct.sample_rate,
32, // Output always 32 bits
block_count*FILEREAD_CHUNK_SIZE);
file_write(&output_file, (uint8_t*)(&output_header_struct), WAV_HEADER_BYTES);
unsigned bytes_per_frame = wav_get_num_bytes_per_frame(&input_header_struct);
printf("Num input channels = %d\n", input_header_struct.num_channels);
printf("Num output channels = %d\n", test_config.num_output_channels);
printf("Discard frames = %d\n", test_config.num_discard_frames);
printf("bytes_per_frame = %d\n", bytes_per_frame);
printf("Block count = %d\n", block_count);
printf("Frame size = %d\n", app_dsp_frame_size());
int32_t input[ FILEREAD_CHUNK_SIZE * MAX_CHANNELS] = {0}; // Array for storing interleaved input read from wav file
int32_t output[FILEREAD_CHUNK_SIZE * MAX_CHANNELS] = {0};
xassert(FILEREAD_CHUNK_SIZE % app_dsp_frame_size() ==0); // frame size must be a factor of read size
int32_t** dsp_input = malloc(sizeof(int32_t*) * input_header_struct.num_channels);
for(int i = 0; i < input_header_struct.num_channels; ++i) {
dsp_input[i] = calloc(app_dsp_frame_size(), sizeof(int32_t));
}
int32_t** dsp_output = malloc(sizeof(int32_t*) * test_config.num_output_channels);
for(int i = 0; i < test_config.num_output_channels; ++i) {
dsp_output[i] = calloc(app_dsp_frame_size(), sizeof(int32_t));
}
// Send a token to indicate that the control parameters, if any, can be sent
chan_out_word(c_control, START_CONTROL_TOKEN);
const int pretest_zero_count = 64000; // MUST MATCH test_stages.py !!!!!
for(int i = 0; i < pretest_zero_count/app_dsp_frame_size(); ++i) {
// push data through pipeline so the control works.
// the amount of data is fixed so that it matches
// the test.
app_dsp_source(dsp_input);
app_dsp_sink(dsp_output);
}
// check that control is done.
SELECT_RES(CASE_THEN(c_control, success), DEFAULT_THEN(error)) {
success:
chan_in_word(c_control);
break;
error:
printf("Error Control should be done by now\n");
_Exit(1);
break;
}
int discard = test_config.num_discard_frames;
for(int i=0; i<block_count; i++)
{
printf("block %d\n", i);
read_input_frame(&input_file, input, &input_header_struct);
int32_t* block_input = input;
int32_t* block_output = output;
// 1. discard initial outputs which will be 0
for(int d = 0; d < discard; d++) {
prepare_input(dsp_input, block_input, input_header_struct.num_channels, app_dsp_frame_size());
app_dsp_source(dsp_input);
block_input += input_header_struct.num_channels * app_dsp_frame_size();
// discard these outputs
app_dsp_sink(dsp_output);
}
// 2. source and sink pipeline
int loop_n = FILEREAD_CHUNK_SIZE/app_dsp_frame_size();//- test_config.num_discard_frames;
for(int j=0; j < loop_n; j++)
{
prepare_input(dsp_input, block_input, input_header_struct.num_channels, app_dsp_frame_size());
app_dsp_source(dsp_input);
app_dsp_sink(dsp_output);
prepare_output(block_output, dsp_output, test_config.num_output_channels, app_dsp_frame_size());
block_input += input_header_struct.num_channels * app_dsp_frame_size();
block_output += test_config.num_output_channels * app_dsp_frame_size();
}
// 3. read only, should empty the pipeline
for(int d = 0; d < discard; d++) {
app_dsp_sink(dsp_output);
prepare_output(block_output, dsp_output, test_config.num_output_channels, app_dsp_frame_size());
block_output += test_config.num_output_channels * app_dsp_frame_size();
}
file_write(&output_file, (uint8_t*)&output[0], test_config.num_output_channels * FILEREAD_CHUNK_SIZE * sizeof(int32_t));
discard = 0; // synchronisation complete
}
file_close(&input_file);
file_close(&output_file);
shutdown_session();
adsp_auto_print_thread_max_ticks();
printf("DONE\n");
_Exit(0);
}

View File

@@ -0,0 +1,14 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#pragma once
typedef struct
{
char *input_filename;
char *output_filename;
int num_discard_frames;
int num_output_channels;
}test_config_t;
void parse_args(const char *args_file, test_config_t *test_config);

View File

@@ -0,0 +1,25 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <xcore/assert.h>
#include <xcore/parallel.h>
#include <xcore/channel.h>
#include <xcore/chanend.h>
DECLARE_JOB(fileio_task, (chanend_t));
DECLARE_JOB(app_dsp_main, (chanend_t));
int main_c()
{
channel_t c_control = chan_alloc();
PAR_JOBS(
PJOB(fileio_task, (c_control.end_a)),
PJOB(app_dsp_main, (c_control.end_b))
);
return 0;
}

View File

@@ -0,0 +1,29 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#include <platform.h>
#include <xs1.h>
#include <xscope.h>
#include <stdlib.h>
#ifdef __XC__
#define chanend_t chanend
#else
#include <xcore/chanend.h>
#endif
#include "xscope_io_device.h"
extern int main_c();
int main (void)
{
chan xscope_chan;
par
{
xscope_host_data(xscope_chan);
on tile[0]: {
xscope_io_init(xscope_chan);
exit(main_c());
}
}
return 0;
}

View File

@@ -0,0 +1,93 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#if SEND_TEST_CONTROL_COMMANDS
#include <string.h>
#include "adsp_control.h"
#include "adsp_instance_id_auto.h"
#include "xcore/hwtimer.h"
#include <xcore/assert.h>
#include <debug_print.h>
#include "run_control_cmds.h"
#include "control_test_params.h"
#include "print.h"
#define CONTROL_COMMAND_TIMEOUT_TICKS 1000000 // one tick is 10ns
#define CONTROL_COMMAND_DELAY_TICKS 1000 // one tick is 10ns
void send_control_cmds(adsp_pipeline_t * m_dsp, chanend_t c_control) {
adsp_stage_control_cmd_t cmd;
int8_t payload_buf[CMD_PAYLOAD_MAX_SIZE];
cmd.instance_id = control_stage_index;
uint8_t values_write[CMD_PAYLOAD_MAX_SIZE];
uint8_t values_read[CMD_PAYLOAD_MAX_SIZE];
adsp_controller_t ctrl;
adsp_controller_init(&ctrl, m_dsp);
// Iterate through all the commands in the stage config
for (int cmd_idx = 0; cmd_idx<CMD_TOTAL_NUM; cmd_idx++)
{
// Fill up the command fields
cmd.cmd_id = control_config[cmd_idx].cmd_id;
cmd.payload_len = control_config[cmd_idx].cmd_size;
cmd.payload = payload_buf;
memset(cmd.payload, 0, cmd.payload_len);
// Write control command to the stage
memcpy(cmd.payload, control_config[cmd_idx].payload, cmd.payload_len);
// Save the payload values for the final check
memcpy(values_write, cmd.payload, cmd.payload_len);
adsp_control_status_t ret = ADSP_CONTROL_BUSY;
uint32_t time_start = get_reference_time();
// Write the data
do {
ret = adsp_write_module_config(&ctrl, &cmd);
// Assert if operation is taking too long
if (get_reference_time() > time_start + CONTROL_COMMAND_TIMEOUT_TICKS) {
xassert(0 && "Timer expired while writing control command");
}
}while(ret == ADSP_CONTROL_BUSY);
xassert(ret == ADSP_CONTROL_SUCCESS);
memset(cmd.payload, 0, cmd.payload_len);
// Add a delay of 10 ms to address the bug described in https://xmosjira.atlassian.net/browse/LCD-257
hwtimer_t t_delay = hwtimer_alloc();
hwtimer_delay(t_delay, CONTROL_COMMAND_DELAY_TICKS);
hwtimer_free(t_delay);
// Read back the written data
ret = ADSP_CONTROL_BUSY;
time_start = get_reference_time();
do {
ret = adsp_read_module_config(&ctrl, &cmd);
// Assert if operation is taking too long
if (get_reference_time() > time_start + CONTROL_COMMAND_TIMEOUT_TICKS) {
xassert(0 && "Timer expired while reading control command");
}
}while(ret == ADSP_CONTROL_BUSY);
xassert(ret == ADSP_CONTROL_SUCCESS);
memcpy(values_read, cmd.payload, cmd.payload_len);
// Check that read and written values match
for(int i=0; i<cmd.payload_len; i++)
{
if(values_read[i] != values_write[i])
{
debug_printf("Command %d: mismatch at index %d. Expected %d, found %d\n", cmd.cmd_id, i, values_write[i], values_read[i]);
xassert(0);
}
}
}
}
#endif

View File

@@ -0,0 +1,8 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#include "adsp_pipeline.h"
// Function to write and read back control parameters.
// Only the commands included in the stage config are used.
void send_control_cmds(adsp_pipeline_t * m_dsp, chanend_t c_control);

View File

@@ -0,0 +1,74 @@
cmake_minimum_required(VERSION 3.21)
include($ENV{XMOS_CMAKE_PATH}/xcommon.cmake)
project(app_synched_source_sink)
set(APP_HW_TARGET XCORE-AI-EXPLORER)
set(COMMON_APP_COMPILER_FLAGS
-Os
-g
-report
-Wall
-Werror
-fxscope
)
file(RELATIVE_PATH REL_PATH_TO_BUILD "${CMAKE_CURRENT_LIST_DIR}" "${CMAKE_BINARY_DIR}")
set(APP_DEPENDENT_MODULES lib_audio_dsp)
set(PIPELINE_DIR ${REL_PATH_TO_BUILD}/dsp_pipeline_default)
set(APP_INCLUDES .. src ${PIPELINE_DIR})
file(GLOB C_SRC CONFIGURE_DEPENDS RELATIVE ${CMAKE_CURRENT_LIST_DIR} src/*.c)
list(APPEND APP_C_SRCS
${C_SRC} ${PIPELINE_DIR}/adsp_generated_auto.c
)
# Read the JSON config file
file(READ "${CMAKE_CURRENT_LIST_DIR}/config.json" JSON_CONTENT)
# Function to extract JSON array into a CMake list
function(json_array_to_list json_content key out_var)
string(JSON len LENGTH "${json_content}" ${key})
math(EXPR max_index "${len} - 1")
set(result)
foreach(i RANGE 0 ${max_index})
string(JSON val GET "${json_content}" ${key} ${i})
list(APPEND result "${val}")
endforeach()
set(${out_var} "${result}" PARENT_SCOPE)
endfunction()
# Extract arrays from JSON
json_array_to_list("${JSON_CONTENT}" FS FSes)
json_array_to_list("${JSON_CONTENT}" N_CHANS N_CHANSes)
json_array_to_list("${JSON_CONTENT}" FRAME_SIZE FRAME_SIZES)
json_array_to_list("${JSON_CONTENT}" N_THREADS N_THREADes)
# Use the extracted values
foreach(FRAME_SIZE ${FRAME_SIZES})
foreach(FS ${FSes})
foreach(N_CHANS ${N_CHANSes})
foreach(N_THREADS ${N_THREADes})
set(APP_COMPILER_FLAGS_${FRAME_SIZE}_${FS}_${N_CHANS}_${N_THREADS}
-DFRAME_SIZE=${FRAME_SIZE}
-DFS=${FS}
-DN_CHANS=${N_CHANS}
-DN_THREADS=${N_THREADS}
${COMMON_APP_COMPILER_FLAGS})
endforeach()
endforeach()
endforeach()
endforeach()
set(XMOS_SANDBOX_DIR ${CMAKE_SOURCE_DIR}/../../../..)
XMOS_REGISTER_APP()

View File

@@ -0,0 +1,7 @@
{
"FS": [96000],
"N_CHANS": [1, 8],
"N_THREADS": [1, 5],
"FRAME_SIZE": [1, 8, 16]
}

View File

@@ -0,0 +1,108 @@
// Copyright 2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
/// This test intended to emulate the scenario which the generated pipeline
/// is used in the XUA application. Source and sink are called on the same
/// thread with strict latency/jitter requirements (must fit in the I2S callback).
///
/// This application will fail if timing is not met.
#include "adsp_generated_auto.h"
#include "stages/adsp_pipeline.h"
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <xcore/parallel.h>
#include <xcore/hwtimer.h>
#ifndef FS
#error "FS not defined"
#endif
#ifndef FRAME_SIZE
#error "FRAME_SIZE not defined"
#endif
#ifndef N_CHANS
#error "N_CHANS not defined"
#endif
#ifndef N_THREADS
#error "N_THREADS not defined"
#endif
#define LOOP_COUNT 10
#define I2S_PERIOD_TICKS ((uint32_t)(100e6 / FS))
void test(adsp_pipeline_t* dsp) {
// allow some time to allow DSP threads to start
int32_t delay_start = get_reference_time();
while(get_reference_time() - delay_start < 10000) {
// do nothing
}
int32_t source_buffer[N_CHANS][FRAME_SIZE];
int32_t* source[N_CHANS];
int32_t sink_buffer[N_CHANS][FRAME_SIZE];
int32_t* sink[N_CHANS];
for (int i = 0; i < N_CHANS; i++) {
source[i] = &source_buffer[i][0];
sink[i] = &sink_buffer[i][0];
}
int frame_count = 0;
uint32_t last_time = get_reference_time();
bool failed = false;
int32_t worst_elapsed = 0;
int32_t times[LOOP_COUNT*FRAME_SIZE];
int32_t *ptimes = times;
for(int i = 0; i < LOOP_COUNT*FRAME_SIZE; i++) {
// check we got back to here fast enough
*ptimes = get_reference_time() - last_time;
if((*ptimes) > worst_elapsed) {
worst_elapsed = *ptimes;
}
++ptimes;
// wait for the next I2S period
while(get_reference_time() - last_time < I2S_PERIOD_TICKS) {
// do nothing
}
last_time = get_reference_time();
if(0 == frame_count) {
frame_count = FRAME_SIZE;
adsp_pipeline_sink(dsp, sink);
adsp_pipeline_source(dsp, source);
}
frame_count -= 1;
}
if(worst_elapsed > I2S_PERIOD_TICKS) {
printf("ERROR Too slow: %lu > %lu\n", worst_elapsed, I2S_PERIOD_TICKS);
failed = true;
}
printf("Done\n");
exit(failed);
}
void do_dsp() {
adsp_pipeline_t* dsp = adsp_auto_pipeline_init();
PAR_FUNCS(
PFUNC(test, dsp),
PFUNC(adsp_auto_pipeline_main, dsp)
);
}
int main(){
printf("Starting DSP test\n");
do_dsp();
}

View File

@@ -0,0 +1,11 @@
<xSCOPEconfig ioMode="basic" enabled="true">
<Probe name="open_file" type="CONTINUOUS" datatype="UINT" units="Value" enabled="true"/>
<Probe name="read_bytes" type="CONTINUOUS" datatype="UINT" units="Value" enabled="true"/>
<Probe name="write_setup" type="CONTINUOUS" datatype="UINT" units="Value" enabled="true"/>
<Probe name="write_bytes" type="CONTINUOUS" datatype="UINT" units="Value" enabled="true"/>
<Probe name="seek" type="CONTINUOUS" datatype="UINT" units="Value" enabled="true"/>
<Probe name="tell" type="CONTINUOUS" datatype="UINT" units="Value" enabled="true"/>
<Probe name="host_quit" type="CONTINUOUS" datatype="UINT" units="Value" enabled="true"/>
<Probe name="host_close" type="CONTINUOUS" datatype="UINT" units="Value" enabled="true"/>
<Probe name="check_version" type="CONTINUOUS" datatype="UINT" units="Value" enabled="true"/>
</xSCOPEconfig>

View File

@@ -0,0 +1,22 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
from python import run_pipeline_xcoreai
import numpy as np
import scipy.signal as spsig
from pathlib import Path
import pytest
def pytest_configure(config):
run_pipeline_xcoreai.FORCE_ADAPTER_ID = config.getoption("--adapter-id")
def pytest_addoption(parser):
parser.addoption(
"--adapter-id", action="store", default=None, help="Force tests to use specific adapter"
)
def pytest_collection_modifyitems(items, config):
for item in items:
if not any(item.iter_markers("group0")):
item.add_marker("group1")

View File

@@ -0,0 +1,28 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
# start example
from audio_dsp.design.pipeline import Pipeline
from audio_dsp.stages import *
p, edge = Pipeline.begin(4)
# thread 0
e0 = p.stage(Bypass, edge[0], "a")
# thread 1
p.next_thread()
e1 = p.stage(Bypass, edge[1:], "b")
e1 = p.stage(Bypass, e1, "c")
# thread 2
p.next_thread()
e = p.stage(Bypass, e0 + e1, "d")
p.set_outputs(e)
# end example
from pathlib import Path
from utils import IMGS_PATH
output_dir = IMGS_PATH / Path(__file__).with_suffix(".gv").name
p.draw(output_dir)

View File

@@ -0,0 +1,36 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
# start example
from audio_dsp.design.pipeline import Pipeline
from audio_dsp.stages import *
p, edge = Pipeline.begin(4)
edge = p.stage(VolumeControl, edge, "volume")
edge = p.stage(LimiterRMS, edge)
p.set_outputs(edge)
# end example
from pathlib import Path
from utils import IMGS_PATH
output_dir = IMGS_PATH / Path(__file__).with_suffix(".gv").name
p.draw(output_dir)
from audio_dsp.design.pipeline import generate_dsp_main
generate_dsp_main(p, Path(__file__).parent/"run_time_dsp/src/dsp")
# start config
config = p["volume"].get_config()
print(config)
# end config
(Path(__file__).parent/"run_time_dsp"/"config.txt").write_text(repr(config))
# check the app builds
from subprocess import run
kwargs = dict(check=True, cwd=Path(__file__).parent/"run_time_dsp")
run(["cmake", "-B", "build", "-G", "Unix Makefiles"], **kwargs)
run(["cmake", "--build", "build"], **kwargs)

View File

@@ -0,0 +1,14 @@
cmake_minimum_required(VERSION 3.21)
include($ENV{XMOS_CMAKE_PATH}/xcommon.cmake)
project(run_time_dsp)
set(APP_HW_TARGET XK-EVK-XU316)
set(APP_DEPENDENT_MODULES lib_audio_dsp)
set(APP_PCA_ENABLE OFF)
set(APP_COMPILER_FLAGS -Wall -Werror)
set(APP_INCLUDES
src
src/dsp)
set(XMOS_SANDBOX_DIR ${CMAKE_CURRENT_LIST_DIR}/../../../../..)
XMOS_REGISTER_APP()

View File

@@ -0,0 +1 @@
{'target_gain': 134217728, 'slew_shift': 7, 'mute_state': 0}

View File

@@ -0,0 +1,7 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#pragma once
#define thread0_stage_index (1)
#define volume_stage_index (2)
#define auto_thread_stage_indices { thread0_stage_index }

View File

@@ -0,0 +1,77 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
// example that is used in the run time guide
// start example
#include <xcore/parallel.h>
#include "cmds.h"
#include "adsp_generated_auto.h"
#include "adsp_instance_id_auto.h"
#include "dsp/signal_chain.h"
#include "control/signal_chain.h"
#include "stages/adsp_control.h"
#include "stages/adsp_pipeline.h"
void control_thread(adsp_controller_t* control) {
// convert desired value to parameter type
float desired_vol_db = -6;
int32_t desired_vol_raw = adsp_dB_to_gain(desired_vol_db);
adsp_stage_control_cmd_t command = {
.instance_id = volume_stage_index,
.cmd_id = CMD_VOLUME_CONTROL_TARGET_GAIN,
.payload_len = sizeof(desired_vol_raw),
.payload = &desired_vol_raw
};
// try write until success
while(ADSP_CONTROL_SUCCESS != adsp_write_module_config(control, &command));
// DONE!
}
void audio_source_sink(adsp_pipeline_t* p) {
// sends and receives audio to the pipeline
}
void dsp_main(void) {
adsp_pipeline_t* dsp = adsp_auto_pipeline_init();
// created a controller instance for each thread.
adsp_controller_t control;
adsp_controller_init(&control, dsp);
PAR_FUNCS(
PFUNC(audio_source_sink, dsp),
PFUNC(control_thread, &control),
PFUNC(adsp_auto_pipeline_main, dsp)
);
}
// end example
// start read
int32_t read_volume_gain(adsp_controller_t* control) {
int32_t gain_raw;
adsp_stage_control_cmd_t command = {
.instance_id = volume_stage_index,
.cmd_id = CMD_VOLUME_CONTROL_GAIN,
.payload_len = sizeof(gain_raw),
.payload = &gain_raw
};
// try write until success
while(ADSP_CONTROL_SUCCESS != adsp_read_module_config(control, &command));
return gain_raw;
}
// end read
// main just to ensure the code compiles. not expected to run
int main() {
(void)read_volume_gain(NULL);
dsp_main();
}

View File

@@ -0,0 +1,11 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
"""
if docs path change, update them here
"""
from pathlib import Path
DOC_PATH = Path(__file__).parents[3] / "doc"
IMGS_PATH = DOC_PATH / "images"

View File

@@ -0,0 +1,2 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.

View 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

View 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)

View 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)

View File

@@ -0,0 +1,2 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.

View File

@@ -0,0 +1,68 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#include <string.h>
#include <stdlib.h>
#include <xcore/assert.h>
#include "cmds.h" // Autogenerated
#include "cmd_offsets.h" // Autogenerated
#include "stages/add_n.h"
void add_n_process(int32_t **input, int32_t **output, void *app_data_state)
{
add_n_state_t *state = app_data_state;
// do while saves instructions for cases
// where the loop will always execute at
// least once
int i = 0;
do {
int32_t *in = input[i];
int32_t *out = output[i];
int64_t acc;
int j = 0;
do
{
acc = (int64_t)(in[j]);
acc += state->config.n;
if (acc > INT32_MAX){
acc = INT32_MAX;
}
else if (acc < INT32_MIN){
acc = INT32_MIN;
}
out[j] = (int32_t) acc;
} while (++j < state->frame_size);
} while (++i < state->n_outputs);
}
void add_n_init(module_instance_t* instance, adsp_bump_allocator_t* allocator, uint8_t id, int n_inputs, int n_outputs, int frame_size)
{
add_n_state_t *state = instance->state;
add_n_config_t *config = instance->control.config;
memset(state, 0, sizeof(add_n_state_t));
state->n_inputs = n_inputs;
state->n_outputs = n_outputs;
state->frame_size = frame_size;
memcpy(&state->config, config, sizeof(add_n_config_t));
}
void add_n_control(void *module_state, module_control_t *control)
{
add_n_state_t *state = module_state;
add_n_config_t *config = control->config;
if(control->config_rw_state == config_write_pending)
{
// Finish the write by updating the working copy with the new config
memcpy(&state->config, config, sizeof(add_n_config_t));
control->config_rw_state = config_none_pending;
}
else if(control->config_rw_state == config_read_pending)
{
memcpy(config, &state->config, sizeof(add_n_config_t));
control->config_rw_state = config_read_updated;
}
}

View File

@@ -0,0 +1,24 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
/// Simple test stage which add a configurable value to the input
///
#pragma once
#include "add_n_config.h" // Autogenerated
#include <stages/bump_allocator.h>
typedef struct
{
add_n_config_t config;
int n_inputs;
int n_outputs;
int frame_size;
}add_n_state_t;
#define ADD_N_STAGE_REQUIRED_MEMORY 0
void add_n_init(module_instance_t* instance, adsp_bump_allocator_t* allocator, uint8_t id, int n_inputs, int n_outputs, int frame_size);
void add_n_process(int32_t **input, int32_t **output, void *app_data_state);
void add_n_control(void *state, module_control_t *control);

View File

@@ -0,0 +1,18 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
from pathlib import Path
from audio_dsp.design.stage import Stage
import numpy
class AddN(Stage):
"""
Stage which adds a fixed constant to all inputs, for testing purposes
"""
def __init__(self, n=0, **kwargs):
super().__init__(config=Path(__file__).parent / "add_n.yaml", **kwargs)
self.create_outputs(self.n_in)
self["n"] = n
def process(self, input: list[numpy.ndarray]):
return [i + self["n"] for i in input]

View File

@@ -0,0 +1,6 @@
module:
add_n:
n:
type: int
help: add_n
includes: []

View File

@@ -0,0 +1,34 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
/// Custom stage which fills the output with the frame index of that sample
/// this is used in test_frame_size.py to check the generated pipeline
/// really has the correct frame size.
#include <stages/bump_allocator.h>
typedef struct
{
int n_inputs;
int n_outputs;
int frame_size;
}frame_count_state_t;
#define FRAME_COUNT_STAGE_REQUIRED_MEMORY 0
void frame_count_init(module_instance_t* instance, adsp_bump_allocator_t* allocator, uint8_t id, int n_inputs, int n_outputs, int frame_size) {
frame_count_state_t *state = instance->state;
state->n_inputs = n_inputs;
state->n_outputs = n_outputs;
state->frame_size = frame_size;
}
void frame_count_process(int32_t **input, int32_t **output, void *app_data_state) {
frame_count_state_t *state = app_data_state;
for(int i = 0; i < state->n_inputs; ++i) {
for(int j = 0; j < state->frame_size; ++j) {
output[i][j] = j;
}
}
}

View File

@@ -0,0 +1,12 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
from audio_dsp.design.stage import Stage
class FrameCount(Stage):
"""
Stage which outputs a frame index
"""
def __init__(self, **kwargs):
super().__init__(name="frame_count", **kwargs)
self.create_outputs(self.n_in)

View File

@@ -0,0 +1,39 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
/// Blocks for a known amount of time.
///
#pragma once
// #include "wait_config.h" // Autogenerated
#include <xcore/hwtimer.h>
#include <stages/bump_allocator.h>
#include <stdint.h>
typedef struct
{
int32_t ticks;
}wait_state_t;
typedef struct
{
int32_t ticks;
} wait_constants_t;
#define WAIT_STAGE_REQUIRED_MEMORY 0
static inline void wait_init(module_instance_t* instance, adsp_bump_allocator_t* allocator, uint8_t id, int n_inputs, int n_outputs, int frame_size) {
wait_constants_t *constants = instance->constants;
wait_state_t *state = instance->state;
state->ticks = constants->ticks;
}
static inline void wait_process(int32_t **input, int32_t **output, void *state) {
int32_t start = get_reference_time();
int32_t ticks = ((wait_state_t *)state)->ticks;
while (get_reference_time() - start < ticks) {
// Do nothing, just wait
}
}
static inline void wait_control(void *state, module_control_t *control) {}

View File

@@ -0,0 +1,20 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
from audio_dsp.design.stage import Stage
def _calc_ticks(ratio, fs, frame_size):
frame_period = frame_size / fs
return int(100e6 * ratio * frame_period)
class Wait(Stage):
"""
Stage which adds a fixed constant to all inputs, for testing purposes
"""
def __init__(self, wait_ratio, **kwargs):
super().__init__(name="wait", **kwargs)
self.create_outputs(self.n_in)
self.set_constant("ticks",
_calc_ticks(wait_ratio, self.fs, self.frame_size),
"int32_t")

View File

@@ -0,0 +1,24 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
from pathlib import Path
import pytest
import sys
from subprocess import run
from filelock import FileLock
from python import build_utils
EXAMPLES_DIR = Path(__file__).parent/"doc_examples"
EXAMPLES = list(EXAMPLES_DIR.glob("*.py"))
@pytest.mark.parametrize("example", EXAMPLES, ids=[e.name for e in EXAMPLES])
def test_doc_examples(example):
"""Run all the Python scripts in doc_examples/"""
with FileLock(build_utils.BUILD_LOCK):
run([sys.executable, example], check=True)

View File

@@ -0,0 +1,92 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
"""
Tests for audio_dsp.stages.signal_chain.Fork
"""
import pytest
from audio_dsp.design.pipeline import Pipeline, generate_dsp_main
from audio_dsp.stages.signal_chain import Fork
from python import build_utils, run_pipeline_xcoreai, audio_helpers
from pathlib import Path
import numpy as np
from filelock import FileLock
import os
import shutil
PKG_DIR = Path(__file__).parent
APP_DIR = PKG_DIR
BUILD_DIR = APP_DIR / "build"
def do_test(p, in_ch, out_ch, folder_name):
"""
Run stereo file into app and check the output matches
using in_ch and out_ch to decide which channels to compare
"""
app_dir = PKG_DIR / folder_name
os.makedirs(app_dir, exist_ok=True)
infile = app_dir / "instage.wav"
outfile = app_dir / "outstage.wav"
n_samps, rate = 1024, 48000
with FileLock(build_utils.BUILD_LOCK):
generate_dsp_main(p, out_dir = BUILD_DIR / "dsp_pipeline_default")
target = "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)
sig0 = np.linspace(-2**26, 2**26, n_samps, dtype=np.int32) << 4 # numbers which should be unmodified through pipeline
# data formats
sig1 = np.linspace(2**26, -2**26, n_samps, dtype=np.int32) << 4
if len(in_ch) == 2:
sig = np.stack((sig0, sig1), axis=1)
else:
sig = sig0.reshape((n_samps, 1))
audio_helpers.write_wav(infile, rate, sig)
xe = app_dir / f"bin/{target}/pipeline_test_{target}.xe"
run_pipeline_xcoreai.run(xe, infile, outfile, 2, 1)
_, out_data = audio_helpers.read_wav(outfile)
sim_out = p.executor().process(sig).data
for in_i, out_i in zip(in_ch, out_ch):
np.testing.assert_equal(sig[:, in_i], out_data[:, out_i])
np.testing.assert_equal(sig[:, in_i], sim_out[:, out_i])
@pytest.mark.group0
@pytest.mark.parametrize("inputs, fork_output", [(2, 0),
(2, 1),
(1, 0)])
def test_fork(fork_output, inputs):
"""
Basic check that the for stage correctly copies data to the expected outputs.
"""
channels = inputs
p, i = Pipeline.begin(channels)
count = 2
fork = p.stage(Fork, i, count = count)
assert len(fork.forks) == count
for f in fork.forks:
assert len(f) == channels
p.set_outputs(fork.forks[fork_output])
if inputs == 1:
do_test(p, [0], (0, 1), folder_name=f"fork_{inputs}_{fork_output}")
else:
do_test(p, (0, 1), (0, 1), folder_name=f"fork_{inputs}_{fork_output}")
@pytest.mark.group0
def test_fork_copies():
"""
Check we can duplicate a channel
"""
channels = 2
p, i = Pipeline.begin(channels, frame_size=2)
fork = p.stage(Fork, i, count = 2)
p.set_outputs(fork.forks[0][0] + fork.forks[1][0])
# input channel 0 comes out both outputs
do_test(p, (0, 0), (0, 1), folder_name=f"fork_copy")

View File

@@ -0,0 +1,64 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
from audio_dsp.design.pipeline import Pipeline, generate_dsp_main
from audio_dsp.dsp.generic import Q_SIG
from stages.frame_count import FrameCount
from python import audio_helpers, build_utils, run_pipeline_xcoreai
import numpy as np
from pathlib import Path
from itertools import cycle
import pytest
import os
import shutil
from filelock import FileLock
PKG_DIR = Path(__file__).parent
APP_DIR = PKG_DIR
BUILD_DIR = APP_DIR / "build"
def shift_to_pipeline(i):
return i << (31 - Q_SIG)
@pytest.mark.parametrize("frame_size", [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024])
def test_frame_size(frame_size):
"""
Using a custom test stage that fills each frame of output with the index
of that outputs position in the frame to check that changing the frame size
in the pipeline design actually changes the frame size in the generated code.
"""
p, i = Pipeline.begin(1, frame_size=frame_size)
s = p.stage(FrameCount, i)
p.set_outputs(s)
app_dir = PKG_DIR / f"test_frame_size_{frame_size}"
os.makedirs(app_dir, exist_ok=True)
infile = app_dir / "inframe.wav"
outfile = app_dir / "outframe.wav"
n_samps, rate = 2048, 48000
with FileLock(build_utils.BUILD_LOCK):
generate_dsp_main(p, out_dir = BUILD_DIR / "dsp_pipeline_default")
target = "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)
sig = np.zeros((n_samps, 1), 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, 1, 1)
_, out_data = audio_helpers.read_wav(outfile)
expected=cycle(range(frame_size))
i = 0
for i, (actual, expected) in enumerate(zip(out_data, expected)):
assert actual == shift_to_pipeline(expected)
assert i + 1 == n_samps

View File

@@ -0,0 +1,262 @@
# 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"

View File

@@ -0,0 +1,25 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
import numpy as np
from python import build_utils, run_pipeline_xcoreai, audio_helpers
from audio_dsp.design.pipeline import Pipeline
from audio_dsp.stages.biquad import Biquad
def test_pipeline_executor():
p, i = Pipeline.begin(4)
n_samps = 100
s = p.stage(Biquad, i)
s = p.stage(Biquad, s)
s0 = p.stage(Biquad, s[:2])
s1 = p.stage(Biquad, s[2:])
s = p.stage(Biquad, s0 + s1)
p.set_outputs(s)
executor = p.executor()
sig = np.ones((n_samps, 4))
ret = executor.process(sig).data
print(ret)

View File

@@ -0,0 +1,236 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
"""
Tests for audio_dsp.stages.signal_chain stages that have a different
number of inputs and outputs
"""
import pytest
from audio_dsp.design.pipeline import Pipeline, generate_dsp_main
from audio_dsp.stages.signal_chain import Adder, Subtractor, Mixer, Switch, SwitchStereo, SwitchSlew, Crossfader, CrossfaderStereo
from audio_dsp.stages.compressor_sidechain import CompressorSidechain, CompressorSidechainStereo
import audio_dsp.dsp.utils as utils
import audio_dsp.dsp.signal_chain as sc
from python import build_utils, run_pipeline_xcoreai, audio_helpers
from pathlib import Path
import numpy as np
import soundfile as sf
import os
import shutil
from filelock import FileLock
PKG_DIR = Path(__file__).parent
APP_DIR = PKG_DIR
BUILD_DIR = APP_DIR / "build"
def do_test(p, folder_name, rtol=None):
"""
Run stereo file into app and check the output matches
using in_ch and out_ch to decide which channels to compare
"""
n_outs = p._n_out
app_dir = PKG_DIR / folder_name
os.makedirs(app_dir, exist_ok=True)
infile = app_dir / "inadder.wav"
outfile = app_dir / "outadder.wav"
n_samps, rate = 1024, 48000
# use the Python dsp_block as a reference implementation
ref_module = p.stages[2].dsp_block
with FileLock(build_utils.BUILD_LOCK):
generate_dsp_main(p, out_dir = BUILD_DIR / "dsp_pipeline_default")
target = "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)
sig0 = np.linspace(-(2**26), 2**26, n_samps, dtype=np.int32) << 4 # numbers which should be unmodified through pipeline
# data formats
if type(ref_module) == sc.subtractor:
# don't overflow output by doing -1 - 0.125
sig1 = np.linspace(-2**23, 2**23, n_samps, dtype=np.int32) << 4
else:
sig1 = np.linspace(2**23, -2**23, n_samps, dtype=np.int32) << 4
if type(p.stages[2]) in [SwitchStereo, CrossfaderStereo, CompressorSidechainStereo]:
sig = np.stack((sig0, sig0, sig1, sig1), axis=1)
else:
sig = np.stack((sig0, sig1), axis=1)
audio_helpers.write_wav(infile, rate, sig)
xe = app_dir / f"bin/{target}/pipeline_test_{target}.xe"
run_pipeline_xcoreai.run(xe, infile, outfile, n_outs, 1)
_, out_data = audio_helpers.read_wav(outfile)
# convert to float scaling and make frames
frame_size = 1
sig_flt = utils.fixed_to_float_array(sig.T >> 4, 27)
signal_frames = utils.frame_signal(sig_flt, frame_size, frame_size)
out_py = np.zeros((n_outs, sig.shape[0]))
# run through Python bit exact implementation
for n in range(len(signal_frames)):
out_py[:, n*frame_size:(n+1)*frame_size] = ref_module.process_frame_xcore(signal_frames[n])
# back to int scaling
out_py_int = utils.float_to_fixed_array(out_py, 27) << 4
if rtol:
np.testing.assert_allclose(out_py_int, out_data.T, rtol=rtol, atol=0)
else:
np.testing.assert_equal(out_py_int, out_data.T)
@pytest.mark.group0
def test_adder():
"""
Test the adder stage adds the same in Python and C
"""
channels = 2
p = Pipeline(channels)
adder = p.stage(Adder, p.i)
p.set_outputs(adder)
do_test(p, "adder")
def test_subtractor():
"""
Test the subtractor stage adds the same in Python and C
"""
channels = 2
p = Pipeline(channels)
adder = p.stage(Subtractor, p.i)
p.set_outputs(adder)
do_test(p, "subtractor")
@pytest.mark.group0
@pytest.mark.parametrize("gain", ([-6, 0]))
def test_mixer(gain):
"""
Test the mixer stage adds the same in Python and C
"""
channels = 2
p = Pipeline(channels)
adder = p.stage(Mixer, p.i, "a")
p["a"].set_gain(gain)
p.set_outputs(adder)
do_test(p, f"mixer_{gain}")
@pytest.mark.group0
def test_compressor_sidechain():
"""
Test the compressor stage compresses the same in Python and C
"""
channels = 2
p = Pipeline(channels)
comp = p.stage(CompressorSidechain, p.i, "c")
p.set_outputs(comp)
p["c"].make_compressor_sidechain(2, -6, 0.001, 0.1)
do_test(p, "comp_side")
@pytest.mark.group0
def test_compressor_sidechain_stereo():
"""
Test the stereo compressor stage compresses the same in Python and C
"""
channels = 4
p = Pipeline(channels)
comp = p.stage(CompressorSidechainStereo, p.i, "c")
p.set_outputs(comp)
p["c"].make_compressor_sidechain(2, -6, 0.001, 0.1)
do_test(p, "comp_side_stereo")
@pytest.mark.parametrize("position", ([0, 1]))
def test_switch(position):
"""
Test the switch stage adds the same in Python and C
"""
channels = 2
p = Pipeline(channels)
switch_dsp = p.stage(Switch, p.i, "s")
p["s"].move_switch(position)
p.set_outputs(switch_dsp)
do_test(p, f"switch_{position}")
@pytest.mark.parametrize("position", ([0, 1]))
def test_switch_slew(position):
"""
Test the slewing switch stage adds the same in Python and C
"""
channels = 2
p = Pipeline(channels)
switch_dsp = p.stage(SwitchSlew, p.i, "s")
p["s"].move_switch(position)
# temp hack to avoid initialisation issues
p["s"].dsp_block.switching=False
p.set_outputs(switch_dsp)
do_test(p, f"switchslew_{position}")
@pytest.mark.parametrize("position", ([0, 1]))
def test_switch_stereo(position):
"""
Test the stereo switch stage adds the same in Python and C
"""
channels = 4
p = Pipeline(channels)
switch_dsp = p.stage(SwitchStereo, p.i, "s")
p["s"].move_switch(position)
p.set_outputs(switch_dsp)
do_test(p, f"switchstereo_{position}")
@pytest.mark.parametrize("mix, tol", [[0, 0],
[0.5, 0],
[1, 0]])
def test_crossfader(mix, tol):
"""
Test the crossfader crossfades the same in Python and C
"""
channels = 2
p = Pipeline(channels)
switch_dsp = p.stage(Crossfader, p.i, "s", mix=mix)
p.set_outputs(switch_dsp)
do_test(p, f"crossfader_{mix}", rtol=tol)
@pytest.mark.parametrize("mix, tol", [[0, 0],
[0.5, 0],
[1, 0]])
def test_crossfader_stereo(mix, tol):
"""
Test the stereo crossfader crossfades the same in Python and C
"""
channels = 4
p = Pipeline(channels)
switch_dsp = p.stage(CrossfaderStereo, p.i, "s", mix=mix)
p.set_outputs(switch_dsp)
do_test(p, f"crossfaderstereo_{mix}", rtol=tol)
if __name__ == "__main__":
test_compressor_sidechain_stereo()

View File

@@ -0,0 +1,55 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
"""Test that StageOutputList manipulation methods work correctly."""
from audio_dsp.design.stage import StageOutputList, StageOutput
from random import randint
def list_of_edges():
return [StageOutput() for _ in range(randint(5, 10))]
def test_stage_output_list_basic():
"""Tests StageOutputList creation."""
input = list_of_edges()
sol = StageOutputList(input)
for i, o in zip(input, sol.edges):
assert i is o
def test_eq():
"""Check that StageOutputList are equal when all contained edges are the same."""
a = list_of_edges()
b = list_of_edges()
assert StageOutputList(a) == StageOutputList(a), "Same edges should be equal"
assert StageOutputList(a) == StageOutputList(a[:]), "Same edges but different list should be equal"
assert StageOutputList(a) != StageOutputList(b), "Different edges should be equal"
def test_add():
"""Check that StageOutputList can be combined easily."""
a = StageOutputList(list_of_edges())
b = StageOutputList(list_of_edges())
actual_add = None + a + [None] + b + None
actual_or = None | a | [None] | b | None
expected = [None] + a.edges + [None] + b.edges + [None]
for exp, act_add, act_or in zip(expected, actual_add.edges, actual_or.edges):
assert exp is act_add
assert exp is act_or
def test_get():
"""Check that StageOutputList can be indexed by int, tuple, slice, iterator"""
edges = list_of_edges()
a = StageOutputList(edges)
assert a[0] == StageOutputList([edges[0]])
assert a[0, 2, 4] == StageOutputList([edges[0], edges[2], edges[4]])
assert a[0:3:2] == StageOutputList([edges[0], edges[2]])
assert a[range(2)] == StageOutputList([edges[0], edges[1]])

View File

@@ -0,0 +1,861 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
"""
Tests for audio_dsp.stages with 2 inputs and 2 ouputs
"""
import pytest
import scipy.signal as spsig
from audio_dsp.design.pipeline import Pipeline, generate_dsp_main
from audio_dsp.stages import *
import audio_dsp.dsp.biquad as bq
from copy import deepcopy
import audio_dsp.dsp.utils as utils
from .python import build_utils, run_pipeline_xcoreai, audio_helpers
import os
from pathlib import Path
import numpy as np
import struct
import yaml
from filelock import FileLock
import shutil
import random
from test.test_utils import q_convert_flt
PKG_DIR = Path(__file__).parent
APP_DIR = PKG_DIR
BUILD_DIR = APP_DIR / "build"
TEST_FRAME_SIZES = 1, 128
fs = 48000
channels = 2 # if this changes need to rewrite test signals
@pytest.fixture(scope="module", params=TEST_FRAME_SIZES)
def frame_size(request):
return request.param
def generate_ref(sig, ref_module, pipeline_channels, frame_size):
"""
Process the signal through a Python stage of a certain frame size
"""
sig_flt = utils.fixed_to_float_array(sig.T >> 4, 27)
signal_frames = utils.frame_signal(sig_flt, frame_size, frame_size)
out_py = np.zeros((pipeline_channels, sig.shape[0]))
# push through zeros to match the test app which does this to support control
pretest_zero_cont = 64e3 # MUST MATCH TEST APP
for _ in range(int(pretest_zero_cont/frame_size)):
ref_module.process_frame_xcore([np.zeros(frame_size) for _ in range(pipeline_channels)])
# run through Python bit exact implementation
for n in range(len(signal_frames)):
out_py[:, n * frame_size : n * frame_size + frame_size] = (
ref_module.process_frame_xcore(signal_frames[n])
)
# back to int scaling, and clip so that values are int32
out_py_int = utils.float_to_fixed_array(out_py, 27) << 4
return out_py_int
def do_test(default_pipeline, tuned_pipeline, dut_frame_size, folder_name, skip_default=False, rtol=0):
"""
Run stereo file into app and check the output matches
using in_ch and out_ch to decide which channels to compare
This test compares the stage running on the device with the simulated
stage. It runs the simulated stage at all the frame sizes in TEST_FRAME_SIZES
to ensure that the output of a stage doesn't change when the frame size changes.
Parameters
----------
default_pipeline: function
function that takes a frame size and returns a pipeline which
has that frame size as the input. It uses the default configuration values.
tuned_pipeline: function
function that takes a frame size, returns a pipeline which
has that frame size as the input, and tunes the pipelines with the desired
configuration values
dut_frame_size : int
The frame size to use for the pipeline that will run on the device.
"""
app_dir = PKG_DIR / folder_name
os.makedirs(app_dir, exist_ok=True)
with FileLock(build_utils.BUILD_LOCK):
for func_p in [default_pipeline, tuned_pipeline]:
# Exit if tuned_pipeline is not defined
if not func_p:
continue
dut_p = func_p(dut_frame_size)
pipeline_channels = len(dut_p.i)
out_dir = None
# Generate uninitialized stages for default_pipeline, only if tuned_pipeline is defined
if func_p == default_pipeline and tuned_pipeline:
out_dir = "dsp_pipeline_tuned"
else:
out_dir = "dsp_pipeline_default"
generate_dsp_main(dut_p, out_dir=BUILD_DIR / out_dir)
n_samps, rate = 1024, 48000
infile = app_dir / "instage.wav"
# signal starts at 0 so that there is no step in the signal
sig_f = np.sin(np.linspace(0, 2*np.pi, n_samps)) * np.linspace(0, 1, n_samps) # a sin wave with ramped gain from 0
sig0 = np.round(sig_f * 2**30).astype(np.int32)
sig1 = np.round(sig_f * 2**27).astype(np.int32)
if pipeline_channels == 2:
sig = np.stack((sig0, sig1), axis=1)
elif pipeline_channels == 1:
sig = sig0
sig = sig.reshape((len(sig), 1))
else:
assert False, f"Unsupported number of channels {pipeline_channels}. Test supports 1 or 2 channels"
audio_helpers.write_wav(infile, rate, sig)
# The reference function should be always tuned_pipeline, it is default_pipeline if tuned_pipeline is not defined
ref_func_p = tuned_pipeline if tuned_pipeline else default_pipeline
ref_p = [ref_func_p(s) for s in TEST_FRAME_SIZES]
out_py_int_all = [
generate_ref(sig, p.stages[2].dsp_block, pipeline_channels, fr)
for p, fr in zip(ref_p, TEST_FRAME_SIZES)
]
for target in ["default", "tuned"]:
# Do not run the control test if tuned_pipeline is not defined
if not tuned_pipeline and target == "tuned":
continue
# Build pipeline test executable. This will download xscope_fileio if not present
build_utils.build(APP_DIR, BUILD_DIR, target)
# old_xe = APP_DIR / f"bin/{target}/pipeline_test_{target}.xe"
# new_xe = app_dir / f"bin/{target}/pipeline_test_{target}.xe"
os.makedirs(app_dir / "bin", exist_ok=True)
shutil.copytree(APP_DIR / "bin", app_dir / "bin", dirs_exist_ok=True)
for target in ["default", "tuned"]:
# Do not run the control test if tuned_pipeline is not defined
if not tuned_pipeline and target == "tuned":
continue
if target == "default" and skip_default:
continue
print(f"Running {target} pipeline test")
outfile = app_dir / f"{target}_outstage.wav"
xe = app_dir / f"bin/{target}/pipeline_test_{target}.xe"
_, _ = audio_helpers.read_wav(infile)
run_pipeline_xcoreai.run(xe, infile, outfile, pipeline_channels, 1, return_stdout=False)
_, out_data = audio_helpers.read_wav(outfile)
if out_data.ndim == 1:
out_data = out_data.reshape(len(out_data), 1)
for out_py_int, ref_frame_size in zip(out_py_int_all, TEST_FRAME_SIZES):
print(f"-- Testing frame size {ref_frame_size} with dut frame size {dut_frame_size}")
for ch in range(pipeline_channels):
# Save Python tracks
audio_helpers.write_wav(app_dir / f"outstage_python_frame{ref_frame_size}_ch{ch}.wav", rate, np.array(out_py_int.T, dtype=np.int32))
diff = out_py_int.T[:, ch] - out_data[:, ch]
print(f"ch {ch}: max diff {max(abs(diff))}")
sol = (~np.equal(out_py_int.T, out_data)).astype(int)
indexes = np.flatnonzero(sol)
print(f"ch {ch}: {len(indexes)} indexes mismatch")
print(f"ch {ch} mismatching indexes = {indexes}")
np.testing.assert_allclose(
out_data,
out_py_int.T,
rtol=rtol,
err_msg=f"dut frame {dut_frame_size}, ref frame {ref_frame_size}",
)
def generate_test_param_file(config_name, stage_config):
"""
Generate a header file with the configuration parameters listed in the arguments.
Parameters
----------
stage_name: string
name of the stage to test
stage_config: dict
dictionary containing the parameter names and their corresponding values
"""
type_data = {}
with open(
Path(__file__).resolve().parents[2] / f"stage_config/{config_name.lower()}.yaml",
"r",
) as fd:
type_data = yaml.safe_load(fd)
# get the actual stage name
stage_name = list(type_data["module"].keys())[0]
# Write the autogenerated header file
with open(
Path(__file__).resolve().parent / f"build/control_test_params.h", "w"
) as f_op:
f_op.write('#include "cmds.h"\n\n')
f_op.write("#define CMD_PAYLOAD_MAX_SIZE 256 // must be 256 to fit in EP0\n")
f_op.write(f"#define CMD_TOTAL_NUM {len(stage_config)}\n\n")
f_op.write("typedef struct control_data_t {\n")
f_op.write("\tuint32_t cmd_id;\n")
f_op.write("\tuint32_t cmd_size;\n")
f_op.write("\tuint8_t payload[CMD_PAYLOAD_MAX_SIZE];\n")
f_op.write("}control_data_t;\n\n")
f_op.write(f"control_data_t control_config[CMD_TOTAL_NUM] = {{\n")
for cmd_name, cmd_payload in stage_config.items():
f_op.write(f"\t{{\n")
f_op.write(f"\t\t.cmd_id = CMD_{stage_name.upper()}_{cmd_name.upper()},\n")
payload_values = []
cmd_payload_list = []
if not isinstance(cmd_payload, list):
cmd_payload_list.append(cmd_payload)
else:
cmd_payload_list = cmd_payload
payload_size = 0
for value in cmd_payload_list:
data_type = type_data["module"][stage_name.lower()][cmd_name.lower()][
"type"
]
# Convert the values into bytearrays and compute the payload length
if data_type in ["uint32_t"]:
value = np.uint32(value)
ba = bytearray(struct.pack("I", value))
payload_size += 4
elif data_type in ["int", "int32_t"]:
value = np.int32(value)
ba = bytearray(struct.pack("i", value))
payload_size += 4
elif data_type in ["float"]:
ba = struct.unpack("4b", struct.pack("f", value))
payload_size += 4
elif data_type in ["int8_t", "uint8_t"]:
ba = bytearray(value & 0xFF)
payload_size += 1
else:
raise ValueError(f"{data_type} is not supported")
payload_values = payload_values + [
"0x{:02X}".format(x & 0xFF) for x in ba
]
f_op.write(f"\t\t.cmd_size = {payload_size},\n")
f_op.write(f"\t\t.payload = {{{', '.join(list(payload_values))}}},\n")
f_op.write(f"\t}},\n")
f_op.write(f"}};\n")
@pytest.mark.group0
@pytest.mark.parametrize(
"method, args",
[
("make_bypass", None),
("make_lowpass", [1000, 0.707]),
("make_highpass", [1000, 0.707]),
("make_bandpass", [1000, 0.707]),
("make_bandstop", [1000, 0.707]),
("make_notch", [1000, 0.707]),
("make_allpass", [1000, 0.707]),
("make_peaking", [1000, 0.707, -6]),
("make_constant_q", [1000, 0.707, -6]),
("make_lowshelf", [1000, 0.707, -6]),
("make_highshelf", [1000, 0.707, -6]),
("make_linkwitz", [200, 0.707, 180, 0.707]),
],
)
def test_biquad(method, args, frame_size):
"""
Test the biquad stage filters the same in Python and C
"""
def default_pipeline(fr):
p, i = Pipeline.begin(channels, frame_size=fr)
o = p.stage(Biquad, i, label="control")
p.set_outputs(o)
return p
def tuned_pipeline(fr):
p = default_pipeline(fr)
bq_method = getattr(p["control"], method)
# Set initialization parameters of the stage
if args:
bq_method(*args)
else:
bq_method()
stage_config = p["control"].get_config()
generate_test_param_file("BIQUAD", stage_config)
return p
folder_name = f"biquad_{frame_size}_{method[5:]}"
do_test(default_pipeline, tuned_pipeline, frame_size, folder_name)
@pytest.mark.group0
@pytest.mark.parametrize(
"method, args, rtol",
[
("make_bypass", None, 0),
("make_lowpass", [1000, 0.707], 0),
("make_highpass", [1000, 0.707], 0),
("make_bandpass", [1000, 0.707], 0),
("make_bandstop", [1000, 0.707], 0),
("make_notch", [1000, 0.707], 0),
("make_allpass", [1000, 0.707], 0),
("make_peaking", [1000, 0.707, -6], 0),
("make_constant_q", [1000, 0.707, -6], 0),
("make_lowshelf", [1000, 0.707, -6], 0),
("make_highshelf", [1000, 0.707, 10], 1e-2), # Fixing test to make it exact is too hard for now
("make_linkwitz", [200, 0.707, 180, 0.707], 0),
],
)
def test_biquad_slew(method, args, frame_size, rtol):
"""
Test the biquad stage filters the same in Python and C
"""
def default_pipeline(fr):
p, i = Pipeline.begin(channels, frame_size=fr)
o = p.stage(BiquadSlew, i, label="control")
p.set_outputs(o)
return p
def tuned_pipeline(fr):
p = default_pipeline(fr)
bq_method = getattr(p["control"], method)
# Set initialization parameters of the stage
if args:
bq_method(*args)
else:
bq_method()
p["control"].set_slew_shift(0)
stage_config = p["control"].get_config()
generate_test_param_file("BIQUAD_SLEW", stage_config)
return p
folder_name = f"biquad_slew_{frame_size}_{method[5:]}"
# only run the control test, as there is no way to init python pipeline differently
# for default & control tests
do_test(default_pipeline, tuned_pipeline, frame_size, folder_name, skip_default=True, rtol=rtol)
filter_spec = [
["lowpass", fs * 0.4, 0.707],
["highpass", fs * 0.001, 1],
["peaking", fs * 1000 / 48000, 5, 10],
["constant_q", fs * 500 / 48000, 1, -10],
["notch", fs * 2000 / 48000, 1],
["lowshelf", fs * 200 / 48000, 1, 3],
["highshelf", fs * 5000 / 48000, 1, -2],
["gain", -2],
]
@pytest.mark.parametrize(
"method, args",
[
("make_butterworth_highpass", [8, 1000]),
("make_butterworth_lowpass", [8, 1000]),
("make_parametric_eq", [filter_spec]),
],
)
def test_cascaded_biquad(method, args, frame_size):
"""
Test the biquad stage filters the same in Python and C
"""
def default_pipeline(fr):
p = Pipeline(channels, frame_size=fr)
o= p.stage(CascadedBiquads, p.i, label="control")
p.set_outputs(o)
return p
def tuned_pipeline(fr):
p = default_pipeline(fr)
# Set initialization parameters of the stage
bq_method = getattr(p["control"], method)
if args:
bq_method(*args)
else:
bq_method()
stage_config = p["control"].get_config()
generate_test_param_file("CASCADED_BIQUADS", stage_config)
return p
folder_name = f"cbq_{frame_size}_{method[5:]}"
do_test(default_pipeline, tuned_pipeline, frame_size, folder_name)
@pytest.mark.parametrize(
"method, args",
[
("make_parametric_eq", [filter_spec + filter_spec]),
],
)
def test_cascaded_biquad16(method, args, frame_size):
"""
Test the biquad stage filters the same in Python and C
"""
seed = frame_size
random.Random(seed*int(fs/1000)).shuffle(args[0])
def default_pipeline(fr):
p = Pipeline(channels, frame_size=fr)
o= p.stage(CascadedBiquads16, p.i, label="control")
p.set_outputs(o)
return p
def tuned_pipeline(fr):
p = default_pipeline(fr)
# Set initialization parameters of the stage
bq_method = getattr(p["control"], method)
if args:
bq_method(*args)
else:
bq_method()
stage_config = p["control"].get_config()
generate_test_param_file("CASCADED_BIQUADS_16", stage_config)
return p
folder_name = f"cbq16_{frame_size}_{method[5:]}"
do_test(default_pipeline, tuned_pipeline, frame_size, folder_name)
@pytest.mark.group0
def test_limiter_rms(frame_size):
"""
Test the limiter stage limits the same in Python and C
"""
def default_pipeline(fr):
p = Pipeline(channels, frame_size=fr)
o = p.stage(LimiterRMS, p.i, label="control")
p.set_outputs(o)
return p
def tuned_pipeline(fr):
p= default_pipeline(fr)
# Set initialization parameters of the stage
p["control"].make_limiter_rms(-6, 0.001, 0.1)
stage_config = p["control"].get_config()
generate_test_param_file("LIMITER_RMS", stage_config)
return p
folder_name = f"limiterrms_{frame_size}"
do_test(default_pipeline, tuned_pipeline, frame_size, folder_name)
def test_limiter_peak(frame_size):
"""
Test the limiter stage limits the same in Python and C
"""
def default_pipeline(fr):
p = Pipeline(channels, frame_size=fr)
o = p.stage(LimiterPeak, p.i, label="control")
p.set_outputs(o)
return p
def tuned_pipeline(fr):
p = default_pipeline(fr)
# Set initialization parameters of the stage
p["control"].make_limiter_peak(-6, 0.001, 0.1)
stage_config = p["control"].get_config()
generate_test_param_file("LIMITER_PEAK", stage_config)
return p
folder_name = f"limiterpeak_{frame_size}"
do_test(default_pipeline, tuned_pipeline, frame_size, folder_name)
@pytest.mark.group0
def test_hard_limiter_peak(frame_size):
"""
Test the limiter stage limits the same in Python and C
"""
def default_pipeline(fr):
p = Pipeline(channels, frame_size=fr)
o = p.stage(HardLimiterPeak, p.i, label="control")
p.set_outputs(o)
return p
def tuned_pipeline(fr):
p = default_pipeline(fr)
# Set initialization parameters of the stage
p["control"].make_hard_limiter_peak(-6, 0.001, 0.1)
stage_config = p.resolve_pipeline()["configs"][2]
generate_test_param_file("HARD_LIMITER_PEAK", stage_config)
return p
folder_name = f"hardlimiterpeak_{frame_size}"
do_test(default_pipeline, tuned_pipeline, frame_size, folder_name)
def test_clipper(frame_size):
"""
Test the clipper stage clips the same in Python and C
"""
def default_pipeline(fr):
p = Pipeline(channels, frame_size=fr)
o = p.stage(Clipper, p.i, label="control")
p.set_outputs(o)
return p
def tuned_pipeline(fr):
p = default_pipeline(fr)
# Set initialization parameters of the stage
p["control"].make_clipper(-6)
stage_config = p["control"].get_config()
generate_test_param_file("CLIPPER", stage_config)
return p
folder_name = f"clipper_{frame_size}"
do_test(default_pipeline, tuned_pipeline, frame_size, folder_name)
@pytest.mark.group0
def test_compressor(frame_size):
"""
Test the compressor stage compresses the same in Python and C
"""
def default_pipeline(fr):
p = Pipeline(channels, frame_size=fr)
o = p.stage(CompressorRMS, p.i, label="control")
p.set_outputs(o)
return p
def tuned_pipeline(fr):
p = default_pipeline(fr)
# Set initialization parameters of the stage
p["control"].make_compressor_rms(2, -6, 0.001, 0.1)
stage_config = p["control"].get_config()
generate_test_param_file("COMPRESSOR_RMS", stage_config)
return p
folder_name = f"compressor_{frame_size}"
do_test(default_pipeline, tuned_pipeline, frame_size, folder_name)
def test_noise_gate(frame_size):
"""
Test the noise gate stage gates the noise the same in Python and C
"""
def default_pipeline(fr):
p = Pipeline(channels, frame_size=fr)
o = p.stage(NoiseGate, p.i, label="control")
p.set_outputs(o)
return p
def tuned_pipeline(fr):
p = default_pipeline(fr)
# Set initialization parameters of the stage
p["control"].make_noise_gate(-6, 0.001, 0.1)
stage_config = p["control"].get_config()
generate_test_param_file("NOISE_GATE", stage_config)
return p
folder_name = f"noise_gate_{frame_size}"
do_test(default_pipeline, tuned_pipeline, frame_size, folder_name)
def test_noise_suppressor_expander(frame_size):
"""
Test the noise suppressor (expander) stage suppress the noise the same in Python and C
"""
def default_pipeline(fr):
p = Pipeline(channels, frame_size=fr)
o = p.stage(NoiseSuppressorExpander, p.i, label="control")
p.set_outputs(o)
return p
def tuned_pipeline(fr):
p = default_pipeline(fr)
# Set initialization parameters of the stage
p["control"].make_noise_suppressor_expander(2, -6, 0.001, 0.1)
stage_config = p["control"].get_config()
generate_test_param_file("NOISE_SUPPRESSOR_EXPANDER", stage_config)
return p
folder_name = f"noise_suppressor_{frame_size}"
do_test(default_pipeline, tuned_pipeline, frame_size, folder_name)
def test_volume(frame_size):
"""
Test the volume stage amplifies the same in Python and C
"""
# The gain_dB and mute_state must match in both default_pipeline() and tuned_pipeline().
# Those values are used to compute the starting gain, and it must match in both applications
gain_dB = -8
mute_state = 0
def default_pipeline(fr):
p = Pipeline(channels, frame_size=fr)
o = p.stage(VolumeControl, p.i, label="control")
p.set_outputs(o)
p["control"].set_gain(gain_dB)
p["control"].set_mute_state(mute_state)
return p
def tuned_pipeline(fr):
p = default_pipeline(fr)
p["control"].make_volume_control(gain_dB, 10, mute_state)
stage_config = p["control"].get_config()
generate_test_param_file("VOLUME_CONTROL", stage_config)
return p
folder_name = f"volume_{frame_size}"
do_test(default_pipeline, tuned_pipeline, frame_size, folder_name)
def test_fixed_gain(frame_size):
"""
Test the volume stage amplifies the same in Python and C
"""
def default_pipeline(fr):
p = Pipeline(channels, frame_size=fr)
o = p.stage(FixedGain, p.i, label="control")
p.set_outputs(o)
return p
def tuned_pipeline(fr):
p = default_pipeline(fr)
# Set initialization parameters of the stage
p["control"].set_gain(-8)
stage_config = p["control"].get_config()
generate_test_param_file("FIXED_GAIN", stage_config)
return p
folder_name = f"fixed_gain_{frame_size}"
do_test(default_pipeline, tuned_pipeline, frame_size, folder_name)
@pytest.mark.parametrize("pregain, mix", [
[0.01, False],
[0.01, True],
[0.3, False],
])
def test_reverb(frame_size, pregain, mix):
"""
Test Reverb stage
"""
def default_pipeline(fr):
reverb_test_channels = 1 # Reverb expects only 1 channel
p = Pipeline(reverb_test_channels, frame_size=fr)
o = p.stage(ReverbRoom, p.i, label="control")
p.set_outputs(o)
return p
def tuned_pipeline(fr):
p = default_pipeline(fr)
# Set initialization parameters of the stage
if mix:
p["control"].set_wet_dry_mix(0.5)
else:
p["control"].set_wet_gain(-1)
p["control"].set_dry_gain(-2)
p["control"].set_pre_gain(pregain)
p["control"].set_room_size(0.4)
p["control"].set_damping(0.5)
p["control"].set_decay(0.6)
p["control"].set_predelay(5)
stage_config = p["control"].get_config()
generate_test_param_file("REVERB_ROOM", stage_config)
return p
folder_name = f"reverbroom_{frame_size}_{pregain}_{int(mix)}"
do_test(default_pipeline, tuned_pipeline, frame_size, folder_name)
@pytest.mark.parametrize("pregain, mix", [
[0.01, False],
[0.01, True],
[0.3, False],
])
def test_reverb_plate(frame_size, pregain, mix):
"""
Test Reverb stage
"""
def default_pipeline(fr):
reverb_test_channels = 2 # Reverb expects only 2 channel
p = Pipeline(reverb_test_channels, frame_size=fr)
o = p.stage(ReverbPlateStereo, p.i, label="control")
p.set_outputs(o)
return p
def tuned_pipeline(fr):
p = default_pipeline(fr)
# Set initialization parameters of the stage
if mix:
p["control"].set_wet_dry_mix(0.5)
else:
p["control"].set_wet_gain(-1)
p["control"].set_dry_gain(-2)
p["control"].set_pre_gain(pregain)
p["control"].set_early_diffusion(0.4)
p["control"].set_late_diffusion(0.4)
p["control"].set_bandwidth(4000)
p["control"].set_damping(0.5)
p["control"].set_decay(0.6)
p["control"].set_predelay(5)
stage_config = p["control"].get_config()
generate_test_param_file("REVERB_PLATE_STEREO", stage_config)
return p
folder_name = f"reverbplate_{frame_size}_{pregain}_{int(mix)}"
do_test(default_pipeline, tuned_pipeline, frame_size, folder_name)
@pytest.mark.parametrize("change_delay", [5, 0])
def test_delay(frame_size, change_delay):
"""
Test Delay stage
"""
def default_pipeline(fr):
p = Pipeline(channels, frame_size=fr)
o = p.stage(
Delay, p.i, max_delay=15, starting_delay=10, label="control"
)
p.set_outputs(o)
return p
def tuned_pipeline(fr):
p = default_pipeline(fr)
# Set initialization parameters of the stage
p["control"].set_delay(change_delay)
stage_config = p["control"].get_config()
generate_test_param_file("DELAY", stage_config)
return p
folder_name = f"delay_{frame_size}_{change_delay}"
do_test(default_pipeline, tuned_pipeline, frame_size, folder_name)
@pytest.fixture(scope="session", autouse=True)
def make_coeffs():
# make sets of coefficients used in the FIR tests
gen_dir = Path(__file__).parent / "autogen"
gen_dir.mkdir(exist_ok=True, parents=True)
# descending coefficients
coeffs = np.arange(10, 0, -1)
coeffs = coeffs / np.sum(coeffs)
np.savetxt(Path(gen_dir, "descending_coeffs.txt"), coeffs)
# simple windowed FIR design
coeffs = spsig.firwin2(512, [0.0, 0.5, 1.0], [1.0, 1.0, 0.0])
np.savetxt(Path(gen_dir, "simple_low_pass.txt"), coeffs)
@pytest.mark.parametrize(
"filter_name", ["descending_coeffs.txt", "simple_low_pass.txt"]
)
def test_fir(frame_size, filter_name):
""" "
Test FIR Stage
"""
filter_path = Path(Path(__file__).parent / "autogen", filter_name)
def default_pipeline(fr):
p = Pipeline(channels, frame_size=fr)
o = p.stage(FirDirect, p.i, coeffs_path=filter_path)
p.set_outputs(o)
return p
folder_name = f"fir_{frame_size}_{filter_name[:5]}"
do_test(default_pipeline, None, frame_size, folder_name)
def test_graphic_eq(frame_size):
"""
Test the volume stage amplifies the same in Python and C
"""
def default_pipeline(fr):
p = Pipeline(channels, frame_size=fr)
o = p.stage(GraphicEq10b, p.i, label="control")
p.set_outputs(o)
return p
def tuned_pipeline(fr):
p = default_pipeline(fr)
# Set initialization parameters of the stage
p["control"].set_gains([-6, 0, -5, 1, -4, 2, -3, 3, -2, 4])
stage_config = p["control"].get_config()
generate_test_param_file("graphic_eq_10b", stage_config)
return p
folder_name = f"geq_{frame_size}"
do_test(default_pipeline, tuned_pipeline, frame_size, folder_name)
if __name__ == "__main__":
test_graphic_eq(1)
# test_volume(1)

View File

@@ -0,0 +1,77 @@
# Copyright 2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
from pathlib import Path
from filelock import FileLock
import subprocess
import pytest
import json
from audio_dsp.design.pipeline import Pipeline, generate_dsp_main
from python import build_utils, run_pipeline_xcoreai, audio_helpers
from stages.wait import Wait
from python.run_pipeline_xcoreai import FORCE_ADAPTER_ID
APP_NAME = "app_synched_source_sink"
APP_DIR = Path(__file__).parent / APP_NAME
VCD_DIR = APP_DIR / "vcd"
BUILD_DIR = APP_DIR / "build"
TEST_CONFIG = APP_DIR / "config.json"
TEST_PARAMS = json.loads(TEST_CONFIG.read_text())
@pytest.fixture
def xfail_selected(request):
"""
Mark some known failures which dont meet timing.
"""
get = lambda x: request.getfixturevalue(x)
if (get("threads"), get("n_chans"), get("frame_size"), get("fs")) in [
(1, 8, 16, 96000),
(5, 8, 16, 96000),
(1, 8, 1, 96000), # would pass with wait_ratio=0.3
(1, 8, 8, 96000)
]:
request.node.add_marker(pytest.mark.xfail(reason="Current benchmarking shows this should fail", strict=True))
@pytest.mark.parametrize("fs", TEST_PARAMS["FS"])
@pytest.mark.parametrize("frame_size", TEST_PARAMS["FRAME_SIZE"])
@pytest.mark.parametrize("n_chans", TEST_PARAMS["N_CHANS"])
@pytest.mark.parametrize("threads", TEST_PARAMS["N_THREADS"])
@pytest.mark.usefixtures('xfail_selected')
def test_synched_source_sync(fs, frame_size, n_chans, threads):
"""
Basic benchmarking to find scenarios that fail.
Test configuration is defined in config.json. This is also read by
CMakeLists.txt to ensure the tests align with the DUT application.
A special "Wait" stage has been written to consume a fixed ratio of each
DSP thread time. The remaining time is available for the DSP thread to do
control and communication.
"""
p, i = Pipeline.begin(n_chans,fs=fs, frame_size=frame_size)
wait_ratio = 0.7 # ratio of thread time spent doing DSP
i = p.stage(Wait, i, wait_ratio=wait_ratio)
for _ in range(threads - 1):
p.next_thread()
i = p.stage(Wait, i, wait_ratio=wait_ratio)
p.set_outputs(i)
config = f"{frame_size}_{fs}_{n_chans}_{threads}"
BUILD_DIR.mkdir(exist_ok=True)
VCD_DIR.mkdir(exist_ok=True)
with FileLock(build_utils.BUILD_LOCK):
generate_dsp_main(p, out_dir = BUILD_DIR / "dsp_pipeline_default")
build_utils.build(APP_DIR, BUILD_DIR, f"app_synched_source_sink_{config}")
vcd_file = VCD_DIR / f"{config}.vcd"
xscope_file = VCD_DIR / f"{config}.xmt"
app = APP_DIR / "bin" / config / f"{APP_NAME}_{config}.xe"
subprocess.run(["xsim", "--xscope", f"-offline {xscope_file}", "--vcd-tracing", f"-o {vcd_file} -tile tile[0] -cores -instructions", app], check=True, timeout=60)

View File

@@ -0,0 +1,147 @@
# Copyright 2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
import pytest
from audio_dsp.design.pipeline import Pipeline, generate_dsp_main
from audio_dsp.stages import *
def test_bad_pipelines():
with pytest.raises(RuntimeError) as excinfo:
p, inputs = Pipeline.begin(2, fs=48000)
# inputs[1] is not used on thread 0
x1 = p.stage(Biquad, inputs[0], 'bq1')
p.next_thread()
# inputs[1] first used on thread 1
x = p.stage(Biquad, x1 + inputs[1], 'bq2')
p.set_outputs(x)
assert "must cross the same number of threads" in str(excinfo.value)
def test_good_pipelines():
p, inputs = Pipeline.begin(2, fs=48000)
# both inputs are not used on this thread
x1 = p.stage(Biquad, inputs[0], 'bq1')
x2 = p.stage(Bypass, inputs[1], 'by1')
p.next_thread()
x = p.stage(Biquad, x1 + x2, 'bq2')
p.set_outputs(x)
def test_parallel_pipelines():
p, inputs = Pipeline.begin(2, fs=48000)
x1 = p.stage(Biquad, inputs[0], 'bq1')
p.next_thread()
x2 = p.stage(Bypass, inputs[1], 'by1')
p.next_thread()
# x1 and x2 have both crossed 1 thread already
x = p.stage(Biquad, x1 + x2, 'bq2')
p.set_outputs(x)
def test_good_complex():
# 4 inputs
p, i = Pipeline.begin(4, fs=48000)
mic1 = p.stage(Fork, i[2], count=3).forks
mic2 = p.stage(Fork, i[3], count=3).forks
p.stage(EnvelopeDetectorRMS, mic1[0], "edr1")
p.stage(EnvelopeDetectorRMS, mic2[0], "edr2")
mic_mono = p.stage(Adder, mic1[1] + mic2[1], "adder1")
mic_mono = p.stage(Fork, mic_mono, count=3).forks
mic_dnr = p.stage(NoiseSuppressorExpander, mic_mono[0], "ns")
peq1 = p.stage(CascadedBiquads, mic_dnr, "peq1")
sw_1 = p.stage(SwitchStereo, mic_mono[1] + mic_mono[2] + mic1[2] + mic2[2], "sw_1")
i_bypass = p.stage(Bypass, i[:2])
p.next_thread()
mic_peq = p.stage(Fork, peq1, count=3).forks
peq2 = p.stage(CascadedBiquads, mic_peq[2], "peq2")
mic_rv = p.stage(Fork, peq2).forks
i_bypass = p.stage(Bypass, i_bypass)
sw_1 = p.stage(Bypass, sw_1)
p.next_thread()
mic_rv = p.stage(ReverbPlateStereo, mic_rv[0] + mic_rv[1], "reverb")
cfs1 = p.stage(CrossfaderStereo, mic_peq[0] + mic_peq[1] + mic_rv[0] + mic_rv[1], "cfs1")
rv_sw = p.stage(SwitchStereo, cfs1 + sw_1, "reverb_sw")
vol1 = p.stage(VolumeControl, rv_sw[0], "vol1")
vol2 = p.stage(VolumeControl, rv_sw[1], "vol2")
i_bypass = p.stage(Bypass, i_bypass)
p.next_thread()
usb_play, usb_mon = p.stage(Fork, i_bypass).forks
usb_play = p.stage(VolumeControl, usb_play, "vol3")
mic_play, mic_mon = p.stage(Fork, vol1 + vol2).forks
usb0 = p.stage(Adder, usb_play[0] + mic_play[0], "adder2")
usb1 = p.stage(Adder, usb_play[1] + mic_play[1], "adder3")
cfs2 = p.stage(CrossfaderStereo, usb_mon[0] + usb_mon[1] + mic_mon[0] + mic_mon[1], "cfs2")
mon0, mon1 = p.stage(VolumeControl, cfs2, "vol4")
p.set_outputs(usb0 + usb1 + mon0 + mon1)
def test_bad_complex():
with pytest.raises(RuntimeError) as excinfo:
# 4 inputs
p, i = Pipeline.begin(4, fs=48000)
mic1 = p.stage(Fork, i[2], count=3).forks
mic2 = p.stage(Fork, i[3], count=3).forks
p.stage(EnvelopeDetectorRMS, mic1[0], "edr1")
p.stage(EnvelopeDetectorRMS, mic2[0], "edr2")
mic_mono = p.stage(Adder, mic1[1] + mic2[1], "adder1")
mic_mono = p.stage(Fork, mic_mono, count=3).forks
mic_dnr = p.stage(NoiseSuppressorExpander, mic_mono[0], "ns")
peq1 = p.stage(CascadedBiquads, mic_dnr, "peq1")
sw_1 = p.stage(SwitchStereo, mic_mono[1] + mic_mono[2] + mic1[2] + mic2[2], "sw_1")
p.next_thread()
mic_peq = p.stage(Fork, peq1, count=3).forks
peq2 = p.stage(CascadedBiquads, mic_peq[2], "peq2")
mic_rv = p.stage(Fork, peq2).forks
p.next_thread()
mic_rv = p.stage(ReverbPlateStereo, mic_rv[0] + mic_rv[1], "reverb")
cfs1 = p.stage(CrossfaderStereo, mic_peq[0] + mic_peq[1] + mic_rv[0] + mic_rv[1], "cfs1")
rv_sw = p.stage(SwitchStereo, cfs1 + sw_1, "reverb_sw")
vol1 = p.stage(VolumeControl, rv_sw[0], "vol1")
vol2 = p.stage(VolumeControl, rv_sw[1], "vol2")
p.next_thread()
usb_play, usb_mon = p.stage(Fork, i[:2]).forks
usb_play = p.stage(VolumeControl, usb_play, "vol3")
mic_play, mic_mon = p.stage(Fork, vol1 + vol2).forks
usb0 = p.stage(Adder, usb_play[0] + mic_play[0], "adder2")
usb1 = p.stage(Adder, usb_play[1] + mic_play[1], "adder3")
cfs2 = p.stage(CrossfaderStereo, usb_mon[0] + usb_mon[1] + mic_mon[0] + mic_mon[1], "cfs2")
mon0, mon1 = p.stage(VolumeControl, cfs2, "vol4")
p.set_outputs(usb0 + usb1 + mon0 + mon1)
assert "must cross the same number of threads" in str(excinfo.value)
if __name__ == "__main__":
# test_bad_pipelines()
test_bad_complex()
test_good_complex()