ableton-js 2.5.4 → 2.7.0

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,23 @@ 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.0](https://github.com/leolabs/ableton.js/compare/v2.6.0...v2.7.0)
8
+
9
+ - :sparkles: Add automatic client-side caching of potentially large props [`68c3edd`](https://github.com/leolabs/ableton.js/commit/68c3edda00918c769afa7c88e54a2a566a0e1f23)
10
+ - :art: Clean up the code a bit [`c183cda`](https://github.com/leolabs/ableton.js/commit/c183cda10deb81e176992ed40624d3f57015a703)
11
+ - :memo: Add some docs for caching [`778da44`](https://github.com/leolabs/ableton.js/commit/778da44ab960cb5074b46e4783abf3d0c57d5a9f)
12
+
13
+ #### [v2.6.0](https://github.com/leolabs/ableton.js/compare/v2.5.4...v2.6.0)
14
+
15
+ > 1 November 2022
16
+
17
+ - :sparkles: Make IDs stable over the lifespan of Live [`0bfcf90`](https://github.com/leolabs/ableton.js/commit/0bfcf904b54d42377877a2e9fab0796986f6aa1a)
18
+ - :sparkles: Add create_track methods [`a7dd6bc`](https://github.com/leolabs/ableton.js/commit/a7dd6bc53e2f49fb133b2eea2a1fe6eb1ff0aac6)
19
+
7
20
  #### [v2.5.4](https://github.com/leolabs/ableton.js/compare/v2.5.3...v2.5.4)
8
21
 
22
+ > 19 October 2022
23
+
9
24
  - :sparkles: Restart the socket when a message can't be sent due to a socket error [`91294c3`](https://github.com/leolabs/ableton.js/commit/91294c3cf195afacd22cbf1863e244bc231c9a33)
10
25
  - :sparkles: Send the UUID back with the error if a requested namespace handler doesn't exist [`8a96a99`](https://github.com/leolabs/ableton.js/commit/8a96a996b8b65c8485d90ac98293ccd605497784)
11
26
  - :memo: Add an example for setting a value [`f334983`](https://github.com/leolabs/ableton.js/commit/f334983a8755eab8415383aae7aff1ecff6e23e2)
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
- nsid?: number;
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,11 +73,12 @@ 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: number | undefined, name: string, args?: Record<string, any> | any[], timeout?: number): Promise<any>;
65
- getProp(ns: string, nsid: number | undefined, prop: string): Promise<any>;
66
- setProp(ns: string, nsid: number | undefined, prop: string, value: any): Promise<any>;
67
- addPropListener(ns: string, nsid: number | undefined, prop: string, listener: (data: any) => any): Promise<() => Promise<boolean | undefined>>;
68
- removePropListener(ns: string, nsid: number | undefined, prop: string, eventId: string, listener: (data: any) => any): Promise<boolean | undefined>;
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>;
79
+ setProp(ns: string, nsid: string | undefined, prop: string, value: any): Promise<any>;
80
+ addPropListener(ns: string, nsid: string | undefined, prop: string, listener: (data: any) => any): Promise<() => Promise<boolean | undefined>>;
81
+ removePropListener(ns: string, nsid: string | undefined, prop: string, eventId: string, listener: (data: any) => any): Promise<boolean | undefined>;
69
82
  /**
70
83
  * Removes all event listeners that were attached to properties.
71
84
  * This is useful for clearing all listeners when Live
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,64 @@ 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
+ console.log("Using cached entry:", { cached: cached });
312
+ return [2 /*return*/, cached.data];
313
+ }
314
+ }
315
+ else {
316
+ if (result.etag) {
317
+ console.log("Setting cached entry:", { cacheKey: cacheKey, result: result });
318
+ this.cache.set(cacheKey, result);
319
+ }
320
+ return [2 /*return*/, result.data];
321
+ }
322
+ return [2 /*return*/];
323
+ }
324
+ });
325
+ });
326
+ };
327
+ Ableton.prototype.getProp = function (ns, nsid, prop, cache) {
328
+ return __awaiter(this, void 0, void 0, function () {
329
+ var params;
272
330
  return __generator(this, function (_a) {
273
- return [2 /*return*/, this.sendCommand(ns, nsid, "get_prop", { prop: prop })];
331
+ params = { ns: ns, nsid: nsid, name: "get_prop", args: { prop: prop } };
332
+ if (cache) {
333
+ return [2 /*return*/, this.sendCachedCommand(params)];
334
+ }
335
+ else {
336
+ return [2 /*return*/, this.sendCommand(params)];
337
+ }
338
+ return [2 /*return*/];
274
339
  });
275
340
  });
276
341
  };
277
342
  Ableton.prototype.setProp = function (ns, nsid, prop, value) {
278
343
  return __awaiter(this, void 0, void 0, function () {
279
344
  return __generator(this, function (_a) {
280
- return [2 /*return*/, this.sendCommand(ns, nsid, "set_prop", { prop: prop, value: value })];
345
+ return [2 /*return*/, this.sendCommand({
346
+ ns: ns,
347
+ nsid: nsid,
348
+ name: "set_prop",
349
+ args: { prop: prop, value: value },
350
+ })];
281
351
  });
282
352
  });
283
353
  };
@@ -289,10 +359,11 @@ var Ableton = /** @class */ (function (_super) {
289
359
  switch (_a.label) {
290
360
  case 0:
291
361
  eventId = uuid_1.v4();
292
- return [4 /*yield*/, this.sendCommand(ns, nsid, "add_listener", {
293
- prop: prop,
362
+ return [4 /*yield*/, this.sendCommand({
363
+ ns: ns,
294
364
  nsid: nsid,
295
- eventId: eventId,
365
+ name: "add_listener",
366
+ args: { prop: prop, nsid: nsid, eventId: eventId },
296
367
  })];
297
368
  case 1:
298
369
  result = _a.sent();
@@ -300,7 +371,7 @@ var Ableton = /** @class */ (function (_super) {
300
371
  this.eventListeners.set(result, [listener]);
301
372
  }
302
373
  else {
303
- this.eventListeners.set(result, __spreadArray(__spreadArray([], this.eventListeners.get(result)), [
374
+ this.eventListeners.set(result, __spreadArray(__spreadArray([], __read(this.eventListeners.get(result))), [
304
375
  listener,
305
376
  ]));
306
377
  }
@@ -325,7 +396,12 @@ var Ableton = /** @class */ (function (_super) {
325
396
  }
326
397
  if (!(listeners.length === 1)) return [3 /*break*/, 2];
327
398
  this.eventListeners.delete(eventId);
328
- return [4 /*yield*/, this.sendCommand(ns, nsid, "remove_listener", { prop: prop, nsid: nsid })];
399
+ return [4 /*yield*/, this.sendCommand({
400
+ ns: ns,
401
+ nsid: nsid,
402
+ name: "remove_listener",
403
+ args: { prop: prop, nsid: nsid },
404
+ })];
329
405
  case 1:
330
406
  _a.sent();
331
407
  return [2 /*return*/, true];
@@ -1,10 +1,18 @@
1
+ import hashlib
2
+ import json
3
+
4
+
1
5
  class Interface(object):
2
6
  obj_ids = dict()
3
7
  listeners = dict()
4
8
 
5
9
  @staticmethod
6
10
  def save_obj(obj):
7
- obj_id = id(obj)
11
+ try:
12
+ obj_id = "live_" + str(obj._live_ptr)
13
+ except:
14
+ obj_id = "id_" + str(id(obj))
15
+
8
16
  Interface.obj_ids[obj_id] = obj
9
17
  return obj_id
10
18
 
@@ -20,25 +28,43 @@ class Interface(object):
20
28
  def get_ns(self, nsid):
21
29
  return Interface.obj_ids[nsid]
22
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
+
23
47
  def handle(self, payload):
24
48
  name = payload.get("name")
25
49
  uuid = payload.get("uuid")
50
+ etag = payload.get("etag")
26
51
  args = payload.get("args", {})
52
+ cache = payload.get("cache", False)
27
53
  ns = self.get_ns(payload.get("nsid"))
28
54
 
29
55
  try:
30
56
  # Try self-defined functions first
31
57
  if hasattr(self, name) and callable(getattr(self, name)):
32
58
  result = getattr(self, name)(ns=ns, **args)
33
- self.socket.send("result", result, uuid)
59
+ self.send_result(result, uuid, etag, cache)
34
60
  # Check if the function exists in the Ableton API as fallback
35
61
  elif hasattr(ns, name) and callable(getattr(ns, name)):
36
62
  if isinstance(args, dict):
37
63
  result = getattr(ns, name)(**args)
38
- self.socket.send("result", result, uuid)
64
+ self.send_result(result, uuid, etag, cache)
39
65
  elif isinstance(args, list):
40
66
  result = getattr(ns, name)(*args)
41
- self.socket.send("result", result, uuid)
67
+ self.send_result(result, uuid, etag, cache)
42
68
  else:
43
69
  self.socket.send("error", "Function call failed: " + str(args) +
44
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.5.4"
13
+ return "2.7.0"
@@ -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
 
@@ -8,9 +8,19 @@ from .Track import Track
8
8
  class Song(Interface):
9
9
  def __init__(self, c_instance, socket):
10
10
  super(Song, self).__init__(c_instance, socket)
11
+ self.song = self.ableton.song()
11
12
 
12
13
  def get_ns(self, nsid):
13
- return self.ableton.song()
14
+ return self.song
15
+
16
+ def create_audio_track(self, ns, index):
17
+ return Track.serialize_track(ns.create_audio_track(index))
18
+
19
+ def create_midi_track(self, ns, index):
20
+ return Track.serialize_track(ns.create_midi_track(index))
21
+
22
+ def create_return_track(self, ns):
23
+ return Track.serialize_track(ns.create_return_track())
14
24
 
15
25
  def get_clip_trigger_quantization(self, ns):
16
26
  return str(ns.clip_trigger_quantization)
@@ -26,7 +26,6 @@ class Track(Interface):
26
26
  def get_clip_slots(self, ns):
27
27
  return list(map(ClipSlot.serialize_clip_slot, ns.clip_slots))
28
28
 
29
-
30
29
  def get_group_track(self, ns):
31
30
  return Track.serialize_track(ns.group_track)
32
31
 
package/ns/clip-slot.d.ts CHANGED
@@ -39,7 +39,7 @@ export interface ObservableProperties {
39
39
  playing_status: PlayingStatus;
40
40
  }
41
41
  export interface RawClipSlot {
42
- id: number;
42
+ id: string;
43
43
  color: number;
44
44
  has_clip: boolean;
45
45
  is_playing: boolean;
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
@@ -128,7 +128,7 @@ export interface ObservableProperties {
128
128
  warping: boolean;
129
129
  }
130
130
  export interface RawClip {
131
- id: number;
131
+ id: string;
132
132
  name: string;
133
133
  color: number;
134
134
  is_audio_clip: boolean;
package/ns/cue-point.d.ts CHANGED
@@ -13,7 +13,7 @@ export interface ObservableProperties {
13
13
  time: number;
14
14
  }
15
15
  export interface RawCuePoint {
16
- id: number;
16
+ id: string;
17
17
  name: string;
18
18
  time: number;
19
19
  }
@@ -25,7 +25,7 @@ export interface ObservableProperties {
25
25
  value: number;
26
26
  }
27
27
  export interface RawDeviceParameter {
28
- id: number;
28
+ id: string;
29
29
  name: string;
30
30
  value: number;
31
31
  is_quantized: boolean;
package/ns/device.d.ts CHANGED
@@ -24,7 +24,7 @@ export interface ObservableProperties {
24
24
  parameters: string;
25
25
  }
26
26
  export interface RawDevice {
27
- id: number;
27
+ id: string;
28
28
  name: string;
29
29
  type: DeviceType;
30
30
  class_name: string;
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
@@ -2,11 +2,14 @@ import { Ableton } from "..";
2
2
  export declare class Namespace<GP, TP, SP, OP> {
3
3
  protected ableton: Ableton;
4
4
  protected ns: string;
5
- protected nsid?: number | undefined;
6
- constructor(ableton: Ableton, ns: string, nsid?: number | undefined);
5
+ protected nsid?: string | undefined;
7
6
  protected transformers: Partial<{
8
7
  [T in Extract<keyof GP, keyof TP>]: (val: GP[T]) => TP[T];
9
8
  }>;
9
+ protected cachedProps: Partial<{
10
+ [T in keyof GP]: boolean;
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.d.ts CHANGED
@@ -29,7 +29,7 @@ export interface ObservableProperties {
29
29
  }
30
30
  export interface RawScene {
31
31
  color: number;
32
- id: number;
32
+ id: string;
33
33
  name: string;
34
34
  }
35
35
  export declare class Scene extends Namespace<GettableProperties, TransformedProperties, SettableProperties, ObservableProperties> {
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.d.ts CHANGED
@@ -111,7 +111,7 @@ export interface ObservableProperties {
111
111
  solo: boolean;
112
112
  }
113
113
  export interface RawTrack {
114
- id: number;
114
+ id: string;
115
115
  name: string;
116
116
  color: number;
117
117
  is_foldable: boolean;
package/ns/track.js CHANGED
@@ -36,12 +36,17 @@ 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) {
42
47
  return this.sendCommand("duplicate_clip_to_arrangement", {
43
48
  clip_id: clipID,
44
- time: time
49
+ time: time,
45
50
  });
46
51
  };
47
52
  return Track;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ableton-js",
3
- "version": "2.5.4",
3
+ "version": "2.7.0",
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;