@volter/dawproject 0.5.67 → 0.5.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@volter/dawproject",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "Apache-2.0",
5
- "version": "0.5.67",
5
+ "version": "0.5.68",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },
package/src/index.ts CHANGED
@@ -73,6 +73,22 @@ export interface ChannelProps {
73
73
  readonly children?: ReactNode;
74
74
  }
75
75
 
76
+ /**
77
+ * A modulator on a `<Channel>`, as Bitwig's LFO: a wave added to one of the channel's own
78
+ * parameters, `target` `volume` or `send:<bus>` (±`depth` dB) or `pan` (±`depth`, the sum
79
+ * clamped to −1…1). One cycle lasts `period` (a note value or a number of beats, so it follows
80
+ * the tempo), counted from the piece's first beat and offset by `phase` (0–1 of a cycle). It adds
81
+ * to the parameter's written value, or to its automation lane where it has one. DAWproject has no
82
+ * modulators: its export writes the moving parameter as automation points.
83
+ */
84
+ export interface LfoProps {
85
+ readonly target: string;
86
+ readonly shape?: 'sine' | 'triangle' | 'square' | 'saw';
87
+ readonly period: Length;
88
+ readonly depth: number;
89
+ readonly phase?: number;
90
+ }
91
+
76
92
  /** DAWproject `Send`: this channel feeds the `effect` channel of the track named `to`, at `level` dB. */
77
93
  export interface SendProps {
78
94
  readonly to: string;
@@ -108,9 +124,13 @@ export interface DeviceProps {
108
124
  readonly params?: Readonly<Record<string, DeviceParam>>;
109
125
  }
110
126
 
111
- /** DAWproject `Clip`: a region of a track's timeline, from bar `at` for `bars` bars. */
127
+ /**
128
+ * DAWproject `Clip`: a region of a track's timeline, from bar `at` for `bars` bars. In a
129
+ * `<ClipSlot>` a clip has no `at`: it is a loop of its own, and its notes' positions count from
130
+ * its start as bar 1.
131
+ */
112
132
  export interface ClipProps {
113
- readonly at: Position;
133
+ readonly at?: Position;
114
134
  readonly bars: number;
115
135
  readonly name?: string;
116
136
  readonly children?: ReactNode;
@@ -131,6 +151,33 @@ export interface NoteProps {
131
151
  readonly artic?: 'staccato' | 'staccatissimo' | 'tenuto' | 'accent' | 'marcato' | 'legato' | 'pizzicato' | 'tremolo';
132
152
  }
133
153
 
154
+ /**
155
+ * DAWproject `Audio`: a recorded file a `<Clip>` plays, instead of notes. `file` is a WAV at a
156
+ * project path; it plays from the clip's start (or from `offset` seconds into the file) for the
157
+ * clip's length, at `gain` dB. A track whose clips hold audio is an audio track: its strip's input
158
+ * is the files, not an instrument.
159
+ */
160
+ export interface AudioProps {
161
+ readonly file: string;
162
+ readonly offset?: number;
163
+ readonly gain?: number;
164
+ /**
165
+ * The take's name, when a clip holds several recordings of one part (one per pass of a loop):
166
+ * the newest (last) plays unless `<Comp>`s pick others.
167
+ */
168
+ readonly take?: string;
169
+ }
170
+
171
+ /**
172
+ * A comp choice in a clip of several takes: from `at` (a position of the piece, inside the clip)
173
+ * the take named `take` plays, until the next `<Comp>`. Before the first, the newest take plays.
174
+ * Where the take changes, the two cross in 5 ms.
175
+ */
176
+ export interface CompProps {
177
+ readonly take: string;
178
+ readonly at: Position;
179
+ }
180
+
134
181
  /**
135
182
  * DAWproject `Points`: an automation lane. In a `<Clip>`, `target` is a MIDI controller
136
183
  * (`cc1` modulation, `cc11` expression, `cc64` sustain, …) or `pitchbend`, with values 0–1
@@ -148,6 +195,22 @@ export interface PointProps {
148
195
  readonly hold?: boolean;
149
196
  }
150
197
 
198
+ /**
199
+ * DAWproject `Scene`: a row of the clip launcher, a child of `<Project>`. Launching it plays each
200
+ * of its `<ClipSlot>`s on that slot's track, looping, from the next bar, and stops the tracks it
201
+ * has no slot for: one scene is one state of the music (a game's explore, combat, calm).
202
+ */
203
+ export interface SceneProps {
204
+ readonly name: string;
205
+ readonly children?: ReactNode;
206
+ }
207
+
208
+ /** DAWproject `ClipSlot`: the launcher cell of the track named `track` in a scene, holding one `<Clip>`. */
209
+ export interface ClipSlotProps {
210
+ readonly track: string;
211
+ readonly children?: ReactNode;
212
+ }
213
+
151
214
  /** DAWproject `Marker`: a named point on the arrangement's timeline. */
152
215
  export interface MarkerProps {
153
216
  readonly at: Position;
@@ -168,7 +231,12 @@ export const Clip = element<ClipProps>('dawproject.Clip');
168
231
  export const Note = element<NoteProps>('dawproject.Note');
169
232
  export const Marker = element<MarkerProps>('dawproject.Marker');
170
233
  export const Points = element<PointsProps>('dawproject.Points');
234
+ export const Audio = element<AudioProps>('dawproject.Audio');
171
235
  export const Point = element<PointProps>('dawproject.Point');
236
+ export const Lfo = element<LfoProps>('dawproject.Lfo');
237
+ export const Comp = element<CompProps>('dawproject.Comp');
238
+ export const Scene = element<SceneProps>('dawproject.Scene');
239
+ export const ClipSlot = element<ClipSlotProps>('dawproject.ClipSlot');
172
240
 
173
241
  /** Every element this package names, keyed by its short name. */
174
242
  export const ELEMENT_TYPES = {
@@ -183,6 +251,11 @@ export const ELEMENT_TYPES = {
183
251
  Marker: 'dawproject.Marker',
184
252
  Points: 'dawproject.Points',
185
253
  Point: 'dawproject.Point',
254
+ Audio: 'dawproject.Audio',
255
+ Lfo: 'dawproject.Lfo',
256
+ Comp: 'dawproject.Comp',
257
+ Scene: 'dawproject.Scene',
258
+ ClipSlot: 'dawproject.ClipSlot',
186
259
  } as const;
187
260
 
188
261
  export type ElementName = keyof typeof ELEMENT_TYPES;
package/src/piece.ts CHANGED
@@ -70,6 +70,29 @@ export interface PieceClip {
70
70
  readonly duration: number;
71
71
  readonly notes: readonly PieceNote[];
72
72
  readonly lanes: readonly PiecePoints[];
73
+ /** The recorded file this clip plays by default (`<Audio>`, the newest take), or `null` for a note clip. */
74
+ readonly audio: PieceAudio | null;
75
+ /** Every `<Audio>` in the clip, oldest first: its takes when there are several. */
76
+ readonly takes: readonly PieceAudio[];
77
+ /** Which take plays from where (`<Comp>`), in time order. */
78
+ readonly comps: readonly PieceComp[];
79
+ }
80
+
81
+ /** A comp choice: from `time` (beats of the piece) the take named `take` plays. */
82
+ export interface PieceComp {
83
+ readonly oid: string | null;
84
+ readonly take: string;
85
+ readonly time: number;
86
+ }
87
+
88
+ /** A clip's recorded file: a WAV at a project path, from `offset` seconds into it, at `gain` dB. */
89
+ export interface PieceAudio {
90
+ readonly oid: string | null;
91
+ readonly file: string;
92
+ readonly offset: number;
93
+ readonly gain: number;
94
+ /** Its take name (`take`), or its place among the clip's takes from 1 when unnamed. */
95
+ readonly take: string;
73
96
  }
74
97
 
75
98
  export interface PieceDevice {
@@ -88,6 +111,18 @@ export interface PieceSend {
88
111
  readonly pre: boolean;
89
112
  }
90
113
 
114
+ /** A channel's LFO (`<Lfo>`): a wave added to `target`, one cycle per `period` beats. */
115
+ export interface PieceLfo {
116
+ readonly oid: string | null;
117
+ readonly target: string;
118
+ readonly shape: 'sine' | 'triangle' | 'square' | 'saw';
119
+ readonly period: number;
120
+ readonly depth: number;
121
+ readonly phase: number;
122
+ /** The period as written (`"h"`, `2`), for an editor field. */
123
+ readonly writtenPeriod: string;
124
+ }
125
+
91
126
  export interface PieceChannel {
92
127
  readonly oid: string | null;
93
128
  /** `submix`: a group track's channel, summing the tracks it contains (DAWproject's role). */
@@ -98,6 +133,7 @@ export interface PieceChannel {
98
133
  readonly solo: boolean;
99
134
  readonly devices: readonly PieceDevice[];
100
135
  readonly sends: readonly PieceSend[];
136
+ readonly lfos: readonly PieceLfo[];
101
137
  }
102
138
 
103
139
  export interface PieceTrack {
@@ -116,6 +152,23 @@ export interface PieceTrack {
116
152
  readonly parent: string | null;
117
153
  }
118
154
 
155
+ /** A launcher cell: the clip the track named `track` loops when the slot is launched. */
156
+ export interface PieceSlot {
157
+ readonly id: string;
158
+ readonly oid: string | null;
159
+ readonly track: string;
160
+ /** Its clip, whose `time` is 0 and whose notes count from its start; `null` for an empty slot. */
161
+ readonly clip: PieceClip | null;
162
+ }
163
+
164
+ /** A row of the clip launcher (`<Scene>`). */
165
+ export interface PieceScene {
166
+ readonly id: string;
167
+ readonly oid: string | null;
168
+ readonly name: string;
169
+ readonly slots: readonly PieceSlot[];
170
+ }
171
+
119
172
  export interface PieceMarker {
120
173
  readonly id: string;
121
174
  readonly oid: string | null;
@@ -127,7 +180,9 @@ export interface Piece {
127
180
  readonly transport: PieceTransport;
128
181
  readonly tracks: readonly PieceTrack[];
129
182
  readonly markers: readonly PieceMarker[];
130
- /** The last beat any clip reaches. */
183
+ /** The clip launcher's rows, in source order. */
184
+ readonly scenes: readonly PieceScene[];
185
+ /** The last beat any arrangement clip reaches. */
131
186
  readonly length: number;
132
187
  /** How many nodes each oid rendered; above 1 means the element repeats. */
133
188
  readonly oidCounts: ReadonlyMap<string, number>;
@@ -190,7 +245,51 @@ export function readPiece(root: DawNode): Piece {
190
245
  const withTempo: PieceTransport = tempoNode ? { ...transport, tempoPoints: readLane(tempoNode, 'transport:tempo', '<Transport>') } : transport;
191
246
  const tracks: PieceTrack[] = [];
192
247
  const markers: PieceMarker[] = [];
248
+ const scenes: PieceScene[] = [];
193
249
  let length = 0;
250
+ /**
251
+ * A clip: on a track, a region from its `at`; in a launcher slot (`arranged` false), a loop of its
252
+ * own whose time is 0, so its notes' `at` counts from its start as bar 1.
253
+ */
254
+ const readClip = (child: DawNode, clipId: string, trackName: string, arranged: boolean): PieceClip => {
255
+ const clipName = str(child.props['name']);
256
+ const where = `<Clip${clipName ? ` "${clipName}"` : ''}> on ${trackName}`;
257
+ const time = arranged ? position(child.props['at'], where) : 0;
258
+ const duration = num(child.props['bars'], 0) * beatsPerBar;
259
+ const notes: PieceNote[] = child.children
260
+ .filter((note) => note.type === 'Note')
261
+ .map((note, noteIndex) => {
262
+ const writtenPitch = String(note.props['pitch'] ?? '');
263
+ const writtenDur = note.props['dur'];
264
+ const start = position(note.props['at'], `A <Note> in ${where}`);
265
+ return {
266
+ id: `${clipId}:note:${noteIndex}`,
267
+ oid: note.oid,
268
+ start,
269
+ time: start - time,
270
+ duration: beatsOf(typeof writtenDur === 'number' ? writtenDur : String(writtenDur ?? '')),
271
+ pitch: midiOf(writtenPitch),
272
+ vel: num(note.props['vel'], 0.7),
273
+ artic: str(note.props['artic']),
274
+ written: { at: String(note.props['at'] ?? ''), pitch: writtenPitch, dur: String(writtenDur ?? '') },
275
+ };
276
+ });
277
+ const lanes = child.children.filter((lane) => lane.type === 'Points').map((lane, laneIndex) => readLane(lane, `${clipId}:lane:${laneIndex}`, where));
278
+ const takes: PieceAudio[] = child.children
279
+ .filter((part) => part.type === 'Audio')
280
+ .map((part, takeIndex) => ({
281
+ oid: part.oid,
282
+ file: str(part.props['file']) ?? '',
283
+ offset: num(part.props['offset'], 0),
284
+ gain: num(part.props['gain'], 0),
285
+ take: str(part.props['take']) ?? String(takeIndex + 1),
286
+ }));
287
+ const comps: PieceComp[] = child.children
288
+ .filter((part) => part.type === 'Comp')
289
+ .map((part) => ({ oid: part.oid, take: str(part.props['take']) ?? '', time: position(part.props['at'], `A <Comp> in ${where}`) }))
290
+ .sort((a, b) => a.time - b.time);
291
+ return { id: clipId, oid: child.oid, name: clipName, time, duration, notes, lanes, audio: takes.at(-1) ?? null, takes, comps };
292
+ };
194
293
  /**
195
294
  * A track, and (after it) every track it contains: a group's children sum into its strip, the
196
295
  * group's channel `role="submix"`. `parent` is the group track's id.
@@ -209,6 +308,27 @@ export function readPiece(root: DawNode): Piece {
209
308
  sends: child.children
210
309
  .filter((send) => send.type === 'Send')
211
310
  .map((send) => ({ oid: send.oid, to: str(send.props['to']) ?? '', level: num(send.props['level'], 0), pre: bool(send.props['pre']) })),
311
+ lfos: child.children
312
+ .filter((lfo) => lfo.type === 'Lfo')
313
+ .map((lfo) => {
314
+ const shape = str(lfo.props['shape']);
315
+ const period = lfo.props['period'];
316
+ return {
317
+ oid: lfo.oid,
318
+ target: str(lfo.props['target']) ?? '',
319
+ shape: shape === 'triangle' || shape === 'square' || shape === 'saw' ? shape : 'sine',
320
+ period: (() => {
321
+ try {
322
+ return beatsOf(typeof period === 'number' ? period : String(period ?? ''));
323
+ } catch (error) {
324
+ throw new Error(`The <Lfo> on ${trackName}'s channel has no period it can cycle over: ${error instanceof Error ? error.message : String(error)}`);
325
+ }
326
+ })(),
327
+ depth: num(lfo.props['depth'], 0),
328
+ phase: num(lfo.props['phase'], 0),
329
+ writtenPeriod: String(period ?? ''),
330
+ };
331
+ }),
212
332
  volume: num(child.props['volume'], 0),
213
333
  pan: num(child.props['pan'], 0),
214
334
  mute: bool(child.props['mute']),
@@ -224,34 +344,9 @@ export function readPiece(root: DawNode): Piece {
224
344
  })),
225
345
  };
226
346
  } else if (child.type === 'Clip') {
227
- const clipId = `${trackId}:clip:${childIndex}`;
228
- const clipName = str(child.props['name']);
229
- const where = `<Clip${clipName ? ` "${clipName}"` : ''}> on ${trackName}`;
230
- const time = position(child.props['at'], where);
231
- const duration = num(child.props['bars'], 0) * beatsPerBar;
232
- const notes: PieceNote[] = child.children
233
- .filter((note) => note.type === 'Note')
234
- .map((note, noteIndex) => {
235
- const writtenPitch = String(note.props['pitch'] ?? '');
236
- const writtenDur = note.props['dur'];
237
- const start = position(note.props['at'], `A <Note> in ${where}`);
238
- return {
239
- id: `${clipId}:note:${noteIndex}`,
240
- oid: note.oid,
241
- start,
242
- time: start - time,
243
- duration: beatsOf(typeof writtenDur === 'number' ? writtenDur : String(writtenDur ?? '')),
244
- pitch: midiOf(writtenPitch),
245
- vel: num(note.props['vel'], 0.7),
246
- artic: str(note.props['artic']),
247
- written: { at: String(note.props['at'] ?? ''), pitch: writtenPitch, dur: String(writtenDur ?? '') },
248
- };
249
- });
250
- const lanes = child.children
251
- .filter((lane) => lane.type === 'Points')
252
- .map((lane, laneIndex) => readLane(lane, `${clipId}:lane:${laneIndex}`, where));
253
- length = Math.max(length, time + duration);
254
- clips.push({ id: clipId, oid: child.oid, name: clipName, time, duration, notes, lanes });
347
+ const clip = readClip(child, `${trackId}:clip:${childIndex}`, trackName, true);
348
+ length = Math.max(length, clip.time + clip.duration);
349
+ clips.push(clip);
255
350
  } else if (child.type === 'Points') {
256
351
  trackLanes.push(readLane(child, `${trackId}:lane:${childIndex}`, `<Track "${trackName}">`));
257
352
  }
@@ -266,9 +361,19 @@ export function readPiece(root: DawNode): Piece {
266
361
  markers.push({ id: `marker:${index}`, oid: node.oid, time: position(node.props['at'], 'A <Marker>'), name: str(node.props['name']) ?? '' });
267
362
  } else if (node.type === 'Track') {
268
363
  readTrack(node, `track:${index}`, null);
364
+ } else if (node.type === 'Scene') {
365
+ const sceneId = `scene:${index}`;
366
+ const slots = node.children.flatMap((slot, slotIndex): PieceSlot[] => {
367
+ if (slot.type !== 'ClipSlot') return [];
368
+ const track = str(slot.props['track']) ?? '';
369
+ const clipNode = slot.children.find((child) => child.type === 'Clip');
370
+ const id = `${sceneId}:slot:${slotIndex}`;
371
+ return [{ id, oid: slot.oid, track, clip: clipNode ? readClip(clipNode, `${id}:clip`, track, false) : null }];
372
+ });
373
+ scenes.push({ id: sceneId, oid: node.oid, name: str(node.props['name']) ?? `Scene ${scenes.length + 1}`, slots });
269
374
  }
270
375
  });
271
- return { transport: withTempo, tracks, markers, length, oidCounts };
376
+ return { transport: withTempo, tracks, markers, scenes, length, oidCounts };
272
377
  }
273
378
 
274
379
  /** Seconds per beat at the piece's tempo (a constant tempo; tempo automation is not read yet). */
package/src/render.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  import type { ComponentType, ReactNode } from 'react';
17
17
  import { createElement } from 'react';
18
18
  import Reconciler from 'react-reconciler';
19
+ import { ELEMENT_TYPES } from './index';
19
20
  import { ConcurrentRoot, DefaultEventPriority } from 'react-reconciler/constants.js';
20
21
 
21
22
  export const OID_PROP = 'data-oid';
@@ -59,7 +60,7 @@ function ownProps(props: Record<string, unknown>): { props: Record<string, unkno
59
60
  function shortType(type: string): string {
60
61
  if (!type.startsWith(TYPE_PREFIX)) {
61
62
  throw new Error(
62
- `<${type}> is not a DAWproject element. A piece renders only @volter/dawproject's elements (Project, Transport, Track, Channel, Device, Clip, Note, Marker) and components made of them.`,
63
+ `<${type}> is not a DAWproject element. A piece renders only @volter/dawproject's elements (${Object.keys(ELEMENT_TYPES).join(', ')}) and components made of them.`,
63
64
  );
64
65
  }
65
66
  return type.slice(TYPE_PREFIX.length);
@@ -163,7 +164,7 @@ const reconciler = Reconciler({
163
164
  resolveEventType: () => null,
164
165
  resolveEventTimeStamp: () => -1.1,
165
166
  rendererPackageName: '@volter/dawproject',
166
- rendererVersion: '0.5.67',
167
+ rendererVersion: '0.5.68',
167
168
  } as never);
168
169
 
169
170
  function snapshot(instance: Instance): DawNode {