diff --git a/.claude/skills/t27f/SKILL.md b/.claude/skills/t27f/SKILL.md index fac8041b27..fde7c70c8a 100644 --- a/.claude/skills/t27f/SKILL.md +++ b/.claude/skills/t27f/SKILL.md @@ -151,6 +151,11 @@ Traps from this tick: - Before trusting a "fast voice" grid result, work out which fixture voices are fast. `voice_room_ms(3)` is 12250 ms, so the measured lesson 1 voice (raw 12300) is already sped up. A change aimed at "fast voices" therefore changes the reel everyone actually sees. +**Letting a caller reach a printer in another spec (t27#10628).** When an overlay's `enable=` is printed in an imported spec, the importing spec cannot clip it. Split the printer into a form that takes the window (`print_alpha(lo, hi)`, `print_rise_from(o, lo, dy)` in token_floor_mux) and the old form as a thin wrapper that passes the spec's own window (`print_fade(c)`, `print_rise(o, c, dy)`). Then run `cmp` on the imported spec's own graph: it must be byte-identical (token_floor_mux: 28336 bytes, 0 differences). Run `cmp` on every mode of the importer that does not use the window too (modes 3 and 4: 0 of 162 graphs differ). + +- Move the ramp along with the window. A card's rise and fade-in start at its first kept frame (`beat_first_lo`), and its fade-out ends at its last kept frame (`beat_last_hi`). If you clip only `enable=`, the card pops in fully risen on the close beat's first frame, because the ramp started on frame 392, under the crossfade. +- A window can end past the reel. `CARD_ON` gives the last card frames 392..450, but the reel's last frame is 449. Clamp the window (`card_hi`) and assert the clamp in a test. + ## Show a spec (the core: exporter for what it reads, by hand for the rest) 1. Load the spec: `mcp__t27__t27_spec` with `path` and `source: true`, or the file from a diff --git a/.trinity/seals/t27f_lesson_reel.json b/.trinity/seals/t27f_lesson_reel.json index ba43902687..8d2236dfd7 100644 --- a/.trinity/seals/t27f_lesson_reel.json +++ b/.trinity/seals/t27f_lesson_reel.json @@ -23,7 +23,7 @@ }, { "spec": "specs/t27f/token_floor_mux.t27", - "spec_hash": "sha256:55c02c60e581640d2f3d6db3d9ba2cdfb21d74b172290b386023e8f264e59a04" + "spec_hash": "sha256:36088094a533d70fcb7e995c53ce129b21367394057c4407f400b9dff9be6d6f" }, { "spec": "specs/t27f/token_floor_scenes.t27", @@ -31,23 +31,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:97c582c40a1e76a50e35c68414ce2653739784f9b0e3b58006deede027640fb3", - "gen_hash_rust": "sha256:f3b086a0827512fdebe957c9bc73b30592914373a1dd9137e57efb851ebf720f", - "gen_hash_verilog": "sha256:cc184e56898034ed556e2e7c9d111c028f56d60bb6adb21952a6b105d9baa0c9", - "gen_hash_zig": "sha256:aa16bbf44971ec5a6eacbd80389bca963b739a76f888cf401df14cb51abea29d", + "gen_hash_c": "sha256:5b85cb24996a66cf14d5816dfc07ffda9653e1acd9922c2b9f73526b5c37c626", + "gen_hash_rust": "sha256:b9edb32d279aaeef4388bc6c5e765676c9d86d86a434fbe019aab7c2ebc38d8e", + "gen_hash_verilog": "sha256:1847abc2cfd9f4c7c65492b702f962329e92c0477455360673e15be927c1ab5a", + "gen_hash_zig": "sha256:8a4ce7a647d698dc6d5e0f0f4999e8a523dbcf599597b89b17988f36b88f7508", "module": "lesson_reel", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-18T20:05:15Z", + "sealed_at": "2026-10-18T20:27:01Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:a380a3fec6de690924f36c45f40ba7aac1e002f4482fdb23d71e472357b16f19", + "spec_hash": "sha256:6097e5dd188754d6c72205a95322c542ab6c3304b1d4259d22dc9a2f5af92862", "spec_path": "specs/t27f/lesson_reel.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 27, - "total": 27, + "passed": 29, + "total": 29, "vacuous": 0 }, "toolchain": { diff --git a/.trinity/seals/t27f_token_floor_mux.json b/.trinity/seals/t27f_token_floor_mux.json index 6f7f575dc7..c48d58df98 100644 --- a/.trinity/seals/t27f_token_floor_mux.json +++ b/.trinity/seals/t27f_token_floor_mux.json @@ -1,5 +1,5 @@ { - "built_by": "t27c-bootstrap@0.5.2+5e4d3b998", + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", "closure": [ { "spec": "specs/automation/owner_voice_reel.t27", @@ -27,16 +27,16 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:18710ba70409c54ce28a6baa8da85579904bf6eeec5e91f4913496216a7fb9cb", - "gen_hash_rust": "sha256:c7240d2db316d11bbff444fed4725b84a77bd566a92e6286dcc4966ae9f2bf06", - "gen_hash_verilog": "sha256:7c6af1b05a4ecfb5560ff39e4728fd96b7ab95c825c428289ab330456bc44dd7", - "gen_hash_zig": "sha256:bcd5f63e11e3fe7de9b9e2dbbe844987430961ac73d8048404c8c42e0c9d6e1c", + "gen_hash_c": "sha256:d4a9f6f33414d4a5e9f62de06f0e27f86ea182916737d818df43995b3fdfe306", + "gen_hash_rust": "sha256:b5bdd6788752a87454d7e36271b94ac41884d08cd230a6256e51fb3c00f2698f", + "gen_hash_verilog": "sha256:3b54ee703bf10b10410b6380c589dc798eadfb590b49be296353c70a2817ccb8", + "gen_hash_zig": "sha256:320cdac0773bf3a9fe387107c00513f82ab6aaa40c8d08dfef746d7bd6d84c80", "module": "token_floor_mux", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-18T15:24:22Z", + "sealed_at": "2026-10-18T20:26:58Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:55c02c60e581640d2f3d6db3d9ba2cdfb21d74b172290b386023e8f264e59a04", + "spec_hash": "sha256:36088094a533d70fcb7e995c53ce129b21367394057c4407f400b9dff9be6d6f", "spec_path": "specs/t27f/token_floor_mux.t27", "tests": { "failed": 0, @@ -47,7 +47,7 @@ "vacuous": 0 }, "toolchain": { - "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", "zig": "0.16.0" } } \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 2b83e4bec6..074f9e335a 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -48,6 +48,7 @@ {"path": "specs/analog/linear_zoh.t27", "t27b": "blocked", "blocker": "ExprCall(@sin)", "reason": "unimplemented"}, {"path": "specs/analog/mos_table.t27", "t27b": "pass"}, {"path": "specs/analog/sar_cdac.t27", "t27b": "pass"}, + {"path": "specs/analog/sar_comparator.t27", "t27b": "pass"}, {"path": "specs/api/c_abi.t27", "t27b": "pass"}, {"path": "specs/api/tri_api_context.t27", "t27b": "pass"}, {"path": "specs/api/tri_api_loop.t27", "t27b": "pass"}, diff --git a/specs/t27f/lesson_reel.t27 b/specs/t27f/lesson_reel.t27 index 8cc273e699..7a67961f17 100644 --- a/specs/t27f/lesson_reel.t27 +++ b/specs/t27f/lesson_reel.t27 @@ -18,8 +18,9 @@ // 4 the beat graph (t27#10603), given LR_MS0..LR_MS2: inputs 0..2 the open, main and close // scene clips, crossfaded on whole frames at the beat clock's cuts and trimmed to the reel // 5 mode 3's two graphs laid over mode 4's beat video (t27#10609): inputs 0..2 the scene -// clips, 3..5 the phrases' wavs, 6 the music. The captions, tags and address are shown on -// whole frames inside the scenes and never on a crossfade (t27#10614, beat_piece_*) +// clips, 3..5 the phrases' wavs, 6 the music. The captions, tags, cards and address are shown +// on whole frames inside the scenes and never on a crossfade (t27#10614, t27#10628, +// beat_piece_*) // Every text that comes from the lesson goes through a textfile, so no quote, colon or percent in // a lesson can break the graph (and drawtext runs with expansion=none). // @@ -43,9 +44,9 @@ // // NOT CLAIMED: the backdrop is token_floor's physics world, not a picture of the lesson; its frame // labels are the physics reel's. Mode 5 lays the same captions and address over every scene, in -// the same places, and only their windows change with the beat. The cards are still on their -// own frames over a crossfade: token_floor_mux prints their enable= and this file does not reach -// it. Both cuts are centred on a sentence change, so a crossfade can begin under the last words +// the same places, and only their windows change with the beat. The cards keep token_floor_mux's +// windows, cut to their scene's frames as the captions are; a card with no room is dropped. +// Both cuts are centred on a sentence change, so a crossfade can begin under the last words // of a phrase: mode 5 then drops a caption with too little room left (the cost tests). Cut 1 is // capped at BEAT_CUT1_LAST_MS so mode 5 holds the address for REEL_CTA_MIN_MS (t27#10629): on a // voice the reel had to speed up, lesson 1's measured voice among them, crossfade 1 then starts @@ -77,6 +78,7 @@ use t27f::token_floor_scenes::TAG_X; use t27f::token_floor_scenes::TAG_Y; use t27f::token_floor_scenes::TAG_FONT; use t27f::token_floor_scenes::CARDS; +use t27f::token_floor_scenes::CARD_ON; use t27f::token_floor_scenes::CARD_LY; use t27f::token_floor_scenes::CARD_PAD; use t27f::token_floor_scenes::PILL_H; @@ -107,6 +109,8 @@ use t27f::token_floor_mux::print_col; use t27f::token_floor_mux::band_font; use t27f::token_floor_mux::print_rise; use t27f::token_floor_mux::print_fade; +use t27f::token_floor_mux::print_rise_from; +use t27f::token_floor_mux::print_alpha; use t27f::reel_topics::TOPICS; use t27f::reel_topics::item; use t27f::reel_topics::id_item; @@ -553,17 +557,54 @@ fn print_tag(o: usize, s: usize, dx: i32, row: usize, f: usize, lang: usize, bea } } -fn print_card_text(o: usize, c: usize, lang: usize, f: usize, font: i32, row: usize, dy: f64) -> void { +// Card c's window on the reel's frames: token_floor_mux's CARD_ON, its last frame inside the reel. +pub fn card_lo(c: usize) -> i32 { + return CARD_ON[2 * c]; +} + +pub fn card_hi(c: usize) -> i32 { + return beat_min(CARD_ON[2 * c + 1], beat_frames() - 1); +} + +// A card text's rise and fade. Without beats, token_floor_mux's own (print_rise, print_fade). With +// beats the card rises from the first frame of its first kept piece, fades out on the last frame +// of its last, and is on only on its pieces (t27#10628). +fn print_card_on(o: usize, c: usize, dy: f64, beats: bool, f0: i32, f1: i32) -> void { + if (beats) { + const lo = card_lo(c); + const hi = card_hi(c); + print_rise_from(o, beat_first_lo(lo, hi, f0, f1), dy); + } else { + print_rise(o, c, dy); + } +} + +fn print_card_off(c: usize, beats: bool, f0: i32, f1: i32) -> void { + if (beats) { + const lo = card_lo(c); + const hi = card_hi(c); + print_alpha(beat_first_lo(lo, hi, f0, f1), beat_last_hi(lo, hi, f0, f1)); + print_pieces(lo, hi, f0, f1); + } else { + print_fade(c); + } +} + +fn print_card_text(o: usize, c: usize, lang: usize, f: usize, font: i32, row: usize, dy: f64, beats: bool, f0: i32, f1: i32) -> void { std.debug.print(",drawtext=fontfile={s}:expansion=none:textfile={s}/{s}/f{d:0>2}.txt:fontsize={d}:fontcolor=", .{ FONT, FILE_DIR, lang_dir(lang), f, font }); print_col(row); std.debug.print(":x={d:.1}", .{card_f(o, 0) + CARD_PAD}); - print_rise(o, c, dy); + print_card_on(o, c, dy, beats, f0, f1); std.debug.print("'", .{}); - print_fade(c); + print_card_off(c, beats, f0, f1); } -fn print_card(o: usize, c: usize, t: usize, lang: usize) -> void { - print_card_text(o, c, lang, F_CARD_LABEL, label_font(o), GREY_ROW, CARD_LY[o * 3]); +// With beats a card with no kept piece is not printed, as a tag or a caption is not. +fn print_card(o: usize, c: usize, t: usize, lang: usize, beats: bool, f0: i32, f1: i32) -> void { + if (beats && beat_pieces(card_lo(c), card_hi(c), f0, f1) == 0) { + return; + } + print_card_text(o, c, lang, F_CARD_LABEL, label_font(o), GREY_ROW, CARD_LY[o * 3], beats, f0, f1); var j : usize = 0; while (j < 2) { const font = code_font(o, line_len(card_code_line(t, c, j))); @@ -571,13 +612,13 @@ fn print_card(o: usize, c: usize, t: usize, lang: usize) -> void { if (j == 1) { row = GOLD_ROW; } - print_card_text(o, c, lang, F_CARD0 + 2 * c + j, font, row, CARD_LY[o * 3 + 1 + j]); + print_card_text(o, c, lang, F_CARD0 + 2 * c + j, font, row, CARD_LY[o * 3 + 1 + j], beats, f0, f1); j = j + 1; } std.debug.print(",drawtext=fontfile={s}:text='PASS':fontsize={d}:fontcolor=0x000000:x={d:.1}", .{ FONT, PASS_FONT[o], pill_x(o) + PASS_DX }); - print_rise(o, c, pill_dy(o) + 0.5 * PILL_H); + print_card_on(o, c, pill_dy(o) + 0.5 * PILL_H, beats, f0, f1); std.debug.print("-text_h/2'", .{}); - print_fade(c); + print_card_off(c, beats, f0, f1); } // The address and its page from ms a to the end. With beats they fade in from the first frame of @@ -635,7 +676,7 @@ pub fn graph(o: usize, t: usize, lang: usize, m0: i32, m1: i32, m2: i32, beats: } var c : usize = 0; while (c < CARDS) { - print_card(o, c, t, lang); + print_card(o, c, t, lang, beats, f0, f1); c = c + 1; } const n = phrases_for(m0, m1, m2); @@ -946,6 +987,19 @@ pub fn beat_first_lo(lo: i32, hi: i32, f0: i32, f1: i32) -> i32 { return lo; } +// The last frame of the last kept piece, where a card's fade out ends in mode 5; hi when none is +// kept. +pub fn beat_last_hi(lo: i32, hi: i32, f0: i32, f1: i32) -> i32 { + var b : usize = 3; + while (b > 0) { + b = b - 1; + if (beat_piece_kept(lo, hi, b, f0, f1)) { + return beat_piece_hi(hi, b, f0, f1); + } + } + return hi; +} + // Mode 5's enable= for overlay lo..hi: exactly its kept pieces' frames. fn print_pieces(lo: i32, hi: i32, f0: i32, f1: i32) -> void { std.debug.print(":enable='", .{}); @@ -1687,6 +1741,95 @@ test "in mode 5 no caption, tag or address is on a crossfade, every lesson, both assert(dropped < windows); } +test "a card's window is token_floor_mux's, inside the reel, and its last kept piece ends where it should" { + assert(card_lo(0) == 98); + assert(card_hi(0) == 176); + assert(card_lo(2) == 392); + // CARD_ON says 450, a frame past the reel's last + assert(CARD_ON[5] == beat_frames()); + assert(card_hi(2) == beat_frames() - 1); + const f0 : i32 = 100; + const f1 : i32 = 300; + // one kept piece: first and last are its own ends + assert(beat_last_hi(95, 160, f0, f1) == 160); + assert(beat_first_lo(95, 160, f0, f1) == 116); + // two kept pieces: the last ends with the window, the first starts with it + assert(beat_last_hi(80, 140, f0, f1) == 140); + assert(beat_last_hi(80, 108, f0, f1) == 99); + // none kept: the window's own end + assert(beat_pieces(102, 110, f0, f1) == 0); + assert(beat_last_hi(102, 110, f0, f1) == 110); +} + +test "in mode 5 no card is on a crossfade, every lesson, both languages, every voice the clock accepts" { + var voices : usize = 0; + var crossing : usize = 0; + var kept : usize = 0; + var t : usize = 0; + while (t < TOPICS) { + var lang : usize = 0; + while (lang < 2) { + var i : usize = 0; + while (i < 4) { + var j : usize = 0; + while (j < 4) { + var k : usize = 0; + while (k < 5) { + const m0 = BEAT_GRID_M0[i]; + const m1 = BEAT_GRID_M1[j]; + const m2 = BEAT_GRID_M2[k]; + if (voice_fits(m0, m1, m2)) { + voices = voices + 1; + const f0 = beat_xfade_frame(beat_cut_ms(t, lang, m0, m1, m2, 0)); + const f1 = beat_xfade_frame(beat_cut_ms(t, lang, m0, m1, m2, 1)); + var c : usize = 0; + while (c < CARDS) { + const lo = card_lo(c); + const hi = card_hi(c); + assert(pieces_clear(lo, hi, f0, f1)); + if (on_xfade(lo, hi, f0, f1)) { + crossing = crossing + 1; + } + if (beat_pieces(lo, hi, f0, f1) > 0) { + kept = kept + 1; + // the rise starts and the fade ends on frames the card is on + const a = beat_first_lo(lo, hi, f0, f1); + const z = beat_last_hi(lo, hi, f0, f1); + assert(a >= lo); + assert(z <= hi); + assert(z >= a); + assert(on_xfade(a, a, f0, f1) == false); + assert(on_xfade(z, z, f0, f1) == false); + } + c = c + 1; + } + // the last card is on in the close beat, which owns 45 frames + assert(beat_pieces(card_lo(2), card_hi(2), f0, f1) == 1); + } + k = k + 1; + } + j = j + 1; + } + i = i + 1; + } + lang = lang + 1; + } + t = t + 1; + } + assert(voices > 0); + assert(kept >= voices); + // control: token_floor_mux's own card windows do meet the crossfades, so the rule has work to do + assert(crossing > 0); + // on lesson 1's measured voice cut 1 is capped, crossfade 1 starts on frame 389, and mode 3's + // last card, on from frame 392, rises under it; mode 5 starts it on the close beat's first frame + const f0 = beat_xfade_frame(beat_cut_ms(0, 0, M_EN0, M_EN1, M_EN2, 0)); + const f1 = beat_xfade_frame(beat_cut_ms(0, 0, M_EN0, M_EN1, M_EN2, 1)); + assert(f1 == 389); + assert(on_xfade(card_lo(2), card_hi(2), f0, f1)); + assert(beat_first_lo(card_lo(2), card_hi(2), f0, f1) == 405); + assert(beat_last_hi(card_lo(2), card_hi(2), f0, f1) == 449); +} + test "the cost on lesson 1's measured voice: a caption across a crossfade with no room is dropped, and every phrase keeps one" { var dropped : usize = 0; var t : usize = 0; diff --git a/specs/t27f/token_floor_mux.t27 b/specs/t27f/token_floor_mux.t27 index cdcb184870..cd63d1eb2b 100644 --- a/specs/t27f/token_floor_mux.t27 +++ b/specs/t27f/token_floor_mux.t27 @@ -321,25 +321,37 @@ fn line_row(l: usize) -> usize { return GOLD_ROW; } -// v, the card's ramp, as an ffmpeg expression in the frame number n: the same clip as card_opacity -fn print_ramp(c: usize) -> void { - std.debug.print("clip(min((n-{d})/{d:.1},({d}-n)/{d:.1}),0,1)", .{ CARD_ON[2 * c], CARD_IN, CARD_ON[2 * c + 1] + 1, CARD_OUT }); +// v, the ramp of a card up on frames lo..hi, as an ffmpeg expression in the frame number n: the +// same clip as card_opacity. A caller with its own window passes it (lesson_reel's mode 5, +// t27#10628); card c's own is CARD_ON. +fn print_ramp(lo: i32, hi: i32) -> void { + std.debug.print("clip(min((n-{d})/{d:.1},({d}-n)/{d:.1}),0,1)", .{ lo, CARD_IN, hi + 1, CARD_OUT }); +} + +// the text's top: its place below the card's top, plus card_drop from frame lo +pub fn print_rise_from(o: usize, lo: i32, dy: f64) -> void { + std.debug.print(":y='{d:.1}+{d:.1}*(1+{d:.1}*max(0,(n-{d})/{d}))*exp(-{d:.1}*max(0,(n-{d})/{d}))", .{ card_f(o, 1) + dy, CARD_RISE, CARD_W, lo, FPS, CARD_W, lo, FPS }); } -// the text's top: its place below the card's top, plus card_drop pub fn print_rise(o: usize, c: usize, dy: f64) -> void { - std.debug.print(":y='{d:.1}+{d:.1}*(1+{d:.1}*max(0,(n-{d})/{d}))*exp(-{d:.1}*max(0,(n-{d})/{d}))", .{ card_f(o, 1) + dy, CARD_RISE, CARD_W, CARD_ON[2 * c], FPS, CARD_W, CARD_ON[2 * c], FPS }); + print_rise_from(o, CARD_ON[2 * c], dy); } -// the smootherstep of the ramp, as card_opacity has it, and the frames the card is up -pub fn print_fade(c: usize) -> void { +// the smootherstep of the ramp over lo..hi, as card_opacity has it, without an enable= +pub fn print_alpha(lo: i32, hi: i32) -> void { std.debug.print(":alpha='pow(", .{}); - print_ramp(c); + print_ramp(lo, hi); std.debug.print(",3)*(", .{}); - print_ramp(c); + print_ramp(lo, hi); std.debug.print("*(6*", .{}); - print_ramp(c); - std.debug.print("-15)+10)':enable='between(n,{d},{d})'", .{ CARD_ON[2 * c], CARD_ON[2 * c + 1] }); + print_ramp(lo, hi); + std.debug.print("-15)+10)'", .{}); +} + +// card c's own fade and the frames it is up +pub fn print_fade(c: usize) -> void { + print_alpha(CARD_ON[2 * c], CARD_ON[2 * c + 1]); + std.debug.print(":enable='between(n,{d},{d})'", .{ CARD_ON[2 * c], CARD_ON[2 * c + 1] }); } fn print_card(o: usize, c: usize) -> void {