@djodjonx/x32-simulator 0.0.3 → 0.0.5
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 +9 -0
- package/README.md +22 -0
- package/dist/server.cjs +4 -5
- package/dist/server.d.cts +1 -10
- package/dist/server.d.mts +1 -10
- package/dist/server.mjs +5 -3
- package/package.json +5 -1
- package/.commitlintrc.json +0 -3
- package/.github/workflows/publish.yml +0 -38
- package/.husky/commit-msg +0 -1
- package/.husky/pre-commit +0 -1
- package/.oxlintrc.json +0 -56
- package/INSTALL.md +0 -107
- package/docs/OSC-Communication.md +0 -184
- package/docs/X32-INTERNAL.md +0 -262
- package/docs/X32-OSC.pdf +0 -0
- package/docs/behringer-x32-x32-osc-remote-protocol-en-44463.pdf +0 -0
- package/src/application/use-cases/BroadcastUpdatesUseCase.ts +0 -120
- package/src/application/use-cases/ManageSessionsUseCase.ts +0 -9
- package/src/application/use-cases/ProcessPacketUseCase.ts +0 -26
- package/src/application/use-cases/SimulationService.ts +0 -146
- package/src/domain/entities/SubscriptionManager.ts +0 -126
- package/src/domain/entities/X32State.ts +0 -78
- package/src/domain/models/MeterConfig.ts +0 -22
- package/src/domain/models/MeterData.ts +0 -59
- package/src/domain/models/OscMessage.ts +0 -93
- package/src/domain/models/X32Address.ts +0 -72
- package/src/domain/models/X32Node.ts +0 -43
- package/src/domain/models/types.ts +0 -86
- package/src/domain/ports/ILogger.ts +0 -27
- package/src/domain/ports/INetworkGateway.ts +0 -8
- package/src/domain/ports/IStateRepository.ts +0 -16
- package/src/domain/services/MeterService.ts +0 -46
- package/src/domain/services/OscMessageHandler.ts +0 -88
- package/src/domain/services/SchemaFactory.ts +0 -308
- package/src/domain/services/SchemaRegistry.ts +0 -67
- package/src/domain/services/StaticResponseService.ts +0 -52
- package/src/domain/services/strategies/BatchStrategy.ts +0 -74
- package/src/domain/services/strategies/MeterStrategy.ts +0 -45
- package/src/domain/services/strategies/NodeDiscoveryStrategy.ts +0 -36
- package/src/domain/services/strategies/OscCommandStrategy.ts +0 -22
- package/src/domain/services/strategies/StateAccessStrategy.ts +0 -71
- package/src/domain/services/strategies/StaticResponseStrategy.ts +0 -42
- package/src/domain/services/strategies/SubscriptionStrategy.ts +0 -56
- package/src/infrastructure/mappers/OscCodec.ts +0 -54
- package/src/infrastructure/repositories/InMemoryStateRepository.ts +0 -37
- package/src/infrastructure/services/ConsoleLogger.ts +0 -177
- package/src/infrastructure/services/UdpNetworkGateway.ts +0 -100
- package/src/presentation/cli/server.ts +0 -194
- package/src/presentation/library/library.ts +0 -139
- package/tests/application/use-cases/BroadcastUpdatesUseCase.test.ts +0 -104
- package/tests/application/use-cases/ManageSessionsUseCase.test.ts +0 -12
- package/tests/application/use-cases/ProcessPacketUseCase.test.ts +0 -49
- package/tests/application/use-cases/SimulationService.test.ts +0 -77
- package/tests/domain/entities/SubscriptionManager.test.ts +0 -50
- package/tests/domain/entities/X32State.test.ts +0 -52
- package/tests/domain/models/MeterData.test.ts +0 -23
- package/tests/domain/models/OscMessage.test.ts +0 -38
- package/tests/domain/models/X32Address.test.ts +0 -30
- package/tests/domain/models/X32Node.test.ts +0 -30
- package/tests/domain/services/MeterService.test.ts +0 -27
- package/tests/domain/services/OscMessageHandler.test.ts +0 -51
- package/tests/domain/services/SchemaRegistry.test.ts +0 -47
- package/tests/domain/services/StaticResponseService.test.ts +0 -15
- package/tests/domain/services/strategies/BatchStrategy.test.ts +0 -41
- package/tests/domain/services/strategies/MeterStrategy.test.ts +0 -19
- package/tests/domain/services/strategies/NodeDiscoveryStrategy.test.ts +0 -22
- package/tests/domain/services/strategies/StateAccessStrategy.test.ts +0 -49
- package/tests/domain/services/strategies/StaticResponseStrategy.test.ts +0 -15
- package/tests/domain/services/strategies/SubscriptionStrategy.test.ts +0 -45
- package/tests/infrastructure/mappers/OscCodec.test.ts +0 -41
- package/tests/infrastructure/repositories/InMemoryStateRepository.test.ts +0 -29
- package/tests/infrastructure/services/ConsoleLogger.test.ts +0 -74
- package/tests/infrastructure/services/UdpNetworkGateway.test.ts +0 -61
- package/tests/presentation/cli/server.test.ts +0 -178
- package/tests/presentation/library/library.test.ts +0 -13
- package/tsconfig.json +0 -21
- package/tsdown.config.ts +0 -15
- package/vitest.config.ts +0 -9
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { Subscriber, RemoteClient } from '../models/types';
|
|
2
|
-
import { ILogger, LogCategory } from '../ports/ILogger';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Manages OSC client subscriptions and their lifecycle.
|
|
6
|
-
*/
|
|
7
|
-
export class SubscriptionManager {
|
|
8
|
-
private logger: ILogger;
|
|
9
|
-
private subscribers: Subscriber[] = [];
|
|
10
|
-
|
|
11
|
-
constructor(logger: ILogger) {
|
|
12
|
-
this.logger = logger;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Cleans up expired subscriptions.
|
|
17
|
-
* Standard X32 subscriptions last 10 seconds.
|
|
18
|
-
*/
|
|
19
|
-
public cleanup() {
|
|
20
|
-
const now = Date.now();
|
|
21
|
-
const initialCount = this.subscribers.length;
|
|
22
|
-
this.subscribers = this.subscribers.filter(s => {
|
|
23
|
-
const active = s.expires > now;
|
|
24
|
-
if (!active) {
|
|
25
|
-
this.logger.debug(LogCategory.SUB, `Expired subscriber`, { type: s.type, ip: s.address, port: s.port, path: 'path' in s ? s.path : s.alias });
|
|
26
|
-
}
|
|
27
|
-
return active;
|
|
28
|
-
});
|
|
29
|
-
if (this.subscribers.length !== initialCount) {
|
|
30
|
-
this.logger.info(LogCategory.SUB, `Cleanup finished`, { removed: initialCount - this.subscribers.length, remaining: this.subscribers.length });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Gets all active subscribers.
|
|
36
|
-
* @returns Array of subscribers.
|
|
37
|
-
*/
|
|
38
|
-
public getSubscribers(): Subscriber[] {
|
|
39
|
-
return this.subscribers;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Adds or renews a path-based subscription (e.g. /subscribe or /xremote).
|
|
44
|
-
* @param rinfo - Client remote info.
|
|
45
|
-
* @param path - Subscription path.
|
|
46
|
-
*/
|
|
47
|
-
public addPathSubscriber(rinfo: RemoteClient, path: string) {
|
|
48
|
-
const key = `${rinfo.address}:${rinfo.port}:${path}`;
|
|
49
|
-
const expires = Date.now() + 10000;
|
|
50
|
-
const existing = this.subscribers.find(s => s.type === 'path' && `${s.address}:${s.port}:${s.path}` === key);
|
|
51
|
-
|
|
52
|
-
if (existing) {
|
|
53
|
-
existing.expires = expires;
|
|
54
|
-
this.logger.debug(LogCategory.SUB, `Renewed subscription`, { ip: rinfo.address, path });
|
|
55
|
-
} else {
|
|
56
|
-
this.subscribers.push({ type: 'path', address: rinfo.address, port: rinfo.port, path, expires });
|
|
57
|
-
this.logger.info(LogCategory.SUB, `New subscription`, { ip: rinfo.address, path });
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Adds or renews a batch subscription.
|
|
63
|
-
* @param rinfo - Client remote info.
|
|
64
|
-
* @param alias - Response alias.
|
|
65
|
-
* @param paths - Target paths.
|
|
66
|
-
* @param start - Start index.
|
|
67
|
-
* @param count - Count.
|
|
68
|
-
* @param factor - Frequency factor.
|
|
69
|
-
* @param args - Command arguments.
|
|
70
|
-
*/
|
|
71
|
-
public addBatchSubscriber(rinfo: RemoteClient, alias: string, paths: string[], start: number, count: number, factor?: number, args?: number[]) {
|
|
72
|
-
const expires = Date.now() + 10000;
|
|
73
|
-
this.subscribers = this.subscribers.filter(s => !(s.type === 'batch' && s.alias === alias && s.address === rinfo.address));
|
|
74
|
-
|
|
75
|
-
this.subscribers.push({ type: 'batch', address: rinfo.address, port: rinfo.port, alias, paths, start, count, factor, expires, args });
|
|
76
|
-
this.logger.info(LogCategory.SUB, `Batch subscription`, { alias, count: paths.length, factor, ip: rinfo.address, args });
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Adds or renews a format subscription.
|
|
81
|
-
* @param rinfo - Client remote info.
|
|
82
|
-
* @param alias - Response alias.
|
|
83
|
-
* @param pattern - Path pattern.
|
|
84
|
-
* @param start - Start index.
|
|
85
|
-
* @param count - Count.
|
|
86
|
-
* @param factor - Frequency factor.
|
|
87
|
-
*/
|
|
88
|
-
public addFormatSubscriber(rinfo: RemoteClient, alias: string, pattern: string, start: number, count: number, factor?: number) {
|
|
89
|
-
const expires = Date.now() + 10000;
|
|
90
|
-
this.subscribers = this.subscribers.filter(s => !(s.type === 'format' && s.alias === alias && s.address === rinfo.address));
|
|
91
|
-
this.subscribers.push({ type: 'format', address: rinfo.address, port: rinfo.port, alias, pattern, start, count, factor, expires });
|
|
92
|
-
this.logger.info(LogCategory.SUB, `Format subscription`, { alias, pattern, factor, ip: rinfo.address });
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Adds or renews a high-frequency meter subscription.
|
|
97
|
-
* @param rinfo - Client remote info.
|
|
98
|
-
* @param meterPath - Target meter path.
|
|
99
|
-
*/
|
|
100
|
-
public addMeterSubscriber(rinfo: RemoteClient, meterPath: string) {
|
|
101
|
-
const expires = Date.now() + 10000;
|
|
102
|
-
const existing = this.subscribers.find(s => s.type === 'meter' && s.meterPath === meterPath && s.address === rinfo.address);
|
|
103
|
-
|
|
104
|
-
this.subscribers = this.subscribers.filter(s => !(s.type === 'meter' && s.meterPath === meterPath && s.address === rinfo.address));
|
|
105
|
-
this.subscribers.push({ type: 'meter', address: rinfo.address, port: rinfo.port, meterPath, expires });
|
|
106
|
-
|
|
107
|
-
if (!existing) {
|
|
108
|
-
this.logger.info(LogCategory.SUB, `Meter subscription`, { path: meterPath, ip: rinfo.address });
|
|
109
|
-
} else {
|
|
110
|
-
this.logger.debug(LogCategory.SUB, `Meter renewal`, { path: meterPath, ip: rinfo.address });
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Removes a subscription.
|
|
116
|
-
* @param rinfo - Client remote info.
|
|
117
|
-
* @param path - Subscription path.
|
|
118
|
-
*/
|
|
119
|
-
public removeSubscriber(rinfo: RemoteClient, path: string) {
|
|
120
|
-
const initial = this.subscribers.length;
|
|
121
|
-
this.subscribers = this.subscribers.filter(s => !(s.address === rinfo.address && s.port === rinfo.port && s.path === path));
|
|
122
|
-
if (this.subscribers.length < initial) {
|
|
123
|
-
this.logger.info(LogCategory.SUB, `Unsubscribed`, { path, ip: rinfo.address });
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'node:events';
|
|
2
|
-
import { X32Node } from '../models/X32Node';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Manages the internal "Digital Twin" state of the X32 console.
|
|
6
|
-
* It acts as a single source of truth for all parameters.
|
|
7
|
-
* Emits 'change' events whenever a value is updated.
|
|
8
|
-
*/
|
|
9
|
-
export class X32State extends EventEmitter {
|
|
10
|
-
/** Map storing all OSC paths and their current values. */
|
|
11
|
-
private state = new Map<string, number | string>();
|
|
12
|
-
private readonly defaultState: Map<string, number | string> = new Map();
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Initializes the state with default values from the schema.
|
|
16
|
-
* @param schema - The schema definition map.
|
|
17
|
-
*/
|
|
18
|
-
constructor(schema: Record<string, X32Node>) {
|
|
19
|
-
super();
|
|
20
|
-
// Pre-calculate default state
|
|
21
|
-
for (const [addr, def] of Object.entries(schema)) {
|
|
22
|
-
this.defaultState.set(addr, def.default);
|
|
23
|
-
}
|
|
24
|
-
this.reset();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Resets all state parameters to their default values defined in the schema.
|
|
29
|
-
*/
|
|
30
|
-
public reset() {
|
|
31
|
-
this.state.clear();
|
|
32
|
-
this.defaultState.forEach((val, key) => {
|
|
33
|
-
this.state.set(key, val);
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Retrieves the value of a specific OSC node.
|
|
39
|
-
* @param address - The full OSC address path.
|
|
40
|
-
* @returns The stored value (number or string) or undefined if not found.
|
|
41
|
-
*/
|
|
42
|
-
public get(address: string): number | string | undefined {
|
|
43
|
-
return this.state.get(address);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Updates the value of a specific OSC node and notifies subscribers.
|
|
48
|
-
* @param address - The full OSC address path.
|
|
49
|
-
* @param value - The new value to store.
|
|
50
|
-
*/
|
|
51
|
-
public set(address: string, value: number | string) {
|
|
52
|
-
this.state.set(address, value);
|
|
53
|
-
this.emit('change', { address, value });
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Specialized logic to handle X32 Mute Groups side-effects.
|
|
58
|
-
* When a mute group is toggled, it iterates through all channels
|
|
59
|
-
* assigned to that group and updates their individual mute status.
|
|
60
|
-
* @param groupIdx - The index of the mute group (1-6).
|
|
61
|
-
* @param isOn - The new state of the group master switch (0 or 1).
|
|
62
|
-
*/
|
|
63
|
-
public handleMuteGroupChange(groupIdx: number, isOn: number) {
|
|
64
|
-
for(let i=1; i<=32; i++) {
|
|
65
|
-
const ch = i.toString().padStart(2, '0');
|
|
66
|
-
const grpVal = this.get(`/ch/${ch}/grp/mute`);
|
|
67
|
-
|
|
68
|
-
if (typeof grpVal === 'number') {
|
|
69
|
-
if ((grpVal & (1 << (groupIdx - 1))) !== 0) {
|
|
70
|
-
const targetMute = isOn === 1 ? 0 : 1;
|
|
71
|
-
const muteAddr = `/ch/${ch}/mix/on`;
|
|
72
|
-
// This set calls emit('change') which will trigger broadcast
|
|
73
|
-
this.set(muteAddr, targetMute);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Configuration for meter counts per path.
|
|
3
|
-
*/
|
|
4
|
-
export const METER_COUNTS: Record<string, number> = {
|
|
5
|
-
'/meters/0': 70,
|
|
6
|
-
'/meters/1': 96,
|
|
7
|
-
'/meters/2': 49,
|
|
8
|
-
'/meters/3': 22,
|
|
9
|
-
'/meters/4': 82,
|
|
10
|
-
'/meters/5': 27,
|
|
11
|
-
'/meters/6': 4,
|
|
12
|
-
'/meters/7': 16,
|
|
13
|
-
'/meters/8': 6,
|
|
14
|
-
'/meters/9': 32,
|
|
15
|
-
'/meters/10': 32,
|
|
16
|
-
'/meters/11': 5,
|
|
17
|
-
'/meters/12': 4,
|
|
18
|
-
'/meters/13': 48,
|
|
19
|
-
'/meters/14': 80,
|
|
20
|
-
'/meters/15': 50,
|
|
21
|
-
'/meters/16': 48,
|
|
22
|
-
};
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Represents a set of meter values for a specific meter path.
|
|
3
|
-
* Handles the storage of float values and serialization to X32-specific binary blobs.
|
|
4
|
-
*/
|
|
5
|
-
export class MeterData {
|
|
6
|
-
private readonly _path: string;
|
|
7
|
-
private readonly _values: number[];
|
|
8
|
-
|
|
9
|
-
// Cache for the binary buffer to avoid re-allocation if values haven't changed (optional optimization,
|
|
10
|
-
// but here we mainly cache the structure or just generate on demand).
|
|
11
|
-
// Given the previous util had a static cache, we can keep it simple here.
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Creates a new MeterData instance.
|
|
15
|
-
* @param path - The meter OSC path (e.g., "/meters/1").
|
|
16
|
-
* @param values - The array of float values (0.0 - 1.0 or similar).
|
|
17
|
-
*/
|
|
18
|
-
constructor(path: string, values: number[]) {
|
|
19
|
-
this._path = path;
|
|
20
|
-
this._values = values;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Gets the meter path.
|
|
25
|
-
*/
|
|
26
|
-
get path(): string {
|
|
27
|
-
return this._path;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Gets the values.
|
|
32
|
-
*/
|
|
33
|
-
get values(): number[] {
|
|
34
|
-
return [...this._values];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Generates an optimized binary blob for X32 meters (mixed endianness).
|
|
39
|
-
* Structure: [Size (Int32BE)] [Count (Int32LE)] [Float1 (LE)] [Float2 (LE)] ...
|
|
40
|
-
* @returns Buffer containing the OSC blob.
|
|
41
|
-
*/
|
|
42
|
-
toBlob(): Buffer {
|
|
43
|
-
const count = this._values.length;
|
|
44
|
-
const totalSize = 4 + 4 + (count * 4);
|
|
45
|
-
|
|
46
|
-
const blob = Buffer.alloc(totalSize);
|
|
47
|
-
|
|
48
|
-
// Header: Size (BE) + Count (LE)
|
|
49
|
-
blob.writeInt32BE(totalSize, 0);
|
|
50
|
-
blob.writeInt32LE(count, 4);
|
|
51
|
-
|
|
52
|
-
// Body: Floats (LE)
|
|
53
|
-
for (let i = 0; i < count; i++) {
|
|
54
|
-
blob.writeFloatLE(this._values[i], 8 + (i * 4));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return blob;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { OscArgumentValue, OscPacket } from './types';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Represents a parsed OSC message ready for handling.
|
|
5
|
-
* Encapsulates the address and arguments, providing helper methods for validation and extraction.
|
|
6
|
-
*/
|
|
7
|
-
export class OscMessage {
|
|
8
|
-
private readonly _address: string;
|
|
9
|
-
private readonly _args: OscArgumentValue[];
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Creates a new OscMessage instance.
|
|
13
|
-
* @param address - The OSC address string.
|
|
14
|
-
* @param args - The list of arguments.
|
|
15
|
-
*/
|
|
16
|
-
constructor(address: string, args: OscArgumentValue[]) {
|
|
17
|
-
this._address = address;
|
|
18
|
-
this._args = args;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Gets the OSC address.
|
|
23
|
-
* @returns The address string.
|
|
24
|
-
*/
|
|
25
|
-
get address(): string {
|
|
26
|
-
return this._address;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Gets the message arguments.
|
|
31
|
-
* @returns Copy of the arguments array.
|
|
32
|
-
*/
|
|
33
|
-
get args(): OscArgumentValue[] {
|
|
34
|
-
return [...this._args]; // Return copy to preserve immutability
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Checks if the address starts with the given prefix.
|
|
39
|
-
* @param prefix - The prefix to check.
|
|
40
|
-
* @returns True if it starts with the prefix.
|
|
41
|
-
*/
|
|
42
|
-
startsWith(prefix: string): boolean {
|
|
43
|
-
return this._address.startsWith(prefix);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Gets an argument at a specific index, safely.
|
|
48
|
-
* @param index - The index of the argument.
|
|
49
|
-
* @returns The argument value or undefined.
|
|
50
|
-
*/
|
|
51
|
-
getArg(index: number): OscArgumentValue | undefined {
|
|
52
|
-
return this._args[index];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Gets an argument as a number, or throws if missing/invalid.
|
|
57
|
-
* @param index - The index.
|
|
58
|
-
* @returns The number value.
|
|
59
|
-
*/
|
|
60
|
-
getArgAsNumber(index: number): number {
|
|
61
|
-
const val = this._args[index];
|
|
62
|
-
if (typeof val !== 'number') {
|
|
63
|
-
throw new Error(`Argument at index ${index} is not a number.`);
|
|
64
|
-
}
|
|
65
|
-
return val;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Gets an argument as a string, or throws if missing/invalid.
|
|
70
|
-
* @param index - The index.
|
|
71
|
-
* @returns The string value.
|
|
72
|
-
*/
|
|
73
|
-
getArgAsString(index: number): string {
|
|
74
|
-
const val = this._args[index];
|
|
75
|
-
if (typeof val !== 'string') {
|
|
76
|
-
throw new Error(`Argument at index ${index} is not a string.`);
|
|
77
|
-
}
|
|
78
|
-
return val;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Factory method to create an OscMessage from a raw packet.
|
|
83
|
-
* @param packet - The raw OSC packet.
|
|
84
|
-
* @returns A new OscMessage instance.
|
|
85
|
-
*/
|
|
86
|
-
static fromPacket(packet: OscPacket): OscMessage {
|
|
87
|
-
const values = packet.args.map(arg =>
|
|
88
|
-
(typeof arg === 'object' && arg !== null && 'value' in arg) ? arg.value : arg
|
|
89
|
-
) as OscArgumentValue[];
|
|
90
|
-
|
|
91
|
-
return new OscMessage(packet.address, values);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Represents an X32 OSC Address path, providing parsing logic and component extraction.
|
|
3
|
-
*/
|
|
4
|
-
export class X32Address {
|
|
5
|
-
private readonly _path: string;
|
|
6
|
-
private readonly _parts: string[];
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Creates a new X32Address.
|
|
10
|
-
* @param path - The full OSC path (e.g., "/ch/01/mix/fader").
|
|
11
|
-
*/
|
|
12
|
-
constructor(path: string) {
|
|
13
|
-
this._path = path;
|
|
14
|
-
// Split by '/' and remove empty strings from leading slash
|
|
15
|
-
this._parts = path.split('/').filter(p => p.length > 0);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Gets the full string representation of the path.
|
|
20
|
-
* @returns The full OSC path string.
|
|
21
|
-
*/
|
|
22
|
-
get path(): string {
|
|
23
|
-
return this._path;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Gets the root category (e.g., "ch", "bus", "config").
|
|
28
|
-
* @returns The first segment of the path.
|
|
29
|
-
*/
|
|
30
|
-
get root(): string | undefined {
|
|
31
|
-
return this._parts[0];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Gets the index/ID part if present (e.g., "01" from "/ch/01").
|
|
36
|
-
* @returns The second segment of the path.
|
|
37
|
-
*/
|
|
38
|
-
get index(): string | undefined {
|
|
39
|
-
return this._parts[1];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Gets the suffix (everything after the ID).
|
|
44
|
-
* e.g., for "/ch/01/mix/fader", returns "/mix/fader".
|
|
45
|
-
* @returns The remaining path after the index.
|
|
46
|
-
*/
|
|
47
|
-
get suffix(): string {
|
|
48
|
-
if (this._parts.length < 3) return '';
|
|
49
|
-
return '/' + this._parts.slice(2).join('/');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Checks if this address belongs to a specific category.
|
|
54
|
-
* @param category - The category to check (e.g., "meters", "ch").
|
|
55
|
-
* @returns True if it matches the category.
|
|
56
|
-
*/
|
|
57
|
-
isCategory(category: string): boolean {
|
|
58
|
-
return this.root === category;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Checks if the address matches a specific pattern.
|
|
63
|
-
* @param pattern - The regex or string pattern.
|
|
64
|
-
* @returns True if it matches the pattern.
|
|
65
|
-
*/
|
|
66
|
-
matches(pattern: RegExp | string): boolean {
|
|
67
|
-
if (typeof pattern === 'string') {
|
|
68
|
-
return this._path === pattern;
|
|
69
|
-
}
|
|
70
|
-
return pattern.test(this._path);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Metadata for a single state node in the X32 schema.
|
|
3
|
-
* Represents a "Knob" or "Variable" on the console.
|
|
4
|
-
*/
|
|
5
|
-
export class X32Node {
|
|
6
|
-
/** Value type (f=float, i=int, s=string). */
|
|
7
|
-
readonly type: 'f' | 'i' | 's';
|
|
8
|
-
/** Default value for reset. */
|
|
9
|
-
readonly default: number | string;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Creates a new X32Node.
|
|
13
|
-
* @param type - The OSC data type ('f', 'i', 's').
|
|
14
|
-
* @param defaultValue - The default value.
|
|
15
|
-
*/
|
|
16
|
-
constructor(type: 'f' | 'i' | 's', defaultValue: number | string) {
|
|
17
|
-
this.type = type;
|
|
18
|
-
this.default = defaultValue;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Validates if a value is compatible with this node's type.
|
|
23
|
-
* @param value - The value to check.
|
|
24
|
-
* @returns True if valid.
|
|
25
|
-
*/
|
|
26
|
-
validate(value: any): boolean {
|
|
27
|
-
if (this.type === 'f') return typeof value === 'number'; // && value >= 0.0 && value <= 1.0 (optional strict check)
|
|
28
|
-
if (this.type === 'i') return typeof value === 'number'; // && Number.isInteger(value)
|
|
29
|
-
if (this.type === 's') return typeof value === 'string';
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Factory method to create from a plain object (for compatibility/migration).
|
|
35
|
-
* @param obj - Plain object.
|
|
36
|
-
* @param obj.type - OSC data type.
|
|
37
|
-
* @param obj.default - Default value.
|
|
38
|
-
* @returns A new X32Node instance.
|
|
39
|
-
*/
|
|
40
|
-
static from(obj: { type: 'f'|'i'|'s', default: number|string }): X32Node {
|
|
41
|
-
return new X32Node(obj.type, obj.default);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Represents a basic OSC argument value.
|
|
3
|
-
*/
|
|
4
|
-
export type OscArgumentValue = Buffer | boolean | number | string;
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Represents an OSC argument, which can be a primitive value or a typed object.
|
|
8
|
-
*/
|
|
9
|
-
export type OscArgument = OscArgumentValue | { type: string; value: OscArgumentValue };
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Represents a decoded OSC packet (bundle or message) as returned by the codec.
|
|
13
|
-
*/
|
|
14
|
-
export interface OscPacket {
|
|
15
|
-
/** The type of OSC packet. */
|
|
16
|
-
oscType: 'bundle' | 'message';
|
|
17
|
-
/** The address pattern. */
|
|
18
|
-
address: string;
|
|
19
|
-
/** The arguments of the message or elements of the bundle. */
|
|
20
|
-
args: OscArgument[];
|
|
21
|
-
/** Elements if this is a bundle. */
|
|
22
|
-
elements?: OscPacket[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Represents a remote OSC client's connection info.
|
|
27
|
-
*/
|
|
28
|
-
export interface RemoteClient {
|
|
29
|
-
/** Target IP address. */
|
|
30
|
-
address: string;
|
|
31
|
-
/** Target port. */
|
|
32
|
-
port: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Represents a parsed OSC message ready for handling.
|
|
37
|
-
* Arguments are already unwrapped from their {type, value} containers.
|
|
38
|
-
*/
|
|
39
|
-
export interface OscMsg {
|
|
40
|
-
/** The OSC address. */
|
|
41
|
-
address: string;
|
|
42
|
-
/** The message arguments. */
|
|
43
|
-
args: OscArgumentValue[];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Represents an active subscription from a client.
|
|
48
|
-
*/
|
|
49
|
-
export interface Subscriber {
|
|
50
|
-
/** Target IP address. */
|
|
51
|
-
address: string;
|
|
52
|
-
/** Target port. */
|
|
53
|
-
port: number;
|
|
54
|
-
/** Expiry timestamp. */
|
|
55
|
-
expires: number;
|
|
56
|
-
/** Subscription type. */
|
|
57
|
-
type: 'batch' | 'format' | 'meter' | 'path';
|
|
58
|
-
/** Optional exact path for 'path' type. */
|
|
59
|
-
path?: string;
|
|
60
|
-
/** Target meter path for 'meter' type. */
|
|
61
|
-
meterPath?: string;
|
|
62
|
-
/** Alias for batch/format responses. */
|
|
63
|
-
alias?: string;
|
|
64
|
-
/** Glob/Wildcard pattern for 'format' type. */
|
|
65
|
-
pattern?: string;
|
|
66
|
-
/** List of paths for 'batch' type. */
|
|
67
|
-
paths?: string[];
|
|
68
|
-
/** Start index for ranges. */
|
|
69
|
-
start?: number;
|
|
70
|
-
/** Number of items in range. */
|
|
71
|
-
count?: number;
|
|
72
|
-
/** Frequency reduction factor. */
|
|
73
|
-
factor?: number;
|
|
74
|
-
/** Parameter arguments for the command. */
|
|
75
|
-
args?: number[];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Structure representing a reply to be sent back to an OSC client.
|
|
80
|
-
*/
|
|
81
|
-
export interface OscReply {
|
|
82
|
-
/** The target OSC address. */
|
|
83
|
-
address: string;
|
|
84
|
-
/** The response arguments. */
|
|
85
|
-
args: OscArgument[];
|
|
86
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Represents data that can be logged.
|
|
3
|
-
*/
|
|
4
|
-
export type LogData = Buffer | Error | LogData[] | boolean | number | string | { [key: string]: LogData } | null | undefined;
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Standard log categories for the domain.
|
|
8
|
-
*/
|
|
9
|
-
export enum LogCategory {
|
|
10
|
-
SYSTEM = 'SYSTEM',
|
|
11
|
-
OSC_IN = 'OSC_IN',
|
|
12
|
-
OSC_OUT = 'OSC_OUT',
|
|
13
|
-
DISPATCH = 'DISPATCH',
|
|
14
|
-
STATE = 'STATE',
|
|
15
|
-
SUB = 'SUB',
|
|
16
|
-
METER = 'METER'
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Interface for the logging port.
|
|
21
|
-
*/
|
|
22
|
-
export interface ILogger {
|
|
23
|
-
debug(category: string, msg: string, data?: LogData): void;
|
|
24
|
-
info(category: string, msg: string, data?: LogData): void;
|
|
25
|
-
warn(category: string, msg: string, data?: LogData): void;
|
|
26
|
-
error(category: string, msg: string, err?: LogData): void;
|
|
27
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { OscPacket, RemoteClient } from '../models/types';
|
|
2
|
-
|
|
3
|
-
export interface INetworkGateway {
|
|
4
|
-
start(port: number, ip: string): Promise<void>;
|
|
5
|
-
stop(): Promise<void>;
|
|
6
|
-
send(target: RemoteClient, address: string, args: any[]): void;
|
|
7
|
-
onPacket(callback: (packet: OscPacket, source: RemoteClient) => void): void;
|
|
8
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { X32State } from '../entities/X32State';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Interface for accessing the mixer state.
|
|
5
|
-
*/
|
|
6
|
-
export interface IStateRepository {
|
|
7
|
-
/**
|
|
8
|
-
* Retrieves the single instance of the mixer state (Singleton in this context).
|
|
9
|
-
*/
|
|
10
|
-
getState(): X32State;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Resets the state to defaults.
|
|
14
|
-
*/
|
|
15
|
-
reset(): void;
|
|
16
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { X32State } from '../entities/X32State';
|
|
2
|
-
import { MeterData } from '../models/MeterData';
|
|
3
|
-
import { METER_COUNTS } from '../models/MeterConfig';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Service responsible for generating meter values and data.
|
|
7
|
-
* Mimics the physics/audio engine of the X32.
|
|
8
|
-
*/
|
|
9
|
-
export class MeterService {
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Generates a MeterData object for a given path.
|
|
13
|
-
* @param path - The meter OSC path (e.g., "/meters/1").
|
|
14
|
-
* @param state - The current X32 state (for correlating faders to meters).
|
|
15
|
-
* @returns MeterData object containing the values.
|
|
16
|
-
*/
|
|
17
|
-
public generateMeterData(path: string, state?: X32State): MeterData {
|
|
18
|
-
const count = METER_COUNTS[path];
|
|
19
|
-
if (count === undefined) {
|
|
20
|
-
return new MeterData(path, []);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const values: number[] = [];
|
|
24
|
-
for (let i = 0; i < count; i++) {
|
|
25
|
-
let val = Math.random() * 0.05; // Noise floor
|
|
26
|
-
|
|
27
|
-
// Simulation logic: Link meters 1-32 to Channel Faders 1-32
|
|
28
|
-
if (state && path === '/meters/1') {
|
|
29
|
-
if (i < 32) {
|
|
30
|
-
const ch = (i + 1).toString().padStart(2, '0');
|
|
31
|
-
// We should use X32Address logic here ideally, but string construction is fast
|
|
32
|
-
const faderPath = `/ch/${ch}/mix/fader`;
|
|
33
|
-
const fader = state.get(faderPath);
|
|
34
|
-
|
|
35
|
-
if (typeof fader === 'number') {
|
|
36
|
-
// If fader is up, show signal (simulated with random variation)
|
|
37
|
-
val = fader > 0.01 ? fader * (0.9 + Math.random() * 0.1) : 0;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
values.push(val);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return new MeterData(path, values);
|
|
45
|
-
}
|
|
46
|
-
}
|