// 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. // -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; 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 == "-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 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 { 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";