Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

fix: negotiate audio sample rate dynamically for hardware compat

Instead of hardcoding 192kHz (which causes XRUN storms on hardware that
can't sustain it), probe the hardware rate range and pick the best
supported rate. Period/buffer sizes scale to the negotiated rate.
Adds plughw fallback if hw: config fails entirely.

Fixes audio on MacBook Pro 2011 (Cirrus Logic CS4206, max 96kHz).

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

+69 -16
+67 -15
fedac/native/src/audio.c
··· 227 227 228 228 static void *audio_thread_fn(void *arg) { 229 229 ACAudio *audio = (ACAudio *)arg; 230 - int16_t buffer[AUDIO_PERIOD_SIZE * AUDIO_CHANNELS]; 230 + const unsigned int period_frames = audio->actual_period ? audio->actual_period : AUDIO_PERIOD_SIZE; 231 + int16_t *buffer = calloc(period_frames * AUDIO_CHANNELS, sizeof(int16_t)); 232 + if (!buffer) { fprintf(stderr, "[audio] thread: alloc failed\n"); return NULL; } 231 233 const double rate = (double)(audio->actual_rate ? audio->actual_rate : AUDIO_SAMPLE_RATE); 232 234 const double dt = 1.0 / rate; 233 235 double mix_divisor = 1.0; // Smooth auto-mix (matches speaker.mjs) ··· 245 247 246 248 pthread_mutex_lock(&audio->lock); 247 249 248 - for (int i = 0; i < AUDIO_PERIOD_SIZE; i++) { 250 + for (unsigned int i = 0; i < period_frames; i++) { 249 251 double mix_l = 0.0, mix_r = 0.0; 250 252 double voice_sum = 0.0; // Total voice weight for auto-mix 251 253 ··· 549 551 } 550 552 551 553 // BPM metronome 552 - audio->beat_elapsed += (double)AUDIO_PERIOD_SIZE * dt; 554 + audio->beat_elapsed += (double)period_frames * dt; 553 555 double beat_interval = 60.0 / audio->bpm; 554 556 if (audio->beat_elapsed >= beat_interval) { 555 557 audio->beat_elapsed -= beat_interval; ··· 558 560 559 561 pthread_mutex_unlock(&audio->lock); 560 562 561 - audio->total_frames += AUDIO_PERIOD_SIZE; 563 + audio->total_frames += period_frames; 562 564 audio->time = (double)audio->total_frames / rate; 563 565 564 566 // Recording tap: send mixed PCM to recorder (if active) 565 567 if (audio->rec_callback) 566 - audio->rec_callback(buffer, AUDIO_PERIOD_SIZE, audio->rec_userdata); 568 + audio->rec_callback(buffer, period_frames, audio->rec_userdata); 567 569 568 570 // Write to ALSA (handle short writes to avoid dropped samples/clicks) 569 571 snd_pcm_t *pcm = (snd_pcm_t *)audio->pcm; 570 - int remaining = AUDIO_PERIOD_SIZE; 572 + int remaining = (int)period_frames; 571 573 int offset = 0; 572 574 while (remaining > 0) { 573 575 int frames = snd_pcm_writei(pcm, buffer + offset * AUDIO_CHANNELS, remaining); ··· 599 601 } 600 602 } 601 603 604 + free(buffer); 602 605 return NULL; 603 606 } 604 607 ··· 676 679 audio->mic_last_error[0] = 0; 677 680 seed_default_sample(audio); 678 681 679 - // TTS PCM ring buffer (5 seconds at output rate) 680 - audio->tts_buf_size = AUDIO_SAMPLE_RATE * 5; 682 + // TTS PCM ring buffer (5 seconds at max output rate) 683 + audio->tts_buf_size = AUDIO_SAMPLE_RATE * 5; // allocated at max, actual_rate adjusts usage 681 684 audio->tts_buf = calloc(audio->tts_buf_size, sizeof(float)); 682 685 audio->tts_read_pos = 0; 683 686 audio->tts_write_pos = 0; ··· 779 782 return audio; 780 783 } 781 784 782 - // Configure ALSA 785 + // Configure ALSA — negotiate rate dynamically. 786 + // Try preferred rates from highest to lowest. The hardware decides what it 787 + // actually supports; we adapt period/buffer sizes to match the negotiated rate. 783 788 snd_pcm_hw_params_t *params; 784 789 snd_pcm_hw_params_alloca(&params); 785 790 snd_pcm_hw_params_any(pcm, params); ··· 787 792 snd_pcm_hw_params_set_format(pcm, params, SND_PCM_FORMAT_S16_LE); 788 793 snd_pcm_hw_params_set_channels(pcm, params, AUDIO_CHANNELS); 789 794 790 - unsigned int rate = AUDIO_SAMPLE_RATE; 795 + // Query hardware rate range 796 + unsigned int rate_min = 0, rate_max = 0; 797 + snd_pcm_hw_params_get_rate_min(params, &rate_min, NULL); 798 + snd_pcm_hw_params_get_rate_max(params, &rate_max, NULL); 799 + fprintf(stderr, "[audio] Hardware rate range: %u–%u Hz\n", rate_min, rate_max); 800 + 801 + // Try rates from highest to lowest — pick the best the hardware supports 802 + unsigned int preferred_rates[] = { 192000, 96000, 48000, 44100, 0 }; 803 + unsigned int rate = 0; 804 + for (int i = 0; preferred_rates[i]; i++) { 805 + unsigned int try_rate = preferred_rates[i]; 806 + if (try_rate > rate_max || try_rate < rate_min) continue; 807 + // Test if this exact rate works 808 + if (snd_pcm_hw_params_test_rate(pcm, params, try_rate, 0) == 0) { 809 + rate = try_rate; 810 + fprintf(stderr, "[audio] Selected rate: %u Hz\n", rate); 811 + break; 812 + } 813 + } 814 + if (rate == 0) { 815 + // Fallback: let ALSA pick the nearest to 48kHz 816 + rate = 48000; 817 + fprintf(stderr, "[audio] No preferred rate supported, falling back to nearest-48kHz\n"); 818 + } 791 819 snd_pcm_hw_params_set_rate_near(pcm, params, &rate, 0); 792 820 793 - snd_pcm_uframes_t period = AUDIO_PERIOD_SIZE; 821 + // Scale period and buffer to ~1ms latency at the negotiated rate 822 + snd_pcm_uframes_t period = rate / 1000; // ~1ms worth of frames 823 + if (period < 64) period = 64; 794 824 snd_pcm_hw_params_set_period_size_near(pcm, params, &period, 0); 795 825 796 - snd_pcm_uframes_t buffer_size = AUDIO_PERIOD_SIZE * 3; // 3 periods (~3ms total at 192kHz) 826 + snd_pcm_uframes_t buffer_size = period * 4; // 4 periods (~4ms total) 797 827 snd_pcm_hw_params_set_buffer_size_near(pcm, params, &buffer_size); 798 828 799 829 err = snd_pcm_hw_params(pcm, params); 800 830 if (err < 0) { 801 - fprintf(stderr, "[audio] Cannot configure ALSA: %s\n", snd_strerror(err)); 831 + fprintf(stderr, "[audio] Cannot configure ALSA at %uHz: %s\n", rate, snd_strerror(err)); 832 + // Last resort: try plughw with default params 833 + fprintf(stderr, "[audio] Trying plughw fallback...\n"); 802 834 snd_pcm_close(pcm); 803 - audio->pcm = NULL; 804 - return audio; 835 + err = snd_pcm_open(&pcm, "plughw:0,0", SND_PCM_STREAM_PLAYBACK, 0); 836 + if (err >= 0) { 837 + snd_pcm_hw_params_any(pcm, params); 838 + snd_pcm_hw_params_set_access(pcm, params, SND_PCM_ACCESS_RW_INTERLEAVED); 839 + snd_pcm_hw_params_set_format(pcm, params, SND_PCM_FORMAT_S16_LE); 840 + snd_pcm_hw_params_set_channels(pcm, params, AUDIO_CHANNELS); 841 + rate = 48000; 842 + snd_pcm_hw_params_set_rate_near(pcm, params, &rate, 0); 843 + period = 256; 844 + snd_pcm_hw_params_set_period_size_near(pcm, params, &period, 0); 845 + buffer_size = 1024; 846 + snd_pcm_hw_params_set_buffer_size_near(pcm, params, &buffer_size); 847 + err = snd_pcm_hw_params(pcm, params); 848 + } 849 + if (err < 0) { 850 + fprintf(stderr, "[audio] All ALSA config attempts failed: %s\n", snd_strerror(err)); 851 + snd_pcm_close(pcm); 852 + audio->pcm = NULL; 853 + return audio; 854 + } 855 + snprintf(audio->audio_device, sizeof(audio->audio_device), "plughw:0,0"); 805 856 } 806 857 807 858 snd_pcm_prepare(pcm); 808 859 audio->pcm = pcm; 809 860 audio->actual_rate = rate; 861 + audio->actual_period = (unsigned int)period; 810 862 811 863 // Update glitch rate for actual sample rate 812 864 audio->glitch_rate = rate / 1600;
+2 -1
fedac/native/src/audio.h
··· 104 104 // System mixer volume (0-100 percent) 105 105 int system_volume; 106 106 int card_index; // ALSA card number (0 or 1) 107 - unsigned int actual_rate; // Negotiated ALSA sample rate (may differ from requested) 107 + unsigned int actual_rate; // Negotiated ALSA sample rate (may differ from requested) 108 + unsigned int actual_period; // Negotiated ALSA period size in frames 108 109 109 110 // TTS PCM buffer (resampled to output rate, mono → stereo in mix) 110 111 float *tts_buf; // ring buffer of mono float samples at output rate