This commit is contained in:
z8
2021-12-08 19:49:06 +01:00
commit 6c9b0aea4a
4 changed files with 1821 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
# MP4 muxer
## Info
This code is only meant to work with ALAC and EAC3 (JOC) streams, for any other codecs you're on your own.
Use at your own risk. If you use this to create files and don't validate them afterwards, don't blame me if something goes wrong.
None of this code will parse MP4 files for you. If your source file is a different MP4 file (or a different format entirely),
you will have to figure out how to parse the required information (see `Usage`) yourself.
For more information, see [here](https://z8.re/blog/mp4).
## Usage
You will need to provide the following info:
- Sample rate
- Bit depth
- mdat box as a bytearray
- Sizes for all samples contained within the mdat box
- Total duration
- (optional: Tags)
Writing tags is optional, even without them the file will play back just fine. They're written to `moov/udta/meta/ilst`.
If you do not create a `Tags` object and/or do not use `set_tags()`, then the `udta` box will not be created.
Note for EAC3 JOC: This code assumes that there is only a single independed substream present and that the
values in the EAC3 specific box within `stsd` of the source file match the following (I left out the reserved bits here):
```python
fscod = 0
bsid = 16
asvc = 0
bsmod = 0
acmod = 7
lfeon = 1
num_dep_sub = 0
ec3_job_flag = 1
joc_complexity_index = 16
```
## Example
```python
from mp4muxer_alac import MP4MuxerALAC
m = MP4MuxerALAC()
m.set_sample_rate(44100)
m.set_bit_depth(16)
sample_sizes = [7424, 6915, 6830, 6737, ..., 2700, 1443]
m.set_sample_sizes(sample_sizes)
m.set_total_duration(10442880)
with open("m.dat", "rb") as f:
mdat = bytearray(f.read())
m.set_mdat_data(mdat)
t = Tags()
t.track_name = "Insert Interesting Title Here"
t.artist = "Artist X & Y"
t.album_artist = "Artist X"
t.composer = "X, Y, Z"
t.album_name = "First Track - Single"
t.genre = "Dubstep;Country"
t.track_number = 1
t.total_number_of_tracks = 1
t.disc_number = 1
t.total_number_of_discs = 1
t.date = "2020-05-22"
t.upc = "5556667778889"
t.label = "We Sell Records Rec."
with open("cover.jpg", "rb") as f:
t.cover_data = bytearray(f.read())
t.cover_format = "jpeg"
t.isrc = "CYZXL20044078"
t.copyright = "℗ 2021 We Sell Records Rec."
t.apple_store_catalog_id = 9999998
t.album_title_id = 9999999
t.playlist_id = 32423444
m.set_tags(t)
m.create()
m.out("A Piece of Music.m4a")
```
For EAC3 JOC the process is similar:
```python
from mp4muxer_eac3 import MP4MuxerEAC3
m = MP4MuxerEAC3()
m.set_sample_rate(sr)
m.set_bit_depth(bd)
m.set_bit_rate(768)
m.set_sample_sizes(sample_sizes)
m.set_timestamp(timestamp)
m.set_mdat_data(mdat)
...
```
+809
View File
@@ -0,0 +1,809 @@
from datetime import datetime
from tags import Tags
def dt_to_unix_ts(ts: datetime) -> int:
return int(ts.timestamp())
def dt_to_mp4_ts(ts: datetime) -> int:
return int(ts.timestamp()) + 2082844800
def s2b(s: str):
return bytearray(s.encode("ascii"))
def i2b(i: int, size: int = 4):
return bytearray(i.to_bytes(size, "big"))
class MP4MuxerALAC:
def __init__(self) -> None:
self.data: bytearray = bytearray()
self.timestamp: datetime = datetime.now()
self.sample_rate: int = 0
self.number_of_samples: int = 0
self.bit_depth: int = 0
self.samples_per_frame: int = 4096
self.channel_count: int = 2
self.sample_sizes: list[int] = []
# these are the only important ones
self.offsets: dict = {
"stco": 0,
"mdat": 0,
}
self.total_duration: int = 0
self.mdat_data: bytearray = bytearray()
self.tags: Tags = None
def create(self) -> None:
self.ftyp()
self.moov()
self.free()
self.mdat()
self.rewrite_stco_chunk()
def out(self, filename: str) -> None:
with open(filename, "wb") as f:
f.write(self.data)
def w(self, b: bytearray):
if isinstance(b, bytes):
b = bytearray(b)
self.data.extend(b)
def set_sample_rate(self, sr: int) -> None:
self.sample_rate = sr
def set_number_of_samples(self, nr: int) -> None:
self.number_of_samples = nr
def set_bit_depth(self, bd: int) -> None:
self.bit_depth = bd
def set_sample_sizes(self, ss: int) -> None:
self.sample_sizes = ss
def set_total_duration(self, td: int) -> None:
self.total_duration = td
def set_mdat_data(self, m: bytearray) -> None:
self.mdat_data = m
def set_tags(self, t: Tags) -> None:
self.tags = t
def set_timestamp(self, t: datetime) -> None:
self.timestamp = t
def ftyp(self) -> None:
major_brand: str = "M4A "
minor_version: int = 0
compatible_brands: list[str] = ["M4A ", "mp42", "isom"]
size: int = 16 + (len(compatible_brands) * 4)
self.w(i2b(size))
self.w(s2b("ftyp"))
self.w(s2b(major_brand))
self.w(i2b(minor_version))
for c in compatible_brands:
self.w(s2b(c))
def moov_size(self) -> int:
total_size: int = 8
total_size += self.mvhd_size()
total_size += self.trak_size()
if self.tags:
total_size += self.udta_size()
return total_size
def moov(self) -> None:
self.w(i2b(self.moov_size()))
self.w(s2b("moov"))
self.mvhd()
self.trak()
if self.tags:
self.udta()
def mvhd_size(self) -> int:
return 108
def mvhd(self) -> None:
version: int = 0
creation_time: datetime = self.timestamp
modification_time: datetime = self.timestamp
time_scale: int = self.sample_rate
duration: int = self.total_duration
rate: int = 0x10000 # 1.0
volume: int = 0x100 # 1.0
next_track_id: int = 2
self.w(i2b(self.mvhd_size()))
self.w(s2b("mvhd"))
self.w(i2b(version))
self.w(i2b(dt_to_mp4_ts(creation_time)))
self.w(i2b(dt_to_mp4_ts(modification_time)))
self.w(i2b(time_scale))
self.w(i2b(duration))
self.w(i2b(rate))
self.w(i2b(volume, 2))
# const bit(16) reserved = 0
self.w(i2b(0, 2))
# const unsigned int(32)[2] reserved = 0
self.w(i2b(0, 8))
# template int(32)[9] matrix
# { 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 }
self.w(i2b(0x10000))
self.w(i2b(0))
self.w(i2b(0))
self.w(i2b(0))
self.w(i2b(0x10000))
self.w(i2b(0))
self.w(i2b(0))
self.w(i2b(0))
self.w(i2b(0x40000000))
# Unity matrix
# bit(32)[6] pre_defined = 0
self.w(i2b(0, 24))
self.w(i2b(next_track_id))
def trak_size(self) -> int:
total_size: int = 8
total_size += self.tkhd_size()
total_size += self.mdia_size()
return total_size
def trak(self) -> None:
self.w(i2b(self.trak_size()))
self.w(s2b("trak"))
self.tkhd()
self.mdia()
def tkhd_size(self) -> int:
return 92
def tkhd(self) -> None:
flags: int = 1
creation_time: datetime = self.timestamp
modification_time: datetime = self.timestamp
track_id: int = 1
duration: int = self.total_duration
layer: int = 0
alternate_group: int = 0
volume: int = 0x100 # 1.0
width: int = 0
height: int = 0
self.w(i2b(self.tkhd_size()))
self.w(s2b("tkhd"))
self.w(i2b(flags))
self.w(i2b(dt_to_mp4_ts(creation_time)))
self.w(i2b(dt_to_mp4_ts(modification_time)))
self.w(i2b(track_id))
# const unsigned int (32) reserved = 0
self.w(i2b(0))
self.w(i2b(duration))
# reserved
self.w(i2b(0))
# const unsigned int (32) [2] reserved = 0
self.w(i2b(0))
self.w(i2b(layer, 2))
self.w(i2b(alternate_group, 2))
# template int (16) volume = {if track_is_audio 0x0100 else 0}
self.w(i2b(volume, 2))
# const unsigned int (16) reserved = 0
self.w(i2b(0, 2))
# template int (32) [9] matrix
# { 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 }
self.w(i2b(0x10000))
self.w(i2b(0))
self.w(i2b(0))
self.w(i2b(0))
self.w(i2b(0x10000))
self.w(i2b(0))
self.w(i2b(0))
self.w(i2b(0))
self.w(i2b(0x40000000))
# Unity matrix
# unsigned int (32) width
self.w(i2b(width))
# unsigned int (32) height
self.w(i2b(height))
def mdia_size(self) -> int:
total_size: int = 8
total_size += self.mdhd_size()
total_size += self.hdlr_size()
total_size += self.minf_size()
return total_size
def mdia(self) -> None:
self.w(i2b(self.mdia_size()))
self.w(s2b("mdia"))
self.mdhd()
self.hdlr()
self.minf()
def mdhd_size(self) -> int:
return 32
def mdhd(self) -> None:
version: int = 0
flags: int = 0
creation_time: datetime = self.timestamp
modification_time: datetime = self.timestamp
time_scale: int = self.sample_rate
duration: int = self.total_duration
language: int = 0x55C4 # undefined
quality: int = 0
self.w(i2b(self.mdhd_size()))
self.w(s2b("mdhd"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.w(i2b(dt_to_mp4_ts(creation_time)))
self.w(i2b(dt_to_mp4_ts(modification_time)))
self.w(i2b(time_scale))
self.w(i2b(duration))
self.w(i2b(language, 2))
self.w(i2b(quality, 2))
def hdlr_size(self) -> int:
return 32
def hdlr(self) -> None:
version: int = 0
flags: int = 0
component_type = "mhlr" # media handler
component_subtype = "soun"
component_name = 0
component_flags = 0
component_flags_mask = 0
self.w(i2b(self.hdlr_size()))
self.w(s2b("hdlr"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.w(s2b(component_type))
self.w(s2b(component_subtype))
self.w(i2b(component_name))
self.w(i2b(component_flags))
self.w(i2b(component_flags_mask))
def minf_size(self) -> int:
total_size: int = 8
total_size += self.smhd_size()
total_size += self.dinf_size()
total_size += self.stbl_size()
return total_size
def minf(self) -> None:
self.w(i2b(self.minf_size()))
self.w(s2b("minf"))
self.smhd()
self.dinf()
self.stbl()
def smhd_size(self) -> int:
return 16
def smhd(self) -> None:
version: int = 0
flags: int = 0
audio_balance: int = 0
self.w(i2b(self.smhd_size()))
self.w(s2b("smhd"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.w(i2b(audio_balance, 2))
self.w(i2b(0, 2)) # reserved
def dinf_size(self) -> int:
total_size: int = 8
total_size += self.dref_size()
return total_size
def dinf(self) -> None:
self.w(i2b(self.dinf_size()))
self.w(s2b("dinf"))
self.dref()
pass
def dref_size(self) -> int:
return 28
def dref(self) -> None:
version: int = 0
flags: int = 0
entry_count: int = 1
self.w(i2b(self.dref_size()))
self.w(s2b("dref"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.w(i2b(entry_count))
data_location_size: int = 12
data_location_name: str = "url "
data_location_version: int = 0
data_location_flags: int = 1 # same file
self.w(i2b(data_location_size))
self.w(s2b(data_location_name))
self.w(i2b(data_location_version, 1))
self.w(i2b(data_location_flags, 3))
def stbl_size(self) -> int:
total_size: int = 8
total_size += self.stsd_size()
total_size += self.stts_size()
total_size += self.stsz_size()
total_size += self.stsc_size()
total_size += self.stco_size()
return total_size
def stbl(self) -> None:
self.w(i2b(self.stbl_size()))
self.w(s2b("stbl"))
self.stsd()
self.stts()
self.stsz()
self.stsc()
self.stco()
def stsd_size(self) -> int:
return 88
def stsd(self) -> None:
version: int = 0
flags: int = 0
count: int = 1
audio_size: int = 72
audio_name: str = "alac"
channel_count: int = self.channel_count
sample_size: int = self.bit_depth
sample_rate: int = self.sample_rate
samples_per_frame: int = self.samples_per_frame
max_coded_frame_size: int = max(self.sample_sizes)
self.w(i2b(self.stsd_size()))
self.w(s2b("stsd"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.w(i2b(count))
self.w(i2b(audio_size))
self.w(s2b(audio_name))
self.w(i2b(0, 6)) # reserved
self.w(i2b(1, 2)) # data reference index
self.w(i2b(0)) # reserved
self.w(i2b(0)) # reserved
self.w(i2b(channel_count, 2))
self.w(i2b(sample_size, 2))
self.w(i2b(0, 2)) # pre-defined
self.w(i2b(0, 2)) # reserved
if sample_rate <= 65535:
self.w(i2b(sample_rate, 2))
else:
self.w(i2b(0, 2))
self.w(i2b(0, 2)) # sample rate (again? set to zero for some reason)
# magic cookie starts here
self.w(i2b(36)) # size
self.w(s2b("alac"))
self.w(i2b(0)) # reserved
self.w(i2b(samples_per_frame))
self.w(i2b(0, 1)) # reserved
self.w(i2b(sample_size, 1))
self.w(i2b(40, 1)) # rice history mult, pb, tuning parameter
self.w(i2b(10, 1)) # rice initial history, mb, tuning parameter
self.w(i2b(14, 1)) # rice kmodifier, kb, tuning parameter
self.w(i2b(channel_count, 1))
self.w(i2b(255, 2)) # maxRun, currently unused
self.w(i2b(max_coded_frame_size))
self.w(i2b(self.sample_rate * self.bit_depth * self.channel_count)) # bitrate
self.w(i2b(sample_rate))
def stts_size(self) -> int:
total_size: int = 16
# size of the stts box depends on the number of entries
number_of_entries: int = 2
if self.total_duration % self.samples_per_frame == 0:
number_of_entries = 1
total_size += 8 * number_of_entries
return total_size
def stts(self) -> None:
version: int = 0
flags: int = 0
number_of_entries: int = 2
if self.total_duration % self.samples_per_frame == 0:
number_of_entries = 1
self.w(i2b(self.stts_size()))
self.w(s2b("stts"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.w(i2b(number_of_entries))
if number_of_entries == 1:
self.w(i2b(len(self.sample_sizes)))
self.w(i2b(self.samples_per_frame))
elif number_of_entries == 2:
self.w(i2b(len(self.sample_sizes) - 1))
self.w(i2b(self.samples_per_frame))
self.w(i2b(1))
self.w(i2b(self.total_duration % self.samples_per_frame))
def stsz_size(self) -> int:
total_size: int = 20
total_size += 4 * len(self.sample_sizes)
return total_size
def stsz(self) -> None:
version: int = 0
flags: int = 0
sample_size: int = 0
sample_count: int = len(self.sample_sizes)
self.w(i2b(self.stsz_size()))
self.w(s2b("stsz"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.w(i2b(sample_size))
self.w(i2b(sample_count))
for s in self.sample_sizes:
self.w(i2b(s))
def stsc_size(self) -> int:
number_of_entries: int = 1
entries_per_second: int = int(round(self.sample_rate / 4096))
last_entry: int = len(self.sample_sizes) % entries_per_second
if last_entry != 0:
number_of_entries = 2
total_size: int = 16 + (number_of_entries * 12)
return total_size
# we only write a single chunk
# not sure what the side effects of this move are
def stsc(self) -> None:
version: int = 0
flags: int = 0
number_of_entries: int = 1
entries_per_second: int = int(round(self.sample_rate / 4096))
last_entry: int = len(self.sample_sizes) % entries_per_second
first_chunk_count = int(
(len(self.sample_sizes) - last_entry) / entries_per_second
)
entries = []
entries.append(
{
"first_chunk": 1,
"samples_per_chunk": entries_per_second,
"sample_description_index": 1,
}
)
if last_entry != 0:
number_of_entries = 2
entries.append(
{
"first_chunk": first_chunk_count + 1,
"samples_per_chunk": last_entry,
"sample_description_index": 1,
}
)
self.w(i2b(self.stsc_size()))
self.w(s2b("stsc"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.w(i2b(number_of_entries))
for s in entries:
first_chunk: int = s["first_chunk"]
samples_per_chunk: int = s["samples_per_chunk"]
sample_description_index: int = s["sample_description_index"]
self.w(i2b(first_chunk))
self.w(i2b(samples_per_chunk))
self.w(i2b(sample_description_index))
def stco_size(self) -> int:
total_size: int = 16
entries_per_second: int = int(round(self.sample_rate / 4096))
last_entry: int = len(self.sample_sizes) % entries_per_second
first_chunk_count = int(
(len(self.sample_sizes) - last_entry) / entries_per_second
)
number_of_stco_entries = first_chunk_count
if last_entry != 0:
number_of_stco_entries += 1
total_size += 4 * number_of_stco_entries
return total_size
def stco(self) -> None:
version: int = 0
flags: int = 0
self.offsets["stco"] = len(self.data)
entries_per_second: int = int(round(self.sample_rate / 4096))
last_entry: int = len(self.sample_sizes) % entries_per_second
first_chunk_count = int(
(len(self.sample_sizes) - last_entry) / entries_per_second
)
number_of_stco_entries = first_chunk_count
if last_entry != 0:
number_of_stco_entries += 1
self.w(i2b(self.stco_size()))
self.w(s2b("stco"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.w(i2b(number_of_stco_entries))
for s in range(number_of_stco_entries):
self.w(i2b(0xFFFFFFFF)) # placeholder value
def free(self) -> None:
self.w(i2b(8))
self.w(s2b("free"))
def mdat(self) -> None:
self.offsets["mdat"] = len(self.data)
self.w(i2b(8 + len(self.mdat_data)))
self.w(s2b("mdat"))
self.w(self.mdat_data)
def rewrite_stco_chunk(self) -> None:
stco_pos: int = self.offsets["stco"] + 16
first_chunk_offset: int = self.offsets["mdat"] + 8
entries_per_second: int = int(round(self.sample_rate / 4096))
last_entry: int = len(self.sample_sizes) % entries_per_second
first_chunk_count = int(
(len(self.sample_sizes) - last_entry) / entries_per_second
)
number_of_stco_entries = first_chunk_count
if last_entry != 0:
number_of_stco_entries += 1
for s in range(number_of_stco_entries):
bytes_to_write = i2b(
first_chunk_offset + (sum(self.sample_sizes[: entries_per_second * s]))
)
for index, b in enumerate(bytes_to_write):
self.data[stco_pos + (s * 4) + index] = b
def udta_size(self) -> int:
return 8 + self.meta_size()
def udta(self) -> None:
self.w(i2b(self.udta_size()))
self.w(s2b("udta"))
self.meta()
def meta_size(self) -> int:
total_size: int = 12
total_size += self.meta_hdlr_size()
total_size += self.ilst_size()
return total_size
def meta(self) -> None:
version: int = 0
flags: int = 0
self.w(i2b(self.meta_size()))
self.w(s2b("meta"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.meta_hdlr()
self.ilst()
def meta_hdlr_size(self) -> int:
return 33
def meta_hdlr(self) -> None:
version: int = 0
flags: int = 0
type_quicktime: int = 0
metadata_type: str = "mdir"
manufacturer: str = "appl"
component_reserved_flags: int = 0
component_reserved_flags_mask: int = 0
component_type_name: int = 0
self.w(i2b(self.meta_hdlr_size()))
self.w(s2b("hdlr"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.w(i2b(type_quicktime))
self.w(s2b(metadata_type))
self.w(s2b(manufacturer))
self.w(i2b(component_reserved_flags))
self.w(i2b(component_reserved_flags_mask))
self.w(i2b(component_type_name, 1))
def ilst_size(self) -> int:
total_size: int = 8
if self.tags.track_name:
total_size += 24 + len(self.tags.track_name.encode("utf-8"))
if self.tags.artist:
total_size += 24 + len(self.tags.artist.encode("utf-8"))
if self.tags.album_artist:
total_size += 24 + len(self.tags.album_artist.encode("utf-8"))
if self.tags.composer:
total_size += 24 + len(self.tags.composer.encode("utf-8"))
if self.tags.album_name:
total_size += 24 + len(self.tags.album_name.encode("utf-8"))
if self.tags.genre:
total_size += 24 + len(self.tags.genre.encode("utf-8"))
if self.tags.date:
total_size += 24 + len(self.tags.date.encode("utf-8"))
if self.tags.isrc:
total_size += 24 + len(self.tags.isrc.encode("utf-8"))
if self.tags.copyright:
total_size += 24 + len(self.tags.copyright.encode("utf-8"))
if self.tags.track_number or self.tags.total_number_of_tracks:
total_size += 32
if self.tags.disc_number or self.tags.total_number_of_discs:
total_size += 32
if self.tags.upc:
total_size += len(self.tags.upc.encode("utf-8")) + 64 + len("UPC")
if self.tags.label:
total_size += len(self.tags.label.encode("utf-8")) + 64 + len("LABEL")
if self.tags.apple_store_catalog_id:
total_size += 28
if self.tags.playlist_id:
total_size += 28
if self.tags.album_title_id:
total_size += 28
if self.tags.cover_data:
total_size += 24 + len(self.tags.cover_data)
return total_size
def write_mp4_tag_utf8(self, box: bytes, content: str):
b: bytearray = bytearray(content.encode("utf-8"))
data_size: int = len(b) + 16
self.w(i2b(data_size + 8))
self.w(box)
self.w(i2b(data_size))
self.w(s2b("data"))
# 0 = binary; 1 = utf-8
kind: int = 1
language: int = 0
self.w(i2b(kind))
self.w(i2b(language))
self.w(b)
def write_mp4_tag_int(self, box: bytes, content: int):
kind: int = 21 # signed integer
language: int = 0
size: int = 24 + len(box)
self.w(i2b(size))
self.w(box)
size -= 8
self.w(i2b(size))
self.w(s2b("data"))
self.w(i2b(kind))
self.w(i2b(language))
self.w(i2b(content))
def write_itunes_tag_utf8(self, box: str, content: str):
b: bytearray = bytearray(content.encode("utf-8"))
full_size: int = len(b) + 64 + len(box)
self.w(i2b(full_size))
self.w(s2b("----"))
mean_size: int = 28
self.w(i2b(mean_size))
self.w(s2b("mean"))
self.w(i2b(0)) # unknown
self.w(s2b("com.apple.iTunes"))
name_size: int = 12 + len(box)
self.w(i2b(name_size))
self.w(s2b("name"))
self.w(i2b(0)) # unknown
self.w(s2b(box))
data_size = len(b) + 16
self.w(i2b(data_size))
self.w(s2b("data"))
# 0 = binary; 1 = utf-8
kind: int = 1
language: int = 0
self.w(i2b(kind))
self.w(i2b(language))
self.w(b)
def write_mp4_tag_tuple_int(self, box: bytes, curr: int, total: int):
self.w(i2b(32)) # size
self.w(box)
self.w(i2b(24)) # data size
self.w(s2b("data"))
kind: int = 0 # binary
language: int = 0
self.w(i2b(kind))
self.w(i2b(language))
self.w(i2b(0, 2)) # reserved
self.w(i2b(curr, 2))
self.w(i2b(total, 2))
self.w(i2b(0, 2)) # reserved
def ilst(self) -> None:
self.w(i2b(self.ilst_size()))
self.w(s2b("ilst"))
if self.tags.track_name:
self.write_mp4_tag_utf8(b"\xA9\x6E\x61\x6D", self.tags.track_name) # ©nam
if self.tags.artist:
self.write_mp4_tag_utf8(b"\xA9\x41\x52\x54", self.tags.artist) # ©ART
if self.tags.album_artist:
self.write_mp4_tag_utf8(b"\x61\x41\x52\x54", self.tags.album_artist) # aART
if self.tags.composer:
self.write_mp4_tag_utf8(b"\xA9\x77\x72\x74", self.tags.composer) # ©wrt
if self.tags.album_name:
self.write_mp4_tag_utf8(b"\xA9\x61\x6C\x62", self.tags.album_name) # ©alb
if self.tags.genre:
self.write_mp4_tag_utf8(b"\xA9\x67\x65\x6E", self.tags.genre) # ©gen
if self.tags.date:
self.write_mp4_tag_utf8(b"\xA9\x64\x61\x79", self.tags.date) # ©day
if self.tags.isrc:
self.write_mp4_tag_utf8(b"\x49\x53\x52\x43", self.tags.isrc) # ISRC
if self.tags.copyright:
self.write_mp4_tag_utf8(b"\x63\x70\x72\x74", self.tags.copyright) # cprt
if self.tags.apple_store_catalog_id:
self.write_mp4_tag_int(
b"\x63\x6E\x49\x44", self.tags.apple_store_catalog_id
) # cnID
if self.tags.playlist_id:
self.write_mp4_tag_int(b"\x70\x6C\x49\x44", self.tags.playlist_id) # plID
if self.tags.album_title_id:
self.write_mp4_tag_int(
b"\x61\x74\x49\x44", self.tags.album_title_id
) # atID
if self.tags.upc:
self.write_itunes_tag_utf8("UPC", self.tags.upc)
if self.tags.label:
self.write_itunes_tag_utf8("LABEL", self.tags.label)
if self.tags.track_number or self.tags.total_number_of_tracks:
curr: int = 0
if self.tags.track_number:
curr = self.tags.track_number
total: int = 0
if self.tags.total_number_of_tracks:
total = self.tags.total_number_of_tracks
self.write_mp4_tag_tuple_int(b"\x74\x72\x6B\x6E", curr, total)
if self.tags.disc_number or self.tags.total_number_of_discs:
curr: int = 0
if self.tags.disc_number:
curr = self.tags.disc_number
total: int = 0
if self.tags.total_number_of_discs:
total = self.tags.total_number_of_discs
self.write_mp4_tag_tuple_int(b"\x64\x69\x73\x6B", curr, total)
if self.tags.cover_data:
if self.tags.cover_format == "jpeg":
kind: int = 13
elif self.tags.cover_format == "png":
kind: int = 14
language: int = 0
b: bytearray = self.tags.cover_data
full_size: int = len(b) + 24
self.w(i2b(full_size))
self.w(s2b("covr"))
data_size: int = full_size - 8
self.w(i2b(data_size))
self.w(s2b("data"))
self.w(i2b(kind))
self.w(i2b(language))
self.w(b)
+890
View File
@@ -0,0 +1,890 @@
from datetime import datetime
from tags import Tags
def dt_to_unix_ts(ts: datetime) -> int:
return int(ts.timestamp())
def dt_to_mp4_ts(ts: datetime) -> int:
return int(ts.timestamp()) + 2082844800
def s2b(s: str):
return bytearray(s.encode("ascii"))
def i2b(i: int, size: int = 4):
return bytearray(i.to_bytes(size, "big"))
class MP4MuxerEAC3:
def __init__(self) -> None:
self.data: bytearray = bytearray()
self.timestamp: datetime = datetime.now()
self.sample_rate: int = 0
self.number_of_samples: int = 0
self.bit_depth: int = 0
self.bit_rate: int = 0
self.sample_delta: int = 1536
self.default_sample_size: int = 3072
self.channel_count: int = 2
self.sample_sizes: list[int] = []
# these are the only important ones
self.offsets: dict = {
"stco": 0,
"mdat": 0,
}
self.mdat_data: bytearray = bytearray()
self.tags: Tags = None
def create(self) -> None:
self.ftyp()
self.moov()
self.free()
self.mdat()
self.rewrite_stco_chunk()
def out(self, filename: str) -> None:
with open(filename, "wb") as f:
f.write(self.data)
def w(self, b: bytearray):
if isinstance(b, bytes):
b = bytearray(b)
self.data.extend(b)
def set_sample_rate(self, sr: int) -> None:
self.sample_rate = sr
def set_number_of_samples(self, nr: int) -> None:
self.number_of_samples = nr
def set_bit_depth(self, bd: int) -> None:
self.bit_depth = bd
def set_bit_rate(self, br) -> None:
self.bit_rate = br
def set_sample_sizes(self, ss: int) -> None:
self.sample_sizes = ss
def set_mdat_data(self, m: bytearray) -> None:
self.mdat_data = m
def set_tags(self, t: Tags) -> None:
self.tags = t
def set_timestamp(self, t: datetime) -> None:
self.timestamp = t
def ftyp(self) -> None:
major_brand: str = "mp42"
minor_version: int = 0
compatible_brands: list[str] = ["mp42", "dby1", "isom"]
# size + box string + major brand + minor version
size: int = 16
for c in compatible_brands:
size += 4
self.w(i2b(size))
self.w(s2b("ftyp"))
self.w(s2b(major_brand))
self.w(i2b(minor_version))
for c in compatible_brands:
self.w(s2b(c))
def moov_size(self) -> int:
total_size: int = 8
total_size += self.mvhd_size()
total_size += self.trak_size()
total_size += self.iods_size()
if self.tags:
total_size += self.udta_size()
return total_size
def moov(self) -> None:
self.w(i2b(self.moov_size()))
self.w(s2b("moov"))
self.mvhd()
self.trak()
self.iods()
if self.tags:
self.udta()
def mvhd_size(self) -> int:
return 108
def mvhd(self) -> None:
version: int = 0
creation_time: datetime = self.timestamp
modification_time: datetime = self.timestamp
time_scale: int = self.sample_rate
duration: int = int(self.sample_delta * len(self.sample_sizes))
# 1.0
rate: int = 0x10000
# 1.0
volume: int = 0x100
next_track_id: int = 2
self.w(i2b(self.mvhd_size()))
self.w(s2b("mvhd"))
self.w(i2b(version))
self.w(i2b(dt_to_mp4_ts(creation_time)))
self.w(i2b(dt_to_mp4_ts(modification_time)))
self.w(i2b(time_scale))
self.w(i2b(duration))
self.w(i2b(rate))
self.w(i2b(volume, 2))
# const bit(16) reserved = 0
self.w(i2b(0, 2))
# const unsigned int(32)[2] reserved = 0
self.w(i2b(0, 8))
# template int(32)[9] matrix
# { 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 }
self.w(i2b(0x10000))
self.w(i2b(0))
self.w(i2b(0))
self.w(i2b(0))
self.w(i2b(0x10000))
self.w(i2b(0))
self.w(i2b(0))
self.w(i2b(0))
self.w(i2b(0x40000000))
# Unity matrix
# bit(32)[6] pre_defined = 0
self.w(i2b(0, 24))
self.w(i2b(next_track_id))
def trak_size(self) -> int:
total_size: int = 8
total_size += self.tkhd_size()
total_size += self.mdia_size()
return total_size
def trak(self) -> None:
self.w(i2b(self.trak_size()))
self.w(s2b("trak"))
self.tkhd()
self.mdia()
def tkhd_size(self) -> int:
return 92
def tkhd(self) -> None:
flags: int = 15
creation_time: datetime = self.timestamp
modification_time: datetime = self.timestamp
track_id: int = 1
duration: int = int(self.sample_delta * len(self.sample_sizes))
layer: int = 0
alternate_group: int = 2
# 1.0
volume: int = 0x100
width: int = 0
height: int = 0
self.w(i2b(self.tkhd_size()))
self.w(s2b("tkhd"))
self.w(i2b(flags))
self.w(i2b(dt_to_mp4_ts(creation_time)))
self.w(i2b(dt_to_mp4_ts(modification_time)))
self.w(i2b(track_id))
# const unsigned int (32) reserved = 0
self.w(i2b(0))
self.w(i2b(duration))
# reserved
self.w(i2b(0))
# const unsigned int (32) [2] reserved = 0
self.w(i2b(0))
self.w(i2b(layer, 2))
self.w(i2b(alternate_group, 2))
# template int (16) volume = {if track_is_audio 0x0100 else 0}
self.w(i2b(volume, 2))
# const unsigned int (16) reserved = 0
self.w(i2b(0, 2))
# template int (32) [9] matrix
# { 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 }
self.w(i2b(0x10000))
self.w(i2b(0))
self.w(i2b(0))
self.w(i2b(0))
self.w(i2b(0x10000))
self.w(i2b(0))
self.w(i2b(0))
self.w(i2b(0))
self.w(i2b(0x40000000))
# Unity matrix
# unsigned int (32) width
self.w(i2b(width))
# unsigned int (32) height
self.w(i2b(height))
def mdia_size(self) -> int:
total_size: int = 8
total_size += self.mdhd_size()
total_size += self.hdlr_size()
total_size += self.minf_size()
return total_size
def mdia(self) -> None:
self.w(i2b(self.mdia_size()))
self.w(s2b("mdia"))
self.mdhd()
self.hdlr()
self.minf()
def mdhd_size(self) -> int:
return 32
def mdhd(self) -> None:
version: int = 0
flags: int = 0
creation_time: datetime = self.timestamp
modification_time: datetime = self.timestamp
time_scale: int = self.sample_rate
duration: int = int(self.sample_delta * len(self.sample_sizes))
language: int = 0x55C4 # undefined
quality: int = 0
self.w(i2b(self.mdhd_size()))
self.w(s2b("mdhd"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.w(i2b(dt_to_mp4_ts(creation_time)))
self.w(i2b(dt_to_mp4_ts(modification_time)))
self.w(i2b(time_scale))
self.w(i2b(duration))
self.w(i2b(language, 2))
self.w(i2b(quality, 2))
def hdlr_size(self) -> int:
return 46
def hdlr(self) -> None:
version = 0
flags = 0
component_type = "mhlr"
component_subtype = "soun"
component_manufacturer = 0
component_flags = 0
component_flags_mask = 0
component_name = "sound handler"
self.w(i2b(self.hdlr_size()))
self.w(s2b("hdlr"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.w(s2b(component_type))
self.w(s2b(component_subtype))
self.w(i2b(component_manufacturer))
self.w(i2b(component_flags))
self.w(i2b(component_flags_mask))
self.w(s2b(component_name))
self.w(bytearray(b"\x00")) # terminating null byte
def minf_size(self) -> int:
total_size: int = 8
total_size += self.smhd_size()
total_size += self.dinf_size()
total_size += self.stbl_size()
return total_size
def minf(self) -> None:
self.w(i2b(self.minf_size()))
self.w(s2b("minf"))
self.smhd()
self.dinf()
self.stbl()
def smhd_size(self) -> int:
return 16
def smhd(self) -> None:
version: int = 0
flags: int = 0
audio_balance: int = 0
self.w(i2b(self.smhd_size()))
self.w(s2b("smhd"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.w(i2b(audio_balance, 2))
self.w(i2b(0, 2)) # reserved
def dinf_size(self) -> int:
total_size: int = 8
total_size += self.dref_size()
return total_size
def dinf(self) -> None:
self.w(i2b(self.dinf_size()))
self.w(s2b("dinf"))
self.dref()
pass
def dref_size(self) -> int:
return 28
def dref(self) -> None:
version: int = 0
flags: int = 0
entry_count: int = 1
self.w(i2b(self.dref_size()))
self.w(s2b("dref"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.w(i2b(entry_count))
data_location_size: int = 12
data_location_name: str = "url "
data_location_version: int = 0
data_location_flags: int = 1 # same file
self.w(i2b(data_location_size))
self.w(s2b(data_location_name))
self.w(i2b(data_location_version, 1))
self.w(i2b(data_location_flags, 3))
def stbl_size(self) -> int:
total_size: int = 8
total_size += self.stsd_size()
total_size += self.stts_size()
total_size += self.stsz_size()
total_size += self.stsc_size()
total_size += self.stco_size()
return total_size
def stbl(self) -> None:
self.data.extend(self.stbl_size().to_bytes(4, "big"))
self.data.extend("stbl".encode("ascii"))
self.stsd()
self.stts()
self.stsz()
self.stsc()
self.stco()
def stsd_size(self) -> int:
return 67
def stsd(self) -> None:
version: int = 0
flags: int = 0
count: int = 1
audio_size = 51
audio_name = "ec-3"
data_reference_index = 1
channel_count = 2
sample_size = self.bit_depth
sample_rate = self.sample_rate
self.w(i2b(self.stsd_size()))
self.w(s2b("stsd"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.w(i2b(count))
self.w(i2b(audio_size))
self.w(s2b(audio_name))
self.w(i2b(0, 6)) # reserved
self.w(i2b(data_reference_index, 2)) # data reference index
self.w(i2b(0)) # reserved
self.w(i2b(0)) # reserved
self.w(i2b(channel_count, 2))
self.w(i2b(sample_size, 2))
self.w(i2b(0, 2)) # pre-defined
self.w(i2b(0, 2)) # reserved
self.w(i2b(sample_rate, 2))
self.w(i2b(0, 2)) # sample rate (again? set to zero for some reason)
# EAC3 specific box
eac3_size = 15
eac3_name = "dec3"
self.data.extend(eac3_size.to_bytes(4, "big"))
self.data.extend(eac3_name.encode("ascii"))
data_rate = self.bit_rate # 13 bits
num_ind_sub = 0 # 3 bits
b = data_rate << 3
b += num_ind_sub
self.data.extend(b.to_bytes(2, "big"))
# independed substrem
fscod = 0 # 2 bits
bsid = 16 # 5 bits
reserved_bit_1 = 0 # 1 bit
asvc = 0 # 1 bit
bsmod = 0 # 3 bits
acmod = 7 # 3 bits
lfeon = 1 # 1 bit
reserved_bit_2 = 0 # 3 bits
num_dep_sub = 0 # 4 bits
reserved_bit_3 = 0 # 1 bit
b = fscod << (8 + 8 + 6)
b += bsid << (8 + 8 + 1)
b += reserved_bit_1 << (8 + 7)
b += asvc << (8 + 6)
b += bsmod << (8 + 4)
b += acmod << (8 + 1)
b += lfeon << 8
b += reserved_bit_2 << 5
b += num_dep_sub << 1
b += reserved_bit_3
self.data.extend(b.to_bytes(3, "big"))
# JOC extension
# both values 1 byte each
ec3_job_flag = 1
joc_complexity_index = 16
self.data.extend(ec3_job_flag.to_bytes(1, "big"))
self.data.extend(joc_complexity_index.to_bytes(1, "big"))
def stts_size(self) -> int:
return 24
def stts(self) -> None:
version: int = 0
flags: int = 0
number_of_entries: int = 1
sample_count: int = len(self.sample_sizes)
self.w(i2b(self.stts_size()))
self.w(s2b("stts"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.w(i2b(number_of_entries))
self.w(i2b(sample_count))
self.w(i2b(self.sample_delta))
def stsz_size(self) -> int:
total_size: int = 20
total_size += 4 * len(self.sample_sizes)
return total_size
def stsz(self) -> None:
version: int = 0
flags: int = 0
sample_size: int = self.default_sample_size
sample_count: int = len(self.sample_sizes)
self.w(i2b(self.stsz_size()))
self.w(s2b("stsz"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.w(i2b(sample_size))
self.w(i2b(sample_count))
for s in self.sample_sizes:
self.w(i2b(s))
def stsc_size(self) -> int:
number_of_entries: int = 1
entries_per_second: int = int(round(self.sample_rate / self.sample_delta))
last_entry: int = len(self.sample_sizes) % entries_per_second
if last_entry != 0:
number_of_entries = 2
total_size: int = 16 + (number_of_entries * 12)
return total_size
# last chunk is forced to 1536, so we only need one entry
def stsc(self) -> None:
version: int = 0
flags: int = 0
number_of_entries: int = 1
entries_per_second: int = int(round(self.sample_rate / self.sample_delta))
last_entry: int = len(self.sample_sizes) % entries_per_second
first_chunk_count = int(
(len(self.sample_sizes) - last_entry) / entries_per_second
)
entries = []
entries.append(
{
"first_chunk": 1,
"samples_per_chunk": entries_per_second,
"sample_description_index": 1,
}
)
if last_entry != 0:
number_of_entries = 2
entries.append(
{
"first_chunk": first_chunk_count + 1,
"samples_per_chunk": last_entry,
"sample_description_index": 1,
}
)
self.w(i2b(self.stsc_size()))
self.w(s2b("stsc"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.w(i2b(number_of_entries))
for s in entries:
first_chunk: int = s["first_chunk"]
samples_per_chunk: int = s["samples_per_chunk"]
sample_description_index: int = s["sample_description_index"]
self.w(i2b(first_chunk))
self.w(i2b(samples_per_chunk))
self.w(i2b(sample_description_index))
def stco_size(self) -> int:
total_size: int = 16
entries_per_second: int = int(round(self.sample_rate / self.sample_delta))
last_entry: int = len(self.sample_sizes) % entries_per_second
first_chunk_count = int(
(len(self.sample_sizes) - last_entry) / entries_per_second
)
number_of_stco_entries = first_chunk_count
if last_entry != 0:
number_of_stco_entries += 1
total_size += 4 * number_of_stco_entries
return total_size
def stco(self) -> None:
version: int = 0
flags: int = 0
self.offsets["stco"] = len(self.data)
entries_per_second: int = int(round(self.sample_rate / self.sample_delta))
last_entry: int = len(self.sample_sizes) % entries_per_second
first_chunk_count = int(
(len(self.sample_sizes) - last_entry) / entries_per_second
)
number_of_stco_entries = first_chunk_count
if last_entry != 0:
number_of_stco_entries += 1
self.w(i2b(self.stco_size()))
self.w(s2b("stco"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.w(i2b(number_of_stco_entries))
for s in range(number_of_stco_entries):
self.w(i2b(0xFFFFFFFF)) # placeholder value
def free(self) -> None:
self.w(i2b(8))
self.w(s2b("free"))
def mdat(self) -> None:
self.offsets["mdat"] = len(self.data)
self.w(i2b(8 + len(self.mdat_data)))
self.w(s2b("mdat"))
self.w(self.mdat_data)
def rewrite_stco_chunk(self) -> None:
stco_pos: int = self.offsets["stco"] + 16
first_chunk_offset: int = self.offsets["mdat"] + 8
entries_per_second: int = int(round(self.sample_rate / self.sample_delta))
last_entry: int = len(self.sample_sizes) % entries_per_second
first_chunk_count = int(
(len(self.sample_sizes) - last_entry) / entries_per_second
)
number_of_stco_entries = first_chunk_count
if last_entry != 0:
number_of_stco_entries += 1
for s in range(number_of_stco_entries):
bytes_to_write = i2b(
first_chunk_offset + (sum(self.sample_sizes[: entries_per_second * s]))
)
for index, b in enumerate(bytes_to_write):
self.data[stco_pos + (s * 4) + index] = b
def udta_size(self) -> int:
return 8 + self.meta_size()
def udta(self) -> None:
self.w(i2b(self.udta_size()))
self.w(s2b("udta"))
self.meta()
def meta_size(self) -> int:
total_size: int = 12
total_size += self.meta_hdlr_size()
total_size += self.ilst_size()
return total_size
def meta(self) -> None:
version: int = 0
flags: int = 0
self.w(i2b(self.meta_size()))
self.w(s2b("meta"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.meta_hdlr()
self.ilst()
def meta_hdlr_size(self) -> int:
return 33
def meta_hdlr(self) -> None:
version: int = 0
flags: int = 0
type_quicktime: int = 0
metadata_type: str = "mdir"
manufacturer: str = "appl"
component_reserved_flags: int = 0
component_reserved_flags_mask: int = 0
component_type_name: int = 0
self.w(i2b(self.meta_hdlr_size()))
self.w(s2b("hdlr"))
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
self.w(i2b(type_quicktime))
self.w(s2b(metadata_type))
self.w(s2b(manufacturer))
self.w(i2b(component_reserved_flags))
self.w(i2b(component_reserved_flags_mask))
self.w(i2b(component_type_name, 1))
def ilst_size(self) -> int:
total_size: int = 8
if self.tags.track_name:
total_size += 24 + len(self.tags.track_name.encode("utf-8"))
if self.tags.artist:
total_size += 24 + len(self.tags.artist.encode("utf-8"))
if self.tags.album_artist:
total_size += 24 + len(self.tags.album_artist.encode("utf-8"))
if self.tags.composer:
total_size += 24 + len(self.tags.composer.encode("utf-8"))
if self.tags.album_name:
total_size += 24 + len(self.tags.album_name.encode("utf-8"))
if self.tags.genre:
total_size += 24 + len(self.tags.genre.encode("utf-8"))
if self.tags.date:
total_size += 24 + len(self.tags.date.encode("utf-8"))
if self.tags.isrc:
total_size += 24 + len(self.tags.isrc.encode("utf-8"))
if self.tags.copyright:
total_size += 24 + len(self.tags.copyright.encode("utf-8"))
if self.tags.track_number or self.tags.total_number_of_tracks:
total_size += 32
if self.tags.disc_number or self.tags.total_number_of_discs:
total_size += 32
if self.tags.upc:
total_size += len(self.tags.upc.encode("utf-8")) + 64 + len("UPC")
if self.tags.label:
total_size += len(self.tags.label.encode("utf-8")) + 64 + len("LABEL")
if self.tags.apple_store_catalog_id:
total_size += 28
if self.tags.playlist_id:
total_size += 28
if self.tags.album_title_id:
total_size += 28
if self.tags.cover_data:
total_size += 24 + len(self.tags.cover_data)
return total_size
def write_mp4_tag_utf8(self, box: bytes, content: str):
b: bytearray = bytearray(content.encode("utf-8"))
data_size: int = len(b) + 16
self.w(i2b(data_size + 8))
self.w(box)
self.w(i2b(data_size))
self.w(s2b("data"))
# 0 = binary; 1 = utf-8
kind: int = 1
language: int = 0
self.w(i2b(kind))
self.w(i2b(language))
self.w(b)
def write_mp4_tag_int(self, box: bytes, content: int):
kind: int = 21 # signed integer
language: int = 0
size: int = 24 + len(box)
self.w(i2b(size))
self.w(box)
size -= 8
self.w(i2b(size))
self.w(s2b("data"))
self.w(i2b(kind))
self.w(i2b(language))
self.w(i2b(content))
def write_itunes_tag_utf8(self, box: str, content: str):
b: bytearray = bytearray(content.encode("utf-8"))
full_size: int = len(b) + 64 + len(box)
self.w(i2b(full_size))
self.w(s2b("----"))
mean_size: int = 28
self.w(i2b(mean_size))
self.w(s2b("mean"))
self.w(i2b(0)) # unknown
self.w(s2b("com.apple.iTunes"))
name_size: int = 12 + len(box)
self.w(i2b(name_size))
self.w(s2b("name"))
self.w(i2b(0)) # unknown
self.w(s2b(box))
data_size = len(b) + 16
self.w(i2b(data_size))
self.w(s2b("data"))
# 0 = binary; 1 = utf-8
kind: int = 1
language: int = 0
self.w(i2b(kind))
self.w(i2b(language))
self.w(b)
def write_mp4_tag_tuple_int(self, box: bytes, curr: int, total: int):
self.w(i2b(32)) # size
self.w(box)
self.w(i2b(24)) # data size
self.w(s2b("data"))
kind: int = 0 # binary
language: int = 0
self.w(i2b(kind))
self.w(i2b(language))
self.w(i2b(0, 2)) # reserved
self.w(i2b(curr, 2))
self.w(i2b(total, 2))
self.w(i2b(0, 2)) # reserved
def ilst(self) -> None:
self.w(i2b(self.ilst_size()))
self.w(s2b("ilst"))
if self.tags.track_name:
self.write_mp4_tag_utf8(b"\xA9\x6E\x61\x6D", self.tags.track_name) # ©nam
if self.tags.artist:
self.write_mp4_tag_utf8(b"\xA9\x41\x52\x54", self.tags.artist) # ©ART
if self.tags.album_artist:
self.write_mp4_tag_utf8(b"\x61\x41\x52\x54", self.tags.album_artist) # aART
if self.tags.composer:
self.write_mp4_tag_utf8(b"\xA9\x77\x72\x74", self.tags.composer) # ©wrt
if self.tags.album_name:
self.write_mp4_tag_utf8(b"\xA9\x61\x6C\x62", self.tags.album_name) # ©alb
if self.tags.genre:
self.write_mp4_tag_utf8(b"\xA9\x67\x65\x6E", self.tags.genre) # ©gen
if self.tags.date:
self.write_mp4_tag_utf8(b"\xA9\x64\x61\x79", self.tags.date) # ©day
if self.tags.isrc:
self.write_mp4_tag_utf8(b"\x49\x53\x52\x43", self.tags.isrc) # ISRC
if self.tags.copyright:
self.write_mp4_tag_utf8(b"\x63\x70\x72\x74", self.tags.copyright) # cprt
if self.tags.apple_store_catalog_id:
self.write_mp4_tag_int(
b"\x63\x6E\x49\x44", self.tags.apple_store_catalog_id
) # cnID
if self.tags.playlist_id:
self.write_mp4_tag_int(b"\x70\x6C\x49\x44", self.tags.playlist_id) # plID
if self.tags.album_title_id:
self.write_mp4_tag_int(
b"\x61\x74\x49\x44", self.tags.album_title_id
) # atID
if self.tags.upc:
self.write_itunes_tag_utf8("UPC", self.tags.upc)
if self.tags.label:
self.write_itunes_tag_utf8("LABEL", self.tags.label)
if self.tags.track_number or self.tags.total_number_of_tracks:
curr: int = 0
if self.tags.track_number:
curr = self.tags.track_number
total: int = 0
if self.tags.total_number_of_tracks:
total = self.tags.total_number_of_tracks
self.write_mp4_tag_tuple_int(b"\x74\x72\x6B\x6E", curr, total)
if self.tags.disc_number or self.tags.total_number_of_discs:
curr: int = 0
if self.tags.disc_number:
curr = self.tags.disc_number
total: int = 0
if self.tags.total_number_of_discs:
total = self.tags.total_number_of_discs
self.write_mp4_tag_tuple_int(b"\x64\x69\x73\x6B", curr, total)
if self.tags.cover_data:
if self.tags.cover_format == "jpeg":
kind: int = 13
elif self.tags.cover_format == "png":
kind: int = 14
language: int = 0
b: bytearray = self.tags.cover_data
full_size: int = len(b) + 24
self.w(i2b(full_size))
self.w(s2b("covr"))
data_size: int = full_size - 8
self.w(i2b(data_size))
self.w(s2b("data"))
self.w(i2b(kind))
self.w(i2b(language))
self.w(b)
def iods_size(self) -> int:
return 27
def iods(self) -> None:
self.w(i2b(self.iods_size()))
self.w(s2b("iods"))
version: int = 0
flags: int = 0
self.w(i2b(version, 1))
self.w(i2b(flags, 3))
# MP4_IOD_Tag
# header
iod_tag_type: int = 16
iod_tag_size: int = 13
object_descriptor_id: int = 1 # 10 bits
url_flag: int = 0 # 1 bit
include_inline_profile_level_flag: int = 0 # 1 bit
reserved_bits: int = 15 # 4 bits
od_profile_level_indication: int = 255
scene_profile_level_indication: int = 255
audio_profile_level_indication: int = 255
visual_profile_level_indication: int = 255
graphics_profile_level_indication: int = 255
self.w(i2b(iod_tag_type, 1))
self.w(i2b(iod_tag_size, 1))
b: int = object_descriptor_id << 6
b += url_flag << 5
b += include_inline_profile_level_flag << 4
b += reserved_bits
self.w(i2b(b, 2))
self.w(i2b(od_profile_level_indication, 1))
self.w(i2b(scene_profile_level_indication, 1))
self.w(i2b(audio_profile_level_indication, 1))
self.w(i2b(visual_profile_level_indication, 1))
self.w(i2b(graphics_profile_level_indication, 1))
# ES_ID_IncTag
es_id_type: int = 14
es_id_size: int = 4
es_id_track_id: int = 1
self.w(i2b(es_id_type, 1))
self.w(i2b(es_id_size, 1))
self.w(i2b(es_id_track_id, 4))
+22
View File
@@ -0,0 +1,22 @@
class Tags:
def __init__(self):
self.track_name: str = None
self.artist: str = None
self.album_name: str = None
self.album_artist: str = None
self.track_number: int = None
self.total_number_of_tracks: int = None
self.disc_number: int = None
self.total_number_of_discs: int = None
self.genre: str = None
self.composer: str = None
self.isrc: str = None
self.copyright: str = None
self.label: str = None
self.upc: str = None
self.apple_store_catalog_id: int = None
self.album_title_id: int = None
self.playlist_id: int = None
self.date: str = None
self.cover_data: bytearray = None
self.cover_format: str = None