diff --git a/packages/docs/docs-dev/serialization/migration.md b/packages/docs/docs-dev/serialization/migration.md new file mode 100644 index 000000000..f3705cc25 --- /dev/null +++ b/packages/docs/docs-dev/serialization/migration.md @@ -0,0 +1,16 @@ +# Project Migration + +`ProjectMigration` upgrades legacy Studio project graphs to the current +schema. After decoding a project or importing a `.dawproject` archive, the +migration step inserts missing boxes and normalizes values so the rest of the +application can operate on a consistent structure. + +- Runs on the skeleton produced by `DawProjectImport.read` before the project + is opened. +- Creates required global helpers such as the `GrooveShuffleBox` and converts + outdated event representations. +- New migration rules can be added to `ProjectMigration.migrate` as the project + graph evolves. + +Return to the [serialization overview](./overview.md). + diff --git a/packages/docs/docs-user/workflows/dawproject.md b/packages/docs/docs-user/workflows/dawproject.md index be8edef75..a58dadee6 100644 --- a/packages/docs/docs-user/workflows/dawproject.md +++ b/packages/docs/docs-user/workflows/dawproject.md @@ -6,8 +6,10 @@ through the `.dawproject` format. 1. **Export a DAWproject file.** Choose *File → Export → DAWproject* to create an archive containing the current session and all referenced audio files. 2. **Import a DAWproject file.** Use *File → Import* and select a `.dawproject` - bundle to start a new session from it. + bundle to start a new session from it. Imported projects are automatically + migrated to the latest Studio schema. 3. **Edit and re-export.** Once imported, the project behaves like any other openDAW session and can be saved or exported again at any time. -For technical details see the [developer serialization guide](../../docs-dev/serialization/studio-dawproject.md). +For technical details see the [developer serialization guide](../../docs-dev/serialization/studio-dawproject.md) +and the [project migration reference](../../docs-dev/serialization/migration.md). diff --git a/packages/lib/dawproject/src/defaults.ts b/packages/lib/dawproject/src/defaults.ts index d6e3aefe0..0b9f360d5 100644 --- a/packages/lib/dawproject/src/defaults.ts +++ b/packages/lib/dawproject/src/defaults.ts @@ -1,4 +1,5 @@ /** + * @packageDocumentation * Core schema definitions for the DAWproject XML format. * * @remarks diff --git a/packages/lib/dawproject/src/utils.ts b/packages/lib/dawproject/src/utils.ts index a0fcde883..9d8e814b4 100644 --- a/packages/lib/dawproject/src/utils.ts +++ b/packages/lib/dawproject/src/utils.ts @@ -1,4 +1,8 @@ /* eslint-disable @typescript-eslint/no-namespace */ +/** + * @packageDocumentation + * Utility helpers for encoding and decoding parameter schemas used in DAWproject. + */ import { BooleanParameterSchema, RealParameterSchema, Unit } from "./defaults"; import { Xml } from "@opendaw/lib-xml"; import { asDefined } from "@opendaw/lib-std"; @@ -14,7 +18,10 @@ export namespace ParameterEncoder { /** * Encodes a boolean parameter. * - * @see {@link BooleanParameterSchema} + * @param id - Unique parameter identifier. + * @param value - Boolean value to store. + * @param name - Optional display name. + * @returns Serialized {@link BooleanParameterSchema} instance. */ export const bool = (id: string, value: boolean, name?: string) => Xml.element( @@ -29,7 +36,12 @@ export namespace ParameterEncoder { /** * Encodes a linear numeric parameter. * - * @see {@link RealParameterSchema} + * @param id - Unique parameter identifier. + * @param value - Linear value to encode. + * @param min - Optional minimum value. + * @param max - Optional maximum value. + * @param name - Optional display name. + * @returns Serialized {@link RealParameterSchema} instance. */ export const linear = ( id: string, @@ -53,7 +65,12 @@ export namespace ParameterEncoder { /** * Encodes a normalized numeric parameter. * - * @see {@link RealParameterSchema} + * @param id - Unique parameter identifier. + * @param value - Normalized value within the range `[min,max]`. + * @param min - Minimum value of the normalized range. + * @param max - Maximum value of the normalized range. + * @param name - Optional display name. + * @returns Serialized {@link RealParameterSchema} instance. */ export const normalized = ( id: string, @@ -84,6 +101,9 @@ export namespace ParameterDecoder { /** * Resolves the numeric value from a {@link RealParameterSchema}. * + * @param schema - Parameter schema to interpret. + * @returns Linearized numeric value. + * * @remarks * Normalized and semitone based parameters are converted to linear values. */ diff --git a/packages/studio/core/src/ProjectMigration.ts b/packages/studio/core/src/ProjectMigration.ts index a381c7c42..32fd68e74 100644 --- a/packages/studio/core/src/ProjectMigration.ts +++ b/packages/studio/core/src/ProjectMigration.ts @@ -19,6 +19,8 @@ import {Capture} from "./capture/Capture" export class ProjectMigration { /** * Applies all available migrations to the provided project skeleton. + * + * @param skeleton - The project graph and mandatory boxes to migrate. */ static migrate({boxGraph, mandatoryBoxes}: ProjectDecoder.Skeleton): void { const {rootBox} = mandatoryBoxes diff --git a/packages/studio/core/src/dawproject/AudioUnitExportLayout.ts b/packages/studio/core/src/dawproject/AudioUnitExportLayout.ts index a1ef08d3a..ed507c191 100644 --- a/packages/studio/core/src/dawproject/AudioUnitExportLayout.ts +++ b/packages/studio/core/src/dawproject/AudioUnitExportLayout.ts @@ -14,7 +14,12 @@ export namespace AudioUnitExportLayout { children: Array } - /** Build a hierarchy of tracks from the current set of audio units. */ + /** + * Build a hierarchy of tracks from the current set of audio units. + * + * @param audioUnits - All audio unit boxes to arrange. + * @returns Ordered track hierarchy describing the signal flow. + */ export const layout = (audioUnits: ReadonlyArray): Array => { const feedsInto = new ArrayMultimap() audioUnits.forEach(unit => { @@ -49,6 +54,14 @@ export namespace AudioUnitExportLayout { .filter(isDefined) } + /** + * Recursively assemble a track tree starting at the given audio unit. + * + * @param audioUnit - The root audio unit for this branch. + * @param feedsInto - Mapping of downstream connections. + * @param visited - Set used to break cycles. + * @returns The assembled track node or `null` when a cycle is detected. + */ const buildTrackRecursive = (audioUnit: AudioUnitBox, feedsInto: ArrayMultimap, visited: Set): Nullable => { @@ -65,6 +78,9 @@ export namespace AudioUnitExportLayout { /** * Debug helper that logs the computed track hierarchy to the console. + * + * @param tracks - The track layout produced by {@link layout}. + * @param indent - Current indentation level (used internally). */ export const printTrackStructure = (tracks: ReadonlyArray, indent = 0): void => { const spaces = " ".repeat(indent) diff --git a/packages/studio/core/src/dawproject/BuiltinDevices.ts b/packages/studio/core/src/dawproject/BuiltinDevices.ts index 69d80fedd..7b312119d 100644 --- a/packages/studio/core/src/dawproject/BuiltinDevices.ts +++ b/packages/studio/core/src/dawproject/BuiltinDevices.ts @@ -11,11 +11,18 @@ import {semitoneToHz} from "@opendaw/lib-dsp" export namespace BuiltinDevices { /** * Create a Revamp equalizer device from a DAWproject {@link EqualizerSchema}. + * + * @param boxGraph - Graph used to create the device and its bands. + * @param equalizer - Source schema describing the equalizer configuration. + * @param field - Host field the device will be inserted into. + * @param index - Position of the device within the host. + * @returns The constructed {@link RevampDeviceBox}. */ export const equalizer = (boxGraph: BoxGraph, equalizer: EqualizerSchema, field: Field | Field, index: int): RevampDeviceBox => { + /** Map DAWproject filter order to Revamp's order index. */ const mapOrder = (order?: int) => { switch (order) { case 1: @@ -31,6 +38,7 @@ export namespace BuiltinDevices { return 3 } } + /** Populate a pass band from the DAWproject schema. */ const readPass = (schema: BandSchema, pass: RevampPass) => { const {order, frequency, q, enabled} = pass order.setValue(mapOrder(schema.order)) @@ -38,6 +46,7 @@ export namespace BuiltinDevices { ifDefined(schema.Q?.value, value => q.setValue(value)) ifDefined(schema.enabled?.value, value => enabled.setValue(value)) } + /** Populate a shelf band from the DAWproject schema. */ const readShelf = (schema: BandSchema, pass: RevampShelf) => { const {frequency, gain, enabled} = pass frequency.setValue(ParameterDecoder.readValue(schema.freq)) diff --git a/packages/studio/core/src/dawproject/DawProject.ts b/packages/studio/core/src/dawproject/DawProject.ts index 517b2bf45..99feb47f9 100644 --- a/packages/studio/core/src/dawproject/DawProject.ts +++ b/packages/studio/core/src/dawproject/DawProject.ts @@ -25,6 +25,9 @@ export namespace DawProject { /** * Decode a DAWproject archive into its metadata, project XML and resource * lookup helpers. + * + * @param buffer - Raw bytes of the `.dawproject` zip archive. + * @returns Parsed metadata, project schema and resource provider. */ export const decode = async (buffer: ArrayBuffer | NonSharedBuffer): Promise<{ metaData: MetaDataSchema, @@ -59,6 +62,10 @@ export namespace DawProject { /** * Encode the current {@link Project} and associated metadata into a * `.dawproject` zip archive. + * + * @param project - The project graph to serialize. + * @param metaData - Metadata describing the project. + * @returns A binary zip archive in the DAWproject format. */ export const encode = (project: Project, metaData: MetaDataSchema): Promise => { const zip = new JSZip() diff --git a/packages/studio/core/src/dawproject/DawProjectExporter.ts b/packages/studio/core/src/dawproject/DawProjectExporter.ts index e0eea5e26..688a8aa07 100644 --- a/packages/studio/core/src/dawproject/DawProjectExporter.ts +++ b/packages/studio/core/src/dawproject/DawProjectExporter.ts @@ -55,12 +55,21 @@ import {DeviceIO} from "./DeviceIO" * Serializes the Studio project graph into the DAWproject XML schema. */ export namespace DawProjectExporter { - /** Callback used to store binary resources such as samples or presets. */ + /** Callback used to store binary resources such as samples or presets. + * + * @param path - Target path for the resource inside the archive. + * @param buffer - Binary data representing the resource. + * @returns A file reference pointing to the stored resource. + */ export interface ResourcePacker {write(path: string, buffer: ArrayBufferLike): FileReferenceSchema} /** * Convert the given {@link Project} into a {@link ProjectSchema} and emit * resources via the provided {@link ResourcePacker}. + * + * @param project - Project graph to export. + * @param resourcePacker - Handler invoked for each binary resource. + * @returns The serialized project schema ready for zipping. */ export const write = (project: Project, resourcePacker: ResourcePacker) => { const ids = new AddressIdEncoder() @@ -80,6 +89,7 @@ export namespace DawProjectExporter { }))) })) + /** Serialize the current tempo and time signature. */ const writeTransport = (): TransportSchema => Xml.element({ tempo: Xml.element({ id: ids.getOrCreate(timelineBox.bpm.address), @@ -92,6 +102,12 @@ export namespace DawProjectExporter { }, TimeSignatureParameterSchema) }, TransportSchema) + /** + * Serialize devices connected to the given field. + * + * @param field - Pointer field hosting the devices. + * @param deviceRole - Role assigned to the device in the channel. + */ const writeDevices = (field: Field, deviceRole: string): ReadonlyArray => field.pointerHub .incoming().map(({box}) => { const enabled: Nullish = ("enabled" in box && isInstanceOf(box.enabled, BooleanField) @@ -117,6 +133,7 @@ export namespace DawProjectExporter { }, BuiltinDeviceSchema) }) + /** Resolve a representative color for an audio unit type. */ const colorForAudioType = (unitType: AudioUnitType): string => { const cssColor = ColorCodes.forAudioType(unitType) if (cssColor === "") {return "red"} @@ -191,6 +208,7 @@ export namespace DawProjectExporter { return writeTracks(tracks) } + /** Serialize an {@link AudioRegionBox} into a {@link ClipsSchema}. */ const writeAudioRegion = (region: AudioRegionBox): ClipsSchema => { const audioFileBox = asInstanceOf(region.file.targetVertex.unwrap("No file at region").box, AudioFileBox) const audioElement = sampleManager.getOrCreate(audioFileBox.address.uuid).data @@ -234,6 +252,7 @@ export namespace DawProjectExporter { }, ClipsSchema) } + /** Serialize a {@link NoteRegionBox} into a {@link ClipsSchema}. */ const writeNoteRegion = (region: NoteRegionBox): ClipsSchema => { const collectionBox = asInstanceOf(region.events.targetVertex .unwrap("No notes in region").box, NoteEventCollectionBox) @@ -264,7 +283,11 @@ export namespace DawProjectExporter { }, ClipsSchema) } - // TODO Implement! + /** + * TODO Implement value region serialization. + * + * @param region - Region to serialize. + */ const writeValueRegion = (region: ValueRegionBox): ClipsSchema => Xml.element({ clips: [Xml.element({ @@ -281,6 +304,7 @@ export namespace DawProjectExporter { }, ClipSchema)] }, ClipsSchema) + /** Serialize all track lanes for the current project. */ const writeLanes = (): ReadonlyArray => { return audioUnits .flatMap(audioUnitBox => audioUnitBox.tracks.pointerHub.incoming() diff --git a/packages/studio/core/src/dawproject/DawProjectImport.ts b/packages/studio/core/src/dawproject/DawProjectImport.ts index c976e141b..16118c23a 100644 --- a/packages/studio/core/src/dawproject/DawProjectImport.ts +++ b/packages/studio/core/src/dawproject/DawProjectImport.ts @@ -79,9 +79,18 @@ import {BuiltinDevices} from "./BuiltinDevices" * {@link ProjectDecoder.Skeleton} ready for further processing. */ export namespace DawProjectImport { + /** Pairing of an audio bus with its owning audio unit. */ type AudioBusUnit = { audioBusBox: AudioBusBox, audioUnitBox: AudioUnitBox } + + /** Pairing of an instrument device and its audio unit wrapper. */ type InstrumentUnit = { instrumentBox: InstrumentBox, audioUnitBox: AudioUnitBox } + /** + * Populate timeline transport fields from a DAWproject transport schema. + * + * @param transport - Transport values from the project schema. + * @param timelineBox - Target timeline box receiving the values. + */ const readTransport = ({tempo, timeSignature}: TransportSchema, {bpm, signature: {nominator, denominator}}: TimelineBox) => { ifDefined(tempo?.value, value => bpm.setValue(value)) @@ -98,6 +107,13 @@ export namespace DawProjectImport { skeleton: ProjectDecoder.Skeleton } + /** + * Create a capture box based on the expected content type. + * + * @param boxGraph - Graph used to create the capture box. + * @param contentType - Either `"audio"` or `"notes"`. + * @returns A matching capture box or {@link Option.None}. + */ const toAudioUnitCapture = (boxGraph: BoxGraph, contentType: Nullish): Option => { if (contentType === "audio") return Option.wrap(CaptureAudioBox.create(boxGraph, UUID.generate())) if (contentType === "notes") return Option.wrap(CaptureMidiBox.create(boxGraph, UUID.generate())) @@ -105,6 +121,10 @@ export namespace DawProjectImport { } /** * Rehydrate a project from a DAWproject schema and its resource provider. + * + * @param schema - Parsed project schema to read from. + * @param resources - Resource provider for binary assets referenced by the schema. + * @returns The project skeleton and collected audio resource identifiers. */ export const read = async (schema: ProjectSchema, resources: DawProject.ResourceProvider): Promise => { const boxGraph = new BoxGraph(Option.wrap(BoxIO.create)) @@ -473,6 +493,12 @@ export namespace DawProjectImport { } } + /** + * Determine the default track type connected to an audio unit. + * + * @param audioUnitBox - The audio unit whose input should be inspected. + * @returns The inferred track type or {@link TrackType.Undefined} if unknown. + */ const readInputTrackType = (audioUnitBox: AudioUnitBox): TrackType => { const inputBox = audioUnitBox.input.pointerHub.incoming().at(0)?.box // TODO Can we find a better way to determine the track type? diff --git a/packages/studio/core/src/dawproject/DeviceIO.ts b/packages/studio/core/src/dawproject/DeviceIO.ts index 117ccf139..a96ce4195 100644 --- a/packages/studio/core/src/dawproject/DeviceIO.ts +++ b/packages/studio/core/src/dawproject/DeviceIO.ts @@ -9,6 +9,9 @@ import {DeviceBox, DeviceBoxUtils} from "@opendaw/studio-adapters" export namespace DeviceIO { /** * Serialize a device box and its dependencies into a compact binary format. + * + * @param box - The device box to serialize. + * @returns Binary representation containing the device and dependencies. */ export const exportDevice = (box: Box): ArrayBufferLike => { const dependencies = Array.from(box.graph.dependenciesOf(box).boxes) @@ -31,6 +34,10 @@ export namespace DeviceIO { /** * Deserialize a previously exported device into the given {@link BoxGraph}. + * + * @param boxGraph - Graph to which the device should be added. + * @param buffer - Binary data produced by {@link exportDevice}. + * @returns The reconstructed device box. */ export const importDevice = (boxGraph: BoxGraph, buffer: ArrayBufferLike): DeviceBox => { const input = new ByteArrayInput(buffer)