From 754c704d02f5b1824ddb1724994c294c65b32f43 Mon Sep 17 00:00:00 2001 From: louis Date: Tue, 24 Apr 2018 23:22:03 +0900 Subject: [PATCH] initial commit: adx loop parser --- LICENSE.txt | 19 ++++++++ adx/__init__.py | 0 adx/adx.py | 44 +++++++++++++++++ adx/adx_parser.py | 119 ++++++++++++++++++++++++++++++++++++++++++++++ divatool.py | 34 +++++++++++++ 5 files changed, 216 insertions(+) create mode 100644 LICENSE.txt create mode 100644 adx/__init__.py create mode 100644 adx/adx.py create mode 100644 adx/adx_parser.py create mode 100755 divatool.py diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..ee668d0 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright 2018 + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/adx/__init__.py b/adx/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/adx/adx.py b/adx/adx.py new file mode 100644 index 0000000..5531c69 --- /dev/null +++ b/adx/adx.py @@ -0,0 +1,44 @@ +import glob +import json +import os +import traceback +from .adx_parser import adx_file + +def extract_loop_data_from_dir(dir_in, fout): + if fout is None: + fout = "adx_loop.json" + files = glob.glob(os.path.join(dir_in, "**/*.adx"), recursive=True) + extract_loop_data_from_files(files, fout) + +def extract_loop_data_from_files(files_in, fout): + if not files_in: + raise FileNotFoundError("No input files found") + collect = {} + for x in files_in: + res = extract_loop_data_from_file(x) + if res is not None: + collect[os.path.splitext(os.path.basename(x))[0]] = res + with open(fout, 'w') as out: + json.dump(collect, out, ensure_ascii=False, indent="\t", sort_keys=True) + +def extract_loop_data_from_file(file_in, fout=None): + if not file_in.endswith(".adx"): + raise ValueError("Input file not .adx") + + try: + with open(file_in, 'rb') as x: + data = x.read(64) + ADX = adx_file(data) + if ADX.l_exists and ADX.is_valid: + res = ADX.parse_loop_data() + else: + res = None + except Exception as e: + traceback.print_exc() + print("Error processing file: " + file_in) + return None + + if fout is not None and res is not None: + with open(fout, 'w') as out: + json.dump(res, out, ensure_ascii=False, indent="\t", sort_keys=True) + return res diff --git a/adx/adx_parser.py b/adx/adx_parser.py new file mode 100644 index 0000000..95ac181 --- /dev/null +++ b/adx/adx_parser.py @@ -0,0 +1,119 @@ +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 adxf.l_style == 4: + adxf.l_flag = adxf.get_val(LOOP4_FLAG_OFF, LOOP4_FLAG_LEN) + adxf.l_start = adxf.get_val(LOOP4_START_OFF, LOOP4_START_LEN) + adxf.l_end = adxf.get_val(LOOP4_END_OFF, LOOP4_END_LEN) + if not adxf.l_flag: + return None + if adxf.l_start == 0 and adxf.l_end == adxf.sam_count: + return None + if adxf.l_start > adxf.sam_count or adxf.l_end > adxf.sam_count: + raise ValueError("Invalid Loop Data") + return None + if not adxf.l_style in LOOP_TYPE_SUP: + raise NotImplementedError("Loop Style {} Not Implemented".format(str(adxf.l_style))) + return None + ret = {} + ret['duration'] = adxf.sam_count / adxf.sam_rate + ret['loop_start'] = {} + ret['loop_start']['seconds'] = adxf.l_start / adxf.sam_rate + ret['loop_start']['samples_native'] = adxf.l_start + ret['loop_start']['samples_48k'] = dumb_round(adxf.l_start / adxf.sam_rate * 48000) + ret['loop_end'] = {} + ret['loop_end']['seconds'] = adxf.l_end / adxf.sam_rate + ret['loop_end']['samples_native'] = adxf.l_end + ret['loop_end']['samples_48k'] = dumb_round(adxf.l_end / adxf.sam_rate * 48000) + return ret diff --git a/divatool.py b/divatool.py new file mode 100755 index 0000000..4ef6dac --- /dev/null +++ b/divatool.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +import argparse +import sys +from adx import adx + +def main(): + parser = argparse.ArgumentParser(description="Unpack and process XDU data") + + subparsers = parser.add_subparsers(metavar="", title="subcommands", dest="command") + + # adx subcommand + parser_adx = subparsers.add_parser("adx", help="ADX stuff") + parser_adx_subparsers = parser_adx.add_subparsers(metavar="", title="subcommands", dest="subcommand") + # adx loop + parser_adx_loop = parser_adx_subparsers.add_parser("loop", help="Extract loop data") + parser_adx_loop.add_argument("--output", "-o", + help="JSON output file for loop data", + type=str, dest="adx_json_out") + parser_adx_loop.add_argument("--input", "-i", + help="Input directory containing ADX files", + type=str, dest="adx_dir_in", required=True) + + args = parser.parse_args() + + if args.command == "adx": + if args.subcommand == "loop": + adx.extract_loop_data_from_dir(args.adx_dir_in, args.adx_json_out) + + return 0 + +if __name__ == '__main__': + main() + sys.exit()