Add code
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
# CAF muxer
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
Only works for ALAC, for a more information see [here](https://z8.re/blog/caf).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Information needed from the source file:
|
||||||
|
|
||||||
|
- Sample rate
|
||||||
|
- Bit depth
|
||||||
|
- Sizes of the individual samples
|
||||||
|
- mdat chunk as a single bytearray
|
||||||
|
- Total duration (exact number of raw audio samples)
|
||||||
|
|
||||||
|
For examples see `examples.py`.
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
class CAF:
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
self.data: bytearray = bytearray()
|
||||||
|
self.input_data: bytearray = None
|
||||||
|
self.mdat_content: bytearray = None
|
||||||
|
self.magic_cookie: bytearray = bytearray()
|
||||||
|
self.number_of_packets: int = 0
|
||||||
|
self.number_of_valid_frames: int = 0
|
||||||
|
self.sample_sizes: list[int] = []
|
||||||
|
self.sample_rate: int = 0
|
||||||
|
self.bit_depth: int = 0
|
||||||
|
self.old_cookie: bool = False
|
||||||
|
|
||||||
|
def load_input_data(self, b: bytearray) -> None:
|
||||||
|
self.input_data = b
|
||||||
|
|
||||||
|
def load_mdat_data(self, b: bytearray) -> None:
|
||||||
|
self.mdat_content = b
|
||||||
|
|
||||||
|
def write_old_cookie(self, magic_cookie: bytes) -> None:
|
||||||
|
self.old_cookie = True
|
||||||
|
self.magic_cookie = magic_cookie
|
||||||
|
|
||||||
|
def encode_vlq(self, values: list[int]) -> list[int]:
|
||||||
|
encoded = []
|
||||||
|
for value in values:
|
||||||
|
bl = int.bit_length(value)
|
||||||
|
length = len(encoded)
|
||||||
|
encoded.append(value & 127)
|
||||||
|
while bl > 7: # value keeps going
|
||||||
|
value >>= 7
|
||||||
|
bl -= 7
|
||||||
|
encoded.insert(length, (value & 127) + 128)
|
||||||
|
return encoded
|
||||||
|
|
||||||
|
def decode_vlq(self, values: list[int]) -> list[int]:
|
||||||
|
decoded = []
|
||||||
|
summed = 0
|
||||||
|
for value in values:
|
||||||
|
summed += value & 127
|
||||||
|
if value >= 128:
|
||||||
|
summed <<= 7
|
||||||
|
else:
|
||||||
|
decoded.append(summed)
|
||||||
|
summed = 0
|
||||||
|
if summed > 0 or not decoded:
|
||||||
|
raise ValueError
|
||||||
|
return decoded
|
||||||
|
|
||||||
|
def bytes_to_int(self, bytes: bytearray) -> int:
|
||||||
|
result = 0
|
||||||
|
for b in bytes:
|
||||||
|
result = result * 256 + int(b)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def read_data_ahead(self, b: bytearray, position: int, offset_ahead: int) -> int:
|
||||||
|
return self.bytes_to_int(b[position - offset_ahead : position])
|
||||||
|
|
||||||
|
def find_box(self, b: bytes, box_name: bytes) -> list[int]:
|
||||||
|
results: list[int] = []
|
||||||
|
ret: int = 0
|
||||||
|
pos: int = 0
|
||||||
|
while ret != -1:
|
||||||
|
ret = (b[pos:]).find(box_name)
|
||||||
|
if ret != -1:
|
||||||
|
results.append(pos + ret)
|
||||||
|
size_of_box = self.read_data_ahead(b, pos + ret, 4)
|
||||||
|
pos = pos + ret + size_of_box
|
||||||
|
return results
|
||||||
|
|
||||||
|
def get_atoms_of_bytes(self, b: bytes) -> list[dict]:
|
||||||
|
atoms: list[dict] = []
|
||||||
|
offset: int = 0
|
||||||
|
while offset < len(b):
|
||||||
|
size: int = self.bytes_to_int(b[offset : offset + 4])
|
||||||
|
name: bytes = b[offset + 4 : offset + 8]
|
||||||
|
atoms.append({"offset": offset + 4, "name": name})
|
||||||
|
offset += size
|
||||||
|
return atoms
|
||||||
|
|
||||||
|
def get_box_data_by_path(self, path: str) -> bytes:
|
||||||
|
boxes: list[str] = path.split("/")
|
||||||
|
b = bytearray(self.input_data)
|
||||||
|
total_offset = 0
|
||||||
|
for box in boxes:
|
||||||
|
all_atoms = self.get_atoms_of_bytes(b)
|
||||||
|
for atom in all_atoms:
|
||||||
|
if box.encode("ascii") == atom["name"]:
|
||||||
|
total_offset += atom["offset"]
|
||||||
|
size = self.read_data_ahead(b, atom["offset"], 4)
|
||||||
|
b = b[atom["offset"] + 4 : atom["offset"] + size + 4]
|
||||||
|
total_offset += 4
|
||||||
|
break
|
||||||
|
return b
|
||||||
|
|
||||||
|
def load_magic_cookie(self):
|
||||||
|
path_to_atom = "moov/trak/mdia/minf/stbl/stsd"
|
||||||
|
stsd_data = self.get_box_data_by_path(path_to_atom)
|
||||||
|
# starting bytes of the ALAC magic cookie
|
||||||
|
offset = stsd_data.find(b"\x00\x00\x00\x24\x61\x6C\x61\x63")
|
||||||
|
self.magic_cookie = stsd_data[offset : offset + 36]
|
||||||
|
|
||||||
|
def write(self, path):
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(self.data)
|
||||||
|
|
||||||
|
def create_file(self):
|
||||||
|
# write file header
|
||||||
|
# write 'caff' string
|
||||||
|
self.data += "caff".encode("ascii")
|
||||||
|
# write file version
|
||||||
|
self.data += int(1).to_bytes(2, "big")
|
||||||
|
# write file flags
|
||||||
|
self.data += int(0).to_bytes(2, "big")
|
||||||
|
|
||||||
|
# write desc
|
||||||
|
# write 'desc' string
|
||||||
|
self.data += "desc".encode("ascii")
|
||||||
|
# write 8 bytes containing 0x20 chunk size
|
||||||
|
self.data += int(32).to_bytes(8, "big")
|
||||||
|
# write sample rate
|
||||||
|
self.data += bytearray(struct.pack(">d", self.sample_rate))
|
||||||
|
# write format id string
|
||||||
|
self.data += "alac".encode("ascii")
|
||||||
|
# write format flags
|
||||||
|
self.data += int(0).to_bytes(4, "big")
|
||||||
|
# write bytes per packet
|
||||||
|
self.data += int(0).to_bytes(4, "big")
|
||||||
|
# write frames per packet
|
||||||
|
self.data += int(4096).to_bytes(4, "big")
|
||||||
|
# write channels per frame
|
||||||
|
self.data += int(2).to_bytes(4, "big")
|
||||||
|
# write bits per channel
|
||||||
|
self.data += int(self.bit_depth).to_bytes(4, "big")
|
||||||
|
|
||||||
|
# write chan
|
||||||
|
# write 'chan' string
|
||||||
|
self.data += "chan".encode("ascii")
|
||||||
|
# write 8 bytes containing 0xC chunk size
|
||||||
|
self.data += int(12).to_bytes(8, "big")
|
||||||
|
# write 4 bytes for mChannelLayoutTag
|
||||||
|
# in our case we just want regular stereo,
|
||||||
|
# which is defined as 101 << 16 | 2
|
||||||
|
self.data += int(101 << 16 | 2).to_bytes(4, "big")
|
||||||
|
# write 4 bytes for mChannelBitmap
|
||||||
|
# leaving this at 0 seems fine
|
||||||
|
self.data += int(0).to_bytes(4, "big")
|
||||||
|
# write 4 bytes for mNumberChannelDescriptions
|
||||||
|
# 0 means we get to skip the CAFChannelDescription
|
||||||
|
self.data += int(0).to_bytes(4, "big")
|
||||||
|
|
||||||
|
if self.write_old_cookie:
|
||||||
|
# write kuki
|
||||||
|
# write 'kuki' string
|
||||||
|
self.data += "kuki".encode("ascii")
|
||||||
|
# write 8 bytes containing 0x30 chunk size
|
||||||
|
self.data += int(48).to_bytes(8, "big")
|
||||||
|
# write 4 bytes containing 0xC format descriptor size
|
||||||
|
self.data += int(12).to_bytes(4, "big")
|
||||||
|
# write 'frma' string
|
||||||
|
self.data += "frma".encode("ascii")
|
||||||
|
# write 'alac' string
|
||||||
|
self.data += "alac".encode("ascii")
|
||||||
|
# write alac magic cookie, 36 bytes long,
|
||||||
|
# starts with 00 00 00 24 61 6C 61 63
|
||||||
|
self.data += self.magic_cookie
|
||||||
|
else:
|
||||||
|
self.data += "kuki".encode("ascii")
|
||||||
|
# size
|
||||||
|
self.data += int(24).to_bytes(8, "big")
|
||||||
|
self.data += int(4096).to_bytes(4, "big")
|
||||||
|
self.data += int(0).to_bytes(1, "big")
|
||||||
|
self.data += int(24).to_bytes(1, "big")
|
||||||
|
self.data += int(40).to_bytes(1, "big")
|
||||||
|
self.data += int(10).to_bytes(1, "big")
|
||||||
|
self.data += int(14).to_bytes(1, "big")
|
||||||
|
# number of channels
|
||||||
|
self.data += int(2).to_bytes(1, "big")
|
||||||
|
self.data += int(255).to_bytes(2, "big")
|
||||||
|
self.data += int(0).to_bytes(4, "big")
|
||||||
|
self.data += int(0).to_bytes(4, "big")
|
||||||
|
self.data += int(self.sample_rate).to_bytes(4, "big")
|
||||||
|
|
||||||
|
# optional:
|
||||||
|
# write info chunk
|
||||||
|
# contains a whole bunch of info about encoder
|
||||||
|
|
||||||
|
# write data
|
||||||
|
# write 'data' string
|
||||||
|
self.data += "data".encode("ascii")
|
||||||
|
# write 8 bytes containing the size of mdat and edit count
|
||||||
|
self.data += int(len(self.mdat_content) + 4).to_bytes(8, "big")
|
||||||
|
# write edit count
|
||||||
|
self.data += int(0).to_bytes(4, "big")
|
||||||
|
# write mdat content
|
||||||
|
self.data += self.mdat_content
|
||||||
|
|
||||||
|
# write pakt
|
||||||
|
# write 'pakt' string
|
||||||
|
self.data += "pakt".encode("ascii")
|
||||||
|
# write 8 bytes containing chunk size
|
||||||
|
encoded_value_pairs = self.encode_vlq(self.sample_sizes)
|
||||||
|
value_pair_bytes = bytearray()
|
||||||
|
for v in encoded_value_pairs:
|
||||||
|
value_pair_bytes += int(v).to_bytes(1, "big")
|
||||||
|
# size needs to be calulated beforehand
|
||||||
|
pakt_size = len(value_pair_bytes) + 8 + 8 + 4 + 4
|
||||||
|
self.data += int(pakt_size).to_bytes(8, "big")
|
||||||
|
# write mNumberPackets
|
||||||
|
self.data += int(self.number_of_packets).to_bytes(8, "big")
|
||||||
|
# write mNumberValidFrames
|
||||||
|
self.data += int(self.number_of_valid_frames).to_bytes(8, "big")
|
||||||
|
# write mPrimingFrames (set to zero in ALAC)
|
||||||
|
self.data += int(0).to_bytes(4, "big")
|
||||||
|
# write mRemainderFrames (also set to zero in ALAC)
|
||||||
|
self.data += int(0).to_bytes(4, "big")
|
||||||
|
# create the list of value pairs
|
||||||
|
self.data += value_pair_bytes
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
from caf import CAF
|
||||||
|
|
||||||
|
# Note:
|
||||||
|
# The mdat chunk can also be loaded from the
|
||||||
|
# input file directly by using get_box_data_by_path()
|
||||||
|
# and specifying "mdat" as the path
|
||||||
|
|
||||||
|
# Example data
|
||||||
|
input_file_name = "input.mp4"
|
||||||
|
sample_rate = 44100
|
||||||
|
bit_depth = 16
|
||||||
|
total_duration = 1206787933
|
||||||
|
sample_sizes = [
|
||||||
|
5756, 5811, 5724, 5757, 5721, 5694, 5629, 5916,
|
||||||
|
5682, 5697, 5675, 5937, 5914, 5899, 5723, 5948,
|
||||||
|
5799, 5780, 5639, 5810, 5826, 5789, 5550, 5468,
|
||||||
|
5371, 5476, 5699, 6783, 6406, 6284, 6149, 5696,
|
||||||
|
5808, 5441, 5685, 5765, 6057, 5817, 5999, 6532,
|
||||||
|
5784, 5562, 5495, 5936, 5849, 6581, 7660, 7961,
|
||||||
|
7745, 7576, 8637, 8983, 7434, 6829, 7058, 8073,
|
||||||
|
8050, 7178, 7103, 7683, 8523, 8708, 8712, 8730,
|
||||||
|
8874, 8882, 8383, 8016, 8091, 7575, 8164, 7426,
|
||||||
|
7186, 7247, 7970, 7773, 7454, 7324, 7216, 7366
|
||||||
|
]
|
||||||
|
|
||||||
|
# Example 1
|
||||||
|
# Without old magic cookie
|
||||||
|
|
||||||
|
c = CAF()
|
||||||
|
with open("mdat.dat", "rb") as f:
|
||||||
|
c.load_mdat_data(bytearray(f.read()))
|
||||||
|
c.sample_rate = sample_rate
|
||||||
|
c.bit_depth = 16
|
||||||
|
c.number_of_valid_frames = total_duration
|
||||||
|
c.number_of_packets = len(sample_sizes)
|
||||||
|
c.sample_sizes = sample_sizes
|
||||||
|
c.create_file()
|
||||||
|
c.write("output.caf")
|
||||||
|
|
||||||
|
# Example 2
|
||||||
|
# With magic old cookie
|
||||||
|
|
||||||
|
c = CAF()
|
||||||
|
with open("mdat.dat", "rb") as f:
|
||||||
|
c.load_mdat_data(bytearray(f.read()))
|
||||||
|
with open(input_file_name, "rb") as f:
|
||||||
|
c.load_input_data(bytearray(f.read()))
|
||||||
|
c.old_cookie = True
|
||||||
|
c.load_magic_cookie()
|
||||||
|
c.bit_depth = 16
|
||||||
|
c.number_of_valid_frames = total_duration
|
||||||
|
c.number_of_packets = len(sample_sizes)
|
||||||
|
c.sample_sizes = sample_sizes
|
||||||
|
c.create_file()
|
||||||
|
c.write("output.caf")
|
||||||
Reference in New Issue
Block a user