@tsln/live-object
LiveObject<C> wraps Max for Live's LiveAPI for Live Object Model class C, so member
names, value types and function arguments are checked against
@tsln/lom-types.
import LiveObject = require("./vendor/live-object");
const track = LiveObject.at("live_set tracks 0"); // LiveObject<"Track"> | null, class inferred from the path
const song = LiveObject.song(); // LiveObject<"Song"> | null; also LiveObject.app(), LiveObject.thisDevice()
if (!track || !song) return;
track.name; // string, read from Live on each access
track.mute = 1; // sends `set mute 1`
track.devices; // LiveObject<"Device">[]
track.insert_device("Simpler", 0); // checked arguments
track.get("name"); // the same members through get(), set(), call() and get_count()
const observer = track.observe("devices", (devices) => post(devices.length, "\n"));
observer?.unobserve(); // or track.unobserve("devices")
for (const device of track.devices) {
if (device.is("SimplerDevice")) device.sample; // narrowed to the subclass
}
new LiveObject<"DeviceIO">(path); // for paths only known at runtime
Children come back as LiveObjects (or null), list children and list properties as arrays,
dictionary properties as parsed objects. Accessors read from Live on every access and return
new objects each time, so store a result in a variable when using it more than once. Like all
Live API access, none of this works in a script's global code; wait for live.thisdevice to bang.
What it costs
A LiveAPI object is the expensive part of reaching Live from JavaScript. Measured in Live 12.4:
- Reading a property of an object you already hold takes about 35 µs, as it does with
live.object. - Making a
LiveAPIobject takes 0.4 to 0.8 ms. - Every
LiveAPIobject made adds to the cost of every later call to the Live API, by about 6 µs for each 1000 objects, and more than that for calls from JavaScript. - A
LiveAPIobject is not given back when nothing refers to it any more. It stays until the script is reloaded or the device removed.
So what counts is how many LiveAPI objects a script makes while it runs, and a LiveObject
makes as few as it can:
- It makes its
LiveAPIobject (api) when it is first used, and not before. The objects in a list of children cost nothing until they are used, and aLiveObjectmade from an id answersidwithout asking Live, so comparing the ids ofsong.trackswith the ones you know makes noLiveAPIobjects. - The
LiveObjects made from the same id share oneLiveAPIobject, as do the ones made fromlive_set,live_appandthis_device. Readingtrack.devicesagain and again makes one for each device, once. TheLiveObjects themselves stay separate, each with its own observers. - The class of an object, which the accessors need, is asked for once.
observe()on an object that hasn't been used yet observes with aLiveAPIobject of theLiveObject's own, which it then uses for everything else. Otherwise, and for every further member, it makes one for the member.
A LiveObject made from any other path, such as live_set tracks 0, has a LiveAPI object to
itself, because the path can come to mean another object. Keep those in variables.
Install
npm install --save-dev @tsln/live-object
Max's require() searches Max's file path and knows nothing of node_modules, so the
package's single CommonJS file, dist/index.js, has to be copied or bundled to somewhere Max
can find it (the max-msp repository bundles it to js/vendor/live-object.js with esbuild).
The types come along with a declaration file next to it:
// src/vendor/live-object.d.ts
import LiveObject = require("@tsln/live-object");
export = LiveObject;
Importing the package pulls in @tsln/max-types and @tsln/lom-types, which it depends on.
List them in types too if scripts use Lom.* or Max globals without importing it.
Files
src/index.tsisLiveObject.src/live-value.tsconverts between LiveAPI atom lists and the valuesLiveObjectexposes.src/lom-meta.tsis generated bypnpm generate:lomat the root of this repository: the members whose values are always lists, and each class's own members, which the dynamic accessors are resolved from.