-
-
Save LoneRabbit/36f2a74c27a7d6a3b443b44d27fd2702 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: | |
Type in CMD where the script is located in: | |
python ddp-to-kunaki.py "my-cool-cd-ddp-dir" "my-cool-cd-kunaki" <-- Yes, you need to put in quotation marks | |
ddp-to-kunaki.py - name of script | |
my-cool-cd-ddp-dir - path to ddp files IN QUOTATION MARKS | |
my-cool-cd-kunaki - name of your cue and iso files for Kunaki IN QUOTATION MARKS | |
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: | |
for i in range(3): | |
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 != b'VVVM' or data_type not in [b'S0', b'D0'] | |
or const_017 != b'017'): | |
print('** Unrecognized line in DDPMS file: "%s"' % line, file=sys.stderr) | |
#print(head, data_type, data_func, const_017, data_file) | |
#print >>sys.stderr, '** Unrecognized line in DDPMS file: "%s"' % line | |
continue | |
files[data_func] = data_file | |
print(files[data_func]) | |
return files | |
def read_ddp_pq_desc(f): | |
entries = [] | |
stopit1 = True | |
while stopit1 == True: | |
line = f.read(64) | |
if line == '': | |
break | |
try: | |
head = line[0:4] | |
track = line[4:6] | |
track = track_lead_out if track == b'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] | |
#print(head, track, index, const_00, minute, second, frame, ee_code, const_gap, isrc, const_blank) | |
if (head != b'VVVS' or const_00 not in [b'00', b' '] or ee_code not in [b'01',b'0S'] | |
or const_gap != b' '): | |
#or any(c != b' ' for c in const_blank) <- The 32 spaces might include strings for album details | |
print('** Unrecognized line in PQ file: "%s"' % line, file=sys.stderr) | |
#print >>sys.stderr, '** Unrecognized line in PQ file: "%s"' % line | |
continue | |
except: | |
print('** Malformed or blank line in PQ file: "%s"' % line, file=sys.stderr) | |
#print >>sys.stderr, '** Malformed line in PQ file: "%s"' % line | |
stopit1 = False | |
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("Usage: %s <ddp-dir> <kunaki-prefix>" % args[0], file=sys.stderr) | |
#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(b'DA') | |
print(image_name) | |
if not image_name: | |
print('** No digital audio found in manifest', file=sys.stderr) | |
#print >>sys.stderr, '** No digital audio found in manifest' | |
return | |
pq_desc_name = files.get(b'PQ DESCR') | |
if not pq_desc_name: | |
print('** No PQ DESCR file found in manifest', file=sys.stderr) | |
#print >>sys.stderr, '** No PQ DESCR file found in manifest' | |
return | |
ddp_path = str.encode(ddp_path) | |
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) |
Update: I just got the CDs today, and... IT WORKS!! I'm so relieved and happy! :D
One note though, it might be a good idea to have some padding at the end of the tracks, especially for the last track. The markers in Reaper ain't really precise, or maybe I'm just doing it wrong. The last track of my CD cuts off a bit too early, but oh well, lesson learned the hard way.
Note: If the command line says ** Malformed or blank line in PQ file: "b''", that is completely normal! From my understanding, it says that because it's the end of the file. You can now test if the outputted files works or not in Kunaki's virtual player feature without having to order the actual CDs now as well!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
FYI: The original creator used Triumph to create the DPP files, I used Reaper to create my DDP files. I just ordered the CDs this script made, I'll update here if it actually works or not.