trackwright 0.3.0__py3-none-any.whl
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.
- trackwright/__init__.py +13 -0
- trackwright/__main__.py +7 -0
- trackwright/bridge.py +323 -0
- trackwright/cli.py +203 -0
- trackwright/commands.py +397 -0
- trackwright/config.py +149 -0
- trackwright/core/__init__.py +1 -0
- trackwright/core/automation.py +410 -0
- trackwright/core/base.py +313 -0
- trackwright/core/commanding.py +300 -0
- trackwright/core/exporting.py +143 -0
- trackwright/core/importing.py +127 -0
- trackwright/core/lifecycle.py +74 -0
- trackwright/core/models.py +467 -0
- trackwright/core/policy.py +58 -0
- trackwright/core/presets.py +88 -0
- trackwright/core/project.py +221 -0
- trackwright/core/ranges.py +355 -0
- trackwright/core/reading.py +229 -0
- trackwright/core/refs.py +88 -0
- trackwright/core/saving.py +72 -0
- trackwright/core/service.py +27 -0
- trackwright/core/settings.py +80 -0
- trackwright/core/writing.py +390 -0
- trackwright/coremidi.py +307 -0
- trackwright/cubase/Trackwright_Bridge.js +1130 -0
- trackwright/cubase/__init__.py +83 -0
- trackwright/doctor.py +69 -0
- trackwright/errors.py +120 -0
- trackwright/exports.py +54 -0
- trackwright/fake.py +1112 -0
- trackwright/host.py +238 -0
- trackwright/hostport.py +74 -0
- trackwright/installer.py +305 -0
- trackwright/launcher.py +150 -0
- trackwright/logs.py +50 -0
- trackwright/ops.py +46 -0
- trackwright/protocol.py +182 -0
- trackwright/py.typed +0 -0
- trackwright/server.py +472 -0
- trackwright/transport.py +130 -0
- trackwright-0.3.0.dist-info/METADATA +190 -0
- trackwright-0.3.0.dist-info/RECORD +46 -0
- trackwright-0.3.0.dist-info/WHEEL +4 -0
- trackwright-0.3.0.dist-info/entry_points.txt +3 -0
- trackwright-0.3.0.dist-info/licenses/LICENSE +661 -0
trackwright/commands.py
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""Cubase commands bound by the script, with the behaviour observed for each one.
|
|
2
|
+
|
|
3
|
+
Commands give no result back: only ``canPerform`` (which depends on context) and whatever state
|
|
4
|
+
can be read afterwards. Each command is classified by what was **observed** in the POC
|
|
5
|
+
(phases 4 and 2b, Cubase Elements 15.0.30):
|
|
6
|
+
|
|
7
|
+
- ``direct``: acts immediately without UI (verified).
|
|
8
|
+
- ``dialog``: opens a Cubase dialog; the user must complete it (verified). Never report as done.
|
|
9
|
+
- ``confirm``: may ask the user to confirm (verified when a removed track has events).
|
|
10
|
+
- ``ui_selection``: acts on the GUI selection/focus, which the API cannot see; results are not
|
|
11
|
+
controllable from the script (verified: Paste went to another track than the one selected).
|
|
12
|
+
- ``unknown``: bound and accepted by Cubase but its effect was not observed.
|
|
13
|
+
|
|
14
|
+
The keys must match ``COMMANDS`` in ``cubase/Trackwright_Bridge.js`` (checked by a test).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import Literal
|
|
21
|
+
|
|
22
|
+
CommandClass = Literal["direct", "dialog", "confirm", "ui_selection", "unknown"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class CommandSpec:
|
|
27
|
+
key: str
|
|
28
|
+
category: str
|
|
29
|
+
name: str
|
|
30
|
+
command_class: CommandClass
|
|
31
|
+
description: str
|
|
32
|
+
evidence: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_SPECS = [
|
|
36
|
+
CommandSpec(
|
|
37
|
+
"zoom.in",
|
|
38
|
+
"Zoom",
|
|
39
|
+
"Zoom In",
|
|
40
|
+
"unknown",
|
|
41
|
+
"Zoom in the focused window",
|
|
42
|
+
"fired, no visible effect (focus-dependent)",
|
|
43
|
+
),
|
|
44
|
+
CommandSpec("zoom.out", "Zoom", "Zoom Out", "unknown", "Zoom out the focused window", "not observed"),
|
|
45
|
+
CommandSpec(
|
|
46
|
+
"edit.undo",
|
|
47
|
+
"Edit",
|
|
48
|
+
"Undo",
|
|
49
|
+
"ui_selection",
|
|
50
|
+
"Undo the last step",
|
|
51
|
+
"verified: one step per call; a paste may create two steps",
|
|
52
|
+
),
|
|
53
|
+
CommandSpec("edit.redo", "Edit", "Redo", "ui_selection", "Redo the last undone step", "not observed"),
|
|
54
|
+
CommandSpec(
|
|
55
|
+
"addtrack.audio", "AddTrack", "Audio", "dialog", "Add an audio track", "verified: opens the Add Track dialog"
|
|
56
|
+
),
|
|
57
|
+
CommandSpec(
|
|
58
|
+
"addtrack.marker",
|
|
59
|
+
"AddTrack",
|
|
60
|
+
"Marker",
|
|
61
|
+
"direct",
|
|
62
|
+
"Add a marker track",
|
|
63
|
+
"verified: created and selected directly",
|
|
64
|
+
),
|
|
65
|
+
CommandSpec(
|
|
66
|
+
"addtrack.group",
|
|
67
|
+
"AddTrack",
|
|
68
|
+
"Group Channel",
|
|
69
|
+
"dialog",
|
|
70
|
+
"Add a group channel",
|
|
71
|
+
"verified: opens the Add Track dialog",
|
|
72
|
+
),
|
|
73
|
+
CommandSpec(
|
|
74
|
+
"addtrack.folder", "AddTrack", "Folder", "dialog", "Add a folder track", "verified: opens the Add Track dialog"
|
|
75
|
+
),
|
|
76
|
+
CommandSpec(
|
|
77
|
+
"project.remove_selected_tracks",
|
|
78
|
+
"Project",
|
|
79
|
+
"Remove Selected Tracks",
|
|
80
|
+
"confirm",
|
|
81
|
+
"Remove the selected tracks",
|
|
82
|
+
"verified: may ask for confirmation when the track has events, unless the user disabled that prompt",
|
|
83
|
+
),
|
|
84
|
+
CommandSpec(
|
|
85
|
+
"project.selected_to_new_folder",
|
|
86
|
+
"Project",
|
|
87
|
+
"Folding: Tracks To Folder",
|
|
88
|
+
"unknown",
|
|
89
|
+
"Move the selected tracks into a new folder",
|
|
90
|
+
"not observed",
|
|
91
|
+
),
|
|
92
|
+
CommandSpec(
|
|
93
|
+
"project.select_track_next", "Project", "Select Track: Next", "direct", "Select the next track", "verified"
|
|
94
|
+
),
|
|
95
|
+
CommandSpec(
|
|
96
|
+
"project.select_track_prev", "Project", "Select Track: Prev", "direct", "Select the previous track", "verified"
|
|
97
|
+
),
|
|
98
|
+
CommandSpec(
|
|
99
|
+
"transport.return_to_zero",
|
|
100
|
+
"Transport",
|
|
101
|
+
"Return to Zero",
|
|
102
|
+
"unknown",
|
|
103
|
+
"Move the cursor to the project start",
|
|
104
|
+
"not observed",
|
|
105
|
+
),
|
|
106
|
+
CommandSpec(
|
|
107
|
+
"transport.locate_next_marker",
|
|
108
|
+
"Transport",
|
|
109
|
+
"Locate Next Marker",
|
|
110
|
+
"direct",
|
|
111
|
+
"Move the cursor to the next marker",
|
|
112
|
+
"verified; without a next marker it goes to the end of the last event (Cubase 15)",
|
|
113
|
+
),
|
|
114
|
+
CommandSpec(
|
|
115
|
+
"transport.locate_prev_marker",
|
|
116
|
+
"Transport",
|
|
117
|
+
"Locate Previous Marker",
|
|
118
|
+
"direct",
|
|
119
|
+
"Move the cursor to the previous marker",
|
|
120
|
+
"verified; ignores the locators; goes to the project start without markers",
|
|
121
|
+
),
|
|
122
|
+
CommandSpec(
|
|
123
|
+
"transport.set_left_locator",
|
|
124
|
+
"Transport",
|
|
125
|
+
"Set Left Locator",
|
|
126
|
+
"direct",
|
|
127
|
+
"Set the left locator at the cursor",
|
|
128
|
+
"verified: left locator at the cursor (TW-1211)",
|
|
129
|
+
),
|
|
130
|
+
CommandSpec(
|
|
131
|
+
"transport.set_right_locator",
|
|
132
|
+
"Transport",
|
|
133
|
+
"Set Right Locator",
|
|
134
|
+
"direct",
|
|
135
|
+
"Set the right locator at the cursor",
|
|
136
|
+
"verified: right locator at the cursor (TW-1211)",
|
|
137
|
+
),
|
|
138
|
+
CommandSpec(
|
|
139
|
+
"transport.locators_to_selection",
|
|
140
|
+
"Transport",
|
|
141
|
+
"Locators to Selection",
|
|
142
|
+
"unknown",
|
|
143
|
+
"Set the locators to the selection range",
|
|
144
|
+
"not observed",
|
|
145
|
+
),
|
|
146
|
+
CommandSpec(
|
|
147
|
+
"marker.insert",
|
|
148
|
+
"Marker",
|
|
149
|
+
"Insert Marker",
|
|
150
|
+
"direct",
|
|
151
|
+
"Insert a marker at the cursor",
|
|
152
|
+
"verified (needs a marker track)",
|
|
153
|
+
),
|
|
154
|
+
CommandSpec("edit.cut", "Edit", "Cut", "ui_selection", "Cut the selection", "not observed"),
|
|
155
|
+
CommandSpec(
|
|
156
|
+
"edit.copy", "Edit", "Copy", "ui_selection", "Copy the selection", "verified: acts on the GUI selection"
|
|
157
|
+
),
|
|
158
|
+
CommandSpec(
|
|
159
|
+
"edit.paste",
|
|
160
|
+
"Edit",
|
|
161
|
+
"Paste",
|
|
162
|
+
"ui_selection",
|
|
163
|
+
"Paste at the cursor",
|
|
164
|
+
"verified: pasted into another track than the one selected through the API",
|
|
165
|
+
),
|
|
166
|
+
CommandSpec("edit.delete", "Edit", "Delete", "ui_selection", "Delete the selection", "not observed"),
|
|
167
|
+
CommandSpec(
|
|
168
|
+
"edit.split_at_cursor",
|
|
169
|
+
"Edit",
|
|
170
|
+
"Split at Cursor",
|
|
171
|
+
"ui_selection",
|
|
172
|
+
"Split the selected events at the cursor",
|
|
173
|
+
"verified: split a MIDI part at the cursor",
|
|
174
|
+
),
|
|
175
|
+
CommandSpec(
|
|
176
|
+
"edit.split_loop",
|
|
177
|
+
"Edit",
|
|
178
|
+
"Split Loop",
|
|
179
|
+
"direct",
|
|
180
|
+
"Split every event crossing the locators",
|
|
181
|
+
"verified: all tracks with events in the range",
|
|
182
|
+
),
|
|
183
|
+
CommandSpec(
|
|
184
|
+
"edit.split_range", "Edit", "Split Range", "ui_selection", "Split at the range selection", "not observed"
|
|
185
|
+
),
|
|
186
|
+
CommandSpec("edit.select_all", "Edit", "Select All", "ui_selection", "Select all events", "not observed"),
|
|
187
|
+
CommandSpec("edit.select_none", "Edit", "Select None", "ui_selection", "Clear the selection", "not observed"),
|
|
188
|
+
CommandSpec(
|
|
189
|
+
"edit.select_in_loop",
|
|
190
|
+
"Edit",
|
|
191
|
+
"Select in Loop",
|
|
192
|
+
"ui_selection",
|
|
193
|
+
"Select the range between the locators",
|
|
194
|
+
"verified: with the Range tool, a range between the locators on all tracks (TW-1211)",
|
|
195
|
+
),
|
|
196
|
+
# TW-1211: range editing and event probing (experiments/tw1211)
|
|
197
|
+
CommandSpec("tool.range", "Tool", "Range Tool", "direct", "Activate the Range Selection tool", "not observed"),
|
|
198
|
+
CommandSpec("tool.select", "Tool", "Select Tool", "direct", "Activate the Object Selection tool", "not observed"),
|
|
199
|
+
CommandSpec(
|
|
200
|
+
"file.export_audio_mixdown",
|
|
201
|
+
"File",
|
|
202
|
+
"Export Audio Mixdown",
|
|
203
|
+
"dialog",
|
|
204
|
+
"Open the Export Audio Mixdown dialog",
|
|
205
|
+
"not observed (TW-1204)",
|
|
206
|
+
),
|
|
207
|
+
CommandSpec("file.save", "File", "Save", "direct", "Save the project", "verified: rewrites the .cpr (TW-1202)"),
|
|
208
|
+
CommandSpec(
|
|
209
|
+
"file.save_new_version",
|
|
210
|
+
"File",
|
|
211
|
+
"Save New Version",
|
|
212
|
+
"unknown",
|
|
213
|
+
"Save the project as a new numbered version",
|
|
214
|
+
"not observed (TW-1202)",
|
|
215
|
+
),
|
|
216
|
+
CommandSpec(
|
|
217
|
+
"file.import_audio",
|
|
218
|
+
"File",
|
|
219
|
+
"Import Audio File",
|
|
220
|
+
"dialog",
|
|
221
|
+
"Open the Import Audio File dialog",
|
|
222
|
+
"not observed (TW-1203)",
|
|
223
|
+
),
|
|
224
|
+
CommandSpec(
|
|
225
|
+
"preset.next", "Preset", "Next", "unknown", "Next preset of the focused plugin", "not observed (TW-1205)"
|
|
226
|
+
),
|
|
227
|
+
CommandSpec(
|
|
228
|
+
"preset.previous",
|
|
229
|
+
"Preset",
|
|
230
|
+
"Previous",
|
|
231
|
+
"unknown",
|
|
232
|
+
"Previous preset of the focused plugin",
|
|
233
|
+
"not observed (TW-1205)",
|
|
234
|
+
),
|
|
235
|
+
CommandSpec(
|
|
236
|
+
"edit.delete_time",
|
|
237
|
+
"Edit",
|
|
238
|
+
"Delete Time",
|
|
239
|
+
"ui_selection",
|
|
240
|
+
"Delete the range and close the gap",
|
|
241
|
+
"verified: removes the range selection and closes the gap on every track in it (TW-1211)",
|
|
242
|
+
),
|
|
243
|
+
CommandSpec(
|
|
244
|
+
"edit.cut_time",
|
|
245
|
+
"Edit",
|
|
246
|
+
"Cut Time",
|
|
247
|
+
"ui_selection",
|
|
248
|
+
"Cut the range and close the gap",
|
|
249
|
+
"verified: like Delete Time, also copies (TW-1211)",
|
|
250
|
+
),
|
|
251
|
+
CommandSpec(
|
|
252
|
+
"edit.insert_silence",
|
|
253
|
+
"Edit",
|
|
254
|
+
"Insert Silence",
|
|
255
|
+
"ui_selection",
|
|
256
|
+
"Insert silence over the range",
|
|
257
|
+
"verified: inserts silence over the range, shifting later events (TW-1211)",
|
|
258
|
+
),
|
|
259
|
+
CommandSpec(
|
|
260
|
+
"edit.crop_range",
|
|
261
|
+
"Edit",
|
|
262
|
+
"Crop Range",
|
|
263
|
+
"ui_selection",
|
|
264
|
+
"Keep only the range",
|
|
265
|
+
"verified: keeps only the range selection (TW-1211)",
|
|
266
|
+
),
|
|
267
|
+
CommandSpec(
|
|
268
|
+
"process.silence",
|
|
269
|
+
"Process",
|
|
270
|
+
"Silence",
|
|
271
|
+
"dialog",
|
|
272
|
+
"Silence the selected audio",
|
|
273
|
+
"verified: asks about audio files shared by several events (TW-1211)",
|
|
274
|
+
),
|
|
275
|
+
CommandSpec(
|
|
276
|
+
"edit.select_all_on_tracks",
|
|
277
|
+
"Edit",
|
|
278
|
+
"Select All on Tracks",
|
|
279
|
+
"ui_selection",
|
|
280
|
+
"Select all events of the selected tracks",
|
|
281
|
+
"verified: all events of the selected tracks (TW-1211)",
|
|
282
|
+
),
|
|
283
|
+
CommandSpec(
|
|
284
|
+
"edit.select_events_under_cursor",
|
|
285
|
+
"Edit",
|
|
286
|
+
"Select Events under Cursor",
|
|
287
|
+
"unknown",
|
|
288
|
+
"Select the events under the cursor",
|
|
289
|
+
"not observed",
|
|
290
|
+
),
|
|
291
|
+
CommandSpec(
|
|
292
|
+
"transport.locate_next_event",
|
|
293
|
+
"Transport",
|
|
294
|
+
"Locate Next Event",
|
|
295
|
+
"direct",
|
|
296
|
+
"Move the cursor to the next event edge",
|
|
297
|
+
"verified: next event edge (start or end) on the selected tracks (TW-1211)",
|
|
298
|
+
),
|
|
299
|
+
CommandSpec(
|
|
300
|
+
"transport.locate_prev_event",
|
|
301
|
+
"Transport",
|
|
302
|
+
"Locate Previous Event",
|
|
303
|
+
"direct",
|
|
304
|
+
"Move the cursor to the previous event edge",
|
|
305
|
+
"not observed; mirror of Locate Next Event",
|
|
306
|
+
),
|
|
307
|
+
CommandSpec(
|
|
308
|
+
"transport.locate_selection",
|
|
309
|
+
"Transport",
|
|
310
|
+
"Locate Selection",
|
|
311
|
+
"direct",
|
|
312
|
+
"Move the cursor to the selection start",
|
|
313
|
+
"verified: cursor to the selection start (TW-1211)",
|
|
314
|
+
),
|
|
315
|
+
CommandSpec(
|
|
316
|
+
"transport.locate_selection_end",
|
|
317
|
+
"Transport",
|
|
318
|
+
"Locate Selection End",
|
|
319
|
+
"direct",
|
|
320
|
+
"Move the cursor to the selection end",
|
|
321
|
+
"verified: cursor to the selection end (TW-1211)",
|
|
322
|
+
),
|
|
323
|
+
CommandSpec(
|
|
324
|
+
"transport.to_left_locator",
|
|
325
|
+
"Transport",
|
|
326
|
+
"To Left Locator",
|
|
327
|
+
"unknown",
|
|
328
|
+
"Move the cursor to the left locator",
|
|
329
|
+
"not observed",
|
|
330
|
+
),
|
|
331
|
+
CommandSpec(
|
|
332
|
+
"audio.detect_silence",
|
|
333
|
+
"Audio",
|
|
334
|
+
"Detect Silence",
|
|
335
|
+
"dialog",
|
|
336
|
+
"Open Detect Silence for the selected audio event",
|
|
337
|
+
"verified: opens its dialog; needs a selected audio event",
|
|
338
|
+
),
|
|
339
|
+
CommandSpec(
|
|
340
|
+
"automation.mode_touch",
|
|
341
|
+
"Automation",
|
|
342
|
+
"Automation Mode - Touch",
|
|
343
|
+
"unknown",
|
|
344
|
+
"Automation mode Touch: writes only while a control is touched",
|
|
345
|
+
"not observed (TW-1206)",
|
|
346
|
+
),
|
|
347
|
+
CommandSpec(
|
|
348
|
+
"automation.mode_auto_latch",
|
|
349
|
+
"Automation",
|
|
350
|
+
"Automation Mode - Auto-Latch",
|
|
351
|
+
"unknown",
|
|
352
|
+
"Automation mode Auto-Latch: writing starts on any change and lasts until stop",
|
|
353
|
+
"not observed (TW-1206)",
|
|
354
|
+
),
|
|
355
|
+
CommandSpec(
|
|
356
|
+
"automation.mode_cross_over",
|
|
357
|
+
"Automation",
|
|
358
|
+
"Automation Mode - Cross-Over",
|
|
359
|
+
"unknown",
|
|
360
|
+
"Automation mode Cross-Over: like Auto-Latch, stops when crossing existing automation",
|
|
361
|
+
"not observed (TW-1206)",
|
|
362
|
+
),
|
|
363
|
+
CommandSpec(
|
|
364
|
+
"automation.next_mode",
|
|
365
|
+
"Automation",
|
|
366
|
+
"Next Automation Mode",
|
|
367
|
+
"unknown",
|
|
368
|
+
"Switch to the next automation mode",
|
|
369
|
+
"not observed (TW-1206)",
|
|
370
|
+
),
|
|
371
|
+
CommandSpec(
|
|
372
|
+
"automation.toggle_write_selected",
|
|
373
|
+
"Automation",
|
|
374
|
+
"Toggle Write Enable Selected Tracks",
|
|
375
|
+
"unknown",
|
|
376
|
+
"Toggle automation write on the selected tracks",
|
|
377
|
+
"used by factory scripts; not observed",
|
|
378
|
+
),
|
|
379
|
+
CommandSpec(
|
|
380
|
+
"transport.start",
|
|
381
|
+
"Transport",
|
|
382
|
+
"Start",
|
|
383
|
+
"direct",
|
|
384
|
+
"Start playback (never records)",
|
|
385
|
+
"verified: the cursor advances (2026-11-13)",
|
|
386
|
+
),
|
|
387
|
+
CommandSpec(
|
|
388
|
+
"transport.stop",
|
|
389
|
+
"Transport",
|
|
390
|
+
"Stop",
|
|
391
|
+
"direct",
|
|
392
|
+
"Stop playback",
|
|
393
|
+
"verified: the cursor stops at once (2026-11-13)",
|
|
394
|
+
),
|
|
395
|
+
]
|
|
396
|
+
|
|
397
|
+
COMMANDS: dict[str, CommandSpec] = {spec.key: spec for spec in _SPECS}
|
trackwright/config.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Configuration: TOML file + environment, validated with pydantic.
|
|
2
|
+
|
|
3
|
+
Lookup order for the file:
|
|
4
|
+
1. the path in ``TRACKWRIGHT_CONFIG`` (must exist);
|
|
5
|
+
2. ``~/.config/trackwright/config.toml`` (optional).
|
|
6
|
+
|
|
7
|
+
Defaults are safe: **writes are disabled** until ``[writes] enabled = true``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import tempfile
|
|
14
|
+
import tomllib
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Literal
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
|
|
19
|
+
|
|
20
|
+
from trackwright.errors import ConfigError
|
|
21
|
+
|
|
22
|
+
CONFIG_ENV = "TRACKWRIGHT_CONFIG"
|
|
23
|
+
DEFAULT_CONFIG_PATH = Path("~/.config/trackwright/config.toml")
|
|
24
|
+
DEFAULT_SCRIPT_DIR = Path("~/Documents/Steinberg/Cubase/MIDI Remote/Driver Scripts/Local/trackwright/bridge")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def default_socket_dir() -> Path:
|
|
28
|
+
"""Private per-user directory under ``$TMPDIR`` (never inside the project: the data volume may be ``noowners``)."""
|
|
29
|
+
base = Path(os.environ.get("TMPDIR") or tempfile.gettempdir())
|
|
30
|
+
return base / f"trackwright-{os.getuid()}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _Section(BaseModel):
|
|
34
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BridgeConfig(_Section):
|
|
38
|
+
to_cubase_port: str = Field(default="Trackwright To Cubase", min_length=1)
|
|
39
|
+
"""Virtual port Python writes to; Cubase sees it as the script's MIDI input."""
|
|
40
|
+
from_cubase_port: str = Field(default="Trackwright From Cubase", min_length=1)
|
|
41
|
+
"""Virtual port Python reads from; Cubase sees it as the script's MIDI output."""
|
|
42
|
+
socket_dir: Path = Field(default_factory=default_socket_dir, validate_default=True)
|
|
43
|
+
"""Directory of the IPC Unix socket (created with mode 0700)."""
|
|
44
|
+
request_timeout: float = Field(default=3.0, ge=0.05, le=30.0)
|
|
45
|
+
"""Default timeout in seconds for one request to Cubase."""
|
|
46
|
+
write_timeout: float = Field(default=5.0, ge=0.05, le=30.0)
|
|
47
|
+
"""Timeout for write operations (they run in an idle tick and read back)."""
|
|
48
|
+
backend: Literal["auto", "coremidi", "rtmidi"] = "auto"
|
|
49
|
+
"""MIDI transport: ``coremidi`` (ctypes, nothing to compile; macOS), ``rtmidi`` (mido + python-rtmidi,
|
|
50
|
+
which must be installed: see pyproject.toml) or ``auto`` (coremidi on macOS, rtmidi elsewhere)."""
|
|
51
|
+
autostart: bool = True
|
|
52
|
+
"""``trackwright serve`` starts the bridge when it is not running (TW-1201)."""
|
|
53
|
+
autostart_timeout: float = Field(default=10.0, ge=0.5, le=60.0)
|
|
54
|
+
"""Seconds to wait for an auto-started bridge to accept connections."""
|
|
55
|
+
|
|
56
|
+
@field_validator("socket_dir", mode="after")
|
|
57
|
+
@classmethod
|
|
58
|
+
def _expand(cls, value: Path) -> Path:
|
|
59
|
+
return value.expanduser()
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def socket_path(self) -> Path:
|
|
63
|
+
return self.socket_dir / "bridge.sock"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class WritesConfig(_Section):
|
|
67
|
+
enabled: bool = False
|
|
68
|
+
"""Master switch. While false every write tool fails with ``WRITES_DISABLED`` (dry runs still work)."""
|
|
69
|
+
allowed_tracks: tuple[str, ...] = ()
|
|
70
|
+
"""Track names that may be modified. Empty means every track (when ``enabled``)."""
|
|
71
|
+
save: bool = False
|
|
72
|
+
"""Allow cubase_save_project to save the project file on disk (TW-1202). Separate from ``enabled``."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class LoggingConfig(_Section):
|
|
76
|
+
level: Literal["TRACE", "DEBUG", "INFO", "SUCCESS", "WARNING", "ERROR", "CRITICAL"] = "INFO"
|
|
77
|
+
file: Path | None = None
|
|
78
|
+
"""Optional log file (rotated at 10 MB, 5 files kept)."""
|
|
79
|
+
|
|
80
|
+
@field_validator("level", mode="before")
|
|
81
|
+
@classmethod
|
|
82
|
+
def _upper(cls, value: Any) -> Any:
|
|
83
|
+
return value.upper() if isinstance(value, str) else value
|
|
84
|
+
|
|
85
|
+
@field_validator("file", mode="after")
|
|
86
|
+
@classmethod
|
|
87
|
+
def _expand(cls, value: Path | None) -> Path | None:
|
|
88
|
+
return value.expanduser() if value is not None else None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ScriptConfig(_Section):
|
|
92
|
+
# validate_default: pydantic does not run validators on defaults otherwise, and "~" would stay literal.
|
|
93
|
+
install_dir: Path = Field(default=DEFAULT_SCRIPT_DIR, validate_default=True)
|
|
94
|
+
"""Where ``trackwright script install`` copies the MIDI Remote script."""
|
|
95
|
+
|
|
96
|
+
@field_validator("install_dir", mode="after")
|
|
97
|
+
@classmethod
|
|
98
|
+
def _expand(cls, value: Path) -> Path:
|
|
99
|
+
return value.expanduser()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class CubaseConfig(_Section):
|
|
103
|
+
app: str = Field(default="Cubase 15", min_length=1)
|
|
104
|
+
"""Application name for ``open -a`` and process name for ``pgrep -x``."""
|
|
105
|
+
launch_timeout: float = Field(default=120.0, ge=5.0, le=600.0)
|
|
106
|
+
"""Seconds cubase_launch waits for the script to answer after opening Cubase."""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Settings(_Section):
|
|
110
|
+
bridge: BridgeConfig = Field(default_factory=BridgeConfig)
|
|
111
|
+
writes: WritesConfig = Field(default_factory=WritesConfig)
|
|
112
|
+
logging: LoggingConfig = Field(default_factory=LoggingConfig)
|
|
113
|
+
script: ScriptConfig = Field(default_factory=ScriptConfig)
|
|
114
|
+
cubase: CubaseConfig = Field(default_factory=CubaseConfig)
|
|
115
|
+
source: Path | None = None
|
|
116
|
+
"""File the settings were loaded from (``None`` = built-in defaults)."""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def config_path(path: Path | None = None, *, environ: dict[str, str] | None = None) -> tuple[Path, bool]:
|
|
120
|
+
"""The configuration file to use and whether it was given explicitly (argument or ``$TRACKWRIGHT_CONFIG``)."""
|
|
121
|
+
env = os.environ if environ is None else environ
|
|
122
|
+
explicit = path or (Path(env[CONFIG_ENV]) if env.get(CONFIG_ENV) else None)
|
|
123
|
+
return (explicit or DEFAULT_CONFIG_PATH).expanduser(), explicit is not None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def load_settings(
|
|
127
|
+
path: Path | None = None, *, environ: dict[str, str] | None = None, missing_ok: bool = False
|
|
128
|
+
) -> Settings:
|
|
129
|
+
"""Load settings from ``path``, ``$TRACKWRIGHT_CONFIG`` or the default location.
|
|
130
|
+
|
|
131
|
+
A missing default file means built-in defaults; a missing explicit file is an error unless ``missing_ok``
|
|
132
|
+
(``trackwright setup`` is about to create it).
|
|
133
|
+
"""
|
|
134
|
+
candidate, explicit = config_path(path, environ=environ)
|
|
135
|
+
if not candidate.exists():
|
|
136
|
+
if explicit and not missing_ok:
|
|
137
|
+
raise ConfigError(f"configuration file not found: {candidate}", details={"path": str(candidate)})
|
|
138
|
+
return Settings()
|
|
139
|
+
try:
|
|
140
|
+
raw = tomllib.loads(candidate.read_text(encoding="utf-8"))
|
|
141
|
+
except (OSError, tomllib.TOMLDecodeError) as exc:
|
|
142
|
+
raise ConfigError(f"cannot read {candidate}: {exc}", details={"path": str(candidate)}) from exc
|
|
143
|
+
if "source" in raw:
|
|
144
|
+
raise ConfigError("'source' is not a configurable key", details={"path": str(candidate)})
|
|
145
|
+
try:
|
|
146
|
+
return Settings(**raw, source=candidate)
|
|
147
|
+
except ValidationError as exc:
|
|
148
|
+
errors = [{"loc": ".".join(str(p) for p in e["loc"]), "msg": e["msg"]} for e in exc.errors()]
|
|
149
|
+
raise ConfigError(f"invalid configuration in {candidate}", details={"errors": errors}) from exc
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Domain layer of the MCP server: references, policy, models and the Cubase service."""
|