@opendaw/studio-core 0.0.22 → 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/Colors.d.ts.map +1 -1
- package/dist/Colors.js +21 -13
- 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 -48
- 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
package/dist/RecordingWorklet.js
CHANGED
@@ -1,44 +1,13 @@
|
|
1
|
-
import {
|
1
|
+
import { ByteArrayInput, Notifier, Option, Progress, Terminable, Terminator, UUID } from "@opendaw/lib-std";
|
2
|
+
import { BPMTools } from "@opendaw/lib-dsp";
|
3
|
+
import { SamplePeaks } from "@opendaw/lib-fusion";
|
2
4
|
import { mergeChunkPlanes, RingBuffer } from "@opendaw/studio-adapters";
|
3
|
-
import {
|
5
|
+
import { SampleStorage } from "./samples/SampleStorage";
|
4
6
|
import { RenderQuantum } from "./RenderQuantum";
|
5
7
|
import { WorkerAgents } from "./WorkerAgents";
|
6
|
-
import {
|
7
|
-
import { BPMTools } from "@opendaw/lib-dsp";
|
8
|
-
class PeaksWriter {
|
9
|
-
numChannels;
|
10
|
-
data;
|
11
|
-
stages;
|
12
|
-
dataOffset = 0;
|
13
|
-
shift = 7;
|
14
|
-
dataIndex;
|
15
|
-
numFrames = 0 | 0;
|
16
|
-
constructor(numChannels) {
|
17
|
-
this.numChannels = numChannels;
|
18
|
-
this.data = Arrays.create(() => new Int32Array(1 << 20), numChannels); // TODO auto-resize
|
19
|
-
this.dataIndex = new Int32Array(numChannels);
|
20
|
-
this.stages = [this];
|
21
|
-
}
|
22
|
-
get numPeaks() { return Math.ceil(this.numFrames / (1 << this.shift)); }
|
23
|
-
unitsEachPeak() { return 1 << this.shift; }
|
24
|
-
append(frames) {
|
25
|
-
for (let channel = 0; channel < this.numChannels; ++channel) {
|
26
|
-
const channelFrames = frames[channel];
|
27
|
-
assert(channelFrames.length === RenderQuantum, "Invalid number of frames.");
|
28
|
-
let min = Number.POSITIVE_INFINITY;
|
29
|
-
let max = Number.NEGATIVE_INFINITY;
|
30
|
-
for (let i = 0; i < RenderQuantum; ++i) {
|
31
|
-
const frame = channelFrames[i];
|
32
|
-
min = Math.min(frame, min);
|
33
|
-
max = Math.max(frame, max);
|
34
|
-
}
|
35
|
-
this.data[channel][this.dataIndex[channel]++] = SamplePeakWorker.pack(min, max);
|
36
|
-
}
|
37
|
-
this.numFrames += RenderQuantum;
|
38
|
-
}
|
39
|
-
nearest(_unitsPerPixel) { return this.stages.at(0) ?? null; }
|
40
|
-
}
|
8
|
+
import { PeaksWriter } from "./PeaksWriter";
|
41
9
|
export class RecordingWorklet extends AudioWorkletNode {
|
10
|
+
#terminator = new Terminator();
|
42
11
|
uuid = UUID.generate();
|
43
12
|
#output;
|
44
13
|
#notifier;
|
@@ -47,7 +16,7 @@ export class RecordingWorklet extends AudioWorkletNode {
|
|
47
16
|
#data = Option.None;
|
48
17
|
#peaks = Option.None;
|
49
18
|
#isRecording = true;
|
50
|
-
#
|
19
|
+
#limitSamples = Number.POSITIVE_INFINITY;
|
51
20
|
#state = { type: "record" };
|
52
21
|
constructor(context, config, outputLatency) {
|
53
22
|
super(context, "recording-processor", {
|
@@ -56,31 +25,30 @@ export class RecordingWorklet extends AudioWorkletNode {
|
|
56
25
|
channelCountMode: "explicit",
|
57
26
|
processorOptions: config
|
58
27
|
});
|
59
|
-
if (isUndefined(outputLatency)) {
|
60
|
-
// TODO Talk to the user
|
61
|
-
console.warn("outputLatency is undefined. Please use Chrome.");
|
62
|
-
}
|
63
28
|
this.#peakWriter = new PeaksWriter(config.numberOfChannels);
|
64
|
-
this.#
|
29
|
+
this.#peaks = Option.wrap(this.#peakWriter);
|
65
30
|
this.#output = [];
|
66
31
|
this.#notifier = new Notifier();
|
67
32
|
this.#reader = RingBuffer.reader(config, array => {
|
68
33
|
if (this.#isRecording) {
|
69
|
-
|
70
|
-
|
34
|
+
this.#output.push(array);
|
35
|
+
const latencyInSamples = (outputLatency * this.context.sampleRate) | 0;
|
36
|
+
if (this.numberOfFrames >= latencyInSamples) {
|
71
37
|
this.#peakWriter.append(array);
|
72
38
|
}
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
}
|
39
|
+
const need = this.numberOfFrames - latencyInSamples;
|
40
|
+
if (need >= this.#limitSamples) {
|
41
|
+
this.#finalize().catch(error => console.warn(error));
|
77
42
|
}
|
78
43
|
}
|
79
44
|
});
|
80
45
|
}
|
46
|
+
own(terminable) { return this.#terminator.own(terminable); }
|
47
|
+
limit(count) { this.#limitSamples = count; }
|
48
|
+
setFillLength(value) { this.#peakWriter.numFrames = value; }
|
81
49
|
get numberOfFrames() { return this.#output.length * RenderQuantum; }
|
82
50
|
get data() { return this.#data; }
|
83
|
-
get peaks() { return this.#peaks; }
|
51
|
+
get peaks() { return this.#peaks.isEmpty() ? Option.wrap(this.#peakWriter) : this.#peaks; }
|
84
52
|
get state() { return this.#state; }
|
85
53
|
invalidate() { }
|
86
54
|
subscribe(observer) {
|
@@ -90,35 +58,41 @@ export class RecordingWorklet extends AudioWorkletNode {
|
|
90
58
|
}
|
91
59
|
return this.#notifier.subscribe(observer);
|
92
60
|
}
|
93
|
-
|
61
|
+
terminate() {
|
94
62
|
this.#reader.stop();
|
95
63
|
this.#isRecording = false;
|
64
|
+
this.#terminator.terminate();
|
65
|
+
}
|
66
|
+
toString() { return `{RecordingWorklet}`; }
|
67
|
+
async #finalize() {
|
68
|
+
this.#isRecording = false;
|
69
|
+
this.#reader.stop();
|
70
|
+
if (this.#output.length === 0) {
|
71
|
+
return;
|
72
|
+
}
|
73
|
+
const totalSamples = this.#limitSamples;
|
96
74
|
const sample_rate = this.context.sampleRate;
|
97
|
-
const numberOfFrames = this.#output.length * RenderQuantum;
|
98
75
|
const numberOfChannels = this.channelCount;
|
99
|
-
const frames = mergeChunkPlanes(this.#output, RenderQuantum,
|
76
|
+
const frames = mergeChunkPlanes(this.#output, RenderQuantum, this.#output.length * RenderQuantum)
|
77
|
+
.map(frame => frame.slice(-totalSamples));
|
100
78
|
const audioData = {
|
101
79
|
sampleRate: sample_rate,
|
102
80
|
numberOfChannels,
|
103
|
-
numberOfFrames,
|
81
|
+
numberOfFrames: totalSamples,
|
104
82
|
frames
|
105
83
|
};
|
106
84
|
this.#data = Option.wrap(audioData);
|
107
|
-
const shifts = SamplePeaks.findBestFit(
|
85
|
+
const shifts = SamplePeaks.findBestFit(totalSamples);
|
108
86
|
const peaks = await WorkerAgents
|
109
|
-
.Peak.generateAsync(Progress.Empty, shifts, frames,
|
87
|
+
.Peak.generateAsync(Progress.Empty, shifts, frames, totalSamples, numberOfChannels);
|
110
88
|
this.#peaks = Option.wrap(SamplePeaks.from(new ByteArrayInput(peaks)));
|
111
89
|
const bpm = BPMTools.detect(frames[0], sample_rate);
|
112
|
-
const duration =
|
90
|
+
const duration = totalSamples / sample_rate;
|
113
91
|
const meta = { name: "Recording", bpm, sample_rate, duration };
|
114
92
|
await SampleStorage.store(this.uuid, audioData, peaks, meta);
|
115
93
|
this.#setState({ type: "loaded" });
|
94
|
+
this.terminate();
|
116
95
|
}
|
117
|
-
terminate() {
|
118
|
-
this.#reader.stop();
|
119
|
-
this.#isRecording = false;
|
120
|
-
}
|
121
|
-
toString() { return `{RecordingWorklet}`; }
|
122
96
|
#setState(value) {
|
123
97
|
this.#state = value;
|
124
98
|
this.#notifier.notify(this.#state);
|
@@ -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,23 +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
|
-
const isNoteOn = MidiData.isNoteOn(data);
|
37
|
-
if (MidiData.isNoteOff(data) || (isNoteOn && MidiData.readVelocity(data) === 0)) {
|
38
|
-
engine.noteOff(this.uuid, MidiData.readPitch(data));
|
39
|
-
}
|
40
|
-
else if (isNoteOn) {
|
41
|
-
engine.noteOn(this.uuid, MidiData.readPitch(data), MidiData.readVelocity(data));
|
42
|
-
}
|
43
|
-
}));
|
35
|
+
}), this.#notifier.subscribe((signal) => manager.project.engine.noteSignal(signal)));
|
44
36
|
}
|
37
|
+
notify(signal) { this.#notifier.notify(signal); }
|
38
|
+
subscribeNotes(observer) { return this.#notifier.subscribe(observer); }
|
45
39
|
get label() {
|
46
40
|
return MidiDevices.get().mapOr(midiAccess => this.deviceId.getValue().match({
|
47
|
-
none: () => this.armed.getValue() ?
|
48
|
-
|
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
|
+
}
|
49
56
|
}), "MIDI not available");
|
50
57
|
}
|
51
58
|
get deviceLabel() {
|
@@ -53,64 +60,76 @@ export class CaptureMidi extends Capture {
|
|
53
60
|
.flatMap(deviceId => MidiDevices.inputs()
|
54
61
|
.map(inputs => inputs.find(input => input.id === deviceId)?.name));
|
55
62
|
}
|
56
|
-
async prepareRecording(
|
57
|
-
|
58
|
-
|
59
|
-
|
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;
|
60
79
|
}
|
61
80
|
const option = this.deviceId.getValue();
|
62
81
|
if (option.nonEmpty()) {
|
63
|
-
const { inputs } = availableMidiDevices.unwrap();
|
64
|
-
const captureDevices = Array.from(inputs.values());
|
65
82
|
const deviceId = option.unwrap();
|
66
|
-
if (isUndefined(
|
83
|
+
if (isUndefined(inputs.find(device => deviceId === device.id))) {
|
67
84
|
return warn(`Could not find MIDI device with id: '${deviceId}'`);
|
68
85
|
}
|
69
86
|
}
|
70
87
|
}
|
71
|
-
startRecording(
|
88
|
+
startRecording() {
|
72
89
|
const availableMidiDevices = MidiDevices.inputs();
|
73
90
|
assert(availableMidiDevices.nonEmpty(), "No MIDI input devices found");
|
74
|
-
return RecordMidi.start({ notifier: this.#notifier, project, capture: this });
|
91
|
+
return RecordMidi.start({ notifier: this.#notifier, project: this.manager.project, capture: this });
|
75
92
|
}
|
76
93
|
async #updateStream() {
|
77
|
-
// TODO Check if the requirements have been changed (are different than the current stream setup)
|
78
94
|
if (MidiDevices.get().isEmpty()) {
|
79
95
|
await MidiDevices.requestPermission();
|
80
96
|
}
|
81
97
|
const availableMidiDevices = MidiDevices.inputs();
|
82
|
-
const
|
83
|
-
const
|
84
|
-
none: () =>
|
85
|
-
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)
|
86
102
|
});
|
87
103
|
const activeNotes = new Int8Array(128);
|
88
|
-
this.#
|
89
|
-
this.#
|
104
|
+
this.#stream.ifSome(terminable => terminable.terminate());
|
105
|
+
this.#stream = Option.wrap(Terminable.many(...capturing.map(input => Events.subscribe(input, "midimessage", (event) => {
|
90
106
|
const data = event.data;
|
91
107
|
if (isDefined(data) &&
|
92
108
|
this.#filterChannel.mapOr(channel => MidiData.readChannel(data) === channel, true)) {
|
109
|
+
const pitch = MidiData.readPitch(data);
|
93
110
|
if (MidiData.isNoteOn(data)) {
|
94
|
-
activeNotes[
|
95
|
-
this.#notifier.notify(event);
|
111
|
+
activeNotes[pitch]++;
|
112
|
+
this.#notifier.notify(NoteSignal.fromEvent(event, this.uuid));
|
96
113
|
}
|
97
|
-
else if (MidiData.isNoteOff(data)) {
|
98
|
-
activeNotes[
|
99
|
-
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));
|
100
117
|
}
|
101
118
|
}
|
102
119
|
})), Terminable.create(() => activeNotes.forEach((count, index) => {
|
103
120
|
if (count > 0) {
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
+
}
|
108
127
|
}
|
109
128
|
}
|
110
129
|
}))));
|
111
130
|
}
|
112
131
|
#stopStream() {
|
113
|
-
this.#
|
114
|
-
this.#
|
132
|
+
this.#stream.ifSome(terminable => terminable.terminate());
|
133
|
+
this.#stream = Option.None;
|
115
134
|
}
|
116
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"}
|