@solidrt/core 0.0.24 → 0.0.25

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.
Files changed (3) hide show
  1. package/package.json +3 -2
  2. package/src/gpu.ts +7 -0
  3. package/src/sound.ts +119 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/core",
3
- "version": "0.0.24",
3
+ "version": "0.0.25",
4
4
  "license": "MIT",
5
5
  "author": "Antoine van Wel",
6
6
  "type": "module",
@@ -11,6 +11,7 @@
11
11
  "./gpu": "./src/gpu.ts",
12
12
  "./image": "./src/image.ts",
13
13
  "./microphone": "./src/microphone.ts",
14
+ "./sound": "./src/sound.ts",
14
15
  "./speech-recognition": "./src/speech-recognition.ts",
15
16
  "./text-input": "./src/text-input.ts",
16
17
  "./jsx-runtime": "./jsx-runtime.d.ts",
@@ -26,7 +27,7 @@
26
27
  "colord": "^2.9.3"
27
28
  },
28
29
  "devDependencies": {
29
- "@solidrt/flux-types": "0.0.24"
30
+ "@solidrt/flux-types": "0.0.25"
30
31
  },
31
32
  "peerDependencies": {
32
33
  "@solidjs/signals": "2.0.0-beta.15",
package/src/gpu.ts CHANGED
@@ -19,6 +19,13 @@ import * as gpu from "flux:gpu"
19
19
  // `<texture params={...}>` when a `<texture>` element is already in the tree.
20
20
  export { destroyTexture, setShaderParams, uploadTexture } from "flux:gpu"
21
21
 
22
+ // captureSnapshot renders a node to a texture and readTexture reads any
23
+ // texture's bytes back. Re-exported raw (no reactive auto-cleanup wrapper):
24
+ // captureSnapshot resolves asynchronously, by which point the reactive owner is
25
+ // no longer current, so the caller owns the returned id and frees it with
26
+ // destroyTexture (as with any texture created after an await).
27
+ export { captureSnapshot, readTexture } from "flux:gpu"
28
+
22
29
  /**
23
30
  * Uploads raw RGBA8 pixels to an immutable GPU texture and returns its id (use
24
31
  * it as `<texture src={id} />`). `data` must be exactly `width * height * 4`
package/src/sound.ts ADDED
@@ -0,0 +1,119 @@
1
+ // Sound playback, reactive (SolidJS) layer. `createSound` decodes an encoded
2
+ // clip (Ogg/Vorbis or WAV) once and owns its lifecycle: the decoded clip is
3
+ // released, and any playing voices stopped, when the reactive owner is disposed.
4
+ // Each play() is cheap (no re-decode). `createSoundStream` is the same but reads
5
+ // a large track from a file path on demand instead of decoding it into memory.
6
+ //
7
+ // The imperative primitive lives in the `flux:audio` module; import
8
+ // { play, load, stream } from "flux:audio" for non-reactive use.
9
+
10
+ import { createSignal, onCleanup } from "@solidjs/signals"
11
+ import { load, stream } from "flux:audio"
12
+ import { file } from "flux:fs"
13
+
14
+ type FluxFile = ReturnType<typeof file>
15
+
16
+ type LoadedSound = ReturnType<typeof load>
17
+
18
+ export type SoundOptions = {
19
+ /** Repeat the clip until stopped. Defaults to false. */
20
+ loop?: boolean
21
+ /** Volume scale, 1.0 leaves the clip unchanged. Defaults to 1.0. */
22
+ gain?: number
23
+ /**
24
+ * Let play() stack overlapping voices instead of restarting. Defaults to
25
+ * true: rapid triggers overlap. Set false for a single-voice sound where each
26
+ * play() cuts off the previous one.
27
+ */
28
+ overlap?: boolean
29
+ }
30
+
31
+ /** Options for a streamed sound. Streams are always single-voice. */
32
+ export type SoundStreamOptions = {
33
+ /** Repeat the track until stopped. Defaults to false. */
34
+ loop?: boolean
35
+ /** Volume scale, 1.0 leaves the track unchanged. Defaults to 1.0. */
36
+ gain?: number
37
+ }
38
+
39
+ /** A decoded sound with reactive lifecycle. */
40
+ export type Sound = {
41
+ /** Start the clip. Overlaps or restarts per the `overlap` option. */
42
+ play(): void
43
+ /** Stop every voice started from this sound. */
44
+ stop(): void
45
+ /** True after play() until stop() (does not track natural completion). */
46
+ playing(): boolean
47
+ /** Set if loading failed. */
48
+ error(): Error | undefined
49
+ }
50
+
51
+ // Shared reactive wrapper: owns the loaded handle, tracks live voices, and
52
+ // disposes both on cleanup. `loader` runs once (may throw -> error signal).
53
+ function reactiveSound(
54
+ loader: () => LoadedSound,
55
+ overlap: boolean,
56
+ playOptions: { loop?: boolean; gain?: number },
57
+ ): Sound {
58
+ let [error, setError] = createSignal<Error | undefined>(undefined, { ownedWrite: true })
59
+ let [playing, setPlaying] = createSignal(false, { ownedWrite: true })
60
+
61
+ let handle: LoadedSound | undefined
62
+ let voices: { stop(): void }[] = []
63
+ try {
64
+ handle = loader()
65
+ } catch (e) {
66
+ setError(e instanceof Error ? e : new Error(String(e)))
67
+ }
68
+
69
+ let stopAll = () => {
70
+ for (let v of voices) v.stop()
71
+ voices = []
72
+ setPlaying(false)
73
+ }
74
+
75
+ onCleanup(() => {
76
+ stopAll()
77
+ if (handle) {
78
+ handle.unload()
79
+ handle = undefined
80
+ }
81
+ })
82
+
83
+ return {
84
+ play() {
85
+ if (!handle) return
86
+ if (!overlap) stopAll()
87
+ voices.push(handle.play(playOptions))
88
+ setPlaying(true)
89
+ },
90
+ stop: stopAll,
91
+ playing,
92
+ error,
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Decodes a sound once and owns its lifecycle: releases the clip and stops its
98
+ * voices when the reactive owner is disposed. play() replays without decoding.
99
+ * For imperative use, call load()/play() from "flux:audio".
100
+ */
101
+ export function createSound(source: Uint8Array, options: SoundOptions = {}): Sound {
102
+ return reactiveSound(() => load(source), options.overlap ?? true, {
103
+ loop: options.loop,
104
+ gain: options.gain,
105
+ })
106
+ }
107
+
108
+ /**
109
+ * Streams a large track, decoding on demand instead of loading it into memory.
110
+ * Single-voice: each play() restarts it. Pass a path (resolved like flux:fs,
111
+ * relative to the process cwd) or a `file()` from flux:fs; a path is wrapped in
112
+ * `file()` for you, so a dev-server-proxied file streams over the proxy. Owns
113
+ * the stream's lifecycle: stopped and released when the reactive owner is
114
+ * disposed. For imperative use, call stream()/play() from "flux:audio".
115
+ */
116
+ export function createSoundStream(source: string | FluxFile, options: SoundStreamOptions = {}): Sound {
117
+ let src = typeof source === "string" ? file(source) : source
118
+ return reactiveSound(() => stream(src), false, { loop: options.loop, gain: options.gain })
119
+ }