Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/docs/docs-dev/serialization/migration.md
Original file line number Diff line number Diff line change
@@ -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).

6 changes: 4 additions & 2 deletions packages/docs/docs-user/workflows/dawproject.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
1 change: 1 addition & 0 deletions packages/lib/dawproject/src/defaults.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/**
* @packageDocumentation
* Core schema definitions for the DAWproject XML format.
*
* @remarks
Expand Down
26 changes: 23 additions & 3 deletions packages/lib/dawproject/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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.
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/studio/core/src/ProjectMigration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion packages/studio/core/src/dawproject/AudioUnitExportLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ export namespace AudioUnitExportLayout {
children: Array<Track>
}

/** 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<AudioUnitBox>): Array<Track> => {
const feedsInto = new ArrayMultimap<AudioUnitBox, AudioUnitBox>()
audioUnits.forEach(unit => {
Expand Down Expand Up @@ -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<AudioUnitBox, AudioUnitBox>,
visited: Set<AudioUnitBox>): Nullable<Track> => {
Expand All @@ -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<Track>, indent = 0): void => {
const spaces = " ".repeat(indent)
Expand Down
9 changes: 9 additions & 0 deletions packages/studio/core/src/dawproject/BuiltinDevices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Pointers.MidiEffectHost> | Field<Pointers.AudioEffectHost>,
index: int): RevampDeviceBox => {
/** Map DAWproject filter order to Revamp's order index. */
const mapOrder = (order?: int) => {
switch (order) {
case 1:
Expand All @@ -31,13 +38,15 @@ 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))
frequency.setValue(semitoneToHz(schema.freq.value))
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))
Expand Down
7 changes: 7 additions & 0 deletions packages/studio/core/src/dawproject/DawProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<ArrayBuffer> => {
const zip = new JSZip()
Expand Down
28 changes: 26 additions & 2 deletions packages/studio/core/src/dawproject/DawProjectExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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),
Expand All @@ -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<DeviceSchema> => field.pointerHub
.incoming().map(({box}) => {
const enabled: Nullish<BooleanParameterSchema> = ("enabled" in box && isInstanceOf(box.enabled, BooleanField)
Expand All @@ -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"}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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({
Expand All @@ -281,6 +304,7 @@ export namespace DawProjectExporter {
}, ClipSchema)]
}, ClipsSchema)

/** Serialize all track lanes for the current project. */
const writeLanes = (): ReadonlyArray<TimelineSchema> => {
return audioUnits
.flatMap(audioUnitBox => audioUnitBox.tracks.pointerHub.incoming()
Expand Down
26 changes: 26 additions & 0 deletions packages/studio/core/src/dawproject/DawProjectImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -98,13 +107,24 @@ 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<string>): Option<CaptureBox> => {
if (contentType === "audio") return Option.wrap(CaptureAudioBox.create(boxGraph, UUID.generate()))
if (contentType === "notes") return Option.wrap(CaptureMidiBox.create(boxGraph, UUID.generate()))
return Option.None
}
/**
* 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<Result> => {
const boxGraph = new BoxGraph<BoxIO.TypeMap>(Option.wrap(BoxIO.create))
Expand Down Expand Up @@ -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?
Expand Down
7 changes: 7 additions & 0 deletions packages/studio/core/src/dawproject/DeviceIO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<BoxIO.TypeMap>, buffer: ArrayBufferLike): DeviceBox => {
const input = new ByteArrayInput(buffer)
Expand Down