@real-music-packages/web-core 0.12.0 → 0.14.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/audio.d.ts +36 -0
- package/dist/audio.js +55 -0
- package/dist/audio.js.map +1 -1
- package/dist/scene/index.d.ts +214 -78
- package/dist/scene/index.js +289 -106
- package/dist/scene/index.js.map +1 -1
- package/package.json +1 -1
package/dist/scene/index.js
CHANGED
|
@@ -177,6 +177,10 @@ var easeOut = (t) => {
|
|
|
177
177
|
const c = clamp(t, 0, 1);
|
|
178
178
|
return 1 - (1 - c) * (1 - c);
|
|
179
179
|
};
|
|
180
|
+
var transitionProgress = (tMs, startMs, durationMs, easing = easeInOut) => {
|
|
181
|
+
if (durationMs <= 0) return 1;
|
|
182
|
+
return easing(invLerp(startMs, startMs + durationMs, tMs));
|
|
183
|
+
};
|
|
180
184
|
|
|
181
185
|
// src/scene/caption.ts
|
|
182
186
|
function activeCue(script, tMs) {
|
|
@@ -2617,6 +2621,96 @@ registerLayer(circleOfFifthsFactory);
|
|
|
2617
2621
|
registerLayer(pitchContourFactory);
|
|
2618
2622
|
registerLayer(sectionMinimapFactory);
|
|
2619
2623
|
|
|
2624
|
+
// src/scene/audioLayers.ts
|
|
2625
|
+
function countInSchedule(opts) {
|
|
2626
|
+
const beats = opts.beats ?? 4;
|
|
2627
|
+
const accent = opts.accentDownbeat ?? true;
|
|
2628
|
+
const beatSec = 60 / opts.bpm;
|
|
2629
|
+
const events = [];
|
|
2630
|
+
for (let i = 0; i < beats; i++) {
|
|
2631
|
+
const isDownbeat = accent && i === 0;
|
|
2632
|
+
events.push({
|
|
2633
|
+
atSec: i * beatSec,
|
|
2634
|
+
note: isDownbeat ? "C6" : "C5",
|
|
2635
|
+
durSec: 0.05,
|
|
2636
|
+
gain: isDownbeat ? 1 : 0.7,
|
|
2637
|
+
kind: "count"
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2640
|
+
return events;
|
|
2641
|
+
}
|
|
2642
|
+
function countInLeadSec(opts) {
|
|
2643
|
+
return (opts.beats ?? 4) * (60 / opts.bpm);
|
|
2644
|
+
}
|
|
2645
|
+
function clickTrackSchedule(opts) {
|
|
2646
|
+
const beatSec = 60 / opts.bpm;
|
|
2647
|
+
const bpb = opts.beatsPerBar ?? 4;
|
|
2648
|
+
const start = opts.startSec ?? 0;
|
|
2649
|
+
const events = [];
|
|
2650
|
+
const n = Math.floor(opts.durationSec / beatSec);
|
|
2651
|
+
for (let i = 0; i < n; i++) {
|
|
2652
|
+
const isDownbeat = i % bpb === 0;
|
|
2653
|
+
events.push({
|
|
2654
|
+
atSec: start + i * beatSec,
|
|
2655
|
+
note: isDownbeat ? "C6" : "C5",
|
|
2656
|
+
durSec: 0.03,
|
|
2657
|
+
gain: isDownbeat ? 0.6 : 0.4,
|
|
2658
|
+
kind: "click"
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
return events;
|
|
2662
|
+
}
|
|
2663
|
+
function droneSchedule(opts) {
|
|
2664
|
+
const gain = opts.gain ?? 0.15;
|
|
2665
|
+
const events = [
|
|
2666
|
+
{ atSec: 0, note: opts.root, durSec: opts.durationSec, gain, kind: "drone" }
|
|
2667
|
+
];
|
|
2668
|
+
if (opts.fifth ?? true) {
|
|
2669
|
+
events.push({ atSec: 0, note: transposeFifth(opts.root), durSec: opts.durationSec, gain, kind: "drone" });
|
|
2670
|
+
}
|
|
2671
|
+
return events;
|
|
2672
|
+
}
|
|
2673
|
+
function transposeFifth(note) {
|
|
2674
|
+
const m = /^([A-G])(#|b)?(\d)$/.exec(note);
|
|
2675
|
+
if (!m) return note;
|
|
2676
|
+
const order = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
|
2677
|
+
const pcMap = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
|
|
2678
|
+
let pc = pcMap[m[1]] + (m[2] === "#" ? 1 : m[2] === "b" ? -1 : 0);
|
|
2679
|
+
let oct = parseInt(m[3], 10);
|
|
2680
|
+
pc += 7;
|
|
2681
|
+
if (pc >= 12) {
|
|
2682
|
+
pc -= 12;
|
|
2683
|
+
oct += 1;
|
|
2684
|
+
}
|
|
2685
|
+
return `${order[pc]}${oct}`;
|
|
2686
|
+
}
|
|
2687
|
+
function duckGainAt(windows, tSec, floor = 0.25, rampSec = 0.2) {
|
|
2688
|
+
for (const w of windows) {
|
|
2689
|
+
if (tSec >= w.startSec - rampSec && tSec <= w.endSec + rampSec) {
|
|
2690
|
+
if (tSec < w.startSec) return lerpGain(1, floor, (tSec - (w.startSec - rampSec)) / rampSec);
|
|
2691
|
+
if (tSec > w.endSec) return lerpGain(floor, 1, (tSec - w.endSec) / rampSec);
|
|
2692
|
+
return floor;
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
return 1;
|
|
2696
|
+
}
|
|
2697
|
+
function lerpGain(a, b, t) {
|
|
2698
|
+
const k = t < 0 ? 0 : t > 1 ? 1 : t;
|
|
2699
|
+
return a + (b - a) * k;
|
|
2700
|
+
}
|
|
2701
|
+
function applySchedule(instrument, schedule, startTime) {
|
|
2702
|
+
for (const ev of schedule) {
|
|
2703
|
+
if (ev.note == null) continue;
|
|
2704
|
+
instrument.triggerAttackRelease(
|
|
2705
|
+
ev.note,
|
|
2706
|
+
ev.durSec ?? 0.05,
|
|
2707
|
+
startTime + ev.atSec,
|
|
2708
|
+
ev.gain ?? 1
|
|
2709
|
+
);
|
|
2710
|
+
}
|
|
2711
|
+
return schedule.length;
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2620
2714
|
// src/scene/camera.ts
|
|
2621
2715
|
function cameraTransform(cam, W, H) {
|
|
2622
2716
|
const s = cam.zoom;
|
|
@@ -2658,7 +2752,13 @@ function resolveTimeline(spec, totalSec) {
|
|
|
2658
2752
|
return spec.timeline.map((seg) => {
|
|
2659
2753
|
const startSec = resolveAnchor(seg.at[0], totalSec);
|
|
2660
2754
|
const endSec = resolveAnchor(seg.at[1], totalSec);
|
|
2661
|
-
return {
|
|
2755
|
+
return {
|
|
2756
|
+
startMs: startSec * 1e3,
|
|
2757
|
+
endMs: endSec * 1e3,
|
|
2758
|
+
layers: seg.layers,
|
|
2759
|
+
audio: seg.audio,
|
|
2760
|
+
transition: seg.transition
|
|
2761
|
+
};
|
|
2662
2762
|
});
|
|
2663
2763
|
}
|
|
2664
2764
|
function visualTimelineMs(resolved) {
|
|
@@ -2674,6 +2774,24 @@ var SCREEN_PINNED_KEYS = /* @__PURE__ */ new Set([
|
|
|
2674
2774
|
"safe-guides",
|
|
2675
2775
|
"mcq-card"
|
|
2676
2776
|
]);
|
|
2777
|
+
function defaultBufferFactory() {
|
|
2778
|
+
const g = globalThis;
|
|
2779
|
+
if (typeof g.OffscreenCanvas === "function") {
|
|
2780
|
+
return (w, h) => {
|
|
2781
|
+
const c = new g.OffscreenCanvas(w, h);
|
|
2782
|
+
return { ctx: c.getContext("2d"), image: c };
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
if (typeof g.document !== "undefined" && g.document?.createElement) {
|
|
2786
|
+
return (w, h) => {
|
|
2787
|
+
const c = g.document.createElement("canvas");
|
|
2788
|
+
c.width = w;
|
|
2789
|
+
c.height = h;
|
|
2790
|
+
return { ctx: c.getContext("2d"), image: c };
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
return void 0;
|
|
2794
|
+
}
|
|
2677
2795
|
async function buildScene(opts) {
|
|
2678
2796
|
const { spec, theme, score } = opts;
|
|
2679
2797
|
const [W, H] = spec.size;
|
|
@@ -2681,6 +2799,8 @@ async function buildScene(opts) {
|
|
|
2681
2799
|
const resolved = resolveTimeline(spec, opts.totalSec);
|
|
2682
2800
|
const safe = safeBox(W, H);
|
|
2683
2801
|
const camera = opts.camera ?? (() => identityCamera(W, H));
|
|
2802
|
+
const bufferFactory = opts.bufferFactory ?? defaultBufferFactory();
|
|
2803
|
+
const hasTransitions = resolved.some((s) => s.transition && s.transition.durationMs > 0);
|
|
2684
2804
|
const clock = { nowMs: () => 0 };
|
|
2685
2805
|
const baseCtx = {
|
|
2686
2806
|
W,
|
|
@@ -2692,7 +2812,8 @@ async function buildScene(opts) {
|
|
|
2692
2812
|
fps
|
|
2693
2813
|
};
|
|
2694
2814
|
const bound = [];
|
|
2695
|
-
for (
|
|
2815
|
+
for (let segIndex = 0; segIndex < resolved.length; segIndex++) {
|
|
2816
|
+
const seg = resolved[segIndex];
|
|
2696
2817
|
for (const sl of seg.layers) {
|
|
2697
2818
|
const factory = getLayerFactory(sl.k);
|
|
2698
2819
|
if (!factory) {
|
|
@@ -2708,32 +2829,175 @@ async function buildScene(opts) {
|
|
|
2708
2829
|
layer,
|
|
2709
2830
|
startMs: seg.startMs,
|
|
2710
2831
|
endMs: seg.endMs,
|
|
2832
|
+
segIndex,
|
|
2711
2833
|
screenPinned: SCREEN_PINNED_KEYS.has(sl.k)
|
|
2712
2834
|
});
|
|
2713
2835
|
}
|
|
2714
2836
|
}
|
|
2715
2837
|
const durationMs = visualTimelineMs(resolved);
|
|
2716
|
-
function
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2838
|
+
function dirVec(dir) {
|
|
2839
|
+
switch (dir) {
|
|
2840
|
+
case "right":
|
|
2841
|
+
return { x: 1, y: 0 };
|
|
2842
|
+
case "up":
|
|
2843
|
+
return { x: 0, y: -1 };
|
|
2844
|
+
case "down":
|
|
2845
|
+
return { x: 0, y: 1 };
|
|
2846
|
+
case "left":
|
|
2847
|
+
default:
|
|
2848
|
+
return { x: -1, y: 0 };
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
function transitionEnvs(tr, p) {
|
|
2852
|
+
if (tr.type === "push") {
|
|
2853
|
+
const v = dirVec(tr.direction);
|
|
2854
|
+
const span = Math.abs(v.x) ? W : H;
|
|
2855
|
+
return {
|
|
2856
|
+
incoming: { alpha: 1, dx: -v.x * span * (1 - p), dy: -v.y * span * (1 - p) },
|
|
2857
|
+
outgoing: { alpha: 1, dx: v.x * span * p, dy: v.y * span * p }
|
|
2858
|
+
};
|
|
2859
|
+
}
|
|
2860
|
+
if (tr.type === "wipe") {
|
|
2861
|
+
const v = dirVec(tr.direction);
|
|
2862
|
+
let inClip;
|
|
2863
|
+
let outClip;
|
|
2864
|
+
if (v.x !== 0) {
|
|
2865
|
+
const w = W * p;
|
|
2866
|
+
if (v.x < 0) {
|
|
2867
|
+
inClip = { x: 0, y: 0, w, h: H };
|
|
2868
|
+
outClip = { x: w, y: 0, w: W - w, h: H };
|
|
2869
|
+
} else {
|
|
2870
|
+
inClip = { x: W - w, y: 0, w, h: H };
|
|
2871
|
+
outClip = { x: 0, y: 0, w: W - w, h: H };
|
|
2872
|
+
}
|
|
2873
|
+
} else {
|
|
2874
|
+
const h = H * p;
|
|
2875
|
+
if (v.y < 0) {
|
|
2876
|
+
inClip = { x: 0, y: 0, w: W, h };
|
|
2877
|
+
outClip = { x: 0, y: h, w: W, h: H - h };
|
|
2878
|
+
} else {
|
|
2879
|
+
inClip = { x: 0, y: H - h, w: W, h };
|
|
2880
|
+
outClip = { x: 0, y: 0, w: W, h: H - h };
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
return {
|
|
2884
|
+
incoming: { alpha: 1, dx: 0, dy: 0, clip: inClip },
|
|
2885
|
+
outgoing: { alpha: 1, dx: 0, dy: 0, clip: outClip }
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
return {
|
|
2889
|
+
incoming: { alpha: p, dx: 0, dy: 0 },
|
|
2890
|
+
outgoing: { alpha: 1 - p, dx: 0, dy: 0 }
|
|
2891
|
+
};
|
|
2892
|
+
}
|
|
2893
|
+
function frameplan(tMs) {
|
|
2894
|
+
const plan = [];
|
|
2895
|
+
for (let i = 0; i < resolved.length; i++) {
|
|
2896
|
+
const seg = resolved[i];
|
|
2897
|
+
if (tMs < seg.startMs || tMs >= seg.endMs) continue;
|
|
2898
|
+
const tr = seg.transition;
|
|
2899
|
+
const prev = i > 0 ? resolved[i - 1] : void 0;
|
|
2900
|
+
const enterEnd = tr ? seg.startMs + Math.max(0, tr.durationMs) : seg.startMs;
|
|
2901
|
+
const inEnter = tr && tr.durationMs > 0 && tMs < enterEnd && prev;
|
|
2902
|
+
if (!inEnter) {
|
|
2903
|
+
plan.push({ segIndex: i, drawTMs: tMs, env: null });
|
|
2904
|
+
continue;
|
|
2905
|
+
}
|
|
2906
|
+
const p = transitionProgress(tMs, seg.startMs, tr.durationMs, tr.easing ?? easeInOut);
|
|
2907
|
+
const { incoming, outgoing } = transitionEnvs(tr, p);
|
|
2908
|
+
const heldTMs = clamp(tMs, prev.startMs, prev.endMs - 1);
|
|
2909
|
+
plan.push({ segIndex: i - 1, drawTMs: heldTMs, env: outgoing });
|
|
2910
|
+
plan.push({ segIndex: i, drawTMs: tMs, env: incoming });
|
|
2911
|
+
}
|
|
2912
|
+
return plan;
|
|
2913
|
+
}
|
|
2914
|
+
function drawSegmentInto(target, segIndex, drawTMs) {
|
|
2915
|
+
const ctx = { ...baseCtx, ctx2d: target };
|
|
2916
|
+
const cam = camera(drawTMs);
|
|
2917
|
+
target.save();
|
|
2918
|
+
applyToContext(target, cam, W, H);
|
|
2722
2919
|
for (const b of bound) {
|
|
2723
|
-
if (b.screenPinned) continue;
|
|
2724
|
-
if (
|
|
2725
|
-
b.layer.draw(ctx,
|
|
2920
|
+
if (b.segIndex !== segIndex || b.screenPinned) continue;
|
|
2921
|
+
if (drawTMs < b.startMs || drawTMs >= b.endMs) continue;
|
|
2922
|
+
b.layer.draw(ctx, drawTMs);
|
|
2726
2923
|
}
|
|
2727
|
-
|
|
2924
|
+
target.restore();
|
|
2925
|
+
target.save();
|
|
2926
|
+
target.setTransform(1, 0, 0, 1, 0, 0);
|
|
2927
|
+
for (const b of bound) {
|
|
2928
|
+
if (b.segIndex !== segIndex || !b.screenPinned) continue;
|
|
2929
|
+
if (drawTMs < b.startMs || drawTMs >= b.endMs) continue;
|
|
2930
|
+
b.layer.draw(ctx, drawTMs);
|
|
2931
|
+
}
|
|
2932
|
+
target.restore();
|
|
2933
|
+
}
|
|
2934
|
+
function compositeEntry(ctx2d, e) {
|
|
2935
|
+
if (e.env === null) {
|
|
2936
|
+
drawSegmentInto(ctx2d, e.segIndex, e.drawTMs);
|
|
2937
|
+
return;
|
|
2938
|
+
}
|
|
2939
|
+
const env = e.env;
|
|
2940
|
+
if (!bufferFactory) {
|
|
2941
|
+
ctx2d.save();
|
|
2942
|
+
ctx2d.globalAlpha = ctx2d.globalAlpha * env.alpha;
|
|
2943
|
+
drawSegmentInto(ctx2d, e.segIndex, e.drawTMs);
|
|
2944
|
+
ctx2d.restore();
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
2947
|
+
const buf = bufferFactory(W, H);
|
|
2948
|
+
drawSegmentInto(buf.ctx, e.segIndex, e.drawTMs);
|
|
2728
2949
|
ctx2d.save();
|
|
2729
2950
|
ctx2d.setTransform(1, 0, 0, 1, 0, 0);
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2951
|
+
if (env.alpha !== 1) ctx2d.globalAlpha = ctx2d.globalAlpha * env.alpha;
|
|
2952
|
+
if (env.clip) {
|
|
2953
|
+
ctx2d.beginPath();
|
|
2954
|
+
ctx2d.rect(env.clip.x, env.clip.y, env.clip.w, env.clip.h);
|
|
2955
|
+
ctx2d.clip();
|
|
2734
2956
|
}
|
|
2957
|
+
ctx2d.drawImage(buf.image, env.dx, env.dy, W, H);
|
|
2735
2958
|
ctx2d.restore();
|
|
2736
2959
|
}
|
|
2960
|
+
function renderFrame(ctx2d, tMs) {
|
|
2961
|
+
clock.nowMs = () => tMs;
|
|
2962
|
+
const ctx = { ...baseCtx, ctx2d };
|
|
2963
|
+
const cam = camera(tMs);
|
|
2964
|
+
if (!hasTransitions) {
|
|
2965
|
+
ctx2d.save();
|
|
2966
|
+
applyToContext(ctx2d, cam, W, H);
|
|
2967
|
+
for (const b of bound) {
|
|
2968
|
+
if (b.screenPinned) continue;
|
|
2969
|
+
if (tMs < b.startMs || tMs >= b.endMs) continue;
|
|
2970
|
+
b.layer.draw(ctx, tMs);
|
|
2971
|
+
}
|
|
2972
|
+
ctx2d.restore();
|
|
2973
|
+
ctx2d.save();
|
|
2974
|
+
ctx2d.setTransform(1, 0, 0, 1, 0, 0);
|
|
2975
|
+
for (const b of bound) {
|
|
2976
|
+
if (!b.screenPinned) continue;
|
|
2977
|
+
if (tMs < b.startMs || tMs >= b.endMs) continue;
|
|
2978
|
+
b.layer.draw(ctx, tMs);
|
|
2979
|
+
}
|
|
2980
|
+
ctx2d.restore();
|
|
2981
|
+
return;
|
|
2982
|
+
}
|
|
2983
|
+
const plan = frameplan(tMs);
|
|
2984
|
+
for (const e of plan) compositeEntry(ctx2d, e);
|
|
2985
|
+
}
|
|
2986
|
+
function scheduleAudio(instrument, baseSec) {
|
|
2987
|
+
let fired = 0;
|
|
2988
|
+
for (const seg of resolved) {
|
|
2989
|
+
if (!seg.audio) continue;
|
|
2990
|
+
const segStartSec = baseSec + seg.startMs / 1e3;
|
|
2991
|
+
if (seg.audio.schedule?.length) {
|
|
2992
|
+
fired += applySchedule(instrument, seg.audio.schedule, segStartSec);
|
|
2993
|
+
}
|
|
2994
|
+
if (seg.audio.onEnter) {
|
|
2995
|
+
const segClock = { nowMs: () => seg.startMs };
|
|
2996
|
+
seg.audio.onEnter(segClock, { instrument, startSec: segStartSec, score });
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
return fired;
|
|
3000
|
+
}
|
|
2737
3001
|
return {
|
|
2738
3002
|
W,
|
|
2739
3003
|
H,
|
|
@@ -2741,6 +3005,7 @@ async function buildScene(opts) {
|
|
|
2741
3005
|
durationMs,
|
|
2742
3006
|
resolved,
|
|
2743
3007
|
renderFrame,
|
|
3008
|
+
scheduleAudio,
|
|
2744
3009
|
dispose() {
|
|
2745
3010
|
for (const b of bound) b.layer.dispose?.();
|
|
2746
3011
|
}
|
|
@@ -2751,6 +3016,13 @@ async function recordSceneSpec(opts) {
|
|
|
2751
3016
|
const record = opts.record ?? recordScenes;
|
|
2752
3017
|
const composite = {
|
|
2753
3018
|
durationMs: built.durationMs,
|
|
3019
|
+
// Schedule any per-segment audio against the capture clock exactly when the
|
|
3020
|
+
// first frame is entered, so it's anchored to the same start the recorder
|
|
3021
|
+
// uses. onEnter fires once per Scene (see recordScenes), so this runs once.
|
|
3022
|
+
onEnter: opts.segmentAudio ? () => {
|
|
3023
|
+
const sa = opts.segmentAudio;
|
|
3024
|
+
built.scheduleAudio(sa.instrument, sa.audioNow() + (sa.lookaheadSec ?? 0.1));
|
|
3025
|
+
} : void 0,
|
|
2754
3026
|
draw: (ctx, t01) => built.renderFrame(ctx, t01 * built.durationMs)
|
|
2755
3027
|
};
|
|
2756
3028
|
return record([composite], {
|
|
@@ -2763,96 +3035,6 @@ async function recordSceneSpec(opts) {
|
|
|
2763
3035
|
});
|
|
2764
3036
|
}
|
|
2765
3037
|
|
|
2766
|
-
// src/scene/audioLayers.ts
|
|
2767
|
-
function countInSchedule(opts) {
|
|
2768
|
-
const beats = opts.beats ?? 4;
|
|
2769
|
-
const accent = opts.accentDownbeat ?? true;
|
|
2770
|
-
const beatSec = 60 / opts.bpm;
|
|
2771
|
-
const events = [];
|
|
2772
|
-
for (let i = 0; i < beats; i++) {
|
|
2773
|
-
const isDownbeat = accent && i === 0;
|
|
2774
|
-
events.push({
|
|
2775
|
-
atSec: i * beatSec,
|
|
2776
|
-
note: isDownbeat ? "C6" : "C5",
|
|
2777
|
-
durSec: 0.05,
|
|
2778
|
-
gain: isDownbeat ? 1 : 0.7,
|
|
2779
|
-
kind: "count"
|
|
2780
|
-
});
|
|
2781
|
-
}
|
|
2782
|
-
return events;
|
|
2783
|
-
}
|
|
2784
|
-
function countInLeadSec(opts) {
|
|
2785
|
-
return (opts.beats ?? 4) * (60 / opts.bpm);
|
|
2786
|
-
}
|
|
2787
|
-
function clickTrackSchedule(opts) {
|
|
2788
|
-
const beatSec = 60 / opts.bpm;
|
|
2789
|
-
const bpb = opts.beatsPerBar ?? 4;
|
|
2790
|
-
const start = opts.startSec ?? 0;
|
|
2791
|
-
const events = [];
|
|
2792
|
-
const n = Math.floor(opts.durationSec / beatSec);
|
|
2793
|
-
for (let i = 0; i < n; i++) {
|
|
2794
|
-
const isDownbeat = i % bpb === 0;
|
|
2795
|
-
events.push({
|
|
2796
|
-
atSec: start + i * beatSec,
|
|
2797
|
-
note: isDownbeat ? "C6" : "C5",
|
|
2798
|
-
durSec: 0.03,
|
|
2799
|
-
gain: isDownbeat ? 0.6 : 0.4,
|
|
2800
|
-
kind: "click"
|
|
2801
|
-
});
|
|
2802
|
-
}
|
|
2803
|
-
return events;
|
|
2804
|
-
}
|
|
2805
|
-
function droneSchedule(opts) {
|
|
2806
|
-
const gain = opts.gain ?? 0.15;
|
|
2807
|
-
const events = [
|
|
2808
|
-
{ atSec: 0, note: opts.root, durSec: opts.durationSec, gain, kind: "drone" }
|
|
2809
|
-
];
|
|
2810
|
-
if (opts.fifth ?? true) {
|
|
2811
|
-
events.push({ atSec: 0, note: transposeFifth(opts.root), durSec: opts.durationSec, gain, kind: "drone" });
|
|
2812
|
-
}
|
|
2813
|
-
return events;
|
|
2814
|
-
}
|
|
2815
|
-
function transposeFifth(note) {
|
|
2816
|
-
const m = /^([A-G])(#|b)?(\d)$/.exec(note);
|
|
2817
|
-
if (!m) return note;
|
|
2818
|
-
const order = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
|
2819
|
-
const pcMap = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
|
|
2820
|
-
let pc = pcMap[m[1]] + (m[2] === "#" ? 1 : m[2] === "b" ? -1 : 0);
|
|
2821
|
-
let oct = parseInt(m[3], 10);
|
|
2822
|
-
pc += 7;
|
|
2823
|
-
if (pc >= 12) {
|
|
2824
|
-
pc -= 12;
|
|
2825
|
-
oct += 1;
|
|
2826
|
-
}
|
|
2827
|
-
return `${order[pc]}${oct}`;
|
|
2828
|
-
}
|
|
2829
|
-
function duckGainAt(windows, tSec, floor = 0.25, rampSec = 0.2) {
|
|
2830
|
-
for (const w of windows) {
|
|
2831
|
-
if (tSec >= w.startSec - rampSec && tSec <= w.endSec + rampSec) {
|
|
2832
|
-
if (tSec < w.startSec) return lerpGain(1, floor, (tSec - (w.startSec - rampSec)) / rampSec);
|
|
2833
|
-
if (tSec > w.endSec) return lerpGain(floor, 1, (tSec - w.endSec) / rampSec);
|
|
2834
|
-
return floor;
|
|
2835
|
-
}
|
|
2836
|
-
}
|
|
2837
|
-
return 1;
|
|
2838
|
-
}
|
|
2839
|
-
function lerpGain(a, b, t) {
|
|
2840
|
-
const k = t < 0 ? 0 : t > 1 ? 1 : t;
|
|
2841
|
-
return a + (b - a) * k;
|
|
2842
|
-
}
|
|
2843
|
-
function applySchedule(instrument, schedule, startTime) {
|
|
2844
|
-
for (const ev of schedule) {
|
|
2845
|
-
if (ev.note == null) continue;
|
|
2846
|
-
instrument.triggerAttackRelease(
|
|
2847
|
-
ev.note,
|
|
2848
|
-
ev.durSec ?? 0.05,
|
|
2849
|
-
startTime + ev.atSec,
|
|
2850
|
-
ev.gain ?? 1
|
|
2851
|
-
);
|
|
2852
|
-
}
|
|
2853
|
-
return schedule.length;
|
|
2854
|
-
}
|
|
2855
|
-
|
|
2856
3038
|
// src/scene/gate.ts
|
|
2857
3039
|
var AV_TOLERANCE_MS = 60;
|
|
2858
3040
|
async function runGate(input) {
|
|
@@ -3304,6 +3486,7 @@ export {
|
|
|
3304
3486
|
staffAnchor,
|
|
3305
3487
|
staffKeyboardRayFactory,
|
|
3306
3488
|
staffRayDemoSpec,
|
|
3489
|
+
transitionProgress,
|
|
3307
3490
|
validateChordTrack,
|
|
3308
3491
|
validateQuiz,
|
|
3309
3492
|
validateSections,
|