Commit Graph
9 Commits
Author SHA1 Message Date
gramps 38c2ad6555 feat(scoreboard): a master on/off switch that also stops the polling
Creator: "even if it's not visible, we want to avoid polling."

ScoreboardEnabled is the layer's master switch in the properties panel. Off means two things:
- the strip frame is null in every scene (per-scene eye toggles stay independent), and
- the cadence policy returns None at every phase — a scoreboard nobody can see must not spend quota.

The switch is persisted (Scoreboard.Enabled), because one that resets would quietly resume polling.

The polling gate is ScoreboardPollsEnabled: master switch AND at least one scene with a visible
scoreboard — so hiding it in all five scenes also stops the polls.

Guards: ScoreboardCadenceTests adds the switch off across live/test/recording; ScoreboardProperties
pins the checkbox and the VM surface.
2026-10-30 14:54:05 -07:00
gramps 5889d71db6 feat(dock): master strip + MONO output (the accessibility switch)
Unit 9, the one that matters most for this creator.

MASTER STRIP (top, nearest the preview):
- one ladder fed by the MEASURED mix — `AudioMixer` now measures the buffer the encoder receives
  (`MasterLevel`, `MasterImbalanceDb`), so the master cannot show a source instead of the output
- `-12.4 dB` readout, and ONE L/R flag that appears above a 6 dB gap instead of a second ladder
- bitrate / FPS / dropped / duration from the existing health poll

MONO OUTPUT:
- `AudioFold.ToMono` averages each stereo pair into both channels, applied after the limiter and
  the sync delay, so the stream AND the recording get what was folded
- `MonoOutput` is persisted (`Audio.MonoOutput`) and restored at startup: an accessibility switch
  that resets each session is an accessibility failure
- one click in the strip, remembered forever

Guard: AudioFoldTests (hard-left reaches the right at half amplitude, hard-right — the inaudible
case — reaches the left, centred material keeps its level, mono buffers untouched) and
MasterStripTests (the strip binds the measured mix + the switch, the store round-trips it, and the
fold is the tested helper rather than new arithmetic). 33 audio tests green.
2026-10-30 13:18:23 -07:00
gramps 841c905c06 Brand logo: one locked image layer per scene above the background, image from App Settings
- Source.IsBrandLogo persisted (additive migration); layer healed on load, removal blocked
- Image stored as an asset; settings keep the asset id and file name
- App Settings: Brand Logo file field with Browse
- Default placement top-left; per-scene eye uses the existing element visibility
Test: BrandLogoTests; layout/background tests pass (34). Not pushed.
2026-10-27 09:25:49 -07:00
gramps 8e1d6f44db Scene strip tile artwork + on-air indicator (creator ruling 2026-10-10). Top-bar tiles now carry their own art: each canonical scene's tile is a per-scene CRT-styled image stored in the internal DB (Scene.ThumbnailAssetId - additive migration, seeded at heal from the creator's five Downloads JPGs via SceneThumbnailSeedPaths), and the preview pane keeps rendering the scene's ORIGINAL background - never the tiles. One shared on-air image (Gemini 4zajyl...jpg) replaces the staged scene's tile and follows the scene swap ('the on-air screen jumps around the top bar as the user swaps scenes'): stored as an Asset BLOB tracked by the OnAirImageAssetId Setting (EnsureOnAirImage, same contract as the alert clip) with MainViewModel.OnAirSeedPath override; RefreshSnapshotsAsync shows it for the staged scene, otherwise thumbnail-or-background art, and a staged Live with no on-air art still falls back to the green placeholder (minis never render live captures). Removed the 6px #22c55e rectangle in PreviewPane's backdrop ('green outline around the preview screen' that surfaced when a scene had no background) - the outer PreviewGlow red border is the real live signal. Doubled the scene-tile labels (FontSize 10 -> 20). Fixed a real bug this surfaced: Save()'s Asset pruner only kept Source/alert-clip rows, so the first save would have DELETED the seeded tile + on-air assets - the keep-set now includes Scene.ThumbnailAssetId and the OnAirImageAssetId Setting. New Settings/Load/Save migrations + SceneThumbnailTests (4 tests: seeded tile in strip, preview keeps original bg, Live-staged tile, no-seed fallback, on-air swap + persistence). 427 pass, 0 build warnings. (Unit: strip tiles + on-air) 2026-10-10 13:27:26 -07:00
gramps 966e718f1e Zoom: fix preview zoom box + shape-aspect anchoring (Unit: zoom box visuals)
Right-click in the live preview now procs a box that is actually visible and
populated with the zoomed region, and each Zoom View Shape sculpts the box per
spec. OBS-style derivative: magnifier box shows the composited region and dims
the surroundings (same pattern as OBS "zoom" / preview region highlights).

Root causes of the invisible box (two WPF traps, recorded in MyMistakes.md):
- Empty <VisualBrush Stretch="None"/> used as an OpacityMask paints nothing,
  so the mask alpha is 0 and the WHOLE box (border included) went transparent.
- ZoomBox lived inside CanvasGrid while its VisualBrush sampled CanvasGrid — a
  circular reference WPF renders as nothing. Hoisted ZoomBox to a sibling of
  CanvasGrid inside a new PreviewCanvas wrapper (same 1920x1080 space).

Shape semantics (MainViewModel.ActivateZoom):
- Circle: square, click point = center.
- VerticalRectangle (phone): tall 9:16, click point = top-left corner.
- HorizontalRectangle (tablet): wide 16:9, click point = top-left corner.
- Boxes stay fully inside 1920x1080 via Math.Clamp.

UI: ZoomRectOutline/ZoomCircleOutline shapes replace the square Border;
circle clips the box to an EllipseGeometry; brush Visual released when inactive.
Zoom View Shape dropdown now uses the standard YtComboBox style.

Test (Good Dog rule, one integration test): ZoomBoxInteractionTests verifies
aspect/anchoring/clamp per shape and the matching outline+clip in the real
MainWindow. Full suite 404 passed, 0 warnings.

Memory: HANDOFF.md zoom state updated; WPF VisualBrush traps appended to
MyMistakes.md.
2026-10-07 20:46:45 -07:00
gramps a11b15e444 feat(alerts): TASK 47 — alert box plays a video (built-in/custom clip) + read-time fade + message ticker
TASK 43's alert box grows a real video celebration. Per-alert IAlertClipDecoder
(ffmpeg bgra + f32le pipes, real-time paced, disposed at drain) plays the shipped
Assets/alert-default.mp4 (stamped into the Asset table at startup) unless the
creator picks their own file — path reference only, never stored in the DB; the
six AlertRenderer animations stay the fallback. ~0.3s fade rides the alpha
envelope on straight-source copies (EOF freeze-frames then fades out); audio
forwards to a new AudioMixer alert ring (8s, 48k stereo) drained at unity — no
duck, creator ruling — scaled by volume × fade. An auto-composed marquee ticker
('Funder — Super Chat · $10.00', 140px/s) scrolls top-of-frame via a
FramePump._alertTicker seam through Render/CompositeLayers, mixed into the cache
signature (dynamic overlay, never baked). New Stream Alerts section in LeftPanel.

Derivative-work references (how OBS/Streamlabs alert boxes do per-alert video):
- https://support.streamlabs.com/hc/en-us/articles/217741147-Setting-Up-Your-Streamlabs-Alerts (custom image/video per alert type + variations)
- https://obsproject.com/kb/stream-tutorial-2-alerts (alert overlay as an on-screen zone)
- https://streamlabs.com/content-hub/widgets/alert-box (per-event alert playback)

Good Dog: AlertLayerVideoTests drives a fake IAlertClipDecoder through the whole
lifecycle in one pass (custom path wins, decoder spawns/disposes, fade envelope
0→127→255, audio volume×fade, ticker scrolls, EOF fade-drain to idle). It caught
the clip branch of Advance not clearing _current before AdvanceToNext — the layer
stayed IsPlaying after drain (MyMistakes post-mortem).

Full vstest 319/319; clean build 0 warnings; scope check green.
2026-09-26 11:15:17 -07:00
gramps b22d08eca6 feat: signed A/V sync offset (−500..+500), negative advances by eating live stream head (OBS eat-head semantics)
Positive offsets still delay the whole mix via the delay line (lip-sync fix);
negative offsets now ARM once at StartLive and drop |N| ms off the pipe's write
head so audio events land earlier when audio runs BEHIND video. Slider relabeled
AUDIO SYNC, Min −500, locked while live/recording (IsEditMode). LayoutStore and
VM clamp to −500..500.

OBS reference for eat-the-head negative sync: https://obsproject.com/kb/obs-studio/buffering-time (negative sync values pull audio earlier by discarding buffered player audio).

Test: StartLive_NegativeOffset_AdvancesAudio_ByDroppingTheStreamHead (6x0.9 head
must be eaten before 0.2 bed reaches the wire).
2026-09-14 12:30:44 -07:00
gramps e8ff4df05f TASK 22: global audio sync offset (positive delay at the mixer out)
Adds AudioSyncOffsetMs (0..500ms, default 0) that delays the whole
interleaved-stereo mix so audio lands on the video when it runs ahead —
OBS's documented lip-sync fix. Positive-only: advancing audio needs a
video-side delay and is out of the audio layer's scope.

- Services/Audio/AudioSyncDelay.cs: pure delay line, flushed on Configure
- AudioMixer: Func<int> syncOffsetMs seam + delay applied post-limiter
- LayoutStore.Settings + MainViewModel.Audio/VM: load/save + binding
- PreviewPane mic bar: SYNC slider + status dot (IntToSyncBrushConverter)
- AudioSyncDelayTests: identity, negative/beyond-500 clamps, 10ms→960 samples

Reference (external scan): https://obs-versions.com/blog/how-to-fix-audio-delay-on-obs
(audio ahead => positive delay). Verified: build 0 warnings; 3/3 delay tests pass.
2026-08-31 18:30:03 -07:00
gramps dcb3637cca refactor: split LayoutStore by concern — 500-limit Phase 3, Commit E
LayoutStore.cs 1402 -> 47-line shell + 5 partials, grouped by functionality
(line count is a consequence, not the goal):

- LayoutStore.Migrations.cs: all DB schema migration (EnsureSchema, GetUserVersion,
  MigrateSourceTable/WebcamConfigTable/ToV3/SceneTable/SceneSocialBarColumn/
  SocialsTable/SocialEntryTable)
- LayoutStore.Load.cs: Load() scene/source read-back
- LayoutStore.Save.cs: Save() full scene tree write
- LayoutStore.Settings.cs: key/value settings + mic/record-folder/reusable-stream
  + license/transition/broadcast/hotkey/window-state GetSetting/UpsertSetting
- LayoutStore.Assets.cs: GetAssetBytes/UpsertAsset blob IO
- LayoutStore.cs: shell — Instance, fields, ctor, Dispose

Converted to public partial class LayoutStore : IDisposable. Pruned per-file
dead usings. Phase 3 DOWN: no production .cs exceeds 500 lines.

Zero behavior change; build 0 warnings; 246 pass, only the 2 known failures.
Docs: ViewModels/index.md Phase 3 tracker updated in same commit.
2026-08-31 09:10:17 -07:00