refactoring

This commit is contained in:
André Michelle
2026-02-16 12:25:23 +01:00
parent 9aa0704798
commit db8763d36c
15 changed files with 347 additions and 399 deletions
@@ -15,7 +15,7 @@ import {StudioService} from "@/service/StudioService"
import {DefaultWorkspace} from "@/ui/workspace/Default"
import {PanelType} from "@/ui/workspace/PanelType"
import {Workspace} from "@/ui/workspace/Workspace"
import {DeviceHost, Devices, ProjectUtils} from "@opendaw/studio-adapters"
import {DeviceHost, Devices, TransferAudioUnits} from "@opendaw/studio-adapters"
import {ContentEditorShortcuts, ContentEditorShortcutsFactory} from "@/ui/shortcuts/ContentEditorShortcuts"
import {PianoPanelShortcuts, PianoPanelShortcutsFactory} from "@/ui/shortcuts/PianoPanelShortcuts"
import {RegionsShortcuts, RegionsShortcutsFactory} from "@/ui/shortcuts/RegionsShortcuts"
@@ -147,8 +147,8 @@ export namespace StudioShortcutManager {
({editing, boxAdapters, userEditingManager, skeleton}) => userEditingManager.audioUnit.get()
.ifSome(({box}) => {
const deviceHost: DeviceHost = boxAdapters.adapterFor(box, Devices.isHost)
const copies = editing.modify(() => ProjectUtils
.extractAudioUnits([deviceHost.audioUnitBoxAdapter().box], skeleton), false).unwrap()
const copies = editing.modify(() => TransferAudioUnits
.transfer([deviceHost.audioUnitBoxAdapter().box], skeleton), false).unwrap()
userEditingManager.audioUnit.edit(copies[0].editing)
}))),
gc.register(gs["workspace-next-screen"].shortcut, () => {
@@ -1,6 +1,6 @@
import {CaptureAudio, MenuItem, MonitoringMode, Project} from "@opendaw/studio-core"
import {isInstanceOf, Procedure, RuntimeNotifier, UUID} from "@opendaw/lib-std"
import {AudioUnitBoxAdapter, DeviceAccepts, ProjectUtils, TrackBoxAdapter, TrackType} from "@opendaw/studio-adapters"
import {AudioUnitBoxAdapter, DeviceAccepts, TrackBoxAdapter, TrackType, TransferAudioUnits} from "@opendaw/studio-adapters"
import {DebugMenus} from "@/ui/menu/debug"
import {MidiImport} from "@/ui/timeline/MidiImport.ts"
import {CaptureMidiBox, TrackBox} from "@opendaw/studio-boxes"
@@ -72,8 +72,8 @@ export const installTrackHeaderMenu = (service: StudioService,
shortcut: GlobalShortcuts["copy-device"].shortcut.format(),
separatorBefore: true
}).setTriggerProcedure(() => {
const copies = editing.modify(() => ProjectUtils
.extractAudioUnits([trackBoxAdapter.audioUnit], project.skeleton), false).unwrap()
const copies = editing.modify(() => TransferAudioUnits
.transfer([trackBoxAdapter.audioUnit], project.skeleton), false).unwrap()
userEditingManager.audioUnit.edit(copies[0].editing)
}),
MenuItem.default({
@@ -90,7 +90,7 @@ export const installTrackHeaderMenu = (service: StudioService,
editing.modify(() => {
const {boxGraph, skeleton} = newProject
boxGraph.beginTransaction()
ProjectUtils.extractAudioUnits([trackBoxAdapter.audioUnit], skeleton)
TransferAudioUnits.transfer([trackBoxAdapter.audioUnit], skeleton)
boxGraph.endTransaction()
})
service.projectProfileService.setProject(newProject, "NEW")
+4 -12
View File
@@ -282,7 +282,7 @@ export class BoxGraph<BoxMap = any> {
return pointers
}
dependenciesOf(box: Box, options: {
dependenciesOf(root: Box | ReadonlyArray<Box>, options: {
excludeBox?: Predicate<Box>
alwaysFollowMandatory?: boolean
stopAtResources?: boolean
@@ -293,20 +293,14 @@ export class BoxGraph<BoxMap = any> {
const boxes = new Set<Box>()
const pointers = new Set<PointerField>()
const trace = (box: Box, isStartingBox: boolean = false): void => {
// Don't apply excludeBox to the starting box - only to its dependencies
if (boxes.has(box) || (!isStartingBox && excludeBox(box))) {return}
boxes.add(box)
// Handle resource boxes especially when stopAtResources is enabled
if (stopAtResources && isDefined(box.resource)) {
// Resource boxes are endpoints, but we still need their "children"
// Children = boxes that point to FIELDS within this box (not the box itself)
box.incomingEdges()
.forEach(pointer => {
pointers.add(pointer)
// Only follow if a pointer targets a FIELD (child), not the BOX (user)
const targetsField = pointer.targetAddress.mapOr(address => !address.isBox(), false)
if (pointer.mandatory && targetsField) {
// For shared resources, skip pointers to mandatory (ownership) fields
if (box.resource === "shared") {
const isOwnershipField = pointer.targetAddress
.flatMap(address => this.findVertex(address))
@@ -316,10 +310,8 @@ export class BoxGraph<BoxMap = any> {
trace(pointer.box)
}
})
// Don't trace outgoing edges - resources are endpoints
return
}
// Normal box traversal
box.outgoingEdges()
.filter(([pointer]) => !pointers.has(pointer))
.forEach(([source, targetAddress]: [PointerField, Address]) => {
@@ -328,7 +320,6 @@ export class BoxGraph<BoxMap = any> {
pointers.add(source)
if (targetVertex.pointerRules.mandatory &&
(alwaysFollowMandatory || targetVertex.pointerHub.incoming()
// Only check pointers to this specific address, not to other fields in the box
.filter(p => p.targetAddress.mapOr(address => address.equals(targetAddress), false))
.every(pointer => pointers.has(pointer)))) {
return trace(targetVertex.box)
@@ -342,8 +333,9 @@ export class BoxGraph<BoxMap = any> {
}
})
}
trace(box, true)
boxes.delete(box)
const startingBoxes: ReadonlyArray<Box> = Array.isArray(root) ? root : [root]
startingBoxes.forEach(startingBox => trace(startingBox, true))
startingBoxes.forEach(startingBox => boxes.delete(startingBox))
return {boxes, pointers: Array.from(pointers).reverse()}
}
@@ -24,9 +24,9 @@ import {
TrackBox
} from "@opendaw/studio-boxes"
import {ProjectSkeleton} from "../project/ProjectSkeleton"
import {ProjectUtils} from "../project/ProjectUtils"
import {TrackType} from "../timeline/TrackType"
import {TransferUtils} from "../transfer"
import {PresetHeader} from "./PresetHeader"
import {TrackType} from "../timeline/TrackType"
export namespace PresetDecoder {
export const decode = (bytes: ArrayBufferLike, target: ProjectSkeleton) => {
@@ -58,7 +58,22 @@ export namespace PresetDecoder {
const sourceAudioUnitBoxes = sourceBoxGraph.boxes()
.filter(box => isInstanceOf(box, AudioUnitBox))
.filter(box => box.type.getValue() !== AudioUnitType.Output)
ProjectUtils.extractAudioUnits(sourceAudioUnitBoxes, target, {excludeTimeline: true})
const excludeBox = (box: Box): boolean =>
TransferUtils.shouldExclude(box) || TransferUtils.excludeTimelinePredicate(box)
const dependencies = Array.from(sourceBoxGraph.dependenciesOf(sourceAudioUnitBoxes, {
alwaysFollowMandatory: true,
stopAtResources: true,
excludeBox
}).boxes)
const {mandatoryBoxes: {rootBox, primaryAudioBus}} = target
const uuidMap = TransferUtils.generateMap(
sourceAudioUnitBoxes, dependencies, rootBox.audioUnits.address.uuid, primaryAudioBus.address.uuid)
TransferUtils.copyBoxes(uuidMap, target.boxGraph, sourceAudioUnitBoxes, dependencies)
TransferUtils.reorderAudioUnits(uuidMap, sourceAudioUnitBoxes, rootBox)
sourceAudioUnitBoxes
.map(source => asInstanceOf(rootBox.graph
.findBox(uuidMap.get(source.address.uuid).target)
.unwrap("Target AudioUnit has not been copied"), AudioUnitBox))
.filter(box => box.type.getValue() !== AudioUnitType.Output)
.forEach((audioUnitBox) => {
const inputBox = audioUnitBox.input.pointerHub.incoming().at(0)?.box
@@ -1,7 +1,8 @@
import {Arrays, ByteArrayOutput} from "@opendaw/lib-std"
import {Box} from "@opendaw/lib-box"
import {AudioUnitBox} from "@opendaw/studio-boxes"
import {ProjectSkeleton} from "../project/ProjectSkeleton"
import {ProjectUtils} from "../project/ProjectUtils"
import {TransferUtils} from "../transfer"
import {PresetHeader} from "./PresetHeader"
export namespace PresetEncoder {
@@ -10,11 +11,23 @@ export namespace PresetEncoder {
header.writeInt(PresetHeader.MAGIC_HEADER_OPEN)
header.writeInt(PresetHeader.FORMAT_VERSION)
const preset = ProjectSkeleton.empty({createOutputCompressor: false, createDefaultUser: false})
preset.boxGraph.beginTransaction()
ProjectUtils.extractAudioUnits([audioUnitBox], preset, {excludeTimeline: true})
preset.boxGraph.endTransaction()
const {boxGraph, mandatoryBoxes: {rootBox, primaryAudioBus}} = preset
const audioUnitBoxes = [audioUnitBox]
const excludeBox = (box: Box): boolean =>
TransferUtils.shouldExclude(box) || TransferUtils.excludeTimelinePredicate(box)
boxGraph.beginTransaction()
const dependencies = Array.from(audioUnitBox.graph.dependenciesOf(audioUnitBoxes, {
alwaysFollowMandatory: true,
stopAtResources: true,
excludeBox
}).boxes)
const uuidMap = TransferUtils.generateMap(
audioUnitBoxes, dependencies, rootBox.audioUnits.address.uuid, primaryAudioBus.address.uuid)
TransferUtils.copyBoxes(uuidMap, boxGraph, audioUnitBoxes, dependencies)
TransferUtils.reorderAudioUnits(uuidMap, audioUnitBoxes, rootBox)
boxGraph.endTransaction()
console.debug("SAVING...")
preset.boxGraph.debugBoxes()
return Arrays.concatArrayBuffers(header.toArrayBuffer(), preset.boxGraph.toArrayBuffer())
boxGraph.debugBoxes()
return Arrays.concatArrayBuffers(header.toArrayBuffer(), boxGraph.toArrayBuffer())
}
}
}
@@ -1,206 +0,0 @@
import {ppqn} from "@opendaw/lib-dsp"
import {
Arrays,
asInstanceOf,
assert,
ByteArrayInput,
isInstanceOf,
Option,
Predicate,
SetMultimap,
SortedSet,
UUID
} from "@opendaw/lib-std"
import {AudioUnitBox, AuxSendBox, BoxIO, BoxVisitor, RootBox, TrackBox} from "@opendaw/studio-boxes"
import {Address, Box, BoxGraph, IndexedBox, PointerField} from "@opendaw/lib-box"
import {ProjectSkeleton} from "./ProjectSkeleton"
import {AnyRegionBox, UnionBoxTypes} from "../unions"
import {AudioUnitOrdering} from "../factories/AudioUnitOrdering"
export namespace ProjectUtils {
type UUIDMapper = { source: UUID.Bytes, target: UUID.Bytes }
const isSameGraph: (a: Box, b: Box) => boolean = ({graph: a}, {graph: b}) => a === b
const compareIndex = (a: IndexedBox, b: IndexedBox) => a.index.getValue() - b.index.getValue()
const excludeTimelinePredicate = (box: Box) => box.accept<BoxVisitor<boolean>>({
visitTrackBox: (_box: TrackBox): boolean => true
}) === true
const shouldExclude = (box: Box): boolean => box.ephemeral || box.name === AuxSendBox.ClassName
export const extractAudioUnits = (audioUnitBoxes: ReadonlyArray<AudioUnitBox>,
{boxGraph, mandatoryBoxes: {primaryAudioBus, rootBox}}: ProjectSkeleton,
options: {
includeAux?: boolean,
includeBus?: boolean,
excludeTimeline?: boolean,
} = {})
: ReadonlyArray<AudioUnitBox> => {
assert(Arrays.satisfy(audioUnitBoxes, isSameGraph), "AudioUnits must share the same BoxGraph")
assert(!options.includeAux && !options.includeBus, "Options are not yet implemented")
const excludeBox = (box: Box): boolean =>
shouldExclude(box) || (options?.excludeTimeline === true && excludeTimelinePredicate(box))
const seen = UUID.newSet<UUID.Bytes>(uuid => uuid)
const dependencies = audioUnitBoxes
.flatMap(box => Array.from(box.graph.dependenciesOf(box, {
alwaysFollowMandatory: true,
stopAtResources: true,
excludeBox
}).boxes))
.filter(box => {
if (seen.hasKey(box.address.uuid)) {return false}
seen.add(box.address.uuid)
return true
})
const uuidMap = generateTransferMap(
audioUnitBoxes, dependencies, rootBox.audioUnits.address.uuid, primaryAudioBus.address.uuid)
copy(uuidMap, boxGraph, audioUnitBoxes, dependencies)
reorderAudioUnits(uuidMap, audioUnitBoxes, rootBox)
console.debug("inTransaction", boxGraph.inTransaction())
return audioUnitBoxes.map(source => asInstanceOf(rootBox.graph
.findBox(uuidMap.get(source.address.uuid).target)
.unwrap("Target Track has not been copied"), AudioUnitBox))
}
export const extractRegions = (regionBoxes: ReadonlyArray<AnyRegionBox>,
{boxGraph, mandatoryBoxes: {primaryAudioBus, rootBox}}: ProjectSkeleton,
insertPosition: ppqn = 0.0): void => {
assert(Arrays.satisfy(regionBoxes, isSameGraph),
"Region smust be from the same BoxGraph")
const regionBoxSet = new Set<AnyRegionBox>(regionBoxes)
const trackBoxSet = new Set<TrackBox>()
const audioUnitBoxSet = new SetMultimap<AudioUnitBox, TrackBox>()
regionBoxes.forEach(regionBox => {
const trackBox = asInstanceOf(regionBox.regions.targetVertex.unwrap().box, TrackBox)
trackBoxSet.add(trackBox)
const audioUnitBox = asInstanceOf(trackBox.tracks.targetVertex.unwrap().box, AudioUnitBox)
audioUnitBoxSet.add(audioUnitBox, trackBox)
})
console.debug(`Found ${audioUnitBoxSet.keyCount()} audioUnits`)
console.debug(`Found ${trackBoxSet.size} tracks`)
const audioUnitBoxes = [...audioUnitBoxSet.keys()]
const excludeBox: Predicate<Box> = (box: Box): boolean =>
shouldExclude(box)
|| (isInstanceOf(box, TrackBox) && !trackBoxSet.has(box))
|| (UnionBoxTypes.isRegionBox(box) && !regionBoxSet.has(box))
const dependencies = audioUnitBoxes
.flatMap(box => Array.from(box.graph.dependenciesOf(box, {
excludeBox,
alwaysFollowMandatory: true,
stopAtResources: true
}).boxes))
const uuidMap = generateTransferMap(
audioUnitBoxes, dependencies, rootBox.audioUnits.address.uuid, primaryAudioBus.address.uuid)
copy(uuidMap, boxGraph, audioUnitBoxes, dependencies)
reorderAudioUnits(uuidMap, audioUnitBoxes, rootBox)
audioUnitBoxSet.forEach((_, trackBoxes) => [...trackBoxes]
.sort(compareIndex)
.forEach((source: TrackBox, index) => {
const box = boxGraph
.findBox(uuidMap.get(source.address.uuid).target)
.unwrap("Target Track has not been copied")
asInstanceOf(box, TrackBox).index.setValue(index)
}))
const minPosition = regionBoxes.reduce((min, region) =>
Math.min(min, region.position.getValue()), Number.MAX_VALUE)
const delta = insertPosition - minPosition
regionBoxes.forEach((source: AnyRegionBox) => {
const box = boxGraph
.findBox(uuidMap.get(source.address.uuid).target)
.unwrap("Target Track has not been copied")
const {position} = UnionBoxTypes.asRegionBox(box)
position.setValue(position.getValue() + delta)
})
}
const generateTransferMap = (audioUnitBoxes: ReadonlyArray<AudioUnitBox>,
dependencies: ReadonlyArray<Box>,
rootBoxUUID: UUID.Bytes,
masterBusBoxUUID: UUID.Bytes): SortedSet<UUID.Bytes, UUIDMapper> => {
const uuidMap = UUID.newSet<UUIDMapper>(({source}) => source)
uuidMap.addMany([
...audioUnitBoxes
.filter(({output: {targetAddress}}) => targetAddress.nonEmpty())
.map(box => ({
source: box.output.targetAddress.unwrap().uuid,
target: masterBusBoxUUID
})),
...audioUnitBoxes
.map(box => ({
source: box.collection.targetAddress.unwrap("AudioUnitBox was not connected to a RootBox").uuid,
target: rootBoxUUID
})),
...audioUnitBoxes
.map(box => ({
source: box.address.uuid,
target: UUID.generate()
})),
...dependencies
.map(box => ({
source: box.address.uuid,
target: box.resource === "preserved" ? box.address.uuid : UUID.generate()
}))
])
return uuidMap
}
const copy = (uuidMap: SortedSet<UUID.Bytes, UUIDMapper>,
boxGraph: BoxGraph,
audioUnitBoxes: ReadonlyArray<AudioUnitBox>,
dependencies: ReadonlyArray<Box>) => {
const existingExternalResourceUUIDs = UUID.newSet<UUID.Bytes>(uuid => uuid)
dependencies.forEach((source: Box) => {
if (source.resource === "preserved" && boxGraph.findBox(source.address.uuid).nonEmpty()) {
existingExternalResourceUUIDs.add(source.address.uuid)
}
})
const isOwnedByExistingExternalResource = (box: Box): boolean => {
for (const [pointer, targetAddress] of box.outgoingEdges()) {
if (pointer.mandatory && !targetAddress.isBox()) {
if (existingExternalResourceUUIDs.hasKey(targetAddress.uuid)) {
return true
}
}
}
return false
}
PointerField.decodeWith({
map: (_pointer: PointerField, newAddress: Option<Address>): Option<Address> =>
newAddress.map(address => uuidMap.opt(address.uuid).match({
none: () => address,
some: ({target}) => address.moveTo(target)
}))
}, () => {
audioUnitBoxes
.forEach((source: AudioUnitBox) => {
const input = new ByteArrayInput(source.toArrayBuffer())
const key = source.name as keyof BoxIO.TypeMap
const uuid = uuidMap.get(source.address.uuid).target
boxGraph.createBox(key, uuid, box => box.read(input))
})
dependencies
.forEach((source: Box) => {
if (existingExternalResourceUUIDs.hasKey(source.address.uuid)) {return}
if (isOwnedByExistingExternalResource(source)) {return}
const input = new ByteArrayInput(source.toArrayBuffer())
const key = source.name as keyof BoxIO.TypeMap
const uuid = uuidMap.get(source.address.uuid).target
boxGraph.createBox(key, uuid, box => box.read(input))
})
})
}
const reorderAudioUnits = (uuidMap: SortedSet<UUID.Bytes, UUIDMapper>,
audioUnitBoxes: ReadonlyArray<AudioUnitBox>,
rootBox: RootBox): void => audioUnitBoxes
.toSorted(compareIndex)
.map(source => (asInstanceOf(rootBox.graph
.findBox(uuidMap.get(source.address.uuid).target)
.unwrap("Target Track has not been copied"), AudioUnitBox)))
.forEach((target) =>
IndexedBox.collectIndexedBoxes(rootBox.audioUnits, AudioUnitBox).toSorted((a, b) => {
const orderA = AudioUnitOrdering[a.type.getValue()]
const orderB = AudioUnitOrdering[b.type.getValue()]
const orderDifference = orderA - orderB
return orderDifference === 0 ? b === target ? -1 : 1 : orderDifference
}).forEach((box, index) => box.index.setValue(index)))
}
@@ -10,11 +10,11 @@ import {
TransientMarkerBox
} from "@opendaw/studio-boxes"
import {AudioUnitType} from "@opendaw/studio-enums"
import {ProjectSkeleton} from "./ProjectSkeleton"
import {ProjectUtils} from "./ProjectUtils"
import {ProjectSkeleton} from "../project/ProjectSkeleton"
import {TrackType} from "../timeline/TrackType"
import {TransferAudioUnits} from "./TransferAudioUnits"
describe("ProjectUtils.extractAudioUnits", () => {
describe("TransferAudioUnits.transfer", () => {
let skeleton: ProjectSkeleton
beforeEach(() => {
@@ -24,231 +24,186 @@ describe("ProjectUtils.extractAudioUnits", () => {
})
})
const createAudioUnitWithInstrument = (skeleton: ProjectSkeleton): {
const createAudioUnitWithInstrument = (target: ProjectSkeleton): {
audioUnitBox: AudioUnitBox,
instrumentBox: TapeDeviceBox,
captureBox: CaptureAudioBox
} => {
const {boxGraph, mandatoryBoxes: {rootBox, primaryAudioBus}} = skeleton
const {boxGraph, mandatoryBoxes: {rootBox, primaryAudioBus}} = target
let audioUnitBox!: AudioUnitBox
let instrumentBox!: TapeDeviceBox
let captureBox!: CaptureAudioBox
boxGraph.beginTransaction()
audioUnitBox = AudioUnitBox.create(boxGraph, UUID.generate(), box => {
box.type.setValue(AudioUnitType.Instrument)
box.collection.refer(rootBox.audioUnits)
box.output.refer(primaryAudioBus.input)
box.index.setValue(1)
})
captureBox = CaptureAudioBox.create(boxGraph, UUID.generate())
audioUnitBox.capture.refer(captureBox)
instrumentBox = TapeDeviceBox.create(boxGraph, UUID.generate(), box => {
box.label.setValue("Test Tape")
box.host.refer(audioUnitBox.input)
})
boxGraph.endTransaction()
return {audioUnitBox, instrumentBox, captureBox}
}
const createAudioRegion = (
skeleton: ProjectSkeleton,
audioUnitBox: AudioUnitBox
): {trackBox: TrackBox, regionBox: AudioRegionBox, audioFileBox: AudioFileBox} => {
const {boxGraph} = skeleton
const createAudioRegion = (target: ProjectSkeleton, audioUnitBox: AudioUnitBox): {
trackBox: TrackBox,
regionBox: AudioRegionBox,
audioFileBox: AudioFileBox
} => {
const {boxGraph} = target
let trackBox!: TrackBox
let regionBox!: AudioRegionBox
let audioFileBox!: AudioFileBox
boxGraph.beginTransaction()
trackBox = TrackBox.create(boxGraph, UUID.generate(), box => {
box.type.setValue(TrackType.Audio)
box.tracks.refer(audioUnitBox.tracks)
box.target.refer(audioUnitBox)
box.index.setValue(0)
})
audioFileBox = AudioFileBox.create(boxGraph, UUID.generate(), box => {
box.startInSeconds.setValue(0.0)
box.endInSeconds.setValue(10.0)
box.fileName.setValue("test-audio.wav")
})
regionBox = AudioRegionBox.create(boxGraph, UUID.generate(), box => {
box.regions.refer(trackBox.regions)
box.file.refer(audioFileBox)
box.position.setValue(0)
box.duration.setValue(1000)
})
boxGraph.endTransaction()
return {trackBox, regionBox, audioFileBox}
}
const addTransientMarkers = (skeleton: ProjectSkeleton, audioFileBox: AudioFileBox, count: number): TransientMarkerBox[] => {
const {boxGraph} = skeleton
const addTransientMarkers = (target: ProjectSkeleton, audioFileBox: AudioFileBox, count: number): TransientMarkerBox[] => {
const {boxGraph} = target
const markers: TransientMarkerBox[] = []
boxGraph.beginTransaction()
for (let i = 0; i < count; i++) {
for (let index = 0; index < count; index++) {
markers.push(TransientMarkerBox.create(boxGraph, UUID.generate(), box => {
box.owner.refer(audioFileBox.transientMarkers)
box.position.setValue(i * 0.1)
box.position.setValue(index * 0.1)
}))
}
boxGraph.endTransaction()
return markers
}
it("should create new AudioUnitBox with new UUID", () => {
it("creates new AudioUnitBox with new UUID", () => {
const {audioUnitBox} = createAudioUnitWithInstrument(skeleton)
const originalUUID = audioUnitBox.address.uuid
skeleton.boxGraph.beginTransaction()
const [copiedAudioUnit] = ProjectUtils.extractAudioUnits([audioUnitBox], skeleton)
const [copiedAudioUnit] = TransferAudioUnits.transfer([audioUnitBox], skeleton)
skeleton.boxGraph.endTransaction()
expect(copiedAudioUnit).toBeDefined()
expect(UUID.equals(copiedAudioUnit.address.uuid, originalUUID)).toBe(false)
})
it("should copy instrument with AudioUnitBox", () => {
it("copies instrument with AudioUnitBox", () => {
const {audioUnitBox, instrumentBox} = createAudioUnitWithInstrument(skeleton)
const originalInstrumentUUID = instrumentBox.address.uuid
skeleton.boxGraph.beginTransaction()
const [copiedAudioUnit] = ProjectUtils.extractAudioUnits([audioUnitBox], skeleton)
const [copiedAudioUnit] = TransferAudioUnits.transfer([audioUnitBox], skeleton)
skeleton.boxGraph.endTransaction()
// Find the copied instrument
const copiedInstrument = copiedAudioUnit.input.pointerHub.incoming().at(0)?.box
expect(copiedInstrument).toBeDefined()
expect(copiedInstrument).toBeInstanceOf(TapeDeviceBox)
expect(UUID.equals(copiedInstrument!.address.uuid, originalInstrumentUUID)).toBe(false)
})
it("should preserve AudioFileBox UUID (shared resource)", () => {
it("preserves AudioFileBox UUID (shared resource)", () => {
const {audioUnitBox} = createAudioUnitWithInstrument(skeleton)
const {audioFileBox} = createAudioRegion(skeleton, audioUnitBox)
const originalFileUUID = audioFileBox.address.uuid
skeleton.boxGraph.beginTransaction()
const [copiedAudioUnit] = ProjectUtils.extractAudioUnits([audioUnitBox], skeleton)
const [copiedAudioUnit] = TransferAudioUnits.transfer([audioUnitBox], skeleton)
skeleton.boxGraph.endTransaction()
// Find the copied region's file reference
const copiedTrack = copiedAudioUnit.tracks.pointerHub.incoming().at(0)?.box as TrackBox
const copiedRegion = copiedTrack?.regions.pointerHub.incoming().at(0)?.box as AudioRegionBox
const copiedFileUUID = copiedRegion?.file.targetAddress.unwrap().uuid
expect(UUID.equals(copiedFileUUID, originalFileUUID)).toBe(true)
})
it("should not duplicate regions when copying twice", () => {
it("does not duplicate regions when copying twice", () => {
const {audioUnitBox} = createAudioUnitWithInstrument(skeleton)
createAudioRegion(skeleton, audioUnitBox)
// First copy
skeleton.boxGraph.beginTransaction()
const [firstCopy] = ProjectUtils.extractAudioUnits([audioUnitBox], skeleton)
const [firstCopy] = TransferAudioUnits.transfer([audioUnitBox], skeleton)
skeleton.boxGraph.endTransaction()
// Count regions in first copy
const firstCopyTrack = firstCopy.tracks.pointerHub.incoming().at(0)?.box as TrackBox
const firstCopyRegionCount = firstCopyTrack?.regions.pointerHub.incoming().length ?? 0
// Second copy
skeleton.boxGraph.beginTransaction()
const [secondCopy] = ProjectUtils.extractAudioUnits([audioUnitBox], skeleton)
const [secondCopy] = TransferAudioUnits.transfer([audioUnitBox], skeleton)
skeleton.boxGraph.endTransaction()
// Count regions in second copy
const secondCopyTrack = secondCopy.tracks.pointerHub.incoming().at(0)?.box as TrackBox
const secondCopyRegionCount = secondCopyTrack?.regions.pointerHub.incoming().length ?? 0
// Verify first copy still has same number of regions (not modified)
const firstCopyRegionCountAfter = firstCopyTrack?.regions.pointerHub.incoming().length ?? 0
expect(firstCopyRegionCount).toBe(1)
expect(secondCopyRegionCount).toBe(1)
expect(firstCopyRegionCountAfter).toBe(1)
})
it("should not include regions from previous copies", () => {
it("does not include regions from previous copies", () => {
const {audioUnitBox} = createAudioUnitWithInstrument(skeleton)
createAudioRegion(skeleton, audioUnitBox)
// Count total AudioRegionBoxes before copies
const initialRegionCount = skeleton.boxGraph.boxes()
.filter(box => box instanceof AudioRegionBox).length
// First copy
skeleton.boxGraph.beginTransaction()
ProjectUtils.extractAudioUnits([audioUnitBox], skeleton)
TransferAudioUnits.transfer([audioUnitBox], skeleton)
skeleton.boxGraph.endTransaction()
const afterFirstCopyCount = skeleton.boxGraph.boxes()
.filter(box => box instanceof AudioRegionBox).length
// Second copy
skeleton.boxGraph.beginTransaction()
ProjectUtils.extractAudioUnits([audioUnitBox], skeleton)
TransferAudioUnits.transfer([audioUnitBox], skeleton)
skeleton.boxGraph.endTransaction()
const afterSecondCopyCount = skeleton.boxGraph.boxes()
.filter(box => box instanceof AudioRegionBox).length
// Each copy should add exactly 1 region
expect(afterFirstCopyCount).toBe(initialRegionCount + 1)
expect(afterSecondCopyCount).toBe(initialRegionCount + 2)
})
it("should skip TransientMarkerBox when AudioFileBox already exists", () => {
it("skips TransientMarkerBox when AudioFileBox already exists", () => {
const {audioUnitBox} = createAudioUnitWithInstrument(skeleton)
const {audioFileBox} = createAudioRegion(skeleton, audioUnitBox)
addTransientMarkers(skeleton, audioFileBox, 5)
const initialMarkerCount = skeleton.boxGraph.boxes()
.filter(box => box instanceof TransientMarkerBox).length
// Copy should not duplicate transient markers
skeleton.boxGraph.beginTransaction()
ProjectUtils.extractAudioUnits([audioUnitBox], skeleton)
TransferAudioUnits.transfer([audioUnitBox], skeleton)
skeleton.boxGraph.endTransaction()
const afterCopyMarkerCount = skeleton.boxGraph.boxes()
.filter(box => box instanceof TransientMarkerBox).length
expect(initialMarkerCount).toBe(5)
expect(afterCopyMarkerCount).toBe(5) // No new markers created
expect(afterCopyMarkerCount).toBe(5)
})
it("should handle multiple copies without exponential growth", () => {
it("handles multiple copies without exponential growth", () => {
const {audioUnitBox} = createAudioUnitWithInstrument(skeleton)
createAudioRegion(skeleton, audioUnitBox)
const copyCount = 5
for (let i = 0; i < copyCount; i++) {
for (let index = 0; index < copyCount; index++) {
skeleton.boxGraph.beginTransaction()
ProjectUtils.extractAudioUnits([audioUnitBox], skeleton)
TransferAudioUnits.transfer([audioUnitBox], skeleton)
skeleton.boxGraph.endTransaction()
}
// Count all AudioRegionBoxes
const totalRegionCount = skeleton.boxGraph.boxes()
.filter(box => box instanceof AudioRegionBox).length
// Should be: 1 original + 5 copies = 6 total
expect(totalRegionCount).toBe(1 + copyCount)
})
it("copies to target graph in cross-project scenario", () => {
const source = ProjectSkeleton.empty({createDefaultUser: false, createOutputCompressor: false})
const {audioUnitBox} = createAudioUnitWithInstrument(source)
skeleton.boxGraph.beginTransaction()
const [copiedAudioUnit] = TransferAudioUnits.transfer([audioUnitBox], skeleton)
skeleton.boxGraph.endTransaction()
expect(copiedAudioUnit.graph).toBe(skeleton.boxGraph)
expect(copiedAudioUnit.graph).not.toBe(source.boxGraph)
})
})
@@ -0,0 +1,36 @@
import {asInstanceOf} from "@opendaw/lib-std"
import {Box} from "@opendaw/lib-box"
import {AudioUnitBox} from "@opendaw/studio-boxes"
import {ProjectSkeleton} from "../project/ProjectSkeleton"
import {TransferUtils} from "./TransferUtils"
export namespace TransferAudioUnits {
/**
* Copies audio units and their dependencies to a target project.
* Preserved resources already present in the target graph are shared, not duplicated.
* @returns the newly created audio unit boxes in the target graph
*/
export const transfer = (audioUnitBoxes: ReadonlyArray<AudioUnitBox>,
{boxGraph: targetBoxGraph, mandatoryBoxes: {primaryAudioBus, rootBox}}: ProjectSkeleton,
options: {
includeAux?: boolean,
includeBus?: boolean,
excludeTimeline?: boolean,
} = {}): ReadonlyArray<AudioUnitBox> => {
const excludeBox = (box: Box): boolean =>
TransferUtils.shouldExclude(box)
|| (options?.excludeTimeline === true && TransferUtils.excludeTimelinePredicate(box))
const dependencies = Array.from(audioUnitBoxes[0].graph.dependenciesOf(audioUnitBoxes, {
alwaysFollowMandatory: true,
stopAtResources: true,
excludeBox
}).boxes)
const uuidMap = TransferUtils.generateMap(
audioUnitBoxes, dependencies, rootBox.audioUnits.address.uuid, primaryAudioBus.address.uuid)
TransferUtils.copyBoxes(uuidMap, targetBoxGraph, audioUnitBoxes, dependencies)
TransferUtils.reorderAudioUnits(uuidMap, audioUnitBoxes, rootBox)
return audioUnitBoxes.map(source => asInstanceOf(rootBox.graph
.findBox(uuidMap.get(source.address.uuid).target)
.unwrap("Target AudioUnit has not been copied"), AudioUnitBox))
}
}
@@ -1,46 +1,26 @@
import "../../polyfill"
import {afterEach, beforeEach, describe, expect, it} from "vitest"
import {Terminable, UUID} from "@opendaw/lib-std"
import {describe, expect, it, beforeEach} from "vitest"
import {UUID} from "@opendaw/lib-std"
import {AudioUnitBox, CaptureAudioBox, TrackBox, ValueEventCollectionBox, ValueRegionBox} from "@opendaw/studio-boxes"
import {AudioUnitType} from "@opendaw/studio-enums"
import {ProjectSkeleton, TrackType} from "@opendaw/studio-adapters"
import {Project, ProjectEnv} from "../../project"
import {ProjectSkeleton} from "../project/ProjectSkeleton"
import {TrackType} from "../timeline/TrackType"
import {TransferRegions} from "./TransferRegions"
const createEnv = (): ProjectEnv => ({
audioContext: {} as AudioContext,
audioWorklets: {} as ProjectEnv["audioWorklets"],
sampleManager: {
getOrCreate: () => ({}) as any,
record: () => {},
remove: () => {},
invalidate: () => {},
register: () => Terminable.Empty
},
soundfontManager: {
getOrCreate: () => ({}) as any,
remove: () => {},
invalidate: () => {}
}
})
const createSkeleton = (): ProjectSkeleton =>
ProjectSkeleton.empty({createDefaultUser: true, createOutputCompressor: false})
const createProject = (): Project => {
const skeleton = ProjectSkeleton.empty({createDefaultUser: true, createOutputCompressor: false})
return Project.fromSkeleton(createEnv(), skeleton, false)
}
const createTrackWithRegion = (project: Project, position: number = 100, duration: number = 200): {
const createTrackWithRegion = (skeleton: ProjectSkeleton, position: number = 100, duration: number = 200): {
audioUnitBox: AudioUnitBox,
trackBox: TrackBox,
regionBox: ValueRegionBox,
collectionBox: ValueEventCollectionBox
} => {
const {boxGraph} = project
const {boxGraph, mandatoryBoxes: {rootBox, primaryAudioBus}} = skeleton
const captureBox = CaptureAudioBox.create(boxGraph, UUID.generate())
const audioUnitBox = AudioUnitBox.create(boxGraph, UUID.generate(), box => {
box.type.setValue(AudioUnitType.Instrument)
box.collection.refer(project.rootBox.audioUnits)
box.output.refer(project.masterBusBox.input)
box.collection.refer(rootBox.audioUnits)
box.output.refer(primaryAudioBus.input)
box.capture.refer(captureBox)
box.index.setValue(1)
})
@@ -63,16 +43,15 @@ const createTrackWithRegion = (project: Project, position: number = 100, duratio
}
describe("TransferRegions.transfer", () => {
let project: Project
let skeleton: ProjectSkeleton
beforeEach(() => {project = createProject()})
afterEach(() => project.terminate())
beforeEach(() => {skeleton = createSkeleton()})
describe("same graph", () => {
it("copies region to same track at new position", () => {
const {boxGraph} = project
const {boxGraph} = skeleton
boxGraph.beginTransaction()
const {trackBox, regionBox} = createTrackWithRegion(project)
const {trackBox, regionBox} = createTrackWithRegion(skeleton)
boxGraph.endTransaction()
boxGraph.beginTransaction()
const copied = TransferRegions.transfer(regionBox, trackBox, 500, false)
@@ -84,9 +63,9 @@ describe("TransferRegions.transfer", () => {
})
it("copies region to different track", () => {
const {boxGraph} = project
const {boxGraph} = skeleton
boxGraph.beginTransaction()
const {trackBox: trackA, regionBox, audioUnitBox} = createTrackWithRegion(project)
const {trackBox: trackA, regionBox, audioUnitBox} = createTrackWithRegion(skeleton)
const trackB = TrackBox.create(boxGraph, UUID.generate(), box => {
box.type.setValue(TrackType.Value)
box.tracks.refer(audioUnitBox.tracks)
@@ -106,9 +85,9 @@ describe("TransferRegions.transfer", () => {
})
it("creates new UUID for copied region", () => {
const {boxGraph} = project
const {boxGraph} = skeleton
boxGraph.beginTransaction()
const {trackBox, regionBox} = createTrackWithRegion(project)
const {trackBox, regionBox} = createTrackWithRegion(skeleton)
boxGraph.endTransaction()
boxGraph.beginTransaction()
const copied = TransferRegions.transfer(regionBox, trackBox, 500, false)
@@ -117,9 +96,9 @@ describe("TransferRegions.transfer", () => {
})
it("preserves region properties", () => {
const {boxGraph} = project
const {boxGraph} = skeleton
boxGraph.beginTransaction()
const {trackBox, regionBox} = createTrackWithRegion(project)
const {trackBox, regionBox} = createTrackWithRegion(skeleton)
boxGraph.endTransaction()
boxGraph.beginTransaction()
const copied = TransferRegions.transfer(regionBox, trackBox, 500, false)
@@ -130,9 +109,9 @@ describe("TransferRegions.transfer", () => {
})
it("deletes source when deleteSource is true", () => {
const {boxGraph} = project
const {boxGraph} = skeleton
boxGraph.beginTransaction()
const {trackBox, regionBox} = createTrackWithRegion(project)
const {trackBox, regionBox} = createTrackWithRegion(skeleton)
boxGraph.endTransaction()
boxGraph.beginTransaction()
TransferRegions.transfer(regionBox, trackBox, 500)
@@ -143,9 +122,9 @@ describe("TransferRegions.transfer", () => {
})
it("keeps source when deleteSource is false", () => {
const {boxGraph} = project
const {boxGraph} = skeleton
boxGraph.beginTransaction()
const {trackBox, regionBox} = createTrackWithRegion(project)
const {trackBox, regionBox} = createTrackWithRegion(skeleton)
boxGraph.endTransaction()
boxGraph.beginTransaction()
TransferRegions.transfer(regionBox, trackBox, 500, false)
@@ -155,9 +134,9 @@ describe("TransferRegions.transfer", () => {
})
it("copies event collection dependency with new UUID", () => {
const {boxGraph} = project
const {boxGraph} = skeleton
boxGraph.beginTransaction()
const {trackBox, regionBox, collectionBox} = createTrackWithRegion(project)
const {trackBox, regionBox, collectionBox} = createTrackWithRegion(skeleton)
boxGraph.endTransaction()
boxGraph.beginTransaction()
const copied = TransferRegions.transfer(regionBox, trackBox, 500, false)
@@ -170,19 +149,18 @@ describe("TransferRegions.transfer", () => {
})
describe("cross graph", () => {
let sourceProject: Project
let sourceSkeleton: ProjectSkeleton
beforeEach(() => {sourceProject = createProject()})
afterEach(() => sourceProject.terminate())
beforeEach(() => {sourceSkeleton = createSkeleton()})
it("copies region to target graph at given position", () => {
const sourceGraph = sourceProject.boxGraph
const sourceGraph = sourceSkeleton.boxGraph
sourceGraph.beginTransaction()
const {regionBox: sourceRegion} = createTrackWithRegion(sourceProject)
const {regionBox: sourceRegion} = createTrackWithRegion(sourceSkeleton)
sourceGraph.endTransaction()
const {boxGraph} = project
const {boxGraph} = skeleton
boxGraph.beginTransaction()
const {trackBox: targetTrack} = createTrackWithRegion(project)
const {trackBox: targetTrack} = createTrackWithRegion(skeleton)
boxGraph.endTransaction()
const regionsBefore = targetTrack.regions.pointerHub.incoming().length
boxGraph.beginTransaction()
@@ -197,13 +175,13 @@ describe("TransferRegions.transfer", () => {
})
it("creates new UUID for copied region", () => {
const sourceGraph = sourceProject.boxGraph
const sourceGraph = sourceSkeleton.boxGraph
sourceGraph.beginTransaction()
const {regionBox: sourceRegion} = createTrackWithRegion(sourceProject)
const {regionBox: sourceRegion} = createTrackWithRegion(sourceSkeleton)
sourceGraph.endTransaction()
const {boxGraph} = project
const {boxGraph} = skeleton
boxGraph.beginTransaction()
const {trackBox: targetTrack} = createTrackWithRegion(project)
const {trackBox: targetTrack} = createTrackWithRegion(skeleton)
boxGraph.endTransaction()
boxGraph.beginTransaction()
const copied = TransferRegions.transfer(sourceRegion, targetTrack, 500, false)
@@ -212,13 +190,13 @@ describe("TransferRegions.transfer", () => {
})
it("preserves region properties", () => {
const sourceGraph = sourceProject.boxGraph
const sourceGraph = sourceSkeleton.boxGraph
sourceGraph.beginTransaction()
const {regionBox: sourceRegion} = createTrackWithRegion(sourceProject, 100, 300)
const {regionBox: sourceRegion} = createTrackWithRegion(sourceSkeleton, 100, 300)
sourceGraph.endTransaction()
const {boxGraph} = project
const {boxGraph} = skeleton
boxGraph.beginTransaction()
const {trackBox: targetTrack} = createTrackWithRegion(project)
const {trackBox: targetTrack} = createTrackWithRegion(skeleton)
boxGraph.endTransaction()
boxGraph.beginTransaction()
const copied = TransferRegions.transfer(sourceRegion, targetTrack, 500, false)
@@ -229,13 +207,13 @@ describe("TransferRegions.transfer", () => {
})
it("keeps source in source graph when deleteSource is false", () => {
const sourceGraph = sourceProject.boxGraph
const sourceGraph = sourceSkeleton.boxGraph
sourceGraph.beginTransaction()
const {regionBox: sourceRegion} = createTrackWithRegion(sourceProject)
const {regionBox: sourceRegion} = createTrackWithRegion(sourceSkeleton)
sourceGraph.endTransaction()
const {boxGraph} = project
const {boxGraph} = skeleton
boxGraph.beginTransaction()
const {trackBox: targetTrack} = createTrackWithRegion(project)
const {trackBox: targetTrack} = createTrackWithRegion(skeleton)
boxGraph.endTransaction()
boxGraph.beginTransaction()
TransferRegions.transfer(sourceRegion, targetTrack, 500, false)
@@ -245,13 +223,13 @@ describe("TransferRegions.transfer", () => {
})
it("deletes source when deleteSource is true", () => {
const sourceGraph = sourceProject.boxGraph
const sourceGraph = sourceSkeleton.boxGraph
sourceGraph.beginTransaction()
const {regionBox: sourceRegion} = createTrackWithRegion(sourceProject)
const {regionBox: sourceRegion} = createTrackWithRegion(sourceSkeleton)
sourceGraph.endTransaction()
const {boxGraph} = project
const {boxGraph} = skeleton
boxGraph.beginTransaction()
const {trackBox: targetTrack} = createTrackWithRegion(project)
const {trackBox: targetTrack} = createTrackWithRegion(skeleton)
boxGraph.endTransaction()
sourceGraph.beginTransaction()
boxGraph.beginTransaction()
@@ -3,7 +3,7 @@ import {Box, PointerField} from "@opendaw/lib-box"
import {ppqn} from "@opendaw/lib-dsp"
import {Pointers} from "@opendaw/studio-enums"
import {BoxIO, TrackBox} from "@opendaw/studio-boxes"
import {AnyRegionBox, UnionBoxTypes} from "@opendaw/studio-adapters"
import {AnyRegionBox, UnionBoxTypes} from "../unions"
export namespace TransferRegions {
/**
@@ -76,4 +76,4 @@ export namespace TransferRegions {
if (deleteSource) {region.delete()}
return asDefined<AnyRegionBox>(result, "Failed to create region copy")
}
}
}
@@ -0,0 +1,164 @@
import {ppqn} from "@opendaw/lib-dsp"
import {
Arrays,
asInstanceOf,
assert,
ByteArrayInput,
isInstanceOf,
Option,
Predicate,
SetMultimap,
SortedSet,
UUID
} from "@opendaw/lib-std"
import {AudioUnitBox, AuxSendBox, BoxIO, BoxVisitor, RootBox, TrackBox} from "@opendaw/studio-boxes"
import {Address, Box, BoxGraph, IndexedBox, PointerField} from "@opendaw/lib-box"
import {ProjectSkeleton} from "../project/ProjectSkeleton"
import {AnyRegionBox, UnionBoxTypes} from "../unions"
import {AudioUnitOrdering} from "../factories/AudioUnitOrdering"
export namespace TransferUtils {
export type UUIDMapper = { source: UUID.Bytes, target: UUID.Bytes }
const isSameGraph = ({graph: a}: Box, {graph: b}: Box): boolean => a === b
const compareIndex = (a: IndexedBox, b: IndexedBox) => a.index.getValue() - b.index.getValue()
export const excludeTimelinePredicate = (box: Box): boolean =>
box.accept<BoxVisitor<boolean>>({visitTrackBox: () => true}) === true
export const shouldExclude = (box: Box): boolean => box.ephemeral || box.name === AuxSendBox.ClassName
export const generateMap = (audioUnitBoxes: ReadonlyArray<AudioUnitBox>,
dependencies: ReadonlyArray<Box>,
rootBoxUUID: UUID.Bytes,
masterBusBoxUUID: UUID.Bytes): SortedSet<UUID.Bytes, UUIDMapper> => {
const uuidMap = UUID.newSet<UUIDMapper>(({source}) => source)
uuidMap.addMany([
...audioUnitBoxes
.filter(({output: {targetAddress}}) => targetAddress.nonEmpty())
.map(box => ({
source: box.output.targetAddress.unwrap().uuid,
target: masterBusBoxUUID
})),
...audioUnitBoxes
.map(box => ({
source: box.collection.targetAddress.unwrap("AudioUnitBox was not connected to a RootBox").uuid,
target: rootBoxUUID
})),
...audioUnitBoxes
.map(box => ({
source: box.address.uuid,
target: UUID.generate()
})),
...dependencies
.map(box => ({
source: box.address.uuid,
target: box.resource === "preserved" ? box.address.uuid : UUID.generate()
}))
])
return uuidMap
}
export const copyBoxes = (uuidMap: SortedSet<UUID.Bytes, UUIDMapper>,
targetBoxGraph: BoxGraph,
audioUnitBoxes: ReadonlyArray<AudioUnitBox>,
dependencies: ReadonlyArray<Box>): void => {
const existingPreservedUuids = UUID.newSet<UUID.Bytes>(uuid => uuid)
dependencies.forEach((source: Box) => {
if (source.resource === "preserved" && targetBoxGraph.findBox(source.address.uuid).nonEmpty()) {
existingPreservedUuids.add(source.address.uuid)
}
})
const isOwnedByExistingPreserved = (box: Box): boolean => {
for (const [pointer, targetAddress] of box.outgoingEdges()) {
if (pointer.mandatory && !targetAddress.isBox()) {
if (existingPreservedUuids.hasKey(targetAddress.uuid)) {return true}
}
}
return false
}
PointerField.decodeWith({
map: (_pointer: PointerField, address: Option<Address>): Option<Address> =>
address.map(addr => uuidMap.opt(addr.uuid).match({
none: () => addr,
some: ({target}) => addr.moveTo(target)
}))
}, () => {
audioUnitBoxes.forEach((source: AudioUnitBox) => {
const input = new ByteArrayInput(source.toArrayBuffer())
const uuid = uuidMap.get(source.address.uuid).target
targetBoxGraph.createBox(source.name as keyof BoxIO.TypeMap, uuid, box => box.read(input))
})
dependencies.forEach((source: Box) => {
if (existingPreservedUuids.hasKey(source.address.uuid)) {return}
if (isOwnedByExistingPreserved(source)) {return}
const input = new ByteArrayInput(source.toArrayBuffer())
const uuid = uuidMap.get(source.address.uuid).target
targetBoxGraph.createBox(source.name as keyof BoxIO.TypeMap, uuid, box => box.read(input))
})
})
}
export const reorderAudioUnits = (uuidMap: SortedSet<UUID.Bytes, UUIDMapper>,
audioUnitBoxes: ReadonlyArray<AudioUnitBox>,
rootBox: RootBox): void => audioUnitBoxes
.toSorted(compareIndex)
.map(source => asInstanceOf(rootBox.graph
.findBox(uuidMap.get(source.address.uuid).target)
.unwrap("Target AudioUnit has not been copied"), AudioUnitBox))
.forEach((target) =>
IndexedBox.collectIndexedBoxes(rootBox.audioUnits, AudioUnitBox).toSorted((a, b) => {
const orderA = AudioUnitOrdering[a.type.getValue()]
const orderB = AudioUnitOrdering[b.type.getValue()]
const orderDifference = orderA - orderB
return orderDifference === 0 ? b === target ? -1 : 1 : orderDifference
}).forEach((box, index) => box.index.setValue(index)))
export const extractRegions = (regionBoxes: ReadonlyArray<AnyRegionBox>,
{boxGraph, mandatoryBoxes: {primaryAudioBus, rootBox}}: ProjectSkeleton,
insertPosition: ppqn = 0.0): void => {
assert(Arrays.satisfy(regionBoxes, isSameGraph),
"Region smust be from the same BoxGraph")
const regionBoxSet = new Set<AnyRegionBox>(regionBoxes)
const trackBoxSet = new Set<TrackBox>()
const audioUnitBoxSet = new SetMultimap<AudioUnitBox, TrackBox>()
regionBoxes.forEach(regionBox => {
const trackBox = asInstanceOf(regionBox.regions.targetVertex.unwrap().box, TrackBox)
trackBoxSet.add(trackBox)
const audioUnitBox = asInstanceOf(trackBox.tracks.targetVertex.unwrap().box, AudioUnitBox)
audioUnitBoxSet.add(audioUnitBox, trackBox)
})
console.debug(`Found ${audioUnitBoxSet.keyCount()} audioUnits`)
console.debug(`Found ${trackBoxSet.size} tracks`)
const audioUnitBoxes = [...audioUnitBoxSet.keys()]
const excludeBox: Predicate<Box> = (box: Box): boolean =>
shouldExclude(box)
|| (isInstanceOf(box, TrackBox) && !trackBoxSet.has(box))
|| (UnionBoxTypes.isRegionBox(box) && !regionBoxSet.has(box))
const dependencies = Array.from(audioUnitBoxes[0].graph.dependenciesOf(audioUnitBoxes, {
alwaysFollowMandatory: true,
stopAtResources: true,
excludeBox
}).boxes)
const uuidMap = generateMap(
audioUnitBoxes, dependencies, rootBox.audioUnits.address.uuid, primaryAudioBus.address.uuid)
copyBoxes(uuidMap, boxGraph, audioUnitBoxes, dependencies)
reorderAudioUnits(uuidMap, audioUnitBoxes, rootBox)
audioUnitBoxSet.forEach((_, trackBoxes) => [...trackBoxes]
.sort(compareIndex)
.forEach((source: TrackBox, index) => {
const box = boxGraph
.findBox(uuidMap.get(source.address.uuid).target)
.unwrap("Target Track has not been copied")
asInstanceOf(box, TrackBox).index.setValue(index)
}))
const minPosition = regionBoxes.reduce((min, region) =>
Math.min(min, region.position.getValue()), Number.MAX_VALUE)
const delta = insertPosition - minPosition
regionBoxes.forEach((source: AnyRegionBox) => {
const box = boxGraph
.findBox(uuidMap.get(source.address.uuid).target)
.unwrap("Target Track has not been copied")
const {position} = UnionBoxTypes.asRegionBox(box)
position.setValue(position.getValue() + delta)
})
}
}
@@ -0,0 +1,3 @@
export * from "./TransferAudioUnits"
export * from "./TransferRegions"
export * from "./TransferUtils"
-1
View File
@@ -16,4 +16,3 @@ export * from "./timeline/TrackResolver"
export * from "./timeline/TimeGrid"
export * from "./timeline/TimelineFocus"
export * from "./timeline/TimelineRange"
export * from "./transfer"
@@ -1 +0,0 @@
export * from "./TransferRegions"
+2 -2
View File
@@ -679,7 +679,7 @@ describe("CopyBuffer", () => {
| `lib/box/src/graph.ts` | Modify | Add `stopAtResources` option to `dependenciesOf` |
| `forge-boxes/src/schema/std/AudioFileBox.ts` | Modify | Add `resource: "external"` |
| `forge-boxes/src/schema/std/SoundfontFileBox.ts` | Modify | Add `resource: "external"` |
| `adapters/src/project/ProjectUtils.ts` | Modify | Refactor to use `stopAtResources` |
| `adapters/src/project/TransferUtils.ts` | Modify | Refactor to use `stopAtResources` |
| `adapters/src/project/CopyBuffer.ts` | Create | CopyBuffer type + serialize/deserialize + clipboard read/write |
| `lib/box/src/graph.test.ts` | Modify | Add tests for `stopAtResources` |
| `adapters/src/project/ProjectUtils.test.ts` | Modify | Refactor tests to use new resource handling |
@@ -713,7 +713,7 @@ Phase 3: Dependency Collection
Phase 4: Refactor ProjectUtils
└── ProjectUtils.ts
└── TransferUtils.ts
Phase 5: Copy/Paste API