@opendaw/studio-core 0.0.103 → 0.0.104

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.
@@ -1 +1 @@
1
- {"version":3,"file":"RecordAutomation.d.ts","sourceRoot":"","sources":["../../src/capture/RecordAutomation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,UAAU,EAAkB,MAAM,kBAAkB,CAAA;AAWpE,OAAO,EAAC,OAAO,EAAC,MAAM,YAAY,CAAA;AAGlC,yBAAiB,gBAAgB,CAAC;IAcvB,MAAM,KAAK,GAAI,SAAS,OAAO,KAAG,UA+HxC,CAAA;CACJ"}
1
+ {"version":3,"file":"RecordAutomation.d.ts","sourceRoot":"","sources":["../../src/capture/RecordAutomation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiD,UAAU,EAAkB,MAAM,kBAAkB,CAAA;AAc5G,OAAO,EAAC,OAAO,EAAC,MAAM,YAAY,CAAA;AAGlC,yBAAiB,gBAAgB,CAAC;IAevB,MAAM,KAAK,GAAI,SAAS,OAAO,KAAG,UAsPxC,CAAA;CACJ"}
@@ -1,111 +1,222 @@
1
- import { Option, Terminable, UUID } from "@opendaw/lib-std";
1
+ import { Option, quantizeCeil, quantizeFloor, Terminable, UUID } from "@opendaw/lib-std";
2
+ import { Interpolation, PPQN } from "@opendaw/lib-dsp";
3
+ import { Address } from "@opendaw/lib-box";
2
4
  import { TrackBox, ValueEventBox, ValueEventCollectionBox, ValueRegionBox } from "@opendaw/studio-boxes";
3
- import { ColorCodes, Devices, TrackBoxAdapter, TrackType, ValueRegionBoxAdapter } from "@opendaw/studio-adapters";
5
+ import { ColorCodes, Devices, InterpolationFieldAdapter, TrackBoxAdapter, TrackType, ValueEventCollectionBoxAdapter } from "@opendaw/studio-adapters";
4
6
  import { RegionClipResolver } from "../ui";
5
7
  export var RecordAutomation;
6
8
  (function (RecordAutomation) {
9
+ const Eplison = 0.01;
7
10
  RecordAutomation.start = (project) => {
8
- const { editing, engine, boxAdapters, parameterFieldAdapters, boxGraph } = project;
9
- const activeRecordings = new Map();
10
- return Terminable.many(parameterFieldAdapters.subscribeWrites(adapter => {
11
+ const { editing, engine, boxAdapters, parameterFieldAdapters, boxGraph, timelineBox } = project;
12
+ const activeRecordings = Address.newSet(state => state.adapter.address);
13
+ let lastPosition = engine.position.getValue();
14
+ const createRegion = (trackBoxAdapter, adapter, startPos, previousUnitValue, value, floating) => {
15
+ const trackBox = trackBoxAdapter.box;
16
+ project.selection.deselect(...trackBoxAdapter.regions.collection.asArray()
17
+ .filter(region => region.isSelected)
18
+ .map(region => region.box));
19
+ RegionClipResolver.fromRange(trackBoxAdapter, startPos, startPos + PPQN.SemiQuaver)();
20
+ const collectionBox = ValueEventCollectionBox.create(boxGraph, UUID.generate());
21
+ const regionBox = ValueRegionBox.create(boxGraph, UUID.generate(), box => {
22
+ box.position.setValue(startPos);
23
+ box.duration.setValue(PPQN.SemiQuaver);
24
+ box.loopDuration.setValue(PPQN.SemiQuaver);
25
+ box.hue.setValue(ColorCodes.forTrackType(TrackType.Value));
26
+ box.label.setValue(adapter.name);
27
+ box.events.refer(collectionBox.owners);
28
+ box.regions.refer(trackBox.regions);
29
+ });
30
+ project.selection.select(regionBox);
31
+ const interpolation = floating ? Interpolation.Linear : Interpolation.None;
32
+ let lastEventBox;
33
+ if (previousUnitValue !== value) {
34
+ ValueEventBox.create(boxGraph, UUID.generate(), box => {
35
+ box.position.setValue(0);
36
+ box.value.setValue(previousUnitValue);
37
+ box.events.refer(collectionBox.events);
38
+ });
39
+ lastEventBox = ValueEventBox.create(boxGraph, UUID.generate(), box => {
40
+ box.position.setValue(0);
41
+ box.index.setValue(1);
42
+ box.value.setValue(value);
43
+ box.events.refer(collectionBox.events);
44
+ });
45
+ InterpolationFieldAdapter.write(lastEventBox.interpolation, interpolation);
46
+ }
47
+ else {
48
+ lastEventBox = ValueEventBox.create(boxGraph, UUID.generate(), box => {
49
+ box.position.setValue(0);
50
+ box.value.setValue(value);
51
+ box.events.refer(collectionBox.events);
52
+ });
53
+ InterpolationFieldAdapter.write(lastEventBox.interpolation, interpolation);
54
+ }
55
+ return {
56
+ adapter, trackBoxAdapter, regionBox, collectionBox,
57
+ startPosition: startPos, floating, lastValue: value,
58
+ lastRelativePosition: 0, lastEventBox
59
+ };
60
+ };
61
+ const findOrCreateTrack = (adapter) => {
62
+ const deviceBox = adapter.field.box;
63
+ const deviceAdapterOpt = Option.tryCatch(() => boxAdapters.adapterFor(deviceBox, Devices.isAny));
64
+ if (deviceAdapterOpt.isEmpty()) {
65
+ console.warn(`Cannot record automation: could not find device adapter for ${deviceBox.name}`);
66
+ return Option.None;
67
+ }
68
+ const deviceAdapter = deviceAdapterOpt.unwrap();
69
+ const audioUnitAdapter = deviceAdapter.audioUnitBoxAdapter();
70
+ const tracks = audioUnitAdapter.tracks;
71
+ const existing = tracks.controls(adapter.field);
72
+ if (existing.nonEmpty()) {
73
+ return Option.wrap(existing.unwrap());
74
+ }
75
+ const trackBox = TrackBox.create(boxGraph, UUID.generate(), box => {
76
+ box.index.setValue(tracks.collection.getMinFreeIndex());
77
+ box.type.setValue(TrackType.Value);
78
+ box.tracks.refer(audioUnitAdapter.box.tracks);
79
+ box.target.refer(adapter.field);
80
+ });
81
+ return Option.wrap(boxAdapters.adapterFor(trackBox, TrackBoxAdapter));
82
+ };
83
+ const handleWrite = ({ adapter, previousUnitValue }) => {
11
84
  if (!engine.isRecording.getValue()) {
12
85
  return;
13
86
  }
14
- const key = adapter.address.toString();
15
87
  const position = engine.position.getValue();
16
88
  const value = adapter.getUnitValue();
17
- let state = activeRecordings.get(key);
18
- if (state === undefined) {
89
+ const existingState = activeRecordings.opt(adapter.address);
90
+ if (existingState.isEmpty()) {
19
91
  editing.modify(() => {
20
- const deviceBox = adapter.field.box;
21
- const deviceAdapterOpt = Option.tryCatch(() => boxAdapters.adapterFor(deviceBox, Devices.isAny));
22
- if (deviceAdapterOpt.isEmpty()) {
23
- console.warn(`Cannot record automation: could not find device adapter for ${deviceBox.name}`);
92
+ const trackOpt = findOrCreateTrack(adapter);
93
+ if (trackOpt.isEmpty()) {
24
94
  return;
25
95
  }
26
- const deviceAdapter = deviceAdapterOpt.unwrap();
27
- const audioUnitAdapter = deviceAdapter.audioUnitBoxAdapter();
28
- const tracks = audioUnitAdapter.tracks;
29
- let trackBox;
30
- let trackBoxAdapter;
31
- const existing = tracks.controls(adapter.field);
32
- if (existing.nonEmpty()) {
33
- trackBoxAdapter = existing.unwrap();
34
- trackBox = trackBoxAdapter.box;
35
- }
36
- else {
37
- trackBox = TrackBox.create(boxGraph, UUID.generate(), box => {
38
- box.index.setValue(tracks.collection.getMinFreeIndex());
39
- box.type.setValue(TrackType.Value);
40
- box.tracks.refer(audioUnitAdapter.box.tracks);
41
- box.target.refer(adapter.field);
42
- });
43
- trackBoxAdapter = boxAdapters.adapterFor(trackBox, TrackBoxAdapter);
44
- }
45
- const collectionBox = ValueEventCollectionBox.create(boxGraph, UUID.generate());
46
- const regionBox = ValueRegionBox.create(boxGraph, UUID.generate(), box => {
47
- box.position.setValue(position);
48
- box.duration.setValue(0);
49
- box.loopDuration.setValue(0);
50
- box.hue.setValue(ColorCodes.forTrackType(TrackType.Value));
51
- box.label.setValue(adapter.name);
52
- box.events.refer(collectionBox.owners);
53
- box.regions.refer(trackBox.regions);
54
- });
55
- const lastEventBox = ValueEventBox.create(boxGraph, UUID.generate(), box => {
56
- box.position.setValue(0);
57
- box.value.setValue(value);
58
- box.events.refer(collectionBox.events);
59
- });
60
- state = {
61
- adapter, trackBox, trackBoxAdapter, regionBox, collectionBox,
62
- startPosition: position, lastValue: value, lastEventPosition: position,
63
- lastRelativePosition: 0, lastEventBox
64
- };
65
- activeRecordings.set(key, state);
96
+ const trackBoxAdapter = trackOpt.unwrap();
97
+ const startPos = quantizeFloor(position, PPQN.SemiQuaver);
98
+ const floating = adapter.valueMapping.floating();
99
+ const state = createRegion(trackBoxAdapter, adapter, startPos, previousUnitValue, value, floating);
100
+ activeRecordings.add(state);
66
101
  });
67
102
  }
68
103
  else {
69
- const currentState = state;
70
- const relativePosition = Math.max(0, position - currentState.startPosition);
71
- if (relativePosition === currentState.lastRelativePosition) {
104
+ const state = existingState.unwrap();
105
+ if (position < state.startPosition) {
106
+ return;
107
+ }
108
+ const relativePosition = position - state.startPosition;
109
+ if (relativePosition === state.lastRelativePosition) {
72
110
  editing.modify(() => {
73
- currentState.lastEventBox.value.setValue(value);
74
- currentState.lastValue = value;
111
+ state.lastEventBox.value.setValue(value);
112
+ state.lastValue = value;
75
113
  }, false);
76
114
  }
77
115
  else {
78
116
  editing.modify(() => {
79
- currentState.lastEventBox = ValueEventBox.create(boxGraph, UUID.generate(), box => {
117
+ const interpolation = state.floating ? Interpolation.Linear : Interpolation.None;
118
+ state.lastEventBox = ValueEventBox.create(boxGraph, UUID.generate(), box => {
80
119
  box.position.setValue(relativePosition);
81
120
  box.value.setValue(value);
82
- box.events.refer(currentState.collectionBox.events);
121
+ box.events.refer(state.collectionBox.events);
83
122
  });
84
- currentState.lastValue = value;
85
- currentState.lastEventPosition = position;
86
- currentState.lastRelativePosition = relativePosition;
123
+ InterpolationFieldAdapter.write(state.lastEventBox.interpolation, interpolation);
124
+ state.lastValue = value;
125
+ state.lastRelativePosition = relativePosition;
87
126
  }, false);
88
127
  }
89
128
  }
90
- }), engine.position.subscribe(owner => {
129
+ };
130
+ const handlePosition = () => {
91
131
  if (!engine.isRecording.getValue()) {
92
132
  return;
93
133
  }
94
- if (activeRecordings.size === 0) {
134
+ if (activeRecordings.size() === 0) {
95
135
  return;
96
136
  }
97
- const position = owner.getValue();
137
+ const currentPosition = engine.position.getValue();
138
+ const loopEnabled = timelineBox.loopArea.enabled.getValue();
139
+ const loopFrom = timelineBox.loopArea.from.getValue();
140
+ const loopTo = timelineBox.loopArea.to.getValue();
141
+ if (loopEnabled && currentPosition < lastPosition) {
142
+ editing.modify(() => {
143
+ const snapshot = [...activeRecordings.values()];
144
+ for (const state of snapshot) {
145
+ if (!state.regionBox.isAttached()) {
146
+ continue;
147
+ }
148
+ const finalDuration = Math.max(PPQN.SemiQuaver, quantizeCeil(loopTo - state.startPosition, PPQN.SemiQuaver));
149
+ const oldDuration = state.regionBox.duration.getValue();
150
+ if (finalDuration > oldDuration) {
151
+ RegionClipResolver.fromRange(state.trackBoxAdapter, state.startPosition + oldDuration, state.startPosition + finalDuration)();
152
+ }
153
+ if (finalDuration !== state.lastRelativePosition) {
154
+ ValueEventBox.create(boxGraph, UUID.generate(), box => {
155
+ box.position.setValue(finalDuration);
156
+ box.value.setValue(state.lastValue);
157
+ box.events.refer(state.collectionBox.events);
158
+ });
159
+ }
160
+ state.regionBox.duration.setValue(finalDuration);
161
+ state.regionBox.loopDuration.setValue(finalDuration);
162
+ simplifyRecordedEvents(state);
163
+ project.selection.deselect(state.regionBox);
164
+ const newStartPos = quantizeFloor(loopFrom, PPQN.SemiQuaver);
165
+ const newState = createRegion(state.trackBoxAdapter, state.adapter, newStartPos, state.lastValue, state.lastValue, state.floating);
166
+ activeRecordings.removeByKey(state.adapter.address);
167
+ activeRecordings.add(newState);
168
+ }
169
+ }, false);
170
+ }
171
+ lastPosition = currentPosition;
98
172
  editing.modify(() => {
99
173
  for (const state of activeRecordings.values()) {
100
- if (state.regionBox.isAttached()) {
101
- const duration = Math.max(0, position - state.startPosition);
102
- state.regionBox.duration.setValue(duration);
103
- state.regionBox.loopDuration.setValue(duration);
174
+ if (!state.regionBox.isAttached()) {
175
+ continue;
176
+ }
177
+ const oldDuration = state.regionBox.duration.getValue();
178
+ const maxDuration = loopEnabled
179
+ ? loopTo - state.startPosition
180
+ : Infinity;
181
+ const newDuration = Math.max(PPQN.SemiQuaver, quantizeCeil(Math.min(maxDuration, currentPosition - state.startPosition), PPQN.SemiQuaver));
182
+ if (newDuration > oldDuration) {
183
+ RegionClipResolver.fromRange(state.trackBoxAdapter, state.startPosition + oldDuration, state.startPosition + newDuration)();
104
184
  }
185
+ state.regionBox.duration.setValue(newDuration);
186
+ state.regionBox.loopDuration.setValue(newDuration);
105
187
  }
106
188
  }, false);
107
- }), Terminable.create(() => {
108
- if (activeRecordings.size === 0) {
189
+ };
190
+ const simplifyRecordedEvents = (state) => {
191
+ if (!state.floating) {
192
+ return;
193
+ }
194
+ const adapter = boxAdapters.adapterFor(state.collectionBox, ValueEventCollectionBoxAdapter);
195
+ const events = [...adapter.events.asArray()];
196
+ const keep = [];
197
+ for (const event of events) {
198
+ while (keep.length >= 2) {
199
+ const a = keep[keep.length - 2];
200
+ const b = keep[keep.length - 1];
201
+ if (a.position === b.position || b.position === event.position) {
202
+ break;
203
+ }
204
+ if (a.interpolation.type !== "linear" || b.interpolation.type !== "linear") {
205
+ break;
206
+ }
207
+ const t = (b.position - a.position) / (event.position - a.position);
208
+ const expected = a.value + t * (event.value - a.value);
209
+ if (Math.abs(b.value - expected) > Eplison) {
210
+ break;
211
+ }
212
+ keep.pop();
213
+ b.box.delete();
214
+ }
215
+ keep.push(event);
216
+ }
217
+ };
218
+ const handleTermination = () => {
219
+ if (activeRecordings.size() === 0) {
109
220
  return;
110
221
  }
111
222
  const finalPosition = engine.position.getValue();
@@ -114,26 +225,28 @@ export var RecordAutomation;
114
225
  if (!state.regionBox.isAttached()) {
115
226
  continue;
116
227
  }
117
- const duration = Math.max(0, finalPosition - state.startPosition);
118
- if (duration <= 0) {
228
+ const finalDuration = Math.max(0, quantizeCeil(finalPosition - state.startPosition, PPQN.SemiQuaver));
229
+ if (finalDuration <= 0) {
119
230
  state.regionBox.delete();
120
231
  continue;
121
232
  }
122
- const regionAdapter = boxAdapters.adapterFor(state.regionBox, ValueRegionBoxAdapter);
123
- regionAdapter.onSelected();
124
- RegionClipResolver.fromRange(state.trackBoxAdapter, state.startPosition, state.startPosition + duration)();
125
- regionAdapter.onDeselected();
126
- if (duration !== state.lastRelativePosition) {
233
+ const oldDuration = state.regionBox.duration.getValue();
234
+ if (finalDuration > oldDuration) {
235
+ RegionClipResolver.fromRange(state.trackBoxAdapter, state.startPosition + oldDuration, state.startPosition + finalDuration)();
236
+ }
237
+ if (finalDuration !== state.lastRelativePosition) {
127
238
  ValueEventBox.create(boxGraph, UUID.generate(), box => {
128
- box.position.setValue(duration);
239
+ box.position.setValue(finalDuration);
129
240
  box.value.setValue(state.lastValue);
130
241
  box.events.refer(state.collectionBox.events);
131
242
  });
132
243
  }
133
- state.regionBox.duration.setValue(duration);
134
- state.regionBox.loopDuration.setValue(duration);
244
+ state.regionBox.duration.setValue(finalDuration);
245
+ state.regionBox.loopDuration.setValue(finalDuration);
246
+ simplifyRecordedEvents(state);
135
247
  }
136
248
  });
137
- }));
249
+ };
250
+ return Terminable.many(parameterFieldAdapters.subscribeWrites(handleWrite), engine.position.subscribe(handlePosition), Terminable.create(handleTermination));
138
251
  };
139
252
  })(RecordAutomation || (RecordAutomation = {}));