diff --git a/firmware/main/desklock_main.c b/firmware/main/desklock_main.c index 12c5fd4..5cd10a5 100644 --- a/firmware/main/desklock_main.c +++ b/firmware/main/desklock_main.c @@ -20,38 +20,106 @@ static const char *TAG = "desklock"; -#define CHIME_RATE 16000 -#define CHIME_SAMPLES (CHIME_RATE / 2) /* 0.5 s */ +#define GONG_RATE 16000 +#define GONG_SECONDS 5 +#define GONG_SAMPLES (GONG_RATE * GONG_SECONDS) +#define GONG_VOLUME 58 /* codec volume: discreet, not loud */ +#define GONG_PEAK 8000.0f /* ~-12 dBFS headroom */ -/* Bring-up proof for the audio path: C5 -> E5 with a fade-out. */ -static void play_boot_chime(void) +/* A distant temple gong in a stone space. + * + * Inharmonic partials with per-partial decay (lows ring for seconds, highs + * die fast), a detuned fundamental pair for slow shimmer, a soft 45 ms + * attack so the strike reads as far away, and three early reflections that + * stand in for cathedral walls. Fundamental sits at 165 Hz — low enough to + * feel voluminous, high enough for the bare 2 W speaker to reproduce. + */ +static const struct { + float ratio; + float amp; + float tau; /* seconds to 1/e */ +} gong_partials[] = { + { 1.000f, 0.34f, 2.6f }, + { 1.004f, 0.22f, 2.2f }, /* beating partner -> shimmer */ + { 1.590f, 0.16f, 1.4f }, + { 2.140f, 0.11f, 0.9f }, + { 2.760f, 0.08f, 0.55f }, + { 3.570f, 0.05f, 0.35f }, + { 4.800f, 0.03f, 0.22f }, +}; + +static const struct { + int delay_ms; + float gain; +} gong_echoes[] = { { 95, 0.45f }, { 210, 0.30f }, { 370, 0.18f } }; + +static void play_boot_gong(void) { esp_codec_dev_handle_t spk = bsp_audio_codec_speaker_init(); if (spk == NULL) { - ESP_LOGW(TAG, "speaker init failed — no chime"); + ESP_LOGW(TAG, "speaker init failed — no gong"); return; } esp_codec_dev_sample_info_t fs = { - .sample_rate = CHIME_RATE, + .sample_rate = GONG_RATE, .channel = 1, .bits_per_sample = 16, }; - esp_codec_dev_set_out_vol(spk, 70); + esp_codec_dev_set_out_vol(spk, GONG_VOLUME); esp_codec_dev_open(spk, &fs); - int16_t *buf = malloc(CHIME_SAMPLES * sizeof(int16_t)); - if (buf != NULL) { - float phase = 0.0f; - for (int i = 0; i < CHIME_SAMPLES; i++) { - float freq = (i < CHIME_SAMPLES / 2) ? 523.25f : 659.25f; - phase += 2.0f * (float)M_PI * freq / CHIME_RATE; - float envelope = 1.0f - (float)i / CHIME_SAMPLES; - buf[i] = (int16_t)(sinf(phase) * 12000.0f * envelope); - } - esp_codec_dev_write(spk, buf, CHIME_SAMPLES * sizeof(int16_t)); - free(buf); - ESP_LOGI(TAG, "boot chime played"); + float *dry = heap_caps_calloc(GONG_SAMPLES, sizeof(float), MALLOC_CAP_SPIRAM); + int16_t *pcm = heap_caps_malloc(GONG_SAMPLES * sizeof(int16_t), MALLOC_CAP_SPIRAM); + if (dry == NULL || pcm == NULL) { + ESP_LOGW(TAG, "no memory for gong"); + goto out; } + + for (size_t p = 0; p < sizeof(gong_partials) / sizeof(gong_partials[0]); p++) { + float step = 2.0f * (float)M_PI * 165.0f * gong_partials[p].ratio / GONG_RATE; + float decay = expf(-1.0f / (gong_partials[p].tau * GONG_RATE)); + float env = gong_partials[p].amp; + float phase = 0.0f; + for (int i = 0; i < GONG_SAMPLES; i++) { + phase += step; + env *= decay; + dry[i] += sinf(phase) * env; + } + } + + const int attack = GONG_RATE * 45 / 1000; /* distant, not percussive */ + for (int i = 0; i < attack; i++) { + dry[i] *= (float)i / attack; + } + + float peak = 1e-6f; + for (int i = 0; i < GONG_SAMPLES; i++) { + float s = dry[i]; + for (size_t e = 0; e < sizeof(gong_echoes) / sizeof(gong_echoes[0]); e++) { + int j = i - GONG_RATE * gong_echoes[e].delay_ms / 1000; + if (j >= 0) { + s += dry[j] * gong_echoes[e].gain; + } + } + const int fade = GONG_RATE * 2 / 5; /* last 400 ms to silence */ + if (i > GONG_SAMPLES - fade) { + s *= (float)(GONG_SAMPLES - i) / fade; + } + dry[i] = s; + if (fabsf(s) > peak) { + peak = fabsf(s); + } + } + for (int i = 0; i < GONG_SAMPLES; i++) { + pcm[i] = (int16_t)(dry[i] * (GONG_PEAK / peak)); + } + + esp_codec_dev_write(spk, pcm, GONG_SAMPLES * sizeof(int16_t)); + ESP_LOGI(TAG, "boot gong played"); + +out: + free(dry); + free(pcm); esp_codec_dev_close(spk); } @@ -89,7 +157,7 @@ void app_main(void) bsp_display_start(); bsp_display_backlight_on(); show_placeholder_face(); - play_boot_chime(); + play_boot_gong(); ESP_LOGI(TAG, "DeskLock up (placeholder face)");