@arcanewizards/tcnet 0.1.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.
@@ -0,0 +1,307 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
+
3
+ var _chunk7IUB6OJ3cjs = require('./chunk-7IUB6OJ3.cjs');
4
+
5
+
6
+
7
+ var _chunkVRXVI5VAcjs = require('./chunk-VRXVI5VA.cjs');
8
+
9
+
10
+ var _chunkVYNI4G3Kcjs = require('./chunk-VYNI4G3K.cjs');
11
+
12
+ // src/monitor.ts
13
+ var _events = require('events'); var _events2 = _interopRequireDefault(_events);
14
+ var MAX_DELTA_MS = 10;
15
+ var layerDataIdToIndex = (dataLayerId) => {
16
+ return dataLayerId - 1;
17
+ };
18
+ var layerIndexToDataId = (index) => {
19
+ return index + 1;
20
+ };
21
+ var asLayerIndex = (rawNumber) => {
22
+ return rawNumber;
23
+ };
24
+ var getLayerId = (node, layer) => {
25
+ return `${_chunkVRXVI5VAcjs.calculateUniqueNodeId.call(void 0, node)}:${layer}`;
26
+ };
27
+ var TCNET_LAYER_INDEXES = Array.from(
28
+ { length: _chunk7IUB6OJ3cjs.TCNET_LAYER_COUNT },
29
+ (_, i) => asLayerIndex(i)
30
+ );
31
+ var createTCNetTimecodeMonitor = (tcNetNode, logger) => {
32
+ const events = new (0, _events2.default)();
33
+ const on = events.on.bind(events);
34
+ const addListener = events.addListener.bind(events);
35
+ const removeListener = events.removeListener.bind(events);
36
+ const nodeStates = /* @__PURE__ */ new Map();
37
+ const getNodeState = (node) => {
38
+ const nodeKey = _chunkVRXVI5VAcjs.calculateUniqueNodeId.call(void 0, node);
39
+ let nodeState = nodeStates.get(nodeKey);
40
+ if (nodeState) {
41
+ return nodeState;
42
+ }
43
+ const layerInfoArray = new Array(_chunk7IUB6OJ3cjs.TCNET_LAYER_COUNT);
44
+ nodeState = {
45
+ node,
46
+ trackInfo: /* @__PURE__ */ new Map(),
47
+ getLayerInfo: (i) => layerInfoArray[i]
48
+ };
49
+ nodeStates.set(nodeKey, nodeState);
50
+ for (const i of TCNET_LAYER_INDEXES) {
51
+ layerInfoArray[i] = {
52
+ name: null,
53
+ status: "IDLE",
54
+ trackId: null,
55
+ pitchBend: 0,
56
+ speed: 0,
57
+ layerState: {
58
+ layerId: `${i}`,
59
+ layerName: `Layer ${i + 1}`,
60
+ totalTime: null,
61
+ info: null,
62
+ playState: {
63
+ state: "stopped",
64
+ currentTimeMillis: 0,
65
+ speed: 1,
66
+ onAir: false
67
+ }
68
+ }
69
+ };
70
+ }
71
+ return nodeState;
72
+ };
73
+ const updateMetadataForLayer = (node, layer) => {
74
+ const trackID = _optionalChain([getNodeState, 'call', _2 => _2(node), 'access', _3 => _3.getLayerInfo, 'call', _4 => _4(layer), 'optionalAccess', _5 => _5.trackId]);
75
+ if (typeof trackID !== "number") {
76
+ return;
77
+ }
78
+ tcNetNode.requestData(node, "METADATA", layerIndexToDataId(layer));
79
+ };
80
+ const updateMetricsForLayer = (node, layer) => {
81
+ const trackID = _optionalChain([getNodeState, 'call', _6 => _6(node), 'access', _7 => _7.getLayerInfo, 'call', _8 => _8(layer), 'optionalAccess', _9 => _9.trackId]);
82
+ if (typeof trackID !== "number") {
83
+ return;
84
+ }
85
+ tcNetNode.requestData(node, "METRICS_DATA", layerIndexToDataId(layer));
86
+ };
87
+ const getProbableTrackInfo = (node, trackID) => {
88
+ if (trackID === null) {
89
+ return null;
90
+ }
91
+ const trackInfoForTrack = getNodeState(node).trackInfo.get(trackID);
92
+ if (!trackInfoForTrack) {
93
+ return null;
94
+ }
95
+ let bestMatch = null;
96
+ for (const info of trackInfoForTrack.values()) {
97
+ const bestMatchCount = _nullishCoalesce(_optionalChain([bestMatch, 'optionalAccess', _10 => _10.matchCount]), () => ( 0));
98
+ if (
99
+ // Best match if we've seen this track the most times
100
+ info.matchCount > bestMatchCount || // Or if we've seen it aa similar same amount of times,
101
+ // but more recently
102
+ info.matchCount > bestMatchCount - 4 && info.lastMatchTime > (_nullishCoalesce(_optionalChain([bestMatch, 'optionalAccess', _11 => _11.lastMatchTime]), () => ( 0)))
103
+ ) {
104
+ bestMatch = info;
105
+ }
106
+ }
107
+ return _nullishCoalesce(_optionalChain([bestMatch, 'optionalAccess', _12 => _12.info]), () => ( null));
108
+ };
109
+ const updatePlayingTimecode = (node, i, info, layer) => {
110
+ if (!layer) {
111
+ return;
112
+ }
113
+ const now = Date.now();
114
+ const effectiveStartTime = now - layer.currentTimeMillis / info.speed;
115
+ const trackInfo = getProbableTrackInfo(node, info.trackId);
116
+ if (!info.layerState || info.layerState.playState.state !== "playing" || info.layerState.playState.state === "playing" && (_chunkVRXVI5VAcjs.differsByMoreThan.call(void 0,
117
+ info.layerState.playState.effectiveStartTime,
118
+ effectiveStartTime,
119
+ MAX_DELTA_MS
120
+ ) || _chunkVRXVI5VAcjs.differsByMoreThan.call(void 0,
121
+ _nullishCoalesce(_optionalChain([info, 'access', _13 => _13.layerState, 'access', _14 => _14.totalTime, 'optionalAccess', _15 => _15.timeMillis]), () => ( 0)),
122
+ layer.totalTimeMillis,
123
+ MAX_DELTA_MS
124
+ )) || info.layerState.info !== trackInfo || info.layerState.playState.speed !== info.speed || info.layerState.playState.onAir !== info.layerState.playState.onAir) {
125
+ info.layerState = {
126
+ layerId: getLayerId(node, i),
127
+ layerName: _nullishCoalesce(info.name, () => ( `Layer ${layerIndexToDataId(i)}`)),
128
+ totalTime: layer.totalTimeMillis > 0 ? {
129
+ timeMillis: layer.totalTimeMillis,
130
+ precisionMillis: 1e3
131
+ } : null,
132
+ info: trackInfo,
133
+ playState: {
134
+ state: "playing",
135
+ effectiveStartTime,
136
+ speed: info.speed,
137
+ onAir: _optionalChain([info, 'access', _16 => _16.layerState, 'optionalAccess', _17 => _17.playState, 'access', _18 => _18.onAir])
138
+ }
139
+ };
140
+ events.emit("timecode-changed", info.layerState);
141
+ }
142
+ };
143
+ const updatePausedTimecode = (node, i, info, layer) => {
144
+ if (!layer) {
145
+ return;
146
+ }
147
+ const trackInfo = getProbableTrackInfo(node, info.trackId);
148
+ if (!info.layerState || info.layerState.playState.state !== "stopped" || info.layerState.playState.state === "stopped" && (_chunkVRXVI5VAcjs.differsByMoreThan.call(void 0,
149
+ info.layerState.playState.currentTimeMillis,
150
+ layer.currentTimeMillis,
151
+ MAX_DELTA_MS
152
+ ) || _chunkVRXVI5VAcjs.differsByMoreThan.call(void 0,
153
+ _nullishCoalesce(_optionalChain([info, 'access', _19 => _19.layerState, 'access', _20 => _20.totalTime, 'optionalAccess', _21 => _21.timeMillis]), () => ( 0)),
154
+ layer.totalTimeMillis,
155
+ MAX_DELTA_MS
156
+ )) || info.layerState.info !== trackInfo || info.layerState.playState.speed !== info.speed || info.layerState.playState.onAir !== info.layerState.playState.onAir) {
157
+ info.layerState = {
158
+ layerId: getLayerId(node, i),
159
+ layerName: _nullishCoalesce(info.name, () => ( `Layer ${layerIndexToDataId(i)}`)),
160
+ totalTime: layer.totalTimeMillis > 0 ? {
161
+ timeMillis: layer.totalTimeMillis,
162
+ precisionMillis: 1e3
163
+ } : null,
164
+ info: trackInfo,
165
+ playState: {
166
+ state: "stopped",
167
+ currentTimeMillis: layer.currentTimeMillis,
168
+ speed: info.speed,
169
+ onAir: _optionalChain([info, 'access', _22 => _22.layerState, 'optionalAccess', _23 => _23.playState, 'access', _24 => _24.onAir])
170
+ }
171
+ };
172
+ events.emit("timecode-changed", info.layerState);
173
+ }
174
+ };
175
+ tcNetNode.on("data", ({ packet, node }) => {
176
+ const nodeState = getNodeState(node);
177
+ if (packet.dataType === "METADATA") {
178
+ if (packet.trackId) {
179
+ logger.warn(
180
+ new (0, _chunkVYNI4G3Kcjs.TCNetProtocolError)(
181
+ `Received unexpected trackId in METADATA packet, implementation needs to be updated to handle this!`
182
+ )
183
+ );
184
+ }
185
+ const info = {
186
+ title: packet.trackTitle || null,
187
+ artist: packet.trackArtist || null
188
+ };
189
+ if (!info.title && !info.artist) {
190
+ return;
191
+ }
192
+ const trackID = _optionalChain([nodeState, 'access', _25 => _25.getLayerInfo, 'call', _26 => _26(
193
+ layerDataIdToIndex(packet.layer)
194
+ ), 'optionalAccess', _27 => _27.trackId]);
195
+ if (typeof trackID !== "number") {
196
+ return;
197
+ }
198
+ let trackInfoForTrack = nodeState.trackInfo.get(trackID);
199
+ if (!trackInfoForTrack) {
200
+ trackInfoForTrack = /* @__PURE__ */ new Map();
201
+ nodeState.trackInfo.set(trackID, trackInfoForTrack);
202
+ }
203
+ const key = `${info.artist} - ${info.title}`;
204
+ const weightedInfo = trackInfoForTrack.get(key) || {
205
+ matchCount: 0,
206
+ lastMatchTime: Date.now(),
207
+ info
208
+ };
209
+ weightedInfo.matchCount++;
210
+ weightedInfo.lastMatchTime = Date.now();
211
+ trackInfoForTrack.set(key, weightedInfo);
212
+ } else if (packet.dataType === "METRICS_DATA") {
213
+ const info = nodeState.getLayerInfo(layerDataIdToIndex(packet.layer));
214
+ if (info) {
215
+ info.pitchBend = _nullishCoalesce(packet.pitchBend, () => ( 0));
216
+ info.speed = 1 + info.pitchBend / 1e4;
217
+ }
218
+ }
219
+ });
220
+ tcNetNode.on("time", ({ packet, node }) => {
221
+ const nodeState = getNodeState(node);
222
+ for (const i of TCNET_LAYER_INDEXES) {
223
+ const info = nodeState.getLayerInfo(i);
224
+ switch (_optionalChain([info, 'optionalAccess', _28 => _28.status])) {
225
+ case "PLAYING":
226
+ case "LOOPING":
227
+ case "CUEDOWN":
228
+ updatePlayingTimecode(node, i, info, packet.layers[i]);
229
+ break;
230
+ case "PAUSED":
231
+ case "STOPPED":
232
+ case "LOADING":
233
+ case "PLATTERDOWN":
234
+ case "HOLD":
235
+ case "FFWD":
236
+ case "FFRV":
237
+ updatePausedTimecode(node, i, info, packet.layers[i]);
238
+ break;
239
+ case "UNKNOWN":
240
+ case "IDLE":
241
+ }
242
+ }
243
+ });
244
+ tcNetNode.on("node-status", ({ packet, node }) => {
245
+ for (const i of TCNET_LAYER_INDEXES) {
246
+ const layer = packet.layers[i];
247
+ const info = getNodeState(node).getLayerInfo(i);
248
+ if (!info || !layer) {
249
+ throw new Error("Inconsistent layer indexes");
250
+ }
251
+ info.status = layer.status;
252
+ info.name = layer.name;
253
+ if (info.trackId !== layer.trackId && layer.trackId > 0) {
254
+ logger.info(
255
+ `Updated trackID for layer ${i}: ${info.trackId} -> ${layer.trackId}`
256
+ );
257
+ info.trackId = layer.trackId;
258
+ updateMetadataForLayer(node, i);
259
+ }
260
+ }
261
+ });
262
+ tcNetNode.on("nodes-changed", (nodes) => {
263
+ const knownNodeIds = new Set(
264
+ Object.values(nodes).map((node) => _chunkVRXVI5VAcjs.calculateUniqueNodeId.call(void 0, node))
265
+ );
266
+ for (const [nodeId, nodeState] of nodeStates.entries()) {
267
+ if (!knownNodeIds.has(nodeId)) {
268
+ for (const i of TCNET_LAYER_INDEXES) {
269
+ const layerState = _optionalChain([nodeState, 'access', _29 => _29.getLayerInfo, 'call', _30 => _30(i), 'optionalAccess', _31 => _31.layerState]);
270
+ if (layerState) {
271
+ events.emit("layer-removed", { layerId: layerState.layerId });
272
+ }
273
+ }
274
+ nodeStates.delete(nodeId);
275
+ }
276
+ }
277
+ });
278
+ const updateLoadedTracks = () => {
279
+ for (const nodeState of nodeStates.values()) {
280
+ for (const i of TCNET_LAYER_INDEXES) {
281
+ const info = nodeState.getLayerInfo(i);
282
+ if (_optionalChain([info, 'optionalAccess', _32 => _32.trackId])) {
283
+ updateMetadataForLayer(nodeState.node, i);
284
+ updateMetricsForLayer(nodeState.node, i);
285
+ }
286
+ }
287
+ }
288
+ };
289
+ let autoUpdater = null;
290
+ tcNetNode.on("ready", () => {
291
+ autoUpdater = setInterval(updateLoadedTracks, 1e3);
292
+ updateLoadedTracks();
293
+ });
294
+ tcNetNode.on("destroy", () => {
295
+ if (autoUpdater) {
296
+ clearInterval(autoUpdater);
297
+ }
298
+ });
299
+ return {
300
+ on,
301
+ addListener,
302
+ removeListener
303
+ };
304
+ };
305
+
306
+
307
+ exports.createTCNetTimecodeMonitor = createTCNetTimecodeMonitor;
@@ -0,0 +1,70 @@
1
+ import { TCNetNode, TCNetLogger } from './types.cjs';
2
+ import '@arcanewizards/net-utils';
3
+ import './protocol.cjs';
4
+
5
+ type TCNetTimecodeTrackInfo = {
6
+ title: string | null;
7
+ artist: string | null;
8
+ };
9
+ type TCNetTimecodePlayState = {
10
+ state: 'playing';
11
+ effectiveStartTime: number;
12
+ /**
13
+ * 1.0 means normal speed, 2.0 means double speed, etc.
14
+ * Can be negative for reverse playback,
15
+ *
16
+ * in which case effectiveStartTime represents the time when the track will reach 0:00.
17
+ */
18
+ speed: number;
19
+ onAir: boolean;
20
+ } | {
21
+ state: 'stopped';
22
+ currentTimeMillis: number;
23
+ /**
24
+ * Speed still exists when stopped,
25
+ * to indicate what speed the track will play at when started.
26
+ */
27
+ speed: number;
28
+ onAir: boolean;
29
+ };
30
+ type TCNetTimecodeState = {
31
+ layerId: string;
32
+ layerName: string;
33
+ /**
34
+ * If available, the total time of the track loaded in this layer.
35
+ *
36
+ * Some timecode sources will not have this information.
37
+ */
38
+ totalTime: {
39
+ timeMillis: number;
40
+ /**
41
+ * How accurate is the totalTimeMillis value,
42
+ * some sources (such as ShowKontrol) are not completely accurate.
43
+ */
44
+ precisionMillis: number;
45
+ } | null;
46
+ /**
47
+ * If available, the metadata info of the track loaded in this layer.
48
+ */
49
+ info: TCNetTimecodeTrackInfo | null;
50
+ playState: TCNetTimecodePlayState;
51
+ };
52
+ type TCNetTimecodeMonitorEventMap = {
53
+ 'timecode-changed': [TCNetTimecodeState];
54
+ 'layer-removed': [Pick<TCNetTimecodeState, 'layerId'>];
55
+ };
56
+ type TCNetTimecodeMonitor = {
57
+ on<K extends keyof TCNetTimecodeMonitorEventMap>(event: K, callback: (...args: TCNetTimecodeMonitorEventMap[K]) => void): void;
58
+ addListener<K extends keyof TCNetTimecodeMonitorEventMap>(event: K, callback: (...args: TCNetTimecodeMonitorEventMap[K]) => void): void;
59
+ removeListener<K extends keyof TCNetTimecodeMonitorEventMap>(event: K, callback: (...args: TCNetTimecodeMonitorEventMap[K]) => void): void;
60
+ };
61
+ /**
62
+ * Create a monitor that listens to TCNet messages to keep track of the current
63
+ * timecodes,
64
+ * accounts for differences in implementation details for different products,
65
+ * and produces a unified and simplified interface for monitoring timecodes
66
+ * with respect the internal clock.
67
+ */
68
+ declare const createTCNetTimecodeMonitor: (tcNetNode: TCNetNode, logger: TCNetLogger) => TCNetTimecodeMonitor;
69
+
70
+ export { type TCNetTimecodeMonitor, type TCNetTimecodeMonitorEventMap, type TCNetTimecodePlayState, type TCNetTimecodeState, type TCNetTimecodeTrackInfo, createTCNetTimecodeMonitor };
@@ -0,0 +1,70 @@
1
+ import { TCNetNode, TCNetLogger } from './types.js';
2
+ import '@arcanewizards/net-utils';
3
+ import './protocol.js';
4
+
5
+ type TCNetTimecodeTrackInfo = {
6
+ title: string | null;
7
+ artist: string | null;
8
+ };
9
+ type TCNetTimecodePlayState = {
10
+ state: 'playing';
11
+ effectiveStartTime: number;
12
+ /**
13
+ * 1.0 means normal speed, 2.0 means double speed, etc.
14
+ * Can be negative for reverse playback,
15
+ *
16
+ * in which case effectiveStartTime represents the time when the track will reach 0:00.
17
+ */
18
+ speed: number;
19
+ onAir: boolean;
20
+ } | {
21
+ state: 'stopped';
22
+ currentTimeMillis: number;
23
+ /**
24
+ * Speed still exists when stopped,
25
+ * to indicate what speed the track will play at when started.
26
+ */
27
+ speed: number;
28
+ onAir: boolean;
29
+ };
30
+ type TCNetTimecodeState = {
31
+ layerId: string;
32
+ layerName: string;
33
+ /**
34
+ * If available, the total time of the track loaded in this layer.
35
+ *
36
+ * Some timecode sources will not have this information.
37
+ */
38
+ totalTime: {
39
+ timeMillis: number;
40
+ /**
41
+ * How accurate is the totalTimeMillis value,
42
+ * some sources (such as ShowKontrol) are not completely accurate.
43
+ */
44
+ precisionMillis: number;
45
+ } | null;
46
+ /**
47
+ * If available, the metadata info of the track loaded in this layer.
48
+ */
49
+ info: TCNetTimecodeTrackInfo | null;
50
+ playState: TCNetTimecodePlayState;
51
+ };
52
+ type TCNetTimecodeMonitorEventMap = {
53
+ 'timecode-changed': [TCNetTimecodeState];
54
+ 'layer-removed': [Pick<TCNetTimecodeState, 'layerId'>];
55
+ };
56
+ type TCNetTimecodeMonitor = {
57
+ on<K extends keyof TCNetTimecodeMonitorEventMap>(event: K, callback: (...args: TCNetTimecodeMonitorEventMap[K]) => void): void;
58
+ addListener<K extends keyof TCNetTimecodeMonitorEventMap>(event: K, callback: (...args: TCNetTimecodeMonitorEventMap[K]) => void): void;
59
+ removeListener<K extends keyof TCNetTimecodeMonitorEventMap>(event: K, callback: (...args: TCNetTimecodeMonitorEventMap[K]) => void): void;
60
+ };
61
+ /**
62
+ * Create a monitor that listens to TCNet messages to keep track of the current
63
+ * timecodes,
64
+ * accounts for differences in implementation details for different products,
65
+ * and produces a unified and simplified interface for monitoring timecodes
66
+ * with respect the internal clock.
67
+ */
68
+ declare const createTCNetTimecodeMonitor: (tcNetNode: TCNetNode, logger: TCNetLogger) => TCNetTimecodeMonitor;
69
+
70
+ export { type TCNetTimecodeMonitor, type TCNetTimecodeMonitorEventMap, type TCNetTimecodePlayState, type TCNetTimecodeState, type TCNetTimecodeTrackInfo, createTCNetTimecodeMonitor };