@djodjonx/x32-simulator 0.0.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/.commitlintrc.json +3 -0
- package/.github/workflows/publish.yml +38 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.oxlintrc.json +56 -0
- package/CHANGELOG.md +11 -0
- package/INSTALL.md +107 -0
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/dist/UdpNetworkGateway-BrroQ6-Q.mjs +1189 -0
- package/dist/UdpNetworkGateway-Ccdd7Us5.cjs +1265 -0
- package/dist/index.cjs +7 -0
- package/dist/index.d.cts +207 -0
- package/dist/index.d.mts +207 -0
- package/dist/index.mjs +3 -0
- package/dist/server.cjs +1060 -0
- package/dist/server.d.cts +10 -0
- package/dist/server.d.mts +10 -0
- package/dist/server.mjs +1055 -0
- package/docs/OSC-Communication.md +184 -0
- package/docs/X32-INTERNAL.md +262 -0
- package/docs/X32-OSC.pdf +0 -0
- package/docs/behringer-x32-x32-osc-remote-protocol-en-44463.pdf +0 -0
- package/package.json +68 -0
- package/src/application/use-cases/BroadcastUpdatesUseCase.ts +120 -0
- package/src/application/use-cases/ManageSessionsUseCase.ts +9 -0
- package/src/application/use-cases/ProcessPacketUseCase.ts +26 -0
- package/src/application/use-cases/SimulationService.ts +122 -0
- package/src/domain/entities/SubscriptionManager.ts +126 -0
- package/src/domain/entities/X32State.ts +78 -0
- package/src/domain/models/MeterConfig.ts +22 -0
- package/src/domain/models/MeterData.ts +59 -0
- package/src/domain/models/OscMessage.ts +93 -0
- package/src/domain/models/X32Address.ts +78 -0
- package/src/domain/models/X32Node.ts +43 -0
- package/src/domain/models/types.ts +96 -0
- package/src/domain/ports/ILogger.ts +27 -0
- package/src/domain/ports/INetworkGateway.ts +8 -0
- package/src/domain/ports/IStateRepository.ts +16 -0
- package/src/domain/services/MeterService.ts +46 -0
- package/src/domain/services/OscMessageHandler.ts +88 -0
- package/src/domain/services/SchemaFactory.ts +308 -0
- package/src/domain/services/SchemaRegistry.ts +67 -0
- package/src/domain/services/StaticResponseService.ts +52 -0
- package/src/domain/services/strategies/BatchStrategy.ts +74 -0
- package/src/domain/services/strategies/MeterStrategy.ts +45 -0
- package/src/domain/services/strategies/NodeDiscoveryStrategy.ts +36 -0
- package/src/domain/services/strategies/OscCommandStrategy.ts +22 -0
- package/src/domain/services/strategies/StateAccessStrategy.ts +71 -0
- package/src/domain/services/strategies/StaticResponseStrategy.ts +42 -0
- package/src/domain/services/strategies/SubscriptionStrategy.ts +56 -0
- package/src/infrastructure/mappers/OscCodec.ts +54 -0
- package/src/infrastructure/repositories/InMemoryStateRepository.ts +21 -0
- package/src/infrastructure/services/ConsoleLogger.ts +177 -0
- package/src/infrastructure/services/UdpNetworkGateway.ts +71 -0
- package/src/presentation/cli/server.ts +194 -0
- package/src/presentation/library/library.ts +9 -0
- package/tests/application/use-cases/BroadcastUpdatesUseCase.test.ts +104 -0
- package/tests/application/use-cases/ManageSessionsUseCase.test.ts +12 -0
- package/tests/application/use-cases/ProcessPacketUseCase.test.ts +49 -0
- package/tests/application/use-cases/SimulationService.test.ts +77 -0
- package/tests/domain/entities/SubscriptionManager.test.ts +50 -0
- package/tests/domain/entities/X32State.test.ts +52 -0
- package/tests/domain/models/MeterData.test.ts +23 -0
- package/tests/domain/models/OscMessage.test.ts +38 -0
- package/tests/domain/models/X32Address.test.ts +30 -0
- package/tests/domain/models/X32Node.test.ts +30 -0
- package/tests/domain/services/MeterService.test.ts +27 -0
- package/tests/domain/services/OscMessageHandler.test.ts +51 -0
- package/tests/domain/services/SchemaRegistry.test.ts +47 -0
- package/tests/domain/services/StaticResponseService.test.ts +15 -0
- package/tests/domain/services/strategies/BatchStrategy.test.ts +41 -0
- package/tests/domain/services/strategies/MeterStrategy.test.ts +19 -0
- package/tests/domain/services/strategies/NodeDiscoveryStrategy.test.ts +22 -0
- package/tests/domain/services/strategies/StateAccessStrategy.test.ts +49 -0
- package/tests/domain/services/strategies/StaticResponseStrategy.test.ts +15 -0
- package/tests/domain/services/strategies/SubscriptionStrategy.test.ts +45 -0
- package/tests/infrastructure/mappers/OscCodec.test.ts +41 -0
- package/tests/infrastructure/repositories/InMemoryStateRepository.test.ts +29 -0
- package/tests/infrastructure/services/ConsoleLogger.test.ts +74 -0
- package/tests/infrastructure/services/UdpNetworkGateway.test.ts +61 -0
- package/tests/presentation/cli/server.test.ts +178 -0
- package/tests/presentation/library/library.test.ts +13 -0
- package/tsconfig.json +21 -0
- package/tsdown.config.ts +15 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
* Metadata for a single state node.
|
|
48
|
+
*/
|
|
49
|
+
export interface X32Node {
|
|
50
|
+
/** Value type (float, int, or string). */
|
|
51
|
+
type: 'f' | 'i' | 's';
|
|
52
|
+
/** Default value for reset. */
|
|
53
|
+
default: number | string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Represents an active subscription from a client.
|
|
58
|
+
*/
|
|
59
|
+
export interface Subscriber {
|
|
60
|
+
/** Target IP address. */
|
|
61
|
+
address: string;
|
|
62
|
+
/** Target port. */
|
|
63
|
+
port: number;
|
|
64
|
+
/** Expiry timestamp. */
|
|
65
|
+
expires: number;
|
|
66
|
+
/** Subscription type. */
|
|
67
|
+
type: 'batch' | 'format' | 'meter' | 'path';
|
|
68
|
+
/** Optional exact path for 'path' type. */
|
|
69
|
+
path?: string;
|
|
70
|
+
/** Target meter path for 'meter' type. */
|
|
71
|
+
meterPath?: string;
|
|
72
|
+
/** Alias for batch/format responses. */
|
|
73
|
+
alias?: string;
|
|
74
|
+
/** Glob/Wildcard pattern for 'format' type. */
|
|
75
|
+
pattern?: string;
|
|
76
|
+
/** List of paths for 'batch' type. */
|
|
77
|
+
paths?: string[];
|
|
78
|
+
/** Start index for ranges. */
|
|
79
|
+
start?: number;
|
|
80
|
+
/** Number of items in range. */
|
|
81
|
+
count?: number;
|
|
82
|
+
/** Frequency reduction factor. */
|
|
83
|
+
factor?: number;
|
|
84
|
+
/** Parameter arguments for the command. */
|
|
85
|
+
args?: number[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Structure representing a reply to be sent back to an OSC client.
|
|
90
|
+
*/
|
|
91
|
+
export interface OscReply {
|
|
92
|
+
/** The target OSC address. */
|
|
93
|
+
address: string;
|
|
94
|
+
/** The response arguments. */
|
|
95
|
+
args: OscArgument[];
|
|
96
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { OscMsg, RemoteClient, OscReply } from '../models/types';
|
|
2
|
+
import { X32State } from '../entities/X32State';
|
|
3
|
+
import { SubscriptionManager } from '../entities/SubscriptionManager';
|
|
4
|
+
import { OscCommandStrategy } from './strategies/OscCommandStrategy';
|
|
5
|
+
import { NodeDiscoveryStrategy } from './strategies/NodeDiscoveryStrategy';
|
|
6
|
+
import { StaticResponseStrategy } from './strategies/StaticResponseStrategy';
|
|
7
|
+
import { SubscriptionStrategy } from './strategies/SubscriptionStrategy';
|
|
8
|
+
import { BatchStrategy } from './strategies/BatchStrategy';
|
|
9
|
+
import { MeterStrategy } from './strategies/MeterStrategy';
|
|
10
|
+
import { StateAccessStrategy } from './strategies/StateAccessStrategy';
|
|
11
|
+
import { ILogger, LogCategory } from '../ports/ILogger';
|
|
12
|
+
import { MeterService } from './MeterService';
|
|
13
|
+
import { SchemaRegistry } from './SchemaRegistry';
|
|
14
|
+
import { StaticResponseService } from './StaticResponseService';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The central dispatcher for incoming OSC messages.
|
|
18
|
+
* Uses a Chain of Responsibility (Strategy pattern) to delegate handling.
|
|
19
|
+
* Order of strategies is critical for correct prioritization.
|
|
20
|
+
*/
|
|
21
|
+
export class OscMessageHandler {
|
|
22
|
+
/** List of active command strategies. */
|
|
23
|
+
private strategies: OscCommandStrategy[];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initializes the handler with all available strategies.
|
|
27
|
+
* @param state - Global mixer state.
|
|
28
|
+
* @param subscriptionManager - Active client sessions.
|
|
29
|
+
* @param logger - Logger instance.
|
|
30
|
+
* @param serverIp - Host IP reported in handshakes.
|
|
31
|
+
* @param serverName - Console name reported in handshakes.
|
|
32
|
+
* @param serverModel - Console model reported in handshakes.
|
|
33
|
+
* @param meterService - Service for metering.
|
|
34
|
+
* @param schemaRegistry - Service for schema validation.
|
|
35
|
+
* @param staticResponseService - Service for static responses.
|
|
36
|
+
*/
|
|
37
|
+
constructor(
|
|
38
|
+
state: X32State,
|
|
39
|
+
subscriptionManager: SubscriptionManager,
|
|
40
|
+
private logger: ILogger,
|
|
41
|
+
serverIp: string,
|
|
42
|
+
serverName: string,
|
|
43
|
+
serverModel: string,
|
|
44
|
+
private meterService: MeterService,
|
|
45
|
+
private schemaRegistry: SchemaRegistry,
|
|
46
|
+
private staticResponseService: StaticResponseService
|
|
47
|
+
) {
|
|
48
|
+
// Order matters! Specific strategies (Discovery, Handshake) first, generic (State Access) last.
|
|
49
|
+
this.strategies = [
|
|
50
|
+
new NodeDiscoveryStrategy(this.schemaRegistry),
|
|
51
|
+
new StaticResponseStrategy(serverIp, serverName, serverModel, this.staticResponseService),
|
|
52
|
+
new SubscriptionStrategy(subscriptionManager, state, logger),
|
|
53
|
+
new BatchStrategy(subscriptionManager, logger),
|
|
54
|
+
new MeterStrategy(subscriptionManager, state, this.meterService),
|
|
55
|
+
new StateAccessStrategy(state, logger, this.schemaRegistry)
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Dispatches an incoming message to the first matching strategy.
|
|
61
|
+
* @param msg - Parsed OSC message.
|
|
62
|
+
* @param source - Source address and port of the packet.
|
|
63
|
+
* @returns Array of replies generated by the strategy.
|
|
64
|
+
*/
|
|
65
|
+
public handle(msg: OscMsg, source: RemoteClient): OscReply[] {
|
|
66
|
+
const addr = msg.address;
|
|
67
|
+
|
|
68
|
+
// Skip noisy subscriptions in high-level logging
|
|
69
|
+
if (!addr.startsWith('/meters')) {
|
|
70
|
+
this.logger.debug(LogCategory.DISPATCH, `Handling`, { addr, args: msg.args });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Delegate to first matching Strategy
|
|
74
|
+
for (const strategy of this.strategies) {
|
|
75
|
+
if (strategy.canHandle(addr)) {
|
|
76
|
+
const replies = strategy.execute(msg, source);
|
|
77
|
+
if (replies.length > 0) {
|
|
78
|
+
this.logger.debug(LogCategory.DISPATCH, `Strategy ${strategy.constructor.name} replied`, { count: replies.length });
|
|
79
|
+
}
|
|
80
|
+
return replies;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Mimic physical hardware: ignore unknown commands
|
|
85
|
+
this.logger.warn(LogCategory.DISPATCH, `Unknown Command`, { addr, args: msg.args, ip: source.address });
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { X32Node } from '../models/X32Node';
|
|
2
|
+
import { STATIC_RESPONSES_DATA } from './StaticResponseService';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Factory service responsible for constructing the X32 OSC Schema.
|
|
6
|
+
* Encapsulates all the logic for generating channel strips, routing blocks, etc.
|
|
7
|
+
*/
|
|
8
|
+
export class SchemaFactory {
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Builds the complete X32 OSC Schema.
|
|
12
|
+
* @returns The constructed schema map.
|
|
13
|
+
*/
|
|
14
|
+
public createSchema(): Record<string, X32Node> {
|
|
15
|
+
const schema: Record<string, X32Node> = {
|
|
16
|
+
...this.generateNodes(32, 'ch'), ...this.generateNodes(16, 'bus'),
|
|
17
|
+
...this.generateNodes(8, 'dca'), ...this.generateNodes(8, 'auxin'),
|
|
18
|
+
...this.generateNodes(8, 'fxrtn'), ...this.generateNodes(6, 'mtx'),
|
|
19
|
+
...Object.fromEntries(Array.from({ length: 8 }, (_, i) => [`/fx/${i + 1}/type`, this.node('i', 0)])),
|
|
20
|
+
...this.generateRange(8, '/fx', '/type', 'i', 0),
|
|
21
|
+
...this.generateRange(8, '/fx', '/source/l', 'i', 0),
|
|
22
|
+
...this.generateRange(8, '/fx', '/source/r', 'i', 0),
|
|
23
|
+
...Object.fromEntries(
|
|
24
|
+
Array.from({ length: 8 }, (_, slot) =>
|
|
25
|
+
Object.entries(this.generateRange(64, `/fx/${slot + 1}/par`, '', 'f', 0.0, 2, 1))
|
|
26
|
+
).flat()
|
|
27
|
+
),
|
|
28
|
+
...this.generateRange(128, '/headamp', '/gain', 'f', 0.0, 3, 0),
|
|
29
|
+
...this.generateRange(128, '/headamp', '/phantom', 'i', 0, 3, 0),
|
|
30
|
+
...this.generateRange(6, '/config/mute', '', 'i', 0),
|
|
31
|
+
...this.generateRange(80, '/-stat/solosw', '', 'i', 0),
|
|
32
|
+
|
|
33
|
+
// Global Status
|
|
34
|
+
'/-stat/selidx': this.node('i', 0),
|
|
35
|
+
'/-stat/sendsonfader': this.node('i', 0),
|
|
36
|
+
'/-stat/bussendbank': this.node('i', 0),
|
|
37
|
+
'/-stat/keysolo': this.node('i', 0),
|
|
38
|
+
|
|
39
|
+
// Screens
|
|
40
|
+
'/-stat/screen/screen': this.node('i', 0),
|
|
41
|
+
'/-stat/screen/CHAN/page': this.node('i', 0),
|
|
42
|
+
'/-stat/screen/METER/page': this.node('i', 0),
|
|
43
|
+
'/-stat/screen/ROUTE/page': this.node('i', 0),
|
|
44
|
+
'/-stat/screen/SETUP/page': this.node('i', 0),
|
|
45
|
+
'/-stat/screen/LIBRARY/page': this.node('i', 0),
|
|
46
|
+
'/-stat/screen/FX/page': this.node('i', 0),
|
|
47
|
+
'/-stat/screen/MON/page': this.node('i', 0),
|
|
48
|
+
'/-stat/screen/USB/page': this.node('i', 0),
|
|
49
|
+
'/-stat/screen/SCENE/page': this.node('i', 0),
|
|
50
|
+
'/-stat/screen/ASSIGN/page': this.node('i', 0),
|
|
51
|
+
|
|
52
|
+
// Telemetry
|
|
53
|
+
'/-stat/talk/A': this.node('i', 0),
|
|
54
|
+
'/-stat/talk/B': this.node('i', 0),
|
|
55
|
+
'/-stat/osc/on': this.node('i', 0),
|
|
56
|
+
'/-prefs/autosel': this.node('i', 1),
|
|
57
|
+
|
|
58
|
+
// Actions
|
|
59
|
+
'/-action/setrtasrc': this.node('i', 0),
|
|
60
|
+
'/-action/playtrack': this.node('i', 0),
|
|
61
|
+
'/-action/goscene': this.node('i', 0),
|
|
62
|
+
'/-action/setscene': this.node('i', 0),
|
|
63
|
+
'/config/routing/AES50A/1-8': this.node('i', 0),
|
|
64
|
+
'/config/routing/AES50B/1-8': this.node('i', 0),
|
|
65
|
+
'/config/routing/CARD/1-8': this.node('i', 0),
|
|
66
|
+
'/config/routing/OUT/1-4': this.node('i', 0),
|
|
67
|
+
'/-prefs/invertmutes': this.node('i', 0),
|
|
68
|
+
|
|
69
|
+
...this.generateChannelStrip('/main/st', 6, false),
|
|
70
|
+
...this.generateChannelStrip('/main/m', 6, false),
|
|
71
|
+
|
|
72
|
+
// Links
|
|
73
|
+
...Object.fromEntries(Array.from({ length: 16 }, (_, i) => [`/config/chlink/${i * 2 + 1}-${i * 2 + 2}`, this.node('i', 0)])),
|
|
74
|
+
...Object.fromEntries(Array.from({ length: 8 }, (_, i) => [`/config/buslink/${i * 2 + 1}-${i * 2 + 2}`, this.node('i', 0)])),
|
|
75
|
+
...Object.fromEntries(Array.from({ length: 4 }, (_, i) => [`/config/auxlink/${i * 2 + 1}-${i * 2 + 2}`, this.node('i', 0)])),
|
|
76
|
+
...Object.fromEntries(Array.from({ length: 4 }, (_, i) => [`/config/fxlink/${i * 2 + 1}-${i * 2 + 2}`, this.node('i', 0)])),
|
|
77
|
+
|
|
78
|
+
...this.generateRange(32, '/config/userrout/in', '', 'i', 0),
|
|
79
|
+
...this.generateRange(48, '/config/userrout/out', '', 'i', 0),
|
|
80
|
+
|
|
81
|
+
// Solo Config
|
|
82
|
+
'/config/solo/level': this.node('f', 0.0),
|
|
83
|
+
'/config/solo/source': this.node('i', 0),
|
|
84
|
+
'/config/solo/sourcetrim': this.node('f', 0.0),
|
|
85
|
+
'/config/solo/exclusive': this.node('i', 0),
|
|
86
|
+
'/config/solo/dim': this.node('i', 0),
|
|
87
|
+
'/config/solo/dimpfl': this.node('i', 0),
|
|
88
|
+
'/config/solo/dimatt': this.node('f', 0.0),
|
|
89
|
+
'/config/solo/mono': this.node('i', 0),
|
|
90
|
+
'/config/solo/chmode': this.node('i', 0),
|
|
91
|
+
'/config/solo/busmode': this.node('i', 0),
|
|
92
|
+
'/config/solo/dcamode': this.node('i', 0),
|
|
93
|
+
'/config/solo/masterctrl': this.node('i', 0),
|
|
94
|
+
'/config/solo/delay': this.node('i', 0),
|
|
95
|
+
'/config/solo/delaytime': this.node('f', 0.0),
|
|
96
|
+
'/config/solo/followsel': this.node('i', 0),
|
|
97
|
+
'/config/solo/followsolo': this.node('i', 0),
|
|
98
|
+
|
|
99
|
+
// Talkback Config
|
|
100
|
+
'/config/talk/enable': this.node('i', 0),
|
|
101
|
+
'/config/talk/source': this.node('i', 0),
|
|
102
|
+
'/config/talk/A/level': this.node('f', 0.0),
|
|
103
|
+
'/config/talk/B/level': this.node('f', 0.0),
|
|
104
|
+
'/config/talk/A/dim': this.node('i', 0),
|
|
105
|
+
'/config/talk/B/dim': this.node('i', 0),
|
|
106
|
+
'/config/talk/A/latch': this.node('i', 0),
|
|
107
|
+
'/config/talk/B/latch': this.node('i', 0),
|
|
108
|
+
'/config/talk/A/destmap': this.node('i', 0),
|
|
109
|
+
'/config/talk/B/destmap': this.node('i', 0),
|
|
110
|
+
|
|
111
|
+
// Oscillator
|
|
112
|
+
'/config/osc/on': this.node('i', 0),
|
|
113
|
+
'/config/osc/type': this.node('i', 0),
|
|
114
|
+
'/config/osc/fsel': this.node('i', 0),
|
|
115
|
+
'/config/osc/f1': this.node('f', 0.5),
|
|
116
|
+
'/config/osc/f2': this.node('f', 0.5),
|
|
117
|
+
'/config/osc/level': this.node('f', 0.0),
|
|
118
|
+
'/config/osc/dest': this.node('i', 0),
|
|
119
|
+
|
|
120
|
+
// Outputs
|
|
121
|
+
...this.generateOutputs('main', 16),
|
|
122
|
+
...this.generateOutputs('aux', 6),
|
|
123
|
+
...this.generateOutputs('p16', 16),
|
|
124
|
+
...this.generateOutputs('aes', 2),
|
|
125
|
+
...this.generateOutputs('rec', 2)
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Static Responses Integration
|
|
129
|
+
Object.keys(STATIC_RESPONSES_DATA).forEach(key => {
|
|
130
|
+
if (key.startsWith('/-') || key.startsWith('/stat')) {
|
|
131
|
+
schema[key] = this.node('i', 0);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Routing Blocks
|
|
136
|
+
const routingBlocks = [
|
|
137
|
+
'/config/routing/IN/1-8', '/config/routing/IN/9-16', '/config/routing/IN/17-24', '/config/routing/IN/25-32',
|
|
138
|
+
'/config/routing/AUX/1-4',
|
|
139
|
+
'/config/routing/OUT/1-4', '/config/routing/OUT/5-8', '/config/routing/OUT/9-12', '/config/routing/OUT/13-16',
|
|
140
|
+
'/config/routing/P16/1-8', '/config/routing/P16/9-16',
|
|
141
|
+
'/config/routing/CARD/1-8', '/config/routing/CARD/9-16', '/config/routing/CARD/17-24', '/config/routing/CARD/25-32',
|
|
142
|
+
'/config/routing/AES50A/1-8', '/config/routing/AES50A/9-16', '/config/routing/AES50A/17-24', '/config/routing/AES50A/25-32', '/config/routing/AES50A/33-40', '/config/routing/AES50A/41-48',
|
|
143
|
+
'/config/routing/AES50B/1-8', '/config/routing/AES50B/9-16', '/config/routing/AES50B/17-24', '/config/routing/AES50B/25-32', '/config/routing/AES50B/33-40', '/config/routing/AES50B/41-48',
|
|
144
|
+
'/config/routing/PLAY/1-8', '/config/routing/PLAY/9-16', '/config/routing/PLAY/17-24', '/config/routing/PLAY/25-32'
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
routingBlocks.forEach(path => {
|
|
148
|
+
schema[path] = this.node('i', 0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return schema;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private node(type: 'f' | 'i' | 's', def: number | string): X32Node {
|
|
155
|
+
return new X32Node(type, def);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private generateChannelStrip(base: string, eqBands: number = 4, hasPreamp: boolean = false): Record<string, X32Node> {
|
|
159
|
+
const nodes: Record<string, X32Node> = {};
|
|
160
|
+
|
|
161
|
+
// Config
|
|
162
|
+
nodes[`${base}/config/name`] = this.node('s', base.split('/').pop()?.toUpperCase() || '');
|
|
163
|
+
nodes[`${base}/config/icon`] = this.node('i', 1);
|
|
164
|
+
nodes[`${base}/config/color`] = this.node('i', 1);
|
|
165
|
+
nodes[`${base}/config/source`] = this.node('i', 1);
|
|
166
|
+
|
|
167
|
+
// Mix
|
|
168
|
+
nodes[`${base}/mix/fader`] = this.node('f', 0.75);
|
|
169
|
+
nodes[`${base}/mix/on`] = this.node('i', 0);
|
|
170
|
+
nodes[`${base}/mix/pan`] = this.node('f', 0.5);
|
|
171
|
+
nodes[`${base}/mix/mono`] = this.node('i', 0);
|
|
172
|
+
nodes[`${base}/mix/mlevel`] = this.node('f', 0.0);
|
|
173
|
+
nodes[`${base}/mix/st`] = this.node('i', 1);
|
|
174
|
+
|
|
175
|
+
if (hasPreamp) {
|
|
176
|
+
nodes[`${base}/preamp/trim`] = this.node('f', 0.5);
|
|
177
|
+
nodes[`${base}/preamp/hpon`] = this.node('i', 0);
|
|
178
|
+
nodes[`${base}/preamp/hpf`] = this.node('f', 0.0);
|
|
179
|
+
nodes[`${base}/preamp/phantom`] = this.node('i', 0);
|
|
180
|
+
nodes[`${base}/preamp/rtnsw`] = this.node('i', 0);
|
|
181
|
+
nodes[`${base}/preamp/invert`] = this.node('i', 0);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Delay
|
|
185
|
+
nodes[`${base}/delay/on`] = this.node('i', 0);
|
|
186
|
+
nodes[`${base}/delay/time`] = this.node('f', 0.0);
|
|
187
|
+
|
|
188
|
+
// Insert
|
|
189
|
+
nodes[`${base}/insert/on`] = this.node('i', 0);
|
|
190
|
+
nodes[`${base}/insert/pos`] = this.node('i', 0);
|
|
191
|
+
nodes[`${base}/insert/sel`] = this.node('i', 0);
|
|
192
|
+
|
|
193
|
+
// Gate
|
|
194
|
+
nodes[`${base}/gate/on`] = this.node('i', 0);
|
|
195
|
+
nodes[`${base}/gate/mode`] = this.node('i', 0);
|
|
196
|
+
nodes[`${base}/gate/thr`] = this.node('f', 0.0);
|
|
197
|
+
nodes[`${base}/gate/range`] = this.node('f', 0.0);
|
|
198
|
+
nodes[`${base}/gate/attack`] = this.node('f', 0.0);
|
|
199
|
+
nodes[`${base}/gate/hold`] = this.node('f', 0.0);
|
|
200
|
+
nodes[`${base}/gate/release`] = this.node('f', 0.0);
|
|
201
|
+
nodes[`${base}/gate/keysrc`] = this.node('i', 0);
|
|
202
|
+
nodes[`${base}/gate/filter/on`] = this.node('i', 0);
|
|
203
|
+
nodes[`${base}/gate/filter/type`] = this.node('i', 0);
|
|
204
|
+
nodes[`${base}/gate/filter/f`] = this.node('f', 0.5);
|
|
205
|
+
|
|
206
|
+
// Dynamics
|
|
207
|
+
nodes[`${base}/dyn/on`] = this.node('i', 0);
|
|
208
|
+
nodes[`${base}/dyn/mode`] = this.node('i', 0);
|
|
209
|
+
nodes[`${base}/dyn/pos`] = this.node('i', 0);
|
|
210
|
+
nodes[`${base}/dyn/det`] = this.node('i', 0);
|
|
211
|
+
nodes[`${base}/dyn/env`] = this.node('i', 0);
|
|
212
|
+
nodes[`${base}/dyn/thr`] = this.node('f', 0.0);
|
|
213
|
+
nodes[`${base}/dyn/ratio`] = this.node('i', 0);
|
|
214
|
+
nodes[`${base}/dyn/knee`] = this.node('f', 0.0);
|
|
215
|
+
nodes[`${base}/dyn/mgain`] = this.node('f', 0.0);
|
|
216
|
+
nodes[`${base}/dyn/attack`] = this.node('f', 0.0);
|
|
217
|
+
nodes[`${base}/dyn/hold`] = this.node('f', 0.0);
|
|
218
|
+
nodes[`${base}/dyn/release`] = this.node('f', 0.0);
|
|
219
|
+
nodes[`${base}/dyn/mix`] = this.node('f', 1.0);
|
|
220
|
+
nodes[`${base}/dyn/auto`] = this.node('i', 0);
|
|
221
|
+
nodes[`${base}/dyn/keysrc`] = this.node('i', 0);
|
|
222
|
+
nodes[`${base}/dyn/filter/on`] = this.node('i', 0);
|
|
223
|
+
nodes[`${base}/dyn/filter/type`] = this.node('i', 0);
|
|
224
|
+
nodes[`${base}/dyn/filter/f`] = this.node('f', 0.5);
|
|
225
|
+
|
|
226
|
+
// EQ
|
|
227
|
+
nodes[`${base}/eq/on`] = this.node('i', 0);
|
|
228
|
+
nodes[`${base}/eq/mode`] = this.node('i', 0);
|
|
229
|
+
for (let b = 1; b <= eqBands; b++) {
|
|
230
|
+
nodes[`${base}/eq/${b}/type`] = this.node('i', 0);
|
|
231
|
+
nodes[`${base}/eq/${b}/f`] = this.node('f', 0.5);
|
|
232
|
+
nodes[`${base}/eq/${b}/g`] = this.node('f', 0.5);
|
|
233
|
+
nodes[`${base}/eq/${b}/q`] = this.node('f', 0.5);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Groups
|
|
237
|
+
nodes[`${base}/grp/dca`] = this.node('i', 0);
|
|
238
|
+
nodes[`${base}/grp/mute`] = this.node('i', 0);
|
|
239
|
+
|
|
240
|
+
return nodes;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private generateNodes(count: number, prefix: string): Record<string, X32Node> {
|
|
244
|
+
const nodes: Record<string, X32Node> = {};
|
|
245
|
+
const isChannelOrAux = prefix === 'ch' || prefix === 'auxin';
|
|
246
|
+
const eqBands = (prefix === 'bus' || prefix === 'mtx') ? 6 : 4;
|
|
247
|
+
|
|
248
|
+
for (let i = 1; i <= count; i++) {
|
|
249
|
+
const padId = i.toString().padStart(2, '0');
|
|
250
|
+
const ids = [i.toString(), padId];
|
|
251
|
+
|
|
252
|
+
ids.forEach(id => {
|
|
253
|
+
const base = `/${prefix}/${id}`;
|
|
254
|
+
|
|
255
|
+
if (prefix === 'dca') {
|
|
256
|
+
nodes[`${base}/config/name`] = this.node('s', `DCA ${id}`);
|
|
257
|
+
nodes[`${base}/config/color`] = this.node('i', 1);
|
|
258
|
+
nodes[`${base}/fader`] = this.node('f', 0.75);
|
|
259
|
+
nodes[`${base}/on`] = this.node('i', 0);
|
|
260
|
+
} else {
|
|
261
|
+
Object.assign(nodes, this.generateChannelStrip(base, eqBands, isChannelOrAux));
|
|
262
|
+
|
|
263
|
+
if (prefix === 'ch' || prefix === 'auxin' || prefix === 'fxrtn' || prefix === 'bus') {
|
|
264
|
+
for (let b = 1; b <= 16; b++) {
|
|
265
|
+
const busId = b.toString().padStart(2, '0');
|
|
266
|
+
nodes[`${base}/mix/${busId}/level`] = this.node('f', 0.0);
|
|
267
|
+
nodes[`${base}/mix/${busId}/on`] = this.node('i', 0);
|
|
268
|
+
nodes[`${base}/mix/${busId}/pan`] = this.node('f', 0.5);
|
|
269
|
+
nodes[`${base}/mix/${busId}/type`] = this.node('i', 0);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return nodes;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private generateRange(count: number, prefix: string, suffix: string, type: 'f'|'i'|'s', def: number|string, pad: number = 2, start: number = 1): Record<string, X32Node> {
|
|
279
|
+
const nodes: Record<string, X32Node> = {};
|
|
280
|
+
for (let i = start; i < start + count; i++) {
|
|
281
|
+
const id = i.toString().padStart(pad, '0');
|
|
282
|
+
nodes[`${prefix}/${i}${suffix}`] = this.node(type, def);
|
|
283
|
+
nodes[`${prefix}/${id}${suffix}`] = this.node(type, def);
|
|
284
|
+
}
|
|
285
|
+
return nodes;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private generateOutputs(prefix: string, count: number): Record<string, X32Node> {
|
|
289
|
+
const nodes: Record<string, X32Node> = {};
|
|
290
|
+
for (let i = 1; i <= count; i++) {
|
|
291
|
+
const id = i.toString().padStart(2, '0');
|
|
292
|
+
const base = `/outputs/${prefix}/${id}`;
|
|
293
|
+
nodes[`${base}/src`] = this.node('i', 0);
|
|
294
|
+
nodes[`${base}/pos`] = this.node('i', 0);
|
|
295
|
+
nodes[`${base}/invert`] = this.node('i', 0);
|
|
296
|
+
nodes[`${base}/delay/on`] = this.node('i', 0);
|
|
297
|
+
nodes[`${base}/delay/time`] = this.node('f', 0.0);
|
|
298
|
+
|
|
299
|
+
if (prefix === 'p16') {
|
|
300
|
+
nodes[`${base}/iQ/group`] = this.node('i', 0);
|
|
301
|
+
nodes[`${base}/iQ/model`] = this.node('i', 0);
|
|
302
|
+
nodes[`${base}/iQ/eq`] = this.node('i', 0);
|
|
303
|
+
nodes[`${base}/iQ/speaker`] = this.node('i', 0);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return nodes;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { X32Node } from '../models/X32Node';
|
|
2
|
+
import { SchemaFactory } from './SchemaFactory';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Service providing access to the X32 OSC Schema.
|
|
6
|
+
* Acts as the "Registry" of all available console parameters.
|
|
7
|
+
*/
|
|
8
|
+
export class SchemaRegistry {
|
|
9
|
+
private readonly _schema: Record<string, X32Node>;
|
|
10
|
+
|
|
11
|
+
constructor(private schemaFactory: SchemaFactory) {
|
|
12
|
+
this._schema = this.schemaFactory.createSchema();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Retrieves the entire schema map.
|
|
18
|
+
* @returns The internal schema definition record.
|
|
19
|
+
*/
|
|
20
|
+
public getSchema(): Record<string, X32Node> {
|
|
21
|
+
return this._schema;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Retrieves the node definition for a given path.
|
|
26
|
+
* @param path - The OSC path.
|
|
27
|
+
* @returns The X32Node definition or undefined if not found.
|
|
28
|
+
*/
|
|
29
|
+
public getNode(path: string): X32Node | undefined {
|
|
30
|
+
return this._schema[path];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Checks if a path exists in the schema.
|
|
35
|
+
* @param path - The path to check.
|
|
36
|
+
* @returns True if the path is registered.
|
|
37
|
+
*/
|
|
38
|
+
public has(path: string): boolean {
|
|
39
|
+
return path in this._schema;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns all paths in the schema.
|
|
44
|
+
* @returns Array of all registered OSC paths.
|
|
45
|
+
*/
|
|
46
|
+
public getAllPaths(): string[] {
|
|
47
|
+
return Object.keys(this._schema);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Maps an absolute X32 global index to its specific OSC root path.
|
|
52
|
+
* Used for batch subscriptions where clients request ranges of channels.
|
|
53
|
+
* @param index - The absolute integer index.
|
|
54
|
+
* @returns The root path string or null if index is out of bounds.
|
|
55
|
+
*/
|
|
56
|
+
public getRootFromIndex(index: number): string | null {
|
|
57
|
+
if (index >= 0 && index <= 31) return `/ch/${(index + 1).toString().padStart(2, '0')}`;
|
|
58
|
+
if (index >= 32 && index <= 39) return `/auxin/${(index - 31).toString().padStart(2, '0')}`;
|
|
59
|
+
if (index >= 40 && index <= 47) return `/fxrtn/${(index - 39).toString().padStart(2, '0')}`;
|
|
60
|
+
if (index >= 48 && index <= 63) return `/bus/${(index - 47).toString().padStart(2, '0')}`;
|
|
61
|
+
if (index >= 64 && index <= 69) return `/mtx/${(index - 63).toString().padStart(2, '0')}`;
|
|
62
|
+
if (index === 70) return '/main/st';
|
|
63
|
+
if (index === 71) return '/main/m';
|
|
64
|
+
if (index >= 72 && index <= 79) return `/dca/${(index - 71).toString().padStart(2, '0')}`;
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|