@alexkroman1/aai-ui 1.8.2 → 1.9.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/dist/_colors-DJUordGv.js +19 -0
- package/dist/_utils-5cs73OrA.js +16 -0
- package/dist/_utils.d.ts +4 -0
- package/dist/aai-logo-B8lDmsut.js +84 -0
- package/dist/audio.d.ts +9 -2
- package/dist/audio.js +41 -24
- package/dist/chat-view-C1XbqDk0.js +186 -0
- package/dist/components/_colors.d.ts +26 -0
- package/dist/components/aai-logo.d.ts +7 -6
- package/dist/components/button.d.ts +11 -13
- package/dist/components/button.js +7 -16
- package/dist/components/chat-view.d.ts +8 -19
- package/dist/components/chat-view.js +2 -98
- package/dist/components/controls.d.ts +1 -1
- package/dist/components/controls.js +2 -39
- package/dist/components/eyebrow.d.ts +12 -0
- package/dist/components/message-list.d.ts +1 -1
- package/dist/components/message-list.js +126 -56
- package/dist/components/sidebar-layout.d.ts +1 -1
- package/dist/components/start-screen.d.ts +4 -3
- package/dist/components/start-screen.js +20 -14
- package/dist/components/text-controls.d.ts +14 -0
- package/dist/components/tool-call-block.d.ts +5 -3
- package/dist/components/tool-call-block.js +1 -1
- package/dist/components/url-chips.d.ts +28 -0
- package/dist/context.d.ts +30 -4
- package/dist/context.js +86 -10
- package/dist/controls-BngrPbOC.js +135 -0
- package/dist/default-client/assets/audio-Cs-6t_Wd.js +1 -0
- package/dist/default-client/assets/capture-processor-C19oBn4L.js +105 -0
- package/dist/default-client/assets/index-Bf4ZTNcx.js +73 -0
- package/dist/default-client/assets/index-Bzlh9i7w.css +2 -0
- package/dist/default-client/assets/playback-processor-BtlzAH78.js +149 -0
- package/dist/default-client/index.html +3 -3
- package/dist/define-client.d.ts +2 -2
- package/dist/define-client.js +18 -24
- package/dist/eyebrow-C6ZFuiz6.js +27 -0
- package/dist/hooks.js +71 -42
- package/dist/index.d.ts +3 -1
- package/dist/index.js +6 -5
- package/dist/session-core-D3NDaySY.js +652 -0
- package/dist/session-core-messages.d.ts +50 -0
- package/dist/session-core-types.d.ts +147 -0
- package/dist/session-core-upload.d.ts +16 -0
- package/dist/session-core.d.ts +2 -68
- package/dist/session-core.js +1 -455
- package/dist/{tool-call-block-wby_jyoY.js → tool-call-block-7f1GTG-P.js} +33 -31
- package/dist/types.d.ts +29 -4
- package/dist/types.js +10 -1
- package/dist/worklets/capture-processor.d.ts +2 -0
- package/dist/worklets/capture-processor.js +80 -25
- package/dist/worklets/playback-processor.d.ts +2 -0
- package/dist/worklets/playback-processor.js +80 -29
- package/package.json +18 -18
- package/styles.css +30 -0
- package/dist/_react-test-utils.d.ts +0 -86
- package/dist/aai-logo-BqFv6JU6.js +0 -21
- package/dist/default-client/assets/audio-UowwhmYo.js +0 -1
- package/dist/default-client/assets/capture-processor-C26lSiVr.js +0 -53
- package/dist/default-client/assets/index-BZqVGR2o.css +0 -2
- package/dist/default-client/assets/index-YW9WUhbL.js +0 -49
- package/dist/default-client/assets/playback-processor-dSA8Im99.js +0 -101
package/dist/context.js
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
|
-
import { createContext, createElement, useContext, useSyncExternalStore } from "react";
|
|
1
|
+
import { createContext, createElement, useCallback, useContext, useEffect, useRef, useSyncExternalStore } from "react";
|
|
2
2
|
//#region context.ts
|
|
3
3
|
const DEFAULT_THEME = {
|
|
4
|
-
bg: "#
|
|
5
|
-
primary: "#
|
|
6
|
-
text: "
|
|
7
|
-
surface: "#
|
|
8
|
-
border: "#
|
|
4
|
+
bg: "#FBF8F2",
|
|
5
|
+
primary: "#3F2BC1",
|
|
6
|
+
text: "#1B1A18",
|
|
7
|
+
surface: "#FFFFFF",
|
|
8
|
+
border: "#DCD7CC"
|
|
9
9
|
};
|
|
10
10
|
const SessionCtx = createContext(null);
|
|
11
11
|
function SessionProvider({ value, children }) {
|
|
12
12
|
return createElement(SessionCtx.Provider, { value }, children);
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Return the raw {@link SessionCore} from context without subscribing to
|
|
16
|
+
* snapshot changes. Useful for accessing stable methods (`start`, `toggle`,
|
|
17
|
+
* `reset`, …) from components that select narrow state via
|
|
18
|
+
* {@link useSessionSelector}.
|
|
19
|
+
*
|
|
20
|
+
* Not part of the package's public export surface — internal to aai-ui
|
|
21
|
+
* components.
|
|
22
|
+
*/
|
|
23
|
+
function useSessionCore() {
|
|
15
24
|
const core = useContext(SessionCtx);
|
|
16
|
-
if (!core) throw new Error("
|
|
25
|
+
if (!core) throw new Error("Session hooks must be used within <SessionProvider>");
|
|
26
|
+
return core;
|
|
27
|
+
}
|
|
28
|
+
function useSession() {
|
|
29
|
+
const core = useSessionCore();
|
|
17
30
|
return {
|
|
18
31
|
...useSyncExternalStore(core.subscribe, core.getSnapshot),
|
|
19
32
|
start: core.start,
|
|
@@ -21,19 +34,82 @@ function useSession() {
|
|
|
21
34
|
resetState: core.resetState,
|
|
22
35
|
reset: core.reset,
|
|
23
36
|
disconnect: core.disconnect,
|
|
24
|
-
toggle: core.toggle
|
|
37
|
+
toggle: core.toggle,
|
|
38
|
+
startRecording: core.startRecording,
|
|
39
|
+
stopRecording: core.stopRecording,
|
|
40
|
+
sendAudioFile: core.sendAudioFile
|
|
25
41
|
};
|
|
26
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Subscribe to a narrow slice of the session snapshot.
|
|
45
|
+
*
|
|
46
|
+
* Unlike {@link useSession} — which re-renders the component on *every*
|
|
47
|
+
* snapshot change — this only triggers a re-render when the selected value
|
|
48
|
+
* changes (per `isEqual`, default `Object.is`). Use it for components that
|
|
49
|
+
* read a single field, e.g. `useSessionSelector((s) => s.running)`.
|
|
50
|
+
*
|
|
51
|
+
* The selector must be pure. It may run on every snapshot change, so keep it
|
|
52
|
+
* cheap; when it returns a derived object, pass a custom `isEqual` to avoid
|
|
53
|
+
* re-renders on referentially-new-but-equal results.
|
|
54
|
+
*
|
|
55
|
+
* @public
|
|
56
|
+
*/
|
|
57
|
+
function useSessionSelector(selector, isEqual = Object.is) {
|
|
58
|
+
const core = useSessionCore();
|
|
59
|
+
const selectorRef = useRef(selector);
|
|
60
|
+
selectorRef.current = selector;
|
|
61
|
+
const isEqualRef = useRef(isEqual);
|
|
62
|
+
isEqualRef.current = isEqual;
|
|
63
|
+
const cacheRef = useRef({ hasValue: false });
|
|
64
|
+
const getSelection = useCallback(() => {
|
|
65
|
+
const next = selectorRef.current(core.getSnapshot());
|
|
66
|
+
const cache = cacheRef.current;
|
|
67
|
+
if (cache.hasValue && isEqualRef.current(cache.value, next)) return cache.value;
|
|
68
|
+
cacheRef.current = {
|
|
69
|
+
hasValue: true,
|
|
70
|
+
value: next
|
|
71
|
+
};
|
|
72
|
+
return next;
|
|
73
|
+
}, [core]);
|
|
74
|
+
return useSyncExternalStore(core.subscribe, getSelection);
|
|
75
|
+
}
|
|
27
76
|
const ThemeCtx = createContext(DEFAULT_THEME);
|
|
28
77
|
function ThemeProvider({ value, children }) {
|
|
29
78
|
const merged = value ? {
|
|
30
79
|
...DEFAULT_THEME,
|
|
31
80
|
...value
|
|
32
81
|
} : DEFAULT_THEME;
|
|
82
|
+
usePageBackground(merged.bg);
|
|
33
83
|
return createElement(ThemeCtx.Provider, { value: merged }, children);
|
|
34
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Paint `html`/`body` with the theme background.
|
|
87
|
+
*
|
|
88
|
+
* Components paint `theme.bg` on their own containers, but the page behind
|
|
89
|
+
* them keeps whatever the static `<style>` in index.html set. Any viewport
|
|
90
|
+
* wider (or taller) than the app column then shows that color as a border
|
|
91
|
+
* around the UI — which is how a cream theme ended up letterboxed in black.
|
|
92
|
+
* Driving it from the theme means a custom `client({ theme })` covers the
|
|
93
|
+
* page too, instead of trading one hardcoded color for another.
|
|
94
|
+
*/
|
|
95
|
+
function usePageBackground(bg) {
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (typeof document === "undefined") return;
|
|
98
|
+
const { body, documentElement: html } = document;
|
|
99
|
+
const previous = {
|
|
100
|
+
body: body.style.background,
|
|
101
|
+
html: html.style.background
|
|
102
|
+
};
|
|
103
|
+
body.style.background = bg;
|
|
104
|
+
html.style.background = bg;
|
|
105
|
+
return () => {
|
|
106
|
+
body.style.background = previous.body;
|
|
107
|
+
html.style.background = previous.html;
|
|
108
|
+
};
|
|
109
|
+
}, [bg]);
|
|
110
|
+
}
|
|
35
111
|
function useTheme() {
|
|
36
112
|
return useContext(ThemeCtx);
|
|
37
113
|
}
|
|
38
114
|
//#endregion
|
|
39
|
-
export {
|
|
115
|
+
export { SessionProvider, ThemeProvider, useSession, useSessionCore, useSessionSelector, useTheme };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useSessionCore, useSessionSelector, useTheme } from "./context.js";
|
|
2
|
+
import { Button } from "./components/button.js";
|
|
3
|
+
import { i as TEXT_MUTED, n as SURFACE_TINT, r as TEXT_FAINT } from "./_colors-DJUordGv.js";
|
|
4
|
+
import clsx from "clsx";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
//#region components/url-chips.tsx
|
|
8
|
+
/** @jsxImportSource react */
|
|
9
|
+
/** How long the "Copied" confirmation replaces the label after a click. */
|
|
10
|
+
const COPIED_FEEDBACK_MS = 1500;
|
|
11
|
+
/**
|
|
12
|
+
* A compact labeled chip showing a URL. Click to copy.
|
|
13
|
+
*
|
|
14
|
+
* The label is what makes a pair of these readable — on its own a bare URL
|
|
15
|
+
* leaves you guessing whether it's the page or the socket.
|
|
16
|
+
*/
|
|
17
|
+
function UrlChip({ label, url, hint, testId, className }) {
|
|
18
|
+
const theme = useTheme();
|
|
19
|
+
const [copied, setCopied] = useState(false);
|
|
20
|
+
function copy() {
|
|
21
|
+
navigator.clipboard?.writeText(url).then(() => {
|
|
22
|
+
setCopied(true);
|
|
23
|
+
setTimeout(() => setCopied(false), COPIED_FEEDBACK_MS);
|
|
24
|
+
}).catch(() => {});
|
|
25
|
+
}
|
|
26
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
27
|
+
type: "button",
|
|
28
|
+
onClick: copy,
|
|
29
|
+
title: `${hint} (click to copy)\n${url}`,
|
|
30
|
+
className: `flex items-center gap-1.5 min-w-0 appearance-none m-0 px-2 py-1 rounded-aai border cursor-pointer outline-none text-[11px] leading-none font-aai-mono ${className ?? ""}`,
|
|
31
|
+
style: {
|
|
32
|
+
background: SURFACE_TINT,
|
|
33
|
+
borderColor: theme.border,
|
|
34
|
+
color: TEXT_FAINT
|
|
35
|
+
},
|
|
36
|
+
"data-testid": testId,
|
|
37
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
38
|
+
className: "uppercase tracking-wide shrink-0",
|
|
39
|
+
style: { color: TEXT_MUTED },
|
|
40
|
+
children: copied ? "Copied" : label
|
|
41
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
42
|
+
className: "truncate",
|
|
43
|
+
"data-testid": `${testId}-url`,
|
|
44
|
+
children: url
|
|
45
|
+
})]
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** The page this UI is served from — what you'd send someone to talk to the agent. */
|
|
49
|
+
function pageUrl() {
|
|
50
|
+
if (typeof window === "undefined") return "";
|
|
51
|
+
return `${window.location.origin}${window.location.pathname}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The session's shareable UI URL.
|
|
55
|
+
*
|
|
56
|
+
* @public
|
|
57
|
+
*/
|
|
58
|
+
function UiUrlChip({ className }) {
|
|
59
|
+
return /* @__PURE__ */ jsx(UrlChip, {
|
|
60
|
+
label: "UI",
|
|
61
|
+
url: pageUrl(),
|
|
62
|
+
hint: "Shareable UI",
|
|
63
|
+
testId: "ui-url-chip",
|
|
64
|
+
...className !== void 0 && { className }
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The session's programmatic WebSocket endpoint — the URL a script or backend
|
|
69
|
+
* can connect to directly instead of using this UI.
|
|
70
|
+
*
|
|
71
|
+
* @public
|
|
72
|
+
*/
|
|
73
|
+
function ApiUrlChip({ className }) {
|
|
74
|
+
return /* @__PURE__ */ jsx(UrlChip, {
|
|
75
|
+
label: "API",
|
|
76
|
+
url: useSessionSelector((s) => s.apiUrl),
|
|
77
|
+
hint: "API endpoint",
|
|
78
|
+
testId: "api-url-chip",
|
|
79
|
+
...className !== void 0 && { className }
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The UI and API URLs side by side. They answer the same question — "how do I
|
|
84
|
+
* reach this agent?" — so they belong together and each needs its label to be
|
|
85
|
+
* told apart. Rendered by the default shell in every session mode (S2S,
|
|
86
|
+
* pipeline, text-only).
|
|
87
|
+
*
|
|
88
|
+
* @public
|
|
89
|
+
*/
|
|
90
|
+
function SessionUrlChips({ className }) {
|
|
91
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
92
|
+
className: `flex items-center gap-1.5 min-w-0 ${className ?? ""}`,
|
|
93
|
+
children: [/* @__PURE__ */ jsx(UiUrlChip, { className: "min-w-0 flex-1" }), /* @__PURE__ */ jsx(ApiUrlChip, { className: "min-w-0 flex-1" })]
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region components/controls.tsx
|
|
98
|
+
/** @jsxImportSource react */
|
|
99
|
+
/**
|
|
100
|
+
* Session control buttons: **Stop / Resume** and **New Conversation**.
|
|
101
|
+
*
|
|
102
|
+
* Reads session state from {@link useSession}. Must be rendered inside a
|
|
103
|
+
* {@link SessionProvider}.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```tsx
|
|
107
|
+
* <Controls className="justify-end" />
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* @param className - Additional CSS class names applied to the container.
|
|
111
|
+
*
|
|
112
|
+
* @public
|
|
113
|
+
*/
|
|
114
|
+
function Controls({ className }) {
|
|
115
|
+
const running = useSessionSelector((s) => s.running);
|
|
116
|
+
const { toggle, reset } = useSessionCore();
|
|
117
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
118
|
+
className: clsx("flex items-center gap-3 shrink-0", className),
|
|
119
|
+
children: [
|
|
120
|
+
/* @__PURE__ */ jsx(Button, {
|
|
121
|
+
variant: "secondary",
|
|
122
|
+
onClick: toggle,
|
|
123
|
+
children: running ? "Stop" : "Resume"
|
|
124
|
+
}),
|
|
125
|
+
/* @__PURE__ */ jsx(Button, {
|
|
126
|
+
variant: "ghost",
|
|
127
|
+
onClick: reset,
|
|
128
|
+
children: "New Conversation"
|
|
129
|
+
}),
|
|
130
|
+
/* @__PURE__ */ jsx(SessionUrlChips, { className: "ml-auto max-w-[60%]" })
|
|
131
|
+
]
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
export { UiUrlChip as i, ApiUrlChip as n, SessionUrlChips as r, Controls as t };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{t as e}from"./index-Bf4ZTNcx.js";async function t(e,t){let n=await new OfflineAudioContext(1,1,t).decodeAudioData(e),r=Math.ceil(n.duration*t),i=new OfflineAudioContext(1,r,t),a=i.createBufferSource();a.buffer=n,a.connect(i.destination),a.start();let o=(await i.startRendering()).getChannelData(0),s=new Int16Array(o.length);for(let e=0;e<o.length;e++){let t=Math.max(-1,Math.min(1,o[e]??0));s[e]=t<0?t*32768:t*32767}return s}async function n(t){let{sttSampleRate:n,ttsSampleRate:r,captureWorkletSrc:i,playbackWorkletSrc:a,onMicData:o}=t,s=r,c=new AudioContext({sampleRate:s,latencyHint:`playback`}),l=navigator.mediaDevices.getUserMedia({audio:{deviceId:{ideal:`default`},echoCancellation:!0,noiseSuppression:!0,autoGainControl:!0,voiceIsolation:!0}}),u;try{[u]=await Promise.all([l,c.resume(),c.audioWorklet.addModule(i),c.audioWorklet.addModule(a)])}catch(e){throw l.then(e=>{for(let t of e.getTracks())t.stop()}).catch(()=>{}),await c.close().catch(e=>{console.warn(`AudioContext close failed:`,e)}),e}let d=c.createMediaStreamSource(u),f=new AudioWorkletNode(c,`capture-processor`,{channelCount:1,channelCountMode:`explicit`,processorOptions:{contextRate:s,sttSampleRate:n,bufferSeconds:e}});d.connect(f),f.port.postMessage({event:`start`}),f.port.onmessage=e=>{e.data.event===`chunk`&&o(e.data.buffer)};let p=null,m=null,h=new AbortController;function g(){if(p)return p;let e=new AudioWorkletNode(c,`playback-processor`,{processorOptions:{sampleRate:s}});return e.connect(c.destination),e.port.onmessage=e=>{e.data.event===`stop`&&(m?.(),m=null)},p=e,e}let _={enqueue(e){h.signal.aborted||e.byteLength!==0&&g().port.postMessage({event:`write`,buffer:new Uint8Array(e)},[e])},done(){return!p||c.state!==`running`?Promise.resolve():new Promise(e=>{m=e,p?.port.postMessage({event:`done`})})},flush(){p&&p.port.postMessage({event:`interrupt`})},async close(){if(!h.signal.aborted){h.abort(),f.port.postMessage({event:`stop`}),d.disconnect(),f.disconnect(),p&&p.disconnect();for(let e of u.getTracks())e.stop();await c.close().catch(()=>{})}},async[Symbol.asyncDispose](){await _.close()}};return _}export{n as createVoiceIO,t as decodeAudioToPcm16};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
var e=new Blob([`
|
|
2
|
+
class CaptureProcessor extends AudioWorkletProcessor {
|
|
3
|
+
constructor(options) {
|
|
4
|
+
super();
|
|
5
|
+
this.recording = false;
|
|
6
|
+
const opts = options.processorOptions || {};
|
|
7
|
+
this.fromRate = opts.contextRate || sampleRate;
|
|
8
|
+
this.toRate = opts.sttSampleRate || sampleRate;
|
|
9
|
+
this.ratio = this.fromRate / this.toRate;
|
|
10
|
+
this.needsResample = this.fromRate !== this.toRate;
|
|
11
|
+
// Streaming-resampler state carried across process() blocks so the output
|
|
12
|
+
// clock doesn't drift and there's no discontinuity at 128-sample block
|
|
13
|
+
// boundaries. \`prev\` is the previous block's last input sample (used to
|
|
14
|
+
// interpolate across the boundary); \`pos\` is the fractional read position,
|
|
15
|
+
// in input samples, relative to an extended [prev, ...input] frame. Start
|
|
16
|
+
// at 1 so the first output is input[0] (not the bogus initial prev).
|
|
17
|
+
this.prev = 0;
|
|
18
|
+
this.pos = 1;
|
|
19
|
+
// Output buffer reused across process() calls -- the render quantum is a
|
|
20
|
+
// fixed size, so allocating per call would churn the realtime audio thread.
|
|
21
|
+
this.resampleBuf = null;
|
|
22
|
+
// Int16 accumulation buffer: flushed to the main thread as one transferred
|
|
23
|
+
// ArrayBuffer once ~bufferSeconds of output samples are batched. Sized 2x
|
|
24
|
+
// the flush target so a whole render quantum always fits before flushing.
|
|
25
|
+
this.targetSamples = Math.max(1, Math.round(this.toRate * (opts.bufferSeconds || 0.1)));
|
|
26
|
+
this.pending = new Int16Array(this.targetSamples * 2);
|
|
27
|
+
this.pendingLen = 0;
|
|
28
|
+
this.port.onmessage = (e) => {
|
|
29
|
+
if (e.data.event === 'start') this.recording = true;
|
|
30
|
+
else if (e.data.event === 'stop') {
|
|
31
|
+
// Final flush so the tail of speech isn't dropped on close.
|
|
32
|
+
this.flush();
|
|
33
|
+
this.recording = false;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
resample(input) {
|
|
39
|
+
const ratio = this.ratio;
|
|
40
|
+
const n = input.length;
|
|
41
|
+
if (n === 0) return new Float32Array(0);
|
|
42
|
+
// At most ceil(n / ratio) + 1 outputs per block (pos starts in [0, ratio)).
|
|
43
|
+
const maxLen = Math.ceil(n / ratio) + 1;
|
|
44
|
+
if (this.resampleBuf === null || this.resampleBuf.length < maxLen) {
|
|
45
|
+
this.resampleBuf = new Float32Array(maxLen);
|
|
46
|
+
}
|
|
47
|
+
const out = this.resampleBuf;
|
|
48
|
+
// Extended frame: index 0 = prev (previous block's last sample),
|
|
49
|
+
// index k>=1 = input[k-1]. Interpolate at fractional positions stepping by
|
|
50
|
+
// ratio, carrying \`pos\` across calls to preserve the sample clock.
|
|
51
|
+
let count = 0;
|
|
52
|
+
let pos = this.pos;
|
|
53
|
+
while (pos < n) {
|
|
54
|
+
const idx = pos | 0;
|
|
55
|
+
const frac = pos - idx;
|
|
56
|
+
const a = idx === 0 ? this.prev : input[idx - 1];
|
|
57
|
+
const b = input[idx];
|
|
58
|
+
out[count++] = a + frac * (b - a);
|
|
59
|
+
pos += ratio;
|
|
60
|
+
}
|
|
61
|
+
// Shift the origin to this block's last sample for the next call.
|
|
62
|
+
this.prev = input[n - 1];
|
|
63
|
+
this.pos = pos - n;
|
|
64
|
+
return out.subarray(0, count);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Convert Float32 -> Int16 and append to the pending batch. Writes through
|
|
68
|
+
// an Int16Array directly (assignment truncates like DataView.setInt16).
|
|
69
|
+
accumulate(samples) {
|
|
70
|
+
let buf = this.pending;
|
|
71
|
+
if (this.pendingLen + samples.length > buf.length) {
|
|
72
|
+
// Defensive: only reachable if a render quantum outproduces the 1x
|
|
73
|
+
// headroom above the flush target (never with 128-sample quanta).
|
|
74
|
+
const grown = new Int16Array((this.pendingLen + samples.length) * 2);
|
|
75
|
+
grown.set(buf.subarray(0, this.pendingLen));
|
|
76
|
+
this.pending = grown;
|
|
77
|
+
buf = grown;
|
|
78
|
+
}
|
|
79
|
+
for (let i = 0; i < samples.length; i++) {
|
|
80
|
+
const s = Math.max(-1, Math.min(1, samples[i]));
|
|
81
|
+
buf[this.pendingLen++] = s < 0 ? s * 0x8000 : s * 0x7fff;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Post the batched samples as one transferred ArrayBuffer and reset.
|
|
86
|
+
flush() {
|
|
87
|
+
if (this.pendingLen === 0) return;
|
|
88
|
+
const buffer = this.pending.buffer.slice(0, this.pendingLen * 2);
|
|
89
|
+
this.pendingLen = 0;
|
|
90
|
+
this.port.postMessage({ event: 'chunk', buffer }, [buffer]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
process(inputs) {
|
|
94
|
+
const input = inputs[0];
|
|
95
|
+
if (!input || !input[0] || !this.recording) return true;
|
|
96
|
+
|
|
97
|
+
const samples = this.needsResample ? this.resample(input[0]) : input[0];
|
|
98
|
+
this.accumulate(samples);
|
|
99
|
+
if (this.pendingLen >= this.targetSamples) this.flush();
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
registerProcessor('capture-processor', CaptureProcessor);
|
|
105
|
+
`],{type:`application/javascript`}),t=URL.createObjectURL(e);export{t as default};
|