c74/M4L/Idea/Alits
Properties
- tags
- Idea, Typescript, c74/M4L
#Alits - #Ableton Live #Typescript #API
- Alits stands for Ableton Live TypeScript - it's an idiomatic, Functional Reactive Programming typescript library for Ableton Live.
Problem Introduction
- The Max for Live API that's accessible in Javascript is not a typical JS or typescript API. Much if not most of the Max/MSP language is about sending messages to objects, and Ableton Live API designed for Max for Live (M4L) inherits the characteristics and assumptions of that approach. Fundamentally, the Ableton Live API in Max/MSP is a string manipulation and messaging API. In c74/max/obj/js, the Ableton/Live Object Modelrelies heavily on instantiating c74/M4L/obj/LiveAPI with c74/M4L/obj/live.path strings, calling getters and setters with strings, and using callbacks rather than promises to receive updates to properties.
Getting an object reference that points to an Ableton Live entity
- A typical API in js/ts would mirror the UML diagram or Entity Relationship Diagram in API objects that return references to each other. For example, a
Trackobject would have aget_clips()function that returnedClipobjects. - In the Max/MSP-centric M4L API, however, one needs to construct paths and validate them. For example, to get clips from a track, one needs to construct a Live Object Model path using a live object model string path, and then instantiate a new
LiveAPIwith that path. As described in the c74/M4L/Ref/Live API Overview,Live objects are accessed using paths according to the Live Object Model. For example, the first clip in the third track can be accessed by the path
live_set tracks 2 clip_slots 0 clip. Alternatively, it can be accessed vialive_set scenes 0 clip_slots 2 clip. Or, if the clip is shown in the detail view, vialive_set view detail_clip.
- To get a reference to any object, one first needs to construct a string path and validate that it refers to anything.
- A typical API in js/ts would mirror the UML diagram or Entity Relationship Diagram in API objects that return references to each other. For example, a
Calling a method on an object that points to an Ableton Live entity
- After obtaining a reference to an object that points to an entity in Ableton Live, calling methods on that object requires some string processing and validation to send messages to it. Quoting the c74/M4L/Ref/Live API Overview/Functions section here,
Many Live objects have functions which can be called by sending call and the function name to live.object, like
call create_scenefor aSongobject.
- See also c74/M4L/Ref/Creating Devices that use the Live API/Calling a function section here, which describes the Max/MSP Live API method of calling a function.
- This
callis mirrored in the c74/M4L/Ref/jsliveapi; see call documentation here: - So, for example, to call Clip.add_new_notes, one must
- After obtaining a reference to an object that points to an entity in Ableton Live, calling methods on that object requires some string processing and validation to send messages to it. Quoting the c74/M4L/Ref/Live API Overview/Functions section here,
Accessing properties of an object that points to an Ableton Live entity
- A typical API in js/ts would allow access attributes directly, for example,
Clip.get_file_path()orClip.get_length(), rather than calling.get(attribute)and then validating if the attribute exists and coercing the results to types in javascript. - Accessing any of the object's members is also string- and messaging-centric; quoting c74/M4L/Ref/Creating Devices that use the Live API here,
The live.path object is used to navigate to the Live objects whose functions you want to call. Each property of the Live object model in a session is associated with an id specific to that particular Song, Track, Clip, Clip Slot, Device, etc.
Sending a message to a live.path object results in an object id being sent out the left outout (where the id follows the object) or middle outlet (where the id follows the path).
- This API is mirrored in the c74/M4L/Ref/jsliveapi/Methods/get (see
getreference documentation here), where to get a property, one needs to call the<LiveAPIInstance>.get(property), wherepropertyhere is, for example, in the case of Clip,file_path,length, or any other instance.
- A typical API in js/ts would allow access attributes directly, for example,
Callbacks and Observing Properties of an object that points to an Ableton Live entity
- In addition to calling c74/M4L/Ref/jsliveapi/Methods/get, there's another way to observe a changes to a property by using c74/M4L/Ref/jsliveapi/Properties/property - LiveAPI.property, which is described as:
The observed property, child or child-list of the object at the current path, if desired. For instance, if the LiveAPI object refers to "live_set tracks 1", setting the property to "mute" would cause changes to the "mute" property of the 2nd track to be reported to the callback function defined in the LiveAPI Constructor.
- For example, the reference docs for c74/M4L/Ref/LOM/Track/Properties/color here is
- So, to observe the Track.color property when the Ableton Live set user changes the color of a particular track, one would do something like:
- ts
const childTrack = new LiveAPI((iargs: IArguments) => trackColorChangedCallback(iargs), "id " + childTrackId) state.colors.push(childTrack.get('color')) childTrack.property = 'color'- Credit: adapted from GitHub/zsteinkamp/m4l-ChiasticSlide by Person/Zack Steinkamp here
- So after this snippet, whenever the user changes the color of a track, the
trackColorChangedCallbackfunction will be called withIArguments.IArgumentsis the typescript interface for the JS-nativeargumentsthat any javascript function is called with; see also Typescript/tsconfig/noLib here.
- In order to turn
IArgumentsinto a JS/Array, one first needs to call c74/max/js/Ref/JS Global Methods/jsthis Methods/arrayfromargs hereThe arrayfromargs() method will convert the arguments property to an
Array, optionally with message as the zeroth element of the array.- While the documentation states "optionally with message as the zeroth element of the array," I couldn't find any way to disable this feature; ironically, the
argumentstoarrayfromargsis undocumented as far as I can tell.
- So by calling
const args = arrayfromargs(iargs), nowargscontains the arguments that the callback was called with as a JS/Array. By looking at thecolorproperty documentation and combined with thearrayfromargsdocumentation above, we can presume that the arguments would be the of length two, and the first argument would be the name of the property,color, and the second would be the value of the property, the newly updated value of thecolorproperty as anint.
- This is particularly unidiomatic. In a modern typescript application, one would observe a property using Promises and utilizing
asyncandawaitin functions to process the resulting updates. Alternatively, and, in my perspective, ideally, some APIs are coded so as to assume a Functional Reactive Programming (FRP) paradigm, for example, using RxJS. For a description of #Observable and this paradigm, see Person/Tim Dwyer's #Blog post Functional Reactive Programming | Tim’s code stuff.
- In addition to calling c74/M4L/Ref/jsliveapi/Methods/get, there's another way to observe a changes to a property by using c74/M4L/Ref/jsliveapi/Properties/property - LiveAPI.property, which is described as:
Problem Example
- For example, a function to get all the c74/M4L/Ref/LOM/Clips from a track (see also Live Object Model - Clip) could be written like the following,
- ts
function getClips(track: LiveAPI): LiveAPI[] { const clips: LiveAPI[] = [] const clipCount = track.getcount('arrangement_clips') const basePath = track.unquotedpath for (let clipIndex = 0; clipIndex < clipCount; clipIndex++) { const clipPath = `${basePath} arrangement_clips ${clipIndex}` try { const clip = createValidatedLiveApi(clipPath) logger(`Found clip ${clipIndex}:`) logger(`- name: ${clip.get('name')}`) logger(`- length: ${clip.get('length')}`) clips.push(clip) } catch (error) { warn(`Error accessing clip ${clipIndex}: ${error.message}`) // Continue to next iteration } } return clips }
- After after obtaining an array of Clip object references with
getClips(track), accessing the members of the objects is also done using a string-based API, as one can see from these lines above: - Above, inside
createValidatedLiveApiis a custom function to validate the path and raise an error if it doesn't refer to anything, or return aLiveAPIobject if it is valid:- ts
/** * Checks if a LiveAPI object is invalid or doesn't exist. * * In Max's JS environment, a LiveAPI object is considered invalid if: * - Its id property is "0" (returned as a string primitive) * - Its info property is "No object" (returned as a string primitive) * * @param api The LiveAPI object to validate * @returns true if the LiveAPI object is invalid/doesn't exist, false otherwise * * @example * const api = new LiveAPI(() => {}, 'live_set') * if (isInvalidLiveApiPath(api)) { * warn('Invalid or nonexistent Live API path') * } */ export function isInvalidLiveApiPath(api: LiveAPI): boolean { return Number(api.id) === 0 || String(api.info) === "No object" } /** * Creates a LiveAPI object at the given path and verifies it exists. * @param path The path to create the LiveAPI object at * @param callback Optional callback function for the LiveAPI constructor * @returns A valid LiveAPI object * @throws InvalidLiveApiPathError if the path doesn't exist */ export function createValidatedLiveApi( path: string, callback: () => void = () => {} ): LiveAPI { const api = new LiveAPI(callback, path) if (isInvalidLiveApiPath(api)) { warn('----------------------------------------') warn('Invalid Live API Path Error:') warn(`Path: ${path}`) warn(`Info: ${api.info}`) warn(`ID: ${api.id}`) warn('----------------------------------------') throw new Error(`Invalid Live API path: ${path}`) } return api }
- For example, a function to get all the c74/M4L/Ref/LOM/Clips from a track (see also Live Object Model - Clip) could be written like the following,
Suggested API - An #Observable Typescript/Library for Live API
- The core characteristics of a more idiomatic typescript library would be:
- The library would be oriented towards a Functional Reactive Programming paradigm using RxJS.
- The library would abstract away and hide the details of using the Ableton/Live Object Model paths from c74/M4L/obj/live.path and treat that as a low-level implementation detail. Advanced users could still utilize methods that enabled using this functionality, but the core assumption of the Alits API is that it would no longer be necessary.
- Each object in the Ableton/Live Object Model would have its own Typescript class.
- Each object would have a
get_children()function that would return an Array of objects of the appropriate Typescript classes in the API. - Each property
- that has
getaccess has a<ObjectInstance>.get_<propertyname>()method. - that has
setaccess has a<ObjectInstance>.set_<propertyname>(...)method. - that has
observeaccess has a<ObjectInstance>.observe_<propertyname>(...)method that returns an observable. Internally, this would call RxJS/subscribe (see also docs for Subscription).
- that has
- Each method would be proxied appropriately onto the object. For example, the typescript class corresponding to Clip would have a
Clip.add_new_notes(...)method.
- Each object would have a
- The library would come with first-class support for logging and error handling.
- The library that implements this API could be written using the GitHub/Repo/Template that Person/Alessandro Petrone wrote for maxing max/msp libraries in #Typescript - GitHub/aptrn/maxmsp-ts-library-template. As the README from that template mentions,
This is a turborepo monorepo (turborepo) template with the following structure:
my-library: Contains your TypeScript librarymaxmsp-test: Contains an instance of this template (GitHub/aptrn/maxmsp-ts-example) withmy-libraryas a dependency.- Since this is is a turborepo monorepo, the library could develop the core of this library in its own package,
alits-core.- Most users will not need the entire API, which could be broken up to simplify versioning and minimize merge conflicts. Other sub-libraries could focus on isolated portions of the library:
alitsitself could reference the entire collection of libraries.
- The core characteristics of a more idiomatic typescript library would be: