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