From 009dd2e6985e954da695f883fab0cd76dcdf9c86 Mon Sep 17 00:00:00 2001 From: ploopyco <54917504+ploopyco@users.noreply.github.com> Date: Fri, 14 Jul 2023 14:53:04 -0400 Subject: [PATCH] Headphones toolbox (#18) (#19) * Initial, very incomplete, toolbox interface. * Refactored things to move ugly structs out of run.c. Experimental config structures. * Extra comments. * Continuted to implement communication protocol. Can now validate Filter Configuration Structures * Config save/load works. * Reverse stereo and preamp. * Fixes and minor features. * Codec confiuguration implementation. * Support sending configs back to the client. * Fix broken audio when changing a filter type. * Fix broken audio when changing a filter type. * Try to create a build workflow * Report version info * Disable interrupts while reconfiguring, reset all filters if one has a type change. * Reset memory on filter type change.. * Shuffle cores. * Slight refactoring. * Only update a filter if something has changed. * Re-order PCM3060 init i2c commands. * Fixed point math tweaks. Treat samples as normalized (-1..1), this doesnt change much, except the filter coefficients get way more bits of precision. * Allow the user to create fully custom filters. * Increase the max fix16 size to -32.0..32.0 as it seems it is possible to create coefficients which are >16. --------- Co-authored-by: George Norton Co-authored-by: George Norton <30636555+george-norton@users.noreply.github.com> --- .github/workflows/cmake.yml | 31 ++ firmware/code/.gitignore | 3 +- firmware/code/CMakeLists.txt | 37 +- firmware/code/bqf.c | 14 +- firmware/code/bqf.h | 11 +- firmware/code/configuration_manager.c | 549 ++++++++++++++++++++++++++ firmware/code/configuration_manager.h | 57 +++ firmware/code/configuration_types.h | 141 +++++++ firmware/code/fix16.c | 72 +++- firmware/code/fix16.h | 26 +- firmware/code/os_descriptors.h | 102 +++++ firmware/code/run.c | 333 ++++++++++++---- firmware/code/run.h | 50 ++- firmware/code/user.c | 64 --- firmware/code/user.h | 35 -- firmware/code/version.h.in | 5 + firmware/tools/CMakeLists.txt | 3 +- firmware/tools/filter_test.c | 17 +- 18 files changed, 1332 insertions(+), 218 deletions(-) create mode 100644 .github/workflows/cmake.yml create mode 100644 firmware/code/configuration_manager.c create mode 100644 firmware/code/configuration_manager.h create mode 100644 firmware/code/configuration_types.h create mode 100644 firmware/code/os_descriptors.h delete mode 100644 firmware/code/user.c delete mode 100644 firmware/code/user.h create mode 100644 firmware/code/version.h.in diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml new file mode 100644 index 0000000..a0d4b9b --- /dev/null +++ b/.github/workflows/cmake.yml @@ -0,0 +1,31 @@ +name: publish +on: workflow_dispatch + +env: + BUILD_TYPE: Release + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: install dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake gcc-arm-none-eabi git python3 g++ + + - name: Build + run: | + mkdir ${{github.workspace}}/build + cd ${{github.workspace}}/build + PICO_SDK_FETCH_FROM_GIT=1 PICO_EXTRAS_FETCH_FROM_GIT=1 cmake ${{github.workspace}}/firmware/code + make -j `nproc` + + - name: Release + uses: softprops/action-gh-release@v1 + with: + draft: true + files: | + build/ploopy_headphones.* diff --git a/firmware/code/.gitignore b/firmware/code/.gitignore index 65a2f78..e735bc1 100644 --- a/firmware/code/.gitignore +++ b/firmware/code/.gitignore @@ -1,4 +1,5 @@ .vscode/ inc/ lib/ -build/ \ No newline at end of file +build/ +version.h \ No newline at end of file diff --git a/firmware/code/CMakeLists.txt b/firmware/code/CMakeLists.txt index 8dd41f5..bbcac28 100644 --- a/firmware/code/CMakeLists.txt +++ b/firmware/code/CMakeLists.txt @@ -16,21 +16,51 @@ add_executable(ploopy_headphones i2s.c fix16.c bqf.c - user.c + configuration_manager.c ) -target_include_directories(ploopy_headphones PRIVATE ${CMAKE_SOURCE_DIR}) +target_include_directories(ploopy_headphones PRIVATE + ${CMAKE_SOURCE_DIR} + ${CMAKE_BINARY_DIR}/generated + ) pico_generate_pio_header(ploopy_headphones ${CMAKE_CURRENT_LIST_DIR}/i2s.pio) +# in case Git is not available, we default to "unknown" +set(GIT_HASH "unknown") + +# find Git and if available set GIT_HASH variable +find_package(Git QUIET) +if(GIT_FOUND) + execute_process( + COMMAND ${GIT_EXECUTABLE} describe --always --dirty + OUTPUT_VARIABLE GIT_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) +endif() + +message(STATUS "Git hash is ${GIT_HASH}") + +# generate file version.hpp based on version.hpp.in +configure_file( + ${CMAKE_CURRENT_LIST_DIR}/version.h.in + ${CMAKE_BINARY_DIR}/generated/version.h + @ONLY + ) + +list(APPEND SOURCES "${CMAKE_CURRENT_BINARY_DIR}/version.cc" version.hh) + target_compile_definitions(ploopy_headphones PRIVATE # ours are zero based, so say so PICO_USBDEV_USE_ZERO_BASED_INTERFACES=1 # need large descriptor PICO_USBDEV_MAX_DESCRIPTOR_SIZE=256 - PICO_USBDEV_ISOCHRONOUS_BUFFER_STRIDE_TYPE=1 + + # make the git hash available to the firmware + GIT_HASH="${GIT_HASH}" ) pico_enable_stdio_usb(ploopy_headphones 0) @@ -49,5 +79,6 @@ target_link_libraries(ploopy_headphones hardware_sync pico_stdlib pico_multicore + pico_unique_id usb_device ) diff --git a/firmware/code/bqf.c b/firmware/code/bqf.c index 4fd7bc7..6e0b8e0 100644 --- a/firmware/code/bqf.c +++ b/firmware/code/bqf.c @@ -25,6 +25,12 @@ #include "bqf.h" +int filter_stages = 0; +bqf_coeff_t bqf_filters_left[MAX_FILTER_STAGES]; +bqf_coeff_t bqf_filters_right[MAX_FILTER_STAGES]; +bqf_mem_t bqf_filters_mem_left[MAX_FILTER_STAGES]; +bqf_mem_t bqf_filters_mem_right[MAX_FILTER_STAGES]; + /** * Configure a low-pass filter. Parameters are as follows: * @@ -477,8 +483,8 @@ fix16_t bqf_transform(fix16_t x, bqf_coeff_t *coefficients, bqf_mem_t *memory) { } void bqf_memreset(bqf_mem_t *memory) { - memory->x_1 = fix16_from_dbl(0.0); - memory->x_2 = fix16_from_dbl(0.0); - memory->y_1 = fix16_from_dbl(0.0); - memory->y_2 = fix16_from_dbl(0.0); + memory->x_1 = fix16_zero; + memory->x_2 = fix16_zero; + memory->y_1 = fix16_zero; + memory->y_2 = fix16_zero; } diff --git a/firmware/code/bqf.h b/firmware/code/bqf.h index 564e91a..b582de6 100644 --- a/firmware/code/bqf.h +++ b/firmware/code/bqf.h @@ -41,6 +41,16 @@ typedef struct _bqf_mem_t { fix16_t y_2; } bqf_mem_t; +// In reality we do not have enough CPU resource to run 8 filtering +// stages without some optimisation. +#define MAX_FILTER_STAGES 8 +extern int filter_stages; + +extern bqf_coeff_t bqf_filters_left[MAX_FILTER_STAGES]; +extern bqf_coeff_t bqf_filters_right[MAX_FILTER_STAGES]; +extern bqf_mem_t bqf_filters_mem_left[MAX_FILTER_STAGES]; +extern bqf_mem_t bqf_filters_mem_right[MAX_FILTER_STAGES]; + #define Q_BUTTERWORTH 0.707106781 #define Q_BESSEL 0.577350269 #define Q_LINKWITZ_RILEY 0.5 @@ -58,5 +68,4 @@ void bqf_highshelf_config(double, double, double, double, bqf_coeff_t *); fix16_t bqf_transform(fix16_t, bqf_coeff_t *, bqf_mem_t *); void bqf_memreset(bqf_mem_t *); - #endif diff --git a/firmware/code/configuration_manager.c b/firmware/code/configuration_manager.c new file mode 100644 index 0000000..7c4fc74 --- /dev/null +++ b/firmware/code/configuration_manager.c @@ -0,0 +1,549 @@ +/** + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include +#include +#include +#include +#include +#include +#include "configuration_manager.h" +#include "configuration_types.h" +#include "bqf.h" +#include "run.h" +#ifndef TEST_TARGET +#include "version.h" +#include "pico_base/pico/version.h" +#include "pico/multicore.h" +#include "pico/stdlib.h" +#include "pico/usb_device.h" +#include "hardware/flash.h" +#include "hardware/sync.h" +#include "hardware/i2c.h" +#endif + +/** + * We have multiple copies of the device configuration. This is the factory + * default configuration, it is static data in the firmware. + * We also potentially have a user configuration stored at the end of flash + * memory. And an in RAM working configuration. + * + * The idea is that when the device boots, it tries to use the user config + * from the end of flash. If that is not present, or is invalid, we use this + * default config instead. + * + * If the user sends an updated configuration over the USB port, it is stored + * in RAM as a working configuration, and is used (until we lose power). If + * the user issues a save command the working configuration is written to flash + * and becomes the new user configuration. + */ +static const default_configuration default_config = { + .set_configuration = { SET_CONFIGURATION, sizeof(default_config) }, + .filters = { + .filter = { FILTER_CONFIGURATION, sizeof(default_config.filters) }, + .f1 = { PEAKING, {0}, 38, -19, 0.9 }, + .f2 = { LOWSHELF, {0}, 2900, 2, 0.7 }, + .f3 = { PEAKING, {0}, 430, 3, 3.5 }, + .f4 = { HIGHSHELF, {0}, 8400, 2, 0.7 }, + .f5 = { PEAKING, {0}, 4800, 3, 5 } + }, + .preprocessing = { .header = { PREPROCESSING_CONFIGURATION, sizeof(default_config.preprocessing) }, -0.2f, false, {0} } +}; + +// Grab the last 4k page of flash for our configuration strutures. +#ifndef TEST_TARGET +static const size_t USER_CONFIGURATION_OFFSET = PICO_FLASH_SIZE_BYTES - FLASH_SECTOR_SIZE; +const uint8_t *user_configuration = (const uint8_t *) (XIP_BASE + USER_CONFIGURATION_OFFSET); +#endif +/** + * TODO: For now, assume we always get a complete configuration but maybe we + * should handle merging configurations where, for example, only a new + * filter_configuration_tlv was received. + */ +#define CFG_BUFFER_SIZE 256 +static uint8_t working_configuration[2][CFG_BUFFER_SIZE]; +static uint8_t inactive_working_configuration = 0; +static uint8_t result_buffer[CFG_BUFFER_SIZE] = { U16_TO_U8S_LE(NOK), U16_TO_U8S_LE(0) }; + +static bool reload_config = false; +static uint16_t write_offset = 0; +static uint16_t read_offset = 0; + +bool validate_filter_configuration(filter_configuration_tlv *filters) +{ + if (filters->header.type != FILTER_CONFIGURATION) { + printf("Error! Not a filter TLV (%x)..\n", filters->header.type); + return false; + } + uint8_t *ptr = (uint8_t *)filters->header.value; + const uint8_t *end = (uint8_t *)filters + filters->header.length; + int count = 0; + while ((ptr + 4) < end) { + const uint32_t type = *(uint32_t *)ptr; + const uint16_t remaining = (uint16_t)(end - ptr); + if (count++ > MAX_FILTER_STAGES) { + printf("Error! Too many filters defined. (%d)\n", count); + return false; + } + switch (type) { + case LOWPASS: + case HIGHPASS: + case BANDPASSSKIRT: + case BANDPASSPEAK: + case NOTCH: + case ALLPASS: { + if (remaining < sizeof(filter2)) { + printf("Error! Not enough data left for filter2 (%d)\n", remaining); + return false; + } + ptr += sizeof(filter2); + break; + } + case PEAKING: + case LOWSHELF: + case HIGHSHELF: { + if (remaining < sizeof(filter3)) { + printf("Error! Not enough data left for filter3 (%d)\n", remaining); + return false; + } + ptr += sizeof(filter3); + break; + } + case CUSTOMIIR: { + filter6 *args = (filter6 *)ptr; + if (remaining < sizeof(filter6)) { + printf("Error! Not enough data left for filter6 (%d)\n", remaining); + return false; + } + if (args->a0 == 0.0) { + printf("Error! The a0 co-efficient of an IIR filter must not be 0.\n"); + return false; + } + ptr += sizeof(filter6); + break; + } + + default: + printf("Unknown filter type\n"); + return false; + } + } + if (ptr != end) { + printf("Error! Did not consume the whole TLV (%p != %p)..\n", ptr, end); + return false; + } + return true; +} + +void apply_filter_configuration(filter_configuration_tlv *filters) { + uint8_t *ptr = (uint8_t *)filters->header.value; + const uint8_t *end = (uint8_t *)filters + filters->header.length; + filter_stages = 0; + bool type_changed = false; + + while ((ptr + 4) < end) { + const uint8_t type = *ptr; + // If you reset the memory, you can hear it when you move the sliders on the UI, + // so try to preserve our remembered values. + // If a filter type changes, we do a memory reset. + static uint8_t bqf_filter_types[MAX_FILTER_STAGES] = { }; + static uint32_t bqf_filter_checksum[MAX_FILTER_STAGES] = { }; + if (type != bqf_filter_types[filter_stages]) { + bqf_filter_types[filter_stages] = type; + type_changed = true; + } + + switch (type) { + case LOWPASS: INIT_FILTER2(lowpass); + case HIGHPASS: INIT_FILTER2(highpass); + case BANDPASSSKIRT: INIT_FILTER2(bandpass_skirt); + case BANDPASSPEAK: INIT_FILTER2(bandpass_peak); + case NOTCH: INIT_FILTER2(notch); + case ALLPASS: INIT_FILTER2(allpass); + case PEAKING: INIT_FILTER3(peaking); + case LOWSHELF: INIT_FILTER3(lowshelf); + case HIGHSHELF: INIT_FILTER3(highshelf); + case CUSTOMIIR: { + filter6 *args = (filter6 *)ptr; + uint32_t checksum = 0; + for (int i = 0; i < sizeof(filter6) / 4; i++) checksum ^= ((uint32_t*) args)[i]; + if (checksum != bqf_filter_checksum[filter_stages]) { + bqf_filters_left[filter_stages].a0 = fix16_from_dbl(1.0); + bqf_filters_left[filter_stages].a1 = fix16_from_dbl(args->a1/args->a0); + bqf_filters_left[filter_stages].a2 = fix16_from_dbl(args->a2/args->a0); + bqf_filters_left[filter_stages].b0 = fix16_from_dbl(args->b0/args->a0); + bqf_filters_left[filter_stages].b1 = fix16_from_dbl(args->b1/args->a0); + bqf_filters_left[filter_stages].b2 = fix16_from_dbl(args->b2/args->a0); + memcpy(&bqf_filters_right[filter_stages], &bqf_filters_left[filter_stages], sizeof(bqf_coeff_t)); + bqf_filter_checksum[filter_stages] = checksum; + } + ptr += sizeof(filter6); + type_changed = true; // Always flush our memory + break; + } + default: + break; + } + if (type_changed) { + // The memory structure stores the last 2 input samples, we can replay them into + // the new filter rather than starting again from scratch. + fix16_t left[2] = { bqf_filters_mem_left[filter_stages].x_2, bqf_filters_mem_left[filter_stages].x_1 }; + fix16_t right[2] = { bqf_filters_mem_right[filter_stages].x_2, bqf_filters_mem_right[filter_stages].x_1 }; + + bqf_memreset(&bqf_filters_mem_left[filter_stages]); + bqf_memreset(&bqf_filters_mem_right[filter_stages]); + + left[0] = bqf_transform(left[0], &bqf_filters_left[filter_stages], &bqf_filters_mem_left[filter_stages]); + left[1] = bqf_transform(left[1], &bqf_filters_left[filter_stages], &bqf_filters_mem_left[filter_stages]); + right[0] = bqf_transform(right[0], &bqf_filters_right[filter_stages], &bqf_filters_mem_right[filter_stages]); + right[1] = bqf_transform(right[1], &bqf_filters_right[filter_stages], &bqf_filters_mem_right[filter_stages]); + } + filter_stages++; + } +} + +bool validate_configuration(tlv_header *config) { + uint8_t *ptr = NULL; + switch (config->type) + { + case SET_CONFIGURATION: + ptr = (uint8_t *) config->value; + break; + case FLASH_HEADER: { + flash_header_tlv* header = (flash_header_tlv*) config; + if (header->magic != FLASH_MAGIC) { + printf("Unexpected magic word (%x)\n", header->magic); + return false; + } + if (header->version > CONFIG_VERSION) { + printf("Config is too new (%d > %d)\n", header->version, CONFIG_VERSION); + return false; + } + if (header->version < MINIMUM_CONFIG_VERSION) { + printf("Config is too old (%d > %d)\n", header->version, MINIMUM_CONFIG_VERSION); + return false; + } + ptr = (uint8_t *) header->tlvs; + break; + } + default: + printf("Unexpected Config type: %d\n", config->type); + return false; + } + const uint8_t *end = (uint8_t *)config + config->length; + while (ptr < end) { + tlv_header* tlv = (tlv_header*) ptr; + if (tlv->length < 4) { + printf("Bad length... %d\n", tlv->length); + return false; + } + switch (tlv->type) { + case FILTER_CONFIGURATION: + if (!validate_filter_configuration((filter_configuration_tlv*) tlv)) { + return false; + } + break; + case PREPROCESSING_CONFIGURATION: { + preprocessing_configuration_tlv* preprocessing_config = (preprocessing_configuration_tlv*) tlv; + if (tlv->length != sizeof(preprocessing_configuration_tlv)) { + printf("Preprocessing size missmatch: %u != %zu\n", tlv->length, sizeof(preprocessing_configuration_tlv)); + return false; + } + break; + } + case PCM3060_CONFIGURATION: { + pcm3060_configuration_tlv* pcm3060_config = (pcm3060_configuration_tlv*) tlv; + if (tlv->length != sizeof(pcm3060_configuration_tlv)) { + printf("PCM3060 config size missmatch: %u != %zu\n", tlv->length, sizeof(pcm3060_configuration_tlv)); + return false; + } + break; + } + default: + // Unknown TLVs are not invalid, just ignored. + break; + } + ptr += tlv->length; + } + return true; +} + +bool apply_configuration(tlv_header *config) { + uint8_t *ptr = NULL; + switch (config->type) + { + case SET_CONFIGURATION: + ptr = (uint8_t *) config->value; + break; + case FLASH_HEADER: { + ptr = (uint8_t *) ((flash_header_tlv*) config)->tlvs; + break; + } + default: + printf("Unexpected Config type: %d\n", config->type); + return false; + } + + const uint8_t *end = (uint8_t *)config + config->length; + while ((ptr + 4) < end) { + tlv_header* tlv = (tlv_header*) ptr; + switch (tlv->type) { + case FILTER_CONFIGURATION: + apply_filter_configuration((filter_configuration_tlv*) tlv); + break; +#ifndef TEST_TARGET + case PREPROCESSING_CONFIGURATION: { + preprocessing_configuration_tlv* preprocessing_config = (preprocessing_configuration_tlv*) tlv; + preprocessing.preamp = fix16_from_dbl(1.0 + preprocessing_config->preamp); + preprocessing.reverse_stereo = preprocessing_config->reverse_stereo; + break; + } + case PCM3060_CONFIGURATION: { + pcm3060_configuration_tlv* pcm3060_config = (pcm3060_configuration_tlv*) tlv; + audio_state.oversampling = pcm3060_config->oversampling; + audio_state.phase = pcm3060_config->phase; + audio_state.rolloff = pcm3060_config->rolloff; + audio_state.de_emphasis = pcm3060_config->de_emphasis; + break; + } +#endif + default: + break; + } + ptr += tlv->length; + } + return true; +} + +void load_config() { +#ifndef TEST_TARGET + flash_header_tlv* hdr = (flash_header_tlv*) user_configuration; + // Try to load data from flash + if (validate_configuration((tlv_header*) user_configuration)) { + apply_configuration((tlv_header*) user_configuration); + return; + } +#endif + // If that is no good, use the default config + apply_configuration((tlv_header*) &default_config); +} + +#ifndef TEST_TARGET +bool __no_inline_not_in_flash_func(save_configuration)() { + const uint8_t active_configuration = inactive_working_configuration ? 0 : 1; + tlv_header* config = (tlv_header*) working_configuration[active_configuration]; + + if (validate_configuration(config)) { + power_down_dac(); + + const size_t config_length = config->length - ((size_t)config->value - (size_t)config); + // Write data to flash + uint8_t flash_buffer[FLASH_PAGE_SIZE]; + flash_header_tlv* flash_header = (flash_header_tlv*) flash_buffer; + flash_header->header.type = FLASH_HEADER; + flash_header->header.length = sizeof(flash_header_tlv) + config_length; + flash_header->magic = FLASH_MAGIC; + flash_header->version = CONFIG_VERSION; + memcpy((void*)(flash_header->tlvs), config->value, config_length); + + uint32_t ints = save_and_disable_interrupts(); + flash_range_erase(USER_CONFIGURATION_OFFSET, FLASH_SECTOR_SIZE); + flash_range_program(USER_CONFIGURATION_OFFSET, flash_buffer, FLASH_PAGE_SIZE); + restore_interrupts(ints); + + power_up_dac(); + + return true; + } + return false; +} + +bool __no_inline_not_in_flash_func(factory_reset)() { + power_down_dac(); + uint32_t ints = save_and_disable_interrupts(); + flash_range_erase(USER_CONFIGURATION_OFFSET, FLASH_SECTOR_SIZE); + restore_interrupts(ints); + power_up_dac(); + return true; +} + +bool process_cmd(tlv_header* cmd) { + tlv_header* result = ((tlv_header*) result_buffer); + switch (cmd->type) { + case SET_CONFIGURATION: + if (validate_configuration(cmd)) { + inactive_working_configuration = inactive_working_configuration ? 0 : 1; + reload_config = true; + result->type = OK; + result->length = 4; + return true; + } + break; + case SAVE_CONFIGURATION: { + if (cmd->length == 4 && save_configuration()) { + result->type = OK; + result->length = 4; + return true; + } + break; + } + case GET_ACTIVE_CONFIGURATION: { + const uint8_t active_configuration = inactive_working_configuration ? 0 : 1; + tlv_header* config = (tlv_header*) working_configuration[active_configuration]; + if (cmd->length == 4 && config->type == SET_CONFIGURATION && validate_configuration(config)) { + result->type = OK; + result->length = config->length; + memcpy((void*)result->value, config->value, config->length - sizeof(tlv_header)); + return true; + } + break; + } + case GET_STORED_CONFIGURATION: { + if (cmd->length == 4) { + flash_header_tlv* config = (flash_header_tlv*) user_configuration; + // Assume the default config struct is good, so this can never fail. + result->type = OK; + // Try to load data from flash + if (validate_configuration((tlv_header*)config)) { + const uint16_t payload_length = MIN(CFG_BUFFER_SIZE-sizeof(tlv_header), config->header.length - ((size_t)config->tlvs - (size_t)config)); + result->length = payload_length + sizeof(tlv_header); + memcpy((void*)result->value, config->tlvs, payload_length); + return true; + } + result->length = default_config.set_configuration.length; + memcpy((void*)result->value, default_config.set_configuration.value, default_config.set_configuration.length - sizeof(tlv_header)); + return true; + } + break; + } + case FACTORY_RESET: { + if (cmd->length == 4 && factory_reset()) { + flash_header_tlv flash_header = { 0 }; + result->type = OK; + result->length = 4; + return true; + } + break; + } + case GET_VERSION: { + if (cmd->length == 4) { + static const char* PICO_SDK_VERSION = PICO_SDK_VERSION_STRING; + size_t firmware_version_len = strnlen(FIRMWARE_GIT_HASH, 64); + size_t pico_sdk_version_len = strnlen(PICO_SDK_VERSION_STRING, 64); + + result->type = OK; + result->length = 4 + sizeof(version_status_tlv) + firmware_version_len + 1 + pico_sdk_version_len + 1; + version_status_tlv* version = ((version_status_tlv*) result->value); + version->header.type = VERSION_STATUS; + version->header.length = sizeof(version_status_tlv); + version->current_version = CONFIG_VERSION; + version->minimum_supported_version = MINIMUM_CONFIG_VERSION; + version->reserved = 0xff; + memcpy((void*) version->version_strings, FIRMWARE_GIT_HASH, firmware_version_len + 1); + memcpy((void*) &(version->version_strings[firmware_version_len + 1]), PICO_SDK_VERSION_STRING, pico_sdk_version_len + 1); + + return true; + } + break; + } + } + result->type = NOK; + result->length = 4; + return false; +} + +// This callback is called when the client sends a message to the device. +// We implement a simple messaging protocol. The client sends us a message that +// we consume here. All messages are constructed of TLV's (Type Length Value). +// In some cases the Value may be a set of TLV's. However, each message has an +// owning TLV, and its length determines the length of the transfer. +// Once we have consumed the whole message, we validate it and populate the result +// buffer with a TLV which we expect the client to read next. +void config_out_packet(struct usb_endpoint *ep) { + struct usb_buffer *buffer = usb_current_out_packet_buffer(ep); + //printf("config_out_packet %d\n", buffer->data_len); + + if (write_offset + buffer->data_len > CFG_BUFFER_SIZE) + { + // Dont actually write, but this will prevent us for attempting to process this command if a zero byte packet arrives later. + write_offset += buffer->data_len; + printf("Error! Overflow receive buffer [write_offset=%d]\n", write_offset); + tlv_header* result = ((tlv_header*) result_buffer); + result->type = NOK; + result->length = 4; + } + else + { + memcpy(&working_configuration[inactive_working_configuration][write_offset], buffer->data, buffer->data_len); + write_offset += buffer->data_len; + + const uint16_t transfer_length = ((tlv_header*) working_configuration[inactive_working_configuration])->length; + if (transfer_length && write_offset >= transfer_length) { + // Command complete, fill the result buffer + write_offset = 0; + process_cmd((tlv_header*) working_configuration[inactive_working_configuration]); + read_offset = 0; + } + } + usb_grow_transfer(ep->current_transfer, 1); + usb_packet_done(ep); +} + +// This callback is called when the client attempts to read data from the device. +// The client should have previously written a command which will have populated the +// result_buffer. The client should attempt to read 4 bytes (the Type and Length) +// then attempt to read the rest of the data once the length is known. +void config_in_packet(struct usb_endpoint *ep) { + assert(ep->current_transfer); + struct usb_buffer *buffer = usb_current_in_packet_buffer(ep); + //printf("config_in_packet %d\n", buffer->data_len); + assert(buffer->data_max >= 3); + + tlv_header* result = ((tlv_header*) result_buffer); + const uint16_t transfer_length = ((tlv_header*) result_buffer)->length; + const uint16_t packet_length = MIN(buffer->data_max, (uint16_t)(transfer_length - read_offset)); + memcpy(buffer->data, &result_buffer[read_offset], packet_length); + buffer->data_len = packet_length; + read_offset += packet_length; + + if (read_offset >= transfer_length) { + // Done + read_offset = 0; + write_offset = 0; + + // If the client reads again, return nothing + result->type = NOK; + result->length = 0; + } + + usb_grow_transfer(ep->current_transfer, 1); + usb_packet_done(ep); +} + +void configuration_ep_on_cancel(struct usb_endpoint *ep) { + printf("Cancel request on config EP\n"); + write_offset = 0; + tlv_header* request = ((tlv_header*) working_configuration[inactive_working_configuration]); + request->type = NOK; + request->length = 0; +} + +void apply_config_changes() { + if (reload_config) { + reload_config = false; + const uint8_t active_configuration = inactive_working_configuration ? 0 : 1; + apply_configuration((tlv_header*) working_configuration[active_configuration]); + } +} +#endif \ No newline at end of file diff --git a/firmware/code/configuration_manager.h b/firmware/code/configuration_manager.h new file mode 100644 index 0000000..2ff115c --- /dev/null +++ b/firmware/code/configuration_manager.h @@ -0,0 +1,57 @@ +/** + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef CONFIGURATION_MANAGER_H +#define CONFIGURATION_MANAGER_H +struct usb_endpoint; + +// TODO: Duplicated from os_descriptors.h +#define U16_HIGH(_u16) ((uint8_t) (((_u16) >> 8) & 0x00ff)) +#define U16_LOW(_u16) ((uint8_t) ((_u16) & 0x00ff)) + +#define U16_TO_U8S_LE(_u16) U16_LOW(_u16), U16_HIGH(_u16) + +#define INIT_FILTER2(T) { \ + filter2 *args = (filter2 *)ptr; \ + uint32_t checksum = 0; \ + for (int i = 0; i < sizeof(filter2) / 4; i++) checksum ^= ((uint32_t*) args)[i]; \ + if (checksum != bqf_filter_checksum[filter_stages]) { \ + bqf_##T##_config(SAMPLING_FREQ, args->f0, args->Q, &bqf_filters_left[filter_stages]); \ + memcpy(&bqf_filters_right[filter_stages], &bqf_filters_left[filter_stages], sizeof(bqf_coeff_t)); \ + bqf_filter_checksum[filter_stages] = checksum; \ + } \ + ptr += sizeof(filter2); \ + break; \ + } + +#define INIT_FILTER3(T) { \ + filter3 *args = (filter3 *)ptr; \ + uint32_t checksum = 0; \ + for (int i = 0; i < sizeof(filter3) / 4; i++) checksum ^= ((uint32_t*) args)[i]; \ + if (checksum != bqf_filter_checksum[filter_stages]) { \ + bqf_##T##_config(SAMPLING_FREQ, args->f0, args->db_gain, args->Q, &bqf_filters_left[filter_stages]); \ + memcpy(&bqf_filters_right[filter_stages], &bqf_filters_left[filter_stages], sizeof(bqf_coeff_t)); \ + bqf_filter_checksum[filter_stages] = checksum; \ + } \ + ptr += sizeof(filter3); \ + break; \ + } + +void config_in_packet(struct usb_endpoint *ep); +void config_out_packet(struct usb_endpoint *ep); +void configuration_ep_on_cancel(struct usb_endpoint *ep); +extern void load_config(); +extern void apply_config_changes(); + +#endif // CONFIGURATION_MANAGER_H \ No newline at end of file diff --git a/firmware/code/configuration_types.h b/firmware/code/configuration_types.h new file mode 100644 index 0000000..f3a603a --- /dev/null +++ b/firmware/code/configuration_types.h @@ -0,0 +1,141 @@ +/** + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef __CONFIGURATION_TYPES_H__ +#define __CONFIGURATION_TYPES_H__ +#include + +#define FLASH_MAGIC 0x2E8AFEDD +#define CONFIG_VERSION 2 +#define MINIMUM_CONFIG_VERSION 1 + +enum structure_types { + // Commands/Responses, these are container TLVs. The Value will be a set of TLV structures. + OK = 0, // Standard response when a command was successful + NOK, // Standard error response + FLASH_HEADER, // A special container for the config stored in flash. Hopefully there is some useful + // metadata in here to allow us to migrate an old config to a new version. + GET_VERSION, // Returns the current config version, and the minimum supported version so clients + // can decide if they can talk to us or not. + SET_CONFIGURATION, // Updates the active configuration with the supplied TLVs + GET_ACTIVE_CONFIGURATION, // Retrieves the current active configuration TLVs from RAM + GET_STORED_CONFIGURATION, // Retrieves the current stored configuration TLVs from Flash + SAVE_CONFIGURATION, // Writes the active configuration to Flash + FACTORY_RESET, // Invalidates the flash memory + + // Configuration structures, these are returned in the body of a command/response + PREPROCESSING_CONFIGURATION = 0x200, + FILTER_CONFIGURATION, + PCM3060_CONFIGURATION, + + // Status structures, these are returned in the body of a command/response but they are + // not persisted as part of the configuration + VERSION_STATUS = 0x400, +}; + +typedef struct __attribute__((__packed__)) _tlv_header { + uint16_t type; + uint16_t length; + const uint8_t value[0]; // Doesn't take up any space, just a convenient way of accessing the value +} tlv_header; + +typedef struct __attribute__((__packed__)) _filter2 { + uint8_t type; + uint8_t reserved[3]; + double f0; + double Q; +} filter2; + +typedef struct __attribute__((__packed__)) _filter3 { + uint8_t type; + uint8_t reserved[3]; + double f0; + double db_gain; + double Q; +} filter3; + +typedef struct __attribute__((__packed__)) _filter6 { + uint8_t type; + uint8_t reserved[3]; + double a0; + double a1; + double a2; + double b0; + double b1; + double b2; +} filter6; + +enum filter_type { + LOWPASS = 0, + HIGHPASS, + BANDPASSSKIRT, + BANDPASSPEAK, + NOTCH, + ALLPASS, + PEAKING, + LOWSHELF, + HIGHSHELF, + CUSTOMIIR +}; + +typedef struct __attribute__((__packed__)) _flash_header_tlv { + tlv_header header; + uint32_t magic; + uint32_t version; + const uint8_t tlvs[0]; +} flash_header_tlv; + +typedef struct __attribute__((__packed__)) _preprocessing_configuration_tlv { + tlv_header header; + double preamp; + uint8_t reverse_stereo; + uint8_t reserved[3]; +} preprocessing_configuration_tlv; + +typedef struct __attribute__((__packed__)) _filter_configuration_tlv { + tlv_header header; + const uint8_t filters[0]; +} filter_configuration_tlv; + +typedef struct __attribute__((__packed__)) _pcm3060_configuration_tlv { + tlv_header header; + const uint8_t oversampling; + const uint8_t phase; + const uint8_t rolloff; + const uint8_t de_emphasis; +} pcm3060_configuration_tlv; + + +typedef struct __attribute__((__packed__)) _version_status_tlv { + tlv_header header; + uint16_t current_version; + uint16_t minimum_supported_version; + uint32_t reserved; + const char version_strings[0]; // Firmware version\0Pico SDK version\0 +} version_status_tlv; + +typedef struct __attribute__((__packed__)) _default_configuration { + tlv_header set_configuration; + const struct __attribute__((__packed__)) { + tlv_header filter; + filter3 f1; + filter3 f2; + filter3 f3; + filter3 f4; + filter3 f5; + } filters; + preprocessing_configuration_tlv preprocessing; +} default_configuration; + +#endif // __CONFIGURATION_TYPES_H__ \ No newline at end of file diff --git a/firmware/code/fix16.c b/firmware/code/fix16.c index b6eea54..50f05d9 100644 --- a/firmware/code/fix16.c +++ b/firmware/code/fix16.c @@ -25,13 +25,52 @@ #include #include "fix16.h" -fix16_t fix16_from_int(int16_t a) { - return a * fix16_one; +#ifdef USE_DOUBLE +fix16_t fix16_from_s16sample(int16_t a) { + return a; } -int16_t fix16_to_int(fix16_t a) { +int16_t fix16_to_s16sample(fix16_t a) { // Handle rounding up front, adding one can cause an overflow/underflow - a+=(fix16_one >> 1); + if (a < 0) { + a -= 0.5; + } else { + a += 0.5; + } + + // Saturate the value if an overflow has occurred + if (a < SHRT_MIN) { + return SHRT_MIN; + } + if (a < SHRT_MAX) { + return SHRT_MAX; + } + return a; +} + +fix16_t fix16_from_dbl(double a) { + return a; +} + +double fix16_to_dbl(fix16_t a) { + return a; +} + +fix16_t fix16_mul(fix16_t inArg0, fix16_t inArg1) { + return inArg0 * inArg1; +} +#else +fix16_t fix16_from_s16sample(int16_t a) { + return a * fix16_lsb; +} + +int16_t fix16_to_s16sample(fix16_t a) { + // Handle rounding up front, adding one can cause an overflow/underflow + if (a < 0) { + a -= (fix16_lsb >> 1); + } else { + a += (fix16_lsb >> 1); + } // Saturate the value if an overflow has occurred uint32_t upper = (a >> 30); @@ -59,12 +98,25 @@ double fix16_to_dbl(fix16_t a) { return (double)a / fix16_one; } -// hic sunt dracones +// We work in 64bits then shift the result to get +// the bit representing 1 back into the correct position. +// i.e. 1*1 == 1, so 20000000^2 >> 25 = 20000000 fix16_t fix16_mul(fix16_t inArg0, fix16_t inArg1) { - int64_t product = (int64_t)inArg0 * inArg1; - - fix16_t result = product >> 15; - result += (product & 0x4000) >> 14; - + const int64_t product = (int64_t)inArg0 * inArg1; + fix16_t result = product >> 25; + // Handle rounding where we are choppping off low order bits + // Disabled for now, too much load. We get crackling when adjusting + // the volume. + #if 0 + if (product & 0x4000) { + if (result >= 0) { + result++; + } + else { + result--; + } + } + #endif return result; } +#endif \ No newline at end of file diff --git a/firmware/code/fix16.h b/firmware/code/fix16.h index 771ab28..42b0c03 100644 --- a/firmware/code/fix16.h +++ b/firmware/code/fix16.h @@ -22,15 +22,31 @@ #ifndef FIX16_H #define FIX16_H +#include #include +// During development, it can be useful to run with real double values for reference. +//#define USE_DOUBLE +#ifdef USE_DOUBLE +typedef double fix16_t; +static const fix16_t fix16_zero = 0; +static const fix16_t fix16_one = 1; +#else +// We normalize all values into the range -32..32 with 1 extra bit for overflows +// and one bit for the sign. We allow fixed point values to overflow, but they +// are clipped at the point they are written back to a s16sample. +// +// The reason for normalizing the samples is because the filter coefficients are +// small (usually well within in the range -32..32), by normalizing everything the coefficients +// get lots of additional bits of precision. typedef int32_t fix16_t; +static const fix16_t fix16_lsb = 0x8000; +static const fix16_t fix16_one = 0x002000000; +static const fix16_t fix16_zero = 0x00000000; +#endif -static const fix16_t fix16_overflow = 0x80000000; -static const fix16_t fix16_one = 0x00008000; - -fix16_t fix16_from_int(int16_t); -int16_t fix16_to_int(fix16_t); +fix16_t fix16_from_s16sample(int16_t); +int16_t fix16_to_s16sample(fix16_t); fix16_t fix16_from_dbl(double); double fix16_to_dbl(fix16_t); diff --git a/firmware/code/os_descriptors.h b/firmware/code/os_descriptors.h new file mode 100644 index 0000000..248eb7c --- /dev/null +++ b/firmware/code/os_descriptors.h @@ -0,0 +1,102 @@ +/** + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef __OS_DESCRIPTORS__ +#define __OS_DESCRIPTORS__ +/** + * This stuff is all required to get a WinUSB driver on the control interface on Windows. + * Without this it is impossible to communicate with the device using libusb. + * + * During device enumeration the OS will request the platform capability binary object store + * (BOS) descriptor. This descriptor contains a magic UUID which signals the device supports + * the Microsoft OS 2.0 capability descriptor. It also signals the "Vendor code" and the size + * of the Microsoft OS 2.0 capability descriptor. + * Next Windows will request the Microsoft OS 2.0 capability descriptor by issuing a device + * vendor setup request, with bRequest equal to the "Vendor code" from the BOS descriptor and + * wIndex == 7. We handle this in ad_setup_request_handler() in run.c. + */ +#include + +#define U16_HIGH(_u16) ((uint8_t) (((_u16) >> 8) & 0x00ff)) +#define U16_LOW(_u16) ((uint8_t) ((_u16) & 0x00ff)) + +#define U32_BYTE3(_u32) ((uint8_t) ((((uint32_t) _u32) >> 24) & 0x000000ff)) // MSB +#define U32_BYTE2(_u32) ((uint8_t) ((((uint32_t) _u32) >> 16) & 0x000000ff)) +#define U32_BYTE1(_u32) ((uint8_t) ((((uint32_t) _u32) >> 8) & 0x000000ff)) +#define U32_BYTE0(_u32) ((uint8_t) (((uint32_t) _u32) & 0x000000ff)) // LSB + +#define U16_TO_U8S_LE(_u16) U16_LOW(_u16), U16_HIGH(_u16) +#define U32_TO_U8S_LE(_u32) U32_BYTE0(_u32), U32_BYTE1(_u32), U32_BYTE2(_u32), U32_BYTE3(_u32) +#define MS_OS_20_DESC_LEN 0xB2 + +typedef enum { + MS_OS_20_SET_HEADER_DESCRIPTOR = 0x00, + MS_OS_20_SUBSET_HEADER_CONFIGURATION = 0x01, + MS_OS_20_SUBSET_HEADER_FUNCTION = 0x02, + MS_OS_20_FEATURE_COMPATBLE_ID = 0x03, + MS_OS_20_FEATURE_REG_PROPERTY = 0x04, + MS_OS_20_FEATURE_MIN_RESUME_TIME = 0x05, + MS_OS_20_FEATURE_MODEL_ID = 0x06, + MS_OS_20_FEATURE_CCGP_DEVICE = 0x07, + MS_OS_20_FEATURE_VENDOR_REVISION = 0x08 +} microsoft_os_20_type_t; + +// Warning: The USB stack expects these descriptors to be a multiple of 64 bytes. Also the offset +// computation breaks down if the size is not a power of two. +static __aligned(4) uint8_t ms_platform_capability_bos_descriptor[PICO_USBDEV_MAX_DESCRIPTOR_SIZE] = { + // BOS Descriptor + // length, descriptor type, total length, number of device caps + 0x5, 0xF, 0x21, 0x0, 0x1, + // Platform device capability descriptor + // length, descriptor type, device capability type, reserved + 0x1C, 0x10, 0x5, 0x00, + // PlatformCapabilityUUID: D8DD60DF-4589-4CC7-9CD2-659D9E648A9F - Microsoft OS 2.0 descriptor capability + 0xDF, 0x60, 0xDD, 0xD8, 0x89, 0x45, 0xC7, 0x4C, 0x9C, 0xD2, 0x65, 0x9D, 0x9E, 0x64, 0x8A, 0x9F, + // Windows version (8.1+) + 0x0, 0x0, 0x3, 0x6, + // Length of desc_ms_os_20 + U16_TO_U8S_LE(MS_OS_20_DESC_LEN), + // Vendor code, alt enum code + 0x1, 0x0 +}; + +uint8_t desc_ms_os_20[PICO_USBDEV_MAX_DESCRIPTOR_SIZE] = { + // Set header: length, type, windows version, total length + U16_TO_U8S_LE(0x000A), U16_TO_U8S_LE(MS_OS_20_SET_HEADER_DESCRIPTOR), U32_TO_U8S_LE(0x06030000), U16_TO_U8S_LE(MS_OS_20_DESC_LEN), + + // Configuration subset header: length, type, configuration index, reserved, configuration total length + U16_TO_U8S_LE(0x0008), U16_TO_U8S_LE(MS_OS_20_SUBSET_HEADER_CONFIGURATION), 0, 0, U16_TO_U8S_LE(MS_OS_20_DESC_LEN-0x0A), + + // Function Subset header: length, type, first interface, reserved, subset length + U16_TO_U8S_LE(0x0008), U16_TO_U8S_LE(MS_OS_20_SUBSET_HEADER_FUNCTION), 2 /*interface*/, 0, U16_TO_U8S_LE(MS_OS_20_DESC_LEN-0x0A-0x08), + + // MS OS 2.0 Compatible ID descriptor: length, type, compatible ID, sub compatible ID + U16_TO_U8S_LE(0x0014), U16_TO_U8S_LE(MS_OS_20_FEATURE_COMPATBLE_ID), 'W', 'I', 'N', 'U', 'S', 'B', 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // sub-compatible + + // MS OS 2.0 Registry property descriptor: length, type + U16_TO_U8S_LE(MS_OS_20_DESC_LEN-0x0A-0x08-0x08-0x14), U16_TO_U8S_LE(MS_OS_20_FEATURE_REG_PROPERTY), + U16_TO_U8S_LE(0x0007), U16_TO_U8S_LE(0x002A), // wPropertyDataType, wPropertyNameLength and PropertyName "DeviceInterfaceGUIDs\0" in UTF-16 + 'D', 0x00, 'e', 0x00, 'v', 0x00, 'i', 0x00, 'c', 0x00, 'e', 0x00, 'I', 0x00, 'n', 0x00, 't', 0x00, 'e', 0x00, + 'r', 0x00, 'f', 0x00, 'a', 0x00, 'c', 0x00, 'e', 0x00, 'G', 0x00, 'U', 0x00, 'I', 0x00, 'D', 0x00, 's', 0x00, 0x00, 0x00, + U16_TO_U8S_LE(0x0050), // wPropertyDataLength + //bPropertyData: “{E8379B1D-6AA3-F426-2EAE-83D18090CA79}”. + '{', 0x00, 'E', 0x00, '8', 0x00, '3', 0x00, '7', 0x00, '9', 0x00, 'B', 0x00, '1', 0x00, 'D', 0x00, '-', 0x00, + '6', 0x00, 'A', 0x00, 'A', 0x00, '3', 0x00, '-', 0x00, 'F', 0x00, '4', 0x00, '2', 0x00, '6', 0x00, '-', 0x00, + '2', 0x00, 'E', 0x00, 'A', 0x00, 'E', 0x00, '-', 0x00, '8', 0x00, '3', 0x00, 'D', 0x00, '1', 0x00, '8', 0x00, + '0', 0x00, '9', 0x00, '0', 0x00, 'C', 0x00, 'A', 0x00, '7', 0x00, '9', 0x00, '}', 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +#endif //__OS_DESCRIPTORS__ \ No newline at end of file diff --git a/firmware/code/run.c b/firmware/code/run.c index 14333a0..a5778d2 100644 --- a/firmware/code/run.c +++ b/firmware/code/run.c @@ -33,47 +33,45 @@ #include "pico/stdlib.h" #include "pico/usb_device.h" +#include "pico/usb_stream_helper.h" #include "pico/multicore.h" #include "pico/bootrom.h" +#include "pico/unique_id.h" #include "AudioClassCommon.h" #include "run.h" #include "ringbuf.h" #include "i2s.h" #include "bqf.h" -#include "user.h" +#include "os_descriptors.h" +#include "configuration_manager.h" i2s_obj_t i2s_write_obj; static uint8_t *userbuf; -bqf_coeff_t bqf_filters_left[MAX_FILTER_STAGES]; -bqf_coeff_t bqf_filters_right[MAX_FILTER_STAGES]; -bqf_mem_t bqf_filters_mem_left[MAX_FILTER_STAGES]; -bqf_mem_t bqf_filters_mem_right[MAX_FILTER_STAGES]; - -static struct { - uint32_t freq; - union { - int16_t volume[2]; - int32_t _volume; - }; - union { - int16_t target_volume[2]; - int32_t _target_volume; - }; - bool mute; -} audio_state = { +audio_state_config audio_state = { .freq = 48000, + .de_emphasis_frequency = 0x1, // 48khz }; +preprocessing_config preprocessing = { + .preamp = fix16_one, + .reverse_stereo = false +}; + +static char spi_serial_number[17] = ""; + enum vendor_cmds { - REBOOT_BOOTLOADER = 0 + REBOOT_BOOTLOADER = 0, + MICROSOFT_COMPATIBLE_ID_FEATURE_DESRIPTOR }; int main(void) { setup(); - define_filters(); + // Ask the configuration_manager to load a user config from flash, + // or use the defaults. + load_config(); // start second core (called "core 1" in the SDK) multicore_launch_core1(core1_entry); @@ -90,44 +88,6 @@ int main(void) { __wfi(); } -// Here's the meat. It's where the data buffer from USB gets transformed from -// PCM data into I2S data that gets shipped out to the PCM3060. It really -// belongs with the other USB-related code due to its utter indecipherability, -// but it's placed here to emphasize its importance. -static void _as_audio_packet(struct usb_endpoint *ep) { - struct usb_buffer *usb_buffer = usb_current_out_packet_buffer(ep); - int16_t *in = (int16_t *) usb_buffer->data; - int32_t *out = (int32_t *) userbuf; - int samples = usb_buffer->data_len / 2; - - for (int i = 0; i < samples; i++) - out[i] = in[i]; - - multicore_fifo_push_blocking(CORE0_READY); - multicore_fifo_push_blocking(samples); - - for (int j = 0; j < filter_stages; j++) { - // Left channel filter - for (int i = 0; i < samples; i += 2) { - fix16_t x_f16 = fix16_from_int((int16_t) out[i]); - - x_f16 = bqf_transform(x_f16, &bqf_filters_left[j], - &bqf_filters_mem_left[j]); - - out[i] = (int32_t) fix16_to_int(x_f16); - } - } - - // Block until core 1 has finished transforming the data - uint32_t ready = multicore_fifo_pop_blocking(); - - i2s_stream_write(&i2s_write_obj, userbuf, samples * 4); - - // keep on truckin' - usb_grow_transfer(ep->current_transfer, 1); - usb_packet_done(ep); -} - static void update_volume() { if (audio_state._volume != audio_state._target_volume) { @@ -143,47 +103,123 @@ static void update_volume() audio_state._volume = audio_state._target_volume; } + + if (audio_state.pcm3060_registers != audio_state._target_pcm3060_registers) { + uint8_t buf[3]; + buf[0] = 68; // register addr + buf[1] = audio_state.target_pcm3060_registers[0]; // Reg 68 + buf[2] = audio_state.target_pcm3060_registers[1]; // Reg 69 + i2c_write_blocking(i2c0, PCM_I2C_ADDR, buf, 3, false); + audio_state.pcm3060_registers = audio_state._target_pcm3060_registers; + } +} + +// Here's the meat. It's where the data buffer from USB gets transformed from +// PCM data into I2S data that gets shipped out to the PCM3060. It really +// belongs with the other USB-related code due to its utter indecipherability, +// but it's placed here to emphasize its importance. +static void _as_audio_packet(struct usb_endpoint *ep) { + struct usb_buffer *usb_buffer = usb_current_out_packet_buffer(ep); + int16_t *in = (int16_t *) usb_buffer->data; + int32_t *out = (int32_t *) userbuf; + int samples = usb_buffer->data_len / 2; + + if (preprocessing.reverse_stereo) { + for (int i = 0; i < samples; i+=2) { + out[i] = in[i+1]; + out[i+1] = in[i]; + } + } + else { + for (int i = 0; i < samples; i++) + out[i] = in[i]; + } + + // Make sure core 1 is ready for us. + multicore_fifo_pop_blocking(); + multicore_fifo_push_blocking(CORE0_READY); + multicore_fifo_push_blocking(samples); + + for (int j = 0; j < filter_stages; j++) { + // Left channel filter + for (int i = 0; i < samples; i += 2) { + fix16_t x_f16 = fix16_mul(fix16_from_s16sample((int16_t) out[i]), preprocessing.preamp); + + x_f16 = bqf_transform(x_f16, &bqf_filters_left[j], + &bqf_filters_mem_left[j]); + + out[i] = (int32_t) fix16_to_s16sample(x_f16); + } + } + + // Block until core 1 has finished transforming the data + uint32_t ready = multicore_fifo_pop_blocking(); + multicore_fifo_push_blocking(CORE0_READY); + + // Update the volume if required. We do this from core1 as + // core0 is more heavily loaded, doing this from core0 can + // lead to audio crackling. + update_volume(); + + // Update filters if required + apply_config_changes(); + + // keep on truckin' + usb_grow_transfer(ep->current_transfer, 1); + usb_packet_done(ep); } void core1_entry() { uint8_t *userbuf = (uint8_t *) multicore_fifo_pop_blocking(); int32_t *out = (int32_t *) userbuf; + // Signal that the thread has started multicore_fifo_push_blocking(CORE1_READY); while (true) { + // Signal to core 0 that we are ready to accept new data + multicore_fifo_push_blocking(CORE1_READY); + // Block until the userbuf is filled with data uint32_t ready = multicore_fifo_pop_blocking(); while (ready != CORE0_READY) ready = multicore_fifo_pop_blocking(); - uint32_t limit = multicore_fifo_pop_blocking(); + const uint32_t samples = multicore_fifo_pop_blocking(); for (int j = 0; j < filter_stages; j++) { - for (int i = 1; i < limit; i += 2) { - fix16_t x_f16 = fix16_from_int((int16_t) out[i]); + for (int i = 1; i < samples; i += 2) { + fix16_t x_f16 = fix16_mul(fix16_from_s16sample((int16_t) out[i]), preprocessing.preamp); x_f16 = bqf_transform(x_f16, &bqf_filters_right[j], &bqf_filters_mem_right[j]); - out[i] = (int16_t) fix16_to_int(x_f16); + out[i] = (int16_t) fix16_to_s16sample(x_f16); } } // Signal to core 0 that the data has all been transformed multicore_fifo_push_blocking(CORE1_READY); - // Update the volume if required. We do this from core1 as - // core0 is more heavily loaded, doing this from core0 can - // lead to audio crackling. - update_volume(); + // Wait for Core 0 to finish running its filtering before we apply config updates + multicore_fifo_pop_blocking(); + + i2s_stream_write(&i2s_write_obj, userbuf, samples * 4); } } void setup() { set_sys_clock_khz(SYSTEM_FREQ / 1000, true); sleep_ms(100); + stdio_init_all(); + for (int i=0; ibmRequestType & USB_REQ_TYPE_RECIPIENT_MASK) == USB_REQ_TYPE_RECIPIENT_INTERFACE) { switch (setup->wValue >> 8u) { case 1: { // mute - usb_start_tiny_control_in_transfer(audio_state.mute, 1); + usb_start_tiny_control_in_transfer((audio_state.mute != 0), 1); return true; } case 2: { // volume @@ -640,11 +719,7 @@ static void audio_cmd_packet(struct usb_endpoint *ep) { if (audio_control_cmd_t.type == USB_REQ_TYPE_RECIPIENT_INTERFACE) { switch (audio_control_cmd_t.cs) { case 1: { // mute - audio_state.mute = buffer->data[0]; - uint8_t buf[2]; - buf[0] = 68; // register addr - buf[1] = buffer->data[0] ? 0x3 : 0x0; // data - i2c_write_blocking(i2c0, PCM_I2C_ADDR, buf, 2, false); + audio_state.mute = buffer->data[0] ? 0x3 : 0x0; break; } case 2: { // volume @@ -676,7 +751,11 @@ static const struct usb_transfer_type _audio_cmd_transfer_type = { static bool as_set_alternate(struct usb_interface *interface, uint alt) { assert(interface == &as_op_interface); - return alt < 2; + switch (alt) { + case 0: power_down_dac(); return true; + case 1: power_up_dac(); return true; + default: return false; + } } static bool do_set_current(struct usb_setup_packet *setup) { @@ -693,22 +772,83 @@ static bool do_set_current(struct usb_setup_packet *setup) { return false; } +static void _tf_send_control_in_ack(__unused struct usb_endpoint *endpoint, __unused struct usb_transfer *transfer) { + assert(endpoint == &usb_control_in); + assert(transfer == &_control_in_transfer); + usb_debug("_tf_setup_control_ack\n"); + static struct usb_transfer _control_out_transfer; + usb_start_empty_transfer(usb_get_control_out_endpoint(), &_control_out_transfer, 0); +} + +static struct usb_stream_transfer _control_in_stream_transfer; +#define _control_in_transfer _control_in_stream_transfer.core +static struct usb_stream_transfer_funcs control_stream_funcs = { + .on_chunk = usb_stream_noop_on_chunk, + .on_packet_complete = usb_stream_noop_on_packet_complete +}; + static bool ad_setup_request_handler(__unused struct usb_device *device, struct usb_setup_packet *setup) { setup = __builtin_assume_aligned(setup, 4); + //("ad_setup_request_handler: Type %u, Request %u, Value %u, Index %u, Length %u\n", setup->bmRequestType, setup->bRequest, setup->wValue, setup->wIndex, setup->wLength); + + if (setup->bmRequestType & USB_DIR_IN) { + if (USB_REQ_TYPE_RECIPIENT_DEVICE == (setup->bmRequestType & USB_REQ_TYPE_TYPE_MASK)) { + if ((setup->bRequest == USB_REQUEST_GET_DESCRIPTOR) && ((setup->wValue >> 8) == 0xF /* BOS */)) { + + struct usb_endpoint *usb_control_in = usb_get_control_in_endpoint(); + static struct usb_stream_transfer_funcs control_stream_funcs = { + .on_chunk = usb_stream_noop_on_chunk, + .on_packet_complete = usb_stream_noop_on_packet_complete + }; + int len = 0x21; + + len = MIN(len, setup->wLength); + usb_stream_setup_transfer(&_control_in_stream_transfer, &control_stream_funcs, ms_platform_capability_bos_descriptor, + sizeof(ms_platform_capability_bos_descriptor), len, _tf_send_control_in_ack); + + _control_in_stream_transfer.ep = usb_control_in; + usb_start_transfer(usb_control_in, &_control_in_stream_transfer.core); + return true; + } + } + } if (USB_REQ_TYPE_TYPE_VENDOR == (setup->bmRequestType & USB_REQ_TYPE_TYPE_MASK)) { // To prevent badly behaving software from accidentally triggering a reboot, e expect // the wValue to be equal to the Ploopy vendor id. if (setup->bRequest == REBOOT_BOOTLOADER && setup->wValue == 0x2E8A) { + power_down_dac(); reset_usb_boot(0, 0); // reset_usb_boot does not return, so we will not respond to this command. return true; } + else if (USB_REQ_TYPE_RECIPIENT_DEVICE == (setup->bmRequestType & USB_REQ_TYPE_RECIPIENT_MASK) && setup->bRequest == MICROSOFT_COMPATIBLE_ID_FEATURE_DESRIPTOR && setup->wIndex == 0x7) + { + const int length = MIN(MS_OS_20_DESC_LEN, setup->wLength); + //printf("Sending %u bytes (%u %u)\n", length, MS_OS_20_DESC_LEN, sizeof(desc_ms_os_20)); + struct usb_endpoint *usb_control_in = usb_get_control_in_endpoint(); + usb_stream_setup_transfer(&_control_in_stream_transfer, &control_stream_funcs, desc_ms_os_20, + sizeof(desc_ms_os_20), length, _tf_send_control_in_ack); + + _control_in_stream_transfer.ep = usb_control_in; + usb_start_transfer(usb_control_in, &_control_in_stream_transfer.core); + + return true; + } } return false; } +static struct usb_stream_transfer _config_in_stream_transfer; +static bool configuration_interface_setup_request_handler(__unused struct usb_interface *interface, struct usb_setup_packet *setup) { + setup = __builtin_assume_aligned(setup, 4); + //printf("configuration_interface_setup_request_handler: Type %u, Request %u, Value %u, Index %u, Length %u\n", setup->bmRequestType, setup->bRequest, setup->wValue, setup->wIndex, setup->wLength); + return false; +} + static bool ac_setup_request_handler(__unused struct usb_interface *interface, struct usb_setup_packet *setup) { setup = __builtin_assume_aligned(setup, 4); + //printf("ac_setup_request_handler: Type %u, Request %u, Value %u, Index %u, Length %u\n", setup->bmRequestType, setup->bRequest, setup->wValue, setup->wIndex, setup->wLength); + if (USB_REQ_TYPE_TYPE_CLASS == (setup->bmRequestType & USB_REQ_TYPE_TYPE_MASK)) { switch (setup->bRequest) { case AUDIO_REQ_SetCurrent: @@ -735,6 +875,8 @@ static bool ac_setup_request_handler(__unused struct usb_interface *interface, s bool _as_setup_request_handler(__unused struct usb_endpoint *ep, struct usb_setup_packet *setup) { setup = __builtin_assume_aligned(setup, 4); + //printf("as_setup_request_handler: Type %u, Request %u, Value %u, Index %u, Length %u\n", setup->bmRequestType, setup->bRequest, setup->wValue, setup->wIndex, setup->wLength); + if (USB_REQ_TYPE_TYPE_CLASS == (setup->bmRequestType & USB_REQ_TYPE_TYPE_MASK)) { switch (setup->bRequest) { case AUDIO_REQ_SetCurrent: @@ -777,9 +919,22 @@ void usb_sound_card_init() { as_sync_transfer.type = &as_sync_transfer_type; usb_set_default_transfer(&ep_op_sync, &as_sync_transfer); + + static struct usb_endpoint *const configuration_endpoints[] = { + &ep_configuration_out, &ep_configuration_in + }; + usb_interface_init(&configuration_interface, &ad_conf.configuration_interface, configuration_endpoints, 2, true); + configuration_interface.setup_request_handler = configuration_interface_setup_request_handler; + + config_in_transfer.type = &config_in_transfer_type; + usb_set_default_transfer(&ep_configuration_in, &config_in_transfer); + config_out_transfer.type = &config_out_transfer_type; + usb_set_default_transfer(&ep_configuration_out, &config_out_transfer); + static struct usb_interface *const boot_device_interfaces[] = { &ac_interface, &as_op_interface, + &configuration_interface }; __unused struct usb_device *device = usb_device_init(&boot_device_descriptor, @@ -795,6 +950,22 @@ void usb_sound_card_init() { usb_device_start(); } +// Some operations will cause popping on the audio output, temporarily +// disabling the DAC sounds much better. +void power_down_dac() { + uint8_t buf[2]; + buf[0] = 64; // register addr + buf[1] = 0xF0; // DAC low power mode + i2c_write_blocking(i2c0, PCM_I2C_ADDR, buf, 2, false); +} + +void power_up_dac() { + uint8_t buf[2]; + buf[0] = 64; // register addr + buf[1] = 0xE0; // DAC normal mode + i2c_write_blocking(i2c0, PCM_I2C_ADDR, buf, 2, false); +} + /***************************************************************************** * USB-related code ends here. ****************************************************************************/ diff --git a/firmware/code/run.h b/firmware/code/run.h index a704114..2a64beb 100644 --- a/firmware/code/run.h +++ b/firmware/code/run.h @@ -28,6 +28,7 @@ #include "ringbuf.h" #include "i2s.h" +#include "fix16.h" /***************************************************************************** * USB-related definitions begin here. @@ -45,6 +46,39 @@ #define MAX_VOLUME ENCODE_DB(0) #define VOLUME_RESOLUTION ENCODE_DB(0.5f) +typedef struct _audio_state_config { + uint32_t freq; + union { + int16_t volume[2]; + int32_t _volume; + }; + union { + int16_t target_volume[2]; + int32_t _target_volume; + }; + union { + struct { + // Register 68 + uint8_t mute: 2; + uint8_t phase: 1; + uint8_t reserved1: 3; + uint8_t oversampling: 1; + uint8_t reserved2: 1; + // Register 69 + uint8_t zero_fn: 1; + uint8_t zero_polarity: 1; + uint8_t reserved3: 2; + uint8_t de_emphasis: 1; + uint8_t de_emphasis_frequency: 2; + uint8_t rolloff: 1; + }; + int8_t target_pcm3060_registers[2]; + int16_t _target_pcm3060_registers; + }; + int16_t pcm3060_registers; +} audio_state_config; +extern audio_state_config audio_state; + typedef struct _audio_device_config { struct usb_configuration_descriptor descriptor; struct usb_interface_descriptor ac_interface; @@ -68,12 +102,23 @@ typedef struct _audio_device_config { USB_Audio_StdDescriptor_StreamEndpoint_Spc_t audio; } ep1; struct usb_endpoint_descriptor_long ep2; + + struct usb_interface_descriptor configuration_interface; + struct usb_endpoint_descriptor ep3; + struct usb_endpoint_descriptor ep4; } audio_device_config; +typedef struct _preprocessing_config { + fix16_t preamp; + int reverse_stereo; +} preprocessing_config; + +extern preprocessing_config preprocessing; + static char *descriptor_strings[] = { "Ploopy Corporation", "Ploopy Headphones", - "000000000001" + "0000000000000001" // Dummy serial number, will be overwritten with the value read from the SPI flash chip. Must be 17bytes. }; /***************************************************************************** @@ -132,5 +177,6 @@ static bool do_set_current(struct usb_setup_packet *); static bool ac_setup_request_handler(__unused struct usb_interface *, struct usb_setup_packet *); bool _as_setup_request_handler(__unused struct usb_endpoint *, struct usb_setup_packet *); void usb_sound_card_init(void); - +extern void power_down_dac(); +extern void power_up_dac(); #endif \ No newline at end of file diff --git a/firmware/code/user.c b/firmware/code/user.c deleted file mode 100644 index ac76599..0000000 --- a/firmware/code/user.c +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright 2022 Colin Lam, Ploopy Corporation - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "user.h" -#include "bqf.h" -#include "run.h" - -int filter_stages = 0; - -/***************************************************************************** - * Here is where your digital signal processing journey begins. Follow this - * guide, and don't forget any steps! - * - * 1. Define the filters that you want to use. Check out "bqf.c" for a - * complete list of what they are and how they work. Using those filters, you - * can create ANY digital signal shape you want. Anything you can dream of. - * 2. You're done! Enjoy the sounds of anything you want. - ****************************************************************************/ - -void define_filters() { - // First filter. - bqf_memreset(&bqf_filters_mem_left[filter_stages]); - bqf_memreset(&bqf_filters_mem_right[filter_stages]); - bqf_peaking_config(SAMPLING_FREQ, 38.0, -19.0, 0.9, &bqf_filters_left[filter_stages]); - bqf_peaking_config(SAMPLING_FREQ, 38.0, -19.0, 0.9, &bqf_filters_right[filter_stages++]); - - // Second filter. - bqf_memreset(&bqf_filters_mem_left[filter_stages]); - bqf_memreset(&bqf_filters_mem_right[filter_stages]); - bqf_lowshelf_config(SAMPLING_FREQ, 2900.0, 3.0, 4.0, &bqf_filters_left[filter_stages]); - bqf_lowshelf_config(SAMPLING_FREQ, 2900.0, 3.0, 4.0, &bqf_filters_right[filter_stages++]); - - // Third filter. - bqf_memreset(&bqf_filters_mem_left[filter_stages]); - bqf_memreset(&bqf_filters_mem_right[filter_stages]); - bqf_peaking_config(SAMPLING_FREQ, 430.0, 6.0, 3.5, &bqf_filters_left[filter_stages]); - bqf_peaking_config(SAMPLING_FREQ, 430.0, 6.0, 3.5, &bqf_filters_right[filter_stages++]); - - // Fourth filter. - bqf_memreset(&bqf_filters_mem_left[filter_stages]); - bqf_memreset(&bqf_filters_mem_right[filter_stages]); - bqf_highshelf_config(SAMPLING_FREQ, 8400.0, 3.0, 4.0, &bqf_filters_left[filter_stages]); - bqf_highshelf_config(SAMPLING_FREQ, 8400.0, 3.0, 4.0, &bqf_filters_right[filter_stages++]); - - // Fifth filter. - bqf_memreset(&bqf_filters_mem_left[filter_stages]); - bqf_memreset(&bqf_filters_mem_right[filter_stages]); - bqf_peaking_config(SAMPLING_FREQ, 4800.0, 6.0, 5.0, &bqf_filters_left[filter_stages]); - bqf_peaking_config(SAMPLING_FREQ, 4800.0, 6.0, 5.0, &bqf_filters_right[filter_stages++]); -} diff --git a/firmware/code/user.h b/firmware/code/user.h deleted file mode 100644 index 290aff2..0000000 --- a/firmware/code/user.h +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright 2022 Colin Lam, Ploopy Corporation - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#ifndef USER_H -#define USER_H - -#include "bqf.h" - -// In reality we do not have enough CPU resource to run 8 filtering -// stages without some optimisation. -#define MAX_FILTER_STAGES 8 -extern int filter_stages; - -extern bqf_coeff_t bqf_filters_left[MAX_FILTER_STAGES]; -extern bqf_coeff_t bqf_filters_right[MAX_FILTER_STAGES]; -extern bqf_mem_t bqf_filters_mem_left[MAX_FILTER_STAGES]; -extern bqf_mem_t bqf_filters_mem_right[MAX_FILTER_STAGES]; - -void define_filters(void); - -#endif diff --git a/firmware/code/version.h.in b/firmware/code/version.h.in new file mode 100644 index 0000000..63f20d2 --- /dev/null +++ b/firmware/code/version.h.in @@ -0,0 +1,5 @@ +#ifndef VERSION_H +#define VERSION_H +/** This template is populated by cmake at build time. */ +static const char* FIRMWARE_GIT_HASH = "@GIT_HASH@"; +#endif \ No newline at end of file diff --git a/firmware/tools/CMakeLists.txt b/firmware/tools/CMakeLists.txt index 9125749..2cf646a 100644 --- a/firmware/tools/CMakeLists.txt +++ b/firmware/tools/CMakeLists.txt @@ -8,9 +8,10 @@ add_executable(filter_test filter_test.c ../code/fix16.c ../code/bqf.c - ../code/user.c + ../code/configuration_manager.c ) +target_compile_definitions(filter_test PRIVATE TEST_TARGET) target_include_directories(filter_test PRIVATE ${CMAKE_SOURCE_DIR}/../code) # TODO: user.c includes run.h to get the definition for SAMPLING_FREQ, but this diff --git a/firmware/tools/filter_test.c b/firmware/tools/filter_test.c index 045ee11..43b55df 100644 --- a/firmware/tools/filter_test.c +++ b/firmware/tools/filter_test.c @@ -2,12 +2,7 @@ #include #include "bqf.h" #include "fix16.h" -#include "user.h" - -bqf_coeff_t bqf_filters_left[MAX_FILTER_STAGES]; -bqf_coeff_t bqf_filters_right[MAX_FILTER_STAGES]; -bqf_mem_t bqf_filters_mem_left[MAX_FILTER_STAGES]; -bqf_mem_t bqf_filters_mem_right[MAX_FILTER_STAGES]; +#include "configuration_manager.h" const char* usage = "Usage: %s INFILE OUTFILE\n\n" "Reads 16bit stereo PCM data from INFILE, runs it through the Ploopy headphones\n" @@ -52,7 +47,7 @@ int main(int argc, char* argv[]) // The smaple proccesing code, essentially the same as the // code in the firmware's run.c file. - define_filters(); + load_config(); for (int i = 0; i < samples; i++) { @@ -64,21 +59,21 @@ int main(int argc, char* argv[]) for (int i = 0; i < samples; i ++) { // Left channel filter - fix16_t x_f16 = fix16_from_int((int16_t) out[i]); + fix16_t x_f16 = fix16_from_s16sample((int16_t) out[i]); x_f16 = bqf_transform(x_f16, &bqf_filters_left[j], &bqf_filters_mem_left[j]); - out[i] = (int32_t) fix16_to_int(x_f16); + out[i] = (int32_t) fix16_to_s16sample(x_f16); // Right channel filter i++; - x_f16 = fix16_from_int((int16_t) out[i]); + x_f16 = fix16_from_s16sample((int16_t) out[i]); x_f16 = bqf_transform(x_f16, &bqf_filters_right[j], &bqf_filters_mem_right[j]); - out[i] = (int16_t) fix16_to_int(x_f16); + out[i] = (int16_t) fix16_to_s16sample(x_f16); } }