mcap/tools/record_probe.jai
Musa Mahmood 3f5d1ea124 [0.1.9] Recording toolbar: a Mic button, to turn the microphone on and off mid-recording
- The toolbar's Mic: on / off (red while on) turns the microphone on and off while recording;
  its state is saved as microphone in [video] for the next recording. A microphone that cannot
  be opened turns it off again, with a toast saying why.
- modules/Screen_Record: record_set_microphone, record_microphone, record_microphone_failed. The
  audio thread opens the microphone only while it is on (so Windows shows it in use only then)
  and closes it when turned off. Record_Options.audio_track gives the MP4 a sound track whatever
  is on at the start, silent while nothing is, as one cannot be added once the header is
  written; the app always asks for it. The sources heard and the failures are kept as they
  happen, not only at the start.
- record-probe -mic-toggle: the microphone on at 3 s and off at 7 s. With system sound and with
  nothing else on, the track runs the whole recording without a break.
2026-10-10 08:16:12 +01:00

617 lines
30 KiB
Plaintext

// record-probe: phase 1 of video-capture-plan.html. Records a screen, or a region of one, with
// modules/Screen_Record (DXGI Desktop Duplication and FFmpeg) and says how it went: which screens
// there are and on which adapter, which duplication call and encoder worked, frames captured, written
// and dropped, time per stage, and the file's size. It also answers what the plan leaves to phase 1:
//
// -capture HOW auto (Desktop Duplication, else Windows.Graphics.Capture), dxgi or wgc.
// -border-test counts yellow pixels along the screen's edges, with GDI, before recording and a
// second in: does Windows draw its capture border (Windows.Graphics.Capture)?
// -keep-border leaves Windows.Graphics.Capture's border on: the border test's control.
// -simulate-unsupported Desktop Duplication fails as on a laptop with two GPUs: the fallback's path.
// -poll Windows.Graphics.Capture polled, as when it will not take its frame handler.
// -pause SECONDS pauses 2 s in, for that long: the video should be that much shorter, with no gap.
// -lose Desktop Duplication lost every 3 s, as a mode change or the secure desktop loses it.
// -cursor-test puts the mouse in the middle of the region while recording, and back after;
// with -snapshot, cursor.png is the frame around it, four times as large.
// -legacy the plain DuplicateOutput, not DuplicateOutput1: does it fail for a process aware
// of each monitor's DPI, as mcap is (this probe is, unless -dpi-unaware)?
// -cuda makes a CUDA context first, as onnxruntime does in mcap: does duplication still work?
// -exclude-test puts two squares on the region, one kept out of captures with
// WDA_EXCLUDEFROMCAPTURE: is it out of the recording, and the other in?
// -animate moves a square across the region the whole time, so the screen changes every
// frame: for measuring throughput.
// -remux IN OUT a recording's fragmented MP4 made a regular one, as the app does, and nothing else.
// -audio WHICH records sound too: system (what the computer plays), mic, or both.
// -mic-toggle with -audio system: the microphone turned on 3 s in, and off at 7 s, as the toolbar does.
// -sync-test every 2 s a white square flashes on the region and a 1 kHz beep plays; then the
// flashes and beeps are found in the file and their offsets given, and how they drift.
// -check FILE reads FILE back with FFmpeg and says what it holds, and nothing else: for a
// recording cut short by a crash.
// -snapshot DIR the last frame captured (captured.png) and a frame decoded back from the file
// (decoded.png), to compare by eye.
//
// Build: jai first.jai - record_probe
// Run: bin/record-probe [-list] [-screen N] [-region X Y W H] [-seconds S] [-fps F] [-encoder NAME]
// [-out FILE.mp4] [-no-cursor] [-legacy] [-cuda] [-exclude-test] [-snapshot DIR]
// [-animate] [-capture auto|dxgi|wgc] [-border-test] [-keep-border] [-cursor-test]
// [-simulate-unsupported] [-poll] [-pause SECONDS] [-lose] [-dpi-unaware]
// Exit code: 0 recorded, 1 failed, 3 Desktop Duplication cannot run here (the fallback's case).
main :: () {
args := get_command_line_arguments();
options: Record_Options;
options.path = tprint("%/record-probe.mp4", get_working_directory());
seconds: float64 = 10;
list_only, cuda, exclude_test, dpi_unaware, animate, border_test, cursor_test := false, false, false, false, false, false, false;
snapshot_folder: string;
pause_seconds: float64;
sync_test := false;
mic_toggle := false;
i := 1;
number :: (index: s64) -> s32 #expand { return cast(s32) string_to_int(`args[index]); }
while i < args.count {
a := args[i];
more := args.count - i - 1;
if a == "-list" { list_only = true; i += 1; continue; }
if a == "-screen" && more >= 1 { options.screen = number(i + 1); i += 2; continue; }
if a == "-region" && more >= 4 {
x, y, w, h := number(i + 1), number(i + 2), number(i + 3), number(i + 4);
options.region = .{x, y, x + w, y + h};
i += 5; continue;
}
if a == "-seconds" && more >= 1 { seconds = string_to_float64(args[i + 1]); i += 2; continue; }
if a == "-fps" && more >= 1 { options.fps = number(i + 1); i += 2; continue; }
if a == "-encoder" && more >= 1 { options.encoder = args[i + 1]; i += 2; continue; }
if a == "-out" && more >= 1 { options.path = args[i + 1]; i += 2; continue; }
if a == "-snapshot" && more >= 1 { snapshot_folder = args[i + 1]; i += 2; continue; }
if a == "-no-cursor" { options.cursor = false; i += 1; continue; }
if a == "-legacy" { options.legacy_duplicate = true; i += 1; continue; }
if a == "-cuda" { cuda = true; i += 1; continue; }
if a == "-exclude-test" { exclude_test = true; i += 1; continue; }
if a == "-dpi-unaware" { dpi_unaware = true; i += 1; continue; }
if a == "-animate" { animate = true; i += 1; continue; }
if a == "-border-test" { border_test = true; i += 1; continue; }
if a == "-keep-border" { options.testing |= .KEEP_BORDER; i += 1; continue; }
if a == "-simulate-unsupported" { options.testing |= .DXGI_UNSUPPORTED; i += 1; continue; }
if a == "-poll" { options.testing |= .WGC_POLL; i += 1; continue; }
if a == "-lose" { options.testing |= .LOSE_DUPLICATION; i += 1; continue; }
if a == "-sync-test" { sync_test = true; i += 1; continue; }
if a == "-mic-toggle" { mic_toggle = true; options.audio_track = true; i += 1; continue; }
if a == "-audio" && more >= 1 {
which := args[i + 1];
options.system_audio = which == "system" || which == "both";
options.microphone = which == "mic" || which == "both";
if !options.system_audio && !options.microphone { print("-audio takes system, mic or both\n"); exit(2); }
i += 2; continue;
}
if a == "-pause" && more >= 1 { pause_seconds = string_to_float64(args[i + 1]); i += 2; continue; }
if a == "-cursor-test" { cursor_test = true; i += 1; continue; }
if a == "-capture" && more >= 1 {
how := args[i + 1];
if how == "auto" options.capture = .AUTO;
else if how == "dxgi" options.capture = .DXGI;
else if how == "wgc" options.capture = .WGC;
else { print("-capture takes auto, dxgi or wgc\n"); exit(2); }
i += 2; continue;
}
if a == "-remux" && more >= 2 {
ok, error, packets := record_remux(args[i + 1], args[i + 2]);
if !ok { print("Cannot remux %: %\n", args[i + 1], error); exit(1); }
print("% packets into %\n", packets, args[i + 2]);
exit(0);
}
if a == "-check" && more >= 1 {
image, info, ok, error := video_poster(args[i + 1]);
if !ok { print("%: cannot be read: %\n", args[i + 1], error); exit(1); }
print("%: % x %, % s, %, % fps; a frame decoded at % s\n", args[i + 1], info.width, info.height, FF(info.duration, 2), info.codec, FF(info.frame_rate, 1), FF(info.poster_time, 2));
exit(0);
}
print("Unknown argument %\n", a);
exit(2);
}
if !dpi_unaware {
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 :: cast(HANDLE) -4;
if !SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) {
error := GetLastError();
ERROR_ACCESS_DENIED :: 5;
if error != ERROR_ACCESS_DENIED print("Could not become per-monitor DPI aware (v2): error %\n", error);
// Access denied: the awareness was set already, as it can be once only. dxgi_open and wgc_open
// refuse when the desktop they are told of is scaled.
}
}
print("DPI awareness: %\n", ifx dpi_unaware then "none" else "per monitor (v2), as mcap");
screens := record_list_screens();
print("Screens:\n");
for screens {
d := it.desktop;
print(" %: % on adapter % (%): % x % at %, %%\n", it_index, it.name, it.adapter_index, it.adapter,
d.right - d.left, d.bottom - d.top, d.left, d.top,
ifx it.rotation == .IDENTITY || it.rotation == .UNSPECIFIED then "" else tprint(", rotated %", it.rotation));
}
if list_only return;
if cuda {
ok, text := cuda_make_context();
print("CUDA: %\n", text);
if !ok exit(1);
}
border_screen := screens[ifx options.screen >= 0 then options.screen else 0].desktop;
border_before: s64;
if border_test border_before = yellow_edge_pixels(border_screen);
recorder: Recorder;
ok, error, fall_back := record_start(*recorder, options);
if !ok {
print("Cannot record: %\n", error);
if fall_back print("Desktop Duplication cannot run here: this is the case for the Windows.Graphics.Capture fallback.\n");
exit(ifx fall_back then cast(s32) 3 else 1);
}
stats := record_stats(*recorder);
if stats.fell_back_from print("Desktop Duplication could not run (%), so:\n", stats.fell_back_from);
print("Recording % x % for % s at up to % fps with % and %, to %\n", stats.width, stats.height, seconds, options.fps, stats.source, stats.encoder, options.path);
region := options.region;
if region.right <= region.left region = screens[ifx options.screen >= 0 then options.screen else 0].desktop;
excluded, control: HWND;
if exclude_test excluded, control = exclusion_windows(region);
saved_cursor: POINT;
cursor_at: POINT;
if cursor_test {
GetCursorPos(*saved_cursor);
cursor_at = .{region.left + (region.right - region.left) / 2, region.top + (region.bottom - region.top) / 2};
SetCursorPos(cursor_at.x, cursor_at.y);
}
flash: HWND;
beep: [] u8;
next_flash: float64 = 1;
if sync_test {
beep = beep_wav();
side := min(cast(s32) 200, (region.right - region.left) / 3, (region.bottom - region.top) / 3);
flash = square_window("record-probe flash", 0x00FFFFFF, region.left + (region.right - region.left - side) / 2, region.top + (region.bottom - region.top - side) / 2, side);
ShowWindow(flash, SW_HIDE);
}
mover: HWND;
if animate mover = square_window("record-probe animate", 0x0000A5FF, region.left, region.top, 120);
start := current_time_monotonic();
last_print := start;
border_during: s64 = -1;
while to_float64_seconds(current_time_monotonic() - start) < seconds {
pump_messages();
if mover { // back and forth across the region, about 240 times a second
elapsed := to_float64_seconds(current_time_monotonic() - start);
span := region.right - region.left - 120;
phase := fmod_cycling(elapsed * 0.5, 2.0);
x := region.left + cast(s32) (span * ifx phase < 1 then phase else 2 - phase);
SetWindowPos(mover, HWND_TOPMOST, x, region.top + (region.bottom - region.top) / 2 - 60, 0, 0, SWP_NOSIZE | SWP_NOACTIVATE);
sleep_milliseconds(4);
} else {
sleep_milliseconds(20);
}
if to_float64_seconds(current_time_monotonic() - last_print) >= 1 {
last_print = current_time_monotonic();
s := record_stats(*recorder);
print(" % s: % captured, % sent (% repeats), % written, % dropped, % lost, % KB\n",
FF(to_float64_seconds(last_print - start), 0), s.captured, s.emitted, s.repeats, s.written, s.dropped, s.lost, s.bytes / 1024);
}
if mic_toggle {
elapsed := to_float64_seconds(current_time_monotonic() - start);
should := elapsed >= 3 && elapsed < 7;
if should != record_microphone(*recorder) && (elapsed < 3.5 || (elapsed >= 7 && elapsed < 7.5)) {
record_set_microphone(*recorder, should);
print(" % s: microphone %\n", FF(elapsed, 1), ifx should then "on" else "off");
}
failed, why := record_microphone_failed(*recorder);
if failed print(" microphone failed: %\n", why);
}
if flash { // a flash and a beep together every 2 s, the flash for 200 ms
elapsed := to_float64_seconds(current_time_monotonic() - start);
if elapsed >= next_flash {
ShowWindow(flash, SW_SHOWNOACTIVATE);
PlaySoundW(beep.data, null, SND_MEMORY | SND_ASYNC);
next_flash += 2;
} else if elapsed >= next_flash - 2 + 0.2 {
ShowWindow(flash, SW_HIDE);
}
}
if pause_seconds > 0 {
elapsed := to_float64_seconds(current_time_monotonic() - start);
should := elapsed >= 2 && elapsed < 2 + pause_seconds;
if should != record_paused(*recorder) {
record_pause(*recorder, should);
print(" % s: %\n", FF(elapsed, 1), ifx should then "paused" else "resumed");
}
}
if border_test && border_during < 0 && to_float64_seconds(current_time_monotonic() - start) >= 1.5
border_during = yellow_edge_pixels(border_screen);
failed, _ := record_failed(*recorder);
if failed break;
}
last, width, height := record_last_frame(*recorder);
if mover DestroyWindow(mover);
if flash DestroyWindow(flash);
if cursor_test SetCursorPos(saved_cursor.x, saved_cursor.y);
stopped, stop_error := record_stop(*recorder);
s := recorder.stats;
print("\n%\n", ifx stopped then "Recorded." else tprint("Recording failed: %", stop_error));
ms_each :: (ticks: s64, count: s64) -> string #expand {
if !count return "-";
return tprint("% ms", FF(cast(float64) ticks / `recorder.frequency * 1000 / count, 2));
}
file_size := file_size_of(options.path);
print(" source: %\n", s.source);
print(" encoder: %\n", s.encoder);
print(" size: % x %\n", s.width, s.height);
print(" seconds: %\n", FF(s.seconds, 1));
print(" captured: % new images of the screen (% a second), % cursor-only changes\n", s.captured, FF(s.captured / s.seconds, 1), s.pointer_moves);
print(" sent: % frames (% a second), % of them repeats of an unchanged screen\n", s.emitted, FF(s.emitted / s.seconds, 1), s.repeats);
print(" written: %, dropped %, duplication lost %, paused % s\n", s.written, s.dropped, s.lost, FF(s.paused_seconds, 1));
print(" each: copy % (per image), read back % , encode % (per frame)\n",
ms_each(s.copy_ticks, s.captured), ms_each(s.readback_ticks, s.emitted), ms_each(s.encode_ticks, s.written));
print(" file: % KB, % MB a minute\n", file_size / 1024, FF(cast(float64) file_size / (1024 * 1024) / s.seconds * 60, 1));
if options.system_audio || options.microphone || options.audio_track {
print(" sound: %; % AAC frames%\n", ifx s.audio then s.audio else "none opened", s.audio_frames, ifx s.audio_errors then tprint(" (%)", s.audio_errors) else "");
sound := sound_report(options.path);
if sound.found print(" in file: % s of sound, % dB RMS, % onsets\n", FF(sound.seconds, 2), FF(sound.rms_db, 1), sound.onsets.count);
else print(" in file: no sound stream %\n", sound.error);
if sync_test && sound.found {
flashes := flash_times(options.path);
print(" sync: % flashes, % beeps\n", flashes.count, sound.onsets.count);
offsets: [..] float64;
offsets.allocator = temp;
for flash_time: flashes {
best: float64 = 1000000000.0;
for sound.onsets if abs(it - flash_time) < abs(best) best = it - flash_time;
if abs(best) < 0.5 array_add(*offsets, best);
}
if offsets.count {
text: [..] string; text.allocator = temp;
for offsets array_add(*text, tprint("%", FF(it * 1000, 0)));
print(" offsets: beep after flash, ms: %\n", join(..text, " ",, temp));
print(" drift: % ms from the first to the last\n", FF((offsets[offsets.count - 1] - offsets[0]) * 1000, 0));
}
}
}
if exclude_test {
excluded_rect := window_rect_in(excluded, region);
control_rect := window_rect_in(control, region);
excluded_seen := colour_fraction(last, width, height, excluded_rect, EXCLUDED_BGR);
control_seen := colour_fraction(last, width, height, control_rect, CONTROL_BGR);
print(" exclusion: the excluded square is % in the last frame, the control square %: %\n",
percent(excluded_seen), percent(control_seen),
ifx control_seen < 0.9 then "inconclusive (the control is not there)"
else ifx excluded_seen < 0.01 then "WDA_EXCLUDEFROMCAPTURE is respected"
else "WDA_EXCLUDEFROMCAPTURE is NOT respected");
DestroyWindow(excluded);
DestroyWindow(control);
}
if border_test {
print(" border: % yellow pixels along the screen's edges before recording, % while: %\n", border_before, border_during,
ifx border_during < 0 then "not measured (too short)"
else ifx border_during > border_before + 200 then "a capture border is drawn"
else "no capture border");
}
if snapshot_folder {
make_directory_if_it_does_not_exist(snapshot_folder, recursive = true);
rgba := bgra_to_rgba(last);
if cursor_test { // 48 x 48 around where the mouse was put, four times as large
SIDE :: 48; SCALE :: 4;
crop := NewArray(SIDE * SCALE * SIDE * SCALE * 4, u8,, temp);
cx, cy := cursor_at.x - region.left - 8, cursor_at.y - region.top - 8;
for y: 0..SIDE * SCALE - 1 for x: 0..SIDE * SCALE - 1 {
sx, sy := clamp(cx + x / SCALE, 0, width - 1), clamp(cy + y / SCALE, 0, height - 1);
memcpy(crop.data + (y * SIDE * SCALE + x) * 4, rgba.data + (cast(s64) sy * width + sx) * 4, 4);
}
cursor_path := tprint("%/cursor.png", snapshot_folder);
stbi_write_png(temp_c_string(cursor_path), SIDE * SCALE, SIDE * SCALE, 4, crop.data, SIDE * SCALE * 4);
print(" cursor: %\n", cursor_path);
}
captured_path := tprint("%/captured.png", snapshot_folder);
stbi_write_png(temp_c_string(captured_path), width, height, 4, rgba.data, width * 4);
image, info, decoded, decode_error := video_poster(options.path);
if decoded {
decoded_path := tprint("%/decoded.png", snapshot_folder);
stbi_write_png(temp_c_string(decoded_path), image.width, image.height, 4, image.pixels, image.width * 4);
print(" snapshot: % and % (decoded at % s of %: %, % fps)\n", captured_path, decoded_path, FF(info.poster_time, 2), FF(info.duration, 2), info.codec, FF(info.frame_rate, 1));
image_free(*image);
} else {
print(" snapshot: %; the file could not be decoded back: %\n", captured_path, decode_error);
}
}
exit(ifx stopped then cast(s32) 0 else 1);
}
#scope_file
EXCLUDED_BGR :: u8.[255, 0, 255]; // magenta
CONTROL_BGR :: u8.[255, 255, 0]; // cyan
// Two squares on the region, topmost: one kept out of captures, one not.
exclusion_windows :: (region: RECT) -> excluded: HWND, control: HWND {
width := region.right - region.left;
height := region.bottom - region.top;
side := min(cast(s32) 160, width / 4, height / 2);
y := region.top + (height - side) / 2;
excluded := square_window("record-probe excluded", 0x00FF00FF, region.left + width / 4 - side / 2, y, side);
control := square_window("record-probe control", 0x00FFFF00, region.left + 3 * width / 4 - side / 2, y, side);
WDA_EXCLUDEFROMCAPTURE :: 0x11;
if !SetWindowDisplayAffinity(excluded, WDA_EXCLUDEFROMCAPTURE) print("SetWindowDisplayAffinity failed; the test cannot say anything.\n");
pump_messages();
return excluded, control;
}
// A square of one colour, topmost, that never takes the focus.
square_window :: (class_name: string, colour: u32, x: s32, y: s32, side: s32) -> HWND {
instance := GetModuleHandleW(null);
wc: WNDCLASSEXW;
wc.cbSize = size_of(WNDCLASSEXW);
wc.lpfnWndProc = xx DefWindowProcW;
wc.hInstance = instance;
wc.hbrBackground = CreateSolidBrush(colour);
wc.lpszClassName = utf8_to_wide(class_name,, temp);
RegisterClassExW(*wc);
return CreateWindowExW(WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE, utf8_to_wide(class_name,, temp), utf8_to_wide(class_name,, temp),
WS_POPUP | WS_VISIBLE, x, y, side, side, null, null, instance, null);
}
// Pixels along a screen's outer 6 pixels that look like Windows' yellow capture border, with GDI.
yellow_edge_pixels :: (screen: RECT) -> s64 {
width, height := screen.right - screen.left, screen.bottom - screen.top;
desktop := GetDC(null);
defer ReleaseDC(null, desktop);
dc := CreateCompatibleDC(desktop);
defer DeleteDC(dc);
bitmap := CreateCompatibleBitmap(desktop, width, height);
defer DeleteObject(bitmap);
old := SelectObject(dc, bitmap);
BitBlt(dc, 0, 0, width, height, desktop, screen.left, screen.top, cast(ROP) (cast(u32) ROP.SRCCOPY | 0x40000000)); // CAPTUREBLT
SelectObject(dc, old);
info: BITMAPINFO;
info.bmiHeader.biSize = size_of(BITMAPINFOHEADER);
info.bmiHeader.biWidth = width;
info.bmiHeader.biHeight = -height;
info.bmiHeader.biPlanes = 1;
info.bmiHeader.biBitCount = 32;
info.bmiHeader.biCompression = BI_RGB;
pixels := NewArray(cast(s64) width * height * 4, u8,, temp);
GetDIBits(dc, bitmap, 0, xx height, pixels.data, *info, DIB_RGB_COLORS);
EDGE :: 6;
count: s64;
for y: 0..height - 1 for x: 0..width - 1 {
if x >= EDGE && x < width - EDGE && y >= EDGE && y < height - EDGE continue;
p := pixels.data + (cast(s64) y * width + x) * 4;
b, g, r := cast(s32) p[0], cast(s32) p[1], cast(s32) p[2];
if r > 200 && g > 120 && b < 90 && r - b > 130 count += 1;
}
return count;
}
// A window's rectangle, in the region's pixels, a few pixels in from its edges.
window_rect_in :: (window: HWND, region: RECT) -> RECT {
r: RECT;
GetWindowRect(window, *r);
return .{r.left - region.left + 4, r.top - region.top + 4, r.right - region.left - 4, r.bottom - region.top - 4};
}
colour_fraction :: (pixels: [] u8, width: s32, height: s32, r: RECT, bgr: [3] u8) -> float64 {
total, same: s64;
for y: max(r.top, 0)..min(r.bottom, height) - 1 for x: max(r.left, 0)..min(r.right, width) - 1 {
p := pixels.data + (cast(s64) y * width + x) * 4;
total += 1;
if abs(cast(s32) p[0] - bgr[0]) <= 8 && abs(cast(s32) p[1] - bgr[1]) <= 8 && abs(cast(s32) p[2] - bgr[2]) <= 8 same += 1;
}
return ifx total then cast(float64) same / total else 0;
}
pump_messages :: () {
msg: MSG;
while PeekMessageW(*msg, null, 0, 0, PM_REMOVE) {
TranslateMessage(*msg);
DispatchMessageW(*msg);
}
}
// A CUDA context on the first device, as onnxruntime makes for OCR and labels in mcap, and kept.
cuda_make_context :: () -> ok: bool, text: string {
library := LoadLibraryW(utf8_to_wide("nvcuda.dll",, temp));
if !library return false, "nvcuda.dll is not installed";
Cu_Init :: #type (flags: u32) -> s32 #c_call;
Cu_Device :: #type (device: *s32, ordinal: s32) -> s32 #c_call;
Cu_Context :: #type (cuda_context: **void, flags: u32, device: s32) -> s32 #c_call;
Cu_Name :: #type (name: *u8, length: s32, device: s32) -> s32 #c_call;
cu_init := cast(Cu_Init) GetProcAddress(library, "cuInit");
cu_device_get := cast(Cu_Device) GetProcAddress(library, "cuDeviceGet");
cu_ctx_create := cast(Cu_Context) GetProcAddress(library, "cuCtxCreate_v2");
cu_name := cast(Cu_Name) GetProcAddress(library, "cuDeviceGetName");
if !cu_init || !cu_device_get || !cu_ctx_create || !cu_name return false, "nvcuda.dll lacks the calls needed";
result := cu_init(0);
if result return false, tprint("cuInit failed (%)", result);
device: s32;
result = cu_device_get(*device, 0);
if result return false, tprint("cuDeviceGet failed (%)", result);
name: [256] u8;
cu_name(name.data, name.count, device);
cuda_context: *void;
result = cu_ctx_create(*cuda_context, 0, device);
if result return false, tprint("cuCtxCreate failed (%)", result);
return true, tprint("a context on %, kept for the recording", to_string(name.data));
}
bgra_to_rgba :: (pixels: [] u8) -> [] u8 {
out := NewArray(pixels.count, u8,, temp);
for i: 0..pixels.count / 4 - 1 {
out[i * 4 + 0] = pixels[i * 4 + 2];
out[i * 4 + 1] = pixels[i * 4 + 1];
out[i * 4 + 2] = pixels[i * 4 + 0];
out[i * 4 + 3] = 255;
}
return out;
}
file_size_of :: (path: string) -> s64 {
_, size, ok := file_modtime_and_size(path);
return ifx ok then size else 0;
}
percent :: (fraction: float64) -> string {
return join(tprint("%", FF(fraction * 100, 1)), "%",, temp);
}
FF :: (value: float64, decimals: s64) -> FormatFloat #expand {
return formatFloat(value, trailing_width = decimals, zero_removal = .NO);
}
SetProcessDpiAwarenessContext :: (value: HANDLE) -> BOOL #foreign user32_dll;
SetWindowDisplayAffinity :: (window: HWND, affinity: u32) -> BOOL #foreign user32_dll;
SetCursorPos :: (x: s32, y: s32) -> BOOL #foreign user32_dll;
// ---------------------------------------------------------------- sound and sync
// A beep of 1 kHz for 150 ms, as a WAV in memory, for PlaySound.
beep_wav :: () -> [] u8 {
RATE :: 48000;
COUNT :: RATE * 150 / 1000;
data := NewArray(44 + COUNT * 2, u8,, context.default_allocator);
put32 :: (at: s64, v: u32) #expand { for 0..3 `data[at + it] = cast,trunc(u8) (v >> (8 * it)); }
put16 :: (at: s64, v: u16) #expand { `data[at] = cast,trunc(u8) v; `data[at + 1] = cast,trunc(u8) (v >> 8); }
memcpy(data.data, "RIFF".data, 4); put32(4, cast(u32) (36 + COUNT * 2)); memcpy(data.data + 8, "WAVEfmt ".data, 8);
put32(16, 16); put16(20, 1); put16(22, 1); put32(24, RATE); put32(28, RATE * 2); put16(32, 2); put16(34, 16);
memcpy(data.data + 36, "data".data, 4); put32(40, COUNT * 2);
for i: 0..COUNT - 1 {
v := cast(s16) (sin(cast(float) i * 2 * PI * 1000 / RATE) * 20000);
put16(44 + i * 2, cast,no_check(u16) v);
}
return data;
}
// What the file's sound holds: its length, its level, and when it goes from quiet to loud (onsets).
Sound_Report :: struct { found: bool; seconds: float64; rms_db: float64; onsets: [..] float64; error: string; }
sound_report :: (path: string) -> Sound_Report {
report: Sound_Report;
report.onsets.allocator = temp;
format: *AVFormatContext;
if avformat_open_input(*format, temp_c_string(path), null, null) < 0 { report.error = "cannot open"; return report; }
defer avformat_close_input(*format);
avformat_find_stream_info(format, null);
index := -1;
for 0..format.nb_streams - 1 if format.streams[it].codecpar.codec_type == .AUDIO { index = it; break; }
if index < 0 return report;
report.found = true;
stream := format.streams[index];
codec := avcodec_find_decoder(stream.codecpar.codec_id);
c := avcodec_alloc_context3(codec);
defer avcodec_free_context(*c);
avcodec_parameters_to_context(c, stream.codecpar);
if avcodec_open2(c, codec, null) < 0 { report.error = "no decoder"; return report; }
packet := av_packet_alloc(); defer av_packet_free(*packet);
frame := av_frame_alloc(); defer av_frame_free(*frame);
WINDOW :: 480; // 10 ms at 48 kHz
sum_all: float64; count_all: s64;
window_sum: float64; window_count: s64; window_start: float64 = -1;
quiet_for: s64 = 100; // windows in a row below the threshold
first_time: float64 = -1; last_time: float64;
handle :: () #expand {
while avcodec_receive_frame(`c, `frame) >= 0 {
t := cast(float64) `frame.pts * `stream.time_base.num / `stream.time_base.den;
if `first_time < 0 `first_time = t;
left := cast(*float) `frame.data[0];
for i: 0..`frame.nb_samples - 1 {
v := cast(float64) left[i];
`sum_all += v * v; `count_all += 1;
if `window_start < 0 `window_start = t + cast(float64) i / 48000;
`window_sum += v * v; `window_count += 1;
if `window_count == WINDOW {
rms := sqrt(`window_sum / WINDOW);
if rms > 0.05 {
if `quiet_for >= 20 array_add(*`report.onsets, `window_start); // after 200 ms of quiet
`quiet_for = 0;
} else {
`quiet_for += 1;
}
`window_sum, `window_count, `window_start = 0, 0, -1;
}
}
`last_time = t + cast(float64) `frame.nb_samples / 48000;
}
}
while av_read_frame(format, packet) >= 0 {
if packet.stream_index == index { avcodec_send_packet(c, packet); handle(); }
av_packet_unref(packet);
}
avcodec_send_packet(c, null);
handle();
report.seconds = last_time - max(first_time, 0);
report.rms_db = ifx count_all && sum_all > 0 then 10 * log(sum_all / count_all) / log(10.0) else -200;
return report;
}
// When the video's middle turns white: the sync test's flashes, as times in seconds.
flash_times :: (path: string) -> [..] float64 {
times: [..] float64;
times.allocator = temp;
format: *AVFormatContext;
if avformat_open_input(*format, temp_c_string(path), null, null) < 0 return times;
defer avformat_close_input(*format);
avformat_find_stream_info(format, null);
index := -1;
for 0..format.nb_streams - 1 if format.streams[it].codecpar.codec_type == .VIDEO { index = it; break; }
if index < 0 return times;
stream := format.streams[index];
codec := avcodec_find_decoder(stream.codecpar.codec_id);
c := avcodec_alloc_context3(codec);
defer avcodec_free_context(*c);
avcodec_parameters_to_context(c, stream.codecpar);
if avcodec_open2(c, codec, null) < 0 return times;
packet := av_packet_alloc(); defer av_packet_free(*packet);
frame := av_frame_alloc(); defer av_frame_free(*frame);
was_white := false;
handle :: () #expand {
while avcodec_receive_frame(`c, `frame) >= 0 {
// The luma of the middle 32 x 32 (the first plane in every YUV format the encoders give).
w, h := `frame.width, `frame.height;
sum: s64;
for y: h / 2 - 16..h / 2 + 15 for x: w / 2 - 16..w / 2 + 15 sum += `frame.data[0][y * `frame.linesize[0] + x];
white := sum / 1024 > 200;
if white && !`was_white array_add(*`times, cast(float64) `frame.best_effort_timestamp * `stream.time_base.num / `stream.time_base.den);
`was_white = white;
}
}
while av_read_frame(format, packet) >= 0 {
if packet.stream_index == index { avcodec_send_packet(c, packet); handle(); }
av_packet_unref(packet);
}
avcodec_send_packet(c, null);
handle();
return times;
}
PlaySoundW :: (sound: *void, module: *void, flags: u32) -> s32 #foreign winmm_dll;
winmm_dll :: #library,system "winmm";
SND_ASYNC :: 0x1;
SND_MEMORY :: 0x4;
user32_dll :: #library,system "user32";
#import "Basic";
#import "String";
#import "File";
#import "File_Utilities";
#import "Math";
#import "Windows";
#import "Windows_Utf8";
#import "Screen_Record";
#import "Image_Decode";
#import "stb_image_write";
#import "ffmpeg";