@m4l-jweb/surface 1.1.0 → 1.2.1
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/package.json +2 -2
- package/src/index.ts +52 -2
- package/src/react.tsx +22 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m4l-jweb/surface",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "m4l-jweb: declare a device's Live parameters as code - the surface Push actually sees - plus a mocked-Live dev harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
],
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"react": ">=18",
|
|
27
|
-
"@m4l-jweb/bridge": "1.1
|
|
27
|
+
"@m4l-jweb/bridge": "1.2.1"
|
|
28
28
|
},
|
|
29
29
|
"peerDependenciesMeta": {
|
|
30
30
|
"react": {
|
package/src/index.ts
CHANGED
|
@@ -164,6 +164,30 @@ export interface WindowSpec {
|
|
|
164
164
|
* primitive rather than changing the first.
|
|
165
165
|
*/
|
|
166
166
|
audio?: boolean;
|
|
167
|
+
/**
|
|
168
|
+
* Output latency of the sounding window's `[jweb~]`, in milliseconds - the ring
|
|
169
|
+
* buffer between Chromium's audio thread and MSP. Max 9 documents 0 as the
|
|
170
|
+
* minimum (~23 ms at 44.1kHz, ~21 ms at 48kHz), warns that the minimum "may
|
|
171
|
+
* result in occasional drop-outs or distortion", and caps the maximum at three
|
|
172
|
+
* times the minimum. Unset leaves the object's own default.
|
|
173
|
+
*
|
|
174
|
+
* Only meaningful with `audio: true`; ignored otherwise.
|
|
175
|
+
*/
|
|
176
|
+
latency?: number;
|
|
177
|
+
/**
|
|
178
|
+
* The `rendermode` attribute stamped on the window's jweb object:
|
|
179
|
+
* 0 = onscreen, 1 = offscreen. The Max reference notes offscreen "is slower".
|
|
180
|
+
* Default is 1, which is what every generated window has shipped with so far.
|
|
181
|
+
*/
|
|
182
|
+
rendermode?: 0 | 1;
|
|
183
|
+
/**
|
|
184
|
+
* Report interval of the window's level tap (`[peakamp~ N]`), in milliseconds.
|
|
185
|
+
* Default 10, which is 100 float messages per second per channel into the
|
|
186
|
+
* wrapper's [js]. Raise it to trade meter smoothness for message traffic.
|
|
187
|
+
*
|
|
188
|
+
* Only meaningful with `audio: true`; ignored otherwise.
|
|
189
|
+
*/
|
|
190
|
+
levelInterval?: number;
|
|
167
191
|
/**
|
|
168
192
|
* Window content from a PREBUILT static directory rather than a component of
|
|
169
193
|
* ours - a whole site, built by something else (its own Astro/vite build), and
|
|
@@ -241,13 +265,39 @@ export const dial = (spec: Omit<DialSpec, "kind">): DialSpec => ({
|
|
|
241
265
|
* currently IS (name, unit, range - see describeParam in @m4l-jweb/bridge), and
|
|
242
266
|
* keeps the scaling straight.
|
|
243
267
|
*/
|
|
244
|
-
|
|
268
|
+
/** 0 .. N-1 as a union. The recursion is bounded by the count the caller wrote. */
|
|
269
|
+
type UpTo<N extends number, Acc extends number[] = []> = Acc["length"] extends N ? Acc[number] : UpTo<N, [...Acc, Acc["length"]]>;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The dials are numbered from 1, and TypeScript cannot add. A lookup tuple is the
|
|
273
|
+
* whole trick: index 0 holds 1, index 7 holds 8. It stops at 32, which is four Push
|
|
274
|
+
* banks - past that `knobPool` still WORKS, its keys just widen back to `string` and
|
|
275
|
+
* `useParam(surface, "s33")` stops being checked.
|
|
276
|
+
*/
|
|
277
|
+
type OneBased = [
|
|
278
|
+
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
|
279
|
+
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32,
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
/** `PoolIds<8>` is `"s1" | ... | "s8"`. */
|
|
283
|
+
export type PoolIds<N extends number, P extends string = "s"> = `${P}${OneBased[UpTo<N>] & number}`;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The KEYS are the point, not just the values. `params: { ...knobPool(8) }` has to
|
|
287
|
+
* leave `keyof P` carrying `s1`..`s8`, or the spread widens the surface to an index
|
|
288
|
+
* signature and every downstream `useParam(surface, "s3")` and `layout.native.params`
|
|
289
|
+
* entry stops type-checking against the declaration it came from.
|
|
290
|
+
*/
|
|
291
|
+
export const knobPool = <N extends number, P extends string = "s">(
|
|
292
|
+
count: N,
|
|
293
|
+
prefix: P = "s" as P,
|
|
294
|
+
): Record<PoolIds<N, P>, DialSpec> => {
|
|
245
295
|
const out: Record<string, DialSpec> = {};
|
|
246
296
|
for (let i = 1; i <= count; i++) {
|
|
247
297
|
// The short name is what Push prints when nothing has borrowed the slot yet.
|
|
248
298
|
out[`${prefix}${i}`] = dial({ range: [0, 1], default: 0, short: `${prefix.toUpperCase()}${i}` });
|
|
249
299
|
}
|
|
250
|
-
return out
|
|
300
|
+
return out as Record<PoolIds<N, P>, DialSpec>;
|
|
251
301
|
};
|
|
252
302
|
export const toggle = (spec: Omit<ToggleSpec, "kind">): ToggleSpec => ({
|
|
253
303
|
kind: "toggle",
|
package/src/react.tsx
CHANGED
|
@@ -212,6 +212,21 @@ export interface PooledControl extends BorrowedControl {
|
|
|
212
212
|
* minimum - the bug that got an earlier attempt at this reverted. The wrapper
|
|
213
213
|
* answers whether each range took, and this hook scales exactly once either way.
|
|
214
214
|
*
|
|
215
|
+
* `widenRange` IS OFF BY DEFAULT, and that is a measured decision rather than
|
|
216
|
+
* caution. A dial whose range was widened at runtime stops following its automation
|
|
217
|
+
* lane and any Rack macro mapped to it: both keep driving the parameter in the
|
|
218
|
+
* BUILD-TIME domain the frozen device gave them, so the value lands at the bottom of
|
|
219
|
+
* the new range and the dial never moves, silently. Left alone, the dials stay 0..1,
|
|
220
|
+
* this hook does the scaling, and automation, macros and Push all keep working - at
|
|
221
|
+
* the cost of the dial's own readout showing 0.44 rather than 600 Hz. Turn it on
|
|
222
|
+
* only where that readout is worth more than the automation.
|
|
223
|
+
*
|
|
224
|
+
* `describe: false` borrows and scales as usual but says NOTHING to Live. A device
|
|
225
|
+
* with two pages - a device view and a window - has two React trees against one
|
|
226
|
+
* pool, and both calling this means the last render wins: the dial ends up named
|
|
227
|
+
* after whichever page rendered most recently, flickering between them. Exactly one
|
|
228
|
+
* page should describe. The other still gets its `PooledControl[]` to draw.
|
|
229
|
+
*
|
|
215
230
|
* KNOWN LIMIT, measured in Live: the name reaches the DEVICE PANEL, not Live's
|
|
216
231
|
* parameter registry or a Rack macro picker, which keep the pool's own `S1..S8`.
|
|
217
232
|
* A frozen device cannot rename a parameter there. So render the name in your own
|
|
@@ -221,6 +236,7 @@ export function useControls<P extends Record<string, ParamSpec>>(
|
|
|
221
236
|
surface: Surface<P>,
|
|
222
237
|
controls: readonly BorrowedControl[],
|
|
223
238
|
poolIds: readonly Extract<keyof P, string>[],
|
|
239
|
+
options: { widenRange?: boolean; describe?: boolean } = {},
|
|
224
240
|
): PooledControl[] {
|
|
225
241
|
/* eslint-disable react-hooks/rules-of-hooks */
|
|
226
242
|
// A pool is dials, so its values are numbers - but `P` is the whole surface and
|
|
@@ -229,6 +245,8 @@ export function useControls<P extends Record<string, ParamSpec>>(
|
|
|
229
245
|
const params = poolIds.map((id) => useParam(surface, id)) as unknown as [number, (v: number) => void][];
|
|
230
246
|
/* eslint-enable react-hooks/rules-of-hooks */
|
|
231
247
|
|
|
248
|
+
const { widenRange = false, describe = true } = options;
|
|
249
|
+
|
|
232
250
|
/** Which dials Live actually widened. Until it says so, the dial is 0..1. */
|
|
233
251
|
const [real, setReal] = useState<boolean[]>([]);
|
|
234
252
|
useEffect(() => {
|
|
@@ -250,6 +268,7 @@ export function useControls<P extends Record<string, ParamSpec>>(
|
|
|
250
268
|
// parameter attributes, and a device re-rendering is not news.
|
|
251
269
|
const described = useRef<string[]>([]);
|
|
252
270
|
useEffect(() => {
|
|
271
|
+
if (!describe) return;
|
|
253
272
|
for (let i = 0; i < poolIds.length; i++) {
|
|
254
273
|
const c = controls[i];
|
|
255
274
|
const key = c ? `${c.name} ${c.min} ${c.max} ${c.unit ?? ""}` : "";
|
|
@@ -257,10 +276,10 @@ export function useControls<P extends Record<string, ParamSpec>>(
|
|
|
257
276
|
described.current[i] = key;
|
|
258
277
|
// A slot nobody is borrowing goes back to its declared identity, or it would
|
|
259
278
|
// keep the name of a control that is no longer there.
|
|
260
|
-
if (c) describeParam(poolIds[i], { name: c.name, unit: c.unit, range: [c.min, c.max] });
|
|
261
|
-
else describeParam(poolIds[i], { name: String(poolIds[i]).toUpperCase(), range: [0, 1] });
|
|
279
|
+
if (c) describeParam(poolIds[i], { name: c.name, unit: c.unit, range: [c.min, c.max], widenRange });
|
|
280
|
+
else describeParam(poolIds[i], { name: String(poolIds[i]).toUpperCase(), range: [0, 1], widenRange });
|
|
262
281
|
}
|
|
263
|
-
}, [controls, poolIds]);
|
|
282
|
+
}, [controls, poolIds, widenRange, describe]);
|
|
264
283
|
|
|
265
284
|
// Seed a slot the first time a given control takes it, so an untouched dial reads
|
|
266
285
|
// what the control says rather than 0.
|