@opendaw/studio-core 0.0.23 → 0.0.24
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/AudioDevices.d.ts.map +1 -1
- package/dist/AudioDevices.js +1 -0
- package/dist/{Worklets.d.ts → AudioWorklets.d.ts} +6 -5
- package/dist/AudioWorklets.d.ts.map +1 -0
- package/dist/{Worklets.js → AudioWorklets.js} +3 -2
- package/dist/EffectFactory.d.ts +1 -1
- package/dist/EffectFactory.d.ts.map +1 -1
- package/dist/Engine.d.ts +6 -16
- package/dist/Engine.d.ts.map +1 -1
- package/dist/EngineFacade.d.ts +10 -9
- package/dist/EngineFacade.d.ts.map +1 -1
- package/dist/EngineFacade.js +28 -25
- package/dist/EngineWorklet.d.ts +7 -7
- package/dist/EngineWorklet.d.ts.map +1 -1
- package/dist/EngineWorklet.js +8 -16
- package/dist/PeaksWriter.d.ts +19 -0
- package/dist/PeaksWriter.d.ts.map +1 -0
- package/dist/PeaksWriter.js +43 -0
- package/dist/RecordingWorklet.d.ts +4 -2
- package/dist/RecordingWorklet.d.ts.map +1 -1
- package/dist/RecordingWorklet.js +36 -62
- package/dist/capture/Capture.d.ts +5 -6
- package/dist/capture/Capture.d.ts.map +1 -1
- package/dist/capture/CaptureAudio.d.ts +4 -5
- package/dist/capture/CaptureAudio.d.ts.map +1 -1
- package/dist/capture/CaptureAudio.js +26 -14
- package/dist/capture/{CaptureManager.d.ts → CaptureDevices.d.ts} +4 -3
- package/dist/capture/CaptureDevices.d.ts.map +1 -0
- package/dist/capture/{CaptureManager.js → CaptureDevices.js} +10 -1
- package/dist/capture/CaptureMidi.d.ts +8 -6
- package/dist/capture/CaptureMidi.d.ts.map +1 -1
- package/dist/capture/CaptureMidi.js +67 -47
- package/dist/capture/RecordAudio.d.ts +2 -2
- package/dist/capture/RecordAudio.d.ts.map +1 -1
- package/dist/capture/RecordAudio.js +50 -36
- package/dist/capture/RecordMidi.d.ts +3 -2
- package/dist/capture/RecordMidi.d.ts.map +1 -1
- package/dist/capture/RecordMidi.js +19 -22
- package/dist/capture/Recording.d.ts +2 -2
- package/dist/capture/Recording.d.ts.map +1 -1
- package/dist/capture/Recording.js +10 -11
- package/dist/dawproject/DawProject.d.ts +1 -1
- package/dist/dawproject/DawProject.d.ts.map +1 -1
- package/dist/dawproject/DawProject.js +3 -2
- package/dist/dawproject/DawProjectExporter.d.ts +1 -1
- package/dist/dawproject/DawProjectExporter.d.ts.map +1 -1
- package/dist/dawproject/DawProjectExporter.test.js +6 -5
- package/dist/index.d.ts +10 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -6
- package/dist/processors.js +3 -3
- package/dist/processors.js.map +4 -4
- package/dist/{Project.d.ts → project/Project.d.ts} +8 -7
- package/dist/project/Project.d.ts.map +1 -0
- package/dist/{Project.js → project/Project.js} +14 -6
- package/dist/{ProjectApi.d.ts → project/ProjectApi.d.ts} +5 -5
- package/dist/project/ProjectApi.d.ts.map +1 -0
- package/dist/{ProjectApi.js → project/ProjectApi.js} +2 -2
- package/dist/project/ProjectBundle.d.ts +8 -0
- package/dist/project/ProjectBundle.d.ts.map +1 -0
- package/dist/project/ProjectBundle.js +90 -0
- package/dist/project/ProjectEnv.d.ts +15 -0
- package/dist/project/ProjectEnv.d.ts.map +1 -0
- package/dist/project/ProjectMeta.d.ts +14 -0
- package/dist/project/ProjectMeta.d.ts.map +1 -0
- package/dist/project/ProjectMeta.js +12 -0
- package/dist/project/ProjectMigration.d.ts.map +1 -0
- package/dist/project/ProjectPaths.d.ts +12 -0
- package/dist/project/ProjectPaths.d.ts.map +1 -0
- package/dist/project/ProjectPaths.js +12 -0
- package/dist/project/ProjectProfile.d.ts +21 -0
- package/dist/project/ProjectProfile.d.ts.map +1 -0
- package/dist/project/ProjectProfile.js +83 -0
- package/dist/samples/MainThreadSampleLoader.d.ts +0 -2
- package/dist/samples/MainThreadSampleLoader.d.ts.map +1 -1
- package/dist/samples/MainThreadSampleLoader.js +1 -26
- package/dist/samples/MainThreadSampleManager.d.ts +1 -0
- package/dist/samples/MainThreadSampleManager.d.ts.map +1 -1
- package/dist/samples/MainThreadSampleManager.js +1 -0
- package/dist/sync-log/Commit.d.ts +1 -1
- package/dist/sync-log/Commit.d.ts.map +1 -1
- package/dist/sync-log/SyncLogReader.d.ts +2 -2
- package/dist/sync-log/SyncLogReader.d.ts.map +1 -1
- package/dist/sync-log/SyncLogReader.js +1 -1
- package/dist/sync-log/SyncLogWriter.d.ts +1 -1
- package/dist/sync-log/SyncLogWriter.d.ts.map +1 -1
- package/dist/workers.js +2 -2
- package/dist/workers.js.map +3 -3
- package/package.json +14 -14
- package/dist/Project.d.ts.map +0 -1
- package/dist/ProjectApi.d.ts.map +0 -1
- package/dist/ProjectEnv.d.ts +0 -5
- package/dist/ProjectEnv.d.ts.map +0 -1
- package/dist/ProjectMigration.d.ts.map +0 -1
- package/dist/Worklets.d.ts.map +0 -1
- package/dist/capture/CaptureManager.d.ts.map +0 -1
- package/dist/capture/RecordingContext.d.ts +0 -10
- package/dist/capture/RecordingContext.d.ts.map +0 -1
- package/dist/capture/RecordingContext.js +0 -1
- /package/dist/{ProjectEnv.js → project/ProjectEnv.js} +0 -0
- /package/dist/{ProjectMigration.d.ts → project/ProjectMigration.d.ts} +0 -0
- /package/dist/{ProjectMigration.js → project/ProjectMigration.js} +0 -0
@@ -1,17 +1,16 @@
|
|
1
1
|
import { MutableObservableValue, Option, Terminable, UUID } from "@opendaw/lib-std";
|
2
2
|
import { AudioUnitBox } from "@opendaw/studio-boxes";
|
3
3
|
import { CaptureBox } from "@opendaw/studio-adapters";
|
4
|
-
import {
|
5
|
-
import { CaptureManager } from "./CaptureManager";
|
4
|
+
import { CaptureDevices } from "./CaptureDevices";
|
6
5
|
export declare abstract class Capture<BOX extends CaptureBox = CaptureBox> implements Terminable {
|
7
6
|
#private;
|
8
|
-
protected constructor(manager:
|
7
|
+
protected constructor(manager: CaptureDevices, audioUnitBox: AudioUnitBox, captureBox: BOX);
|
9
8
|
abstract get label(): string;
|
10
9
|
abstract get deviceLabel(): Option<string>;
|
11
|
-
abstract prepareRecording(
|
12
|
-
abstract startRecording(
|
10
|
+
abstract prepareRecording(): Promise<void>;
|
11
|
+
abstract startRecording(): Terminable;
|
13
12
|
get uuid(): UUID.Format;
|
14
|
-
get manager():
|
13
|
+
get manager(): CaptureDevices;
|
15
14
|
get audioUnitBox(): AudioUnitBox;
|
16
15
|
get captureBox(): BOX;
|
17
16
|
get armed(): MutableObservableValue<boolean>;
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"Capture.d.ts","sourceRoot":"","sources":["../../src/capture/Capture.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,sBAAsB,EACtB,MAAM,EACN,UAAU,EAEV,IAAI,EACP,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAC,YAAY,EAAC,MAAM,uBAAuB,CAAA;AAClD,OAAO,EAAC,UAAU,EAAC,MAAM,0BAA0B,CAAA;
|
1
|
+
{"version":3,"file":"Capture.d.ts","sourceRoot":"","sources":["../../src/capture/Capture.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,sBAAsB,EACtB,MAAM,EACN,UAAU,EAEV,IAAI,EACP,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAC,YAAY,EAAC,MAAM,uBAAuB,CAAA;AAClD,OAAO,EAAC,UAAU,EAAC,MAAM,0BAA0B,CAAA;AACnD,OAAO,EAAC,cAAc,EAAC,MAAM,kBAAkB,CAAA;AAE/C,8BAAsB,OAAO,CAAC,GAAG,SAAS,UAAU,GAAG,UAAU,CAAE,YAAW,UAAU;;IAUpF,SAAS,aAAa,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,GAAG;IAmB1F,QAAQ,KAAK,KAAK,IAAI,MAAM,CAAA;IAC5B,QAAQ,KAAK,WAAW,IAAI,MAAM,CAAC,MAAM,CAAC,CAAA;IAC1C,QAAQ,CAAC,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAC1C,QAAQ,CAAC,cAAc,IAAI,UAAU;IAErC,IAAI,IAAI,IAAI,IAAI,CAAC,MAAM,CAAyC;IAChE,IAAI,OAAO,IAAI,cAAc,CAAuB;IACpD,IAAI,YAAY,IAAI,YAAY,CAA4B;IAC5D,IAAI,UAAU,IAAI,GAAG,CAA0B;IAC/C,IAAI,KAAK,IAAI,sBAAsB,CAAC,OAAO,CAAC,CAAqB;IACjE,IAAI,QAAQ,IAAI,sBAAsB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAwB;IAE9E,GAAG,CAAC,CAAC,SAAS,UAAU,EAAE,UAAU,EAAE,CAAC,GAAG,CAAC;IAC3C,MAAM,CAAC,CAAC,SAAS,UAAU,EAAE,GAAG,WAAW,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,IAAI;IACpE,SAAS,IAAI,IAAI;CACpB"}
|
@@ -1,18 +1,17 @@
|
|
1
1
|
import { MutableObservableOption, Option, Terminable } from "@opendaw/lib-std";
|
2
2
|
import { AudioUnitBox, CaptureAudioBox } from "@opendaw/studio-boxes";
|
3
3
|
import { Capture } from "./Capture";
|
4
|
-
import {
|
5
|
-
import { RecordingContext } from "./RecordingContext";
|
4
|
+
import { CaptureDevices } from "./CaptureDevices";
|
6
5
|
export declare class CaptureAudio extends Capture<CaptureAudioBox> {
|
7
6
|
#private;
|
8
|
-
constructor(manager:
|
7
|
+
constructor(manager: CaptureDevices, audioUnitBox: AudioUnitBox, captureAudioBox: CaptureAudioBox);
|
9
8
|
get gainDb(): number;
|
10
9
|
get stream(): MutableObservableOption<MediaStream>;
|
11
10
|
get streamDeviceId(): Option<string>;
|
12
11
|
get label(): string;
|
13
12
|
get deviceLabel(): Option<string>;
|
14
13
|
get streamMediaTrack(): Option<MediaStreamTrack>;
|
15
|
-
prepareRecording(
|
16
|
-
startRecording(
|
14
|
+
prepareRecording(): Promise<void>;
|
15
|
+
startRecording(): Terminable;
|
17
16
|
}
|
18
17
|
//# sourceMappingURL=CaptureAudio.d.ts.map
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"CaptureAudio.d.ts","sourceRoot":"","sources":["../../src/capture/CaptureAudio.ts"],"names":[],"mappings":"AAAA,OAAO,
|
1
|
+
{"version":3,"file":"CaptureAudio.d.ts","sourceRoot":"","sources":["../../src/capture/CaptureAudio.ts"],"names":[],"mappings":"AAAA,OAAO,EAKH,uBAAuB,EACvB,MAAM,EAEN,UAAU,EAEb,MAAM,kBAAkB,CAAA;AAEzB,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;AAI/C,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,KAAK,IAAI,MAAM,CAAsE;IAEzF,IAAI,WAAW,IAAI,MAAM,CAAC,MAAM,CAAC,CAA+D;IAEhG,IAAI,gBAAgB,IAAI,MAAM,CAAC,gBAAgB,CAAC,CAE/C;IAEK,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAcvC,cAAc,IAAI,UAAU;CA+D/B"}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import {
|
1
|
+
import { abort, isDefined, isUndefined, MutableObservableOption, Option, safeExecute, Terminable, warn } from "@opendaw/lib-std";
|
2
2
|
import { Promises } from "@opendaw/lib-runtime";
|
3
3
|
import { Capture } from "./Capture";
|
4
4
|
import { RecordAudio } from "./RecordAudio";
|
@@ -39,14 +39,29 @@ export class CaptureAudio extends Capture {
|
|
39
39
|
get streamMediaTrack() {
|
40
40
|
return this.#stream.flatMap(stream => Option.wrap(stream.getAudioTracks().at(0)));
|
41
41
|
}
|
42
|
-
async prepareRecording(
|
43
|
-
|
42
|
+
async prepareRecording() {
|
43
|
+
const { project } = this.manager;
|
44
|
+
const { env: { audioContext } } = project;
|
45
|
+
if (isUndefined(audioContext.outputLatency)) {
|
46
|
+
const approved = await safeExecute(project.env.dialogs?.approve, "Warning", "Your browser does not support 'output latency'. This will cause timing issue while recording.", "Ignore", "Cancel");
|
47
|
+
if (!approved) {
|
48
|
+
return abort("Recording cancelled");
|
49
|
+
}
|
50
|
+
}
|
51
|
+
return this.#streamGenerator();
|
52
|
+
}
|
53
|
+
startRecording() {
|
54
|
+
const { project } = this.manager;
|
55
|
+
const { env: { audioContext, audioWorklets, sampleManager } } = project;
|
44
56
|
const streamOption = this.#stream;
|
45
|
-
|
57
|
+
if (streamOption.isEmpty()) {
|
58
|
+
console.warn("No audio stream available for recording.");
|
59
|
+
return Terminable.Empty;
|
60
|
+
}
|
46
61
|
const mediaStream = streamOption.unwrap();
|
47
62
|
const channelCount = mediaStream.getAudioTracks().at(0)?.getSettings().channelCount ?? 1;
|
48
63
|
const numChunks = 128;
|
49
|
-
const recordingWorklet =
|
64
|
+
const recordingWorklet = audioWorklets.createRecording(channelCount, numChunks, audioContext.outputLatency);
|
50
65
|
return RecordAudio.start({
|
51
66
|
recordingWorklet,
|
52
67
|
mediaStream,
|
@@ -61,20 +76,16 @@ export class CaptureAudio extends Capture {
|
|
61
76
|
if (this.#stream.nonEmpty()) {
|
62
77
|
const stream = this.#stream.unwrap();
|
63
78
|
const settings = stream.getAudioTracks().at(0)?.getSettings();
|
64
|
-
console.debug(stream.getAudioTracks());
|
65
79
|
if (isDefined(settings)) {
|
66
80
|
const deviceId = this.deviceId.getValue().unwrapOrUndefined();
|
67
|
-
|
68
|
-
const satisfyChannelCount = settings.channelCount === channelCount;
|
69
|
-
const satisfiedDeviceId = isUndefined(deviceId) || deviceId === settings.deviceId;
|
70
|
-
if (satisfiedDeviceId && satisfyChannelCount) {
|
81
|
+
if (isUndefined(deviceId) || deviceId === settings.deviceId) {
|
71
82
|
return Promise.resolve();
|
72
83
|
}
|
73
84
|
}
|
74
85
|
}
|
75
86
|
this.#stopStream();
|
76
87
|
const deviceId = this.deviceId.getValue().unwrapOrUndefined();
|
77
|
-
const channelCount = this.#requestChannels.unwrapOrElse(1);
|
88
|
+
const channelCount = this.#requestChannels.unwrapOrElse(1);
|
78
89
|
return AudioDevices.requestStream({
|
79
90
|
deviceId: { exact: deviceId },
|
80
91
|
sampleRate: this.manager.project.engine.sampleRate(),
|
@@ -82,12 +93,13 @@ export class CaptureAudio extends Capture {
|
|
82
93
|
echoCancellation: false,
|
83
94
|
noiseSuppression: false,
|
84
95
|
autoGainControl: false,
|
85
|
-
channelCount
|
96
|
+
channelCount: { ideal: channelCount }
|
86
97
|
}).then(stream => {
|
87
98
|
const tracks = stream.getAudioTracks();
|
88
|
-
const
|
99
|
+
const track = tracks.at(0);
|
100
|
+
const settings = track?.getSettings();
|
89
101
|
const gotDeviceId = settings?.deviceId;
|
90
|
-
console.debug(`new stream
|
102
|
+
console.debug(`new stream. device requested: ${stream.id}, got: ${gotDeviceId ?? "Default"}. channelCount requested: ${channelCount}, got: ${settings?.channelCount}`);
|
91
103
|
if (isUndefined(deviceId) || deviceId === gotDeviceId) {
|
92
104
|
this.#stream.wrap(stream);
|
93
105
|
}
|
@@ -1,12 +1,13 @@
|
|
1
1
|
import { Option, Terminable, UUID } from "@opendaw/lib-std";
|
2
|
-
import { Project } from "../Project";
|
2
|
+
import { Project } from "../project/Project";
|
3
3
|
import { Capture } from "./Capture";
|
4
|
-
export declare class
|
4
|
+
export declare class CaptureDevices implements Terminable {
|
5
5
|
#private;
|
6
6
|
constructor(project: Project);
|
7
7
|
get project(): Project;
|
8
8
|
get(uuid: UUID.Format): Option<Capture>;
|
9
|
+
setArm(subject: Capture, exclusive: boolean): void;
|
9
10
|
filterArmed(): ReadonlyArray<Capture>;
|
10
11
|
terminate(): void;
|
11
12
|
}
|
12
|
-
//# sourceMappingURL=
|
13
|
+
//# sourceMappingURL=CaptureDevices.d.ts.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"CaptureDevices.d.ts","sourceRoot":"","sources":["../../src/capture/CaptureDevices.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,MAAM,EAA2B,UAAU,EAAE,IAAI,EAAC,MAAM,kBAAkB,CAAA;AAEpH,OAAO,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAA;AAC1C,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,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,GAAG,IAAI;IAUlD,WAAW,IAAI,aAAa,CAAC,OAAO,CAAC;IAKrC,SAAS,IAAI,IAAI;CAKpB"}
|
@@ -2,7 +2,7 @@ import { asInstanceOf, isDefined, UUID } from "@opendaw/lib-std";
|
|
2
2
|
import { AudioUnitBox } from "@opendaw/studio-boxes";
|
3
3
|
import { CaptureMidi } from "./CaptureMidi";
|
4
4
|
import { CaptureAudio } from "./CaptureAudio";
|
5
|
-
export class
|
5
|
+
export class CaptureDevices {
|
6
6
|
#project;
|
7
7
|
#subscription;
|
8
8
|
#captures;
|
@@ -26,6 +26,15 @@ export class CaptureManager {
|
|
26
26
|
}
|
27
27
|
get project() { return this.#project; }
|
28
28
|
get(uuid) { return this.#captures.opt(uuid); }
|
29
|
+
setArm(subject, exclusive) {
|
30
|
+
const arming = !subject.armed.getValue();
|
31
|
+
subject.armed.setValue(arming);
|
32
|
+
if (arming && exclusive) {
|
33
|
+
this.#captures.values()
|
34
|
+
.filter(capture => subject !== capture)
|
35
|
+
.forEach(capture => capture.armed.setValue(false));
|
36
|
+
}
|
37
|
+
}
|
29
38
|
filterArmed() {
|
30
39
|
return this.#captures.values()
|
31
40
|
.filter(capture => capture.armed.getValue() && capture.audioUnitBox.input.pointerHub.nonEmpty());
|
@@ -1,14 +1,16 @@
|
|
1
|
-
import { Option, Terminable } from "@opendaw/lib-std";
|
1
|
+
import { Observer, Option, Subscription, Terminable } from "@opendaw/lib-std";
|
2
2
|
import { AudioUnitBox, CaptureMidiBox } from "@opendaw/studio-boxes";
|
3
|
+
import { NoteSignal } from "@opendaw/studio-adapters";
|
3
4
|
import { Capture } from "./Capture";
|
4
|
-
import {
|
5
|
-
import { CaptureManager } from "./CaptureManager";
|
5
|
+
import { CaptureDevices } from "./CaptureDevices";
|
6
6
|
export declare class CaptureMidi extends Capture<CaptureMidiBox> {
|
7
7
|
#private;
|
8
|
-
constructor(manager:
|
8
|
+
constructor(manager: CaptureDevices, audioUnitBox: AudioUnitBox, captureMidiBox: CaptureMidiBox);
|
9
|
+
notify(signal: NoteSignal): void;
|
10
|
+
subscribeNotes(observer: Observer<NoteSignal>): Subscription;
|
9
11
|
get label(): string;
|
10
12
|
get deviceLabel(): Option<string>;
|
11
|
-
prepareRecording(
|
12
|
-
startRecording(
|
13
|
+
prepareRecording(): Promise<void>;
|
14
|
+
startRecording(): Terminable;
|
13
15
|
}
|
14
16
|
//# sourceMappingURL=CaptureMidi.d.ts.map
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"CaptureMidi.d.ts","sourceRoot":"","sources":["../../src/capture/CaptureMidi.ts"],"names":[],"mappings":"AAAA,OAAO,
|
1
|
+
{"version":3,"file":"CaptureMidi.d.ts","sourceRoot":"","sources":["../../src/capture/CaptureMidi.ts"],"names":[],"mappings":"AAAA,OAAO,EAOH,QAAQ,EACR,MAAM,EACN,YAAY,EACZ,UAAU,EAEb,MAAM,kBAAkB,CAAA;AAIzB,OAAO,EAAC,YAAY,EAAE,cAAc,EAAC,MAAM,uBAAuB,CAAA;AAClE,OAAO,EAAC,UAAU,EAAC,MAAM,0BAA0B,CAAA;AAEnD,OAAO,EAAC,OAAO,EAAC,MAAM,WAAW,CAAA;AACjC,OAAO,EAAC,cAAc,EAAC,MAAM,kBAAkB,CAAA;AAG/C,qBAAa,WAAY,SAAQ,OAAO,CAAC,cAAc,CAAC;;gBAOxC,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc;IA8B/F,MAAM,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAEhC,cAAc,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,YAAY;IAE5D,IAAI,KAAK,IAAI,MAAM,CAgBlB;IAED,IAAI,WAAW,IAAI,MAAM,CAAC,MAAM,CAAC,CAIhC;IAEK,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAuBvC,cAAc,IAAI,UAAU;CAgD/B"}
|
@@ -1,23 +1,26 @@
|
|
1
|
-
import {
|
1
|
+
import { assert, isDefined, isUndefined, Notifier, Option, Terminable, warn } from "@opendaw/lib-std";
|
2
2
|
import { Events } from "@opendaw/lib-dom";
|
3
3
|
import { MidiData } from "@opendaw/lib-midi";
|
4
|
+
import { Promises } from "@opendaw/lib-runtime";
|
5
|
+
import { NoteSignal } from "@opendaw/studio-adapters";
|
6
|
+
import { MidiDevices } from "../MidiDevices";
|
4
7
|
import { Capture } from "./Capture";
|
5
8
|
import { RecordMidi } from "./RecordMidi";
|
6
|
-
import { MidiDevices } from "../MidiDevices";
|
7
|
-
import { Promises } from "@opendaw/lib-runtime";
|
8
9
|
export class CaptureMidi extends Capture {
|
9
10
|
#streamGenerator;
|
10
11
|
#notifier = new Notifier();
|
11
12
|
#filterChannel = Option.None;
|
12
|
-
#
|
13
|
+
#stream = Option.None;
|
13
14
|
constructor(manager, audioUnitBox, captureMidiBox) {
|
14
15
|
super(manager, audioUnitBox, captureMidiBox);
|
15
16
|
this.#streamGenerator = Promises.sequential(() => this.#updateStream());
|
16
|
-
this.ownAll(captureMidiBox.channel.
|
17
|
+
this.ownAll(captureMidiBox.channel.catchupAndSubscribe(async (owner) => {
|
17
18
|
const channel = owner.getValue();
|
18
19
|
this.#filterChannel = channel >= 0 ? Option.wrap(channel) : Option.None;
|
19
|
-
|
20
|
-
|
20
|
+
if (this.armed.getValue()) {
|
21
|
+
await this.#streamGenerator();
|
22
|
+
}
|
23
|
+
}), captureMidiBox.deviceId.subscribe(async () => {
|
21
24
|
if (this.armed.getValue()) {
|
22
25
|
await this.#streamGenerator();
|
23
26
|
}
|
@@ -29,22 +32,27 @@ export class CaptureMidi extends Capture {
|
|
29
32
|
else {
|
30
33
|
this.#stopStream();
|
31
34
|
}
|
32
|
-
}), this.#notifier.subscribe(
|
33
|
-
console.debug(MidiData.debug(event.data));
|
34
|
-
const data = asDefined(event.data);
|
35
|
-
const engine = manager.project.engine;
|
36
|
-
if (MidiData.isNoteOn(data)) {
|
37
|
-
engine.noteOn(this.uuid, MidiData.readPitch(data), MidiData.readVelocity(data));
|
38
|
-
}
|
39
|
-
else if (MidiData.isNoteOff(data)) {
|
40
|
-
engine.noteOff(this.uuid, MidiData.readPitch(data));
|
41
|
-
}
|
42
|
-
}));
|
35
|
+
}), this.#notifier.subscribe((signal) => manager.project.engine.noteSignal(signal)));
|
43
36
|
}
|
37
|
+
notify(signal) { this.#notifier.notify(signal); }
|
38
|
+
subscribeNotes(observer) { return this.#notifier.subscribe(observer); }
|
44
39
|
get label() {
|
45
40
|
return MidiDevices.get().mapOr(midiAccess => this.deviceId.getValue().match({
|
46
|
-
none: () => this.armed.getValue() ?
|
47
|
-
|
41
|
+
none: () => this.armed.getValue() ? this.#filterChannel.match({
|
42
|
+
none: () => `Listening to all devices`,
|
43
|
+
some: channel => `Listening to all devices on channel '${channel}'`
|
44
|
+
}) : "Arm to listen to MIDI device...",
|
45
|
+
some: id => {
|
46
|
+
const device = midiAccess.inputs.get(id);
|
47
|
+
if (isUndefined(device)) {
|
48
|
+
return `⚠️ Could not find device with id '${id}'`;
|
49
|
+
}
|
50
|
+
const deviceName = device.name ?? "Unknown device";
|
51
|
+
return this.#filterChannel.match({
|
52
|
+
none: () => `Listening to ${deviceName}`,
|
53
|
+
some: channel => `Listening to ${deviceName} on channel '${channel}'`
|
54
|
+
});
|
55
|
+
}
|
48
56
|
}), "MIDI not available");
|
49
57
|
}
|
50
58
|
get deviceLabel() {
|
@@ -52,64 +60,76 @@ export class CaptureMidi extends Capture {
|
|
52
60
|
.flatMap(deviceId => MidiDevices.inputs()
|
53
61
|
.map(inputs => inputs.find(input => input.id === deviceId)?.name));
|
54
62
|
}
|
55
|
-
async prepareRecording(
|
56
|
-
|
57
|
-
|
58
|
-
|
63
|
+
async prepareRecording() {
|
64
|
+
if (MidiDevices.get().isEmpty()) {
|
65
|
+
if (MidiDevices.canRequestMidiAccess()) {
|
66
|
+
await MidiDevices.requestPermission();
|
67
|
+
}
|
68
|
+
else {
|
69
|
+
return warn("MIDI not available");
|
70
|
+
}
|
71
|
+
}
|
72
|
+
const optInputs = MidiDevices.inputs();
|
73
|
+
if (optInputs.isEmpty()) {
|
74
|
+
return warn("MIDI not available");
|
75
|
+
}
|
76
|
+
const inputs = optInputs.unwrap();
|
77
|
+
if (inputs.length === 0) {
|
78
|
+
return;
|
59
79
|
}
|
60
80
|
const option = this.deviceId.getValue();
|
61
81
|
if (option.nonEmpty()) {
|
62
|
-
const { inputs } = availableMidiDevices.unwrap();
|
63
|
-
const captureDevices = Array.from(inputs.values());
|
64
82
|
const deviceId = option.unwrap();
|
65
|
-
if (isUndefined(
|
83
|
+
if (isUndefined(inputs.find(device => deviceId === device.id))) {
|
66
84
|
return warn(`Could not find MIDI device with id: '${deviceId}'`);
|
67
85
|
}
|
68
86
|
}
|
69
87
|
}
|
70
|
-
startRecording(
|
88
|
+
startRecording() {
|
71
89
|
const availableMidiDevices = MidiDevices.inputs();
|
72
90
|
assert(availableMidiDevices.nonEmpty(), "No MIDI input devices found");
|
73
|
-
return RecordMidi.start({ notifier: this.#notifier, project, capture: this });
|
91
|
+
return RecordMidi.start({ notifier: this.#notifier, project: this.manager.project, capture: this });
|
74
92
|
}
|
75
93
|
async #updateStream() {
|
76
|
-
// TODO Check if the requirements have been changed (are different than the current stream setup)
|
77
94
|
if (MidiDevices.get().isEmpty()) {
|
78
95
|
await MidiDevices.requestPermission();
|
79
96
|
}
|
80
97
|
const availableMidiDevices = MidiDevices.inputs();
|
81
|
-
const
|
82
|
-
const
|
83
|
-
none: () =>
|
84
|
-
some: id =>
|
98
|
+
const available = availableMidiDevices.unwrap();
|
99
|
+
const capturing = this.deviceId.getValue().match({
|
100
|
+
none: () => available,
|
101
|
+
some: id => available.filter(device => id === device.id)
|
85
102
|
});
|
86
103
|
const activeNotes = new Int8Array(128);
|
87
|
-
this.#
|
88
|
-
this.#
|
104
|
+
this.#stream.ifSome(terminable => terminable.terminate());
|
105
|
+
this.#stream = Option.wrap(Terminable.many(...capturing.map(input => Events.subscribe(input, "midimessage", (event) => {
|
89
106
|
const data = event.data;
|
90
107
|
if (isDefined(data) &&
|
91
108
|
this.#filterChannel.mapOr(channel => MidiData.readChannel(data) === channel, true)) {
|
109
|
+
const pitch = MidiData.readPitch(data);
|
92
110
|
if (MidiData.isNoteOn(data)) {
|
93
|
-
activeNotes[
|
94
|
-
this.#notifier.notify(event);
|
111
|
+
activeNotes[pitch]++;
|
112
|
+
this.#notifier.notify(NoteSignal.fromEvent(event, this.uuid));
|
95
113
|
}
|
96
|
-
else if (MidiData.isNoteOff(data)) {
|
97
|
-
activeNotes[
|
98
|
-
this.#notifier.notify(event);
|
114
|
+
else if (MidiData.isNoteOff(data) && activeNotes[pitch] > 0) {
|
115
|
+
activeNotes[pitch]--;
|
116
|
+
this.#notifier.notify(NoteSignal.fromEvent(event, this.uuid));
|
99
117
|
}
|
100
118
|
}
|
101
119
|
})), Terminable.create(() => activeNotes.forEach((count, index) => {
|
102
120
|
if (count > 0) {
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
121
|
+
for (let channel = 0; channel < 16; channel++) {
|
122
|
+
const event = new MessageEvent("midimessage", { data: MidiData.noteOff(channel, index) });
|
123
|
+
const signal = NoteSignal.fromEvent(event, this.uuid);
|
124
|
+
for (let i = 0; i < count; i++) {
|
125
|
+
this.#notifier.notify(signal);
|
126
|
+
}
|
107
127
|
}
|
108
128
|
}
|
109
129
|
}))));
|
110
130
|
}
|
111
131
|
#stopStream() {
|
112
|
-
this.#
|
113
|
-
this.#
|
132
|
+
this.#stream.ifSome(terminable => terminable.terminate());
|
133
|
+
this.#stream = Option.None;
|
114
134
|
}
|
115
135
|
}
|
@@ -1,8 +1,8 @@
|
|
1
1
|
import { Terminable } from "@opendaw/lib-std";
|
2
2
|
import { SampleManager } from "@opendaw/studio-adapters";
|
3
|
-
import { Project } from "../Project";
|
4
|
-
import { Capture } from "./Capture";
|
3
|
+
import { Project } from "../project/Project";
|
5
4
|
import { RecordingWorklet } from "../RecordingWorklet";
|
5
|
+
import { Capture } from "./Capture";
|
6
6
|
export declare namespace RecordAudio {
|
7
7
|
type RecordAudioContext = {
|
8
8
|
recordingWorklet: RecordingWorklet;
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"RecordAudio.d.ts","sourceRoot":"","sources":["../../src/capture/RecordAudio.ts"],"names":[],"mappings":"AAAA,OAAO,
|
1
|
+
{"version":3,"file":"RecordAudio.d.ts","sourceRoot":"","sources":["../../src/capture/RecordAudio.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2C,UAAU,EAAmB,MAAM,kBAAkB,CAAA;AAGvG,OAAO,EAAC,aAAa,EAAY,MAAM,0BAA0B,CAAA;AACjE,OAAO,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAA;AAC1C,OAAO,EAAC,gBAAgB,EAAC,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAC,OAAO,EAAC,MAAM,WAAW,CAAA;AAIjC,yBAAiB,WAAW,CAAC;IACzB,KAAK,kBAAkB,GAAG;QACtB,gBAAgB,EAAE,gBAAgB,CAAA;QAClC,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,0FAAwF,kBAAkB,KACxG,UAqEL,CAAA;;CACJ"}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { Option, quantizeFloor, Terminable, Terminator, UUID } from "@opendaw/lib-std";
|
1
|
+
import { Option, quantizeCeil, quantizeFloor, Terminable, Terminator, UUID } from "@opendaw/lib-std";
|
2
2
|
import { dbToGain, PPQN } from "@opendaw/lib-dsp";
|
3
3
|
import { AudioFileBox, AudioRegionBox } from "@opendaw/studio-boxes";
|
4
4
|
import { TrackType } from "@opendaw/studio-adapters";
|
@@ -17,50 +17,64 @@ export var RecordAudio;
|
|
17
17
|
const streamGain = audioContext.createGain();
|
18
18
|
streamGain.gain.value = dbToGain(gainDb);
|
19
19
|
streamSource.connect(streamGain);
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
recordingWorklet.own(Terminable.create(() => {
|
21
|
+
streamGain.disconnect();
|
22
|
+
streamSource.disconnect();
|
23
|
+
}));
|
24
|
+
let recordingData = Option.None;
|
25
|
+
const createRecordingData = (position) => editing.modify(() => {
|
26
|
+
const fileDateString = new Date()
|
27
|
+
.toISOString()
|
28
|
+
.replaceAll("T", "-")
|
29
|
+
.replaceAll(".", "-")
|
30
|
+
.replaceAll(":", "-")
|
31
|
+
.replaceAll("Z", "");
|
32
|
+
const fileName = `Recording-${fileDateString}`;
|
33
|
+
const fileBox = AudioFileBox.create(boxGraph, uuid, box => box.fileName.setValue(fileName));
|
34
|
+
const regionBox = AudioRegionBox.create(boxGraph, UUID.generate(), box => {
|
35
|
+
box.file.refer(fileBox);
|
36
|
+
box.regions.refer(trackBox.regions);
|
37
|
+
box.position.setValue(position);
|
38
|
+
box.hue.setValue(ColorCodes.forTrackType(TrackType.Audio));
|
39
|
+
box.label.setValue("Recording");
|
40
|
+
});
|
41
|
+
return { fileBox, regionBox };
|
42
|
+
});
|
43
|
+
const { bpm, env: { audioContext: { sampleRate } } } = project;
|
44
|
+
terminator.ownAll(Terminable.create(() => {
|
45
|
+
if (recordingWorklet.numberOfFrames === 0 || recordingData.isEmpty()) {
|
46
|
+
console.debug("Abort recording audio.");
|
47
|
+
sampleManager.remove(uuid);
|
48
|
+
recordingWorklet.terminate();
|
49
|
+
}
|
50
|
+
else {
|
51
|
+
const { regionBox: { duration } } = recordingData.unwrap("No recording data available");
|
52
|
+
recordingWorklet.limit(PPQN.pulsesToSamples(duration.getValue(), bpm, sampleRate) | 0);
|
53
|
+
}
|
54
|
+
}), engine.position.catchupAndSubscribe(owner => {
|
55
|
+
if (!engine.isRecording.getValue()) {
|
23
56
|
return;
|
24
57
|
}
|
25
|
-
|
58
|
+
if (recordingData.isEmpty()) {
|
59
|
+
streamGain.connect(recordingWorklet);
|
60
|
+
recordingData = createRecordingData(quantizeFloor(owner.getValue(), beats));
|
61
|
+
}
|
62
|
+
const { regionBox } = recordingData.unwrap();
|
26
63
|
editing.modify(() => {
|
27
64
|
if (regionBox.isAttached()) {
|
28
65
|
const { duration, loopDuration } = regionBox;
|
29
|
-
const newDuration =
|
66
|
+
const newDuration = quantizeCeil(engine.position.getValue(), beats) - regionBox.position.getValue();
|
30
67
|
duration.setValue(newDuration);
|
31
68
|
loopDuration.setValue(newDuration);
|
69
|
+
const totalSamples = PPQN.pulsesToSamples(newDuration, project.bpm, sampleRate) | 0;
|
70
|
+
recordingWorklet.setFillLength(totalSamples);
|
71
|
+
}
|
72
|
+
else {
|
73
|
+
terminator.terminate();
|
74
|
+
recordingData = Option.None;
|
32
75
|
}
|
33
76
|
}, 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()));
|
77
|
+
}));
|
64
78
|
return terminator;
|
65
79
|
};
|
66
80
|
})(RecordAudio || (RecordAudio = {}));
|
@@ -1,9 +1,10 @@
|
|
1
1
|
import { Notifier, Terminable } from "@opendaw/lib-std";
|
2
|
-
import {
|
2
|
+
import { NoteSignal } from "@opendaw/studio-adapters";
|
3
|
+
import { Project } from "../project/Project";
|
3
4
|
import { Capture } from "./Capture";
|
4
5
|
export declare namespace RecordMidi {
|
5
6
|
type RecordMidiContext = {
|
6
|
-
notifier: Notifier<
|
7
|
+
notifier: Notifier<NoteSignal>;
|
7
8
|
project: Project;
|
8
9
|
capture: Capture;
|
9
10
|
};
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"RecordMidi.d.ts","sourceRoot":"","sources":["../../src/capture/RecordMidi.ts"],"names":[],"mappings":"AAAA,OAAO,
|
1
|
+
{"version":3,"file":"RecordMidi.d.ts","sourceRoot":"","sources":["../../src/capture/RecordMidi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,QAAQ,EAAuC,UAAU,EAAmB,MAAM,kBAAkB,CAAA;AAGlH,OAAO,EAAC,UAAU,EAAY,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAA;AAC1C,OAAO,EAAC,OAAO,EAAC,MAAM,WAAW,CAAA;AAIjC,yBAAiB,UAAU,CAAC;IACxB,KAAK,iBAAiB,GAAG;QACrB,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;QAC/B,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,OAAO,CAAA;KACnB,CAAA;IAED,MAAM,CAAC,MAAM,KAAK,GAAI,gCAA8B,iBAAiB,KAAG,UAiEvE,CAAA;;CACJ"}
|