Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
421 commits
Select commit Hold shift + click to select a range
1199c60
merge: pull in the deletion-guard fix
vanceingalls Aug 12, 2026
3bb1e8b
merge: pull in the deletion-guard fix
vanceingalls Aug 12, 2026
13d3dd3
merge: pull in the deletion-guard fix
vanceingalls Aug 12, 2026
824d4c8
merge: pull in the deletion-guard fix
vanceingalls Aug 12, 2026
228241a
merge: pull in the deletion-guard fix
vanceingalls Aug 12, 2026
6edd737
Merge remote-tracking branch 'origin/main' into wa-2-fx-preview
vanceingalls Aug 12, 2026
5ee0b17
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
6e3c8d1
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
fe19f99
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
0c16352
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
cc70cd1
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
df8d168
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
943e8d7
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
7da9de5
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
31f4862
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
e830e08
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
b82bb88
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
dd81d79
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
294297d
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
8920686
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
8d18a1c
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
1c0fc66
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
e1468ab
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
b3979ad
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
c894fac
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
e7632fc
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
7935409
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
7cf032d
fix(core): dedupe the wet/dry mix math between delayFeedback and chor…
vanceingalls Aug 12, 2026
0d92b36
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
9f6e04c
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
f951af6
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
43b03b6
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
4b068fb
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
ff6db14
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
c9c535c
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
b0998a8
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
edff77f
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
652fea0
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
9cb9a76
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
830a21c
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
42b945d
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
8baa70c
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
039005a
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
f32f45d
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
e9a74fb
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
2115c1f
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
3d025d9
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
04205bf
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
13d13b9
Merge remote-tracking branch 'origin/main' into wa-3-automation-model
vanceingalls Aug 12, 2026
e98a651
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
be2fcbc
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
ed9f61e
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
17b6003
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
830d887
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
bd9083d
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
2a722ef
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
7f4f249
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
e79620a
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
c8b53f6
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
f5b428b
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
4c87e9a
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
b08614c
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
c59bf9d
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
73bae9a
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
e6fed38
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
ef13bad
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
f70a76a
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
bc22dab
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
7ae0fd4
Merge remote-tracking branch 'origin/main' into wa-5-automation-runti…
vanceingalls Aug 12, 2026
b28e620
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
199dbeb
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
150e8f4
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
6f378b0
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
908d909
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
8870d86
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
a697226
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
1b2109d
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
e214048
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
a8acb12
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
434286c
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
581b6a0
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
535ac26
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
866e753
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
b0b4b29
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
9094910
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
ccaa33d
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
76fbfcb
fix(core): remove the build-audio-fx-runtime.ts stray resurrected by …
vanceingalls Aug 12, 2026
0467055
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
a664bb8
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
4fe82ee
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
c96b429
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
16a471c
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
182af24
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
1bfdf40
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
6bcd100
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
c812ecb
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
e8a9a2c
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
ed09bbb
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
282315d
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
815739e
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
2df9ff9
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
6381e9a
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
b1e779a
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
a4699e4
Merge remote-tracking branch 'origin/main' into wa-10-lane-component
vanceingalls Aug 12, 2026
7359e64
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
dac4f9e
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
44ec112
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
b886ae4
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
006027f
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
b8e5e83
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
c44f776
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
b6e650f
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
5987b0f
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
477f57b
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
f676c61
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
f224f68
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
ce19e90
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
d572ef8
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
55aa6b0
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
7bd053a
fix(studio): pull TimelineLanes under the 600-line cap
vanceingalls Aug 12, 2026
6232f24
merge: pull in the file-size fix
vanceingalls Aug 12, 2026
c6f1db7
merge: pull in the file-size fix
vanceingalls Aug 12, 2026
f99bf5e
merge: pull in the file-size fix
vanceingalls Aug 12, 2026
43407fe
merge: pull in the file-size fix
vanceingalls Aug 12, 2026
2936314
merge: pull in the file-size fix
vanceingalls Aug 12, 2026
c03fd4a
fix(studio): split the extracted pointerdown handler under the CRAP t…
vanceingalls Aug 13, 2026
1a5bac7
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
a9f33b6
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
0cfd6fd
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
b35c9d8
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
afcbd9e
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
3a38cc2
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
a259d14
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
4cd60ec
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
4652d4f
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
836760e
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
fb286aa
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
2c5e7ee
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
cefde3c
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
7f2b063
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
ab23ff5
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
0bdfaca
Merge remote-tracking branch 'origin/main' into wa-11-panel-writes
vanceingalls Aug 13, 2026
76703a8
merge: pull in the PR #3025 conflict fix
vanceingalls Aug 13, 2026
919e588
merge: pull in the PR #3025 conflict fix
vanceingalls Aug 13, 2026
d938c79
merge: pull in the PR #3025 conflict fix
vanceingalls Aug 13, 2026
d1222df
merge: pull in the PR #3025 conflict fix
vanceingalls Aug 13, 2026
fe0e70e
merge: pull in the PR #3025 conflict fix
vanceingalls Aug 13, 2026
6815d46
merge: pull in the PR #3025 conflict fix
vanceingalls Aug 13, 2026
7fc994b
merge: pull in the PR #3025 conflict fix
vanceingalls Aug 13, 2026
00bb341
merge: pull in the PR #3025 conflict fix
vanceingalls Aug 13, 2026
7bf1fff
merge: pull in the PR #3025 conflict fix
vanceingalls Aug 13, 2026
2284323
merge: pull in the PR #3025 conflict fix
vanceingalls Aug 13, 2026
10a280b
merge: pull in the PR #3025 conflict fix
vanceingalls Aug 13, 2026
9bdf345
merge: pull in the PR #3025 conflict fix
vanceingalls Aug 13, 2026
e5d70cd
merge: pull in the PR #3025 conflict fix
vanceingalls Aug 13, 2026
c14c5fe
merge: pull in the PR #3025 conflict fix
vanceingalls Aug 13, 2026
cf02c1e
fix(studio): drop the unused DomEditSelection import in PropertyPanel…
vanceingalls Aug 13, 2026
50e48e1
merge: pull in the unused-import fix
vanceingalls Aug 13, 2026
144ae1a
merge: pull in the unused-import fix
vanceingalls Aug 13, 2026
4f9317e
merge: pull in the unused-import fix
vanceingalls Aug 13, 2026
86ace73
merge: pull in the unused-import fix
vanceingalls Aug 13, 2026
e4b4734
merge: pull in the unused-import fix
vanceingalls Aug 13, 2026
add851f
merge: pull in the unused-import fix
vanceingalls Aug 13, 2026
1afa49b
merge: pull in the unused-import fix
vanceingalls Aug 13, 2026
5f70922
merge: pull in the unused-import fix
vanceingalls Aug 13, 2026
45aefad
merge: pull in the unused-import fix
vanceingalls Aug 13, 2026
3e48807
merge: pull in the unused-import fix
vanceingalls Aug 13, 2026
c0e4d35
merge: pull in the unused-import fix
vanceingalls Aug 13, 2026
996db02
merge: pull in the unused-import fix
vanceingalls Aug 13, 2026
569ee8d
merge: pull in the unused-import fix
vanceingalls Aug 13, 2026
0f4ec28
Merge remote-tracking branch 'origin/main' into wa-15-lane-selection
vanceingalls Aug 13, 2026
f4e0584
merge: pull in the PR #3050 conflict fix
vanceingalls Aug 13, 2026
92069b5
merge: pull in the PR #3050 conflict fix
vanceingalls Aug 13, 2026
d0570f8
merge: pull in the PR #3050 conflict fix
vanceingalls Aug 13, 2026
ccacc78
merge: pull in the PR #3050 conflict fix
vanceingalls Aug 13, 2026
1eede7f
merge: pull in the PR #3050 conflict fix
vanceingalls Aug 13, 2026
9341941
merge: pull in the PR #3050 conflict fix
vanceingalls Aug 13, 2026
ae71729
Merge remote-tracking branch 'origin/main' into wa-16-lane-shapes
vanceingalls Aug 13, 2026
cb920a4
merge: pull in the PR #3055 conflict fix
vanceingalls Aug 13, 2026
8fbcc15
merge: pull in the PR #3055 conflict fix
vanceingalls Aug 13, 2026
4279188
merge: pull in the PR #3055 conflict fix
vanceingalls Aug 13, 2026
3a43040
merge: pull in the PR #3055 conflict fix
vanceingalls Aug 13, 2026
57e696e
merge: pull in the PR #3055 conflict fix
vanceingalls Aug 13, 2026
3eae9eb
merge: pull in the PR #3055 conflict fix
vanceingalls Aug 13, 2026
5cafa11
merge: pull in the PR #3055 conflict fix
vanceingalls Aug 13, 2026
146f9ac
merge: pull in the PR #3055 conflict fix
vanceingalls Aug 13, 2026
9e2ffa3
merge: pull in the PR #3055 conflict fix
vanceingalls Aug 13, 2026
3e6597f
Merge remote-tracking branch 'origin/main' into wa-18a-retime-stretch
vanceingalls Aug 13, 2026
69bff86
merge: pull in the PR #3207 conflict fix
vanceingalls Aug 13, 2026
d59e6cf
merge: pull in the PR #3207 conflict fix
vanceingalls Aug 13, 2026
1656bdb
merge: pull in the PR #3207 conflict fix
vanceingalls Aug 13, 2026
13f40e5
merge: pull in the PR #3207 conflict fix
vanceingalls Aug 13, 2026
c461a7d
merge: pull in the PR #3207 conflict fix
vanceingalls Aug 13, 2026
3165b4a
merge: pull in the PR #3207 conflict fix
vanceingalls Aug 13, 2026
f9c26b2
merge: pull in the PR #3207 conflict fix
vanceingalls Aug 13, 2026
ea18fb5
Merge remote-tracking branch 'origin/main' into wa-18b-reschedule
vanceingalls Aug 13, 2026
8e748ac
merge: pull in the PR #3208 conflict fix
vanceingalls Aug 13, 2026
1befa91
merge: pull in the PR #3208 conflict fix
vanceingalls Aug 13, 2026
bcc3747
merge: pull in the PR #3208 conflict fix
vanceingalls Aug 13, 2026
0122e1b
merge: pull in the PR #3208 conflict fix
vanceingalls Aug 13, 2026
de04d9f
merge: pull in the PR #3208 conflict fix
vanceingalls Aug 13, 2026
9f3963d
merge: pull in the PR #3208 conflict fix
vanceingalls Aug 13, 2026
4dacf28
fix(studio): close the typecheck and fallow gaps wa-18b-reschedule op…
vanceingalls Aug 13, 2026
32d4918
merge: pull in the wa-18b-reschedule fallow/typecheck fix
vanceingalls Aug 13, 2026
252b1c2
chore: merge in the wa-18b-reschedule fallow/typecheck fix
vanceingalls Aug 13, 2026
aed1e7b
chore: merge in the wa-18b-reschedule fallow/typecheck fix
vanceingalls Aug 13, 2026
d993284
chore: merge in the wa-18b-reschedule fallow/typecheck fix
vanceingalls Aug 13, 2026
00d0b2a
chore: merge in the wa-18b-reschedule fallow/typecheck fix
vanceingalls Aug 13, 2026
67bcf88
chore: merge in the wa-18b-reschedule fallow/typecheck fix
vanceingalls Aug 13, 2026
9e828a0
fix(core): cap the via conic's weight so an edge-clamped via point ca…
vanceingalls Aug 13, 2026
36b63cc
chore: merge in the via-conic NaN fix
vanceingalls Aug 13, 2026
adbc910
chore: merge in the via-conic NaN fix
vanceingalls Aug 13, 2026
024470c
chore: merge in the via-conic NaN fix
vanceingalls Aug 13, 2026
4ceeb6f
chore: merge in the via-conic NaN fix
vanceingalls Aug 13, 2026
f21d4d1
chore: merge in the via-conic NaN fix
vanceingalls Aug 13, 2026
9fc52ee
chore: merge in the via-conic NaN fix
vanceingalls Aug 13, 2026
f6fc7e9
fix(studio-server): fingerprint the proactive waveform cache key too
vanceingalls Aug 13, 2026
248fd70
chore: merge in the waveform cache key fix
vanceingalls Aug 13, 2026
32598dd
chore: merge in the waveform cache key fix
vanceingalls Aug 13, 2026
3722f9c
chore: merge in the waveform cache key fix
vanceingalls Aug 13, 2026
bed05ba
chore: merge in the waveform cache key fix
vanceingalls Aug 13, 2026
99f0c4b
style(docs): run oxfmt on the /hyperframes-audio skill docs
vanceingalls Aug 13, 2026
cc631a2
chore: merge in the skill-docs format fix
vanceingalls Aug 13, 2026
009fb88
fix(core): stop \b from missing underscore-separated names, guard cli…
vanceingalls Aug 13, 2026
64eccec
chore: merge in the carve regex+overlap fix
vanceingalls Aug 13, 2026
06aff52
fix(studio): widen PropertyPanel's resetModules render timeout again
vanceingalls Aug 13, 2026
93c7abf
chore: merge in the PropertyPanel timeout fix
vanceingalls Aug 13, 2026
a81b652
chore: merge in the PropertyPanel timeout fix
vanceingalls Aug 13, 2026
cfd5551
chore: merge in the PropertyPanel timeout fix
vanceingalls Aug 13, 2026
4efaa33
chore: merge in the PropertyPanel timeout fix
vanceingalls Aug 13, 2026
011a9f0
chore: merge in the PropertyPanel timeout fix
vanceingalls Aug 13, 2026
aa4c875
chore: merge in the PropertyPanel timeout fix
vanceingalls Aug 13, 2026
13285b6
fix(studio): stop the single-candidate auto-apply carve firing twice
vanceingalls Aug 13, 2026
e965111
chore: merge in the duplicate-effect carve fix
vanceingalls Aug 13, 2026
7565f5d
chore: merge in the duplicate-effect carve fix
vanceingalls Aug 13, 2026
9a94c82
chore: pull in main after #3210's squash merge
vanceingalls Aug 13, 2026
49c619b
chore: merge in the main restack from #3210
vanceingalls Aug 13, 2026
df4daca
chore: merge in the main restack from #3210
vanceingalls Aug 13, 2026
16bf7ca
chore: merge in the main restack from #3210
vanceingalls Aug 13, 2026
df5ea4d
chore: merge in the main restack from #3210
vanceingalls Aug 13, 2026
93921f0
feat(core): carve against every voice over a bed, always (#3212)
vanceingalls Aug 13, 2026
03c1634
fix(studio): port the carve UI off the removed source/dynamic fields
vanceingalls Aug 13, 2026
9a7e19e
chore: merge in the carve UI type-shape fix
vanceingalls Aug 13, 2026
590043c
chore: merge in the carve UI type-shape fix
vanceingalls Aug 13, 2026
b1401f7
chore: merge in the carve UI type-shape fix
vanceingalls Aug 13, 2026
2284c8c
chore: merge in the carve UI type-shape fix
vanceingalls Aug 13, 2026
de8f963
chore: pull in main after #3211's squash merge
vanceingalls Aug 13, 2026
9fae6e8
chore: pull in main after #3211's squash merge
vanceingalls Aug 13, 2026
4e46fae
chore: pull in main after #3211's squash merge
vanceingalls Aug 13, 2026
66f372a
chore: pull in main after #3214's squash merge
vanceingalls Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1114,15 +1114,21 @@ describe("AudioFxGroup carve source list", () => {
});

it("keeps the picker when the stored voice is not among the candidates", () => {
// The stored track was renamed, or classifies as music now. Reading the one
// remaining candidate out would quietly claim the carve listens to it.
// The stored track is still there but no longer classifies as a voice.
// Reading the one remaining candidate out would quietly claim the carve
// listens to it. (A stored track that is GONE is a different case — see the
// deleted-voice tests, which re-analyse rather than sit on a measurement of
// something that is not there.)
const bed = document.createElement("audio");
bed.id = "bed";
bed.setAttribute(
"data-fx-carve",
JSON.stringify({ enabled: true, sources: ["gone"], strength: 0.25 }),
JSON.stringify({ enabled: true, sources: ["backing-music"], strength: 0.25 }),
);
document.body.append(bed);
const stored = document.createElement("audio");
stored.id = "backing-music";
document.body.append(stored);
const voice = document.createElement("audio");
voice.id = "narration";
document.body.append(voice);
Expand All @@ -1136,7 +1142,7 @@ describe("AudioFxGroup carve source list", () => {
dataAttributes: {
"fx-carve": JSON.stringify({
enabled: true,
sources: ["gone"],
sources: ["backing-music"],
strength: 0.25,
}),
},
Expand Down Expand Up @@ -1310,3 +1316,154 @@ describe("AudioFxGroup carve across tracks", () => {
]);
});
});

/**
* The filters and envelopes a carve produces are a MEASUREMENT of specific
* tracks. Delete one and they describe something nobody can hear any more — the
* bed keeps ducking for a voice that is gone.
*/
describe("AudioFxGroup carve against a deleted voice", () => {
const CARVED_CHAIN = JSON.stringify({
version: 1,
nodes: [
{ type: "peaking", id: "c1", enabled: true, fromCarve: true, params: { frequency: 1000 } },
{ type: "lowpass", id: "k1", enabled: true, params: { frequency: 8000 } },
],
});
const CARVED_AUTOMATION = JSON.stringify({
version: 1,
lanes: [
{ target: "fx.c1.gain", points: [{ t: 0, v: -6 }] },
{ target: "fx.k1.frequency", points: [{ t: 0, v: 8000 }] },
],
});

/**
* A bed carving against `sources`, with only `present` still in the composition.
*
* The timeline is what says a track is gone — not the preview DOM, which keeps
* a deleted element around — so the store is seeded and the document is left
* holding every track, which is exactly the mismatch the studio produces.
*/
function mountCarved(sources: string[], present: string[]) {
const carve = JSON.stringify({ enabled: true, sources, strength: 0.25 });
const bed = document.createElement("audio");
bed.id = "bed";
document.body.append(bed);
for (const id of new Set([...sources, ...present])) {
const el = document.createElement("audio");
el.id = id;
document.body.append(el);
}
usePlayerStore.setState({
elements: [
{ id: "bed", tag: "audio", start: 0, duration: 10, track: 0 },
...present.map((id) => ({ id, tag: "audio", start: 0, duration: 10, track: 1 })),
] as never,
});
const onSetAttributeQuiet = vi.fn();
const host = document.createElement("div");
document.body.append(host);
act(() => {
createRoot(host).render(
<AudioFxGroup
element={
{
dataAttributes: {
"fx-carve": carve,
"fx-chain": CARVED_CHAIN,
automation: CARVED_AUTOMATION,
},
id: "bed",
element: bed,
} as unknown as DomEditSelection
}
onSetAttributeQuiet={onSetAttributeQuiet}
onSetAttributeLive={vi.fn()}
/>,
);
});
return { host, onSetAttributeQuiet };
}

it("re-analyses against the voices that are left", () => {
// Two voices were measured together into one set of bands. With one gone that
// set answers a question nobody asked; the survivor has to be measured again.
const { onSetAttributeQuiet } = mountCarved(["narration", "guest"], ["narration"]);
const write = writeTo(onSetAttributeQuiet.mock.calls, "data-fx-carve");
expect(JSON.parse(String(write![1]))).toMatchObject({
enabled: true,
sources: ["narration"],
});
});

it("leaves a carve alone while every voice it names is still there", () => {
const { onSetAttributeQuiet } = mountCarved(["narration", "guest"], ["narration", "guest"]);
expect(onSetAttributeQuiet.mock.calls.some((c) => c[0] === "data-fx-carve")).toBe(false);
});

it("does not fall back to carving against an effect when no voice is left", async () => {
// Found in the studio, not here: deleting the narration emptied the source
// list, and the panel filled it again with the only audio in the composition
// — a 200 ms explosion. The picker's fallback (offer everything rather than
// hide the track somebody needs) is for the AUTHOR to choose from. The panel
// choosing off it is the panel deciding, and that is never the answer.
const carve = JSON.stringify({ enabled: true, sources: [], strength: 0.25 });
const bed = document.createElement("audio");
bed.id = "bed";
bed.setAttribute("data-fx-carve", carve);
document.body.append(bed);
const sfx = document.createElement("audio");
sfx.id = "sfx-explosion";
document.body.append(sfx);
const onSetAttributeQuiet = vi.fn();
const host = document.createElement("div");
document.body.append(host);
act(() => {
createRoot(host).render(
<AudioFxGroup
element={
{
dataAttributes: { "fx-carve": carve },
id: "bed",
element: bed,
} as unknown as DomEditSelection
}
onSetAttributeQuiet={onSetAttributeQuiet}
onSetAttributeLive={vi.fn()}
/>,
);
});
await act(async () => {});
// Nothing written: the carve waits rather than picking the explosion.
expect(onSetAttributeQuiet.mock.calls.some((c) => c[0] === "data-fx-carve")).toBe(false);
// Still offered, so the author can say "actually, listen to that one".
expect(
Array.from(host.querySelectorAll("[data-carve-source]")).map((e) =>
e.getAttribute("data-carve-source"),
),
).toEqual(["sfx-explosion"]);
});

it("drops what it generated when the last voice goes and none is left to pick", async () => {
// Staying on with nothing to listen to is honest — a voice may come back, and
// "off" is a different thing the author chose. What cannot stay is the output:
// those filters and that envelope are making room for nobody.
const { onSetAttributeQuiet } = mountCarved(["narration"], []);
// The three writes are sequenced, not fired together: each is a
// read-modify-write against the same file, so the carve write lands only
// after the two that strip its output.
await act(async () => {});
const carve = writeTo(onSetAttributeQuiet.mock.calls, "data-fx-carve");
expect(JSON.parse(String(carve![1]))).toMatchObject({ enabled: true, sources: [] });

const chain = writeTo(onSetAttributeQuiet.mock.calls, "data-fx-chain");
// The hand-added low-pass survives; only what the carve minted goes.
expect(JSON.parse(String(chain![1])).nodes.map((n: { id: string }) => n.id)).toEqual(["k1"]);

const automation = writeTo(onSetAttributeQuiet.mock.calls, "data-automation");
expect(
JSON.parse(String(automation![1])).lanes.map((l: { target: string }) => l.target),
).toEqual(["fx.k1.frequency"]);
});
});
107 changes: 88 additions & 19 deletions packages/studio/src/components/editor/propertyPanelAudioFxGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/**
* The audio FX panel's bridge to the element/attribute world.
*
Expand Down Expand Up @@ -48,6 +48,7 @@
} from "./propertyPanelAutomation";
import type { DomEditSelection } from "./domEditingTypes";
import { useLivePlayheadTime } from "../../hooks/useLivePlayheadTime";
import { usePlayerStore } from "../../player";

/**
* Rate the carve source is decoded at. Analysis is self-consistent because it
Expand Down Expand Up @@ -199,10 +200,12 @@
* commit, which does not exist yet.
*/
const setCarve = async (next: HfCarveSettings | null): Promise<void> => {
// Envelopes the carve wrote outlive it otherwise, and an automated gain
// ignores the panel's own depth — so switching dynamic off would leave the
// filters still following the voice with nothing saying they do.
if (!next?.enabled) {
// What the carve generated is only justified by the voices it was measured
// from: switched off, or left naming none — every source deleted, say —
// there is nothing those filters are making room for. Left behind they keep
// dipping the bed with nothing in the panel to explain them.
const generatedOutputStands = Boolean(next?.enabled) && (next?.sources.length ?? 0) > 0;
if (!generatedOutputStands) {
const carriedOver = withoutCarveLanes(automation, chain);
if (carriedOver.lanes.length !== automation.lanes.length) {
await onSetAttributeQuiet(
Expand All @@ -211,7 +214,7 @@
);
}
}
if (!next?.enabled) {
if (!generatedOutputStands) {
const kept = chain.nodes.filter((n) => !n.fromCarve);
if (kept.length !== chain.nodes.length) {
await onSetAttributeQuiet(
Expand Down Expand Up @@ -300,9 +303,12 @@
* first, and if filtering would leave nothing at all every track comes back. A
* picker that hides the track somebody needs is worse than a long one.
*/
const sourceOptions: AudioTrackOption[] = (() => {
const { sourceOptions, autoSourceIds } = ((): {
sourceOptions: AudioTrackOption[];
autoSourceIds: string[];
} => {
const doc = element.element?.ownerDocument;
if (!doc) return [];
if (!doc) return { sourceOptions: [], autoSourceIds: [] };
const others = Array.from(doc.querySelectorAll<HTMLAudioElement>("audio[id]")).filter(
(a) => a.id !== element.id,
);
Expand All @@ -324,11 +330,75 @@
}));
const plausible = described.filter((t) => t.kind === "voice" || t.kind === "unknown");
const offered = plausible.length > 0 ? plausible : described;
return offered
.sort((a, b) => (a.kind === "voice" ? 0 : 1) - (b.kind === "voice" ? 0 : 1))
.map(({ id, label }) => ({ id, label }));
const byVoiceFirst = (list: typeof described) =>
[...list].sort((a, b) => (a.kind === "voice" ? 0 : 1) - (b.kind === "voice" ? 0 : 1));
return {
sourceOptions: byVoiceFirst(offered).map(({ id, label }) => ({ id, label })),
// What the panel may pick WITHOUT being asked — never the fallback. The
// fallback exists so the picker can still show a track whose name reads as
// music or as an effect, because a name is a hint and the author may know
// better. Choosing off that list is a different act: it is the panel
// deciding, and "the only audio left is a 200 ms explosion" is not a voice
// to make room for. A bed surrounded by nothing plausible waits instead.
autoSourceIds: byVoiceFirst(plausible).map((t) => t.id),
};
})();

/**
* The voices this carve names that are still in the composition.
*
* Existence, not the candidate list: a voice can stop being offered without
* being gone (it stopped overlapping the bed), and dropping it then would
* quietly rewrite a relationship the author set. Deleted is the case that has
* to be noticed, because what the carve produced was measured from that track.
*
* Asked of the timeline rather than of `element.element.ownerDocument`, which
* is the preview's DOM and outlives a delete: measured in the studio, a bed
* selected right after its voice was deleted still found that voice through
* the document, so the carve sat on a measurement of a track the timeline had
* already dropped. The store is what the delete actually edited.
*/
const timelineElements = usePlayerStore((s) => s.elements);
const survivingSources = ((): string[] => {
if (!carve) return [];
const present = new Set(timelineElements.map((el) => el.domId ?? el.id));
// Absence only means deletion once the timeline is known to describe THIS
// composition, and the bed being in it is the proof. Without that check a
// store that is empty — not loaded yet, or a panel mounted outside the
// player — reads as "every voice was deleted" and throws away a carve that
// is perfectly fine. Unchanged sources are what the prune treats as nothing
// to do.
if (!element.id || !present.has(element.id)) return carve.sources;
return carve.sources.filter((id) => present.has(id));
})();

/**
* A deleted voice re-analyses the bed.
*
* The filters and envelopes are a measurement of specific tracks, so losing one
* makes them a measurement of something that is no longer there — the bed keeps
* ducking for a voice nobody can hear. `analyse` already skips a source it
* cannot find, but nothing asked it to run again.
*
* Pruning is the whole trigger: `setCarve` re-analyses when the source list
* changes, so the surviving voices are re-measured together. Losing the LAST
* one leaves an empty list, which the effects below repoint at whatever
* candidates remain — and if there are none, `setCarve` drops what the carve
* generated, since there is nothing left it could be making room for.
*
* Keyed on the survivors rather than on the candidates: a voice that had
* stopped overlapping was never in the candidate list, so its deletion would
* not change that identity and this would never fire.
*/
useEffect(() => {
if (carvedAgainstBy || !carve?.enabled) return;
if (survivingSources.length === carve.sources.length) return;
void setCarve({ ...carve, sources: survivingSources });
// Keyed on the identity of the decision, not on setCarve — which is rebuilt
// every render and would re-fire this.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [carve, carvedAgainstBy, survivingSources.join(" ")]);

/**
* A bed with voices above it carves itself.
*
Expand All @@ -346,14 +416,14 @@
* off stores `enabled: false`, which is also a configured carve. That is the whole
* reason the flag exists rather than "off" being an absent attribute.
*/
const candidateIds = sourceOptions.map((o) => o.id).join("\u0000");
const candidateIds = autoSourceIds.join("\u0000");
useEffect(() => {
// Exactly one candidate is the sibling effect's case below, not this one's:
// both guards passing for a single candidate fired two setCarve calls with
// the same result — two decodes, two FFT runs, two concurrent attribute
// writes.
if (carvedAgainstBy || sourceOptions.length <= 1) return;
const all = sourceOptions.map((o) => o.id);
if (carvedAgainstBy || autoSourceIds.length <= 1) return;
const all = autoSourceIds;
// Nothing configured: the default carve, pointed at everything it could hear.
if (carve === null) {
void setCarve({ ...DEFAULT_CARVE, sources: all });
Expand Down Expand Up @@ -387,24 +457,23 @@
* reason the flag exists rather than "off" being an absent attribute.
*/
useEffect(() => {
if (carvedAgainstBy || sourceOptions.length !== 1) return;
const only = sourceOptions[0];
if (carvedAgainstBy || autoSourceIds.length !== 1) return;
const only = autoSourceIds[0];
if (!only) return;
// Nothing configured: the default carve, pointed at the one candidate.
if (carve === null) {
void setCarve({ ...DEFAULT_CARVE, sources: [only.id] });
void setCarve({ ...DEFAULT_CARVE, sources: [only] });
return;
}
// Configured but with no voice yet — a carve switched on before there was
// anything to listen to, or one whose source was cleared. The panel reads the
// sole candidate out as the source, so it has to be the stored one too;
// otherwise the card claims a relationship the attribute does not record.
if (carve.enabled && carve.sources.length === 0)
void setCarve({ ...carve, sources: [only.id] });
if (carve.enabled && carve.sources.length === 0) void setCarve({ ...carve, sources: [only] });
// Deliberately keyed on the identity of the decision, not on setCarve — which
// is rebuilt every render and would re-fire this.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [carve, carvedAgainstBy, sourceOptions.length, sourceOptions[0]?.id]);
}, [carve, carvedAgainstBy, autoSourceIds.length, autoSourceIds[0]]);

const [analysing, setAnalysing] = useState(false);

Expand Down
3 changes: 3 additions & 0 deletions packages/studio/src/hooks/useElementLifecycleOps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ export function useElementLifecycleOps({
kind: "timeline",
files: { [targetPath]: patchedContent },
readFile: async () => originalContent,
// remove-element already wrote the removal, so disk holds THAT — not
// the content read at the top. Undo still goes back to the original.
diskContent: { [targetPath]: patchedContent },
writeFile: writeProjectFile,
recordEdit: editHistory.recordEdit,
});
Expand Down
3 changes: 3 additions & 0 deletions packages/studio/src/hooks/useTimelineEditing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,9 @@ export function useTimelineEditing({
kind: "timeline",
files: { [targetPath]: patchedContent },
readFile: async () => originalContent,
// remove-element already wrote the removal, so disk holds THAT — not the
// content read at the top. Undo still goes back to the original.
diskContent: { [targetPath]: removedContent },
writeFile: writeProjectFile,
recordEdit,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ export function AutomationValueInput({
}: AutomationValueInputProps) {
return (
<input
className="hf-automation-value absolute rounded-[3px] border border-panel-border-input bg-panel-bg-2 px-1 font-mono text-[9px] text-panel-text-1"
// pointer-events-auto: the lane band around it takes none, so that clips
// sharing the row do not cover each other's envelopes (see the lane).
className="hf-automation-value pointer-events-auto absolute rounded-[3px] border border-panel-border-input bg-panel-bg-2 px-1 font-mono text-[9px] text-panel-text-1"
style={{ left: leftPx, top: 1, width: 44, zIndex: 4 }}
// The lane is a pointer surface, and the timeline above it owns single-key
// shortcuts. Without stopping both, a press lands on the lane instead of
Expand Down
Loading
Loading