refactoring
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
+43
-88
@@ -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))
|
||||
}
|
||||
}
|
||||
+48
-70
@@ -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()
|
||||
+2
-2
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user