autowatch = 1; inlets = 1; outlets = 1; /** * Build step 2: the only part of this project that touches the Live API. * * `LiveAPI` exists exclusively inside Max's `js`/`v8` objects — Node for Max * cannot see it — so this script watches the LOM and forwards raw values out * its outlet to `node.script scale-device.js`, which does the thinking. * * Plain ES5 on purpose: the `js` object runs an ES5 engine, so this is the one * file in the repo that is neither TypeScript nor compiled. Keep it dumb. * * Messages in: * bang start observing (wire this to live.thisdevice) * source song read the Song's scale — the control bar's global scale * source clip read the selected clip's scale, falling back to the Song * refresh re-read and re-send everything * debug 1 log each push to the Max console * * Messages out: reset, source, root, mode, name, intervals, commit. */ var SONG_PROPS = ['root_note', 'scale_name', 'scale_mode']; var CLIP_PROPS = ['root_note', 'scale_name']; var CLIP_PATH = 'live_set view detail_clip'; var COALESCE_MS = 30; var started = false; var scaleSource = 'song'; var logging = 0; var songObservers = []; var clipObservers = []; var selectionObserver = null; var pushTask = null; setinletassist(0, 'bang to start; source song|clip, refresh, debug 0|1'); setoutletassist(0, 'to node.script: reset/source/root/mode/name/intervals/commit'); /* --- messages ---------------------------------------------------------- */ // live.thisdevice bangs once the device is fully loaded, which is the earliest // moment a LiveAPI object may be created. function bang() { start(); } function source(which) { which = String(which); if (which !== 'song' && which !== 'clip') { error('scale-observer: source must be song or clip\n'); return; } scaleSource = which; schedulePush(); } function refresh() { schedulePush(); } function debug(value) { logging = value ? 1 : 0; } /* --- observing --------------------------------------------------------- */ function start() { if (started) return; started = true; pushTask = new Task(push, this); for (var i = 0; i < SONG_PROPS.length; i++) { songObservers.push(observe('live_set', SONG_PROPS[i])); } // Which clip is selected changes independently of that clip's scale, so it // needs its own observer that re-points the clip observers when it fires. selectionObserver = new LiveAPI(onSelectionChanged, 'live_set view'); selectionObserver.property = 'detail_clip'; observeClip(); schedulePush(); } function observe(path, property) { var api = new LiveAPI(onChanged, path); api.property = property; return api; } function onChanged() { schedulePush(); } function onSelectionChanged() { observeClip(); schedulePush(); } // A LiveAPI binds to the object its path resolved to at creation time, so // selecting a different clip means throwing these away and making new ones. function observeClip() { release(clipObservers); clipObservers = []; var clip = new LiveAPI(noop, CLIP_PATH); if (!clip.id || clip.id == 0) return; for (var i = 0; i < CLIP_PROPS.length; i++) { if (has(clip, CLIP_PROPS[i])) { clipObservers.push(observe(CLIP_PATH, CLIP_PROPS[i])); } } } function release(observers) { for (var i = 0; i < observers.length; i++) { try { observers[i].property = ''; } catch (e) { // Live is tearing the object down anyway. } } } function notifydeleted() { release(songObservers); release(clipObservers); if (selectionObserver) release([selectionObserver]); if (pushTask) pushTask.freepeer(); } /* --- reading and sending ------------------------------------------------ */ // Several properties change together when the scale changes; coalesce so the // device gets one burst of messages instead of three. function schedulePush() { if (!started) return; pushTask.cancel(); pushTask.schedule(COALESCE_MS); } function push() { var scale = null; if (scaleSource === 'clip') scale = readClip(); if (!scale) scale = readSong(); if (!scale) return; if (logging) { post( 'scale-observer: ' + scale.source + ' root=' + scale.root + ' name=' + scale.name + ' intervals=' + scale.intervals + '\n' ); } emit(scale); } function readSong() { var api = new LiveAPI(noop, 'live_set'); if (!api.id) return null; var root = first(get(api, 'root_note')); if (root === null) return null; return { source: 'song', root: root, name: joined(get(api, 'scale_name')), mode: first(get(api, 'scale_mode')), // scale_intervals only exists from Live 12.1; the device falls back to // resolving the name when this comes back empty. intervals: get(api, 'scale_intervals') }; } function readClip() { var api = new LiveAPI(noop, CLIP_PATH); if (!api.id || api.id == 0) return null; var root = first(get(api, 'root_note')); var name = joined(get(api, 'scale_name')); if (root === null || !name) return null; var song = new LiveAPI(noop, 'live_set'); return { source: 'clip', root: root, name: name, // Clips carry no scale_mode of their own; the Song's flag still says // whether Live is enforcing the scale at all. mode: first(get(song, 'scale_mode')), intervals: get(api, 'scale_intervals') }; } function emit(scale) { outlet(0, 'reset'); outlet(0, 'source', scale.source); outlet(0, 'root', scale.root); if (scale.mode !== null) outlet(0, 'mode', scale.mode); if (scale.name) outlet(0, 'name', scale.name); if (scale.intervals && scale.intervals.length) { outlet(0, ['intervals'].concat(scale.intervals)); } outlet(0, 'commit'); } /* --- LOM helpers -------------------------------------------------------- */ function noop() {} // Asking for a property the running Live does not have prints an error in the // Max console, so check `info` before reaching for anything version-dependent. function has(api, property) { try { var info = api.info; if (typeof info !== 'string') return true; return info.indexOf('property ' + property) !== -1; } catch (e) { return true; } } function get(api, property) { if (!has(api, property)) return null; try { return api.get(property); } catch (e) { return null; } } function first(value) { if (value === null || value === undefined) return null; if (value instanceof Array) return value.length ? value[0] : null; return value; } // A symbol property comes back as an array, sometimes split on its spaces. function joined(value) { if (value === null || value === undefined) return ''; if (value instanceof Array) return value.join(' '); return String(value); }