Headphones toolbox (#18)
* 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
b1b72e0f6b
|
@ -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/
|
||||
inc/
|
||||
lib/
|
||||
build/
|
||||
build/
|
||||
version.h
|
|
@ -16,21 +16,51 @@ add_executable(ploopy_headphones
|
|||
i2s.c
|
||||
fix16.c
|
||||
bqf.c
|
||||
user.c
|
||||
configuration_manager.c
|
||||
)
|
||||
|
||||
target_include_directories(ploopy_headphones PRIVATE ${CMAKE_SOURCE_DIR})
|
||||
target_include_directories(ploopy_headphones PRIVATE
|
||||
${CMAKE_SOURCE_DIR}
|
||||
${CMAKE_BINARY_DIR}/generated
|
||||
)
|
||||
|
||||
pico_generate_pio_header(ploopy_headphones ${CMAKE_CURRENT_LIST_DIR}/i2s.pio)
|
||||
|
||||
# in case Git is not available, we default to "unknown"
|
||||
set(GIT_HASH "unknown")
|
||||
|
||||
# find Git and if available set GIT_HASH variable
|
||||
find_package(Git QUIET)
|
||||
if(GIT_FOUND)
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} describe --always --dirty
|
||||
OUTPUT_VARIABLE GIT_HASH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_QUIET
|
||||
)
|
||||
endif()
|
||||
|
||||
message(STATUS "Git hash is ${GIT_HASH}")
|
||||
|
||||
# generate file version.hpp based on version.hpp.in
|
||||
configure_file(
|
||||
${CMAKE_CURRENT_LIST_DIR}/version.h.in
|
||||
${CMAKE_BINARY_DIR}/generated/version.h
|
||||
@ONLY
|
||||
)
|
||||
|
||||
list(APPEND SOURCES "${CMAKE_CURRENT_BINARY_DIR}/version.cc" version.hh)
|
||||
|
||||
target_compile_definitions(ploopy_headphones PRIVATE
|
||||
# ours are zero based, so say so
|
||||
PICO_USBDEV_USE_ZERO_BASED_INTERFACES=1
|
||||
|
||||
# need large descriptor
|
||||
PICO_USBDEV_MAX_DESCRIPTOR_SIZE=256
|
||||
|
||||
PICO_USBDEV_ISOCHRONOUS_BUFFER_STRIDE_TYPE=1
|
||||
|
||||
# make the git hash available to the firmware
|
||||
GIT_HASH="${GIT_HASH}"
|
||||
)
|
||||
|
||||
pico_enable_stdio_usb(ploopy_headphones 0)
|
||||
|
@ -49,5 +79,6 @@ target_link_libraries(ploopy_headphones
|
|||
hardware_sync
|
||||
pico_stdlib
|
||||
pico_multicore
|
||||
pico_unique_id
|
||||
usb_device
|
||||
)
|
||||
|
|
|
@ -25,6 +25,12 @@
|
|||
|
||||
#include "bqf.h"
|
||||
|
||||
int filter_stages = 0;
|
||||
bqf_coeff_t bqf_filters_left[MAX_FILTER_STAGES];
|
||||
bqf_coeff_t bqf_filters_right[MAX_FILTER_STAGES];
|
||||
bqf_mem_t bqf_filters_mem_left[MAX_FILTER_STAGES];
|
||||
bqf_mem_t bqf_filters_mem_right[MAX_FILTER_STAGES];
|
||||
|
||||
/**
|
||||
* Configure a low-pass filter. Parameters are as follows:
|
||||
*
|
||||
|
@ -477,8 +483,8 @@ fix16_t bqf_transform(fix16_t x, bqf_coeff_t *coefficients, bqf_mem_t *memory) {
|
|||
}
|
||||
|
||||
void bqf_memreset(bqf_mem_t *memory) {
|
||||
memory->x_1 = fix16_from_dbl(0.0);
|
||||
memory->x_2 = fix16_from_dbl(0.0);
|
||||
memory->y_1 = fix16_from_dbl(0.0);
|
||||
memory->y_2 = fix16_from_dbl(0.0);
|
||||
memory->x_1 = fix16_zero;
|
||||
memory->x_2 = fix16_zero;
|
||||
memory->y_1 = fix16_zero;
|
||||
memory->y_2 = fix16_zero;
|
||||
}
|
||||
|
|
|
@ -41,6 +41,16 @@ typedef struct _bqf_mem_t {
|
|||
fix16_t y_2;
|
||||
} bqf_mem_t;
|
||||
|
||||
// In reality we do not have enough CPU resource to run 8 filtering
|
||||
// stages without some optimisation.
|
||||
#define MAX_FILTER_STAGES 8
|
||||
extern int filter_stages;
|
||||
|
||||
extern bqf_coeff_t bqf_filters_left[MAX_FILTER_STAGES];
|
||||
extern bqf_coeff_t bqf_filters_right[MAX_FILTER_STAGES];
|
||||
extern bqf_mem_t bqf_filters_mem_left[MAX_FILTER_STAGES];
|
||||
extern bqf_mem_t bqf_filters_mem_right[MAX_FILTER_STAGES];
|
||||
|
||||
#define Q_BUTTERWORTH 0.707106781
|
||||
#define Q_BESSEL 0.577350269
|
||||
#define Q_LINKWITZ_RILEY 0.5
|
||||
|
@ -58,5 +68,4 @@ void bqf_highshelf_config(double, double, double, double, bqf_coeff_t *);
|
|||
fix16_t bqf_transform(fix16_t, bqf_coeff_t *, bqf_mem_t *);
|
||||
void bqf_memreset(bqf_mem_t *);
|
||||
|
||||
|
||||
#endif
|
||||
|
|
|
@ -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 "fix16.h"
|
||||
|
||||
fix16_t fix16_from_int(int16_t a) {
|
||||
return a * fix16_one;
|
||||
#ifdef USE_DOUBLE
|
||||
fix16_t fix16_from_s16sample(int16_t a) {
|
||||
return a;
|
||||
}
|
||||
|
||||
int16_t fix16_to_int(fix16_t a) {
|
||||
int16_t fix16_to_s16sample(fix16_t a) {
|
||||
// Handle rounding up front, adding one can cause an overflow/underflow
|
||||
a+=(fix16_one >> 1);
|
||||
if (a < 0) {
|
||||
a -= 0.5;
|
||||
} else {
|
||||
a += 0.5;
|
||||
}
|
||||
|
||||
// Saturate the value if an overflow has occurred
|
||||
if (a < SHRT_MIN) {
|
||||
return SHRT_MIN;
|
||||
}
|
||||
if (a < SHRT_MAX) {
|
||||
return SHRT_MAX;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
fix16_t fix16_from_dbl(double a) {
|
||||
return a;
|
||||
}
|
||||
|
||||
double fix16_to_dbl(fix16_t a) {
|
||||
return a;
|
||||
}
|
||||
|
||||
fix16_t fix16_mul(fix16_t inArg0, fix16_t inArg1) {
|
||||
return inArg0 * inArg1;
|
||||
}
|
||||
#else
|
||||
fix16_t fix16_from_s16sample(int16_t a) {
|
||||
return a * fix16_lsb;
|
||||
}
|
||||
|
||||
int16_t fix16_to_s16sample(fix16_t a) {
|
||||
// Handle rounding up front, adding one can cause an overflow/underflow
|
||||
if (a < 0) {
|
||||
a -= (fix16_lsb >> 1);
|
||||
} else {
|
||||
a += (fix16_lsb >> 1);
|
||||
}
|
||||
|
||||
// Saturate the value if an overflow has occurred
|
||||
uint32_t upper = (a >> 30);
|
||||
|
@ -59,12 +98,25 @@ double fix16_to_dbl(fix16_t a) {
|
|||
return (double)a / fix16_one;
|
||||
}
|
||||
|
||||
// hic sunt dracones
|
||||
// We work in 64bits then shift the result to get
|
||||
// the bit representing 1 back into the correct position.
|
||||
// i.e. 1*1 == 1, so 20000000^2 >> 25 = 20000000
|
||||
fix16_t fix16_mul(fix16_t inArg0, fix16_t inArg1) {
|
||||
int64_t product = (int64_t)inArg0 * inArg1;
|
||||
|
||||
fix16_t result = product >> 15;
|
||||
result += (product & 0x4000) >> 14;
|
||||
|
||||
const int64_t product = (int64_t)inArg0 * inArg1;
|
||||
fix16_t result = product >> 25;
|
||||
// Handle rounding where we are choppping off low order bits
|
||||
// Disabled for now, too much load. We get crackling when adjusting
|
||||
// the volume.
|
||||
#if 0
|
||||
if (product & 0x4000) {
|
||||
if (result >= 0) {
|
||||
result++;
|
||||
}
|
||||
else {
|
||||
result--;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
#endif
|
|
@ -22,15 +22,31 @@
|
|||
#ifndef FIX16_H
|
||||
#define FIX16_H
|
||||
|
||||
#include <stdbool.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;
|
||||
static const fix16_t fix16_lsb = 0x8000;
|
||||
static const fix16_t fix16_one = 0x002000000;
|
||||
static const fix16_t fix16_zero = 0x00000000;
|
||||
#endif
|
||||
|
||||
static const fix16_t fix16_overflow = 0x80000000;
|
||||
static const fix16_t fix16_one = 0x00008000;
|
||||
|
||||
fix16_t fix16_from_int(int16_t);
|
||||
int16_t fix16_to_int(fix16_t);
|
||||
fix16_t fix16_from_s16sample(int16_t);
|
||||
int16_t fix16_to_s16sample(fix16_t);
|
||||
fix16_t fix16_from_dbl(double);
|
||||
double fix16_to_dbl(fix16_t);
|
||||
|
||||
|
|
|
@ -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/usb_device.h"
|
||||
#include "pico/usb_stream_helper.h"
|
||||
#include "pico/multicore.h"
|
||||
#include "pico/bootrom.h"
|
||||
#include "pico/unique_id.h"
|
||||
#include "AudioClassCommon.h"
|
||||
|
||||
#include "run.h"
|
||||
#include "ringbuf.h"
|
||||
#include "i2s.h"
|
||||
#include "bqf.h"
|
||||
#include "user.h"
|
||||
#include "os_descriptors.h"
|
||||
#include "configuration_manager.h"
|
||||
|
||||
i2s_obj_t i2s_write_obj;
|
||||
static uint8_t *userbuf;
|
||||
|
||||
bqf_coeff_t bqf_filters_left[MAX_FILTER_STAGES];
|
||||
bqf_coeff_t bqf_filters_right[MAX_FILTER_STAGES];
|
||||
bqf_mem_t bqf_filters_mem_left[MAX_FILTER_STAGES];
|
||||
bqf_mem_t bqf_filters_mem_right[MAX_FILTER_STAGES];
|
||||
|
||||
static struct {
|
||||
uint32_t freq;
|
||||
union {
|
||||
int16_t volume[2];
|
||||
int32_t _volume;
|
||||
};
|
||||
union {
|
||||
int16_t target_volume[2];
|
||||
int32_t _target_volume;
|
||||
};
|
||||
bool mute;
|
||||
} audio_state = {
|
||||
audio_state_config audio_state = {
|
||||
.freq = 48000,
|
||||
.de_emphasis_frequency = 0x1, // 48khz
|
||||
};
|
||||
|
||||
preprocessing_config preprocessing = {
|
||||
.preamp = fix16_one,
|
||||
.reverse_stereo = false
|
||||
};
|
||||
|
||||
static char spi_serial_number[17] = "";
|
||||
|
||||
enum vendor_cmds {
|
||||
REBOOT_BOOTLOADER = 0
|
||||
REBOOT_BOOTLOADER = 0,
|
||||
MICROSOFT_COMPATIBLE_ID_FEATURE_DESRIPTOR
|
||||
};
|
||||
|
||||
int main(void) {
|
||||
setup();
|
||||
|
||||
define_filters();
|
||||
// Ask the configuration_manager to load a user config from flash,
|
||||
// or use the defaults.
|
||||
load_config();
|
||||
|
||||
// start second core (called "core 1" in the SDK)
|
||||
multicore_launch_core1(core1_entry);
|
||||
|
@ -90,44 +88,6 @@ int main(void) {
|
|||
__wfi();
|
||||
}
|
||||
|
||||
// Here's the meat. It's where the data buffer from USB gets transformed from
|
||||
// PCM data into I2S data that gets shipped out to the PCM3060. It really
|
||||
// belongs with the other USB-related code due to its utter indecipherability,
|
||||
// but it's placed here to emphasize its importance.
|
||||
static void _as_audio_packet(struct usb_endpoint *ep) {
|
||||
struct usb_buffer *usb_buffer = usb_current_out_packet_buffer(ep);
|
||||
int16_t *in = (int16_t *) usb_buffer->data;
|
||||
int32_t *out = (int32_t *) userbuf;
|
||||
int samples = usb_buffer->data_len / 2;
|
||||
|
||||
for (int i = 0; i < samples; i++)
|
||||
out[i] = in[i];
|
||||
|
||||
multicore_fifo_push_blocking(CORE0_READY);
|
||||
multicore_fifo_push_blocking(samples);
|
||||
|
||||
for (int j = 0; j < filter_stages; j++) {
|
||||
// Left channel filter
|
||||
for (int i = 0; i < samples; i += 2) {
|
||||
fix16_t x_f16 = fix16_from_int((int16_t) out[i]);
|
||||
|
||||
x_f16 = bqf_transform(x_f16, &bqf_filters_left[j],
|
||||
&bqf_filters_mem_left[j]);
|
||||
|
||||
out[i] = (int32_t) fix16_to_int(x_f16);
|
||||
}
|
||||
}
|
||||
|
||||
// Block until core 1 has finished transforming the data
|
||||
uint32_t ready = multicore_fifo_pop_blocking();
|
||||
|
||||
i2s_stream_write(&i2s_write_obj, userbuf, samples * 4);
|
||||
|
||||
// keep on truckin'
|
||||
usb_grow_transfer(ep->current_transfer, 1);
|
||||
usb_packet_done(ep);
|
||||
}
|
||||
|
||||
static void update_volume()
|
||||
{
|
||||
if (audio_state._volume != audio_state._target_volume) {
|
||||
|
@ -143,47 +103,123 @@ static void update_volume()
|
|||
|
||||
audio_state._volume = audio_state._target_volume;
|
||||
}
|
||||
|
||||
if (audio_state.pcm3060_registers != audio_state._target_pcm3060_registers) {
|
||||
uint8_t buf[3];
|
||||
buf[0] = 68; // register addr
|
||||
buf[1] = audio_state.target_pcm3060_registers[0]; // Reg 68
|
||||
buf[2] = audio_state.target_pcm3060_registers[1]; // Reg 69
|
||||
i2c_write_blocking(i2c0, PCM_I2C_ADDR, buf, 3, false);
|
||||
audio_state.pcm3060_registers = audio_state._target_pcm3060_registers;
|
||||
}
|
||||
}
|
||||
|
||||
// Here's the meat. It's where the data buffer from USB gets transformed from
|
||||
// PCM data into I2S data that gets shipped out to the PCM3060. It really
|
||||
// belongs with the other USB-related code due to its utter indecipherability,
|
||||
// but it's placed here to emphasize its importance.
|
||||
static void _as_audio_packet(struct usb_endpoint *ep) {
|
||||
struct usb_buffer *usb_buffer = usb_current_out_packet_buffer(ep);
|
||||
int16_t *in = (int16_t *) usb_buffer->data;
|
||||
int32_t *out = (int32_t *) userbuf;
|
||||
int samples = usb_buffer->data_len / 2;
|
||||
|
||||
if (preprocessing.reverse_stereo) {
|
||||
for (int i = 0; i < samples; i+=2) {
|
||||
out[i] = in[i+1];
|
||||
out[i+1] = in[i];
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (int i = 0; i < samples; i++)
|
||||
out[i] = in[i];
|
||||
}
|
||||
|
||||
// Make sure core 1 is ready for us.
|
||||
multicore_fifo_pop_blocking();
|
||||
multicore_fifo_push_blocking(CORE0_READY);
|
||||
multicore_fifo_push_blocking(samples);
|
||||
|
||||
for (int j = 0; j < filter_stages; j++) {
|
||||
// Left channel filter
|
||||
for (int i = 0; i < samples; i += 2) {
|
||||
fix16_t x_f16 = fix16_mul(fix16_from_s16sample((int16_t) out[i]), preprocessing.preamp);
|
||||
|
||||
x_f16 = bqf_transform(x_f16, &bqf_filters_left[j],
|
||||
&bqf_filters_mem_left[j]);
|
||||
|
||||
out[i] = (int32_t) fix16_to_s16sample(x_f16);
|
||||
}
|
||||
}
|
||||
|
||||
// Block until core 1 has finished transforming the data
|
||||
uint32_t ready = multicore_fifo_pop_blocking();
|
||||
multicore_fifo_push_blocking(CORE0_READY);
|
||||
|
||||
// Update the volume if required. We do this from core1 as
|
||||
// core0 is more heavily loaded, doing this from core0 can
|
||||
// lead to audio crackling.
|
||||
update_volume();
|
||||
|
||||
// Update filters if required
|
||||
apply_config_changes();
|
||||
|
||||
// keep on truckin'
|
||||
usb_grow_transfer(ep->current_transfer, 1);
|
||||
usb_packet_done(ep);
|
||||
}
|
||||
|
||||
void core1_entry() {
|
||||
uint8_t *userbuf = (uint8_t *) multicore_fifo_pop_blocking();
|
||||
int32_t *out = (int32_t *) userbuf;
|
||||
|
||||
// Signal that the thread has started
|
||||
multicore_fifo_push_blocking(CORE1_READY);
|
||||
|
||||
while (true) {
|
||||
// Signal to core 0 that we are ready to accept new data
|
||||
multicore_fifo_push_blocking(CORE1_READY);
|
||||
|
||||
// Block until the userbuf is filled with data
|
||||
uint32_t ready = multicore_fifo_pop_blocking();
|
||||
while (ready != CORE0_READY)
|
||||
ready = multicore_fifo_pop_blocking();
|
||||
|
||||
uint32_t limit = multicore_fifo_pop_blocking();
|
||||
const uint32_t samples = multicore_fifo_pop_blocking();
|
||||
|
||||
for (int j = 0; j < filter_stages; j++) {
|
||||
for (int i = 1; i < limit; i += 2) {
|
||||
fix16_t x_f16 = fix16_from_int((int16_t) out[i]);
|
||||
for (int i = 1; i < samples; i += 2) {
|
||||
fix16_t x_f16 = fix16_mul(fix16_from_s16sample((int16_t) out[i]), preprocessing.preamp);
|
||||
|
||||
x_f16 = bqf_transform(x_f16, &bqf_filters_right[j],
|
||||
&bqf_filters_mem_right[j]);
|
||||
|
||||
out[i] = (int16_t) fix16_to_int(x_f16);
|
||||
out[i] = (int16_t) fix16_to_s16sample(x_f16);
|
||||
}
|
||||
}
|
||||
|
||||
// Signal to core 0 that the data has all been transformed
|
||||
multicore_fifo_push_blocking(CORE1_READY);
|
||||
|
||||
// Update the volume if required. We do this from core1 as
|
||||
// core0 is more heavily loaded, doing this from core0 can
|
||||
// lead to audio crackling.
|
||||
update_volume();
|
||||
// Wait for Core 0 to finish running its filtering before we apply config updates
|
||||
multicore_fifo_pop_blocking();
|
||||
|
||||
i2s_stream_write(&i2s_write_obj, userbuf, samples * 4);
|
||||
}
|
||||
}
|
||||
|
||||
void setup() {
|
||||
set_sys_clock_khz(SYSTEM_FREQ / 1000, true);
|
||||
sleep_ms(100);
|
||||
stdio_init_all();
|
||||
|
||||
for (int i=0; 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);
|
||||
|
||||
// Configure DAC PWM
|
||||
|
@ -211,7 +247,7 @@ void setup() {
|
|||
// 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
|
||||
// updating the volume.
|
||||
i2c_init(i2c0, 400000);
|
||||
i2c_init(i2c0, 100000);
|
||||
gpio_set_function(PCM3060_SDA_PIN, GPIO_FUNC_I2C);
|
||||
gpio_set_function(PCM3060_SCL_PIN, GPIO_FUNC_I2C);
|
||||
gpio_pull_up(PCM3060_SDA_PIN);
|
||||
|
@ -229,11 +265,6 @@ void setup() {
|
|||
// Don't remove this. Don't do it.
|
||||
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
|
||||
buf[0] = 64; // register addr
|
||||
buf[1] = 0xE0; // data
|
||||
|
@ -242,6 +273,11 @@ void setup() {
|
|||
// Same here, pal. Hands off.
|
||||
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.ws_pin = PCM3060_DAC_WS_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),
|
||||
.bDescriptorType = DTYPE_Configuration,
|
||||
.wTotalLength = sizeof(ad_conf),
|
||||
.bNumInterfaces = 2,
|
||||
.bNumInterfaces = 3,
|
||||
.bConfigurationValue = 0x01,
|
||||
.iConfiguration = 0x00,
|
||||
.bmAttributes = 0x80,
|
||||
|
@ -439,16 +475,45 @@ static const audio_device_config ad_conf = {
|
|||
.bRefresh = 2,
|
||||
.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 as_op_interface;
|
||||
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 = {
|
||||
.bLength = 18,
|
||||
.bDescriptorType = 0x01,
|
||||
.bcdUSB = 0x0110,
|
||||
.bcdUSB = 0x0210,
|
||||
.bDeviceClass = 0x00,
|
||||
.bDeviceSubClass = 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_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) {
|
||||
if ((setup->bmRequestType & USB_REQ_TYPE_RECIPIENT_MASK) == USB_REQ_TYPE_RECIPIENT_INTERFACE) {
|
||||
switch (setup->wValue >> 8u) {
|
||||
case 1: { // mute
|
||||
usb_start_tiny_control_in_transfer(audio_state.mute, 1);
|
||||
usb_start_tiny_control_in_transfer((audio_state.mute != 0), 1);
|
||||
return true;
|
||||
}
|
||||
case 2: { // volume
|
||||
|
@ -640,11 +719,7 @@ static void audio_cmd_packet(struct usb_endpoint *ep) {
|
|||
if (audio_control_cmd_t.type == USB_REQ_TYPE_RECIPIENT_INTERFACE) {
|
||||
switch (audio_control_cmd_t.cs) {
|
||||
case 1: { // mute
|
||||
audio_state.mute = buffer->data[0];
|
||||
uint8_t buf[2];
|
||||
buf[0] = 68; // register addr
|
||||
buf[1] = buffer->data[0] ? 0x3 : 0x0; // data
|
||||
i2c_write_blocking(i2c0, PCM_I2C_ADDR, buf, 2, false);
|
||||
audio_state.mute = buffer->data[0] ? 0x3 : 0x0;
|
||||
break;
|
||||
}
|
||||
case 2: { // volume
|
||||
|
@ -676,7 +751,11 @@ static const struct usb_transfer_type _audio_cmd_transfer_type = {
|
|||
|
||||
static bool as_set_alternate(struct usb_interface *interface, uint alt) {
|
||||
assert(interface == &as_op_interface);
|
||||
return alt < 2;
|
||||
switch (alt) {
|
||||
case 0: power_down_dac(); return true;
|
||||
case 1: power_up_dac(); return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool do_set_current(struct usb_setup_packet *setup) {
|
||||
|
@ -693,22 +772,83 @@ static bool do_set_current(struct usb_setup_packet *setup) {
|
|||
return false;
|
||||
}
|
||||
|
||||
static void _tf_send_control_in_ack(__unused struct usb_endpoint *endpoint, __unused struct usb_transfer *transfer) {
|
||||
assert(endpoint == &usb_control_in);
|
||||
assert(transfer == &_control_in_transfer);
|
||||
usb_debug("_tf_setup_control_ack\n");
|
||||
static struct usb_transfer _control_out_transfer;
|
||||
usb_start_empty_transfer(usb_get_control_out_endpoint(), &_control_out_transfer, 0);
|
||||
}
|
||||
|
||||
static struct usb_stream_transfer _control_in_stream_transfer;
|
||||
#define _control_in_transfer _control_in_stream_transfer.core
|
||||
static struct usb_stream_transfer_funcs control_stream_funcs = {
|
||||
.on_chunk = usb_stream_noop_on_chunk,
|
||||
.on_packet_complete = usb_stream_noop_on_packet_complete
|
||||
};
|
||||
|
||||
static bool ad_setup_request_handler(__unused struct usb_device *device, struct usb_setup_packet *setup) {
|
||||
setup = __builtin_assume_aligned(setup, 4);
|
||||
//("ad_setup_request_handler: Type %u, Request %u, Value %u, Index %u, Length %u\n", setup->bmRequestType, setup->bRequest, setup->wValue, setup->wIndex, setup->wLength);
|
||||
|
||||
if (setup->bmRequestType & USB_DIR_IN) {
|
||||
if (USB_REQ_TYPE_RECIPIENT_DEVICE == (setup->bmRequestType & USB_REQ_TYPE_TYPE_MASK)) {
|
||||
if ((setup->bRequest == USB_REQUEST_GET_DESCRIPTOR) && ((setup->wValue >> 8) == 0xF /* BOS */)) {
|
||||
|
||||
struct usb_endpoint *usb_control_in = usb_get_control_in_endpoint();
|
||||
static struct usb_stream_transfer_funcs control_stream_funcs = {
|
||||
.on_chunk = usb_stream_noop_on_chunk,
|
||||
.on_packet_complete = usb_stream_noop_on_packet_complete
|
||||
};
|
||||
int len = 0x21;
|
||||
|
||||
len = MIN(len, setup->wLength);
|
||||
usb_stream_setup_transfer(&_control_in_stream_transfer, &control_stream_funcs, ms_platform_capability_bos_descriptor,
|
||||
sizeof(ms_platform_capability_bos_descriptor), len, _tf_send_control_in_ack);
|
||||
|
||||
_control_in_stream_transfer.ep = usb_control_in;
|
||||
usb_start_transfer(usb_control_in, &_control_in_stream_transfer.core);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (USB_REQ_TYPE_TYPE_VENDOR == (setup->bmRequestType & USB_REQ_TYPE_TYPE_MASK)) {
|
||||
// To prevent badly behaving software from accidentally triggering a reboot, e expect
|
||||
// the wValue to be equal to the Ploopy vendor id.
|
||||
if (setup->bRequest == REBOOT_BOOTLOADER && setup->wValue == 0x2E8A) {
|
||||
power_down_dac();
|
||||
reset_usb_boot(0, 0);
|
||||
// reset_usb_boot does not return, so we will not respond to this command.
|
||||
return true;
|
||||
}
|
||||
else if (USB_REQ_TYPE_RECIPIENT_DEVICE == (setup->bmRequestType & USB_REQ_TYPE_RECIPIENT_MASK) && setup->bRequest == MICROSOFT_COMPATIBLE_ID_FEATURE_DESRIPTOR && setup->wIndex == 0x7)
|
||||
{
|
||||
const int length = MIN(MS_OS_20_DESC_LEN, setup->wLength);
|
||||
//printf("Sending %u bytes (%u %u)\n", length, MS_OS_20_DESC_LEN, sizeof(desc_ms_os_20));
|
||||
struct usb_endpoint *usb_control_in = usb_get_control_in_endpoint();
|
||||
usb_stream_setup_transfer(&_control_in_stream_transfer, &control_stream_funcs, desc_ms_os_20,
|
||||
sizeof(desc_ms_os_20), length, _tf_send_control_in_ack);
|
||||
|
||||
_control_in_stream_transfer.ep = usb_control_in;
|
||||
usb_start_transfer(usb_control_in, &_control_in_stream_transfer.core);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static struct usb_stream_transfer _config_in_stream_transfer;
|
||||
static bool configuration_interface_setup_request_handler(__unused struct usb_interface *interface, struct usb_setup_packet *setup) {
|
||||
setup = __builtin_assume_aligned(setup, 4);
|
||||
//printf("configuration_interface_setup_request_handler: Type %u, Request %u, Value %u, Index %u, Length %u\n", setup->bmRequestType, setup->bRequest, setup->wValue, setup->wIndex, setup->wLength);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool ac_setup_request_handler(__unused struct usb_interface *interface, struct usb_setup_packet *setup) {
|
||||
setup = __builtin_assume_aligned(setup, 4);
|
||||
//printf("ac_setup_request_handler: Type %u, Request %u, Value %u, Index %u, Length %u\n", setup->bmRequestType, setup->bRequest, setup->wValue, setup->wIndex, setup->wLength);
|
||||
|
||||
if (USB_REQ_TYPE_TYPE_CLASS == (setup->bmRequestType & USB_REQ_TYPE_TYPE_MASK)) {
|
||||
switch (setup->bRequest) {
|
||||
case AUDIO_REQ_SetCurrent:
|
||||
|
@ -735,6 +875,8 @@ static bool ac_setup_request_handler(__unused struct usb_interface *interface, s
|
|||
|
||||
bool _as_setup_request_handler(__unused struct usb_endpoint *ep, struct usb_setup_packet *setup) {
|
||||
setup = __builtin_assume_aligned(setup, 4);
|
||||
//printf("as_setup_request_handler: Type %u, Request %u, Value %u, Index %u, Length %u\n", setup->bmRequestType, setup->bRequest, setup->wValue, setup->wIndex, setup->wLength);
|
||||
|
||||
if (USB_REQ_TYPE_TYPE_CLASS == (setup->bmRequestType & USB_REQ_TYPE_TYPE_MASK)) {
|
||||
switch (setup->bRequest) {
|
||||
case AUDIO_REQ_SetCurrent:
|
||||
|
@ -777,9 +919,22 @@ void usb_sound_card_init() {
|
|||
as_sync_transfer.type = &as_sync_transfer_type;
|
||||
usb_set_default_transfer(&ep_op_sync, &as_sync_transfer);
|
||||
|
||||
|
||||
static struct usb_endpoint *const configuration_endpoints[] = {
|
||||
&ep_configuration_out, &ep_configuration_in
|
||||
};
|
||||
usb_interface_init(&configuration_interface, &ad_conf.configuration_interface, configuration_endpoints, 2, true);
|
||||
configuration_interface.setup_request_handler = configuration_interface_setup_request_handler;
|
||||
|
||||
config_in_transfer.type = &config_in_transfer_type;
|
||||
usb_set_default_transfer(&ep_configuration_in, &config_in_transfer);
|
||||
config_out_transfer.type = &config_out_transfer_type;
|
||||
usb_set_default_transfer(&ep_configuration_out, &config_out_transfer);
|
||||
|
||||
static struct usb_interface *const boot_device_interfaces[] = {
|
||||
&ac_interface,
|
||||
&as_op_interface,
|
||||
&configuration_interface
|
||||
};
|
||||
|
||||
__unused struct usb_device *device = usb_device_init(&boot_device_descriptor,
|
||||
|
@ -795,6 +950,22 @@ void usb_sound_card_init() {
|
|||
usb_device_start();
|
||||
}
|
||||
|
||||
// Some operations will cause popping on the audio output, temporarily
|
||||
// disabling the DAC sounds much better.
|
||||
void power_down_dac() {
|
||||
uint8_t buf[2];
|
||||
buf[0] = 64; // register addr
|
||||
buf[1] = 0xF0; // DAC low power mode
|
||||
i2c_write_blocking(i2c0, PCM_I2C_ADDR, buf, 2, false);
|
||||
}
|
||||
|
||||
void power_up_dac() {
|
||||
uint8_t buf[2];
|
||||
buf[0] = 64; // register addr
|
||||
buf[1] = 0xE0; // DAC normal mode
|
||||
i2c_write_blocking(i2c0, PCM_I2C_ADDR, buf, 2, false);
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* USB-related code ends here.
|
||||
****************************************************************************/
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
|
||||
#include "ringbuf.h"
|
||||
#include "i2s.h"
|
||||
#include "fix16.h"
|
||||
|
||||
/*****************************************************************************
|
||||
* USB-related definitions begin here.
|
||||
|
@ -45,6 +46,39 @@
|
|||
#define MAX_VOLUME ENCODE_DB(0)
|
||||
#define VOLUME_RESOLUTION ENCODE_DB(0.5f)
|
||||
|
||||
typedef struct _audio_state_config {
|
||||
uint32_t freq;
|
||||
union {
|
||||
int16_t volume[2];
|
||||
int32_t _volume;
|
||||
};
|
||||
union {
|
||||
int16_t target_volume[2];
|
||||
int32_t _target_volume;
|
||||
};
|
||||
union {
|
||||
struct {
|
||||
// Register 68
|
||||
uint8_t mute: 2;
|
||||
uint8_t phase: 1;
|
||||
uint8_t reserved1: 3;
|
||||
uint8_t oversampling: 1;
|
||||
uint8_t reserved2: 1;
|
||||
// Register 69
|
||||
uint8_t zero_fn: 1;
|
||||
uint8_t zero_polarity: 1;
|
||||
uint8_t reserved3: 2;
|
||||
uint8_t de_emphasis: 1;
|
||||
uint8_t de_emphasis_frequency: 2;
|
||||
uint8_t rolloff: 1;
|
||||
};
|
||||
int8_t target_pcm3060_registers[2];
|
||||
int16_t _target_pcm3060_registers;
|
||||
};
|
||||
int16_t pcm3060_registers;
|
||||
} audio_state_config;
|
||||
extern audio_state_config audio_state;
|
||||
|
||||
typedef struct _audio_device_config {
|
||||
struct usb_configuration_descriptor descriptor;
|
||||
struct usb_interface_descriptor ac_interface;
|
||||
|
@ -68,12 +102,23 @@ typedef struct _audio_device_config {
|
|||
USB_Audio_StdDescriptor_StreamEndpoint_Spc_t audio;
|
||||
} ep1;
|
||||
struct usb_endpoint_descriptor_long ep2;
|
||||
|
||||
struct usb_interface_descriptor configuration_interface;
|
||||
struct usb_endpoint_descriptor ep3;
|
||||
struct usb_endpoint_descriptor ep4;
|
||||
} audio_device_config;
|
||||
|
||||
typedef struct _preprocessing_config {
|
||||
fix16_t preamp;
|
||||
int reverse_stereo;
|
||||
} preprocessing_config;
|
||||
|
||||
extern preprocessing_config preprocessing;
|
||||
|
||||
static char *descriptor_strings[] = {
|
||||
"Ploopy Corporation",
|
||||
"Ploopy Headphones",
|
||||
"000000000001"
|
||||
"0000000000000001" // Dummy serial number, will be overwritten with the value read from the SPI flash chip. Must be 17bytes.
|
||||
};
|
||||
|
||||
/*****************************************************************************
|
||||
|
@ -132,5 +177,6 @@ static bool do_set_current(struct usb_setup_packet *);
|
|||
static bool ac_setup_request_handler(__unused struct usb_interface *, struct usb_setup_packet *);
|
||||
bool _as_setup_request_handler(__unused struct usb_endpoint *, struct usb_setup_packet *);
|
||||
void usb_sound_card_init(void);
|
||||
|
||||
extern void power_down_dac();
|
||||
extern void power_up_dac();
|
||||
#endif
|
|
@ -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
|
||||
../code/fix16.c
|
||||
../code/bqf.c
|
||||
../code/user.c
|
||||
../code/configuration_manager.c
|
||||
)
|
||||
|
||||
target_compile_definitions(filter_test PRIVATE TEST_TARGET)
|
||||
target_include_directories(filter_test PRIVATE ${CMAKE_SOURCE_DIR}/../code)
|
||||
|
||||
# TODO: user.c includes run.h to get the definition for SAMPLING_FREQ, but this
|
||||
|
|
|
@ -2,12 +2,7 @@
|
|||
#include <stdlib.h>
|
||||
#include "bqf.h"
|
||||
#include "fix16.h"
|
||||
#include "user.h"
|
||||
|
||||
bqf_coeff_t bqf_filters_left[MAX_FILTER_STAGES];
|
||||
bqf_coeff_t bqf_filters_right[MAX_FILTER_STAGES];
|
||||
bqf_mem_t bqf_filters_mem_left[MAX_FILTER_STAGES];
|
||||
bqf_mem_t bqf_filters_mem_right[MAX_FILTER_STAGES];
|
||||
#include "configuration_manager.h"
|
||||
|
||||
const char* usage = "Usage: %s INFILE OUTFILE\n\n"
|
||||
"Reads 16bit stereo PCM data from INFILE, runs it through the Ploopy headphones\n"
|
||||
|
@ -52,7 +47,7 @@ int main(int argc, char* argv[])
|
|||
|
||||
// The smaple proccesing code, essentially the same as the
|
||||
// code in the firmware's run.c file.
|
||||
define_filters();
|
||||
load_config();
|
||||
|
||||
for (int i = 0; i < samples; i++)
|
||||
{
|
||||
|
@ -64,21 +59,21 @@ int main(int argc, char* argv[])
|
|||
for (int i = 0; i < samples; i ++)
|
||||
{
|
||||
// Left channel filter
|
||||
fix16_t x_f16 = fix16_from_int((int16_t) out[i]);
|
||||
fix16_t x_f16 = fix16_from_s16sample((int16_t) out[i]);
|
||||
|
||||
x_f16 = bqf_transform(x_f16, &bqf_filters_left[j],
|
||||
&bqf_filters_mem_left[j]);
|
||||
|
||||
out[i] = (int32_t) fix16_to_int(x_f16);
|
||||
out[i] = (int32_t) fix16_to_s16sample(x_f16);
|
||||
|
||||
// Right channel filter
|
||||
i++;
|
||||
x_f16 = fix16_from_int((int16_t) out[i]);
|
||||
x_f16 = fix16_from_s16sample((int16_t) out[i]);
|
||||
|
||||
x_f16 = bqf_transform(x_f16, &bqf_filters_right[j],
|
||||
&bqf_filters_mem_right[j]);
|
||||
|
||||
out[i] = (int16_t) fix16_to_int(x_f16);
|
||||
out[i] = (int16_t) fix16_to_s16sample(x_f16);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue