@opendaw/studio-core 0.0.131 → 0.0.132
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/EffectBox.d.ts +2 -2
- package/dist/EffectBox.d.ts.map +1 -1
- package/dist/EffectFactories.d.ts +3 -0
- package/dist/EffectFactories.d.ts.map +1 -1
- package/dist/EffectFactories.js +19 -3
- package/dist/processors.js +27 -27
- package/dist/processors.js.map +4 -4
- package/dist/project/audio/AudioContentModifier.d.ts.map +1 -1
- package/dist/project/audio/AudioContentModifier.js +43 -3
- package/dist/ui/clipboard/types/DevicesClipboardHandler.d.ts.map +1 -1
- package/dist/ui/clipboard/types/DevicesClipboardHandler.js +22 -12
- package/dist/ui/clipboard/types/DevicesClipboardHandler.test.js +124 -4
- package/dist/ysync/YSync.d.ts.map +1 -1
- package/dist/ysync/YSync.js +83 -37
- package/dist/ysync/YSync.test.d.ts +2 -0
- package/dist/ysync/YSync.test.d.ts.map +1 -0
- package/dist/ysync/YSync.test.js +259 -0
- package/package.json +13 -13
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioContentModifier.d.ts","sourceRoot":"","sources":["../../../src/project/audio/AudioContentModifier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,IAAI,
|
|
1
|
+
{"version":3,"file":"AudioContentModifier.d.ts","sourceRoot":"","sources":["../../../src/project/audio/AudioContentModifier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,IAAI,EAA4D,MAAM,kBAAkB,CAAA;AAS3G,OAAO,EAAC,sBAAsB,EAA8C,MAAM,0BAA0B,CAAA;AAK5G,yBAAiB,oBAAoB,CAAC;IAC3B,MAAM,cAAc,GAAU,UAAU,aAAa,CAAC,sBAAsB,CAAC,KAAG,OAAO,CAAC,IAAI,CAqBlG,CAAA;IAEM,MAAM,cAAc,GAAU,UAAU,aAAa,CAAC,sBAAsB,CAAC,KAAG,OAAO,CAAC,IAAI,CA6BlG,CAAA;IAEM,MAAM,aAAa,GAAU,UAAU,aAAa,CAAC,sBAAsB,CAAC,KAAG,OAAO,CAAC,IAAI,CA+CjG,CAAA;CAoDJ"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EmptyExec, isDefined, isInstanceOf, RuntimeNotifier, UUID } from "@opendaw/lib-std";
|
|
1
|
+
import { EmptyExec, isDefined, isInstanceOf, isNotNull, RuntimeNotifier, UUID } from "@opendaw/lib-std";
|
|
2
2
|
import { TimeBase } from "@opendaw/lib-dsp";
|
|
3
3
|
import { AudioPitchStretchBox, AudioTimeStretchBox, TransientMarkerBox, WarpMarkerBox } from "@opendaw/studio-boxes";
|
|
4
4
|
import { AudioRegionBoxAdapter } from "@opendaw/studio-adapters";
|
|
@@ -15,6 +15,12 @@ export var AudioContentModifier;
|
|
|
15
15
|
return () => audioAdapters.forEach((adapter) => {
|
|
16
16
|
const audibleDuration = adapter.optWarpMarkers
|
|
17
17
|
.mapOr(warpMarkers => warpMarkers.last()?.seconds ?? 0, 0);
|
|
18
|
+
const loopOffsetSeconds = isInstanceOf(adapter, AudioRegionBoxAdapter)
|
|
19
|
+
? adapter.optWarpMarkers.mapOr(warpMarkers => warpPositionToSeconds(warpMarkers, adapter.loopOffset), 0)
|
|
20
|
+
: 0;
|
|
21
|
+
if (loopOffsetSeconds !== 0) {
|
|
22
|
+
adapter.box.waveformOffset.setValue(adapter.waveformOffset.getValue() + loopOffsetSeconds);
|
|
23
|
+
}
|
|
18
24
|
adapter.box.playMode.defer();
|
|
19
25
|
adapter.asPlayModeTimeStretch.ifSome(({ box }) => {
|
|
20
26
|
if (box.pointerHub.filter(Pointers.AudioPlayMode).length === 0) {
|
|
@@ -57,7 +63,8 @@ export var AudioContentModifier;
|
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
else {
|
|
60
|
-
|
|
66
|
+
const { ppqn, seconds } = sampleExtent(adapter);
|
|
67
|
+
AudioContentHelpers.addDefaultWarpMarkers(boxGraph, pitchStretch, ppqn, seconds);
|
|
61
68
|
}
|
|
62
69
|
switchTimeBaseToMusical(adapter);
|
|
63
70
|
});
|
|
@@ -101,7 +108,8 @@ export var AudioContentModifier;
|
|
|
101
108
|
}
|
|
102
109
|
}
|
|
103
110
|
else {
|
|
104
|
-
|
|
111
|
+
const { ppqn, seconds } = sampleExtent(adapter);
|
|
112
|
+
AudioContentHelpers.addDefaultWarpMarkers(boxGraph, timeStretch, ppqn, seconds);
|
|
105
113
|
}
|
|
106
114
|
if (isDefined(transients) && adapter.file.transients.length() === 0) {
|
|
107
115
|
const markersField = adapter.file.box.transientMarkers;
|
|
@@ -113,6 +121,38 @@ export var AudioContentModifier;
|
|
|
113
121
|
switchTimeBaseToMusical(adapter);
|
|
114
122
|
});
|
|
115
123
|
};
|
|
124
|
+
const warpPositionToSeconds = (warpMarkers, position) => {
|
|
125
|
+
const length = warpMarkers.length();
|
|
126
|
+
if (length === 0) {
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
const first = warpMarkers.first();
|
|
130
|
+
const last = warpMarkers.last();
|
|
131
|
+
if (!isNotNull(first) || !isNotNull(last)) {
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
if (position <= first.position) {
|
|
135
|
+
return first.seconds;
|
|
136
|
+
}
|
|
137
|
+
if (position >= last.position) {
|
|
138
|
+
return last.seconds;
|
|
139
|
+
}
|
|
140
|
+
for (let i = 0; i < length - 1; i++) {
|
|
141
|
+
const left = warpMarkers.optAt(i);
|
|
142
|
+
const right = warpMarkers.optAt(i + 1);
|
|
143
|
+
if (isNotNull(left) && isNotNull(right) && position >= left.position && position < right.position) {
|
|
144
|
+
const alpha = (position - left.position) / (right.position - left.position);
|
|
145
|
+
return left.seconds + alpha * (right.seconds - left.seconds);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return last.seconds;
|
|
149
|
+
};
|
|
150
|
+
const sampleExtent = (adapter) => {
|
|
151
|
+
if (isInstanceOf(adapter, AudioRegionBoxAdapter)) {
|
|
152
|
+
return { ppqn: adapter.loopDuration, seconds: adapter.box.loopDuration.getValue() };
|
|
153
|
+
}
|
|
154
|
+
return { ppqn: adapter.duration, seconds: adapter.box.duration.getValue() };
|
|
155
|
+
};
|
|
116
156
|
const switchTimeBaseToSeconds = ({ box, timeBase }, audibleDuration) => {
|
|
117
157
|
if (timeBase === TimeBase.Seconds) {
|
|
118
158
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DevicesClipboardHandler.d.ts","sourceRoot":"","sources":["../../../../src/ui/clipboard/types/DevicesClipboardHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,OAAO,EAKP,MAAM,EAEN,QAAQ,EAGX,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAM,QAAQ,EAAC,MAAM,kBAAkB,CAAA;AAG9C,OAAO,EAEH,WAAW,EACX,gBAAgB,EAEhB,UAAU,EAGV,iBAAiB,EAGpB,MAAM,0BAA0B,CAAA;AACjC,OAAO,EAAC,cAAc,EAAE,gBAAgB,EAAC,MAAM,qBAAqB,CAAA;AAGpE,KAAK,gBAAgB,GAAG,cAAc,CAAC,SAAS,CAAC,CAAA;AAajD,yBAAiB,gBAAgB,CAAC;IAC9B,KAAY,OAAO,GAAG;QAClB,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;QACtC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;QACzB,QAAQ,CAAC,SAAS,EAAE,iBAAiB,CAAC,gBAAgB,CAAC,CAAA;QACvD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;QAC3B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;QACjC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAA;KACjD,CAAA;IAyBM,MAAM,aAAa,GAAI,oEAOG,OAAO,KAAG,gBAAgB,CAAC,gBAAgB,
|
|
1
|
+
{"version":3,"file":"DevicesClipboardHandler.d.ts","sourceRoot":"","sources":["../../../../src/ui/clipboard/types/DevicesClipboardHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,OAAO,EAKP,MAAM,EAEN,QAAQ,EAGX,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAM,QAAQ,EAAC,MAAM,kBAAkB,CAAA;AAG9C,OAAO,EAEH,WAAW,EACX,gBAAgB,EAEhB,UAAU,EAGV,iBAAiB,EAGpB,MAAM,0BAA0B,CAAA;AACjC,OAAO,EAAC,cAAc,EAAE,gBAAgB,EAAC,MAAM,qBAAqB,CAAA;AAGpE,KAAK,gBAAgB,GAAG,cAAc,CAAC,SAAS,CAAC,CAAA;AAajD,yBAAiB,gBAAgB,CAAC;IAC9B,KAAY,OAAO,GAAG;QAClB,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;QACtC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;QACzB,QAAQ,CAAC,SAAS,EAAE,iBAAiB,CAAC,gBAAgB,CAAC,CAAA;QACvD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;QAC3B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;QACjC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAA;KACjD,CAAA;IAyBM,MAAM,aAAa,GAAI,oEAOG,OAAO,KAAG,gBAAgB,CAAC,gBAAgB,CA4N3E,CAAA;CACJ"}
|
|
@@ -71,9 +71,10 @@ export var DevicesClipboard;
|
|
|
71
71
|
.map(pointer => pointer.box);
|
|
72
72
|
const mandatoryDeps = Array.from(boxGraph.dependenciesOf(box, {
|
|
73
73
|
alwaysFollowMandatory: true,
|
|
74
|
+
stopAtResources: true,
|
|
74
75
|
excludeBox: (dep) => dep.ephemeral || DeviceBoxUtils.isDeviceBox(dep)
|
|
75
76
|
|| dep.name === RootBox.ClassName
|
|
76
|
-
}).boxes).filter(dep =>
|
|
77
|
+
}).boxes).filter(dep => dep.resource !== "preserved");
|
|
77
78
|
const preserved = [box, ...ownedChildren].flatMap(root => Array.from(boxGraph.dependenciesOf(root, {
|
|
78
79
|
alwaysFollowMandatory: true,
|
|
79
80
|
excludeBox: (dep) => dep.ephemeral || DeviceBoxUtils.isDeviceBox(dep)
|
|
@@ -94,6 +95,7 @@ export var DevicesClipboard;
|
|
|
94
95
|
trackContent.push(regionPointer.box);
|
|
95
96
|
const regionDeps = Array.from(boxGraph.dependenciesOf(regionPointer.box, {
|
|
96
97
|
alwaysFollowMandatory: true,
|
|
98
|
+
stopAtResources: true,
|
|
97
99
|
excludeBox: (dep) => dep.ephemeral
|
|
98
100
|
|| isInstanceOf(dep, TrackBox)
|
|
99
101
|
|| DeviceBoxUtils.isDeviceBox(dep)
|
|
@@ -212,7 +214,7 @@ export var DevicesClipboard;
|
|
|
212
214
|
if (pointer.pointerType === Pointers.AudioEffectHost) {
|
|
213
215
|
return Option.wrap(host.audioEffectsField.address);
|
|
214
216
|
}
|
|
215
|
-
if (pointer.pointerType === Pointers.TrackCollection
|
|
217
|
+
if (pointer.pointerType === Pointers.TrackCollection) {
|
|
216
218
|
return Option.wrap(host.audioUnitBoxAdapter().tracksField.address);
|
|
217
219
|
}
|
|
218
220
|
if (pointer.pointerType === Pointers.Automation && replaceInstrument) {
|
|
@@ -227,8 +229,18 @@ export var DevicesClipboard;
|
|
|
227
229
|
}
|
|
228
230
|
return Option.None;
|
|
229
231
|
},
|
|
230
|
-
excludeBox: box =>
|
|
231
|
-
|
|
232
|
+
excludeBox: box => {
|
|
233
|
+
if (replaceInstrument) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
if (DeviceBoxUtils.isInstrumentDeviceBox(box)) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
if (isInstanceOf(box, TrackBox)) {
|
|
240
|
+
return metadata.hasInstrument;
|
|
241
|
+
}
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
232
244
|
});
|
|
233
245
|
const deviceBoxes = boxes.filter(box => DeviceBoxUtils.isDeviceBox(box));
|
|
234
246
|
const newMidiEffects = deviceBoxes
|
|
@@ -239,14 +251,12 @@ export var DevicesClipboard;
|
|
|
239
251
|
.sort((a, b) => a.index.getValue() - b.index.getValue());
|
|
240
252
|
newMidiEffects.forEach((box, idx) => box.index.setValue(midiInsertIndex + idx));
|
|
241
253
|
newAudioEffects.forEach((box, idx) => box.index.setValue(audioInsertIndex + idx));
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
allTracks.forEach((track, idx) => track.index.setValue(idx));
|
|
249
|
-
}
|
|
254
|
+
const tracksField = host.audioUnitBoxAdapter().tracksField;
|
|
255
|
+
const allTracks = tracksField.pointerHub.filter(Pointers.TrackCollection)
|
|
256
|
+
.filter(pointer => isInstanceOf(pointer.box, TrackBox))
|
|
257
|
+
.map(pointer => pointer.box)
|
|
258
|
+
.sort((trackA, trackB) => trackA.index.getValue() - trackB.index.getValue());
|
|
259
|
+
allTracks.forEach((track, idx) => track.index.setValue(idx));
|
|
250
260
|
selection.select(...deviceBoxes.map(box => boxAdapters.adapterFor(box, Devices.isAny)));
|
|
251
261
|
});
|
|
252
262
|
}
|
|
@@ -195,9 +195,10 @@ describe("DevicesClipboardHandler", () => {
|
|
|
195
195
|
.map(pointer => pointer.box);
|
|
196
196
|
const mandatoryDeps = Array.from(boxGraph.dependenciesOf(deviceBox, {
|
|
197
197
|
alwaysFollowMandatory: true,
|
|
198
|
+
stopAtResources: true,
|
|
198
199
|
excludeBox: (dep) => dep.ephemeral || DeviceBoxUtils.isDeviceBox(dep)
|
|
199
200
|
|| dep.name === RootBox.ClassName
|
|
200
|
-
}).boxes).filter(dep =>
|
|
201
|
+
}).boxes).filter(dep => dep.resource !== "preserved");
|
|
201
202
|
const preserved = [deviceBox, ...ownedChildren].flatMap(root => Array.from(boxGraph.dependenciesOf(root, {
|
|
202
203
|
alwaysFollowMandatory: true,
|
|
203
204
|
excludeBox: (dep) => dep.ephemeral || DeviceBoxUtils.isDeviceBox(dep)
|
|
@@ -215,6 +216,7 @@ describe("DevicesClipboardHandler", () => {
|
|
|
215
216
|
trackContent.push(regionPointer.box);
|
|
216
217
|
const regionDeps = Array.from(boxGraph.dependenciesOf(regionPointer.box, {
|
|
217
218
|
alwaysFollowMandatory: true,
|
|
219
|
+
stopAtResources: true,
|
|
218
220
|
excludeBox: (dep) => dep.ephemeral
|
|
219
221
|
|| isInstanceOf(dep, TrackBox)
|
|
220
222
|
|| DeviceBoxUtils.isDeviceBox(dep)
|
|
@@ -232,7 +234,7 @@ describe("DevicesClipboardHandler", () => {
|
|
|
232
234
|
return true;
|
|
233
235
|
});
|
|
234
236
|
};
|
|
235
|
-
const makePasteMapper = (targetAudioUnit, replaceInstrument) => ({
|
|
237
|
+
const makePasteMapper = (targetAudioUnit, replaceInstrument, hasInstrument = true) => ({
|
|
236
238
|
mapPointer: (pointer) => {
|
|
237
239
|
if (pointer.pointerType === Pointers.InstrumentHost && replaceInstrument) {
|
|
238
240
|
return Option.wrap(targetAudioUnit.input.address);
|
|
@@ -243,7 +245,7 @@ describe("DevicesClipboardHandler", () => {
|
|
|
243
245
|
if (pointer.pointerType === Pointers.MIDIEffectHost) {
|
|
244
246
|
return Option.wrap(targetAudioUnit.midiEffects.address);
|
|
245
247
|
}
|
|
246
|
-
if (pointer.pointerType === Pointers.TrackCollection
|
|
248
|
+
if (pointer.pointerType === Pointers.TrackCollection) {
|
|
247
249
|
return Option.wrap(targetAudioUnit.tracks.address);
|
|
248
250
|
}
|
|
249
251
|
if (pointer.pointerType === Pointers.Automation && replaceInstrument) {
|
|
@@ -251,7 +253,18 @@ describe("DevicesClipboardHandler", () => {
|
|
|
251
253
|
}
|
|
252
254
|
return Option.None;
|
|
253
255
|
},
|
|
254
|
-
excludeBox: (box) =>
|
|
256
|
+
excludeBox: (box) => {
|
|
257
|
+
if (replaceInstrument) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
if (DeviceBoxUtils.isInstrumentDeviceBox(box)) {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
if (isInstanceOf(box, TrackBox)) {
|
|
264
|
+
return hasInstrument;
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
255
268
|
});
|
|
256
269
|
// ─────────────────────────────────────────────────────────
|
|
257
270
|
// Audio effect paste
|
|
@@ -284,6 +297,113 @@ describe("DevicesClipboardHandler", () => {
|
|
|
284
297
|
});
|
|
285
298
|
});
|
|
286
299
|
// ─────────────────────────────────────────────────────────
|
|
300
|
+
// Audio effect with automation: copy scope + paste round-trip
|
|
301
|
+
// ─────────────────────────────────────────────────────────
|
|
302
|
+
describe("audio effect with automation", () => {
|
|
303
|
+
it("includes ValueEventCollectionBox when copying effect with automation events", () => {
|
|
304
|
+
const audioUnit = createAudioUnit(source);
|
|
305
|
+
const effect = addAudioEffect(source, audioUnit, "Comp", 0);
|
|
306
|
+
const autoTrack = addAutomationTrack(source, audioUnit, effect.threshold, 0);
|
|
307
|
+
addValueRegion(source, autoTrack, 0, 960);
|
|
308
|
+
const deps = collectDeviceDependencies(effect, source.boxGraph);
|
|
309
|
+
expect(deps.filter(box => isInstanceOf(box, TrackBox)).length).toBe(1);
|
|
310
|
+
expect(deps.filter(box => isInstanceOf(box, ValueRegionBox)).length).toBe(1);
|
|
311
|
+
expect(deps.filter(box => isInstanceOf(box, ValueEventCollectionBox)).length).toBe(1);
|
|
312
|
+
});
|
|
313
|
+
it("includes mirror regions on different tracks of the same device", () => {
|
|
314
|
+
const audioUnit = createAudioUnit(source);
|
|
315
|
+
const effect = addAudioEffect(source, audioUnit, "Comp", 0);
|
|
316
|
+
const trackA = addAutomationTrack(source, audioUnit, effect.threshold, 0);
|
|
317
|
+
const trackB = addAutomationTrack(source, audioUnit, effect.ratio, 1);
|
|
318
|
+
const regionA = addValueRegion(source, trackA, 0, 960);
|
|
319
|
+
const sharedEvents = regionA.events.targetVertex.unwrap().box;
|
|
320
|
+
let regionB;
|
|
321
|
+
source.boxGraph.beginTransaction();
|
|
322
|
+
regionB = ValueRegionBox.create(source.boxGraph, UUID.generate(), box => {
|
|
323
|
+
box.regions.refer(trackB.regions);
|
|
324
|
+
box.events.refer(sharedEvents.owners);
|
|
325
|
+
box.position.setValue(0);
|
|
326
|
+
box.duration.setValue(960);
|
|
327
|
+
});
|
|
328
|
+
source.boxGraph.endTransaction();
|
|
329
|
+
const deps = collectDeviceDependencies(effect, source.boxGraph);
|
|
330
|
+
const regions = deps.filter(box => isInstanceOf(box, ValueRegionBox));
|
|
331
|
+
expect(regions.length).toBe(2);
|
|
332
|
+
expect(regions).toContain(regionA);
|
|
333
|
+
expect(regions).toContain(regionB);
|
|
334
|
+
expect(deps.filter(box => isInstanceOf(box, ValueEventCollectionBox)).length).toBe(1);
|
|
335
|
+
expect(deps.filter(box => isInstanceOf(box, TrackBox)).length).toBe(2);
|
|
336
|
+
});
|
|
337
|
+
it("does not pull in regions from unrelated devices that mirror the same collection", () => {
|
|
338
|
+
const audioUnitA = createAudioUnit(source, 1);
|
|
339
|
+
const effectA = addAudioEffect(source, audioUnitA, "CompA", 0);
|
|
340
|
+
const trackA = addAutomationTrack(source, audioUnitA, effectA.threshold, 0);
|
|
341
|
+
const regionA = addValueRegion(source, trackA, 0, 960);
|
|
342
|
+
const sharedEvents = regionA.events.targetVertex.unwrap().box;
|
|
343
|
+
const audioUnitB = createAudioUnit(source, 2);
|
|
344
|
+
const effectB = addAudioEffect(source, audioUnitB, "CompB", 0);
|
|
345
|
+
const trackB = addAutomationTrack(source, audioUnitB, effectB.threshold, 0);
|
|
346
|
+
source.boxGraph.beginTransaction();
|
|
347
|
+
const unrelatedRegion = ValueRegionBox.create(source.boxGraph, UUID.generate(), box => {
|
|
348
|
+
box.regions.refer(trackB.regions);
|
|
349
|
+
box.events.refer(sharedEvents.owners);
|
|
350
|
+
box.position.setValue(0);
|
|
351
|
+
box.duration.setValue(960);
|
|
352
|
+
});
|
|
353
|
+
source.boxGraph.endTransaction();
|
|
354
|
+
const deps = collectDeviceDependencies(effectA, source.boxGraph);
|
|
355
|
+
const regions = deps.filter(box => isInstanceOf(box, ValueRegionBox));
|
|
356
|
+
expect(regions).toContain(regionA);
|
|
357
|
+
expect(regions).not.toContain(unrelatedRegion);
|
|
358
|
+
const tracks = deps.filter(box => isInstanceOf(box, TrackBox));
|
|
359
|
+
expect(tracks).toContain(trackA);
|
|
360
|
+
expect(tracks).not.toContain(trackB);
|
|
361
|
+
});
|
|
362
|
+
it("round-trip paste of effect with automation events does not throw", () => {
|
|
363
|
+
const sourceAU = createAudioUnit(source);
|
|
364
|
+
const effect = addAudioEffect(source, sourceAU, "Comp", 0);
|
|
365
|
+
const autoTrack = addAutomationTrack(source, sourceAU, effect.threshold, 0);
|
|
366
|
+
addValueRegion(source, autoTrack, 0, 960);
|
|
367
|
+
const deps = collectDeviceDependencies(effect, source.boxGraph);
|
|
368
|
+
const data = ClipboardUtils.serializeBoxes([effect, ...deps]);
|
|
369
|
+
const targetAU = createAudioUnit(target);
|
|
370
|
+
const editing = new BoxEditing(target.boxGraph);
|
|
371
|
+
expect(() => {
|
|
372
|
+
editing.modify(() => {
|
|
373
|
+
ClipboardUtils.deserializeBoxes(data, target.boxGraph, makePasteMapper(targetAU, false, false));
|
|
374
|
+
});
|
|
375
|
+
}).not.toThrow();
|
|
376
|
+
const pastedEffects = targetAU.audioEffects.pointerHub.incoming()
|
|
377
|
+
.filter(pointer => isInstanceOf(pointer.box, CompressorDeviceBox));
|
|
378
|
+
expect(pastedEffects.length).toBe(1);
|
|
379
|
+
const pastedTracks = targetAU.tracks.pointerHub.filter(Pointers.TrackCollection)
|
|
380
|
+
.filter(pointer => isInstanceOf(pointer.box, TrackBox));
|
|
381
|
+
expect(pastedTracks.length).toBe(1);
|
|
382
|
+
});
|
|
383
|
+
it("pasted automation track targets the pasted device's parameter", () => {
|
|
384
|
+
const sourceAU = createAudioUnit(source);
|
|
385
|
+
const effect = addAudioEffect(source, sourceAU, "Comp", 0);
|
|
386
|
+
const autoTrack = addAutomationTrack(source, sourceAU, effect.threshold, 0);
|
|
387
|
+
addValueRegion(source, autoTrack, 0, 960);
|
|
388
|
+
const deps = collectDeviceDependencies(effect, source.boxGraph);
|
|
389
|
+
const data = ClipboardUtils.serializeBoxes([effect, ...deps]);
|
|
390
|
+
const targetAU = createAudioUnit(target);
|
|
391
|
+
const editing = new BoxEditing(target.boxGraph);
|
|
392
|
+
editing.modify(() => {
|
|
393
|
+
ClipboardUtils.deserializeBoxes(data, target.boxGraph, makePasteMapper(targetAU, false, false));
|
|
394
|
+
});
|
|
395
|
+
const pastedEffect = targetAU.audioEffects.pointerHub.incoming()
|
|
396
|
+
.filter(pointer => isInstanceOf(pointer.box, CompressorDeviceBox))
|
|
397
|
+
.map(pointer => pointer.box)[0];
|
|
398
|
+
const pastedTrack = targetAU.tracks.pointerHub.filter(Pointers.TrackCollection)
|
|
399
|
+
.filter(pointer => isInstanceOf(pointer.box, TrackBox))
|
|
400
|
+
.map(pointer => pointer.box)[0];
|
|
401
|
+
expect(pastedTrack).toBeDefined();
|
|
402
|
+
const targetVertex = pastedTrack.target.targetVertex.unwrap();
|
|
403
|
+
expect(targetVertex.box).toBe(pastedEffect);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
// ─────────────────────────────────────────────────────────
|
|
287
407
|
// Instrument paste
|
|
288
408
|
// ─────────────────────────────────────────────────────────
|
|
289
409
|
describe("paste instrument", () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"YSync.d.ts","sourceRoot":"","sources":["../../src/ysync/YSync.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"YSync.d.ts","sourceRoot":"","sources":["../../src/ysync/YSync.ts"],"names":[],"mappings":"AAAA,OAAO,EAUH,QAAQ,EAER,UAAU,EAIb,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAEH,QAAQ,EAOX,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,CAAC,MAAM,KAAK,CAAA;AAIxB,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;IACvB,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IACtB,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IACrB,QAAQ,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;CAC/B,CAAA;AAED,qBAAa,KAAK,CAAC,CAAC,CAAE,YAAW,UAAU;;IACvC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAQ;IAEjC,gBAAgB;IAChB,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,GAAG,OAAO;WAItB,YAAY,CAAC,CAAC,EAAE,EAAC,QAAQ,EAAE,KAAK,EAAC,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;WAcnE,QAAQ,CAAC,CAAC,EAAE,EAAC,QAAQ,EAAE,KAAK,EAAC,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBAqChE,EAAC,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAC,EAAE,SAAS,CAAC,CAAC,CAAC;IASrD,SAAS,IAAI,IAAI;CAiOpB"}
|
package/dist/ysync/YSync.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { asDefined, asInstanceOf, assert, EmptyExec, isUndefined, Option, panic, Terminable, Terminator, tryCatch, UUID } from "@opendaw/lib-std";
|
|
1
|
+
import { asDefined, asInstanceOf, assert, EmptyExec, isInstanceOf, isUndefined, Option, panic, Terminable, Terminator, tryCatch, UUID } from "@opendaw/lib-std";
|
|
2
|
+
import { optimizeUpdates } from "@opendaw/lib-box";
|
|
2
3
|
import { YMapper } from "./YMapper";
|
|
3
4
|
import * as Y from "yjs";
|
|
4
5
|
export class YSync {
|
|
@@ -200,50 +201,95 @@ export class YSync {
|
|
|
200
201
|
return Terminable.many(this.#boxGraph.subscribeTransaction({
|
|
201
202
|
onBeginTransaction: EmptyExec,
|
|
202
203
|
onEndTransaction: (rolledBack) => {
|
|
204
|
+
const pending = this.#updates.splice(0);
|
|
203
205
|
if (this.#ignoreUpdates || rolledBack) {
|
|
204
|
-
this.#updates.length = 0;
|
|
205
206
|
return;
|
|
206
207
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const { address: { fieldKeys }, newValue } = update;
|
|
221
|
-
let field = boxObject.get("fields");
|
|
222
|
-
for (let i = 0; i < fieldKeys.length - 1; i++) {
|
|
223
|
-
field = asDefined(field.get(String(fieldKeys[i])), `No field at '${fieldKeys[i]}'`);
|
|
224
|
-
}
|
|
225
|
-
field.set(String(fieldKeys[fieldKeys.length - 1]), newValue);
|
|
226
|
-
}
|
|
227
|
-
else if (update.type === "pointer") {
|
|
228
|
-
const key = UUID.toString(update.address.uuid);
|
|
229
|
-
const boxObject = asDefined(this.#boxes.get(key), "Could not find box");
|
|
230
|
-
const { address: { fieldKeys }, newAddress } = update;
|
|
231
|
-
let field = boxObject.get("fields");
|
|
232
|
-
for (let i = 0; i < fieldKeys.length - 1; i++) {
|
|
233
|
-
field = asDefined(field.get(String(fieldKeys[i])), `No field at '${fieldKeys[i]}'`);
|
|
234
|
-
}
|
|
235
|
-
field.set(String(fieldKeys[fieldKeys.length - 1]), newAddress.mapOr(address => address.toString(), null));
|
|
236
|
-
}
|
|
237
|
-
else if (update.type === "delete") {
|
|
238
|
-
this.#boxes.delete(UUID.toString(update.uuid));
|
|
239
|
-
}
|
|
240
|
-
}), "[openDAW] updates");
|
|
241
|
-
this.#updates.length = 0;
|
|
208
|
+
const optimized = optimizeUpdates(pending);
|
|
209
|
+
if (optimized.length === 0) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const result = tryCatch(() => this.#getDoc()
|
|
213
|
+
.transact(() => optimized.forEach(update => this.#applyUpdate(update)), "[openDAW] updates"));
|
|
214
|
+
if (result.status === "failure") {
|
|
215
|
+
console.error("[YSync] flush failed, dropping updates", {
|
|
216
|
+
count: optimized.length,
|
|
217
|
+
error: result.error
|
|
218
|
+
});
|
|
219
|
+
throw result.error;
|
|
220
|
+
}
|
|
242
221
|
}
|
|
243
222
|
}), this.#boxGraph.subscribeToAllUpdatesImmediate({
|
|
244
223
|
onUpdate: (update) => this.#updates.push(update)
|
|
245
224
|
}));
|
|
246
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* TRANSFER ONE CHANGE FROM OPENDAW TO YJS
|
|
228
|
+
*/
|
|
229
|
+
#applyUpdate(update) {
|
|
230
|
+
if (update.type === "new") {
|
|
231
|
+
const uuid = update.uuid;
|
|
232
|
+
const key = UUID.toString(uuid);
|
|
233
|
+
const optBox = this.#boxGraph.findBox(uuid);
|
|
234
|
+
if (optBox.isEmpty()) {
|
|
235
|
+
// Phantom box: created and removed in same transaction.
|
|
236
|
+
// optimizeUpdates should have filtered this, but guard in case.
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this.#boxes.set(key, YMapper.createBoxMap(optBox.unwrap()));
|
|
240
|
+
}
|
|
241
|
+
else if (update.type === "primitive") {
|
|
242
|
+
const key = UUID.toString(update.address.uuid);
|
|
243
|
+
const boxObject = this.#boxes.get(key);
|
|
244
|
+
if (!isInstanceOf(boxObject, Y.Map)) {
|
|
245
|
+
console.warn(`[YSync] primitive update skipped: box '${key}' missing`);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const field = this.#resolveFieldMap(boxObject, key, update.address.fieldKeys);
|
|
249
|
+
if (field === undefined) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
field.set(String(update.address.fieldKeys[update.address.fieldKeys.length - 1]), update.newValue);
|
|
253
|
+
}
|
|
254
|
+
else if (update.type === "pointer") {
|
|
255
|
+
const key = UUID.toString(update.address.uuid);
|
|
256
|
+
const boxObject = this.#boxes.get(key);
|
|
257
|
+
if (!isInstanceOf(boxObject, Y.Map)) {
|
|
258
|
+
console.warn(`[YSync] pointer update skipped: box '${key}' missing`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const field = this.#resolveFieldMap(boxObject, key, update.address.fieldKeys);
|
|
262
|
+
if (field === undefined) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
field.set(String(update.address.fieldKeys[update.address.fieldKeys.length - 1]), update.newAddress.mapOr(address => address.toString(), null));
|
|
266
|
+
}
|
|
267
|
+
else if (update.type === "delete") {
|
|
268
|
+
this.#boxes.delete(UUID.toString(update.uuid));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Walks from the box map down to the Y.Map that owns `fieldKeys[last]`.
|
|
273
|
+
* Returns `undefined` (with a warning) instead of throwing if the path
|
|
274
|
+
* cannot be resolved — protects the yjs transaction from partial writes.
|
|
275
|
+
*/
|
|
276
|
+
#resolveFieldMap(boxObject, key, fieldKeys) {
|
|
277
|
+
const fieldsValue = boxObject.get("fields");
|
|
278
|
+
if (!isInstanceOf(fieldsValue, Y.Map)) {
|
|
279
|
+
console.warn(`[YSync] box '${key}' missing 'fields' Y.Map; skipping update`);
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
let field = fieldsValue;
|
|
283
|
+
for (let i = 0; i < fieldKeys.length - 1; i++) {
|
|
284
|
+
const next = field.get(String(fieldKeys[i]));
|
|
285
|
+
if (!isInstanceOf(next, Y.Map)) {
|
|
286
|
+
console.warn(`[YSync] box '${key}' field path broken at '${fieldKeys[i]}'; skipping update`);
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
field = next;
|
|
290
|
+
}
|
|
291
|
+
return field;
|
|
292
|
+
}
|
|
247
293
|
#getDoc() {
|
|
248
294
|
return asDefined(this.#boxes.doc, "Y.Map is not connect to Y.Doc");
|
|
249
295
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"YSync.test.d.ts","sourceRoot":"","sources":["../../src/ysync/YSync.test.ts"],"names":[],"mappings":""}
|