Replace beeps with a cathedral gong
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped

Additive gong synthesis: seven inharmonic partials with per-partial
decay (fundamental 165Hz rings 2.6s, highs die in 0.2s), a detuned
pair for shimmer, 45ms distant attack, and three feedback-comb
reflections (95/210/370ms, net gain .93) for a dense stone-space tail.
Normalized to -12dBFS at codec volume 58: quiet but voluminous.
Synthesized into PSRAM at boot (~4s), plays 5s, blocking is fine for
bring-up — audio moves to its own task in phase 2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 20:41:02 +02:00
co-authored by Claude Fable 5
parent 60c73e135c
commit bfe792f006
+88 -20
View File
@@ -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)");