ableton-js 3.1.8 → 3.2.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 +16 -0
- package/README.md +3 -6
- package/index.d.ts +36 -4
- package/index.js +316 -528
- package/midi-script/Socket.py +27 -4
- package/midi-script/version.py +1 -1
- package/ns/application-view.d.ts +3 -3
- package/ns/application-view.js +30 -116
- package/ns/application-view.spec.js +7 -57
- package/ns/application.js +10 -68
- package/ns/application.spec.js +7 -57
- package/ns/clip-slot.js +26 -43
- package/ns/clip.js +68 -132
- package/ns/cue-point.js +10 -67
- package/ns/device-parameter.js +9 -26
- package/ns/device.js +12 -29
- package/ns/index.d.ts +2 -2
- package/ns/index.js +47 -98
- package/ns/internal.js +11 -73
- package/ns/midi.js +18 -36
- package/ns/mixer-device.js +20 -37
- package/ns/mixer-device.spec.js +9 -64
- package/ns/scene.js +14 -73
- package/ns/song-view.js +21 -79
- package/ns/song-view.spec.js +7 -57
- package/ns/song.js +105 -271
- package/ns/song.spec.js +25 -112
- package/ns/track.js +23 -44
- package/package.json +6 -6
- package/util/cache.d.ts +3 -3
- package/util/cache.js +1 -3
- package/util/color.js +21 -33
- package/util/note.d.ts +1 -1
- package/util/note.js +4 -4
- package/util/package-version.js +5 -5
- package/util/package-version.spec.js +6 -6
- package/util/tests.js +12 -62
package/index.js
CHANGED
|
@@ -1,184 +1,101 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __extends = (this && this.__extends) || (function () {
|
|
3
|
-
var extendStatics = function (d, b) {
|
|
4
|
-
extendStatics = Object.setPrototypeOf ||
|
|
5
|
-
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
|
6
|
-
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
|
7
|
-
return extendStatics(d, b);
|
|
8
|
-
};
|
|
9
|
-
return function (d, b) {
|
|
10
|
-
if (typeof b !== "function" && b !== null)
|
|
11
|
-
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
|
|
12
|
-
extendStatics(d, b);
|
|
13
|
-
function __() { this.constructor = d; }
|
|
14
|
-
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
|
15
|
-
};
|
|
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
|
-
};
|
|
28
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
29
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
30
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
31
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
32
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
33
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
34
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
35
|
-
});
|
|
36
|
-
};
|
|
37
|
-
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
38
|
-
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
39
|
-
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
40
|
-
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
41
|
-
function step(op) {
|
|
42
|
-
if (f) throw new TypeError("Generator is already executing.");
|
|
43
|
-
while (_) try {
|
|
44
|
-
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
45
|
-
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
46
|
-
switch (op[0]) {
|
|
47
|
-
case 0: case 1: t = op; break;
|
|
48
|
-
case 4: _.label++; return { value: op[1], done: false };
|
|
49
|
-
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
50
|
-
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
51
|
-
default:
|
|
52
|
-
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
53
|
-
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
54
|
-
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
55
|
-
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
56
|
-
if (t[2]) _.ops.pop();
|
|
57
|
-
_.trys.pop(); continue;
|
|
58
|
-
}
|
|
59
|
-
op = body.call(thisArg, _);
|
|
60
|
-
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
61
|
-
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
62
|
-
}
|
|
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
|
-
};
|
|
80
|
-
var __spreadArray = (this && this.__spreadArray) || function (to, from) {
|
|
81
|
-
for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
|
|
82
|
-
to[j] = from[i];
|
|
83
|
-
return to;
|
|
84
|
-
};
|
|
85
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
86
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
87
4
|
};
|
|
88
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
89
6
|
exports.getPackageVersion = exports.Ableton = exports.TimeoutError = void 0;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const dgram_1 = __importDefault(require("dgram"));
|
|
10
|
+
const events_1 = require("events");
|
|
11
|
+
const uuid_1 = require("uuid");
|
|
12
|
+
const semver_1 = __importDefault(require("semver"));
|
|
13
|
+
const zlib_1 = require("zlib");
|
|
14
|
+
const lru_cache_1 = __importDefault(require("lru-cache"));
|
|
15
|
+
const fs_1 = require("fs");
|
|
16
|
+
const promises_1 = require("fs/promises");
|
|
17
|
+
const song_1 = require("./ns/song");
|
|
18
|
+
const internal_1 = require("./ns/internal");
|
|
19
|
+
const application_1 = require("./ns/application");
|
|
20
|
+
const midi_1 = require("./ns/midi");
|
|
21
|
+
const package_version_1 = require("./util/package-version");
|
|
22
|
+
const cache_1 = require("./util/cache");
|
|
23
|
+
const SERVER_PORT_FILE = "ableton-js-server.port";
|
|
24
|
+
const CLIENT_PORT_FILE = "ableton-js-client.port";
|
|
25
|
+
class TimeoutError extends Error {
|
|
26
|
+
message;
|
|
27
|
+
payload;
|
|
28
|
+
constructor(message, payload) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.message = message;
|
|
31
|
+
this.payload = payload;
|
|
115
32
|
}
|
|
116
|
-
|
|
117
|
-
}(Error));
|
|
33
|
+
}
|
|
118
34
|
exports.TimeoutError = TimeoutError;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
35
|
+
class Ableton extends events_1.EventEmitter {
|
|
36
|
+
options;
|
|
37
|
+
client;
|
|
38
|
+
msgMap = new Map();
|
|
39
|
+
eventListeners = new Map();
|
|
40
|
+
heartbeatInterval;
|
|
41
|
+
_isConnected = false;
|
|
42
|
+
buffer = [];
|
|
43
|
+
latency = 0;
|
|
44
|
+
serverPort;
|
|
45
|
+
cache;
|
|
46
|
+
song = new song_1.Song(this);
|
|
47
|
+
application = new application_1.Application(this);
|
|
48
|
+
internal = new internal_1.Internal(this);
|
|
49
|
+
midi = new midi_1.Midi(this);
|
|
50
|
+
clientPortFile;
|
|
51
|
+
serverPortFile;
|
|
52
|
+
logger;
|
|
53
|
+
clientState = "closed";
|
|
54
|
+
cancelDisconnectEvent = false;
|
|
55
|
+
constructor(options) {
|
|
56
|
+
super();
|
|
57
|
+
this.options = options;
|
|
58
|
+
this.logger = options?.logger;
|
|
59
|
+
this.cache = new lru_cache_1.default({
|
|
60
|
+
max: 500,
|
|
61
|
+
ttl: 1000 * 60 * 10,
|
|
62
|
+
...options?.cacheOptions,
|
|
63
|
+
});
|
|
64
|
+
this.clientPortFile = path_1.default.join(os_1.default.tmpdir(), this.options?.clientPortFile ?? CLIENT_PORT_FILE);
|
|
65
|
+
this.serverPortFile = path_1.default.join(os_1.default.tmpdir(), this.options?.serverPortFile ?? SERVER_PORT_FILE);
|
|
141
66
|
}
|
|
142
|
-
|
|
143
|
-
var _a;
|
|
67
|
+
handleConnect(type) {
|
|
144
68
|
if (!this._isConnected) {
|
|
145
69
|
this._isConnected = true;
|
|
146
|
-
|
|
70
|
+
this.logger?.info("Live connected", { type });
|
|
147
71
|
this.emit("connect", type);
|
|
148
72
|
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
var _a;
|
|
73
|
+
}
|
|
74
|
+
handleDisconnect(type) {
|
|
152
75
|
if (this._isConnected) {
|
|
153
76
|
this._isConnected = false;
|
|
154
77
|
this.eventListeners.clear();
|
|
155
|
-
this.msgMap.forEach(
|
|
78
|
+
this.msgMap.forEach((msg) => msg.clearTimeout());
|
|
156
79
|
this.msgMap.clear();
|
|
157
|
-
|
|
80
|
+
this.logger?.info("Live disconnected", { type });
|
|
158
81
|
this.emit("disconnect", type);
|
|
159
82
|
}
|
|
160
|
-
}
|
|
83
|
+
}
|
|
161
84
|
/**
|
|
162
85
|
* If connected, returns immediately. Otherwise,
|
|
163
86
|
* it waits for a connection event before returning.
|
|
164
87
|
*/
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
])];
|
|
177
|
-
}
|
|
178
|
-
return [2 /*return*/];
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
};
|
|
88
|
+
async waitForConnection() {
|
|
89
|
+
if (this._isConnected) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
return Promise.race([
|
|
94
|
+
new Promise((res) => this.once("connect", res)),
|
|
95
|
+
this.internal.get("ping").catch(() => new Promise(() => { })),
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
182
99
|
/**
|
|
183
100
|
* Starts the server and waits for a connection with Live to be established.
|
|
184
101
|
*
|
|
@@ -186,230 +103,142 @@ var Ableton = /** @class */ (function (_super) {
|
|
|
186
103
|
* If set, the function will throw an error if it can't establish a connection
|
|
187
104
|
* in the given time. Should be higher than 2000ms to avoid false positives.
|
|
188
105
|
*/
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
this.client = dgram_1.default.createSocket({ type: "udp4" });
|
|
203
|
-
this.client.addListener("message", this.handleIncoming.bind(this));
|
|
204
|
-
this.client.addListener("listening", function () { return __awaiter(_this, void 0, void 0, function () {
|
|
205
|
-
var port;
|
|
206
|
-
var _a, _b;
|
|
207
|
-
return __generator(this, function (_c) {
|
|
208
|
-
switch (_c.label) {
|
|
209
|
-
case 0:
|
|
210
|
-
port = (_a = this.client) === null || _a === void 0 ? void 0 : _a.address().port;
|
|
211
|
-
(_b = this.logger) === null || _b === void 0 ? void 0 : _b.info("Client is bound and listening", { port: port });
|
|
212
|
-
// Write used port to a file so Live can read from it
|
|
213
|
-
return [4 /*yield*/, promises_1.writeFile(this.clientPortFile, String(port))];
|
|
214
|
-
case 1:
|
|
215
|
-
// Write used port to a file so Live can read from it
|
|
216
|
-
_c.sent();
|
|
217
|
-
return [2 /*return*/];
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
}); });
|
|
221
|
-
_j.label = 1;
|
|
222
|
-
case 1:
|
|
223
|
-
_j.trys.push([1, 3, , 4]);
|
|
224
|
-
// Try binding to the port that was used last for better start performance
|
|
225
|
-
(_b = this.logger) === null || _b === void 0 ? void 0 : _b.info("Checking if a stored port exists", {
|
|
226
|
-
file: this.clientPortFile,
|
|
227
|
-
});
|
|
228
|
-
return [4 /*yield*/, promises_1.readFile(this.clientPortFile)];
|
|
229
|
-
case 2:
|
|
230
|
-
clientPort = _j.sent();
|
|
231
|
-
port = Number(clientPort.toString());
|
|
232
|
-
(_c = this.logger) === null || _c === void 0 ? void 0 : _c.info("Trying to bind to the most recent port", { port: port });
|
|
233
|
-
this.client.bind(port, "127.0.0.1");
|
|
234
|
-
return [3 /*break*/, 4];
|
|
235
|
-
case 3:
|
|
236
|
-
error_1 = _j.sent();
|
|
237
|
-
(_d = this.logger) === null || _d === void 0 ? void 0 : _d.info("Couldn't bind to last port, binding to any free port instead", { error: error_1 });
|
|
238
|
-
this.client.bind(undefined, "127.0.0.1");
|
|
239
|
-
return [3 /*break*/, 4];
|
|
240
|
-
case 4:
|
|
241
|
-
// Wait for the server port file to exist
|
|
242
|
-
return [4 /*yield*/, new Promise(function (res) { return __awaiter(_this, void 0, void 0, function () {
|
|
243
|
-
var serverPort, e_1;
|
|
244
|
-
var _this = this;
|
|
245
|
-
var _a;
|
|
246
|
-
return __generator(this, function (_b) {
|
|
247
|
-
switch (_b.label) {
|
|
248
|
-
case 0:
|
|
249
|
-
_b.trys.push([0, 2, , 3]);
|
|
250
|
-
return [4 /*yield*/, promises_1.readFile(this.serverPortFile)];
|
|
251
|
-
case 1:
|
|
252
|
-
serverPort = _b.sent();
|
|
253
|
-
this.serverPort = Number(serverPort.toString());
|
|
254
|
-
(_a = this.logger) === null || _a === void 0 ? void 0 : _a.info("Server port:", { port: this.serverPort });
|
|
255
|
-
res();
|
|
256
|
-
return [3 /*break*/, 3];
|
|
257
|
-
case 2:
|
|
258
|
-
e_1 = _b.sent();
|
|
259
|
-
return [3 /*break*/, 3];
|
|
260
|
-
case 3:
|
|
261
|
-
// Set up a watcher in case the server port changes
|
|
262
|
-
fs_1.watchFile(this.serverPortFile, function (curr) { return __awaiter(_this, void 0, void 0, function () {
|
|
263
|
-
var serverPort, newPort;
|
|
264
|
-
var _a;
|
|
265
|
-
return __generator(this, function (_b) {
|
|
266
|
-
switch (_b.label) {
|
|
267
|
-
case 0:
|
|
268
|
-
if (!curr.isFile()) return [3 /*break*/, 2];
|
|
269
|
-
return [4 /*yield*/, promises_1.readFile(this.serverPortFile)];
|
|
270
|
-
case 1:
|
|
271
|
-
serverPort = _b.sent();
|
|
272
|
-
newPort = Number(serverPort.toString());
|
|
273
|
-
if (!isNaN(newPort) && newPort !== this.serverPort) {
|
|
274
|
-
(_a = this.logger) === null || _a === void 0 ? void 0 : _a.info("Server port changed:", { port: newPort });
|
|
275
|
-
this.serverPort = Number(serverPort.toString());
|
|
276
|
-
}
|
|
277
|
-
res();
|
|
278
|
-
_b.label = 2;
|
|
279
|
-
case 2: return [2 /*return*/];
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
}); });
|
|
283
|
-
return [2 /*return*/];
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
}); })];
|
|
287
|
-
case 5:
|
|
288
|
-
// Wait for the server port file to exist
|
|
289
|
-
_j.sent();
|
|
290
|
-
(_e = this.logger) === null || _e === void 0 ? void 0 : _e.info("Checking connection...");
|
|
291
|
-
connection = this.waitForConnection();
|
|
292
|
-
if (!timeoutMs) return [3 /*break*/, 7];
|
|
293
|
-
timeout = new Promise(function (_, rej) {
|
|
294
|
-
return setTimeout(function () { return rej("Connection timed out."); }, timeoutMs);
|
|
295
|
-
});
|
|
296
|
-
return [4 /*yield*/, Promise.race([connection, timeout])];
|
|
297
|
-
case 6:
|
|
298
|
-
_j.sent();
|
|
299
|
-
return [3 /*break*/, 9];
|
|
300
|
-
case 7: return [4 /*yield*/, connection];
|
|
301
|
-
case 8:
|
|
302
|
-
_j.sent();
|
|
303
|
-
_j.label = 9;
|
|
304
|
-
case 9:
|
|
305
|
-
(_f = this.logger) === null || _f === void 0 ? void 0 : _f.info("Got connection!");
|
|
306
|
-
this.clientState = "started";
|
|
307
|
-
this.handleConnect("start");
|
|
308
|
-
heartbeat = function () { return __awaiter(_this, void 0, void 0, function () {
|
|
309
|
-
var e_2;
|
|
310
|
-
return __generator(this, function (_a) {
|
|
311
|
-
switch (_a.label) {
|
|
312
|
-
case 0:
|
|
313
|
-
_a.trys.push([0, 2, 3, 4]);
|
|
314
|
-
return [4 /*yield*/, this.internal.get("ping")];
|
|
315
|
-
case 1:
|
|
316
|
-
_a.sent();
|
|
317
|
-
this.handleConnect("heartbeat");
|
|
318
|
-
return [3 /*break*/, 4];
|
|
319
|
-
case 2:
|
|
320
|
-
e_2 = _a.sent();
|
|
321
|
-
if (!this.cancelDisconnectEvent) {
|
|
322
|
-
this.handleDisconnect("heartbeat");
|
|
323
|
-
}
|
|
324
|
-
return [3 /*break*/, 4];
|
|
325
|
-
case 3:
|
|
326
|
-
this.cancelDisconnectEvent = false;
|
|
327
|
-
return [7 /*endfinally*/];
|
|
328
|
-
case 4: return [2 /*return*/];
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
}); };
|
|
332
|
-
this.heartbeatInterval = setInterval(heartbeat, (_h = (_g = this.options) === null || _g === void 0 ? void 0 : _g.heartbeatInterval) !== null && _h !== void 0 ? _h : 2000);
|
|
333
|
-
heartbeat();
|
|
334
|
-
this.internal
|
|
335
|
-
.get("version")
|
|
336
|
-
.then(function (v) {
|
|
337
|
-
var _a;
|
|
338
|
-
var jsVersion = package_version_1.getPackageVersion();
|
|
339
|
-
if (semver_1.default.lt(v, jsVersion)) {
|
|
340
|
-
(_a = _this.logger) === null || _a === void 0 ? void 0 : _a.warn("The installed version of your AbletonJS plugin (" + v + ") is lower than the JS library (" + jsVersion + ").", "Please update your AbletonJS plugin to the latest version: https://git.io/JvaOu");
|
|
341
|
-
}
|
|
342
|
-
})
|
|
343
|
-
.catch(function () { });
|
|
344
|
-
return [2 /*return*/];
|
|
345
|
-
}
|
|
346
|
-
});
|
|
106
|
+
async start(timeoutMs) {
|
|
107
|
+
if (this.clientState !== "closed") {
|
|
108
|
+
this.logger?.warn("Tried calling start, but client is already " + this.clientState);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.clientState = "starting";
|
|
112
|
+
this.client = dgram_1.default.createSocket({ type: "udp4" });
|
|
113
|
+
this.client.addListener("message", this.handleIncoming.bind(this));
|
|
114
|
+
this.client.addListener("listening", async () => {
|
|
115
|
+
const port = this.client?.address().port;
|
|
116
|
+
this.logger?.info("Client is bound and listening", { port });
|
|
117
|
+
// Write used port to a file so Live can read from it
|
|
118
|
+
await (0, promises_1.writeFile)(this.clientPortFile, String(port));
|
|
347
119
|
});
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
120
|
+
try {
|
|
121
|
+
// Try binding to the port that was used last for better start performance
|
|
122
|
+
this.logger?.info("Checking if a stored port exists", {
|
|
123
|
+
file: this.clientPortFile,
|
|
124
|
+
});
|
|
125
|
+
const clientPort = await (0, promises_1.readFile)(this.clientPortFile);
|
|
126
|
+
const port = Number(clientPort.toString());
|
|
127
|
+
this.logger?.info("Trying to bind to the most recent port", { port });
|
|
128
|
+
this.client.bind(port, "127.0.0.1");
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
this.logger?.info("Couldn't bind to last port, binding to any free port instead", { error });
|
|
132
|
+
this.client.bind(undefined, "127.0.0.1");
|
|
133
|
+
}
|
|
134
|
+
// Wait for the server port file to exist
|
|
135
|
+
await new Promise(async (res) => {
|
|
136
|
+
try {
|
|
137
|
+
const serverPort = await (0, promises_1.readFile)(this.serverPortFile);
|
|
138
|
+
this.serverPort = Number(serverPort.toString());
|
|
139
|
+
this.logger?.info("Server port:", { port: this.serverPort });
|
|
140
|
+
res();
|
|
141
|
+
}
|
|
142
|
+
catch (e) { }
|
|
143
|
+
// Set up a watcher in case the server port changes
|
|
144
|
+
(0, fs_1.watchFile)(this.serverPortFile, async (curr) => {
|
|
145
|
+
if (curr.isFile()) {
|
|
146
|
+
const serverPort = await (0, promises_1.readFile)(this.serverPortFile);
|
|
147
|
+
const newPort = Number(serverPort.toString());
|
|
148
|
+
if (!isNaN(newPort) && newPort !== this.serverPort) {
|
|
149
|
+
this.logger?.info("Server port changed:", { port: newPort });
|
|
150
|
+
this.serverPort = Number(serverPort.toString());
|
|
151
|
+
}
|
|
152
|
+
res();
|
|
375
153
|
}
|
|
376
154
|
});
|
|
377
155
|
});
|
|
378
|
-
|
|
156
|
+
this.logger?.info("Checking connection...");
|
|
157
|
+
const connection = this.waitForConnection();
|
|
158
|
+
if (timeoutMs) {
|
|
159
|
+
const timeout = new Promise((_, rej) => setTimeout(() => rej("Connection timed out."), timeoutMs));
|
|
160
|
+
await Promise.race([connection, timeout]);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
await connection;
|
|
164
|
+
}
|
|
165
|
+
this.logger?.info("Got connection!");
|
|
166
|
+
this.clientState = "started";
|
|
167
|
+
this.handleConnect("start");
|
|
168
|
+
const heartbeat = async () => {
|
|
169
|
+
try {
|
|
170
|
+
await this.internal.get("ping");
|
|
171
|
+
this.handleConnect("heartbeat");
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
if (!this.cancelDisconnectEvent) {
|
|
175
|
+
this.handleDisconnect("heartbeat");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
this.cancelDisconnectEvent = false;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
this.heartbeatInterval = setInterval(heartbeat, this.options?.heartbeatInterval ?? 2000);
|
|
183
|
+
heartbeat();
|
|
184
|
+
this.internal
|
|
185
|
+
.get("version")
|
|
186
|
+
.then((v) => {
|
|
187
|
+
const jsVersion = (0, package_version_1.getPackageVersion)();
|
|
188
|
+
if (semver_1.default.lt(v, jsVersion)) {
|
|
189
|
+
this.logger?.warn(`The installed version of your AbletonJS plugin (${v}) is lower than the JS library (${jsVersion}).`, "Please update your AbletonJS plugin to the latest version: https://git.io/JvaOu");
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
.catch(() => { });
|
|
193
|
+
}
|
|
194
|
+
/** Closes the client */
|
|
195
|
+
async close() {
|
|
196
|
+
this.logger?.info("Closing the client");
|
|
197
|
+
(0, fs_1.unwatchFile)(this.serverPortFile);
|
|
198
|
+
if (this.heartbeatInterval) {
|
|
199
|
+
clearInterval(this.heartbeatInterval);
|
|
200
|
+
}
|
|
201
|
+
if (this.client) {
|
|
202
|
+
const closePromise = new Promise((res) => this.client?.once("close", res));
|
|
203
|
+
this.client.close();
|
|
204
|
+
await closePromise;
|
|
205
|
+
}
|
|
206
|
+
this.clientState = "closed";
|
|
207
|
+
this._isConnected = false;
|
|
208
|
+
this.logger?.info("Client closed");
|
|
209
|
+
}
|
|
379
210
|
/**
|
|
380
211
|
* Returns the latency between the last command and its response.
|
|
381
212
|
* This is a rough measurement, so don't rely too much on it.
|
|
382
213
|
*/
|
|
383
|
-
|
|
214
|
+
getPing() {
|
|
384
215
|
return this.latency;
|
|
385
|
-
}
|
|
386
|
-
|
|
216
|
+
}
|
|
217
|
+
setPing(latency) {
|
|
387
218
|
this.latency = latency;
|
|
388
219
|
this.emit("ping", this.latency);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
var _a;
|
|
220
|
+
}
|
|
221
|
+
handleIncoming(msg, info) {
|
|
392
222
|
try {
|
|
393
|
-
|
|
394
|
-
|
|
223
|
+
const index = msg[0];
|
|
224
|
+
const message = msg.slice(1);
|
|
395
225
|
this.buffer[index] = message;
|
|
396
226
|
// 0xFF signals that the end of the buffer has been reached
|
|
397
227
|
if (index === 255) {
|
|
398
|
-
this.handleUncompressedMessage(zlib_1.unzipSync(Buffer.concat(this.buffer.filter(
|
|
228
|
+
this.handleUncompressedMessage((0, zlib_1.unzipSync)(Buffer.concat(this.buffer.filter((b) => b))).toString());
|
|
399
229
|
this.buffer = [];
|
|
400
230
|
}
|
|
401
231
|
}
|
|
402
232
|
catch (e) {
|
|
403
233
|
this.buffer = [];
|
|
404
|
-
|
|
234
|
+
this.logger?.warn("Couldn't handle message:", { error: e });
|
|
405
235
|
this.emit("error", e);
|
|
406
236
|
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
var _a, _b, _c, _d;
|
|
237
|
+
}
|
|
238
|
+
handleUncompressedMessage(msg) {
|
|
410
239
|
this.emit("raw_message", msg);
|
|
411
|
-
|
|
412
|
-
|
|
240
|
+
const data = JSON.parse(msg);
|
|
241
|
+
const functionCallback = this.msgMap.get(data.uuid);
|
|
413
242
|
this.emit("message", data);
|
|
414
243
|
if (data.event === "result" && functionCallback) {
|
|
415
244
|
this.msgMap.delete(data.uuid);
|
|
@@ -426,215 +255,174 @@ var Ableton = /** @class */ (function (_super) {
|
|
|
426
255
|
// If some heartbeat ping from the old connection is still pending,
|
|
427
256
|
// cancel it to prevent a double disconnect/connect event.
|
|
428
257
|
this.cancelDisconnectEvent = true;
|
|
429
|
-
if (
|
|
430
|
-
|
|
258
|
+
if (data.data?.port && data.data?.port !== this.serverPort) {
|
|
259
|
+
this.logger?.info("Got new server port via connect:", {
|
|
431
260
|
port: data.data.port,
|
|
432
261
|
});
|
|
433
262
|
this.serverPort = data.data.port;
|
|
434
263
|
}
|
|
435
264
|
return this.handleConnect(this.clientState === "starting" ? "start" : "realtime");
|
|
436
265
|
}
|
|
437
|
-
|
|
266
|
+
const eventCallback = this.eventListeners.get(data.event);
|
|
438
267
|
if (eventCallback) {
|
|
439
|
-
return eventCallback.forEach(
|
|
268
|
+
return eventCallback.forEach((cb) => cb(data.data));
|
|
440
269
|
}
|
|
441
270
|
if (data.uuid) {
|
|
442
|
-
|
|
443
|
-
msg
|
|
271
|
+
this.logger?.warn("Message could not be assigned to any request:", {
|
|
272
|
+
msg,
|
|
444
273
|
});
|
|
445
274
|
}
|
|
446
|
-
}
|
|
275
|
+
}
|
|
447
276
|
/**
|
|
448
277
|
* Sends a raw command to Ableton. Usually, you won't need this.
|
|
449
278
|
* A good starting point in general is the `song` prop.
|
|
450
279
|
*/
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
});
|
|
483
|
-
_this.sendRaw(msg);
|
|
484
|
-
})];
|
|
485
|
-
});
|
|
486
|
-
});
|
|
487
|
-
};
|
|
488
|
-
Ableton.prototype.sendCachedCommand = function (command, timeout) {
|
|
489
|
-
var _a, _b;
|
|
490
|
-
return __awaiter(this, void 0, void 0, function () {
|
|
491
|
-
var args, cacheKey, cached, result;
|
|
492
|
-
return __generator(this, function (_c) {
|
|
493
|
-
switch (_c.label) {
|
|
494
|
-
case 0:
|
|
495
|
-
args = (_b = (_a = command.args) === null || _a === void 0 ? void 0 : _a.prop) !== null && _b !== void 0 ? _b : JSON.stringify(command.args);
|
|
496
|
-
cacheKey = [command.ns, command.nsid, args].filter(Boolean).join("/");
|
|
497
|
-
cached = this.cache.get(cacheKey);
|
|
498
|
-
return [4 /*yield*/, this.sendCommand(__assign(__assign({}, command), { etag: cached === null || cached === void 0 ? void 0 : cached.etag, cache: true }), timeout)];
|
|
499
|
-
case 1:
|
|
500
|
-
result = _c.sent();
|
|
501
|
-
if (cache_1.isCached(result)) {
|
|
502
|
-
if (!cached) {
|
|
503
|
-
throw new Error("Tried to get an object that isn't cached.");
|
|
504
|
-
}
|
|
505
|
-
else {
|
|
506
|
-
return [2 /*return*/, cached.data];
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
else {
|
|
510
|
-
if (result.etag) {
|
|
511
|
-
this.cache.set(cacheKey, result);
|
|
512
|
-
}
|
|
513
|
-
return [2 /*return*/, result.data];
|
|
514
|
-
}
|
|
515
|
-
return [2 /*return*/];
|
|
516
|
-
}
|
|
280
|
+
async sendCommand(command) {
|
|
281
|
+
return new Promise((res, rej) => {
|
|
282
|
+
const msgId = (0, uuid_1.v4)();
|
|
283
|
+
const payload = {
|
|
284
|
+
uuid: msgId,
|
|
285
|
+
...command,
|
|
286
|
+
};
|
|
287
|
+
const msg = JSON.stringify(payload);
|
|
288
|
+
const timeout = this.options?.commandTimeoutMs ?? 2000;
|
|
289
|
+
const timeoutId = setTimeout(() => {
|
|
290
|
+
const arg = JSON.stringify(command.args);
|
|
291
|
+
const cls = command.nsid
|
|
292
|
+
? `${command.ns}(${command.nsid})`
|
|
293
|
+
: command.ns;
|
|
294
|
+
rej(new TimeoutError([
|
|
295
|
+
`The command ${cls}.${command.name}(${arg}) timed out after ${timeout} ms.`,
|
|
296
|
+
`Please make sure that Ableton is running and that you have the latest`,
|
|
297
|
+
`version of AbletonJS' MIDI script installed and renamed to "AbletonJS".`,
|
|
298
|
+
].join(" "), payload));
|
|
299
|
+
}, timeout);
|
|
300
|
+
const currentTimestamp = Date.now();
|
|
301
|
+
this.msgMap.set(msgId, {
|
|
302
|
+
res: (result) => {
|
|
303
|
+
this.setPing(Date.now() - currentTimestamp);
|
|
304
|
+
clearTimeout(timeoutId);
|
|
305
|
+
res(result);
|
|
306
|
+
},
|
|
307
|
+
rej,
|
|
308
|
+
clearTimeout: () => {
|
|
309
|
+
clearTimeout(timeoutId);
|
|
310
|
+
},
|
|
517
311
|
});
|
|
312
|
+
this.sendRaw(msg);
|
|
518
313
|
});
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
else {
|
|
529
|
-
return [2 /*return*/, this.sendCommand(params)];
|
|
530
|
-
}
|
|
531
|
-
return [2 /*return*/];
|
|
532
|
-
});
|
|
314
|
+
}
|
|
315
|
+
async sendCachedCommand(command) {
|
|
316
|
+
const args = command.args?.prop ?? JSON.stringify(command.args);
|
|
317
|
+
const cacheKey = [command.ns, command.nsid, args].filter(Boolean).join("/");
|
|
318
|
+
const cached = this.cache.get(cacheKey);
|
|
319
|
+
const result = await this.sendCommand({
|
|
320
|
+
...command,
|
|
321
|
+
etag: cached?.etag,
|
|
322
|
+
cache: true,
|
|
533
323
|
});
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
324
|
+
if ((0, cache_1.isCached)(result)) {
|
|
325
|
+
if (!cached) {
|
|
326
|
+
throw new Error("Tried to get an object that isn't cached.");
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
return cached.data;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
if (result.etag) {
|
|
334
|
+
this.cache.set(cacheKey, result);
|
|
335
|
+
}
|
|
336
|
+
return result.data;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async getProp(ns, nsid, prop, cache) {
|
|
340
|
+
const params = { ns, nsid, name: "get_prop", args: { prop } };
|
|
341
|
+
if (cache) {
|
|
342
|
+
return this.sendCachedCommand(params);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
return this.sendCommand(params);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async setProp(ns, nsid, prop, value) {
|
|
349
|
+
return this.sendCommand({
|
|
350
|
+
ns,
|
|
351
|
+
nsid,
|
|
352
|
+
name: "set_prop",
|
|
353
|
+
args: { prop, value },
|
|
545
354
|
});
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
eventId = uuid_1.v4();
|
|
555
|
-
return [4 /*yield*/, this.sendCommand({
|
|
556
|
-
ns: ns,
|
|
557
|
-
nsid: nsid,
|
|
558
|
-
name: "add_listener",
|
|
559
|
-
args: { prop: prop, nsid: nsid, eventId: eventId },
|
|
560
|
-
})];
|
|
561
|
-
case 1:
|
|
562
|
-
result = _a.sent();
|
|
563
|
-
if (!this.eventListeners.has(result)) {
|
|
564
|
-
this.eventListeners.set(result, [listener]);
|
|
565
|
-
}
|
|
566
|
-
else {
|
|
567
|
-
this.eventListeners.set(result, __spreadArray(__spreadArray([], __read(this.eventListeners.get(result))), [
|
|
568
|
-
listener,
|
|
569
|
-
]));
|
|
570
|
-
}
|
|
571
|
-
return [2 /*return*/, function () { return _this.removePropListener(ns, nsid, prop, result, listener); }];
|
|
572
|
-
}
|
|
573
|
-
});
|
|
355
|
+
}
|
|
356
|
+
async addPropListener(ns, nsid, prop, listener) {
|
|
357
|
+
const eventId = (0, uuid_1.v4)();
|
|
358
|
+
const result = await this.sendCommand({
|
|
359
|
+
ns,
|
|
360
|
+
nsid,
|
|
361
|
+
name: "add_listener",
|
|
362
|
+
args: { prop, nsid, eventId },
|
|
574
363
|
});
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
}
|
|
364
|
+
if (!this.eventListeners.has(result)) {
|
|
365
|
+
this.eventListeners.set(result, [listener]);
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
this.eventListeners.set(result, [
|
|
369
|
+
...this.eventListeners.get(result),
|
|
370
|
+
listener,
|
|
371
|
+
]);
|
|
372
|
+
}
|
|
373
|
+
return () => this.removePropListener(ns, nsid, prop, result, listener);
|
|
374
|
+
}
|
|
375
|
+
async removePropListener(ns, nsid, prop, eventId, listener) {
|
|
376
|
+
const listeners = this.eventListeners.get(eventId);
|
|
377
|
+
if (!listeners) {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
if (listeners.length > 1) {
|
|
381
|
+
this.eventListeners.set(eventId, listeners.filter((l) => l !== listener));
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
if (listeners.length === 1) {
|
|
385
|
+
this.eventListeners.delete(eventId);
|
|
386
|
+
await this.sendCommand({
|
|
387
|
+
ns,
|
|
388
|
+
nsid,
|
|
389
|
+
name: "remove_listener",
|
|
390
|
+
args: { prop, nsid },
|
|
603
391
|
});
|
|
604
|
-
|
|
605
|
-
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
606
395
|
/**
|
|
607
396
|
* Removes all event listeners that were attached to properties.
|
|
608
397
|
* This is useful for clearing all listeners when Live
|
|
609
398
|
* disconnects, for example.
|
|
610
399
|
*/
|
|
611
|
-
|
|
400
|
+
removeAllPropListeners() {
|
|
612
401
|
this.eventListeners.clear();
|
|
613
|
-
}
|
|
614
|
-
|
|
402
|
+
}
|
|
403
|
+
sendRaw(msg) {
|
|
615
404
|
if (!this.client || !this.serverPort) {
|
|
616
405
|
throw new Error("The client hasn't been started yet. Please call start() first.");
|
|
617
406
|
}
|
|
618
|
-
|
|
407
|
+
const buffer = (0, zlib_1.deflateSync)(Buffer.from(msg));
|
|
619
408
|
// Based on this thread, 7500 bytes seems like a safe value
|
|
620
409
|
// https://stackoverflow.com/questions/22819214/udp-message-too-long
|
|
621
|
-
|
|
622
|
-
|
|
410
|
+
const byteLimit = 7500;
|
|
411
|
+
const chunks = Math.ceil(buffer.byteLength / byteLimit);
|
|
623
412
|
// Split the message into chunks if it becomes too large
|
|
624
|
-
for (
|
|
625
|
-
|
|
413
|
+
for (let i = 0; i < chunks; i++) {
|
|
414
|
+
const chunk = Buffer.concat([
|
|
626
415
|
// Add a counter to the message, the last message is always 255
|
|
627
416
|
Buffer.alloc(1, i + 1 === chunks ? 255 : i),
|
|
628
417
|
buffer.slice(i * byteLimit, i * byteLimit + byteLimit),
|
|
629
418
|
]);
|
|
630
419
|
this.client.send(chunk, 0, chunk.length, this.serverPort, "127.0.0.1");
|
|
631
420
|
}
|
|
632
|
-
}
|
|
633
|
-
|
|
421
|
+
}
|
|
422
|
+
isConnected() {
|
|
634
423
|
return this._isConnected;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
}(events_1.EventEmitter));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
638
426
|
exports.Ableton = Ableton;
|
|
639
427
|
var package_version_2 = require("./util/package-version");
|
|
640
428
|
Object.defineProperty(exports, "getPackageVersion", { enumerable: true, get: function () { return package_version_2.getPackageVersion; } });
|