@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.
- package/dist/AudioInputDevices.d.ts +8 -0
- package/dist/AudioInputDevices.d.ts.map +1 -0
- package/dist/AudioInputDevices.js +30 -0
- package/dist/AudioUnitOrdering.d.ts +3 -0
- package/dist/AudioUnitOrdering.d.ts.map +1 -0
- package/dist/AudioUnitOrdering.js +7 -0
- package/dist/EffectBox.d.ts +2 -2
- package/dist/EffectBox.d.ts.map +1 -1
- package/dist/Engine.d.ts +31 -10
- package/dist/Engine.d.ts.map +1 -1
- package/dist/EngineFacade.d.ts +22 -11
- package/dist/EngineFacade.d.ts.map +1 -1
- package/dist/EngineFacade.js +39 -22
- package/dist/EngineWorklet.d.ts +22 -13
- package/dist/EngineWorklet.d.ts.map +1 -1
- package/dist/EngineWorklet.js +47 -56
- package/dist/MeterWorklet.d.ts.map +1 -1
- package/dist/MeterWorklet.js +2 -2
- package/dist/Project.d.ts +7 -3
- package/dist/Project.d.ts.map +1 -1
- package/dist/Project.js +22 -4
- package/dist/ProjectApi.d.ts +0 -1
- package/dist/ProjectApi.d.ts.map +1 -1
- package/dist/ProjectApi.js +16 -11
- package/dist/ProjectEnv.d.ts +1 -0
- package/dist/ProjectEnv.d.ts.map +1 -1
- package/dist/ProjectMigration.d.ts.map +1 -1
- package/dist/ProjectMigration.js +20 -3
- package/dist/RecordingWorklet.d.ts +15 -3
- package/dist/RecordingWorklet.d.ts.map +1 -1
- package/dist/RecordingWorklet.js +111 -16
- package/dist/WorkerAgents.d.ts +2 -2
- package/dist/WorkerAgents.d.ts.map +1 -1
- package/dist/Worklets.d.ts +1 -1
- package/dist/Worklets.d.ts.map +1 -1
- package/dist/Worklets.js +2 -2
- package/dist/capture/Capture.d.ts +22 -0
- package/dist/capture/Capture.d.ts.map +1 -0
- package/dist/capture/Capture.js +32 -0
- package/dist/capture/CaptureAudio.d.ts +17 -0
- package/dist/capture/CaptureAudio.d.ts.map +1 -0
- package/dist/capture/CaptureAudio.js +106 -0
- package/dist/capture/CaptureManager.d.ts +12 -0
- package/dist/capture/CaptureManager.d.ts.map +1 -0
- package/dist/capture/CaptureManager.js +38 -0
- package/dist/capture/CaptureMidi.d.ts +13 -0
- package/dist/capture/CaptureMidi.d.ts.map +1 -0
- package/dist/capture/CaptureMidi.js +50 -0
- package/dist/capture/RecordAudio.d.ts +21 -0
- package/dist/capture/RecordAudio.d.ts.map +1 -0
- package/dist/capture/RecordAudio.js +66 -0
- package/dist/capture/RecordMidi.d.ts +15 -0
- package/dist/capture/RecordMidi.d.ts.map +1 -0
- package/dist/capture/RecordMidi.js +85 -0
- package/dist/capture/RecordTrack.d.ts +7 -0
- package/dist/capture/RecordTrack.d.ts.map +1 -0
- package/dist/capture/RecordTrack.js +23 -0
- package/dist/capture/Recording.d.ts +9 -0
- package/dist/capture/Recording.d.ts.map +1 -0
- package/dist/capture/Recording.js +65 -0
- package/dist/capture/RecordingContext.d.ts +14 -0
- package/dist/capture/RecordingContext.d.ts.map +1 -0
- package/dist/capture/RecordingContext.js +1 -0
- package/dist/dawproject/AudioUnitExportLayout.d.ts +10 -0
- package/dist/dawproject/AudioUnitExportLayout.d.ts.map +1 -0
- package/dist/dawproject/AudioUnitExportLayout.js +64 -0
- package/dist/dawproject/BuiltinDevices.d.ts +9 -0
- package/dist/dawproject/BuiltinDevices.d.ts.map +1 -0
- package/dist/dawproject/BuiltinDevices.js +70 -0
- package/dist/dawproject/DawProject.d.ts +22 -0
- package/dist/dawproject/DawProject.d.ts.map +1 -0
- package/dist/dawproject/DawProject.js +49 -0
- package/dist/dawproject/DawProjectExporter.d.ts +9 -0
- package/dist/dawproject/DawProjectExporter.d.ts.map +1 -0
- package/dist/dawproject/DawProjectExporter.js +252 -0
- package/dist/dawproject/DawProjectExporter.test.d.ts +2 -0
- package/dist/dawproject/DawProjectExporter.test.d.ts.map +1 -0
- package/dist/dawproject/DawProjectExporter.test.js +49 -0
- package/dist/dawproject/DawProjectImport.d.ts +12 -0
- package/dist/dawproject/DawProjectImport.d.ts.map +1 -0
- package/dist/dawproject/DawProjectImport.js +389 -0
- package/dist/dawproject/DawProjectImport.test.d.ts +2 -0
- package/dist/dawproject/DawProjectImport.test.d.ts.map +1 -0
- package/dist/dawproject/DawProjectImport.test.js +15 -0
- package/dist/dawproject/DeviceIO.d.ts +8 -0
- package/dist/dawproject/DeviceIO.d.ts.map +1 -0
- package/dist/dawproject/DeviceIO.js +63 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/processors.js +3 -3
- package/dist/processors.js.map +4 -4
- package/dist/samples/MainThreadSampleLoader.d.ts +1 -0
- package/dist/samples/MainThreadSampleLoader.d.ts.map +1 -1
- package/dist/samples/MainThreadSampleLoader.js +10 -6
- package/dist/samples/MainThreadSampleManager.d.ts +5 -5
- package/dist/samples/MainThreadSampleManager.d.ts.map +1 -1
- package/dist/samples/MainThreadSampleManager.js +1 -0
- package/dist/samples/SampleProvider.d.ts +2 -2
- package/dist/samples/SampleProvider.d.ts.map +1 -1
- package/dist/samples/SampleStorage.d.ts.map +1 -1
- package/dist/samples/SampleStorage.js +2 -3
- package/dist/workers.js +2 -2
- package/dist/workers.js.map +4 -4
- package/package.json +17 -17
- package/dist/DawProjectIO.d.ts +0 -7
- package/dist/DawProjectIO.d.ts.map +0 -1
- package/dist/DawProjectIO.js +0 -73
- package/dist/samples/SamplePeaks.d.ts +0 -6
- package/dist/samples/SamplePeaks.d.ts.map +0 -1
- 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 = {}));
|