129 lines
3.4 KiB
Python
129 lines
3.4 KiB
Python
from math import modf # fuck me rounding is hard
|
|
|
|
# header map
|
|
# https://wiki.multimedia.cx/index.php/CRI_ADX_file
|
|
|
|
MAGIC_OFF = 0x00
|
|
MAGIC_LEN = 2
|
|
MAGIC = 0x8000
|
|
|
|
DATA_OFF_OFF = 0x02
|
|
DATA_OFF_LEN = 2
|
|
DATA_OFF_MIN = 0x38
|
|
|
|
FORMAT_OFF = 0x04
|
|
FORMAT_LEN = 1
|
|
FORMAT = 3 # always 3 for adx apparently
|
|
|
|
LOOP_TYPE_OFF = 0x12
|
|
LOOP_TYPE_LEN = 1
|
|
LOOP_TYPE_SUP = [4] # supported loop styles
|
|
|
|
SRATE_OFF = 0x08
|
|
SRATE_LEN = 4
|
|
|
|
SCOUNT_OFF = 0x0C
|
|
SCOUNT_LEN = 4
|
|
|
|
CRYPT_OFF = 0x13
|
|
CRYPT_LEN = 1
|
|
|
|
LOOP4_FLAG_OFF = 0x24
|
|
LOOP4_FLAG_LEN = 4
|
|
|
|
LOOP4_START_OFF = 0x28
|
|
LOOP4_START_LEN = 4
|
|
|
|
LOOP4_END_OFF = 0x30
|
|
LOOP4_END_LEN = 4
|
|
|
|
|
|
# class independent functions
|
|
|
|
|
|
def dumb_round(number):
|
|
dec = modf(number)[0]
|
|
if dec <= 0.5:
|
|
return int(number)
|
|
else:
|
|
return int(number + 1)
|
|
|
|
|
|
class adx_file(object):
|
|
magic = 0
|
|
form = 0
|
|
l_style = 0
|
|
encrypted = 0
|
|
sam_rate = 0
|
|
sam_count = 0
|
|
l_flag = 0
|
|
l_start = 0
|
|
l_end = 0
|
|
l_exists = 0
|
|
d_offset = 0
|
|
is_valid = 0
|
|
|
|
data = None
|
|
|
|
def __init__(self, data):
|
|
# first, validate
|
|
self.data = data
|
|
self.magic = self.get_val(MAGIC_OFF, MAGIC_LEN)
|
|
self.form = self.get_val(FORMAT_OFF, FORMAT_LEN)
|
|
self.l_style = self.get_val(LOOP_TYPE_OFF, LOOP_TYPE_LEN)
|
|
self.encrypted = self.get_val(CRYPT_OFF, CRYPT_LEN)
|
|
self.d_offset = self.get_val(DATA_OFF_OFF, DATA_OFF_LEN)
|
|
if self.d_offset < DATA_OFF_MIN:
|
|
return None
|
|
self.l_exists = 1
|
|
# now load things for math
|
|
self.sam_rate = self.get_val(SRATE_OFF, SRATE_LEN)
|
|
self.sam_count = self.get_val(SCOUNT_OFF, SCOUNT_LEN)
|
|
self.validate()
|
|
|
|
def get_val(self, offset, length):
|
|
return int.from_bytes(
|
|
self.data[offset : offset + length], byteorder="big", signed=False
|
|
)
|
|
|
|
def validate(self):
|
|
if self.magic != MAGIC:
|
|
raise ValueError("Invalid ADX File")
|
|
if self.form != FORMAT:
|
|
raise ValueError("Invalid ADX File")
|
|
if self.encrypted:
|
|
raise NotImplementedError("Encryption Not Supported")
|
|
self.is_valid = 1
|
|
|
|
def parse_loop_data(self):
|
|
if self.l_style == 4:
|
|
self.l_flag = self.get_val(LOOP4_FLAG_OFF, LOOP4_FLAG_LEN)
|
|
self.l_start = self.get_val(LOOP4_START_OFF, LOOP4_START_LEN)
|
|
self.l_end = self.get_val(LOOP4_END_OFF, LOOP4_END_LEN)
|
|
if not self.l_flag:
|
|
return None
|
|
if self.l_start == 0 and self.l_end == self.sam_count:
|
|
return None
|
|
if self.l_start > self.sam_count or self.l_end > self.sam_count:
|
|
raise ValueError("Invalid Loop Data")
|
|
return None
|
|
if self.l_style not in LOOP_TYPE_SUP:
|
|
raise NotImplementedError(
|
|
"Loop Style {} Not Implemented".format(str(self.l_style))
|
|
)
|
|
return None
|
|
ret = {}
|
|
ret["duration"] = self.sam_count / self.sam_rate
|
|
ret["loop_start"] = {}
|
|
ret["loop_start"]["seconds"] = self.l_start / self.sam_rate
|
|
ret["loop_start"]["samples_native"] = self.l_start
|
|
ret["loop_start"]["samples_48k"] = dumb_round(
|
|
self.l_start / self.sam_rate * 48000
|
|
)
|
|
ret["loop_end"] = {}
|
|
ret["loop_end"]["seconds"] = self.l_end / self.sam_rate
|
|
ret["loop_end"]["samples_native"] = self.l_end
|
|
ret["loop_end"]["samples_48k"] = dumb_round(
|
|
self.l_end / self.sam_rate * 48000
|
|
)
|
|
return ret
|