Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

macos: scripted input + per-frame PNG dump + WAV audio tap

Three mechanisms the demo pipeline needs to turn a live session into a
synced video:

- AC_INJECT_SEQUENCE="key,ms|key,ms|...": timeline of keypresses.
Each entry's ms is relative to the prior event (cumulative delay),
so "n,2300|o,120|t,120|e,120|p,120|a,120|t,120|enter,400" types
'notepat<enter>' starting 2.3 s after launch with 120 ms between
chars. Main loop fires paired keyboard:down + keyboard:up events so
notepat's key-release handlers run naturally.

- AC_FRAME_DUMP_DIR=<dir>: after each render_frame(), upscale the
framebuffer density× nearest-neighbor and write frame_%05d.png.
Pairs with ffmpeg for a clean PNG-sequence-to-video pipeline.

- AC_WAV_OUT=<path>: tap the CoreAudio render callback and append
every frame's interleaved stereo float32 to a WAVE_FORMAT_IEEE_FLOAT
@ 48 kHz file. Crucially the tap state lives at module scope
(g_wav_file / g_wav_samples), not inside struct Audio, so piece
jumps — which destroy + recreate the per-piece audio engine —
don't break the recording. audio_wav_stop() patches RIFF + data
chunk sizes before close. SDL3 backend gets a stub (demo runs
AUDIO=core exclusively).

Verified end-to-end: boot-anim → prompt → type 'notepat<enter>' →
jump into notepat → play 'c d e f' → 5 s of synced 1280x800 h264 +
float32 stereo aac encoded to mkv cleanly.

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

+178
+6
fedac/native/macos/audio.h
··· 40 40 void audio_arm_latency(Audio *a, float threshold); 41 41 uint64_t audio_latency_ns(Audio *a); 42 42 43 + // WAV output tap — writes every frame the audio callback produces to a 44 + // float32 stereo @ 48 kHz WAVE file. Used by the demo recorder to pair 45 + // perfect-sync audio with the per-frame PNG dumps. Returns 1 on success. 46 + int audio_wav_start(Audio *a, const char *path); 47 + void audio_wav_stop (Audio *a); 48 + 43 49 #endif
+66
fedac/native/macos/audio_coreaudio.c
··· 26 26 return (uint64_t)ts.tv_sec * 1000000000ULL + ts.tv_nsec; 27 27 } 28 28 29 + // WAV output tap lives at module scope so it survives piece jumps (which 30 + // destroy + recreate the per-piece Audio struct). The CoreAudio callback 31 + // on each Audio instance reads this pointer and appends its buffer. 32 + // Not guarded by a lock — the callback runs on one thread and main.c 33 + // mutates only via audio_wav_start/stop called between frames. 34 + static FILE *g_wav_file = NULL; 35 + static uint64_t g_wav_samples = 0; 36 + 29 37 struct Audio { 30 38 AudioUnit au; 31 39 SynthCore synth; ··· 43 51 // L/R interleaved; CoreAudio expects non-interleaved on the two buffers 44 52 // of the AudioBufferList). 45 53 float *scratch; 54 + 46 55 int scratch_cap; 47 56 48 57 int actual_frames; // negotiated device buffer size ··· 93 102 if (a->trigger_ns && !a->emit_ns && peak > a->emit_threshold) { 94 103 a->emit_ns = now_ns(); 95 104 } 105 + 106 + // WAV tap: append interleaved stereo float32 samples straight from 107 + // the scratch buffer (the data we're about to hand to CoreAudio). 108 + // Tap state is module-global so piece jumps don't lose the handle. 109 + if (g_wav_file) { 110 + fwrite(a->scratch, sizeof(float), (size_t)need, g_wav_file); 111 + g_wav_samples += frames; 112 + } 113 + (void)need; 96 114 return noErr; 115 + } 116 + 117 + // Open a WAV output tap at module scope so it survives piece jumps. 118 + // Writes a WAVE_FORMAT_IEEE_FLOAT stereo @ 48 kHz header with 119 + // placeholder length fields; audio_wav_stop() patches them. 120 + int audio_wav_start(Audio *a, const char *path) { 121 + (void)a; 122 + if (!path) return 0; 123 + if (g_wav_file) { fclose(g_wav_file); g_wav_file = NULL; } 124 + FILE *f = fopen(path, "wb"); 125 + if (!f) { fprintf(stderr, "[wav] fopen(%s) failed\n", path); return 0; } 126 + uint8_t hdr[44] = { 127 + 'R','I','F','F', 0,0,0,0, // ChunkID + ChunkSize (patched) 128 + 'W','A','V','E', 129 + 'f','m','t',' ', 16,0,0,0, // Subchunk1ID + Size 130 + 0x03,0x00, // AudioFormat = IEEE_FLOAT 131 + 0x02,0x00, // NumChannels = 2 132 + 0x80,0xBB,0x00,0x00, // SampleRate = 48000 133 + 0x00,0xDC,0x05,0x00, // ByteRate = 48000 * 2 * 4 = 384000 134 + 0x08,0x00, // BlockAlign = 2 * 4 = 8 135 + 0x20,0x00, // BitsPerSample = 32 136 + 'd','a','t','a', 0,0,0,0 // Subchunk2ID + Size (patched) 137 + }; 138 + fwrite(hdr, 1, sizeof hdr, f); 139 + g_wav_file = f; 140 + g_wav_samples = 0; 141 + fprintf(stderr, "[wav] tap opened → %s\n", path); 142 + return 1; 143 + } 144 + 145 + // Patch RIFF + data chunk sizes and close. Safe to call even if start 146 + // was never invoked. 147 + void audio_wav_stop(Audio *a) { 148 + (void)a; 149 + if (!g_wav_file) return; 150 + FILE *f = g_wav_file; 151 + g_wav_file = NULL; 152 + fflush(f); 153 + // bytes_per_sample = 4, stereo = 2 channels = 8 bytes per frame. 154 + uint32_t data_bytes = (uint32_t)(g_wav_samples * 8); 155 + uint32_t riff_bytes = 36 + data_bytes; 156 + fseek(f, 4, SEEK_SET); 157 + fwrite(&riff_bytes, 4, 1, f); 158 + fseek(f, 40, SEEK_SET); 159 + fwrite(&data_bytes, 4, 1, f); 160 + fclose(f); 161 + fprintf(stderr, "[wav] tap closed (%llu frames)\n", 162 + (unsigned long long)g_wav_samples); 97 163 } 98 164 99 165 // Request a specific hardware buffer size on the default output device.
+9
fedac/native/macos/audio_sdl3.c
··· 133 133 free(a); 134 134 } 135 135 136 + // WAV tap: not yet implemented for the SDL3 backend (demo pipeline runs 137 + // AUDIO=core). Stub so the header interface stays uniform. 138 + int audio_wav_start(Audio *a, const char *path) { 139 + (void)a; (void)path; 140 + fprintf(stderr, "[wav] SDL3 backend has no WAV tap yet — use AUDIO=core\n"); 141 + return 0; 142 + } 143 + void audio_wav_stop(Audio *a) { (void)a; } 144 + 136 145 uint64_t audio_synth(Audio *a, WaveType w, double freq, double dur, double vol, 137 146 double att, double dec, double pan) { 138 147 return a ? synth_synth(&a->synth, w, freq, dur, vol, att, dec, pan) : 0;
+97
fedac/native/macos/main.c
··· 815 815 // event so notepat's sound.synth path fires in a headless run. 816 816 const char *inject_key = getenv("AC_INJECT_KEY"); 817 817 int injected = 0; 818 + 819 + // AC_INJECT_SEQUENCE="<key>,<ms>|<key>,<ms>|…" — scripted typing 820 + // timeline. The first key fires at <ms> from start; each subsequent 821 + // key fires <ms> after the prior event. Supports multi-char names 822 + // (e.g. "enter", "space", "backspace") for the prompt. Used by the 823 + // demo recorder to type "notepat<enter>" without real input. 824 + const char *seq_env = getenv("AC_INJECT_SEQUENCE"); 825 + typedef struct { char key[32]; int at_ms; } SeqEvent; 826 + SeqEvent *seq = NULL; 827 + int seq_len = 0, seq_cur = 0; 828 + if (seq_env && seq_env[0]) { 829 + // First pass: count segments to allocate. 830 + int count = 1; 831 + for (const char *p = seq_env; *p; p++) if (*p == '|') count++; 832 + seq = calloc(count, sizeof *seq); 833 + int cumul = 0; 834 + const char *p = seq_env; 835 + while (*p && seq_len < count) { 836 + const char *comma = strchr(p, ','); 837 + const char *pipe = strchr(p, '|'); 838 + if (!pipe) pipe = p + strlen(p); 839 + if (!comma || comma > pipe) break; 840 + int klen = (int)(comma - p); 841 + if (klen >= (int)sizeof(seq[0].key)) klen = sizeof(seq[0].key) - 1; 842 + memcpy(seq[seq_len].key, p, klen); 843 + seq[seq_len].key[klen] = 0; 844 + int dly = atoi(comma + 1); 845 + cumul += dly; 846 + seq[seq_len].at_ms = cumul; 847 + seq_len++; 848 + if (!*pipe) break; 849 + p = pipe + 1; 850 + } 851 + fprintf(stderr, "[sequence] parsed %d events from AC_INJECT_SEQUENCE\n", seq_len); 852 + } 853 + 854 + // AC_FRAME_DUMP_DIR=<dir> — dump every rendered frame as 855 + // frame_%05d.png. Used by the demo recorder to turn a live session 856 + // into an mkv. Density-2 upscaling applied so the saved PNGs match 857 + // what you see on screen (chunky retro pixels stay crisp). 858 + const char *frame_dump_dir = getenv("AC_FRAME_DUMP_DIR"); 859 + int frame_dump_idx = 0; 860 + 861 + // AC_WAV_OUT=<path> — tap audio callback output into a float32 stereo 862 + // @ 48 kHz WAVE file. Paired with frame-dump, ffmpeg can mux the two 863 + // into a demo video with sample-accurate sound. 864 + const char *wav_out = getenv("AC_WAV_OUT"); 865 + if (wav_out && wav_out[0]) { 866 + Audio *au = piece_audio(pc); 867 + if (au) audio_wav_start(au, wav_out); 868 + } 869 + 818 870 Uint64 start_tick = SDL_GetTicks(); 819 871 820 872 int running = 1; ··· 830 882 if (headless_ms > 0 && (int)(SDL_GetTicks() - start_tick) >= headless_ms) { 831 883 running = 0; 832 884 break; 885 + } 886 + // Scripted input timeline — dispatch the next pending key when 887 + // its cumulative delay has elapsed. Each dispatch fires a paired 888 + // down+up event so the piece's keyup handlers (notepat releases 889 + // notes on keyup) run naturally. 890 + while (seq && seq_cur < seq_len && 891 + (int)(SDL_GetTicks() - start_tick) >= seq[seq_cur].at_ms) { 892 + PieceEvent pd = {0}, pu = {0}; 893 + snprintf(pd.key, sizeof pd.key, "%s", seq[seq_cur].key); 894 + snprintf(pd.type, sizeof pd.type, "keyboard:down:%s", seq[seq_cur].key); 895 + snprintf(pu.key, sizeof pu.key, "%s", seq[seq_cur].key); 896 + snprintf(pu.type, sizeof pu.type, "keyboard:up:%s", seq[seq_cur].key); 897 + piece_act(pc, &pd); 898 + piece_act(pc, &pu); 899 + fprintf(stderr, "[sequence] fired %s @ %dms\n", 900 + seq[seq_cur].key, seq[seq_cur].at_ms); 901 + seq_cur++; 833 902 } 834 903 if (inject_key && !injected && (SDL_GetTicks() - start_tick) >= 300) { 835 904 PieceEvent pe = {0}; ··· 921 990 piece_sim(pc); 922 991 render_frame(&rctx); 923 992 993 + // Per-frame PNG dump for offline recording. Upscales density× 994 + // nearest-neighbor so the chunky-pixel aesthetic reads correctly 995 + // at the recorded resolution. Slow (hundreds of small PNGs), so 996 + // gated on the env var. 997 + if (frame_dump_dir) { 998 + int d = rctx.density < 1 ? 1 : rctx.density; 999 + const uint32_t *src = fb.pixels; 1000 + int out_w = fb.width, out_h = fb.height; 1001 + uint32_t *scaled = NULL; 1002 + if (d > 1) { 1003 + scaled = upscale_nn(fb.pixels, fb.width, fb.height, d); 1004 + if (scaled) { src = scaled; out_w *= d; out_h *= d; } 1005 + } 1006 + char path[1200]; 1007 + snprintf(path, sizeof path, "%s/frame_%05d.png", 1008 + frame_dump_dir, frame_dump_idx++); 1009 + png_write_argb(path, src, out_w, out_h, out_w); 1010 + free(scaled); 1011 + } 1012 + 924 1013 // Piece-swap: the piece (or any global code) can set 925 1014 // globalThis.__pending_jump = "<name>". We poll between frames, 926 1015 // destroy the old ctx, and load the new piece. Target resolves ··· 974 1063 uninstall_global_hotkey(); 975 1064 if (tray) SDL_DestroyTray(tray); 976 1065 1066 + // Close the WAV tap before destroying the audio unit — otherwise the 1067 + // CoreAudio callback may still be mid-fwrite when we fclose() the 1068 + // file. audio_wav_stop() is idempotent when no tap is active. 1069 + if (wav_out && wav_out[0]) { 1070 + Audio *au = piece_audio(pc); 1071 + if (au) audio_wav_stop(au); 1072 + } 1073 + free(seq); 977 1074 piece_destroy(pc); 978 1075 free(fb.pixels); 979 1076 SDL_DestroyTexture(tex);