@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.
Files changed (86) hide show
  1. package/.commitlintrc.json +3 -0
  2. package/.github/workflows/publish.yml +38 -0
  3. package/.husky/commit-msg +1 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.oxlintrc.json +56 -0
  6. package/CHANGELOG.md +11 -0
  7. package/INSTALL.md +107 -0
  8. package/LICENSE +21 -0
  9. package/README.md +141 -0
  10. package/dist/UdpNetworkGateway-BrroQ6-Q.mjs +1189 -0
  11. package/dist/UdpNetworkGateway-Ccdd7Us5.cjs +1265 -0
  12. package/dist/index.cjs +7 -0
  13. package/dist/index.d.cts +207 -0
  14. package/dist/index.d.mts +207 -0
  15. package/dist/index.mjs +3 -0
  16. package/dist/server.cjs +1060 -0
  17. package/dist/server.d.cts +10 -0
  18. package/dist/server.d.mts +10 -0
  19. package/dist/server.mjs +1055 -0
  20. package/docs/OSC-Communication.md +184 -0
  21. package/docs/X32-INTERNAL.md +262 -0
  22. package/docs/X32-OSC.pdf +0 -0
  23. package/docs/behringer-x32-x32-osc-remote-protocol-en-44463.pdf +0 -0
  24. package/package.json +68 -0
  25. package/src/application/use-cases/BroadcastUpdatesUseCase.ts +120 -0
  26. package/src/application/use-cases/ManageSessionsUseCase.ts +9 -0
  27. package/src/application/use-cases/ProcessPacketUseCase.ts +26 -0
  28. package/src/application/use-cases/SimulationService.ts +122 -0
  29. package/src/domain/entities/SubscriptionManager.ts +126 -0
  30. package/src/domain/entities/X32State.ts +78 -0
  31. package/src/domain/models/MeterConfig.ts +22 -0
  32. package/src/domain/models/MeterData.ts +59 -0
  33. package/src/domain/models/OscMessage.ts +93 -0
  34. package/src/domain/models/X32Address.ts +78 -0
  35. package/src/domain/models/X32Node.ts +43 -0
  36. package/src/domain/models/types.ts +96 -0
  37. package/src/domain/ports/ILogger.ts +27 -0
  38. package/src/domain/ports/INetworkGateway.ts +8 -0
  39. package/src/domain/ports/IStateRepository.ts +16 -0
  40. package/src/domain/services/MeterService.ts +46 -0
  41. package/src/domain/services/OscMessageHandler.ts +88 -0
  42. package/src/domain/services/SchemaFactory.ts +308 -0
  43. package/src/domain/services/SchemaRegistry.ts +67 -0
  44. package/src/domain/services/StaticResponseService.ts +52 -0
  45. package/src/domain/services/strategies/BatchStrategy.ts +74 -0
  46. package/src/domain/services/strategies/MeterStrategy.ts +45 -0
  47. package/src/domain/services/strategies/NodeDiscoveryStrategy.ts +36 -0
  48. package/src/domain/services/strategies/OscCommandStrategy.ts +22 -0
  49. package/src/domain/services/strategies/StateAccessStrategy.ts +71 -0
  50. package/src/domain/services/strategies/StaticResponseStrategy.ts +42 -0
  51. package/src/domain/services/strategies/SubscriptionStrategy.ts +56 -0
  52. package/src/infrastructure/mappers/OscCodec.ts +54 -0
  53. package/src/infrastructure/repositories/InMemoryStateRepository.ts +21 -0
  54. package/src/infrastructure/services/ConsoleLogger.ts +177 -0
  55. package/src/infrastructure/services/UdpNetworkGateway.ts +71 -0
  56. package/src/presentation/cli/server.ts +194 -0
  57. package/src/presentation/library/library.ts +9 -0
  58. package/tests/application/use-cases/BroadcastUpdatesUseCase.test.ts +104 -0
  59. package/tests/application/use-cases/ManageSessionsUseCase.test.ts +12 -0
  60. package/tests/application/use-cases/ProcessPacketUseCase.test.ts +49 -0
  61. package/tests/application/use-cases/SimulationService.test.ts +77 -0
  62. package/tests/domain/entities/SubscriptionManager.test.ts +50 -0
  63. package/tests/domain/entities/X32State.test.ts +52 -0
  64. package/tests/domain/models/MeterData.test.ts +23 -0
  65. package/tests/domain/models/OscMessage.test.ts +38 -0
  66. package/tests/domain/models/X32Address.test.ts +30 -0
  67. package/tests/domain/models/X32Node.test.ts +30 -0
  68. package/tests/domain/services/MeterService.test.ts +27 -0
  69. package/tests/domain/services/OscMessageHandler.test.ts +51 -0
  70. package/tests/domain/services/SchemaRegistry.test.ts +47 -0
  71. package/tests/domain/services/StaticResponseService.test.ts +15 -0
  72. package/tests/domain/services/strategies/BatchStrategy.test.ts +41 -0
  73. package/tests/domain/services/strategies/MeterStrategy.test.ts +19 -0
  74. package/tests/domain/services/strategies/NodeDiscoveryStrategy.test.ts +22 -0
  75. package/tests/domain/services/strategies/StateAccessStrategy.test.ts +49 -0
  76. package/tests/domain/services/strategies/StaticResponseStrategy.test.ts +15 -0
  77. package/tests/domain/services/strategies/SubscriptionStrategy.test.ts +45 -0
  78. package/tests/infrastructure/mappers/OscCodec.test.ts +41 -0
  79. package/tests/infrastructure/repositories/InMemoryStateRepository.test.ts +29 -0
  80. package/tests/infrastructure/services/ConsoleLogger.test.ts +74 -0
  81. package/tests/infrastructure/services/UdpNetworkGateway.test.ts +61 -0
  82. package/tests/presentation/cli/server.test.ts +178 -0
  83. package/tests/presentation/library/library.test.ts +13 -0
  84. package/tsconfig.json +21 -0
  85. package/tsdown.config.ts +15 -0
  86. package/vitest.config.ts +9 -0
@@ -0,0 +1,184 @@
1
+ # Protocol Analysis and Implementation Strategies regarding the Behringer X32 Digital Mixing Console Ecosystem
2
+
3
+ ## 1. Introduction to Networked Audio Control Systems
4
+ The transition from analog to digital mixing consoles has fundamentally altered the landscape of live sound reinforcement, studio recording, and broadcast audio. Among the myriad devices that have driven this paradigm shift, the **Behringer X32** and **Midas M32** families of digital consoles stand as preeminent examples of commercial success and technical ubiquity.
5
+
6
+ A central feature of these ecosystems is their capability for remote control and telemetry via standard network protocols. Unlike their analog predecessors, which relied on voltage-controlled amplifiers and physical patching, modern digital consoles operate as sophisticated computers that process audio signals via **Digital Signal Processing (DSP)** algorithms. This architecture allows the control surface—the physical faders, knobs, and buttons—to be decoupled from the audio processing engine. Consequently, the manipulation of audio parameters can be achieved not only through the physical interface but also via external software applications and automated scripts communicating over a Local Area Network (LAN).
7
+
8
+ The mechanism governing this external communication is the **Open Sound Control (OSC)** protocol. Optimized for modern networking technology, OSC provides a flexible, URL-style symbolic naming scheme (e.g., `/ch/01/mix/fader`) that is significantly more human-readable and granular than the legacy MIDI standard.
9
+
10
+ For the X32 ecosystem, this protocol facilitates the operation of:
11
+ * The official **X32-Edit** software (PC/Mac/Linux)
12
+ * The **X32-Mix** app (iPad)
13
+ * The **Mixing Station** app (Android)
14
+ * A host of third-party integration tools
15
+
16
+ ---
17
+
18
+ ## 2. The Documentation Landscape
19
+ A critical challenge for developers and integrators working with the X32 platform is the scarcity and incompleteness of official documentation provided by the manufacturer, Music Tribe. While Behringer released a preliminary OSC document in late 2012 (Version 1.01), it has not been officially updated to reflect substantial feature additions (Firmware 2.x, 3.x, and 4.x), such as new effects engines, extended routing options, and the "User" layer.
20
+
21
+ ### 2.1 The "Unofficial" Standard
22
+ To bridge this information gap, the user community—spearheaded by researcher and developer Patrick-Gilles Maillot—undertook a massive effort to reverse-engineer the complete protocol. The resulting document, **"Unofficial X32/M32 OSC Remote Protocol,"** is widely regarded as the definitive technical reference.
23
+
24
+ > **Note:** This documentation distinguishes itself by detailing data types, value ranges, and context-dependent behaviors of the network stack, such as the critical distinction between the `/subscribe` command and the `/xremote` command.
25
+
26
+ The documentation is maintained in a live repository, covering nuanced differences between hardware models (X32 Rack, Core, Compact) which share a common firmware core but report different hardware identifiers.
27
+
28
+ ---
29
+
30
+ ## 3. Network Transport and Architecture
31
+ The foundation of the X32’s remote control capability is the **User Datagram Protocol (UDP)**.
32
+
33
+ ### 3.1 UDP vs. TCP in Audio Control
34
+ TCP is connection-oriented, guaranteeing packet delivery via handshakes and acknowledgments (ACKs). In live mixing, however, **latency is the enemy**. If a packet containing a fader move is dropped, pausing the stream for retransmission is undesirable. The X32 network stack is therefore **stateless** and "fire-and-forget." It listens for incoming UDP datagrams, processes valid OSC messages, and sends replies to the source address without maintaining a persistent socket connection.
35
+
36
+ ### 3.2 Port Assignments and Addressing
37
+ * **Target Port:** UDP Port **10023** (Hard-coded).
38
+ * **Source Port:** Ephemeral (chosen by the client OS).
39
+
40
+ **Critical Implementation Detail:** The X32 sends replies back to the *source IP* and *source port* of the incoming packet. Developers must bind to a specific local UDP socket for both sending and receiving. If the OS assigns a new port for the next message, the reply from the X32 will be sent to the original port and potentially discarded.
41
+
42
+ ### 3.3 Endianness and Data Representation
43
+ The OSC specification 1.0 mandates Big-Endian (network byte order).
44
+ * **Integers:** 32-bit signed ($i$)
45
+ * **Floats:** 32-bit IEEE 754 ($f$)
46
+ * **Strings:** Null-terminated ASCII ($s$)
47
+ * **Blobs:** Int32 size count + binary data ($b$)
48
+
49
+ **The Deviation:** While OSC headers are Big-Endian, the internal content of binary blobs returned by the `/meters` command is encoded in **Little-Endian** format, reflecting the internal architecture of the DSP (likely Analog Devices SHARC).
50
+
51
+ ---
52
+
53
+ ## 4. Device Discovery and Enumeration
54
+ Applications utilize the `/xinfo` mechanism to locate the console, as IP addresses may be dynamic (DHCP).
55
+
56
+ ### 4.1 The `/xinfo` Handshake
57
+ 1. **Query:** Client sends `/xinfo` (Broadcast or Unicast) on port 10023.
58
+ 2. **Response:** X32 replies with ` /xinfo ,ssss`.
59
+
60
+ **Response Arguments:**
61
+ 1. **IP Address:** The console's self-reported IP (e.g., `192.168.1.50`).
62
+ 2. **Console Name:** User-defined network name (e.g., `X32-FOH`).
63
+ 3. **Model Identifier:** Hardware ID used to customize UI (e.g., `X32`, `X32RACK`, `X32CORE`).
64
+ 4. **Firmware Version:** Critical for version control and compatibility checks (e.g., `4.06`).
65
+
66
+ ### 4.2 Application Implementation
67
+ A typical implementation creates a UDP socket with `SO_BROADCAST` permissions, sends the `/xinfo` packet, and employs a short timeout (e.g., 2 seconds) to listen for the "business card" response.
68
+
69
+ ---
70
+
71
+ ## 5. Session Persistence and State Synchronization
72
+ Since UDP is stateless, the X32 does not inherently know if a client is "connected." To support real-time animation of faders, the protocol implements a subscription-based heartbeat.
73
+
74
+ ### 5.1 The `/xremote` Command
75
+ * **Function:** Acts as a subscription request.
76
+ * **Behavior:** The X32 adds the sender to an internal list of "active remote clients" and mirrors **every** parameter change on the console to that client.
77
+ * **Timeout:** A watchdog timer (approx. 10 seconds) purges clients if no further command is received.
78
+ * **Keep-Alive:** Clients must resend `/xremote` in a background loop (every 7–9 seconds) to reset the watchdog.
79
+
80
+ ### 5.2 `/xremote` vs. `/subscribe`
81
+ * **`/xremote`**: The "firehose" command. Requests *all* console changes. Efficient for full desktop editors.
82
+ * **`/subscribe`**: Requests updates for specific addresses or ranges. Efficient for mobile apps focusing on specific features.
83
+
84
+ For intercepting active channel info, `/xremote` is superior as it guarantees any selection change is broadcast without a priori knowledge.
85
+
86
+ ---
87
+
88
+ ## 6. Intercepting Active Channel Information
89
+ "Active Channel" refers to the channel currently selected by the **SEL** button on the hardware surface.
90
+
91
+ ### 6.1 The `/-stat/selidx` Command
92
+ The X32 broadcasts the selection state via:
93
+ * **OSC Path:** `/-stat/selidx`
94
+ * **Data Type:** 32-bit Integer (`,i`)
95
+ * **Value Range:** 0 to 71
96
+
97
+ ### 6.2 Decoding the Selection Index
98
+ The integer corresponds to a fixed map of channel strips:
99
+
100
+ | Index Range (Int) | Channel Type | Specific Channels | OSC Address Path |
101
+ | :--- | :--- | :--- | :--- |
102
+ | **0 – 31** | Input Channels | Channels 1 – 32 | `/ch/01` ... `/ch/32` |
103
+ | **32 – 39** | Aux Inputs | Aux 1 – 6, USB L/R | `/auxin/01` ... |
104
+ | **40 – 47** | FX Returns | FX 1L, 1R ... 4R | `/fxrtn/01` ... |
105
+ | **48 – 63** | Mix Buses | Bus 1 – 16 | `/bus/01` ... |
106
+ | **64 – 69** | Matrix Outputs | Matrix 1 – 6 | `/mtx/01` ... |
107
+ | **70** | Main LR | Stereo Master Bus | `/main/st` |
108
+ | **71** | Mono/Center | Mono Bus | `/main/m` |
109
+
110
+ > **Important:** DCAs are absent from this range as they are control groups, not processing paths with full channel strips.
111
+
112
+ ### 6.3 Implementation of Interception
113
+ To intercept this data, a custom script acts as a parallel client:
114
+ 1. **Subscription:** Send `/xremote` to register for updates.
115
+ 2. **Listening:** Listen on the bound UDP port.
116
+ 3. **Parsing:** When `/-stat/selidx` is received, parse the integer.
117
+ 4. **Logic:** Map the integer (e.g., `0`) to the channel (`Channel 1`).
118
+
119
+ ### 6.4 The `/-prefs/autosel` Variable
120
+ * **autosel ON:** Touching a fader triggers `/-stat/selidx`.
121
+ * **autosel OFF:** Only pressing the "Select" button triggers the message.
122
+
123
+ ---
124
+
125
+ ## 7. Information About Channels: Command Hierarchy
126
+ The root address for inputs is `/ch/` using a two-digit string index (`01`–`32`).
127
+
128
+ ### 7.1 Functional Blocks
129
+
130
+ **Configuration (`/config`)**
131
+ * `/ch/{n}/config/name`: String (User name)
132
+ * `/ch/{n}/config/icon`: Integer (Icon index)
133
+ * `/ch/{n}/config/color`: Integer (Color index)
134
+
135
+ **Preamp (`/preamp`)**
136
+ * `/ch/{n}/preamp/gain`: Float (0.0–1.0 mapping to -12dB to +60dB)
137
+ * `/ch/{n}/preamp/rtnsw`: Integer (48V Phantom Power, 0/1)
138
+
139
+ **Gate & Dynamics (`/gate`, `/dyn`)**
140
+ * `/ch/{n}/gate/on`: Integer (Active/Bypass)
141
+ * `/ch/{n}/dyn/ratio`: Enum (Compression ratio)
142
+
143
+ **Equalizer (`/eq`)**
144
+ * `/ch/{n}/eq/{b}/f`: Float (Frequency, log mapping 20Hz–20kHz)
145
+ * `/ch/{n}/eq/{b}/g`: Float (Gain, -15dB to +15dB)
146
+
147
+ **Mix & Fader (`/mix`)**
148
+ * `/ch/{n}/mix/fader`: Float (Level, 0.75 ≈ 0dB)
149
+ * `/ch/{n}/mix/on`: Integer (**Note:** 1 = Unmuted/On, 0 = Muted/Off)
150
+ * `/ch/{n}/mix/pan`: Float (0.0 = Left, 1.0 = Right)
151
+
152
+ ### 7.2 "Sends on Fader" Telemetry
153
+ Scripts can monitor `/-stat/sendsonfader` (0/1) to detect this mode. If active, fader moves control bus sends rather than the main mix.
154
+
155
+ ---
156
+
157
+ ## 8. Metering and Bulk Data Transfer
158
+ To avoid network flooding, metering is handled via OSC Blobs.
159
+
160
+ ### 8.1 The `/meters` Command
161
+ Request: `/meters ,s "/meters/1"` (where `/meters/1` covers input channels).
162
+
163
+ ### 8.2 Blob Structure and Endianness
164
+ The response uses ` /meters ,b <binary_data>`.
165
+ 1. **Size (Int32):** Big-Endian
166
+ 2. **Count (Int32):** **Little-Endian**
167
+ 3. **Data (Float32 Array):** **Little-Endian**
168
+
169
+ **Critical:** Developers must switch byte-parsing logic after extracting the blob header to avoid nonsensical numeric values.
170
+
171
+ ---
172
+
173
+ ## 9. Application Analysis: X32-Edit
174
+ The official software follows this blueprint:
175
+ 1. **Startup:** Broadcast `/xinfo`.
176
+ 2. **Connect:** Establish unicast UDP session to port 10023.
177
+ 3. **Sync:** Send `/xremote` and dump console state (via `/status` or node requests).
178
+ 4. **Operation:** Loop `/xremote` every ~9s; process `/ch/...` and `/meters` updates.
179
+ 5. **Termination:** Stop heartbeat; session times out.
180
+
181
+ ---
182
+
183
+ ## 10. Conclusion
184
+ The Behringer X32 OSC protocol is a robust system for remote audio control. By utilizing **UDP port 10023**, initiating discovery via **/xinfo**, and maintaining session persistence via **/xremote**, developers can effectively communicate with the console. Furthermore, intercepting "Active Channel" information is achievable by monitoring the **`/-stat/selidx`** command within an active subscription, enabling the creation of sophisticated custom controllers and automated workflows.
@@ -0,0 +1,262 @@
1
+ Voici le texte converti en format Markdown structuré et technique.
2
+
3
+ # Rapport Technique Exhaustif : Conception et Implémentation d'un Simulateur de Console Numérique Behringer X32 via le Protocole Open Sound Control (OSC) en TypeScript
4
+
5
+ ## 1. Introduction et Contexte Architectural
6
+
7
+ La conception d'un simulateur haute fidélité pour la console de mixage numérique **Behringer X32** représente un défi d'ingénierie logicielle complexe, nécessitant une maîtrise approfondie des protocoles réseaux temps réel et de l'architecture des systèmes embarqués audio. Ce rapport technique a pour objectif de fournir une spécification exhaustive pour le développement d'un émulateur "serveur" en **TypeScript**, capable d'interagir de manière indiscernable avec les clients officiels tels que l'application **X32-Mix** (macOS/iPad) et des plugins tiers communiquant via OSC.
8
+
9
+ Contrairement à une simple télécommande qui envoie des commandes, un simulateur doit répliquer le comportement d'état (**"State Machine"**) de la console physique. Il doit :
10
+
11
+ * Gérer les connexions concurrentes.
12
+ * Maintenir la cohérence des données entre les clients.
13
+ * Répondre aux mécanismes de découverte ("Handshake").
14
+ * **Point crucial :** Générer les flux de données de vumètres ("Metering") selon un format binaire propriétaire spécifique.
15
+
16
+ L'analyse des documents techniques et des travaux de rétro-ingénierie révèle que le protocole X32 OSC s'écarte des spécifications standards sur des points critiques tels que l'endianness (boutisme) des données binaires et la gestion des souscriptions.
17
+
18
+ ---
19
+
20
+ ## 2. Fondamentaux du Protocole de Transport et Configuration Réseau
21
+
22
+ ### 2.1 Architecture UDP et Gestion des Ports
23
+
24
+ Le socle de communication de l'écosystème X32 repose quasi exclusivement sur le protocole **UDP (User Datagram Protocol)**. Pour un simulateur, cela implique que la couche réseau doit être résiliente à la perte de paquets et ne pas dépendre de mécanismes de reconnexion complexes propres à TCP.
25
+
26
+ * **Port d'écoute :** **10023** (Standard pour X32/M32).
27
+ * *Note :* La série X-Air utilise le port 10024.
28
+
29
+
30
+ * **Implémentation Node.js :** Utilisation recommandée du module natif `dgram`.
31
+ * **Binding :** Lier le socket à `0.0.0.0` pour accepter les diffusions ("broadcasts").
32
+
33
+ ### 2.2 Structure des Paquets OSC et Alignement Mémoire
34
+
35
+ Le protocole suit des règles d'alignement mémoire strictes (32 bits / 4 octets). Chaque composant (Address Pattern, Type Tag String, Arguments) doit être aligné.
36
+
37
+ **Règle de Padding :** Si une chaîne (incluant son `\0` terminal) ne remplit pas un multiple de 4 octets, elle doit être complétée par des octets nuls.
38
+
39
+ > **Exemple pour la commande `/info` :**
40
+ > * Chaîne : `/info` (5 chars) + `\0` = 6 octets.
41
+ > * Multiple de 4 supérieur = 8.
42
+ > * Padding nécessaire = 2 octets nuls.
43
+ > * Hexadécimal : `2f 69 6e 66 6f 00 00 00`
44
+ >
45
+ >
46
+
47
+ Une erreur d'un seul octet rendra le paquet illisible pour X32-Mix.
48
+
49
+ ### 2.3 Typage des Données et Robustesse du Serveur
50
+
51
+ Le simulateur doit impérativement inclure les chaînes de types (Type Tags) dans les réponses.
52
+
53
+ * **Int32 (`i`)** : Entier signé 32 bits, **Big-Endian**. (Énumérations, ID, Booléens).
54
+ * **Float32 (`f`)** : Flottant IEEE 754 32 bits, **Big-Endian**. (Niveaux, Fréquences).
55
+ * **String (`s`)** : Chaîne ASCII terminée par nul + Padding.
56
+ * **Blob (`b`)** : "Binary Large Object". *Voir section 6.2 pour la spécificité d'endianness.*
57
+
58
+ ---
59
+
60
+ ## 3. Séquence d'Initialisation et Protocole de Découverte
61
+
62
+ ### 3.1 Découverte du Serveur : La commande `/info`
63
+
64
+ Dès le lancement, X32-Mix émet `/info`. Le simulateur doit répondre immédiatement avec 4 arguments de type string (`,ssss`).
65
+
66
+ | Argument | Valeur Type | Description |
67
+ | --- | --- | --- |
68
+ | **Arg 1** | `V2.05` | Version du protocole serveur. |
69
+ | **Arg 2** | `osc-server` | Nom réseau de la console. |
70
+ | **Arg 3** | `X32` | **Critique.** Modèle de console. Utiliser `X32`, `M32`, `X32RACK`. Une valeur inconnue bloque le chargement de l'UI. |
71
+ | **Arg 4** | `2.12` | Version du firmware (ex: `4.06`). |
72
+
73
+ ### 3.2 Vérification d'État : La commande `/status`
74
+
75
+ Réponse attendue (`,sss`) :
76
+
77
+ 1. **État :** `active` (Moteur audio fonctionnel).
78
+ 2. **IP :** Adresse IP locale du simulateur.
79
+ 3. **Nom :** Nom de la console.
80
+
81
+ ### 3.3 Négociation de la Session : Le mécanisme `/xremote`
82
+
83
+ La X32 est un serveur passif par défaut. Le client doit s'abonner aux mises à jour.
84
+
85
+ 1. **Réception :** Le simulateur reçoit `/xremote`.
86
+ 2. **Enregistrement :** Ajout de l'IP/Port du client dans une "Subscribers List".
87
+ 3. **Watchdog :** Timer de 10 secondes. Sans renouvellement, le client est éjecté.
88
+ 4. **Feedback :** Tant que le client est abonné, **chaque** changement d'état interne (fader, mute) doit générer un message OSC sortant vers le client pour éviter l'effet "rubber-banding" (saut de fader).
89
+
90
+ ---
91
+
92
+ ## 4. Architecture de la Carte Mémoire (Address Map)
93
+
94
+ ### 4.1 Hiérarchie Principale
95
+
96
+ * `/ch/[01..32]` : Canaux d'entrée.
97
+ * `/auxin/[01..08]` : Auxiliaires.
98
+ * `/fxrtn/[01..08]` : Retours d'effets.
99
+ * `/bus/[01..16]` : Bus de mixage.
100
+ * `/mtx/[01..06]` : Matrices.
101
+ * `/main/st`, `/main/m` : Sorties Master.
102
+ * `/dca/[1..8]` : DCA.
103
+ * `/config`, `/headamp` : Configuration et Préamplis.
104
+
105
+ ### 4.2 Structure d'une Tranche ("Channel Strip")
106
+
107
+ Sous-modules pour `/ch/01/...` :
108
+
109
+ * `/config` : `name`, `icon`, `color`.
110
+ * `/preamp` : `trim`, `inv` (phase), `hpf` (passe-haut).
111
+ * `/gate` : `mode`, `thr`, `attack`, `hold`, `release`.
112
+ * `/dyn` : Compresseur (`ratio`, `thr`, `knee`, etc.).
113
+ * `/eq` : Paramétrique 4 bandes (`/eq/1` à `/eq/4`). Paramètres : `type`, `f`, `g`, `q`.
114
+ * `/mix` :
115
+ * `fader` : Float (0.0 - 1.0).
116
+ * `on` : Mute (0/1). **Attention :** Logique inversée possible selon le contexte, mais généralement 1 = Audio Passant.
117
+ * `pan` : 0.0 (G) - 1.0 (D).
118
+
119
+
120
+
121
+ ### 4.3 Normalisation des Valeurs
122
+
123
+ Les valeurs sont normalisées sur `[0.0, 1.0]`.
124
+
125
+ * **Faders :** Non-linéaire. `0.0` = -∞ dB, `0.75` ≈ 0 dB, `1.0` = +10 dB.
126
+ * **Fréquences :** Logarithmique (20 Hz - 20 kHz).
127
+
128
+ ---
129
+
130
+ ## 5. Gestion des Scènes et Fichiers
131
+
132
+ Commandes clés à supporter :
133
+
134
+ * `/-action/goscene ,i [index]` : Charger une scène.
135
+ * `/-action/setscene ,i [index]` : Définir la scène courante.
136
+
137
+ Le simulateur doit parser les fichiers `.scn` (texte clair contenant des commandes OSC) et les appliquer ligne par ligne à l'état interne.
138
+
139
+ ---
140
+
141
+ ## 6. Implémentation Avancée du Système de Vumètres (Metering)
142
+
143
+ ### 6.1 La Commande `/meters`
144
+
145
+ Requête client : `/meters ,si [Chemin] [Temps]`
146
+
147
+ * `/meters/0` : Global.
148
+ * `/meters/1` : Canaux détaillés (Entrées + Gate + Dyn).
149
+ * Mise à jour continue (~50ms) pendant 10s si Temps = 0.
150
+
151
+ ### 6.2 Le Format Binaire Hybride (Mixed Endianness)
152
+
153
+ **Anomalie Critique :** Alors que l'en-tête OSC est en Big-Endian, le contenu du Blob ("Payload") contenant les vumètres utilise le **Little-Endian** pour les flottants.
154
+
155
+ **Structure du Blob :**
156
+
157
+ 1. **Taille du Blob (Int32)** : **Big-Endian** (Format OSC Standard).
158
+ 2. **Taille Interne (Int32)** : **Big-Endian** (Début du Payload).
159
+ 3. **Nombre d'Éléments (Int32)** : **Little-Endian**.
160
+ 4. **Données (Float32 Array)** : **Little-Endian**.
161
+
162
+ **Implémentation TypeScript :**
163
+ L'utilisation de bibliothèques OSC standard échouera ici. Il faut construire le buffer manuellement.
164
+
165
+ ```typescript
166
+ // Exemple conceptuel de génération de blob pour /meters/1 (96 valeurs)
167
+ function generateMeterBlob(values: number[]): Buffer {
168
+ const numFloats = values.length; // 96
169
+ const headerBytes = 4; // Taille interne
170
+ const countBytes = 4; // Nombre d'éléments
171
+ const dataBytes = numFloats * 4;
172
+ const totalBlobSize = headerBytes + countBytes + dataBytes;
173
+
174
+ // Création du buffer pour le contenu du blob
175
+ const blobBuffer = Buffer.alloc(totalBlobSize);
176
+ let offset = 0;
177
+
178
+ // 1. Taille Interne (Big Endian) - Spécificité X32
179
+ blobBuffer.writeInt32BE(totalBlobSize, offset);
180
+ offset += 4;
181
+
182
+ // 2. Compteur d'éléments (Little Endian)
183
+ blobBuffer.writeInt32LE(numFloats, offset);
184
+ offset += 4;
185
+
186
+ // 3. Données Flottantes (Little Endian)
187
+ for (let i = 0; i < numFloats; i++) {
188
+ blobBuffer.writeFloatLE(values[i], offset);
189
+ offset += 4;
190
+ }
191
+
192
+ return blobBuffer;
193
+ }
194
+
195
+ ```
196
+
197
+ ### 6.3 Mapping des Vumètres (`/meters/1`)
198
+
199
+ * **0-31 :** Canaux 1-32 (Pre-Fader).
200
+ * **32-63 :** Gate Gain Reduction.
201
+ * **64-95 :** Compressor Gain Reduction.
202
+
203
+ ---
204
+
205
+ ## 7. Architecture Logicielle du Simulateur en TypeScript
206
+
207
+ ### 7.1 Gestionnaire d'État (State Store)
208
+
209
+ Utiliser une structure arborescente émettant des événements.
210
+
211
+ ```typescript
212
+ type X32Parameter = number | string;
213
+
214
+ class X32Node {
215
+ private value: X32Parameter;
216
+ private path: string;
217
+
218
+ setValue(val: X32Parameter) {
219
+ this.value = val;
220
+ this.emit('change', this.path, this.value);
221
+ }
222
+ }
223
+
224
+ ```
225
+
226
+ ### 7.2 Boucle de Rétroaction
227
+
228
+ Le serveur UDP s'abonne au Store :
229
+ `StateStore.on('change', (path, value) => { Server.broadcast(path, value); });`
230
+
231
+ ---
232
+
233
+ ## 8. Méthodologie de Rétro-Ingénierie avec le Simulateur
234
+
235
+ 1. **Mode Espion (Snooping) :** Logger chaque paquet entrant pour mapper les commandes inconnues envoyées par X32-Mix.
236
+ 2. **Validation par Écho :** Envoyer périodiquement des commandes au client pour vérifier si l'UI réagit (boutons qui tournent).
237
+ 3. **Analyse Wireshark :** Filtrer sur `udp.port == 10023` pour valider les séquences d'initialisation et l'endianness des blobs.
238
+
239
+ ---
240
+
241
+ ## 9. Conclusion et Tableaux Récapitulatifs
242
+
243
+ La réussite du simulateur repose sur le respect strict des spécificités Behringer : **Port 10023**, **Handshake précis**, **Souscription active**, et **Endianness hybride** pour les vumètres.
244
+
245
+ ### Tableau Récapitulatif des Types OSC X32
246
+
247
+ | Type Tag | Description | Encodage Réseau (Standard) | Encodage Vumètres (Blob Payload) |
248
+ | --- | --- | --- | --- |
249
+ | `,i` | Entier 32 bits | Big-Endian | N/A |
250
+ | `,f` | Flottant 32 bits | Big-Endian | **Little-Endian** |
251
+ | `,s` | Chaîne | ASCII + Padding | N/A |
252
+ | `,b` | Blob | Taille en Big-Endian | **Mixte** (Taille BE, Data LE) |
253
+
254
+ ### Tableau des Principaux Chemins OSC
255
+
256
+ | Chemin | Description | Arguments Typiques |
257
+ | --- | --- | --- |
258
+ | `/info` | Découverte serveur | Réponse: `,ssss` |
259
+ | `/status` | État système | Réponse: `,sss` |
260
+ | `/xremote` | Souscription | Aucun |
261
+ | `/ch/[01-32]/mix/fader` | Fader Canal | `,f` [0.0 - 1.0] |
262
+ | `/meters/[id]` | Requête Vumètres | `,b` (Blob réponse) |
Binary file
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@djodjonx/x32-simulator",
3
+ "version": "0.0.1",
4
+ "description": "X32 Simulator",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.mts",
9
+ "bin": {
10
+ "x32-simulator": "./dist/server.mjs"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "import": {
15
+ "types": "./dist/index.d.mts",
16
+ "default": "./dist/index.mjs"
17
+ },
18
+ "require": {
19
+ "types": "./dist/index.d.cts",
20
+ "default": "./dist/index.cjs"
21
+ }
22
+ }
23
+ },
24
+ "scripts": {
25
+ "dev": "tsdown --watch",
26
+ "build": "tsdown",
27
+ "test": "vitest run",
28
+ "test:coverage": "vitest run --coverage",
29
+ "test:watch": "vitest",
30
+ "lint": "oxlint -c .oxlintrc.json",
31
+ "type-check": "tsc --noEmit",
32
+ "prepare": "husky",
33
+ "release": "standard-version"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/djodjonx/x32-simulator.git"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "registry": "https://registry.npmjs.org/"
42
+ },
43
+ "keywords": [],
44
+ "author": "Jonathan Moutier <djo.moutier@gmail.com>",
45
+ "license": "MIT",
46
+ "lint-staged": {
47
+ "*.ts": [
48
+ "npm run lint",
49
+ "bash -c 'tsc --noEmit'"
50
+ ]
51
+ },
52
+ "devDependencies": {
53
+ "@commitlint/cli": "^20.3.0",
54
+ "@commitlint/config-conventional": "^20.3.0",
55
+ "@types/node-osc": "^9.1.0",
56
+ "@vitest/coverage-v8": "^4.0.16",
57
+ "husky": "^9.1.7",
58
+ "lint-staged": "^16.2.7",
59
+ "oxlint": "^1.38.0",
60
+ "standard-version": "^9.5.0",
61
+ "tsdown": "^0.19.0-beta.4",
62
+ "typescript": "^5.9.3",
63
+ "vitest": "^4.0.16"
64
+ },
65
+ "dependencies": {
66
+ "node-osc": "^11.2.2"
67
+ }
68
+ }
@@ -0,0 +1,120 @@
1
+ import { SubscriptionManager } from '../../domain/entities/SubscriptionManager';
2
+ import { INetworkGateway } from '../../domain/ports/INetworkGateway';
3
+ import { X32State } from '../../domain/entities/X32State';
4
+ import { SchemaRegistry } from '../../domain/services/SchemaRegistry';
5
+ import { MeterService } from '../../domain/services/MeterService';
6
+ import { ILogger, LogCategory } from '../../domain/ports/ILogger';
7
+
8
+ export class BroadcastUpdatesUseCase {
9
+ constructor(
10
+ private subscriptionManager: SubscriptionManager,
11
+ private state: X32State,
12
+ private gateway: INetworkGateway,
13
+ private logger: ILogger,
14
+ private meterService: MeterService,
15
+ private schemaRegistry: SchemaRegistry
16
+ ) {}
17
+
18
+ public execute(): void {
19
+ this.subscriptionManager.getSubscribers().forEach(sub => {
20
+ if (sub.type === 'path' || !sub.alias) {
21
+ if (sub.type === 'meter' && sub.meterPath) {
22
+ const meterData = this.meterService.generateMeterData(sub.meterPath, this.state);
23
+ this.gateway.send({ address: sub.address, port: sub.port }, sub.meterPath, [meterData.toBlob()]);
24
+ }
25
+ return;
26
+ }
27
+
28
+ const rinfo = { address: sub.address, port: sub.port };
29
+
30
+ if (sub.type === 'batch' && sub.paths) {
31
+ if (sub.paths.length === 1 && sub.paths[0].startsWith('/meters')) {
32
+ const meterData = this.meterService.generateMeterData(sub.paths[0], this.state);
33
+ this.gateway.send(rinfo, sub.alias, [meterData.toBlob()]);
34
+ return;
35
+ }
36
+
37
+ let stride: number;
38
+ const dataSize = sub.paths.length * 4;
39
+ const factor = sub.factor || 0;
40
+ const count = sub.count || 0;
41
+
42
+ if (factor > 0) {
43
+ stride = Math.max(factor * 4, dataSize);
44
+ } else {
45
+ stride = (sub.paths.length === 3) ? 16 : dataSize;
46
+ }
47
+
48
+ const blobSize = count * stride;
49
+ if (isNaN(blobSize) || blobSize < 0) {
50
+ this.logger.error(LogCategory.SYSTEM, `Invalid blob size`, { alias: sub.alias, blobSize, count, stride, factor, dataSize });
51
+ return;
52
+ }
53
+
54
+ const blob = Buffer.alloc(blobSize);
55
+
56
+ for (let i = 0; i < count; i++) {
57
+ const root = this.schemaRegistry.getRootFromIndex(sub.start! + i);
58
+ let currentOffset = i * stride;
59
+
60
+ if (!root) { continue; }
61
+
62
+ sub.paths.forEach((s, pIdx) => {
63
+ if (pIdx * 4 >= stride) return;
64
+
65
+ if (s.startsWith('/meters')) {
66
+ blob.writeFloatLE(0.0, currentOffset);
67
+ } else {
68
+ const target = `${root}${s}`;
69
+ const node = this.schemaRegistry.getNode(target);
70
+ let val = this.state.get(target);
71
+ if (val === undefined) val = node ? node.default : 0;
72
+
73
+ if (typeof val === 'number') {
74
+ if (node && node.type === 'f') {
75
+ blob.writeFloatLE(val, currentOffset);
76
+ }
77
+ else blob.writeInt32LE(val, currentOffset);
78
+ } else blob.writeInt32LE(0, currentOffset);
79
+ }
80
+ currentOffset += 4;
81
+ });
82
+ }
83
+ this.gateway.send(rinfo, sub.alias, [blob]);
84
+ } else if (sub.type === 'format' && sub.pattern) {
85
+ const stride = sub.factor ? sub.factor * 4 : 4;
86
+ const start = sub.start || 0;
87
+ const count = sub.count || 0;
88
+
89
+ const totalSize = (start + count) * stride;
90
+ if (isNaN(totalSize) || totalSize < 0) return;
91
+
92
+ const blob = Buffer.alloc(totalSize);
93
+ let offset = start * stride;
94
+
95
+ for (let i = start; i < start + count; i++) {
96
+ const id = i.toString().padStart(2, '0');
97
+ const target = sub.pattern!.replace(/\*\*?/, id);
98
+ let val = this.state.get(target);
99
+ if (val === undefined) val = 0;
100
+ if (typeof val === 'number') {
101
+ const node = this.schemaRegistry.getNode(target);
102
+ if (node && node.type === 'f') blob.writeFloatLE(val, offset);
103
+ else blob.writeInt32LE(val, offset);
104
+ } else blob.writeInt32LE(0, offset);
105
+ offset += stride;
106
+ }
107
+ this.gateway.send(rinfo, sub.alias, [blob]);
108
+ }
109
+ });
110
+ }
111
+
112
+ public broadcastSingleChange(path: string, value: number | string): void {
113
+ this.subscriptionManager.getSubscribers().forEach(sub => {
114
+ const match = path === sub.path || sub.path === '/xremote' || (sub.path && sub.path.includes('*') && path.startsWith(sub.path.split('*')[0]));
115
+ if (match) {
116
+ this.gateway.send({ address: sub.address, port: sub.port }, path, [value]);
117
+ }
118
+ });
119
+ }
120
+ }
@@ -0,0 +1,9 @@
1
+ import { SubscriptionManager } from '../../domain/entities/SubscriptionManager';
2
+
3
+ export class ManageSessionsUseCase {
4
+ constructor(private subscriptionManager: SubscriptionManager) {}
5
+
6
+ public cleanup(): void {
7
+ this.subscriptionManager.cleanup();
8
+ }
9
+ }
@@ -0,0 +1,26 @@
1
+ import { OscPacket, OscArgument, RemoteClient } from '../../domain/models/types';
2
+ import { OscMessageHandler } from '../../domain/services/OscMessageHandler';
3
+ import { INetworkGateway } from '../../domain/ports/INetworkGateway';
4
+
5
+ export class ProcessPacketUseCase {
6
+ constructor(
7
+ private messageHandler: OscMessageHandler,
8
+ private gateway: INetworkGateway
9
+ ) {}
10
+
11
+ public execute(packet: OscPacket, rinfo: RemoteClient): void {
12
+ if (packet.oscType === 'bundle') {
13
+ packet.elements?.forEach((el: OscPacket) => this.execute(el, rinfo));
14
+ } else if (packet.oscType === 'message') {
15
+ const args = packet.args.map((arg: OscArgument) => {
16
+ if (typeof arg === 'object' && arg !== null && 'value' in arg) return arg.value;
17
+ return arg;
18
+ });
19
+
20
+ const replies = this.messageHandler.handle({ address: packet.address, args }, rinfo);
21
+ replies.forEach(reply => {
22
+ this.gateway.send(rinfo, reply.address, reply.args);
23
+ });
24
+ }
25
+ }
26
+ }