feat(firmware): volume control goes to eleven

Rework the speaker volume from a 0-100 percentage to an 11-step level
(0..11, mapped to the codec's percent), with mute that remembers the
prior level so unmute restores it. Any non-silent change plays the gong
as feedback; a new change cuts the in-flight gong off and restarts it
rather than queueing another. The tap overlay shows the level number and
0..11 bar, and a "11" drifts up off the bar when you hit maximum.

Also lands the device side of gateway volume commands: gw_client routes a
"command" message to face_volume_command, which applies the change and
shows a compact auto-hiding volume HUD (a centered bar) — the gateway
half that sends these lives in a following commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
This commit is contained in:
2026-07-15 21:27:48 +02:00
co-authored by Claude Opus 4.8
parent 9521deddc5
commit cdb23bda05
5 changed files with 228 additions and 26 deletions
+55 -15
View File
@@ -29,13 +29,20 @@ static const char *TAG = "audio";
#define GONG_SECONDS 5
#define GONG_SAMPLES (RATE * GONG_SECONDS)
#define GONG_VOLUME 75
#define GONG_PEAK 14000.0f
/* Speaker volume is an 11-step level (0..AUDIO_VOL_MAX). This one goes to eleven. */
#define VOL_DEFAULT_LEVEL 8 /* ~73%, matches the previous 75 default */
static esp_codec_dev_handle_t s_spk;
static esp_codec_dev_handle_t s_mic;
static int s_vol = GONG_VOLUME; /* speaker out volume 0..100 */
static int s_level = VOL_DEFAULT_LEVEL; /* speaker volume, 0..AUDIO_VOL_MAX */
static bool s_muted;
static int level_to_pct(int level) { return level * 100 / AUDIO_VOL_MAX; }
static int16_t *s_gong;
#define GONG_CHUNK 1600 /* 100 ms — cut-off granularity for a restart */
static volatile uint32_t s_gong_gen; /* bumped per request; a change mid-play restarts */
static volatile bool s_gong_active;
static uint8_t *s_reply;
static volatile size_t s_reply_len;
@@ -153,7 +160,20 @@ static void audio_task(void *arg)
* ourselves. The tail delay covers the DMA that plays after write()
* returns, preventing a playback-boundary false wake. */
if (job == JOB_GONG && s_gong != NULL) {
esp_codec_dev_write(s_spk, s_gong, GONG_SAMPLES * sizeof(int16_t));
/* Play in chunks so a new request (a fresh volume change) cuts the
* current gong off and restarts, instead of queueing another 5 s. */
s_playing = true;
uint32_t gen;
do {
gen = s_gong_gen;
for (size_t off = 0; off < GONG_SAMPLES && s_gong_gen == gen; off += GONG_CHUNK) {
size_t n = GONG_SAMPLES - off;
if (n > GONG_CHUNK) n = GONG_CHUNK;
esp_codec_dev_write(s_spk, s_gong + off, n * sizeof(int16_t));
}
} while (s_gong_gen != gen); /* a new request arrived mid-play → restart */
s_playing = false;
s_gong_active = false;
} else if (job == JOB_CHIME && s_chime != NULL) {
s_playing = true;
esp_codec_dev_write(s_spk, s_chime, CHIME_SAMPLES * sizeof(int16_t));
@@ -277,7 +297,7 @@ void audio_init(void)
};
s_spk = bsp_audio_codec_speaker_init();
if (s_spk != NULL) {
esp_codec_dev_set_out_vol(s_spk, s_vol);
esp_codec_dev_set_out_vol(s_spk, level_to_pct(s_level));
esp_codec_dev_open(s_spk, &fs);
}
s_mic = bsp_audio_codec_microphone_init();
@@ -304,8 +324,14 @@ void audio_play_chime(void)
void audio_play_gong(void)
{
job_t job = JOB_GONG;
xQueueSend(s_jobs, &job, 0);
s_gong_gen++; /* (re)start; cuts any in-flight gong */
if (!s_gong_active) {
s_gong_active = true;
job_t job = JOB_GONG;
if (xQueueSend(s_jobs, &job, 0) != pdTRUE) {
s_gong_active = false; /* queue full — nothing will run it */
}
}
}
/* Streaming = the detect_task forwards AFE audio upstream. Wake word and touch
@@ -350,20 +376,34 @@ bool audio_is_playing(void)
return s_playing;
}
void audio_volume_set(int vol)
void audio_level_set(int level)
{
if (vol < 0) {
vol = 0;
} else if (vol > 100) {
vol = 100;
if (level < 0) {
level = 0;
} else if (level > AUDIO_VOL_MAX) {
level = AUDIO_VOL_MAX;
}
s_vol = vol;
s_level = level;
s_muted = false; /* setting a level unmutes */
if (s_spk != NULL) {
esp_codec_dev_set_out_vol(s_spk, s_vol);
esp_codec_dev_set_out_vol(s_spk, level_to_pct(s_level));
}
}
int audio_volume_get(void)
int audio_level_get(void)
{
return s_vol;
return s_level;
}
void audio_mute(bool on)
{
s_muted = on; /* gate output; s_level is preserved */
if (s_spk != NULL) {
esp_codec_dev_set_out_vol(s_spk, on ? 0 : level_to_pct(s_level));
}
}
bool audio_is_muted(void)
{
return s_muted;
}