@vosjs/studio-core 0.43.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3531,16 +3531,17 @@ function planAutoZoom(track, options) {
3531
3531
  const withoutDrags = (list) => list.map((cl) => cl.filter((c) => !dragPress.has(round2(c.t)))).filter((cl) => cl.length > 0);
3532
3532
  const clusters = withoutDrags(grouped.clusters);
3533
3533
  const surfaces = withoutDrags(grouped.surfaces);
3534
- const dragSpans = drags.map((d, i) => ({
3535
- id: `g${i}`,
3536
- in: round2(Math.max(0, d.t0 - lead)),
3537
- out: round2(d.t1 + hold),
3538
- level: clampZoomLevel(Math.min(Math.max(dragLevel, 1.1), maxLevel)),
3539
- cx: round2(d.nx),
3540
- cy: round2(d.ny),
3541
- focusMode: "auto",
3542
- source: "auto"
3534
+ const dragWork = drags.map((d) => ({
3535
+ in: Math.max(0, d.t0 - lead),
3536
+ out: d.t1 + hold,
3537
+ cx: d.nx,
3538
+ cy: d.ny,
3539
+ level: Math.min(Math.max(dragLevel, 1.1), maxLevel)
3543
3540
  }));
3541
+ for (let i = 0; i + 1 < dragWork.length; i++) {
3542
+ if (dragWork[i].out > dragWork[i + 1].in)
3543
+ dragWork[i].out = dragWork[i + 1].in;
3544
+ }
3544
3545
  const eligible = clusters.filter((c) => c.length >= minClusterClicks);
3545
3546
  const clickSpans = [];
3546
3547
  const dragReserved = surfaces.map((cluster, i) => {
@@ -3612,6 +3613,19 @@ function planAutoZoom(track, options) {
3612
3613
  if (typing[i].out > typing[i + 1].in) typing[i].out = typing[i + 1].in;
3613
3614
  }
3614
3615
  typing = typing.filter((t) => t.out - t.in >= 0.3);
3616
+ for (const w of [...clickSpans, ...typing]) cedeTo(w, dragWork);
3617
+ typing = typing.filter((t) => !t.dead && t.out - t.in >= 0.3);
3618
+ for (const z of clickSpans) if (z.out - z.in < 0.3) z.dead = true;
3619
+ const dragSpans = dragWork.filter((d) => d.out - d.in >= 0.3).map((d, i) => ({
3620
+ id: `g${i}`,
3621
+ in: round2(d.in),
3622
+ out: round2(d.out),
3623
+ level: clampZoomLevel(d.level),
3624
+ cx: round2(d.cx),
3625
+ cy: round2(d.cy),
3626
+ focusMode: "auto",
3627
+ source: "auto"
3628
+ }));
3615
3629
  const zSpans = clickSpans.filter((s) => !s.dead).sort((a, b) => a.in - b.in).map((s, i) => ({
3616
3630
  id: `z${i}`,
3617
3631
  in: round2(s.in),
@@ -3645,6 +3659,17 @@ function planAutoZoom(track, options) {
3645
3659
  ]).filter((d) => firstStep === void 0 || d.out > firstStep);
3646
3660
  return within([...spans, ...dwells]).sort((a, b) => a.in - b.in);
3647
3661
  }
3662
+ function cedeTo(w, keep) {
3663
+ for (const k of keep) {
3664
+ if (w.dead || w.in >= k.out || w.out <= k.in) continue;
3665
+ if (k.in <= w.in && k.out >= w.out) w.dead = true;
3666
+ else if (k.in > w.in && k.out < w.out) {
3667
+ if (k.in - w.in >= w.out - k.out) w.out = k.in;
3668
+ else w.in = k.out;
3669
+ } else if (k.in > w.in) w.out = k.in;
3670
+ else w.in = k.out;
3671
+ }
3672
+ }
3648
3673
  function typingSessions(track, width, height, typingGap) {
3649
3674
  const pings = track.filter((e) => e.type === "key").map((e) => ({ t: e.t / 1e3, rect: e.rect, x: e.x, y: e.y }));
3650
3675
  const all = [];
@@ -4245,6 +4270,51 @@ function sourceAt(rated, t) {
4245
4270
  return null;
4246
4271
  }
4247
4272
 
4273
+ // src/lower/loudness.ts
4274
+ var NORMALIZE_MIN_LUFS = -30;
4275
+ var NORMALIZE_MAX_LUFS = -6;
4276
+ var NORMALIZE_PEAK_CEILING_DB = -1;
4277
+ var NORMALIZE_MAX_LIFT_DB = 24;
4278
+ var finite = (v) => typeof v === "number" && Number.isFinite(v);
4279
+ var clamp014 = (v) => Math.max(0, Math.min(1, v));
4280
+ function loudnessMatches(clip) {
4281
+ const l = clip.loudness;
4282
+ return !!l && typeof l === "object" && finite(l.lufs) && typeof l.src === "string" && l.src === clip.key;
4283
+ }
4284
+ function clipGain(clip) {
4285
+ const trim = finite(clip.gain) ? Math.max(0, clip.gain) : 1;
4286
+ if (!finite(clip.normalize)) return { gain: clamp014(trim), basis: "gain" };
4287
+ if (!clip.loudness || typeof clip.loudness !== "object")
4288
+ return { gain: clamp014(trim), basis: "unmeasured" };
4289
+ if (!loudnessMatches(clip)) return { gain: clamp014(trim), basis: "stale" };
4290
+ const l = clip.loudness;
4291
+ const target = Math.max(
4292
+ NORMALIZE_MIN_LUFS,
4293
+ Math.min(NORMALIZE_MAX_LUFS, clip.normalize)
4294
+ );
4295
+ const asked = Math.pow(10, (target - l.lufs) / 20) * trim;
4296
+ let gain = asked;
4297
+ let capped;
4298
+ const lift = Math.pow(10, NORMALIZE_MAX_LIFT_DB / 20);
4299
+ if (gain > lift) {
4300
+ gain = lift;
4301
+ capped = "lift";
4302
+ }
4303
+ if (finite(l.peakDb)) {
4304
+ const peak = Math.pow(10, (NORMALIZE_PEAK_CEILING_DB - l.peakDb) / 20);
4305
+ if (gain > peak) {
4306
+ gain = peak;
4307
+ capped = "peak";
4308
+ }
4309
+ }
4310
+ return {
4311
+ gain: Math.max(0, gain),
4312
+ basis: "normalize",
4313
+ asked,
4314
+ ...capped ? { capped } : {}
4315
+ };
4316
+ }
4317
+
4248
4318
  // src/lower/audioEnvelope.ts
4249
4319
  function clipEnvelope(clip) {
4250
4320
  const span = clipLength(clip);
@@ -4256,7 +4326,7 @@ function clipEnvelope(clip) {
4256
4326
  fi *= scale;
4257
4327
  fo *= scale;
4258
4328
  }
4259
- const g = Math.max(0, Math.min(1, clip.gain));
4329
+ const g = clipGain(clip).gain;
4260
4330
  const pts = [];
4261
4331
  pts.push({ t: clip.start, g: fi > 0 ? 0 : g });
4262
4332
  if (fi > 0) pts.push({ t: clip.start + fi, g });
@@ -4293,8 +4363,8 @@ function followFocusEvents(span, cursor, space, layout, options = {}) {
4293
4363
  }
4294
4364
  const pts = cursor.filter((e) => e.type === "move" || e.type === "down" || e.type === "up").map((e) => ({
4295
4365
  t: e.t / 1e3,
4296
- nx: clamp014(e.x / space.w),
4297
- ny: clamp014(e.y / space.h)
4366
+ nx: clamp015(e.x / space.w),
4367
+ ny: clamp015(e.y / space.h)
4298
4368
  }));
4299
4369
  if (!pts.length) return { entry: null, events: [] };
4300
4370
  const entry = clampFocus(span.cx, span.cy, level, layout, camera);
@@ -4372,7 +4442,7 @@ function sampleAt2(pts, t) {
4372
4442
  }
4373
4443
  return pts[pts.length - 1];
4374
4444
  }
4375
- function clamp014(v) {
4445
+ function clamp015(v) {
4376
4446
  return Math.max(0, Math.min(1, v));
4377
4447
  }
4378
4448
  function round4(v) {
@@ -8731,17 +8801,22 @@ function studioLayerData(input, duration, pins) {
8731
8801
  // preview scheduler and the export's offline mix). `duckEnv` (the mic-derived
8732
8802
  // duck multiplier curve) is merged in asynchronously by useComposition — it
8733
8803
  // needs a decoded recording, which a sync lowering can't produce.
8734
- audio: (layers.audio ?? []).map((c) => ({
8735
- key: c.key,
8736
- start: round8(c.start),
8737
- in: round8(c.in),
8738
- out: round8(c.out),
8739
- gain: round8(c.gain),
8740
- loop: !!c.loop,
8741
- len: round8(clipLength(c)),
8742
- duck: !!c.duck,
8743
- env: clipEnvelope(c).map((p) => ({ t: round8(p.t), g: round8(p.g) }))
8744
- })),
8804
+ // A measured `normalize` sets the gain (it may exceed 1); a clip with
8805
+ // no target keeps its own, byte for byte.
8806
+ audio: (layers.audio ?? []).map((c) => {
8807
+ const g = clipGain(c);
8808
+ return {
8809
+ key: c.key,
8810
+ start: round8(c.start),
8811
+ in: round8(c.in),
8812
+ out: round8(c.out),
8813
+ gain: round8(g.basis === "normalize" ? g.gain : c.gain),
8814
+ loop: !!c.loop,
8815
+ len: round8(clipLength(c)),
8816
+ duck: !!c.duck,
8817
+ env: clipEnvelope(c).map((p) => ({ t: round8(p.t), g: round8(p.g) }))
8818
+ };
8819
+ }),
8745
8820
  // Object clips: numbers resolved HERE; shapes are the drafted engine spec.
8746
8821
  ...layers.objects && layers.objects.length ? {
8747
8822
  objects: layers.objects.map((o) => ({
@@ -11326,10 +11401,10 @@ function cursorFocusAt(doc, srcT) {
11326
11401
  let best = track[0];
11327
11402
  for (const e of track)
11328
11403
  if (Math.abs(e.t - ms) < Math.abs(best.t - ms)) best = e;
11329
- const clamp015 = (v) => Math.max(0, Math.min(1, v));
11404
+ const clamp016 = (v) => Math.max(0, Math.min(1, v));
11330
11405
  return {
11331
- cx: round13(clamp015(best.x / width)),
11332
- cy: round13(clamp015(best.y / height))
11406
+ cx: round13(clamp016(best.x / width)),
11407
+ cy: round13(clamp016(best.y / height))
11333
11408
  };
11334
11409
  }
11335
11410
  var tiltLane = {
@@ -12197,6 +12272,133 @@ function duckCurve(rms, segments, durationSec2, opts = DEFAULT_DUCK) {
12197
12272
  return points;
12198
12273
  }
12199
12274
 
12275
+ // src/docFeatures.ts
12276
+ var overlaysOf = (doc) => Array.isArray(doc.overlays) ? doc.overlays : [];
12277
+ var audioOf = (doc) => Array.isArray(doc.audio) ? doc.audio : [];
12278
+ var anchored = (o) => !!o && !!o.anchor && typeof o.anchor === "object";
12279
+ function bakeAnchors(doc) {
12280
+ const layers = overlaysOf(doc);
12281
+ if (!layers.some(anchored)) return { doc, notes: [] };
12282
+ const notes = [];
12283
+ let resolved = doc;
12284
+ if (isRecordingDoc(doc)) {
12285
+ const rated = ratedSegments(migrateMotion(doc));
12286
+ resolved = resolveOverlayAnchors(doc, rated);
12287
+ for (const o of layers) {
12288
+ if (!anchored(o) || !o.anchor) continue;
12289
+ const r = resolveAnchor(doc, o.anchor, rated);
12290
+ if (r.start === null)
12291
+ notes.push(
12292
+ `overlay ${o.id}: its step ${r.miss === "cut" ? "is cut out of the film" : r.miss === "skipped" ? "was skipped by the recorder" : "is not in the take"}, so it plays at its fallback start (${o.start.toFixed(2)}s)`
12293
+ );
12294
+ }
12295
+ }
12296
+ const overlays = overlaysOf(resolved).map((o) => {
12297
+ if (!anchored(o)) return o;
12298
+ const { anchor: _anchor, ...rest } = o;
12299
+ return rest;
12300
+ });
12301
+ return { doc: { ...resolved, overlays }, notes };
12302
+ }
12303
+ var DOC_FEATURES = [
12304
+ {
12305
+ id: "text-runs",
12306
+ since: "0.38.0",
12307
+ name: "styled text runs",
12308
+ before: "would draw those layers without their words",
12309
+ uses: (doc) => overlaysOf(doc).filter((o) => o?.kind === "text" && Array.isArray(o.text)).length
12310
+ },
12311
+ {
12312
+ id: "overlay-anchor",
12313
+ since: "0.43.0",
12314
+ name: "caption anchors",
12315
+ before: "would show them at their fallback start",
12316
+ uses: (doc) => overlaysOf(doc).filter(anchored).length,
12317
+ bake: bakeAnchors
12318
+ },
12319
+ {
12320
+ id: "text-elapsed",
12321
+ since: "0.43.0",
12322
+ name: "{elapsed} in text",
12323
+ before: "would show the placeholder as it is written",
12324
+ // Filled only on an anchored layer: anywhere else every release shows
12325
+ // the placeholder as written.
12326
+ uses: (doc) => overlaysOf(doc).filter(
12327
+ (o) => anchored(o) && o.kind === "text" && hasElapsed(o.text)
12328
+ ).length,
12329
+ // The anchors' bake fills it.
12330
+ bake: bakeAnchors
12331
+ },
12332
+ {
12333
+ id: "audio-normalize",
12334
+ since: "0.44.0",
12335
+ name: "loudness targets on sound",
12336
+ before: "would play those clips at their gain alone",
12337
+ // Only a target that applies: one with no measurement for its key is
12338
+ // ignored by every release alike.
12339
+ uses: (doc) => audioOf(doc).filter((a) => a && clipGain(a).basis === "normalize").length,
12340
+ bake: (doc) => {
12341
+ const notes = [];
12342
+ const audio = audioOf(doc).map((a) => {
12343
+ if (!a || a.normalize === void 0) return a;
12344
+ const { normalize: _normalize, ...rest } = a;
12345
+ const g = clipGain(a);
12346
+ if (g.basis !== "normalize") return rest;
12347
+ const gain = Math.round(Math.min(1, g.gain) * 1e3) / 1e3;
12348
+ if (g.gain > 1)
12349
+ notes.push(
12350
+ `audio ${a.id}: its target asks for gain ${g.gain.toFixed(2)}, which an older renderer cannot play; baked at 1, so it plays ${(20 * Math.log10(g.gain)).toFixed(1)} dB quieter than asked`
12351
+ );
12352
+ return { ...rest, gain };
12353
+ });
12354
+ return { doc: { ...doc, audio }, notes };
12355
+ }
12356
+ }
12357
+ ];
12358
+ function compareVersions(a, b) {
12359
+ const parts = (v) => v.trim().replace(/^v/, "").split(/[-+]/)[0].split(".").map((n) => parseInt(n, 10) || 0);
12360
+ const x = parts(a);
12361
+ const y = parts(b);
12362
+ for (let i = 0; i < Math.max(x.length, y.length, 3); i++) {
12363
+ const d = (x[i] ?? 0) - (y[i] ?? 0);
12364
+ if (d) return d < 0 ? -1 : 1;
12365
+ }
12366
+ return 0;
12367
+ }
12368
+ function docFeatures(doc) {
12369
+ const out = [];
12370
+ for (const feature of DOC_FEATURES) {
12371
+ const count = feature.uses(doc);
12372
+ if (count > 0) out.push({ feature, count });
12373
+ }
12374
+ return out;
12375
+ }
12376
+ function featuresNewerThan(doc, version) {
12377
+ return docFeatures(doc).filter(
12378
+ (u) => compareVersions(u.feature.since, version) > 0
12379
+ );
12380
+ }
12381
+ function bakeDocFeatures(doc, version) {
12382
+ let next = structuredClone(doc);
12383
+ const baked = [];
12384
+ const unbakeable = [];
12385
+ const notes = [];
12386
+ const done = /* @__PURE__ */ new Set();
12387
+ for (const { feature } of featuresNewerThan(doc, version)) {
12388
+ if (!feature.bake) {
12389
+ unbakeable.push(feature);
12390
+ continue;
12391
+ }
12392
+ baked.push(feature);
12393
+ if (done.has(feature.bake)) continue;
12394
+ done.add(feature.bake);
12395
+ const r = feature.bake(next);
12396
+ next = r.doc;
12397
+ notes.push(...r.notes);
12398
+ }
12399
+ return { doc: next, baked, unbakeable, notes };
12400
+ }
12401
+
12200
12402
  // src/backdrop.ts
12201
12403
  function backdropMedia(backdrop) {
12202
12404
  return {
@@ -12317,6 +12519,7 @@ export {
12317
12519
  DIGEST_CROP_MAX,
12318
12520
  DIGEST_FULL_MAX,
12319
12521
  DIGEST_VERSION,
12522
+ DOC_FEATURES,
12320
12523
  DOC_SCHEMA_VERSION,
12321
12524
  DRAG_FIT_LEVEL,
12322
12525
  DRAG_MIN_S,
@@ -12358,6 +12561,10 @@ export {
12358
12561
  MIXED,
12359
12562
  MOTION_DELTA,
12360
12563
  MOTION_EASE,
12564
+ NORMALIZE_MAX_LIFT_DB,
12565
+ NORMALIZE_MAX_LUFS,
12566
+ NORMALIZE_MIN_LUFS,
12567
+ NORMALIZE_PEAK_CEILING_DB,
12361
12568
  OBJECT_DEFAULT_SCALE,
12362
12569
  OVERLAY_FONT_FACES,
12363
12570
  OVERLAY_LINE_HEIGHT,
@@ -12439,6 +12646,7 @@ export {
12439
12646
  aspectRatioValue,
12440
12647
  audioLane,
12441
12648
  backdropMedia,
12649
+ bakeDocFeatures,
12442
12650
  boldWeightFor,
12443
12651
  buildDigest,
12444
12652
  calloutBox,
@@ -12473,9 +12681,11 @@ export {
12473
12681
  clampZoomLevel,
12474
12682
  clearStyle,
12475
12683
  clipEnvelope,
12684
+ clipGain,
12476
12685
  clipLength,
12477
12686
  clipsFrom,
12478
12687
  commonStyle,
12688
+ compareVersions,
12479
12689
  computeCardLayout,
12480
12690
  computeMicRms,
12481
12691
  computePeaks,
@@ -12496,6 +12706,7 @@ export {
12496
12706
  destinationsForChannel,
12497
12707
  diffInput,
12498
12708
  docCardLayout,
12709
+ docFeatures,
12499
12710
  docFreezes,
12500
12711
  docMediaList,
12501
12712
  docOutputDuration,
@@ -12525,6 +12736,7 @@ export {
12525
12736
  expectedFrameSize,
12526
12737
  exportSizeFor,
12527
12738
  extractClicks,
12739
+ featuresNewerThan,
12528
12740
  findStep,
12529
12741
  focusBounds,
12530
12742
  focusForViewportCentre,
@@ -12572,6 +12784,7 @@ export {
12572
12784
  liveZoomStyleName,
12573
12785
  lookFromBrand,
12574
12786
  lookKindForGround,
12787
+ loudnessMatches,
12575
12788
  lowerProgramDoc,
12576
12789
  lowerStudioDoc,
12577
12790
  lowerToComposition,