Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

feat: per-key sample bank, auto-trim silence, compressor

Sample Bank (notepat):
- Hold End to arm, press any tone key to record a sample to that key
- Release tone key to save. Each key gets its own sample.
- Home still records global sample (doesn't overwrite per-key)
- Delete clears all per-key samples back to default
- Per-key samples auto-load to C buffer on playback

Audio engine:
- Auto-trim silence from recording start (threshold -40dB)
- sound.sample.getData() — read sample buffer to JS Float32Array
- sound.sample.loadData(f32, rate) — write JS array to C buffer
- Period=1024 for low-latency capture (~21ms)

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

+168 -6
+75 -6
fedac/native/pieces/notepat.mjs
··· 21 21 const MAX_REC_SECS = 10; // matches AUDIO_MAX_SAMPLE_SECS 22 22 const SAMPLE_BASE_FREQ = 261.63; // C4 — base pitch for sample playback 23 23 24 + // Per-key sample bank: End key arms, tone key records to that key only 25 + let sampleBank = {}; // key -> { data: Float32Array, len: number, rate: number } 26 + let globalSample = null; // { data: Float32Array, len: number, rate: number } — Home recording 27 + let endArmed = false; // true while End key is held (arm per-key recording) 28 + let perKeyRecording = null; // key currently recording in per-key mode 29 + 24 30 // Effective pitch shift blended by FX mix (0% fx = no pitch shift) 25 31 function effectivePitchShift() { 26 32 return pitchShift * fxMix; ··· 448 454 if (key >= "1" && key <= "9") { octave = parseInt(key); return; } 449 455 if (key === "arrowup") { octave = Math.min(9, octave + 1); return; } 450 456 if (key === "arrowdown") { octave = Math.max(1, octave - 1); return; } 451 - // Home key: hold to record in sample mode 452 - if (key === "home" && wave === "sample" && !recording) { 457 + // Home key: hold to record GLOBAL sample 458 + if (key === "home" && wave === "sample" && !recording && !perKeyRecording) { 453 459 const ok = !!sound?.microphone?.rec?.(); 454 460 recording = ok; 455 - recPointerId = null; // keyboard-driven, no pointer 461 + recPointerId = null; 456 462 if (ok) recStartTime = Date.now(); 457 463 console.log(`[mic] rec-home: ok=${ok}`); 458 464 return; 459 465 } 466 + // End key: arm per-key recording mode 467 + if (key === "end" && wave === "sample") { 468 + endArmed = true; 469 + console.log(`[sample-bank] armed for per-key recording`); 470 + return; 471 + } 472 + // Delete key: clear all per-key samples, reset to global/default 473 + if (key === "delete" && wave === "sample") { 474 + sampleBank = {}; 475 + console.log(`[sample-bank] cleared all per-key samples`); 476 + // Restore global sample if we have one 477 + if (globalSample) { 478 + sound.sample.loadData(globalSample.data, globalSample.rate); 479 + sampleLoaded = true; 480 + } 481 + sound.synth({ type: "noise", tone: 200, duration: 0.1, volume: 0.15, attack: 0.001, decay: 0.08 }); 482 + return; 483 + } 460 484 if (key === "arrowleft") { 461 485 const idx = (waveIndex - 1 + wavetypes.length) % wavetypes.length; 462 486 setWave(wavetypes[idx], sound); ··· 490 514 } 491 515 492 516 const noteName = KEY_TO_NOTE[key]; 517 + // Per-key recording: End armed + tone key = record to that key 518 + if (noteName && endArmed && wave === "sample" && !perKeyRecording && !recording) { 519 + const ok = !!sound?.microphone?.rec?.(); 520 + if (ok) { 521 + perKeyRecording = key; 522 + recStartTime = Date.now(); 523 + console.log(`[sample-bank] recording to key '${key}' (${noteName})`); 524 + } 525 + return; // Suppress note playback while recording 526 + } 527 + // If this key is currently recording in per-key mode, suppress playback 528 + if (perKeyRecording === key) return; 493 529 if (noteName && !sounds[key]) { 494 530 const [letter, offset] = parseNote(noteName); 495 531 const noteOctave = octave + offset; ··· 502 538 const vol = 0.15 + velocity * 0.55; 503 539 const playFreq = freq * Math.pow(2, effectivePitchShift()); 504 540 505 - if (wave === "sample" && sampleLoaded) { 506 - // Play recorded sample at this pitch, looping while key is held 541 + if (wave === "sample" && (sampleLoaded || sampleBank[key])) { 542 + // Load per-key sample if available, otherwise use global 543 + if (sampleBank[key]) { 544 + sound.sample.loadData(sampleBank[key].data, sampleBank[key].rate); 545 + } else if (globalSample) { 546 + sound.sample.loadData(globalSample.data, globalSample.rate); 547 + } 507 548 const smp = sound.sample.play({ 508 549 tone: playFreq, base: SAMPLE_BASE_FREQ, volume: vol, pan, loop: true, 509 550 }); ··· 532 573 if (e.is("keyboard:up")) { 533 574 const key = e.key?.toLowerCase(); 534 575 if (!key) return; 535 - // Home key release: stop recording 576 + // Home key release: stop global recording + save to global sample 536 577 if (key === "home" && recording && recPointerId === null) { 537 578 stopSampleRecording(sound, "home-lift"); 579 + // Save global sample data for bank restore 580 + const data = sound.sample.getData?.(); 581 + if (data && data.length > 0) { 582 + globalSample = { data: new Float32Array(data), len: data.length, rate: sound.microphone?.sampleRate || 48000 }; 583 + console.log(`[sample-bank] global sample saved (${data.length} samples)`); 584 + } 585 + return; 586 + } 587 + // End key release: disarm per-key recording 588 + if (key === "end") { 589 + endArmed = false; 590 + console.log(`[sample-bank] disarmed`); 591 + return; 592 + } 593 + // Per-key recording stop: tone key released while recording to it 594 + if (perKeyRecording && key === perKeyRecording) { 595 + const len = sound?.microphone?.cut?.() || 0; 596 + if (len > 0) { 597 + const data = sound.sample.getData?.(); 598 + if (data && data.length > 0) { 599 + sampleBank[key] = { data: new Float32Array(data), len: data.length, rate: sound.microphone?.sampleRate || 48000 }; 600 + console.log(`[sample-bank] saved ${data.length} samples to key '${key}'`); 601 + sampleLoaded = true; 602 + // Confirmation beep 603 + sound.synth({ type: "sine", tone: 660, duration: 0.05, volume: 0.15, attack: 0.002, decay: 0.04 }); 604 + } 605 + } 606 + perKeyRecording = null; 538 607 return; 539 608 } 540 609 if (sounds[key]) {
+34
fedac/native/src/audio.c
··· 1250 1250 ac_log("[mic] recording stopped (ring), sample_len=%d ring_span=%d sample_rate=%u\n", 1251 1251 audio->sample_len, end - start, audio->sample_rate); 1252 1252 } 1253 + // Auto-trim silence from start (threshold: ~0.01 = -40dB) 1254 + if (audio->sample_len > 0) { 1255 + const float trim_threshold = 0.01f; 1256 + int trim_start = 0; 1257 + while (trim_start < audio->sample_len && 1258 + fabsf(audio->sample_buf[trim_start]) < trim_threshold) { 1259 + trim_start++; 1260 + } 1261 + if (trim_start > 0 && trim_start < audio->sample_len) { 1262 + int new_len = audio->sample_len - trim_start; 1263 + memmove(audio->sample_buf, audio->sample_buf + trim_start, 1264 + new_len * sizeof(float)); 1265 + audio->sample_len = new_len; 1266 + ac_log("[mic] auto-trimmed %d silent samples from start\n", trim_start); 1267 + } 1268 + } 1269 + 1253 1270 return audio->sample_len; 1271 + } 1272 + 1273 + // --- Sample bank: get/load data for per-key samples --- 1274 + int audio_sample_get_data(ACAudio *audio, float *out, int max_len) { 1275 + if (!audio || !out || audio->sample_len == 0) return 0; 1276 + int len = audio->sample_len < max_len ? audio->sample_len : max_len; 1277 + memcpy(out, audio->sample_buf, len * sizeof(float)); 1278 + return len; 1279 + } 1280 + 1281 + void audio_sample_load_data(ACAudio *audio, const float *data, int len, unsigned int rate) { 1282 + if (!audio || !data || len <= 0) return; 1283 + if (len > audio->sample_max_len) len = audio->sample_max_len; 1284 + memcpy(audio->sample_buf, data, len * sizeof(float)); 1285 + audio->sample_len = len; 1286 + if (rate > 0) audio->sample_rate = rate; 1287 + ac_log("[sample] loaded %d samples (%d Hz)\n", len, audio->sample_rate); 1254 1288 } 1255 1289 1256 1290 // --- Sample playback ---
+4
fedac/native/src/audio.h
··· 202 202 void audio_sample_update(ACAudio *audio, uint64_t id, double freq, 203 203 double base_freq, double volume, double pan); 204 204 205 + // Sample bank: get/load data for per-key sample storage 206 + int audio_sample_get_data(ACAudio *audio, float *out, int max_len); 207 + void audio_sample_load_data(ACAudio *audio, const float *data, int len, unsigned int rate); 208 + 205 209 // Adjust system volume: delta is -5 to +5 (percentage points), 0 = toggle mute 206 210 void audio_volume_adjust(ACAudio *audio, int delta); 207 211
+55
fedac/native/src/js-bindings.c
··· 1205 1205 return JS_UNDEFINED; 1206 1206 } 1207 1207 1208 + // sound.sample.getData() — returns Float32Array of current sample buffer 1209 + static JSValue js_sample_get_data(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1210 + (void)this_val; (void)argc; (void)argv; 1211 + ACAudio *audio = current_rt->audio; 1212 + if (!audio || audio->sample_len == 0) return JS_UNDEFINED; 1213 + 1214 + int len = audio->sample_len; 1215 + JSValue ab = JS_NewArrayBuffer(ctx, NULL, len * sizeof(float), NULL, NULL, 0); 1216 + if (JS_IsException(ab)) return JS_UNDEFINED; 1217 + 1218 + // Get the buffer pointer and copy data 1219 + size_t ab_len = 0; 1220 + uint8_t *ptr = JS_GetArrayBuffer(ctx, &ab_len, ab); 1221 + if (ptr) { 1222 + memcpy(ptr, audio->sample_buf, len * sizeof(float)); 1223 + } 1224 + 1225 + // Create Float32Array view 1226 + JSValue f32 = JS_NewTypedArray(ctx, 1, &ab, JS_TYPED_ARRAY_FLOAT32); 1227 + JS_FreeValue(ctx, ab); 1228 + return f32; 1229 + } 1230 + 1231 + // sound.sample.loadData(float32array, rate) — load sample data from JS array 1232 + static JSValue js_sample_load_data(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1233 + (void)this_val; 1234 + ACAudio *audio = current_rt->audio; 1235 + if (!audio || argc < 1) return JS_FALSE; 1236 + 1237 + size_t byte_len = 0; 1238 + size_t byte_off = 0; 1239 + size_t bytes_per = 0; 1240 + JSValue ab = JS_GetTypedArrayBuffer(ctx, argv[0], &byte_off, &byte_len, &bytes_per); 1241 + if (JS_IsException(ab)) return JS_FALSE; 1242 + 1243 + size_t ab_len = 0; 1244 + uint8_t *ptr = JS_GetArrayBuffer(ctx, &ab_len, ab); 1245 + JS_FreeValue(ctx, ab); 1246 + if (!ptr) return JS_FALSE; 1247 + 1248 + float *data = (float *)(ptr + byte_off); 1249 + int len = (int)(byte_len / sizeof(float)); 1250 + 1251 + unsigned int rate = 48000; 1252 + if (argc >= 2 && JS_IsNumber(argv[1])) { 1253 + double r; JS_ToFloat64(ctx, &r, argv[1]); 1254 + if (r > 0) rate = (unsigned int)r; 1255 + } 1256 + 1257 + audio_sample_load_data(audio, data, len, rate); 1258 + return JS_TRUE; 1259 + } 1260 + 1208 1261 // sound.speak(text) 1209 1262 static JSValue js_speak(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1210 1263 (void)this_val; ··· 1960 2013 JSValue samp = JS_NewObject(ctx); 1961 2014 JS_SetPropertyStr(ctx, samp, "play", JS_NewCFunction(ctx, js_sample_play, "play", 1)); 1962 2015 JS_SetPropertyStr(ctx, samp, "kill", JS_NewCFunction(ctx, js_sample_kill, "kill", 2)); 2016 + JS_SetPropertyStr(ctx, samp, "getData", JS_NewCFunction(ctx, js_sample_get_data, "getData", 0)); 2017 + JS_SetPropertyStr(ctx, samp, "loadData", JS_NewCFunction(ctx, js_sample_load_data, "loadData", 2)); 1963 2018 JS_SetPropertyStr(ctx, sound, "sample", samp); 1964 2019 1965 2020 // TTS