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 +15 -0
- package/README.md +36 -3
- package/index.d.ts +23 -10
- package/index.js +106 -30
- package/midi-script/Interface.py +30 -4
- package/midi-script/Internal.py +1 -1
- package/midi-script/Socket.py +1 -0
- package/midi-script/Song.py +11 -1
- package/midi-script/Track.py +0 -1
- package/ns/clip-slot.d.ts +1 -1
- package/ns/clip-slot.js +3 -0
- package/ns/clip.d.ts +1 -1
- package/ns/cue-point.d.ts +1 -1
- package/ns/device-parameter.d.ts +1 -1
- package/ns/device.d.ts +1 -1
- package/ns/device.js +4 -1
- package/ns/index.d.ts +12 -2
- package/ns/index.js +18 -4
- package/ns/midi.d.ts +2 -2
- package/ns/midi.js +7 -6
- package/ns/scene.d.ts +1 -1
- package/ns/scene.js +3 -0
- package/ns/song-view.js +14 -6
- package/ns/song.js +15 -7
- package/ns/track.d.ts +1 -1
- package/ns/track.js +6 -1
- package/package.json +2 -1
- package/util/cache.d.ts +14 -0
- package/util/cache.js +7 -0
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?:
|
|
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(
|
|
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(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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(
|
|
87
|
-
|
|
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 (
|
|
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
|
|
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 (
|
|
278
|
+
res: function (result) {
|
|
256
279
|
_this.setPing(Date.now() - currentTimestamp);
|
|
257
280
|
clearTimeout(timeoutId);
|
|
258
|
-
res(
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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(
|
|
293
|
-
|
|
362
|
+
return [4 /*yield*/, this.sendCommand({
|
|
363
|
+
ns: ns,
|
|
294
364
|
nsid: nsid,
|
|
295
|
-
|
|
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(
|
|
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];
|
package/midi-script/Interface.py
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
64
|
+
self.send_result(result, uuid, etag, cache)
|
|
39
65
|
elif isinstance(args, list):
|
|
40
66
|
result = getattr(ns, name)(*args)
|
|
41
|
-
self.
|
|
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)
|
package/midi-script/Internal.py
CHANGED
package/midi-script/Socket.py
CHANGED
package/midi-script/Song.py
CHANGED
|
@@ -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.
|
|
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)
|
package/midi-script/Track.py
CHANGED
package/ns/clip-slot.d.ts
CHANGED
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
package/ns/cue-point.d.ts
CHANGED
package/ns/device-parameter.d.ts
CHANGED
package/ns/device.d.ts
CHANGED
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(
|
|
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?:
|
|
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:
|
|
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
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
|
|
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 &&
|
|
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
|
-
}(
|
|
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:
|
|
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
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(
|
|
66
|
-
selected_track: function (track) { return new track_1.Track(
|
|
67
|
-
selected_scene: function (scene) { return new scene_1.Scene(
|
|
68
|
-
highlighted_clip_slot: function (
|
|
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(
|
|
76
|
-
|
|
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(
|
|
105
|
-
master_track: function (track) { return new track_1.Track(
|
|
106
|
-
return_tracks: function (tracks) { return tracks.map(function (t) { return new track_1.Track(
|
|
107
|
-
tracks: function (tracks) { return tracks.map(function (t) { return new track_1.Track(
|
|
108
|
-
visible_tracks: function (tracks) { return tracks.map(function (t) { return new track_1.Track(
|
|
109
|
-
scenes: function (scenes) { return scenes.map(function (s) { return new scene_1.Scene(
|
|
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.
|
|
208
|
+
return [2 /*return*/, this.sendCachedCommand("get_data", { key: key })];
|
|
201
209
|
});
|
|
202
210
|
});
|
|
203
211
|
};
|
package/ns/track.d.ts
CHANGED
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.
|
|
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
|
}
|
package/util/cache.d.ts
ADDED
|
@@ -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>;
|