@swiftbrowser/react 0.18.3 → 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 +50 -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 +2 -0
- 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/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/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +944 -28
- 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);
|
|
@@ -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;
|
|
@@ -4502,6 +5363,9 @@ function Form(props) {
|
|
|
4502
5363
|
function Section(props) {
|
|
4503
5364
|
const { own, modifiers } = splitProps(props);
|
|
4504
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;
|
|
4505
5369
|
let header = supplement(own.header);
|
|
4506
5370
|
let footer = supplement(own.footer);
|
|
4507
5371
|
if (pinned?.includes("sectionHeaders") && header.text !== null && !header.view) header = { text: null, view: /* @__PURE__ */ jsx(Text, { children: header.text }) };
|
|
@@ -4510,7 +5374,15 @@ function Section(props) {
|
|
|
4510
5374
|
if (header.view) children.push(sb("sectionheader", {}, void 0, header.view));
|
|
4511
5375
|
if (own.children !== void 0) children.push(wrapRows(own.children));
|
|
4512
5376
|
if (footer.view) children.push(sb("sectionfooter", {}, void 0, footer.view));
|
|
4513
|
-
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
|
+
);
|
|
4514
5386
|
}
|
|
4515
5387
|
function wrapRows(children) {
|
|
4516
5388
|
let offset = 0;
|
|
@@ -5182,22 +6054,10 @@ function DisclosureGroup(props) {
|
|
|
5182
6054
|
const label = typeof own.label === "string" ? /* @__PURE__ */ jsx(Text, { children: own.label }) : own.label;
|
|
5183
6055
|
const toggle = () => withAnimation({ type: "easeInOut", duration: 0.25 }, () => setExpanded(!expanded));
|
|
5184
6056
|
return applyModifiers(
|
|
5185
|
-
|
|
5186
|
-
/* @__PURE__ */ jsx(
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
/* @__PURE__ */ jsx(
|
|
5190
|
-
Image$1,
|
|
5191
|
-
{
|
|
5192
|
-
systemName: "chevron.right",
|
|
5193
|
-
font: { textStyle: "footnote", weight: "semibold" },
|
|
5194
|
-
foregroundStyle: "tint",
|
|
5195
|
-
rotationEffect: expanded ? 90 : 0
|
|
5196
|
-
}
|
|
5197
|
-
)
|
|
5198
|
-
] }) }),
|
|
5199
|
-
expanded && /* @__PURE__ */ jsx(VStack, { alignment: "leading", spacing: 8, padding: ["top", 8], transition: { kind: "opacity" }, children: own.children })
|
|
5200
|
-
] }),
|
|
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
|
+
]),
|
|
5201
6061
|
modifiers
|
|
5202
6062
|
);
|
|
5203
6063
|
}
|
|
@@ -5293,16 +6153,46 @@ function ColorPicker(props) {
|
|
|
5293
6153
|
modifiers
|
|
5294
6154
|
);
|
|
5295
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
|
+
}
|
|
5296
6192
|
function ShareLink(props) {
|
|
5297
6193
|
const { own, modifiers } = splitProps(props);
|
|
5298
6194
|
const share = () => {
|
|
5299
|
-
|
|
5300
|
-
const params = {};
|
|
5301
|
-
if (own.subject) params["title"] = own.subject;
|
|
5302
|
-
if (isURL) params["url"] = String(own.item);
|
|
5303
|
-
const text = [own.message, isURL ? null : String(own.item)].filter(Boolean).join("\n");
|
|
5304
|
-
if (text) params["text"] = text;
|
|
5305
|
-
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));
|
|
5306
6196
|
};
|
|
5307
6197
|
return applyModifiers(
|
|
5308
6198
|
/* @__PURE__ */ jsx(Button, { action: share, children: own.children ?? /* @__PURE__ */ jsx(Label, { title: own.title ?? "Share", systemImage: "square.and.arrow.up" }) }),
|
|
@@ -5923,6 +6813,23 @@ function KeyframeAnimator({
|
|
|
5923
6813
|
return children(value);
|
|
5924
6814
|
}
|
|
5925
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,
|
|
5926
6833
|
AVPlayer,
|
|
5927
6834
|
AVQueuePlayer,
|
|
5928
6835
|
AngularGradient,
|
|
@@ -5953,6 +6860,7 @@ export {
|
|
|
5953
6860
|
ForEach,
|
|
5954
6861
|
Form,
|
|
5955
6862
|
Gauge,
|
|
6863
|
+
GenerationError,
|
|
5956
6864
|
GeometryChangeView,
|
|
5957
6865
|
GeometryReader,
|
|
5958
6866
|
GraphicsContext,
|
|
@@ -5968,6 +6876,7 @@ export {
|
|
|
5968
6876
|
LOADING_INDICATOR_DELAY_MS,
|
|
5969
6877
|
Label,
|
|
5970
6878
|
LabeledContent,
|
|
6879
|
+
LanguageModelSession,
|
|
5971
6880
|
Layout,
|
|
5972
6881
|
LazyHGrid,
|
|
5973
6882
|
LazyHStack,
|
|
@@ -5991,6 +6900,7 @@ export {
|
|
|
5991
6900
|
ProgressView,
|
|
5992
6901
|
RadialGradient,
|
|
5993
6902
|
Rectangle,
|
|
6903
|
+
ResponseStream,
|
|
5994
6904
|
RoundedRectangle,
|
|
5995
6905
|
Row,
|
|
5996
6906
|
RuleMark,
|
|
@@ -6006,6 +6916,7 @@ export {
|
|
|
6006
6916
|
SplitSelectionContext,
|
|
6007
6917
|
Stepper,
|
|
6008
6918
|
SwiftUIView,
|
|
6919
|
+
SystemLanguageModel,
|
|
6009
6920
|
Tab,
|
|
6010
6921
|
TabView,
|
|
6011
6922
|
Table,
|
|
@@ -6052,6 +6963,7 @@ export {
|
|
|
6052
6963
|
moveItems,
|
|
6053
6964
|
nextGeometry,
|
|
6054
6965
|
niceStep,
|
|
6966
|
+
noteInName,
|
|
6055
6967
|
opacity,
|
|
6056
6968
|
openURL,
|
|
6057
6969
|
padding,
|
|
@@ -6060,8 +6972,10 @@ export {
|
|
|
6060
6972
|
providingScrollSettings,
|
|
6061
6973
|
readBinding,
|
|
6062
6974
|
rgb,
|
|
6975
|
+
samplerZones,
|
|
6063
6976
|
scrollGeometryOf,
|
|
6064
6977
|
shapeStyle,
|
|
6978
|
+
shareParamsOf,
|
|
6065
6979
|
signInWithApple,
|
|
6066
6980
|
system,
|
|
6067
6981
|
takeScrollSettings,
|
|
@@ -6069,6 +6983,7 @@ export {
|
|
|
6069
6983
|
unitPoint,
|
|
6070
6984
|
useAVPlayer,
|
|
6071
6985
|
useAppStorage,
|
|
6986
|
+
useAudioEngine,
|
|
6072
6987
|
useAudioRecorder,
|
|
6073
6988
|
useColorScheme,
|
|
6074
6989
|
useContextBridge,
|
|
@@ -6086,6 +7001,7 @@ export {
|
|
|
6086
7001
|
useScenePhase,
|
|
6087
7002
|
useSpeechRecognition,
|
|
6088
7003
|
useSpeechSynthesis,
|
|
7004
|
+
useSystemLanguageModel,
|
|
6089
7005
|
useUndoManager,
|
|
6090
7006
|
useViewEnvironment,
|
|
6091
7007
|
useWakeLock,
|