@opendaw/studio-core 0.0.19 → 0.0.21

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 (110) hide show
  1. package/dist/AudioDevices.d.ts +8 -0
  2. package/dist/AudioDevices.d.ts.map +1 -0
  3. package/dist/AudioDevices.js +34 -0
  4. package/dist/AudioUnitOrdering.d.ts +3 -0
  5. package/dist/AudioUnitOrdering.d.ts.map +1 -0
  6. package/dist/AudioUnitOrdering.js +7 -0
  7. package/dist/EffectBox.d.ts +2 -2
  8. package/dist/EffectBox.d.ts.map +1 -1
  9. package/dist/Engine.d.ts +31 -10
  10. package/dist/Engine.d.ts.map +1 -1
  11. package/dist/EngineFacade.d.ts +22 -11
  12. package/dist/EngineFacade.d.ts.map +1 -1
  13. package/dist/EngineFacade.js +39 -22
  14. package/dist/EngineWorklet.d.ts +22 -13
  15. package/dist/EngineWorklet.d.ts.map +1 -1
  16. package/dist/EngineWorklet.js +47 -56
  17. package/dist/MeterWorklet.d.ts.map +1 -1
  18. package/dist/MeterWorklet.js +2 -2
  19. package/dist/MidiDevices.d.ts +12 -0
  20. package/dist/MidiDevices.d.ts.map +1 -0
  21. package/dist/MidiDevices.js +92 -0
  22. package/dist/Project.d.ts +14 -3
  23. package/dist/Project.d.ts.map +1 -1
  24. package/dist/Project.js +27 -4
  25. package/dist/ProjectApi.d.ts +0 -1
  26. package/dist/ProjectApi.d.ts.map +1 -1
  27. package/dist/ProjectApi.js +16 -11
  28. package/dist/ProjectMigration.d.ts.map +1 -1
  29. package/dist/ProjectMigration.js +20 -3
  30. package/dist/RecordingWorklet.d.ts +15 -3
  31. package/dist/RecordingWorklet.d.ts.map +1 -1
  32. package/dist/RecordingWorklet.js +111 -16
  33. package/dist/WorkerAgents.d.ts +2 -2
  34. package/dist/WorkerAgents.d.ts.map +1 -1
  35. package/dist/Worklets.d.ts +1 -1
  36. package/dist/Worklets.d.ts.map +1 -1
  37. package/dist/Worklets.js +2 -2
  38. package/dist/capture/Capture.d.ts +22 -0
  39. package/dist/capture/Capture.d.ts.map +1 -0
  40. package/dist/capture/Capture.js +32 -0
  41. package/dist/capture/CaptureAudio.d.ts +17 -0
  42. package/dist/capture/CaptureAudio.d.ts.map +1 -0
  43. package/dist/capture/CaptureAudio.js +106 -0
  44. package/dist/capture/CaptureManager.d.ts +12 -0
  45. package/dist/capture/CaptureManager.d.ts.map +1 -0
  46. package/dist/capture/CaptureManager.js +38 -0
  47. package/dist/capture/CaptureMidi.d.ts +13 -0
  48. package/dist/capture/CaptureMidi.d.ts.map +1 -0
  49. package/dist/capture/CaptureMidi.js +106 -0
  50. package/dist/capture/RecordAudio.d.ts +19 -0
  51. package/dist/capture/RecordAudio.d.ts.map +1 -0
  52. package/dist/capture/RecordAudio.js +66 -0
  53. package/dist/capture/RecordMidi.d.ts +13 -0
  54. package/dist/capture/RecordMidi.d.ts.map +1 -0
  55. package/dist/capture/RecordMidi.js +85 -0
  56. package/dist/capture/RecordTrack.d.ts +7 -0
  57. package/dist/capture/RecordTrack.d.ts.map +1 -0
  58. package/dist/capture/RecordTrack.js +23 -0
  59. package/dist/capture/Recording.d.ts +9 -0
  60. package/dist/capture/Recording.d.ts.map +1 -0
  61. package/dist/capture/Recording.js +65 -0
  62. package/dist/capture/RecordingContext.d.ts +10 -0
  63. package/dist/capture/RecordingContext.d.ts.map +1 -0
  64. package/dist/capture/RecordingContext.js +1 -0
  65. package/dist/dawproject/AudioUnitExportLayout.d.ts +10 -0
  66. package/dist/dawproject/AudioUnitExportLayout.d.ts.map +1 -0
  67. package/dist/dawproject/AudioUnitExportLayout.js +64 -0
  68. package/dist/dawproject/BuiltinDevices.d.ts +9 -0
  69. package/dist/dawproject/BuiltinDevices.d.ts.map +1 -0
  70. package/dist/dawproject/BuiltinDevices.js +70 -0
  71. package/dist/dawproject/{DawProjectIO.d.ts → DawProject.d.ts} +6 -4
  72. package/dist/dawproject/DawProject.d.ts.map +1 -0
  73. package/dist/dawproject/DawProject.js +48 -0
  74. package/dist/dawproject/DawProjectExporter.d.ts +7 -6
  75. package/dist/dawproject/DawProjectExporter.d.ts.map +1 -1
  76. package/dist/dawproject/DawProjectExporter.js +245 -22
  77. package/dist/dawproject/DawProjectExporter.test.js +38 -6
  78. package/dist/dawproject/DawProjectImport.d.ts +12 -0
  79. package/dist/dawproject/DawProjectImport.d.ts.map +1 -0
  80. package/dist/dawproject/DawProjectImport.js +389 -0
  81. package/dist/dawproject/DawProjectImport.test.js +6 -7
  82. package/dist/dawproject/DeviceIO.d.ts +8 -0
  83. package/dist/dawproject/DeviceIO.d.ts.map +1 -0
  84. package/dist/dawproject/DeviceIO.js +63 -0
  85. package/dist/index.d.ts +13 -3
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +13 -3
  88. package/dist/processors.js +3 -3
  89. package/dist/processors.js.map +4 -4
  90. package/dist/samples/MainThreadSampleLoader.d.ts +1 -0
  91. package/dist/samples/MainThreadSampleLoader.d.ts.map +1 -1
  92. package/dist/samples/MainThreadSampleLoader.js +10 -6
  93. package/dist/samples/MainThreadSampleManager.d.ts +5 -5
  94. package/dist/samples/MainThreadSampleManager.d.ts.map +1 -1
  95. package/dist/samples/MainThreadSampleManager.js +1 -0
  96. package/dist/samples/SampleProvider.d.ts +2 -2
  97. package/dist/samples/SampleProvider.d.ts.map +1 -1
  98. package/dist/samples/SampleStorage.d.ts.map +1 -1
  99. package/dist/samples/SampleStorage.js +2 -3
  100. package/dist/workers.js +2 -2
  101. package/dist/workers.js.map +4 -4
  102. package/package.json +15 -15
  103. package/dist/dawproject/DawProjectIO.d.ts.map +0 -1
  104. package/dist/dawproject/DawProjectIO.js +0 -31
  105. package/dist/dawproject/DawProjectImporter.d.ts +0 -12
  106. package/dist/dawproject/DawProjectImporter.d.ts.map +0 -1
  107. package/dist/dawproject/DawProjectImporter.js +0 -273
  108. package/dist/samples/SamplePeaks.d.ts +0 -6
  109. package/dist/samples/SamplePeaks.d.ts.map +0 -1
  110. package/dist/samples/SamplePeaks.js +0 -9
@@ -0,0 +1,32 @@
1
+ import { DefaultObservableValue, MappedMutableObservableValue, Option, Terminator } from "@opendaw/lib-std";
2
+ export class Capture {
3
+ #terminator = new Terminator();
4
+ #manager;
5
+ #audioUnitBox;
6
+ #captureBox;
7
+ #deviceId;
8
+ #armed;
9
+ constructor(manager, audioUnitBox, captureBox) {
10
+ this.#manager = manager;
11
+ this.#audioUnitBox = audioUnitBox;
12
+ this.#captureBox = captureBox;
13
+ this.#deviceId = new MappedMutableObservableValue(captureBox.deviceId, {
14
+ fx: x => x.length > 0 ? Option.wrap(x) : Option.None,
15
+ fy: y => y.match({ none: () => "", some: x => x })
16
+ });
17
+ this.#armed = this.#terminator.own(new DefaultObservableValue(false));
18
+ this.#terminator.ownAll(this.#captureBox.deviceId.catchupAndSubscribe(owner => {
19
+ const id = owner.getValue();
20
+ this.#deviceId.setValue(id.length > 0 ? Option.wrap(id) : Option.None);
21
+ }));
22
+ }
23
+ get uuid() { return this.#audioUnitBox.address.uuid; }
24
+ get manager() { return this.#manager; }
25
+ get audioUnitBox() { return this.#audioUnitBox; }
26
+ get captureBox() { return this.#captureBox; }
27
+ get armed() { return this.#armed; }
28
+ get deviceId() { return this.#deviceId; }
29
+ own(terminable) { return this.#terminator.own(terminable); }
30
+ ownAll(...terminables) { this.#terminator.ownAll(...terminables); }
31
+ terminate() { this.#terminator.terminate(); }
32
+ }
@@ -0,0 +1,17 @@
1
+ import { MutableObservableOption, Option, Terminable } from "@opendaw/lib-std";
2
+ import { AudioUnitBox, CaptureAudioBox } from "@opendaw/studio-boxes";
3
+ import { Capture } from "./Capture";
4
+ import { CaptureManager } from "./CaptureManager";
5
+ import { RecordingContext } from "./RecordingContext";
6
+ export declare class CaptureAudio extends Capture<CaptureAudioBox> {
7
+ #private;
8
+ constructor(manager: CaptureManager, audioUnitBox: AudioUnitBox, captureAudioBox: CaptureAudioBox);
9
+ get gainDb(): number;
10
+ get stream(): MutableObservableOption<MediaStream>;
11
+ get streamDeviceId(): Option<string>;
12
+ get deviceLabel(): Option<string>;
13
+ get streamMediaTrack(): Option<MediaStreamTrack>;
14
+ prepareRecording({}: RecordingContext): Promise<void>;
15
+ startRecording({ audioContext, worklets, project, sampleManager }: RecordingContext): Terminable;
16
+ }
17
+ //# sourceMappingURL=CaptureAudio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CaptureAudio.d.ts","sourceRoot":"","sources":["../../src/capture/CaptureAudio.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuC,uBAAuB,EAAE,MAAM,EAAE,UAAU,EAAO,MAAM,kBAAkB,CAAA;AAExH,OAAO,EAAC,YAAY,EAAE,eAAe,EAAC,MAAM,uBAAuB,CAAA;AACnE,OAAO,EAAC,OAAO,EAAC,MAAM,WAAW,CAAA;AACjC,OAAO,EAAC,cAAc,EAAC,MAAM,kBAAkB,CAAA;AAE/C,OAAO,EAAC,gBAAgB,EAAC,MAAM,oBAAoB,CAAA;AAGnD,qBAAa,YAAa,SAAQ,OAAO,CAAC,eAAe,CAAC;;gBAQ1C,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,eAAe;IA4BjG,IAAI,MAAM,IAAI,MAAM,CAAsB;IAE1C,IAAI,MAAM,IAAI,uBAAuB,CAAC,WAAW,CAAC,CAAsB;IAExE,IAAI,cAAc,IAAI,MAAM,CAAC,MAAM,CAAC,CAEnC;IAED,IAAI,WAAW,IAAI,MAAM,CAAC,MAAM,CAAC,CAEhC;IAED,IAAI,gBAAgB,IAAI,MAAM,CAAC,gBAAgB,CAAC,CAE/C;IAEK,gBAAgB,CAAC,EAAE,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3D,cAAc,CAAC,EAAC,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,aAAa,EAAC,EAAE,gBAAgB,GAAG,UAAU;CA6DjG"}
@@ -0,0 +1,106 @@
1
+ import { assert, isDefined, isUndefined, MutableObservableOption, Option, warn } from "@opendaw/lib-std";
2
+ import { Promises } from "@opendaw/lib-runtime";
3
+ import { Capture } from "./Capture";
4
+ import { RecordAudio } from "./RecordAudio";
5
+ import { AudioDevices } from "../AudioDevices";
6
+ export class CaptureAudio extends Capture {
7
+ #stream;
8
+ #streamGenerator;
9
+ #requestChannels = Option.None;
10
+ #gainDb = 0.0;
11
+ constructor(manager, audioUnitBox, captureAudioBox) {
12
+ super(manager, audioUnitBox, captureAudioBox);
13
+ this.#stream = new MutableObservableOption();
14
+ this.#streamGenerator = Promises.sequential(() => this.#updateStream());
15
+ this.ownAll(captureAudioBox.requestChannels.catchupAndSubscribe(owner => {
16
+ const channels = owner.getValue();
17
+ this.#requestChannels = channels === 1 || channels === 2 ? Option.wrap(channels) : Option.None;
18
+ }), captureAudioBox.gainDb.catchupAndSubscribe(owner => this.#gainDb = owner.getValue()), captureAudioBox.deviceId.catchupAndSubscribe(async () => {
19
+ if (this.armed.getValue()) {
20
+ await this.#streamGenerator();
21
+ }
22
+ }), this.armed.catchupAndSubscribe(async (owner) => {
23
+ const armed = owner.getValue();
24
+ if (armed) {
25
+ await this.#streamGenerator();
26
+ }
27
+ else {
28
+ this.#stopStream();
29
+ }
30
+ }));
31
+ }
32
+ get gainDb() { return this.#gainDb; }
33
+ get stream() { return this.#stream; }
34
+ get streamDeviceId() {
35
+ return this.streamMediaTrack.map(settings => settings.getSettings().deviceId ?? "");
36
+ }
37
+ get deviceLabel() {
38
+ return this.streamMediaTrack.map(track => track.label ?? "");
39
+ }
40
+ get streamMediaTrack() {
41
+ return this.#stream.flatMap(stream => Option.wrap(stream.getAudioTracks().at(0)));
42
+ }
43
+ async prepareRecording({}) {
44
+ return this.#streamGenerator();
45
+ }
46
+ startRecording({ audioContext, worklets, project, sampleManager }) {
47
+ const streamOption = this.#stream;
48
+ assert(streamOption.nonEmpty(), "Stream not prepared.");
49
+ const mediaStream = streamOption.unwrap();
50
+ const channelCount = mediaStream.getAudioTracks().at(0)?.getSettings().channelCount ?? 1;
51
+ const numChunks = 128;
52
+ const recordingWorklet = worklets.createRecording(channelCount, numChunks, audioContext.outputLatency);
53
+ return RecordAudio.start({
54
+ recordingWorklet,
55
+ mediaStream,
56
+ sampleManager,
57
+ audioContext,
58
+ project,
59
+ capture: this,
60
+ gainDb: this.#gainDb
61
+ });
62
+ }
63
+ async #updateStream() {
64
+ if (this.#stream.nonEmpty()) {
65
+ const stream = this.#stream.unwrap();
66
+ const settings = stream.getAudioTracks().at(0)?.getSettings();
67
+ console.debug(stream.getAudioTracks());
68
+ if (isDefined(settings)) {
69
+ const deviceId = this.deviceId.getValue().unwrapOrUndefined();
70
+ const channelCount = this.#requestChannels.unwrapOrElse(1);
71
+ const satisfyChannelCount = settings.channelCount === channelCount;
72
+ const satisfiedDeviceId = isUndefined(deviceId) || deviceId === settings.deviceId;
73
+ if (satisfiedDeviceId && satisfyChannelCount) {
74
+ return Promise.resolve();
75
+ }
76
+ }
77
+ }
78
+ this.#stopStream();
79
+ const deviceId = this.deviceId.getValue().unwrapOrUndefined();
80
+ const channelCount = this.#requestChannels.unwrapOrElse(1); // as of today, browsers cap MediaStream audio to stereo.
81
+ return AudioDevices.requestStream({
82
+ deviceId: { exact: deviceId },
83
+ sampleRate: this.manager.project.engine.sampleRate(),
84
+ sampleSize: 32,
85
+ echoCancellation: false,
86
+ noiseSuppression: false,
87
+ autoGainControl: false,
88
+ channelCount
89
+ }).then(stream => {
90
+ const tracks = stream.getAudioTracks();
91
+ const settings = tracks.at(0)?.getSettings();
92
+ const gotDeviceId = settings?.deviceId;
93
+ console.debug(`new stream id: ${stream.id}, device: ${gotDeviceId ?? "Default"}`);
94
+ if (isUndefined(deviceId) || deviceId === gotDeviceId) {
95
+ this.#stream.wrap(stream);
96
+ }
97
+ else {
98
+ stream.getAudioTracks().forEach(track => track.stop());
99
+ return warn(`Could not find audio device with id: '${deviceId} in ${gotDeviceId}'`);
100
+ }
101
+ });
102
+ }
103
+ #stopStream() {
104
+ this.#stream.clear(stream => stream.getAudioTracks().forEach(track => track.stop()));
105
+ }
106
+ }
@@ -0,0 +1,12 @@
1
+ import { Option, Terminable, UUID } from "@opendaw/lib-std";
2
+ import { Project } from "../Project";
3
+ import { Capture } from "./Capture";
4
+ export declare class CaptureManager implements Terminable {
5
+ #private;
6
+ constructor(project: Project);
7
+ get project(): Project;
8
+ get(uuid: UUID.Format): Option<Capture>;
9
+ filterArmed(): ReadonlyArray<Capture>;
10
+ terminate(): void;
11
+ }
12
+ //# sourceMappingURL=CaptureManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CaptureManager.d.ts","sourceRoot":"","sources":["../../src/capture/CaptureManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,MAAM,EAA2B,UAAU,EAAE,IAAI,EAAC,MAAM,kBAAkB,CAAA;AAEpH,OAAO,EAAC,OAAO,EAAC,MAAM,YAAY,CAAA;AAClC,OAAO,EAAC,OAAO,EAAC,MAAM,WAAW,CAAA;AAIjC,qBAAa,cAAe,YAAW,UAAU;;gBAKjC,OAAO,EAAE,OAAO;IAiB5B,IAAI,OAAO,IAAI,OAAO,CAAuB;IAE7C,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC;IAEvC,WAAW,IAAI,aAAa,CAAC,OAAO,CAAC;IAKrC,SAAS,IAAI,IAAI;CAKpB"}
@@ -0,0 +1,38 @@
1
+ import { asInstanceOf, isDefined, UUID } from "@opendaw/lib-std";
2
+ import { AudioUnitBox } from "@opendaw/studio-boxes";
3
+ import { CaptureMidi } from "./CaptureMidi";
4
+ import { CaptureAudio } from "./CaptureAudio";
5
+ export class CaptureManager {
6
+ #project;
7
+ #subscription;
8
+ #captures;
9
+ constructor(project) {
10
+ this.#project = project;
11
+ this.#captures = UUID.newSet(unit => unit.uuid);
12
+ this.#subscription = this.#project.rootBox.audioUnits.pointerHub.catchupAndSubscribeTransactual({
13
+ onAdd: ({ box }) => {
14
+ const audioUnitBox = asInstanceOf(box, AudioUnitBox);
15
+ const capture = audioUnitBox.capture.targetVertex
16
+ .ifSome(({ box }) => box.accept({
17
+ visitCaptureMidiBox: (box) => new CaptureMidi(this, audioUnitBox, box),
18
+ visitCaptureAudioBox: (box) => new CaptureAudio(this, audioUnitBox, box)
19
+ }));
20
+ if (isDefined(capture)) {
21
+ this.#captures.add(capture);
22
+ }
23
+ },
24
+ onRemove: ({ box: { address: { uuid } } }) => this.#captures.removeByKeyIfExist(uuid)?.terminate()
25
+ });
26
+ }
27
+ get project() { return this.#project; }
28
+ get(uuid) { return this.#captures.opt(uuid); }
29
+ filterArmed() {
30
+ return this.#captures.values()
31
+ .filter(capture => capture.armed.getValue() && capture.audioUnitBox.input.pointerHub.nonEmpty());
32
+ }
33
+ terminate() {
34
+ this.#subscription.terminate();
35
+ this.#captures.forEach(capture => capture.terminate());
36
+ this.#captures.clear();
37
+ }
38
+ }
@@ -0,0 +1,13 @@
1
+ import { Option, Terminable } from "@opendaw/lib-std";
2
+ import { AudioUnitBox, CaptureMidiBox } from "@opendaw/studio-boxes";
3
+ import { Capture } from "./Capture";
4
+ import { RecordingContext } from "./RecordingContext";
5
+ import { CaptureManager } from "./CaptureManager";
6
+ export declare class CaptureMidi extends Capture<CaptureMidiBox> {
7
+ #private;
8
+ constructor(manager: CaptureManager, audioUnitBox: AudioUnitBox, captureMidiBox: CaptureMidiBox);
9
+ get deviceLabel(): Option<string>;
10
+ prepareRecording({}: RecordingContext): Promise<void>;
11
+ startRecording({ project }: RecordingContext): Terminable;
12
+ }
13
+ //# sourceMappingURL=CaptureMidi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CaptureMidi.d.ts","sourceRoot":"","sources":["../../src/capture/CaptureMidi.ts"],"names":[],"mappings":"AAAA,OAAO,EAQH,MAAM,EAEN,UAAU,EAEb,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EAAC,YAAY,EAAE,cAAc,EAAC,MAAM,uBAAuB,CAAA;AAClE,OAAO,EAAC,OAAO,EAAC,MAAM,WAAW,CAAA;AAEjC,OAAO,EAAC,gBAAgB,EAAC,MAAM,oBAAoB,CAAA;AACnD,OAAO,EAAC,cAAc,EAAC,MAAM,kBAAkB,CAAA;AAI/C,qBAAa,WAAY,SAAQ,OAAO,CAAC,cAAc,CAAC;;gBAQxC,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc;IAsC/F,IAAI,WAAW,IAAI,MAAM,CAAC,MAAM,CAAC,CAA0C;IAErE,gBAAgB,CAAC,EAAE,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB3D,cAAc,CAAC,EAAC,OAAO,EAAC,EAAE,gBAAgB,GAAG,UAAU;CA8C1D"}
@@ -0,0 +1,106 @@
1
+ import { asDefined, assert, isDefined, isUndefined, Notifier, Option, Terminable, warn } from "@opendaw/lib-std";
2
+ import { Events } from "@opendaw/lib-dom";
3
+ import { MidiData } from "@opendaw/lib-midi";
4
+ import { Capture } from "./Capture";
5
+ import { RecordMidi } from "./RecordMidi";
6
+ import { MidiDevices } from "../MidiDevices";
7
+ import { Promises } from "@opendaw/lib-runtime";
8
+ export class CaptureMidi extends Capture {
9
+ #streamGenerator;
10
+ #notifier = new Notifier();
11
+ #filterChannel = Option.None;
12
+ #streaming = Option.None;
13
+ constructor(manager, audioUnitBox, captureMidiBox) {
14
+ super(manager, audioUnitBox, captureMidiBox);
15
+ this.#streamGenerator = Promises.sequential(() => this.#updateStream());
16
+ this.ownAll(captureMidiBox.channel.subscribe(async (owner) => {
17
+ const channel = owner.getValue();
18
+ this.#filterChannel = channel >= 0 ? Option.wrap(channel) : Option.None;
19
+ await this.#streamGenerator();
20
+ }), captureMidiBox.deviceId.catchupAndSubscribe(async () => {
21
+ if (this.armed.getValue()) {
22
+ await this.#streamGenerator();
23
+ }
24
+ }), this.armed.catchupAndSubscribe(async (owner) => {
25
+ const armed = owner.getValue();
26
+ if (armed) {
27
+ await this.#streamGenerator();
28
+ }
29
+ else {
30
+ this.#stopStream();
31
+ }
32
+ }), this.#notifier.subscribe(event => {
33
+ console.debug(MidiData.debug(event.data));
34
+ const data = asDefined(event.data);
35
+ const engine = manager.project.engine;
36
+ const isNoteOn = MidiData.isNoteOn(data);
37
+ if (MidiData.isNoteOff(data) || (isNoteOn && MidiData.readVelocity(data) === 0)) {
38
+ engine.noteOff(this.uuid, MidiData.readPitch(data));
39
+ }
40
+ else if (isNoteOn) {
41
+ engine.noteOn(this.uuid, MidiData.readPitch(data), MidiData.readVelocity(data));
42
+ }
43
+ }));
44
+ }
45
+ get deviceLabel() { return Option.wrap("MIDI coming soon."); }
46
+ async prepareRecording({}) {
47
+ const availableMidiDevices = MidiDevices.get();
48
+ if (availableMidiDevices.isEmpty()) {
49
+ return Promise.reject("MIDI is not available");
50
+ }
51
+ const option = this.deviceId.getValue();
52
+ if (option.nonEmpty()) {
53
+ const { inputs } = availableMidiDevices.unwrap();
54
+ const captureDevices = Array.from(inputs.values());
55
+ const deviceId = option.unwrap();
56
+ if (isUndefined(captureDevices.find(device => deviceId === device.id))) {
57
+ return warn(`Could not find MIDI device with id: '${deviceId}'`);
58
+ }
59
+ }
60
+ }
61
+ startRecording({ project }) {
62
+ const availableMidiDevices = MidiDevices.inputs();
63
+ assert(availableMidiDevices.nonEmpty(), "No MIDI input devices found");
64
+ return RecordMidi.start({ notifier: this.#notifier, project, capture: this });
65
+ }
66
+ async #updateStream() {
67
+ // TODO Check if the requirements have been changed (are different than the current stream setup)
68
+ if (MidiDevices.get().isEmpty()) {
69
+ await MidiDevices.requestPermission();
70
+ }
71
+ const availableMidiDevices = MidiDevices.inputs();
72
+ const inputs = availableMidiDevices.unwrap();
73
+ const captureDevices = this.deviceId.getValue().match({
74
+ none: () => inputs,
75
+ some: id => inputs.filter(device => id === device.id)
76
+ });
77
+ const activeNotes = new Int8Array(128);
78
+ this.#streaming.ifSome(terminable => terminable.terminate());
79
+ this.#streaming = Option.wrap(Terminable.many(...captureDevices.map(input => Events.subscribe(input, "midimessage", (event) => {
80
+ const data = event.data;
81
+ if (isDefined(data) &&
82
+ this.#filterChannel.mapOr(channel => MidiData.readChannel(data) === channel, true)) {
83
+ if (MidiData.isNoteOn(data)) {
84
+ activeNotes[MidiData.readPitch(data)]++;
85
+ this.#notifier.notify(event);
86
+ }
87
+ else if (MidiData.isNoteOff(data)) {
88
+ activeNotes[MidiData.readPitch(data)]--;
89
+ this.#notifier.notify(event);
90
+ }
91
+ }
92
+ })), Terminable.create(() => activeNotes.forEach((count, index) => {
93
+ if (count > 0) {
94
+ // TODO respect channel!
95
+ const event = new MessageEvent("midimessage", { data: MidiData.noteOff(index, count) });
96
+ for (let i = 0; i < count; i++) {
97
+ this.#notifier.notify(event);
98
+ }
99
+ }
100
+ }))));
101
+ }
102
+ #stopStream() {
103
+ this.#streaming.ifSome(terminable => terminable.terminate());
104
+ this.#streaming = Option.None;
105
+ }
106
+ }
@@ -0,0 +1,19 @@
1
+ import { Terminable } from "@opendaw/lib-std";
2
+ import { SampleManager } from "@opendaw/studio-adapters";
3
+ import { Project } from "../Project";
4
+ import { Capture } from "./Capture";
5
+ import { RecordingWorklet } from "../RecordingWorklet";
6
+ export declare namespace RecordAudio {
7
+ type RecordAudioContext = {
8
+ recordingWorklet: RecordingWorklet;
9
+ mediaStream: MediaStream;
10
+ sampleManager: SampleManager;
11
+ audioContext: AudioContext;
12
+ project: Project;
13
+ capture: Capture;
14
+ gainDb: number;
15
+ };
16
+ export const start: ({ recordingWorklet, mediaStream, sampleManager, audioContext, project, capture, gainDb }: RecordAudioContext) => Terminable;
17
+ export {};
18
+ }
19
+ //# sourceMappingURL=RecordAudio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecordAudio.d.ts","sourceRoot":"","sources":["../../src/capture/RecordAudio.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,UAAU,EAAmB,MAAM,kBAAkB,CAAA;AAGpF,OAAO,EAAC,aAAa,EAAY,MAAM,0BAA0B,CAAA;AACjE,OAAO,EAAC,OAAO,EAAC,MAAM,YAAY,CAAA;AAClC,OAAO,EAAC,OAAO,EAAC,MAAM,WAAW,CAAA;AAEjC,OAAO,EAAC,gBAAgB,EAAC,MAAM,qBAAqB,CAAA;AAGpD,yBAAiB,WAAW,CAAC;IACzB,KAAK,kBAAkB,GAAG;QACtB,gBAAgB,EAAE,gBAAgB,CAAA;QAClC,WAAW,EAAE,WAAW,CAAA;QACxB,aAAa,EAAE,aAAa,CAAA;QAC5B,YAAY,EAAE,YAAY,CAAA;QAC1B,OAAO,EAAE,OAAO,CAAA;QAChB,OAAO,EAAE,OAAO,CAAA;QAChB,MAAM,EAAE,MAAM,CAAA;KACjB,CAAA;IAED,MAAM,CAAC,MAAM,KAAK,GACd,0FAEG,kBAAkB,KAAG,UA2D3B,CAAA;;CACJ"}
@@ -0,0 +1,66 @@
1
+ import { Option, quantizeFloor, Terminable, Terminator, UUID } from "@opendaw/lib-std";
2
+ import { dbToGain, PPQN } from "@opendaw/lib-dsp";
3
+ import { AudioFileBox, AudioRegionBox } from "@opendaw/studio-boxes";
4
+ import { TrackType } from "@opendaw/studio-adapters";
5
+ import { RecordTrack } from "./RecordTrack";
6
+ import { ColorCodes } from "../ColorCodes";
7
+ export var RecordAudio;
8
+ (function (RecordAudio) {
9
+ RecordAudio.start = ({ recordingWorklet, mediaStream, sampleManager, audioContext, project, capture, gainDb }) => {
10
+ const terminator = new Terminator();
11
+ const beats = PPQN.fromSignature(1, project.timelineBox.signature.denominator.getValue());
12
+ const { editing, engine, boxGraph } = project;
13
+ const trackBox = RecordTrack.findOrCreate(editing, capture.audioUnitBox, TrackType.Audio);
14
+ const uuid = recordingWorklet.uuid;
15
+ sampleManager.record(recordingWorklet);
16
+ const streamSource = audioContext.createMediaStreamSource(mediaStream);
17
+ const streamGain = audioContext.createGain();
18
+ streamGain.gain.value = dbToGain(gainDb);
19
+ streamSource.connect(streamGain);
20
+ let writing = Option.None;
21
+ const resizeRegion = () => {
22
+ if (writing.isEmpty()) {
23
+ return;
24
+ }
25
+ const { regionBox } = writing.unwrap();
26
+ editing.modify(() => {
27
+ if (regionBox.isAttached()) {
28
+ const { duration, loopDuration } = regionBox;
29
+ const newDuration = Math.floor(PPQN.samplesToPulses(recordingWorklet.numberOfFrames, project.timelineBox.bpm.getValue(), audioContext.sampleRate));
30
+ duration.setValue(newDuration);
31
+ loopDuration.setValue(newDuration);
32
+ }
33
+ }, false);
34
+ };
35
+ terminator.ownAll(Terminable.create(() => {
36
+ recordingWorklet.finalize().then();
37
+ streamGain.disconnect();
38
+ streamSource.disconnect();
39
+ }), engine.position.catchupAndSubscribe(owner => {
40
+ if (writing.isEmpty() && engine.isRecording.getValue()) {
41
+ streamGain.connect(recordingWorklet);
42
+ writing = editing.modify(() => {
43
+ const position = quantizeFloor(owner.getValue(), beats);
44
+ const fileDateString = new Date()
45
+ .toISOString()
46
+ .replaceAll("T", "-")
47
+ .replaceAll(".", "-")
48
+ .replaceAll(":", "-")
49
+ .replaceAll("Z", "");
50
+ const fileName = `Recording-${fileDateString}`;
51
+ const fileBox = AudioFileBox.create(boxGraph, uuid, box => box.fileName.setValue(fileName));
52
+ const regionBox = AudioRegionBox.create(boxGraph, UUID.generate(), box => {
53
+ box.file.refer(fileBox);
54
+ box.regions.refer(trackBox.regions);
55
+ box.position.setValue(position);
56
+ box.hue.setValue(ColorCodes.forTrackType(TrackType.Audio));
57
+ box.label.setValue("Recording");
58
+ });
59
+ return { fileBox, regionBox };
60
+ });
61
+ }
62
+ resizeRegion();
63
+ }), Terminable.create(() => resizeRegion()));
64
+ return terminator;
65
+ };
66
+ })(RecordAudio || (RecordAudio = {}));
@@ -0,0 +1,13 @@
1
+ import { Notifier, Terminable } from "@opendaw/lib-std";
2
+ import { Project } from "../Project";
3
+ import { Capture } from "./Capture";
4
+ export declare namespace RecordMidi {
5
+ type RecordMidiContext = {
6
+ notifier: Notifier<MIDIMessageEvent>;
7
+ project: Project;
8
+ capture: Capture;
9
+ };
10
+ export const start: ({ notifier, project, capture }: RecordMidiContext) => Terminable;
11
+ export {};
12
+ }
13
+ //# sourceMappingURL=RecordMidi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecordMidi.d.ts","sourceRoot":"","sources":["../../src/capture/RecordMidi.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,QAAQ,EAIR,UAAU,EAGb,MAAM,kBAAkB,CAAA;AAKzB,OAAO,EAAC,OAAO,EAAC,MAAM,YAAY,CAAA;AAClC,OAAO,EAAC,OAAO,EAAC,MAAM,WAAW,CAAA;AAIjC,yBAAiB,UAAU,CAAC;IACxB,KAAK,iBAAiB,GAAG;QACrB,QAAQ,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAC;QACrC,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,OAAO,CAAA;KACnB,CAAA;IAED,MAAM,CAAC,MAAM,KAAK,GAAI,gCAA8B,iBAAiB,KAAG,UAiEvE,CAAA;;CACJ"}
@@ -0,0 +1,85 @@
1
+ import { isUndefined, Option, quantizeCeil, quantizeFloor, Terminator, UUID } from "@opendaw/lib-std";
2
+ import { PPQN } from "@opendaw/lib-dsp";
3
+ import { MidiData } from "@opendaw/lib-midi";
4
+ import { NoteEventBox, NoteEventCollectionBox, NoteRegionBox } from "@opendaw/studio-boxes";
5
+ import { TrackType } from "@opendaw/studio-adapters";
6
+ import { RecordTrack } from "./RecordTrack";
7
+ import { ColorCodes } from "../ColorCodes";
8
+ export var RecordMidi;
9
+ (function (RecordMidi) {
10
+ RecordMidi.start = ({ notifier, project, capture }) => {
11
+ console.debug("RecordMidi.start");
12
+ const beats = PPQN.fromSignature(1, project.timelineBox.signature.denominator.getValue());
13
+ const { editing, boxGraph } = project;
14
+ const trackBox = RecordTrack.findOrCreate(editing, capture.audioUnitBox, TrackType.Notes);
15
+ const terminator = new Terminator();
16
+ const activeNotes = new Map();
17
+ let writing = Option.None;
18
+ terminator.own(project.engine.position.catchupAndSubscribe(owner => {
19
+ if (writing.isEmpty()) {
20
+ return;
21
+ }
22
+ const writePosition = owner.getValue();
23
+ const { region, collection } = writing.unwrap();
24
+ editing.modify(() => {
25
+ if (region.isAttached() && collection.isAttached()) {
26
+ const { position, duration, loopDuration } = region;
27
+ const newDuration = quantizeCeil(writePosition, beats) - position.getValue();
28
+ duration.setValue(newDuration);
29
+ loopDuration.setValue(newDuration);
30
+ for (const event of activeNotes.values()) {
31
+ if (event.isAttached()) {
32
+ event.duration.setValue(writePosition - event.position.getValue());
33
+ }
34
+ else {
35
+ activeNotes.delete(event.pitch.getValue());
36
+ }
37
+ }
38
+ }
39
+ else {
40
+ writing = Option.None;
41
+ }
42
+ }, false);
43
+ }));
44
+ terminator.ownAll(notifier.subscribe((event) => {
45
+ if (!project.engine.isRecording.getValue()) {
46
+ return;
47
+ }
48
+ const data = event.data;
49
+ if (isUndefined(data)) {
50
+ return;
51
+ }
52
+ const position = project.engine.position.getValue();
53
+ if (MidiData.isNoteOn(data)) {
54
+ const pitch = MidiData.readParam1(data);
55
+ if (writing.isEmpty()) {
56
+ editing.modify(() => {
57
+ const collection = NoteEventCollectionBox.create(boxGraph, UUID.generate());
58
+ const region = NoteRegionBox.create(boxGraph, UUID.generate(), box => {
59
+ box.regions.refer(trackBox.regions);
60
+ box.events.refer(collection.owners);
61
+ box.position.setValue(quantizeFloor(position, beats));
62
+ box.hue.setValue(ColorCodes.forTrackType(TrackType.Notes));
63
+ });
64
+ writing = Option.wrap({ region, collection });
65
+ }, false);
66
+ }
67
+ const { collection } = writing.unwrap();
68
+ editing.modify(() => {
69
+ activeNotes.set(pitch, NoteEventBox.create(boxGraph, UUID.generate(), box => {
70
+ box.position.setValue(position);
71
+ box.duration.setValue(1.0);
72
+ box.pitch.setValue(pitch);
73
+ box.velocity.setValue(MidiData.readParam2(data) / 127.0);
74
+ box.events.refer(collection.events);
75
+ }));
76
+ }, false);
77
+ }
78
+ else if (MidiData.isNoteOff(data)) {
79
+ const pitch = MidiData.readParam1(data);
80
+ activeNotes.delete(pitch);
81
+ }
82
+ }));
83
+ return terminator;
84
+ };
85
+ })(RecordMidi || (RecordMidi = {}));
@@ -0,0 +1,7 @@
1
+ import { AudioUnitBox, TrackBox } from "@opendaw/studio-boxes";
2
+ import { TrackType } from "@opendaw/studio-adapters";
3
+ import { Editing } from "@opendaw/lib-box";
4
+ export declare namespace RecordTrack {
5
+ const findOrCreate: (editing: Editing, audioUnitBox: AudioUnitBox, type: TrackType) => TrackBox;
6
+ }
7
+ //# sourceMappingURL=RecordTrack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecordTrack.d.ts","sourceRoot":"","sources":["../../src/capture/RecordTrack.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAE,QAAQ,EAAC,MAAM,uBAAuB,CAAA;AAE5D,OAAO,EAAC,SAAS,EAAC,MAAM,0BAA0B,CAAA;AAClD,OAAO,EAAC,OAAO,EAAC,MAAM,kBAAkB,CAAA;AAExC,yBAAiB,WAAW,CAAC;IAClB,MAAM,YAAY,GAAI,SAAS,OAAO,EAAE,cAAc,YAAY,EAAE,MAAM,SAAS,KAAG,QAe5F,CAAA;CACJ"}
@@ -0,0 +1,23 @@
1
+ import { TrackBox } from "@opendaw/studio-boxes";
2
+ import { asInstanceOf, UUID } from "@opendaw/lib-std";
3
+ export var RecordTrack;
4
+ (function (RecordTrack) {
5
+ RecordTrack.findOrCreate = (editing, audioUnitBox, type) => {
6
+ let index = 0 | 0;
7
+ for (const trackBox of audioUnitBox.tracks.pointerHub.incoming()
8
+ .map(({ box }) => asInstanceOf(box, TrackBox))) {
9
+ const hasNoRegions = trackBox.regions.pointerHub.isEmpty();
10
+ const acceptsNotes = trackBox.type.getValue() === type;
11
+ if (hasNoRegions && acceptsNotes) {
12
+ return trackBox;
13
+ }
14
+ index = Math.max(index, trackBox.index.getValue());
15
+ }
16
+ return editing.modify(() => TrackBox.create(audioUnitBox.graph, UUID.generate(), box => {
17
+ box.type.setValue(type);
18
+ box.index.setValue(index + 1);
19
+ box.tracks.refer(audioUnitBox.tracks);
20
+ box.target.refer(audioUnitBox);
21
+ })).unwrap("Could not create TrackBox");
22
+ };
23
+ })(RecordTrack || (RecordTrack = {}));
@@ -0,0 +1,9 @@
1
+ import { Terminable } from "@opendaw/lib-std";
2
+ import { RecordingContext } from "./RecordingContext";
3
+ export declare class Recording {
4
+ #private;
5
+ static get isRecording(): boolean;
6
+ static start(context: RecordingContext, countIn: boolean): Promise<Terminable>;
7
+ private constructor();
8
+ }
9
+ //# sourceMappingURL=Recording.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Recording.d.ts","sourceRoot":"","sources":["../../src/capture/Recording.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+B,UAAU,EAAmB,MAAM,kBAAkB,CAAA;AAE3F,OAAO,EAAC,gBAAgB,EAAC,MAAM,oBAAoB,CAAA;AAMnD,qBAAa,SAAS;;IAClB,MAAM,KAAK,WAAW,IAAI,OAAO,CAA2B;WAE/C,KAAK,CAAC,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC;IA+DpF,OAAO;CACV"}
@@ -0,0 +1,65 @@
1
+ import { asInstanceOf, assert, Option, Terminable, Terminator, warn } from "@opendaw/lib-std";
2
+ import { Promises } from "@opendaw/lib-runtime";
3
+ import { AudioUnitBox } from "@opendaw/studio-boxes";
4
+ import { AudioUnitType } from "@opendaw/studio-enums";
5
+ import { InstrumentFactories } from "../InstrumentFactories";
6
+ export class Recording {
7
+ static get isRecording() { return this.#isRecording; }
8
+ static async start(context, countIn) {
9
+ if (this.#isRecording) {
10
+ return Promise.resolve(Terminable.Empty);
11
+ }
12
+ this.#isRecording = true;
13
+ assert(this.#instance.isEmpty(), "Recording already in progress");
14
+ const { project } = context;
15
+ this.#prepare(project);
16
+ const { captureManager, engine, editing } = project;
17
+ const terminator = new Terminator();
18
+ const captures = captureManager.filterArmed();
19
+ if (captures.length === 0) {
20
+ this.#isRecording = false;
21
+ return warn("No track is armed for Recording");
22
+ }
23
+ const { status, error } = await Promises.tryCatch(Promise.all(captures.map(capture => capture.prepareRecording(context))));
24
+ if (status === "rejected") {
25
+ this.#isRecording = false;
26
+ return warn(`Could not prepare recording: ${error}`);
27
+ }
28
+ terminator.ownAll(...captures.map(capture => capture.startRecording(context)));
29
+ engine.startRecording(countIn);
30
+ const { isRecording, isCountingIn } = engine;
31
+ const stop = () => {
32
+ if (isRecording.getValue() || isCountingIn.getValue()) {
33
+ return;
34
+ }
35
+ editing.mark();
36
+ terminator.terminate();
37
+ this.#isRecording = false;
38
+ };
39
+ terminator.ownAll(engine.isRecording.subscribe(stop), engine.isCountingIn.subscribe(stop), Terminable.create(() => Recording.#instance = Option.None));
40
+ this.#instance = Option.wrap(new Recording());
41
+ return terminator;
42
+ }
43
+ static #prepare({ api, captureManager, editing, rootBox, userEditingManager }) {
44
+ const captures = captureManager.filterArmed();
45
+ const instruments = rootBox.audioUnits.pointerHub.incoming()
46
+ .map(({ box }) => asInstanceOf(box, AudioUnitBox))
47
+ .filter(box => box.type.getValue() === AudioUnitType.Instrument);
48
+ if (instruments.length === 0) {
49
+ const { audioUnitBox } = editing
50
+ .modify(() => api.createInstrument(InstrumentFactories.Tape))
51
+ .unwrap("Could not create Tape");
52
+ captureManager.get(audioUnitBox.address.uuid)
53
+ .unwrap("Could not unwrap capture")
54
+ .armed.setValue(true);
55
+ }
56
+ else if (captures.length === 0) {
57
+ userEditingManager.audioUnit.get()
58
+ .ifSome(({ box: { address: { uuid } } }) => captureManager.get(uuid)
59
+ .ifSome(capture => capture.armed.setValue(true))); // auto arm editing audio-unit
60
+ }
61
+ }
62
+ static #isRecording = false;
63
+ static #instance = Option.None;
64
+ constructor() { }
65
+ }