Compare commits
9 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
9c036f07f8 | |
![]() |
84306c0922 | |
![]() |
41d4023961 | |
![]() |
1e6896f918 | |
![]() |
fd6cbf54d5 | |
![]() |
c03b463390 | |
![]() |
5779320f68 | |
![]() |
8ac4089aa0 | |
![]() |
85ab78a2b8 |
|
@ -14,7 +14,6 @@ add_executable(ploopy_headphones
|
|||
run.c
|
||||
ringbuf.c
|
||||
i2s.c
|
||||
fix16.c
|
||||
bqf.c
|
||||
configuration_manager.c
|
||||
)
|
||||
|
@ -61,6 +60,11 @@ target_compile_definitions(ploopy_headphones PRIVATE
|
|||
|
||||
# make the git hash available to the firmware
|
||||
GIT_HASH="${GIT_HASH}"
|
||||
|
||||
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)
|
||||
|
|
|
@ -467,21 +467,6 @@ void bqf_highshelf_config(double fs, double f0, double dBgain, double Q,
|
|||
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) {
|
||||
memory->x_1 = fix16_zero;
|
||||
memory->x_2 = fix16_zero;
|
||||
|
|
|
@ -41,9 +41,9 @@ typedef struct _bqf_mem_t {
|
|||
fix3_28_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
|
||||
// More filters should be possible, but the config structure
|
||||
// might grow beyond the current 512 byte size.
|
||||
#define MAX_FILTER_STAGES 20
|
||||
extern int 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_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 *);
|
||||
|
||||
#include "bqf.inl"
|
||||
#endif
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -52,13 +52,29 @@ 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 }
|
||||
.f1 = { PEAKING, {0}, 38.5, -21.0, 1.4 },
|
||||
.f2 = { PEAKING, {0}, 60, -6.7, 0.5 },
|
||||
.f3 = { LOWSHELF, {0}, 105, 2.0, 0.71 },
|
||||
.f4 = { PEAKING, {0}, 280, -3.5, 1.1 },
|
||||
.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.
|
||||
|
@ -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
|
||||
* 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 inactive_working_configuration = 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 read_offset = 0;
|
||||
|
||||
typedef enum {
|
||||
NormalOperation,
|
||||
SaveRequested,
|
||||
Saving
|
||||
} State;
|
||||
static State saveState = NormalOperation;
|
||||
|
||||
bool validate_filter_configuration(filter_configuration_tlv *filters)
|
||||
{
|
||||
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);
|
||||
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");
|
||||
return false;
|
||||
}
|
||||
|
@ -179,7 +202,7 @@ void apply_filter_configuration(filter_configuration_tlv *filters) {
|
|||
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 = 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].a2 = fix3_28_from_dbl(args->a2/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
|
||||
case PREPROCESSING_CONFIGURATION: {
|
||||
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;
|
||||
break;
|
||||
}
|
||||
|
@ -340,16 +364,20 @@ void load_config() {
|
|||
}
|
||||
|
||||
#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;
|
||||
tlv_header* config = (tlv_header*) working_configuration[active_configuration];
|
||||
|
||||
switch (saveState) {
|
||||
case SaveRequested:
|
||||
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();
|
||||
|
||||
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];
|
||||
uint8_t flash_buffer[CFG_BUFFER_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;
|
||||
|
@ -359,13 +387,26 @@ bool __no_inline_not_in_flash_func(save_configuration)() {
|
|||
|
||||
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);
|
||||
flash_range_program(USER_CONFIGURATION_OFFSET, flash_buffer, CFG_BUFFER_SIZE);
|
||||
restore_interrupts(ints);
|
||||
saveState = Saving;
|
||||
|
||||
power_up_dac();
|
||||
|
||||
// Return true, so the caller skips processing audio
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -391,7 +432,14 @@ bool process_cmd(tlv_header* cmd) {
|
|||
}
|
||||
break;
|
||||
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->length = 4;
|
||||
return true;
|
||||
|
|
|
@ -52,6 +52,7 @@ 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 bool save_config();
|
||||
extern void apply_config_changes();
|
||||
|
||||
#endif // CONFIGURATION_MANAGER_H
|
|
@ -17,8 +17,8 @@
|
|||
#include <stdint.h>
|
||||
|
||||
#define FLASH_MAGIC 0x2E8AFEDD
|
||||
#define CONFIG_VERSION 2
|
||||
#define MINIMUM_CONFIG_VERSION 1
|
||||
#define CONFIG_VERSION 4
|
||||
#define MINIMUM_CONFIG_VERSION 4
|
||||
|
||||
enum structure_types {
|
||||
// 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 {
|
||||
uint8_t type;
|
||||
uint8_t reserved[3];
|
||||
double f0;
|
||||
double Q;
|
||||
float f0;
|
||||
float Q;
|
||||
} filter2;
|
||||
|
||||
typedef struct __attribute__((__packed__)) _filter3 {
|
||||
uint8_t type;
|
||||
uint8_t reserved[3];
|
||||
double f0;
|
||||
double db_gain;
|
||||
double Q;
|
||||
float f0;
|
||||
float db_gain;
|
||||
float Q;
|
||||
} 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 {
|
||||
uint8_t type;
|
||||
uint8_t reserved[3];
|
||||
|
@ -96,9 +98,13 @@ typedef struct __attribute__((__packed__)) _flash_header_tlv {
|
|||
const uint8_t tlvs[0];
|
||||
} flash_header_tlv;
|
||||
|
||||
/// @brief Holds values relating to processing surrounding the EQ calculation.
|
||||
typedef struct __attribute__((__packed__)) _preprocessing_configuration_tlv {
|
||||
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 reserved[3];
|
||||
} preprocessing_configuration_tlv;
|
||||
|
@ -134,6 +140,16 @@ typedef struct __attribute__((__packed__)) _default_configuration {
|
|||
filter3 f3;
|
||||
filter3 f4;
|
||||
filter3 f5;
|
||||
filter3 f6;
|
||||
filter3 f7;
|
||||
filter3 f8;
|
||||
filter3 f9;
|
||||
filter3 f10;
|
||||
filter3 f11;
|
||||
filter3 f12;
|
||||
filter3 f13;
|
||||
filter3 f14;
|
||||
filter3 f15;
|
||||
} filters;
|
||||
preprocessing_configuration_tlv preprocessing;
|
||||
} default_configuration;
|
||||
|
|
|
@ -25,13 +25,6 @@
|
|||
#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
|
||||
|
||||
/// @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
|
||||
|
@ -46,15 +39,15 @@ static const fix3_28_t fix16_one = 0x10000000;
|
|||
/// @brief Represents zero in fixed point world.
|
||||
static const fix3_28_t fix16_zero = 0x00000000;
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
fix3_28_t norm_fix3_28_from_s16sample(int16_t);
|
||||
|
||||
int16_t norm_fix3_28_to_s16sample(fix3_28_t);
|
||||
|
||||
fix3_28_t fix3_28_from_dbl(double);
|
||||
|
||||
fix3_28_t fix16_mul(fix3_28_t, fix3_28_t);
|
||||
|
||||
static inline fix3_28_t norm_fix3_28_from_s16sample(int16_t);
|
||||
|
||||
static inline int32_t norm_fix3_28_to_s16sample(fix3_28_t);
|
||||
|
||||
static inline fix3_28_t fix3_28_from_flt(float);
|
||||
|
||||
static inline fix3_28_t fix3_28_from_dbl(double);
|
||||
|
||||
static inline fix3_28_t fix16_mul(fix3_28_t, fix3_28_t);
|
||||
|
||||
#include "fix16.inl"
|
||||
#endif
|
|
@ -25,61 +25,25 @@
|
|||
#include <limits.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[.
|
||||
/// @param a Signed 16-bit integer.
|
||||
/// @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
|
||||
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
|
||||
our fixed point number. 28-16 = 12, so we shift the incoming value by
|
||||
that much to covert it to the desired Q3.28 format and do the normalization
|
||||
all in one go.
|
||||
our fixed point number. 28-16 = 12 + the sign bit = 13, so we shift the
|
||||
incoming value by that much to covert it to the desired Q3.28 format and
|
||||
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
|
||||
/// calculated sample to one that the DAC can understand.
|
||||
/// @param a
|
||||
/// @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
|
||||
|
||||
// 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
|
||||
uint32_t upper = (a >> 30);
|
||||
uint32_t upper = (a >> 29);
|
||||
if (a < 0) {
|
||||
if (~upper)
|
||||
{
|
||||
return SHRT_MIN;
|
||||
if (~upper) {
|
||||
return 0xff800000;
|
||||
}
|
||||
} else {
|
||||
if (upper)
|
||||
{
|
||||
return SHRT_MAX;
|
||||
if (upper) {
|
||||
return 0x00efffff;
|
||||
}
|
||||
}
|
||||
/* 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
|
||||
by shifting it back by the same amount we shifted it in the first place. */
|
||||
return (a >> 12);
|
||||
by shifting it but we output 24bts, so the shift is reduced. */
|
||||
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;
|
||||
temp += (double)((temp >= 0) ? 0.5f : -0.5f);
|
||||
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 inArg1 Q3.28 format fixed point number.
|
||||
/// @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) {
|
||||
const int64_t product = (int64_t)inArg0 * inArg1;
|
||||
static inline fix3_28_t fix16_mul(fix3_28_t inArg0, fix3_28_t 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.
|
||||
To bring this number back to the right order of magnitude, we need to shift
|
||||
it to the right by 28. */
|
||||
fix3_28_t result = product >> 28;
|
||||
#if HANDLE_CARRY
|
||||
// Handle carry from lower bits to upper part of result.
|
||||
uint32_t BD = B*D;
|
||||
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
|
||||
// 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;
|
||||
}
|
||||
if (product_lo < BD)
|
||||
product_hi++;
|
||||
#endif
|
||||
|
||||
return product_hi;
|
||||
}
|
|
@ -52,10 +52,12 @@ static uint8_t *userbuf;
|
|||
audio_state_config audio_state = {
|
||||
.freq = 48000,
|
||||
.de_emphasis_frequency = 0x1, // 48khz
|
||||
.interface = 0
|
||||
};
|
||||
|
||||
preprocessing_config preprocessing = {
|
||||
.preamp = fix16_one,
|
||||
.postEQGain = fix16_one,
|
||||
.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
|
||||
// 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) {
|
||||
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);
|
||||
int16_t *in = (int16_t *) usb_buffer->data;
|
||||
int32_t *out = (int32_t *) userbuf;
|
||||
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) {
|
||||
for (int i = 0; i < samples; i+=2) {
|
||||
out[i] = in[i+1];
|
||||
|
@ -135,22 +149,23 @@ static void _as_audio_packet(struct usb_endpoint *ep) {
|
|||
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) {
|
||||
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],
|
||||
&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);
|
||||
}
|
||||
}
|
||||
|
||||
// Block until core 1 has finished transforming the data
|
||||
uint32_t ready = multicore_fifo_pop_blocking();
|
||||
|
@ -169,7 +184,7 @@ static void _as_audio_packet(struct usb_endpoint *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();
|
||||
int32_t *out = (int32_t *) userbuf;
|
||||
|
||||
|
@ -182,20 +197,23 @@ void core1_entry() {
|
|||
|
||||
// Block until the userbuf is filled with data
|
||||
uint32_t ready = multicore_fifo_pop_blocking();
|
||||
while (ready != CORE0_READY)
|
||||
ready = multicore_fifo_pop_blocking();
|
||||
if (ready == CORE0_ABORTED) continue;
|
||||
|
||||
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) {
|
||||
/* 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);
|
||||
|
||||
/* 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],
|
||||
&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
|
||||
|
@ -247,7 +265,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, 100000);
|
||||
i2c_init(i2c0, 400000);
|
||||
gpio_set_function(PCM3060_SDA_PIN, GPIO_FUNC_I2C);
|
||||
gpio_set_function(PCM3060_SCL_PIN, GPIO_FUNC_I2C);
|
||||
gpio_pull_up(PCM3060_SDA_PIN);
|
||||
|
@ -273,9 +291,9 @@ void setup() {
|
|||
// Same here, pal. Hands off.
|
||||
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[1] = 0x03; // data
|
||||
buf[1] = 0x02; // data
|
||||
i2c_write_blocking(i2c0, PCM_I2C_ADDR, buf, 2, false);
|
||||
|
||||
i2s_write_obj.sck_pin = PCM3060_DAC_SCK_PIN;
|
||||
|
@ -291,6 +309,7 @@ void setup() {
|
|||
* IF YOU DO, YOU COULD BLOW UP YOUR HARDWARE! *
|
||||
* YOU WERE WARNED!!!!!!!!!!!!!!!! *
|
||||
****************************************************************************/
|
||||
// TODO: roundf will be much faster than round, but it might mess with timings
|
||||
void configure_neg_switch_pwm() {
|
||||
gpio_set_function(NEG_SWITCH_PWM_PIN, GPIO_FUNC_PWM);
|
||||
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) {
|
||||
assert(interface == &as_op_interface);
|
||||
audio_state.interface = alt;
|
||||
switch (alt) {
|
||||
case 0: power_down_dac(); return true;
|
||||
case 1: power_up_dac(); return true;
|
||||
|
|
|
@ -76,6 +76,7 @@ typedef struct _audio_state_config {
|
|||
int16_t _target_pcm3060_registers;
|
||||
};
|
||||
int16_t pcm3060_registers;
|
||||
int8_t interface;
|
||||
} audio_state_config;
|
||||
extern audio_state_config audio_state;
|
||||
|
||||
|
@ -110,6 +111,8 @@ typedef struct _audio_device_config {
|
|||
|
||||
typedef struct _preprocessing_config {
|
||||
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;
|
||||
} preprocessing_config;
|
||||
|
||||
|
@ -147,6 +150,7 @@ static char *descriptor_strings[] = {
|
|||
#define SAMPLING_FREQ (CODEC_FREQ / 192)
|
||||
|
||||
#define CORE0_READY 19813219
|
||||
#define CORE0_ABORTED 91231891
|
||||
#define CORE1_READY 72965426
|
||||
|
||||
/*****************************************************************************
|
||||
|
|
|
@ -6,7 +6,6 @@ set(CMAKE_CXX_STANDARD 17)
|
|||
|
||||
add_executable(filter_test
|
||||
filter_test.c
|
||||
../code/fix16.c
|
||||
../code/bqf.c
|
||||
../code/configuration_manager.c
|
||||
)
|
||||
|
|
|
@ -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):
|
||||
|
||||
```
|
||||
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.
|
||||
|
|
|
@ -32,7 +32,7 @@ int main(int argc, char* argv[])
|
|||
// we dont need to store the whole input and output files in memory.
|
||||
int samples = input_size / 2;
|
||||
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);
|
||||
fclose(input);
|
||||
|
@ -54,31 +54,39 @@ int main(int argc, char* argv[])
|
|||
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 ++)
|
||||
{
|
||||
// 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],
|
||||
&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
|
||||
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],
|
||||
&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.
|
||||
fwrite(out, samples, sizeof(int16_t), output);
|
||||
for (int i=0; i<samples; i++) {
|
||||
fwrite(&out[i], 3, sizeof(int8_t), output);
|
||||
}
|
||||
fclose(output);
|
||||
|
||||
free(in);
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,7 @@
|
|||
There are three sizes of headband, because good things come in threes. The parts can be identified by the mark on the top of the band. For example, the SMALL size of headband has a small triangular mark.
|
||||
|
||||
The sizes are relative, and don't correspond to any particular size of head. You might want a small size headband if you have a "small" head, or if you have a "large" head but want a higher clamping force.
|
||||
|
||||
For instructions on how to use these headbands, consult the assembly wiki (specifically, Part 4): https://github.com/ploopyco/headphones/wiki/Part-4%3A-Headband
|
||||
|
||||
We're always looking for feedback, particularly on ergonomics. If you try all three of these designs and find none of them fit you, please email us at contact@ploopy.co There's a decent chance we can create another size that'll work for you.
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue