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 +14 -0
- package/README.md +36 -3
- package/index.d.ts +19 -6
- package/index.js +104 -30
- package/midi-script/Clip.py +3 -0
- package/midi-script/Interface.py +25 -3
- package/midi-script/Internal.py +1 -1
- package/midi-script/Socket.py +1 -0
- package/ns/clip-slot.js +3 -0
- package/ns/clip.d.ts +2 -0
- package/ns/clip.js +7 -6
- 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.js +3 -0
- package/ns/song-view.js +14 -6
- package/ns/song.js +15 -7
- package/ns/track.js +5 -0
- 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,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(
|
|
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(
|
|
65
|
-
|
|
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(
|
|
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,62 @@ 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
|
+
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
|
-
|
|
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(
|
|
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(
|
|
293
|
-
|
|
360
|
+
return [4 /*yield*/, this.sendCommand({
|
|
361
|
+
ns: ns,
|
|
294
362
|
nsid: nsid,
|
|
295
|
-
|
|
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(
|
|
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];
|
package/midi-script/Clip.py
CHANGED
|
@@ -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
|
|
package/midi-script/Interface.py
CHANGED
|
@@ -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.
|
|
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.
|
|
64
|
+
self.send_result(result, uuid, etag, cache)
|
|
43
65
|
elif isinstance(args, list):
|
|
44
66
|
result = getattr(ns, name)(*args)
|
|
45
|
-
self.
|
|
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)
|
package/midi-script/Internal.py
CHANGED
package/midi-script/Socket.py
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
|
@@ -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(
|
|
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
|
|
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:
|
|
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.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.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.
|
|
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
|
}
|
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>;
|