Created
April 9, 2019 00:14
-
-
Save mzero/79677b2dd4128ec05c85574464752d8c to your computer and use it in GitHub Desktop.
Convert DDP mastered CDs to Kunaki's CUE format
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Convert DDP mastered CDs to Kunaki's CUE format. | |
tl;dr: | |
python ddp-to-kunaki.py my-cool-cd-ddp-dir my-cool-cd-kunaki | |
This will produce two files: | |
my-cool-cd-kunaki.CUE <-- the markers | |
my-cool-cd-kunaki.iso <-- the audio (*not* an ISO format file!) | |
These are the same as the two files that Kunaki's CD reading software | |
generates. You upload to them using the "Audio ISO and CUE file" section | |
of Kunaki's upload page. | |
Note: The .iso file is a hard link to the audio file in the DDP directory, | |
so that it dosn't need to copy 700MB for no good reason. | |
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- | |
Details: | |
In DDP, the file PQ_DESC contains the marker information. | |
An example: | |
$ hexdump -v -e '64/1 "%c" "\n"' my-cool-cd-ddp-dir/PQ_DESCR | |
VVVS00000000000001 | |
VVVS01000000000001 QZ4JJ1688502 | |
VVVS01010000020001 QZ4JJ1688502 | |
VVVS02000004540001 QZ4JJ1688503 | |
VVVS02010004570001 QZ4JJ1688503 | |
VVVS03000013300001 QZ4JJ1688504 | |
VVVS03010013330001 QZ4JJ1688504 | |
VVVS04000018383901 QZ4JJ1688505 | |
VVVS04010018413901 QZ4JJ1688505 | |
VVVS05000023181401 QZ4JJ1688506 | |
VVVS05010023231401 QZ4JJ1688506 | |
VVVS06000030462201 QZ4JJ1688507 | |
VVVS06010030502201 QZ4JJ1688507 | |
VVVS07000043143701 QZ4JJ1688508 | |
VVVS07010043173701 QZ4JJ1688508 | |
VVVS08000047293701 QZ4JJ1688509 | |
VVVS08010047323701 QZ4JJ1688509 | |
VVVS09000055080901 QZ4JJ1688510 | |
VVVS09010055110901 QZ4JJ1688510 | |
VVVS10000061043001 QZ4JJ1688511 | |
VVVS10010061073001 QZ4JJ1688511 | |
VVVS11000065150601 QZ4JJ1688512 | |
VVVS11010065150601 QZ4JJ1688512 | |
VVVS12000070237401 QZ4JJ1688513 | |
VVVS12010070267401 QZ4JJ1688513 | |
VVVSAA010074387401 | |
VVVSAA010074387401 | |
The encoding is actually records of 64 ASCII characters, no newlines. | |
The format seems to be: | |
"VVVS" <tt> <ii> "00" <mm><ss><ff> <ee> " " <isrc-code-str> <32 spaces> | |
tt = track number, "AA" for lead-out | |
ii = index number | |
mmssff = minute, second, frame | |
ee = "01" or "0S" (the later only on audio track markers) | |
Kunaki's CUE file is binary, and looks like: | |
$ hexdump -e '"%04_ax :" 8/1 " %02x" "\n"' my-cool-cd-kunaki.CUE | |
0000 : 01 00 00 01 00 00 00 00 | |
0008 : 01 01 00 00 00 00 00 00 | |
0010 : 21 01 01 00 00 00 02 00 | |
0018 : 21 02 01 00 00 04 39 00 | |
0020 : 21 03 01 00 00 0d 21 00 | |
0028 : 21 04 01 00 00 12 29 27 | |
0030 : 21 05 01 00 00 17 17 0e | |
0038 : 21 06 01 00 00 1e 32 16 | |
0040 : 21 07 01 00 00 2b 11 25 | |
0048 : 21 08 01 00 00 2f 20 25 | |
0050 : 21 09 01 00 00 37 0b 09 | |
0058 : 21 0a 01 00 00 3d 07 1e | |
0060 : 21 0b 01 00 00 41 0f 06 | |
0068 : 21 0c 01 00 00 46 1a 4a | |
0070 : 21 aa 01 01 00 4a 26 4a | |
0078 : 03 | |
This seems to be similar to the information in a CD's TOC and Q subcode, though | |
with values in binary, not BCD. The format seems to be: | |
<ctrl><adr> <tt> <ii> <xx> 00 <mm> <ss> <ff> | |
ctrl = bits: quad, 0, copy allowed, pre-emphasis | |
adr = 1 for track info, | |
3 is isrc code info, but here seems to be an end marker | |
tt = track number | |
ii = index number | |
xx = ???, seems to be 00 for tracks, and 01 for lead-in and lead-out | |
mmssff = minute, second, frame | |
""" | |
import collections | |
import os | |
import struct | |
import sys | |
TocEntry = collections.namedtuple('TocEntry', | |
['track', 'index', 'minute', 'second', 'frame']) | |
track_lead_in = 0 | |
track_lead_out = 0xAA | |
def read_ddp_manifest(f): | |
files = {} | |
while True: | |
line = f.read(128) | |
if line == '': | |
break | |
head = line[0:4] | |
data_type = line[4:6] | |
data_func = line[30:40].strip() | |
const_017 = line[71:74] | |
data_file = line[74:86].strip() | |
if (head != 'VVVM' or data_type not in ['S0', 'D0'] | |
or const_017 != '017'): | |
print >>sys.stderr, '** Unrecognized line in DDPMS file: "%s"' % line | |
continue | |
files[data_func] = data_file | |
return files | |
def read_ddp_pq_desc(f): | |
entries = [] | |
while True: | |
line = f.read(64) | |
if line == '': | |
break | |
try: | |
head = line[0:4] | |
track = line[4:6] | |
track = track_lead_out if track == 'AA' else int(track) | |
index = int(line[6:8]) | |
const_00 = line[8:10] | |
minute = int(line[10:12]) | |
second = int(line[12:14]) | |
frame = int(line[14:16]) | |
ee_code = line[16:18] | |
const_gap = line[18:20] | |
isrc = line[20:32] | |
const_blank = line[32:64] | |
if (head != 'VVVS' or const_00 != '00' or ee_code not in ['01','0S'] | |
or const_gap != ' ' or any(c != ' ' for c in const_blank)): | |
print >>sys.stderr, '** Unrecognized line in PQ file: "%s"' % line | |
continue | |
except: | |
print >>sys.stderr, '** Malformed line in PQ file: "%s"' % line | |
continue | |
entries.append(TocEntry(track, index, minute, second, frame)) | |
if len(entries) >= 2 and entries[-1] == entries[-2]: | |
del entries[-1] | |
return entries | |
def write_kunaki_cue(f, entries): | |
for track, index, minute, second, frame in entries: | |
# if index == 0 and track > 1: | |
# continue | |
ctrl = 0 if track == 0 or (track == 1 and index == 0) else 2 | |
adr = 1 | |
xx = 1 if track in (track_lead_in, track_lead_out) else 0 | |
bytes = struct.pack('BBBBBBBB', | |
ctrl << 4 | adr, track, index, xx, | |
0, minute, second, frame) | |
f.write(bytes) | |
f.write(struct.pack('B', 3)) | |
def main(args): | |
if len(args) != 3: | |
print >>sys.stderr, "Usage: %s <ddp-dir> <kunaki-prefix>" % args[0] | |
sys.exit(1) | |
ddp_path = args[1] | |
kunkai_path = args[2] | |
ddp_manifest = os.path.join(ddp_path, 'DDPMS') | |
with open(ddp_manifest, 'rb') as f_ms: | |
files = read_ddp_manifest(f_ms) | |
image_name = files.get('DA') | |
if not image_name: | |
print >>sys.stderr, '** No digital audio found in manifest' | |
return | |
pq_desc_name = files.get('PQ DESCR') | |
if not pq_desc_name: | |
print >>sys.stderr, '** No PQ DESCR file found in manifest' | |
return | |
ddp_image = os.path.join(ddp_path, image_name) | |
ddp_pq_desc = os.path.join(ddp_path, pq_desc_name) | |
kunaki_iso = kunkai_path + '.iso' | |
kunaki_cue = kunkai_path + '.CUE' | |
with open(ddp_pq_desc, 'rb') as f_pq: | |
entries = read_ddp_pq_desc(f_pq) | |
with open(kunaki_cue, 'wb') as f_cue: | |
write_kunaki_cue(f_cue, entries) | |
os.link(ddp_image, kunaki_iso) | |
if __name__ == '__main__': | |
main(sys.argv) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment