@opendaw/studio-core 0.0.130 → 0.0.132

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.
Files changed (71) hide show
  1. package/dist/AssetService.d.ts.map +1 -1
  2. package/dist/AssetService.js +11 -2
  3. package/dist/AudioOfflineRenderer.js +1 -1
  4. package/dist/EffectBox.d.ts +2 -2
  5. package/dist/EffectBox.d.ts.map +1 -1
  6. package/dist/EffectFactories.d.ts +3 -0
  7. package/dist/EffectFactories.d.ts.map +1 -1
  8. package/dist/EffectFactories.js +19 -3
  9. package/dist/Engine.d.ts +1 -1
  10. package/dist/Engine.d.ts.map +1 -1
  11. package/dist/EngineFacade.d.ts +1 -1
  12. package/dist/EngineFacade.d.ts.map +1 -1
  13. package/dist/EngineFacade.js +2 -2
  14. package/dist/EngineWorklet.d.ts +1 -1
  15. package/dist/EngineWorklet.d.ts.map +1 -1
  16. package/dist/EngineWorklet.js +9 -46
  17. package/dist/MonitoringRouter.d.ts +10 -0
  18. package/dist/MonitoringRouter.d.ts.map +1 -0
  19. package/dist/MonitoringRouter.js +89 -0
  20. package/dist/capture/CaptureAudio.d.ts +10 -0
  21. package/dist/capture/CaptureAudio.d.ts.map +1 -1
  22. package/dist/capture/CaptureAudio.js +105 -30
  23. package/dist/capture/RecordAudio.d.ts.map +1 -1
  24. package/dist/capture/RecordAudio.js +10 -2
  25. package/dist/capture/Recording.js +1 -1
  26. package/dist/processors.js +28 -28
  27. package/dist/processors.js.map +4 -4
  28. package/dist/project/AudioWavExport.d.ts +6 -0
  29. package/dist/project/AudioWavExport.d.ts.map +1 -0
  30. package/dist/project/AudioWavExport.js +15 -0
  31. package/dist/project/NoteMidiExport.d.ts +9 -0
  32. package/dist/project/NoteMidiExport.d.ts.map +1 -0
  33. package/dist/project/NoteMidiExport.js +27 -0
  34. package/dist/project/Project.d.ts.map +1 -1
  35. package/dist/project/Project.js +1 -2
  36. package/dist/project/ProjectApi.d.ts +3 -1
  37. package/dist/project/ProjectApi.d.ts.map +1 -1
  38. package/dist/project/ProjectApi.js +8 -0
  39. package/dist/project/ProjectStorage.d.ts.map +1 -1
  40. package/dist/project/ProjectStorage.js +1 -0
  41. package/dist/project/audio/AudioContentModifier.d.ts.map +1 -1
  42. package/dist/project/audio/AudioContentModifier.js +43 -3
  43. package/dist/project/index.d.ts +2 -0
  44. package/dist/project/index.d.ts.map +1 -1
  45. package/dist/project/index.js +2 -0
  46. package/dist/project/migration/MigrateValueEventCollection.test.js +3 -3
  47. package/dist/samples/OpenSampleAPI.d.ts +1 -0
  48. package/dist/samples/OpenSampleAPI.d.ts.map +1 -1
  49. package/dist/samples/OpenSampleAPI.js +1 -0
  50. package/dist/samples/SampleService.js +1 -1
  51. package/dist/soundfont/DefaultSoundfontLoader.d.ts.map +1 -1
  52. package/dist/soundfont/DefaultSoundfontLoader.js +3 -0
  53. package/dist/soundfont/OpenSoundfontAPI.d.ts +1 -0
  54. package/dist/soundfont/OpenSoundfontAPI.d.ts.map +1 -1
  55. package/dist/soundfont/OpenSoundfontAPI.js +1 -0
  56. package/dist/soundfont/SoundfontService.js +1 -1
  57. package/dist/sync-log/SyncLogWriter.d.ts.map +1 -1
  58. package/dist/sync-log/SyncLogWriter.js +3 -2
  59. package/dist/ui/clipboard/ClipboardUtils.js +1 -1
  60. package/dist/ui/clipboard/types/DevicesClipboardHandler.d.ts.map +1 -1
  61. package/dist/ui/clipboard/types/DevicesClipboardHandler.js +22 -12
  62. package/dist/ui/clipboard/types/DevicesClipboardHandler.test.js +124 -4
  63. package/dist/ysync/YService.d.ts.map +1 -1
  64. package/dist/ysync/YService.js +0 -5
  65. package/dist/ysync/YSync.d.ts +1 -0
  66. package/dist/ysync/YSync.d.ts.map +1 -1
  67. package/dist/ysync/YSync.js +127 -84
  68. package/dist/ysync/YSync.test.d.ts +2 -0
  69. package/dist/ysync/YSync.test.d.ts.map +1 -0
  70. package/dist/ysync/YSync.test.js +259 -0
  71. package/package.json +16 -15
@@ -8,29 +8,45 @@ import { RenderQuantum } from "../RenderQuantum";
8
8
  export class CaptureAudio extends Capture {
9
9
  #stream;
10
10
  #streamGenerator;
11
+ #monitorGainNode;
12
+ #monitorPanNode;
11
13
  #monitoringMode = "off";
12
14
  #requestChannels = Option.None;
13
15
  #gainDb = 0.0;
16
+ #monitorVolumeDb = 0.0;
17
+ #monitorPan = 0.0;
18
+ #monitorMuted = false;
14
19
  #audioChain = null;
15
20
  #preparedWorklet = null;
21
+ #monitorOutputDeviceId = Option.None;
22
+ #monitorAudioElement = null;
23
+ #monitorStreamDest = null;
16
24
  constructor(manager, audioUnitBox, captureAudioBox) {
17
25
  super(manager, audioUnitBox, captureAudioBox);
26
+ const { audioContext } = this.manager.project.env;
27
+ this.#monitorGainNode = audioContext.createGain();
28
+ this.#monitorGainNode.gain.value = dbToGain(this.#monitorVolumeDb);
29
+ this.#monitorPanNode = audioContext.createStereoPanner();
30
+ this.#monitorPanNode.pan.value = this.#monitorPan;
31
+ this.#monitorGainNode.connect(this.#monitorPanNode);
18
32
  this.#stream = new MutableObservableOption();
19
33
  this.#streamGenerator = Promises.sequentialize(() => this.#updateStream());
20
- this.ownAll(captureAudioBox.requestChannels.catchupAndSubscribe(owner => {
34
+ this.ownAll(Terminable.create(() => {
35
+ this.#disconnectMonitoring();
36
+ if (isDefined(this.#monitorAudioElement)) {
37
+ this.#monitorAudioElement.pause();
38
+ this.#monitorAudioElement.srcObject = null;
39
+ }
40
+ this.#monitorGainNode.disconnect();
41
+ this.#monitorPanNode.disconnect();
42
+ }), captureAudioBox.requestChannels.catchupAndSubscribe(owner => {
21
43
  const channels = owner.getValue();
22
44
  this.#requestChannels = channels === 1 || channels === 2 ? Option.wrap(channels) : Option.None;
23
45
  this.#stream.ifSome(stream => this.#rebuildAudioChain(stream));
24
- // Re-register monitoring if in effects mode (channel count may have changed)
25
- if (this.#monitoringMode === "effects" && isDefined(this.#audioChain)) {
26
- const engine = this.manager.project.engine;
27
- engine.unregisterMonitoringSource(this.audioUnitBox.address.uuid);
28
- engine.registerMonitoringSource(this.audioUnitBox.address.uuid, this.#audioChain.gainNode, this.#audioChain.channelCount);
29
- }
30
46
  }), captureAudioBox.gainDb.catchupAndSubscribe(owner => {
31
47
  this.#gainDb = owner.getValue();
32
48
  if (isDefined(this.#audioChain)) {
33
- this.#audioChain.gainNode.gain.value = dbToGain(this.#gainDb);
49
+ this.#audioChain.recordGainNode.gain.value = dbToGain(this.#gainDb);
34
50
  }
35
51
  }), captureAudioBox.deviceId.catchupAndSubscribe(async () => {
36
52
  if (this.armed.getValue()) {
@@ -71,8 +87,62 @@ export class CaptureAudio extends Capture {
71
87
  get streamMediaTrack() {
72
88
  return this.#stream.flatMap(stream => Option.wrap(stream.getAudioTracks().at(0)));
73
89
  }
74
- get outputNode() { return Option.wrap(this.#audioChain?.gainNode); }
90
+ get outputNode() { return Option.wrap(this.#audioChain?.recordGainNode); }
75
91
  get effectiveChannelCount() { return this.#audioChain?.channelCount ?? 1; }
92
+ get monitorGainNode() { return this.#monitorGainNode; }
93
+ get monitorPanNode() { return this.#monitorPanNode; }
94
+ get monitorVolumeDb() { return this.#monitorVolumeDb; }
95
+ set monitorVolumeDb(value) {
96
+ this.#monitorVolumeDb = value;
97
+ this.#monitorGainNode.gain.value = this.#monitorMuted ? 0 : dbToGain(this.#monitorVolumeDb);
98
+ }
99
+ get monitorPan() { return this.#monitorPan; }
100
+ set monitorPan(value) {
101
+ this.#monitorPan = value;
102
+ this.#monitorPanNode.pan.value = value;
103
+ }
104
+ get monitorMuted() { return this.#monitorMuted; }
105
+ set monitorMuted(value) {
106
+ this.#monitorMuted = value;
107
+ this.#monitorGainNode.gain.value = value ? 0 : dbToGain(this.#monitorVolumeDb);
108
+ }
109
+ get monitorOutputDeviceId() { return this.#monitorOutputDeviceId; }
110
+ async setMonitorOutputDevice(deviceId) {
111
+ const oldDestination = this.#monitorDestination();
112
+ this.#monitorOutputDeviceId = deviceId;
113
+ if (isDefined(this.#monitorAudioElement)) {
114
+ this.#monitorAudioElement.pause();
115
+ this.#monitorAudioElement.srcObject = null;
116
+ this.#monitorAudioElement = null;
117
+ }
118
+ if (isDefined(this.#monitorStreamDest)) {
119
+ this.#monitorStreamDest.disconnect();
120
+ this.#monitorStreamDest = null;
121
+ }
122
+ if (deviceId.nonEmpty()) {
123
+ const { audioContext } = this.manager.project.env;
124
+ this.#monitorStreamDest = audioContext.createMediaStreamDestination();
125
+ const audio = new Audio();
126
+ audio.srcObject = this.#monitorStreamDest.stream;
127
+ try {
128
+ await audio.setSinkId(deviceId.unwrap());
129
+ await audio.play();
130
+ this.#monitorAudioElement = audio;
131
+ }
132
+ catch (reason) {
133
+ audio.srcObject = null;
134
+ this.#monitorStreamDest.disconnect();
135
+ this.#monitorStreamDest = null;
136
+ this.#monitorOutputDeviceId = Option.None;
137
+ RuntimeNotifier.info({ headline: "Output Device Error", message: `${reason}` });
138
+ return;
139
+ }
140
+ }
141
+ if (this.#monitoringMode !== "off" && isDefined(this.#audioChain)) {
142
+ this.#monitorPanNode.disconnect(oldDestination);
143
+ this.#monitorPanNode.connect(this.#monitorDestination());
144
+ }
145
+ }
76
146
  async prepareRecording() {
77
147
  const { project } = this.manager;
78
148
  const { env: { audioContext, audioWorklets, sampleManager, sampleService } } = project;
@@ -92,12 +162,12 @@ export class CaptureAudio extends Capture {
92
162
  if (!isDefined(audioChain)) {
93
163
  return Promise.reject("No audio chain available for recording.");
94
164
  }
95
- const { gainNode, channelCount } = audioChain;
165
+ const { recordGainNode, channelCount } = audioChain;
96
166
  const recordingWorklet = audioWorklets.createRecording(channelCount, RenderQuantum);
97
167
  recordingWorklet.bpm = project.timelineBox.bpm.getValue();
98
168
  recordingWorklet.sampleService = sampleService;
99
169
  sampleManager.record(recordingWorklet);
100
- gainNode.connect(recordingWorklet);
170
+ recordGainNode.connect(recordingWorklet);
101
171
  this.#preparedWorklet = recordingWorklet;
102
172
  }
103
173
  startRecording() {
@@ -110,10 +180,10 @@ export class CaptureAudio extends Capture {
110
180
  return Terminable.Empty;
111
181
  }
112
182
  this.#preparedWorklet = null;
113
- const { gainNode } = audioChain;
183
+ const { recordGainNode } = audioChain;
114
184
  return RecordAudio.start({
115
185
  recordingWorklet,
116
- sourceNode: gainNode,
186
+ sourceNode: recordGainNode,
117
187
  sampleManager,
118
188
  project,
119
189
  capture: this,
@@ -157,47 +227,51 @@ export class CaptureAudio extends Capture {
157
227
  });
158
228
  }
159
229
  #stopStream() {
230
+ this.#disconnectMonitoring();
160
231
  this.#destroyAudioChain();
161
232
  this.#stream.clear(stream => stream.getAudioTracks().forEach(track => track.stop()));
162
233
  }
163
234
  #rebuildAudioChain(stream) {
164
- const wasMonitoringMode = this.#monitoringMode !== "off" && isDefined(this.#audioChain) ? this.#monitoringMode : "off";
235
+ this.#disconnectMonitoring();
165
236
  this.#destroyAudioChain();
166
237
  const { audioContext } = this.manager.project.env;
167
238
  const sourceNode = audioContext.createMediaStreamSource(stream);
168
- const gainNode = audioContext.createGain();
169
- gainNode.gain.value = dbToGain(this.#gainDb);
239
+ const recordGainNode = audioContext.createGain();
240
+ recordGainNode.gain.value = dbToGain(this.#gainDb);
170
241
  const streamChannelCount = Math.min(stream.getAudioTracks().at(0)?.getSettings().channelCount ?? 2, 2);
171
242
  const channelCount = this.#requestChannels.unwrapOrElse(streamChannelCount);
172
- gainNode.channelCount = channelCount;
173
- gainNode.channelCountMode = "explicit";
174
- sourceNode.connect(gainNode);
175
- this.#audioChain = { sourceNode, gainNode, channelCount };
176
- if (wasMonitoringMode !== "off" || this.#monitoringMode !== "off") {
177
- this.#connectMonitoring();
178
- }
243
+ recordGainNode.channelCount = channelCount;
244
+ recordGainNode.channelCountMode = "explicit";
245
+ sourceNode.connect(recordGainNode);
246
+ this.#audioChain = { sourceNode, recordGainNode, channelCount };
247
+ this.#connectMonitoring();
179
248
  }
180
249
  #destroyAudioChain() {
181
250
  if (isDefined(this.#audioChain)) {
182
- const { sourceNode, gainNode } = this.#audioChain;
251
+ const { sourceNode, recordGainNode } = this.#audioChain;
183
252
  sourceNode.disconnect();
184
- gainNode.disconnect();
253
+ recordGainNode.disconnect();
185
254
  this.#audioChain = null;
186
255
  }
187
256
  }
257
+ #monitorDestination() {
258
+ return this.#monitorStreamDest ?? this.manager.project.env.audioContext.destination;
259
+ }
188
260
  #connectMonitoring() {
189
261
  if (!isDefined(this.#audioChain)) {
190
262
  return;
191
263
  }
264
+ const { sourceNode, channelCount } = this.#audioChain;
192
265
  switch (this.#monitoringMode) {
193
266
  case "off":
194
267
  break;
195
268
  case "direct":
196
- this.#audioChain.gainNode.connect(this.manager.project.env.audioContext.destination);
269
+ sourceNode.connect(this.#monitorGainNode);
270
+ this.#monitorPanNode.connect(this.#monitorDestination());
197
271
  break;
198
272
  case "effects":
199
- const engine = this.manager.project.engine;
200
- engine.registerMonitoringSource(this.audioUnitBox.address.uuid, this.#audioChain.gainNode, this.#audioChain.channelCount);
273
+ this.manager.project.engine.registerMonitoringSource(this.audioUnitBox.address.uuid, sourceNode, channelCount, this.#monitorGainNode);
274
+ this.#monitorPanNode.connect(this.#monitorDestination());
201
275
  break;
202
276
  }
203
277
  }
@@ -209,11 +283,12 @@ export class CaptureAudio extends Capture {
209
283
  case "off":
210
284
  break;
211
285
  case "direct":
212
- this.#audioChain.gainNode.disconnect(this.manager.project.env.audioContext.destination);
286
+ this.#audioChain.sourceNode.disconnect(this.#monitorGainNode);
287
+ this.#monitorPanNode.disconnect(this.#monitorDestination());
213
288
  break;
214
289
  case "effects":
215
- this.#audioChain.gainNode.disconnect();
216
290
  this.manager.project.engine.unregisterMonitoringSource(this.audioUnitBox.address.uuid);
291
+ this.#monitorPanNode.disconnect(this.#monitorDestination());
217
292
  break;
218
293
  }
219
294
  }
@@ -1 +1 @@
1
- {"version":3,"file":"RecordAudio.d.ts","sourceRoot":"","sources":["../../src/capture/RecordAudio.ts"],"names":[],"mappings":"AAAA,OAAO,EAMH,UAAU,EAIb,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EAAa,mBAAmB,EAA2B,MAAM,0BAA0B,CAAA;AAClG,OAAO,EAAC,OAAO,EAAC,MAAM,YAAY,CAAA;AAClC,OAAO,EAAC,gBAAgB,EAAC,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAC,OAAO,EAAC,MAAM,WAAW,CAAA;AAIjC,yBAAiB,WAAW,CAAC;IACzB,KAAK,kBAAkB,GAAG;QACtB,gBAAgB,EAAE,gBAAgB,CAAA;QAClC,UAAU,EAAE,SAAS,CAAA;QACrB,aAAa,EAAE,mBAAmB,CAAA;QAClC,OAAO,EAAE,OAAO,CAAA;QAChB,OAAO,EAAE,OAAO,CAAA;QAChB,aAAa,EAAE,MAAM,CAAA;KACxB,CAAA;IAOD,MAAM,CAAC,MAAM,KAAK,GACd,kFAAgF,kBAAkB,KAChG,UA8ML,CAAA;;CACJ"}
1
+ {"version":3,"file":"RecordAudio.d.ts","sourceRoot":"","sources":["../../src/capture/RecordAudio.ts"],"names":[],"mappings":"AAAA,OAAO,EAMH,UAAU,EAIb,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EAAa,mBAAmB,EAA2B,MAAM,0BAA0B,CAAA;AAClG,OAAO,EAAC,OAAO,EAAC,MAAM,YAAY,CAAA;AAClC,OAAO,EAAC,gBAAgB,EAAC,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAC,OAAO,EAAC,MAAM,WAAW,CAAA;AAIjC,yBAAiB,WAAW,CAAC;IACzB,KAAK,kBAAkB,GAAG;QACtB,gBAAgB,EAAE,gBAAgB,CAAA;QAClC,UAAU,EAAE,SAAS,CAAA;QACrB,aAAa,EAAE,mBAAmB,CAAA;QAClC,OAAO,EAAE,OAAO,CAAA;QAChB,OAAO,EAAE,OAAO,CAAA;QAChB,aAAa,EAAE,MAAM,CAAA;KACxB,CAAA;IAOD,MAAM,CAAC,MAAM,KAAK,GACd,kFAAgF,kBAAkB,KAChG,UAsNL,CAAA;;CACJ"}
@@ -124,8 +124,9 @@ export var RecordAudio;
124
124
  pointer.refer(newFileBox.transientMarkers);
125
125
  }
126
126
  oldFileBox.delete();
127
- });
127
+ }, false);
128
128
  });
129
+ editing.mark();
129
130
  };
130
131
  terminator.ownAll(Terminable.create(() => {
131
132
  tryCatch(() => sourceNode.disconnect(recordingWorklet));
@@ -178,10 +179,17 @@ export var RecordAudio;
178
179
  editing.modify(() => {
179
180
  currentTake.ifSome(take => {
180
181
  const actualDurationInSeconds = take.regionBox.duration.getValue();
182
+ if (actualDurationInSeconds <= 0) {
183
+ take.regionBox.delete();
184
+ currentTake = Option.None;
185
+ return;
186
+ }
181
187
  finalizeTake(take, actualDurationInSeconds);
182
188
  currentWaveformOffset += actualDurationInSeconds;
183
189
  });
184
- startNewTake(loopFrom);
190
+ if (currentTake.nonEmpty()) {
191
+ startNewTake(loopFrom);
192
+ }
185
193
  }, false);
186
194
  }
187
195
  lastPosition = currentPosition;
@@ -37,7 +37,7 @@ export class Recording {
37
37
  if (isRecording.getValue() || isCountingIn.getValue()) {
38
38
  return;
39
39
  }
40
- editing.modify(() => terminator.terminate()); // finalizes recording
40
+ editing.modify(() => terminator.terminate(), false); // finalizes recording
41
41
  this.#isRecording = false;
42
42
  };
43
43
  terminator.ownAll(engine.isRecording.subscribe(stop), engine.isCountingIn.subscribe(stop), Terminable.create(() => Recording.#instance = Option.None));