* 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 <george_norton_uk@hotmail.com> Co-authored-by: George Norton <30636555+george-norton@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									0564b9c3a5
								
							
						
					
					
						commit
						009dd2e698
					
				|  | @ -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.* | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| .vscode/ | .vscode/ | ||||||
| inc/ | inc/ | ||||||
| lib/ | lib/ | ||||||
| build/ | build/ | ||||||
|  | version.h | ||||||
|  | @ -16,21 +16,51 @@ add_executable(ploopy_headphones | ||||||
|     i2s.c |     i2s.c | ||||||
|     fix16.c |     fix16.c | ||||||
|     bqf.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) | 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 | target_compile_definitions(ploopy_headphones PRIVATE | ||||||
|     # ours are zero based, so say so |     # ours are zero based, so say so | ||||||
|     PICO_USBDEV_USE_ZERO_BASED_INTERFACES=1 |     PICO_USBDEV_USE_ZERO_BASED_INTERFACES=1 | ||||||
| 
 | 
 | ||||||
|     # need large descriptor |     # need large descriptor | ||||||
|     PICO_USBDEV_MAX_DESCRIPTOR_SIZE=256 |     PICO_USBDEV_MAX_DESCRIPTOR_SIZE=256 | ||||||
| 
 |  | ||||||
|     PICO_USBDEV_ISOCHRONOUS_BUFFER_STRIDE_TYPE=1 |     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) | pico_enable_stdio_usb(ploopy_headphones 0) | ||||||
|  | @ -49,5 +79,6 @@ target_link_libraries(ploopy_headphones | ||||||
|     hardware_sync |     hardware_sync | ||||||
|     pico_stdlib |     pico_stdlib | ||||||
|     pico_multicore |     pico_multicore | ||||||
|  |     pico_unique_id | ||||||
|     usb_device |     usb_device | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | @ -25,6 +25,12 @@ | ||||||
| 
 | 
 | ||||||
| #include "bqf.h" | #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: |  * 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) { | void bqf_memreset(bqf_mem_t *memory) { | ||||||
|     memory->x_1 = fix16_from_dbl(0.0); |     memory->x_1 = fix16_zero; | ||||||
|     memory->x_2 = fix16_from_dbl(0.0); |     memory->x_2 = fix16_zero; | ||||||
|     memory->y_1 = fix16_from_dbl(0.0); |     memory->y_1 = fix16_zero; | ||||||
|     memory->y_2 = fix16_from_dbl(0.0); |     memory->y_2 = fix16_zero; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -41,6 +41,16 @@ typedef struct _bqf_mem_t { | ||||||
|     fix16_t y_2; |     fix16_t y_2; | ||||||
| } bqf_mem_t; | } 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_BUTTERWORTH 0.707106781 | ||||||
| #define Q_BESSEL 0.577350269 | #define Q_BESSEL 0.577350269 | ||||||
| #define Q_LINKWITZ_RILEY 0.5 | #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 *); | fix16_t bqf_transform(fix16_t, bqf_coeff_t *, bqf_mem_t *); | ||||||
| void bqf_memreset(bqf_mem_t *); | void bqf_memreset(bqf_mem_t *); | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | @ -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 <http://www.gnu.org/licenses/>.
 | ||||||
|  |  */ | ||||||
|  | #include <math.h> | ||||||
|  | #include <stdlib.h> | ||||||
|  | #include <stdio.h> | ||||||
|  | #include <string.h> | ||||||
|  | #include <inttypes.h> | ||||||
|  | #include <stdbool.h> | ||||||
|  | #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 | ||||||
|  | @ -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 <http://www.gnu.org/licenses/>.
 | ||||||
|  |  */ | ||||||
|  | #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
 | ||||||
|  | @ -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 <http://www.gnu.org/licenses/>.
 | ||||||
|  |  */ | ||||||
|  | #ifndef __CONFIGURATION_TYPES_H__ | ||||||
|  | #define __CONFIGURATION_TYPES_H__ | ||||||
|  | #include <stdint.h> | ||||||
|  | 
 | ||||||
|  | #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__
 | ||||||
|  | @ -25,13 +25,52 @@ | ||||||
| #include <limits.h> | #include <limits.h> | ||||||
| #include "fix16.h" | #include "fix16.h" | ||||||
| 
 | 
 | ||||||
| fix16_t fix16_from_int(int16_t a) { | #ifdef USE_DOUBLE | ||||||
|     return a * fix16_one; | 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
 |     // 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
 |     // Saturate the value if an overflow has occurred
 | ||||||
|     uint32_t upper = (a >> 30); |     uint32_t upper = (a >> 30); | ||||||
|  | @ -59,12 +98,25 @@ double fix16_to_dbl(fix16_t a) { | ||||||
|     return (double)a / fix16_one; |     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) { | fix16_t fix16_mul(fix16_t inArg0, fix16_t inArg1) { | ||||||
|     int64_t product = (int64_t)inArg0 * inArg1; |     const int64_t product = (int64_t)inArg0 * inArg1; | ||||||
| 
 |     fix16_t result = product >> 25; | ||||||
|     fix16_t result = product >> 15; |     // Handle rounding where we are choppping off low order bits
 | ||||||
|     result += (product & 0x4000) >> 14; |     // 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; |     return result; | ||||||
| } | } | ||||||
|  | #endif | ||||||
|  | @ -22,15 +22,31 @@ | ||||||
| #ifndef FIX16_H | #ifndef FIX16_H | ||||||
| #define FIX16_H | #define FIX16_H | ||||||
| 
 | 
 | ||||||
|  | #include <stdbool.h> | ||||||
| #include <inttypes.h> | #include <inttypes.h> | ||||||
| 
 | 
 | ||||||
|  | // 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; | 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; | fix16_t fix16_from_s16sample(int16_t); | ||||||
| static const fix16_t fix16_one = 0x00008000; | int16_t fix16_to_s16sample(fix16_t); | ||||||
| 
 |  | ||||||
| fix16_t fix16_from_int(int16_t); |  | ||||||
| int16_t fix16_to_int(fix16_t); |  | ||||||
| fix16_t fix16_from_dbl(double); | fix16_t fix16_from_dbl(double); | ||||||
| double fix16_to_dbl(fix16_t); | double fix16_to_dbl(fix16_t); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 <http://www.gnu.org/licenses/>.
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | #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 <stdint.h> | ||||||
|  | 
 | ||||||
|  | #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__
 | ||||||
|  | @ -33,47 +33,45 @@ | ||||||
| 
 | 
 | ||||||
| #include "pico/stdlib.h" | #include "pico/stdlib.h" | ||||||
| #include "pico/usb_device.h" | #include "pico/usb_device.h" | ||||||
|  | #include "pico/usb_stream_helper.h" | ||||||
| #include "pico/multicore.h" | #include "pico/multicore.h" | ||||||
| #include "pico/bootrom.h" | #include "pico/bootrom.h" | ||||||
|  | #include "pico/unique_id.h" | ||||||
| #include "AudioClassCommon.h" | #include "AudioClassCommon.h" | ||||||
| 
 | 
 | ||||||
| #include "run.h" | #include "run.h" | ||||||
| #include "ringbuf.h" | #include "ringbuf.h" | ||||||
| #include "i2s.h" | #include "i2s.h" | ||||||
| #include "bqf.h" | #include "bqf.h" | ||||||
| #include "user.h" | #include "os_descriptors.h" | ||||||
|  | #include "configuration_manager.h" | ||||||
| 
 | 
 | ||||||
| i2s_obj_t i2s_write_obj; | i2s_obj_t i2s_write_obj; | ||||||
| static uint8_t *userbuf; | static uint8_t *userbuf; | ||||||
| 
 | 
 | ||||||
| bqf_coeff_t bqf_filters_left[MAX_FILTER_STAGES]; | audio_state_config audio_state = { | ||||||
| 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 = { |  | ||||||
|     .freq = 48000, |     .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 { | enum vendor_cmds { | ||||||
|     REBOOT_BOOTLOADER = 0 |     REBOOT_BOOTLOADER = 0, | ||||||
|  |     MICROSOFT_COMPATIBLE_ID_FEATURE_DESRIPTOR | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| int main(void) { | int main(void) { | ||||||
|     setup(); |     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)
 |     // start second core (called "core 1" in the SDK)
 | ||||||
|     multicore_launch_core1(core1_entry); |     multicore_launch_core1(core1_entry); | ||||||
|  | @ -90,44 +88,6 @@ int main(void) { | ||||||
|         __wfi(); |         __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() | static void update_volume() | ||||||
| { | { | ||||||
|     if (audio_state._volume != audio_state._target_volume) { |     if (audio_state._volume != audio_state._target_volume) { | ||||||
|  | @ -143,47 +103,123 @@ static void update_volume() | ||||||
| 
 | 
 | ||||||
|         audio_state._volume = audio_state._target_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() { | void core1_entry() { | ||||||
|     uint8_t *userbuf = (uint8_t *) multicore_fifo_pop_blocking(); |     uint8_t *userbuf = (uint8_t *) multicore_fifo_pop_blocking(); | ||||||
|     int32_t *out = (int32_t *) userbuf; |     int32_t *out = (int32_t *) userbuf; | ||||||
| 
 | 
 | ||||||
|  |     // Signal that the thread has started
 | ||||||
|     multicore_fifo_push_blocking(CORE1_READY); |     multicore_fifo_push_blocking(CORE1_READY); | ||||||
| 
 | 
 | ||||||
|     while (true) { |     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
 |         // Block until the userbuf is filled with data
 | ||||||
|         uint32_t ready = multicore_fifo_pop_blocking(); |         uint32_t ready = multicore_fifo_pop_blocking(); | ||||||
|         while (ready != CORE0_READY) |         while (ready != CORE0_READY) | ||||||
|             ready = multicore_fifo_pop_blocking(); |             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 j = 0; j < filter_stages; j++) { | ||||||
|             for (int i = 1; i < limit; i += 2) { |             for (int i = 1; i < samples; i += 2) { | ||||||
|                 fix16_t x_f16 = fix16_from_int((int16_t) out[i]); |                 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], |                 x_f16 = bqf_transform(x_f16, &bqf_filters_right[j], | ||||||
|                     &bqf_filters_mem_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
 |         // Signal to core 0 that the data has all been transformed
 | ||||||
|         multicore_fifo_push_blocking(CORE1_READY); |         multicore_fifo_push_blocking(CORE1_READY); | ||||||
| 
 | 
 | ||||||
|         // Update the volume if required. We do this from core1 as
 |         // Wait for Core 0 to finish running its filtering before we apply config updates
 | ||||||
|         // core0 is more heavily loaded, doing this from core0 can
 |         multicore_fifo_pop_blocking(); | ||||||
|         // lead to audio crackling.
 | 
 | ||||||
|         update_volume(); |         i2s_stream_write(&i2s_write_obj, userbuf, samples * 4); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void setup() { | void setup() { | ||||||
|     set_sys_clock_khz(SYSTEM_FREQ / 1000, true); |     set_sys_clock_khz(SYSTEM_FREQ / 1000, true); | ||||||
|     sleep_ms(100); |     sleep_ms(100); | ||||||
|  |     stdio_init_all(); | ||||||
| 
 | 
 | ||||||
|  |     for (int i=0; i<MAX_FILTER_STAGES; i++) { | ||||||
|  |         bqf_memreset(&bqf_filters_mem_left[i]); | ||||||
|  |         bqf_memreset(&bqf_filters_mem_right[i]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pico_get_unique_board_id_string(spi_serial_number, 17); | ||||||
|  |     descriptor_strings[2] = spi_serial_number; | ||||||
|     userbuf = malloc(sizeof(uint8_t) * RINGBUF_LEN_IN_BYTES); |     userbuf = malloc(sizeof(uint8_t) * RINGBUF_LEN_IN_BYTES); | ||||||
|      |      | ||||||
|     // Configure DAC PWM
 |     // Configure DAC PWM
 | ||||||
|  | @ -211,7 +247,7 @@ void setup() { | ||||||
|     // The PCM3060 supports standard mode (100kbps) or fast mode (400kbps)
 |     // The PCM3060 supports standard mode (100kbps) or fast mode (400kbps)
 | ||||||
|     // we run in fast mode so we dont block the core for too long while
 |     // we run in fast mode so we dont block the core for too long while
 | ||||||
|     // updating the volume.
 |     // updating the volume.
 | ||||||
|     i2c_init(i2c0, 400000); |     i2c_init(i2c0, 100000); | ||||||
|     gpio_set_function(PCM3060_SDA_PIN, GPIO_FUNC_I2C); |     gpio_set_function(PCM3060_SDA_PIN, GPIO_FUNC_I2C); | ||||||
|     gpio_set_function(PCM3060_SCL_PIN, GPIO_FUNC_I2C); |     gpio_set_function(PCM3060_SCL_PIN, GPIO_FUNC_I2C); | ||||||
|     gpio_pull_up(PCM3060_SDA_PIN); |     gpio_pull_up(PCM3060_SDA_PIN); | ||||||
|  | @ -229,11 +265,6 @@ void setup() { | ||||||
|     // Don't remove this. Don't do it.
 |     // Don't remove this. Don't do it.
 | ||||||
|     sleep_ms(200); |     sleep_ms(200); | ||||||
| 
 | 
 | ||||||
|     // Set data format to 16 bit right justified, MSB first
 |  | ||||||
|     buf[0] = 67;   // register addr
 |  | ||||||
|     buf[1] = 0x03; // data
 |  | ||||||
|     i2c_write_blocking(i2c0, PCM_I2C_ADDR, buf, 2, false); |  | ||||||
| 
 |  | ||||||
|     // Enable DAC
 |     // Enable DAC
 | ||||||
|     buf[0] = 64; // register addr
 |     buf[0] = 64; // register addr
 | ||||||
|     buf[1] = 0xE0; // data
 |     buf[1] = 0xE0; // data
 | ||||||
|  | @ -242,6 +273,11 @@ void setup() { | ||||||
|     // Same here, pal. Hands off.
 |     // Same here, pal. Hands off.
 | ||||||
|     sleep_ms(100); |     sleep_ms(100); | ||||||
| 
 | 
 | ||||||
|  |     // Set data format to 16 bit right justified, MSB first
 | ||||||
|  |     buf[0] = 67;   // register addr
 | ||||||
|  |     buf[1] = 0x03; // data
 | ||||||
|  |     i2c_write_blocking(i2c0, PCM_I2C_ADDR, buf, 2, false); | ||||||
|  | 
 | ||||||
|     i2s_write_obj.sck_pin = PCM3060_DAC_SCK_PIN; |     i2s_write_obj.sck_pin = PCM3060_DAC_SCK_PIN; | ||||||
|     i2s_write_obj.ws_pin = PCM3060_DAC_WS_PIN; |     i2s_write_obj.ws_pin = PCM3060_DAC_WS_PIN; | ||||||
|     i2s_write_obj.sd_pin = PCM3060_DAC_SD_PIN; |     i2s_write_obj.sd_pin = PCM3060_DAC_SD_PIN; | ||||||
|  | @ -296,7 +332,7 @@ static const audio_device_config ad_conf = { | ||||||
|         .bLength = sizeof(ad_conf.descriptor), |         .bLength = sizeof(ad_conf.descriptor), | ||||||
|         .bDescriptorType = DTYPE_Configuration, |         .bDescriptorType = DTYPE_Configuration, | ||||||
|         .wTotalLength = sizeof(ad_conf), |         .wTotalLength = sizeof(ad_conf), | ||||||
|         .bNumInterfaces = 2, |         .bNumInterfaces = 3, | ||||||
|         .bConfigurationValue = 0x01, |         .bConfigurationValue = 0x01, | ||||||
|         .iConfiguration = 0x00, |         .iConfiguration = 0x00, | ||||||
|         .bmAttributes = 0x80, |         .bmAttributes = 0x80, | ||||||
|  | @ -439,16 +475,45 @@ static const audio_device_config ad_conf = { | ||||||
|         .bRefresh = 2, |         .bRefresh = 2, | ||||||
|         .bSyncAddr = 0, |         .bSyncAddr = 0, | ||||||
|     }, |     }, | ||||||
|  |     .configuration_interface = { | ||||||
|  |         .bLength = sizeof(ad_conf.configuration_interface), | ||||||
|  |         .bDescriptorType = DTYPE_Interface, | ||||||
|  |         .bInterfaceNumber = 0x02, | ||||||
|  |         .bAlternateSetting = 0x00, | ||||||
|  |         .bNumEndpoints = 0x02, | ||||||
|  |         .bInterfaceClass = 0xff, // Vendor Specific
 | ||||||
|  |         .bInterfaceSubClass = 0, | ||||||
|  |         .bInterfaceProtocol = 0, | ||||||
|  |         .iInterface = 0x00 | ||||||
|  |     }, | ||||||
|  |     .ep3 = { | ||||||
|  |         .bLength = sizeof(ad_conf.ep3), | ||||||
|  |         .bDescriptorType = 0x05, | ||||||
|  |         .bEndpointAddress = 0x03, | ||||||
|  |         .bmAttributes = 0x2, | ||||||
|  |         .wMaxPacketSize = 0x40, | ||||||
|  |         .bInterval = 0x0 | ||||||
|  |     }, | ||||||
|  |     .ep4 = { | ||||||
|  |         .bLength = sizeof(ad_conf.ep3), | ||||||
|  |         .bDescriptorType = 0x05, | ||||||
|  |         .bEndpointAddress = 0x84, | ||||||
|  |         .bmAttributes = 0x2, | ||||||
|  |         .wMaxPacketSize = 0x40, | ||||||
|  |         .bInterval = 0x0 | ||||||
|  |     } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| static struct usb_interface ac_interface; | static struct usb_interface ac_interface; | ||||||
| static struct usb_interface as_op_interface; | static struct usb_interface as_op_interface; | ||||||
| static struct usb_endpoint ep_op_out, ep_op_sync; | static struct usb_endpoint ep_op_out, ep_op_sync; | ||||||
|  | static struct usb_interface configuration_interface; | ||||||
|  | static struct usb_endpoint ep_configuration_in, ep_configuration_out; | ||||||
| 
 | 
 | ||||||
| static const struct usb_device_descriptor boot_device_descriptor = { | static const struct usb_device_descriptor boot_device_descriptor = { | ||||||
|     .bLength            = 18, |     .bLength            = 18, | ||||||
|     .bDescriptorType    = 0x01, |     .bDescriptorType    = 0x01, | ||||||
|     .bcdUSB             = 0x0110, |     .bcdUSB             = 0x0210, | ||||||
|     .bDeviceClass       = 0x00, |     .bDeviceClass       = 0x00, | ||||||
|     .bDeviceSubClass    = 0x00, |     .bDeviceSubClass    = 0x00, | ||||||
|     .bDeviceProtocol    = 0x00, |     .bDeviceProtocol    = 0x00, | ||||||
|  | @ -520,11 +585,25 @@ static const struct usb_transfer_type as_sync_transfer_type = { | ||||||
| static struct usb_transfer as_transfer; | static struct usb_transfer as_transfer; | ||||||
| static struct usb_transfer as_sync_transfer; | static struct usb_transfer as_sync_transfer; | ||||||
| 
 | 
 | ||||||
|  | static const struct usb_transfer_type config_in_transfer_type = { | ||||||
|  |     .on_packet = config_in_packet, | ||||||
|  |     .initial_packet_count = 1, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | static const struct usb_transfer_type config_out_transfer_type = { | ||||||
|  |     .on_packet = config_out_packet, | ||||||
|  |     .on_cancel = configuration_ep_on_cancel, | ||||||
|  |     .initial_packet_count = 1, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | static struct usb_transfer config_in_transfer; | ||||||
|  | static struct usb_transfer config_out_transfer; | ||||||
|  | 
 | ||||||
| static bool do_get_current(struct usb_setup_packet *setup) { | static bool do_get_current(struct usb_setup_packet *setup) { | ||||||
|     if ((setup->bmRequestType & USB_REQ_TYPE_RECIPIENT_MASK) == USB_REQ_TYPE_RECIPIENT_INTERFACE) { |     if ((setup->bmRequestType & USB_REQ_TYPE_RECIPIENT_MASK) == USB_REQ_TYPE_RECIPIENT_INTERFACE) { | ||||||
|         switch (setup->wValue >> 8u) { |         switch (setup->wValue >> 8u) { | ||||||
|             case 1: { // mute
 |             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; |                 return true; | ||||||
|             } |             } | ||||||
|             case 2: { // volume
 |             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) { |         if (audio_control_cmd_t.type == USB_REQ_TYPE_RECIPIENT_INTERFACE) { | ||||||
|             switch (audio_control_cmd_t.cs) { |             switch (audio_control_cmd_t.cs) { | ||||||
|                 case 1: { // mute
 |                 case 1: { // mute
 | ||||||
|                     audio_state.mute = buffer->data[0]; |                     audio_state.mute = buffer->data[0] ? 0x3 : 0x0; | ||||||
|                     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); |  | ||||||
|                     break; |                     break; | ||||||
|                 } |                 } | ||||||
|                 case 2: { // volume
 |                 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) { | static bool as_set_alternate(struct usb_interface *interface, uint alt) { | ||||||
|     assert(interface == &as_op_interface); |     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) { | 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; |     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) { | static bool ad_setup_request_handler(__unused struct usb_device *device, struct usb_setup_packet *setup) { | ||||||
|     setup = __builtin_assume_aligned(setup, 4); |     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)) { |     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
 |         // To prevent badly behaving software from accidentally triggering a reboot, e expect
 | ||||||
|         // the wValue to be equal to the Ploopy vendor id.
 |         // the wValue to be equal to the Ploopy vendor id.
 | ||||||
|         if (setup->bRequest == REBOOT_BOOTLOADER && setup->wValue == 0x2E8A) { |         if (setup->bRequest == REBOOT_BOOTLOADER && setup->wValue == 0x2E8A) { | ||||||
|  |             power_down_dac(); | ||||||
|             reset_usb_boot(0, 0); |             reset_usb_boot(0, 0); | ||||||
|             // reset_usb_boot does not return, so we will not respond to this command.
 |             // reset_usb_boot does not return, so we will not respond to this command.
 | ||||||
|             return true; |             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; |     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) { | static bool ac_setup_request_handler(__unused struct usb_interface *interface, struct usb_setup_packet *setup) { | ||||||
|     setup = __builtin_assume_aligned(setup, 4); |     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)) { |     if (USB_REQ_TYPE_TYPE_CLASS == (setup->bmRequestType & USB_REQ_TYPE_TYPE_MASK)) { | ||||||
|         switch (setup->bRequest) { |         switch (setup->bRequest) { | ||||||
|             case AUDIO_REQ_SetCurrent: |             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) { | bool _as_setup_request_handler(__unused struct usb_endpoint *ep, struct usb_setup_packet *setup) { | ||||||
|     setup = __builtin_assume_aligned(setup, 4); |     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)) { |     if (USB_REQ_TYPE_TYPE_CLASS == (setup->bmRequestType & USB_REQ_TYPE_TYPE_MASK)) { | ||||||
|         switch (setup->bRequest) { |         switch (setup->bRequest) { | ||||||
|             case AUDIO_REQ_SetCurrent: |             case AUDIO_REQ_SetCurrent: | ||||||
|  | @ -777,9 +919,22 @@ void usb_sound_card_init() { | ||||||
|     as_sync_transfer.type = &as_sync_transfer_type; |     as_sync_transfer.type = &as_sync_transfer_type; | ||||||
|     usb_set_default_transfer(&ep_op_sync, &as_sync_transfer); |     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[] = { |     static struct usb_interface *const boot_device_interfaces[] = { | ||||||
|         &ac_interface, |         &ac_interface, | ||||||
|         &as_op_interface, |         &as_op_interface, | ||||||
|  |         &configuration_interface | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     __unused struct usb_device *device = usb_device_init(&boot_device_descriptor, |     __unused struct usb_device *device = usb_device_init(&boot_device_descriptor, | ||||||
|  | @ -795,6 +950,22 @@ void usb_sound_card_init() { | ||||||
|     usb_device_start(); |     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. |  * USB-related code ends here. | ||||||
|  ****************************************************************************/ |  ****************************************************************************/ | ||||||
|  |  | ||||||
|  | @ -28,6 +28,7 @@ | ||||||
| 
 | 
 | ||||||
| #include "ringbuf.h" | #include "ringbuf.h" | ||||||
| #include "i2s.h" | #include "i2s.h" | ||||||
|  | #include "fix16.h" | ||||||
| 
 | 
 | ||||||
| /*****************************************************************************
 | /*****************************************************************************
 | ||||||
|  * USB-related definitions begin here. |  * USB-related definitions begin here. | ||||||
|  | @ -45,6 +46,39 @@ | ||||||
| #define MAX_VOLUME ENCODE_DB(0) | #define MAX_VOLUME ENCODE_DB(0) | ||||||
| #define VOLUME_RESOLUTION ENCODE_DB(0.5f) | #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 { | typedef struct _audio_device_config { | ||||||
|     struct usb_configuration_descriptor descriptor; |     struct usb_configuration_descriptor descriptor; | ||||||
|     struct usb_interface_descriptor ac_interface; |     struct usb_interface_descriptor ac_interface; | ||||||
|  | @ -68,12 +102,23 @@ typedef struct _audio_device_config { | ||||||
|         USB_Audio_StdDescriptor_StreamEndpoint_Spc_t audio; |         USB_Audio_StdDescriptor_StreamEndpoint_Spc_t audio; | ||||||
|     } ep1; |     } ep1; | ||||||
|     struct usb_endpoint_descriptor_long ep2; |     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; | } audio_device_config; | ||||||
| 
 | 
 | ||||||
|  | typedef struct _preprocessing_config { | ||||||
|  |     fix16_t preamp; | ||||||
|  |     int reverse_stereo; | ||||||
|  | } preprocessing_config; | ||||||
|  | 
 | ||||||
|  | extern preprocessing_config preprocessing; | ||||||
|  | 
 | ||||||
| static char *descriptor_strings[] = { | static char *descriptor_strings[] = { | ||||||
|     "Ploopy Corporation", |     "Ploopy Corporation", | ||||||
|     "Ploopy Headphones", |     "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 *); | 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 *); | bool _as_setup_request_handler(__unused struct usb_endpoint *, struct usb_setup_packet *); | ||||||
| void usb_sound_card_init(void); | void usb_sound_card_init(void); | ||||||
| 
 | extern void power_down_dac(); | ||||||
|  | extern void power_up_dac(); | ||||||
| #endif | #endif | ||||||
|  | @ -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 <http://www.gnu.org/licenses/>.
 |  | ||||||
|  */ |  | ||||||
| 
 |  | ||||||
| #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++]); |  | ||||||
| } |  | ||||||
|  | @ -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 <http://www.gnu.org/licenses/>.
 |  | ||||||
|  */ |  | ||||||
| 
 |  | ||||||
| #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 |  | ||||||
|  | @ -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 | ||||||
|  | @ -8,9 +8,10 @@ add_executable(filter_test | ||||||
|     filter_test.c |     filter_test.c | ||||||
|     ../code/fix16.c |     ../code/fix16.c | ||||||
|     ../code/bqf.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) | 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 | # TODO: user.c includes run.h to get the definition for SAMPLING_FREQ, but this | ||||||
|  |  | ||||||
|  | @ -2,12 +2,7 @@ | ||||||
| #include <stdlib.h> | #include <stdlib.h> | ||||||
| #include "bqf.h" | #include "bqf.h" | ||||||
| #include "fix16.h" | #include "fix16.h" | ||||||
| #include "user.h" | #include "configuration_manager.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]; |  | ||||||
| 
 | 
 | ||||||
| const char* usage = "Usage: %s INFILE OUTFILE\n\n" | const char* usage = "Usage: %s INFILE OUTFILE\n\n" | ||||||
|     "Reads 16bit stereo PCM data from INFILE, runs it through the Ploopy headphones\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
 |     // The smaple proccesing code, essentially the same as the
 | ||||||
|     // code in the firmware's run.c file.
 |     // code in the firmware's run.c file.
 | ||||||
|     define_filters(); |     load_config(); | ||||||
| 
 | 
 | ||||||
|     for (int i = 0; i < samples; i++) |     for (int i = 0; i < samples; i++) | ||||||
|     { |     { | ||||||
|  | @ -64,21 +59,21 @@ int main(int argc, char* argv[]) | ||||||
|         for (int i = 0; i < samples; i ++) |         for (int i = 0; i < samples; i ++) | ||||||
|         { |         { | ||||||
|             // Left channel filter
 |             // 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], |             x_f16 = bqf_transform(x_f16, &bqf_filters_left[j], | ||||||
|                 &bqf_filters_mem_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
 |             // Right channel filter
 | ||||||
|             i++; |             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], |             x_f16 = bqf_transform(x_f16, &bqf_filters_right[j], | ||||||
|                 &bqf_filters_mem_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); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	 ploopyco
						ploopyco