ableton-js 2.6.0 → 2.7.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/CHANGELOG.md CHANGED
@@ -4,8 +4,22 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ #### [v2.7.1](https://github.com/leolabs/ableton.js/compare/v2.7.0...v2.7.1)
8
+
9
+ - :sparkles: Add the ability to listen to changes in clip notes [`a1c5112`](https://github.com/leolabs/ableton.js/commit/a1c5112542261493b6c55289c203e7250c7b7d15)
10
+
11
+ #### [v2.7.0](https://github.com/leolabs/ableton.js/compare/v2.6.0...v2.7.0)
12
+
13
+ > 2 November 2022
14
+
15
+ - :sparkles: Add automatic client-side caching of potentially large props [`68c3edd`](https://github.com/leolabs/ableton.js/commit/68c3edda00918c769afa7c88e54a2a566a0e1f23)
16
+ - :art: Clean up the code a bit [`c183cda`](https://github.com/leolabs/ableton.js/commit/c183cda10deb81e176992ed40624d3f57015a703)
17
+ - :memo: Add some docs for caching [`778da44`](https://github.com/leolabs/ableton.js/commit/778da44ab960cb5074b46e4783abf3d0c57d5a9f)
18
+
7
19
  #### [v2.6.0](https://github.com/leolabs/ableton.js/compare/v2.5.4...v2.6.0)
8
20
 
21
+ > 1 November 2022
22
+
9
23
  - :sparkles: Make IDs stable over the lifespan of Live [`0bfcf90`](https://github.com/leolabs/ableton.js/commit/0bfcf904b54d42377877a2e9fab0796986f6aa1a)
10
24
  - :sparkles: Add create_track methods [`a7dd6bc`](https://github.com/leolabs/ableton.js/commit/a7dd6bc53e2f49fb133b2eea2a1fe6eb1ff0aac6)
11
25
 
package/README.md CHANGED
@@ -58,7 +58,7 @@ const test = async () => {
58
58
  // Get the current tempo
59
59
  const tempo = await ableton.song.get("tempo");
60
60
  console.log(tempo);
61
-
61
+
62
62
  // Set the tempo
63
63
  await ableton.song.set("tempo", 85);
64
64
  };
@@ -103,6 +103,17 @@ chunk. The last chunk always has the index 0xFF. This indicates to the JS
103
103
  library that the previous received messages should be stiched together,
104
104
  unzipped, and processed.
105
105
 
106
+ ### Caching
107
+
108
+ Certain props are cached on the client to reduce the bandwidth over UDP. To do
109
+ this, the Ableton plugin generates an MD5 hash of the prop, called ETag, and
110
+ sends it to the client along with the data.
111
+
112
+ The client stores both the ETag and the data in an LRU cache and sends the
113
+ latest stored ETag to the plugin the next time the same prop is requested. If
114
+ the data still matches the ETag, the plugin responds with a placeholder object
115
+ and the client returns the cached data.
116
+
106
117
  ### Commands
107
118
 
108
119
  A command payload consists of the following properties:
@@ -113,7 +124,9 @@ A command payload consists of the following properties:
113
124
  "ns": "song", // The command namespace
114
125
  "nsid": null, // The namespace id, for example to address a specific track or device
115
126
  "name": "get_prop", // Command name
116
- "args": { "prop": "current_song_time" } // Command arguments
127
+ "args": { "prop": "current_song_time" }, // Command arguments
128
+ "etag": "4e0794e44c7eb58bdbbbf7268e8237b4", // MD5 hash of the data if it might be cached locally
129
+ "cache": true // If this is true, the plugin will calculate an etag and return a placeholder if it matches the provided one
117
130
  }
118
131
  ```
119
132
 
@@ -123,7 +136,27 @@ The MIDI Script answers with a JSON object looking like this:
123
136
  {
124
137
  "data": 0.0, // The command's return value, can be of any JSON-compatible type
125
138
  "event": "result", // This can be 'result' or 'error'
126
- "uuid": "a20f25a0-83e2-11e9-bbe1-bd3a580ef903"
139
+ "uuid": "a20f25a0-83e2-11e9-bbe1-bd3a580ef903" // The same UUID that was used to send the command
140
+ }
141
+ ```
142
+
143
+ If you're getting a cached prop, the JSON object could look like this:
144
+
145
+ ```js
146
+ {
147
+ "data": { "data": 0.0, "etag": "4e0794e44c7eb58bdbbbf7268e8237b4" },
148
+ "event": "result", // This can be 'result' or 'error'
149
+ "uuid": "a20f25a0-83e2-11e9-bbe1-bd3a580ef903" // The same UUID that was used to send the command
150
+ }
151
+ ```
152
+
153
+ Or, if the data hasn't changed, it looks like this:
154
+
155
+ ```js
156
+ {
157
+ "data": { "__cached": true },
158
+ "event": "result", // This can be 'result' or 'error'
159
+ "uuid": "a20f25a0-83e2-11e9-bbe1-bd3a580ef903" // The same UUID that was used to send the command
127
160
  }
128
161
  ```
129
162
 
package/index.d.ts CHANGED
@@ -1,14 +1,18 @@
1
1
  /// <reference types="node" />
2
2
  import { EventEmitter } from "events";
3
+ import LruCache from "lru-cache";
3
4
  import { Song } from "./ns/song";
4
5
  import { Internal } from "./ns/internal";
5
6
  import { Application } from "./ns/application";
6
7
  import { Midi } from "./ns/midi";
8
+ import { Cache } from "./util/cache";
7
9
  interface Command {
8
10
  uuid: string;
9
11
  ns: string;
10
12
  nsid?: string;
11
13
  name: string;
14
+ etag?: string;
15
+ cache?: boolean;
12
16
  args?: {
13
17
  [k: string]: any;
14
18
  };
@@ -31,10 +35,14 @@ export declare class TimeoutError extends Error {
31
35
  payload: Command;
32
36
  constructor(message: string, payload: Command);
33
37
  }
38
+ export interface AbletonOptions {
39
+ host?: string;
40
+ sendPort?: number;
41
+ listenPort?: number;
42
+ heartbeatInterval?: number;
43
+ cacheOptions?: LruCache.Options<string, any>;
44
+ }
34
45
  export declare class Ableton extends EventEmitter implements ConnectionEventEmitter {
35
- private host;
36
- private sendPort;
37
- private listenPort;
38
46
  private client;
39
47
  private msgMap;
40
48
  private eventListeners;
@@ -43,11 +51,15 @@ export declare class Ableton extends EventEmitter implements ConnectionEventEmit
43
51
  private cancelConnectionEvent;
44
52
  private buffer;
45
53
  private latency;
54
+ private host;
55
+ private sendPort;
56
+ private listenPort;
57
+ cache: Cache;
46
58
  song: Song;
47
59
  application: Application;
48
60
  internal: Internal;
49
61
  midi: Midi;
50
- constructor(host?: string, sendPort?: number, listenPort?: number, heartbeatInterval?: number);
62
+ constructor(options?: AbletonOptions);
51
63
  close(): void;
52
64
  /**
53
65
  * Returns the latency between the last command and its response.
@@ -61,8 +73,9 @@ export declare class Ableton extends EventEmitter implements ConnectionEventEmit
61
73
  * Sends a raw command to Ableton. Usually, you won't need this.
62
74
  * A good starting point in general is the `song` prop.
63
75
  */
64
- sendCommand(ns: string, nsid: string | undefined, name: string, args?: Record<string, any> | any[], timeout?: number): Promise<any>;
65
- getProp(ns: string, nsid: string | undefined, prop: string): Promise<any>;
76
+ sendCommand(command: Omit<Command, "uuid">, timeout?: number): Promise<any>;
77
+ sendCachedCommand(command: Omit<Command, "uuid" | "cache">, timeout?: number): Promise<any>;
78
+ getProp(ns: string, nsid: string | undefined, prop: string, cache?: boolean): Promise<any>;
66
79
  setProp(ns: string, nsid: string | undefined, prop: string, value: any): Promise<any>;
67
80
  addPropListener(ns: string, nsid: string | undefined, prop: string, listener: (data: any) => any): Promise<() => Promise<boolean | undefined>>;
68
81
  removePropListener(ns: string, nsid: string | undefined, prop: string, eventId: string, listener: (data: any) => any): Promise<boolean | undefined>;
package/index.js CHANGED
@@ -14,6 +14,17 @@ var __extends = (this && this.__extends) || (function () {
14
14
  d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
15
15
  };
16
16
  })();
17
+ var __assign = (this && this.__assign) || function () {
18
+ __assign = Object.assign || function(t) {
19
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
20
+ s = arguments[i];
21
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
22
+ t[p] = s[p];
23
+ }
24
+ return t;
25
+ };
26
+ return __assign.apply(this, arguments);
27
+ };
17
28
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
18
29
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
19
30
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -50,6 +61,22 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
50
61
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
51
62
  }
52
63
  };
64
+ var __read = (this && this.__read) || function (o, n) {
65
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
66
+ if (!m) return o;
67
+ var i = m.call(o), r, ar = [], e;
68
+ try {
69
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
70
+ }
71
+ catch (error) { e = { error: error }; }
72
+ finally {
73
+ try {
74
+ if (r && !r.done && (m = i["return"])) m.call(i);
75
+ }
76
+ finally { if (e) throw e.error; }
77
+ }
78
+ return ar;
79
+ };
53
80
  var __spreadArray = (this && this.__spreadArray) || function (to, from) {
54
81
  for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
55
82
  to[j] = from[i];
@@ -65,11 +92,13 @@ var events_1 = require("events");
65
92
  var uuid_1 = require("uuid");
66
93
  var semver_1 = __importDefault(require("semver"));
67
94
  var zlib_1 = require("zlib");
95
+ var lru_cache_1 = __importDefault(require("lru-cache"));
68
96
  var song_1 = require("./ns/song");
69
97
  var internal_1 = require("./ns/internal");
70
98
  var application_1 = require("./ns/application");
71
99
  var midi_1 = require("./ns/midi");
72
100
  var package_version_1 = require("./util/package-version");
101
+ var cache_1 = require("./util/cache");
73
102
  var TimeoutError = /** @class */ (function (_super) {
74
103
  __extends(TimeoutError, _super);
75
104
  function TimeoutError(message, payload) {
@@ -83,15 +112,9 @@ var TimeoutError = /** @class */ (function (_super) {
83
112
  exports.TimeoutError = TimeoutError;
84
113
  var Ableton = /** @class */ (function (_super) {
85
114
  __extends(Ableton, _super);
86
- function Ableton(host, sendPort, listenPort, heartbeatInterval) {
87
- if (host === void 0) { host = "127.0.0.1"; }
88
- if (sendPort === void 0) { sendPort = 39041; }
89
- if (listenPort === void 0) { listenPort = 39031; }
90
- if (heartbeatInterval === void 0) { heartbeatInterval = 2000; }
115
+ function Ableton(options) {
116
+ var _a, _b, _c, _d;
91
117
  var _this = _super.call(this) || this;
92
- _this.host = host;
93
- _this.sendPort = sendPort;
94
- _this.listenPort = listenPort;
95
118
  _this.msgMap = new Map();
96
119
  _this.eventListeners = new Map();
97
120
  _this._isConnected = false;
@@ -102,9 +125,13 @@ var Ableton = /** @class */ (function (_super) {
102
125
  _this.application = new application_1.Application(_this);
103
126
  _this.internal = new internal_1.Internal(_this);
104
127
  _this.midi = new midi_1.Midi(_this);
128
+ _this.host = (_a = options === null || options === void 0 ? void 0 : options.host) !== null && _a !== void 0 ? _a : "127.0.0.1";
129
+ _this.sendPort = (_b = options === null || options === void 0 ? void 0 : options.sendPort) !== null && _b !== void 0 ? _b : 39041;
130
+ _this.listenPort = (_c = options === null || options === void 0 ? void 0 : options.listenPort) !== null && _c !== void 0 ? _c : 39031;
105
131
  _this.client = dgram_1.default.createSocket({ type: "udp4" });
106
- _this.client.bind(_this.listenPort, host);
132
+ _this.client.bind(_this.listenPort, _this.host);
107
133
  _this.client.addListener("message", _this.handleIncoming.bind(_this));
134
+ _this.cache = new lru_cache_1.default(__assign({ max: 500, ttl: 1000 * 60 * 10 }, options === null || options === void 0 ? void 0 : options.cacheOptions));
108
135
  var heartbeat = function () { return __awaiter(_this, void 0, void 0, function () {
109
136
  var e_1;
110
137
  return __generator(this, function (_a) {
@@ -136,7 +163,7 @@ var Ableton = /** @class */ (function (_super) {
136
163
  }
137
164
  });
138
165
  }); };
139
- _this.heartbeatInterval = setInterval(heartbeat, heartbeatInterval);
166
+ _this.heartbeatInterval = setInterval(heartbeat, (_d = options === null || options === void 0 ? void 0 : options.heartbeatInterval) !== null && _d !== void 0 ? _d : 2000);
140
167
  heartbeat();
141
168
  _this.internal
142
169
  .get("version")
@@ -225,24 +252,20 @@ var Ableton = /** @class */ (function (_super) {
225
252
  * Sends a raw command to Ableton. Usually, you won't need this.
226
253
  * A good starting point in general is the `song` prop.
227
254
  */
228
- Ableton.prototype.sendCommand = function (ns, nsid, name, args, timeout) {
255
+ Ableton.prototype.sendCommand = function (command, timeout) {
229
256
  if (timeout === void 0) { timeout = 2000; }
230
257
  return __awaiter(this, void 0, void 0, function () {
231
258
  var _this = this;
232
259
  return __generator(this, function (_a) {
233
260
  return [2 /*return*/, new Promise(function (res, rej) {
234
261
  var msgId = uuid_1.v4();
235
- var payload = {
236
- uuid: msgId,
237
- ns: ns,
238
- nsid: nsid,
239
- name: name,
240
- args: args,
241
- };
262
+ var payload = __assign({ uuid: msgId }, command);
242
263
  var msg = JSON.stringify(payload);
243
264
  var timeoutId = setTimeout(function () {
244
- var arg = JSON.stringify(args);
245
- var cls = nsid ? ns + "(" + nsid + ")" : ns;
265
+ var arg = JSON.stringify(command.args);
266
+ var cls = command.nsid
267
+ ? command.ns + "(" + command.nsid + ")"
268
+ : command.ns;
246
269
  rej(new TimeoutError([
247
270
  "The command " + cls + "." + name + "(" + arg + ") timed out after " + timeout + " ms.",
248
271
  "Please make sure that Ableton is running and that you have the latest",
@@ -252,10 +275,10 @@ var Ableton = /** @class */ (function (_super) {
252
275
  }, timeout);
253
276
  var currentTimestamp = Date.now();
254
277
  _this.msgMap.set(msgId, {
255
- res: function (data) {
278
+ res: function (result) {
256
279
  _this.setPing(Date.now() - currentTimestamp);
257
280
  clearTimeout(timeoutId);
258
- res(data);
281
+ res(result);
259
282
  },
260
283
  rej: rej,
261
284
  clearTimeout: function () {
@@ -267,17 +290,62 @@ var Ableton = /** @class */ (function (_super) {
267
290
  });
268
291
  });
269
292
  };
270
- Ableton.prototype.getProp = function (ns, nsid, prop) {
293
+ Ableton.prototype.sendCachedCommand = function (command, timeout) {
294
+ var _a, _b;
271
295
  return __awaiter(this, void 0, void 0, function () {
296
+ var args, cacheKey, cached, result;
297
+ return __generator(this, function (_c) {
298
+ switch (_c.label) {
299
+ case 0:
300
+ args = (_b = (_a = command.args) === null || _a === void 0 ? void 0 : _a.prop) !== null && _b !== void 0 ? _b : JSON.stringify(command.args);
301
+ cacheKey = [command.ns, command.nsid, args].filter(Boolean).join("/");
302
+ cached = this.cache.get(cacheKey);
303
+ return [4 /*yield*/, this.sendCommand(__assign(__assign({}, command), { etag: cached === null || cached === void 0 ? void 0 : cached.etag, cache: true }), timeout)];
304
+ case 1:
305
+ result = _c.sent();
306
+ if (cache_1.isCached(result)) {
307
+ if (!cached) {
308
+ throw new Error("Tried to get an object that isn't cached.");
309
+ }
310
+ else {
311
+ return [2 /*return*/, cached.data];
312
+ }
313
+ }
314
+ else {
315
+ if (result.etag) {
316
+ this.cache.set(cacheKey, result);
317
+ }
318
+ return [2 /*return*/, result.data];
319
+ }
320
+ return [2 /*return*/];
321
+ }
322
+ });
323
+ });
324
+ };
325
+ Ableton.prototype.getProp = function (ns, nsid, prop, cache) {
326
+ return __awaiter(this, void 0, void 0, function () {
327
+ var params;
272
328
  return __generator(this, function (_a) {
273
- return [2 /*return*/, this.sendCommand(ns, nsid, "get_prop", { prop: prop })];
329
+ params = { ns: ns, nsid: nsid, name: "get_prop", args: { prop: prop } };
330
+ if (cache) {
331
+ return [2 /*return*/, this.sendCachedCommand(params)];
332
+ }
333
+ else {
334
+ return [2 /*return*/, this.sendCommand(params)];
335
+ }
336
+ return [2 /*return*/];
274
337
  });
275
338
  });
276
339
  };
277
340
  Ableton.prototype.setProp = function (ns, nsid, prop, value) {
278
341
  return __awaiter(this, void 0, void 0, function () {
279
342
  return __generator(this, function (_a) {
280
- return [2 /*return*/, this.sendCommand(ns, nsid, "set_prop", { prop: prop, value: value })];
343
+ return [2 /*return*/, this.sendCommand({
344
+ ns: ns,
345
+ nsid: nsid,
346
+ name: "set_prop",
347
+ args: { prop: prop, value: value },
348
+ })];
281
349
  });
282
350
  });
283
351
  };
@@ -289,10 +357,11 @@ var Ableton = /** @class */ (function (_super) {
289
357
  switch (_a.label) {
290
358
  case 0:
291
359
  eventId = uuid_1.v4();
292
- return [4 /*yield*/, this.sendCommand(ns, nsid, "add_listener", {
293
- prop: prop,
360
+ return [4 /*yield*/, this.sendCommand({
361
+ ns: ns,
294
362
  nsid: nsid,
295
- eventId: eventId,
363
+ name: "add_listener",
364
+ args: { prop: prop, nsid: nsid, eventId: eventId },
296
365
  })];
297
366
  case 1:
298
367
  result = _a.sent();
@@ -300,7 +369,7 @@ var Ableton = /** @class */ (function (_super) {
300
369
  this.eventListeners.set(result, [listener]);
301
370
  }
302
371
  else {
303
- this.eventListeners.set(result, __spreadArray(__spreadArray([], this.eventListeners.get(result)), [
372
+ this.eventListeners.set(result, __spreadArray(__spreadArray([], __read(this.eventListeners.get(result))), [
304
373
  listener,
305
374
  ]));
306
375
  }
@@ -325,7 +394,12 @@ var Ableton = /** @class */ (function (_super) {
325
394
  }
326
395
  if (!(listeners.length === 1)) return [3 /*break*/, 2];
327
396
  this.eventListeners.delete(eventId);
328
- return [4 /*yield*/, this.sendCommand(ns, nsid, "remove_listener", { prop: prop, nsid: nsid })];
397
+ return [4 /*yield*/, this.sendCommand({
398
+ ns: ns,
399
+ nsid: nsid,
400
+ name: "remove_listener",
401
+ args: { prop: prop, nsid: nsid },
402
+ })];
329
403
  case 1:
330
404
  _a.sent();
331
405
  return [2 /*return*/, true];
@@ -26,6 +26,9 @@ class Clip(Interface):
26
26
  def get_available_warp_modes(self, ns):
27
27
  return list(ns.available_warp_modes)
28
28
 
29
+ def get_notes(self, ns, from_time=0, from_pitch=0, time_span=99999999999999, pitch_span=128):
30
+ return ns.get_notes(from_time, from_pitch, time_span, pitch_span)
31
+
29
32
  def set_notes(self, ns, notes):
30
33
  return ns.set_notes(tuple(notes))
31
34
 
@@ -1,3 +1,7 @@
1
+ import hashlib
2
+ import json
3
+
4
+
1
5
  class Interface(object):
2
6
  obj_ids = dict()
3
7
  listeners = dict()
@@ -24,25 +28,43 @@ class Interface(object):
24
28
  def get_ns(self, nsid):
25
29
  return Interface.obj_ids[nsid]
26
30
 
31
+ def send_result(self, result, uuid, etag, cache):
32
+ """Sends an empty response if the etag matches the result, or the result together with an etag."""
33
+ if not cache:
34
+ return self.socket.send("result", result, uuid)
35
+
36
+ def jsonReplace(o):
37
+ return str(o)
38
+
39
+ hash = hashlib.md5(json.dumps(
40
+ result, default=jsonReplace, ensure_ascii=False).encode()).hexdigest()
41
+
42
+ if hash == etag:
43
+ return self.socket.send("result", {"__cached": True}, uuid)
44
+ else:
45
+ return self.socket.send("result", {"data": result, "etag": hash}, uuid)
46
+
27
47
  def handle(self, payload):
28
48
  name = payload.get("name")
29
49
  uuid = payload.get("uuid")
50
+ etag = payload.get("etag")
30
51
  args = payload.get("args", {})
52
+ cache = payload.get("cache", False)
31
53
  ns = self.get_ns(payload.get("nsid"))
32
54
 
33
55
  try:
34
56
  # Try self-defined functions first
35
57
  if hasattr(self, name) and callable(getattr(self, name)):
36
58
  result = getattr(self, name)(ns=ns, **args)
37
- self.socket.send("result", result, uuid)
59
+ self.send_result(result, uuid, etag, cache)
38
60
  # Check if the function exists in the Ableton API as fallback
39
61
  elif hasattr(ns, name) and callable(getattr(ns, name)):
40
62
  if isinstance(args, dict):
41
63
  result = getattr(ns, name)(**args)
42
- self.socket.send("result", result, uuid)
64
+ self.send_result(result, uuid, etag, cache)
43
65
  elif isinstance(args, list):
44
66
  result = getattr(ns, name)(*args)
45
- self.socket.send("result", result, uuid)
67
+ self.send_result(result, uuid, etag, cache)
46
68
  else:
47
69
  self.socket.send("error", "Function call failed: " + str(args) +
48
70
  " are invalid arguments", uuid)
@@ -10,4 +10,4 @@ class Internal(Interface):
10
10
  return self
11
11
 
12
12
  def get_version(self, ns):
13
- return "2.6.0"
13
+ return "2.7.1"
@@ -2,6 +2,7 @@ import socket
2
2
  import json
3
3
  import struct
4
4
  import zlib
5
+ import hashlib
5
6
  from threading import Timer
6
7
 
7
8
 
package/ns/clip-slot.js CHANGED
@@ -37,6 +37,9 @@ var ClipSlot = /** @class */ (function (_super) {
37
37
  clip: function (c) { return (c ? new clip_1.Clip(ableton, c) : null); },
38
38
  color: function (c) { return new color_1.Color(c); },
39
39
  };
40
+ _this.cachedProps = {
41
+ clip: true,
42
+ };
40
43
  return _this;
41
44
  }
42
45
  /**
package/ns/clip.d.ts CHANGED
@@ -77,6 +77,7 @@ export interface GettableProperties {
77
77
  }
78
78
  export interface TransformedProperties {
79
79
  color: Color;
80
+ notes: Note[];
80
81
  selected_notes: Note[];
81
82
  }
82
83
  export interface SettableProperties {
@@ -117,6 +118,7 @@ export interface ObservableProperties {
117
118
  loop_start: number;
118
119
  muted: boolean;
119
120
  name: string;
121
+ notes: NoteTuple[];
120
122
  pitch_coarse: number;
121
123
  pitch_fine: number;
122
124
  playing_position: number;
package/ns/clip.js CHANGED
@@ -99,6 +99,7 @@ var Clip = /** @class */ (function (_super) {
99
99
  _this.raw = raw;
100
100
  _this.transformers = {
101
101
  color: function (c) { return new color_1.Color(c); },
102
+ notes: function (n) { return n.map(note_1.tupleToNote); },
102
103
  selected_notes: function (n) { return n.map(note_1.tupleToNote); },
103
104
  };
104
105
  return _this;
@@ -183,12 +184,12 @@ var Clip = /** @class */ (function (_super) {
183
184
  var notes;
184
185
  return __generator(this, function (_a) {
185
186
  switch (_a.label) {
186
- case 0: return [4 /*yield*/, this.sendCommand("get_notes", [
187
- fromTime,
188
- fromPitch,
189
- timeSpan,
190
- pitchSpan,
191
- ])];
187
+ case 0: return [4 /*yield*/, this.sendCommand("get_notes", {
188
+ from_time: fromTime,
189
+ from_pitch: fromPitch,
190
+ time_span: timeSpan,
191
+ pitch_span: pitchSpan,
192
+ })];
192
193
  case 1:
193
194
  notes = _a.sent();
194
195
  return [2 /*return*/, notes.map(note_1.tupleToNote)];
package/ns/device.js CHANGED
@@ -31,7 +31,10 @@ var Device = /** @class */ (function (_super) {
31
31
  var _this = _super.call(this, ableton, "device", raw.id) || this;
32
32
  _this.raw = raw;
33
33
  _this.transformers = {
34
- parameters: function (ps) { return ps.map(function (p) { return new device_parameter_1.DeviceParameter(_this.ableton, p); }); },
34
+ parameters: function (ps) { return ps.map(function (p) { return new device_parameter_1.DeviceParameter(ableton, p); }); },
35
+ };
36
+ _this.cachedProps = {
37
+ parameters: true,
35
38
  };
36
39
  return _this;
37
40
  }
package/ns/index.d.ts CHANGED
@@ -3,10 +3,13 @@ export declare class Namespace<GP, TP, SP, OP> {
3
3
  protected ableton: Ableton;
4
4
  protected ns: string;
5
5
  protected nsid?: string | undefined;
6
- constructor(ableton: Ableton, ns: string, nsid?: string | undefined);
7
6
  protected transformers: Partial<{
8
- [T in Extract<keyof GP, keyof TP>]: (val: GP[T]) => TP[T];
7
+ [T in keyof TP]: (val: T extends keyof GP ? GP[T] : unknown) => TP[T];
8
+ }>;
9
+ protected cachedProps: Partial<{
10
+ [T in keyof GP]: boolean;
9
11
  }>;
12
+ constructor(ableton: Ableton, ns: string, nsid?: string | undefined);
10
13
  get<T extends keyof GP>(prop: T): Promise<T extends keyof TP ? TP[T] : GP[T]>;
11
14
  set<T extends keyof SP>(prop: T, value: SP[T]): Promise<null>;
12
15
  addListener<T extends keyof OP>(prop: T, listener: (data: T extends keyof TP ? TP[T] : OP[T]) => any): Promise<() => Promise<boolean | undefined>>;
@@ -16,5 +19,12 @@ export declare class Namespace<GP, TP, SP, OP> {
16
19
  */
17
20
  sendCommand(name: string, args?: {
18
21
  [k: string]: any;
22
+ }, etag?: string, timeout?: number): Promise<any>;
23
+ /**
24
+ * Sends a raw function invocation to Ableton and expects the
25
+ * result to be a CacheResponse with `data` and an `etag`.
26
+ */
27
+ protected sendCachedCommand(name: string, args?: {
28
+ [k: string]: any;
19
29
  }, timeout?: number): Promise<any>;
20
30
  }
package/ns/index.js CHANGED
@@ -43,13 +43,16 @@ var Namespace = /** @class */ (function () {
43
43
  this.ns = ns;
44
44
  this.nsid = nsid;
45
45
  this.transformers = {};
46
+ this.cachedProps = {};
46
47
  }
47
48
  Namespace.prototype.get = function (prop) {
48
49
  return __awaiter(this, void 0, void 0, function () {
49
- var res, transformer;
50
+ var cache, res, transformer;
50
51
  return __generator(this, function (_a) {
51
52
  switch (_a.label) {
52
- case 0: return [4 /*yield*/, this.ableton.getProp(this.ns, this.nsid, String(prop))];
53
+ case 0:
54
+ cache = !!this.cachedProps[prop];
55
+ return [4 /*yield*/, this.ableton.getProp(this.ns, this.nsid, String(prop), cache)];
53
56
  case 1:
54
57
  res = _a.sent();
55
58
  transformer = this.transformers[prop];
@@ -91,10 +94,21 @@ var Namespace = /** @class */ (function () {
91
94
  * Sends a raw function invocation to Ableton.
92
95
  * This should be used with caution.
93
96
  */
94
- Namespace.prototype.sendCommand = function (name, args, timeout) {
97
+ Namespace.prototype.sendCommand = function (name, args, etag, timeout) {
95
98
  return __awaiter(this, void 0, void 0, function () {
96
99
  return __generator(this, function (_a) {
97
- return [2 /*return*/, this.ableton.sendCommand(this.ns, this.nsid, name, args, timeout)];
100
+ return [2 /*return*/, this.ableton.sendCommand({ ns: this.ns, nsid: this.nsid, name: name, args: args, etag: etag }, timeout)];
101
+ });
102
+ });
103
+ };
104
+ /**
105
+ * Sends a raw function invocation to Ableton and expects the
106
+ * result to be a CacheResponse with `data` and an `etag`.
107
+ */
108
+ Namespace.prototype.sendCachedCommand = function (name, args, timeout) {
109
+ return __awaiter(this, void 0, void 0, function () {
110
+ return __generator(this, function (_a) {
111
+ return [2 /*return*/, this.ableton.sendCachedCommand({ ns: this.ns, nsid: this.nsid, name: name, args: args }, timeout)];
98
112
  });
99
113
  });
100
114
  };
package/ns/midi.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Namespace } from "./index";
2
- import { Ableton } from "../index";
1
+ import { Ableton } from "..";
2
+ import { Namespace } from ".";
3
3
  export declare enum MidiCommand {
4
4
  NoteOn = 128,
5
5
  NoteOff = 144,
package/ns/midi.js CHANGED
@@ -16,7 +16,7 @@ var __extends = (this && this.__extends) || (function () {
16
16
  })();
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.Midi = exports.MidiMessage = exports.MidiCommand = void 0;
19
- var index_1 = require("./index");
19
+ var _1 = require(".");
20
20
  var MidiCommand;
21
21
  (function (MidiCommand) {
22
22
  MidiCommand[MidiCommand["NoteOn"] = 128] = "NoteOn";
@@ -70,17 +70,18 @@ var MidiMessage = /** @class */ (function () {
70
70
  return {
71
71
  command: this.command,
72
72
  controller: this.parameter1,
73
- value: this.parameter2
73
+ value: this.parameter2,
74
74
  };
75
75
  };
76
76
  MidiMessage.prototype.toNote = function () {
77
- if (this.command !== MidiCommand.NoteOn && this.command !== MidiCommand.NoteOff) {
77
+ if (this.command !== MidiCommand.NoteOn &&
78
+ this.command !== MidiCommand.NoteOff) {
78
79
  throw "not a midi note message";
79
80
  }
80
81
  return {
81
82
  command: this.command,
82
83
  key: this.parameter1,
83
- velocity: this.parameter2
84
+ velocity: this.parameter2,
84
85
  };
85
86
  };
86
87
  return MidiMessage;
@@ -91,10 +92,10 @@ var Midi = /** @class */ (function (_super) {
91
92
  function Midi(ableton) {
92
93
  var _this = _super.call(this, ableton, "midi") || this;
93
94
  _this.transformers = {
94
- midi: function (msg) { return new MidiMessage(msg); }
95
+ midi: function (msg) { return new MidiMessage(msg); },
95
96
  };
96
97
  return _this;
97
98
  }
98
99
  return Midi;
99
- }(index_1.Namespace));
100
+ }(_1.Namespace));
100
101
  exports.Midi = Midi;
package/ns/scene.js CHANGED
@@ -28,6 +28,9 @@ var Scene = /** @class */ (function (_super) {
28
28
  return clip_slots.map(function (c) { return new clip_slot_1.ClipSlot(_this.ableton, c); });
29
29
  },
30
30
  };
31
+ _this.cachedProps = {
32
+ clip_slots: true,
33
+ };
31
34
  return _this;
32
35
  }
33
36
  return Scene;
package/ns/song-view.js CHANGED
@@ -62,18 +62,26 @@ var SongView = /** @class */ (function (_super) {
62
62
  function SongView(ableton) {
63
63
  var _this = _super.call(this, ableton, "song-view") || this;
64
64
  _this.transformers = {
65
- selected_parameter: function (param) { return new device_parameter_1.DeviceParameter(_this.ableton, param); },
66
- selected_track: function (track) { return new track_1.Track(_this.ableton, track); },
67
- selected_scene: function (scene) { return new scene_1.Scene(_this.ableton, scene); },
68
- highlighted_clip_slot: function (clipSlot) { return new clip_slot_1.ClipSlot(_this.ableton, clipSlot); },
65
+ selected_parameter: function (param) { return new device_parameter_1.DeviceParameter(ableton, param); },
66
+ selected_track: function (track) { return new track_1.Track(ableton, track); },
67
+ selected_scene: function (scene) { return new scene_1.Scene(ableton, scene); },
68
+ highlighted_clip_slot: function (slot) { return new clip_slot_1.ClipSlot(ableton, slot); },
69
+ };
70
+ _this.cachedProps = {
71
+ selected_parameter: true,
72
+ selected_track: true,
73
+ selected_scene: true,
74
+ highlighted_clip_slot: true,
69
75
  };
70
76
  return _this;
71
77
  }
72
78
  SongView.prototype.selectDevice = function (device) {
73
79
  return __awaiter(this, void 0, void 0, function () {
74
80
  return __generator(this, function (_a) {
75
- return [2 /*return*/, this.ableton.sendCommand(this.ns, undefined, "select_device", {
76
- device_id: device.raw.id,
81
+ return [2 /*return*/, this.ableton.sendCommand({
82
+ ns: this.ns,
83
+ name: "select_device",
84
+ args: { device_id: device.raw.id },
77
85
  })];
78
86
  });
79
87
  });
package/ns/song.js CHANGED
@@ -101,12 +101,20 @@ var Song = /** @class */ (function (_super) {
101
101
  var _this = _super.call(this, ableton, "song") || this;
102
102
  _this.view = new song_view_1.SongView(_this.ableton);
103
103
  _this.transformers = {
104
- cue_points: function (points) { return points.map(function (c) { return new cue_point_1.CuePoint(_this.ableton, c); }); },
105
- master_track: function (track) { return new track_1.Track(_this.ableton, track); },
106
- return_tracks: function (tracks) { return tracks.map(function (t) { return new track_1.Track(_this.ableton, t); }); },
107
- tracks: function (tracks) { return tracks.map(function (t) { return new track_1.Track(_this.ableton, t); }); },
108
- visible_tracks: function (tracks) { return tracks.map(function (t) { return new track_1.Track(_this.ableton, t); }); },
109
- scenes: function (scenes) { return scenes.map(function (s) { return new scene_1.Scene(_this.ableton, s); }); },
104
+ cue_points: function (points) { return points.map(function (c) { return new cue_point_1.CuePoint(ableton, c); }); },
105
+ master_track: function (track) { return new track_1.Track(ableton, track); },
106
+ return_tracks: function (tracks) { return tracks.map(function (t) { return new track_1.Track(ableton, t); }); },
107
+ tracks: function (tracks) { return tracks.map(function (t) { return new track_1.Track(ableton, t); }); },
108
+ visible_tracks: function (tracks) { return tracks.map(function (t) { return new track_1.Track(ableton, t); }); },
109
+ scenes: function (scenes) { return scenes.map(function (s) { return new scene_1.Scene(ableton, s); }); },
110
+ };
111
+ _this.cachedProps = {
112
+ cue_points: true,
113
+ master_track: true,
114
+ return_tracks: true,
115
+ tracks: true,
116
+ visible_tracks: true,
117
+ scenes: true,
110
118
  };
111
119
  return _this;
112
120
  }
@@ -197,7 +205,7 @@ var Song = /** @class */ (function (_super) {
197
205
  Song.prototype.getData = function (key) {
198
206
  return __awaiter(this, void 0, void 0, function () {
199
207
  return __generator(this, function (_a) {
200
- return [2 /*return*/, this.sendCommand("get_data", { key: key })];
208
+ return [2 /*return*/, this.sendCachedCommand("get_data", { key: key })];
201
209
  });
202
210
  });
203
211
  };
package/ns/track.js CHANGED
@@ -36,6 +36,11 @@ var Track = /** @class */ (function (_super) {
36
36
  return clip_slots.map(function (c) { return new clip_slot_1.ClipSlot(ableton, c); });
37
37
  },
38
38
  };
39
+ _this.cachedProps = {
40
+ arrangement_clips: true,
41
+ devices: true,
42
+ clip_slots: true,
43
+ };
39
44
  return _this;
40
45
  }
41
46
  Track.prototype.duplicateClipToArrangement = function (clipID, time) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ableton-js",
3
- "version": "2.6.0",
3
+ "version": "2.7.1",
4
4
  "description": "Control Ableton Live from Node",
5
5
  "main": "index.js",
6
6
  "author": "Leo Bernard <admin@leolabs.org>",
@@ -46,6 +46,7 @@
46
46
  "typescript": "^4.3.2"
47
47
  },
48
48
  "dependencies": {
49
+ "lru-cache": "^7.14.0",
49
50
  "semver": "^7.3.5",
50
51
  "uuid": "^8.3.2"
51
52
  }
@@ -0,0 +1,14 @@
1
+ import type LruCache from "lru-cache";
2
+ export declare type CachedResponse = {
3
+ __cached: true;
4
+ };
5
+ export declare type CacheResponse = CachedResponse | {
6
+ data: any;
7
+ etag: string;
8
+ };
9
+ export declare const isCached: (obj: CacheResponse) => obj is CachedResponse;
10
+ export interface CacheObject {
11
+ etag: string;
12
+ data: any;
13
+ }
14
+ export declare type Cache = LruCache<string, CacheObject>;
package/util/cache.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isCached = void 0;
4
+ var isCached = function (obj) {
5
+ return obj && "__cached" in obj;
6
+ };
7
+ exports.isCached = isCached;