Skip to content

Commit c7bae1b

Browse files
committed
Add initial support for USM alpha chunks
1 parent 1d4929b commit c7bae1b

File tree

6 files changed

+141
-337
lines changed

6 files changed

+141
-337
lines changed

CHANGELOG.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7-
## Unreleased
7+
## [0.2.5] - 2022-04-08
88
### Added
99
- New operation in command-line called `encryptusm` which encrypts an existing USM file.
10+
- Support for new USM Chunk `@ALP` which is used for alpha transparency videos. Currently, supports probe and extraction operations only. Thank you to [EmirioBomb](https://github.com/EmirioBomb) for bringing this to my attention and providing sample files.
11+
12+
### Changed
13+
- Renamed Usm constructor parameters.
1014

1115
### Removed
1216
- Check for chunk header in `chunk_size_and_padding` function
@@ -49,7 +53,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4953
- Fixed bug in extractusm that causes it to fail when output directory doesn't exist.
5054
- Fixed bug where program fails when directory exists.
5155

52-
[Unreleased]: https://github.com/donmai-me/WannaCRI/compare/0.2.4...HEAD
56+
[Unreleased]: https://github.com/donmai-me/WannaCRI/compare/0.2.5...HEAD
57+
[0.2.5]: https://github.com/donmai-me/WannaCRI/compare/0.2.4...0.2.5
5358
[0.2.4]: https://github.com/donmai-me/WannaCRI/compare/0.2.3...0.2.4
5459
[0.2.3]: https://github.com/donmai-me/WannaCRI/compare/0.2.2...0.2.3
5560
[0.2.2]: https://github.com/donmai-me/WannaCRI/compare/0.2.1...0.2.2

wannacri/usm/media/protocols.py

+18-243
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,21 @@ def metadata_pages(self, pages: Optional[List[UsmPage]]):
5252
def channel_number(self) -> int:
5353
return self._channel_number
5454

55+
@channel_number.setter
56+
def channel_number(self, new_channel_number: int):
57+
if new_channel_number < 0:
58+
raise ValueError(f"Given negative channel number: {new_channel_number}")
59+
60+
self._channel_number = new_channel_number
61+
5562
@property
5663
def header_page(self) -> UsmPage:
5764
return self._header_page
5865

66+
@header_page.setter
67+
def header_page(self, new_header_page: UsmPage):
68+
self._header_page = new_header_page
69+
5970
@property
6071
def filename(self) -> str:
6172
"""A slugified filename, which is stored inside the crid_page.
@@ -68,6 +79,12 @@ def filename(self) -> str:
6879
result: str = filename.val.split("/")[-1]
6980
return slugify(result, allow_unicode=True)
7081

82+
@filename.setter
83+
def filename(self, new_filename: str):
84+
new_filename = slugify(new_filename, allow_unicode=True)
85+
self.crid_page["filename"].type = ElementType.STRING
86+
self.crid_page["filename"].val = new_filename
87+
7188
def __len__(self) -> int:
7289
"""The number of packets a Usm videos or audios has."""
7390
return self._length
@@ -89,6 +106,7 @@ class UsmVideo(UsmMedia, Protocol):
89106
# Classes that explicitly inherit UsmVideo should have this attribute
90107
# to use the default stream and chunks methods.
91108
_stream: Generator[Tuple[bytes, bool], None, None]
109+
is_alpha: bool
92110

93111
def stream(
94112
self, mode: OpMode = OpMode.NONE, key: Optional[bytes] = None
@@ -317,246 +335,3 @@ def chunks(
317335
channel_number=self.channel_number,
318336
),
319337
]
320-
321-
322-
# TODO: Delete the comments when done rewriting
323-
324-
# class VP9:
325-
# def __init__(self, filepath: str):
326-
# self.filename = os.path.basename(filepath)
327-
# self.filesize = os.path.getsize(filepath)
328-
# self.info = ffmpeg.probe(filepath, show_entries="packet=dts,pts_time,pos,flags")
329-
330-
# if len(self.info.get("streams")) == 0:
331-
# raise Exception("No streams found")
332-
# elif len(self.info.get("streams")) > 1:
333-
# raise NotImplementedError("Can only accept one stream")
334-
# elif self.info.get("format").get("format_name") != "ivf":
335-
# raise Exception("Not an ivf file")
336-
# elif self.info.get("streams")[0].get("codec_name") != "vp9":
337-
# raise Exception("Not a VP9 videos")
338-
339-
# video_stream = self.info.get("streams")[0]
340-
# self.bitrate = int(self.info.get("format").get("bit_rate"))
341-
# self.width = int(video_stream.get("width"))
342-
# self.height = int(video_stream.get("height"))
343-
# self.framerate_n = int(video_stream.get("r_frame_rate").split("/")[0])
344-
# self.framerate_d = int(video_stream.get("r_frame_rate").split("/")[1])
345-
346-
# self.frames = self.info.get("packets")
347-
# self.keyframes = [frame for frame in self.frames if frame.get("flags") == "K_"]
348-
# self.total_frames = len(self.frames)
349-
# self.file = open(filepath, "rb")
350-
351-
# def export(
352-
# self, encrypt: bool, key: Optional[int] = None, encoding: str = "UTF-8"
353-
# ) -> bytes:
354-
# if encrypt:
355-
# video_key1, video_key2, _ = generate_keys(key)
356-
357-
# debug = open("FRAME_TIME_DEBUG", "w+")
358-
# stream_chunks = bytearray()
359-
# keyframe_usm_offsets = []
360-
# max_frame_size = 0
361-
# max_packed_frame_size = 0
362-
# max_keyframe_to_keyframe_size = 0
363-
# current_keyframe_to_keyframe_size = 0
364-
# self.file.seek(0, 0)
365-
# for i, frame in enumerate(self.frames):
366-
# # frame_time formula is based on existing usm files.
367-
# # TODO: Does this hold up for videos that's not 30fps?
368-
# debug.write("Frame {}: time = {}".format(i, frame.get("pts_time")))
369-
# # frame_time = int(99.86891 * i)
370-
# frame_time = int(i * 99.9)
371-
# if frame in self.keyframes:
372-
# keyframe_usm_offsets.append(len(stream_chunks))
373-
374-
# if i == len(self.frames) - 1:
375-
# # Last frame
376-
# frame_size = self.filesize - int(frame.get("pos"))
377-
# else:
378-
# frame_size = int(self.frames[i + 1].get("pos")) - int(frame.get("pos"))
379-
380-
# if i == 0:
381-
# # Include 32 byte header for first frame
382-
# max_frame_size = frame_size + 32
383-
# packet = self.file.read(frame_size + 32)
384-
# else:
385-
# if frame_size > max_frame_size:
386-
# max_frame_size = frame_size
387-
388-
# packet = self.file.read(frame_size)
389-
390-
# if frame.get("flags") != "K_":
391-
# current_keyframe_to_keyframe_size += frame_size
392-
# elif current_keyframe_to_keyframe_size > max_keyframe_to_keyframe_size:
393-
# max_keyframe_to_keyframe_size = current_keyframe_to_keyframe_size
394-
# current_keyframe_to_keyframe_size = frame_size
395-
396-
# if encrypt:
397-
# packet = encrypt_video_packet(packet, video_key1, video_key2)
398-
399-
# padding_size = 0x20 - (len(packet) % 0x20) if len(packet) % 0x20 != 0 else 0
400-
# packed_frame_chunk = generate_packed_chunk(
401-
# "@SFV",
402-
# 0,
403-
# int(100 * self.framerate_n / self.framerate_d),
404-
# frame_time,
405-
# packet,
406-
# padding_size,
407-
# )
408-
# if len(packed_frame_chunk) > max_packed_frame_size:
409-
# max_packed_frame_size = len(packed_frame_chunk)
410-
411-
# stream_chunks += packed_frame_chunk
412-
413-
# stream_chunks += generate_packed_chunk(
414-
# "@SFV",
415-
# 2,
416-
# int(self.framerate_n / self.framerate_d),
417-
# 0,
418-
# bytes("#CONTENTS END ===============", "UTF-8") + bytes(1),
419-
# 0,
420-
# )
421-
422-
# seek_chunks = bytearray()
423-
# keyframe_pages = []
424-
# video_header_end_offset = get_video_header_end_offset(len(self.keyframes))
425-
# # Offset of videos header end offset plus length of videos header end
426-
# stream_offset = video_header_end_offset + 0x40
427-
# for i, keyframe in enumerate(self.keyframes):
428-
# keyframe_usm_offset = keyframe_usm_offsets[i]
429-
# keyframe_offset = stream_offset + keyframe_usm_offset
430-
# keyframe_page = UsmPage("VIDEO_SEEKINFO", i)
431-
# keyframe_page.add("ofs_byte", ElementType.clonglong, keyframe_offset)
432-
# keyframe_page.add("ofs_frmid", ElementType.cuint, keyframe.get("dts"))
433-
# keyframe_page.add("num_skip", ElementType.cushort, 0)
434-
# keyframe_page.add("resv", ElementType.cushort, 0)
435-
# keyframe_pages.append(keyframe_page)
436-
437-
# seek_chunks_payload = pack_pages(keyframe_pages, string_padding=1)
438-
# # 0x20 bytes for chunk header.
439-
# seek_chunks_padding = (
440-
# video_header_end_offset - 0xA40 - 0x20 - len(seek_chunks_payload)
441-
# )
442-
# seek_chunks += generate_packed_chunk(
443-
# "@SFV",
444-
# 3,
445-
# int(self.framerate_n / self.framerate_d),
446-
# 0,
447-
# seek_chunks_payload,
448-
# seek_chunks_padding,
449-
# )
450-
# metadata_size = len(seek_chunks)
451-
# seek_chunks += generate_packed_chunk(
452-
# "@SFV",
453-
# 2,
454-
# int(self.framerate_n / self.framerate_d),
455-
# 0,
456-
# bytes("#METADATA END ===============", "UTF-8") + bytes(1),
457-
# 0,
458-
# )
459-
460-
# header_page = UsmPage("VIDEO_HDRINFO", 0)
461-
# header_page.add("width", ElementType.cint, self.width)
462-
# header_page.add("height", ElementType.cint, self.height)
463-
# header_page.add("mat_width", ElementType.cint, self.width)
464-
# header_page.add("mat_height", ElementType.cint, self.height)
465-
# header_page.add("disp_width", ElementType.cint, self.width)
466-
# header_page.add("disp_height", ElementType.cint, self.height)
467-
# header_page.add("scrn_width", ElementType.cint, 0)
468-
# header_page.add("mpeg_dcprec", ElementType.cchar, 0)
469-
# header_page.add("mpeg_codec", ElementType.cchar, 9)
470-
# # TODO: Check if videos has transparency
471-
# header_page.add("alpha_type", ElementType.cint, 0)
472-
# header_page.add("total_frames", ElementType.cint, self.total_frames)
473-
474-
# framerate_n = self.framerate_n
475-
# framerate_d = self.framerate_d
476-
477-
# if framerate_d < 1000 and framerate_d != 1000:
478-
# framerate_d *= 1000
479-
# framerate_n *= 1000
480-
481-
# header_page.add("framerate_n", ElementType.cint, framerate_n)
482-
# header_page.add("framerate_d", ElementType.cint, framerate_d)
483-
# header_page.add("metadata_count", ElementType.cint, 1)
484-
# header_page.add("metadata_size", ElementType.cint, metadata_size)
485-
# header_page.add("ixsize", ElementType.cint, max_packed_frame_size)
486-
# header_page.add("pre_padding", ElementType.cint, 0)
487-
# header_page.add("max_picture_size", ElementType.cint, 0)
488-
# header_page.add("color_space", ElementType.cint, 0)
489-
# header_page.add("picture_type", ElementType.cint, 0)
490-
491-
# header_chunk_payload = pack_pages([header_page])
492-
# header_chunk_padding = 0xA00 - 0x800 - 0x20 - len(header_chunk_payload)
493-
# header_chunk = bytearray()
494-
# header_chunk += generate_packed_chunk(
495-
# "@SFV",
496-
# 1,
497-
# int(self.framerate_n / self.framerate_d),
498-
# 0,
499-
# header_chunk_payload,
500-
# header_chunk_padding,
501-
# )
502-
# header_chunk += generate_packed_chunk(
503-
# "@SFV",
504-
# 2,
505-
# int(self.framerate_n / self.framerate_d),
506-
# 0,
507-
# bytes("#HEADER END ===============", "UTF-8") + bytes(1),
508-
# 0,
509-
# )
510-
511-
# directory_pages = []
512-
# for i in range(0, 2):
513-
# if i == 0:
514-
# filename = os.path.splitext(self.filename)[0] + ".usm"
515-
# # filename = r"I:\000125 千本桜\000125.usm"
516-
# filesize = (
517-
# 0x800 + len(header_chunk) + len(seek_chunks) + len(stream_chunks)
518-
# )
519-
# stmid = 0
520-
# chno = -1
521-
# minchk = 1
522-
# # TODO: Find formula for minbuf for usm
523-
# minbuf = round(1.98746 * max_frame_size)
524-
# minbuf += 0x10 - (minbuf % 0x10) if minbuf % 0x10 != 0 else 0
525-
# else:
526-
# filename = self.filename
527-
# # filename = r"I:\000125 千本桜\000125.ivf"
528-
529-
# filesize = self.filesize
530-
# stmid = 1079199318 # @SFV
531-
# chno = 0
532-
# minchk = 3
533-
# minbuf = max_frame_size
534-
535-
# directory_part = UsmPage("CRIUSF_DIR_STREAM", 0)
536-
# directory_part.add("fmtver", ElementType.cint, 16777984)
537-
# directory_part.add("filename", ElementType.cstring, filename)
538-
# directory_part.add("filesize", ElementType.cint, filesize)
539-
# directory_part.add("datasize", ElementType.cint, 0)
540-
# directory_part.add("stmid", ElementType.cint, stmid)
541-
# directory_part.add("chno", ElementType.cshort, chno)
542-
# directory_part.add("minchk", ElementType.cshort, minchk)
543-
# directory_part.add("minbuf", ElementType.cint, minbuf)
544-
# directory_part.add("avbps", ElementType.cint, self.bitrate)
545-
# directory_pages.append(directory_part)
546-
547-
# directory_chunk_payload = pack_pages(directory_pages, encoding, 5)
548-
# directory_chunk_padding = 0x800 - 0x20 - len(directory_chunk_payload)
549-
# directory_chunk = bytearray()
550-
# directory_chunk += generate_packed_chunk(
551-
# "CRID",
552-
# 1,
553-
# int(self.framerate_n / self.framerate_d),
554-
# 0,
555-
# directory_chunk_payload,
556-
# directory_chunk_padding,
557-
# )
558-
# result = directory_chunk + header_chunk + seek_chunks + stream_chunks
559-
# return bytes(result)
560-
561-
# def close(self):
562-
# self.file.close()

wannacri/usm/media/video.py

+2
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ def __init__(
2121
length: int,
2222
channel_number: int = 0,
2323
metadata_pages: Optional[List[UsmPage]] = None,
24+
is_alpha: bool = False,
2425
):
2526
self._stream = stream
2627
self._crid_page = crid_page
2728
self._header_page = header_page
2829
self._length = length
2930
self._channel_number = channel_number
3031
self._metadata_pages = metadata_pages
32+
self.is_alpha = is_alpha
3133

3234

3335
class Vp9(UsmVideo):

wannacri/usm/tools.py

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def is_valid_chunk(signature: bytes) -> bool:
3939
bytes("CRID", "UTF-8"), # CRI USF DIR STREAM
4040
bytes("@SFV", "UTF-8"), # Video
4141
bytes("@SFA", "UTF-8"), # Audio
42+
bytes("@ALP", "UTF-8")
4243
]
4344
return signature[:4] in valid_signatures
4445

wannacri/usm/types.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class ChunkType(Enum):
88
INFO = bytearray("CRID", "UTF-8")
99
VIDEO = bytearray("@SFV", "UTF-8")
1010
AUDIO = bytearray("@SFA", "UTF-8")
11+
ALPHA = bytearray("@ALP", "UTF-8")
1112

1213
@staticmethod
1314
def from_bytes(data: bytes) -> ChunkType:
@@ -19,6 +20,9 @@ def from_bytes(data: bytes) -> ChunkType:
1920

2021
raise ValueError(f"Unknown chunk signature: {bytes_to_hex(data[:4])}")
2122

23+
def __str__(self):
24+
return str(self.value, "UTF-8")
25+
2226

2327
class PayloadType(Enum):
2428
STREAM = 0
@@ -61,7 +65,7 @@ class ElementType(Enum):
6165
ULONGLONG = 0x17 # 8 bytes
6266
FLOAT = 0x18 # 4 bytes
6367
# TODO: Confirm DOUBLE's existence
64-
# DOUBLE = 0x19 # 8 bytes
68+
# DOUBLE = 0x19 # 8 bytes
6569
STRING = 0x1A # Null byte terminated
6670
BYTES = 0x1B # Bytes
6771

0 commit comments

Comments
 (0)