Monorepo for Aesthetic.Computer
aesthetic.computer
1// audio.c — ALSA sound engine for ac-native
2// Dedicated audio thread with multi-voice synthesis, envelopes, and effects.
3
4#include "audio.h"
5#include <stdio.h>
6#include <stdlib.h>
7#include <string.h>
8#include <math.h>
9#include <time.h>
10#include <dirent.h>
11#include <unistd.h>
12#include <sched.h>
13#include <alsa/asoundlib.h>
14#include <alsa/use-case.h>
15
16// Defined in ac-native.c — writes to USB log and stderr.
17extern void ac_log(const char *fmt, ...);
18
19// Forward declarations
20static int read_system_volume_card(int card);
21
22// ============================================================
23// Note frequency table (octave 0 base frequencies)
24// ============================================================
25
26static const struct { const char *name; double freq; } note_table[] = {
27 {"c", 16.3516}, {"cs", 17.3239}, {"db", 17.3239},
28 {"d", 18.3540}, {"ds", 19.4454}, {"eb", 19.4454},
29 {"e", 20.6017}, {"f", 21.8268}, {"fs", 23.1247},
30 {"gb", 23.1247}, {"g", 24.4997}, {"gs", 25.9565},
31 {"ab", 25.9565}, {"a", 27.5000}, {"as", 29.1352},
32 {"bb", 29.1352}, {"b", 30.8677},
33};
34#define NOTE_TABLE_SIZE (sizeof(note_table) / sizeof(note_table[0]))
35
36double audio_note_to_freq(const char *note) {
37 if (!note || !*note) return 440.0;
38
39 // Try parsing as a number first
40 char *end;
41 double d = strtod(note, &end);
42 if (end != note && *end == '\0') return d;
43
44 // Parse note string: "C4", "4C#", "C#4", "5A", etc.
45 int octave = 4;
46 char name_buf[8] = {0};
47 int ni = 0;
48 const char *p = note;
49
50 // Check if starts with digit (octave prefix: "4C#")
51 if (*p >= '0' && *p <= '9') {
52 octave = *p - '0';
53 p++;
54 }
55
56 // Read note name
57 while (*p && ni < 3) {
58 char ch = *p;
59 if (ch >= 'A' && ch <= 'G') ch += 32; // lowercase
60 if ((ch >= 'a' && ch <= 'g') || ch == '#' || ch == 's' || ch == 'b') {
61 // Map 'f' for flat and '#' for sharp
62 if (ch == '#') { name_buf[ni++] = 's'; }
63 else { name_buf[ni++] = ch; }
64 p++;
65 } else break;
66 }
67 name_buf[ni] = '\0';
68
69 // Trailing octave number
70 if (*p >= '0' && *p <= '9') {
71 octave = *p - '0';
72 }
73
74 // Lookup base frequency
75 double base = 440.0; // fallback
76 for (int i = 0; i < (int)NOTE_TABLE_SIZE; i++) {
77 if (strcmp(name_buf, note_table[i].name) == 0) {
78 base = note_table[i].freq;
79 break;
80 }
81 }
82
83 return base * pow(2.0, octave);
84}
85
86// ============================================================
87// Oscillator sample generation
88// ============================================================
89
90static inline uint32_t xorshift32(uint32_t *state) {
91 uint32_t x = *state;
92 x ^= x << 13;
93 x ^= x >> 17;
94 x ^= x << 5;
95 *state = x;
96 return x;
97}
98
99static inline double clampd(double x, double lo, double hi) {
100 if (x < lo) return lo;
101 if (x > hi) return hi;
102 return x;
103}
104
105static inline double compute_envelope(ACVoice *v) {
106 double env = 1.0;
107
108 // Attack ramp
109 if (v->attack > 0.0 && v->elapsed < v->attack) {
110 env = v->elapsed / v->attack;
111 }
112
113 // Decay (near end of duration)
114 if (!isinf(v->duration) && v->decay > 0.0) {
115 double decay_start = v->duration - v->decay;
116 if (decay_start < 0.0) decay_start = 0.0;
117 if (v->elapsed > decay_start) {
118 double decay_progress = (v->elapsed - decay_start) / v->decay;
119 if (decay_progress > 1.0) decay_progress = 1.0;
120 env *= (1.0 - decay_progress);
121 }
122 }
123
124 return env;
125}
126
127// Fractional-delay read from a ring buffer. `delay` is in samples, allows
128// non-integer values via linear interpolation between adjacent samples.
129// Returns the sample `delay` positions behind the write cursor.
130static inline double whistle_frac_read(const float *buf, int N, int w, double delay) {
131 if (delay < 0.0) delay = 0.0;
132 if (delay > (double)(N - 2)) delay = (double)(N - 2);
133 double rd = (double)w - delay;
134 while (rd < 0.0) rd += (double)N;
135 int i0 = (int)rd;
136 int i1 = (i0 + 1) % N;
137 double f = rd - (double)i0;
138 return (double)buf[i0] * (1.0 - f) + (double)buf[i1] * f;
139}
140
141// Cook/STK digital waveguide flute model.
142// The signal flow (see reports/research for full derivation):
143//
144// breath ──► (+) ──► jetDelay ──► NL(x*(x*x-1)) ──► dcBlock ──► (+) ──► boreDelay ──┬──► out
145// ▲ ▲ │
146// │ −jetRefl·temp │ +endRefl·temp │
147// │ │ │
148// └───────── 1-pole LPF ◄───────────────────────────┴──────────────────┘
149//
150// The BORE delay line (length = SR/freq) is the primary resonator. Its
151// closed-loop feedback generates ALL harmonics automatically via comb
152// filtering — the delay line is inherently a periodic waveguide that
153// sustains exactly at integer multiples of its natural pitch.
154//
155// The JET delay (length ≈ 0.32 × bore) models the air jet's travel time
156// across the embouchure hole. The cubic nonlinearity x*(x*x-1) has
157// negative-slope region at x=0 which makes it a LIMIT-CYCLE GENERATOR —
158// it converts steady DC breath pressure into sustained oscillation.
159// This is qualitatively different from tanh, which is monotonic and
160// can only saturate.
161//
162// The 1-pole LPF in the loop models bore losses (viscothermal damping)
163// so the tone darkens as harmonics decay faster than the fundamental.
164//
165// The DC blocker after the NL removes the bias the cubic would pump
166// into the bore loop, which would otherwise drive it into clipping.
167static inline double generate_whistle_sample(ACVoice *v, double sample_rate) {
168 double env = compute_envelope(v);
169 // Breath envelope — DC pressure component + noise modulation + vibrato.
170 // CRITICAL: the DC component is what drives the nonlinearity into
171 // self-oscillation. Without a steady DC term, noise alone cannot
172 // sustain the limit cycle.
173 double breath_target = 0.18 + 0.82 * sqrt(env);
174 double breath_slew = env > v->whistle_breath ? 0.012 : 0.003;
175 v->whistle_breath += (breath_target - v->whistle_breath) * breath_slew;
176
177 // Vibrato LFO — ~5 Hz, small depth
178 v->whistle_vibrato_phase += 5.0 / sample_rate;
179 if (v->whistle_vibrato_phase >= 1.0) v->whistle_vibrato_phase -= 1.0;
180 double vibrato = sin(2.0 * M_PI * v->whistle_vibrato_phase) * 0.03;
181
182 // Breath noise — multiplicatively modulates the DC breath pressure.
183 // Low gain so the noise rides on top of the steady breath instead of
184 // replacing it. Attack phase gets slightly more chiff.
185 double white = ((double)xorshift32(&v->noise_seed) / (double)UINT32_MAX) * 2.0 - 1.0;
186 double onset = 1.0 - env;
187 double noise_gain = 0.08 + 0.05 * onset;
188 double breath = v->whistle_breath * (1.0 + noise_gain * white + vibrato);
189
190 // Bore and jet delay lengths — bore = SR/freq (one wavelength),
191 // jet = 0.32 × bore (Cook's flute ratio; 0.45 for pennywhistle,
192 // 0.5 for ocarina). Clamp to the delay buffer sizes.
193 double freq = clampd(v->frequency, 110.0, sample_rate * 0.20);
194 double bore_delay = sample_rate / freq;
195 double jet_delay = bore_delay * 0.32;
196 // Cap to buffer sizes with safety margin
197 const int BORE_N = 2048;
198 const int JET_N = 512;
199 if (bore_delay > (double)(BORE_N - 2)) bore_delay = (double)(BORE_N - 2);
200 if (jet_delay > (double)(JET_N - 2)) jet_delay = (double)(JET_N - 2);
201
202 // Read bore output and apply 1-pole loop LPF (models bore damping).
203 // 0.35/0.65 coefficients give ~0.65 DC gain — closes the loop just
204 // under unity so it sustains but doesn't blow up. The LPF rolls off
205 // high harmonics so the tone darkens naturally, unlike a biquad
206 // which would over-narrow the spectrum.
207 double bore_out = whistle_frac_read(v->whistle_bore_buf, BORE_N, v->whistle_bore_w, bore_delay);
208 v->whistle_lp1 = 0.35 * (-bore_out) + 0.65 * v->whistle_lp1;
209 double temp = v->whistle_lp1;
210
211 // Jet drive: breath pressure minus jet reflection from bore feedback
212 double jet_refl = 0.5;
213 double end_refl = 0.5;
214 double pd = breath - jet_refl * temp;
215
216 // Write to jet delay, read back with fractional delay
217 v->whistle_jet_buf[v->whistle_jet_w] = (float)pd;
218 v->whistle_jet_w = (v->whistle_jet_w + 1) % JET_N;
219 pd = whistle_frac_read(v->whistle_jet_buf, JET_N, v->whistle_jet_w, jet_delay);
220
221 // THE CUBIC NONLINEARITY — y = x*(x*x - 1). Negative slope at x=0
222 // creates a limit-cycle generator. This is the secret sauce that
223 // makes the tone WHISTLE instead of being filtered noise.
224 pd = pd * (pd * pd - 1.0);
225 if (pd > 1.0) pd = 1.0;
226 if (pd < -1.0) pd = -1.0;
227
228 // 1-pole DC blocker — removes the bias the cubic pumps into the loop.
229 // y[n] = x[n] - x[n-1] + 0.995*y[n-1]
230 double y = pd - v->whistle_hp_x1 + 0.995 * v->whistle_hp_y1;
231 v->whistle_hp_x1 = pd;
232 v->whistle_hp_y1 = y;
233
234 // Close the bore loop: combine the NL-filtered jet output with the
235 // end reflection from the bore delay.
236 double into_bore = y + end_refl * temp;
237 v->whistle_bore_buf[v->whistle_bore_w] = (float)into_bore;
238 v->whistle_bore_w = (v->whistle_bore_w + 1) % BORE_N;
239
240 // Output is a tap off the bore loop. 0.3 gain matches STK Flute.
241 return 0.3 * into_bore;
242}
243
244// ============================================================
245// Gun synthesis — two models per preset
246// ============================================================
247//
248// CLASSIC (default) — three-layer kick+snare-style synthesis:
249//
250// noise ──► BPF (mid-Q, ~2-6kHz) ──► amp_env(crack) ──┐
251// │
252// sin/tri ──► pitch_sweep(start→end) ──► amp_env(boom)─┼─► sum ──► out
253// │
254// noise ──► LPF (low-Q, ~200-2000Hz) ──► env(attack+decay)─┘
255//
256// • crack: instantaneous transient, exp decay 5-30 ms
257// • boom: pitched sine/triangle with fast downward sweep
258// (~250→40 Hz over 30-100 ms), exp amp decay
259// • tail: noisy residual rumble, optional linear attack ramp,
260// exp decay 100-800 ms
261//
262// This is how kick+snare drum synthesis works, applied to gunshots.
263// Cheap, predictable, sounds like the gunshot SFX you remember from
264// classic sample libraries and arcade games.
265//
266// PHYSICAL — digital waveguide barrel resonance + body modes:
267//
268// excitation ──► (+) ──► boreDelay ─┬──► muzzleHPF ──► out
269// ▲ │
270// │ breech_reflect │
271// │ ▼
272// └─── boreLP ◄──── (−1 open-end refl)
273//
274// excitation ──► 3× bodyModes ──► +out (parallel)
275//
276// Bore length sets the cavity resonance ("boom" frequency); body
277// modes give metallic character. Better for cavity-dominated sounds
278// (grenade, RPG launch) where the bore behavior actually matters.
279//
280// Common to both: secondary trigger (N-wave / 2nd click), sustain fire
281// (LMG retrigger), ricochet pitch sweep on release.
282//
283// Bore buffer is SHARED with the whistle (whistle_bore_buf) — only the
284// physical model uses it.
285
286typedef struct {
287 GunModel model;
288 // --- Common (both models) ---
289 double master_amp; // overall layer scaling (0.4–2.0)
290 double secondary_delay_ms; // 0 = no 2nd shot; else delay before re-trigger
291 double secondary_amp; // amplitude of 2nd shot relative to primary
292 int sustain_fire; // 1 = retrigger while held (LMG)
293 double retrig_period_ms; // ms between retrigs (60000/RPM)
294 // --- Classic-only ---
295 double click_amp; // sub-ms HF transient gain (0=off, ~0.6 typical)
296 double click_decay_ms; // very fast (0.3-0.8 ms) — the "tk" snap
297 double crack_amp; // 0..1 mix gain
298 double crack_decay_ms; // exp decay time of crack envelope
299 double crack_fc; // BPF center Hz (2000-8000 typical)
300 double crack_q; // BPF Q (1.0-3.0 typical)
301 double boom_amp; // 0..1 mix gain
302 double boom_freq_start; // Hz at trigger
303 double boom_freq_end; // Hz settled (≈40-80)
304 double boom_pitch_decay_ms; // time const for pitch sweep (10-50)
305 double boom_amp_decay_ms; // amp decay time (30-200)
306 double tail_amp; // 0..1 mix gain
307 double tail_attack_ms; // 0 = instant
308 double tail_decay_ms; // 100-800
309 double tail_fc; // LPF cutoff Hz (200-2000)
310 double tail_q; // LPF Q (0.5-1.5)
311 // --- Physical-only ---
312 double bore_length_s; // seconds (= 2L/c)
313 double bore_loss; // bore LPF alpha
314 double breech_reflect; // 0..1
315 double pressure; // excitation peak
316 double env_rate; // excitation decay rate (1/sec)
317 double noise_gain; // turbulent noise on excitation
318 double body_freq[3]; // mode freqs Hz
319 double body_q[3]; // mode Q
320 double body_amp[3]; // mode mix amplitudes
321 double radiation; // muzzle HPF coeff
322} GunPresetParams;
323
324// Per-weapon parameters. Most presets use CLASSIC for clean impact
325// sounds. Cavity-dominated weapons (grenade, RPG) keep the PHYSICAL
326// bore model where its long resonance helps.
327static const GunPresetParams gun_presets[GUN_PRESET_COUNT] = {
328 // --- GUN_PISTOL (9mm, L≈100mm) — sharp crack, tiny sub, quick tail
329 { .model = GUN_MODEL_CLASSIC, .master_amp = 1.1,
330 .click_amp = 0.65, .click_decay_ms = 0.5,
331 .crack_amp = 0.95, .crack_decay_ms = 7.0, .crack_fc = 3800, .crack_q = 2.6,
332 .boom_amp = 0.55, .boom_freq_start = 220, .boom_freq_end = 55,
333 .boom_pitch_decay_ms = 14, .boom_amp_decay_ms = 55,
334 .tail_amp = 0.35, .tail_attack_ms = 0, .tail_decay_ms = 110,
335 .tail_fc = 900, .tail_q = 0.8,
336 // Physical alt (warB): short barrel, bright body modes
337 .bore_length_s = 0.000588, .bore_loss = 0.55, .breech_reflect = 0.92,
338 .pressure = 1.2, .env_rate = 3000.0, .noise_gain = 0.6,
339 .body_freq = {1500, 4000, 8500}, .body_q = {12, 10, 8},
340 .body_amp = {0.30, 0.20, 0.15}, .radiation = 0.985 },
341 // --- GUN_RIFLE (AR-15, L≈400mm) — bright crack + supersonic N-wave tap
342 { .model = GUN_MODEL_CLASSIC, .master_amp = 1.2,
343 .click_amp = 0.75, .click_decay_ms = 0.6,
344 .crack_amp = 1.05, .crack_decay_ms = 8.0, .crack_fc = 4500, .crack_q = 3.0,
345 .boom_amp = 0.70, .boom_freq_start = 280, .boom_freq_end = 50,
346 .boom_pitch_decay_ms = 18, .boom_amp_decay_ms = 90,
347 .tail_amp = 0.45, .tail_attack_ms = 0, .tail_decay_ms = 220,
348 .tail_fc = 1100, .tail_q = 0.7,
349 .secondary_delay_ms = 0.9, .secondary_amp = 0.55,
350 // Physical alt: longer bore, deep mode ring + N-wave secondary
351 .bore_length_s = 0.00235, .bore_loss = 0.50, .breech_reflect = 0.95,
352 .pressure = 1.5, .env_rate = 2500.0, .noise_gain = 0.5,
353 .body_freq = {800, 2400, 6000}, .body_q = {14, 12, 10},
354 .body_amp = {0.35, 0.25, 0.15}, .radiation = 0.988 },
355 // --- GUN_SHOTGUN (12ga, L≈660mm, wide bore) — big low boom, noisy tail
356 { .model = GUN_MODEL_CLASSIC, .master_amp = 1.4,
357 .click_amp = 0.55, .click_decay_ms = 0.8,
358 .crack_amp = 0.65, .crack_decay_ms = 12, .crack_fc = 2200, .crack_q = 1.8,
359 .boom_amp = 1.10, .boom_freq_start = 260, .boom_freq_end = 38,
360 .boom_pitch_decay_ms = 22, .boom_amp_decay_ms = 130,
361 .tail_amp = 0.85, .tail_attack_ms = 4, .tail_decay_ms = 380,
362 .tail_fc = 700, .tail_q = 0.6,
363 // Physical alt: wide bore, low body modes
364 .bore_length_s = 0.00388, .bore_loss = 0.40, .breech_reflect = 0.88,
365 .pressure = 1.8, .env_rate = 1800.0, .noise_gain = 0.9,
366 .body_freq = {400, 1200, 3500}, .body_q = {10, 8, 7},
367 .body_amp = {0.40, 0.25, 0.15}, .radiation = 0.965 },
368 // --- GUN_SMG (MP5, L≈225mm) — bright fast crack, full-auto ~1000 RPM
369 { .model = GUN_MODEL_CLASSIC, .master_amp = 0.95,
370 .click_amp = 0.55, .click_decay_ms = 0.4,
371 .crack_amp = 0.85, .crack_decay_ms = 5.0, .crack_fc = 4200, .crack_q = 2.5,
372 .boom_amp = 0.40, .boom_freq_start = 200, .boom_freq_end = 60,
373 .boom_pitch_decay_ms = 10, .boom_amp_decay_ms = 40,
374 .tail_amp = 0.28, .tail_attack_ms = 0, .tail_decay_ms = 80,
375 .tail_fc = 1200, .tail_q = 0.7,
376 .sustain_fire = 1, .retrig_period_ms = 60, // 1000 RPM
377 // Physical alt
378 .bore_length_s = 0.00132, .bore_loss = 0.58, .breech_reflect = 0.92,
379 .pressure = 1.0, .env_rate = 3500.0, .noise_gain = 0.5,
380 .body_freq = {1200, 3500, 7500}, .body_q = {12, 10, 8},
381 .body_amp = {0.30, 0.20, 0.13}, .radiation = 0.978 },
382 // --- GUN_SUPPRESSED — tiny click, no boom, mid-range "pfft"
383 { .model = GUN_MODEL_CLASSIC, .master_amp = 0.7,
384 .click_amp = 0.08, .click_decay_ms = 0.4,
385 .crack_amp = 0.30, .crack_decay_ms = 6.0, .crack_fc = 1600, .crack_q = 1.1,
386 .boom_amp = 0.10, .boom_freq_start = 150, .boom_freq_end = 80,
387 .boom_pitch_decay_ms = 8, .boom_amp_decay_ms = 30,
388 .tail_amp = 0.85, .tail_attack_ms = 6, .tail_decay_ms = 140,
389 .tail_fc = 1800, .tail_q = 0.6,
390 // Physical alt: heavy bore loss = absorptive baffles, low radiation
391 .bore_length_s = 0.00100, .bore_loss = 0.85, .breech_reflect = 0.80,
392 .pressure = 0.5, .env_rate = 1500.0, .noise_gain = 1.0,
393 .body_freq = {600, 1500, 3000}, .body_q = {6, 5, 4},
394 .body_amp = {0.15, 0.10, 0.05}, .radiation = 0.85 },
395 // --- GUN_LMG (M60, L≈560mm) — rifle-class retriggered ~600 RPM
396 // Master amp tamed so the first shot doesn't stand out from the
397 // sustained burst (sustain-fire weapons also start their envelopes
398 // at the average jitter level — see gun_init_voice).
399 { .model = GUN_MODEL_CLASSIC, .master_amp = 0.9,
400 .click_amp = 0.55, .click_decay_ms = 0.5,
401 .crack_amp = 0.85, .crack_decay_ms = 7.0, .crack_fc = 3500, .crack_q = 2.6,
402 .boom_amp = 0.65, .boom_freq_start = 250, .boom_freq_end = 48,
403 .boom_pitch_decay_ms = 16, .boom_amp_decay_ms = 75,
404 .tail_amp = 0.40, .tail_attack_ms = 0, .tail_decay_ms = 160,
405 .tail_fc = 950, .tail_q = 0.7,
406 .sustain_fire = 1, .retrig_period_ms = 100, // 600 RPM
407 // Physical alt
408 .bore_length_s = 0.00329, .bore_loss = 0.48, .breech_reflect = 0.94,
409 .pressure = 1.4, .env_rate = 2200.0, .noise_gain = 0.55,
410 .body_freq = {600, 1800, 4500}, .body_q = {12, 10, 8},
411 .body_amp = {0.35, 0.25, 0.15}, .radiation = 0.982 },
412 // --- GUN_SNIPER (.50, L≈740mm) — huge crack + N-wave + long tail
413 { .model = GUN_MODEL_CLASSIC, .master_amp = 1.5,
414 .click_amp = 0.85, .click_decay_ms = 0.7,
415 .crack_amp = 1.20, .crack_decay_ms = 11, .crack_fc = 5000, .crack_q = 3.2,
416 .boom_amp = 1.20, .boom_freq_start = 320, .boom_freq_end = 36,
417 .boom_pitch_decay_ms = 28, .boom_amp_decay_ms = 180,
418 .tail_amp = 0.70, .tail_attack_ms = 3, .tail_decay_ms = 500,
419 .tail_fc = 850, .tail_q = 0.8,
420 .secondary_delay_ms = 1.4, .secondary_amp = 0.70,
421 // Physical alt: high pressure, long ring
422 .bore_length_s = 0.00435, .bore_loss = 0.35, .breech_reflect = 0.97,
423 .pressure = 2.0, .env_rate = 1500.0, .noise_gain = 0.7,
424 .body_freq = {350, 950, 2800}, .body_q = {14, 12, 10},
425 .body_amp = {0.50, 0.30, 0.15}, .radiation = 0.992 },
426 // --- GUN_GRENADE — large cavity, slow release. Default = PHYSICAL
427 // (the long bore resonance makes the cavity feel right). Classic
428 // alt is a very low boom + heavy noisy tail for the kaboom.
429 { .model = GUN_MODEL_PHYSICAL,
430 .bore_length_s = 0.01000, .bore_loss = 0.25, .breech_reflect = 0.60,
431 .pressure = 1.6, .env_rate = 400.0, .noise_gain = 1.5,
432 .body_freq = {80, 250, 1200}, .body_q = {6, 5, 4},
433 .body_amp = {0.60, 0.35, 0.15}, .radiation = 0.70,
434 // Classic alt (warA): tiny click, huge boom, very long tail
435 .master_amp = 1.6,
436 .click_amp = 0.40, .click_decay_ms = 1.0,
437 .crack_amp = 0.45, .crack_decay_ms = 25, .crack_fc = 800, .crack_q = 0.7,
438 .boom_amp = 1.50, .boom_freq_start = 150, .boom_freq_end = 28,
439 .boom_pitch_decay_ms = 60, .boom_amp_decay_ms = 350,
440 .tail_amp = 1.50, .tail_attack_ms = 12, .tail_decay_ms = 800,
441 .tail_fc = 400, .tail_q = 0.4 },
442 // --- GUN_RPG — long motor burn + delayed boom. Default = PHYSICAL
443 // (the slow bore loop nicely models the rocket exhaust whoosh).
444 { .model = GUN_MODEL_PHYSICAL,
445 .bore_length_s = 0.00300, .bore_loss = 0.30, .breech_reflect = 0.50,
446 .pressure = 1.2, .env_rate = 150.0, .noise_gain = 2.5,
447 .body_freq = {200, 600, 2000}, .body_q = {4, 3, 3},
448 .body_amp = {0.40, 0.30, 0.20}, .radiation = 0.60,
449 .secondary_delay_ms = 250, .secondary_amp = 1.5,
450 // Classic alt: launch click, sustained noise (motor) + delayed boom
451 .master_amp = 1.3,
452 .click_amp = 0.30, .click_decay_ms = 0.8,
453 .crack_amp = 0.40, .crack_decay_ms = 20, .crack_fc = 1500, .crack_q = 0.8,
454 .boom_amp = 0.30, .boom_freq_start = 120, .boom_freq_end = 60,
455 .boom_pitch_decay_ms = 30, .boom_amp_decay_ms = 100,
456 .tail_amp = 2.00, .tail_attack_ms = 80, .tail_decay_ms = 600,
457 .tail_fc = 600, .tail_q = 0.5 },
458 // --- GUN_RELOAD — magazine clack: bright HF click + bandpass burst
459 { .model = GUN_MODEL_CLASSIC, .master_amp = 0.75,
460 .click_amp = 0.85, .click_decay_ms = 0.4,
461 .crack_amp = 0.90, .crack_decay_ms = 4.0, .crack_fc = 4500, .crack_q = 3.0,
462 .boom_amp = 0.0, .boom_freq_start = 0, .boom_freq_end = 0,
463 .boom_pitch_decay_ms = 1, .boom_amp_decay_ms = 1,
464 .tail_amp = 0.20, .tail_attack_ms = 0, .tail_decay_ms = 30,
465 .tail_fc = 2500, .tail_q = 0.6,
466 .secondary_delay_ms = 80, .secondary_amp = 0.65,
467 // Physical alt: tiny bore = sharp metallic transient + insert click
468 .bore_length_s = 0.00010, .bore_loss = 0.70, .breech_reflect = 0.90,
469 .pressure = 0.6, .env_rate = 4000.0, .noise_gain = 0.3,
470 .body_freq = {2200, 4500, 8000}, .body_q = {10, 8, 6},
471 .body_amp = {0.40, 0.30, 0.15}, .radiation = 0.92 },
472 // --- GUN_COCK — bolt-action click-clack (two crisp clicks)
473 { .model = GUN_MODEL_CLASSIC, .master_amp = 0.8,
474 .click_amp = 0.90, .click_decay_ms = 0.4,
475 .crack_amp = 1.00, .crack_decay_ms = 5.0, .crack_fc = 3800, .crack_q = 3.2,
476 .boom_amp = 0.0, .boom_freq_start = 0, .boom_freq_end = 0,
477 .boom_pitch_decay_ms = 1, .boom_amp_decay_ms = 1,
478 .tail_amp = 0.15, .tail_attack_ms = 0, .tail_decay_ms = 25,
479 .tail_fc = 2000, .tail_q = 0.6,
480 .secondary_delay_ms = 55, .secondary_amp = 0.80,
481 // Physical alt
482 .bore_length_s = 0.00015, .bore_loss = 0.65, .breech_reflect = 0.88,
483 .pressure = 0.7, .env_rate = 3500.0, .noise_gain = 0.35,
484 .body_freq = {1800, 4200, 7500}, .body_q = {10, 8, 7},
485 .body_amp = {0.45, 0.25, 0.15}, .radiation = 0.92 },
486 // --- GUN_RICOCHET — pitched ping with downward pitch on release
487 { .model = GUN_MODEL_CLASSIC, .master_amp = 0.85,
488 .click_amp = 0.40, .click_decay_ms = 0.5,
489 .crack_amp = 0.35, .crack_decay_ms = 7.0, .crack_fc = 5500, .crack_q = 3.0,
490 .boom_amp = 0.95, .boom_freq_start = 1800,.boom_freq_end = 1500,
491 .boom_pitch_decay_ms = 60, .boom_amp_decay_ms = 350,
492 .tail_amp = 0.20, .tail_attack_ms = 0, .tail_decay_ms = 200,
493 .tail_fc = 3000, .tail_q = 1.0,
494 // Physical alt: high-Q metallic ring (ricochet really IS that)
495 .bore_length_s = 0.00040, .bore_loss = 0.15, .breech_reflect = 0.90,
496 .pressure = 0.8, .env_rate = 600.0, .noise_gain = 0.3,
497 .body_freq = {3000, 5500, 9000}, .body_q = {30, 25, 20},
498 .body_amp = {0.40, 0.25, 0.15}, .radiation = 0.975 },
499};
500
501// ----- helper: precompute 2-pole resonant filter coefficients -----
502// y = b0*x + a1*y[n-1] - a2*y[n-2]
503// a1 = 2·r·cos(w), a2 = r², r = exp(-π·f / (Q·sr)), w = 2π·f/sr
504// Output peak gain ≈ 1/(1-a1+a2) at DC and varies with Q. The b0 input
505// gain is scaled so the resonant peak is approximately unity, making
506// per-layer mix amps map to roughly equal loudness regardless of Q.
507static inline void compute_resonator(double f, double q, double sr,
508 double *a1, double *a2, double *b0) {
509 if (q < 0.4) q = 0.4;
510 if (f < 20.0) f = 20.0;
511 if (f > sr * 0.45) f = sr * 0.45;
512 double r = exp(-M_PI * f / (q * sr));
513 double w = 2.0 * M_PI * f / sr;
514 *a1 = 2.0 * r * cos(w);
515 *a2 = r * r;
516 // Peak gain of a 2-pole resonator ≈ 1/(1 - r). Pre-attenuate input
517 // by that factor so the resonant peak stays near unity amplitude.
518 *b0 = (1.0 - r);
519}
520
521// Initialize a voice's gun state from a preset. Called from audio_synth_gun.
522// `force_model` overrides the preset's default model: -1 = preset default,
523// 0 = CLASSIC, 1 = PHYSICAL. The preset table holds parameters for both
524// models so the override always finds a populated config.
525static void gun_init_voice(ACVoice *v, GunPreset preset, double sr,
526 int force_model) {
527 if (preset < 0 || preset >= GUN_PRESET_COUNT) preset = GUN_PISTOL;
528 const GunPresetParams *p = &gun_presets[preset];
529
530 v->gun_preset = (int)preset;
531 v->gun_model = (force_model == 0 || force_model == 1)
532 ? force_model : (int)p->model;
533 v->gun_pressure = p->master_amp > 0.0 ? p->master_amp : 1.0;
534 // Sustain-fire weapons (SMG/LMG) start at the same gentler level
535 // their internal retrigger uses (avg jitter ≈ 0.95) so the first
536 // shot blends with the rapid-fire stream instead of standing out.
537 v->gun_pressure_env = p->sustain_fire ? 0.92 : 1.0;
538 v->gun_secondary_trig = p->secondary_delay_ms > 0
539 ? p->secondary_delay_ms * 0.001 * sr : 0.0;
540 v->gun_secondary_amp = p->secondary_amp;
541 v->gun_sustain_fire = p->sustain_fire;
542 v->gun_retrig_timer = 0.0;
543 v->gun_retrig_period = p->retrig_period_ms * 0.001;
544
545 // Pitch sweep: nominal 1.0 at trigger. Ricochet sets target<1.0 on
546 // release so boom freq drops (doppler-style).
547 v->gun_pitch_mult = 1.0;
548 v->gun_pitch_target = 1.0;
549 v->gun_pitch_slew = 1.0 / (0.3 * sr);
550
551 if (v->gun_model == GUN_MODEL_CLASSIC) {
552 // Crack: exp decay multiplier from time-constant tau (in ms).
553 double tau_crack = (p->crack_decay_ms > 0.1 ? p->crack_decay_ms : 0.1) * 0.001;
554 v->gun_env_decay_mult = exp(-1.0 / (tau_crack * sr));
555
556 // Boom: pitch sweep from start→end via geometric approach.
557 // After tau seconds, distance to target is ~e^{-1} of original.
558 v->gun_boom_freq_start = p->boom_freq_start;
559 v->gun_boom_freq_end = p->boom_freq_end;
560 v->gun_boom_freq = p->boom_freq_start;
561 v->gun_boom_phase = 0.0;
562 double tau_pitch = (p->boom_pitch_decay_ms > 0.1 ? p->boom_pitch_decay_ms : 0.1) * 0.001;
563 v->gun_boom_pitch_mult = exp(-1.0 / (tau_pitch * sr));
564 double tau_boom = (p->boom_amp_decay_ms > 0.1 ? p->boom_amp_decay_ms : 0.1) * 0.001;
565 v->gun_boom_decay_mult = exp(-1.0 / (tau_boom * sr));
566 v->gun_boom_env = (p->boom_amp > 0.0) ? (p->sustain_fire ? 0.92 : 1.0) : 0.0;
567
568 // Tail: linear attack ramp + exp decay.
569 v->gun_tail_env = (p->tail_attack_ms > 0.0) ? 0.0 : 1.0;
570 if (p->tail_attack_ms > 0.0) {
571 v->gun_tail_attack_inc = 1.0 / (p->tail_attack_ms * 0.001 * sr);
572 } else {
573 v->gun_tail_attack_inc = 0.0;
574 }
575 double tau_tail = (p->tail_decay_ms > 0.1 ? p->tail_decay_ms : 0.1) * 0.001;
576 v->gun_tail_decay_mult = exp(-1.0 / (tau_tail * sr));
577
578 // Filter coeffs: body slot [0] = crack BPF, [1] = tail LPF.
579 compute_resonator(p->crack_fc, p->crack_q, sr,
580 &v->gun_body_a1[0], &v->gun_body_a2[0], &v->gun_crack_b0);
581 compute_resonator(p->tail_fc, p->tail_q, sr,
582 &v->gun_body_a1[1], &v->gun_body_a2[1], &v->gun_tail_b0);
583 v->gun_tail_b1 = 0.0;
584 v->gun_tail_b2 = 0.0;
585 v->gun_body_y1[0] = v->gun_body_y2[0] = 0.0;
586 v->gun_body_y1[1] = v->gun_body_y2[1] = 0.0;
587 v->gun_body_y1[2] = v->gun_body_y2[2] = 0.0;
588 // Layer mix gains.
589 v->gun_body_amp[0] = p->crack_amp;
590 v->gun_body_amp[1] = p->boom_amp;
591 v->gun_body_amp[2] = p->tail_amp;
592 // Click layer (sub-ms HF transient — adds the "tk" snap).
593 v->gun_click_amp = p->click_amp;
594 v->gun_click_env = (p->click_amp > 0.0) ? (p->sustain_fire ? 0.92 : 1.0) : 0.0;
595 v->gun_click_prev = 0.0;
596 double tau_click = (p->click_decay_ms > 0.05 ? p->click_decay_ms : 0.05) * 0.001;
597 v->gun_click_decay_mult = exp(-1.0 / (tau_click * sr));
598 // Physical-only fields zeroed for safety.
599 v->gun_bore_delay = 0.0;
600 v->gun_bore_loss = 0.0;
601 v->gun_bore_lp = 0.0;
602 v->gun_breech_reflect = 0.0;
603 v->gun_noise_gain = 0.0;
604 v->gun_radiation_a = 0.0;
605 v->gun_rad_prev = 0.0;
606 memset(v->whistle_bore_buf, 0, sizeof(v->whistle_bore_buf));
607 v->whistle_bore_w = 0;
608 } else {
609 // PHYSICAL model — DWG bore + body modes.
610 v->gun_bore_delay = p->bore_length_s * sr;
611 if (v->gun_bore_delay < 4.0) v->gun_bore_delay = 4.0;
612 if (v->gun_bore_delay > 2040.0) v->gun_bore_delay = 2040.0;
613 v->gun_bore_loss = p->bore_loss;
614 v->gun_bore_lp = 0.0;
615 v->gun_breech_reflect = p->breech_reflect;
616 v->gun_pressure = p->pressure; // physical uses its own pressure scale
617 v->gun_env_decay_mult = exp(-p->env_rate / sr);
618 v->gun_noise_gain = p->noise_gain;
619 v->gun_radiation_a = p->radiation;
620 v->gun_rad_prev = 0.0;
621 for (int i = 0; i < 3; i++) {
622 double a1, a2, b0_unused;
623 compute_resonator(p->body_freq[i], p->body_q[i], sr, &a1, &a2, &b0_unused);
624 v->gun_body_a1[i] = a1;
625 v->gun_body_a2[i] = a2;
626 v->gun_body_amp[i] = p->body_amp[i];
627 v->gun_body_y1[i] = 0.0;
628 v->gun_body_y2[i] = 0.0;
629 }
630 memset(v->whistle_bore_buf, 0, sizeof(v->whistle_bore_buf));
631 v->whistle_bore_w = 0;
632 // Friedlander pulse params. t+ derived from env_rate so existing
633 // preset tunings still feel right: shorter env_rate → wider pulse
634 // (grenade ~7ms, pistol ~1ms). Friedlander A = 1.5 is a good
635 // default for the positive-phase decay shape.
636 v->gun_phys_t = 0.0;
637 v->gun_phys_t_plus = (3.0 / (p->env_rate > 100 ? p->env_rate : 100.0)) * sr;
638 if (v->gun_phys_t_plus < 32.0) v->gun_phys_t_plus = 32.0; // ≥ ~0.17ms
639 if (v->gun_phys_t_plus > 4096.0) v->gun_phys_t_plus = 4096.0; // ≤ ~21ms
640 v->gun_phys_friedlander_a = 1.5;
641 v->gun_phys_neg_amp = 0.18;
642 // Ground reflection — fixed ~3.5ms tap with 18% gain. Caps at
643 // 1023 samples = ~5.3ms at 192kHz. Tunable per-preset later.
644 v->gun_phys_echo_delay = 0.0035 * sr;
645 if (v->gun_phys_echo_delay > 1023.0) v->gun_phys_echo_delay = 1023.0;
646 v->gun_phys_echo_amp = 0.22;
647 memset(v->gun_phys_echo_buf, 0, sizeof(v->gun_phys_echo_buf));
648 v->gun_phys_echo_w = 0;
649 // Classic-only fields zeroed.
650 v->gun_boom_phase = 0.0;
651 v->gun_boom_freq = 0.0;
652 v->gun_boom_freq_start = 0.0;
653 v->gun_boom_freq_end = 0.0;
654 v->gun_boom_pitch_mult = 1.0;
655 v->gun_boom_env = 0.0;
656 v->gun_boom_decay_mult = 1.0;
657 v->gun_tail_env = 0.0;
658 v->gun_tail_attack_inc = 0.0;
659 v->gun_tail_decay_mult = 1.0;
660 v->gun_crack_b0 = 0.0;
661 v->gun_tail_b0 = v->gun_tail_b1 = v->gun_tail_b2 = 0.0;
662 v->gun_click_amp = 0.0;
663 v->gun_click_env = 0.0;
664 v->gun_click_decay_mult = 1.0;
665 v->gun_click_prev = 0.0;
666 v->gun_phys_t = 0.0;
667 v->gun_phys_t_plus = 0.0;
668 v->gun_phys_friedlander_a = 0.0;
669 v->gun_phys_neg_amp = 0.0;
670 v->gun_phys_echo_delay = 0.0;
671 v->gun_phys_echo_amp = 0.0;
672 v->gun_phys_echo_w = 0;
673 }
674}
675
676// Called when a gun voice enters VOICE_KILLING — sets up release-time
677// behaviors (ricochet pitch drop applies to both models via gun_pitch_mult).
678static inline void gun_on_release(ACVoice *v) {
679 if (v->type != WAVE_GUN) return;
680 if (v->gun_preset == GUN_RICOCHET) {
681 // Drop pitch on release — for classic this scales boom freq down;
682 // for physical it stretches the bore delay (doppler).
683 v->gun_pitch_target = (v->gun_model == GUN_MODEL_CLASSIC) ? 0.35 : 2.8;
684 }
685}
686
687// Three-layer kick/snare-style gunshot synthesis. Output is summed
688// crack (BPF noise) + boom (pitched osc with downward sweep) + tail
689// (LPF noise with attack-decay), then scaled by master amp and the
690// piece-supplied envelope.
691static inline double generate_gun_classic_sample(ACVoice *v, double sr) {
692 // --- Secondary trigger (rifle N-wave / 2nd click of cock/reload) ---
693 if (v->gun_secondary_trig > 0.0) {
694 v->gun_secondary_trig -= 1.0;
695 if (v->gun_secondary_trig <= 0.0) {
696 v->gun_pressure_env = v->gun_secondary_amp; // refire crack
697 v->gun_boom_env = v->gun_secondary_amp * 0.6; // gentler boom
698 v->gun_click_env = v->gun_secondary_amp; // refire click too
699 v->gun_secondary_trig = 0.0;
700 }
701 }
702
703 // --- LMG sustain-fire retrigger ---
704 if (v->gun_sustain_fire && v->state == VOICE_ACTIVE
705 && isinf(v->duration) && v->gun_retrig_period > 0.0) {
706 v->gun_retrig_timer += 1.0 / sr;
707 if (v->gun_retrig_timer >= v->gun_retrig_period) {
708 v->gun_retrig_timer -= v->gun_retrig_period;
709 double j = (double)xorshift32(&v->noise_seed) / (double)UINT32_MAX;
710 double jitter = 0.82 + j * 0.32; // ±18%
711 v->gun_pressure_env = jitter;
712 v->gun_boom_env = jitter;
713 v->gun_click_env = jitter;
714 v->gun_boom_freq = v->gun_boom_freq_start; // restart pitch sweep
715 // Tail keeps decaying (no re-attack) so rapid-fire feels continuous.
716 }
717 }
718
719 // --- Pitch sweep (ricochet release doppler) ---
720 if (v->gun_pitch_mult != v->gun_pitch_target) {
721 v->gun_pitch_mult += (v->gun_pitch_target - v->gun_pitch_mult) * 0.00012;
722 }
723
724 // === Layer 0: CLICK — sub-millisecond HF transient ===
725 // 1-zero HPF on white noise (y = x - x[n-1]) emphasizes the highest
726 // frequencies. Combined with a ~0.5ms tau exp envelope it reads as
727 // the sharp "tk" attack you expect at the front of a gunshot —
728 // without it, the BPF crack on its own sounds like a shaped hiss.
729 double click = 0.0;
730 if (v->gun_click_env > 0.00002 && v->gun_click_amp > 0.0) {
731 double white = ((double)xorshift32(&v->noise_seed) / (double)UINT32_MAX) * 2.0 - 1.0;
732 double hp = white - v->gun_click_prev;
733 v->gun_click_prev = white;
734 click = hp * v->gun_click_env * v->gun_click_amp;
735 v->gun_click_env *= v->gun_click_decay_mult;
736 }
737
738 // === Layer 1: CRACK — bandpass-filtered noise burst ===
739 double crack = 0.0;
740 if (v->gun_pressure_env > 0.00002 && v->gun_body_amp[0] > 0.0) {
741 double white = ((double)xorshift32(&v->noise_seed) / (double)UINT32_MAX) * 2.0 - 1.0;
742 // 2-pole resonator (bandpass-like) on white noise.
743 double y = v->gun_crack_b0 * white
744 + v->gun_body_a1[0] * v->gun_body_y1[0]
745 - v->gun_body_a2[0] * v->gun_body_y2[0];
746 v->gun_body_y2[0] = v->gun_body_y1[0];
747 v->gun_body_y1[0] = y;
748 crack = y * v->gun_pressure_env * v->gun_body_amp[0];
749 v->gun_pressure_env *= v->gun_env_decay_mult;
750 }
751
752 // === Layer 2: BOOM — pitched triangle with exponential pitch drop ===
753 double boom = 0.0;
754 if (v->gun_boom_env > 0.00002 && v->gun_body_amp[1] > 0.0) {
755 // Geometric approach toward end freq. For typical 14ms tau at
756 // 192kHz, this glides 250→55 Hz audibly within ~50ms.
757 v->gun_boom_freq = v->gun_boom_freq_end
758 + (v->gun_boom_freq - v->gun_boom_freq_end) * v->gun_boom_pitch_mult;
759 double f = v->gun_boom_freq * v->gun_pitch_mult;
760 if (f < 1.0) f = 1.0;
761 v->gun_boom_phase += f / sr;
762 if (v->gun_boom_phase >= 1.0) v->gun_boom_phase -= 1.0;
763 if (v->gun_boom_phase < 0.0) v->gun_boom_phase += 1.0;
764 // Triangle wave — fatter low-end punch than sine, less harsh than square.
765 double tp = v->gun_boom_phase;
766 double s = (tp < 0.5) ? (4.0 * tp - 1.0) : (3.0 - 4.0 * tp);
767 boom = s * v->gun_boom_env * v->gun_body_amp[1];
768 v->gun_boom_env *= v->gun_boom_decay_mult;
769 }
770
771 // === Layer 3: TAIL — lowpass-filtered noise rumble ===
772 double tail = 0.0;
773 if (v->gun_body_amp[2] > 0.0) {
774 // Envelope: linear ramp during attack phase, then exp decay.
775 if (v->gun_tail_attack_inc > 0.0) {
776 v->gun_tail_env += v->gun_tail_attack_inc;
777 if (v->gun_tail_env >= 1.0) {
778 v->gun_tail_env = 1.0;
779 v->gun_tail_attack_inc = 0.0; // attack done; switch to decay
780 }
781 } else if (v->gun_tail_env > 0.00001) {
782 v->gun_tail_env *= v->gun_tail_decay_mult;
783 }
784 if (v->gun_tail_env > 0.00001) {
785 double white = ((double)xorshift32(&v->noise_seed) / (double)UINT32_MAX) * 2.0 - 1.0;
786 // 2-pole resonator at low freq, low Q ≈ 1-pole-ish lowpass behavior.
787 double y = v->gun_tail_b0 * white
788 + v->gun_body_a1[1] * v->gun_body_y1[1]
789 - v->gun_body_a2[1] * v->gun_body_y2[1];
790 v->gun_body_y2[1] = v->gun_body_y1[1];
791 v->gun_body_y1[1] = y;
792 tail = y * v->gun_tail_env * v->gun_body_amp[2];
793 }
794 }
795
796 // Click also retriggers on secondary/sustain events because gun_click_env
797 // gets reset in those branches via gun_pressure_env (no — actually it
798 // doesn't; we only reset crack/boom there). Fold a small click retrigger
799 // into the secondary path so reload+cock 2nd hits feel just as crisp.
800 double out = (click + crack + boom + tail) * v->gun_pressure;
801 return out * compute_envelope(v);
802}
803
804// Physical-model gunshot — Friedlander blast wave excitation feeding a
805// DWG bore + parallel body modes + muzzle radiation HPF + ground-echo
806// tap. Better for cavity-dominated weapons (grenade, RPG launch tube)
807// where the bore length is meaningful.
808//
809// The Friedlander waveform models the actual pressure-vs-time curve of
810// a free-air blast wave:
811// P(t) = P_peak · (1 − t/t+) · exp(−A·t/t+) for 0 ≤ t ≤ t+
812// P(t) ≈ −P_peak · neg_amp · (1−tn) · exp(−2·tn) for t > t+
813// (where tn = (t−t+) / (4·t+))
814// (Friedlander 1946; widely used in blast-wave acoustics — see Mengual
815// et al. 2017 "Procedural Synthesis of Gunshot Sounds…")
816//
817// The ground-echo tap (a single delayed copy of the radiated signal,
818// ~3-5 ms behind, attenuated) gives the spatial sense of an outdoor
819// shot — without it, the whole thing sounds anechoic and wrong.
820static inline double generate_gun_physical_sample(ACVoice *v, double sr) {
821 // === Excitation: Friedlander envelope shaping a noise burst ===
822 // Pure Friedlander pulses are too smooth between samples — the muzzle
823 // radiation HPF (1-zero differentiator at α≈0.985) annihilates anything
824 // that doesn't change between adjacent samples, killing the radiated
825 // path entirely. So we use Friedlander as the AMPLITUDE ENVELOPE of a
826 // noise burst (rather than the signal itself). The smooth shape gives
827 // us the right onset/decay character; the noise content gives the HPF
828 // and bore loop high-frequency material to actually radiate and ring.
829 double t = v->gun_phys_t;
830 double t_plus = v->gun_phys_t_plus;
831 double A = v->gun_phys_friedlander_a;
832 double pulse = 0.0;
833 if (t < t_plus) {
834 // Positive phase — sharp peak then exp decay.
835 double f = t / t_plus;
836 pulse = (1.0 - f) * exp(-A * f);
837 } else if (t < t_plus * 5.0) {
838 // Negative phase — sub-atmospheric dip after the wave passes.
839 double tn = (t - t_plus) / (t_plus * 4.0);
840 pulse = -v->gun_phys_neg_amp * (1.0 - tn) * exp(-2.0 * tn);
841 }
842 uint32_t n = xorshift32(&v->noise_seed);
843 double white = ((double)n / (double)UINT32_MAX) * 2.0 - 1.0;
844 // Smooth deterministic component + noise rider. noise_gain mixes the
845 // turbulent content. The smooth term keeps low-freq energy for the
846 // bore loop; the noise term feeds the radiation HPF + body modes.
847 double excite = v->gun_pressure * pulse * (1.0 + v->gun_noise_gain * white);
848 v->gun_phys_t += 1.0;
849
850 // Secondary trigger — rifle N-wave or RPG delayed explosion. Restarts
851 // the Friedlander pulse from t=0 with a scaled peak.
852 if (v->gun_secondary_trig > 0.0) {
853 v->gun_secondary_trig -= 1.0;
854 if (v->gun_secondary_trig <= 0.0) {
855 v->gun_phys_t = 0.0;
856 v->gun_pressure *= v->gun_secondary_amp;
857 v->gun_secondary_trig = 0.0;
858 }
859 }
860
861 // Sustain fire — restart pulse at jitter scale.
862 if (v->gun_sustain_fire && v->state == VOICE_ACTIVE
863 && isinf(v->duration) && v->gun_retrig_period > 0.0) {
864 v->gun_retrig_timer += 1.0 / sr;
865 if (v->gun_retrig_timer >= v->gun_retrig_period) {
866 v->gun_retrig_timer -= v->gun_retrig_period;
867 v->gun_phys_t = 0.0;
868 double j = (double)xorshift32(&v->noise_seed) / (double)UINT32_MAX;
869 // Tiny per-shot pressure variance around 1.0 (no permanent drift).
870 v->gun_pressure *= 0.92 + j * 0.16;
871 }
872 }
873
874 // Pitch sweep approach (ricochet — currently classic-only, kept for parity).
875 if (v->gun_pitch_mult != v->gun_pitch_target) {
876 v->gun_pitch_mult += (v->gun_pitch_target - v->gun_pitch_mult) * 0.00012;
877 }
878 double bore_delay = v->gun_bore_delay * v->gun_pitch_mult;
879 if (bore_delay < 4.0) bore_delay = 4.0;
880 if (bore_delay > 2040.0) bore_delay = 2040.0;
881
882 // === Bore: closed breech (+refl) / open muzzle (−refl + LPF damping) ===
883 const int BORE_N = 2048;
884 double bore_out = whistle_frac_read(v->whistle_bore_buf, BORE_N,
885 v->whistle_bore_w, bore_delay);
886 v->gun_bore_lp = v->gun_bore_loss * (-bore_out)
887 + (1.0 - v->gun_bore_loss) * v->gun_bore_lp;
888 double refl = v->gun_bore_lp;
889 double into_bore = excite + refl * v->gun_breech_reflect;
890 v->whistle_bore_buf[v->whistle_bore_w] = (float)into_bore;
891 v->whistle_bore_w = (v->whistle_bore_w + 1) % BORE_N;
892
893 // === Muzzle radiation: 1-zero HPF (open end emphasizes highs) ===
894 double radiated = into_bore - v->gun_radiation_a * v->gun_rad_prev;
895 v->gun_rad_prev = into_bore;
896
897 // === Body modes: parallel pole-pair resonators on the excitation ===
898 double body = 0.0;
899 for (int i = 0; i < 3; i++) {
900 double y = excite
901 + v->gun_body_a1[i] * v->gun_body_y1[i]
902 - v->gun_body_a2[i] * v->gun_body_y2[i];
903 v->gun_body_y2[i] = v->gun_body_y1[i];
904 v->gun_body_y1[i] = y;
905 body += y * v->gun_body_amp[i];
906 }
907
908 double dry = radiated * 0.55 + body * 0.45;
909
910 // === Ground reflection echo — short delayed copy. Even at low
911 // amplitude this turns the dry shot into a "fired outdoors" shot.
912 double echo_out = 0.0;
913 if (v->gun_phys_echo_amp > 0.0 && v->gun_phys_echo_delay > 1.0) {
914 const int ECHO_N = 1024;
915 int read_pos = v->gun_phys_echo_w - (int)v->gun_phys_echo_delay;
916 while (read_pos < 0) read_pos += ECHO_N;
917 echo_out = (double)v->gun_phys_echo_buf[read_pos % ECHO_N]
918 * v->gun_phys_echo_amp;
919 v->gun_phys_echo_buf[v->gun_phys_echo_w] = (float)dry;
920 v->gun_phys_echo_w = (v->gun_phys_echo_w + 1) % ECHO_N;
921 }
922
923 return (dry + echo_out) * compute_envelope(v);
924}
925
926static inline double generate_gun_sample(ACVoice *v, double sr) {
927 if (v->gun_model == GUN_MODEL_PHYSICAL) {
928 return generate_gun_physical_sample(v, sr);
929 }
930 return generate_gun_classic_sample(v, sr);
931}
932
933static inline double compute_fade(ACVoice *v) {
934 if (v->state != VOICE_KILLING) return 1.0;
935 if (v->fade_duration <= 0.0) return 0.0;
936 double progress = v->fade_elapsed / v->fade_duration;
937 if (progress >= 1.0) return 0.0;
938 return 1.0 - progress;
939}
940
941static inline double generate_sample(ACVoice *v, double sample_rate) {
942 double s;
943 switch (v->type) {
944 case WAVE_SINE:
945 s = sin(2.0 * M_PI * v->phase);
946 break;
947 case WAVE_SQUARE:
948 s = v->phase < 0.5 ? 1.0 : -1.0;
949 break;
950 case WAVE_TRIANGLE: {
951 // Offset phase by 0.25 to start at zero crossing (matches synth.mjs)
952 double tp = v->phase + 0.25;
953 if (tp >= 1.0) tp -= 1.0;
954 s = 4.0 * fabs(tp - 0.5) - 1.0;
955 break;
956 }
957 case WAVE_SAWTOOTH:
958 s = 2.0 * v->phase - 1.0;
959 break;
960 case WAVE_NOISE: {
961 // Filtered white noise using biquad LPF
962 double white = ((double)xorshift32(&v->noise_seed) / (double)UINT32_MAX) * 2.0 - 1.0;
963 double y = v->noise_b0 * white + v->noise_b1 * v->noise_x1 + v->noise_b2 * v->noise_x2
964 - v->noise_a1 * v->noise_y1 - v->noise_a2 * v->noise_y2;
965 v->noise_x2 = v->noise_x1;
966 v->noise_x1 = white;
967 v->noise_y2 = v->noise_y1;
968 v->noise_y1 = y;
969 s = y;
970 break;
971 }
972 case WAVE_WHISTLE:
973 s = generate_whistle_sample(v, sample_rate);
974 break;
975 case WAVE_GUN:
976 s = generate_gun_sample(v, sample_rate);
977 break;
978 default:
979 s = 0.0;
980 }
981
982 // Smooth frequency toward target (uses precomputed alpha from caller)
983 if (v->target_frequency > 0 && v->frequency != v->target_frequency) {
984 v->frequency += (v->target_frequency - v->frequency) * 0.0003; // ~5ms at 192kHz
985 }
986
987 // Advance phase for basic oscillators; whistle/gun use their own DWG state.
988 if (v->type != WAVE_WHISTLE && v->type != WAVE_GUN) {
989 v->phase += v->frequency / sample_rate;
990 if (v->phase >= 1.0) v->phase -= 1.0;
991 }
992
993 return s;
994}
995
996// Setup biquad LPF coefficients for noise voice
997static void setup_noise_filter(ACVoice *v, double sample_rate) {
998 double cutoff = v->frequency;
999 if (cutoff < 20.0) cutoff = 20.0;
1000 if (cutoff > sample_rate / 2.0) cutoff = sample_rate / 2.0;
1001
1002 double Q = 1.0;
1003 double w0 = 2.0 * M_PI * cutoff / sample_rate;
1004 double alpha = sin(w0) / (2.0 * Q);
1005
1006 double b0 = (1.0 - cos(w0)) / 2.0;
1007 double b1 = 1.0 - cos(w0);
1008 double b2 = (1.0 - cos(w0)) / 2.0;
1009 double a0 = 1.0 + alpha;
1010 double a1 = -2.0 * cos(w0);
1011 double a2 = 1.0 - alpha;
1012
1013 v->noise_b0 = b0 / a0;
1014 v->noise_b1 = b1 / a0;
1015 v->noise_b2 = b2 / a0;
1016 v->noise_a1 = a1 / a0;
1017 v->noise_a2 = a2 / a0;
1018 v->noise_x1 = v->noise_x2 = v->noise_y1 = v->noise_y2 = 0.0;
1019}
1020
1021// ============================================================
1022// Audio thread
1023// ============================================================
1024
1025#define ROOM_DELAY_SAMPLES (int)(0.12 * AUDIO_SAMPLE_RATE) // 120ms
1026#define ROOM_SIZE (ROOM_DELAY_SAMPLES * 3)
1027#define ROOM_FEEDBACK 0.3
1028#define ROOM_MIX 0.35
1029
1030// Soft clamp (tanh-style) to prevent harsh digital clipping
1031// Smooth curve: starts compressing gently above 0.6, hard-limits at ~0.95
1032static inline double soft_clip(double x) {
1033 if (x > 0.6) {
1034 double over = x - 0.6;
1035 return 0.6 + 0.35 * (1.0 - 1.0 / (1.0 + over * 2.5));
1036 }
1037 if (x < -0.6) {
1038 double over = -x - 0.6;
1039 return -0.6 - 0.35 * (1.0 - 1.0 / (1.0 + over * 2.5));
1040 }
1041 return x;
1042}
1043
1044// Compressor state (per-channel peak follower)
1045static double comp_env = 0.0; // envelope follower level
1046static unsigned long xrun_count = 0;
1047static unsigned long short_write_count = 0;
1048
1049static void mix_sample_voice(SampleVoice *sv, const float *buf, int slen, int smax,
1050 double rate, double *mix_l, double *mix_r) {
1051 if (!sv || !sv->active || !buf || slen <= 0 || smax <= 0) {
1052 if (sv) sv->active = 0;
1053 return;
1054 }
1055
1056 if (slen > smax) slen = smax;
1057
1058 // Fade envelope (5ms attack/release at output rate)
1059 double fade_speed = 1.0 / (0.005 * rate);
1060 if (sv->fade < sv->fade_target) {
1061 sv->fade += fade_speed;
1062 if (sv->fade > sv->fade_target) sv->fade = sv->fade_target;
1063 } else if (sv->fade > sv->fade_target) {
1064 sv->fade -= fade_speed;
1065 if (sv->fade <= 0.0) { sv->fade = 0.0; sv->active = 0; return; }
1066 }
1067
1068 // Pan controls both amplitude and a small Haas-style stereo offset.
1069 double delay_samps = sv->pan * 0.0004 * rate;
1070 double pos_l = sv->position - (delay_samps > 0 ? delay_samps : 0);
1071 double pos_r = sv->position + (delay_samps > 0 ? 0 : delay_samps);
1072 if (pos_l < 0) pos_l = 0;
1073 if (pos_r < 0) pos_r = 0;
1074
1075 int p0l = (int)pos_l;
1076 if (sv->loop) {
1077 p0l = ((p0l % slen) + slen) % slen;
1078 } else if (p0l >= slen) {
1079 sv->active = 0;
1080 return;
1081 }
1082 int p1l = p0l + 1;
1083 if (p1l >= slen) p1l = sv->loop ? 0 : p0l;
1084 if (p0l >= smax || p1l >= smax) { sv->active = 0; return; }
1085 double fl = pos_l - p0l;
1086 double samp_l = buf[p0l] * (1.0 - fl) + buf[p1l] * fl;
1087
1088 int p0r = (int)pos_r;
1089 if (sv->loop) {
1090 p0r = ((p0r % slen) + slen) % slen;
1091 } else if (p0r >= slen) {
1092 p0r = slen - 1;
1093 }
1094 if (p0r < 0) p0r = 0;
1095 int p1r = p0r + 1;
1096 if (p1r >= slen) p1r = sv->loop ? 0 : p0r;
1097 if (p0r >= smax || p1r >= smax) { sv->active = 0; return; }
1098 double fr = pos_r - p0r;
1099 double samp_r = buf[p0r] * (1.0 - fr) + buf[p1r] * fr;
1100
1101 double vol = sv->volume * sv->fade;
1102 double l_gain = sv->pan <= 0 ? 1.0 : 1.0 - sv->pan * 0.6;
1103 double r_gain = sv->pan >= 0 ? 1.0 : 1.0 + sv->pan * 0.6;
1104 *mix_l += samp_l * vol * l_gain;
1105 *mix_r += samp_r * vol * r_gain;
1106
1107 sv->position += sv->speed;
1108 if (sv->position >= slen) {
1109 if (sv->loop) {
1110 while (sv->position >= slen) sv->position -= slen;
1111 } else {
1112 sv->active = 0;
1113 }
1114 } else if (sv->position < 0.0) {
1115 if (sv->loop) {
1116 while (sv->position < 0.0) sv->position += slen;
1117 } else {
1118 sv->active = 0;
1119 }
1120 }
1121}
1122
1123static void *audio_thread_fn(void *arg) {
1124 ACAudio *audio = (ACAudio *)arg;
1125 const unsigned int period_frames = audio->actual_period ? audio->actual_period : AUDIO_PERIOD_SIZE;
1126 int16_t *buffer = calloc(period_frames * AUDIO_CHANNELS, sizeof(int16_t));
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 }
1133 const double rate = (double)(audio->actual_rate ? audio->actual_rate : AUDIO_SAMPLE_RATE);
1134 const double dt = 1.0 / rate;
1135 double mix_divisor = 1.0; // Smooth auto-mix (matches speaker.mjs)
1136 // Auto-mix smoothing: fast-ish attack, slower release to avoid zipper clicks.
1137 const double mix_att_coeff = 1.0 - exp(-1.0 / (0.004 * rate)); // ~4ms
1138 const double mix_rel_coeff = 1.0 - exp(-1.0 / (0.060 * rate)); // ~60ms
1139
1140 // Drum bus peak compressor — gives percussion proper "stacking" feel.
1141 // The drum bus sums additively (no auto-mix divide) so rapid hits
1142 // would otherwise saturate through soft_clip tanh, flattening peaks
1143 // and making each new hit sound QUIETER. A real peak compressor
1144 // with fast attack / slower release keeps the drum bus below ~0.95
1145 // so transients retain impact AND the compressor recovers between
1146 // hits so each kick/snare feels punchy on its own.
1147 double drum_gain = 1.0;
1148 const double DRUM_THRESH = 0.95;
1149 // 5ms attack — slower than a 2ms beater transient so the first peak
1150 // of each hit passes through at full amplitude before compression
1151 // engages. This preserves the "snap" of each individual kick/snare.
1152 const double drum_att_coeff = 1.0 - exp(-1.0 / (0.005 * rate));
1153 // 200ms release — recovers quickly enough that successive hits at
1154 // typical tempos (120-200 BPM, 300-500ms between hits) each get
1155 // the benefit of full dynamic range.
1156 const double drum_rel_coeff = 1.0 - exp(-1.0 / (0.200 * rate));
1157
1158 // Set real-time priority to prevent audio glitches from background tasks
1159 struct sched_param sp = { .sched_priority = 50 };
1160 if (pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp) != 0)
1161 fprintf(stderr, "[audio] Warning: couldn't set RT priority\n");
1162
1163 while (audio->running) {
1164 memset(buffer, 0, sizeof(buffer));
1165
1166 pthread_mutex_lock(&audio->lock);
1167
1168 for (unsigned int i = 0; i < period_frames; i++) {
1169 // Split the voice bus in two: TONES get auto-mix normalization
1170 // (divide by total voice weight so held chords stay balanced),
1171 // DRUMS stack additively (so a kick+snare+hat transient sums to
1172 // a louder peak instead of ducking itself). soft_clip at the end
1173 // catches any drum peak excess with tanh saturation — which
1174 // gives percussion a natural analog "push" character.
1175 //
1176 // Heuristic: a voice is percussive if it has a SHORT FINITE
1177 // duration (< 0.5s). Held tones (duration = Infinity) and
1178 // long one-shot tones always go through the auto-mix bus.
1179 double tone_l = 0.0, tone_r = 0.0;
1180 double drum_l = 0.0, drum_r = 0.0;
1181 double voice_sum = 0.0; // Tone-only voice weight for auto-mix
1182
1183 for (int v = 0; v < AUDIO_MAX_VOICES; v++) {
1184 ACVoice *voice = &audio->voices[v];
1185 if (voice->state == VOICE_INACTIVE) continue;
1186
1187 double s = generate_sample(voice, rate);
1188 double env = compute_envelope(voice);
1189 double fade = compute_fade(voice);
1190 double amp = s * env * fade * voice->volume;
1191
1192 double left_gain = (1.0 - voice->pan) * 0.5;
1193 double right_gain = (1.0 + voice->pan) * 0.5;
1194
1195 int is_percussive = !isinf(voice->duration) && voice->duration < 0.5;
1196 if (is_percussive) {
1197 // Drum bus — no auto-mix normalization. Drums stack
1198 // additively and rely on soft_clip for peak control.
1199 drum_l += amp * left_gain;
1200 drum_r += amp * right_gain;
1201 } else {
1202 // Tone bus — contributes to voice_sum for auto-mix.
1203 tone_l += amp * left_gain;
1204 tone_r += amp * right_gain;
1205 if (voice->state == VOICE_KILLING) {
1206 voice_sum += voice->volume * (1.0 - voice->fade_elapsed / voice->fade_duration);
1207 } else {
1208 voice_sum += voice->volume;
1209 }
1210 }
1211
1212 voice->elapsed += dt;
1213 if (voice->state == VOICE_KILLING) {
1214 voice->fade_elapsed += dt;
1215 if (voice->fade_elapsed >= voice->fade_duration)
1216 voice->state = VOICE_INACTIVE;
1217 } else if (!isinf(voice->duration) && voice->elapsed >= voice->duration) {
1218 voice->state = VOICE_INACTIVE;
1219 }
1220 }
1221
1222 // Smooth auto-mix divisor — fast attack, slow release.
1223 // Applied ONLY to the tone bus. Drums bypass it entirely.
1224 double target = voice_sum > 1.0 ? voice_sum : 1.0;
1225 if (mix_divisor < target)
1226 mix_divisor += (target - mix_divisor) * mix_att_coeff;
1227 else if (mix_divisor > target)
1228 mix_divisor += (target - mix_divisor) * mix_rel_coeff;
1229 if (mix_divisor < 1.0) mix_divisor = 1.0;
1230
1231 tone_l /= mix_divisor;
1232 tone_r /= mix_divisor;
1233
1234 // Drum bus peak compressor: detect peak, attack fast if over
1235 // threshold, release slow. Unlike the tone auto-mix divide,
1236 // this preserves individual hit dynamics — a single drum hit
1237 // passes through at full amplitude, but sustained buildup
1238 // from overlapping hits gets gain-reduced gracefully so they
1239 // stack linearly instead of saturating through soft_clip.
1240 {
1241 double peak = fabs(drum_l);
1242 double peak_r = fabs(drum_r);
1243 if (peak_r > peak) peak = peak_r;
1244 double target = (peak > DRUM_THRESH) ? (DRUM_THRESH / peak) : 1.0;
1245 if (target < drum_gain) {
1246 drum_gain += (target - drum_gain) * drum_att_coeff;
1247 } else {
1248 drum_gain += (target - drum_gain) * drum_rel_coeff;
1249 }
1250 drum_l *= drum_gain;
1251 drum_r *= drum_gain;
1252 }
1253
1254 // Merge the two buses. Drums land compressed to ~0.95 peak
1255 // so they retain impact without saturating the final output.
1256 double mix_l = tone_l + drum_l;
1257 double mix_r = tone_r + drum_r;
1258
1259 // Mix sample voices (pitch-shifted playback)
1260 // Lock already held from line 246 — safe to read sample_buf
1261 for (int v = 0; v < AUDIO_MAX_SAMPLE_VOICES; v++) {
1262 SampleVoice *sv = &audio->sample_voices[v];
1263 mix_sample_voice(sv, audio->sample_buf, audio->sample_len, audio->sample_max_len,
1264 rate, &mix_l, &mix_r);
1265 }
1266
1267 // Dedicated global replay voice. Uses its own buffer so reverse
1268 // playback can overlap normal sample-bank activity.
1269 mix_sample_voice(&audio->replay_voice, audio->replay_buf,
1270 audio->replay_len, audio->replay_max_len,
1271 rate, &mix_l, &mix_r);
1272 // (lock released at end of buffer loop)
1273
1274 // Mix DJ deck audio (lock-free: single consumer = audio thread)
1275 // Speed control: advance ring read by `speed` samples per output sample
1276 // with linear interpolation for smooth pitch shifting / scratching.
1277 for (int d = 0; d < AUDIO_MAX_DECKS; d++) {
1278 ACDeck *dk = &audio->decks[d];
1279 if (!dk->active || !dk->playing || !dk->decoder) continue;
1280 ACDeckDecoder *dec = dk->decoder;
1281 double spd = dec->speed;
1282 if (spd < -4.0) spd = -4.0;
1283 if (spd > 4.0) spd = 4.0;
1284 int64_t avail = dec->ring_write - dec->ring_read;
1285 if (avail <= 1) continue;
1286 // Fractional ring position for interpolation
1287 double frac_pos = dec->ring_frac;
1288 int64_t base = dec->ring_read;
1289 int64_t idx0 = base + (int64_t)frac_pos;
1290 if (idx0 < base || idx0 + 1 >= dec->ring_write) {
1291 // Not enough data — skip
1292 continue;
1293 }
1294 double t = frac_pos - (int64_t)frac_pos;
1295 int ri0 = (idx0 % dec->ring_size) * 2;
1296 int ri1 = ((idx0 + 1) % dec->ring_size) * 2;
1297 float sl = dec->ring[ri0] * (1.0f - (float)t) + dec->ring[ri1] * (float)t;
1298 float sr = dec->ring[ri0 + 1] * (1.0f - (float)t) + dec->ring[ri1 + 1] * (float)t;
1299 // Advance fractional position by speed
1300 dec->ring_frac += spd;
1301 // Consume whole samples from ring
1302 int consumed = (int)dec->ring_frac;
1303 if (consumed > 0) {
1304 dec->ring_read += consumed;
1305 dec->ring_frac -= consumed;
1306 } else if (consumed < 0) {
1307 // Reverse: clamp to not go before ring_read
1308 // (reverse scratching won't replay old audio, just stops)
1309 dec->ring_frac = 0;
1310 }
1311 // Crossfader: 0.0 = full deck A, 1.0 = full deck B
1312 float cf = (d == 0)
1313 ? (1.0f - audio->crossfader)
1314 : audio->crossfader;
1315 float vol = dk->volume * cf * audio->deck_master_volume;
1316 mix_l += sl * vol;
1317 mix_r += sr * vol;
1318 // Wake decoder thread if ring drained below 50%
1319 if ((dec->ring_write - dec->ring_read) < dec->ring_size / 2) {
1320 pthread_mutex_lock(&dec->mutex);
1321 pthread_cond_signal(&dec->cond);
1322 pthread_mutex_unlock(&dec->mutex);
1323 }
1324 }
1325
1326 // Smooth room_mix toward target (~10ms at 192kHz)
1327 if (audio->room_mix != audio->target_room_mix) {
1328 audio->room_mix += (audio->target_room_mix - audio->room_mix) * 0.00005f;
1329 }
1330
1331 // Smooth fx_mix toward target
1332 if (audio->fx_mix != audio->target_fx_mix) {
1333 audio->fx_mix += (audio->target_fx_mix - audio->fx_mix) * 0.00005f;
1334 }
1335
1336 // Smooth bitcrush mix toward target
1337 if (audio->glitch_mix != audio->target_glitch_mix) {
1338 audio->glitch_mix += (audio->target_glitch_mix - audio->glitch_mix) * 0.00005f;
1339 }
1340
1341 // Save dry signal before FX chain
1342 double dry_l = mix_l, dry_r = mix_r;
1343
1344 // Capture recent dry output for true reverse replay. This stores
1345 // the actual mixed audio (not note events) before room/glitch/TTS
1346 // so the reverse replay can run back through the live FX chain.
1347 if (audio->output_history_buf && audio->output_history_size > 0) {
1348 unsigned int stride = audio->output_history_downsample_n;
1349 if (stride == 0) stride = 1;
1350 audio->output_history_downsample_pos++;
1351 if (audio->output_history_downsample_pos >= stride) {
1352 audio->output_history_downsample_pos = 0;
1353 uint64_t wp = audio->output_history_write_pos;
1354 audio->output_history_buf[wp % (uint64_t)audio->output_history_size] =
1355 (float)((dry_l + dry_r) * 0.5);
1356 audio->output_history_write_pos = wp + 1;
1357 }
1358 }
1359
1360 // Room (reverb) effect — tap delays based on actual sample rate
1361 if (audio->room_enabled && audio->room_buf_l) {
1362 float rmix = audio->room_mix;
1363 int rs = audio->room_size;
1364
1365 // At 0% mix, skip all reverb processing (no buffer feed, no output)
1366 if (rmix > 0.001f) {
1367 int room_delay = (int)(0.12 * rate); // 120ms in samples
1368 int tap1 = (audio->room_pos - room_delay + rs) % rs;
1369 int tap2 = (audio->room_pos - room_delay * 2 + rs) % rs;
1370 int tap3 = (audio->room_pos - room_delay * 3 + rs) % rs;
1371
1372 // Weighted sum of taps, normalized
1373 float wet_l = (audio->room_buf_l[tap1] * 0.5f
1374 + audio->room_buf_l[tap2] * 0.3f
1375 + audio->room_buf_l[tap3] * 0.2f);
1376 float wet_r = (audio->room_buf_r[tap1] * 0.5f
1377 + audio->room_buf_r[tap2] * 0.3f
1378 + audio->room_buf_r[tap3] * 0.2f);
1379
1380 // Feed buffer: dry input + attenuated wet feedback
1381 float fb_l = (float)mix_l + wet_l * ROOM_FEEDBACK;
1382 float fb_r = (float)mix_r + wet_r * ROOM_FEEDBACK;
1383 // Damping — ensures reverb tail always decays
1384 fb_l *= 0.995f;
1385 fb_r *= 0.995f;
1386 // Soft-limit feedback to avoid hard-clamp discontinuities under transients.
1387 fb_l = tanhf(fb_l * 0.65f) / 0.65f;
1388 fb_r = tanhf(fb_r * 0.65f) / 0.65f;
1389 audio->room_buf_l[audio->room_pos] = fb_l;
1390 audio->room_buf_r[audio->room_pos] = fb_r;
1391
1392 // Mix wet into output
1393 mix_l = mix_l * (1.0 - rmix) + wet_l * rmix;
1394 mix_r = mix_r * (1.0 - rmix) + wet_r * rmix;
1395 } else {
1396 // Mix is ~0%: just clear the current buffer position (drain residue)
1397 audio->room_buf_l[audio->room_pos] = 0.0f;
1398 audio->room_buf_r[audio->room_pos] = 0.0f;
1399 }
1400 audio->room_pos = (audio->room_pos + 1) % rs;
1401 }
1402
1403 // Glitch (sample-hold + bitcrush)
1404 // `glitch_mix` scales the intensity of the stage itself, while
1405 // `fx_mix` still controls the dry/wet blend of the whole FX chain.
1406 {
1407 float gmix = audio->glitch_mix;
1408 if (gmix > 0.001f) {
1409 float crush = gmix * gmix;
1410 int hold_interval = 1 + (int)roundf((float)(audio->glitch_rate - 1) * crush);
1411 int bits = 12 - (int)roundf(gmix * 8.0f); // 12-bit -> 4-bit
1412 if (bits < 4) bits = 4;
1413 if (bits > 12) bits = 12;
1414 int levels = 1 << bits;
1415
1416 audio->glitch_counter++;
1417 if (audio->glitch_counter >= hold_interval) {
1418 audio->glitch_counter = 0;
1419 audio->glitch_hold_l = roundf((float)mix_l * levels) / levels;
1420 audio->glitch_hold_r = roundf((float)mix_r * levels) / levels;
1421 }
1422
1423 mix_l = mix_l * (1.0f - gmix) + audio->glitch_hold_l * gmix;
1424 mix_r = mix_r * (1.0f - gmix) + audio->glitch_hold_r * gmix;
1425 }
1426 }
1427
1428 // Blend dry/wet based on FX mix
1429 {
1430 float fxm = audio->fx_mix;
1431 if (fxm < 0.999f) {
1432 mix_l = dry_l * (1.0 - fxm) + mix_l * fxm;
1433 mix_r = dry_r * (1.0 - fxm) + mix_r * fxm;
1434 }
1435 }
1436
1437 // Mix in TTS audio after FX chain (bypasses reverb/glitch)
1438 // Fade envelope prevents hard-start/stop clicks
1439 {
1440 int tts_has_data = audio->tts_buf &&
1441 (audio->tts_read_pos != audio->tts_write_pos);
1442 // ~3ms ramp at 192kHz (1/576 per sample)
1443 float ramp = 1.0f / 576.0f;
1444 if (tts_has_data) {
1445 audio->tts_fade += ramp;
1446 if (audio->tts_fade > 1.0f) audio->tts_fade = 1.0f;
1447 float tts_sample = audio->tts_buf[audio->tts_read_pos]
1448 * audio->tts_volume * audio->tts_fade;
1449 mix_l += tts_sample;
1450 mix_r += tts_sample;
1451 audio->tts_read_pos = (audio->tts_read_pos + 1) % audio->tts_buf_size;
1452 } else {
1453 // Fade out: keep adding the last scaled zero-ish sample
1454 if (audio->tts_fade > 0.0f) {
1455 audio->tts_fade -= ramp;
1456 if (audio->tts_fade < 0.0f) audio->tts_fade = 0.0f;
1457 }
1458 }
1459 }
1460
1461 // Compressor: peak-following gain reduction (threshold 0.4, ratio ~8:1)
1462 {
1463 double peak = fabs(mix_l);
1464 double pr = fabs(mix_r);
1465 if (pr > peak) peak = pr;
1466 // Attack: very fast (0.2ms), Release: medium (40ms)
1467 double att_coeff = 1.0 - exp(-1.0 / (0.0002 * rate));
1468 double rel_coeff = 1.0 - exp(-1.0 / (0.04 * rate));
1469 if (peak > comp_env)
1470 comp_env += att_coeff * (peak - comp_env);
1471 else
1472 comp_env += rel_coeff * (peak - comp_env);
1473 if (comp_env > 0.4) {
1474 double gain = 0.4 + (comp_env - 0.4) * 0.125; // ~8:1 ratio above threshold
1475 double reduction = gain / comp_env;
1476 mix_l *= reduction;
1477 mix_r *= reduction;
1478 }
1479 }
1480
1481 // Apply system volume (software gain — hardware mixer may not attenuate)
1482 {
1483 int sv = audio->system_volume;
1484 // -1 means no Master mixer found (SOF cards) — treat as 100%
1485 if (sv < 0) sv = 100;
1486 double vol = sv * 0.01; // 0-100 → 0.0-1.0
1487 // Use squared curve for more natural volume perception
1488 vol = vol * vol;
1489 mix_l *= vol;
1490 mix_r *= vol;
1491 }
1492
1493 // Soft clip and convert to int16
1494 mix_l = soft_clip(mix_l);
1495 mix_r = soft_clip(mix_r);
1496
1497 buffer[i * 2] = (int16_t)(mix_l * 26000);
1498 buffer[i * 2 + 1] = (int16_t)(mix_r * 26000);
1499
1500 /* DAPM keepalive: inject ±1 LSB when the buffer would
1501 * otherwise be all zeros. At S32_LE (after <<16) this
1502 * becomes ±65536 ≈ -90 dBFS — utterly inaudible.
1503 * Previous ±160 was audible as 24kHz fizz through the
1504 * amp. ±1 is enough to prevent the SOF DSP silence
1505 * detector from powering down the SSP1 BE DAI. */
1506 if (buffer[i * 2] == 0 && buffer[i * 2 + 1] == 0) {
1507 buffer[i * 2] = (i & 1) ? 1 : -1;
1508 buffer[i * 2 + 1] = (i & 1) ? -1 : 1;
1509 }
1510
1511 // HDMI audio: 1-pole low-pass filter + downsample
1512 // (volume already applied above to mix_l/mix_r)
1513 if (audio->hdmi_pcm) {
1514 // LP filter (alpha ≈ 0.18 → ~3kHz cutoff at 48kHz)
1515 float alpha = 0.18f;
1516 audio->hdmi_lp_l = alpha * (float)mix_l + (1.0f - alpha) * audio->hdmi_lp_l;
1517 audio->hdmi_lp_r = alpha * (float)mix_r + (1.0f - alpha) * audio->hdmi_lp_r;
1518 // Downsample: one HDMI sample per N primary samples
1519 audio->hdmi_downsample_pos++;
1520 if (audio->hdmi_downsample_pos >= audio->hdmi_downsample_n) {
1521 audio->hdmi_downsample_pos = 0;
1522 int pp = audio->hdmi_period_pos;
1523 if (pp + 1 < (int)(sizeof(audio->hdmi_period) / sizeof(int16_t)) / 2) {
1524 audio->hdmi_period[pp * 2] = (int16_t)(audio->hdmi_lp_l * 28000);
1525 audio->hdmi_period[pp * 2 + 1] = (int16_t)(audio->hdmi_lp_r * 28000);
1526 audio->hdmi_period_pos++;
1527 if (audio->hdmi_period_pos >= audio->hdmi_period_size) {
1528 snd_pcm_t *hpcm = (snd_pcm_t *)audio->hdmi_pcm;
1529 int hw = snd_pcm_writei(hpcm, audio->hdmi_period,
1530 audio->hdmi_period_size);
1531 if (hw == -EPIPE || hw == -ESTRPIPE)
1532 snd_pcm_recover(hpcm, hw, 1);
1533 audio->hdmi_period_pos = 0;
1534 }
1535 }
1536 }
1537 }
1538
1539 // Store waveform for visualization
1540 int wp = audio->waveform_pos;
1541 audio->waveform_left[wp] = (float)mix_l;
1542 audio->waveform_right[wp] = (float)mix_r;
1543 audio->waveform_pos = (wp + 1) % AUDIO_WAVEFORM_SIZE;
1544
1545 // Track amplitude
1546 float al = fabsf((float)mix_l);
1547 float ar = fabsf((float)mix_r);
1548 audio->amplitude_left = audio->amplitude_left * 0.99f + al * 0.01f;
1549 audio->amplitude_right = audio->amplitude_right * 0.99f + ar * 0.01f;
1550 }
1551
1552 // BPM metronome
1553 audio->beat_elapsed += (double)period_frames * dt;
1554 double beat_interval = 60.0 / audio->bpm;
1555 if (audio->beat_elapsed >= beat_interval) {
1556 audio->beat_elapsed -= beat_interval;
1557 audio->beat_triggered = 1;
1558 }
1559
1560 pthread_mutex_unlock(&audio->lock);
1561
1562 audio->total_frames += period_frames;
1563 audio->time = (double)audio->total_frames / rate;
1564
1565 // Recording tap: send mixed PCM to recorder (if active). Used by
1566 // the MP4 tape recorder (recorder.c) for the audio track.
1567 if (audio->rec_callback)
1568 audio->rec_callback(buffer, period_frames, audio->rec_userdata);
1569
1570 // Write to ALSA (handle short writes to avoid dropped samples/clicks)
1571 snd_pcm_t *pcm = (snd_pcm_t *)audio->pcm;
1572 /* Tee to parallel PCM (sof-rt5682+max98360a auto-route).
1573 * Best-effort, never blocks the primary write — short writes,
1574 * EPIPE underruns, and even outright failures are tolerated
1575 * because the *real* output is the primary PCM. The DAPM
1576 * jack-sense in the codec mutes whichever side isn't being
1577 * driven by the active jack state. */
1578 snd_pcm_t *pcm2 = (snd_pcm_t *)audio->headphone_pcm;
1579 if (pcm2) {
1580 int rem2 = (int)period_frames;
1581 int off2 = 0;
1582 while (rem2 > 0) {
1583 int f2 = snd_pcm_writei(pcm2,
1584 buffer + off2 * AUDIO_CHANNELS,
1585 rem2);
1586 if (f2 == -EAGAIN) break; /* don't spin on secondary */
1587 if (f2 < 0) {
1588 snd_pcm_recover(pcm2, f2, 1);
1589 break;
1590 }
1591 rem2 -= f2; off2 += f2;
1592 }
1593 }
1594 /* Widen int16→int32 for S32_LE PCMs (SOF topology).
1595 * The SSP1 BE DAI runs S24_LE. SOF DSP uses the bottom 24
1596 * bits of the S32 container (bits 23:0). Shifting int16 by
1597 * 8 places our 16-bit audio in bits 23:8, which fills the
1598 * top portion of the 24-bit window — correct for S24-in-S32
1599 * bottom-aligned format. (<<16 put data in bits 31:16 which
1600 * the DSP's 24-bit window barely saw → super quiet.) */
1601 const void *write_buf = buffer;
1602 if (buffer32) {
1603 for (int j = 0; j < (int)(period_frames * AUDIO_CHANNELS); j++)
1604 buffer32[j] = (int32_t)buffer[j] << 8;
1605 write_buf = buffer32;
1606 }
1607 int remaining = (int)period_frames;
1608 int offset = 0;
1609 while (remaining > 0) {
1610 const void *wptr = buffer32
1611 ? (const void *)(buffer32 + offset * AUDIO_CHANNELS)
1612 : (const void *)(buffer + offset * AUDIO_CHANNELS);
1613 int frames = snd_pcm_writei(pcm, wptr, remaining);
1614 if (frames == -EAGAIN) continue;
1615 if (frames < 0) {
1616 int rec = snd_pcm_recover(pcm, frames, 1);
1617 if (frames == -EPIPE || frames == -ESTRPIPE) {
1618 xrun_count++;
1619 if ((xrun_count % 32) == 1) {
1620 fprintf(stderr, "[audio] XRUN recovered x%lu\n", xrun_count);
1621 }
1622 }
1623 if (rec < 0) {
1624 fprintf(stderr, "[audio] ALSA write failed: %s\n", snd_strerror(rec));
1625 break;
1626 }
1627 continue;
1628 }
1629 if (frames == 0) continue;
1630 if (frames < remaining) {
1631 short_write_count++;
1632 if ((short_write_count % 64) == 1) {
1633 fprintf(stderr, "[audio] Short write x%lu (%d/%d)\n",
1634 short_write_count, frames, remaining);
1635 }
1636 }
1637 remaining -= frames;
1638 offset += frames;
1639 }
1640 }
1641
1642 free(buffer);
1643 free(buffer32);
1644 return NULL;
1645}
1646
1647// ============================================================
1648// Public API
1649// ============================================================
1650
1651// Seed a small default sample so sample mode is playable before first mic recording.
1652// This roughly matches the "startup" one-shot feel used in web notepat.
1653static void seed_default_sample(ACAudio *audio) {
1654 if (!audio || !audio->sample_buf || audio->sample_max_len <= 0) return;
1655
1656 const unsigned int rate = 48000;
1657 int len = (int)(0.55 * (double)rate); // 550ms one-shot
1658 if (len > audio->sample_max_len) len = audio->sample_max_len;
1659
1660 double p1 = 0.0, p2 = 0.0, p3 = 0.0;
1661 for (int i = 0; i < len; i++) {
1662 double t = (double)i / (double)rate;
1663 double env = exp(-6.0 * t) * (1.0 - exp(-35.0 * t)); // fast attack, exponential decay
1664 double f0 = 240.0 + 50.0 * sin(t * 6.0); // slight wobble
1665 double f1 = f0 * 2.01;
1666 double f2 = f0 * 3.02;
1667 p1 += 2.0 * M_PI * f0 / (double)rate;
1668 p2 += 2.0 * M_PI * f1 / (double)rate;
1669 p3 += 2.0 * M_PI * f2 / (double)rate;
1670
1671 double s = 0.78 * sin(p1) + 0.22 * sin(p2 + 0.25) + 0.08 * sin(p3 + 0.15);
1672 audio->sample_buf[i] = (float)(s * env * 0.85);
1673 }
1674 for (int i = len; i < audio->sample_max_len; i++) audio->sample_buf[i] = 0.0f;
1675
1676 audio->sample_len = len;
1677 audio->sample_rate = rate;
1678 ac_log("[sample] seeded default startup sample (%d frames @ %u Hz)\n",
1679 audio->sample_len, audio->sample_rate);
1680}
1681
1682ACAudio *audio_init(void) {
1683 ACAudio *audio = calloc(1, sizeof(ACAudio));
1684 if (!audio) return NULL;
1685
1686 audio->bpm = 120.0;
1687 audio->actual_rate = AUDIO_SAMPLE_RATE; // default, overwritten after ALSA negotiation
1688 audio->glitch_rate = AUDIO_SAMPLE_RATE / 1600;
1689 pthread_mutex_init(&audio->lock, NULL);
1690
1691 // Allocate reverb buffers
1692 audio->room_size = ROOM_SIZE;
1693 audio->room_mix = 0.0f; // Start dry, trackpad Y controls
1694 audio->room_enabled = 1; // Always on, mix controls wet amount
1695 audio->glitch_mix = 0.0f;
1696 audio->target_glitch_mix = 0.0f;
1697 audio->fx_mix = 1.0f; // FX chain fully wet by default
1698 audio->target_fx_mix = 1.0f;
1699 audio->room_buf_l = calloc(ROOM_SIZE, sizeof(float));
1700 audio->room_buf_r = calloc(ROOM_SIZE, sizeof(float));
1701
1702 // Sample buffer (10 seconds at max 48kHz capture rate)
1703 audio->sample_max_len = 48000 * AUDIO_MAX_SAMPLE_SECS;
1704 audio->sample_buf = calloc(audio->sample_max_len, sizeof(float));
1705 audio->sample_buf_back = calloc(audio->sample_max_len, sizeof(float));
1706 audio->sample_len = 0;
1707 audio->sample_rate = 48000; // default, overwritten by actual capture rate
1708 audio->sample_next_id = 1;
1709 audio->replay_max_len = AUDIO_OUTPUT_HISTORY_RATE * AUDIO_OUTPUT_HISTORY_SECS;
1710 audio->replay_buf = calloc(audio->replay_max_len, sizeof(float));
1711 audio->replay_buf_back = calloc(audio->replay_max_len, sizeof(float));
1712 audio->replay_len = 0;
1713 audio->replay_rate = AUDIO_OUTPUT_HISTORY_RATE;
1714 memset(&audio->replay_voice, 0, sizeof(audio->replay_voice));
1715 audio->mic_connected = 0;
1716 audio->mic_hot = 0;
1717 audio->mic_level = 0.0f;
1718 audio->mic_last_chunk = 0;
1719 audio->capture_thread_running = 0;
1720 memset(audio->mic_waveform, 0, sizeof(audio->mic_waveform));
1721 audio->mic_waveform_pos = 0;
1722 audio->mic_ring = calloc(audio->sample_max_len, sizeof(float));
1723 audio->mic_ring_pos = 0;
1724 audio->rec_start_ring_pos = 0;
1725 audio->output_history_buf = calloc(AUDIO_OUTPUT_HISTORY_RATE * AUDIO_OUTPUT_HISTORY_SECS, sizeof(float));
1726 audio->output_history_size = AUDIO_OUTPUT_HISTORY_RATE * AUDIO_OUTPUT_HISTORY_SECS;
1727 audio->output_history_rate = AUDIO_OUTPUT_HISTORY_RATE;
1728 audio->output_history_downsample_n = 1;
1729 audio->output_history_downsample_pos = 0;
1730 audio->output_history_write_pos = 0;
1731 snprintf(audio->mic_device, sizeof(audio->mic_device), "none");
1732 audio->mic_last_error[0] = 0;
1733 seed_default_sample(audio);
1734
1735 // DJ decks: initialize with default volumes
1736 audio->crossfader = 0.5f; // centered
1737 audio->deck_master_volume = 0.8f; // default master
1738 for (int d = 0; d < AUDIO_MAX_DECKS; d++) {
1739 audio->decks[d].active = 0;
1740 audio->decks[d].playing = 0;
1741 audio->decks[d].volume = 1.0f;
1742 audio->decks[d].decoder = NULL;
1743 }
1744
1745 // TTS PCM ring buffer (5 seconds at max output rate)
1746 audio->tts_buf_size = AUDIO_SAMPLE_RATE * 5; // allocated at max, actual_rate adjusts usage
1747 audio->tts_buf = calloc(audio->tts_buf_size, sizeof(float));
1748 audio->tts_read_pos = 0;
1749 audio->tts_write_pos = 0;
1750 audio->tts_volume = 2.5f; // Boost flite output (naturally quiet)
1751
1752 snprintf(audio->audio_device, sizeof(audio->audio_device), "none");
1753 snprintf(audio->audio_status, sizeof(audio->audio_status), "initializing");
1754 audio->audio_init_retries = 0;
1755
1756 // Wait for sound card to appear
1757 fprintf(stderr, "[audio] Waiting for sound card...\n");
1758 int card_found = 0;
1759 for (int w = 0; w < 400; w++) { // up to 8 seconds
1760 if (access("/dev/snd/pcmC0D0p", F_OK) == 0 ||
1761 access("/dev/snd/pcmC1D0p", F_OK) == 0 ||
1762 access("/dev/snd/pcmC2D0p", F_OK) == 0) { card_found = 1; break; }
1763 usleep(20000);
1764 }
1765 if (!card_found) {
1766 // Distinguish: HDA controller present (codec probe failed) vs no hardware at all
1767 if (access("/dev/snd/controlC0", F_OK) == 0) {
1768 fprintf(stderr, "[audio] WARNING: HDA controller present but codec not probed after 8s\n");
1769 snprintf(audio->audio_status, sizeof(audio->audio_status), "HDA ctrl ok, codec not probed");
1770 } else {
1771 fprintf(stderr, "[audio] WARNING: no sound card after 8s wait\n");
1772 snprintf(audio->audio_status, sizeof(audio->audio_status), "no card (8s timeout)");
1773 }
1774 }
1775
1776 // Dump sound card info for diagnostics (write to USB log if mounted)
1777 FILE *alog = fopen("/mnt/ac-audio.log", "w");
1778 if (!alog) alog = stderr; // fallback to stderr
1779 {
1780 FILE *cards = fopen("/proc/asound/cards", "r");
1781 if (cards) {
1782 char line[256];
1783 fprintf(alog, "[audio] === /proc/asound/cards ===\n");
1784 while (fgets(line, sizeof(line), cards))
1785 fprintf(alog, "[audio] %s", line);
1786 fclose(cards);
1787 } else {
1788 fprintf(alog, "[audio] WARNING: /proc/asound/cards not found!\n");
1789 }
1790 // Also check /dev/snd/
1791 DIR *snddir = opendir("/dev/snd");
1792 if (snddir) {
1793 struct dirent *ent;
1794 fprintf(alog, "[audio] /dev/snd/:");
1795 while ((ent = readdir(snddir))) {
1796 if (ent->d_name[0] != '.') fprintf(alog, " %s", ent->d_name);
1797 }
1798 fprintf(alog, "\n");
1799 closedir(snddir);
1800 } else {
1801 fprintf(alog, "[audio] WARNING: /dev/snd/ not found!\n");
1802 }
1803 }
1804
1805 // Open ALSA — try multiple cards and devices, with retries for race conditions.
1806 // On fast NVMe boots the HDA codec may not be fully probed when we first try.
1807 // If AC_AUDIO_DEVICE is set, try it first (used for stream tee via asound.conf).
1808 snd_pcm_t *pcm = NULL;
1809
1810 // SOF rt5682+max98360a (G7/Drawcia and friends) splits playback across two
1811 // PCMs: SSP0 → RT5682 headphones (PCM 0) and SSP1 → MAX98360A speakers
1812 // (PCM 1). The historical hw:0,0 default routes everything to headphones,
1813 // which is why speakers stayed silent even with the Speaker UCM verb fully
1814 // applied. Ask UCM what PCM it maps Speaker(s) to and try that FIRST so
1815 // playback hits the speaker amp by default; the headphone PCM still wins
1816 // if/when AC_AUDIO_DEVICE overrides or jack-sense flips routing later.
1817 char ucm_speaker_pcm[64] = "";
1818 char ucm_headphone_pcm[64] = "";
1819 {
1820 char card_id[32] = "";
1821 int spk_card = 0; /* card index where Speaker UCM lives */
1822 for (int c = 0; c < 4 && !card_id[0]; c++) {
1823 char p[64]; snprintf(p, sizeof(p), "/proc/asound/card%d/id", c);
1824 FILE *fp = fopen(p, "r");
1825 if (fp) {
1826 if (fgets(card_id, sizeof(card_id), fp)) {
1827 char *nl = strchr(card_id, '\n'); if (nl) *nl = 0;
1828 }
1829 fclose(fp);
1830 if (card_id[0]) spk_card = c;
1831 }
1832 }
1833 if (card_id[0]) {
1834 const char *cands[] = { card_id, "sof-rt5682", "sof-cs42l42",
1835 "sof-nau8825", "sof-da7219", NULL };
1836 snd_use_case_mgr_t *uc = NULL;
1837 for (int i = 0; cands[i] && (!ucm_speaker_pcm[0] || !ucm_headphone_pcm[0]); i++) {
1838 if (snd_use_case_mgr_open(&uc, cands[i]) != 0) continue;
1839 if (snd_use_case_set(uc, "_verb", "HiFi") == 0) {
1840 const char *spk_names[] = { "Speaker", "Speakers", NULL };
1841 for (int s = 0; spk_names[s] && !ucm_speaker_pcm[0]; s++) {
1842 char id[64]; const char *val = NULL;
1843 snprintf(id, sizeof(id), "PlaybackPCM/%s", spk_names[s]);
1844 if (snd_use_case_get(uc, id, &val) == 0 && val) {
1845 /* UCM v2 returns strings like
1846 * "_ucm0002.hw:sofrt5682,0" — that is a
1847 * UCM-internal namespace tag, not a path
1848 * snd_pcm_open accepts. Strip the
1849 * "_ucmNNNN." prefix to get the underlying
1850 * "hw:CARD,DEV" form, then convert the card
1851 * id to a numeric index since snd_pcm_open
1852 * also rejects "hw:sofrt5682,0". */
1853 const char *clean = val;
1854 const char *dot = strchr(clean, '.');
1855 if (dot && strncmp(clean, "_ucm", 4) == 0)
1856 clean = dot + 1;
1857 const char *comma = strrchr(clean, ',');
1858 if (comma && strncmp(clean, "hw:", 3) == 0) {
1859 snprintf(ucm_speaker_pcm,
1860 sizeof(ucm_speaker_pcm),
1861 "hw:%d%s", spk_card, comma);
1862 } else {
1863 snprintf(ucm_speaker_pcm,
1864 sizeof(ucm_speaker_pcm),
1865 "%s", clean);
1866 }
1867 fprintf(stderr,
1868 "[audio] UCM Speaker PCM: raw=%s -> %s (%s/%s)\n",
1869 val, ucm_speaker_pcm, cands[i], spk_names[s]);
1870 free((void *)val);
1871 }
1872 }
1873 const char *hp_names[] = { "Headphone", "Headphones",
1874 "Headset", NULL };
1875 for (int s = 0; hp_names[s] && !ucm_headphone_pcm[0]; s++) {
1876 char id[64]; const char *val = NULL;
1877 snprintf(id, sizeof(id), "PlaybackPCM/%s", hp_names[s]);
1878 if (snd_use_case_get(uc, id, &val) == 0 && val) {
1879 const char *clean = val;
1880 const char *dot = strchr(clean, '.');
1881 if (dot && strncmp(clean, "_ucm", 4) == 0)
1882 clean = dot + 1;
1883 const char *comma = strrchr(clean, ',');
1884 if (comma && strncmp(clean, "hw:", 3) == 0) {
1885 snprintf(ucm_headphone_pcm,
1886 sizeof(ucm_headphone_pcm),
1887 "hw:%d%s", spk_card, comma);
1888 } else {
1889 snprintf(ucm_headphone_pcm,
1890 sizeof(ucm_headphone_pcm),
1891 "%s", clean);
1892 }
1893 fprintf(stderr,
1894 "[audio] UCM Headphone PCM: raw=%s -> %s (%s/%s)\n",
1895 val, ucm_headphone_pcm, cands[i], hp_names[s]);
1896 free((void *)val);
1897 }
1898 }
1899 }
1900 snd_use_case_mgr_close(uc);
1901 uc = NULL;
1902 }
1903 }
1904 }
1905
1906 /* Build the device probe list. If UCM gave us a Speaker PCM, prepend it
1907 * (and a `plug:` wrapped variant for rate negotiation safety). The legacy
1908 * fallback list still runs after, so non-SOF boards behave as before. */
1909 const char *devices_default[] = {
1910 "hw:0,0", "hw:1,0", "hw:0,1", "hw:1,1",
1911 "hw:0,2", "hw:0,3", "hw:1,2", "hw:1,3",
1912 "plughw:0,0", "plughw:1,0",
1913 "default", NULL
1914 };
1915 const char *devices_with_spk[16] = {0};
1916 const char **devices = devices_default;
1917 char ucm_speaker_plug[80] = "";
1918 if (ucm_speaker_pcm[0]) {
1919 snprintf(ucm_speaker_plug, sizeof(ucm_speaker_plug),
1920 "plughw%s", ucm_speaker_pcm + 2); /* hw:0,0 → plughw:0,0 */
1921 int n = 0;
1922 /* Prefer raw hw: first — SOF topology FE PCM runs at S32_LE
1923 * internally, and we now negotiate S32_LE directly so the DSP
1924 * does zero conversion. plughw: as fallback if hw: fails. */
1925 devices_with_spk[n++] = ucm_speaker_pcm;
1926 devices_with_spk[n++] = ucm_speaker_plug;
1927 for (int i = 0; devices_default[i] && n < 15; i++)
1928 devices_with_spk[n++] = devices_default[i];
1929 devices_with_spk[n] = NULL;
1930 devices = devices_with_spk;
1931 }
1932 int err = -1;
1933 int card_idx = 0;
1934
1935 // AC_AUDIO_DEVICE override — try the env var device before the hardcoded list.
1936 const char *env_dev = getenv("AC_AUDIO_DEVICE");
1937 if (env_dev && env_dev[0]) {
1938 err = snd_pcm_open(&pcm, env_dev, SND_PCM_STREAM_PLAYBACK, 0);
1939 if (err >= 0) {
1940 fprintf(stderr, "[audio] Opened AC_AUDIO_DEVICE=%s\n", env_dev);
1941 snprintf(audio->audio_device, sizeof(audio->audio_device), "%s", env_dev);
1942 if (sscanf(env_dev, "hw:%d", &card_idx) != 1 &&
1943 sscanf(env_dev, "plughw:%d", &card_idx) != 1)
1944 card_idx = 0;
1945 } else {
1946 fprintf(stderr, "[audio] AC_AUDIO_DEVICE=%s failed: %s — falling back\n",
1947 env_dev, snd_strerror(err));
1948 }
1949 }
1950
1951 for (int attempt = 0; attempt < 5 && err < 0; attempt++) {
1952 if (attempt > 0) {
1953 fprintf(alog, "[audio] Retry %d/4 — waiting 2s for codec probe...\n", attempt);
1954 fprintf(stderr, "[audio] Retry %d/4 — waiting 2s for codec probe...\n", attempt);
1955 usleep(2000000); // 2 seconds between retries
1956 }
1957 for (int i = 0; devices[i]; i++) {
1958 audio->audio_init_retries++;
1959 err = snd_pcm_open(&pcm, devices[i], SND_PCM_STREAM_PLAYBACK, 0);
1960 if (err >= 0) {
1961 fprintf(alog, "[audio] Opened ALSA device: %s (attempt %d)\n", devices[i], attempt);
1962 fprintf(stderr, "[audio] Opened ALSA device: %s (attempt %d)\n", devices[i], attempt);
1963 snprintf(audio->audio_device, sizeof(audio->audio_device), "%s", devices[i]);
1964 if (sscanf(devices[i], "hw:%d", &card_idx) != 1 &&
1965 sscanf(devices[i], "plughw:%d", &card_idx) != 1)
1966 card_idx = 0;
1967 break;
1968 }
1969 if (attempt == 0)
1970 fprintf(alog, "[audio] Failed %s: %s\n", devices[i], snd_strerror(err));
1971 }
1972 }
1973 audio->card_index = card_idx;
1974 if (alog != stderr) { fflush(alog); fclose(alog); }
1975 if (err < 0) {
1976 fprintf(stderr, "[audio] Cannot open any ALSA device after 5 attempts\n");
1977 snprintf(audio->audio_status, sizeof(audio->audio_status), "no ALSA device found");
1978 // Audio is optional — return the struct but with no PCM
1979 audio->pcm = NULL;
1980 return audio;
1981 }
1982
1983 // Configure ALSA — negotiate rate dynamically.
1984 // Try preferred rates from highest to lowest. The hardware decides what it
1985 // actually supports; we adapt period/buffer sizes to match the negotiated rate.
1986 snd_pcm_hw_params_t *params;
1987 snd_pcm_hw_params_alloca(¶ms);
1988 snd_pcm_hw_params_any(pcm, params);
1989 snd_pcm_hw_params_set_access(pcm, params, SND_PCM_ACCESS_RW_INTERLEAVED);
1990 /* SOF topology FE PCMs use S32_LE internally; the SSP1 BE DAI
1991 * (MAX98360A) runs S24_LE. Writing S16_LE to this pipeline
1992 * causes 48dB attenuation + quantization noise ("crunchy quiet").
1993 * Try S32_LE first — if the FE PCM accepts it, we write int32
1994 * samples and the DSP does zero conversion. Fall back to S16_LE
1995 * for non-SOF hardware (HDA, USB, etc). */
1996 audio->use_s32 = 0;
1997 if (snd_pcm_hw_params_set_format(pcm, params, SND_PCM_FORMAT_S32_LE) == 0) {
1998 audio->use_s32 = 1;
1999 fprintf(stderr, "[audio] Negotiated S32_LE format\n");
2000 } else {
2001 snd_pcm_hw_params_any(pcm, params);
2002 snd_pcm_hw_params_set_access(pcm, params, SND_PCM_ACCESS_RW_INTERLEAVED);
2003 snd_pcm_hw_params_set_format(pcm, params, SND_PCM_FORMAT_S16_LE);
2004 fprintf(stderr, "[audio] Negotiated S16_LE format (S32_LE not supported)\n");
2005 }
2006 snd_pcm_hw_params_set_channels(pcm, params, AUDIO_CHANNELS);
2007
2008 // Query hardware rate range
2009 unsigned int rate_min = 0, rate_max = 0;
2010 snd_pcm_hw_params_get_rate_min(params, &rate_min, NULL);
2011 snd_pcm_hw_params_get_rate_max(params, &rate_max, NULL);
2012 fprintf(stderr, "[audio] Hardware rate range: %u–%u Hz\n", rate_min, rate_max);
2013
2014 // Pick sample rate: 48kHz is the safe default that all hardware can sustain.
2015 // Many codecs (e.g. Cirrus Logic CS4206) claim 192kHz support but can't
2016 // sustain it without constant XRUNs. Only use high rates on known-good
2017 // hardware (ThinkPad HDA with Realtek codec handles 192kHz fine).
2018 // Heuristic: if max rate > 48kHz AND min rate <= 32kHz, the codec is
2019 // likely a laptop HDA that works better at 48kHz.
2020 unsigned int rate = 48000;
2021 if (rate_max >= 192000 && rate_min > 44100) {
2022 // Dedicated audio interface — likely supports high rates reliably
2023 rate = 192000;
2024 } else if (rate_max >= 96000 && rate_min > 44100) {
2025 rate = 96000;
2026 }
2027 // Override: environment variable AC_AUDIO_RATE forces a specific rate
2028 const char *env_rate = getenv("AC_AUDIO_RATE");
2029 if (env_rate) {
2030 unsigned int r = (unsigned int)atoi(env_rate);
2031 if (r >= rate_min && r <= rate_max) rate = r;
2032 }
2033 fprintf(stderr, "[audio] Selected rate: %u Hz (hw range %u–%u)\n", rate, rate_min, rate_max);
2034 snd_pcm_hw_params_set_rate_near(pcm, params, &rate, 0);
2035
2036 // Period + buffer sizing. The old config aimed for ~1ms latency (period =
2037 // rate/1000, buffer = 4 periods) which works on HDA-direct codecs but
2038 // breaks SOF+MAX98360A on Jasper Lake Chromebooks: the MAX98357A DAPM
2039 // event handler toggles the amp's SD_MODE GPIO on every PMU/PMD event,
2040 // and with a 4ms buffer the stream underruns constantly → DAPM rapid-
2041 // cycles the amp on/off → audio never stabilizes → speakers stay silent
2042 // despite mixer, codec, and GPIO all looking correct. We saw 10,686
2043 // sdmode toggles in a single boot's kmsg on the G7 at the 1ms setting.
2044 //
2045 // Probe whether we're on a SOF platform (sound/soc/sof is one path) and
2046 // bump to 10ms / 40ms which is what ChromeOS itself uses. Non-SOF HDA
2047 // devices (ThinkPads, most laptops) keep the tight latency because their
2048 // amp/codec model doesn't gate on per-period DAPM events.
2049 int sof_active = (access("/sys/class/sound/card0/id", R_OK) == 0) &&
2050 (access("/proc/asound/card0/codec97#0", F_OK) != 0);
2051 snd_pcm_uframes_t period;
2052 snd_pcm_uframes_t buffer_size;
2053 if (sof_active) {
2054 period = rate / 100; // 10ms (480 frames at 48kHz)
2055 buffer_size = period * 4; // 40ms total — SOF DAPM-friendly
2056 fprintf(stderr, "[audio] SOF platform detected — period=%lu buffer=%lu (10ms/40ms)\n",
2057 (unsigned long)period, (unsigned long)buffer_size);
2058 } else {
2059 period = rate / 1000; // 1ms on HDA-direct paths
2060 if (period < 64) period = 64;
2061 buffer_size = period * 4;
2062 }
2063 snd_pcm_hw_params_set_period_size_near(pcm, params, &period, 0);
2064 snd_pcm_hw_params_set_buffer_size_near(pcm, params, &buffer_size);
2065
2066 err = snd_pcm_hw_params(pcm, params);
2067 if (err < 0) {
2068 fprintf(stderr, "[audio] Cannot configure ALSA at %uHz: %s\n", rate, snd_strerror(err));
2069 // Last resort: try plughw with default params
2070 fprintf(stderr, "[audio] Trying plughw fallback...\n");
2071 snd_pcm_close(pcm);
2072 err = snd_pcm_open(&pcm, "plughw:0,0", SND_PCM_STREAM_PLAYBACK, 0);
2073 if (err >= 0) {
2074 snd_pcm_hw_params_any(pcm, params);
2075 snd_pcm_hw_params_set_access(pcm, params, SND_PCM_ACCESS_RW_INTERLEAVED);
2076 snd_pcm_hw_params_set_format(pcm, params, SND_PCM_FORMAT_S16_LE);
2077 snd_pcm_hw_params_set_channels(pcm, params, AUDIO_CHANNELS);
2078 rate = 48000;
2079 snd_pcm_hw_params_set_rate_near(pcm, params, &rate, 0);
2080 period = 256;
2081 snd_pcm_hw_params_set_period_size_near(pcm, params, &period, 0);
2082 buffer_size = 1024;
2083 snd_pcm_hw_params_set_buffer_size_near(pcm, params, &buffer_size);
2084 err = snd_pcm_hw_params(pcm, params);
2085 }
2086 if (err < 0) {
2087 fprintf(stderr, "[audio] All ALSA config attempts failed: %s\n", snd_strerror(err));
2088 snd_pcm_close(pcm);
2089 audio->pcm = NULL;
2090 return audio;
2091 }
2092 snprintf(audio->audio_device, sizeof(audio->audio_device), "plughw:0,0");
2093 }
2094
2095 snd_pcm_prepare(pcm);
2096 audio->pcm = pcm;
2097 audio->actual_rate = rate;
2098 audio->actual_period = (unsigned int)period;
2099
2100 /* Open the *other* PCM for jack-sense auto-routing.
2101 *
2102 * On sof-rt5682+max98360a, the SOF topology exposes two FE PCMs:
2103 * PCM 0 (UCM "Headphone") → SSP0 → RT5682 codec → headphone jack
2104 * PCM 1 (UCM "Speaker" ) → SSP1 → MAX98360A → speaker amp
2105 *
2106 * Each PCM goes to a *different* DAI, and the codec's DAPM jack-sense
2107 * mutes whichever side isn't currently in use. So if we open BOTH and
2108 * tee the same audio to both, the hardware automatically picks the
2109 * right output: speakers when no jack is plugged, headphones when one
2110 * is plugged. No userspace jack monitor needed.
2111 *
2112 * Only fires when the secondary PCM is a different device than the one
2113 * we already opened — duplicate-open of the same hw: device would just
2114 * fail with -EBUSY. */
2115 audio->headphone_pcm = NULL;
2116 {
2117 const char *secondary = NULL;
2118 /* Opt-in: opening the headphone PCM at boot was powering up
2119 * the HP DAPM path even with UCM Headphones disabled, which
2120 * overrode jack-sense and silenced the MAX98360A amp. Keep
2121 * the secondary PCM closed by default; a future jack-watcher
2122 * thread will open/close it in response to plug events. Set
2123 * AC_AUDIO_TEE=1 to force-open anyway (for ThinkPad etc.
2124 * single-PCM HDA hardware, where it's a noop). */
2125 const char *tee_env = getenv("AC_AUDIO_TEE");
2126 int tee_enabled = (tee_env && tee_env[0] == '1');
2127 if (tee_enabled && ucm_speaker_pcm[0] && ucm_headphone_pcm[0] &&
2128 strcmp(ucm_speaker_pcm, ucm_headphone_pcm) != 0) {
2129 /* Pick whichever the main PCM didn't open. */
2130 if (strstr(audio->audio_device, ucm_speaker_pcm))
2131 secondary = ucm_headphone_pcm;
2132 else if (strstr(audio->audio_device, ucm_headphone_pcm))
2133 secondary = ucm_speaker_pcm;
2134 else
2135 secondary = ucm_headphone_pcm; /* legacy fallback opened */
2136 }
2137 if (secondary) {
2138 snd_pcm_t *pcm2 = NULL;
2139 int e2 = snd_pcm_open(&pcm2, secondary,
2140 SND_PCM_STREAM_PLAYBACK, 0);
2141 if (e2 == 0) {
2142 /* Same params as the main PCM so audio thread can write
2143 * the same int16 buffer to both without resampling. */
2144 snd_pcm_hw_params_t *hp; snd_pcm_hw_params_alloca(&hp);
2145 snd_pcm_hw_params_any(pcm2, hp);
2146 snd_pcm_hw_params_set_access(pcm2, hp,
2147 SND_PCM_ACCESS_RW_INTERLEAVED);
2148 snd_pcm_hw_params_set_format(pcm2, hp, SND_PCM_FORMAT_S16_LE);
2149 snd_pcm_hw_params_set_channels(pcm2, hp, AUDIO_CHANNELS);
2150 unsigned int r2 = audio->actual_rate;
2151 snd_pcm_hw_params_set_rate_near(pcm2, hp, &r2, 0);
2152 snd_pcm_uframes_t p2 = audio->actual_period;
2153 snd_pcm_hw_params_set_period_size_near(pcm2, hp, &p2, 0);
2154 snd_pcm_uframes_t b2 = audio->actual_period * 4;
2155 snd_pcm_hw_params_set_buffer_size_near(pcm2, hp, &b2);
2156 int herr = snd_pcm_hw_params(pcm2, hp);
2157 if (herr == 0) {
2158 snd_pcm_prepare(pcm2);
2159 audio->headphone_pcm = pcm2;
2160 fprintf(stderr,
2161 "[audio] Parallel PCM opened: %s (%uHz, %lufrm) — auto-routing enabled\n",
2162 secondary, r2, (unsigned long)p2);
2163 } else {
2164 fprintf(stderr,
2165 "[audio] Parallel PCM hw_params failed for %s: %s — auto-routing disabled\n",
2166 secondary, snd_strerror(herr));
2167 snd_pcm_close(pcm2);
2168 }
2169 } else {
2170 fprintf(stderr,
2171 "[audio] Parallel PCM open failed for %s: %s — auto-routing disabled\n",
2172 secondary, snd_strerror(e2));
2173 }
2174 }
2175 }
2176
2177 // Update glitch rate for actual sample rate
2178 audio->glitch_rate = rate / 1600;
2179
2180 // Recent output history targets ~48k mono regardless of playback rate.
2181 unsigned int hist_target = rate > AUDIO_OUTPUT_HISTORY_RATE ? AUDIO_OUTPUT_HISTORY_RATE : rate;
2182 unsigned int hist_stride = rate > hist_target ? (rate + hist_target / 2) / hist_target : 1;
2183 if (hist_stride == 0) hist_stride = 1;
2184 audio->output_history_rate = rate / hist_stride;
2185 if (audio->output_history_rate == 0) audio->output_history_rate = rate;
2186 audio->output_history_downsample_n = hist_stride;
2187 audio->output_history_downsample_pos = 0;
2188 audio->output_history_size = (int)(audio->output_history_rate * AUDIO_OUTPUT_HISTORY_SECS);
2189 if (audio->output_history_size <= 0) {
2190 audio->output_history_size = AUDIO_OUTPUT_HISTORY_RATE * AUDIO_OUTPUT_HISTORY_SECS;
2191 audio->output_history_rate = AUDIO_OUTPUT_HISTORY_RATE;
2192 audio->output_history_downsample_n = 1;
2193 }
2194
2195 // Reallocate room buffers for actual rate
2196 int actual_room_size = (int)(0.12 * rate) * 3;
2197 if (actual_room_size != audio->room_size) {
2198 free(audio->room_buf_l); free(audio->room_buf_r);
2199 audio->room_size = actual_room_size;
2200 audio->room_buf_l = calloc(actual_room_size, sizeof(float));
2201 audio->room_buf_r = calloc(actual_room_size, sizeof(float));
2202 audio->room_pos = 0;
2203 }
2204
2205 /* Log the actual negotiated params — channels and format are
2206 * particularly important for diagnosing the "crunchy quiet" bug
2207 * on SOF boards where SSP1 may expect different bit depth. */
2208 {
2209 snd_pcm_format_t fmt;
2210 unsigned int ch = 0;
2211 snd_pcm_hw_params_get_format(params, &fmt);
2212 snd_pcm_hw_params_get_channels(params, &ch);
2213 fprintf(stderr, "[audio] ALSA: %uHz %uch fmt=%s period=%lu buf=%lu (%.1fms)\n",
2214 rate, ch, snd_pcm_format_name(fmt),
2215 (unsigned long)period, (unsigned long)buffer_size,
2216 (double)period / rate * 1000.0);
2217 }
2218 snprintf(audio->audio_status, sizeof(audio->audio_status),
2219 "ok %uHz %lufrm", rate, (unsigned long)period);
2220 if (rate != AUDIO_SAMPLE_RATE)
2221 fprintf(stderr, "[audio] WARNING: got %uHz instead of %dHz\n", rate, AUDIO_SAMPLE_RATE);
2222
2223 // ChromeOS UCM verb activation. sof-rt5682 on Jasper Lake has no
2224 // upstream UCM, so without this the Speaker verb's csets
2225 // ('Spk Switch on' plus DSP pipeline routes) never fire and the
2226 // MAX98360A amp receives no I2S even with SD_MODE asserted. The
2227 // WeirdTreeThing/alsa-ucm-conf-cros bundle in /usr/share/alsa/ucm2/
2228 // provides the downstream ChromeOS versions. Noop on boards whose
2229 // UCM is already upstream (ThinkPad HDA, Macs) — snd_use_case_mgr_open
2230 // returns -ENOENT and we fall through to the manual mixer path below.
2231 {
2232 char card_id[32] = "";
2233 char id_path[64];
2234 snprintf(id_path, sizeof(id_path), "/proc/asound/card%d/id", card_idx);
2235 FILE *idfp = fopen(id_path, "r");
2236 if (idfp) {
2237 if (fgets(card_id, sizeof(card_id), idfp)) {
2238 char *nl = strchr(card_id, '\n'); if (nl) *nl = 0;
2239 }
2240 fclose(idfp);
2241 }
2242 if (card_id[0]) {
2243 /* The kernel strips hyphens from card IDs (so our machine
2244 * driver name `jsl_rt5682_def` + topology `sof-rt5682` both
2245 * become card id `sofrt5682`). The ChromeOS UCM tree keeps
2246 * the canonical hyphenated names. Try a few permutations so
2247 * whichever matches wins. */
2248 const char *candidates[] = {
2249 card_id, /* e.g. "sofrt5682" */
2250 "sof-rt5682",
2251 "sof-cs42l42",
2252 "sof-nau8825",
2253 "sof-da7219",
2254 NULL
2255 };
2256 snd_use_case_mgr_t *uc = NULL;
2257 int uerr = -1;
2258 const char *opened = NULL;
2259 for (int i = 0; candidates[i]; i++) {
2260 uerr = snd_use_case_mgr_open(&uc, candidates[i]);
2261 if (uerr == 0) { opened = candidates[i]; break; }
2262 }
2263 if (uerr == 0) {
2264 fprintf(stderr, "[audio] UCM: opened '%s' (card=%s)\n",
2265 opened, card_id);
2266 if (snd_use_case_set(uc, "_verb", "HiFi") == 0) {
2267 fprintf(stderr, "[audio] UCM: _verb=HiFi set\n");
2268 } else {
2269 fprintf(stderr, "[audio] UCM: _verb=HiFi failed\n");
2270 }
2271 /* Enable ONLY Speaker at boot — enumerating every device
2272 * (including Headphones/Headset) was running ChromeOS
2273 * UCM EnableSequences that set `Headphone Jack Switch on`
2274 * + `HPOL/HPOR Playback Switch 1`. That forces DAPM to
2275 * route audio through the RT5682 headphone path and
2276 * powers down the MAX98360A amp (kmsg showed `sdmode
2277 * to 0` at 35s and never recovering).
2278 *
2279 * The rt5682-init BootSequence from WeirdTreeThing's
2280 * UCM deliberately ships with HP jack/switch OFF so
2281 * the speaker amp stays live when nothing is plugged
2282 * in. Keep that intact — only run the Speaker (and
2283 * mic) EnableSequence, not any headphone one. Jack-
2284 * plug routing becomes a later follow-up (a jack-
2285 * state watcher thread that flips _enadev on plug). */
2286 const char **devlist = NULL;
2287 int ndev = snd_use_case_get_list(uc, "_devices/HiFi",
2288 &devlist);
2289 int enabled_speaker = 0;
2290 if (ndev > 0 && devlist) {
2291 for (int i = 0; i < ndev; i += 2) {
2292 const char *dev = devlist[i];
2293 if (!dev || !dev[0]) continue;
2294 /* Skip anything that would re-enable the
2295 * headphone path at boot. Speakers first,
2296 * mics/HDMI are safe (no DAPM routing to HP). */
2297 if (strstr(dev, "Headphone") ||
2298 strstr(dev, "Headset") ||
2299 strstr(dev, "Headphones")) {
2300 fprintf(stderr,
2301 "[audio] UCM: skip _enadev=%s (jack-gated)\n",
2302 dev);
2303 continue;
2304 }
2305 int rr = snd_use_case_set(uc, "_enadev", dev);
2306 fprintf(stderr, "[audio] UCM: _enadev=%s %s\n",
2307 dev, rr == 0 ? "ok" : "FAIL");
2308 if (rr == 0 && (strstr(dev, "Speaker") ||
2309 strstr(dev, "Speakers")))
2310 enabled_speaker = 1;
2311 }
2312 snd_use_case_free_list(devlist, ndev);
2313 } else {
2314 /* Fallback: enable Speaker variants only. */
2315 const char *names[] = {"Speaker", "Speakers", NULL};
2316 for (int i = 0; names[i]; i++) {
2317 if (snd_use_case_set(uc, "_enadev", names[i]) == 0) {
2318 fprintf(stderr, "[audio] UCM: _enadev=%s ok\n",
2319 names[i]);
2320 enabled_speaker = 1;
2321 }
2322 }
2323 }
2324 if (!enabled_speaker)
2325 fprintf(stderr, "[audio] UCM: WARNING no Speaker device enabled\n");
2326 snd_use_case_mgr_close(uc);
2327 } else {
2328 fprintf(stderr, "[audio] UCM: no config matched card '%s' — manual mixer fallback\n",
2329 card_id);
2330 }
2331 }
2332
2333 /* Defensive audio diagnostic — dump the full ASoC DAPM graph and
2334 * every kcontrol's current value so post-mortem log analysis can
2335 * tell whether a PGA is sitting at -inf, a DAPM widget is stuck
2336 * OFF, or a DAI isn't active. These files are debugfs-backed so
2337 * require CONFIG_DEBUG_FS=y and debugfs mounted at
2338 * /sys/kernel/debug (init already does the mount). Non-fatal if
2339 * absent. */
2340 {
2341 const char *dbg_dir = "/sys/kernel/debug/asoc/card0";
2342 if (access(dbg_dir, R_OK) == 0) {
2343 fprintf(stderr, "[audio-diag] ASoC debugfs dump — %s\n", dbg_dir);
2344 /* dapm/ subdir has one file per widget with its power state,
2345 * input/output connections, and active stream info. */
2346 DIR *dapm = opendir("/sys/kernel/debug/asoc/card0/dapm");
2347 if (dapm) {
2348 struct dirent *de;
2349 while ((de = readdir(dapm))) {
2350 if (de->d_name[0] == '.') continue;
2351 char widget_path[256];
2352 snprintf(widget_path, sizeof(widget_path),
2353 "/sys/kernel/debug/asoc/card0/dapm/%s",
2354 de->d_name);
2355 FILE *wf = fopen(widget_path, "r");
2356 if (!wf) continue;
2357 /* Each widget file's first line is the state:
2358 * "WidgetName: On in 0 out 0 stream ..." */
2359 char wline[256];
2360 if (fgets(wline, sizeof(wline), wf)) {
2361 char *nl = strchr(wline, '\n');
2362 if (nl) *nl = 0;
2363 fprintf(stderr, "[audio-diag] dapm: %s\n", wline);
2364 }
2365 fclose(wf);
2366 }
2367 closedir(dapm);
2368 }
2369 /* /sys/kernel/debug/gpio dump shows MAX98360A SD_MODE +
2370 * RT5682 IRQ GPIO current state so we can tell if the
2371 * amp was powered at snapshot time. */
2372 FILE *gf = fopen("/sys/kernel/debug/gpio", "r");
2373 if (gf) {
2374 char gline[256];
2375 int lines = 0;
2376 while (lines < 30 && fgets(gline, sizeof(gline), gf)) {
2377 char *nl = strchr(gline, '\n');
2378 if (nl) *nl = 0;
2379 if (strstr(gline, "sdmode") || strstr(gline, "RT58") ||
2380 strstr(gline, "gpiochip")) {
2381 fprintf(stderr, "[audio-diag] gpio: %s\n", gline);
2382 lines++;
2383 }
2384 }
2385 fclose(gf);
2386 }
2387 } else {
2388 fprintf(stderr, "[audio-diag] debugfs not mounted — no ASoC state available\n");
2389 }
2390 }
2391 }
2392
2393 // Unmute ALL outputs (HDA Intel codecs have many controls that can mute)
2394 char mixer_card[16];
2395 snprintf(mixer_card, sizeof(mixer_card), "hw:%d", card_idx);
2396 fprintf(stderr, "[audio] Using mixer: %s\n", mixer_card);
2397
2398 snd_mixer_t *mixer = NULL;
2399 if (snd_mixer_open(&mixer, 0) >= 0) {
2400 snd_mixer_attach(mixer, mixer_card);
2401 snd_mixer_selem_register(mixer, NULL, NULL);
2402 snd_mixer_load(mixer);
2403
2404 snd_mixer_elem_t *elem;
2405 for (elem = snd_mixer_first_elem(mixer); elem; elem = snd_mixer_elem_next(elem)) {
2406 const char *name = snd_mixer_selem_get_name(elem);
2407 if (!snd_mixer_selem_is_active(elem)) continue;
2408
2409 // Log all mixer elements — with pre-set values so a silent audio
2410 // log can be diagnosed without flashing again. If a playback-
2411 // switch element is already off before our unmute, or a volume
2412 // element reports a suspicious range (e.g. min==max at 0), we
2413 // want to know which one.
2414 fprintf(stderr, "[audio] Mixer: %s", name);
2415 if (snd_mixer_selem_has_playback_volume(elem)) {
2416 long vmin = 0, vmax = 0, vcur = 0;
2417 snd_mixer_selem_get_playback_volume_range(elem, &vmin, &vmax);
2418 snd_mixer_selem_get_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT, &vcur);
2419 fprintf(stderr, " [vol %ld..%ld now=%ld]", vmin, vmax, vcur);
2420 long dbmin = 0, dbmax = 0, dbcur = 0;
2421 if (snd_mixer_selem_get_playback_dB_range(elem, &dbmin, &dbmax) == 0 &&
2422 snd_mixer_selem_get_playback_dB(elem, SND_MIXER_SCHN_FRONT_LEFT, &dbcur) == 0) {
2423 fprintf(stderr, " [dB %.1f..%.1f now=%.1f]",
2424 dbmin / 100.0, dbmax / 100.0, dbcur / 100.0);
2425 }
2426 }
2427 if (snd_mixer_selem_has_playback_switch(elem)) {
2428 int sw = 0;
2429 snd_mixer_selem_get_playback_switch(elem, SND_MIXER_SCHN_FRONT_LEFT, &sw);
2430 fprintf(stderr, " [sw now=%s]", sw ? "on" : "OFF");
2431 }
2432 if (snd_mixer_selem_has_capture_switch(elem)) fprintf(stderr, " [cap-sw]");
2433 if (snd_mixer_selem_has_capture_volume(elem)) fprintf(stderr, " [cap-vol]");
2434 fprintf(stderr, "\n");
2435
2436 // Unmute every playback switch we find — except the ones
2437 // the UCM BootSequence explicitly turns off to keep audio
2438 // routed to the speaker amp on unplugged-headphone state.
2439 // Flipping "Headphone Jack Switch" on re-enables the HP
2440 // DAPM path and silences MAX98360A even when nothing is
2441 // plugged in (see G7/Drawcia debug session).
2442 if (snd_mixer_selem_has_playback_switch(elem)) {
2443 int skip = 0;
2444 const char *jack_gated[] = {
2445 "Headphone Jack", /* rt5682 */
2446 "Headphone Jack Switch",
2447 "HPOL Playback",
2448 "HPOR Playback",
2449 "Headset",
2450 NULL
2451 };
2452 for (int j = 0; jack_gated[j]; j++) {
2453 if (strstr(name, jack_gated[j])) { skip = 1; break; }
2454 }
2455 if (!skip) {
2456 snd_mixer_selem_set_playback_switch_all(elem, 1);
2457 fprintf(stderr, "[audio] Unmuted: %s\n", name);
2458 } else {
2459 fprintf(stderr, "[audio] Skip unmute (jack-gated): %s\n", name);
2460 }
2461 }
2462
2463 // Set volume to max for output controls
2464 if (snd_mixer_selem_has_playback_volume(elem)) {
2465 long min, max;
2466 snd_mixer_selem_get_playback_volume_range(elem, &min, &max);
2467 snd_mixer_selem_set_playback_volume_all(elem, max);
2468 fprintf(stderr, "[audio] Volume %s: %ld/%ld\n", name, max, max);
2469 }
2470
2471 // NOTE: Do NOT touch capture mixer controls here — on the 11e Yoga
2472 // Gen 5, enabling capture switches at boot (before any capture PCM
2473 // is open) puts the HDA codec into a bad state that causes EIO when
2474 // the capture stream is later opened with period/buffer params.
2475 }
2476 snd_mixer_close(mixer);
2477 } else {
2478 fprintf(stderr, "[audio] Cannot open mixer\n");
2479 }
2480
2481 // Read initial system volume
2482 audio->system_volume = read_system_volume_card(card_idx);
2483 fprintf(stderr, "[audio] System volume: %d%%\n", audio->system_volume);
2484
2485 // HDMI audio disabled — opening HDMI PCM streams on the same HDA controller
2486 // can exhaust controller streams and cause EIO on capture.
2487 audio->hdmi_pcm = NULL;
2488 fprintf(stderr, "[audio] HDMI audio: disabled\n");
2489
2490 // Start audio thread
2491 audio->running = 1;
2492 pthread_create(&audio->thread, NULL, audio_thread_fn, audio);
2493
2494 /* Force PCI runtime-PM to "on" for the sound card now that the
2495 * driver is loaded and card_idx is known. The init-script attempt
2496 * may fire before probe — doing it here guarantees the sysfs node
2497 * exists. Without this the SOF DSP auto-suspends after ~20-40s
2498 * of silence, which stops the SSP1 BE DAI and drops MAX98360A
2499 * sdmode to 0 (speaker amp off), and the amp never comes back.
2500 * Also try the generic PCI "power_save" disable for HDA paths. */
2501 {
2502 char pm_path[128];
2503 snprintf(pm_path, sizeof(pm_path),
2504 "/sys/class/sound/card%d/device/power/control", card_idx);
2505 FILE *pm = fopen(pm_path, "w");
2506 if (pm) {
2507 fputs("on", pm);
2508 fclose(pm);
2509 fprintf(stderr, "[audio] Disabled runtime-PM: %s\n", pm_path);
2510 } else {
2511 fprintf(stderr, "[audio] Could not set runtime-PM: %s\n", pm_path);
2512 }
2513 /* Also try the autosuspend delay — set to -1 (never) */
2514 snprintf(pm_path, sizeof(pm_path),
2515 "/sys/class/sound/card%d/device/power/autosuspend_delay_ms",
2516 card_idx);
2517 pm = fopen(pm_path, "w");
2518 if (pm) {
2519 fputs("-1", pm);
2520 fclose(pm);
2521 fprintf(stderr, "[audio] Set autosuspend_delay=-1: %s\n", pm_path);
2522 }
2523 }
2524
2525 fprintf(stderr, "[audio] Ready\n");
2526 return audio;
2527}
2528
2529uint64_t audio_synth(ACAudio *audio, WaveType type, double freq,
2530 double duration, double volume, double attack,
2531 double decay, double pan) {
2532 if (!audio) return 0;
2533
2534 pthread_mutex_lock(&audio->lock);
2535
2536 // Find free voice slot
2537 int slot = -1;
2538 for (int i = 0; i < AUDIO_MAX_VOICES; i++) {
2539 if (audio->voices[i].state == VOICE_INACTIVE) {
2540 slot = i;
2541 break;
2542 }
2543 }
2544 if (slot < 0) {
2545 // Steal oldest voice
2546 double oldest = 0;
2547 slot = 0;
2548 for (int i = 0; i < AUDIO_MAX_VOICES; i++) {
2549 if (audio->voices[i].elapsed > oldest) {
2550 oldest = audio->voices[i].elapsed;
2551 slot = i;
2552 }
2553 }
2554 }
2555
2556 ACVoice *v = &audio->voices[slot];
2557 memset(v, 0, sizeof(ACVoice));
2558 v->state = VOICE_ACTIVE;
2559 v->type = type;
2560 v->phase = 0.0;
2561 v->frequency = freq;
2562 v->target_frequency = freq;
2563 v->volume = volume;
2564 v->pan = pan;
2565 v->attack = attack > 0 ? attack : 0.005;
2566 v->decay = decay > 0 ? decay : 0.1;
2567 v->duration = duration;
2568 v->id = ++audio->next_id;
2569 v->started_at = audio->time;
2570
2571 if (type == WAVE_NOISE || type == WAVE_WHISTLE || type == WAVE_GUN) {
2572 v->noise_seed = (uint32_t)(audio->next_id * 2654435761u);
2573 }
2574 if (type == WAVE_NOISE) {
2575 setup_noise_filter(v, (double)(audio->actual_rate ? audio->actual_rate : AUDIO_SAMPLE_RATE));
2576 } else if (type == WAVE_GUN) {
2577 // Caller (audio_synth_gun) sets the preset via gun_init_voice
2578 // after this base init runs.
2579 } else if (type == WAVE_WHISTLE) {
2580 // Clear the waveguide state — bore + jet delay buffers and the
2581 // loop filter / DC blocker. Without this, leftover state from a
2582 // previous voice reuse would produce startup artifacts.
2583 memset(v->whistle_bore_buf, 0, sizeof(v->whistle_bore_buf));
2584 memset(v->whistle_jet_buf, 0, sizeof(v->whistle_jet_buf));
2585 v->whistle_bore_w = 0;
2586 v->whistle_jet_w = 0;
2587 v->whistle_breath = 0.0;
2588 v->whistle_vibrato_phase = 0.0;
2589 v->whistle_lp1 = 0.0;
2590 v->whistle_hp_x1 = 0.0;
2591 v->whistle_hp_y1 = 0.0;
2592 }
2593
2594 pthread_mutex_unlock(&audio->lock);
2595 return v->id;
2596}
2597
2598void audio_kill(ACAudio *audio, uint64_t id, double fade) {
2599 if (!audio) return;
2600 pthread_mutex_lock(&audio->lock);
2601 for (int i = 0; i < AUDIO_MAX_VOICES; i++) {
2602 if (audio->voices[i].id == id && audio->voices[i].state == VOICE_ACTIVE) {
2603 audio->voices[i].state = VOICE_KILLING;
2604 audio->voices[i].fade_duration = fade > 0 ? fade : 0.025;
2605 audio->voices[i].fade_elapsed = 0.0;
2606 // Gun-specific release behaviors (e.g. ricochet pitch drop).
2607 if (audio->voices[i].type == WAVE_GUN) {
2608 gun_on_release(&audio->voices[i]);
2609 }
2610 break;
2611 }
2612 }
2613 pthread_mutex_unlock(&audio->lock);
2614}
2615
2616uint64_t audio_synth_gun(ACAudio *audio, GunPreset preset, double duration,
2617 double volume, double attack, double decay,
2618 double pan, double pressure_scale, int force_model) {
2619 if (!audio) return 0;
2620 // Delegate base voice setup (slot alloc, envelope fields, noise seed).
2621 // Frequency is unused for guns — the DWG cavity resonance comes from
2622 // the preset's bore_length, not v->frequency. We pass 110 to keep
2623 // the smoothing code happy.
2624 uint64_t id = audio_synth(audio, WAVE_GUN, 110.0, duration, volume,
2625 attack, decay, pan);
2626 if (!id) return 0;
2627
2628 pthread_mutex_lock(&audio->lock);
2629 ACVoice *v = NULL;
2630 for (int i = 0; i < AUDIO_MAX_VOICES; i++) {
2631 if (audio->voices[i].id == id) { v = &audio->voices[i]; break; }
2632 }
2633 if (v) {
2634 double sr = (double)(audio->actual_rate ? audio->actual_rate
2635 : AUDIO_SAMPLE_RATE);
2636 gun_init_voice(v, preset, sr, force_model);
2637 if (pressure_scale > 0.0 && pressure_scale != 1.0) {
2638 v->gun_pressure *= pressure_scale;
2639 }
2640 }
2641 pthread_mutex_unlock(&audio->lock);
2642 return id;
2643}
2644
2645// Apply a single per-shot param override to a freshly-initialized gun
2646// voice. Called by the JS bindings between audio_synth_gun() and the
2647// audio thread's first read of the voice — lets the inspector's
2648// drag-to-edit cards push live tuning values into the next shot
2649// without rebuilding gun_presets[]. Unknown keys are silently ignored.
2650//
2651// Layer-state fields (envelopes, biquad coefficients, the Friedlander
2652// pulse position) are NOT exposed; we only change the constants the
2653// preset would have set in init.
2654void audio_gun_voice_set_param(ACAudio *audio, uint64_t id,
2655 const char *key, double value) {
2656 if (!audio || !key) return;
2657 pthread_mutex_lock(&audio->lock);
2658 ACVoice *v = NULL;
2659 for (int i = 0; i < AUDIO_MAX_VOICES; i++) {
2660 if (audio->voices[i].id == id && audio->voices[i].type == WAVE_GUN) {
2661 v = &audio->voices[i];
2662 break;
2663 }
2664 }
2665 if (!v) { pthread_mutex_unlock(&audio->lock); return; }
2666
2667 double sr = (double)(audio->actual_rate ? audio->actual_rate
2668 : AUDIO_SAMPLE_RATE);
2669 if (v->gun_model == GUN_MODEL_CLASSIC) {
2670 if (strcmp(key, "click_amp") == 0) v->gun_click_amp = value;
2671 else if (strcmp(key, "click_decay_ms") == 0) {
2672 double tau = (value > 0.05 ? value : 0.05) * 0.001;
2673 v->gun_click_decay_mult = exp(-1.0 / (tau * sr));
2674 }
2675 else if (strcmp(key, "crack_amp") == 0) v->gun_body_amp[0] = value;
2676 else if (strcmp(key, "crack_decay_ms") == 0) {
2677 double tau = (value > 0.1 ? value : 0.1) * 0.001;
2678 v->gun_env_decay_mult = exp(-1.0 / (tau * sr));
2679 }
2680 else if (strcmp(key, "crack_fc") == 0 || strcmp(key, "crack_q") == 0) {
2681 // Both freq and Q feed the same biquad, recompute together.
2682 // For partial updates we just recompute with the latest value
2683 // and keep the other from existing coefs (lossy but adequate).
2684 // Approximate Q from a2 = r² → r = √a2 → tau = -π·f/(Q·sr·ln r).
2685 double a2 = v->gun_body_a2[0];
2686 double r = a2 > 0 ? sqrt(a2) : 0.95;
2687 double cur_w = acos(v->gun_body_a1[0] / (2.0 * r));
2688 double cur_f = cur_w * sr / (2.0 * M_PI);
2689 double cur_q = -M_PI * cur_f / (sr * log(r > 0.0001 ? r : 0.0001));
2690 double f = (strcmp(key, "crack_fc") == 0) ? value : cur_f;
2691 double q = (strcmp(key, "crack_q") == 0) ? value : cur_q;
2692 compute_resonator(f, q, sr, &v->gun_body_a1[0],
2693 &v->gun_body_a2[0], &v->gun_crack_b0);
2694 }
2695 else if (strcmp(key, "boom_amp") == 0) v->gun_body_amp[1] = value;
2696 else if (strcmp(key, "boom_freq_start") == 0) {
2697 v->gun_boom_freq_start = value;
2698 v->gun_boom_freq = value;
2699 }
2700 else if (strcmp(key, "boom_freq_end") == 0) v->gun_boom_freq_end = value;
2701 else if (strcmp(key, "boom_pitch_decay_ms") == 0) {
2702 double tau = (value > 0.1 ? value : 0.1) * 0.001;
2703 v->gun_boom_pitch_mult = exp(-1.0 / (tau * sr));
2704 }
2705 else if (strcmp(key, "boom_amp_decay_ms") == 0) {
2706 double tau = (value > 0.1 ? value : 0.1) * 0.001;
2707 v->gun_boom_decay_mult = exp(-1.0 / (tau * sr));
2708 }
2709 else if (strcmp(key, "tail_amp") == 0) v->gun_body_amp[2] = value;
2710 else if (strcmp(key, "tail_decay_ms") == 0) {
2711 double tau = (value > 0.1 ? value : 0.1) * 0.001;
2712 v->gun_tail_decay_mult = exp(-1.0 / (tau * sr));
2713 }
2714 else if (strcmp(key, "tail_fc") == 0 || strcmp(key, "tail_q") == 0) {
2715 double a2 = v->gun_body_a2[1];
2716 double r = a2 > 0 ? sqrt(a2) : 0.95;
2717 double cur_w = acos(v->gun_body_a1[1] / (2.0 * r));
2718 double cur_f = cur_w * sr / (2.0 * M_PI);
2719 double cur_q = -M_PI * cur_f / (sr * log(r > 0.0001 ? r : 0.0001));
2720 double f = (strcmp(key, "tail_fc") == 0) ? value : cur_f;
2721 double q = (strcmp(key, "tail_q") == 0) ? value : cur_q;
2722 compute_resonator(f, q, sr, &v->gun_body_a1[1],
2723 &v->gun_body_a2[1], &v->gun_tail_b0);
2724 }
2725 } else {
2726 // Physical model overrides.
2727 if (strcmp(key, "pressure") == 0) v->gun_pressure = value;
2728 else if (strcmp(key, "env_rate") == 0) {
2729 v->gun_phys_t_plus = (3.0 / (value > 100 ? value : 100.0)) * sr;
2730 if (v->gun_phys_t_plus < 32.0) v->gun_phys_t_plus = 32.0;
2731 if (v->gun_phys_t_plus > 4096.0) v->gun_phys_t_plus = 4096.0;
2732 }
2733 else if (strcmp(key, "bore_length_s") == 0) {
2734 v->gun_bore_delay = value * sr;
2735 if (v->gun_bore_delay < 4.0) v->gun_bore_delay = 4.0;
2736 if (v->gun_bore_delay > 2040.0) v->gun_bore_delay = 2040.0;
2737 }
2738 else if (strcmp(key, "bore_loss") == 0) v->gun_bore_loss = value;
2739 else if (strcmp(key, "breech_reflect") == 0) v->gun_breech_reflect = value;
2740 else if (strcmp(key, "noise_gain") == 0) v->gun_noise_gain = value;
2741 else if (strcmp(key, "radiation") == 0) v->gun_radiation_a = value;
2742 else if (strncmp(key, "body_freq", 9) == 0 ||
2743 strncmp(key, "body_q", 6) == 0) {
2744 int idx = key[strlen(key) - 1] - '0';
2745 if (idx < 0 || idx > 2) { pthread_mutex_unlock(&audio->lock); return; }
2746 // Recompute the resonator with one swapped param, the other inferred.
2747 double a2 = v->gun_body_a2[idx];
2748 double r = a2 > 0 ? sqrt(a2) : 0.95;
2749 double cur_w = acos(v->gun_body_a1[idx] / (2.0 * r));
2750 double cur_f = cur_w * sr / (2.0 * M_PI);
2751 double cur_q = -M_PI * cur_f / (sr * log(r > 0.0001 ? r : 0.0001));
2752 double f = (strncmp(key, "body_freq", 9) == 0) ? value : cur_f;
2753 double q = (strncmp(key, "body_q", 6) == 0) ? value : cur_q;
2754 double b0_unused;
2755 compute_resonator(f, q, sr, &v->gun_body_a1[idx],
2756 &v->gun_body_a2[idx], &b0_unused);
2757 }
2758 else if (strncmp(key, "body_amp", 8) == 0) {
2759 int idx = key[strlen(key) - 1] - '0';
2760 if (idx >= 0 && idx <= 2) v->gun_body_amp[idx] = value;
2761 }
2762 }
2763 pthread_mutex_unlock(&audio->lock);
2764}
2765
2766void audio_update(ACAudio *audio, uint64_t id, double freq,
2767 double volume, double pan) {
2768 if (!audio) return;
2769 pthread_mutex_lock(&audio->lock);
2770 for (int i = 0; i < AUDIO_MAX_VOICES; i++) {
2771 if (audio->voices[i].id == id && audio->voices[i].state != VOICE_INACTIVE) {
2772 if (freq > 0) audio->voices[i].target_frequency = freq;
2773 if (volume >= 0) audio->voices[i].volume = volume;
2774 if (pan > -2.0) audio->voices[i].pan = pan;
2775 break;
2776 }
2777 }
2778 pthread_mutex_unlock(&audio->lock);
2779}
2780
2781int audio_beat_check(ACAudio *audio) {
2782 if (!audio) return 0;
2783 int triggered = audio->beat_triggered;
2784 if (triggered) audio->beat_triggered = 0;
2785 return triggered;
2786}
2787
2788void audio_set_bpm(ACAudio *audio, double bpm) {
2789 if (!audio || bpm <= 0) return;
2790 pthread_mutex_lock(&audio->lock);
2791 audio->bpm = bpm;
2792 pthread_mutex_unlock(&audio->lock);
2793}
2794
2795void audio_room_toggle(ACAudio *audio) {
2796 if (!audio) return;
2797 audio->room_enabled = !audio->room_enabled;
2798 fprintf(stderr, "[audio] Room: %s\n", audio->room_enabled ? "ON" : "OFF");
2799}
2800
2801void audio_glitch_toggle(ACAudio *audio) {
2802 if (!audio) return;
2803 if (audio->glitch_enabled || audio->target_glitch_mix > 0.001f || audio->glitch_mix > 0.001f) {
2804 audio->glitch_enabled = 0;
2805 audio->target_glitch_mix = 0.0f;
2806 } else {
2807 audio->glitch_enabled = 1;
2808 audio->target_glitch_mix = 1.0f;
2809 }
2810 fprintf(stderr, "[audio] Glitch: %s (mix %.2f)\n",
2811 audio->glitch_enabled ? "ON" : "OFF",
2812 audio->target_glitch_mix);
2813}
2814
2815void audio_set_room_mix(ACAudio *audio, float mix) {
2816 if (!audio) return;
2817 if (mix < 0.0f) mix = 0.0f;
2818 if (mix > 1.0f) mix = 1.0f;
2819 audio->target_room_mix = mix;
2820}
2821
2822void audio_set_glitch_mix(ACAudio *audio, float mix) {
2823 if (!audio) return;
2824 if (mix < 0.0f) mix = 0.0f;
2825 if (mix > 1.0f) mix = 1.0f;
2826 audio->target_glitch_mix = mix;
2827 audio->glitch_enabled = mix > 0.001f;
2828 if (!audio->glitch_enabled) audio->glitch_counter = 0;
2829}
2830
2831void audio_set_fx_mix(ACAudio *audio, float mix) {
2832 if (!audio) return;
2833 if (mix < 0.0f) mix = 0.0f;
2834 if (mix > 1.0f) mix = 1.0f;
2835 audio->target_fx_mix = mix;
2836}
2837
2838// --- Hot-mic capture thread ---
2839// Device opens once (on wave-enter), stays running. Always reads to keep
2840// ALSA happy and the level meter live. Only writes to sample_buf when
2841// recording flag is set. Instant recording with zero device-open latency.
2842//
2843// IMPORTANT: HDMI audio must be DISABLED in audio_init and playback buffer
2844// must be 3 periods (not 6) — otherwise the HDA controller runs out of
2845// streams and capture gets EIO.
2846static void *capture_thread_func(void *arg) {
2847 ACAudio *audio = (ACAudio *)arg;
2848 snd_pcm_t *cap = NULL;
2849
2850 const char *devices[] = {"hw:0,0", "hw:1,0", "hw:0,6", "hw:0,7",
2851 "plughw:0,0", "plughw:1,0", "default", NULL};
2852 for (int i = 0; devices[i]; i++) {
2853 if (snd_pcm_open(&cap, devices[i], SND_PCM_STREAM_CAPTURE, 0) == 0) {
2854 snprintf(audio->mic_device, sizeof(audio->mic_device), "%s", devices[i]);
2855 ac_log("[mic] opened capture device: %s\n", devices[i]);
2856 break;
2857 }
2858 cap = NULL;
2859 }
2860 if (!cap) {
2861 snprintf(audio->mic_last_error, sizeof(audio->mic_last_error),
2862 "no capture device found");
2863 ac_log("[mic] no capture device found\n");
2864 audio->mic_hot = 0;
2865 return NULL;
2866 }
2867
2868 // Enable capture mixer switches now that capture PCM is open.
2869 // (Safe to do after snd_pcm_open — avoids the pre-open EIO bug on some HDA.)
2870 {
2871 char mixer_card[16];
2872 snprintf(mixer_card, sizeof(mixer_card), "hw:%d", audio->card_index);
2873 snd_mixer_t *cmix = NULL;
2874 if (snd_mixer_open(&cmix, 0) >= 0) {
2875 snd_mixer_attach(cmix, mixer_card);
2876 snd_mixer_selem_register(cmix, NULL, NULL);
2877 snd_mixer_load(cmix);
2878 snd_mixer_elem_t *el;
2879 for (el = snd_mixer_first_elem(cmix); el; el = snd_mixer_elem_next(el)) {
2880 if (!snd_mixer_selem_is_active(el)) continue;
2881 if (snd_mixer_selem_has_capture_switch(el)) {
2882 snd_mixer_selem_set_capture_switch_all(el, 1);
2883 ac_log("[mic] enabled capture switch: %s\n", snd_mixer_selem_get_name(el));
2884 }
2885 if (snd_mixer_selem_has_capture_volume(el)) {
2886 long cmin, cmax;
2887 snd_mixer_selem_get_capture_volume_range(el, &cmin, &cmax);
2888 snd_mixer_selem_set_capture_volume_all(el, cmax);
2889 ac_log("[mic] capture volume %s: %ld/%ld\n", snd_mixer_selem_get_name(el), cmax, cmax);
2890 }
2891 }
2892 snd_mixer_close(cmix);
2893 }
2894 }
2895
2896 snd_pcm_hw_params_t *hw;
2897 snd_pcm_hw_params_alloca(&hw);
2898 snd_pcm_hw_params_any(cap, hw);
2899 snd_pcm_hw_params_set_access(cap, hw, SND_PCM_ACCESS_RW_INTERLEAVED);
2900 snd_pcm_hw_params_set_format(cap, hw, SND_PCM_FORMAT_S16_LE);
2901
2902 unsigned int channels = 1;
2903 if (snd_pcm_hw_params_set_channels(cap, hw, 1) < 0) {
2904 channels = 2;
2905 snd_pcm_hw_params_set_channels(cap, hw, 2);
2906 }
2907
2908 unsigned int rate = 48000;
2909 snd_pcm_hw_params_set_rate_near(cap, hw, &rate, NULL);
2910
2911 // Set period=1024 (~21ms) for low-latency capture. This previously
2912 // caused EIO but the root causes were: HDMI audio open (exhausting
2913 // HDA streams), 6-period playback buffer, and capture mixer in
2914 // audio_init. All three are now fixed.
2915 snd_pcm_uframes_t period_frames = 1024;
2916 snd_pcm_hw_params_set_period_size_near(cap, hw, &period_frames, NULL);
2917 snd_pcm_uframes_t buffer_frames = 8192;
2918 snd_pcm_hw_params_set_buffer_size_near(cap, hw, &buffer_frames);
2919
2920 if (snd_pcm_hw_params(cap, hw) < 0) {
2921 snprintf(audio->mic_last_error, sizeof(audio->mic_last_error),
2922 "failed to configure capture");
2923 ac_log("[mic] failed to configure capture\n");
2924 snd_pcm_close(cap);
2925 audio->mic_hot = 0;
2926 return NULL;
2927 }
2928
2929 // Enable capture mixer (safe here — after PCM open, in capture thread)
2930 {
2931 int cnum = 0;
2932 const char *d = audio->mic_device;
2933 while (*d && (*d < '0' || *d > '9')) d++;
2934 if (*d) cnum = atoi(d);
2935 char ccard[16];
2936 snprintf(ccard, sizeof(ccard), "hw:%d", cnum);
2937 snd_mixer_t *cmix = NULL;
2938 if (snd_mixer_open(&cmix, 0) >= 0) {
2939 snd_mixer_attach(cmix, ccard);
2940 snd_mixer_selem_register(cmix, NULL, NULL);
2941 snd_mixer_load(cmix);
2942 snd_mixer_elem_t *elem;
2943 for (elem = snd_mixer_first_elem(cmix); elem; elem = snd_mixer_elem_next(elem)) {
2944 if (!snd_mixer_selem_is_active(elem)) continue;
2945 if (snd_mixer_selem_has_capture_switch(elem)) {
2946 snd_mixer_selem_set_capture_switch_all(elem, 1);
2947 ac_log("[mic] capture switch ON: %s\n", snd_mixer_selem_get_name(elem));
2948 }
2949 if (snd_mixer_selem_has_capture_volume(elem)) {
2950 long cmin, cmax;
2951 snd_mixer_selem_get_capture_volume_range(elem, &cmin, &cmax);
2952 long cset = cmin + ((cmax - cmin) * 9) / 10;
2953 snd_mixer_selem_set_capture_volume_all(elem, cset);
2954 ac_log("[mic] capture volume %s: %ld/%ld\n",
2955 snd_mixer_selem_get_name(elem), cset, cmax);
2956 }
2957 }
2958 snd_mixer_close(cmix);
2959 }
2960 }
2961
2962 audio->sample_rate = rate;
2963 audio->mic_connected = 1;
2964 ac_log("[mic] hot-mic running at %u Hz, %u ch\n", rate, channels);
2965
2966 int16_t buf[1024 * 2];
2967 while (audio->mic_hot) {
2968 int n = snd_pcm_readi(cap, buf, 512);
2969 if (n < 0) {
2970 n = snd_pcm_recover(cap, n, 0);
2971 if (n < 0) {
2972 snprintf(audio->mic_last_error, sizeof(audio->mic_last_error),
2973 "capture read failed: %s", snd_strerror(n));
2974 ac_log("[mic] capture read failed: %s\n", snd_strerror(n));
2975 break;
2976 }
2977 continue;
2978 }
2979
2980 float peak = 0.0f;
2981 // Aggressive compressor + hard limiter to prevent clipping.
2982 // Matches the note compression style in the synth output.
2983 static float env = 0.0f; // envelope follower
2984 static float comp_gain = 1.0f; // current gain
2985 const float threshold = 0.15f; // compress early (mic input is often hot)
2986 const float ratio = 12.0f; // aggressive compression
2987 const float attack = 0.005f; // fast attack
2988 const float release = 0.00005f; // slow release (smooth)
2989 const float limiter = 0.9f; // hard limiter ceiling
2990
2991 for (int s = 0; s < n; s++) {
2992 float sample;
2993 if (channels == 1) {
2994 sample = buf[s] / 32768.0f;
2995 } else {
2996 sample = (buf[s * 2] + buf[s * 2 + 1]) / 65536.0f;
2997 }
2998
2999 // Envelope follower
3000 float abs_s = fabsf(sample);
3001 if (abs_s > env)
3002 env += attack * (abs_s - env);
3003 else
3004 env += release * (abs_s - env);
3005
3006 // Compute gain reduction
3007 if (env > threshold) {
3008 float over = env - threshold;
3009 float reduced = threshold + over / ratio;
3010 comp_gain = reduced / env;
3011 } else {
3012 comp_gain += 0.0002f * (1.0f - comp_gain);
3013 }
3014
3015 sample *= comp_gain;
3016
3017 // Hard limiter — prevent any clipping
3018 if (sample > limiter) sample = limiter;
3019 else if (sample < -limiter) sample = -limiter;
3020
3021 if (abs_s > peak) peak = abs_s;
3022
3023 // Always write to ring buffer
3024 audio->mic_ring[audio->mic_ring_pos % audio->sample_max_len] = sample;
3025 audio->mic_ring_pos++;
3026
3027 // Direct-write when recording
3028 if (audio->recording && audio->sample_write_pos < audio->sample_max_len) {
3029 audio->sample_buf[audio->sample_write_pos++] = sample;
3030 }
3031 }
3032 // If we skipped the first chunk, mark that we've consumed it
3033 // by writing at least 0 (sample_write_pos stays 0, next chunk writes)
3034 audio->mic_level = peak;
3035
3036 if (audio->recording && audio->sample_write_pos >= audio->sample_max_len) {
3037 audio->sample_len = audio->sample_write_pos;
3038 audio->recording = 0;
3039 ac_log("[mic] recording buffer full (%d samples)\n", audio->sample_len);
3040 }
3041 }
3042
3043 ac_log("[mic] hot-mic thread exiting, device=%s\n", audio->mic_device);
3044 snd_pcm_close(cap);
3045 audio->mic_connected = 0;
3046 audio->recording = 0;
3047 return NULL;
3048}
3049
3050int audio_mic_open(ACAudio *audio) {
3051 if (!audio || audio->mic_hot || audio->capture_thread_running) return -1;
3052 audio->mic_hot = 1;
3053 audio->capture_thread_running = 1;
3054 audio->mic_last_error[0] = 0;
3055 ac_log("[mic] opening hot-mic\n");
3056 if (pthread_create(&audio->capture_thread, NULL, capture_thread_func, audio) != 0) {
3057 audio->mic_hot = 0;
3058 audio->capture_thread_running = 0;
3059 ac_log("[mic] failed to create capture thread\n");
3060 return -1;
3061 }
3062 return 0;
3063}
3064
3065void audio_mic_close(ACAudio *audio) {
3066 if (!audio) return;
3067 audio->recording = 0;
3068 audio->mic_hot = 0;
3069 if (audio->capture_thread_running) {
3070 pthread_join(audio->capture_thread, NULL);
3071 audio->capture_thread_running = 0;
3072 }
3073 ac_log("[mic] hot-mic closed\n");
3074}
3075
3076int audio_mic_start(ACAudio *audio) {
3077 if (!audio || audio->recording) return -1;
3078 if (!audio->mic_hot) {
3079 int rc = audio_mic_open(audio);
3080 if (rc != 0) return rc;
3081 }
3082 // Kill any playing sample voices
3083 for (int i = 0; i < AUDIO_MAX_SAMPLE_VOICES; i++)
3084 audio->sample_voices[i].active = 0;
3085 audio->rec_start_ring_pos = audio->mic_ring_pos;
3086 audio->sample_len = 0;
3087 audio->sample_write_pos = 0;
3088 __sync_synchronize();
3089 audio->recording = 1;
3090 ac_log("[mic] recording started (instant), ring_pos=%d\n", audio->rec_start_ring_pos);
3091 return 0;
3092}
3093
3094int audio_mic_stop(ACAudio *audio) {
3095 if (!audio) return 0;
3096 audio->recording = 0;
3097 __sync_synchronize();
3098
3099 // Kill all sample voices BEFORE touching sample_buf —
3100 // playback thread reads sample_buf[]/sample_len without locks
3101 for (int i = 0; i < AUDIO_MAX_SAMPLE_VOICES; i++)
3102 audio->sample_voices[i].active = 0;
3103 __sync_synchronize();
3104
3105 int direct_len = audio->sample_write_pos;
3106 if (direct_len > 0) {
3107 audio->sample_len = direct_len;
3108 ac_log("[mic] recording stopped (direct), sample_len=%d sample_rate=%u\n",
3109 audio->sample_len, audio->sample_rate);
3110 } else {
3111 // Fallback: extract from ring buffer
3112 int start = audio->rec_start_ring_pos;
3113 int end = audio->mic_ring_pos;
3114 int len = end - start;
3115 if (len < 0) len = 0;
3116 if (len > audio->sample_max_len) len = audio->sample_max_len;
3117 for (int i = 0; i < len; i++) {
3118 audio->sample_buf[i] = audio->mic_ring[(start + i) % audio->sample_max_len];
3119 }
3120 audio->sample_len = len;
3121 ac_log("[mic] recording stopped (ring), sample_len=%d ring_span=%d sample_rate=%u\n",
3122 audio->sample_len, end - start, audio->sample_rate);
3123 }
3124 // Auto-trim silence from start (threshold: ~0.01 = -40dB)
3125 if (audio->sample_len > 0) {
3126 const float trim_threshold = 0.01f;
3127 int trim_start = 0;
3128 while (trim_start < audio->sample_len &&
3129 fabsf(audio->sample_buf[trim_start]) < trim_threshold) {
3130 trim_start++;
3131 }
3132 if (trim_start > 0 && trim_start < audio->sample_len) {
3133 int new_len = audio->sample_len - trim_start;
3134 memmove(audio->sample_buf, audio->sample_buf + trim_start,
3135 new_len * sizeof(float));
3136 audio->sample_len = new_len;
3137 ac_log("[mic] auto-trimmed %d silent samples from start\n", trim_start);
3138 }
3139 }
3140
3141 return audio->sample_len;
3142}
3143
3144// --- Sample bank: get/load data for per-key samples ---
3145int audio_sample_get_data(ACAudio *audio, float *out, int max_len) {
3146 if (!audio || !out || audio->sample_len == 0) return 0;
3147 int len = audio->sample_len < max_len ? audio->sample_len : max_len;
3148 memcpy(out, audio->sample_buf, len * sizeof(float));
3149 return len;
3150}
3151
3152int audio_output_get_recent(ACAudio *audio, float *out, int max_len, unsigned int *out_rate) {
3153 if (!audio || !out || max_len <= 0 || !audio->output_history_buf || audio->output_history_size <= 0) {
3154 if (out_rate) *out_rate = 0;
3155 return 0;
3156 }
3157
3158 pthread_mutex_lock(&audio->lock);
3159 if (out_rate) *out_rate = audio->output_history_rate;
3160
3161 uint64_t write_pos = audio->output_history_write_pos;
3162 int available = write_pos < (uint64_t)audio->output_history_size
3163 ? (int)write_pos
3164 : audio->output_history_size;
3165 int len = available < max_len ? available : max_len;
3166 uint64_t start = write_pos - (uint64_t)len;
3167 for (int i = 0; i < len; i++) {
3168 out[i] = audio->output_history_buf[(start + (uint64_t)i) % (uint64_t)audio->output_history_size];
3169 }
3170
3171 pthread_mutex_unlock(&audio->lock);
3172 return len;
3173}
3174
3175void audio_sample_load_data(ACAudio *audio, const float *data, int len, unsigned int rate) {
3176 if (!audio || !data || len <= 0 || !audio->sample_buf_back) return;
3177 if (len > audio->sample_max_len) len = audio->sample_max_len;
3178 // Write to back buffer (only JS thread writes here — safe without lock)
3179 memcpy(audio->sample_buf_back, data, len * sizeof(float));
3180 if (len < audio->sample_max_len)
3181 memset(audio->sample_buf_back + len, 0, (audio->sample_max_len - len) * sizeof(float));
3182 // Swap pointers under lock — audio callback checks sample_loading flag
3183 pthread_mutex_lock(&audio->lock);
3184 float *tmp = audio->sample_buf;
3185 audio->sample_buf = audio->sample_buf_back;
3186 audio->sample_buf_back = tmp;
3187 audio->sample_len = len;
3188 if (rate > 0) audio->sample_rate = rate;
3189 __sync_synchronize();
3190 pthread_mutex_unlock(&audio->lock);
3191 // Log peak value and first few samples for debugging
3192 float peak = 0.0f;
3193 for (int i = 0; i < len; i++) {
3194 float a = fabsf(audio->sample_buf[i]);
3195 if (a > peak) peak = a;
3196 }
3197 ac_log("[sample] loaded %d samples (%d Hz) peak=%.4f first=[%.3f,%.3f,%.3f,%.3f]\n",
3198 len, audio->sample_rate, peak,
3199 len > 0 ? audio->sample_buf[0] : 0,
3200 len > 1 ? audio->sample_buf[1] : 0,
3201 len > 2 ? audio->sample_buf[2] : 0,
3202 len > 3 ? audio->sample_buf[3] : 0);
3203}
3204
3205void audio_replay_load_data(ACAudio *audio, const float *data, int len, unsigned int rate) {
3206 if (!audio || !data || len <= 0 || !audio->replay_buf_back) return;
3207 if (len > audio->replay_max_len) len = audio->replay_max_len;
3208
3209 memcpy(audio->replay_buf_back, data, len * sizeof(float));
3210 if (len < audio->replay_max_len)
3211 memset(audio->replay_buf_back + len, 0, (audio->replay_max_len - len) * sizeof(float));
3212
3213 pthread_mutex_lock(&audio->lock);
3214 audio->replay_voice.active = 0;
3215 float *tmp = audio->replay_buf;
3216 audio->replay_buf = audio->replay_buf_back;
3217 audio->replay_buf_back = tmp;
3218 audio->replay_len = len;
3219 if (rate > 0) audio->replay_rate = rate;
3220 __sync_synchronize();
3221 pthread_mutex_unlock(&audio->lock);
3222}
3223
3224// --- Sample playback ---
3225uint64_t audio_sample_play(ACAudio *audio, double freq, double base_freq,
3226 double volume, double pan, int loop) {
3227 if (!audio || audio->sample_len == 0) return 0;
3228 pthread_mutex_lock(&audio->lock);
3229
3230 // Find free slot (or steal oldest)
3231 int slot = -1;
3232 for (int i = 0; i < AUDIO_MAX_SAMPLE_VOICES; i++) {
3233 if (!audio->sample_voices[i].active) { slot = i; break; }
3234 }
3235 if (slot < 0) slot = 0; // steal first
3236
3237 SampleVoice *sv = &audio->sample_voices[slot];
3238 sv->active = 1;
3239 sv->loop = loop;
3240 sv->position = 0.0;
3241 // Speed: pitch ratio * rate conversion (capture rate → output rate)
3242 sv->speed = (freq / base_freq) * ((double)audio->sample_rate / (double)audio->actual_rate);
3243 sv->volume = volume;
3244 sv->pan = pan;
3245 sv->fade = 0.0;
3246 sv->fade_target = 1.0;
3247 sv->id = audio->sample_next_id++;
3248
3249 pthread_mutex_unlock(&audio->lock);
3250 ac_log("[sample] play freq=%.1f base=%.1f speed=%.4f rate=%u/%u len=%d id=%lu\n",
3251 freq, base_freq, sv->speed, audio->sample_rate, audio->actual_rate,
3252 audio->sample_len, (unsigned long)sv->id);
3253 return sv->id;
3254}
3255
3256uint64_t audio_replay_play(ACAudio *audio, double freq, double base_freq,
3257 double volume, double pan, int loop) {
3258 if (!audio || audio->replay_len == 0) return 0;
3259 pthread_mutex_lock(&audio->lock);
3260
3261 SampleVoice *sv = &audio->replay_voice;
3262 sv->active = 1;
3263 sv->loop = loop;
3264 sv->position = 0.0;
3265 sv->speed = (freq / base_freq) * ((double)audio->replay_rate / (double)audio->actual_rate);
3266 sv->volume = volume;
3267 sv->pan = pan;
3268 sv->fade = 0.0;
3269 sv->fade_target = 1.0;
3270 sv->id = audio->sample_next_id++;
3271
3272 pthread_mutex_unlock(&audio->lock);
3273 return sv->id;
3274}
3275
3276void audio_sample_kill(ACAudio *audio, uint64_t id, double fade) {
3277 if (!audio) return;
3278 pthread_mutex_lock(&audio->lock);
3279 for (int i = 0; i < AUDIO_MAX_SAMPLE_VOICES; i++) {
3280 if (audio->sample_voices[i].active && audio->sample_voices[i].id == id) {
3281 if (fade <= 0.001) {
3282 audio->sample_voices[i].active = 0;
3283 } else {
3284 audio->sample_voices[i].fade_target = 0.0;
3285 }
3286 break;
3287 }
3288 }
3289 pthread_mutex_unlock(&audio->lock);
3290}
3291
3292void audio_replay_kill(ACAudio *audio, uint64_t id, double fade) {
3293 if (!audio) return;
3294 pthread_mutex_lock(&audio->lock);
3295 SampleVoice *sv = &audio->replay_voice;
3296 if (sv->active && sv->id == id) {
3297 if (fade <= 0.001) sv->active = 0;
3298 else sv->fade_target = 0.0;
3299 }
3300 pthread_mutex_unlock(&audio->lock);
3301}
3302
3303void audio_sample_update(ACAudio *audio, uint64_t id, double freq,
3304 double base_freq, double volume, double pan) {
3305 if (!audio) return;
3306 pthread_mutex_lock(&audio->lock);
3307 for (int i = 0; i < AUDIO_MAX_SAMPLE_VOICES; i++) {
3308 SampleVoice *sv = &audio->sample_voices[i];
3309 if (sv->active && sv->id == id) {
3310 if (freq > 0 && base_freq > 0)
3311 sv->speed = (freq / base_freq) * ((double)audio->sample_rate / (double)audio->actual_rate);
3312 if (volume >= 0) sv->volume = volume;
3313 if (pan > -2) sv->pan = pan;
3314 break;
3315 }
3316 }
3317 pthread_mutex_unlock(&audio->lock);
3318}
3319
3320void audio_replay_update(ACAudio *audio, uint64_t id, double freq,
3321 double base_freq, double volume, double pan) {
3322 if (!audio) return;
3323 pthread_mutex_lock(&audio->lock);
3324 SampleVoice *sv = &audio->replay_voice;
3325 if (sv->active && sv->id == id) {
3326 if (freq > 0 && base_freq > 0)
3327 sv->speed = (freq / base_freq) * ((double)audio->replay_rate / (double)audio->actual_rate);
3328 if (volume >= 0) sv->volume = volume;
3329 if (pan > -2) sv->pan = pan;
3330 }
3331 pthread_mutex_unlock(&audio->lock);
3332}
3333
3334// Read current Master mixer volume as 0-100 percentage
3335static int read_system_volume_card(int card) {
3336 snd_mixer_t *mixer = NULL;
3337 if (snd_mixer_open(&mixer, 0) < 0) return -1;
3338 char card_name[16];
3339 snprintf(card_name, sizeof(card_name), "hw:%d", card);
3340 snd_mixer_attach(mixer, card_name);
3341 snd_mixer_selem_register(mixer, NULL, NULL);
3342 snd_mixer_load(mixer);
3343
3344 int pct = -1;
3345 snd_mixer_elem_t *elem;
3346 for (elem = snd_mixer_first_elem(mixer); elem; elem = snd_mixer_elem_next(elem)) {
3347 if (!snd_mixer_selem_is_active(elem)) continue;
3348 if (strcasecmp(snd_mixer_selem_get_name(elem), "Master") != 0) continue;
3349 if (snd_mixer_selem_has_playback_volume(elem)) {
3350 long min, max, cur;
3351 snd_mixer_selem_get_playback_volume_range(elem, &min, &max);
3352 snd_mixer_selem_get_playback_volume(elem, 0, &cur);
3353 if (max > min) pct = (int)((cur - min) * 100 / (max - min));
3354 }
3355 break;
3356 }
3357 snd_mixer_close(mixer);
3358 return pct;
3359}
3360
3361static int muted = 0;
3362static long pre_mute_volume = -1;
3363
3364// Unmute all playback switches in the mixer — but skip jack-gated
3365// ones so we don't re-enable the headphone DAPM path (see audio_init
3366// above for the MAX98360A silencing story).
3367static void unmute_all_switches(snd_mixer_t *mixer) {
3368 snd_mixer_elem_t *elem;
3369 const char *jack_gated[] = {
3370 "Headphone Jack", "Headphone Jack Switch",
3371 "HPOL Playback", "HPOR Playback", "Headset", NULL
3372 };
3373 for (elem = snd_mixer_first_elem(mixer); elem; elem = snd_mixer_elem_next(elem)) {
3374 if (!snd_mixer_selem_is_active(elem)) continue;
3375 if (!snd_mixer_selem_has_playback_switch(elem)) continue;
3376 const char *name = snd_mixer_selem_get_name(elem);
3377 int skip = 0;
3378 for (int j = 0; jack_gated[j]; j++) {
3379 if (name && strstr(name, jack_gated[j])) { skip = 1; break; }
3380 }
3381 if (!skip)
3382 snd_mixer_selem_set_playback_switch_all(elem, 1);
3383 }
3384}
3385
3386void audio_volume_adjust(ACAudio *audio, int delta) {
3387 if (!audio || !audio->pcm) return;
3388
3389 char card_name[16];
3390 snprintf(card_name, sizeof(card_name), "hw:%d", audio->card_index);
3391
3392 snd_mixer_t *mixer = NULL;
3393 if (snd_mixer_open(&mixer, 0) < 0) return;
3394 snd_mixer_attach(mixer, card_name);
3395 snd_mixer_selem_register(mixer, NULL, NULL);
3396 snd_mixer_load(mixer);
3397
3398 // Adjust ALL playback volume elements — on Realtek ALC codecs,
3399 // Master controls digital gain, Speaker/Headphone control the amplifier.
3400 // Both need to be set for audible volume change.
3401 /* RT5682 exposes "DAC1" for digital volume; SOF cards expose
3402 * "PGA*.0 * Master" pipeline PGAs. HDA laptops expose Master/PCM.
3403 * Try everything — first match wins but we run through the whole
3404 * list so volume keys work regardless of hardware. */
3405 const char *try_names[] = {"Master", "Speaker", "Headphone", "PCM",
3406 "DAC1", "DAC2",
3407 "PGA1.0 1 Master", "PGA2.0 2 Master",
3408 "PGA5.0 5 Master", "PGA6.0 6 Master",
3409 "PGA7.0 7 Master", NULL};
3410 int adjusted = 0;
3411 for (int n = 0; try_names[n]; n++) {
3412 snd_mixer_elem_t *elem = NULL;
3413 for (snd_mixer_elem_t *e = snd_mixer_first_elem(mixer); e; e = snd_mixer_elem_next(e)) {
3414 if (!snd_mixer_selem_is_active(e)) continue;
3415 const char *name = snd_mixer_selem_get_name(e);
3416 if (strcasecmp(name, try_names[n]) == 0 && snd_mixer_selem_has_playback_volume(e))
3417 { elem = e; break; }
3418 }
3419 if (!elem) continue;
3420
3421 if (delta == 0) {
3422 // Toggle mute
3423 long min, max, cur;
3424 snd_mixer_selem_get_playback_volume_range(elem, &min, &max);
3425 snd_mixer_selem_get_playback_volume(elem, 0, &cur);
3426 if (!muted) {
3427 pre_mute_volume = cur;
3428 snd_mixer_selem_set_playback_volume_all(elem, min);
3429 } else {
3430 long restore = (pre_mute_volume > min) ? pre_mute_volume : max * 80 / 100;
3431 snd_mixer_selem_set_playback_volume_all(elem, restore);
3432 }
3433 ac_log("[audio] volume: mute toggle '%s' on %s\n", try_names[n], card_name);
3434 } else {
3435 long min, max, cur;
3436 snd_mixer_selem_get_playback_volume_range(elem, &min, &max);
3437 snd_mixer_selem_get_playback_volume(elem, 0, &cur);
3438 long step = (max - min) * 5 / 100;
3439 if (step < 1) step = 1;
3440 long newvol = cur + step * delta;
3441 if (newvol < min) newvol = min;
3442 if (newvol > max) newvol = max;
3443 snd_mixer_selem_set_playback_volume_all(elem, newvol);
3444 ac_log("[audio] volume: '%s' %ld→%ld (range %ld-%ld)\n", try_names[n], cur, newvol, min, max);
3445 }
3446 adjusted++;
3447 }
3448 if (delta == 0) { muted = !muted; }
3449 if (adjusted) {
3450 unmute_all_switches(mixer);
3451 if (delta != 0) muted = 0;
3452 } else {
3453 // No elements found — log what's available
3454 ac_log("[audio] volume: no playback elements on %s. Available:\n", card_name);
3455 for (snd_mixer_elem_t *e = snd_mixer_first_elem(mixer); e; e = snd_mixer_elem_next(e))
3456 ac_log("[audio] %s%s\n", snd_mixer_selem_get_name(e),
3457 snd_mixer_selem_has_playback_volume(e) ? " [vol]" : "");
3458 }
3459 snd_mixer_close(mixer);
3460
3461 // Update cached system volume. On SOF cards without a "Master" mixer,
3462 // read_system_volume_card returns -1. In that case, use software-only
3463 // volume: start at 100 and step ±5 with volume keys.
3464 if (muted) {
3465 audio->system_volume = 0;
3466 } else {
3467 int hw_vol = read_system_volume_card(audio->card_index);
3468 if (hw_vol >= 0) {
3469 audio->system_volume = hw_vol;
3470 } else {
3471 // No Master mixer — software gain mode
3472 int sv = audio->system_volume;
3473 if (sv < 0) sv = 100; // first call: default 100%
3474 if (delta > 0) sv = (sv + 5 > 100) ? 100 : sv + 5;
3475 else if (delta < 0) sv = (sv - 5 < 0) ? 0 : sv - 5;
3476 audio->system_volume = sv;
3477 ac_log("[audio] Software volume: %d%%\n", sv);
3478 }
3479 }
3480}
3481
3482void audio_boot_beep(ACAudio *audio) {
3483 if (!audio || !audio->pcm) return;
3484 // Two-tone "doo-dah" — distinct from old single ping (OTA test marker)
3485 audio_synth(audio, WAVE_SINE, 660.0, 0.12, 0.8, 0.002, 0.08, -0.15); // E5
3486 usleep(80000);
3487 audio_synth(audio, WAVE_SINE, 990.0, 0.15, 0.9, 0.002, 0.10, 0.15); // B5
3488}
3489
3490// Prewarm: play a near-silent note so ALSA buffers are filled and ready
3491void audio_prewarm(ACAudio *audio) {
3492 if (!audio || !audio->pcm) return;
3493 audio_synth(audio, WAVE_SINE, 440.0, 0.05, 0.001, 0.001, 0.04, 0.0);
3494}
3495
3496void audio_ready_melody(ACAudio *audio) {
3497 if (!audio || !audio->pcm) return;
3498 // Quick ascending 3-note toot: C5 → E5 → G5 (major triad) at full volume
3499 audio_synth(audio, WAVE_TRIANGLE, 523.25, 0.15, 0.7, 0.003, 0.10, -0.2); // C5
3500 usleep(60000); // 60ms gap
3501 audio_synth(audio, WAVE_TRIANGLE, 659.25, 0.15, 0.7, 0.003, 0.10, 0.0); // E5
3502 usleep(60000);
3503 audio_synth(audio, WAVE_TRIANGLE, 783.99, 0.20, 0.8, 0.003, 0.14, 0.2); // G5
3504}
3505
3506void audio_shutdown_sound(ACAudio *audio) {
3507 if (!audio || !audio->pcm) return;
3508 // Descending 3-note chime: G5 → E5 → C5 at full volume
3509 audio_synth(audio, WAVE_TRIANGLE, 783.99, 0.15, 0.7, 0.003, 0.10, 0.2); // G5
3510 usleep(60000);
3511 audio_synth(audio, WAVE_TRIANGLE, 659.25, 0.15, 0.7, 0.003, 0.10, 0.0); // E5
3512 usleep(60000);
3513 audio_synth(audio, WAVE_TRIANGLE, 523.25, 0.20, 0.8, 0.003, 0.14, -0.2); // C5
3514 // Wait for notes to finish playing before shutdown
3515 usleep(250000);
3516}
3517
3518// Save sample buffer to disk as raw floats with a small header
3519// Format: [uint32_t sample_rate] [uint32_t sample_len] [float * sample_len]
3520int audio_sample_save(ACAudio *audio, const char *path) {
3521 if (!audio || !audio->sample_buf || audio->sample_len <= 0) return -1;
3522 FILE *f = fopen(path, "wb");
3523 if (!f) return -1;
3524 uint32_t rate = (uint32_t)audio->sample_rate;
3525 uint32_t len = (uint32_t)audio->sample_len;
3526 fwrite(&rate, sizeof(rate), 1, f);
3527 fwrite(&len, sizeof(len), 1, f);
3528 fwrite(audio->sample_buf, sizeof(float), len, f);
3529 fclose(f);
3530 sync();
3531 return (int)len;
3532}
3533
3534// Load sample buffer from disk
3535int audio_sample_load(ACAudio *audio, const char *path) {
3536 if (!audio || !audio->sample_buf) return -1;
3537 FILE *f = fopen(path, "rb");
3538 if (!f) return -1;
3539 uint32_t rate, len;
3540 if (fread(&rate, sizeof(rate), 1, f) != 1 ||
3541 fread(&len, sizeof(len), 1, f) != 1) {
3542 fclose(f);
3543 return -1;
3544 }
3545 if (len > (uint32_t)audio->sample_max_len) len = (uint32_t)audio->sample_max_len;
3546 if (fread(audio->sample_buf, sizeof(float), len, f) != len) {
3547 fclose(f);
3548 return -1;
3549 }
3550 fclose(f);
3551 audio->sample_len = (int)len;
3552 audio->sample_rate = (int)rate;
3553 return (int)len;
3554}
3555
3556// --- DJ deck API ---
3557
3558int audio_deck_load(ACAudio *audio, int deck, const char *path) {
3559 if (!audio || deck < 0 || deck >= AUDIO_MAX_DECKS) return -1;
3560 ACDeck *dk = &audio->decks[deck];
3561
3562 // Create decoder if needed
3563 if (!dk->decoder) {
3564 dk->decoder = deck_decoder_create(audio->actual_rate);
3565 if (!dk->decoder) return -1;
3566 }
3567
3568 dk->playing = 0;
3569 dk->active = 0;
3570 int ret = deck_decoder_load(dk->decoder, path);
3571 if (ret == 0) {
3572 dk->active = 1;
3573 // Generate waveform peaks for visualization (decoded in background thread)
3574 deck_decoder_generate_peaks(dk->decoder, 1024);
3575 }
3576 return ret;
3577}
3578
3579void audio_deck_play(ACAudio *audio, int deck) {
3580 if (!audio || deck < 0 || deck >= AUDIO_MAX_DECKS) return;
3581 ACDeck *dk = &audio->decks[deck];
3582 if (!dk->active || !dk->decoder) return;
3583 dk->playing = 1;
3584 deck_decoder_play(dk->decoder);
3585}
3586
3587void audio_deck_pause(ACAudio *audio, int deck) {
3588 if (!audio || deck < 0 || deck >= AUDIO_MAX_DECKS) return;
3589 ACDeck *dk = &audio->decks[deck];
3590 if (!dk->decoder) return;
3591 dk->playing = 0;
3592 deck_decoder_pause(dk->decoder);
3593}
3594
3595void audio_deck_seek(ACAudio *audio, int deck, double seconds) {
3596 if (!audio || deck < 0 || deck >= AUDIO_MAX_DECKS) return;
3597 ACDeck *dk = &audio->decks[deck];
3598 if (!dk->active || !dk->decoder) return;
3599 deck_decoder_seek(dk->decoder, seconds);
3600}
3601
3602void audio_deck_set_speed(ACAudio *audio, int deck, double speed) {
3603 if (!audio || deck < 0 || deck >= AUDIO_MAX_DECKS) return;
3604 ACDeck *dk = &audio->decks[deck];
3605 if (!dk->decoder) return;
3606 deck_decoder_set_speed(dk->decoder, speed);
3607}
3608
3609void audio_deck_set_volume(ACAudio *audio, int deck, float vol) {
3610 if (!audio || deck < 0 || deck >= AUDIO_MAX_DECKS) return;
3611 if (vol < 0.0f) vol = 0.0f;
3612 if (vol > 1.0f) vol = 1.0f;
3613 audio->decks[deck].volume = vol;
3614}
3615
3616void audio_deck_set_crossfader(ACAudio *audio, float value) {
3617 if (!audio) return;
3618 if (value < 0.0f) value = 0.0f;
3619 if (value > 1.0f) value = 1.0f;
3620 audio->crossfader = value;
3621}
3622
3623void audio_deck_set_master_volume(ACAudio *audio, float value) {
3624 if (!audio) return;
3625 if (value < 0.0f) value = 0.0f;
3626 if (value > 1.0f) value = 1.0f;
3627 audio->deck_master_volume = value;
3628}
3629
3630void audio_destroy(ACAudio *audio) {
3631 if (!audio) return;
3632 audio->running = 0;
3633 audio_mic_close(audio);
3634 // Destroy DJ decks
3635 for (int d = 0; d < AUDIO_MAX_DECKS; d++) {
3636 if (audio->decks[d].decoder) {
3637 deck_decoder_destroy(audio->decks[d].decoder);
3638 audio->decks[d].decoder = NULL;
3639 }
3640 }
3641 if (audio->pcm) {
3642 pthread_join(audio->thread, NULL);
3643 snd_pcm_close((snd_pcm_t *)audio->pcm);
3644 }
3645 if (audio->headphone_pcm) snd_pcm_close((snd_pcm_t *)audio->headphone_pcm);
3646 if (audio->hdmi_pcm) snd_pcm_close((snd_pcm_t *)audio->hdmi_pcm);
3647 free(audio->room_buf_l);
3648 free(audio->room_buf_r);
3649 free(audio->sample_buf);
3650 free(audio->sample_buf_back);
3651 free(audio->mic_ring);
3652 free(audio->replay_buf);
3653 free(audio->replay_buf_back);
3654 free(audio->output_history_buf);
3655 free(audio->tts_buf);
3656 pthread_mutex_destroy(&audio->lock);
3657 free(audio);
3658}