@opendaw/studio-core 0.0.18 → 0.0.20

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 (111) hide show
  1. package/dist/AudioInputDevices.d.ts +8 -0
  2. package/dist/AudioInputDevices.d.ts.map +1 -0
  3. package/dist/AudioInputDevices.js +30 -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/Project.d.ts +7 -3
  20. package/dist/Project.d.ts.map +1 -1
  21. package/dist/Project.js +22 -4
  22. package/dist/ProjectApi.d.ts +0 -1
  23. package/dist/ProjectApi.d.ts.map +1 -1
  24. package/dist/ProjectApi.js +16 -11
  25. package/dist/ProjectEnv.d.ts +1 -0
  26. package/dist/ProjectEnv.d.ts.map +1 -1
  27. package/dist/ProjectMigration.d.ts.map +1 -1
  28. package/dist/ProjectMigration.js +20 -3
  29. package/dist/RecordingWorklet.d.ts +15 -3
  30. package/dist/RecordingWorklet.d.ts.map +1 -1
  31. package/dist/RecordingWorklet.js +111 -16
  32. package/dist/WorkerAgents.d.ts +2 -2
  33. package/dist/WorkerAgents.d.ts.map +1 -1
  34. package/dist/Worklets.d.ts +1 -1
  35. package/dist/Worklets.d.ts.map +1 -1
  36. package/dist/Worklets.js +2 -2
  37. package/dist/capture/Capture.d.ts +22 -0
  38. package/dist/capture/Capture.d.ts.map +1 -0
  39. package/dist/capture/Capture.js +32 -0
  40. package/dist/capture/CaptureAudio.d.ts +17 -0
  41. package/dist/capture/CaptureAudio.d.ts.map +1 -0
  42. package/dist/capture/CaptureAudio.js +106 -0
  43. package/dist/capture/CaptureManager.d.ts +12 -0
  44. package/dist/capture/CaptureManager.d.ts.map +1 -0
  45. package/dist/capture/CaptureManager.js +38 -0
  46. package/dist/capture/CaptureMidi.d.ts +13 -0
  47. package/dist/capture/CaptureMidi.d.ts.map +1 -0
  48. package/dist/capture/CaptureMidi.js +50 -0
  49. package/dist/capture/RecordAudio.d.ts +21 -0
  50. package/dist/capture/RecordAudio.d.ts.map +1 -0
  51. package/dist/capture/RecordAudio.js +66 -0
  52. package/dist/capture/RecordMidi.d.ts +15 -0
  53. package/dist/capture/RecordMidi.d.ts.map +1 -0
  54. package/dist/capture/RecordMidi.js +85 -0
  55. package/dist/capture/RecordTrack.d.ts +7 -0
  56. package/dist/capture/RecordTrack.d.ts.map +1 -0
  57. package/dist/capture/RecordTrack.js +23 -0
  58. package/dist/capture/Recording.d.ts +9 -0
  59. package/dist/capture/Recording.d.ts.map +1 -0
  60. package/dist/capture/Recording.js +65 -0
  61. package/dist/capture/RecordingContext.d.ts +14 -0
  62. package/dist/capture/RecordingContext.d.ts.map +1 -0
  63. package/dist/capture/RecordingContext.js +1 -0
  64. package/dist/dawproject/AudioUnitExportLayout.d.ts +10 -0
  65. package/dist/dawproject/AudioUnitExportLayout.d.ts.map +1 -0
  66. package/dist/dawproject/AudioUnitExportLayout.js +64 -0
  67. package/dist/dawproject/BuiltinDevices.d.ts +9 -0
  68. package/dist/dawproject/BuiltinDevices.d.ts.map +1 -0
  69. package/dist/dawproject/BuiltinDevices.js +70 -0
  70. package/dist/dawproject/DawProject.d.ts +22 -0
  71. package/dist/dawproject/DawProject.d.ts.map +1 -0
  72. package/dist/dawproject/DawProject.js +49 -0
  73. package/dist/dawproject/DawProjectExporter.d.ts +9 -0
  74. package/dist/dawproject/DawProjectExporter.d.ts.map +1 -0
  75. package/dist/dawproject/DawProjectExporter.js +252 -0
  76. package/dist/dawproject/DawProjectExporter.test.d.ts +2 -0
  77. package/dist/dawproject/DawProjectExporter.test.d.ts.map +1 -0
  78. package/dist/dawproject/DawProjectExporter.test.js +49 -0
  79. package/dist/dawproject/DawProjectImport.d.ts +12 -0
  80. package/dist/dawproject/DawProjectImport.d.ts.map +1 -0
  81. package/dist/dawproject/DawProjectImport.js +389 -0
  82. package/dist/dawproject/DawProjectImport.test.d.ts +2 -0
  83. package/dist/dawproject/DawProjectImport.test.d.ts.map +1 -0
  84. package/dist/dawproject/DawProjectImport.test.js +15 -0
  85. package/dist/dawproject/DeviceIO.d.ts +8 -0
  86. package/dist/dawproject/DeviceIO.d.ts.map +1 -0
  87. package/dist/dawproject/DeviceIO.js +63 -0
  88. package/dist/index.d.ts +12 -2
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +12 -2
  91. package/dist/processors.js +3 -3
  92. package/dist/processors.js.map +4 -4
  93. package/dist/samples/MainThreadSampleLoader.d.ts +1 -0
  94. package/dist/samples/MainThreadSampleLoader.d.ts.map +1 -1
  95. package/dist/samples/MainThreadSampleLoader.js +10 -6
  96. package/dist/samples/MainThreadSampleManager.d.ts +5 -5
  97. package/dist/samples/MainThreadSampleManager.d.ts.map +1 -1
  98. package/dist/samples/MainThreadSampleManager.js +1 -0
  99. package/dist/samples/SampleProvider.d.ts +2 -2
  100. package/dist/samples/SampleProvider.d.ts.map +1 -1
  101. package/dist/samples/SampleStorage.d.ts.map +1 -1
  102. package/dist/samples/SampleStorage.js +2 -3
  103. package/dist/workers.js +2 -2
  104. package/dist/workers.js.map +4 -4
  105. package/package.json +17 -17
  106. package/dist/DawProjectIO.d.ts +0 -7
  107. package/dist/DawProjectIO.d.ts.map +0 -1
  108. package/dist/DawProjectIO.js +0 -73
  109. package/dist/samples/SamplePeaks.d.ts +0 -6
  110. package/dist/samples/SamplePeaks.d.ts.map +0 -1
  111. package/dist/samples/SamplePeaks.js +0 -9
@@ -0,0 +1,106 @@
1
+ import { assert, isDefined, isUndefined, ObservableOption, Option, warn } from "@opendaw/lib-std";
2
+ import { Capture } from "./Capture";
3
+ import { RecordAudio } from "./RecordAudio";
4
+ import { AudioInputDevices } from "../AudioInputDevices";
5
+ import { Promises } from "@opendaw/lib-runtime";
6
+ export class CaptureAudio extends Capture {
7
+ #stream;
8
+ #streamGenerator;
9
+ #requestChannels = Option.None;
10
+ #gainDb = 0.0;
11
+ constructor(manager, audioUnitBox, captureBox) {
12
+ super(manager, audioUnitBox, captureBox);
13
+ this.#stream = new ObservableOption();
14
+ this.#streamGenerator = Promises.sequential(() => this.#updateStream());
15
+ this.ownAll(captureBox.requestChannels.catchupAndSubscribe(owner => {
16
+ const channels = owner.getValue();
17
+ this.#requestChannels = channels === 1 || channels === 2 ? Option.wrap(channels) : Option.None;
18
+ }), captureBox.gainDb.catchupAndSubscribe(owner => this.#gainDb = owner.getValue()), captureBox.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, engine, 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
+ return RecordAudio.start({
53
+ recordingWorklet: worklets.createRecording(channelCount, numChunks, audioContext.outputLatency),
54
+ mediaStream,
55
+ sampleManager,
56
+ audioContext,
57
+ engine,
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 AudioInputDevices.requestStream({
82
+ deviceId: { exact: deviceId },
83
+ sampleRate: this.manager.project.env.sampleRate,
84
+ sampleSize: 32,
85
+ echoCancellation: false,
86
+ noiseSuppression: false,
87
+ autoGainControl: false,
88
+ channelCount: { exact: 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
+ prepareRecording({ requestMIDIAccess }: RecordingContext): Promise<void>;
10
+ startRecording({ project, engine }: RecordingContext): Terminable;
11
+ get deviceLabel(): Option<string>;
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,EAAiD,MAAM,EAAS,UAAU,EAAC,MAAM,kBAAkB,CAAA;AAG1G,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;AAE/C,qBAAa,WAAY,SAAQ,OAAO,CAAC,cAAc,CAAC;;gBAKxC,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc;IAWzF,gBAAgB,CAAC,EAAC,iBAAiB,EAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAe5E,cAAc,CAAC,EAAC,OAAO,EAAE,MAAM,EAAC,EAAE,gBAAgB,GAAG,UAAU;IAyB/D,IAAI,WAAW,IAAI,MAAM,CAAC,MAAM,CAAC,CAAqB;CACzD"}
@@ -0,0 +1,50 @@
1
+ import { assert, isDefined, isUndefined, Notifier, Option, panic, Terminable } 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
+ export class CaptureMidi extends Capture {
7
+ #midiAccess = Option.None;
8
+ #filterChannel = Option.None;
9
+ constructor(manager, audioUnitBox, captureMidiBox) {
10
+ super(manager, audioUnitBox, captureMidiBox);
11
+ this.ownAll(captureMidiBox.channel.catchupAndSubscribe(owner => {
12
+ const channel = owner.getValue();
13
+ this.#filterChannel = channel >= 0 ? Option.wrap(channel) : Option.None;
14
+ }));
15
+ }
16
+ async prepareRecording({ requestMIDIAccess }) {
17
+ return requestMIDIAccess()
18
+ .then(midiAccess => {
19
+ const option = this.deviceId.getValue();
20
+ if (option.nonEmpty()) {
21
+ const captureDevices = Array.from(midiAccess.inputs.values());
22
+ const id = option.unwrap();
23
+ if (isUndefined(captureDevices.find(device => id === device.id))) {
24
+ return panic(`Could not find MIDI device with id: '${id}'`);
25
+ }
26
+ }
27
+ this.#midiAccess = Option.wrap(midiAccess);
28
+ });
29
+ }
30
+ startRecording({ project, engine }) {
31
+ assert(this.#midiAccess.nonEmpty(), "Stream not prepared.");
32
+ const midiAccess = this.#midiAccess.unwrap();
33
+ const notifier = new Notifier();
34
+ const captureDevices = Array.from(midiAccess.inputs.values());
35
+ this.deviceId.getValue().ifSome(id => captureDevices.filter(device => id === device.id));
36
+ return Terminable.many(Terminable.many(...captureDevices.map(input => Events.subscribe(input, "midimessage", (event) => {
37
+ const data = event.data;
38
+ if (isDefined(data) &&
39
+ this.#filterChannel.mapOr(channel => MidiData.readChannel(data) === channel, true)) {
40
+ notifier.notify(event);
41
+ }
42
+ }))), RecordMidi.start({
43
+ notifier,
44
+ engine,
45
+ project,
46
+ capture: this
47
+ }));
48
+ }
49
+ get deviceLabel() { return Option.None; }
50
+ }
@@ -0,0 +1,21 @@
1
+ import { Terminable } from "@opendaw/lib-std";
2
+ import { SampleManager } from "@opendaw/studio-adapters";
3
+ import { Engine } from "../Engine";
4
+ import { Project } from "../Project";
5
+ import { Capture } from "./Capture";
6
+ import { RecordingWorklet } from "../RecordingWorklet";
7
+ export declare namespace RecordAudio {
8
+ type RecordAudioContext = {
9
+ recordingWorklet: RecordingWorklet;
10
+ mediaStream: MediaStream;
11
+ sampleManager: SampleManager;
12
+ audioContext: AudioContext;
13
+ engine: Engine;
14
+ project: Project;
15
+ capture: Capture;
16
+ gainDb: number;
17
+ };
18
+ export const start: ({ recordingWorklet, mediaStream, sampleManager, audioContext, engine, project, capture, gainDb }: RecordAudioContext) => Terminable;
19
+ export {};
20
+ }
21
+ //# 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,MAAM,EAAC,MAAM,WAAW,CAAA;AAChC,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,MAAM,EAAE,MAAM,CAAA;QACd,OAAO,EAAE,OAAO,CAAA;QAChB,OAAO,EAAE,OAAO,CAAA;QAChB,MAAM,EAAE,MAAM,CAAA;KACjB,CAAA;IAED,MAAM,CAAC,MAAM,KAAK,GACd,kGAEG,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, engine, project, capture, gainDb }) => {
10
+ const terminator = new Terminator();
11
+ const beats = PPQN.fromSignature(1, project.timelineBox.signature.denominator.getValue());
12
+ const { editing, 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,15 @@
1
+ import { Notifier, Terminable } from "@opendaw/lib-std";
2
+ import { Engine } from "../Engine";
3
+ import { Project } from "../Project";
4
+ import { Capture } from "./Capture";
5
+ export declare namespace RecordMidi {
6
+ type RecordMidiContext = {
7
+ notifier: Notifier<MIDIMessageEvent>;
8
+ engine: Engine;
9
+ project: Project;
10
+ capture: Capture;
11
+ };
12
+ export const start: ({ notifier, engine, project, capture }: RecordMidiContext) => Terminable;
13
+ export {};
14
+ }
15
+ //# 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,MAAM,EAAC,MAAM,WAAW,CAAA;AAChC,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,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,OAAO,CAAA;KACnB,CAAA;IAED,MAAM,CAAC,MAAM,KAAK,GAAI,wCAAsC,iBAAiB,KAAG,UAiE/E,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, engine, 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(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 (!engine.isRecording.getValue()) {
46
+ return;
47
+ }
48
+ const data = event.data;
49
+ if (isUndefined(data)) {
50
+ return;
51
+ }
52
+ const position = 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 { engine, project } = context;
15
+ this.#prepare(project);
16
+ const { captureManager, 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
+ }
@@ -0,0 +1,14 @@
1
+ import { Provider } from "@opendaw/lib-std";
2
+ import { SampleManager } from "@opendaw/studio-adapters";
3
+ import { Project } from "../Project";
4
+ import { Engine } from "../Engine";
5
+ import { Worklets } from "../Worklets";
6
+ export interface RecordingContext {
7
+ project: Project;
8
+ worklets: Worklets;
9
+ engine: Engine;
10
+ audioContext: AudioContext;
11
+ sampleManager: SampleManager;
12
+ requestMIDIAccess: Provider<Promise<MIDIAccess>>;
13
+ }
14
+ //# sourceMappingURL=RecordingContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecordingContext.d.ts","sourceRoot":"","sources":["../../src/capture/RecordingContext.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,QAAQ,EAAC,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAC,aAAa,EAAC,MAAM,0BAA0B,CAAA;AACtD,OAAO,EAAC,OAAO,EAAC,MAAM,YAAY,CAAA;AAClC,OAAO,EAAC,MAAM,EAAC,MAAM,WAAW,CAAA;AAChC,OAAO,EAAC,QAAQ,EAAC,MAAM,aAAa,CAAA;AAEpC,MAAM,WAAW,gBAAgB;IAC7B,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,QAAQ,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,YAAY,EAAE,YAAY,CAAA;IAC1B,aAAa,EAAE,aAAa,CAAA;IAC5B,iBAAiB,EAAE,QAAQ,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAA;CACnD"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import { AudioUnitBox } from "@opendaw/studio-boxes";
2
+ export declare namespace AudioUnitExportLayout {
3
+ interface Track {
4
+ audioUnit: AudioUnitBox;
5
+ children: Array<Track>;
6
+ }
7
+ const layout: (audioUnits: ReadonlyArray<AudioUnitBox>) => Array<Track>;
8
+ const printTrackStructure: (tracks: ReadonlyArray<Track>, indent?: number) => void;
9
+ }
10
+ //# sourceMappingURL=AudioUnitExportLayout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AudioUnitExportLayout.d.ts","sourceRoot":"","sources":["../../src/dawproject/AudioUnitExportLayout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,YAAY,EAAC,MAAM,uBAAuB,CAAA;AAK/D,yBAAiB,qBAAqB,CAAC;IACnC,UAAiB,KAAK;QAClB,SAAS,EAAE,YAAY,CAAA;QACvB,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC,CAAA;KACzB;IAEM,MAAM,MAAM,GAAI,YAAY,aAAa,CAAC,YAAY,CAAC,KAAG,KAAK,CAAC,KAAK,CAgC3E,CAAA;IAgBM,MAAM,mBAAmB,GAAI,QAAQ,aAAa,CAAC,KAAK,CAAC,EAAE,eAAU,KAAG,IAU9E,CAAA;CACJ"}
@@ -0,0 +1,64 @@
1
+ import { AudioBusBox, AudioUnitBox } from "@opendaw/studio-boxes";
2
+ import { ArrayMultimap, asInstanceOf, isDefined, isInstanceOf, Option } from "@opendaw/lib-std";
3
+ import { AudioUnitType } from "@opendaw/studio-enums";
4
+ import { DeviceBoxUtils } from "@opendaw/studio-adapters";
5
+ export var AudioUnitExportLayout;
6
+ (function (AudioUnitExportLayout) {
7
+ AudioUnitExportLayout.layout = (audioUnits) => {
8
+ const feedsInto = new ArrayMultimap();
9
+ audioUnits.forEach(unit => {
10
+ unit.output.targetVertex.ifSome(({ box }) => {
11
+ if (isInstanceOf(box, AudioBusBox)) {
12
+ box.output.targetVertex.ifSome(({ box: targetUnit }) => {
13
+ const audioUnit = asInstanceOf(targetUnit, AudioUnitBox);
14
+ if (audioUnit.type.getValue() !== AudioUnitType.Output) {
15
+ feedsInto.add(audioUnit, unit);
16
+ }
17
+ });
18
+ }
19
+ });
20
+ });
21
+ // Roots are:
22
+ // 1. Units with no output
23
+ // 2. Units that connect directly to Output (become independent roots)
24
+ // 3. The Output unit itself (as a standalone root)
25
+ const roots = audioUnits.filter(unit => {
26
+ if (unit.type.getValue() === AudioUnitType.Output) {
27
+ return true;
28
+ }
29
+ if (unit.output.targetVertex.isEmpty()) {
30
+ return true;
31
+ }
32
+ return unit.output.targetVertex
33
+ .flatMap(({ box }) => isInstanceOf(box, AudioBusBox) ? box.output.targetVertex : Option.None)
34
+ .map(({ box }) => asInstanceOf(box, AudioUnitBox).type.getValue() === AudioUnitType.Output)
35
+ .unwrapOrElse(false);
36
+ });
37
+ const visited = new Set();
38
+ return roots
39
+ .map(root => buildTrackRecursive(root, feedsInto, visited))
40
+ .filter(isDefined);
41
+ };
42
+ const buildTrackRecursive = (audioUnit, feedsInto, visited) => {
43
+ if (visited.has(audioUnit)) {
44
+ console.warn(`Cycle detected at AudioUnitBox`, audioUnit);
45
+ return null;
46
+ }
47
+ visited.add(audioUnit);
48
+ const children = feedsInto.get(audioUnit)
49
+ .map(childUnit => buildTrackRecursive(childUnit, feedsInto, visited))
50
+ .filter(isDefined);
51
+ return { audioUnit, children };
52
+ };
53
+ AudioUnitExportLayout.printTrackStructure = (tracks, indent = 0) => {
54
+ const spaces = " ".repeat(indent);
55
+ tracks.forEach(track => {
56
+ const inputBox = track.audioUnit.input.pointerHub.incoming().at(0)?.box;
57
+ const label = DeviceBoxUtils.lookupLabelField(inputBox).getValue();
58
+ console.debug(`${spaces}⌙ ${label} (${track.audioUnit.address.toString()})`);
59
+ if (track.children.length > 0) {
60
+ AudioUnitExportLayout.printTrackStructure(track.children, indent + 2);
61
+ }
62
+ });
63
+ };
64
+ })(AudioUnitExportLayout || (AudioUnitExportLayout = {}));