Skip to content

Instantly share code, notes, and snippets.

@LoneRabbit
Forked from mzero/ddp-to-kunaki.py
Last active December 9, 2023 19:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save LoneRabbit/36f2a74c27a7d6a3b443b44d27fd2702 to your computer and use it in GitHub Desktop.
Save LoneRabbit/36f2a74c27a7d6a3b443b44d27fd2702 to your computer and use it in GitHub Desktop.
Convert DDP mastered CDs to Kunaki's CUE format
"""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)
@LoneRabbit
Copy link
Author

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.

@LoneRabbit
Copy link
Author

Update: I just got the CDs today, and... IT WORKS!! I'm so relieved and happy! :D

@LoneRabbit
Copy link
Author

LoneRabbit commented Aug 16, 2021

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.

@LoneRabbit
Copy link
Author

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