Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

audio: add master volume + drive (tanh soft-sat) FX

Lands the DSP + JS binding plumbing for the two remaining FX that
notepat has wanted on a slider but didn't have a target for:

sound.volume.setMix(value) 0..2 (0..200%) master output gain
Applied AFTER the existing FX chain and
compressor but BEFORE system_volume (the
ALSA hardware mixer) and soft_clip. So
the slider acts as a per-user soft gain
that the hardware master still rides on top of.

sound.drive.setMix(value) 0..1 dry/wet blend to a tanh soft-clipper
with pre-gain (1+5*mix) and post-attenuation
(0.8). 0 is clean bypass; mid settings add
tube-ish harmonic warmth; 1.0 is obvious
saturation.

Both have the same exponential smoother as fx/room/glitch mixes (≈1 s
time constant at 48 kHz) so slider sweeps don't zipper. Defaults:
master_volume = 1.0 (unity)
drive_mix = 0.0 (bypass)

Follow-up commit will add the UI sliders + trackpad X/Y bindings in
notepat.mjs next to the existing echo/pitch/crush rows. The C path is
landing first so the next oven build has the audio side ready and the
UI work can layer on without a second build round-trip.

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

+104
+58
fedac/native/src/audio.c
··· 1434 1434 audio->glitch_mix += (audio->target_glitch_mix - audio->glitch_mix) * 0.00005f; 1435 1435 } 1436 1436 1437 + // Smooth master volume + drive toward target (same 1s time const) 1438 + if (audio->master_volume != audio->target_master_volume) { 1439 + audio->master_volume += (audio->target_master_volume - audio->master_volume) * 0.00005f; 1440 + } 1441 + if (audio->drive_mix != audio->target_drive_mix) { 1442 + audio->drive_mix += (audio->target_drive_mix - audio->drive_mix) * 0.00005f; 1443 + } 1444 + 1437 1445 // Save dry signal before FX chain 1438 1446 double dry_l = mix_l, dry_r = mix_r; 1439 1447 ··· 1572 1580 mix_l *= reduction; 1573 1581 mix_r *= reduction; 1574 1582 } 1583 + } 1584 + 1585 + // User-controlled drive (tanh soft-saturation) BEFORE system 1586 + // volume so the harmonic character is independent of hardware 1587 + // gain. drive_mix is a dry/wet blend: 0 = pure bypass, 1 = fully 1588 + // driven (pre-gain × 6 into tanh, attenuated back to roughly 1589 + // unity peak). At mid settings you get pleasing tube-ish warmth. 1590 + if (audio->drive_mix > 0.001f) { 1591 + float dm = audio->drive_mix; 1592 + float pre_gain = 1.0f + dm * 5.0f; 1593 + double driven_l = tanh(mix_l * pre_gain) * 0.8; 1594 + double driven_r = tanh(mix_r * pre_gain) * 0.8; 1595 + mix_l = mix_l * (1.0 - dm) + driven_l * dm; 1596 + mix_r = mix_r * (1.0 - dm) + driven_r * dm; 1597 + } 1598 + 1599 + // User-controlled master volume (0..2 = 0..200%). Applied after 1600 + // drive so the slider feels like a "louder/quieter" control that 1601 + // doesn't change the tone character the user dialled in. 1602 + { 1603 + float mv = audio->master_volume; 1604 + mix_l *= mv; 1605 + mix_r *= mv; 1575 1606 } 1576 1607 1577 1608 // Apply system volume (software gain). system_volume can go ··· 1797 1828 audio->target_glitch_mix = 0.0f; 1798 1829 audio->fx_mix = 1.0f; // FX chain fully wet by default 1799 1830 audio->target_fx_mix = 1.0f; 1831 + // User master volume starts at 1.0 (unity gain) — the pre-existing 1832 + // system_volume path still provides the hardware mixer control, so 1833 + // this is a per-user soft gain on top. 1834 + audio->master_volume = 1.0f; 1835 + audio->target_master_volume = 1.0f; 1836 + audio->drive_mix = 0.0f; // Clean bypass until user dials drive 1837 + audio->target_drive_mix = 0.0f; 1800 1838 audio->room_buf_l = calloc(ROOM_SIZE, sizeof(float)); 1801 1839 audio->room_buf_r = calloc(ROOM_SIZE, sizeof(float)); 1802 1840 ··· 2977 3015 if (mix < 0.0f) mix = 0.0f; 2978 3016 if (mix > 1.0f) mix = 1.0f; 2979 3017 audio->target_fx_mix = mix; 3018 + } 3019 + 3020 + // User-exposed master gain. Range 0..2 (200%) — above that you're almost 3021 + // certainly just hitting soft_clip and colouring the signal, so clamp 3022 + // before that to avoid giving false "louder" feedback in the UI slider. 3023 + void audio_set_master_volume(ACAudio *audio, float value) { 3024 + if (!audio) return; 3025 + if (value < 0.0f) value = 0.0f; 3026 + if (value > 2.0f) value = 2.0f; 3027 + audio->target_master_volume = value; 3028 + } 3029 + 3030 + // Drive amount 0..1 dry/wet blend. 0 = clean bypass, 1 = fully driven 3031 + // (pre-gain × 6 into tanh, attenuated back). Smoothed per-sample so 3032 + // sliding the fader doesn't audibly zipper. 3033 + void audio_set_drive_mix(ACAudio *audio, float value) { 3034 + if (!audio) return; 3035 + if (value < 0.0f) value = 0.0f; 3036 + if (value > 1.0f) value = 1.0f; 3037 + audio->target_drive_mix = value; 2980 3038 } 2981 3039 2982 3040 // --- Hot-mic capture thread ---
+14
fedac/native/src/audio.h
··· 253 253 float fx_mix; // 0.0 = fully dry, 1.0 = fully wet (smoothed) 254 254 float target_fx_mix; // target (set by JS, smoothed per sample) 255 255 256 + // User-controlled master output gain (applied right before soft_clip). 257 + // Defaults to 1.0; 0.0 silent; >1.0 amplifies (use carefully — soft_clip 258 + // still protects against speaker-blowing peaks). 259 + float master_volume; 260 + float target_master_volume; 261 + 262 + // Drive / tanh soft-saturation (dry/wet blend). 0.0 = clean pass-through, 263 + // 1.0 = fully driven (pre-gain 6× → tanh → attenuation). Adds harmonic 264 + // warmth at low settings and obvious distortion at high settings. 265 + float drive_mix; 266 + float target_drive_mix; 267 + 256 268 // System mixer volume (0-100 percent) 257 269 int system_volume; 258 270 int card_index; // ALSA card number (0 or 1) ··· 392 404 void audio_set_room_mix(ACAudio *audio, float mix); 393 405 void audio_set_glitch_mix(ACAudio *audio, float mix); 394 406 void audio_set_fx_mix(ACAudio *audio, float mix); 407 + void audio_set_master_volume(ACAudio *audio, float value); 408 + void audio_set_drive_mix(ACAudio *audio, float value); 395 409 396 410 // Microphone — hot-mic mode (device stays open, recording toggles buffering) 397 411 int audio_mic_open(ACAudio *audio); // open device + start hot-mic thread
+32
fedac/native/src/js-bindings.c
··· 1184 1184 return JS_UNDEFINED; 1185 1185 } 1186 1186 1187 + // sound.volume.setMix(value) — user-controlled master output gain (0..2) 1188 + static JSValue js_set_master_volume(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1189 + (void)this_val; 1190 + if (argc < 1 || !current_rt->audio) return JS_UNDEFINED; 1191 + double v; 1192 + JS_ToFloat64(ctx, &v, argv[0]); 1193 + audio_set_master_volume(current_rt->audio, (float)v); 1194 + return JS_UNDEFINED; 1195 + } 1196 + 1197 + // sound.drive.setMix(value) — tanh soft-clip dry/wet blend (0..1) 1198 + static JSValue js_set_drive_mix(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1199 + (void)this_val; 1200 + if (argc < 1 || !current_rt->audio) return JS_UNDEFINED; 1201 + double v; 1202 + JS_ToFloat64(ctx, &v, argv[0]); 1203 + audio_set_drive_mix(current_rt->audio, (float)v); 1204 + return JS_UNDEFINED; 1205 + } 1206 + 1187 1207 // sound.microphone.open() — open device + start hot-mic thread 1188 1208 static JSValue js_mic_open(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1189 1209 (void)this_val; (void)argc; (void)argv; ··· 2848 2868 JS_SetPropertyStr(ctx, fx, "setMix", JS_NewCFunction(ctx, js_set_fx_mix, "setMix", 1)); 2849 2869 JS_SetPropertyStr(ctx, fx, "mix", JS_NewFloat64(ctx, rt->audio ? rt->audio->fx_mix : 1.0)); 2850 2870 JS_SetPropertyStr(ctx, sound, "fx", fx); 2871 + 2872 + // volume (user master output gain) 2873 + JSValue volume = JS_NewObject(ctx); 2874 + JS_SetPropertyStr(ctx, volume, "setMix", JS_NewCFunction(ctx, js_set_master_volume, "setMix", 1)); 2875 + JS_SetPropertyStr(ctx, volume, "mix", JS_NewFloat64(ctx, rt->audio ? rt->audio->master_volume : 1.0)); 2876 + JS_SetPropertyStr(ctx, sound, "volume", volume); 2877 + 2878 + // drive (tanh soft-clip saturation) 2879 + JSValue drive = JS_NewObject(ctx); 2880 + JS_SetPropertyStr(ctx, drive, "setMix", JS_NewCFunction(ctx, js_set_drive_mix, "setMix", 1)); 2881 + JS_SetPropertyStr(ctx, drive, "mix", JS_NewFloat64(ctx, rt->audio ? rt->audio->drive_mix : 0.0)); 2882 + JS_SetPropertyStr(ctx, sound, "drive", drive); 2851 2883 2852 2884 // microphone 2853 2885 JSValue mic = JS_NewObject(ctx);