@opendaw/studio-core 0.0.103 → 0.0.105
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/dist/capture/RecordAutomation.d.ts.map +1 -1
- package/dist/capture/RecordAutomation.js +195 -82
- package/dist/processors.js +17 -17
- package/dist/processors.js.map +3 -3
- package/dist/project/ProjectApi.d.ts +2 -1
- package/dist/project/ProjectApi.d.ts.map +1 -1
- package/dist/project/ProjectApi.js +11 -1
- package/dist/project/ProjectApi.test.d.ts +2 -0
- package/dist/project/ProjectApi.test.d.ts.map +1 -0
- package/dist/project/ProjectApi.test.js +109 -0
- package/dist/project/polyfill.d.ts +1 -0
- package/dist/project/polyfill.d.ts.map +1 -0
- package/dist/project/polyfill.js +14 -0
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RecordAutomation.d.ts","sourceRoot":"","sources":["../../src/capture/RecordAutomation.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
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,
|
|
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 =
|
|
10
|
-
|
|
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
|
-
|
|
18
|
-
if (
|
|
89
|
+
const existingState = activeRecordings.opt(adapter.address);
|
|
90
|
+
if (existingState.isEmpty()) {
|
|
19
91
|
editing.modify(() => {
|
|
20
|
-
const
|
|
21
|
-
|
|
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
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
111
|
+
state.lastEventBox.value.setValue(value);
|
|
112
|
+
state.lastValue = value;
|
|
75
113
|
}, false);
|
|
76
114
|
}
|
|
77
115
|
else {
|
|
78
116
|
editing.modify(() => {
|
|
79
|
-
|
|
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(
|
|
121
|
+
box.events.refer(state.collectionBox.events);
|
|
83
122
|
});
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
123
|
+
InterpolationFieldAdapter.write(state.lastEventBox.interpolation, interpolation);
|
|
124
|
+
state.lastValue = value;
|
|
125
|
+
state.lastRelativePosition = relativePosition;
|
|
87
126
|
}, false);
|
|
88
127
|
}
|
|
89
128
|
}
|
|
90
|
-
}
|
|
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
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
}
|
|
108
|
-
|
|
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
|
|
118
|
-
if (
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (
|
|
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(
|
|
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(
|
|
134
|
-
state.regionBox.loopDuration.setValue(
|
|
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 = {}));
|