@opendaw/studio-core 0.0.126 → 0.0.128
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/AssetService.d.ts.map +1 -1
- package/dist/AssetService.js +7 -7
- package/dist/AudioConsolidation.d.ts.map +1 -1
- package/dist/AudioConsolidation.js +2 -2
- package/dist/EffectBox.d.ts +2 -2
- package/dist/EffectBox.d.ts.map +1 -1
- package/dist/EffectFactories.d.ts +20 -14
- package/dist/EffectFactories.d.ts.map +1 -1
- package/dist/EffectFactories.js +88 -20
- package/dist/EffectFactory.d.ts +1 -0
- package/dist/EffectFactory.d.ts.map +1 -1
- package/dist/Engine.d.ts +2 -1
- package/dist/Engine.d.ts.map +1 -1
- package/dist/EngineFacade.d.ts +2 -1
- package/dist/EngineFacade.d.ts.map +1 -1
- package/dist/EngineFacade.js +3 -0
- package/dist/EngineWorklet.d.ts +2 -1
- package/dist/EngineWorklet.d.ts.map +1 -1
- package/dist/EngineWorklet.js +11 -1
- package/dist/Mixer.d.ts.map +1 -1
- package/dist/Mixer.js +3 -2
- package/dist/OfflineEngineRenderer.d.ts.map +1 -1
- package/dist/OfflineEngineRenderer.js +41 -3
- package/dist/StudioPreferences.d.ts +1 -1
- package/dist/StudioSettings.d.ts +1 -1
- package/dist/StudioSettings.js +2 -2
- package/dist/capture/RecordAudio.d.ts.map +1 -1
- package/dist/capture/RecordAudio.js +48 -18
- package/dist/capture/RecordAutomation.d.ts.map +1 -1
- package/dist/capture/RecordAutomation.js +219 -198
- package/dist/capture/RecordMidi.d.ts.map +1 -1
- package/dist/capture/RecordMidi.js +1 -1
- package/dist/cloud/CloudBackupSamples.js +1 -1
- package/dist/dawproject/DawProjectExporter.js +1 -1
- package/dist/dawproject/DawProjectService.d.ts.map +1 -1
- package/dist/dawproject/DawProjectService.js +3 -16
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/midi/MidiDevices.d.ts.map +1 -1
- package/dist/midi/MidiDevices.js +8 -2
- package/dist/offline-engine.js +1 -1
- package/dist/offline-engine.js.map +3 -3
- package/dist/processors.js +37 -25
- package/dist/processors.js.map +4 -4
- package/dist/project/Project.d.ts.map +1 -1
- package/dist/project/Project.js +30 -5
- package/dist/project/Recovery.js +1 -1
- package/dist/project/migration/MigrateAudioClipBox.d.ts.map +1 -1
- package/dist/project/migration/MigrateAudioClipBox.js +7 -0
- package/dist/project/migration/MigrateAudioRegionBox.d.ts.map +1 -1
- package/dist/project/migration/MigrateAudioRegionBox.js +7 -0
- package/dist/samples/OpenSampleAPI.d.ts.map +1 -1
- package/dist/samples/OpenSampleAPI.js +1 -1
- package/dist/samples/SampleService.js +1 -1
- package/dist/samples/SampleStorage.d.ts.map +1 -1
- package/dist/samples/SampleStorage.js +1 -1
- package/dist/ui/clipboard/ClipboardManager.d.ts.map +1 -1
- package/dist/ui/clipboard/ClipboardManager.js +18 -4
- package/dist/ui/clipboard/types/AudioUnitsClipboardHandler.d.ts.map +1 -1
- package/dist/ui/clipboard/types/AudioUnitsClipboardHandler.js +8 -2
- package/dist/ui/clipboard/types/DevicesClipboardHandler.d.ts.map +1 -1
- package/dist/ui/clipboard/types/DevicesClipboardHandler.js +77 -10
- package/dist/ui/clipboard/types/DevicesClipboardHandler.test.d.ts +2 -0
- package/dist/ui/clipboard/types/DevicesClipboardHandler.test.d.ts.map +1 -0
- package/dist/ui/clipboard/types/DevicesClipboardHandler.test.js +1154 -0
- package/dist/ui/timeline/RegionClipResolver.d.ts.map +1 -1
- package/dist/ui/timeline/RegionClipResolver.js +21 -29
- package/dist/ui/timeline/TimeGrid.d.ts +2 -0
- package/dist/ui/timeline/TimeGrid.d.ts.map +1 -1
- package/dist/ui/timeline/TimeGrid.js +13 -1
- package/dist/workers-main.js +1 -1
- package/dist/workers-main.js.map +3 -3
- package/dist/ysync/YService.d.ts +6 -1
- package/dist/ysync/YService.d.ts.map +1 -1
- package/dist/ysync/YService.js +2 -2
- package/dist/ysync/YSync.d.ts.map +1 -1
- package/dist/ysync/YSync.js +1 -0
- package/package.json +15 -15
- package/dist/WavFile.d.ts +0 -7
- package/dist/WavFile.d.ts.map +0 -1
- package/dist/WavFile.js +0 -120
|
@@ -0,0 +1,1154 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach } from "vitest";
|
|
2
|
+
import { isDefined, isInstanceOf, Option, UUID } from "@opendaw/lib-std";
|
|
3
|
+
import { BoxEditing } from "@opendaw/lib-box";
|
|
4
|
+
import { ApparatDeviceBox, AudioFileBox, AudioUnitBox, CompressorDeviceBox, MIDIOutputBox, MIDIOutputDeviceBox, NoteEventCollectionBox, NoteRegionBox, PlayfieldDeviceBox, PlayfieldSampleBox, RootBox, TapeDeviceBox, TrackBox, VaporisateurDeviceBox, ValueEventCollectionBox, ValueRegionBox, WerkstattParameterBox, WerkstattSampleBox } from "@opendaw/studio-boxes";
|
|
5
|
+
import { AudioUnitType, Pointers } from "@opendaw/studio-enums";
|
|
6
|
+
import { DeviceBoxUtils, ProjectSkeleton, TrackType } from "@opendaw/studio-adapters";
|
|
7
|
+
import { ClipboardUtils } from "../ClipboardUtils";
|
|
8
|
+
describe("DevicesClipboardHandler", () => {
|
|
9
|
+
let source;
|
|
10
|
+
let target;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
source = ProjectSkeleton.empty({ createDefaultUser: true, createOutputMaximizer: false });
|
|
13
|
+
target = ProjectSkeleton.empty({ createDefaultUser: true, createOutputMaximizer: false });
|
|
14
|
+
});
|
|
15
|
+
const createAudioUnit = (skeleton, index = 1) => {
|
|
16
|
+
const { boxGraph, mandatoryBoxes: { rootBox, primaryAudioBusBox } } = skeleton;
|
|
17
|
+
let audioUnitBox;
|
|
18
|
+
boxGraph.beginTransaction();
|
|
19
|
+
audioUnitBox = AudioUnitBox.create(boxGraph, UUID.generate(), box => {
|
|
20
|
+
box.type.setValue(AudioUnitType.Instrument);
|
|
21
|
+
box.collection.refer(rootBox.audioUnits);
|
|
22
|
+
box.output.refer(primaryAudioBusBox.input);
|
|
23
|
+
box.index.setValue(index);
|
|
24
|
+
});
|
|
25
|
+
boxGraph.endTransaction();
|
|
26
|
+
return audioUnitBox;
|
|
27
|
+
};
|
|
28
|
+
const addTapeInstrument = (skeleton, audioUnit, label) => {
|
|
29
|
+
const { boxGraph } = skeleton;
|
|
30
|
+
let device;
|
|
31
|
+
boxGraph.beginTransaction();
|
|
32
|
+
device = TapeDeviceBox.create(boxGraph, UUID.generate(), box => {
|
|
33
|
+
box.label.setValue(label);
|
|
34
|
+
box.host.refer(audioUnit.input);
|
|
35
|
+
});
|
|
36
|
+
boxGraph.endTransaction();
|
|
37
|
+
return device;
|
|
38
|
+
};
|
|
39
|
+
const addApparatInstrument = (skeleton, audioUnit, label) => {
|
|
40
|
+
const { boxGraph } = skeleton;
|
|
41
|
+
let device;
|
|
42
|
+
boxGraph.beginTransaction();
|
|
43
|
+
device = ApparatDeviceBox.create(boxGraph, UUID.generate(), box => {
|
|
44
|
+
box.label.setValue(label);
|
|
45
|
+
box.host.refer(audioUnit.input);
|
|
46
|
+
});
|
|
47
|
+
boxGraph.endTransaction();
|
|
48
|
+
return device;
|
|
49
|
+
};
|
|
50
|
+
const addVaporisateur = (skeleton, audioUnit, label) => {
|
|
51
|
+
const { boxGraph } = skeleton;
|
|
52
|
+
let device;
|
|
53
|
+
boxGraph.beginTransaction();
|
|
54
|
+
device = VaporisateurDeviceBox.create(boxGraph, UUID.generate(), box => {
|
|
55
|
+
box.label.setValue(label);
|
|
56
|
+
box.host.refer(audioUnit.input);
|
|
57
|
+
});
|
|
58
|
+
boxGraph.endTransaction();
|
|
59
|
+
return device;
|
|
60
|
+
};
|
|
61
|
+
const addAutomationTrack = (skeleton, audioUnit, automationTarget, index) => {
|
|
62
|
+
const { boxGraph } = skeleton;
|
|
63
|
+
let trackBox;
|
|
64
|
+
boxGraph.beginTransaction();
|
|
65
|
+
trackBox = TrackBox.create(boxGraph, UUID.generate(), box => {
|
|
66
|
+
box.type.setValue(TrackType.Value);
|
|
67
|
+
box.tracks.refer(audioUnit.tracks);
|
|
68
|
+
box.target.refer(automationTarget);
|
|
69
|
+
box.index.setValue(index);
|
|
70
|
+
});
|
|
71
|
+
boxGraph.endTransaction();
|
|
72
|
+
return trackBox;
|
|
73
|
+
};
|
|
74
|
+
const addPlayfieldInstrument = (skeleton, audioUnit, label) => {
|
|
75
|
+
const { boxGraph } = skeleton;
|
|
76
|
+
let device;
|
|
77
|
+
boxGraph.beginTransaction();
|
|
78
|
+
device = PlayfieldDeviceBox.create(boxGraph, UUID.generate(), box => {
|
|
79
|
+
box.label.setValue(label);
|
|
80
|
+
box.host.refer(audioUnit.input);
|
|
81
|
+
});
|
|
82
|
+
boxGraph.endTransaction();
|
|
83
|
+
return device;
|
|
84
|
+
};
|
|
85
|
+
const addPlayfieldSample = (skeleton, playfield, fileName, midiNote) => {
|
|
86
|
+
const { boxGraph } = skeleton;
|
|
87
|
+
let sample;
|
|
88
|
+
let audioFile;
|
|
89
|
+
boxGraph.beginTransaction();
|
|
90
|
+
audioFile = AudioFileBox.create(boxGraph, UUID.generate(), box => {
|
|
91
|
+
box.fileName.setValue(fileName);
|
|
92
|
+
box.startInSeconds.setValue(0);
|
|
93
|
+
box.endInSeconds.setValue(1);
|
|
94
|
+
});
|
|
95
|
+
sample = PlayfieldSampleBox.create(boxGraph, UUID.generate(), box => {
|
|
96
|
+
box.device.refer(playfield.samples);
|
|
97
|
+
box.file.refer(audioFile);
|
|
98
|
+
box.icon.setValue("drum");
|
|
99
|
+
box.index.setValue(midiNote);
|
|
100
|
+
});
|
|
101
|
+
boxGraph.endTransaction();
|
|
102
|
+
return { sample, audioFile };
|
|
103
|
+
};
|
|
104
|
+
const addTrack = (skeleton, audioUnit, trackType, index = 0) => {
|
|
105
|
+
const { boxGraph } = skeleton;
|
|
106
|
+
let trackBox;
|
|
107
|
+
boxGraph.beginTransaction();
|
|
108
|
+
trackBox = TrackBox.create(boxGraph, UUID.generate(), box => {
|
|
109
|
+
box.type.setValue(trackType);
|
|
110
|
+
box.tracks.refer(audioUnit.tracks);
|
|
111
|
+
box.target.refer(audioUnit);
|
|
112
|
+
box.index.setValue(index);
|
|
113
|
+
});
|
|
114
|
+
boxGraph.endTransaction();
|
|
115
|
+
return trackBox;
|
|
116
|
+
};
|
|
117
|
+
const addNoteRegion = (skeleton, trackBox, position, duration) => {
|
|
118
|
+
const { boxGraph } = skeleton;
|
|
119
|
+
let region;
|
|
120
|
+
boxGraph.beginTransaction();
|
|
121
|
+
const events = NoteEventCollectionBox.create(boxGraph, UUID.generate());
|
|
122
|
+
region = NoteRegionBox.create(boxGraph, UUID.generate(), box => {
|
|
123
|
+
box.regions.refer(trackBox.regions);
|
|
124
|
+
box.events.refer(events.owners);
|
|
125
|
+
box.position.setValue(position);
|
|
126
|
+
box.duration.setValue(duration);
|
|
127
|
+
});
|
|
128
|
+
boxGraph.endTransaction();
|
|
129
|
+
return region;
|
|
130
|
+
};
|
|
131
|
+
const addValueRegion = (skeleton, trackBox, position, duration) => {
|
|
132
|
+
const { boxGraph } = skeleton;
|
|
133
|
+
let region;
|
|
134
|
+
boxGraph.beginTransaction();
|
|
135
|
+
const events = ValueEventCollectionBox.create(boxGraph, UUID.generate());
|
|
136
|
+
region = ValueRegionBox.create(boxGraph, UUID.generate(), box => {
|
|
137
|
+
box.regions.refer(trackBox.regions);
|
|
138
|
+
box.events.refer(events.owners);
|
|
139
|
+
box.position.setValue(position);
|
|
140
|
+
box.duration.setValue(duration);
|
|
141
|
+
});
|
|
142
|
+
boxGraph.endTransaction();
|
|
143
|
+
return region;
|
|
144
|
+
};
|
|
145
|
+
const addAudioEffect = (skeleton, audioUnit, label, index) => {
|
|
146
|
+
const { boxGraph } = skeleton;
|
|
147
|
+
let effect;
|
|
148
|
+
boxGraph.beginTransaction();
|
|
149
|
+
effect = CompressorDeviceBox.create(boxGraph, UUID.generate(), box => {
|
|
150
|
+
box.label.setValue(label);
|
|
151
|
+
box.host.refer(audioUnit.audioEffects);
|
|
152
|
+
box.index.setValue(index);
|
|
153
|
+
});
|
|
154
|
+
boxGraph.endTransaction();
|
|
155
|
+
return effect;
|
|
156
|
+
};
|
|
157
|
+
const addWerkstattParam = (skeleton, paramsField, label, value, index) => {
|
|
158
|
+
const { boxGraph } = skeleton;
|
|
159
|
+
let param;
|
|
160
|
+
boxGraph.beginTransaction();
|
|
161
|
+
param = WerkstattParameterBox.create(boxGraph, UUID.generate(), box => {
|
|
162
|
+
box.owner.refer(paramsField);
|
|
163
|
+
box.label.setValue(label);
|
|
164
|
+
box.index.setValue(index);
|
|
165
|
+
box.value.setValue(value);
|
|
166
|
+
box.defaultValue.setValue(value);
|
|
167
|
+
});
|
|
168
|
+
boxGraph.endTransaction();
|
|
169
|
+
return param;
|
|
170
|
+
};
|
|
171
|
+
const addWerkstattSample = (skeleton, samplesField, label, fileName, index) => {
|
|
172
|
+
const { boxGraph } = skeleton;
|
|
173
|
+
let sampleBox;
|
|
174
|
+
let audioFile;
|
|
175
|
+
boxGraph.beginTransaction();
|
|
176
|
+
audioFile = AudioFileBox.create(boxGraph, UUID.generate(), box => {
|
|
177
|
+
box.fileName.setValue(fileName);
|
|
178
|
+
box.startInSeconds.setValue(0);
|
|
179
|
+
box.endInSeconds.setValue(1);
|
|
180
|
+
});
|
|
181
|
+
sampleBox = WerkstattSampleBox.create(boxGraph, UUID.generate(), box => {
|
|
182
|
+
box.owner.refer(samplesField);
|
|
183
|
+
box.label.setValue(label);
|
|
184
|
+
box.index.setValue(index);
|
|
185
|
+
box.file.refer(audioFile);
|
|
186
|
+
});
|
|
187
|
+
boxGraph.endTransaction();
|
|
188
|
+
return { sampleBox, audioFile };
|
|
189
|
+
};
|
|
190
|
+
// Mirrors the exact dependency collection logic from DevicesClipboardHandler.copyDevices
|
|
191
|
+
const collectDeviceDependencies = (deviceBox, boxGraph, audioUnit) => {
|
|
192
|
+
const ownedChildren = deviceBox.incomingEdges()
|
|
193
|
+
.filter(pointer => pointer.mandatory && !pointer.box.ephemeral
|
|
194
|
+
&& !isDefined(pointer.box.resource))
|
|
195
|
+
.map(pointer => pointer.box);
|
|
196
|
+
const mandatoryDeps = Array.from(boxGraph.dependenciesOf(deviceBox, {
|
|
197
|
+
alwaysFollowMandatory: true,
|
|
198
|
+
excludeBox: (dep) => dep.ephemeral || DeviceBoxUtils.isDeviceBox(dep)
|
|
199
|
+
|| dep.name === RootBox.ClassName
|
|
200
|
+
}).boxes).filter(dep => !isDefined(dep.resource));
|
|
201
|
+
const preserved = [deviceBox, ...ownedChildren].flatMap(root => Array.from(boxGraph.dependenciesOf(root, {
|
|
202
|
+
alwaysFollowMandatory: true,
|
|
203
|
+
excludeBox: (dep) => dep.ephemeral || DeviceBoxUtils.isDeviceBox(dep)
|
|
204
|
+
}).boxes).filter(dep => dep.resource === "preserved"));
|
|
205
|
+
const trackContent = [];
|
|
206
|
+
if (audioUnit !== undefined) {
|
|
207
|
+
const trackPointers = audioUnit.tracks.pointerHub.incoming();
|
|
208
|
+
const tracks = trackPointers
|
|
209
|
+
.filter(pointer => isInstanceOf(pointer.box, TrackBox))
|
|
210
|
+
.map(pointer => pointer.box);
|
|
211
|
+
for (const track of tracks) {
|
|
212
|
+
trackContent.push(track);
|
|
213
|
+
const regionPointers = track.regions.pointerHub.incoming();
|
|
214
|
+
for (const regionPointer of regionPointers) {
|
|
215
|
+
trackContent.push(regionPointer.box);
|
|
216
|
+
const regionDeps = Array.from(boxGraph.dependenciesOf(regionPointer.box, {
|
|
217
|
+
alwaysFollowMandatory: true,
|
|
218
|
+
excludeBox: (dep) => dep.ephemeral
|
|
219
|
+
|| isInstanceOf(dep, TrackBox)
|
|
220
|
+
|| DeviceBoxUtils.isDeviceBox(dep)
|
|
221
|
+
}).boxes);
|
|
222
|
+
trackContent.push(...regionDeps);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const seen = new Set();
|
|
227
|
+
return [...ownedChildren, ...mandatoryDeps, ...preserved, ...trackContent].filter(box => {
|
|
228
|
+
const uuid = UUID.toString(box.address.uuid);
|
|
229
|
+
if (seen.has(uuid))
|
|
230
|
+
return false;
|
|
231
|
+
seen.add(uuid);
|
|
232
|
+
return true;
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
const makePasteMapper = (targetAudioUnit, replaceInstrument) => ({
|
|
236
|
+
mapPointer: (pointer) => {
|
|
237
|
+
if (pointer.pointerType === Pointers.InstrumentHost && replaceInstrument) {
|
|
238
|
+
return Option.wrap(targetAudioUnit.input.address);
|
|
239
|
+
}
|
|
240
|
+
if (pointer.pointerType === Pointers.AudioEffectHost) {
|
|
241
|
+
return Option.wrap(targetAudioUnit.audioEffects.address);
|
|
242
|
+
}
|
|
243
|
+
if (pointer.pointerType === Pointers.MIDIEffectHost) {
|
|
244
|
+
return Option.wrap(targetAudioUnit.midiEffects.address);
|
|
245
|
+
}
|
|
246
|
+
if (pointer.pointerType === Pointers.TrackCollection && replaceInstrument) {
|
|
247
|
+
return Option.wrap(targetAudioUnit.tracks.address);
|
|
248
|
+
}
|
|
249
|
+
if (pointer.pointerType === Pointers.Automation && replaceInstrument) {
|
|
250
|
+
return Option.wrap(targetAudioUnit.address);
|
|
251
|
+
}
|
|
252
|
+
return Option.None;
|
|
253
|
+
},
|
|
254
|
+
excludeBox: (box) => !replaceInstrument && (DeviceBoxUtils.isInstrumentDeviceBox(box) || isInstanceOf(box, TrackBox))
|
|
255
|
+
});
|
|
256
|
+
// ─────────────────────────────────────────────────────────
|
|
257
|
+
// Audio effect paste
|
|
258
|
+
// ─────────────────────────────────────────────────────────
|
|
259
|
+
describe("paste audio effects", () => {
|
|
260
|
+
it("deserializes a single audio effect", () => {
|
|
261
|
+
const sourceAU = createAudioUnit(source);
|
|
262
|
+
const effect = addAudioEffect(source, sourceAU, "Compressor", 0);
|
|
263
|
+
const data = ClipboardUtils.serializeBoxes([effect]);
|
|
264
|
+
const targetAU = createAudioUnit(target);
|
|
265
|
+
const editing = new BoxEditing(target.boxGraph);
|
|
266
|
+
editing.modify(() => {
|
|
267
|
+
ClipboardUtils.deserializeBoxes(data, target.boxGraph, makePasteMapper(targetAU, false));
|
|
268
|
+
});
|
|
269
|
+
const pasted = targetAU.audioEffects.pointerHub.incoming();
|
|
270
|
+
expect(pasted.length).toBe(1);
|
|
271
|
+
expect(isInstanceOf(pasted[0].box, CompressorDeviceBox)).toBe(true);
|
|
272
|
+
});
|
|
273
|
+
it("deserializes multiple audio effects", () => {
|
|
274
|
+
const sourceAU = createAudioUnit(source);
|
|
275
|
+
const effectA = addAudioEffect(source, sourceAU, "Comp A", 0);
|
|
276
|
+
const effectB = addAudioEffect(source, sourceAU, "Comp B", 1);
|
|
277
|
+
const data = ClipboardUtils.serializeBoxes([effectA, effectB]);
|
|
278
|
+
const targetAU = createAudioUnit(target);
|
|
279
|
+
const editing = new BoxEditing(target.boxGraph);
|
|
280
|
+
editing.modify(() => {
|
|
281
|
+
ClipboardUtils.deserializeBoxes(data, target.boxGraph, makePasteMapper(targetAU, false));
|
|
282
|
+
});
|
|
283
|
+
expect(targetAU.audioEffects.pointerHub.incoming().length).toBe(2);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
// ─────────────────────────────────────────────────────────
|
|
287
|
+
// Instrument paste
|
|
288
|
+
// ─────────────────────────────────────────────────────────
|
|
289
|
+
describe("paste instrument", () => {
|
|
290
|
+
it("pastes instrument when replaceInstrument is true", () => {
|
|
291
|
+
const sourceAU = createAudioUnit(source);
|
|
292
|
+
addTapeInstrument(source, sourceAU, "Source Tape");
|
|
293
|
+
const sourceInstrument = sourceAU.input.pointerHub.incoming()[0].box;
|
|
294
|
+
const data = ClipboardUtils.serializeBoxes([sourceInstrument]);
|
|
295
|
+
const targetAU = createAudioUnit(target);
|
|
296
|
+
const editing = new BoxEditing(target.boxGraph);
|
|
297
|
+
editing.modify(() => {
|
|
298
|
+
ClipboardUtils.deserializeBoxes(data, target.boxGraph, makePasteMapper(targetAU, true));
|
|
299
|
+
});
|
|
300
|
+
expect(targetAU.input.pointerHub.incoming().length).toBe(1);
|
|
301
|
+
});
|
|
302
|
+
it("excludes instrument when replaceInstrument is false", () => {
|
|
303
|
+
const sourceAU = createAudioUnit(source);
|
|
304
|
+
addTapeInstrument(source, sourceAU, "Source Tape");
|
|
305
|
+
const sourceInstrument = sourceAU.input.pointerHub.incoming()[0].box;
|
|
306
|
+
const data = ClipboardUtils.serializeBoxes([sourceInstrument]);
|
|
307
|
+
const targetAU = createAudioUnit(target);
|
|
308
|
+
addTapeInstrument(target, targetAU, "Existing Tape");
|
|
309
|
+
const editing = new BoxEditing(target.boxGraph);
|
|
310
|
+
editing.modify(() => {
|
|
311
|
+
ClipboardUtils.deserializeBoxes(data, target.boxGraph, makePasteMapper(targetAU, false));
|
|
312
|
+
});
|
|
313
|
+
const inputs = targetAU.input.pointerHub.incoming();
|
|
314
|
+
expect(inputs.length).toBe(1);
|
|
315
|
+
expect(inputs[0].box.label.getValue()).toBe("Existing Tape");
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
// ─────────────────────────────────────────────────────────
|
|
319
|
+
// TrackBox exclusion
|
|
320
|
+
// ─────────────────────────────────────────────────────────
|
|
321
|
+
describe("TrackBox exclusion", () => {
|
|
322
|
+
it("does not collect TrackBox as device dependency", () => {
|
|
323
|
+
const audioUnit = createAudioUnit(source);
|
|
324
|
+
const instrument = addTapeInstrument(source, audioUnit, "Test");
|
|
325
|
+
addTrack(source, audioUnit, TrackType.Notes, 0);
|
|
326
|
+
addTrack(source, audioUnit, TrackType.Value, 1);
|
|
327
|
+
const deps = collectDeviceDependencies(instrument, source.boxGraph);
|
|
328
|
+
expect(deps.filter(box => isInstanceOf(box, TrackBox)).length).toBe(0);
|
|
329
|
+
});
|
|
330
|
+
it("does not collect note regions from tracks", () => {
|
|
331
|
+
const audioUnit = createAudioUnit(source);
|
|
332
|
+
const instrument = addTapeInstrument(source, audioUnit, "Tape");
|
|
333
|
+
const noteTrack = addTrack(source, audioUnit, TrackType.Notes, 0);
|
|
334
|
+
addNoteRegion(source, noteTrack, 0, 480);
|
|
335
|
+
addNoteRegion(source, noteTrack, 480, 480);
|
|
336
|
+
expect(noteTrack.regions.pointerHub.incoming().length).toBe(2);
|
|
337
|
+
const deps = collectDeviceDependencies(instrument, source.boxGraph);
|
|
338
|
+
const allBoxes = [instrument, ...deps];
|
|
339
|
+
expect(allBoxes.filter(box => isInstanceOf(box, TrackBox)).length).toBe(0);
|
|
340
|
+
expect(allBoxes.filter(box => isInstanceOf(box, NoteRegionBox)).length).toBe(0);
|
|
341
|
+
expect(allBoxes.filter(box => isInstanceOf(box, NoteEventCollectionBox)).length).toBe(0);
|
|
342
|
+
});
|
|
343
|
+
it("does not collect value regions from automation tracks", () => {
|
|
344
|
+
const audioUnit = createAudioUnit(source);
|
|
345
|
+
const instrument = addTapeInstrument(source, audioUnit, "Tape");
|
|
346
|
+
const autoTrack = addTrack(source, audioUnit, TrackType.Value, 0);
|
|
347
|
+
addValueRegion(source, autoTrack, 0, 960);
|
|
348
|
+
const deps = collectDeviceDependencies(instrument, source.boxGraph);
|
|
349
|
+
expect(deps.filter(box => isInstanceOf(box, TrackBox)).length).toBe(0);
|
|
350
|
+
expect(deps.filter(box => isInstanceOf(box, ValueRegionBox)).length).toBe(0);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
// ─────────────────────────────────────────────────────────
|
|
354
|
+
// Werkstatt/Apparat owned children
|
|
355
|
+
// ─────────────────────────────────────────────────────────
|
|
356
|
+
describe("Werkstatt/Apparat owned children", () => {
|
|
357
|
+
it("collects WerkstattParameterBox as owned child", () => {
|
|
358
|
+
const audioUnit = createAudioUnit(source);
|
|
359
|
+
const apparat = addApparatInstrument(source, audioUnit, "Apparat");
|
|
360
|
+
addWerkstattParam(source, apparat.parameters, "cutoff", 0.5, 0);
|
|
361
|
+
addWerkstattParam(source, apparat.parameters, "resonance", 0.3, 1);
|
|
362
|
+
const deps = collectDeviceDependencies(apparat, source.boxGraph);
|
|
363
|
+
expect(deps.filter(box => isInstanceOf(box, WerkstattParameterBox)).length).toBe(2);
|
|
364
|
+
});
|
|
365
|
+
it("collects WerkstattSampleBox as owned child", () => {
|
|
366
|
+
const audioUnit = createAudioUnit(source);
|
|
367
|
+
const apparat = addApparatInstrument(source, audioUnit, "Apparat");
|
|
368
|
+
addWerkstattSample(source, apparat.samples, "kick", "kick.wav", 0);
|
|
369
|
+
const deps = collectDeviceDependencies(apparat, source.boxGraph);
|
|
370
|
+
expect(deps.filter(box => isInstanceOf(box, WerkstattSampleBox)).length).toBe(1);
|
|
371
|
+
});
|
|
372
|
+
it("collects AudioFileBox referenced by WerkstattSampleBox", () => {
|
|
373
|
+
const audioUnit = createAudioUnit(source);
|
|
374
|
+
const apparat = addApparatInstrument(source, audioUnit, "Apparat");
|
|
375
|
+
addWerkstattSample(source, apparat.samples, "grain", "grain.wav", 0);
|
|
376
|
+
const deps = collectDeviceDependencies(apparat, source.boxGraph);
|
|
377
|
+
const allBoxes = [apparat, ...deps];
|
|
378
|
+
expect(allBoxes.filter(box => isInstanceOf(box, WerkstattSampleBox)).length).toBe(1);
|
|
379
|
+
expect(allBoxes.filter(box => isInstanceOf(box, AudioFileBox)).length).toBe(1);
|
|
380
|
+
});
|
|
381
|
+
it("collects multiple parameters and samples together", () => {
|
|
382
|
+
const audioUnit = createAudioUnit(source);
|
|
383
|
+
const apparat = addApparatInstrument(source, audioUnit, "Apparat");
|
|
384
|
+
addWerkstattParam(source, apparat.parameters, "cutoff", 0.5, 0);
|
|
385
|
+
addWerkstattParam(source, apparat.parameters, "resonance", 0.3, 1);
|
|
386
|
+
addWerkstattSample(source, apparat.samples, "kick", "kick.wav", 0);
|
|
387
|
+
addWerkstattSample(source, apparat.samples, "snare", "snare.wav", 1);
|
|
388
|
+
const deps = collectDeviceDependencies(apparat, source.boxGraph);
|
|
389
|
+
expect(deps.filter(box => isInstanceOf(box, WerkstattParameterBox)).length).toBe(2);
|
|
390
|
+
expect(deps.filter(box => isInstanceOf(box, WerkstattSampleBox)).length).toBe(2);
|
|
391
|
+
expect(deps.filter(box => isInstanceOf(box, AudioFileBox)).length).toBe(2);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
// ─────────────────────────────────────────────────────────
|
|
395
|
+
// Playfield sample collection
|
|
396
|
+
// ─────────────────────────────────────────────────────────
|
|
397
|
+
describe("Playfield sample collection", () => {
|
|
398
|
+
it("PlayfieldSampleBox is tagged as device", () => {
|
|
399
|
+
const audioUnit = createAudioUnit(source);
|
|
400
|
+
const playfield = addPlayfieldInstrument(source, audioUnit, "Playfield");
|
|
401
|
+
const { sample } = addPlayfieldSample(source, playfield, "kick.wav", 36);
|
|
402
|
+
expect(DeviceBoxUtils.isDeviceBox(sample)).toBe(true);
|
|
403
|
+
});
|
|
404
|
+
it("collects PlayfieldSampleBox as owned child despite device tags", () => {
|
|
405
|
+
const audioUnit = createAudioUnit(source);
|
|
406
|
+
const playfield = addPlayfieldInstrument(source, audioUnit, "Playfield");
|
|
407
|
+
addPlayfieldSample(source, playfield, "kick.wav", 36);
|
|
408
|
+
const deps = collectDeviceDependencies(playfield, source.boxGraph);
|
|
409
|
+
expect(deps.filter(box => isInstanceOf(box, PlayfieldSampleBox)).length).toBe(1);
|
|
410
|
+
});
|
|
411
|
+
it("collects all PlayfieldSampleBoxes with multiple samples", () => {
|
|
412
|
+
const audioUnit = createAudioUnit(source);
|
|
413
|
+
const playfield = addPlayfieldInstrument(source, audioUnit, "Playfield");
|
|
414
|
+
addPlayfieldSample(source, playfield, "kick.wav", 36);
|
|
415
|
+
addPlayfieldSample(source, playfield, "snare.wav", 38);
|
|
416
|
+
addPlayfieldSample(source, playfield, "hihat.wav", 42);
|
|
417
|
+
const deps = collectDeviceDependencies(playfield, source.boxGraph);
|
|
418
|
+
expect(deps.filter(box => isInstanceOf(box, PlayfieldSampleBox)).length).toBe(3);
|
|
419
|
+
});
|
|
420
|
+
it("collects AudioFileBox for each PlayfieldSampleBox", () => {
|
|
421
|
+
const audioUnit = createAudioUnit(source);
|
|
422
|
+
const playfield = addPlayfieldInstrument(source, audioUnit, "Playfield");
|
|
423
|
+
addPlayfieldSample(source, playfield, "kick.wav", 36);
|
|
424
|
+
addPlayfieldSample(source, playfield, "snare.wav", 38);
|
|
425
|
+
const deps = collectDeviceDependencies(playfield, source.boxGraph);
|
|
426
|
+
const allBoxes = [playfield, ...deps];
|
|
427
|
+
expect(allBoxes.filter(box => isInstanceOf(box, PlayfieldSampleBox)).length).toBe(2);
|
|
428
|
+
expect(allBoxes.filter(box => isInstanceOf(box, AudioFileBox)).length).toBe(2);
|
|
429
|
+
});
|
|
430
|
+
it("shares AudioFileBox when two samples reference the same file", () => {
|
|
431
|
+
const audioUnit = createAudioUnit(source);
|
|
432
|
+
const playfield = addPlayfieldInstrument(source, audioUnit, "Playfield");
|
|
433
|
+
const { audioFile: sharedFile } = addPlayfieldSample(source, playfield, "kick.wav", 36);
|
|
434
|
+
source.boxGraph.beginTransaction();
|
|
435
|
+
PlayfieldSampleBox.create(source.boxGraph, UUID.generate(), box => {
|
|
436
|
+
box.device.refer(playfield.samples);
|
|
437
|
+
box.file.refer(sharedFile);
|
|
438
|
+
box.icon.setValue("drum");
|
|
439
|
+
box.index.setValue(48);
|
|
440
|
+
});
|
|
441
|
+
source.boxGraph.endTransaction();
|
|
442
|
+
const deps = collectDeviceDependencies(playfield, source.boxGraph);
|
|
443
|
+
const allBoxes = [playfield, ...deps];
|
|
444
|
+
expect(allBoxes.filter(box => isInstanceOf(box, PlayfieldSampleBox)).length).toBe(2);
|
|
445
|
+
expect(allBoxes.filter(box => isInstanceOf(box, AudioFileBox)).length).toBe(1);
|
|
446
|
+
});
|
|
447
|
+
it("clipboard contains device + samples + audio files", () => {
|
|
448
|
+
const audioUnit = createAudioUnit(source);
|
|
449
|
+
const playfield = addPlayfieldInstrument(source, audioUnit, "Playfield");
|
|
450
|
+
addPlayfieldSample(source, playfield, "kick.wav", 36);
|
|
451
|
+
addPlayfieldSample(source, playfield, "snare.wav", 38);
|
|
452
|
+
addPlayfieldSample(source, playfield, "hihat.wav", 42);
|
|
453
|
+
const deps = collectDeviceDependencies(playfield, source.boxGraph);
|
|
454
|
+
const allBoxes = [playfield, ...deps];
|
|
455
|
+
expect(allBoxes.length).toBe(1 + 3 + 3);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
// ─────────────────────────────────────────────────────────
|
|
459
|
+
// Track re-indexing after instrument replacement
|
|
460
|
+
// ─────────────────────────────────────────────────────────
|
|
461
|
+
describe("track re-indexing after instrument replacement", () => {
|
|
462
|
+
const reindexSurvivingTracks = (audioUnit) => {
|
|
463
|
+
const surviving = audioUnit.tracks.pointerHub.filter(Pointers.TrackCollection)
|
|
464
|
+
.filter(pointer => isInstanceOf(pointer.box, TrackBox))
|
|
465
|
+
.map(pointer => pointer.box)
|
|
466
|
+
.sort((trackA, trackB) => trackA.index.getValue() - trackB.index.getValue());
|
|
467
|
+
surviving.forEach((track, idx) => track.index.setValue(idx));
|
|
468
|
+
};
|
|
469
|
+
const getTrackIndices = (audioUnit) => audioUnit.tracks.pointerHub.filter(Pointers.TrackCollection)
|
|
470
|
+
.filter(pointer => isInstanceOf(pointer.box, TrackBox))
|
|
471
|
+
.map(pointer => pointer.box.index.getValue())
|
|
472
|
+
.sort();
|
|
473
|
+
it("leaves gap when middle track is deleted without re-indexing", () => {
|
|
474
|
+
const audioUnit = createAudioUnit(source);
|
|
475
|
+
addTapeInstrument(source, audioUnit, "Test");
|
|
476
|
+
addTrack(source, audioUnit, TrackType.Notes, 0);
|
|
477
|
+
const middle = addTrack(source, audioUnit, TrackType.Notes, 1);
|
|
478
|
+
addTrack(source, audioUnit, TrackType.Notes, 2);
|
|
479
|
+
source.boxGraph.beginTransaction();
|
|
480
|
+
middle.delete();
|
|
481
|
+
source.boxGraph.endTransaction();
|
|
482
|
+
expect(getTrackIndices(audioUnit)).toEqual([0, 2]);
|
|
483
|
+
});
|
|
484
|
+
it("re-indexes to contiguous after middle track deletion", () => {
|
|
485
|
+
const audioUnit = createAudioUnit(source);
|
|
486
|
+
addTapeInstrument(source, audioUnit, "Test");
|
|
487
|
+
const track0 = addTrack(source, audioUnit, TrackType.Notes, 0);
|
|
488
|
+
const track1 = addTrack(source, audioUnit, TrackType.Notes, 1);
|
|
489
|
+
const track2 = addTrack(source, audioUnit, TrackType.Notes, 2);
|
|
490
|
+
source.boxGraph.beginTransaction();
|
|
491
|
+
track1.delete();
|
|
492
|
+
reindexSurvivingTracks(audioUnit);
|
|
493
|
+
source.boxGraph.endTransaction();
|
|
494
|
+
expect(getTrackIndices(audioUnit)).toEqual([0, 1]);
|
|
495
|
+
expect(track0.index.getValue()).toBe(0);
|
|
496
|
+
expect(track2.index.getValue()).toBe(1);
|
|
497
|
+
});
|
|
498
|
+
it("re-indexes after deleting multiple non-adjacent tracks", () => {
|
|
499
|
+
const audioUnit = createAudioUnit(source);
|
|
500
|
+
addTapeInstrument(source, audioUnit, "Test");
|
|
501
|
+
const track0 = addTrack(source, audioUnit, TrackType.Notes, 0);
|
|
502
|
+
const track1 = addTrack(source, audioUnit, TrackType.Notes, 1);
|
|
503
|
+
const track2 = addTrack(source, audioUnit, TrackType.Notes, 2);
|
|
504
|
+
const track3 = addTrack(source, audioUnit, TrackType.Notes, 3);
|
|
505
|
+
source.boxGraph.beginTransaction();
|
|
506
|
+
track1.delete();
|
|
507
|
+
track3.delete();
|
|
508
|
+
reindexSurvivingTracks(audioUnit);
|
|
509
|
+
source.boxGraph.endTransaction();
|
|
510
|
+
expect(getTrackIndices(audioUnit)).toEqual([0, 1]);
|
|
511
|
+
expect(track0.index.getValue()).toBe(0);
|
|
512
|
+
expect(track2.index.getValue()).toBe(1);
|
|
513
|
+
});
|
|
514
|
+
it("no-op when no tracks are deleted", () => {
|
|
515
|
+
const audioUnit = createAudioUnit(source);
|
|
516
|
+
addTapeInstrument(source, audioUnit, "Test");
|
|
517
|
+
addTrack(source, audioUnit, TrackType.Notes, 0);
|
|
518
|
+
addTrack(source, audioUnit, TrackType.Notes, 1);
|
|
519
|
+
source.boxGraph.beginTransaction();
|
|
520
|
+
reindexSurvivingTracks(audioUnit);
|
|
521
|
+
source.boxGraph.endTransaction();
|
|
522
|
+
expect(getTrackIndices(audioUnit)).toEqual([0, 1]);
|
|
523
|
+
});
|
|
524
|
+
it("handles all tracks deleted", () => {
|
|
525
|
+
const audioUnit = createAudioUnit(source);
|
|
526
|
+
addTapeInstrument(source, audioUnit, "Test");
|
|
527
|
+
const track0 = addTrack(source, audioUnit, TrackType.Notes, 0);
|
|
528
|
+
const track1 = addTrack(source, audioUnit, TrackType.Notes, 1);
|
|
529
|
+
source.boxGraph.beginTransaction();
|
|
530
|
+
track0.delete();
|
|
531
|
+
track1.delete();
|
|
532
|
+
reindexSurvivingTracks(audioUnit);
|
|
533
|
+
source.boxGraph.endTransaction();
|
|
534
|
+
expect(getTrackIndices(audioUnit)).toEqual([]);
|
|
535
|
+
});
|
|
536
|
+
it("re-indexes after deleting first track", () => {
|
|
537
|
+
const audioUnit = createAudioUnit(source);
|
|
538
|
+
addTapeInstrument(source, audioUnit, "Test");
|
|
539
|
+
const track0 = addTrack(source, audioUnit, TrackType.Notes, 0);
|
|
540
|
+
const track1 = addTrack(source, audioUnit, TrackType.Notes, 1);
|
|
541
|
+
const track2 = addTrack(source, audioUnit, TrackType.Notes, 2);
|
|
542
|
+
source.boxGraph.beginTransaction();
|
|
543
|
+
track0.delete();
|
|
544
|
+
reindexSurvivingTracks(audioUnit);
|
|
545
|
+
source.boxGraph.endTransaction();
|
|
546
|
+
expect(getTrackIndices(audioUnit)).toEqual([0, 1]);
|
|
547
|
+
expect(track1.index.getValue()).toBe(0);
|
|
548
|
+
expect(track2.index.getValue()).toBe(1);
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
// ─────────────────────────────────────────────────────────
|
|
552
|
+
// Full instrument copy: tracks + regions + events
|
|
553
|
+
// ─────────────────────────────────────────────────────────
|
|
554
|
+
describe("full instrument copy: tracks, regions, events", () => {
|
|
555
|
+
it("collects automation track targeting instrument as ownedChild", () => {
|
|
556
|
+
const audioUnit = createAudioUnit(source);
|
|
557
|
+
const vaporisateur = addVaporisateur(source, audioUnit, "Vapo");
|
|
558
|
+
addAutomationTrack(source, audioUnit, vaporisateur.cutoff, 0);
|
|
559
|
+
const deps = collectDeviceDependencies(vaporisateur, source.boxGraph, audioUnit);
|
|
560
|
+
expect(deps.filter(box => isInstanceOf(box, TrackBox)).length).toBe(1);
|
|
561
|
+
});
|
|
562
|
+
it("collects multiple automation tracks targeting different parameters", () => {
|
|
563
|
+
const audioUnit = createAudioUnit(source);
|
|
564
|
+
const vaporisateur = addVaporisateur(source, audioUnit, "Vapo");
|
|
565
|
+
addAutomationTrack(source, audioUnit, vaporisateur.cutoff, 0);
|
|
566
|
+
addAutomationTrack(source, audioUnit, vaporisateur.resonance, 1);
|
|
567
|
+
const deps = collectDeviceDependencies(vaporisateur, source.boxGraph, audioUnit);
|
|
568
|
+
expect(deps.filter(box => isInstanceOf(box, TrackBox)).length).toBe(2);
|
|
569
|
+
});
|
|
570
|
+
it("collects note track (targets AudioUnitBox)", () => {
|
|
571
|
+
const audioUnit = createAudioUnit(source);
|
|
572
|
+
const vaporisateur = addVaporisateur(source, audioUnit, "Vapo");
|
|
573
|
+
addTrack(source, audioUnit, TrackType.Notes, 0);
|
|
574
|
+
const deps = collectDeviceDependencies(vaporisateur, source.boxGraph, audioUnit);
|
|
575
|
+
expect(deps.filter(box => isInstanceOf(box, TrackBox)).length).toBe(1);
|
|
576
|
+
});
|
|
577
|
+
it("collects note track with its regions and event collections", () => {
|
|
578
|
+
const audioUnit = createAudioUnit(source);
|
|
579
|
+
const vaporisateur = addVaporisateur(source, audioUnit, "Vapo");
|
|
580
|
+
const noteTrack = addTrack(source, audioUnit, TrackType.Notes, 0);
|
|
581
|
+
addNoteRegion(source, noteTrack, 0, 480);
|
|
582
|
+
addNoteRegion(source, noteTrack, 480, 480);
|
|
583
|
+
const deps = collectDeviceDependencies(vaporisateur, source.boxGraph, audioUnit);
|
|
584
|
+
const allBoxes = [vaporisateur, ...deps];
|
|
585
|
+
expect(allBoxes.filter(box => isInstanceOf(box, TrackBox)).length).toBe(1);
|
|
586
|
+
expect(allBoxes.filter(box => isInstanceOf(box, NoteRegionBox)).length).toBe(2);
|
|
587
|
+
expect(allBoxes.filter(box => isInstanceOf(box, NoteEventCollectionBox)).length).toBe(2);
|
|
588
|
+
});
|
|
589
|
+
it("collects automation track with its value regions and event collections", () => {
|
|
590
|
+
const audioUnit = createAudioUnit(source);
|
|
591
|
+
const vaporisateur = addVaporisateur(source, audioUnit, "Vapo");
|
|
592
|
+
const autoTrack = addAutomationTrack(source, audioUnit, vaporisateur.cutoff, 0);
|
|
593
|
+
addValueRegion(source, autoTrack, 0, 960);
|
|
594
|
+
addValueRegion(source, autoTrack, 960, 960);
|
|
595
|
+
const deps = collectDeviceDependencies(vaporisateur, source.boxGraph, audioUnit);
|
|
596
|
+
const allBoxes = [vaporisateur, ...deps];
|
|
597
|
+
expect(allBoxes.filter(box => isInstanceOf(box, TrackBox)).length).toBe(1);
|
|
598
|
+
expect(allBoxes.filter(box => isInstanceOf(box, ValueRegionBox)).length).toBe(2);
|
|
599
|
+
expect(allBoxes.filter(box => isInstanceOf(box, ValueEventCollectionBox)).length).toBe(2);
|
|
600
|
+
});
|
|
601
|
+
it("collects both note track and automation track", () => {
|
|
602
|
+
const audioUnit = createAudioUnit(source);
|
|
603
|
+
const vaporisateur = addVaporisateur(source, audioUnit, "Vapo");
|
|
604
|
+
addTrack(source, audioUnit, TrackType.Notes, 0);
|
|
605
|
+
addAutomationTrack(source, audioUnit, vaporisateur.cutoff, 1);
|
|
606
|
+
const deps = collectDeviceDependencies(vaporisateur, source.boxGraph, audioUnit);
|
|
607
|
+
expect(deps.filter(box => isInstanceOf(box, TrackBox)).length).toBe(2);
|
|
608
|
+
});
|
|
609
|
+
it("collects complete instrument with 2 tracks, 4 regions, 4 event collections", () => {
|
|
610
|
+
const audioUnit = createAudioUnit(source);
|
|
611
|
+
const vaporisateur = addVaporisateur(source, audioUnit, "Vapo");
|
|
612
|
+
const noteTrack = addTrack(source, audioUnit, TrackType.Notes, 0);
|
|
613
|
+
addNoteRegion(source, noteTrack, 0, 480);
|
|
614
|
+
addNoteRegion(source, noteTrack, 480, 480);
|
|
615
|
+
const autoTrack = addAutomationTrack(source, audioUnit, vaporisateur.cutoff, 1);
|
|
616
|
+
addValueRegion(source, autoTrack, 0, 960);
|
|
617
|
+
addValueRegion(source, autoTrack, 960, 960);
|
|
618
|
+
const deps = collectDeviceDependencies(vaporisateur, source.boxGraph, audioUnit);
|
|
619
|
+
const allBoxes = [vaporisateur, ...deps];
|
|
620
|
+
expect(allBoxes.filter(box => isInstanceOf(box, VaporisateurDeviceBox)).length).toBe(1);
|
|
621
|
+
expect(allBoxes.filter(box => isInstanceOf(box, TrackBox)).length).toBe(2);
|
|
622
|
+
expect(allBoxes.filter(box => isInstanceOf(box, NoteRegionBox)).length).toBe(2);
|
|
623
|
+
expect(allBoxes.filter(box => isInstanceOf(box, NoteEventCollectionBox)).length).toBe(2);
|
|
624
|
+
expect(allBoxes.filter(box => isInstanceOf(box, ValueRegionBox)).length).toBe(2);
|
|
625
|
+
expect(allBoxes.filter(box => isInstanceOf(box, ValueEventCollectionBox)).length).toBe(2);
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
// ─────────────────────────────────────────────────────────
|
|
629
|
+
// Paste replace: automation track override (no duplicates)
|
|
630
|
+
// ─────────────────────────────────────────────────────────
|
|
631
|
+
describe("paste replace automation override", () => {
|
|
632
|
+
it("paste-replace creates new automation track from clipboard", () => {
|
|
633
|
+
const sourceAU = createAudioUnit(source);
|
|
634
|
+
const sourceVapo = addVaporisateur(source, sourceAU, "Source Vapo");
|
|
635
|
+
addAutomationTrack(source, sourceAU, sourceVapo.cutoff, 0);
|
|
636
|
+
const deps = collectDeviceDependencies(sourceVapo, source.boxGraph, sourceAU);
|
|
637
|
+
const allSourceBoxes = [sourceVapo, ...deps];
|
|
638
|
+
const data = ClipboardUtils.serializeBoxes(allSourceBoxes);
|
|
639
|
+
const targetAU = createAudioUnit(target);
|
|
640
|
+
target.boxGraph.beginTransaction();
|
|
641
|
+
const boxes = ClipboardUtils.deserializeBoxes(data, target.boxGraph, makePasteMapper(targetAU, true));
|
|
642
|
+
target.boxGraph.endTransaction();
|
|
643
|
+
const pastedTracks = boxes.filter(box => isInstanceOf(box, TrackBox));
|
|
644
|
+
expect(pastedTracks.length).toBe(1);
|
|
645
|
+
const pastedInstruments = boxes.filter(box => isInstanceOf(box, VaporisateurDeviceBox));
|
|
646
|
+
expect(pastedInstruments.length).toBe(1);
|
|
647
|
+
});
|
|
648
|
+
it("excludes automation tracks when replaceInstrument is false", () => {
|
|
649
|
+
const sourceAU = createAudioUnit(source);
|
|
650
|
+
const sourceVapo = addVaporisateur(source, sourceAU, "Source Vapo");
|
|
651
|
+
addAutomationTrack(source, sourceAU, sourceVapo.cutoff, 0);
|
|
652
|
+
const deps = collectDeviceDependencies(sourceVapo, source.boxGraph, sourceAU);
|
|
653
|
+
const allSourceBoxes = [sourceVapo, ...deps];
|
|
654
|
+
const data = ClipboardUtils.serializeBoxes(allSourceBoxes);
|
|
655
|
+
const targetAU = createAudioUnit(target);
|
|
656
|
+
addVaporisateur(target, targetAU, "Existing Vapo");
|
|
657
|
+
target.boxGraph.beginTransaction();
|
|
658
|
+
const boxes = ClipboardUtils.deserializeBoxes(data, target.boxGraph, makePasteMapper(targetAU, false));
|
|
659
|
+
target.boxGraph.endTransaction();
|
|
660
|
+
const pastedTracks = boxes.filter(box => isInstanceOf(box, TrackBox));
|
|
661
|
+
expect(pastedTracks.length).toBe(0);
|
|
662
|
+
const pastedInstruments = boxes.filter(box => isInstanceOf(box, VaporisateurDeviceBox));
|
|
663
|
+
expect(pastedInstruments.length).toBe(0);
|
|
664
|
+
});
|
|
665
|
+
it("parameter cannot have two automation tracks after paste-replace", () => {
|
|
666
|
+
const sourceAU = createAudioUnit(source);
|
|
667
|
+
const sourceVapo = addVaporisateur(source, sourceAU, "Source Vapo");
|
|
668
|
+
addAutomationTrack(source, sourceAU, sourceVapo.cutoff, 0);
|
|
669
|
+
const deps = collectDeviceDependencies(sourceVapo, source.boxGraph, sourceAU);
|
|
670
|
+
const data = ClipboardUtils.serializeBoxes([sourceVapo, ...deps]);
|
|
671
|
+
const targetAU = createAudioUnit(target);
|
|
672
|
+
const targetVapo = addVaporisateur(target, targetAU, "Target Vapo");
|
|
673
|
+
addAutomationTrack(target, targetAU, targetVapo.cutoff, 0);
|
|
674
|
+
const oldTrackCount = targetAU.tracks.pointerHub.filter(Pointers.TrackCollection)
|
|
675
|
+
.filter(pointer => isInstanceOf(pointer.box, TrackBox)).length;
|
|
676
|
+
expect(oldTrackCount).toBe(1);
|
|
677
|
+
target.boxGraph.beginTransaction();
|
|
678
|
+
const oldUuid = targetVapo.address.uuid;
|
|
679
|
+
for (const pointer of targetAU.tracks.pointerHub.filter(Pointers.TrackCollection)) {
|
|
680
|
+
if (isInstanceOf(pointer.box, TrackBox)) {
|
|
681
|
+
pointer.box.target.targetVertex.ifSome(targetVertex => {
|
|
682
|
+
if (UUID.equals(targetVertex.box.address.uuid, oldUuid)) {
|
|
683
|
+
pointer.box.delete();
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
targetVapo.delete();
|
|
689
|
+
const boxes = ClipboardUtils.deserializeBoxes(data, target.boxGraph, makePasteMapper(targetAU, true));
|
|
690
|
+
target.boxGraph.endTransaction();
|
|
691
|
+
const newTracks = boxes.filter(box => isInstanceOf(box, TrackBox));
|
|
692
|
+
expect(newTracks.length).toBe(1);
|
|
693
|
+
const allTracks = targetAU.tracks.pointerHub.filter(Pointers.TrackCollection)
|
|
694
|
+
.filter(pointer => isInstanceOf(pointer.box, TrackBox));
|
|
695
|
+
expect(allTracks.length).toBe(1);
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
// ─────────────────────────────────────────────────────────
|
|
699
|
+
// Paste note track target remapping (Pointers.Automation)
|
|
700
|
+
// ─────────────────────────────────────────────────────────
|
|
701
|
+
describe("paste note track target remapping", () => {
|
|
702
|
+
it("remaps note track target (Pointers.Automation) to target AudioUnitBox", () => {
|
|
703
|
+
const sourceAU = createAudioUnit(source);
|
|
704
|
+
const sourceVapo = addVaporisateur(source, sourceAU, "Source");
|
|
705
|
+
addTrack(source, sourceAU, TrackType.Notes, 0);
|
|
706
|
+
const deps = collectDeviceDependencies(sourceVapo, source.boxGraph, sourceAU);
|
|
707
|
+
const data = ClipboardUtils.serializeBoxes([sourceVapo, ...deps]);
|
|
708
|
+
const targetAU = createAudioUnit(target);
|
|
709
|
+
const editing = new BoxEditing(target.boxGraph);
|
|
710
|
+
editing.modify(() => {
|
|
711
|
+
ClipboardUtils.deserializeBoxes(data, target.boxGraph, makePasteMapper(targetAU, true));
|
|
712
|
+
});
|
|
713
|
+
const pastedTracks = targetAU.tracks.pointerHub.incoming()
|
|
714
|
+
.filter(pointer => isInstanceOf(pointer.box, TrackBox));
|
|
715
|
+
expect(pastedTracks.length).toBe(1);
|
|
716
|
+
});
|
|
717
|
+
it("remaps note track + automation track targets on paste-replace", () => {
|
|
718
|
+
const sourceAU = createAudioUnit(source);
|
|
719
|
+
const sourceVapo = addVaporisateur(source, sourceAU, "Source");
|
|
720
|
+
addTrack(source, sourceAU, TrackType.Notes, 0);
|
|
721
|
+
addAutomationTrack(source, sourceAU, sourceVapo.cutoff, 1);
|
|
722
|
+
const deps = collectDeviceDependencies(sourceVapo, source.boxGraph, sourceAU);
|
|
723
|
+
const data = ClipboardUtils.serializeBoxes([sourceVapo, ...deps]);
|
|
724
|
+
const targetAU = createAudioUnit(target);
|
|
725
|
+
const editing = new BoxEditing(target.boxGraph);
|
|
726
|
+
editing.modify(() => {
|
|
727
|
+
ClipboardUtils.deserializeBoxes(data, target.boxGraph, makePasteMapper(targetAU, true));
|
|
728
|
+
});
|
|
729
|
+
const pastedTracks = targetAU.tracks.pointerHub.incoming()
|
|
730
|
+
.filter(pointer => isInstanceOf(pointer.box, TrackBox));
|
|
731
|
+
expect(pastedTracks.length).toBe(2);
|
|
732
|
+
});
|
|
733
|
+
it("note track with regions pastes without crash via BoxEditing.modify", () => {
|
|
734
|
+
const sourceAU = createAudioUnit(source);
|
|
735
|
+
const sourceVapo = addVaporisateur(source, sourceAU, "Source");
|
|
736
|
+
const noteTrack = addTrack(source, sourceAU, TrackType.Notes, 0);
|
|
737
|
+
addNoteRegion(source, noteTrack, 0, 480);
|
|
738
|
+
addNoteRegion(source, noteTrack, 480, 480);
|
|
739
|
+
addAutomationTrack(source, sourceAU, sourceVapo.cutoff, 1);
|
|
740
|
+
const deps = collectDeviceDependencies(sourceVapo, source.boxGraph, sourceAU);
|
|
741
|
+
const data = ClipboardUtils.serializeBoxes([sourceVapo, ...deps]);
|
|
742
|
+
const targetAU = createAudioUnit(target);
|
|
743
|
+
const editing = new BoxEditing(target.boxGraph);
|
|
744
|
+
expect(() => {
|
|
745
|
+
editing.modify(() => {
|
|
746
|
+
ClipboardUtils.deserializeBoxes(data, target.boxGraph, makePasteMapper(targetAU, true));
|
|
747
|
+
});
|
|
748
|
+
}).not.toThrow();
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
// ─────────────────────────────────────────────────────────
|
|
752
|
+
// Paste into target without selected instrument
|
|
753
|
+
// ─────────────────────────────────────────────────────────
|
|
754
|
+
describe("paste without selected instrument", () => {
|
|
755
|
+
it("pastes instrument when no instrument is selected (replaceInstrument false)", () => {
|
|
756
|
+
const sourceAU = createAudioUnit(source);
|
|
757
|
+
const sourceVapo = addVaporisateur(source, sourceAU, "Source");
|
|
758
|
+
addTrack(source, sourceAU, TrackType.Notes, 0);
|
|
759
|
+
const deps = collectDeviceDependencies(sourceVapo, source.boxGraph, sourceAU);
|
|
760
|
+
const data = ClipboardUtils.serializeBoxes([sourceVapo, ...deps]);
|
|
761
|
+
const targetAU = createAudioUnit(target);
|
|
762
|
+
target.boxGraph.beginTransaction();
|
|
763
|
+
const boxes = ClipboardUtils.deserializeBoxes(data, target.boxGraph, {
|
|
764
|
+
mapPointer: pointer => {
|
|
765
|
+
if (pointer.pointerType === Pointers.InstrumentHost) {
|
|
766
|
+
return Option.wrap(targetAU.input.address);
|
|
767
|
+
}
|
|
768
|
+
if (pointer.pointerType === Pointers.TrackCollection) {
|
|
769
|
+
return Option.wrap(targetAU.tracks.address);
|
|
770
|
+
}
|
|
771
|
+
if (pointer.pointerType === Pointers.Automation) {
|
|
772
|
+
return Option.wrap(targetAU.address);
|
|
773
|
+
}
|
|
774
|
+
return Option.None;
|
|
775
|
+
},
|
|
776
|
+
excludeBox: () => false
|
|
777
|
+
});
|
|
778
|
+
target.boxGraph.endTransaction();
|
|
779
|
+
expect(boxes.filter(box => isInstanceOf(box, VaporisateurDeviceBox)).length).toBe(1);
|
|
780
|
+
expect(boxes.filter(box => isInstanceOf(box, TrackBox)).length).toBe(1);
|
|
781
|
+
});
|
|
782
|
+
});
|
|
783
|
+
// ─────────────────────────────────────────────────────────
|
|
784
|
+
// End-to-end paste-replace track index integrity
|
|
785
|
+
// ─────────────────────────────────────────────────────────
|
|
786
|
+
describe("paste-replace track index integrity", () => {
|
|
787
|
+
const getTrackIndices = (audioUnit) => audioUnit.tracks.pointerHub.filter(Pointers.TrackCollection)
|
|
788
|
+
.filter(pointer => isInstanceOf(pointer.box, TrackBox))
|
|
789
|
+
.map(pointer => pointer.box.index.getValue())
|
|
790
|
+
.sort();
|
|
791
|
+
const simulatePasteReplace = (sourceInstrument, sourceAudioUnit, sourceBoxGraph, targetAudioUnit, targetInstrument, targetBoxGraph) => {
|
|
792
|
+
const deps = collectDeviceDependencies(sourceInstrument, sourceBoxGraph, sourceAudioUnit);
|
|
793
|
+
const data = ClipboardUtils.serializeBoxes([sourceInstrument, ...deps]);
|
|
794
|
+
targetBoxGraph.beginTransaction();
|
|
795
|
+
for (const pointer of targetAudioUnit.tracks.pointerHub.filter(Pointers.TrackCollection)) {
|
|
796
|
+
if (isInstanceOf(pointer.box, TrackBox)) {
|
|
797
|
+
pointer.box.delete();
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
targetInstrument.delete();
|
|
801
|
+
const boxes = ClipboardUtils.deserializeBoxes(data, targetBoxGraph, makePasteMapper(targetAudioUnit, true));
|
|
802
|
+
const allTracks = targetAudioUnit.tracks.pointerHub.filter(Pointers.TrackCollection)
|
|
803
|
+
.filter(pointer => isInstanceOf(pointer.box, TrackBox))
|
|
804
|
+
.map(pointer => pointer.box)
|
|
805
|
+
.sort((trackA, trackB) => trackA.index.getValue() - trackB.index.getValue());
|
|
806
|
+
allTracks.forEach((track, idx) => track.index.setValue(idx));
|
|
807
|
+
targetBoxGraph.endTransaction();
|
|
808
|
+
return boxes;
|
|
809
|
+
};
|
|
810
|
+
it("replaces all target tracks with source tracks", () => {
|
|
811
|
+
const sourceAU = createAudioUnit(source);
|
|
812
|
+
const sourceVapo = addVaporisateur(source, sourceAU, "Source");
|
|
813
|
+
addTrack(source, sourceAU, TrackType.Notes, 0);
|
|
814
|
+
addAutomationTrack(source, sourceAU, sourceVapo.cutoff, 5);
|
|
815
|
+
const targetAU = createAudioUnit(target);
|
|
816
|
+
addTrack(target, targetAU, TrackType.Notes, 0);
|
|
817
|
+
const targetVapo = addVaporisateur(target, targetAU, "Target");
|
|
818
|
+
addAutomationTrack(target, targetAU, targetVapo.cutoff, 1);
|
|
819
|
+
simulatePasteReplace(sourceVapo, sourceAU, source.boxGraph, targetAU, targetVapo, target.boxGraph);
|
|
820
|
+
const indices = getTrackIndices(targetAU);
|
|
821
|
+
expect(indices).toEqual([0, 1]);
|
|
822
|
+
});
|
|
823
|
+
it("replaces all target tracks even when source has more tracks", () => {
|
|
824
|
+
const sourceAU = createAudioUnit(source);
|
|
825
|
+
const sourceVapo = addVaporisateur(source, sourceAU, "Source");
|
|
826
|
+
addTrack(source, sourceAU, TrackType.Notes, 0);
|
|
827
|
+
addAutomationTrack(source, sourceAU, sourceVapo.cutoff, 10);
|
|
828
|
+
addAutomationTrack(source, sourceAU, sourceVapo.resonance, 20);
|
|
829
|
+
const targetAU = createAudioUnit(target);
|
|
830
|
+
addTrack(target, targetAU, TrackType.Notes, 0);
|
|
831
|
+
addTrack(target, targetAU, TrackType.Notes, 1);
|
|
832
|
+
const targetVapo = addVaporisateur(target, targetAU, "Target");
|
|
833
|
+
addAutomationTrack(target, targetAU, targetVapo.cutoff, 2);
|
|
834
|
+
simulatePasteReplace(sourceVapo, sourceAU, source.boxGraph, targetAU, targetVapo, target.boxGraph);
|
|
835
|
+
const indices = getTrackIndices(targetAU);
|
|
836
|
+
expect(indices).toEqual([0, 1, 2]);
|
|
837
|
+
});
|
|
838
|
+
it("paste-replace with no existing tracks produces contiguous from 0", () => {
|
|
839
|
+
const sourceAU = createAudioUnit(source);
|
|
840
|
+
const sourceVapo = addVaporisateur(source, sourceAU, "Source");
|
|
841
|
+
addAutomationTrack(source, sourceAU, sourceVapo.cutoff, 7);
|
|
842
|
+
const targetAU = createAudioUnit(target);
|
|
843
|
+
const targetVapo = addVaporisateur(target, targetAU, "Target");
|
|
844
|
+
simulatePasteReplace(sourceVapo, sourceAU, source.boxGraph, targetAU, targetVapo, target.boxGraph);
|
|
845
|
+
const indices = getTrackIndices(targetAU);
|
|
846
|
+
expect(indices).toEqual([0]);
|
|
847
|
+
});
|
|
848
|
+
it("paste-replace onto self preserves note track and replaces automation", () => {
|
|
849
|
+
const audioUnit = createAudioUnit(source);
|
|
850
|
+
const vapo = addVaporisateur(source, audioUnit, "Vapo");
|
|
851
|
+
addTrack(source, audioUnit, TrackType.Notes, 0);
|
|
852
|
+
addAutomationTrack(source, audioUnit, vapo.cutoff, 1);
|
|
853
|
+
const deps = collectDeviceDependencies(vapo, source.boxGraph, audioUnit);
|
|
854
|
+
const data = ClipboardUtils.serializeBoxes([vapo, ...deps]);
|
|
855
|
+
source.boxGraph.beginTransaction();
|
|
856
|
+
for (const pointer of audioUnit.tracks.pointerHub.filter(Pointers.TrackCollection)) {
|
|
857
|
+
if (isInstanceOf(pointer.box, TrackBox)) {
|
|
858
|
+
pointer.box.delete();
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
vapo.delete();
|
|
862
|
+
ClipboardUtils.deserializeBoxes(data, source.boxGraph, makePasteMapper(audioUnit, true));
|
|
863
|
+
const allTracks = audioUnit.tracks.pointerHub.filter(Pointers.TrackCollection)
|
|
864
|
+
.filter(pointer => isInstanceOf(pointer.box, TrackBox))
|
|
865
|
+
.map(pointer => pointer.box)
|
|
866
|
+
.sort((trackA, trackB) => trackA.index.getValue() - trackB.index.getValue());
|
|
867
|
+
allTracks.forEach((track, idx) => track.index.setValue(idx));
|
|
868
|
+
source.boxGraph.endTransaction();
|
|
869
|
+
const indices = getTrackIndices(audioUnit);
|
|
870
|
+
expect(indices).toEqual([0, 1]);
|
|
871
|
+
const trackCount = audioUnit.tracks.pointerHub.filter(Pointers.TrackCollection)
|
|
872
|
+
.filter(pointer => isInstanceOf(pointer.box, TrackBox)).length;
|
|
873
|
+
expect(trackCount).toBe(2);
|
|
874
|
+
const instrumentCount = audioUnit.input.pointerHub.incoming().length;
|
|
875
|
+
expect(instrumentCount).toBe(1);
|
|
876
|
+
});
|
|
877
|
+
it("no index gaps when source track had high index", () => {
|
|
878
|
+
const sourceAU = createAudioUnit(source);
|
|
879
|
+
const sourceVapo = addVaporisateur(source, sourceAU, "Source");
|
|
880
|
+
addTrack(source, sourceAU, TrackType.Notes, 0);
|
|
881
|
+
addAutomationTrack(source, sourceAU, sourceVapo.cutoff, 99);
|
|
882
|
+
const targetAU = createAudioUnit(target);
|
|
883
|
+
const targetVapo = addVaporisateur(target, targetAU, "Target");
|
|
884
|
+
simulatePasteReplace(sourceVapo, sourceAU, source.boxGraph, targetAU, targetVapo, target.boxGraph);
|
|
885
|
+
const indices = getTrackIndices(targetAU);
|
|
886
|
+
expect(indices).toEqual([0, 1]);
|
|
887
|
+
});
|
|
888
|
+
it("no duplicate indices after paste-replace", () => {
|
|
889
|
+
const sourceAU = createAudioUnit(source);
|
|
890
|
+
const sourceVapo = addVaporisateur(source, sourceAU, "Source");
|
|
891
|
+
addTrack(source, sourceAU, TrackType.Notes, 0);
|
|
892
|
+
addAutomationTrack(source, sourceAU, sourceVapo.cutoff, 0);
|
|
893
|
+
const targetAU = createAudioUnit(target);
|
|
894
|
+
addTrack(target, targetAU, TrackType.Notes, 0);
|
|
895
|
+
const targetVapo = addVaporisateur(target, targetAU, "Target");
|
|
896
|
+
simulatePasteReplace(sourceVapo, sourceAU, source.boxGraph, targetAU, targetVapo, target.boxGraph);
|
|
897
|
+
const indices = getTrackIndices(targetAU);
|
|
898
|
+
expect(indices).toEqual([0, 1]);
|
|
899
|
+
const uniqueIndices = new Set(indices);
|
|
900
|
+
expect(uniqueIndices.size).toBe(indices.length);
|
|
901
|
+
});
|
|
902
|
+
});
|
|
903
|
+
// ─────────────────────────────────────────────────────────
|
|
904
|
+
// Effect index management
|
|
905
|
+
// ─────────────────────────────────────────────────────────
|
|
906
|
+
describe("effect index management", () => {
|
|
907
|
+
it("inserts at position 0 and shifts existing effects", () => {
|
|
908
|
+
const sourceAU = createAudioUnit(source);
|
|
909
|
+
const sourceEffect = addAudioEffect(source, sourceAU, "New", 0);
|
|
910
|
+
const data = ClipboardUtils.serializeBoxes([sourceEffect]);
|
|
911
|
+
const targetAU = createAudioUnit(target);
|
|
912
|
+
const existingA = addAudioEffect(target, targetAU, "A", 0);
|
|
913
|
+
const existingB = addAudioEffect(target, targetAU, "B", 1);
|
|
914
|
+
const editing = new BoxEditing(target.boxGraph);
|
|
915
|
+
editing.modify(() => {
|
|
916
|
+
for (const pointer of targetAU.audioEffects.pointerHub.incoming()) {
|
|
917
|
+
if (isInstanceOf(pointer.box, CompressorDeviceBox)) {
|
|
918
|
+
const idx = pointer.box.index.getValue();
|
|
919
|
+
if (idx >= 0)
|
|
920
|
+
pointer.box.index.setValue(idx + 1);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
const boxes = ClipboardUtils.deserializeBoxes(data, target.boxGraph, makePasteMapper(targetAU, false));
|
|
924
|
+
boxes.filter((box) => isInstanceOf(box, CompressorDeviceBox))
|
|
925
|
+
.forEach((box, idx) => box.index.setValue(idx));
|
|
926
|
+
});
|
|
927
|
+
expect(existingA.index.getValue()).toBe(1);
|
|
928
|
+
expect(existingB.index.getValue()).toBe(2);
|
|
929
|
+
expect(targetAU.audioEffects.pointerHub.incoming().length).toBe(3);
|
|
930
|
+
});
|
|
931
|
+
it("inserts after selected effect and shifts only subsequent", () => {
|
|
932
|
+
const sourceAU = createAudioUnit(source);
|
|
933
|
+
const sourceEffect = addAudioEffect(source, sourceAU, "New", 0);
|
|
934
|
+
const data = ClipboardUtils.serializeBoxes([sourceEffect]);
|
|
935
|
+
const targetAU = createAudioUnit(target);
|
|
936
|
+
const existingA = addAudioEffect(target, targetAU, "A", 0);
|
|
937
|
+
addAudioEffect(target, targetAU, "B", 1);
|
|
938
|
+
const existingC = addAudioEffect(target, targetAU, "C", 2);
|
|
939
|
+
const insertIndex = 2;
|
|
940
|
+
const editing = new BoxEditing(target.boxGraph);
|
|
941
|
+
editing.modify(() => {
|
|
942
|
+
for (const pointer of targetAU.audioEffects.pointerHub.incoming()) {
|
|
943
|
+
if (isInstanceOf(pointer.box, CompressorDeviceBox)) {
|
|
944
|
+
const idx = pointer.box.index.getValue();
|
|
945
|
+
if (idx >= insertIndex)
|
|
946
|
+
pointer.box.index.setValue(idx + 1);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
const boxes = ClipboardUtils.deserializeBoxes(data, target.boxGraph, makePasteMapper(targetAU, false));
|
|
950
|
+
boxes.filter((box) => isInstanceOf(box, CompressorDeviceBox))
|
|
951
|
+
.forEach((box, idx) => box.index.setValue(insertIndex + idx));
|
|
952
|
+
});
|
|
953
|
+
expect(existingA.index.getValue()).toBe(0);
|
|
954
|
+
expect(existingC.index.getValue()).toBe(3);
|
|
955
|
+
expect(targetAU.audioEffects.pointerHub.incoming().length).toBe(4);
|
|
956
|
+
const indices = targetAU.audioEffects.pointerHub.incoming()
|
|
957
|
+
.filter(pointer => isInstanceOf(pointer.box, CompressorDeviceBox))
|
|
958
|
+
.map(pointer => pointer.box.index.getValue())
|
|
959
|
+
.sort();
|
|
960
|
+
expect(indices).toEqual([0, 1, 2, 3]);
|
|
961
|
+
});
|
|
962
|
+
});
|
|
963
|
+
// ─────────────────────────────────────────────────────────
|
|
964
|
+
// isInstanceOf type narrowing
|
|
965
|
+
// ─────────────────────────────────────────────────────────
|
|
966
|
+
// ─────────────────────────────────────────────────────────
|
|
967
|
+
// MIDIOutputDeviceBox copy & paste
|
|
968
|
+
// ─────────────────────────────────────────────────────────
|
|
969
|
+
const addMIDIOutputInstrument = (skeleton, audioUnit, label) => {
|
|
970
|
+
const { boxGraph } = skeleton;
|
|
971
|
+
let device;
|
|
972
|
+
boxGraph.beginTransaction();
|
|
973
|
+
device = MIDIOutputDeviceBox.create(boxGraph, UUID.generate(), box => {
|
|
974
|
+
box.label.setValue(label);
|
|
975
|
+
box.host.refer(audioUnit.input);
|
|
976
|
+
});
|
|
977
|
+
boxGraph.endTransaction();
|
|
978
|
+
return device;
|
|
979
|
+
};
|
|
980
|
+
const connectMIDIOutput = (skeleton, deviceBox, outputId, outputLabel) => {
|
|
981
|
+
const { boxGraph, mandatoryBoxes: { rootBox } } = skeleton;
|
|
982
|
+
let midiOutputBox;
|
|
983
|
+
boxGraph.beginTransaction();
|
|
984
|
+
midiOutputBox = MIDIOutputBox.create(boxGraph, UUID.generate(), box => {
|
|
985
|
+
box.id.setValue(outputId);
|
|
986
|
+
box.label.setValue(outputLabel);
|
|
987
|
+
box.root.refer(rootBox.outputMidiDevices);
|
|
988
|
+
});
|
|
989
|
+
deviceBox.device.refer(midiOutputBox.device);
|
|
990
|
+
boxGraph.endTransaction();
|
|
991
|
+
return midiOutputBox;
|
|
992
|
+
};
|
|
993
|
+
describe("MIDIOutputDeviceBox copy & paste", () => {
|
|
994
|
+
it("collects MIDIOutputBox as dependency when device pointer is set", () => {
|
|
995
|
+
const audioUnit = createAudioUnit(source);
|
|
996
|
+
const midiDevice = addMIDIOutputInstrument(source, audioUnit, "MIDIOut");
|
|
997
|
+
connectMIDIOutput(source, midiDevice, "output-1", "MIDI Port 1");
|
|
998
|
+
const deps = collectDeviceDependencies(midiDevice, source.boxGraph);
|
|
999
|
+
expect(deps.filter(box => isInstanceOf(box, MIDIOutputBox)).length).toBe(1);
|
|
1000
|
+
});
|
|
1001
|
+
it("does not collect RootBox as dependency", () => {
|
|
1002
|
+
const audioUnit = createAudioUnit(source);
|
|
1003
|
+
const midiDevice = addMIDIOutputInstrument(source, audioUnit, "MIDIOut");
|
|
1004
|
+
connectMIDIOutput(source, midiDevice, "output-1", "MIDI Port 1");
|
|
1005
|
+
const deps = collectDeviceDependencies(midiDevice, source.boxGraph);
|
|
1006
|
+
expect(deps.filter(box => isInstanceOf(box, RootBox)).length).toBe(0);
|
|
1007
|
+
});
|
|
1008
|
+
it("does not collect MIDIOutputBox when device pointer is empty", () => {
|
|
1009
|
+
const audioUnit = createAudioUnit(source);
|
|
1010
|
+
const midiDevice = addMIDIOutputInstrument(source, audioUnit, "MIDIOut");
|
|
1011
|
+
const deps = collectDeviceDependencies(midiDevice, source.boxGraph);
|
|
1012
|
+
expect(deps.filter(box => isInstanceOf(box, MIDIOutputBox)).length).toBe(0);
|
|
1013
|
+
});
|
|
1014
|
+
it("paste MIDIOutput with empty device pointer does not fill it", () => {
|
|
1015
|
+
const sourceAU = createAudioUnit(source);
|
|
1016
|
+
const sourceMidi = addMIDIOutputInstrument(source, sourceAU, "Source MIDI");
|
|
1017
|
+
const deps = collectDeviceDependencies(sourceMidi, source.boxGraph, sourceAU);
|
|
1018
|
+
const data = ClipboardUtils.serializeBoxes([sourceMidi, ...deps]);
|
|
1019
|
+
const targetAU = createAudioUnit(target);
|
|
1020
|
+
target.boxGraph.beginTransaction();
|
|
1021
|
+
const boxes = ClipboardUtils.deserializeBoxes(data, target.boxGraph, {
|
|
1022
|
+
mapPointer: (pointer, address) => {
|
|
1023
|
+
if (address.isEmpty()) {
|
|
1024
|
+
return Option.None;
|
|
1025
|
+
}
|
|
1026
|
+
if (pointer.pointerType === Pointers.InstrumentHost) {
|
|
1027
|
+
return Option.wrap(targetAU.input.address);
|
|
1028
|
+
}
|
|
1029
|
+
if (pointer.pointerType === Pointers.MIDIDevice) {
|
|
1030
|
+
return Option.wrap(target.mandatoryBoxes.rootBox.outputMidiDevices.address);
|
|
1031
|
+
}
|
|
1032
|
+
if (pointer.pointerType === Pointers.TrackCollection) {
|
|
1033
|
+
return Option.wrap(targetAU.tracks.address);
|
|
1034
|
+
}
|
|
1035
|
+
if (pointer.pointerType === Pointers.Automation) {
|
|
1036
|
+
return Option.wrap(targetAU.address);
|
|
1037
|
+
}
|
|
1038
|
+
return Option.None;
|
|
1039
|
+
},
|
|
1040
|
+
excludeBox: () => false
|
|
1041
|
+
});
|
|
1042
|
+
target.boxGraph.endTransaction();
|
|
1043
|
+
const pastedDevice = boxes.find(box => isInstanceOf(box, MIDIOutputDeviceBox));
|
|
1044
|
+
expect(pastedDevice).toBeDefined();
|
|
1045
|
+
expect(pastedDevice.device.isEmpty()).toBe(true);
|
|
1046
|
+
});
|
|
1047
|
+
it("paste-replace MIDIOutput with connected device does not throw", () => {
|
|
1048
|
+
const sourceAU = createAudioUnit(source);
|
|
1049
|
+
const sourceMidi = addMIDIOutputInstrument(source, sourceAU, "Source MIDI");
|
|
1050
|
+
connectMIDIOutput(source, sourceMidi, "output-1", "MIDI Port 1");
|
|
1051
|
+
const deps = collectDeviceDependencies(sourceMidi, source.boxGraph, sourceAU);
|
|
1052
|
+
const data = ClipboardUtils.serializeBoxes([sourceMidi, ...deps]);
|
|
1053
|
+
const targetAU = createAudioUnit(target);
|
|
1054
|
+
const targetVapo = addVaporisateur(target, targetAU, "Target Vapo");
|
|
1055
|
+
expect(() => {
|
|
1056
|
+
target.boxGraph.beginTransaction();
|
|
1057
|
+
targetVapo.delete();
|
|
1058
|
+
ClipboardUtils.deserializeBoxes(data, target.boxGraph, {
|
|
1059
|
+
...makePasteMapper(targetAU, true),
|
|
1060
|
+
mapPointer: (pointer, address) => {
|
|
1061
|
+
const base = makePasteMapper(targetAU, true).mapPointer(pointer);
|
|
1062
|
+
if (base.nonEmpty()) {
|
|
1063
|
+
return base;
|
|
1064
|
+
}
|
|
1065
|
+
if (pointer.pointerType === Pointers.MIDIDevice) {
|
|
1066
|
+
return Option.wrap(target.mandatoryBoxes.rootBox.outputMidiDevices.address);
|
|
1067
|
+
}
|
|
1068
|
+
return Option.None;
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
target.boxGraph.endTransaction();
|
|
1072
|
+
}).not.toThrow();
|
|
1073
|
+
});
|
|
1074
|
+
it("pasted MIDIOutputBox.root points to target RootBox.outputMidiDevices", () => {
|
|
1075
|
+
const sourceAU = createAudioUnit(source);
|
|
1076
|
+
const sourceMidi = addMIDIOutputInstrument(source, sourceAU, "Source MIDI");
|
|
1077
|
+
connectMIDIOutput(source, sourceMidi, "output-1", "MIDI Port 1");
|
|
1078
|
+
const deps = collectDeviceDependencies(sourceMidi, source.boxGraph, sourceAU);
|
|
1079
|
+
const data = ClipboardUtils.serializeBoxes([sourceMidi, ...deps]);
|
|
1080
|
+
const targetAU = createAudioUnit(target);
|
|
1081
|
+
target.boxGraph.beginTransaction();
|
|
1082
|
+
const boxes = ClipboardUtils.deserializeBoxes(data, target.boxGraph, {
|
|
1083
|
+
mapPointer: (pointer, address) => {
|
|
1084
|
+
if (pointer.pointerType === Pointers.InstrumentHost) {
|
|
1085
|
+
return Option.wrap(targetAU.input.address);
|
|
1086
|
+
}
|
|
1087
|
+
if (pointer.pointerType === Pointers.MIDIDevice) {
|
|
1088
|
+
return Option.wrap(target.mandatoryBoxes.rootBox.outputMidiDevices.address);
|
|
1089
|
+
}
|
|
1090
|
+
if (pointer.pointerType === Pointers.TrackCollection) {
|
|
1091
|
+
return Option.wrap(targetAU.tracks.address);
|
|
1092
|
+
}
|
|
1093
|
+
if (pointer.pointerType === Pointers.Automation) {
|
|
1094
|
+
return Option.wrap(targetAU.address);
|
|
1095
|
+
}
|
|
1096
|
+
return Option.None;
|
|
1097
|
+
},
|
|
1098
|
+
excludeBox: () => false
|
|
1099
|
+
});
|
|
1100
|
+
target.boxGraph.endTransaction();
|
|
1101
|
+
const pastedMidiOutputBox = boxes.find(box => isInstanceOf(box, MIDIOutputBox));
|
|
1102
|
+
expect(pastedMidiOutputBox).toBeDefined();
|
|
1103
|
+
expect(pastedMidiOutputBox.root.targetVertex.unwrap().box).toBe(target.mandatoryBoxes.rootBox);
|
|
1104
|
+
});
|
|
1105
|
+
it("pasted MIDIOutputDeviceBox.device points to pasted MIDIOutputBox", () => {
|
|
1106
|
+
const sourceAU = createAudioUnit(source);
|
|
1107
|
+
const sourceMidi = addMIDIOutputInstrument(source, sourceAU, "Source MIDI");
|
|
1108
|
+
connectMIDIOutput(source, sourceMidi, "output-1", "MIDI Port 1");
|
|
1109
|
+
const deps = collectDeviceDependencies(sourceMidi, source.boxGraph, sourceAU);
|
|
1110
|
+
const data = ClipboardUtils.serializeBoxes([sourceMidi, ...deps]);
|
|
1111
|
+
const targetAU = createAudioUnit(target);
|
|
1112
|
+
target.boxGraph.beginTransaction();
|
|
1113
|
+
const boxes = ClipboardUtils.deserializeBoxes(data, target.boxGraph, {
|
|
1114
|
+
mapPointer: (pointer, address) => {
|
|
1115
|
+
if (pointer.pointerType === Pointers.InstrumentHost) {
|
|
1116
|
+
return Option.wrap(targetAU.input.address);
|
|
1117
|
+
}
|
|
1118
|
+
if (pointer.pointerType === Pointers.MIDIDevice) {
|
|
1119
|
+
return Option.wrap(target.mandatoryBoxes.rootBox.outputMidiDevices.address);
|
|
1120
|
+
}
|
|
1121
|
+
if (pointer.pointerType === Pointers.TrackCollection) {
|
|
1122
|
+
return Option.wrap(targetAU.tracks.address);
|
|
1123
|
+
}
|
|
1124
|
+
if (pointer.pointerType === Pointers.Automation) {
|
|
1125
|
+
return Option.wrap(targetAU.address);
|
|
1126
|
+
}
|
|
1127
|
+
return Option.None;
|
|
1128
|
+
},
|
|
1129
|
+
excludeBox: () => false
|
|
1130
|
+
});
|
|
1131
|
+
target.boxGraph.endTransaction();
|
|
1132
|
+
const pastedDevice = boxes.find(box => isInstanceOf(box, MIDIOutputDeviceBox));
|
|
1133
|
+
const pastedMidiOutput = boxes.find(box => isInstanceOf(box, MIDIOutputBox));
|
|
1134
|
+
expect(pastedDevice).toBeDefined();
|
|
1135
|
+
expect(pastedMidiOutput).toBeDefined();
|
|
1136
|
+
expect(pastedDevice.device.targetVertex.unwrap().box).toBe(pastedMidiOutput);
|
|
1137
|
+
});
|
|
1138
|
+
});
|
|
1139
|
+
describe("isInstanceOf type narrowing", () => {
|
|
1140
|
+
it("narrows TrackBox type directly without intermediate null variable", () => {
|
|
1141
|
+
const audioUnit = createAudioUnit(source);
|
|
1142
|
+
addTrack(source, audioUnit, TrackType.Audio);
|
|
1143
|
+
const pointers = audioUnit.tracks.pointerHub.incoming();
|
|
1144
|
+
expect(pointers.length).toBe(1);
|
|
1145
|
+
expect(isInstanceOf(pointers[0].box, TrackBox)).toBe(true);
|
|
1146
|
+
if (isInstanceOf(pointers[0].box, TrackBox)) {
|
|
1147
|
+
expect(pointers[0].box.index.getValue()).toBe(0);
|
|
1148
|
+
}
|
|
1149
|
+
else {
|
|
1150
|
+
expect.unreachable("Expected TrackBox");
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
});
|
|
1154
|
+
});
|