Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions components/dsp_processor/Kconfig.projbuild
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ menu "ESP32 DSP processor config"
help
Use software volume mixer instead of hardware mixer (when your DAC does not support hardware volume control).

config SNAPCLIENT_VOLUME_CURVE_DB_RANGE
int "Volume curve range in dB (0=linear, 60=standard, 90=max)"
default 60
depends on USE_DSP_PROCESSOR
range 0 90
help
Controls the shape of the software volume curve.
0 = linear passthrough (y = x, no shaping).
>0 = exponential curve y = a * exp(b * x) where
a = 10^(-dB/20), mapping slider [0,1] to amplitude
[10^(-dB/20), 1.0] for perceptually linear loudness.
60 dB is standard for consumer audio.

config USE_DSP_SOFT_CLIP
bool "Use cubic soft clipping"
default false
Expand Down
62 changes: 60 additions & 2 deletions components/dsp_processor/dsp_processor.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@


#include <math.h>
#include <stdint.h>
#include <string.h>
#include <sys/time.h>
Expand Down Expand Up @@ -48,6 +49,36 @@ static dsp_all_params_t all_params;
static ptype_t *filter = NULL;

static double dynamic_vol = 1.0;
static double last_slider_position = 1.0;

// Exponential volume curve: y = curve_a * expf(curve_b * x)
// where curve_a = 10^(-dB_range/20) and curve_b = -log(curve_a).
// When db_range == 0 the curve is bypassed (linear passthrough).
// Near-zero uses a linear ramp up to VOLUME_CURVE_ROLLOFF_THRESHOLD
// to guarantee true silence at slider = 0.
#ifndef CONFIG_SNAPCLIENT_VOLUME_CURVE_DB_RANGE
#define CONFIG_SNAPCLIENT_VOLUME_CURVE_DB_RANGE 60
#endif
#define VOLUME_CURVE_ROLLOFF_THRESHOLD 0.1f

static float volume_curve_db_range = (float)CONFIG_SNAPCLIENT_VOLUME_CURVE_DB_RANGE;
static float curve_a;
static float curve_b;
static float curve_threshold_val;

static void compute_curve(void) {
curve_a = powf(10.0f, -volume_curve_db_range / 20.0f);
curve_b = -logf(curve_a);
curve_threshold_val = curve_a * expf(curve_b * VOLUME_CURVE_ROLLOFF_THRESHOLD);
}

static double apply_volume_curve(double x) {
if (volume_curve_db_range == 0.0f)
return x;
if (x <= VOLUME_CURVE_ROLLOFF_THRESHOLD)
return x / VOLUME_CURVE_ROLLOFF_THRESHOLD * curve_threshold_val;
return curve_a * expf((float)x * curve_b);
}

static bool init = false;

Expand Down Expand Up @@ -93,6 +124,9 @@ void dsp_processor_init(void) {
// dsp_processor_settings component will call dsp_processor_set_params_for_flow()
// and dsp_processor_switch_flow() during its init to apply saved settings.

// Use Kconfig default; saved value is loaded by dsp_settings_init() later.
compute_curve();

if (params_mutex == NULL) {
params_mutex = xSemaphoreCreateMutex();
if (params_mutex == NULL) {
Expand Down Expand Up @@ -778,10 +812,34 @@ int dsp_processor_worker(char *pcmChnk, uint16_t len, uint32_t samplerate, int c
void dsp_processor_set_volome(double volume) {
ESP_LOGD(TAG, "%s: volume=%f", __func__, volume);
if (volume >= 0 && volume <= 1.0) {
ESP_LOGI(TAG, "Set volume to %f", volume);
dynamic_vol = volume;
last_slider_position = volume;
double curve = apply_volume_curve(volume);
ESP_LOGI(TAG, "Set volume to %f (raw %f, range=%.0f dB)", curve, volume,
volume_curve_db_range);
dynamic_vol = curve;
}
}

float dsp_processor_get_volume_curve_db_range(void) {
return volume_curve_db_range;
}

void dsp_processor_set_volume_curve_db_range(float db_range) {
if (db_range < 0.0f || db_range > 90.0f) {
ESP_LOGW(TAG, "%s: db_range %f out of range (0-90), clamping", __func__, db_range);
db_range = fminf(fmaxf(db_range, 0.0f), 90.0f);
}
if (db_range != volume_curve_db_range) {
float old_db_range = volume_curve_db_range;
volume_curve_db_range = db_range;
compute_curve();
double new_curve = apply_volume_curve(last_slider_position);
ESP_LOGI(TAG, "Volume curve updated: %.0f->%.0f dB, dynamic_vol: %f->%f",
old_db_range, volume_curve_db_range, dynamic_vol, new_curve);
dynamic_vol = new_curve;
}
}

/**
* Set parameters for a specific flow (without switching to it)
*/
Expand Down
6 changes: 6 additions & 0 deletions components/dsp_processor/include/dsp_processor.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ esp_err_t dsp_processor_update_filter_params(filterParams_t *params);

void dsp_processor_set_volome(double volume);

// Set the volume curve dB range (0 = linear, 60 = standard, 90 = max).
// Maps slider [0,1] to amplitude [10^(-dB/20), 1.0].
// Takes effect immediately; caller is responsible for persisting to NVS.
float dsp_processor_get_volume_curve_db_range(void);
void dsp_processor_set_volume_curve_db_range(float db_range);

/**
* Set parameters for a specific flow (without switching to it)
* This allows updating parameters for a flow that's not currently active
Expand Down
150 changes: 138 additions & 12 deletions components/dsp_processor_settings/dsp_processor_settings.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ static const char *NVS_KEY_ACTIVE_FLOW = "active_flow";
// Mutex for thread-safe NVS access
static SemaphoreHandle_t dsp_settings_mutex = NULL;


/**
* Generate flow-specific NVS key
* Format: "flow_<id>_<param>" (e.g., "flow_5_fc_1")
Expand All @@ -45,6 +44,22 @@ esp_err_t dsp_settings_init(void) {
}

ESP_LOGI(TAG, "%s: DSP settings manager initialized", __func__);

#ifdef CONFIG_SNAPCLIENT_USE_SOFT_VOL
// Load volume curve from NVS now that the mutex exists, before
// restoring DSP params. This must happen here (not in dsp_processor_init)
// because the NVS helpers require dsp_settings_mutex.
float saved_db_range;
if (dsp_settings_load_volume_curve_db_range(&saved_db_range) == ESP_OK) {
dsp_processor_set_volume_curve_db_range(saved_db_range);
ESP_LOGI(TAG, "Restored volume curve from NVS: %.0f dB",
saved_db_range);
} else {
ESP_LOGI(TAG, "No saved volume curve in NVS, using default (%.0f dB)",
(float)CONFIG_SNAPCLIENT_VOLUME_CURVE_DB_RANGE);
}
#endif

// Restore DSP parameters into dsp_processor so the processor has the
// persisted configuration after both modules are initialized.
// Note: caller (main) must initialize dsp_processor before calling
Expand All @@ -57,24 +72,31 @@ esp_err_t dsp_settings_init(void) {
for (int f = 0; f < DSP_FLOW_COUNT; f++) {
filterParams_t params;
memset(&params, 0, sizeof(params));
if (dsp_settings_get_flow_params((dspFlows_t)f, &params) == ESP_OK) {
esp_err_t e = dsp_processor_set_params_for_flow((dspFlows_t)f, &params);
if (dsp_settings_get_flow_params((dspFlows_t)f, &params) ==
ESP_OK) {
esp_err_t e =
dsp_processor_set_params_for_flow((dspFlows_t)f, &params);
if (e != ESP_OK) {
ESP_LOGW(TAG, "%s: Failed to apply params for flow %d: %s", __func__, f, esp_err_to_name(e));
ESP_LOGW(TAG, "%s: Failed to apply params for flow %d: %s",
__func__, f, esp_err_to_name(e));
} else {
ESP_LOGD(TAG, "%s: Restored params for flow %d: fc_1=%.2f gain_1=%.2f fc_3=%.2f gain_3=%.2f",
__func__, f, params.fc_1, params.gain_1, params.fc_3, params.gain_3);
ESP_LOGD(TAG,
"%s: Restored params for flow %d: fc_1=%.2f "
"gain_1=%.2f fc_3=%.2f gain_3=%.2f",
__func__, f, params.fc_1, params.gain_1,
params.fc_3, params.gain_3);
}
} else {
ESP_LOGD(TAG, "%s: No stored params for flow %d", __func__, f);
}
}
}

// Finally, instruct dsp_processor to switch to the active flow
esp_err_t se = dsp_processor_switch_flow(active);
if (se != ESP_OK) {
ESP_LOGW(TAG, "%s: dsp_processor_switch_flow failed: %s", __func__, esp_err_to_name(se));
ESP_LOGW(TAG, "%s: dsp_processor_switch_flow failed: %s", __func__,
esp_err_to_name(se));
}
return ESP_OK;
}
Expand Down Expand Up @@ -258,6 +280,16 @@ esp_err_t dsp_settings_get_json(char *json_out, size_t max_len) {
(void)dsp_settings_load_active_flow(&active_flow);
cJSON_AddNumberToObject(root, "active_flow", (int)active_flow);

#ifdef CONFIG_SNAPCLIENT_USE_SOFT_VOL
// Always emit the current in-memory value; NVS value takes precedence if present
float vol_curve = dsp_processor_get_volume_curve_db_range();
float saved_vol_curve = -1.0f;
if (dsp_settings_load_volume_curve_db_range(&saved_vol_curve) == ESP_OK) {
vol_curve = saved_vol_curve;
}
cJSON_AddNumberToObject(root, "volume_curve_db_range", vol_curve);
#endif

// Add flow schema with current values
cJSON *schema = cJSON_CreateArray();
if (!schema) {
Expand Down Expand Up @@ -499,6 +531,22 @@ esp_err_t dsp_settings_set_from_json(const char *json_in) {
}
}

#ifdef CONFIG_SNAPCLIENT_USE_SOFT_VOL
// Handle volume curve dB range
cJSON *vol_curve = cJSON_GetObjectItem(root, "volume_curve_db_range");
if (cJSON_IsNumber(vol_curve)) {
float vol_curve_val = (float)vol_curve->valueint;
dsp_processor_set_volume_curve_db_range(vol_curve_val);
esp_err_t save_err =
dsp_settings_save_volume_curve_db_range(vol_curve_val);
if (save_err != ESP_OK) {
ESP_LOGW(TAG, "%s: Failed to save volume_curve_db_range", __func__);
if (err == ESP_OK)
err = save_err;
}
}
#endif

// Iterate through all items and save flow parameters
// Expecting keys like "flow_5_fc_1", "flow_5_gain_1", etc.
cJSON *item = NULL;
Expand Down Expand Up @@ -585,7 +633,9 @@ esp_err_t dsp_settings_set_flow_params(dspFlows_t flow,
return ESP_ERR_INVALID_ARG;
}

ESP_LOGI(TAG, "Setting params for flow %d: fc_1=%.1f gain_1=%.1f fc_3=%.1f gain_3=%.1f",
ESP_LOGI(TAG,
"Setting params for flow %d: fc_1=%.1f gain_1=%.1f fc_3=%.1f "
"gain_3=%.1f",
flow, params->fc_1, params->gain_1, params->fc_3, params->gain_3);

// Save to NVS
Expand All @@ -600,10 +650,12 @@ esp_err_t dsp_settings_set_flow_params(dspFlows_t flow,
// If the flow we just saved is currently active, apply it to the
// DSP processor. Read active flow from NVS on demand.
dspFlows_t active = dspfStereo;
if (dsp_settings_load_active_flow(&active) == ESP_OK && active == flow) {
if (dsp_settings_load_active_flow(&active) == ESP_OK &&
active == flow) {
esp_err_t e = dsp_processor_set_params_for_flow(flow, params);
if (e == ESP_OK) {
ESP_LOGD(TAG, "Applied params to DSP processor for active flow");
ESP_LOGD(TAG,
"Applied params to DSP processor for active flow");
} else {
ESP_LOGW(TAG, "Failed to apply params to DSP processor: %s",
esp_err_to_name(e));
Expand Down Expand Up @@ -632,7 +684,8 @@ esp_err_t dsp_settings_switch_active_flow(dspFlows_t flow) {
if (dsp_settings_get_flow_params(flow, &params) == ESP_OK) {
esp_err_t e = dsp_processor_set_params_for_flow(flow, &params);
if (e != ESP_OK) {
ESP_LOGW(TAG, "Failed to set params for flow on DSP processor: %s",
ESP_LOGW(TAG,
"Failed to set params for flow on DSP processor: %s",
esp_err_to_name(e));
}
} else {
Expand All @@ -651,3 +704,76 @@ esp_err_t dsp_settings_switch_active_flow(dspFlows_t flow) {

return err;
}

static const char *NVS_KEY_VOLUME_CURVE_DB_RANGE = "vol_crv_db_rng";

esp_err_t dsp_settings_save_volume_curve_db_range(float db_range) {
ESP_LOGD(TAG, "%s: db_range=%f", __func__, db_range);

if (!dsp_settings_mutex)
return ESP_ERR_INVALID_STATE;

if (db_range < 0.0f || db_range > 90.0f) {
ESP_LOGW(TAG, "%s: db_range %f out of range (0-90)", __func__,
db_range);
return ESP_ERR_INVALID_ARG;
}

if (xSemaphoreTake(dsp_settings_mutex, pdMS_TO_TICKS(5000)) != pdTRUE)
return ESP_ERR_TIMEOUT;

nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h);
if (err != ESP_OK) {
xSemaphoreGive(dsp_settings_mutex);
ESP_LOGE(TAG, "%s: Failed to open NVS: %s", __func__,
esp_err_to_name(err));
return err;
}

// Store as int32 (multiply by 10 for 1 decimal place precision)
int32_t val = (int32_t)(db_range * 10.0f);
err = nvs_set_i32(h, NVS_KEY_VOLUME_CURVE_DB_RANGE, val);
if (err == ESP_OK)
err = nvs_commit(h);

nvs_close(h);
xSemaphoreGive(dsp_settings_mutex);

if (err == ESP_OK)
ESP_LOGI(TAG, "%s: Saved volume curve dB range: %f", __func__,
db_range);
else
ESP_LOGE(TAG, "%s: Failed to save: %s", __func__, esp_err_to_name(err));

return err;
}

esp_err_t dsp_settings_load_volume_curve_db_range(float *db_range) {
ESP_LOGD(TAG, "%s: entered", __func__);

if (!db_range || !dsp_settings_mutex)
return ESP_ERR_INVALID_ARG;

if (xSemaphoreTake(dsp_settings_mutex, pdMS_TO_TICKS(5000)) != pdTRUE)
return ESP_ERR_TIMEOUT;

nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &h);
if (err == ESP_OK) {
int32_t val = 0;
err = nvs_get_i32(h, NVS_KEY_VOLUME_CURVE_DB_RANGE, &val);
nvs_close(h);
if (err == ESP_OK) {
*db_range = (float)val / 10.0f;
ESP_LOGI(TAG, "%s: Loaded volume curve dB range: %f", __func__,
*db_range);
} else if (err != ESP_ERR_NVS_NOT_FOUND) {
ESP_LOGW(TAG, "%s: NVS read error: %s", __func__,
esp_err_to_name(err));
}
}

xSemaphoreGive(dsp_settings_mutex);
return err;
}
14 changes: 14 additions & 0 deletions components/dsp_processor_settings/include/dsp_processor_settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,20 @@ esp_err_t dsp_settings_set_flow_params(dspFlows_t flow,
*/
esp_err_t dsp_settings_switch_active_flow(dspFlows_t flow);

/**
* Save volume curve dB range to NVS
* @param db_range dB range value (0-90)
* @return ESP_OK on success
*/
esp_err_t dsp_settings_save_volume_curve_db_range(float db_range);

/**
* Load volume curve dB range from NVS
* @param db_range Pointer to store the loaded value
* @return ESP_OK on success, ESP_ERR_NVS_NOT_FOUND if not set
*/
esp_err_t dsp_settings_load_volume_curve_db_range(float *db_range);

#ifdef __cplusplus
}
#endif
Expand Down
Loading
Loading