Compare commits

...

6 Commits
PM11 ... master

Author SHA1 Message Date
George Norton 9c036f07f8
Fix save config (#27)
* Fix save config

* Fix save config when no audio is playing

* Fix build issue with the github workflow compiler
2023-10-16 18:35:38 -04:00
PloopyCo 84306c0922 Adjusted Oratory's default EQ:
- Less bass. Excursion limits happen at 30-50Hz first, so reducing response in this range makes it a little less likely this will be a problem. Plus, I like it.
- A little more response above 12kHz.

Updated config version to 4.

Added a parameter for post EQ gain. Applied once after EQ is calculated.
2023-09-14 05:06:57 -04:00
George Norton 41d4023961
Run the DAC in 24bit mode. (#25)
* Run the DAC in 24bit mode.

* Update comment.

* Remove accidental paste

* Fix distortion.

* Shift up the samples into -1..1, not much different, but we get an extra bit of resolution at the low end.
2023-09-14 04:49:56 -04:00
George Norton 1e6896f918
Enable oratory's 15 band EQ (#23)
* Attempt at optimizing so the new filtering runs better.

* Additional improvements.

* Further optimisations.

* Seems to work OK with 10 filters. Just noise with 11.

* Increase config buffer size, and make the bqf_transform function inline

* Remove extra loop and process input evently across both cores.

* Enable 15 band EQ.

* Shift some load of core1

* Revert buffer size change

* Mark USB transfer as done sooner.

* Fast multiply.

* Fix build failure.

* Rollback changes we dont need.

* Fix save config to flash

* Increase filter stages to 20. We cant quite run that many though.

* Replace a few doubles with floats. According to the raspberry-pi-pico-c-sdk manual, doubles are around 3 times slower than floats.
2023-08-23 15:47:01 -04:00
ploopyco fd6cbf54d5 refactored volume updates and filter configs to run on core 1 2023-08-16 13:31:56 -04:00
ploopyco c03b463390
- Replaced default EQ filter set with an 8-band EQ based on measurements from Oratory1990 and VSG (#21)
- Default configuration reverses the left/right channels in the DAC, because the headphones are wired backwards (oopie!)

Co-authored-by: PloopyCo <contact@ploopy.co>
2023-08-15 03:15:21 -04:00
17 changed files with 261 additions and 186 deletions

View File

@ -14,7 +14,6 @@ add_executable(ploopy_headphones
run.c run.c
ringbuf.c ringbuf.c
i2s.c i2s.c
fix16.c
bqf.c bqf.c
configuration_manager.c configuration_manager.c
) )
@ -63,6 +62,9 @@ target_compile_definitions(ploopy_headphones PRIVATE
GIT_HASH="${GIT_HASH}" GIT_HASH="${GIT_HASH}"
PICO_XOSC_STARTUP_DELAY_MULTIPLIER=64 PICO_XOSC_STARTUP_DELAY_MULTIPLIER=64
# Performance, avoid calls to ____wrap___aeabi_lmul_veneer when doing 64bit multiplies
PICO_INT64_OPS_IN_RAM=1
) )
pico_enable_stdio_usb(ploopy_headphones 0) pico_enable_stdio_usb(ploopy_headphones 0)

View File

@ -467,21 +467,6 @@ void bqf_highshelf_config(double fs, double f0, double dBgain, double Q,
coefficients->a2 = fix3_28_from_dbl(a2); coefficients->a2 = fix3_28_from_dbl(a2);
} }
fix3_28_t bqf_transform(fix3_28_t x, bqf_coeff_t *coefficients, bqf_mem_t *memory) {
fix3_28_t y = fix16_mul(coefficients->b0, x) -
fix16_mul(coefficients->a1, memory->y_1) +
fix16_mul(coefficients->b1, memory->x_1) -
fix16_mul(coefficients->a2, memory->y_2) +
fix16_mul(coefficients->b2, memory->x_2);
memory->x_2 = memory->x_1;
memory->x_1 = x;
memory->y_2 = memory->y_1;
memory->y_1 = y;
return y;
}
void bqf_memreset(bqf_mem_t *memory) { void bqf_memreset(bqf_mem_t *memory) {
memory->x_1 = fix16_zero; memory->x_1 = fix16_zero;
memory->x_2 = fix16_zero; memory->x_2 = fix16_zero;

View File

@ -41,9 +41,9 @@ typedef struct _bqf_mem_t {
fix3_28_t y_2; fix3_28_t y_2;
} bqf_mem_t; } bqf_mem_t;
// In reality we do not have enough CPU resource to run 8 filtering // More filters should be possible, but the config structure
// stages without some optimisation. // might grow beyond the current 512 byte size.
#define MAX_FILTER_STAGES 8 #define MAX_FILTER_STAGES 20
extern int filter_stages; extern int filter_stages;
extern bqf_coeff_t bqf_filters_left[MAX_FILTER_STAGES]; extern bqf_coeff_t bqf_filters_left[MAX_FILTER_STAGES];
@ -65,7 +65,8 @@ void bqf_peaking_config(double, double, double, double, bqf_coeff_t *);
void bqf_lowshelf_config(double, double, double, double, bqf_coeff_t *); void bqf_lowshelf_config(double, double, double, double, bqf_coeff_t *);
void bqf_highshelf_config(double, double, double, double, bqf_coeff_t *); void bqf_highshelf_config(double, double, double, double, bqf_coeff_t *);
fix3_28_t bqf_transform(fix3_28_t, bqf_coeff_t *, bqf_mem_t *); static inline fix3_28_t bqf_transform(fix3_28_t, bqf_coeff_t *, bqf_mem_t *);
void bqf_memreset(bqf_mem_t *); void bqf_memreset(bqf_mem_t *);
#include "bqf.inl"
#endif #endif

36
firmware/code/bqf.inl Normal file
View File

@ -0,0 +1,36 @@
/**
* 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/>.
*
* SPECIAL THANKS TO:
* Robert Bristow-Johnson, a.k.a. RBJ
* for his exceptional work on biquad formulae as applied to digital
* audio filtering, summarised in his pamphlet, "Audio EQ Cookbook".
*/
static inline fix3_28_t bqf_transform(fix3_28_t x, bqf_coeff_t *coefficients, bqf_mem_t *memory) {
fix3_28_t y = fix16_mul(coefficients->b0, x) -
fix16_mul(coefficients->a1, memory->y_1) +
fix16_mul(coefficients->b1, memory->x_1) -
fix16_mul(coefficients->a2, memory->y_2) +
fix16_mul(coefficients->b2, memory->x_2);
memory->x_2 = memory->x_1;
memory->x_1 = x;
memory->y_2 = memory->y_1;
memory->y_1 = y;
return y;
}

View File

@ -52,13 +52,29 @@ static const default_configuration default_config = {
.set_configuration = { SET_CONFIGURATION, sizeof(default_config) }, .set_configuration = { SET_CONFIGURATION, sizeof(default_config) },
.filters = { .filters = {
.filter = { FILTER_CONFIGURATION, sizeof(default_config.filters) }, .filter = { FILTER_CONFIGURATION, sizeof(default_config.filters) },
.f1 = { PEAKING, {0}, 38, -19, 0.9 }, .f1 = { PEAKING, {0}, 38.5, -21.0, 1.4 },
.f2 = { LOWSHELF, {0}, 2900, 2, 0.7 }, .f2 = { PEAKING, {0}, 60, -6.7, 0.5 },
.f3 = { PEAKING, {0}, 430, 3, 3.5 }, .f3 = { LOWSHELF, {0}, 105, 2.0, 0.71 },
.f4 = { HIGHSHELF, {0}, 8400, 2, 0.7 }, .f4 = { PEAKING, {0}, 280, -3.5, 1.1 },
.f5 = { PEAKING, {0}, 4800, 3, 5 } .f5 = { PEAKING, {0}, 350, -1.6, 6.0 },
.f6 = { PEAKING, {0}, 425, 7.8, 1.3 },
.f7 = { PEAKING, {0}, 500, -2.0, 7.0 },
.f8 = { PEAKING, {0}, 690, -5.5, 3.0 },
.f9 = { PEAKING, {0}, 1000, -2.2, 5.0 },
.f10 = { PEAKING, {0}, 1530, -4.0, 2.5 },
.f11 = { PEAKING, {0}, 2250, 6.0, 2.0 },
.f12 = { PEAKING, {0}, 3430, -12.2, 2.0 },
.f13 = { PEAKING, {0}, 4800, 4.0, 2.0 },
.f14 = { PEAKING, {0}, 6200, -15.0, 3.0 },
.f15 = { HIGHSHELF, {0}, 12000, -3.0, 0.71 }
}, },
.preprocessing = { .header = { PREPROCESSING_CONFIGURATION, sizeof(default_config.preprocessing) }, -0.2f, false, {0} } .preprocessing = {
.header = { PREPROCESSING_CONFIGURATION, sizeof(default_config.preprocessing) },
-0.376265f, // pre-EQ gain of -4.1dB
0.4125f, // post-EQ gain, set to ~3dB (1.4x, less the 1 that is added when config is applied)
true,
{0}
}
}; };
// Grab the last 4k page of flash for our configuration strutures. // Grab the last 4k page of flash for our configuration strutures.
@ -71,7 +87,7 @@ const uint8_t *user_configuration = (const uint8_t *) (XIP_BASE + USER_CONFIGURA
* should handle merging configurations where, for example, only a new * should handle merging configurations where, for example, only a new
* filter_configuration_tlv was received. * filter_configuration_tlv was received.
*/ */
#define CFG_BUFFER_SIZE 256 #define CFG_BUFFER_SIZE 512
static uint8_t working_configuration[2][CFG_BUFFER_SIZE]; static uint8_t working_configuration[2][CFG_BUFFER_SIZE];
static uint8_t inactive_working_configuration = 0; 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 uint8_t result_buffer[CFG_BUFFER_SIZE] = { U16_TO_U8S_LE(NOK), U16_TO_U8S_LE(0) };
@ -80,6 +96,13 @@ static bool reload_config = false;
static uint16_t write_offset = 0; static uint16_t write_offset = 0;
static uint16_t read_offset = 0; static uint16_t read_offset = 0;
typedef enum {
NormalOperation,
SaveRequested,
Saving
} State;
static State saveState = NormalOperation;
bool validate_filter_configuration(filter_configuration_tlv *filters) bool validate_filter_configuration(filter_configuration_tlv *filters)
{ {
if (filters->header.type != FILTER_CONFIGURATION) { if (filters->header.type != FILTER_CONFIGURATION) {
@ -126,7 +149,7 @@ bool validate_filter_configuration(filter_configuration_tlv *filters)
printf("Error! Not enough data left for filter6 (%d)\n", remaining); printf("Error! Not enough data left for filter6 (%d)\n", remaining);
return false; return false;
} }
if (args->a0 == 0.0) { if (args->a0 == 0.0f) {
printf("Error! The a0 co-efficient of an IIR filter must not be 0.\n"); printf("Error! The a0 co-efficient of an IIR filter must not be 0.\n");
return false; return false;
} }
@ -179,7 +202,7 @@ void apply_filter_configuration(filter_configuration_tlv *filters) {
uint32_t checksum = 0; uint32_t checksum = 0;
for (int i = 0; i < sizeof(filter6) / 4; i++) checksum ^= ((uint32_t*) args)[i]; for (int i = 0; i < sizeof(filter6) / 4; i++) checksum ^= ((uint32_t*) args)[i];
if (checksum != bqf_filter_checksum[filter_stages]) { if (checksum != bqf_filter_checksum[filter_stages]) {
bqf_filters_left[filter_stages].a0 = fix3_28_from_dbl(1.0); bqf_filters_left[filter_stages].a0 = fix16_one;
bqf_filters_left[filter_stages].a1 = fix3_28_from_dbl(args->a1/args->a0); bqf_filters_left[filter_stages].a1 = fix3_28_from_dbl(args->a1/args->a0);
bqf_filters_left[filter_stages].a2 = fix3_28_from_dbl(args->a2/args->a0); bqf_filters_left[filter_stages].a2 = fix3_28_from_dbl(args->a2/args->a0);
bqf_filters_left[filter_stages].b0 = fix3_28_from_dbl(args->b0/args->a0); bqf_filters_left[filter_stages].b0 = fix3_28_from_dbl(args->b0/args->a0);
@ -305,7 +328,8 @@ bool apply_configuration(tlv_header *config) {
#ifndef TEST_TARGET #ifndef TEST_TARGET
case PREPROCESSING_CONFIGURATION: { case PREPROCESSING_CONFIGURATION: {
preprocessing_configuration_tlv* preprocessing_config = (preprocessing_configuration_tlv*) tlv; preprocessing_configuration_tlv* preprocessing_config = (preprocessing_configuration_tlv*) tlv;
preprocessing.preamp = fix3_28_from_dbl(1.0 + preprocessing_config->preamp); preprocessing.preamp = fix3_28_from_flt(1.0f + preprocessing_config->preamp);
preprocessing.postEQGain = fix3_28_from_flt(1.0f + preprocessing_config->postEQGain);
preprocessing.reverse_stereo = preprocessing_config->reverse_stereo; preprocessing.reverse_stereo = preprocessing_config->reverse_stereo;
break; break;
} }
@ -340,16 +364,20 @@ void load_config() {
} }
#ifndef TEST_TARGET #ifndef TEST_TARGET
bool __no_inline_not_in_flash_func(save_configuration)() { bool __no_inline_not_in_flash_func(save_config)() {
const uint8_t active_configuration = inactive_working_configuration ? 0 : 1; const uint8_t active_configuration = inactive_working_configuration ? 0 : 1;
tlv_header* config = (tlv_header*) working_configuration[active_configuration]; tlv_header* config = (tlv_header*) working_configuration[active_configuration];
switch (saveState) {
case SaveRequested:
if (validate_configuration(config)) { if (validate_configuration(config)) {
/* Turn the DAC off so we don't make a huge noise when disrupting
real time audio operation. */
power_down_dac(); power_down_dac();
const size_t config_length = config->length - ((size_t)config->value - (size_t)config); const size_t config_length = config->length - ((size_t)config->value - (size_t)config);
// Write data to flash // Write data to flash
uint8_t flash_buffer[FLASH_PAGE_SIZE]; uint8_t flash_buffer[CFG_BUFFER_SIZE];
flash_header_tlv* flash_header = (flash_header_tlv*) flash_buffer; flash_header_tlv* flash_header = (flash_header_tlv*) flash_buffer;
flash_header->header.type = FLASH_HEADER; flash_header->header.type = FLASH_HEADER;
flash_header->header.length = sizeof(flash_header_tlv) + config_length; flash_header->header.length = sizeof(flash_header_tlv) + config_length;
@ -359,13 +387,26 @@ bool __no_inline_not_in_flash_func(save_configuration)() {
uint32_t ints = save_and_disable_interrupts(); uint32_t ints = save_and_disable_interrupts();
flash_range_erase(USER_CONFIGURATION_OFFSET, FLASH_SECTOR_SIZE); flash_range_erase(USER_CONFIGURATION_OFFSET, FLASH_SECTOR_SIZE);
flash_range_program(USER_CONFIGURATION_OFFSET, flash_buffer, FLASH_PAGE_SIZE); flash_range_program(USER_CONFIGURATION_OFFSET, flash_buffer, CFG_BUFFER_SIZE);
restore_interrupts(ints); restore_interrupts(ints);
saveState = Saving;
power_up_dac(); // Return true, so the caller skips processing audio
return true; return true;
} }
// Validation failed, give up.
saveState = NormalOperation;
break;
case Saving:
/* Turn the DAC off so we don't make a huge noise when disrupting
real time audio operation. */
power_up_dac();
saveState = NormalOperation;
return false;
default:
break;
}
return false; return false;
} }
@ -391,7 +432,14 @@ bool process_cmd(tlv_header* cmd) {
} }
break; break;
case SAVE_CONFIGURATION: { case SAVE_CONFIGURATION: {
if (cmd->length == 4 && save_configuration()) { if (cmd->length == 4) {
saveState = SaveRequested;
if (audio_state.interface == 0) {
// The OS will configure the alternate "zero" interface when the device is not in use
// in this sate we can write to flash now. Otherwise, defer the save until we get the next
// usb packet.
save_config();
}
result->type = OK; result->type = OK;
result->length = 4; result->length = 4;
return true; return true;

View File

@ -52,6 +52,7 @@ void config_in_packet(struct usb_endpoint *ep);
void config_out_packet(struct usb_endpoint *ep); void config_out_packet(struct usb_endpoint *ep);
void configuration_ep_on_cancel(struct usb_endpoint *ep); void configuration_ep_on_cancel(struct usb_endpoint *ep);
extern void load_config(); extern void load_config();
extern bool save_config();
extern void apply_config_changes(); extern void apply_config_changes();
#endif // CONFIGURATION_MANAGER_H #endif // CONFIGURATION_MANAGER_H

View File

@ -17,8 +17,8 @@
#include <stdint.h> #include <stdint.h>
#define FLASH_MAGIC 0x2E8AFEDD #define FLASH_MAGIC 0x2E8AFEDD
#define CONFIG_VERSION 2 #define CONFIG_VERSION 4
#define MINIMUM_CONFIG_VERSION 1 #define MINIMUM_CONFIG_VERSION 4
enum structure_types { enum structure_types {
// Commands/Responses, these are container TLVs. The Value will be a set of TLV structures. // Commands/Responses, these are container TLVs. The Value will be a set of TLV structures.
@ -53,18 +53,20 @@ typedef struct __attribute__((__packed__)) _tlv_header {
typedef struct __attribute__((__packed__)) _filter2 { typedef struct __attribute__((__packed__)) _filter2 {
uint8_t type; uint8_t type;
uint8_t reserved[3]; uint8_t reserved[3];
double f0; float f0;
double Q; float Q;
} filter2; } filter2;
typedef struct __attribute__((__packed__)) _filter3 { typedef struct __attribute__((__packed__)) _filter3 {
uint8_t type; uint8_t type;
uint8_t reserved[3]; uint8_t reserved[3];
double f0; float f0;
double db_gain; float db_gain;
double Q; float Q;
} filter3; } filter3;
// WARNING: We wont be able to support more than 8 of these filters
// due to the config structure size.
typedef struct __attribute__((__packed__)) _filter6 { typedef struct __attribute__((__packed__)) _filter6 {
uint8_t type; uint8_t type;
uint8_t reserved[3]; uint8_t reserved[3];
@ -96,9 +98,13 @@ typedef struct __attribute__((__packed__)) _flash_header_tlv {
const uint8_t tlvs[0]; const uint8_t tlvs[0];
} flash_header_tlv; } flash_header_tlv;
/// @brief Holds values relating to processing surrounding the EQ calculation.
typedef struct __attribute__((__packed__)) _preprocessing_configuration_tlv { typedef struct __attribute__((__packed__)) _preprocessing_configuration_tlv {
tlv_header header; tlv_header header;
double preamp; /// @brief Gain applied to input signal before EQ chain. Use to avoid clipping due to overflow in the biquad filters of the EQ.
float preamp;
/// @brief Gain applied to the output of the EQ chain. Used to set output volume.
float postEQGain;
uint8_t reverse_stereo; uint8_t reverse_stereo;
uint8_t reserved[3]; uint8_t reserved[3];
} preprocessing_configuration_tlv; } preprocessing_configuration_tlv;
@ -134,6 +140,16 @@ typedef struct __attribute__((__packed__)) _default_configuration {
filter3 f3; filter3 f3;
filter3 f4; filter3 f4;
filter3 f5; filter3 f5;
filter3 f6;
filter3 f7;
filter3 f8;
filter3 f9;
filter3 f10;
filter3 f11;
filter3 f12;
filter3 f13;
filter3 f14;
filter3 f15;
} filters; } filters;
preprocessing_configuration_tlv preprocessing; preprocessing_configuration_tlv preprocessing;
} default_configuration; } default_configuration;

View File

@ -25,13 +25,6 @@
#include <stdbool.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
/// @brief Fixed point math type, in format Q3.28. One sign bit, 3 bits for left-of-decimal /// @brief Fixed point math type, in format Q3.28. One sign bit, 3 bits for left-of-decimal
///and 28 for right-of-decimal. This arrangment works because we normalize the incoming USB ///and 28 for right-of-decimal. This arrangment works because we normalize the incoming USB
@ -46,15 +39,15 @@ static const fix3_28_t fix16_one = 0x10000000;
/// @brief Represents zero in fixed point world. /// @brief Represents zero in fixed point world.
static const fix3_28_t fix16_zero = 0x00000000; static const fix3_28_t fix16_zero = 0x00000000;
#endif static inline fix3_28_t norm_fix3_28_from_s16sample(int16_t);
static inline int32_t norm_fix3_28_to_s16sample(fix3_28_t);
fix3_28_t norm_fix3_28_from_s16sample(int16_t);
static inline fix3_28_t fix3_28_from_flt(float);
int16_t norm_fix3_28_to_s16sample(fix3_28_t);
static inline fix3_28_t fix3_28_from_dbl(double);
fix3_28_t fix3_28_from_dbl(double);
static inline fix3_28_t fix16_mul(fix3_28_t, fix3_28_t);
fix3_28_t fix16_mul(fix3_28_t, fix3_28_t);
#include "fix16.inl"
#endif #endif

View File

@ -25,61 +25,25 @@
#include <limits.h> #include <limits.h>
#include "fix16.h" #include "fix16.h"
#ifdef USE_DOUBLE
fix16_t fix16_from_s16sample(int16_t a) {
return a;
}
int16_t fix16_to_s16sample(fix16_t a) {
// Handle rounding up front, adding one can cause an overflow/underflow
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
/// @brief Produces a fixed point number from a 16-bit signed integer, normalized to ]-1,1[. /// @brief Produces a fixed point number from a 16-bit signed integer, normalized to ]-1,1[.
/// @param a Signed 16-bit integer. /// @param a Signed 16-bit integer.
/// @return A fixed point number in Q3.28 format, with input normalized to ]-1,1[. /// @return A fixed point number in Q3.28 format, with input normalized to ]-1,1[.
fix3_28_t norm_fix3_28_from_s16sample(int16_t a) { static inline fix3_28_t norm_fix3_28_from_s16sample(int16_t a) {
/* So, we're using a Q3.28 fixed point system here, and we want the incoming /* So, we're using a Q3.28 fixed point system here, and we want the incoming
audio signal to be represented as a number between -1 and 1. To do this, audio signal to be represented as a number between -1 and 1. To do this,
we need the 16-bit value to map to the 28-bit right-of-decimal field in we need the 16-bit value to map to the 28-bit right-of-decimal field in
our fixed point number. 28-16 = 12, so we shift the incoming value by our fixed point number. 28-16 = 12 + the sign bit = 13, so we shift the
that much to covert it to the desired Q3.28 format and do the normalization incoming value by that much to covert it to the desired Q3.28 format and
all in one go. do the normalization all in one go.
*/ */
return (fix3_28_t)a << 12; return (fix3_28_t)a << 13;
} }
/// @brief Convert fixed point samples into signed integer. Used to convert /// @brief Convert fixed point samples into signed integer. Used to convert
/// calculated sample to one that the DAC can understand. /// calculated sample to one that the DAC can understand.
/// @param a /// @param a
/// @return Signed 16-bit integer. /// @return Signed 16-bit integer.
int16_t norm_fix3_28_to_s16sample(fix3_28_t a) { static inline int32_t norm_fix3_28_to_s16sample(fix3_28_t a) {
// Handle rounding up front, adding one can cause an overflow/underflow // Handle rounding up front, adding one can cause an overflow/underflow
// It's not clear exactly how this works, so we'll disable it for now. // It's not clear exactly how this works, so we'll disable it for now.
@ -92,26 +56,29 @@ int16_t norm_fix3_28_to_s16sample(fix3_28_t a) {
*/ */
// 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 >> 29);
if (a < 0) { if (a < 0) {
if (~upper) if (~upper) {
{ return 0xff800000;
return SHRT_MIN;
} }
} else { } else {
if (upper) if (upper) {
{ return 0x00efffff;
return SHRT_MAX;
} }
} }
/* When we converted the USB audio sample to a fixed point number, we applied /* When we converted the USB audio sample to a fixed point number, we applied
a normalization, or a gain of 1/65536. To convert it back, we can undo that a normalization, or a gain of 1/65536. To convert it back, we can undo that
by shifting it back by the same amount we shifted it in the first place. */ by shifting it but we output 24bts, so the shift is reduced. */
return (a >> 12); return (a >> 6);
} }
static inline fix3_28_t fix3_28_from_flt(float a) {
float temp = a * fix16_one;
temp += ((temp >= 0) ? 0.5f : -0.5f);
return (fix3_28_t)temp;
}
fix3_28_t fix3_28_from_dbl(double a) { static inline fix3_28_t fix3_28_from_dbl(double a) {
double temp = a * fix16_one; double temp = a * fix16_one;
temp += (double)((temp >= 0) ? 0.5f : -0.5f); temp += (double)((temp >= 0) ? 0.5f : -0.5f);
return (fix3_28_t)temp; return (fix3_28_t)temp;
@ -121,27 +88,22 @@ fix3_28_t fix3_28_from_dbl(double a) {
/// @param inArg0 Q3.28 format fixed point number. /// @param inArg0 Q3.28 format fixed point number.
/// @param inArg1 Q3.28 format fixed point number. /// @param inArg1 Q3.28 format fixed point number.
/// @return A Q3.28 fixed point number that represents the truncated result of inArg0 x inArg1. /// @return A Q3.28 fixed point number that represents the truncated result of inArg0 x inArg1.
fix3_28_t fix16_mul(fix3_28_t inArg0, fix3_28_t inArg1) { static inline fix3_28_t fix16_mul(fix3_28_t inArg0, fix3_28_t inArg1) {
const int64_t product = (int64_t)inArg0 * inArg1; int32_t A = (inArg0 >> 14), C = (inArg1 >> 14);
uint32_t B = (inArg0 & 0x3FFF), D = (inArg1 & 0x3FFF);
int32_t AC = A*C;
int32_t AD_CB = A*D + C*B;
int32_t product_hi = AC + (AD_CB >> 14);
/* Since we're expecting 2 Q3.28 numbers, the multiplication result should be a Q7.56 number. #if HANDLE_CARRY
To bring this number back to the right order of magnitude, we need to shift // Handle carry from lower bits to upper part of result.
it to the right by 28. */ uint32_t BD = B*D;
fix3_28_t result = product >> 28; uint32_t ad_cb_temp = AD_CB << 14;
uint32_t product_lo = BD + ad_cb_temp;
// Handle rounding where we are choppping off low order bits if (product_lo < BD)
// Disabled for now, too much load. We get crackling when adjusting product_hi++;
// the volume.
#if 0
if (product & 0x4000) {
if (result >= 0) {
result++;
}
else {
result--;
}
}
#endif
return result;
}
#endif #endif
return product_hi;
}

View File

@ -52,10 +52,12 @@ static uint8_t *userbuf;
audio_state_config audio_state = { audio_state_config audio_state = {
.freq = 48000, .freq = 48000,
.de_emphasis_frequency = 0x1, // 48khz .de_emphasis_frequency = 0x1, // 48khz
.interface = 0
}; };
preprocessing_config preprocessing = { preprocessing_config preprocessing = {
.preamp = fix16_one, .preamp = fix16_one,
.postEQGain = fix16_one,
.reverse_stereo = false .reverse_stereo = false
}; };
@ -118,12 +120,24 @@ static void update_volume()
// PCM data into I2S data that gets shipped out to the PCM3060. It really // 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, // belongs with the other USB-related code due to its utter indecipherability,
// but it's placed here to emphasize its importance. // but it's placed here to emphasize its importance.
static void _as_audio_packet(struct usb_endpoint *ep) { static void __no_inline_not_in_flash_func(_as_audio_packet)(struct usb_endpoint *ep) {
struct usb_buffer *usb_buffer = usb_current_out_packet_buffer(ep); struct usb_buffer *usb_buffer = usb_current_out_packet_buffer(ep);
int16_t *in = (int16_t *) usb_buffer->data; int16_t *in = (int16_t *) usb_buffer->data;
int32_t *out = (int32_t *) userbuf; int32_t *out = (int32_t *) userbuf;
int samples = usb_buffer->data_len / 2; int samples = usb_buffer->data_len / 2;
// Make sure core 1 is ready for us.
multicore_fifo_pop_blocking();
if (save_config()) {
// Skip processing while we are writing to flash
multicore_fifo_push_blocking(CORE0_ABORTED);
// keep on truckin'
usb_grow_transfer(ep->current_transfer, 1);
usb_packet_done(ep);
return;
}
if (preprocessing.reverse_stereo) { if (preprocessing.reverse_stereo) {
for (int i = 0; i < samples; i+=2) { for (int i = 0; i < samples; i+=2) {
out[i] = in[i+1]; out[i] = in[i+1];
@ -135,22 +149,23 @@ static void _as_audio_packet(struct usb_endpoint *ep) {
out[i] = in[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(CORE0_READY);
multicore_fifo_push_blocking(samples); multicore_fifo_push_blocking(samples);
for (int j = 0; j < filter_stages; j++) {
// Left channel filter // Left channel filter
for (int i = 0; i < samples; i += 2) { for (int i = 0; i < samples; i += 2) {
fix3_28_t x_f16 = fix16_mul(norm_fix3_28_from_s16sample((int16_t) out[i]), preprocessing.preamp); fix3_28_t x_f16 = fix16_mul(norm_fix3_28_from_s16sample((int16_t) out[i]), preprocessing.preamp);
for (int j = 0; j < filter_stages; j++) {
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]);
}
/* Apply post-EQ gain. */
x_f16 = fix16_mul( x_f16, preprocessing.postEQGain);
out[i] = (int32_t) norm_fix3_28_to_s16sample(x_f16); out[i] = (int32_t) norm_fix3_28_to_s16sample(x_f16);
} }
}
// Block until core 1 has finished transforming the data // Block until core 1 has finished transforming the data
uint32_t ready = multicore_fifo_pop_blocking(); uint32_t ready = multicore_fifo_pop_blocking();
@ -169,7 +184,7 @@ static void _as_audio_packet(struct usb_endpoint *ep) {
usb_packet_done(ep); usb_packet_done(ep);
} }
void core1_entry() { void __no_inline_not_in_flash_func(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;
@ -182,20 +197,23 @@ void core1_entry() {
// 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) if (ready == CORE0_ABORTED) continue;
ready = multicore_fifo_pop_blocking();
const uint32_t samples = multicore_fifo_pop_blocking(); const uint32_t samples = multicore_fifo_pop_blocking();
for (int j = 0; j < filter_stages; j++) { /* Right channel EQ. */
for (int i = 1; i < samples; i += 2) { for (int i = 1; i < samples; i += 2) {
/* Apply EQ pre-filter gain to avoid clipping. */
fix3_28_t x_f16 = fix16_mul(norm_fix3_28_from_s16sample((int16_t) out[i]), preprocessing.preamp); fix3_28_t x_f16 = fix16_mul(norm_fix3_28_from_s16sample((int16_t) out[i]), preprocessing.preamp);
/* Apply the biquad filters one by one. */
for (int j = 0; j < filter_stages; j++) {
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) norm_fix3_28_to_s16sample(x_f16);
} }
/* Apply post-EQ gain. */
x_f16 = fix16_mul( x_f16, preprocessing.postEQGain);
out[i] = (int32_t) norm_fix3_28_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
@ -247,7 +265,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, 100000); i2c_init(i2c0, 400000);
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);
@ -273,9 +291,9 @@ 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 // Set data format to 24 bit right justified, MSB first
buf[0] = 67; // register addr buf[0] = 67; // register addr
buf[1] = 0x03; // data buf[1] = 0x02; // data
i2c_write_blocking(i2c0, PCM_I2C_ADDR, buf, 2, false); 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;
@ -291,6 +309,7 @@ void setup() {
* IF YOU DO, YOU COULD BLOW UP YOUR HARDWARE! * * IF YOU DO, YOU COULD BLOW UP YOUR HARDWARE! *
* YOU WERE WARNED!!!!!!!!!!!!!!!! * * YOU WERE WARNED!!!!!!!!!!!!!!!! *
****************************************************************************/ ****************************************************************************/
// TODO: roundf will be much faster than round, but it might mess with timings
void configure_neg_switch_pwm() { void configure_neg_switch_pwm() {
gpio_set_function(NEG_SWITCH_PWM_PIN, GPIO_FUNC_PWM); gpio_set_function(NEG_SWITCH_PWM_PIN, GPIO_FUNC_PWM);
uint slice_num = pwm_gpio_to_slice_num(NEG_SWITCH_PWM_PIN); uint slice_num = pwm_gpio_to_slice_num(NEG_SWITCH_PWM_PIN);
@ -751,6 +770,7 @@ 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);
audio_state.interface = alt;
switch (alt) { switch (alt) {
case 0: power_down_dac(); return true; case 0: power_down_dac(); return true;
case 1: power_up_dac(); return true; case 1: power_up_dac(); return true;

View File

@ -76,6 +76,7 @@ typedef struct _audio_state_config {
int16_t _target_pcm3060_registers; int16_t _target_pcm3060_registers;
}; };
int16_t pcm3060_registers; int16_t pcm3060_registers;
int8_t interface;
} audio_state_config; } audio_state_config;
extern audio_state_config audio_state; extern audio_state_config audio_state;
@ -110,6 +111,8 @@ typedef struct _audio_device_config {
typedef struct _preprocessing_config { typedef struct _preprocessing_config {
fix3_28_t preamp; fix3_28_t preamp;
/// @brief Apply this gain after applying EQ, to set output volume without causing overflow in the EQ calculations.
fix3_28_t postEQGain;
int reverse_stereo; int reverse_stereo;
} preprocessing_config; } preprocessing_config;
@ -147,6 +150,7 @@ static char *descriptor_strings[] = {
#define SAMPLING_FREQ (CODEC_FREQ / 192) #define SAMPLING_FREQ (CODEC_FREQ / 192)
#define CORE0_READY 19813219 #define CORE0_READY 19813219
#define CORE0_ABORTED 91231891
#define CORE1_READY 72965426 #define CORE1_READY 72965426
/***************************************************************************** /*****************************************************************************

View File

@ -6,7 +6,6 @@ set(CMAKE_CXX_STANDARD 17)
add_executable(filter_test add_executable(filter_test
filter_test.c filter_test.c
../code/fix16.c
../code/bqf.c ../code/bqf.c
../code/configuration_manager.c ../code/configuration_manager.c
) )

View File

@ -17,7 +17,7 @@ Run `filter_test` to process the PCM samples. The `filter_test` program takes tw
You can listen to the PCM files using ffplay (which is usually included with ffmpeg): You can listen to the PCM files using ffplay (which is usually included with ffmpeg):
``` ```
ffplay -f s16le -ar 48000 -ac 2 output.pcm ffplay -f s24le -ar 48000 -ac 2 output.pcm
``` ```
If there are no obvious problems, go ahead and flash your firmware. If there are no obvious problems, go ahead and flash your firmware.

View File

@ -32,7 +32,7 @@ int main(int argc, char* argv[])
// we dont need to store the whole input and output files in memory. // we dont need to store the whole input and output files in memory.
int samples = input_size / 2; int samples = input_size / 2;
int16_t *in = (int16_t *) calloc(samples, sizeof(int16_t)); int16_t *in = (int16_t *) calloc(samples, sizeof(int16_t));
int16_t *out = (int16_t *) calloc(samples, sizeof(int16_t)); int32_t *out = (int32_t *) calloc(samples, sizeof(int32_t));
fread(in, samples, sizeof(int16_t), input); fread(in, samples, sizeof(int16_t), input);
fclose(input); fclose(input);
@ -54,31 +54,39 @@ int main(int argc, char* argv[])
out[i] = in[i]; out[i] = in[i];
} }
for (int j = 0; j < filter_stages; j++) const fix3_28_t preamp = fix3_28_from_flt(0.92f);
{
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_s16sample((int16_t) out[i]); fix3_28_t x_f16 = fix16_mul(norm_fix3_28_from_s16sample((int16_t) out[i]), preamp);
for (int j = 0; j < filter_stages; j++)
{
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_s16sample(x_f16); out[i] = (int32_t) norm_fix3_28_to_s16sample(x_f16);
// Right channel filter // Right channel filter
i++; i++;
x_f16 = fix16_from_s16sample((int16_t) out[i]); x_f16 = fix16_mul(norm_fix3_28_from_s16sample((int16_t) out[i]), preamp);
for (int j = 0; j < filter_stages; j++)
{
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_s16sample(x_f16);
} }
out[i] = (int32_t) norm_fix3_28_to_s16sample(x_f16);
//printf("%08x\n", out[i]);
} }
// Write out the processed audio. // Write out the processed audio.
fwrite(out, samples, sizeof(int16_t), output); for (int i=0; i<samples; i++) {
fwrite(&out[i], 3, sizeof(int8_t), output);
}
fclose(output); fclose(output);
free(in); free(in);