@opendaw/studio-core 0.0.83 → 0.0.84
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/ExternalLib.d.ts +3 -2
- package/dist/ExternalLib.d.ts.map +1 -1
- package/dist/ExternalLib.js +2 -2
- package/dist/Preferences.d.ts +4 -0
- package/dist/Preferences.d.ts.map +1 -1
- package/dist/Preferences.js +4 -2
- package/dist/capture/CaptureDevices.js +1 -1
- package/dist/capture/RecordAudio.d.ts.map +1 -1
- package/dist/capture/RecordAudio.js +17 -9
- package/dist/dawproject/BuiltinDevices.d.ts +1 -1
- package/dist/dawproject/DawProject.d.ts.map +1 -1
- package/dist/dawproject/DawProject.js +17 -3
- package/dist/midi/MIDILearning.d.ts +4 -16
- package/dist/midi/MIDILearning.d.ts.map +1 -1
- package/dist/midi/MIDILearning.js +95 -62
- package/dist/processors.js +17 -17
- package/dist/processors.js.map +4 -4
- package/dist/project/Project.d.ts.map +1 -1
- package/dist/project/Project.js +1 -0
- package/dist/project/ProjectBundle.d.ts.map +1 -1
- package/dist/project/ProjectBundle.js +17 -3
- package/dist/project/ProjectMigration.d.ts.map +1 -1
- package/dist/project/ProjectMigration.js +4 -3
- package/dist/project/ProjectValidation.d.ts.map +1 -1
- package/dist/project/ProjectValidation.js +15 -13
- package/dist/project/audio/AudioContentFactory.d.ts.map +1 -1
- package/dist/project/audio/AudioContentFactory.js +3 -3
- package/dist/project/audio/AudioContentModifier.d.ts.map +1 -1
- package/dist/project/audio/AudioContentModifier.js +0 -1
- package/dist/project/audio/AudioFileBoxFactory.d.ts.map +1 -1
- package/dist/project/audio/AudioFileBoxFactory.js +5 -0
- package/dist/samples/DefaultSampleLoaderManager.d.ts.map +1 -1
- package/dist/samples/DefaultSampleLoaderManager.js +4 -1
- package/dist/soundfont/DefaultSoundfontLoader.d.ts.map +1 -1
- package/dist/soundfont/DefaultSoundfontLoader.js +8 -7
- package/dist/soundfont/SoundfontService.d.ts.map +1 -1
- package/dist/soundfont/SoundfontService.js +3 -6
- package/dist/workers-main.js +1 -1
- package/dist/workers-main.js.map +2 -2
- package/package.json +15 -15
package/dist/ExternalLib.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { Promises } from "@opendaw/lib-runtime";
|
|
1
2
|
export declare namespace ExternalLib {
|
|
2
|
-
const JSZip: () => Promise<import("jszip")
|
|
3
|
-
const SoundFont2: () => Promise<typeof import("soundfont2").SoundFont2
|
|
3
|
+
const JSZip: () => Promise<Promises.RejectedResult | Promises.ResolveResult<import("jszip")>>;
|
|
4
|
+
const SoundFont2: () => Promise<Promises.RejectedResult | Promises.ResolveResult<typeof import("soundfont2").SoundFont2>>;
|
|
4
5
|
}
|
|
5
6
|
//# sourceMappingURL=ExternalLib.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExternalLib.d.ts","sourceRoot":"","sources":["../src/ExternalLib.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ExternalLib.d.ts","sourceRoot":"","sources":["../src/ExternalLib.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,QAAQ,EAAC,MAAM,sBAAsB,CAAA;AAG7C,yBAAiB,WAAW,CAAC;IAMlB,MAAM,KAAK,kFAC+C,CAAA;IAE1D,MAAM,UAAU,yGACgD,CAAA;CAC1E"}
|
package/dist/ExternalLib.js
CHANGED
|
@@ -5,6 +5,6 @@ export var ExternalLib;
|
|
|
5
5
|
console.debug(`ExternalLib.importFailure count: ${count}, online: ${navigator.onLine}`, error);
|
|
6
6
|
return count < 10;
|
|
7
7
|
};
|
|
8
|
-
ExternalLib.JSZip = async () => await Promises.guardedRetry(() => import("jszip").then(({ default: JSZip }) => JSZip), callback);
|
|
9
|
-
ExternalLib.SoundFont2 = async () => await Promises.guardedRetry(() => import("soundfont2").then(({ SoundFont2 }) => SoundFont2), callback);
|
|
8
|
+
ExternalLib.JSZip = async () => await Promises.tryCatch(Promises.guardedRetry(() => import("jszip").then(({ default: JSZip }) => JSZip), callback));
|
|
9
|
+
ExternalLib.SoundFont2 = async () => await Promises.tryCatch(Promises.guardedRetry(() => import("soundfont2").then(({ SoundFont2 }) => SoundFont2), callback));
|
|
10
10
|
})(ExternalLib || (ExternalLib = {}));
|
package/dist/Preferences.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { z } from "zod";
|
|
|
2
2
|
import { Observer, Subscription } from "@opendaw/lib-std";
|
|
3
3
|
declare const PreferencesSchema: z.ZodObject<{
|
|
4
4
|
"visible-help-hints": z.ZodDefault<z.ZodBoolean>;
|
|
5
|
+
"note-audition-while-editing": z.ZodDefault<z.ZodBoolean>;
|
|
6
|
+
"modifying-controls-wheel": z.ZodDefault<z.ZodBoolean>;
|
|
5
7
|
"auto-open-clips": z.ZodDefault<z.ZodBoolean>;
|
|
6
8
|
"auto-create-output-compressor": z.ZodDefault<z.ZodBoolean>;
|
|
7
9
|
"footer-show-fps-meter": z.ZodDefault<z.ZodBoolean>;
|
|
@@ -13,6 +15,8 @@ export type Preferences = z.infer<typeof PreferencesSchema>;
|
|
|
13
15
|
export declare const Preferences: {
|
|
14
16
|
values: {
|
|
15
17
|
"visible-help-hints": boolean;
|
|
18
|
+
"note-audition-while-editing": boolean;
|
|
19
|
+
"modifying-controls-wheel": boolean;
|
|
16
20
|
"auto-open-clips": boolean;
|
|
17
21
|
"auto-create-output-compressor": boolean;
|
|
18
22
|
"footer-show-fps-meter": boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Preferences.d.ts","sourceRoot":"","sources":["../src/Preferences.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAA;AACrB,OAAO,EAAsB,QAAQ,EAAE,YAAY,EAAW,MAAM,kBAAkB,CAAA;AAEtF,QAAA,MAAM,iBAAiB
|
|
1
|
+
{"version":3,"file":"Preferences.d.ts","sourceRoot":"","sources":["../src/Preferences.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAA;AACrB,OAAO,EAAsB,QAAQ,EAAE,YAAY,EAAW,MAAM,kBAAkB,CAAA;AAEtF,QAAA,MAAM,iBAAiB;;;;;;;;;;iBAUrB,CAAA;AAEF,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAA;AAE3D,eAAO,MAAM,WAAW;;;;;;;;;;;;0BAkCM,GAAG,SAAS,MAAM,WAAW,YACrC,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,YAAY,GAAG,KAAG,YAAY;CAK1E,CAAA"}
|
package/dist/Preferences.js
CHANGED
|
@@ -2,12 +2,14 @@ import { z } from "zod";
|
|
|
2
2
|
import { isDefined, Notifier, tryCatch } from "@opendaw/lib-std";
|
|
3
3
|
const PreferencesSchema = z.object({
|
|
4
4
|
"visible-help-hints": z.boolean().default(true),
|
|
5
|
-
"
|
|
5
|
+
"note-audition-while-editing": z.boolean().default(true),
|
|
6
|
+
"modifying-controls-wheel": z.boolean().default(false),
|
|
7
|
+
"auto-open-clips": z.boolean().default(true),
|
|
6
8
|
"auto-create-output-compressor": z.boolean().default(true),
|
|
7
9
|
"footer-show-fps-meter": z.boolean().default(false),
|
|
8
10
|
"footer-show-build-infos": z.boolean().default(false),
|
|
9
11
|
"dragging-use-pointer-lock": z.boolean().default(false),
|
|
10
|
-
"enable-beta-features": z.boolean().default(false)
|
|
12
|
+
"enable-beta-features": z.boolean().default(false)
|
|
11
13
|
});
|
|
12
14
|
export const Preferences = (() => {
|
|
13
15
|
const STORAGE_KEY = "preferences";
|
|
@@ -31,7 +31,7 @@ export class CaptureDevices {
|
|
|
31
31
|
},
|
|
32
32
|
onRemoved: ({ box: { address: { uuid } } }) => {
|
|
33
33
|
this.#captures.removeByKeyIfExist(uuid)?.terminate();
|
|
34
|
-
this.#captureSubscriptions.
|
|
34
|
+
this.#captureSubscriptions.removeByKeyIfExist(uuid)?.subscription.terminate();
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
}
|
|
@@ -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,EAA6B,UAAU,EAAmB,MAAM,kBAAkB,CAAA;AAUzF,OAAO,EAAa,mBAAmB,EAAY,MAAM,0BAA0B,CAAA;AACnF,OAAO,EAAC,OAAO,EAAC,MAAM,YAAY,CAAA;AAClC,OAAO,EAAC,gBAAgB,EAAC,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAC,OAAO,EAAC,MAAM,WAAW,CAAA;AAGjC,yBAAiB,WAAW,CAAC;IACzB,KAAK,kBAAkB,GAAG;QACtB,gBAAgB,EAAE,gBAAgB,CAAA;QAClC,WAAW,EAAE,WAAW,CAAA;QACxB,aAAa,EAAE,mBAAmB,CAAA;QAClC,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,UAuFL,CAAA;;CACJ"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Option,
|
|
2
|
-
import { dbToGain, PPQN } from "@opendaw/lib-dsp";
|
|
3
|
-
import { AudioFileBox, AudioRegionBox, ValueEventCollectionBox } from "@opendaw/studio-boxes";
|
|
1
|
+
import { Option, quantizeFloor, Terminable, Terminator, UUID } from "@opendaw/lib-std";
|
|
2
|
+
import { dbToGain, PPQN, TimeBase } from "@opendaw/lib-dsp";
|
|
3
|
+
import { AudioFileBox, AudioPitchStretchBox, AudioRegionBox, ValueEventCollectionBox, WarpMarkerBox } from "@opendaw/studio-boxes";
|
|
4
4
|
import { ColorCodes, TrackType } from "@opendaw/studio-adapters";
|
|
5
5
|
import { RecordTrack } from "./RecordTrack";
|
|
6
6
|
export var RecordAudio;
|
|
@@ -31,16 +31,21 @@ export var RecordAudio;
|
|
|
31
31
|
const fileName = `Recording-${fileDateString}`;
|
|
32
32
|
const fileBox = AudioFileBox.create(boxGraph, uuid, box => box.fileName.setValue(fileName));
|
|
33
33
|
const collectionBox = ValueEventCollectionBox.create(boxGraph, UUID.generate());
|
|
34
|
+
const stretchBox = AudioPitchStretchBox.create(boxGraph, UUID.generate());
|
|
35
|
+
WarpMarkerBox.create(boxGraph, UUID.generate(), box => box.owner.refer(stretchBox.warpMarkers));
|
|
36
|
+
const warpMarkerBox = WarpMarkerBox.create(boxGraph, UUID.generate(), box => box.owner.refer(stretchBox.warpMarkers));
|
|
34
37
|
const regionBox = AudioRegionBox.create(boxGraph, UUID.generate(), box => {
|
|
35
38
|
box.file.refer(fileBox);
|
|
36
39
|
box.events.refer(collectionBox.owners);
|
|
37
40
|
box.regions.refer(trackBox.regions);
|
|
38
41
|
box.position.setValue(position);
|
|
39
42
|
box.hue.setValue(ColorCodes.forTrackType(TrackType.Audio));
|
|
43
|
+
box.timeBase.setValue(TimeBase.Musical);
|
|
40
44
|
box.label.setValue("Recording");
|
|
45
|
+
box.playMode.refer(stretchBox);
|
|
41
46
|
});
|
|
42
47
|
project.selection.select(regionBox);
|
|
43
|
-
return { fileBox, regionBox };
|
|
48
|
+
return { fileBox, regionBox, warpMarkerBox };
|
|
44
49
|
});
|
|
45
50
|
const { tempoMap, env: { audioContext: { sampleRate } } } = project;
|
|
46
51
|
terminator.ownAll(Terminable.create(() => {
|
|
@@ -62,15 +67,18 @@ export var RecordAudio;
|
|
|
62
67
|
streamGain.connect(recordingWorklet);
|
|
63
68
|
recordingData = createRecordingData(quantizeFloor(owner.getValue(), beats));
|
|
64
69
|
}
|
|
65
|
-
const { regionBox } = recordingData.unwrap();
|
|
70
|
+
const { regionBox, warpMarkerBox } = recordingData.unwrap();
|
|
66
71
|
editing.modify(() => {
|
|
67
72
|
if (regionBox.isAttached()) {
|
|
68
73
|
const { duration, loopDuration } = regionBox;
|
|
69
|
-
const
|
|
70
|
-
duration.setValue(
|
|
71
|
-
loopDuration.setValue(
|
|
72
|
-
|
|
74
|
+
const distanceInPPQN = Math.floor(engine.position.getValue() - regionBox.position.getValue());
|
|
75
|
+
duration.setValue(distanceInPPQN);
|
|
76
|
+
loopDuration.setValue(distanceInPPQN);
|
|
77
|
+
warpMarkerBox.position.setValue(distanceInPPQN);
|
|
78
|
+
const seconds = tempoMap.intervalToSeconds(0, distanceInPPQN);
|
|
79
|
+
const totalSamples = Math.ceil(seconds * sampleRate);
|
|
73
80
|
recordingWorklet.setFillLength(totalSamples);
|
|
81
|
+
warpMarkerBox.seconds.setValue(seconds);
|
|
74
82
|
}
|
|
75
83
|
else {
|
|
76
84
|
terminator.terminate();
|
|
@@ -4,6 +4,6 @@ import { EqualizerSchema } from "@opendaw/lib-dawproject";
|
|
|
4
4
|
import { Pointers } from "@opendaw/studio-enums";
|
|
5
5
|
import { RevampDeviceBox } from "@opendaw/studio-boxes";
|
|
6
6
|
export declare namespace BuiltinDevices {
|
|
7
|
-
const equalizer: (boxGraph: BoxGraph, equalizer: EqualizerSchema, field: Field<Pointers.
|
|
7
|
+
const equalizer: (boxGraph: BoxGraph, equalizer: EqualizerSchema, field: Field<Pointers.MIDIEffectHost> | Field<Pointers.AudioEffectHost>, index: int) => RevampDeviceBox;
|
|
8
8
|
}
|
|
9
9
|
//# sourceMappingURL=BuiltinDevices.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DawProject.d.ts","sourceRoot":"","sources":["../../src/dawproject/DawProject.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"DawProject.d.ts","sourceRoot":"","sources":["../../src/dawproject/DawProject.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+C,IAAI,EAAC,MAAM,kBAAkB,CAAA;AAEnF,OAAO,EAAsB,cAAc,EAAE,aAAa,EAAC,MAAM,yBAAyB,CAAA;AAC1F,OAAO,EAAC,eAAe,EAAE,mBAAmB,EAAC,MAAM,0BAA0B,CAAA;AAI7E,yBAAiB,UAAU,CAAC;IACxB,KAAY,QAAQ,GAAG;QAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,WAAW,CAAA;KAAE,CAAA;IAE5F,UAAiB,gBAAgB;QAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAA;QAChC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAA;KACvC;IAEM,MAAM,MAAM,GAAU,QAAQ,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC,KAAG,OAAO,CAAC;QAC7E,QAAQ,EAAE,cAAc,CAAC;QACzB,OAAO,EAAE,aAAa,CAAC;QACvB,SAAS,EAAE,gBAAgB,CAAA;KAC9B,CAgCA,CAAA;IAEM,MAAM,MAAM,GAAU,UAAU,eAAe,EACzB,eAAe,mBAAmB,EAClC,UAAU,cAAc,KAAG,OAAO,CAAC,WAAW,CAwB1E,CAAA;CACJ"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { asDefined, isDefined, panic, UUID } from "@opendaw/lib-std";
|
|
1
|
+
import { asDefined, isDefined, panic, RuntimeNotifier, UUID } from "@opendaw/lib-std";
|
|
2
2
|
import { Xml } from "@opendaw/lib-xml";
|
|
3
3
|
import { FileReferenceSchema, MetaDataSchema, ProjectSchema } from "@opendaw/lib-dawproject";
|
|
4
4
|
import { DawProjectExporter } from "./DawProjectExporter";
|
|
@@ -6,7 +6,14 @@ import { ExternalLib } from "../ExternalLib";
|
|
|
6
6
|
export var DawProject;
|
|
7
7
|
(function (DawProject) {
|
|
8
8
|
DawProject.decode = async (buffer) => {
|
|
9
|
-
const JSZip = await ExternalLib.JSZip();
|
|
9
|
+
const { status, value: JSZip, error } = await ExternalLib.JSZip();
|
|
10
|
+
if (status === "rejected") {
|
|
11
|
+
await RuntimeNotifier.info({
|
|
12
|
+
headline: "Error",
|
|
13
|
+
message: `Could not load JSZip: ${String(error)}`
|
|
14
|
+
});
|
|
15
|
+
return Promise.reject(error);
|
|
16
|
+
}
|
|
10
17
|
const zip = await JSZip.loadAsync(buffer);
|
|
11
18
|
const metaDataXml = await zip.file("metadata.xml")?.async("string");
|
|
12
19
|
const metaData = isDefined(metaDataXml) ? Xml.parse(metaDataXml, MetaDataSchema) : Xml.element({}, MetaDataSchema);
|
|
@@ -30,7 +37,14 @@ export var DawProject;
|
|
|
30
37
|
};
|
|
31
38
|
};
|
|
32
39
|
DawProject.encode = async (skeleton, sampleManager, metaData) => {
|
|
33
|
-
const JSZip = await ExternalLib.JSZip();
|
|
40
|
+
const { status, value: JSZip, error } = await ExternalLib.JSZip();
|
|
41
|
+
if (status === "rejected") {
|
|
42
|
+
await RuntimeNotifier.info({
|
|
43
|
+
headline: "Error",
|
|
44
|
+
message: `Could not load JSZip: ${String(error)}`
|
|
45
|
+
});
|
|
46
|
+
return Promise.reject(error);
|
|
47
|
+
}
|
|
34
48
|
const zip = new JSZip();
|
|
35
49
|
const projectSchema = DawProjectExporter.write(skeleton, sampleManager, {
|
|
36
50
|
write: (path, buffer) => {
|
|
@@ -1,26 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Address,
|
|
1
|
+
import { Terminable } from "@opendaw/lib-std";
|
|
2
|
+
import { Address, Field, PrimitiveField, PrimitiveValues } from "@opendaw/lib-box";
|
|
3
3
|
import { Pointers } from "@opendaw/studio-enums";
|
|
4
4
|
import { Project } from "../project";
|
|
5
|
-
export type MIDIConnectionJSON = ({
|
|
6
|
-
type: "control";
|
|
7
|
-
controlId: byte;
|
|
8
|
-
}) & {
|
|
9
|
-
address: AddressJSON;
|
|
10
|
-
channel: byte;
|
|
11
|
-
} & JSONValue;
|
|
12
|
-
export interface MIDIConnection extends Terminable {
|
|
13
|
-
address: Address;
|
|
14
|
-
label: Provider<string>;
|
|
15
|
-
toJSON(): MIDIConnectionJSON;
|
|
16
|
-
}
|
|
17
5
|
export declare class MIDILearning implements Terminable {
|
|
18
6
|
#private;
|
|
19
7
|
constructor(project: Project);
|
|
8
|
+
followUser(field: Field<Pointers.MIDIControllers>): void;
|
|
20
9
|
hasMidiConnection(address: Address): boolean;
|
|
21
10
|
forgetMidiConnection(address: Address): void;
|
|
22
|
-
learnMIDIControls(field: PrimitiveField<PrimitiveValues, Pointers.
|
|
23
|
-
toJSON(): ReadonlyArray<MIDIConnectionJSON>;
|
|
11
|
+
learnMIDIControls(field: PrimitiveField<PrimitiveValues, Pointers.MIDIControl | Pointers>): Promise<void>;
|
|
24
12
|
terminate(): void;
|
|
25
13
|
}
|
|
26
14
|
//# sourceMappingURL=MIDILearning.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MIDILearning.d.ts","sourceRoot":"","sources":["../../src/midi/MIDILearning.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"MIDILearning.d.ts","sourceRoot":"","sources":["../../src/midi/MIDILearning.ts"],"names":[],"mappings":"AAAA,OAAO,EAYH,UAAU,EAGb,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAC,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,eAAe,EAAC,MAAM,kBAAkB,CAAA;AAEhF,OAAO,EAAC,QAAQ,EAAC,MAAM,uBAAuB,CAAA;AAC9C,OAAO,EAAC,OAAO,EAAC,MAAM,YAAY,CAAA;AAelC,qBAAa,YAAa,YAAW,UAAU;;gBAS/B,OAAO,EAAE,OAAO;IAK5B,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC,eAAe,CAAC,GAAG,IAAI;IAkBxD,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO;IAI5C,oBAAoB,CAAC,OAAO,EAAE,OAAO;IAK/B,iBAAiB,CAAC,KAAK,EAAE,cAAc,CAAC,eAAe,EAAE,QAAQ,CAAC,WAAW,GAAG,QAAQ,CAAC;IAuC/F,SAAS,IAAI,IAAI;CA8DpB"}
|
|
@@ -1,107 +1,140 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { asDefined, asInstanceOf, EmptyExec, Errors, isNotNull, Option, RuntimeNotifier, Terminator, UUID } from "@opendaw/lib-std";
|
|
2
2
|
import { Address } from "@opendaw/lib-box";
|
|
3
3
|
import { MidiData } from "@opendaw/lib-midi";
|
|
4
4
|
import { MidiDevices } from "./MidiDevices";
|
|
5
5
|
import { AnimationFrame } from "@opendaw/lib-dom";
|
|
6
|
+
import { MIDIControllerBox } from "@opendaw/studio-boxes";
|
|
7
|
+
// will be part of MIDI preferences. Should not filter for the device-id
|
|
8
|
+
// because it is different for the same hardware in different browsers.
|
|
9
|
+
const respectDeviceID = false;
|
|
6
10
|
export class MIDILearning {
|
|
7
11
|
#terminator = new Terminator();
|
|
8
12
|
#project;
|
|
9
13
|
#connections;
|
|
14
|
+
#optMIDIContollers = Option.None;
|
|
15
|
+
#optFieldSubscription = Option.None;
|
|
10
16
|
constructor(project) {
|
|
11
17
|
this.#project = project;
|
|
12
|
-
this.#connections = Address.newSet(connection => connection.address);
|
|
18
|
+
this.#connections = Address.newSet(connection => connection.box.address);
|
|
19
|
+
}
|
|
20
|
+
followUser(field) {
|
|
21
|
+
this.#killAllConnections();
|
|
22
|
+
this.#optFieldSubscription.ifSome(subscription => subscription.terminate());
|
|
23
|
+
this.#optFieldSubscription = Option.None;
|
|
24
|
+
this.#optMIDIContollers = Option.wrap(field);
|
|
25
|
+
this.#optFieldSubscription = Option.wrap(field.pointerHub.catchupAndSubscribe({
|
|
26
|
+
onAdded: ({ box: anyBox }) => {
|
|
27
|
+
if (MidiDevices.get().isEmpty() && MidiDevices.canRequestMidiAccess()) {
|
|
28
|
+
MidiDevices.requestPermission().then(EmptyExec, EmptyExec);
|
|
29
|
+
}
|
|
30
|
+
const box = asInstanceOf(anyBox, MIDIControllerBox);
|
|
31
|
+
const { subscription, handleEvent } = this.#registerMIDIControllerBox(box);
|
|
32
|
+
this.#connections.add({ box, subscription, handleEvent });
|
|
33
|
+
},
|
|
34
|
+
onRemoved: ({ box: { address } }) => this.#connections.removeByKey(address).subscription.terminate()
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
hasMidiConnection(address) {
|
|
38
|
+
return this.#findConnectionByParameterAddress(address).nonEmpty();
|
|
39
|
+
}
|
|
40
|
+
forgetMidiConnection(address) {
|
|
41
|
+
const connection = this.#findConnectionByParameterAddress(address).unwrap("No connection to forget");
|
|
42
|
+
this.#project.editing.modify(() => asDefined(connection).box.delete());
|
|
13
43
|
}
|
|
14
|
-
hasMidiConnection(address) { return this.#connections.hasKey(address); }
|
|
15
|
-
forgetMidiConnection(address) { this.#connections.removeByKey(address).terminate(); }
|
|
16
44
|
async learnMIDIControls(field) {
|
|
45
|
+
if (this.#optMIDIContollers.isEmpty()) {
|
|
46
|
+
return RuntimeNotifier.info({
|
|
47
|
+
headline: "Learn Midi Controller...",
|
|
48
|
+
message: "No user accepting midi controls."
|
|
49
|
+
});
|
|
50
|
+
}
|
|
17
51
|
if (!MidiDevices.canRequestMidiAccess()) {
|
|
18
52
|
return;
|
|
19
53
|
}
|
|
20
54
|
await MidiDevices.requestPermission();
|
|
21
55
|
const learnLifecycle = this.#terminator.spawn();
|
|
22
56
|
const abortController = new AbortController();
|
|
57
|
+
// TODO subscribeMessageEvents is not really suffient, because it does not distinguish between midi devices.
|
|
23
58
|
learnLifecycle.own(MidiDevices.subscribeMessageEvents((event) => {
|
|
24
59
|
const data = event.data;
|
|
25
60
|
if (data === null) {
|
|
26
61
|
return;
|
|
27
62
|
}
|
|
63
|
+
const deviceId = event.target instanceof MIDIInput ? event.target.id : "";
|
|
28
64
|
if (MidiData.isController(data)) {
|
|
29
65
|
learnLifecycle.terminate();
|
|
30
66
|
abortController.abort(Errors.AbortError);
|
|
31
|
-
|
|
67
|
+
const midiControllersField = this.#optMIDIContollers.unwrap();
|
|
68
|
+
const { editing } = this.#project;
|
|
69
|
+
const optBox = editing.modify(() => MIDIControllerBox.create(this.#project.boxGraph, UUID.generate(), box => {
|
|
70
|
+
box.controllers.refer(midiControllersField);
|
|
71
|
+
box.parameter.refer(field);
|
|
72
|
+
box.deviceId.setValue(deviceId);
|
|
73
|
+
box.deviceChannel.setValue(MidiData.readChannel(data));
|
|
74
|
+
box.controlId.setValue(MidiData.readParam1(data));
|
|
75
|
+
}));
|
|
76
|
+
this.#connections.get(optBox.unwrap("Could not create MIDIControllerBox").address).handleEvent(event);
|
|
32
77
|
}
|
|
33
78
|
}));
|
|
34
79
|
return RuntimeNotifier.info({
|
|
35
|
-
headline: "Learn Midi
|
|
36
|
-
message: "
|
|
80
|
+
headline: "Learn Midi Controller...",
|
|
81
|
+
message: "Turn a controller on your midi-device...",
|
|
37
82
|
okText: "Cancel",
|
|
38
83
|
abortSignal: abortController.signal
|
|
39
84
|
}).then(() => learnLifecycle.terminate(), Errors.CatchAbort);
|
|
40
85
|
}
|
|
41
|
-
toJSON() {
|
|
42
|
-
return this.#connections.values().map(connection => connection.toJSON());
|
|
43
|
-
}
|
|
44
86
|
terminate() {
|
|
45
87
|
this.#killAllConnections();
|
|
46
88
|
this.#terminator.terminate();
|
|
47
89
|
}
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
observer(event);
|
|
53
|
-
}
|
|
54
|
-
const subscription = MidiDevices.subscribeMessageEvents(observer, channel);
|
|
55
|
-
this.#connections.add({
|
|
56
|
-
address: field.address,
|
|
57
|
-
toJSON: () => ({
|
|
58
|
-
type: "control",
|
|
59
|
-
address: field.address.toJSON(),
|
|
60
|
-
channel,
|
|
61
|
-
controlId
|
|
62
|
-
}),
|
|
63
|
-
label: () => this.#project.parameterFieldAdapters.get(field.address).name,
|
|
64
|
-
terminate: () => {
|
|
65
|
-
terminate();
|
|
66
|
-
subscription.terminate();
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
#killAllConnections() {
|
|
71
|
-
this.#connections.forEach(({ terminate }) => terminate());
|
|
72
|
-
this.#connections.clear();
|
|
73
|
-
}
|
|
74
|
-
#createMidiControlObserver(project, adapter, controlId) {
|
|
90
|
+
#registerMIDIControllerBox({ parameter: { targetAddress }, controlId, deviceId, deviceChannel }) {
|
|
91
|
+
const address = targetAddress.unwrap("No parameter address");
|
|
92
|
+
const adapter = this.#project.parameterFieldAdapters.get(address);
|
|
93
|
+
const { editing } = this.#project;
|
|
75
94
|
const registration = adapter.registerMidiControl();
|
|
76
95
|
let pendingValue = null;
|
|
77
|
-
const update = (value) =>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
const update = (value) => editing.modify(() => adapter.setUnitValue(value), false);
|
|
97
|
+
const handleEvent = (event) => {
|
|
98
|
+
const data = event.data;
|
|
99
|
+
if (data === null) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const id = event.target instanceof MIDIInput ? event.target.id : "";
|
|
103
|
+
if (MidiData.isController(data)
|
|
104
|
+
&& MidiData.readParam1(data) === controlId.getValue() && (!respectDeviceID || id === deviceId.getValue())) {
|
|
105
|
+
const value = MidiData.asValue(data);
|
|
106
|
+
if (pendingValue === null) {
|
|
107
|
+
update(value);
|
|
108
|
+
pendingValue = value;
|
|
109
|
+
AnimationFrame.once(() => {
|
|
110
|
+
if (isNotNull(pendingValue)) {
|
|
111
|
+
update(pendingValue);
|
|
112
|
+
pendingValue = null;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
83
115
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (pendingValue === null) {
|
|
87
|
-
update(value);
|
|
88
|
-
pendingValue = value;
|
|
89
|
-
AnimationFrame.once(() => {
|
|
90
|
-
if (isNotNull(pendingValue)) {
|
|
91
|
-
update(pendingValue);
|
|
92
|
-
pendingValue = null;
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
pendingValue = value;
|
|
98
|
-
}
|
|
116
|
+
else {
|
|
117
|
+
pendingValue = value;
|
|
99
118
|
}
|
|
100
|
-
},
|
|
101
|
-
terminate: () => {
|
|
102
|
-
pendingValue = null;
|
|
103
|
-
registration.terminate();
|
|
104
119
|
}
|
|
105
120
|
};
|
|
121
|
+
const channel = deviceChannel.getValue() === -1 ? undefined : deviceChannel.getValue();
|
|
122
|
+
const subscription = MidiDevices.subscribeMessageEvents(handleEvent, channel);
|
|
123
|
+
return {
|
|
124
|
+
subscription: {
|
|
125
|
+
terminate: () => {
|
|
126
|
+
pendingValue = null;
|
|
127
|
+
subscription.terminate();
|
|
128
|
+
registration.terminate();
|
|
129
|
+
}
|
|
130
|
+
}, handleEvent
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
#killAllConnections() {
|
|
134
|
+
this.#connections.forEach(({ subscription }) => subscription.terminate());
|
|
135
|
+
this.#connections.clear();
|
|
136
|
+
}
|
|
137
|
+
#findConnectionByParameterAddress(address) {
|
|
138
|
+
return Option.wrap(this.#connections.values().find(({ box }) => box.parameter.targetAddress.unwrap() === address));
|
|
106
139
|
}
|
|
107
140
|
}
|