vtskit 0.1.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.
- vtskit/__init__.py +25 -0
- vtskit/errors.py +8 -0
- vtskit/ifo.py +113 -0
- vtskit/lang.py +648 -0
- vtskit/menu.py +171 -0
- vtskit/nav.py +256 -0
- vtskit/ps.py +484 -0
- vtskit/spu.py +262 -0
- vtskit/subtitles.py +126 -0
- vtskit/titleset.py +967 -0
- vtskit/vm.py +482 -0
- vtskit/vmg.py +159 -0
- vtskit-0.1.0.dist-info/METADATA +205 -0
- vtskit-0.1.0.dist-info/RECORD +18 -0
- vtskit-0.1.0.dist-info/WHEEL +5 -0
- vtskit-0.1.0.dist-info/licenses/COPYING +674 -0
- vtskit-0.1.0.dist-info/licenses/COPYING.LESSER +165 -0
- vtskit-0.1.0.dist-info/top_level.txt +1 -0
vtskit/vm.py
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"""The DVD virtual machine's instructions, as the 8-byte commands a program
|
|
2
|
+
chain carries (pre, post and cell commands, and a button's command).
|
|
3
|
+
|
|
4
|
+
The encoding is https://dvdnav.mplayerhq.hu/dvdinfo/vmi.html (types 0 to
|
|
5
|
+
3; the combined set-compare-link types 4 to 6 are not written). Each
|
|
6
|
+
instruction here is a function returning an `Instruction`; `assemble`
|
|
7
|
+
turns a list of instructions and `If` blocks into the commands, resolving
|
|
8
|
+
the Goto lines, the way dvdauthor compiles its language (tests/test_vm.py
|
|
9
|
+
checks the bytes against dvdauthor's for each form):
|
|
10
|
+
|
|
11
|
+
* an instruction that can carry a comparison does so (the Link family and
|
|
12
|
+
the Goto with a register or a number; Jump and Call with two registers;
|
|
13
|
+
the Set families with a register or a number);
|
|
14
|
+
* otherwise `If` becomes a Goto past the body on the opposite comparison,
|
|
15
|
+
and a Goto needs a line to land on, so a NOP ends a program whose last
|
|
16
|
+
instruction is conditional;
|
|
17
|
+
* a Set (of a general register or a system register) followed by a Link
|
|
18
|
+
within the domain (LinkPGCN, LinkCN, LinkTailPGC, RSM, ...), neither
|
|
19
|
+
comparing, becomes the one "set then link" command (`g1 = 0; jump menu
|
|
20
|
+
2;` is one command in dvdauthor's output).
|
|
21
|
+
|
|
22
|
+
`Label` and `Goto` place a program's own branches (a menu's button
|
|
23
|
+
dispatcher).
|
|
24
|
+
|
|
25
|
+
Registers: `GPRM(n)` is general register n (0-15), `SPRM(n)` system
|
|
26
|
+
register n (0-23); a plain int is a number.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import struct
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
|
|
32
|
+
from .errors import VtsError
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Register(int):
|
|
36
|
+
"""A register operand; a plain int is an immediate value."""
|
|
37
|
+
def __new__(cls, code):
|
|
38
|
+
return int.__new__(cls, code)
|
|
39
|
+
|
|
40
|
+
def __repr__(self):
|
|
41
|
+
return f"SPRM({self - 0x80})" if self >= 0x80 else f"GPRM({int(self)})"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def GPRM(n):
|
|
45
|
+
if not 0 <= n <= 15:
|
|
46
|
+
raise VtsError(f"GPRM {n}: general registers are 0 to 15")
|
|
47
|
+
return Register(n)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def SPRM(n):
|
|
51
|
+
if not 0 <= n <= 23:
|
|
52
|
+
raise VtsError(f"SPRM {n}: system registers are 0 to 23")
|
|
53
|
+
return Register(0x80 + n)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
ASTN, SPSTN, AGLN, TTN, VTS_TTN, TT_PGCN, PTTN, HL_BTNN = (SPRM(n) for n in range(1, 9))
|
|
57
|
+
|
|
58
|
+
# comparisons (vmi.html, "CMP op")
|
|
59
|
+
BC, EQ, NE, GE, GT, LE, LT = range(1, 8)
|
|
60
|
+
_INVERSE = {EQ: NE, NE: EQ, GE: LT, LT: GE, GT: LE, LE: GT}
|
|
61
|
+
# set operations ("SET op")
|
|
62
|
+
MOV, SWP, ADD, SUB, MUL, DIV, MOD, RND, AND, OR, XOR = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
|
|
63
|
+
# the Link subset (operand 3 of Link, cmd 1)
|
|
64
|
+
TOP_CELL, NEXT_CELL, PREV_CELL, TOP_PG, NEXT_PG, PREV_PG = 1, 2, 3, 5, 6, 7
|
|
65
|
+
TOP_PGC, NEXT_PGC, PREV_PGC, GOUP_PGC, TAIL_PGC, RSM = 9, 10, 11, 12, 13, 16
|
|
66
|
+
# menus, for JumpSS and CallSS
|
|
67
|
+
MENU_TITLE, MENU_ROOT, MENU_SUBPICTURE, MENU_AUDIO, MENU_ANGLE, MENU_PTT = 2, 3, 4, 5, 6, 7
|
|
68
|
+
|
|
69
|
+
# user operations a program chain may forbid (dvdinfo uops.html), as bits
|
|
70
|
+
(UOP_TIME_SEARCH, UOP_PTT_SEARCH, UOP_TITLE_PLAY, UOP_STOP, UOP_GO_UP, UOP_TIME_PTT_SEARCH,
|
|
71
|
+
UOP_PREV_TOP_PG, UOP_NEXT_PG, UOP_FORWARD_SCAN, UOP_BACKWARD_SCAN, UOP_MENU_TITLE, UOP_MENU_ROOT,
|
|
72
|
+
UOP_MENU_SUBPICTURE, UOP_MENU_AUDIO, UOP_MENU_ANGLE, UOP_MENU_PTT, UOP_RESUME, UOP_BUTTON,
|
|
73
|
+
UOP_STILL_OFF, UOP_PAUSE_ON, UOP_AUDIO_CHANGE, UOP_SUBPICTURE_CHANGE, UOP_ANGLE_CHANGE,
|
|
74
|
+
UOP_KARAOKE_MIX, UOP_VIDEO_MODE) = (1 << n for n in range(25))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class Condition:
|
|
79
|
+
"""`a` (a register) compared with `b` (a register or a number)."""
|
|
80
|
+
op: int
|
|
81
|
+
a: Register
|
|
82
|
+
b: int
|
|
83
|
+
|
|
84
|
+
def __post_init__(self):
|
|
85
|
+
if self.op not in _INVERSE and self.op != BC:
|
|
86
|
+
raise VtsError(f"comparison {self.op} is not one of BC, EQ, NE, GE, GT, LE, LT")
|
|
87
|
+
if not isinstance(self.a, Register):
|
|
88
|
+
raise VtsError("the first operand of a comparison is a register")
|
|
89
|
+
if not isinstance(self.b, Register) and not 0 <= self.b <= 0xFFFF:
|
|
90
|
+
raise VtsError(f"{self.b} does not fit a 16-bit value")
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def immediate(self):
|
|
94
|
+
return not isinstance(self.b, Register)
|
|
95
|
+
|
|
96
|
+
def inverse(self):
|
|
97
|
+
if self.op == BC:
|
|
98
|
+
raise VtsError("a bitwise comparison has no opposite")
|
|
99
|
+
return Condition(_INVERSE[self.op], self.a, self.b)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def compare(a, op, b):
|
|
103
|
+
return Condition(op, a, b)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# where a type puts its comparison: (byte of cr1, byte of cr2, bytes of the
|
|
107
|
+
# immediate, immediate allowed)
|
|
108
|
+
_SLOTS = {
|
|
109
|
+
"special": (3, 5, (4, 6), True), # type 0
|
|
110
|
+
"link": (3, 5, (4, 6), True), # type 1, Direct 0
|
|
111
|
+
"jump": (6, 7, None, False), # type 1, Direct 1
|
|
112
|
+
"set": (6, 7, None, False), # type 2 (SetSystem)
|
|
113
|
+
"gprm": (2, 7, (6, 8), True), # type 3 (Set)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True)
|
|
118
|
+
class Instruction:
|
|
119
|
+
kind: str
|
|
120
|
+
body: bytes # the 8 bytes without a comparison
|
|
121
|
+
condition: object = None
|
|
122
|
+
|
|
123
|
+
def encode(self):
|
|
124
|
+
out = bytearray(self.body)
|
|
125
|
+
c = self.condition
|
|
126
|
+
if c is not None:
|
|
127
|
+
cr1, cr2, imm, allowed = _SLOTS[self.kind]
|
|
128
|
+
out[1] |= c.op << 4
|
|
129
|
+
out[cr1] = c.a
|
|
130
|
+
if c.immediate:
|
|
131
|
+
if not allowed:
|
|
132
|
+
raise VtsError(f"a {self.kind} instruction compares registers only")
|
|
133
|
+
out[1] |= 0x80
|
|
134
|
+
struct.pack_into(">H", out, imm[0], c.b)
|
|
135
|
+
else:
|
|
136
|
+
out[cr2] = c.b
|
|
137
|
+
return bytes(out)
|
|
138
|
+
|
|
139
|
+
def when(self, condition):
|
|
140
|
+
"""This instruction made conditional (VtsError if it cannot be)."""
|
|
141
|
+
if self.condition is not None:
|
|
142
|
+
raise VtsError("the instruction already has a comparison")
|
|
143
|
+
if self.kind == "set" and self.body[1] & 0x0F:
|
|
144
|
+
raise VtsError("a set instruction with a link cannot also compare")
|
|
145
|
+
if condition.immediate and not _SLOTS[self.kind][3]:
|
|
146
|
+
raise VtsError(f"a {self.kind} instruction compares registers only")
|
|
147
|
+
return Instruction(self.kind, self.body, condition)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _ins(kind, *bytes_):
|
|
151
|
+
return Instruction(kind, bytes(bytes_).ljust(8, b"\0"))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _check(name, value, low, high):
|
|
155
|
+
if not low <= value <= high:
|
|
156
|
+
raise VtsError(f"{name} {value}: {low} to {high}")
|
|
157
|
+
return value
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# --- type 0: special --------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
def nop():
|
|
163
|
+
return _ins("special", 0, 0)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def goto(line):
|
|
167
|
+
"""Go to line `line` (1-based) of the same command list."""
|
|
168
|
+
return _ins("special", 0, 1, 0, 0, 0, 0, 0, _check("line", line, 1, 255))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def break_():
|
|
172
|
+
return _ins("special", 0, 2)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# --- type 1, Direct 0: links within the domain -----------------------------
|
|
176
|
+
|
|
177
|
+
def _button(button):
|
|
178
|
+
return _check("button", button, 0, 63) << 10
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def link(subset, button=0):
|
|
182
|
+
"""LinkTopCell, LinkNextCell, ... LinkTailPGC, RSM: one of the Link
|
|
183
|
+
subset constants, with a button to highlight (0: none)."""
|
|
184
|
+
if subset not in (TOP_CELL, NEXT_CELL, PREV_CELL, TOP_PG, NEXT_PG, PREV_PG, TOP_PGC,
|
|
185
|
+
NEXT_PGC, PREV_PGC, GOUP_PGC, TAIL_PGC, RSM):
|
|
186
|
+
raise VtsError(f"{subset} is not a Link subset code")
|
|
187
|
+
return Instruction("link", struct.pack(">BBHHH", 0x20, 1, 0, 0, _button(button) | subset))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def link_pgcn(pgcn):
|
|
191
|
+
return Instruction("link", struct.pack(">BBHHH", 0x20, 4, 0, 0, _check("PGC", pgcn, 1, 32767)))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def link_pttn(pttn, button=0):
|
|
195
|
+
return Instruction("link", struct.pack(">BBHHH", 0x20, 5, 0, 0,
|
|
196
|
+
_button(button) | _check("PTT", pttn, 1, 99)))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def link_pgn(pgn, button=0):
|
|
200
|
+
return Instruction("link", struct.pack(">BBHHH", 0x20, 6, 0, 0,
|
|
201
|
+
_button(button) | _check("program", pgn, 1, 255)))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def link_cn(cn, button=0):
|
|
205
|
+
return Instruction("link", struct.pack(">BBHHH", 0x20, 7, 0, 0,
|
|
206
|
+
_button(button) | _check("cell", cn, 1, 255)))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# --- type 1, Direct 1: jumps and calls between domains ----------------------
|
|
210
|
+
|
|
211
|
+
def exit():
|
|
212
|
+
return _ins("jump", 0x30, 1)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def jump_tt(ttn):
|
|
216
|
+
"""To title `ttn` of the disc (from a menu)."""
|
|
217
|
+
return _ins("jump", 0x30, 2, 0, 0, 0, _check("title", ttn, 1, 99))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def jump_vts_tt(ttn):
|
|
221
|
+
"""To title `ttn` of this titleset."""
|
|
222
|
+
return _ins("jump", 0x30, 3, 0, 0, 0, _check("title", ttn, 1, 99))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def jump_vts_ptt(ttn, pttn):
|
|
226
|
+
"""To chapter `pttn` of title `ttn` of this titleset."""
|
|
227
|
+
return Instruction("jump", struct.pack(">BBHBBH", 0x30, 5, _check("PTT", pttn, 1, 1023), 0,
|
|
228
|
+
_check("title", ttn, 1, 99), 0))
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def jump_ss_fp():
|
|
232
|
+
"""To the first-play program chain."""
|
|
233
|
+
return _ins("jump", 0x30, 6)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def jump_ss_vmgm_menu(menu):
|
|
237
|
+
"""To the VMG's menu of this kind (MENU_TITLE)."""
|
|
238
|
+
return _ins("jump", 0x30, 6, 0, 0, 0, 0x40 | _check("menu", menu, 2, 7))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def jump_ss_vmgm_pgc(pgcn):
|
|
242
|
+
"""To program chain `pgcn` of the VMG's menus."""
|
|
243
|
+
return Instruction("jump", struct.pack(">BBHBBH", 0x30, 6, _check("PGC", pgcn, 1, 32767),
|
|
244
|
+
0, 0xC0, 0))
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def jump_ss_vtsm(vts, ttn, menu):
|
|
248
|
+
"""To the menu of kind `menu` (MENU_ROOT, ...) of titleset `vts`, for
|
|
249
|
+
its title `ttn`."""
|
|
250
|
+
return Instruction("jump", struct.pack(">BBHBBH", 0x30, 6, _check("title", ttn, 1, 99),
|
|
251
|
+
_check("titleset", vts, 1, 99),
|
|
252
|
+
0x80 | _check("menu", menu, 2, 7), 0))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _call(rsm_cell, byte5, pgcn=0):
|
|
256
|
+
return Instruction("jump", struct.pack(">BBHBBH", 0x30, 8, pgcn,
|
|
257
|
+
_check("resume cell", rsm_cell, 0, 255), byte5, 0))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def call_ss_fp(rsm_cell=1):
|
|
261
|
+
"""To the first-play program chain, to resume at cell `rsm_cell`."""
|
|
262
|
+
return _call(rsm_cell, 0)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def call_ss_vmgm_menu(menu, rsm_cell=1):
|
|
266
|
+
return _call(rsm_cell, 0x40 | _check("menu", menu, 2, 7))
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def call_ss_vtsm_menu(menu, rsm_cell=1):
|
|
270
|
+
return _call(rsm_cell, 0x80 | _check("menu", menu, 2, 7))
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def call_ss_vmgm_pgc(pgcn, rsm_cell=1):
|
|
274
|
+
return _call(rsm_cell, 0xC0, _check("PGC", pgcn, 1, 32767))
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# --- type 2: the system registers -------------------------------------------
|
|
278
|
+
|
|
279
|
+
def set_stn(audio=None, subpicture=None, angle=None):
|
|
280
|
+
"""SetSTN: the audio stream (0-7), subpicture stream (0-63, +64 hidden) and angle
|
|
281
|
+
(1-9) to play, each a number or a GPRM, those given."""
|
|
282
|
+
values = (audio, subpicture, angle)
|
|
283
|
+
if all(v is None for v in values):
|
|
284
|
+
raise VtsError("SetSTN sets at least one of audio, subpicture and angle")
|
|
285
|
+
regs = [isinstance(v, Register) for v in values if v is not None]
|
|
286
|
+
if any(regs) and not all(regs):
|
|
287
|
+
raise VtsError("SetSTN takes all registers or all numbers")
|
|
288
|
+
direct = not regs[0]
|
|
289
|
+
limits = (("audio", 0, 7), ("subpicture", 0, 127), ("angle", 1, 9)) # bit 6: do not show
|
|
290
|
+
out = bytearray(((0x50 if direct else 0x40) | 1, 0, 0, 0, 0, 0, 0, 0))
|
|
291
|
+
for k, (v, (name, low, high)) in enumerate(zip(values, limits)):
|
|
292
|
+
if v is None:
|
|
293
|
+
continue
|
|
294
|
+
if isinstance(v, Register):
|
|
295
|
+
if v >= 0x80:
|
|
296
|
+
raise VtsError("SetSTN reads general registers only")
|
|
297
|
+
out[3 + k] = 0x80 | v
|
|
298
|
+
else:
|
|
299
|
+
out[3 + k] = 0x80 | _check(name, v, low, high)
|
|
300
|
+
return Instruction("set", bytes(out))
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def set_hl_btnn(value):
|
|
304
|
+
"""SetHL_BTNN: the highlighted button, as button number * 1024, a number
|
|
305
|
+
or a GPRM."""
|
|
306
|
+
if isinstance(value, Register):
|
|
307
|
+
if value >= 0x80:
|
|
308
|
+
raise VtsError("SetHL_BTNN reads general registers only")
|
|
309
|
+
return _ins("set", 0x46, 0, 0, 0, 0, value)
|
|
310
|
+
return Instruction("set", struct.pack(">BBHHH", 0x56, 0, 0, _check("value", value, 0, 0xFFFF), 0))
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def set_nvtmr(seconds, pgcn):
|
|
314
|
+
"""SetNVTMR: the navigation timer, a number of seconds or a GPRM, and
|
|
315
|
+
the program chain to jump to when it runs out."""
|
|
316
|
+
pgcn = _check("PGC", pgcn, 1, 32767)
|
|
317
|
+
if isinstance(seconds, Register):
|
|
318
|
+
return Instruction("set", struct.pack(">BBBBHH", 0x42, 0, 0, seconds, pgcn, 0))
|
|
319
|
+
return Instruction("set", struct.pack(">BBHHH", 0x52, 0, _check("seconds", seconds, 0, 0xFFFF),
|
|
320
|
+
pgcn, 0))
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def set_gprmmd(gprm, value, counter=False):
|
|
324
|
+
"""SetGPRMMD: general register `gprm` set to a number or a register, in
|
|
325
|
+
counter mode (counting seconds) if `counter`."""
|
|
326
|
+
mode = (0x80 if counter else 0) | _check("GPRM", gprm, 0, 15)
|
|
327
|
+
if isinstance(value, Register):
|
|
328
|
+
return Instruction("set", struct.pack(">BBBBBBH", 0x43, 0, 0, value, 0, mode, 0))
|
|
329
|
+
return Instruction("set", struct.pack(">BBHBBH", 0x53, 0, _check("value", value, 0, 0xFFFF),
|
|
330
|
+
0, mode, 0))
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# --- type 3: arithmetic on the general registers ----------------------------
|
|
334
|
+
|
|
335
|
+
def set_gprm(gprm, op, value):
|
|
336
|
+
"""Set: `gprm` op= `value`, a number or a register (MOV, SWP, ADD, SUB,
|
|
337
|
+
MUL, DIV, MOD, RND, AND, OR, XOR)."""
|
|
338
|
+
_check("GPRM", gprm, 0, 15)
|
|
339
|
+
if not MOV <= op <= XOR:
|
|
340
|
+
raise VtsError(f"{op} is not a set operation")
|
|
341
|
+
if isinstance(value, Register):
|
|
342
|
+
return Instruction("gprm", struct.pack(">BBHBBH", 0x60 | op, 0, gprm, 0, value, 0))
|
|
343
|
+
return Instruction("gprm", struct.pack(">BBHHH", 0x70 | op, 0, gprm,
|
|
344
|
+
_check("value", value, 0, 0xFFFF), 0))
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# --- programs ---------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
@dataclass
|
|
350
|
+
class If:
|
|
351
|
+
"""`then` (instructions and Ifs) when `condition` holds, else `orelse`.
|
|
352
|
+
`condition` is a Condition, or a list of them that must all hold."""
|
|
353
|
+
condition: object
|
|
354
|
+
then: list
|
|
355
|
+
orelse: list = ()
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@dataclass
|
|
359
|
+
class Any:
|
|
360
|
+
"""`then` when any of the `conditions` holds, else `orelse`."""
|
|
361
|
+
conditions: list
|
|
362
|
+
then: list
|
|
363
|
+
orelse: list = ()
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@dataclass(frozen=True)
|
|
367
|
+
class Label:
|
|
368
|
+
"""A place a Goto lands on; `name` is any hashable."""
|
|
369
|
+
name: object
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@dataclass(frozen=True)
|
|
373
|
+
class Goto:
|
|
374
|
+
"""Go to the Label `name`, when `condition` holds (always if None)."""
|
|
375
|
+
name: object
|
|
376
|
+
condition: object = None
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _transfers(ins):
|
|
380
|
+
"""Whether this instruction always leaves the command list."""
|
|
381
|
+
return isinstance(ins, Instruction) and ins.condition is None and \
|
|
382
|
+
(ins.kind == "jump" or (ins.kind in ("link", "set", "gprm") and ins.body[1] & 0x0F))
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _fold(placed):
|
|
386
|
+
"""A Set followed by a Link, neither comparing and nothing landing
|
|
387
|
+
between them, as one command."""
|
|
388
|
+
out = []
|
|
389
|
+
for p in placed:
|
|
390
|
+
prev = out[-1] if out else None
|
|
391
|
+
if isinstance(p, Instruction) and p.kind == "link" and p.condition is None \
|
|
392
|
+
and isinstance(prev, Instruction) and prev.kind in ("set", "gprm") \
|
|
393
|
+
and prev.condition is None and not prev.body[1] & 0x0F:
|
|
394
|
+
body = bytearray(prev.body)
|
|
395
|
+
body[1] |= p.body[1] & 0x0F
|
|
396
|
+
body[6:8] = p.body[6:8]
|
|
397
|
+
out[-1] = Instruction(prev.kind, bytes(body))
|
|
398
|
+
else:
|
|
399
|
+
out.append(p)
|
|
400
|
+
return out
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _conditions(c):
|
|
404
|
+
return list(c) if isinstance(c, (list, tuple)) else [c]
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _flatten(items, out):
|
|
408
|
+
"""Place instructions and Ifs into `out`: [Instruction | ("goto", target
|
|
409
|
+
list id, inverse)]; returns the list."""
|
|
410
|
+
for item in items:
|
|
411
|
+
if isinstance(item, (If, Any)):
|
|
412
|
+
conds = _conditions(item.conditions if isinstance(item, Any) else item.condition)
|
|
413
|
+
body = []
|
|
414
|
+
_flatten(item.then, body)
|
|
415
|
+
orelse = []
|
|
416
|
+
_flatten(item.orelse, orelse)
|
|
417
|
+
# one plain instruction that can take the comparison: fold it in
|
|
418
|
+
if len(conds) == 1 and len(body) == 1 and not orelse and \
|
|
419
|
+
isinstance(body[0], Instruction) and body[0].condition is None:
|
|
420
|
+
try:
|
|
421
|
+
out.append(body[0].when(conds[0]))
|
|
422
|
+
continue
|
|
423
|
+
except VtsError:
|
|
424
|
+
pass
|
|
425
|
+
end = object()
|
|
426
|
+
else_start = object()
|
|
427
|
+
start = object()
|
|
428
|
+
skip = else_start if orelse else end
|
|
429
|
+
if isinstance(item, Any):
|
|
430
|
+
for c in conds[:-1]:
|
|
431
|
+
out.append(("goto", start, c))
|
|
432
|
+
out.append(("goto", skip, conds[-1].inverse()))
|
|
433
|
+
out.append(("label", start))
|
|
434
|
+
else:
|
|
435
|
+
for c in conds:
|
|
436
|
+
out.append(("goto", skip, c.inverse()))
|
|
437
|
+
out.extend(body)
|
|
438
|
+
if orelse:
|
|
439
|
+
if not (body and _transfers(body[-1])):
|
|
440
|
+
out.append(("goto", end, None))
|
|
441
|
+
out.append(("label", else_start))
|
|
442
|
+
out.extend(orelse)
|
|
443
|
+
out.append(("label", end))
|
|
444
|
+
elif isinstance(item, Instruction):
|
|
445
|
+
out.append(item)
|
|
446
|
+
elif isinstance(item, Label):
|
|
447
|
+
out.append(("label", item.name))
|
|
448
|
+
elif isinstance(item, Goto):
|
|
449
|
+
out.append(("goto", item.name, item.condition))
|
|
450
|
+
else:
|
|
451
|
+
raise VtsError(f"{item!r} is not an instruction")
|
|
452
|
+
return out
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def assemble(items):
|
|
456
|
+
"""The commands (8 bytes each) of a list of instructions and Ifs."""
|
|
457
|
+
placed = _fold(_flatten(items, []))
|
|
458
|
+
lines = {}
|
|
459
|
+
n = 0
|
|
460
|
+
for p in placed:
|
|
461
|
+
if isinstance(p, tuple) and p[0] == "label":
|
|
462
|
+
lines[p[1]] = n + 1
|
|
463
|
+
else:
|
|
464
|
+
n += 1
|
|
465
|
+
for p in placed:
|
|
466
|
+
if isinstance(p, tuple) and p[0] == "goto" and p[1] not in lines:
|
|
467
|
+
raise VtsError(f"Goto {p[1]!r}: no such Label")
|
|
468
|
+
needs_landing = any(isinstance(p, tuple) and p[0] == "goto" and lines[p[1]] == n + 1
|
|
469
|
+
for p in placed)
|
|
470
|
+
if needs_landing:
|
|
471
|
+
placed.append(nop())
|
|
472
|
+
n += 1
|
|
473
|
+
if n > 128:
|
|
474
|
+
raise VtsError(f"{n} commands (a program chain holds at most 128)")
|
|
475
|
+
out = []
|
|
476
|
+
for p in placed:
|
|
477
|
+
if isinstance(p, Instruction):
|
|
478
|
+
out.append(p.encode())
|
|
479
|
+
elif p[0] == "goto":
|
|
480
|
+
ins = goto(lines[p[1]])
|
|
481
|
+
out.append((ins.when(p[2]) if p[2] is not None else ins).encode())
|
|
482
|
+
return out
|
vtskit/vmg.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Write the video manager of a disc: VIDEO_TS.IFO and its BUP, and
|
|
2
|
+
VIDEO_TS.VOB when the disc has a title menu.
|
|
3
|
+
|
|
4
|
+
The VMG IFO (https://dvdnav.mplayerhq.hu/dvdinfo/ifo.html, ifo_vmg.html)
|
|
5
|
+
holds the VMGI_MAT header with the first-play program chain right after
|
|
6
|
+
it, TT_SRPT (every title of the disc: its titleset, number there, chapters,
|
|
7
|
+
angles, and where the titleset starts on the disc), VTS_ATRT (a copy of
|
|
8
|
+
each titleset's stream attributes) and, with menus, VMGM_PGCI_UT (one
|
|
9
|
+
language unit), VMGM_C_ADT and VMGM_VOBU_ADMAP. The parental masks and
|
|
10
|
+
the text data are absent. Each table starts at a sector; the BUP is the
|
|
11
|
+
IFO again.
|
|
12
|
+
|
|
13
|
+
From dvdauthor's output: the VMG takes its IFO, its menu VOB and its BUP
|
|
14
|
+
on the disc, 32 sectors at least, and the first titleset starts right
|
|
15
|
+
after; the first-play chain's playback time is 0 with the disc's
|
|
16
|
+
frame-rate flag, and it jumps to the title menu when there is one, else to
|
|
17
|
+
title 1; a title's type is 0x3C (one sequential program chain, jumps
|
|
18
|
+
allowed everywhere).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import struct
|
|
23
|
+
|
|
24
|
+
from . import nav, vm
|
|
25
|
+
from .errors import VtsError
|
|
26
|
+
from .titleset import SECTOR, VOB_FILE_SECTORS, _pad, menu_domain
|
|
27
|
+
|
|
28
|
+
VMG_SECTORS = 32 # the least room dvdauthor gives the VMG
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def write(folder, titlesets, menus=(), first_play=None, language="en", provider="vtskit",
|
|
32
|
+
pal=None, pan_scan=True, letterbox=True):
|
|
33
|
+
"""Write VIDEO_TS.IFO and VIDEO_TS.BUP (and VIDEO_TS.VOB) for
|
|
34
|
+
`titlesets` (the Titleset results of vtskit.titleset.write, numbered
|
|
35
|
+
1, 2, ... in order) and the VMG's `menus` (vtskit.menu.Menu; the one
|
|
36
|
+
with entry "title" answers the Title key). `first_play`: the first-play
|
|
37
|
+
chain's commands (8 bytes each; by default, as dvdauthor chooses it: a
|
|
38
|
+
jump to the title menu, else to titleset 1's root menu if that titleset
|
|
39
|
+
has menus, else to title 1). `pal`: the disc's standard, else the first
|
|
40
|
+
titleset's. Returns the sectors of the whole VIDEO_TS, VMG first."""
|
|
41
|
+
if not titlesets:
|
|
42
|
+
raise VtsError("a disc has at least one titleset")
|
|
43
|
+
if [t.vts for t in titlesets] != list(range(1, len(titlesets) + 1)):
|
|
44
|
+
raise VtsError("titlesets are numbered 1, 2, ... in order")
|
|
45
|
+
if sum(len(t.titles) for t in titlesets) > 99:
|
|
46
|
+
raise VtsError("a disc has at most 99 titles")
|
|
47
|
+
for m in menus:
|
|
48
|
+
if m.entry not in (None, "title"):
|
|
49
|
+
raise VtsError(f"a VMG menu answers the Title key or none, not {m.entry!r}")
|
|
50
|
+
if first_play is None:
|
|
51
|
+
if menus:
|
|
52
|
+
first_play = vm.assemble([vm.jump_ss_vmgm_menu(vm.MENU_TITLE)])
|
|
53
|
+
elif titlesets[0].menus:
|
|
54
|
+
first_play = vm.assemble([vm.jump_ss_vtsm(1, 1, vm.MENU_ROOT)])
|
|
55
|
+
else:
|
|
56
|
+
first_play = vm.assemble([vm.jump_tt(1)])
|
|
57
|
+
for c in first_play:
|
|
58
|
+
if len(c) != 8:
|
|
59
|
+
raise VtsError("a VM command is 8 bytes")
|
|
60
|
+
if not 1 <= len(first_play) <= 128:
|
|
61
|
+
raise VtsError("the first-play chain has 1 to 128 commands")
|
|
62
|
+
provider = provider.encode("ascii", "replace")[:32].ljust(32, b"\0")
|
|
63
|
+
if pal is None:
|
|
64
|
+
pal = titlesets[0].titles[0].stream.frame == 3600
|
|
65
|
+
|
|
66
|
+
# the first-play program chain: no cells, pre commands only
|
|
67
|
+
fp = bytearray(0xEC)
|
|
68
|
+
fp[4:8] = nav.elapsed(0, 3600 if pal else 3003)
|
|
69
|
+
struct.pack_into(">H", fp, 0xE4, 0xEC)
|
|
70
|
+
fp += struct.pack(">HHHH", len(first_play), 0, 0, 8 + 8 * len(first_play) - 1)
|
|
71
|
+
fp += b"".join(first_play)
|
|
72
|
+
|
|
73
|
+
menu_written, menu_data, menu_tables, menu_attributes = [], [], None, bytes(0x100)
|
|
74
|
+
if menus:
|
|
75
|
+
# (a video manager whose menus are commands alone: dvdauthor marks
|
|
76
|
+
# the picture format 0xFFF8)
|
|
77
|
+
menu_written, menu_data, *menu_tables, menu_attributes = menu_domain(
|
|
78
|
+
menus, language, 0, pan_scan, letterbox, 3600 if pal else 3003, b"\xff\xf8", domain="vmgm")
|
|
79
|
+
menu_sectors = sum(w.sectors for w in menu_written)
|
|
80
|
+
if menu_sectors > VOB_FILE_SECTORS:
|
|
81
|
+
raise VtsError("the VMG menus are larger than one VOB file")
|
|
82
|
+
|
|
83
|
+
# the tables, each at a sector
|
|
84
|
+
order = [("tt_srpt", None)]
|
|
85
|
+
if menus:
|
|
86
|
+
order.append(("vmgm_pgci_ut", menu_tables[0]))
|
|
87
|
+
order.append(("vts_atrt", None))
|
|
88
|
+
if menus:
|
|
89
|
+
order += [("vmgm_c_adt", menu_tables[1]), ("vmgm_admap", menu_tables[2])]
|
|
90
|
+
head = _pad(bytes(0x400) + fp)
|
|
91
|
+
ifo_sectors = len(head) // SECTOR
|
|
92
|
+
for name, t in order:
|
|
93
|
+
if t is not None:
|
|
94
|
+
ifo_sectors += len(_pad(t)) // SECTOR
|
|
95
|
+
ifo_sectors += 2 # TT_SRPT and VTS_ATRT below, a sector each (checked)
|
|
96
|
+
vmg_sectors = max(2 * ifo_sectors + menu_sectors, VMG_SECTORS)
|
|
97
|
+
|
|
98
|
+
# TT_SRPT: the titles of the disc
|
|
99
|
+
entries = b""
|
|
100
|
+
start = vmg_sectors
|
|
101
|
+
count = 0
|
|
102
|
+
for ts in titlesets:
|
|
103
|
+
for t in ts.titles:
|
|
104
|
+
count += 1
|
|
105
|
+
# (the title's type: one sequential chain, jumps everywhere, and the
|
|
106
|
+
# two search operations it forbids, as in its program chain)
|
|
107
|
+
entries += struct.pack(">BBHHBBI", 0x3C | (t.uops & 3), t.angles, len(t.programs) or 1,
|
|
108
|
+
0, ts.vts, t.number, start)
|
|
109
|
+
start += ts.sectors
|
|
110
|
+
tt_srpt = struct.pack(">HHI", count, 0, 8 + len(entries) - 1) + entries
|
|
111
|
+
|
|
112
|
+
# VTS_ATRT: each titleset's attributes
|
|
113
|
+
atrs = []
|
|
114
|
+
for ts in titlesets:
|
|
115
|
+
if len(ts.attributes) != 0x300:
|
|
116
|
+
raise VtsError(f"titleset {ts.vts}: attributes are 0x300 bytes")
|
|
117
|
+
atrs.append(struct.pack(">II", 8 + 0x300 - 1, 0) + ts.attributes)
|
|
118
|
+
offsets, o = [], 8 + 4 * len(atrs)
|
|
119
|
+
for a in atrs:
|
|
120
|
+
offsets.append(o)
|
|
121
|
+
o += len(a)
|
|
122
|
+
vts_atrt = struct.pack(">HHI", len(atrs), 0, o - 1) + \
|
|
123
|
+
struct.pack(f">{len(atrs)}I", *offsets) + b"".join(atrs)
|
|
124
|
+
|
|
125
|
+
tables = {"tt_srpt": _pad(tt_srpt), "vts_atrt": _pad(vts_atrt)}
|
|
126
|
+
if menus:
|
|
127
|
+
tables.update(vmgm_pgci_ut=_pad(menu_tables[0]), vmgm_c_adt=_pad(menu_tables[1]),
|
|
128
|
+
vmgm_admap=_pad(menu_tables[2]))
|
|
129
|
+
sectors, pointers = len(head) // SECTOR, {}
|
|
130
|
+
for name, _ in order:
|
|
131
|
+
pointers[name] = sectors
|
|
132
|
+
sectors += len(tables[name]) // SECTOR
|
|
133
|
+
if sectors != ifo_sectors:
|
|
134
|
+
raise VtsError("the title table or the attributes outgrew a sector") # (99 titles fit)
|
|
135
|
+
mat = bytearray(head)
|
|
136
|
+
mat[:12] = b"DVDVIDEO-VMG"
|
|
137
|
+
struct.pack_into(">I", mat, 0x0C, vmg_sectors - 1)
|
|
138
|
+
struct.pack_into(">I", mat, 0x1C, sectors - 1)
|
|
139
|
+
struct.pack_into(">H", mat, 0x20, 0x0011)
|
|
140
|
+
struct.pack_into(">HHB", mat, 0x26, 1, 1, 1) # one volume, this is it, side 1
|
|
141
|
+
struct.pack_into(">H", mat, 0x3E, len(titlesets))
|
|
142
|
+
mat[0x40:0x60] = provider
|
|
143
|
+
struct.pack_into(">I", mat, 0x80, 0x400 + len(fp) - 1)
|
|
144
|
+
struct.pack_into(">I", mat, 0x84, 0x400)
|
|
145
|
+
struct.pack_into(">I", mat, 0xC0, sectors if menus else 0)
|
|
146
|
+
struct.pack_into(">II", mat, 0xC4, pointers["tt_srpt"], pointers.get("vmgm_pgci_ut", 0))
|
|
147
|
+
struct.pack_into(">II", mat, 0xCC, 0, pointers["vts_atrt"])
|
|
148
|
+
struct.pack_into(">II", mat, 0xD8, pointers.get("vmgm_c_adt", 0), pointers.get("vmgm_admap", 0))
|
|
149
|
+
mat[0x100:0x200] = menu_attributes
|
|
150
|
+
ifo = bytes(mat) + b"".join(tables[name] for name, _ in order)
|
|
151
|
+
|
|
152
|
+
os.makedirs(folder, exist_ok=True)
|
|
153
|
+
for name in ("VIDEO_TS.IFO", "VIDEO_TS.BUP"):
|
|
154
|
+
with open(os.path.join(folder, name), "wb") as f:
|
|
155
|
+
f.write(ifo)
|
|
156
|
+
if menu_sectors:
|
|
157
|
+
with open(os.path.join(folder, "VIDEO_TS.VOB"), "wb") as f:
|
|
158
|
+
f.write(b"".join(menu_data))
|
|
159
|
+
return start
|