@swiftbrowser/react 0.18.2 → 0.19.0
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/README.md +53 -7
- package/dist/audioEngine.d.ts +359 -0
- package/dist/audioEngine.d.ts.map +1 -0
- package/dist/components/containers.d.ts +4 -1
- package/dist/components/containers.d.ts.map +1 -1
- package/dist/components/controls.d.ts +8 -1
- package/dist/components/controls.d.ts.map +1 -1
- package/dist/components/lists.d.ts +3 -0
- package/dist/components/lists.d.ts.map +1 -1
- package/dist/components/menus.d.ts.map +1 -1
- package/dist/components/stacks.d.ts +6 -2
- package/dist/components/stacks.d.ts.map +1 -1
- package/dist/components/system.d.ts +14 -4
- package/dist/components/system.d.ts.map +1 -1
- package/dist/components/text.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +949 -31
- package/dist/index.js.map +1 -1
- package/dist/languageModel.d.ts +136 -0
- package/dist/languageModel.d.ts.map +1 -0
- package/dist/modifiers.d.ts +14 -0
- package/dist/modifiers.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { registerHostContent, notifyHostContentChange, unregisterHostContent } f
|
|
|
15
15
|
import { createRoot } from "react-dom/client";
|
|
16
16
|
import { flushSync } from "react-dom";
|
|
17
17
|
import { chooseColor } from "@swiftbrowser/web/src/colorPicker";
|
|
18
|
+
import { base64Encode } from "@swiftbrowser/web/src/base64";
|
|
18
19
|
import { timingFor } from "@swiftbrowser/web/src/animation";
|
|
19
20
|
const ViewEnvironmentContext = createContext({});
|
|
20
21
|
function useViewEnvironment() {
|
|
@@ -1433,7 +1434,8 @@ function ScrollView(props) {
|
|
|
1433
1434
|
const scroll = { axes, ...refresh.props, ...geometry.props };
|
|
1434
1435
|
const handlers = refresh.handlers || geometry.handlers ? { ...refresh.handlers, ...geometry.handlers } : void 0;
|
|
1435
1436
|
if (own.scrollTargetBehavior) scroll["snap"] = own.scrollTargetBehavior;
|
|
1436
|
-
if (own.scrollDismissesKeyboard) scroll["dismissesKeyboard"] = true;
|
|
1437
|
+
if (own.scrollDismissesKeyboard === true || own.scrollDismissesKeyboard === "immediately") scroll["dismissesKeyboard"] = true;
|
|
1438
|
+
else if (own.scrollDismissesKeyboard === "interactively") scroll["dismissesKeyboard"] = "interactively";
|
|
1437
1439
|
if (own.scrollIndicators === "hidden") scroll["showsIndicators"] = false;
|
|
1438
1440
|
if (own.defaultScrollAnchor !== void 0) scroll["defaultScrollAnchor"] = unitPoint(own.defaultScrollAnchor);
|
|
1439
1441
|
const edgeEffect = edgeEffectProp(settings);
|
|
@@ -1717,7 +1719,7 @@ function Label(props) {
|
|
|
1717
1719
|
if (style === "titleOnly") return applyModifiers(title, modifiers);
|
|
1718
1720
|
return applyModifiers(
|
|
1719
1721
|
/* @__PURE__ */ jsxs(HStack, { spacing: 8, children: [
|
|
1720
|
-
styled({ labelIcon: true }, icon),
|
|
1722
|
+
style === "titleAndIcon" ? icon : styled({ labelIcon: true }, icon),
|
|
1721
1723
|
title
|
|
1722
1724
|
] }),
|
|
1723
1725
|
modifiers
|
|
@@ -2185,6 +2187,8 @@ const MODIFIER_KEYS = /* @__PURE__ */ new Set([
|
|
|
2185
2187
|
"navigationTitle",
|
|
2186
2188
|
"navigationBarTitleDisplayMode",
|
|
2187
2189
|
"toolbarRole",
|
|
2190
|
+
"toolbarVisibility",
|
|
2191
|
+
"statusBarHidden",
|
|
2188
2192
|
"toolbar",
|
|
2189
2193
|
"searchable",
|
|
2190
2194
|
"navigationDestination",
|
|
@@ -2259,6 +2263,7 @@ const MODIFIER_KEYS = /* @__PURE__ */ new Set([
|
|
|
2259
2263
|
"onKeyPress",
|
|
2260
2264
|
"sensoryFeedback",
|
|
2261
2265
|
"textCase",
|
|
2266
|
+
"textSelection",
|
|
2262
2267
|
"coordinateSpace",
|
|
2263
2268
|
"contentTransition",
|
|
2264
2269
|
"scrollDisabled",
|
|
@@ -2446,7 +2451,8 @@ const STYLED_KEYS = /* @__PURE__ */ new Set([
|
|
|
2446
2451
|
"matchedGeometryEffect",
|
|
2447
2452
|
"containerRelativeFrame",
|
|
2448
2453
|
"contentTransition",
|
|
2449
|
-
"scrollDisabled"
|
|
2454
|
+
"scrollDisabled",
|
|
2455
|
+
"textSelection"
|
|
2450
2456
|
]);
|
|
2451
2457
|
const ENVIRONMENT_KEYS = /* @__PURE__ */ new Set([
|
|
2452
2458
|
"buttonStyle",
|
|
@@ -2598,6 +2604,17 @@ function apply(key, value, node, sides) {
|
|
|
2598
2604
|
case "toolbarRole":
|
|
2599
2605
|
sides.push(sb(PREF_KIND, { toolbarRole: value }, void 0, void 0, "navscreen"));
|
|
2600
2606
|
return node;
|
|
2607
|
+
case "toolbarVisibility": {
|
|
2608
|
+
const bars = value;
|
|
2609
|
+
const props = {};
|
|
2610
|
+
if (bars.navigationBar && bars.navigationBar !== "automatic") props["navigationBarHidden"] = bars.navigationBar === "hidden";
|
|
2611
|
+
if (bars.bottomBar && bars.bottomBar !== "automatic") props["bottomBarHidden"] = bars.bottomBar === "hidden";
|
|
2612
|
+
sides.push(sb(PREF_KIND, props, void 0, void 0, "navscreen"));
|
|
2613
|
+
return node;
|
|
2614
|
+
}
|
|
2615
|
+
case "statusBarHidden":
|
|
2616
|
+
sides.push(sb(PREF_KIND, { statusBarHidden: Boolean(value) }, void 0, void 0, "navscreen"));
|
|
2617
|
+
return node;
|
|
2601
2618
|
case "toolbar":
|
|
2602
2619
|
sides.push(sb(HOIST_KIND, void 0, void 0, value, "navscreen"));
|
|
2603
2620
|
return node;
|
|
@@ -2631,8 +2648,8 @@ function apply(key, value, node, sides) {
|
|
|
2631
2648
|
const count = Math.floor(spec.count ?? 1);
|
|
2632
2649
|
const props = count > 1 ? { count } : {};
|
|
2633
2650
|
if ("perform" in spec) {
|
|
2634
|
-
const { perform } = spec;
|
|
2635
|
-
return sb("tapgesture", { ...props, location: true }, { tap: (event) =>
|
|
2651
|
+
const { perform: perform2 } = spec;
|
|
2652
|
+
return sb("tapgesture", { ...props, location: true }, { tap: (event) => perform2({ x: event.x ?? 0, y: event.y ?? 0 }) }, node);
|
|
2636
2653
|
}
|
|
2637
2654
|
return sb("tapgesture", props, { tap: () => spec.action() }, node);
|
|
2638
2655
|
}
|
|
@@ -2852,6 +2869,8 @@ function apply(key, value, node, sides) {
|
|
|
2852
2869
|
}
|
|
2853
2870
|
case "scrollDisabled":
|
|
2854
2871
|
return styled({ scrollDisabled: value === true }, node);
|
|
2872
|
+
case "textSelection":
|
|
2873
|
+
return styled({ textSelection: value === "enabled" }, node);
|
|
2855
2874
|
case "contentTransition": {
|
|
2856
2875
|
if (value === "identity") return node;
|
|
2857
2876
|
if (typeof value === "object") {
|
|
@@ -3574,6 +3593,848 @@ function useAudioRecorder(options = {}) {
|
|
|
3574
3593
|
);
|
|
3575
3594
|
return { available: host !== void 0, status, time, levels, error, record, pause, resume, stop, cancel, discard, trim };
|
|
3576
3595
|
}
|
|
3596
|
+
let nextId$1 = 1;
|
|
3597
|
+
const newId = () => nextId$1++;
|
|
3598
|
+
const hostNow$1 = () => browserEnvironment.audioEngine;
|
|
3599
|
+
function unsupported() {
|
|
3600
|
+
return new Error("unsupported: this page or app has no audio engine");
|
|
3601
|
+
}
|
|
3602
|
+
function perform$1(command2) {
|
|
3603
|
+
const host = hostNow$1();
|
|
3604
|
+
if (!host) return Promise.reject(unsupported());
|
|
3605
|
+
return host.perform(command2);
|
|
3606
|
+
}
|
|
3607
|
+
class AVAudioTime {
|
|
3608
|
+
spec;
|
|
3609
|
+
constructor(sampleTime, atRate) {
|
|
3610
|
+
this.spec = { sampleTime: Math.round(sampleTime), sampleRate: atRate };
|
|
3611
|
+
}
|
|
3612
|
+
static secondsFromNow(seconds) {
|
|
3613
|
+
const time = Object.create(AVAudioTime.prototype);
|
|
3614
|
+
time.spec = { delay: Math.max(0, seconds) };
|
|
3615
|
+
return time;
|
|
3616
|
+
}
|
|
3617
|
+
get isSampleTimeValid() {
|
|
3618
|
+
return "sampleTime" in this.spec;
|
|
3619
|
+
}
|
|
3620
|
+
get sampleTime() {
|
|
3621
|
+
return "sampleTime" in this.spec ? this.spec.sampleTime : 0;
|
|
3622
|
+
}
|
|
3623
|
+
get sampleRate() {
|
|
3624
|
+
return "sampleTime" in this.spec ? this.spec.sampleRate : 0;
|
|
3625
|
+
}
|
|
3626
|
+
/** The time in seconds: a sample time's place on its timeline, or the delay. */
|
|
3627
|
+
get seconds() {
|
|
3628
|
+
return "sampleTime" in this.spec ? this.spec.sampleTime / this.spec.sampleRate : this.spec.delay;
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
const released = typeof FinalizationRegistry !== "undefined" ? new FinalizationRegistry((id) => void perform$1({ action: "release", id }).catch(() => void 0)) : null;
|
|
3632
|
+
class AVAudioFile {
|
|
3633
|
+
constructor(id, url, length, processingFormat) {
|
|
3634
|
+
this.id = id;
|
|
3635
|
+
this.url = url;
|
|
3636
|
+
this.length = length;
|
|
3637
|
+
this.processingFormat = processingFormat;
|
|
3638
|
+
released?.register(this, id, this);
|
|
3639
|
+
}
|
|
3640
|
+
id;
|
|
3641
|
+
url;
|
|
3642
|
+
length;
|
|
3643
|
+
processingFormat;
|
|
3644
|
+
/** Where the next `read` starts, in frames. */
|
|
3645
|
+
framePosition = 0;
|
|
3646
|
+
/** Opens a file for reading; rejects with `notFound: …` or `decode: …`. */
|
|
3647
|
+
static async open(url) {
|
|
3648
|
+
const id = newId();
|
|
3649
|
+
const info = await perform$1({ action: "openFile", file: id, url });
|
|
3650
|
+
return new AVAudioFile(id, url, info.length, { sampleRate: info.sampleRate, channelCount: info.channelCount });
|
|
3651
|
+
}
|
|
3652
|
+
/** Seconds. */
|
|
3653
|
+
get duration() {
|
|
3654
|
+
return this.length / this.processingFormat.sampleRate;
|
|
3655
|
+
}
|
|
3656
|
+
/** Reads frames from `framePosition` into `buffer` (up to its capacity, or `frameCount`) and moves past them. */
|
|
3657
|
+
async read(into, frameCount) {
|
|
3658
|
+
const frames = Math.max(0, Math.min(frameCount ?? into.frameCapacity, into.frameCapacity, this.length - this.framePosition));
|
|
3659
|
+
const info = await perform$1({ action: "readFile", file: this.id, buffer: into.id, start: this.framePosition, frames });
|
|
3660
|
+
into.frameLength = info.frameLength;
|
|
3661
|
+
into.format = { sampleRate: info.sampleRate, channelCount: info.channelCount };
|
|
3662
|
+
this.framePosition += info.frameLength;
|
|
3663
|
+
}
|
|
3664
|
+
/** Lets the host free the decoded audio (what a scheduled segment still plays keeps playing). */
|
|
3665
|
+
close() {
|
|
3666
|
+
released?.unregister(this);
|
|
3667
|
+
void perform$1({ action: "release", id: this.id }).catch(() => void 0);
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
class AVAudioPCMBuffer {
|
|
3671
|
+
constructor(format, frameCapacity) {
|
|
3672
|
+
this.format = format;
|
|
3673
|
+
this.frameCapacity = frameCapacity;
|
|
3674
|
+
released?.register(this, this.id, this);
|
|
3675
|
+
}
|
|
3676
|
+
format;
|
|
3677
|
+
frameCapacity;
|
|
3678
|
+
id = newId();
|
|
3679
|
+
frameLength = 0;
|
|
3680
|
+
/** Seconds of audio it holds. */
|
|
3681
|
+
get duration() {
|
|
3682
|
+
return this.frameLength / this.format.sampleRate;
|
|
3683
|
+
}
|
|
3684
|
+
/** Fills the buffer with the app's samples, one array per channel (up to its capacity). */
|
|
3685
|
+
async write(channels) {
|
|
3686
|
+
const frames = Math.min(this.frameCapacity, ...channels.map((data) => data.length));
|
|
3687
|
+
await perform$1({
|
|
3688
|
+
action: "writeBuffer",
|
|
3689
|
+
buffer: this.id,
|
|
3690
|
+
sampleRate: this.format.sampleRate,
|
|
3691
|
+
channels: channels.map((data) => data.length === frames ? data : data.subarray(0, frames))
|
|
3692
|
+
});
|
|
3693
|
+
this.frameLength = frames;
|
|
3694
|
+
this.format = { sampleRate: this.format.sampleRate, channelCount: channels.length };
|
|
3695
|
+
}
|
|
3696
|
+
/** Lets the host free the audio. */
|
|
3697
|
+
close() {
|
|
3698
|
+
released?.unregister(this);
|
|
3699
|
+
void perform$1({ action: "release", id: this.id }).catch(() => void 0);
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
class AVAudioNode {
|
|
3703
|
+
id = newId();
|
|
3704
|
+
/** The engine it is attached to, or null. */
|
|
3705
|
+
engine = null;
|
|
3706
|
+
values = {};
|
|
3707
|
+
get(key, fallback) {
|
|
3708
|
+
return this.values[key] ?? fallback;
|
|
3709
|
+
}
|
|
3710
|
+
set(key, value) {
|
|
3711
|
+
this.values[key] = value;
|
|
3712
|
+
this.engine?.fire({ action: "set", node: this.id, values: { [key]: value } });
|
|
3713
|
+
}
|
|
3714
|
+
/** The properties to attach it with. */
|
|
3715
|
+
attachValues() {
|
|
3716
|
+
return { ...this.values };
|
|
3717
|
+
}
|
|
3718
|
+
/** How many EQ bands it has (only an EQ has any). */
|
|
3719
|
+
get bandCount() {
|
|
3720
|
+
return void 0;
|
|
3721
|
+
}
|
|
3722
|
+
/** What its engine must be for a call to reach it. */
|
|
3723
|
+
attached() {
|
|
3724
|
+
if (!this.engine) throw new Error("notAttached: attach the node to an engine first");
|
|
3725
|
+
return this.engine;
|
|
3726
|
+
}
|
|
3727
|
+
/** Its output's level now (0 silent … 1 at 0 dB, from -50 dB), as a tap on it would measure; 0 when not attached. */
|
|
3728
|
+
async meter() {
|
|
3729
|
+
if (!this.engine) return 0;
|
|
3730
|
+
const result = await this.engine.send({ action: "meter", node: this.id });
|
|
3731
|
+
return typeof result?.level === "number" ? result.level : 0;
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
class MixingNode extends AVAudioNode {
|
|
3735
|
+
get volume() {
|
|
3736
|
+
return this.get("volume", 1);
|
|
3737
|
+
}
|
|
3738
|
+
set volume(value) {
|
|
3739
|
+
this.set("volume", value);
|
|
3740
|
+
}
|
|
3741
|
+
get pan() {
|
|
3742
|
+
return this.get("pan", 0);
|
|
3743
|
+
}
|
|
3744
|
+
set pan(value) {
|
|
3745
|
+
this.set("pan", value);
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
class AVAudioMixerNode extends MixingNode {
|
|
3749
|
+
kind = "mixer";
|
|
3750
|
+
get outputVolume() {
|
|
3751
|
+
return this.get("outputVolume", 1);
|
|
3752
|
+
}
|
|
3753
|
+
set outputVolume(value) {
|
|
3754
|
+
this.set("outputVolume", value);
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
class AVAudioOutputNode extends AVAudioNode {
|
|
3758
|
+
kind = null;
|
|
3759
|
+
}
|
|
3760
|
+
class AVAudioInputNode extends MixingNode {
|
|
3761
|
+
kind = null;
|
|
3762
|
+
}
|
|
3763
|
+
class AVAudioPlayerNode extends MixingNode {
|
|
3764
|
+
kind = "player";
|
|
3765
|
+
playing = false;
|
|
3766
|
+
get isPlaying() {
|
|
3767
|
+
return this.playing && this.engine?.isRunning === true;
|
|
3768
|
+
}
|
|
3769
|
+
scheduleFile(file, at = null, completionHandler) {
|
|
3770
|
+
return this.schedule({ action: "scheduleFile", node: this.id, file: file.id, ...at ? { at: at.spec } : {} }, completionHandler);
|
|
3771
|
+
}
|
|
3772
|
+
scheduleSegment(file, startingFrame, frameCount, at = null, completionHandler) {
|
|
3773
|
+
return this.schedule(
|
|
3774
|
+
{
|
|
3775
|
+
action: "scheduleFile",
|
|
3776
|
+
node: this.id,
|
|
3777
|
+
file: file.id,
|
|
3778
|
+
start: Math.max(0, Math.floor(startingFrame)),
|
|
3779
|
+
frames: Math.max(0, Math.floor(frameCount)),
|
|
3780
|
+
...at ? { at: at.spec } : {}
|
|
3781
|
+
},
|
|
3782
|
+
completionHandler
|
|
3783
|
+
);
|
|
3784
|
+
}
|
|
3785
|
+
scheduleBuffer(buffer, at = null, options = {}, completionHandler) {
|
|
3786
|
+
const command2 = {
|
|
3787
|
+
action: "scheduleBuffer",
|
|
3788
|
+
node: this.id,
|
|
3789
|
+
buffer: buffer.id,
|
|
3790
|
+
...at ? { at: at.spec } : {}
|
|
3791
|
+
};
|
|
3792
|
+
if (options.loops) command2.loops = true;
|
|
3793
|
+
if (options.interruptsAtLoop) command2.interruptsAtLoop = true;
|
|
3794
|
+
else if (options.interrupts) command2.interrupts = true;
|
|
3795
|
+
return this.schedule(command2, completionHandler);
|
|
3796
|
+
}
|
|
3797
|
+
schedule(command2, completionHandler) {
|
|
3798
|
+
const engine = this.attached();
|
|
3799
|
+
return engine.send(command2).then(() => completionHandler?.());
|
|
3800
|
+
}
|
|
3801
|
+
/** Starts (or resumes) the timeline, now or at `at`; the engine must be running. */
|
|
3802
|
+
play(at = null) {
|
|
3803
|
+
const engine = this.attached();
|
|
3804
|
+
this.playing = true;
|
|
3805
|
+
engine.fire({ action: "play", node: this.id, ...at ? { at: at.spec } : {} });
|
|
3806
|
+
}
|
|
3807
|
+
/** Holds the timeline where it is; `play()` goes on from there. */
|
|
3808
|
+
pause() {
|
|
3809
|
+
this.playing = false;
|
|
3810
|
+
this.engine?.fire({ action: "pausePlayer", node: this.id });
|
|
3811
|
+
}
|
|
3812
|
+
/** Drops everything scheduled (each completion called) and returns the timeline to 0. */
|
|
3813
|
+
stop() {
|
|
3814
|
+
this.playing = false;
|
|
3815
|
+
this.engine?.fire({ action: "stopPlayer", node: this.id });
|
|
3816
|
+
}
|
|
3817
|
+
/** Where the timeline is now (`playerTime(forNodeTime: lastRenderTime)`), or null before it plays. */
|
|
3818
|
+
async playerTime() {
|
|
3819
|
+
const time = await this.attached().send({ action: "playerTime", node: this.id });
|
|
3820
|
+
return time && typeof time.sampleTime === "number" ? new AVAudioTime(time.sampleTime, time.sampleRate) : null;
|
|
3821
|
+
}
|
|
3822
|
+
/** Called by the engine when it stops: the player stops with it. */
|
|
3823
|
+
engineStopped() {
|
|
3824
|
+
this.playing = false;
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
const NOTE_CLASSES = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
|
|
3828
|
+
function noteInName(url) {
|
|
3829
|
+
const name = decodeURIComponent(url.split(/[/\\]/).pop() ?? "").replace(/\.[^.]*$/, "");
|
|
3830
|
+
const named = [...name.matchAll(/(?:^|[^A-Za-z])([A-Ga-g])([#♯]|b|♭)?(-?\d)(?!\d)/g)].pop();
|
|
3831
|
+
if (named) {
|
|
3832
|
+
const pitchClass = NOTE_CLASSES[named[1].toLowerCase()];
|
|
3833
|
+
const accidental = named[2] === "#" || named[2] === "♯" ? 1 : named[2] ? -1 : 0;
|
|
3834
|
+
const note = (Number(named[3]) + 1) * 12 + pitchClass + accidental;
|
|
3835
|
+
return note >= 0 && note <= 127 ? note : null;
|
|
3836
|
+
}
|
|
3837
|
+
const numbered = [...name.matchAll(/(?:^|\D)(\d{1,3})(?!\d)/g)].pop();
|
|
3838
|
+
if (numbered) {
|
|
3839
|
+
const note = Number(numbered[1]);
|
|
3840
|
+
return note <= 127 ? note : null;
|
|
3841
|
+
}
|
|
3842
|
+
return null;
|
|
3843
|
+
}
|
|
3844
|
+
function samplerZones(samples) {
|
|
3845
|
+
const sorted = [...samples].sort((a, b) => a.root - b.root).filter((sample, i, all) => i === 0 || sample.root !== all[i - 1].root);
|
|
3846
|
+
return sorted.map((sample, i) => {
|
|
3847
|
+
const below = sorted[i - 1];
|
|
3848
|
+
const above = sorted[i + 1];
|
|
3849
|
+
return {
|
|
3850
|
+
file: sample.file,
|
|
3851
|
+
root: sample.root,
|
|
3852
|
+
low: below ? Math.floor((below.root + sample.root) / 2) + 1 : 0,
|
|
3853
|
+
high: above ? Math.floor((sample.root + above.root) / 2) : 127
|
|
3854
|
+
};
|
|
3855
|
+
});
|
|
3856
|
+
}
|
|
3857
|
+
class AVAudioUnitSampler extends MixingNode {
|
|
3858
|
+
kind = "sampler";
|
|
3859
|
+
files = [];
|
|
3860
|
+
/** Decibels, -90 … 12. */
|
|
3861
|
+
get overallGain() {
|
|
3862
|
+
return this.get("overallGain", 0);
|
|
3863
|
+
}
|
|
3864
|
+
set overallGain(value) {
|
|
3865
|
+
this.set("overallGain", value);
|
|
3866
|
+
}
|
|
3867
|
+
/** Cents, -2400 … 2400. */
|
|
3868
|
+
get globalTuning() {
|
|
3869
|
+
return this.get("globalTuning", 0);
|
|
3870
|
+
}
|
|
3871
|
+
set globalTuning(value) {
|
|
3872
|
+
this.set("globalTuning", value);
|
|
3873
|
+
}
|
|
3874
|
+
/** -100 left … 100 right. */
|
|
3875
|
+
get stereoPan() {
|
|
3876
|
+
return this.get("stereoPan", 0);
|
|
3877
|
+
}
|
|
3878
|
+
set stereoPan(value) {
|
|
3879
|
+
this.set("stereoPan", value);
|
|
3880
|
+
}
|
|
3881
|
+
async loadAudioFiles(urls, options = {}) {
|
|
3882
|
+
const engine = this.attached();
|
|
3883
|
+
const roots2 = urls.map((url, i) => options.rootNotes?.[i] ?? noteInName(url) ?? 60);
|
|
3884
|
+
const files = await Promise.all(urls.map((url) => AVAudioFile.open(url)));
|
|
3885
|
+
const zones = samplerZones(files.map((file, i) => ({ file: file.id, root: roots2[i] })));
|
|
3886
|
+
await engine.send({ action: "loadZones", node: this.id, zones, attack: options.attack ?? 2e-3, release: options.release ?? 0.15 });
|
|
3887
|
+
const previous = this.files;
|
|
3888
|
+
this.files = files;
|
|
3889
|
+
for (const file of previous) file.close();
|
|
3890
|
+
}
|
|
3891
|
+
/** Loads an `.aupreset`, a DLS or a SoundFont's first instrument (natively; a page rejects with `unsupported: …`). */
|
|
3892
|
+
async loadInstrument(url) {
|
|
3893
|
+
await this.attached().send({ action: "loadInstrument", node: this.id, url });
|
|
3894
|
+
}
|
|
3895
|
+
/** Loads one instrument of a SoundFont or DLS bank by program and bank (natively; a page rejects with `unsupported: …`). */
|
|
3896
|
+
async loadSoundBankInstrument(url, program, bankMSB = 121, bankLSB = 0) {
|
|
3897
|
+
await this.attached().send({ action: "loadInstrument", node: this.id, url, program, bankMSB, bankLSB });
|
|
3898
|
+
}
|
|
3899
|
+
startNote(note, withVelocity, onChannel = 0) {
|
|
3900
|
+
this.attached().fire({ action: "startNote", node: this.id, note: Math.round(note), velocity: Math.round(withVelocity), channel: onChannel });
|
|
3901
|
+
}
|
|
3902
|
+
stopNote(note, onChannel = 0) {
|
|
3903
|
+
this.attached().fire({ action: "stopNote", node: this.id, note: Math.round(note), channel: onChannel });
|
|
3904
|
+
}
|
|
3905
|
+
/** A MIDI controller: 64 the sustain pedal, 7 volume, 11 expression, 10 pan, 123 all notes off. */
|
|
3906
|
+
sendController(controller, withValue, onChannel = 0) {
|
|
3907
|
+
this.attached().fire({ action: "controller", node: this.id, controller, value: Math.round(withValue), channel: onChannel });
|
|
3908
|
+
}
|
|
3909
|
+
/** The pitch wheel: 0 … 16383, 8192 centered (±2 semitones). */
|
|
3910
|
+
sendPitchBend(pitchbend, onChannel = 0) {
|
|
3911
|
+
this.attached().fire({ action: "pitchBend", node: this.id, value: Math.round(pitchbend), channel: onChannel });
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
class AVAudioUnitEffect extends AVAudioNode {
|
|
3915
|
+
get bypass() {
|
|
3916
|
+
return this.get("bypass", false);
|
|
3917
|
+
}
|
|
3918
|
+
set bypass(value) {
|
|
3919
|
+
this.set("bypass", value);
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
class AVAudioUnitReverb extends AVAudioUnitEffect {
|
|
3923
|
+
kind = "reverb";
|
|
3924
|
+
constructor() {
|
|
3925
|
+
super();
|
|
3926
|
+
this.values["preset"] = "mediumHall";
|
|
3927
|
+
this.values["wetDryMix"] = 100;
|
|
3928
|
+
}
|
|
3929
|
+
get wetDryMix() {
|
|
3930
|
+
return this.get("wetDryMix", 100);
|
|
3931
|
+
}
|
|
3932
|
+
set wetDryMix(value) {
|
|
3933
|
+
this.set("wetDryMix", value);
|
|
3934
|
+
}
|
|
3935
|
+
loadFactoryPreset(preset) {
|
|
3936
|
+
this.values["preset"] = preset;
|
|
3937
|
+
this.engine?.fire({ action: "preset", node: this.id, preset });
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
class AVAudioUnitDelay extends AVAudioUnitEffect {
|
|
3941
|
+
kind = "delay";
|
|
3942
|
+
constructor() {
|
|
3943
|
+
super();
|
|
3944
|
+
Object.assign(this.values, { delayTime: 1, feedback: 50, lowPassCutoff: 15e3, wetDryMix: 100 });
|
|
3945
|
+
}
|
|
3946
|
+
get delayTime() {
|
|
3947
|
+
return this.get("delayTime", 1);
|
|
3948
|
+
}
|
|
3949
|
+
set delayTime(value) {
|
|
3950
|
+
this.set("delayTime", value);
|
|
3951
|
+
}
|
|
3952
|
+
get feedback() {
|
|
3953
|
+
return this.get("feedback", 50);
|
|
3954
|
+
}
|
|
3955
|
+
set feedback(value) {
|
|
3956
|
+
this.set("feedback", value);
|
|
3957
|
+
}
|
|
3958
|
+
get lowPassCutoff() {
|
|
3959
|
+
return this.get("lowPassCutoff", 15e3);
|
|
3960
|
+
}
|
|
3961
|
+
set lowPassCutoff(value) {
|
|
3962
|
+
this.set("lowPassCutoff", value);
|
|
3963
|
+
}
|
|
3964
|
+
get wetDryMix() {
|
|
3965
|
+
return this.get("wetDryMix", 100);
|
|
3966
|
+
}
|
|
3967
|
+
set wetDryMix(value) {
|
|
3968
|
+
this.set("wetDryMix", value);
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
class AVAudioUnitDistortion extends AVAudioUnitEffect {
|
|
3972
|
+
kind = "distortion";
|
|
3973
|
+
constructor() {
|
|
3974
|
+
super();
|
|
3975
|
+
Object.assign(this.values, { preGain: -6, wetDryMix: 50 });
|
|
3976
|
+
}
|
|
3977
|
+
get preGain() {
|
|
3978
|
+
return this.get("preGain", -6);
|
|
3979
|
+
}
|
|
3980
|
+
set preGain(value) {
|
|
3981
|
+
this.set("preGain", value);
|
|
3982
|
+
}
|
|
3983
|
+
get wetDryMix() {
|
|
3984
|
+
return this.get("wetDryMix", 50);
|
|
3985
|
+
}
|
|
3986
|
+
set wetDryMix(value) {
|
|
3987
|
+
this.set("wetDryMix", value);
|
|
3988
|
+
}
|
|
3989
|
+
loadFactoryPreset(preset) {
|
|
3990
|
+
this.values["preset"] = preset;
|
|
3991
|
+
this.engine?.fire({ action: "preset", node: this.id, preset });
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
class AVAudioUnitEQFilterParameters {
|
|
3995
|
+
constructor(eq, index) {
|
|
3996
|
+
this.eq = eq;
|
|
3997
|
+
this.index = index;
|
|
3998
|
+
}
|
|
3999
|
+
eq;
|
|
4000
|
+
index;
|
|
4001
|
+
key(field) {
|
|
4002
|
+
return `bands.${this.index}.${field}`;
|
|
4003
|
+
}
|
|
4004
|
+
get filterType() {
|
|
4005
|
+
return this.eq.band(this.key("filterType"), "parametric");
|
|
4006
|
+
}
|
|
4007
|
+
set filterType(value) {
|
|
4008
|
+
this.eq.setBand(this.key("filterType"), value);
|
|
4009
|
+
}
|
|
4010
|
+
/** Hz. */
|
|
4011
|
+
get frequency() {
|
|
4012
|
+
return this.eq.band(this.key("frequency"), 1e3);
|
|
4013
|
+
}
|
|
4014
|
+
set frequency(value) {
|
|
4015
|
+
this.eq.setBand(this.key("frequency"), value);
|
|
4016
|
+
}
|
|
4017
|
+
/** Octaves (0.05 … 5). */
|
|
4018
|
+
get bandwidth() {
|
|
4019
|
+
return this.eq.band(this.key("bandwidth"), 0.5);
|
|
4020
|
+
}
|
|
4021
|
+
set bandwidth(value) {
|
|
4022
|
+
this.eq.setBand(this.key("bandwidth"), value);
|
|
4023
|
+
}
|
|
4024
|
+
/** Decibels (-96 … 24). */
|
|
4025
|
+
get gain() {
|
|
4026
|
+
return this.eq.band(this.key("gain"), 0);
|
|
4027
|
+
}
|
|
4028
|
+
set gain(value) {
|
|
4029
|
+
this.eq.setBand(this.key("gain"), value);
|
|
4030
|
+
}
|
|
4031
|
+
get bypass() {
|
|
4032
|
+
return this.eq.band(this.key("bypass"), true);
|
|
4033
|
+
}
|
|
4034
|
+
set bypass(value) {
|
|
4035
|
+
this.eq.setBand(this.key("bypass"), value);
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
class AVAudioUnitEQ extends AVAudioUnitEffect {
|
|
4039
|
+
kind = "eq";
|
|
4040
|
+
bands;
|
|
4041
|
+
constructor(numberOfBands = 1) {
|
|
4042
|
+
super();
|
|
4043
|
+
const count = Math.max(1, Math.min(32, Math.floor(numberOfBands)));
|
|
4044
|
+
this.bands = Array.from({ length: count }, (_, i) => new AVAudioUnitEQFilterParameters(this, i));
|
|
4045
|
+
for (let i = 0; i < count; i += 1) {
|
|
4046
|
+
Object.assign(this.values, {
|
|
4047
|
+
[`bands.${i}.filterType`]: "parametric",
|
|
4048
|
+
[`bands.${i}.frequency`]: 1e3,
|
|
4049
|
+
[`bands.${i}.bandwidth`]: 0.5,
|
|
4050
|
+
[`bands.${i}.gain`]: 0,
|
|
4051
|
+
[`bands.${i}.bypass`]: true
|
|
4052
|
+
});
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
get bandCount() {
|
|
4056
|
+
return this.bands.length;
|
|
4057
|
+
}
|
|
4058
|
+
get globalGain() {
|
|
4059
|
+
return this.get("globalGain", 0);
|
|
4060
|
+
}
|
|
4061
|
+
set globalGain(value) {
|
|
4062
|
+
this.set("globalGain", value);
|
|
4063
|
+
}
|
|
4064
|
+
/** A band's field (for `AVAudioUnitEQFilterParameters`). */
|
|
4065
|
+
band(key, fallback) {
|
|
4066
|
+
return this.values[key] ?? fallback;
|
|
4067
|
+
}
|
|
4068
|
+
setBand(key, value) {
|
|
4069
|
+
this.set(key, value);
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
class AVAudioUnitTimePitch extends AVAudioUnitEffect {
|
|
4073
|
+
kind = "timePitch";
|
|
4074
|
+
get pitch() {
|
|
4075
|
+
return this.get("pitch", 0);
|
|
4076
|
+
}
|
|
4077
|
+
set pitch(value) {
|
|
4078
|
+
this.set("pitch", value);
|
|
4079
|
+
}
|
|
4080
|
+
get rate() {
|
|
4081
|
+
return this.get("rate", 1);
|
|
4082
|
+
}
|
|
4083
|
+
set rate(value) {
|
|
4084
|
+
this.set("rate", value);
|
|
4085
|
+
}
|
|
4086
|
+
get overlap() {
|
|
4087
|
+
return this.get("overlap", 8);
|
|
4088
|
+
}
|
|
4089
|
+
set overlap(value) {
|
|
4090
|
+
this.set("overlap", value);
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
const engines = typeof FinalizationRegistry !== "undefined" ? new FinalizationRegistry((id) => void perform$1({ action: "destroy", engine: id }).catch(() => void 0)) : null;
|
|
4094
|
+
class AVAudioEngine {
|
|
4095
|
+
id = newId();
|
|
4096
|
+
mainMixerNode = new AVAudioMixerNode();
|
|
4097
|
+
outputNode = new AVAudioOutputNode();
|
|
4098
|
+
inputNode = new AVAudioInputNode();
|
|
4099
|
+
attachedNodes = /* @__PURE__ */ new Set();
|
|
4100
|
+
/** Where failures of calls that answer nothing go (a console warning by default). */
|
|
4101
|
+
onError = (error) => console.warn(`AVAudioEngine: ${error.message}`);
|
|
4102
|
+
running = false;
|
|
4103
|
+
disposed = false;
|
|
4104
|
+
/** Whether this page or app has an audio engine. */
|
|
4105
|
+
static get isAvailable() {
|
|
4106
|
+
return hostNow$1() !== void 0;
|
|
4107
|
+
}
|
|
4108
|
+
constructor() {
|
|
4109
|
+
for (const node of [this.mainMixerNode, this.outputNode, this.inputNode]) {
|
|
4110
|
+
node.engine = this;
|
|
4111
|
+
this.attachedNodes.add(node);
|
|
4112
|
+
}
|
|
4113
|
+
this.fire({ action: "create", engine: this.id, mixer: this.mainMixerNode.id, output: this.outputNode.id, input: this.inputNode.id });
|
|
4114
|
+
engines?.register(this, this.id, this);
|
|
4115
|
+
}
|
|
4116
|
+
get isRunning() {
|
|
4117
|
+
return this.running;
|
|
4118
|
+
}
|
|
4119
|
+
/** Sends a command and answers its result. */
|
|
4120
|
+
send(command2) {
|
|
4121
|
+
if (this.disposed) return Promise.reject(new Error("disposed: the engine was disposed"));
|
|
4122
|
+
return perform$1(command2);
|
|
4123
|
+
}
|
|
4124
|
+
/** Sends a command that answers nothing; a failure goes to `onError`. */
|
|
4125
|
+
fire(command2) {
|
|
4126
|
+
this.send(command2).catch((error) => this.onError(error instanceof Error ? error : new Error(String(error))));
|
|
4127
|
+
}
|
|
4128
|
+
attach(node) {
|
|
4129
|
+
if (node.engine === this) return;
|
|
4130
|
+
if (node.engine) throw new Error("attached: the node is attached to another engine");
|
|
4131
|
+
if (!node.kind) throw new Error("unsupported: an engine makes its own mixer, output and input");
|
|
4132
|
+
node.engine = this;
|
|
4133
|
+
this.attachedNodes.add(node);
|
|
4134
|
+
const bands = node.bandCount;
|
|
4135
|
+
this.fire({ action: "attach", engine: this.id, node: node.id, kind: node.kind, values: node.attachValues(), ...bands !== void 0 ? { bands } : {} });
|
|
4136
|
+
}
|
|
4137
|
+
detach(node) {
|
|
4138
|
+
if (node.engine !== this || !node.kind) return;
|
|
4139
|
+
this.fire({ action: "detach", engine: this.id, node: node.id });
|
|
4140
|
+
if (node instanceof AVAudioPlayerNode) node.engineStopped();
|
|
4141
|
+
node.engine = null;
|
|
4142
|
+
this.attachedNodes.delete(node);
|
|
4143
|
+
}
|
|
4144
|
+
/**
|
|
4145
|
+
* Connects `from`'s output to `to` (an array: to each of them), replacing
|
|
4146
|
+
* where it went before; a node that is not a mixer takes one input, so
|
|
4147
|
+
* what was connected into it is let go. `format` is accepted for Swift's
|
|
4148
|
+
* shape: the host picks one that fits both ends.
|
|
4149
|
+
*/
|
|
4150
|
+
connect(from, to, format = null) {
|
|
4151
|
+
const destinations = Array.isArray(to) ? to : [to];
|
|
4152
|
+
for (const node of [from, ...destinations]) {
|
|
4153
|
+
if (node.engine !== this) throw new Error("notAttached: attach both nodes to the engine before connecting them");
|
|
4154
|
+
}
|
|
4155
|
+
this.fire({ action: "connect", engine: this.id, from: from.id, to: destinations.map((node) => node.id) });
|
|
4156
|
+
}
|
|
4157
|
+
disconnectNodeOutput(node) {
|
|
4158
|
+
this.fire({ action: "disconnectOutput", engine: this.id, node: node.id });
|
|
4159
|
+
}
|
|
4160
|
+
disconnectNodeInput(node) {
|
|
4161
|
+
this.fire({ action: "disconnectInput", engine: this.id, node: node.id });
|
|
4162
|
+
}
|
|
4163
|
+
prepare() {
|
|
4164
|
+
this.fire({ action: "prepare", engine: this.id });
|
|
4165
|
+
}
|
|
4166
|
+
/** Starts rendering; with the input connected, asks for the microphone (rejecting `permission: …` when refused). */
|
|
4167
|
+
async start() {
|
|
4168
|
+
await this.send({ action: "start", engine: this.id });
|
|
4169
|
+
this.running = true;
|
|
4170
|
+
}
|
|
4171
|
+
/** Pauses rendering; players hold their place and play on at `start()`. */
|
|
4172
|
+
pause() {
|
|
4173
|
+
this.running = false;
|
|
4174
|
+
this.fire({ action: "pause", engine: this.id });
|
|
4175
|
+
}
|
|
4176
|
+
/** Stops rendering; players stop and drop what they had scheduled. */
|
|
4177
|
+
stop() {
|
|
4178
|
+
this.running = false;
|
|
4179
|
+
for (const node of this.attachedNodes) if (node instanceof AVAudioPlayerNode) node.engineStopped();
|
|
4180
|
+
this.fire({ action: "stop", engine: this.id });
|
|
4181
|
+
}
|
|
4182
|
+
/** Silences every node (players stop, notes end) without stopping the engine. */
|
|
4183
|
+
reset() {
|
|
4184
|
+
for (const node of this.attachedNodes) if (node instanceof AVAudioPlayerNode) node.engineStopped();
|
|
4185
|
+
this.fire({ action: "reset", engine: this.id });
|
|
4186
|
+
}
|
|
4187
|
+
/** Stops the engine and frees it on the host; it cannot be used after. */
|
|
4188
|
+
dispose() {
|
|
4189
|
+
if (this.disposed) return;
|
|
4190
|
+
engines?.unregister(this);
|
|
4191
|
+
void perform$1({ action: "destroy", engine: this.id }).catch(() => void 0);
|
|
4192
|
+
this.disposed = true;
|
|
4193
|
+
this.running = false;
|
|
4194
|
+
for (const node of this.attachedNodes) node.engine = null;
|
|
4195
|
+
this.attachedNodes.clear();
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
function useAudioEngine(build) {
|
|
4199
|
+
const [engine] = useState(() => {
|
|
4200
|
+
if (!AVAudioEngine.isAvailable) return null;
|
|
4201
|
+
const made = new AVAudioEngine();
|
|
4202
|
+
build?.(made);
|
|
4203
|
+
return made;
|
|
4204
|
+
});
|
|
4205
|
+
useEffect(() => () => engine?.dispose(), [engine]);
|
|
4206
|
+
return engine;
|
|
4207
|
+
}
|
|
4208
|
+
let nextId = 1;
|
|
4209
|
+
const hostNow = () => browserEnvironment.languageModel;
|
|
4210
|
+
function perform(command2) {
|
|
4211
|
+
const host = hostNow();
|
|
4212
|
+
if (!host) return Promise.reject(new GenerationError("assetsUnavailable", "This page or app has no language model."));
|
|
4213
|
+
return host.perform(command2).catch((error) => {
|
|
4214
|
+
throw GenerationError.from(error);
|
|
4215
|
+
});
|
|
4216
|
+
}
|
|
4217
|
+
const KINDS = {
|
|
4218
|
+
unavailable: "assetsUnavailable",
|
|
4219
|
+
context: "exceededContextWindowSize",
|
|
4220
|
+
guardrail: "guardrailViolation",
|
|
4221
|
+
language: "unsupportedLanguageOrLocale",
|
|
4222
|
+
concurrent: "concurrentRequests",
|
|
4223
|
+
rateLimited: "rateLimited",
|
|
4224
|
+
refusal: "refusal"
|
|
4225
|
+
};
|
|
4226
|
+
class GenerationError extends Error {
|
|
4227
|
+
constructor(kind, message) {
|
|
4228
|
+
super(message);
|
|
4229
|
+
this.kind = kind;
|
|
4230
|
+
this.name = "GenerationError";
|
|
4231
|
+
}
|
|
4232
|
+
kind;
|
|
4233
|
+
/** The error a host's tagged message (`context: …`) stands for. */
|
|
4234
|
+
static from(error) {
|
|
4235
|
+
if (error instanceof GenerationError) return error;
|
|
4236
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4237
|
+
const match = /^(\w+): ?([\s\S]*)$/.exec(message);
|
|
4238
|
+
const kind = match ? KINDS[match[1]] : void 0;
|
|
4239
|
+
return kind ? new GenerationError(kind, match[2]) : new GenerationError("unknown", message);
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
class SystemLanguageModel {
|
|
4243
|
+
static default = new SystemLanguageModel();
|
|
4244
|
+
constructor() {
|
|
4245
|
+
}
|
|
4246
|
+
/** Views showing the availability, asked again when a download starts or the model goes. */
|
|
4247
|
+
listeners = /* @__PURE__ */ new Set();
|
|
4248
|
+
/** Whether the model can answer now, or why not. */
|
|
4249
|
+
async availability() {
|
|
4250
|
+
if (!hostNow()) return { status: "unavailable", reason: "deviceNotEligible" };
|
|
4251
|
+
try {
|
|
4252
|
+
return await perform({ action: "availability" });
|
|
4253
|
+
} catch {
|
|
4254
|
+
return { status: "unavailable", reason: "deviceNotEligible" };
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
async isAvailable() {
|
|
4258
|
+
return (await this.availability()).status === "available";
|
|
4259
|
+
}
|
|
4260
|
+
/**
|
|
4261
|
+
* Starts fetching a model the app fetches itself (availability's
|
|
4262
|
+
* `download`), which the viewer asked for: show its size first. Not part
|
|
4263
|
+
* of FoundationModels, whose model the system manages.
|
|
4264
|
+
*/
|
|
4265
|
+
async download() {
|
|
4266
|
+
await perform({ action: "download" });
|
|
4267
|
+
this.changed();
|
|
4268
|
+
}
|
|
4269
|
+
/**
|
|
4270
|
+
* Makes the model with this `id` (one of availability's `models`) the one
|
|
4271
|
+
* that answers, from the next session on; remembered across launches.
|
|
4272
|
+
*/
|
|
4273
|
+
async selectModel(id) {
|
|
4274
|
+
await perform({ action: "select", id });
|
|
4275
|
+
this.changed();
|
|
4276
|
+
}
|
|
4277
|
+
/** Stops such a download, or removes the model from the device to free its space. */
|
|
4278
|
+
async deleteDownload() {
|
|
4279
|
+
await perform({ action: "deleteDownload" });
|
|
4280
|
+
this.changed();
|
|
4281
|
+
}
|
|
4282
|
+
/** @internal `useSystemLanguageModel` hears downloads start and models go. */
|
|
4283
|
+
listen(listener) {
|
|
4284
|
+
this.listeners.add(listener);
|
|
4285
|
+
return () => this.listeners.delete(listener);
|
|
4286
|
+
}
|
|
4287
|
+
changed() {
|
|
4288
|
+
for (const listener of this.listeners) listener();
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
function useSystemLanguageModel(model = SystemLanguageModel.default) {
|
|
4292
|
+
const [availability, setAvailability] = useState(null);
|
|
4293
|
+
useEffect(() => {
|
|
4294
|
+
let live = true;
|
|
4295
|
+
let timer;
|
|
4296
|
+
const check = () => {
|
|
4297
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
4298
|
+
timer = void 0;
|
|
4299
|
+
void model.availability().then((next) => {
|
|
4300
|
+
if (!live) return;
|
|
4301
|
+
setAvailability((current) => JSON.stringify(current) === JSON.stringify(next) ? current : next);
|
|
4302
|
+
if (timer !== void 0) return;
|
|
4303
|
+
if (next.download?.progress !== void 0) timer = setTimeout(check, 1e3);
|
|
4304
|
+
else if (next.status === "unavailable" && next.reason === "modelNotReady" && !next.download) timer = setTimeout(check, 5e3);
|
|
4305
|
+
});
|
|
4306
|
+
};
|
|
4307
|
+
check();
|
|
4308
|
+
const stop = model.listen(check);
|
|
4309
|
+
return () => {
|
|
4310
|
+
live = false;
|
|
4311
|
+
stop();
|
|
4312
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
4313
|
+
};
|
|
4314
|
+
}, [model]);
|
|
4315
|
+
return availability;
|
|
4316
|
+
}
|
|
4317
|
+
class ResponseStream {
|
|
4318
|
+
/** @internal Made by `LanguageModelSession.streamResponse`. */
|
|
4319
|
+
constructor(session, opened, prompt, options, ended) {
|
|
4320
|
+
this.ended = ended;
|
|
4321
|
+
this.started = opened.then(
|
|
4322
|
+
() => perform({ action: "stream", session: session.id, stream: this.id, prompt, ...options ? { options } : {} })
|
|
4323
|
+
);
|
|
4324
|
+
this.started.catch(() => this.end());
|
|
4325
|
+
}
|
|
4326
|
+
ended;
|
|
4327
|
+
id = nextId++;
|
|
4328
|
+
started;
|
|
4329
|
+
finished = false;
|
|
4330
|
+
last = { content: "" };
|
|
4331
|
+
async *[Symbol.asyncIterator]() {
|
|
4332
|
+
try {
|
|
4333
|
+
await this.started;
|
|
4334
|
+
while (!this.finished) {
|
|
4335
|
+
const { content, done } = await perform({ action: "next", stream: this.id });
|
|
4336
|
+
if (done) this.end();
|
|
4337
|
+
if (content !== this.last.content) {
|
|
4338
|
+
this.last = { content };
|
|
4339
|
+
yield this.last;
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
4342
|
+
} catch (error) {
|
|
4343
|
+
this.end();
|
|
4344
|
+
throw error;
|
|
4345
|
+
} finally {
|
|
4346
|
+
this.cancel();
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
/** The whole response, once it has ended. */
|
|
4350
|
+
async collect() {
|
|
4351
|
+
for await (const snapshot of this) this.last = snapshot;
|
|
4352
|
+
return this.last;
|
|
4353
|
+
}
|
|
4354
|
+
/** Stops the response; the iteration ends with what came so far. */
|
|
4355
|
+
cancel() {
|
|
4356
|
+
if (this.finished) return;
|
|
4357
|
+
this.end();
|
|
4358
|
+
void this.started.then(() => perform({ action: "cancel", stream: this.id })).catch(() => {
|
|
4359
|
+
});
|
|
4360
|
+
}
|
|
4361
|
+
end() {
|
|
4362
|
+
if (this.finished) return;
|
|
4363
|
+
this.finished = true;
|
|
4364
|
+
this.ended();
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
class LanguageModelSession {
|
|
4368
|
+
id = nextId++;
|
|
4369
|
+
instructions;
|
|
4370
|
+
tools;
|
|
4371
|
+
opened;
|
|
4372
|
+
responses = 0;
|
|
4373
|
+
closed = false;
|
|
4374
|
+
constructor(options = {}) {
|
|
4375
|
+
this.instructions = options.instructions;
|
|
4376
|
+
this.tools = options.tools ?? [];
|
|
4377
|
+
const tools = this.tools.map(({ name, description, parameters }) => ({ name, description, parameters }));
|
|
4378
|
+
this.opened = perform({
|
|
4379
|
+
action: "open",
|
|
4380
|
+
session: this.id,
|
|
4381
|
+
...options.instructions ? { instructions: options.instructions } : {},
|
|
4382
|
+
...tools.length > 0 ? { tools } : {}
|
|
4383
|
+
});
|
|
4384
|
+
this.opened.catch(() => {
|
|
4385
|
+
});
|
|
4386
|
+
if (tools.length > 0) void this.opened.then(() => this.serveTools()).catch(() => {
|
|
4387
|
+
});
|
|
4388
|
+
}
|
|
4389
|
+
/** Runs the model's tool calls as they come, until the session closes. */
|
|
4390
|
+
async serveTools() {
|
|
4391
|
+
while (!this.closed) {
|
|
4392
|
+
const request = await perform({ action: "toolCall", session: this.id });
|
|
4393
|
+
if (!request || request.closed || request.call === void 0) return;
|
|
4394
|
+
const tool = this.tools.find((candidate) => candidate.name === request.name);
|
|
4395
|
+
let answer;
|
|
4396
|
+
try {
|
|
4397
|
+
if (!tool) throw new Error(`no tool named ${String(request.name)}`);
|
|
4398
|
+
const output = await tool.call(request.arguments ?? {});
|
|
4399
|
+
answer = { output: typeof output === "string" ? output : JSON.stringify(output ?? null) };
|
|
4400
|
+
} catch (error) {
|
|
4401
|
+
answer = { error: error instanceof Error ? error.message : String(error) };
|
|
4402
|
+
}
|
|
4403
|
+
await perform({ action: "toolResult", session: this.id, call: request.call, ...answer });
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4406
|
+
/** Whether a response is under way. */
|
|
4407
|
+
get isResponding() {
|
|
4408
|
+
return this.responses > 0;
|
|
4409
|
+
}
|
|
4410
|
+
/** Loads the model ahead of the first prompt, so it answers sooner. */
|
|
4411
|
+
prewarm() {
|
|
4412
|
+
void this.opened.then(() => perform({ action: "prewarm", session: this.id })).catch(() => {
|
|
4413
|
+
});
|
|
4414
|
+
}
|
|
4415
|
+
async respond(to, options) {
|
|
4416
|
+
this.responses += 1;
|
|
4417
|
+
try {
|
|
4418
|
+
await this.opened;
|
|
4419
|
+
return await perform({ action: "respond", session: this.id, prompt: to, ...options ? { options } : {} });
|
|
4420
|
+
} finally {
|
|
4421
|
+
this.responses -= 1;
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
streamResponse(to, options) {
|
|
4425
|
+
this.responses += 1;
|
|
4426
|
+
return new ResponseStream(this, this.opened, to, options, () => {
|
|
4427
|
+
this.responses -= 1;
|
|
4428
|
+
});
|
|
4429
|
+
}
|
|
4430
|
+
/** Ends the session: the host lets its conversation go. */
|
|
4431
|
+
close() {
|
|
4432
|
+
if (this.closed) return;
|
|
4433
|
+
this.closed = true;
|
|
4434
|
+
void this.opened.then(() => perform({ action: "close", session: this.id })).catch(() => {
|
|
4435
|
+
});
|
|
4436
|
+
}
|
|
4437
|
+
}
|
|
3577
4438
|
function wakeLockAPI() {
|
|
3578
4439
|
const nav = globalThis.navigator;
|
|
3579
4440
|
return nav?.wakeLock ?? null;
|
|
@@ -3832,7 +4693,8 @@ function ProgressView(props) {
|
|
|
3832
4693
|
}
|
|
3833
4694
|
function ToolbarItem(props) {
|
|
3834
4695
|
const hidden = props.sharedBackgroundVisibility === "hidden";
|
|
3835
|
-
|
|
4696
|
+
const items = withEnvironment({ labelStyle: "iconOnly" }, props.children);
|
|
4697
|
+
return sb("toolbar", hidden ? { placement: toolbarPlacement(props.placement), sharedBackground: "hidden" } : { placement: toolbarPlacement(props.placement) }, void 0, items);
|
|
3836
4698
|
}
|
|
3837
4699
|
const ToolbarItemGroup = ToolbarItem;
|
|
3838
4700
|
function ToolbarSpacer(props) {
|
|
@@ -4501,6 +5363,9 @@ function Form(props) {
|
|
|
4501
5363
|
function Section(props) {
|
|
4502
5364
|
const { own, modifiers } = splitProps(props);
|
|
4503
5365
|
const pinned = useContext(PinnedContext);
|
|
5366
|
+
const [expanded, setExpanded] = own.isExpanded !== void 0 ? readBinding(own.isExpanded, own.onExpandedChange) : [true, () => {
|
|
5367
|
+
}];
|
|
5368
|
+
const collapsible = own.isExpanded !== void 0;
|
|
4504
5369
|
let header = supplement(own.header);
|
|
4505
5370
|
let footer = supplement(own.footer);
|
|
4506
5371
|
if (pinned?.includes("sectionHeaders") && header.text !== null && !header.view) header = { text: null, view: /* @__PURE__ */ jsx(Text, { children: header.text }) };
|
|
@@ -4509,7 +5374,15 @@ function Section(props) {
|
|
|
4509
5374
|
if (header.view) children.push(sb("sectionheader", {}, void 0, header.view));
|
|
4510
5375
|
if (own.children !== void 0) children.push(wrapRows(own.children));
|
|
4511
5376
|
if (footer.view) children.push(sb("sectionfooter", {}, void 0, footer.view));
|
|
4512
|
-
return applyModifiers(
|
|
5377
|
+
return applyModifiers(
|
|
5378
|
+
sb(
|
|
5379
|
+
"section",
|
|
5380
|
+
collapsible ? { header: header.text, footer: footer.text, collapsible: true, expanded } : { header: header.text, footer: footer.text },
|
|
5381
|
+
collapsible ? { tap: () => setExpanded(!expanded) } : void 0,
|
|
5382
|
+
children
|
|
5383
|
+
),
|
|
5384
|
+
modifiers
|
|
5385
|
+
);
|
|
4513
5386
|
}
|
|
4514
5387
|
function wrapRows(children) {
|
|
4515
5388
|
let offset = 0;
|
|
@@ -4620,10 +5493,11 @@ function Host(props) {
|
|
|
4620
5493
|
function Menu(props) {
|
|
4621
5494
|
const { own, modifiers } = splitProps(props);
|
|
4622
5495
|
const label = own.title !== void 0 ? /* @__PURE__ */ jsx(Text, { children: own.title }) : own.label;
|
|
5496
|
+
const items = withEnvironment({ labelStyle: "automatic" }, own.children);
|
|
4623
5497
|
return applyModifiers(
|
|
4624
5498
|
sb("menu", {}, void 0, [
|
|
4625
5499
|
/* @__PURE__ */ jsx(Fragment, { children: sb("zstack", { alignment: "center" }, void 0, label) }, "label"),
|
|
4626
|
-
/* @__PURE__ */ jsx(Fragment, { children:
|
|
5500
|
+
/* @__PURE__ */ jsx(Fragment, { children: items }, "items")
|
|
4627
5501
|
]),
|
|
4628
5502
|
modifiers
|
|
4629
5503
|
);
|
|
@@ -5180,22 +6054,10 @@ function DisclosureGroup(props) {
|
|
|
5180
6054
|
const label = typeof own.label === "string" ? /* @__PURE__ */ jsx(Text, { children: own.label }) : own.label;
|
|
5181
6055
|
const toggle = () => withAnimation({ type: "easeInOut", duration: 0.25 }, () => setExpanded(!expanded));
|
|
5182
6056
|
return applyModifiers(
|
|
5183
|
-
|
|
5184
|
-
/* @__PURE__ */ jsx(
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
/* @__PURE__ */ jsx(
|
|
5188
|
-
Image$1,
|
|
5189
|
-
{
|
|
5190
|
-
systemName: "chevron.right",
|
|
5191
|
-
font: { textStyle: "footnote", weight: "semibold" },
|
|
5192
|
-
foregroundStyle: "tint",
|
|
5193
|
-
rotationEffect: expanded ? 90 : 0
|
|
5194
|
-
}
|
|
5195
|
-
)
|
|
5196
|
-
] }) }),
|
|
5197
|
-
expanded && /* @__PURE__ */ jsx(VStack, { alignment: "leading", spacing: 8, padding: ["top", 8], transition: { kind: "opacity" }, children: own.children })
|
|
5198
|
-
] }),
|
|
6057
|
+
sb("disclosuregroup", { expanded }, { tap: toggle }, [
|
|
6058
|
+
/* @__PURE__ */ jsx(Fragment, { children: sb("zstack", { alignment: "center" }, void 0, label) }, "label"),
|
|
6059
|
+
/* @__PURE__ */ jsx(Fragment, { children: own.children }, "content")
|
|
6060
|
+
]),
|
|
5199
6061
|
modifiers
|
|
5200
6062
|
);
|
|
5201
6063
|
}
|
|
@@ -5291,16 +6153,46 @@ function ColorPicker(props) {
|
|
|
5291
6153
|
modifiers
|
|
5292
6154
|
);
|
|
5293
6155
|
}
|
|
6156
|
+
const isFile = (item) => typeof item === "object" && !(item instanceof URL) && "data" in item;
|
|
6157
|
+
const MIME_BY_EXTENSION = {
|
|
6158
|
+
txt: "text/plain",
|
|
6159
|
+
md: "text/markdown",
|
|
6160
|
+
csv: "text/csv",
|
|
6161
|
+
json: "application/json",
|
|
6162
|
+
html: "text/html",
|
|
6163
|
+
pdf: "application/pdf",
|
|
6164
|
+
png: "image/png",
|
|
6165
|
+
jpg: "image/jpeg",
|
|
6166
|
+
jpeg: "image/jpeg",
|
|
6167
|
+
gif: "image/gif",
|
|
6168
|
+
svg: "image/svg+xml",
|
|
6169
|
+
m4a: "audio/mp4",
|
|
6170
|
+
mp3: "audio/mpeg",
|
|
6171
|
+
mp4: "video/mp4",
|
|
6172
|
+
mov: "video/quicktime",
|
|
6173
|
+
zip: "application/zip"
|
|
6174
|
+
};
|
|
6175
|
+
async function sharedFile(file) {
|
|
6176
|
+
const bytes = typeof file.data === "string" ? new TextEncoder().encode(file.data) : file.data instanceof Uint8Array ? file.data : file.data instanceof ArrayBuffer ? new Uint8Array(file.data) : new Uint8Array(await file.data.arrayBuffer());
|
|
6177
|
+
const extension = file.name.includes(".") ? file.name.slice(file.name.lastIndexOf(".") + 1).toLowerCase() : "";
|
|
6178
|
+
return { name: file.name, type: file.type ?? MIME_BY_EXTENSION[extension] ?? "application/octet-stream", data: base64Encode(bytes) };
|
|
6179
|
+
}
|
|
6180
|
+
async function shareParamsOf(props) {
|
|
6181
|
+
const item = props.item;
|
|
6182
|
+
const isURL = item instanceof URL || typeof item === "string" && /^[a-z][a-z0-9+.-]*:\/\//i.test(item);
|
|
6183
|
+
const params = {};
|
|
6184
|
+
if (props.subject) params.title = props.subject;
|
|
6185
|
+
if (isURL) params.url = String(item);
|
|
6186
|
+
const text = [props.message, typeof item === "string" && !isURL ? item : null].filter(Boolean).join("\n");
|
|
6187
|
+
if (text) params.text = text;
|
|
6188
|
+
const files = [...isFile(item) ? [item] : [], ...props.files ?? []];
|
|
6189
|
+
if (files.length) params.files = await Promise.all(files.map(sharedFile));
|
|
6190
|
+
return params;
|
|
6191
|
+
}
|
|
5294
6192
|
function ShareLink(props) {
|
|
5295
6193
|
const { own, modifiers } = splitProps(props);
|
|
5296
6194
|
const share = () => {
|
|
5297
|
-
|
|
5298
|
-
const params = {};
|
|
5299
|
-
if (own.subject) params["title"] = own.subject;
|
|
5300
|
-
if (isURL) params["url"] = String(own.item);
|
|
5301
|
-
const text = [own.message, isURL ? null : String(own.item)].filter(Boolean).join("\n");
|
|
5302
|
-
if (text) params["text"] = text;
|
|
5303
|
-
performHostRequest({ op: "request", id: 0, kind: "share", params }, browserEnvironment).catch((error) => console.error(error));
|
|
6195
|
+
shareParamsOf(own).then((params) => performHostRequest({ op: "request", id: 0, kind: "share", params }, browserEnvironment)).catch((error) => console.error(error));
|
|
5304
6196
|
};
|
|
5305
6197
|
return applyModifiers(
|
|
5306
6198
|
/* @__PURE__ */ jsx(Button, { action: share, children: own.children ?? /* @__PURE__ */ jsx(Label, { title: own.title ?? "Share", systemImage: "square.and.arrow.up" }) }),
|
|
@@ -5921,6 +6813,23 @@ function KeyframeAnimator({
|
|
|
5921
6813
|
return children(value);
|
|
5922
6814
|
}
|
|
5923
6815
|
export {
|
|
6816
|
+
AVAudioEngine,
|
|
6817
|
+
AVAudioFile,
|
|
6818
|
+
AVAudioInputNode,
|
|
6819
|
+
AVAudioMixerNode,
|
|
6820
|
+
AVAudioNode,
|
|
6821
|
+
AVAudioOutputNode,
|
|
6822
|
+
AVAudioPCMBuffer,
|
|
6823
|
+
AVAudioPlayerNode,
|
|
6824
|
+
AVAudioTime,
|
|
6825
|
+
AVAudioUnitDelay,
|
|
6826
|
+
AVAudioUnitDistortion,
|
|
6827
|
+
AVAudioUnitEQ,
|
|
6828
|
+
AVAudioUnitEQFilterParameters,
|
|
6829
|
+
AVAudioUnitEffect,
|
|
6830
|
+
AVAudioUnitReverb,
|
|
6831
|
+
AVAudioUnitSampler,
|
|
6832
|
+
AVAudioUnitTimePitch,
|
|
5924
6833
|
AVPlayer,
|
|
5925
6834
|
AVQueuePlayer,
|
|
5926
6835
|
AngularGradient,
|
|
@@ -5951,6 +6860,7 @@ export {
|
|
|
5951
6860
|
ForEach,
|
|
5952
6861
|
Form,
|
|
5953
6862
|
Gauge,
|
|
6863
|
+
GenerationError,
|
|
5954
6864
|
GeometryChangeView,
|
|
5955
6865
|
GeometryReader,
|
|
5956
6866
|
GraphicsContext,
|
|
@@ -5966,6 +6876,7 @@ export {
|
|
|
5966
6876
|
LOADING_INDICATOR_DELAY_MS,
|
|
5967
6877
|
Label,
|
|
5968
6878
|
LabeledContent,
|
|
6879
|
+
LanguageModelSession,
|
|
5969
6880
|
Layout,
|
|
5970
6881
|
LazyHGrid,
|
|
5971
6882
|
LazyHStack,
|
|
@@ -5989,6 +6900,7 @@ export {
|
|
|
5989
6900
|
ProgressView,
|
|
5990
6901
|
RadialGradient,
|
|
5991
6902
|
Rectangle,
|
|
6903
|
+
ResponseStream,
|
|
5992
6904
|
RoundedRectangle,
|
|
5993
6905
|
Row,
|
|
5994
6906
|
RuleMark,
|
|
@@ -6004,6 +6916,7 @@ export {
|
|
|
6004
6916
|
SplitSelectionContext,
|
|
6005
6917
|
Stepper,
|
|
6006
6918
|
SwiftUIView,
|
|
6919
|
+
SystemLanguageModel,
|
|
6007
6920
|
Tab,
|
|
6008
6921
|
TabView,
|
|
6009
6922
|
Table,
|
|
@@ -6050,6 +6963,7 @@ export {
|
|
|
6050
6963
|
moveItems,
|
|
6051
6964
|
nextGeometry,
|
|
6052
6965
|
niceStep,
|
|
6966
|
+
noteInName,
|
|
6053
6967
|
opacity,
|
|
6054
6968
|
openURL,
|
|
6055
6969
|
padding,
|
|
@@ -6058,8 +6972,10 @@ export {
|
|
|
6058
6972
|
providingScrollSettings,
|
|
6059
6973
|
readBinding,
|
|
6060
6974
|
rgb,
|
|
6975
|
+
samplerZones,
|
|
6061
6976
|
scrollGeometryOf,
|
|
6062
6977
|
shapeStyle,
|
|
6978
|
+
shareParamsOf,
|
|
6063
6979
|
signInWithApple,
|
|
6064
6980
|
system,
|
|
6065
6981
|
takeScrollSettings,
|
|
@@ -6067,6 +6983,7 @@ export {
|
|
|
6067
6983
|
unitPoint,
|
|
6068
6984
|
useAVPlayer,
|
|
6069
6985
|
useAppStorage,
|
|
6986
|
+
useAudioEngine,
|
|
6070
6987
|
useAudioRecorder,
|
|
6071
6988
|
useColorScheme,
|
|
6072
6989
|
useContextBridge,
|
|
@@ -6084,6 +7001,7 @@ export {
|
|
|
6084
7001
|
useScenePhase,
|
|
6085
7002
|
useSpeechRecognition,
|
|
6086
7003
|
useSpeechSynthesis,
|
|
7004
|
+
useSystemLanguageModel,
|
|
6087
7005
|
useUndoManager,
|
|
6088
7006
|
useViewEnvironment,
|
|
6089
7007
|
useWakeLock,
|