@solidrt/core 0.0.11 → 0.0.13

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/AGENTS.md CHANGED
@@ -68,10 +68,14 @@ Peer deps @solidjs/signals and @solidjs/universal must match (currently
68
68
  onKeyDown/Up, onTextInput, onFocus/onBlur. Text entry: focus a node with an
69
69
  `onTextInput` handler (setFocus activates the on-screen keyboard).
70
70
 
71
- - Reactivity is plain SolidJS (`createSignal`, `createEffect`, ... from
72
- @solidjs/signals). Per-frame work: `onFrame((tick, frame) => {})` (returns a
73
- cleanup; auto-cleaned inside a reactive scope). Also onResize, onLayout,
74
- onWindowFocus, onWindowBlur.
71
+ - Reactivity is SolidJS 2.0 (`@solidjs/signals`), NOT Solid 1.x. `createSignal`
72
+ is as you expect, but `createEffect` takes the 2.0 two-function shape: a
73
+ TRACKED compute that reads signals and returns a value, then an UNTRACKED
74
+ effect that receives it - `createEffect(() => count(), (c) => ...)`. The 1.x
75
+ single-callback form `createEffect(() => { ...count()... })` does NOT track
76
+ here. Per-frame work: `onFrame((tick, frame) => {})` (returns a cleanup;
77
+ auto-cleaned inside a reactive scope) or standard `requestAnimationFrame`.
78
+ Also onResize, onLayout, onWindowFocus, onWindowBlur.
75
79
 
76
80
  - Device/GPU access via subpath imports: @solidrt/core/camera, /microphone,
77
81
  /speech, /gpu. Image flow: `decodeImage(bytes)` -> `createTexture(data,w,h)`
package/jsx-runtime.d.ts CHANGED
@@ -3,6 +3,7 @@ import type {
3
3
  RectProps,
4
4
  OvalProps,
5
5
  PathProps,
6
+ SvgProps,
6
7
  ViewProps,
7
8
  TextProps,
8
9
  TextureProps,
@@ -32,12 +33,14 @@ export namespace JSX {
32
33
  rect: RectProps & LayoutProps
33
34
  oval: OvalProps & LayoutProps
34
35
  path: PathProps & LayoutProps
36
+ svg: SvgProps & LayoutProps
35
37
  texture: TextureProps & LayoutProps
36
38
  audio: AudioProps
37
39
  "d-view": ViewProps
38
40
  "d-rect": RectProps
39
41
  "d-oval": OvalProps
40
42
  "d-path": PathProps
43
+ "d-svg": SvgProps
41
44
  "d-texture": TextureProps
42
45
  "d-text": TextProps
43
46
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/core",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "license": "MIT",
5
5
  "author": "Antoine van Wel",
6
6
  "type": "module",
@@ -9,8 +9,10 @@
9
9
  ".": "./src/index.ts",
10
10
  "./camera": "./src/camera.ts",
11
11
  "./gpu": "./src/gpu.ts",
12
+ "./image": "./src/image.ts",
12
13
  "./microphone": "./src/microphone.ts",
13
- "./speech": "./src/speech.ts",
14
+ "./speech-recognition": "./src/speech-recognition.ts",
15
+ "./text-input": "./src/text-input.ts",
14
16
  "./jsx-runtime": "./jsx-runtime.d.ts",
15
17
  "./jsx-runtime-dev": "./jsx-runtime.d.ts"
16
18
  },
@@ -23,7 +25,7 @@
23
25
  "colord": "^2.9.3"
24
26
  },
25
27
  "devDependencies": {
26
- "@solidrt/flux-types": "0.0.11"
28
+ "@solidrt/flux-types": "0.0.13"
27
29
  },
28
30
  "peerDependencies": {
29
31
  "@solidjs/signals": "2.0.0-beta.14",
package/src/camera.ts CHANGED
@@ -1,8 +1,13 @@
1
- // Camera capture. A camera streams into a texture id, so a viewfinder is just
2
- // <texture src={cam.texture} />. Opening the camera IS the permission request
3
- // (SDL semantics): the promise resolves once the stream is configured and
4
- // rejects if the user denies access.
1
+ // Camera capture, reactive (SolidJS) layer. A camera streams into a texture id,
2
+ // so a viewfinder is just <texture src={cam.texture} />. Opening the camera IS
3
+ // the permission request (SDL semantics): it resolves once the stream is
4
+ // configured and rejects if the user denies access.
5
+ //
6
+ // The imperative primitive lives in the `flux:camera` module; import { open,
7
+ // listCameras, scanImage } from "flux:camera" for non-reactive use.
5
8
 
9
+ import { createSignal, onCleanup } from "@solidjs/signals"
10
+ import { listCameras, open } from "flux:camera"
6
11
  import { on } from "srt:events"
7
12
 
8
13
  export type CameraFacing = "front" | "back" | "unknown"
@@ -21,58 +26,96 @@ export type BarcodeResult = {
21
26
  }
22
27
 
23
28
  export type CameraOptions = {
24
- /** Explicit device id from listCameras(); takes precedence over facing. */
29
+ /** Explicit device id from flux:camera listCameras(); takes precedence over facing. */
25
30
  camera?: number
26
31
  /** Pick the first camera with this facing (falls back to the first camera). */
27
32
  facing?: "front" | "back"
28
33
  /** Size hint; the device picks the closest supported mode. */
29
34
  width?: number
30
35
  height?: number
31
- /** Decode these barcode formats from the stream (delivered via onBarcode). */
36
+ /** Decode these barcode formats from the stream (delivered via the barcode signal). */
32
37
  scan?: BarcodeFormat[]
33
38
  }
34
39
 
35
- export type Camera = {
36
- /** Texture id updated every frame while open; render with <texture src={...}>. */
37
- texture: number
38
- /** Actual stream size (may differ from the requested hint). */
39
- width: number
40
- height: number
41
- /** Receive decoded barcodes (requires the scan option; replaces any previous callback). */
42
- onBarcode(callback: (result: BarcodeResult) => void): void
43
- /** Release the device. The texture keeps showing the last frame. */
44
- close(): void
45
- }
40
+ let devicesAccessor: (() => CameraInfo[]) | undefined
46
41
 
47
- export function listCameras(): CameraInfo[] {
48
- return camera.listCameras()
42
+ /**
43
+ * Current camera list as a reactive accessor: re-enumerates on hotplug. Also
44
+ * initializes the camera subsystem (required before hotplug events fire).
45
+ * App-lifetime: there is one camera subsystem, so no cleanup is needed.
46
+ *
47
+ * Coverage caveat (SDL 3.4.8): only Android delivers both add and remove. On
48
+ * Linux you get add events but not remove (removal is broken upstream); on
49
+ * macOS/Windows there is no camera hotplug at all.
50
+ */
51
+ export function cameraDevices(): CameraInfo[] {
52
+ if (!devicesAccessor) {
53
+ let [devices, setDevices] = createSignal<CameraInfo[]>(listCameras())
54
+ on("cameraDeviceChange", () => setDevices(listCameras()))
55
+ devicesAccessor = devices
56
+ }
57
+ return devicesAccessor()
49
58
  }
50
59
 
51
- // Camera hotplug. Re-enumerate with listCameras() to see the new device set.
52
- // Events only flow once the camera subsystem is up, i.e. after the first
53
- // listCameras() or openCamera() call. Returns an unsubscribe function.
54
- // Coverage caveat (SDL 3.4.8): only Android delivers both add and remove. On
55
- // Linux you get added=true but not added=false (removal is broken upstream);
56
- // on macOS/Windows there is no camera hotplug at all, so nothing fires.
60
+ /**
61
+ * Low-level fallback: subscribe to camera hotplug events with a callback. Prefer
62
+ * `cameraDevices()` unless the reactive accessor does not fit. Re-enumerate with
63
+ * `listCameras()` (from flux:camera) inside the callback to get the new set.
64
+ * Returns an unsubscribe function.
65
+ */
57
66
  export function onDeviceChange(callback: (event: { added: boolean }) => void): () => void {
58
67
  return on("cameraDeviceChange", callback)
59
68
  }
60
69
 
61
- // One-shot scan of an RGBA8 pixel buffer for QR codes; composes with
62
- // decodeImage: scanBarcodes(img.data, img.width, img.height).
63
- export function scanBarcodes(data: Uint8Array, width: number, height: number): BarcodeResult[] {
64
- return camera.scanImage(data, width, height)
70
+ /** A live camera as reactive accessors. */
71
+ export type CameraStream = {
72
+ /** Texture id once the stream is up, undefined while opening; render with <texture src={...}>. */
73
+ texture(): number | undefined
74
+ /** Actual stream size, undefined while opening. */
75
+ width(): number | undefined
76
+ height(): number | undefined
77
+ /** The most recently decoded barcode (requires the scan option). */
78
+ barcode(): BarcodeResult | undefined
79
+ /** Set if opening failed (e.g. permission denied). */
80
+ error(): Error | undefined
65
81
  }
66
82
 
67
- export async function openCamera(options: CameraOptions = {}): Promise<Camera> {
68
- let opened = await camera.open(options)
69
- return {
70
- texture: opened.texture,
71
- width: opened.width,
72
- height: opened.height,
73
- onBarcode: (callback: (result: BarcodeResult) => void) => camera.setBarcodeCallback(opened.handle, callback),
74
- close: () => camera.close(opened.handle),
75
- }
76
- }
83
+ /**
84
+ * Opens a camera and exposes it as reactive signals: read texture() in JSX and
85
+ * it appears once the stream is configured. Closes automatically when the
86
+ * reactive owner is disposed (e.g. the component unmounts). For imperative use,
87
+ * call open() from "flux:camera" directly.
88
+ */
89
+ export function createCamera(options: CameraOptions = {}): CameraStream {
90
+ let [texture, setTexture] = createSignal<number | undefined>(undefined)
91
+ let [width, setWidth] = createSignal<number | undefined>(undefined)
92
+ let [height, setHeight] = createSignal<number | undefined>(undefined)
93
+ let [barcode, setBarcode] = createSignal<BarcodeResult | undefined>(undefined)
94
+ let [error, setError] = createSignal<Error | undefined>(undefined)
95
+ let session: Awaited<ReturnType<typeof open>> | undefined
96
+ let disposed = false
97
+
98
+ open(options)
99
+ .then((cam) => {
100
+ if (disposed) {
101
+ cam.close()
102
+ return
103
+ }
104
+ session = cam
105
+ if (options.scan) cam.onBarcode((result) => setBarcode(result))
106
+ setTexture(cam.texture)
107
+ setWidth(cam.width)
108
+ setHeight(cam.height)
109
+ })
110
+ .catch((e) => setError(e instanceof Error ? e : new Error(String(e))))
111
+
112
+ onCleanup(() => {
113
+ disposed = true
114
+ if (session) {
115
+ session.close()
116
+ session = undefined
117
+ }
118
+ })
77
119
 
78
- export { CameraView, type CameraViewProps } from "./camera-view"
120
+ return { texture, width, height, barcode, error }
121
+ }
package/src/color.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { colord, extend } from "colord"
2
+ import namesPlugin from "colord/plugins/names"
3
+ extend([namesPlugin])
4
+
5
+ /**
6
+ * Parses a CSS color string (named, hex, `rgb()`, `hsl()`, ...) into a packed
7
+ * `0xRRGGBBAA` u32: red in the high byte, alpha in the low byte. Alpha is scaled
8
+ * from colord's 0..1 to 0..255. This is the wire format the runtime expects for
9
+ * the `color` property.
10
+ */
11
+ export function parseColor(color: string): number {
12
+ let { r, g, b, a } = colord(color).toRgb()
13
+ return (((r & 0xFF) << 24) | ((g & 0xFF) << 16) | ((b & 0xFF) << 8) | ((a * 255) & 0xFF)) >>> 0
14
+ }
15
+
16
+ // A color stop: `offset` is 0..1 along the gradient, `color` any CSS color string.
17
+ export type GradientStop = { offset: number; color: string }
18
+
19
+ type Stop = { offset: number; color: number }
20
+
21
+ // A gradient fill value, produced by createLinearGradient / createRadialGradient
22
+ // and passed to a paint `color` prop. Coordinates are relative (0..1 of the
23
+ // element's box), so one gradient can be reused on elements of any size. Branded
24
+ // so the renderer can tell it from a solid color string. The object crosses to
25
+ // the runtime as-is and is decoded by key (see properties/paint.rs).
26
+ export type Gradient =
27
+ | { readonly __gradient: "linear"; x0: number; y0: number; x1: number; y1: number; stops: Stop[] }
28
+ | { readonly __gradient: "radial"; cx: number; cy: number; r: number; circle: boolean; stops: Stop[] }
29
+
30
+ /**
31
+ * A linear gradient between two points, each given in 0..1 of the element's box
32
+ * ((0,0) top-left, (1,1) bottom-right). Stops are clamped at the ends.
33
+ */
34
+ export function createLinearGradient(
35
+ x0: number, y0: number, x1: number, y1: number, stops: GradientStop[],
36
+ ): Gradient {
37
+ return { __gradient: "linear", x0, y0, x1, y1, stops: parseStops(stops) }
38
+ }
39
+
40
+ /**
41
+ * A radial gradient centered at `(cx, cy)` (0..1 of the box) with radius `r`
42
+ * (0..1). Defaults to an ellipse that follows the box's aspect ratio; pass
43
+ * `{ shape: "circle" }` to keep a true circle (radius is then a fraction of the
44
+ * shorter side).
45
+ */
46
+ export function createRadialGradient(
47
+ cx: number, cy: number, r: number, stops: GradientStop[], opts?: { shape?: "ellipse" | "circle" },
48
+ ): Gradient {
49
+ return { __gradient: "radial", cx, cy, r, circle: opts?.shape === "circle", stops: parseStops(stops) }
50
+ }
51
+
52
+ export function isGradient(value: unknown): value is Gradient {
53
+ return typeof value === "object" && value !== null && "__gradient" in value
54
+ }
55
+
56
+ function parseStops(stops: GradientStop[]): Stop[] {
57
+ return stops.map((s) => ({ offset: s.offset, color: parseColor(s.color) }))
58
+ }
package/src/core.ts CHANGED
@@ -1,12 +1,4 @@
1
- import { colord, extend } from "colord"
2
- import namesPlugin from "colord/plugins/names"
3
- import type { MeasureTextOptions } from "./types"
4
- extend([namesPlugin])
5
-
6
- export function parseColorToU32(color: string): number {
7
- let { r, g, b, a } = colord(color).toRgb()
8
- return (((r & 0xFF) << 24) | ((g & 0xFF) << 16) | ((b & 0xFF) << 8) | ((a * 255) & 0xFF)) >>> 0
9
- }
1
+ import * as tree from "flux:rendertree"
10
2
 
11
3
  let handlers = new Map<number, Map<string, Function>>()
12
4
 
@@ -36,6 +28,13 @@ export function cleanupNodeHandlers(nodeId: number): void {
36
28
  let focusedNodeId: number | null = null
37
29
  let textInputActive = false
38
30
 
31
+ /**
32
+ * Moves keyboard focus to `nodeId`, or clears it with `null`. Fires `onBlur` on
33
+ * the previously focused node and `onFocus` on the new one. As a side effect,
34
+ * the on-screen keyboard is activated when the newly focused node has an
35
+ * `onTextInput` handler and deactivated otherwise. No-op if the node is already
36
+ * focused.
37
+ */
39
38
  export function setFocus(nodeId: number | null): void {
40
39
  if (nodeId === focusedNodeId) return
41
40
  let oldId = focusedNodeId
@@ -49,7 +48,7 @@ export function setFocus(nodeId: number | null): void {
49
48
  let wantActive = nodeId != null && getEventHandler(nodeId, "onTextInput") != null
50
49
  if (wantActive !== textInputActive) {
51
50
  textInputActive = wantActive
52
- ffi.setTextInputActive(wantActive)
51
+ tree.setTextInputActive(wantActive)
53
52
  }
54
53
  }
55
54
 
@@ -64,15 +63,22 @@ export interface BoundingBox {
64
63
  height: number
65
64
  }
66
65
 
67
- // Returns the node's window-relative bounding box from the most recently
68
- // computed layout, or null if the node has no layout or has not been laid out
69
- // yet. This is a snapshot read, not reactive: call it inside onLayout (or an
70
- // event handler) to get values for the current frame. Phase 1 composes only
71
- // translations; x/y are wrong when a rotate/scale sits anywhere above the node.
66
+ /**
67
+ * Returns the node's window-relative bounding box from the most recently
68
+ * computed layout, or `null` if the node has no layout or has not been laid out
69
+ * yet. This is a snapshot read, not reactive: call it inside `onLayout` (or an
70
+ * event handler) to get values for the current frame. Phase 1 composes only
71
+ * translations; x/y are wrong when a rotate/scale sits anywhere above the node.
72
+ */
72
73
  export function getBoundingBox(node: { id: number }): BoundingBox | null {
73
- return ffi.getBoundingBox(node.id)
74
+ return tree.getBoundingBox(node.id)
74
75
  }
75
76
 
76
- export function measureText(text: string, options?: MeasureTextOptions): { width: number, height: number } {
77
- return ffi.measureText(text, options)
77
+ /**
78
+ * Measures the rendered size of `text` in layout pixels under the given font
79
+ * options (family, size, weight, style, maxLines), without adding it to the
80
+ * tree. Useful for sizing or laying out around text before it is drawn.
81
+ */
82
+ export function measureText(text: string, options?: tree.MeasureTextOptions): { width: number, height: number } {
83
+ return tree.measureText(text, options)
78
84
  }
package/src/gpu.ts CHANGED
@@ -1,34 +1,52 @@
1
- export type DecodedImage = {
2
- data: Uint8Array
3
- width: number
4
- height: number
5
- }
1
+ // GPU textures and shaders, reactive (SolidJS) layer: the create* helpers free
2
+ // their texture automatically when the reactive owner is disposed. The imperative
3
+ // primitive lives in the `flux:gpu` module; import { uploadTexture,
4
+ // setShaderParams, destroyTexture, ... } from "flux:gpu" for non-reactive use.
6
5
 
7
- export function decodeImage(bytes: Uint8Array): DecodedImage {
8
- return gpu.decodeImage(bytes)
9
- }
6
+ import { getOwner, onCleanup } from "@solidjs/signals"
7
+ import * as gpu from "flux:gpu"
10
8
 
9
+ /**
10
+ * Uploads raw RGBA8 pixels to an immutable GPU texture and returns its id (use
11
+ * it as `<texture src={id} />`). `data` must be exactly `width * height * 4`
12
+ * bytes; a mismatch throws. For pixels you intend to mutate and re-upload, use
13
+ * `createMutableTexture` instead. When called inside a reactive scope the
14
+ * texture is freed automatically once that owner is disposed; when called
15
+ * outside one (e.g. after an `await`, where the owner is no longer current)
16
+ * nothing is registered and you must call `destroyTexture` (from flux:gpu)
17
+ * yourself.
18
+ */
11
19
  export function createTexture(data: Uint8Array, width: number, height: number): number {
12
- return gpu.createTexture(data, width, height)
20
+ let id = gpu.createTexture(data, width, height)
21
+ if (getOwner()) onCleanup(() => gpu.destroyTexture(id))
22
+ return id
13
23
  }
14
24
 
15
- // The texture keeps reading from `data` (which may hold multiple frames):
16
- // mutate it in place, then call uploadTexture to push the pixels to the GPU.
25
+ /**
26
+ * Creates a GPU texture you intend to update over time: seed it with `data`,
27
+ * then call `uploadTexture(id, data)` (from flux:gpu) to push new pixels. `data`
28
+ * is RGBA8 and must hold at least `width * height * 4` bytes (it may hold several
29
+ * frames). Like `createTexture`, the texture is freed automatically when the
30
+ * reactive owner is disposed; created outside a reactive scope you must call
31
+ * `destroyTexture` (from flux:gpu) yourself.
32
+ */
17
33
  export function createMutableTexture(data: Uint8Array, width: number, height: number): number {
18
- return gpu.createMutableTexture(data, width, height)
19
- }
20
-
21
- export function uploadTexture(textureId: number, offset: number = 0): void {
22
- gpu.uploadTexture(textureId, offset)
34
+ let id = gpu.createMutableTexture(data, width, height)
35
+ if (getOwner()) onCleanup(() => gpu.destroyTexture(id))
36
+ return id
23
37
  }
24
38
 
25
- // Compile a GLSL ES 3.00 fragment shader and render it into a texture, returning
26
- // the texture id (usable anywhere a normal texture id is, e.g. <texture src>).
27
- // The fragment body may reference vUV (0..1, top-left origin), iResolution,
28
- // iTime, and any `uniform float` it declares; pass their values via `params`.
29
- // `textures` binds each declared `uniform sampler2D` to an existing texture id
30
- // (e.g. a camera or decoded image) so the shader can read it; those inputs are
31
- // re-sampled on every setShaderParams call, so live sources stay current.
39
+ /**
40
+ * Compiles a GLSL ES 3.00 fragment shader and renders it into a texture,
41
+ * returning the texture id (usable anywhere a normal texture id is, e.g.
42
+ * `<texture src>`). The fragment body may reference `vUV` (0..1, top-left
43
+ * origin), `iResolution`, `iTime`, and any `uniform float` it declares; pass
44
+ * their values via `params`. `textures` binds each declared `uniform sampler2D`
45
+ * to an existing texture id (e.g. a camera or decoded image) so the shader can
46
+ * read it; those inputs are re-sampled on every `setShaderParams` call, so live
47
+ * sources stay current. Frees the texture and shader program when the reactive
48
+ * owner is disposed; create outside any reactive scope for app-lifetime shaders.
49
+ */
32
50
  export function createShader(
33
51
  fragmentSrc: string,
34
52
  width: number,
@@ -36,11 +54,7 @@ export function createShader(
36
54
  params?: Record<string, number>,
37
55
  textures?: Record<string, number>,
38
56
  ): number {
39
- return gpu.createShader(fragmentSrc, width, height, params, textures)
40
- }
41
-
42
- // Re-render an existing shader texture with new param values and request a
43
- // frame. Use this to animate (e.g. update iTime each frame).
44
- export function setShaderParams(textureId: number, params: Record<string, number>): void {
45
- gpu.setShaderParams(textureId, params)
57
+ let id = gpu.createShader(fragmentSrc, width, height, params, textures)
58
+ if (getOwner()) onCleanup(() => gpu.destroyTexture(id))
59
+ return id
46
60
  }
package/src/image.ts ADDED
@@ -0,0 +1,19 @@
1
+ // CPU image codec: decode encoded image bytes into raw RGBA8 pixels (and, in
2
+ // future, encode them back). Kept separate from the GPU/texture APIs because no
3
+ // GPU is involved; pair decodeImage with createTexture from "@solidrt/core/gpu"
4
+ // to upload the result.
5
+
6
+ export type DecodedImage = {
7
+ data: Uint8Array
8
+ width: number
9
+ height: number
10
+ }
11
+
12
+ /**
13
+ * Decodes encoded image bytes (PNG, JPEG, and the other formats the runtime's
14
+ * image decoder supports) into raw, tightly-packed RGBA8 pixels plus the
15
+ * decoded dimensions. Feed the result straight into `createTexture`.
16
+ */
17
+ export function decodeImage(bytes: Uint8Array): DecodedImage {
18
+ return image.decodeImage(bytes)
19
+ }
package/src/index.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  export * from "./renderer"
2
- export { setFocus, getFocusedNodeId, measureText, getBoundingBox, parseColorToU32 } from "./core"
2
+ export { setFocus, getFocusedNodeId, measureText, getBoundingBox } from "./core"
3
3
  export type { BoundingBox } from "./core"
4
+ export { parseColor, createLinearGradient, createRadialGradient } from "./color"
5
+ export type { Gradient, GradientStop } from "./color"
4
6
  export { onFrame, onLayout, onResize, onWindowFocus, onWindowBlur } from "./window"
5
- export { createTexture, decodeImage } from "./gpu"
6
- export type { DecodedImage } from "./gpu"
7
+ export { windowSize, safeArea, displayScale, windowFocused, keyboardHeight } from "./window"
8
+ export { createTexture } from "./gpu"
9
+ export { decodeImage } from "./image"
10
+ export type { DecodedImage } from "./image"
7
11
  export type {
8
12
  LayoutProps,
9
13
  TransformProps,
@@ -22,6 +26,6 @@ export type {
22
26
  TextProps,
23
27
  TextureProps,
24
28
  AudioProps,
25
- MeasureTextOptions,
26
29
  Color,
27
30
  } from "./types"
31
+ export type { MeasureTextOptions } from "flux:rendertree"
package/src/microphone.ts CHANGED
@@ -1,40 +1,58 @@
1
- // Microphone capture. A session delivers raw mono float32 samples at the
2
- // requested sample rate (the device format is converted by SDL); read()
3
- // drains whatever was captured since the last call. Captured audio buffers
4
- // until read, so poll read() regularly (e.g. once per frame) while open.
1
+ // Microphone capture, reactive (SolidJS) layer. A session delivers raw mono
2
+ // float32 samples at the requested sample rate (the device format is converted
3
+ // by SDL); read() drains whatever was captured since the last call. Captured
4
+ // audio buffers until read, so poll read() regularly (e.g. once per frame) while
5
+ // open.
6
+ //
7
+ // The imperative primitive lives in the `flux:microphone` module; import
8
+ // { open, listMicrophones } from "flux:microphone" for non-reactive use.
5
9
 
6
- export type MicrophoneInfo = {
7
- id: number
8
- name: string
9
- }
10
+ import { createSignal, onCleanup } from "@solidjs/signals"
11
+ import { open } from "flux:microphone"
10
12
 
11
13
  export type MicrophoneOptions = {
12
- /** Explicit device id from listMicrophones(); default is the system default recording device. */
14
+ /** Explicit device id from flux:microphone listMicrophones(); default is the system default. */
13
15
  microphone?: number
14
16
  /** Sample rate of the delivered samples (the device rate is converted). Default 16000. */
15
17
  sampleRate?: number
16
18
  }
17
19
 
18
- export type Microphone = {
19
- /** Sample rate of read() samples. */
20
- sampleRate: number
21
- /** Drain the mono float32 samples captured since the last read. */
20
+ /** A live microphone with reactive lifecycle. */
21
+ export type MicrophoneStream = {
22
+ /** Sample rate of read() samples (0 if opening failed). */
23
+ sampleRate(): number
24
+ /** Drain the mono float32 samples captured since the last read (empty if not open). */
22
25
  read(): Float32Array
23
- /** Release the device. */
24
- close(): void
26
+ /** Set if opening failed. */
27
+ error(): Error | undefined
25
28
  }
26
29
 
27
- export function listMicrophones(): MicrophoneInfo[] {
28
- return microphone.listMicrophones()
29
- }
30
+ /**
31
+ * Opens a microphone and owns its lifecycle: closes when the reactive owner is
32
+ * disposed. Capture stays pull-based, so read() drains samples on demand (e.g.
33
+ * once per frame). For imperative use, call open() from "flux:microphone".
34
+ */
35
+ export function createMicrophone(options: MicrophoneOptions = {}): MicrophoneStream {
36
+ let [error, setError] = createSignal<Error | undefined>(undefined)
37
+ let session: ReturnType<typeof open> | undefined
38
+ let rate = 0
39
+ try {
40
+ session = open(options)
41
+ rate = session.sampleRate
42
+ } catch (e) {
43
+ setError(e instanceof Error ? e : new Error(String(e)))
44
+ }
45
+
46
+ onCleanup(() => {
47
+ if (session) {
48
+ session.close()
49
+ session = undefined
50
+ }
51
+ })
30
52
 
31
- // Async to leave room for an OS permission prompt on platforms that need one
32
- // (the desktop backends open synchronously).
33
- export async function openMicrophone(options: MicrophoneOptions = {}): Promise<Microphone> {
34
- let opened = microphone.open(options)
35
53
  return {
36
- sampleRate: opened.sampleRate,
37
- read: () => microphone.read(opened.handle),
38
- close: () => microphone.close(opened.handle),
54
+ sampleRate: () => rate,
55
+ read: () => (session ? session.read() : new Float32Array(0)),
56
+ error,
39
57
  }
40
58
  }