Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

native: negotiate S32_LE format for SOF speaker PCM (fixes crunchy/quiet)

SOF topology FE PCMs use S32_LE internally and the SSP1 BE DAI
(MAX98360A) runs S24_LE. Writing S16_LE samples to hw:0,0 caused
48dB attenuation + quantization noise because the DSP interpreted
16-bit values as 32-bit.

Try S32_LE format first in hw_params. If accepted, the audio thread
widens int16 samples to int32 (left-shift 16) before snd_pcm_writei.
Falls back to S16_LE for non-SOF hardware. Reverts to hw:0,0 as
primary device (plughw: broke audio entirely on SOF).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+42 -8
+41 -8
fedac/native/src/audio.c
··· 1124 1124 ACAudio *audio = (ACAudio *)arg; 1125 1125 const unsigned int period_frames = audio->actual_period ? audio->actual_period : AUDIO_PERIOD_SIZE; 1126 1126 int16_t *buffer = calloc(period_frames * AUDIO_CHANNELS, sizeof(int16_t)); 1127 - if (!buffer) { fprintf(stderr, "[audio] thread: alloc failed\n"); return NULL; } 1127 + int32_t *buffer32 = NULL; 1128 + if (audio->use_s32) 1129 + buffer32 = calloc(period_frames * AUDIO_CHANNELS, sizeof(int32_t)); 1130 + if (!buffer || (audio->use_s32 && !buffer32)) { 1131 + fprintf(stderr, "[audio] thread: alloc failed\n"); return NULL; 1132 + } 1128 1133 const double rate = (double)(audio->actual_rate ? audio->actual_rate : AUDIO_SAMPLE_RATE); 1129 1134 const double dt = 1.0 / rate; 1130 1135 double mix_divisor = 1.0; // Smooth auto-mix (matches speaker.mjs) ··· 1588 1593 rem2 -= f2; off2 += f2; 1589 1594 } 1590 1595 } 1596 + /* Widen int16→int32 for S32_LE PCMs (SOF topology). 1597 + * Left-shift by 16 places the 16-bit sample in the top half 1598 + * of the 32-bit word, which maps correctly to the S24_LE BE 1599 + * DAI after the DSP truncates the bottom 8 bits. */ 1600 + const void *write_buf = buffer; 1601 + if (buffer32) { 1602 + for (int j = 0; j < (int)(period_frames * AUDIO_CHANNELS); j++) 1603 + buffer32[j] = (int32_t)buffer[j] << 16; 1604 + write_buf = buffer32; 1605 + } 1591 1606 int remaining = (int)period_frames; 1592 1607 int offset = 0; 1593 1608 while (remaining > 0) { 1594 - int frames = snd_pcm_writei(pcm, buffer + offset * AUDIO_CHANNELS, remaining); 1609 + const void *wptr = buffer32 1610 + ? (const void *)(buffer32 + offset * AUDIO_CHANNELS) 1611 + : (const void *)(buffer + offset * AUDIO_CHANNELS); 1612 + int frames = snd_pcm_writei(pcm, wptr, remaining); 1595 1613 if (frames == -EAGAIN) continue; 1596 1614 if (frames < 0) { 1597 1615 int rec = snd_pcm_recover(pcm, frames, 1); ··· 1621 1639 } 1622 1640 1623 1641 free(buffer); 1642 + free(buffer32); 1624 1643 return NULL; 1625 1644 } 1626 1645 ··· 1899 1918 snprintf(ucm_speaker_plug, sizeof(ucm_speaker_plug), 1900 1919 "plughw%s", ucm_speaker_pcm + 2); /* hw:0,0 → plughw:0,0 */ 1901 1920 int n = 0; 1902 - /* Prefer plug-wrapped device FIRST — the plug layer handles 1903 - * format/rate/channel conversion, which fixes the "crunchy 1904 - * quiet" audio on SOF boards where the SSP1 BE DAI runs at 1905 - * a different format than our S16_LE 48kHz stereo request. */ 1921 + /* Prefer raw hw: first — SOF topology FE PCM runs at S32_LE 1922 + * internally, and we now negotiate S32_LE directly so the DSP 1923 + * does zero conversion. plughw: as fallback if hw: fails. */ 1924 + devices_with_spk[n++] = ucm_speaker_pcm; 1906 1925 devices_with_spk[n++] = ucm_speaker_plug; 1907 - devices_with_spk[n++] = ucm_speaker_pcm; 1908 1926 for (int i = 0; devices_default[i] && n < 15; i++) 1909 1927 devices_with_spk[n++] = devices_default[i]; 1910 1928 devices_with_spk[n] = NULL; ··· 1968 1986 snd_pcm_hw_params_alloca(&params); 1969 1987 snd_pcm_hw_params_any(pcm, params); 1970 1988 snd_pcm_hw_params_set_access(pcm, params, SND_PCM_ACCESS_RW_INTERLEAVED); 1971 - snd_pcm_hw_params_set_format(pcm, params, SND_PCM_FORMAT_S16_LE); 1989 + /* SOF topology FE PCMs use S32_LE internally; the SSP1 BE DAI 1990 + * (MAX98360A) runs S24_LE. Writing S16_LE to this pipeline 1991 + * causes 48dB attenuation + quantization noise ("crunchy quiet"). 1992 + * Try S32_LE first — if the FE PCM accepts it, we write int32 1993 + * samples and the DSP does zero conversion. Fall back to S16_LE 1994 + * for non-SOF hardware (HDA, USB, etc). */ 1995 + audio->use_s32 = 0; 1996 + if (snd_pcm_hw_params_set_format(pcm, params, SND_PCM_FORMAT_S32_LE) == 0) { 1997 + audio->use_s32 = 1; 1998 + fprintf(stderr, "[audio] Negotiated S32_LE format\n"); 1999 + } else { 2000 + snd_pcm_hw_params_any(pcm, params); 2001 + snd_pcm_hw_params_set_access(pcm, params, SND_PCM_ACCESS_RW_INTERLEAVED); 2002 + snd_pcm_hw_params_set_format(pcm, params, SND_PCM_FORMAT_S16_LE); 2003 + fprintf(stderr, "[audio] Negotiated S16_LE format (S32_LE not supported)\n"); 2004 + } 1972 2005 snd_pcm_hw_params_set_channels(pcm, params, AUDIO_CHANNELS); 1973 2006 1974 2007 // Query hardware rate range
+1
fedac/native/src/audio.h
··· 244 244 int card_index; // ALSA card number (0 or 1) 245 245 unsigned int actual_rate; // Negotiated ALSA sample rate (may differ from requested) 246 246 unsigned int actual_period; // Negotiated ALSA period size in frames 247 + int use_s32; // 1 if PCM negotiated S32_LE (SOF boards), 0 for S16_LE 247 248 248 249 // TTS PCM buffer (resampled to output rate, mono → stereo in mix) 249 250 float *tts_buf; // ring buffer of mono float samples at output rate