From 85a3bf883606db01778b5a9f89bcda0d7a362c60 Mon Sep 17 00:00:00 2001 From: philippe44 Date: Mon, 28 Aug 2023 17:51:50 -0700 Subject: [PATCH] add loudness (alpha) --- components/platform_console/cmd_config.c | 35 +++- components/squeezelite/equalizer.c | 210 ++++++++++++++++------- components/squeezelite/equalizer.h | 8 +- components/squeezelite/output_bt.c | 3 +- components/squeezelite/output_embedded.c | 15 +- components/squeezelite/output_i2s.c | 7 +- main/esp_app_main.c | 1 + 7 files changed, 205 insertions(+), 74 deletions(-) diff --git a/components/platform_console/cmd_config.c b/components/platform_console/cmd_config.c index e33d4f7e..080ae6c5 100644 --- a/components/platform_console/cmd_config.c +++ b/components/platform_console/cmd_config.c @@ -32,6 +32,7 @@ const char * desc_rotary= "Rotary Control"; const char * desc_ledvu= "Led Strip Options"; extern const struct adac_s *dac_set[]; +extern void equalizer_set_loudness(uint8_t); #define CODECS_BASE "flac|pcm|mp3|ogg" #if NO_FAAD @@ -140,6 +141,7 @@ static struct { } spdif_args; static struct { struct arg_str *jack_behavior; + struct arg_int *loudness; struct arg_end *end; } audio_args; static struct { @@ -447,6 +449,30 @@ static int do_audio_cmd(int argc, char **argv){ fclose(f); return 1; } + + err = ESP_OK; // suppress any error code that might have happened in a previous step + + if(audio_args.loudness->count>0){ + char p[4]={0}; + int loudness_val = audio_args.loudness->ival[0]; + if( loudness_val < 0 || loudness_val>100){ + nerrors++; + fprintf(f,"Invalid loudness value %d. Valid values are between 0 and 100.\n",loudness_val); + } + // it's not necessary to store loudness in NVS as set_loudness does it, but it does not hurt + else { + itoa(loudness_val,p,10); + err = config_set_value(NVS_TYPE_STR, "loudness", p); + } + if(err!=ESP_OK){ + nerrors++; + fprintf(f,"Error setting Loudness value %s. %s\n",p, esp_err_to_name(err)); + } + else { + fprintf(f,"Loudness changed to %s\n",p); + equalizer_set_loudness(loudness_val); + } + } if(audio_args.jack_behavior->count>0){ err = ESP_OK; // suppress any error code that might have happened in a previous step @@ -712,7 +738,7 @@ static int do_i2s_cmd(int argc, char **argv) cmd_send_messaging(argv[0],MESSAGING_ERROR,"DAC Configuration is locked on this platform\n"); return 1; } - //strcpy(i2s_dac_pin.model, "I2S"); + ESP_LOGD(TAG,"Processing i2s command %s with %d parameters",argv[0],argc); esp_err_t err=ESP_OK; @@ -927,7 +953,10 @@ cJSON * audio_cb(){ cJSON * values = cJSON_CreateObject(); char * p = config_alloc_get_default(NVS_TYPE_STR, "jack_mutes_amp", "n", 0); cJSON_AddStringToObject(values,"jack_behavior",(strcmp(p,"1") == 0 ||strcasecmp(p,"y") == 0)?"Headphones":"Subwoofer"); - FREE_AND_NULL(p); + FREE_AND_NULL(p); + p = config_alloc_get_default(NVS_TYPE_STR, "loudness", "0", 0); + cJSON_AddStringToObject(values,"loudness",p); + FREE_AND_NULL(p); return values; } cJSON * bt_source_cb(){ @@ -1354,6 +1383,8 @@ static void register_ledvu_config(void){ static void register_audio_config(void){ audio_args.jack_behavior = arg_str0("j", "jack_behavior","Headphones|Subwoofer","On supported DAC, determines the audio jack behavior. Selecting headphones will cause the external amp to be muted on insert, while selecting Subwoofer will keep the amp active all the time."); + audio_args.loudness = arg_int0("l", "loudness","0-100","Sets the loudness level, from 0 to 100. 0 will disable the loudness completely."); + audio_args.end = arg_end(6); audio_args.end = arg_end(6); const esp_console_cmd_t cmd = { .command = CFG_TYPE_AUDIO("general"), diff --git a/components/squeezelite/equalizer.c b/components/squeezelite/equalizer.c index 9b083560..4038f69e 100644 --- a/components/squeezelite/equalizer.c +++ b/components/squeezelite/equalizer.c @@ -1,4 +1,4 @@ -/* +/* * Squeezelite for esp32 * * (c) Philippe G. 2020, philippe_44@outlook.com @@ -8,71 +8,92 @@ * */ +#include "math.h" #include "platform_config.h" -#include "squeezelite.h" +#include "squeezelite.h" #include "equalizer.h" #include "esp_equalizer.h" #define EQ_BANDS 10 - + static log_level loglevel = lINFO; - -static struct { + +static EXT_RAM_ATTR struct { void *handle; - float gain[EQ_BANDS]; + float loudness, volume; + uint32_t samplerate; + float gain[EQ_BANDS], loudness_gain[EQ_BANDS]; bool update; -} equalizer = { .update = true }; +} equalizer; + +#define POLYNOME_COUNT 6 + +static const float loudness_envelope_coefficients[EQ_BANDS][POLYNOME_COUNT] = { + {5.5169301499257067e+001, 6.3671410796029004e-001, + -4.2663226432095233e-002, 8.1063072336581246e-004, + -7.3621858933917722e-006, 2.5349489594339575e-008}, + {3.7716143859944118e+001, 1.2355293276538579e+000, + -6.6435374582217863e-002, 1.2976763440259382e-003, + -1.1978732496353172e-005, 4.1664114634622593e-008}, + {2.5103632377146837e+001, 1.3259150615414637e+000, + -6.6332442135695099e-002, 1.2845279812261677e-003, + -1.1799885217545631e-005, 4.0925911584040685e-008}, + {1.3159168212144563e+001, 8.8149357628440639e-001, + -4.0384121097225931e-002, 7.3843501027501322e-004, + -6.5508794453097008e-006, 2.2221997141120518e-008}, + {5.1337853800151700e+000, 4.0817077967582394e-001, + -1.4107826528626457e-002, 1.5251066311713760e-004, + -3.6689819583740298e-007, -2.0390798774727989e-009}, + {3.1432364156464315e-001, 9.1260548140023004e-002, + -3.5012124633183438e-004, -8.6023911664606992e-005, + 1.6785606828245921e-006, -8.8269731094371646e-009}, + {-4.0965062397075833e+000, 1.3667010948271402e-001, + 2.4775896786988390e-004, -9.6620399661858641e-005, + 1.7733690952379155e-006, -9.1583104942496635e-009}, + {-9.0275786029994176e+000, 2.6226938845184250e-001, + -6.5777547972402156e-003, 1.0045957188977551e-004, + -7.8851000325128971e-007, 2.4639885209682384e-009}, + {-4.4275018199195815e+000, 4.5399572638241725e-001, + -2.4034902766833462e-002, 5.9828953622534668e-004, + -6.2893971217140864e-006, 2.3133296592719627e-008}, + {1.4243299202697818e+001, 3.6984458807056630e-001, + -3.0413994109395680e-002, 7.6700105080386904e-004, + -8.2777185209388079e-006, 3.1352890650784970e-008} }; + +/**************************************************************************************** + * calculate loudness gains + */ +static void calculate_loudness(void) { + for (int i = 0; i < EQ_BANDS && equalizer.loudness > 0; i++) { + for (int j = 0; j < POLYNOME_COUNT; j++) { + equalizer.loudness_gain[i] += + loudness_envelope_coefficients[i][j] * pow(equalizer.volume, j); + } + equalizer.loudness_gain[i] *= equalizer.loudness; + } +} /**************************************************************************************** * initialize equalizer */ void equalizer_init(void) { - s8_t gain[EQ_BANDS] = { }; + // handle equalizer char *config = config_alloc_get(NVS_TYPE_STR, "equalizer"); - char *p = strtok(config, ", !"); - + char *p = strtok(config, ", !"); + for (int i = 0; p && i < EQ_BANDS; i++) { - gain[i] = atoi(p); + equalizer.gain[i] = atoi(p); p = strtok(NULL, ", :"); } - - free(config); - equalizer_update(gain); - - LOG_INFO("initializing equalizer"); -} - -/**************************************************************************************** - * open equalizer - */ -void equalizer_open(u32_t sample_rate) { - // in any case, need to clear update flag - equalizer.update = false; - - if (sample_rate != 11025 && sample_rate != 22050 && sample_rate != 44100 && sample_rate != 48000) { - LOG_WARN("equalizer only supports 11025, 22050, 44100 and 48000 sample rates, not %u", sample_rate); - return; - } - - equalizer.handle = esp_equalizer_init(2, sample_rate, EQ_BANDS, 0); - - if (equalizer.handle) { - bool active = false; - - for (int i = 0; i < EQ_BANDS; i++) { - esp_equalizer_set_band_value(equalizer.handle, equalizer.gain[i], i, 0); - esp_equalizer_set_band_value(equalizer.handle, equalizer.gain[i], i, 1); - active |= equalizer.gain[i] != 0; - } - - // do not activate equalizer if all gain are 0 - if (!active) equalizer_close(); - - LOG_INFO("equalizer initialized %u", active); - } else { - LOG_WARN("can't init equalizer"); - } -} + + free(config); + + // handle loudness + config = config_alloc_get(NVS_TYPE_STR, "loudness"); + equalizer.loudness = atof(config) / 100.0; + + free(config); +} /**************************************************************************************** * close equalizer @@ -82,36 +103,99 @@ void equalizer_close(void) { esp_equalizer_uninit(equalizer.handle); equalizer.handle = NULL; } -} +} /**************************************************************************************** - * update equalizer gain + * change sample rate */ -void equalizer_update(s8_t *gain) { - char config[EQ_BANDS * 4 + 1] = { }; +void equalizer_set_samplerate(uint32_t samplerate) { + if (equalizer.samplerate != samplerate) equalizer_close(); + equalizer.samplerate = samplerate; + equalizer.update = true; + + LOG_INFO("equalizer sample rate %u", samplerate); +} + +/**************************************************************************************** + * get volume update and recalculate loudness according to + */ +void equalizer_set_volume(unsigned left, unsigned right) { + equalizer.volume = (left + right) / 2; + // do classic dB conversion and scale it 0..100 + if (equalizer.volume) equalizer.volume = log2(equalizer.volume); + equalizer.volume = equalizer.volume / 16.0 * 100.0; + calculate_loudness(); + equalizer.update = true; +} + +/**************************************************************************************** + * change gains from LMS + */ +void equalizer_set_gain(int8_t *gain) { + char config[EQ_BANDS * 4 + 1] = { }; int n = 0; - + for (int i = 0; i < EQ_BANDS; i++) { equalizer.gain[i] = gain[i]; n += sprintf(config + n, "%d,", gain[i]); } - + config[n-1] = '\0'; - config_set_value(NVS_TYPE_STR, "equalizer", config); + config_set_value(NVS_TYPE_STR, "equalizer", config); equalizer.update = true; + + LOG_INFO("equalizer gain %s", config); } /**************************************************************************************** - * process equalizer + * change loudness from LMS */ -void equalizer_process(u8_t *buf, u32_t bytes, u32_t sample_rate) { +void equalizer_set_loudness(uint8_t loudness) { + // update loudness gains as a factor of loudness and volume + equalizer.loudness = loudness / 100.0; + calculate_loudness(); + + char p[4]; + itoa(loudness, p, 10); + config_set_value(NVS_TYPE_STR, "loudness", p); + equalizer.update = true; + + LOG_INFO("equalizer loudness %u", (unsigned) loudness); +} + +/**************************************************************************************** + * process equalizer + */ +void equalizer_process(uint8_t *buf, uint32_t bytes) { // don't want to process with output locked, so take the small risk to miss one parametric update if (equalizer.update) { - equalizer_close(); - equalizer_open(sample_rate); + equalizer.update = false; + + if (equalizer.samplerate != 11025 && equalizer.samplerate != 22050 && equalizer.samplerate != 44100 && equalizer.samplerate != 48000) { + LOG_WARN("equalizer only supports 11025, 22050, 44100 and 48000 sample rates, not %u", equalizer.samplerate); + return; + } + + if (!equalizer.handle && ((equalizer.handle = esp_equalizer_init(2, equalizer.samplerate, EQ_BANDS, 0)) == NULL)) { + LOG_WARN("can't init equalizer"); + return; + } + + bool active = false; + for (int i = 0; i < EQ_BANDS; i++) { + float gain = equalizer.gain[i] + equalizer.loudness_gain[i]; + esp_equalizer_set_band_value(equalizer.handle, gain, i, 0); + esp_equalizer_set_band_value(equalizer.handle, gain, i, 1); + active |= gain != 0; + LOG_INFO("EQUALIZER INDEX %u => gain:%.2f, loudness:%.2f,", i, equalizer.gain[i], equalizer.loudness_gain[i]); + } + + // at the end do not activate equalizer if all gain are 0 + if (!active) equalizer_close(); + LOG_INFO("equalizer %s", active ? "actived" : "deactivated"); } - + if (equalizer.handle) { - esp_equalizer_process(equalizer.handle, buf, bytes, sample_rate, 2); - } + esp_equalizer_process(equalizer.handle, buf, bytes, equalizer.samplerate, 2); + } } \ No newline at end of file diff --git a/components/squeezelite/equalizer.h b/components/squeezelite/equalizer.h index 0d487717..22acfa17 100644 --- a/components/squeezelite/equalizer.h +++ b/components/squeezelite/equalizer.h @@ -11,7 +11,9 @@ #pragma once void equalizer_init(void); -void equalizer_open(u32_t sample_rate); void equalizer_close(void); -void equalizer_update(s8_t *gain); -void equalizer_process(u8_t *buf, u32_t bytes, u32_t sample_rate); +void equalizer_set_samplerate(uint32_t samplerate); +void equalizer_set_gain(int8_t *gain); +void equalizer_set_loudness(u8_t loudness); +void equalizer_set_volume(unsigned left, unsigned right); +void equalizer_process(uint8_t *buf, uint32_t bytes); diff --git a/components/squeezelite/output_bt.c b/components/squeezelite/output_bt.c index 50d81611..c37f9ad2 100644 --- a/components/squeezelite/output_bt.c +++ b/components/squeezelite/output_bt.c @@ -69,6 +69,7 @@ void output_init_bt(log_level level, char *device, unsigned output_buf_size, cha char *p = config_alloc_get_default(NVS_TYPE_STR, "stats", "n", 0); stats = p && (*p == '1' || *p == 'Y' || *p == 'y'); free(p); + equalizer_set_samplerate(output.current_sample_rate); } void output_close_bt(void) { @@ -143,7 +144,7 @@ int32_t output_bt_data(uint8_t *data, int32_t len) { output.frames_in_process = oframes; UNLOCK; - equalizer_process(data, oframes * BYTES_PER_FRAME, output.current_sample_rate); + equalizer_process(data, oframes * BYTES_PER_FRAME); SET_MIN_MAX(TIME_MEASUREMENT_GET(start_timer),lock_out_time); SET_MIN_MAX((len-oframes*BYTES_PER_FRAME), rec); diff --git a/components/squeezelite/output_embedded.c b/components/squeezelite/output_embedded.c index 101c914f..0fc611b3 100644 --- a/components/squeezelite/output_embedded.c +++ b/components/squeezelite/output_embedded.c @@ -44,6 +44,11 @@ static void (*close_cb)(void); struct eqlz_packet { char opcode[4]; }; + +struct loud_packet { + char opcode[4]; + u8_t loudness; +}; #pragma pack(pop) static bool handler(u8_t *data, int len){ @@ -53,7 +58,12 @@ static bool handler(u8_t *data, int len){ s8_t *gain = (s8_t*) (data + sizeof(struct eqlz_packet)); LOG_INFO("got equalizer %d", len); // update will be done at next opportunity - equalizer_update(gain); + equalizer_set_gain(gain); + } else if (!strncmp((char*) data, "loud", 4)) { + struct loud_packet *packet = (struct loud_packet*) data; + LOG_INFO("got loudness %d", packet->loudness); + // update will be done at next opportunity + equalizer_set_loudness(packet->loudness); } else { res = false; } @@ -114,7 +124,8 @@ void set_volume(unsigned left, unsigned right) { output.gainL = left; output.gainR = right; UNLOCK; - } + } + equalizer_set_volume(left, right); } bool test_open(const char *device, unsigned rates[], bool userdef_rates) { diff --git a/components/squeezelite/output_i2s.c b/components/squeezelite/output_i2s.c index 6c1dd6c8..83b3af4a 100644 --- a/components/squeezelite/output_i2s.c +++ b/components/squeezelite/output_i2s.c @@ -376,6 +376,8 @@ void output_init_i2s(log_level level, char *device, unsigned output_buf_size, ch i2s_stop(CONFIG_I2S_NUM); i2s_zero_dma_buffer(CONFIG_I2S_NUM); isI2SStarted=false; + + equalizer_set_samplerate(output.current_sample_rate); adac->power(ADAC_STANDBY); @@ -582,14 +584,13 @@ static void output_thread_i2s(void *arg) { i2s_zero_dma_buffer(CONFIG_I2S_NUM); #if BYTES_PER_FRAME == 4 - equalizer_close(); - equalizer_open(output.current_sample_rate); + equalizer_set_samplerate(output.current_sample_rate); #endif } #if BYTES_PER_FRAME == 4 // run equalizer - equalizer_process(obuf, oframes * BYTES_PER_FRAME, output.current_sample_rate); + equalizer_process(obuf, oframes * BYTES_PER_FRAME); #endif // we assume that here we have been able to entirely fill the DMA buffers diff --git a/main/esp_app_main.c b/main/esp_app_main.c index 06ea9a96..bb817941 100644 --- a/main/esp_app_main.c +++ b/main/esp_app_main.c @@ -254,6 +254,7 @@ void register_default_nvs(){ register_default_string_val("ap_pwd", CONFIG_DEFAULT_AP_PASSWORD); register_default_string_val("bypass_wm", "0"); register_default_string_val("equalizer", ""); + register_default_string_val("loudness", "0"); register_default_string_val("actrls_config", ""); register_default_string_val("lms_ctrls_raw", "n"); register_default_string_val("rotary_config", CONFIG_ROTARY_ENCODER);