@real-music-packages/web-core 0.12.0 → 0.13.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 +147 -78
- package/dist/scene/index.js +114 -91
- package/dist/scene/index.js.map +1 -1
- package/package.json +1 -1
package/dist/scene/index.js
CHANGED
|
@@ -2617,6 +2617,96 @@ registerLayer(circleOfFifthsFactory);
|
|
|
2617
2617
|
registerLayer(pitchContourFactory);
|
|
2618
2618
|
registerLayer(sectionMinimapFactory);
|
|
2619
2619
|
|
|
2620
|
+
// src/scene/audioLayers.ts
|
|
2621
|
+
function countInSchedule(opts) {
|
|
2622
|
+
const beats = opts.beats ?? 4;
|
|
2623
|
+
const accent = opts.accentDownbeat ?? true;
|
|
2624
|
+
const beatSec = 60 / opts.bpm;
|
|
2625
|
+
const events = [];
|
|
2626
|
+
for (let i = 0; i < beats; i++) {
|
|
2627
|
+
const isDownbeat = accent && i === 0;
|
|
2628
|
+
events.push({
|
|
2629
|
+
atSec: i * beatSec,
|
|
2630
|
+
note: isDownbeat ? "C6" : "C5",
|
|
2631
|
+
durSec: 0.05,
|
|
2632
|
+
gain: isDownbeat ? 1 : 0.7,
|
|
2633
|
+
kind: "count"
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
return events;
|
|
2637
|
+
}
|
|
2638
|
+
function countInLeadSec(opts) {
|
|
2639
|
+
return (opts.beats ?? 4) * (60 / opts.bpm);
|
|
2640
|
+
}
|
|
2641
|
+
function clickTrackSchedule(opts) {
|
|
2642
|
+
const beatSec = 60 / opts.bpm;
|
|
2643
|
+
const bpb = opts.beatsPerBar ?? 4;
|
|
2644
|
+
const start = opts.startSec ?? 0;
|
|
2645
|
+
const events = [];
|
|
2646
|
+
const n = Math.floor(opts.durationSec / beatSec);
|
|
2647
|
+
for (let i = 0; i < n; i++) {
|
|
2648
|
+
const isDownbeat = i % bpb === 0;
|
|
2649
|
+
events.push({
|
|
2650
|
+
atSec: start + i * beatSec,
|
|
2651
|
+
note: isDownbeat ? "C6" : "C5",
|
|
2652
|
+
durSec: 0.03,
|
|
2653
|
+
gain: isDownbeat ? 0.6 : 0.4,
|
|
2654
|
+
kind: "click"
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
return events;
|
|
2658
|
+
}
|
|
2659
|
+
function droneSchedule(opts) {
|
|
2660
|
+
const gain = opts.gain ?? 0.15;
|
|
2661
|
+
const events = [
|
|
2662
|
+
{ atSec: 0, note: opts.root, durSec: opts.durationSec, gain, kind: "drone" }
|
|
2663
|
+
];
|
|
2664
|
+
if (opts.fifth ?? true) {
|
|
2665
|
+
events.push({ atSec: 0, note: transposeFifth(opts.root), durSec: opts.durationSec, gain, kind: "drone" });
|
|
2666
|
+
}
|
|
2667
|
+
return events;
|
|
2668
|
+
}
|
|
2669
|
+
function transposeFifth(note) {
|
|
2670
|
+
const m = /^([A-G])(#|b)?(\d)$/.exec(note);
|
|
2671
|
+
if (!m) return note;
|
|
2672
|
+
const order = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
|
2673
|
+
const pcMap = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
|
|
2674
|
+
let pc = pcMap[m[1]] + (m[2] === "#" ? 1 : m[2] === "b" ? -1 : 0);
|
|
2675
|
+
let oct = parseInt(m[3], 10);
|
|
2676
|
+
pc += 7;
|
|
2677
|
+
if (pc >= 12) {
|
|
2678
|
+
pc -= 12;
|
|
2679
|
+
oct += 1;
|
|
2680
|
+
}
|
|
2681
|
+
return `${order[pc]}${oct}`;
|
|
2682
|
+
}
|
|
2683
|
+
function duckGainAt(windows, tSec, floor = 0.25, rampSec = 0.2) {
|
|
2684
|
+
for (const w of windows) {
|
|
2685
|
+
if (tSec >= w.startSec - rampSec && tSec <= w.endSec + rampSec) {
|
|
2686
|
+
if (tSec < w.startSec) return lerpGain(1, floor, (tSec - (w.startSec - rampSec)) / rampSec);
|
|
2687
|
+
if (tSec > w.endSec) return lerpGain(floor, 1, (tSec - w.endSec) / rampSec);
|
|
2688
|
+
return floor;
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
return 1;
|
|
2692
|
+
}
|
|
2693
|
+
function lerpGain(a, b, t) {
|
|
2694
|
+
const k = t < 0 ? 0 : t > 1 ? 1 : t;
|
|
2695
|
+
return a + (b - a) * k;
|
|
2696
|
+
}
|
|
2697
|
+
function applySchedule(instrument, schedule, startTime) {
|
|
2698
|
+
for (const ev of schedule) {
|
|
2699
|
+
if (ev.note == null) continue;
|
|
2700
|
+
instrument.triggerAttackRelease(
|
|
2701
|
+
ev.note,
|
|
2702
|
+
ev.durSec ?? 0.05,
|
|
2703
|
+
startTime + ev.atSec,
|
|
2704
|
+
ev.gain ?? 1
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
return schedule.length;
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2620
2710
|
// src/scene/camera.ts
|
|
2621
2711
|
function cameraTransform(cam, W, H) {
|
|
2622
2712
|
const s = cam.zoom;
|
|
@@ -2658,7 +2748,7 @@ function resolveTimeline(spec, totalSec) {
|
|
|
2658
2748
|
return spec.timeline.map((seg) => {
|
|
2659
2749
|
const startSec = resolveAnchor(seg.at[0], totalSec);
|
|
2660
2750
|
const endSec = resolveAnchor(seg.at[1], totalSec);
|
|
2661
|
-
return { startMs: startSec * 1e3, endMs: endSec * 1e3, layers: seg.layers };
|
|
2751
|
+
return { startMs: startSec * 1e3, endMs: endSec * 1e3, layers: seg.layers, audio: seg.audio };
|
|
2662
2752
|
});
|
|
2663
2753
|
}
|
|
2664
2754
|
function visualTimelineMs(resolved) {
|
|
@@ -2734,6 +2824,21 @@ async function buildScene(opts) {
|
|
|
2734
2824
|
}
|
|
2735
2825
|
ctx2d.restore();
|
|
2736
2826
|
}
|
|
2827
|
+
function scheduleAudio(instrument, baseSec) {
|
|
2828
|
+
let fired = 0;
|
|
2829
|
+
for (const seg of resolved) {
|
|
2830
|
+
if (!seg.audio) continue;
|
|
2831
|
+
const segStartSec = baseSec + seg.startMs / 1e3;
|
|
2832
|
+
if (seg.audio.schedule?.length) {
|
|
2833
|
+
fired += applySchedule(instrument, seg.audio.schedule, segStartSec);
|
|
2834
|
+
}
|
|
2835
|
+
if (seg.audio.onEnter) {
|
|
2836
|
+
const segClock = { nowMs: () => seg.startMs };
|
|
2837
|
+
seg.audio.onEnter(segClock, { instrument, startSec: segStartSec, score });
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
return fired;
|
|
2841
|
+
}
|
|
2737
2842
|
return {
|
|
2738
2843
|
W,
|
|
2739
2844
|
H,
|
|
@@ -2741,6 +2846,7 @@ async function buildScene(opts) {
|
|
|
2741
2846
|
durationMs,
|
|
2742
2847
|
resolved,
|
|
2743
2848
|
renderFrame,
|
|
2849
|
+
scheduleAudio,
|
|
2744
2850
|
dispose() {
|
|
2745
2851
|
for (const b of bound) b.layer.dispose?.();
|
|
2746
2852
|
}
|
|
@@ -2751,6 +2857,13 @@ async function recordSceneSpec(opts) {
|
|
|
2751
2857
|
const record = opts.record ?? recordScenes;
|
|
2752
2858
|
const composite = {
|
|
2753
2859
|
durationMs: built.durationMs,
|
|
2860
|
+
// Schedule any per-segment audio against the capture clock exactly when the
|
|
2861
|
+
// first frame is entered, so it's anchored to the same start the recorder
|
|
2862
|
+
// uses. onEnter fires once per Scene (see recordScenes), so this runs once.
|
|
2863
|
+
onEnter: opts.segmentAudio ? () => {
|
|
2864
|
+
const sa = opts.segmentAudio;
|
|
2865
|
+
built.scheduleAudio(sa.instrument, sa.audioNow() + (sa.lookaheadSec ?? 0.1));
|
|
2866
|
+
} : void 0,
|
|
2754
2867
|
draw: (ctx, t01) => built.renderFrame(ctx, t01 * built.durationMs)
|
|
2755
2868
|
};
|
|
2756
2869
|
return record([composite], {
|
|
@@ -2763,96 +2876,6 @@ async function recordSceneSpec(opts) {
|
|
|
2763
2876
|
});
|
|
2764
2877
|
}
|
|
2765
2878
|
|
|
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
2879
|
// src/scene/gate.ts
|
|
2857
2880
|
var AV_TOLERANCE_MS = 60;
|
|
2858
2881
|
async function runGate(input) {
|