cue2ccd.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. #!/usr/bin/env python3
  2. import os
  3. import sys
  4. def printhelp():
  5. # $1 - input cue
  6. # $2 - input bin
  7. return 0
  8. def frames2minsec(tf):
  9. # u = uncleaned, c = cleaned
  10. us = int(tf/75)
  11. cf = int(tf - (us*75))
  12. cm = int(us /60)
  13. cs = int(us - (cm * 60))
  14. #print("m={} s={} f={}".format(cm,cs,cf))
  15. formatted_timing = "{0:02d}:{1:02d}:{2:02d}".format(cm,cs,cf)
  16. return formatted_timing
  17. def minsec2frames(input_msf):
  18. minutes = int(input_msf.split(":")[0])
  19. seconds = int(input_msf.split(":")[1])
  20. frames = int(input_msf.split(":")[2])
  21. total_frames = (((minutes * 60) + seconds) *75) + frames
  22. return total_frames
  23. def frames2bytes(input_frames):
  24. # TODO check to make sure this is a clean number with no decimal or remainder, otherwise this indicates the bytes per frame of 2352 is not correct
  25. _bytes = input_frames * 2352
  26. return _bytes
  27. def bytes2frames(input_bytes):
  28. # TODO check to make sure this is a clean number with no decimal or remainder, otherwise this indicates the bytes per frame of 2352 is not correct
  29. _frames = input_bytes / 2352
  30. _frames_str = str(_frames)
  31. after_decimal = int(_frames_str.split('.')[1])
  32. if after_decimal != 0:
  33. print("INPUTBYTES: {0} IS NOT CLEANLY DIVISIBLE BY 2352!! {0} / 2352 = {1}".format(input_bytes, _frames))
  34. print("exiting...")
  35. quit()
  36. else:
  37. return int(_frames)
  38. def getsizeofbin(bin_file_location):
  39. size_of_bin = os.path.getsize(bin_file_location)
  40. return size_of_bin
  41. def generate_nested_dict_from_cuefile(cue_file_location):
  42. with open(cue_file_location, "r") as myfile:
  43. cuelist=myfile.read().splitlines()
  44. disc_toc = {}
  45. linelist = []
  46. cueline = 0
  47. linelist = cuelist[cueline].split()
  48. file_list = []
  49. frames_before_current_track = 0
  50. accumulated_frames = 0
  51. pregap_spacing = None
  52. def cuestep():
  53. nonlocal cueline
  54. nonlocal cuelist
  55. cueline +=1
  56. if cueline >= len(cuelist):
  57. return "END_OF_FILE"
  58. else:
  59. linelist = cuelist[cueline].split()
  60. return linelist
  61. while linelist != "END_OF_FILE":
  62. if linelist[0].lower() == 'catalog':
  63. linelist = cuestep()
  64. if linelist[0].lower() == 'file':
  65. current_file = ' '.join(linelist).split('"')[1]
  66. file_list.append(current_file)
  67. try:
  68. current_file_size = getsizeofbin(cue_file_location.rsplit('/', 1)[0] + '/' + current_file)
  69. except IOError:
  70. print('ERROR: file not found: {}'.format(current_file))
  71. print('FIX THE CUESHEET FILE LOCATIONS, OR RENAME INPUT FILES. EXITING...')
  72. quit()
  73. linelist = cuestep()
  74. elif linelist[0].lower() == 'track':
  75. track_number = "{0:02d}".format(int(linelist[1]))
  76. disc_toc['track' + str(track_number)] = {}
  77. disc_toc['track' + str(track_number)]['file'] = current_file
  78. disc_toc['track' + str(track_number)]['file_size'] = current_file_size
  79. disc_toc['track' + str(track_number)]['mode'] = linelist[-1]
  80. disc_toc['track' + str(track_number)]['indexes'] = {}
  81. linelist = cuestep()
  82. #print(disc_toc)
  83. while linelist[0].lower != 'track' and linelist != "END_OF_FILE":
  84. if linelist[0].lower() == 'index' or linelist[0].lower() == 'pregap':
  85. if linelist[0].lower() == 'pregap':
  86. pregap_spacing = linelist[1]
  87. linelist.append(linelist[1])
  88. linelist[0] = 'INDEX'
  89. linelist[1] = '0'
  90. index_number = int(linelist[1])
  91. disc_toc['track' + str(track_number)]['indexes'][str(index_number)] = {}
  92. if len(file_list) > 1: #
  93. # at the begining of a disc we assume all times given in the cuefile are absolute,
  94. # however as soon as a second file is detected, we know all tracks coming forward
  95. # will be relative to their FILE declaration, either way, track 1 times are acceptable for both aboslute and relative values
  96. disc_toc['track' + str(track_number)]['indexes'][str(index_number)]['rtime'] = linelist[-1]
  97. if len(disc_toc['track' + str(track_number)]['indexes']) == 1:
  98. accumulated_frames += bytes2frames(disc_toc['track' + "{0:02d}".format(int(track_number) - 1)]['file_size'])
  99. disc_toc['track' + str(track_number)]['indexes'][str(index_number)]['atime'] = frames2minsec(accumulated_frames + minsec2frames(disc_toc['track' + str(track_number)]['indexes'][str(index_number)]['rtime']))
  100. else:
  101. # first file in cuesheet, or all times given are absolute from the cuesheet
  102. disc_toc['track' + str(track_number)]['indexes'][str(index_number)]['atime'] = linelist[-1]
  103. first_index_in_track = sorted(disc_toc['track' + str(track_number)]['indexes'])[0]
  104. accumulated_frames = minsec2frames(disc_toc['track' + str(track_number)]['indexes'][str(first_index_in_track)]['atime'])
  105. disc_toc['track' + str(track_number)]['indexes'][str(index_number)]['rtime'] = frames2minsec(minsec2frames(linelist[-1]) - accumulated_frames)
  106. linelist = cuestep()
  107. elif linelist[0].lower() == 'pregap':
  108. disc_toc['track' + str(track_number)]['pregap'] = linelist[-1]
  109. linelist = cuestep()
  110. elif linelist[0].lower() == 'postgap':
  111. print("POSTGAP NOT IMPLEMENTED")
  112. print('exiting...')
  113. quit()
  114. disc_toc['track' + str(track_number)]['postgap'] = linelist[-1]
  115. linelist = cuestep()
  116. elif linelist[0].lower() == 'title':
  117. linelist = cuestep()
  118. elif linelist[0].lower() == 'performer':
  119. linelist = cuestep()
  120. elif linelist[0].lower() == 'songwriter':
  121. linelist = cuestep()
  122. elif linelist[0].lower() == 'track' or linelist[0].lower() == 'file':
  123. break
  124. else:
  125. print("Command: {} is not supported, exiting...".format(linelist[0].lower()))
  126. quit()
  127. if pregap_spacing:
  128. disc_toc['track' + str(track_number)]['indexes']['0']['rtime'] = "00:00:00"
  129. disc_toc['track' + str(track_number)]['indexes']['0']['atime'] = frames2minsec(minsec2frames(disc_toc['track' + str(track_number)]['indexes']['1']['atime']) - minsec2frames(pregap_spacing))
  130. disc_toc['track' + str(track_number)]['indexes']['1']['rtime'] = pregap_spacing
  131. pregap_spacing = None
  132. elif linelist[0].lower() != 'file' \
  133. and linelist[0].lower() != 'track' \
  134. and linelist[0].lower() != 'index' \
  135. and linelist[0].lower() != 'pregap' \
  136. and linelist[0].lower() != 'pregap' \
  137. and linelist[0].lower() != 'catalog':
  138. print('the only supported cue command words are FILE TRACK INDEX PREGAP POSTGAP CATALOG')
  139. print('{} is not a valid cue command. exiting...'.format(linelist[0]))
  140. quit()
  141. return disc_toc
  142. def generate_ccd(disc_toc_dict, leadout_pos):
  143. # get full size of bin
  144. leadout_timing = frames2minsec(leadout_pos)
  145. lo_m = int(leadout_timing.split(":")[0])
  146. lo_s = int(leadout_timing.split(":")[1]) + 2
  147. lo_f = int(leadout_timing.split(":")[2])
  148. if lo_s >= 60:
  149. lo_s -= 60
  150. lo_m += 1
  151. ccd = '[CloneCD]' + '\n'
  152. ccd += 'Version=3' + '\n'
  153. ccd += '' + '\n'
  154. ccd += '[Disc]' + '\n'
  155. ccd += 'TocEntries=' + str(len(disc_toc) + 3) + '\n'
  156. ccd += 'Sessions=1' + '\n'
  157. ccd += 'DataTracksScrambled=0' + '\n'
  158. ccd += 'CDTextLength=0' + '\n'
  159. ccd += '' + '\n'
  160. ccd += '[Session 1]' + '\n'
  161. ccd += 'PreGapMode=1' + '\n' # TODO
  162. ccd += 'PreGapSubC=0' + '\n' # TODO
  163. ccd += '' + '\n'
  164. ccd += '[Entry 0]' + '\n'
  165. ccd += 'Session=1' + '\n'
  166. ccd += 'Point=0xa0' + '\n'
  167. ccd += 'ADR=0x01' + '\n'
  168. ccd += 'Control=0x04' + '\n'
  169. ccd += 'TrackNo=0' + '\n'
  170. ccd += 'AMin=0' + '\n'
  171. ccd += 'ASec=0' + '\n'
  172. ccd += 'AFrame=0' + '\n'
  173. ccd += 'ALBA=-150' + '\n'
  174. ccd += 'Zero=0' + '\n'
  175. ccd += 'PMin=1' + '\n'
  176. ccd += 'PSec=0' + '\n'
  177. ccd += 'PFrame=0' + '\n'
  178. ccd += 'PLBA=4350' + '\n'
  179. ccd += '' + '\n'
  180. ccd += '[Entry 1]' + '\n'
  181. ccd += 'Session=1' + '\n'
  182. ccd += 'Point=0xa1' + '\n'
  183. ccd += 'ADR=0x01' + '\n'
  184. ccd += 'Control=0x00' + '\n'
  185. ccd += 'TrackNo=0' + '\n'
  186. ccd += 'AMin=0' + '\n'
  187. ccd += 'ASec=0' + '\n'
  188. ccd += 'AFrame=0' + '\n'
  189. ccd += 'ALBA=-150' + '\n'
  190. ccd += 'Zero=0' + '\n'
  191. ccd += 'PMin=' + str(len(disc_toc)) + '\n' # this must be set to how many total tracks there are
  192. ccd += 'PSec=0' + '\n'
  193. ccd += 'PFrame=0' + '\n'
  194. ccd += 'PLBA=' + str(minsec2frames("{}:00:00".format(len(disc_toc))) - 150) + '\n' # this is set to the equivalent frames for the PMin set earlier, minus ALBA
  195. ccd += '' + '\n'
  196. ccd += '[Entry 2]' + '\n'
  197. ccd += 'Session=1' + '\n'
  198. ccd += 'Point=0xa2' + '\n'
  199. ccd += 'ADR=0x01' + '\n'
  200. ccd += 'Control=0x00' + '\n'
  201. ccd += 'TrackNo=0' + '\n'
  202. ccd += 'AMin=0' + '\n'
  203. ccd += 'ASec=0' + '\n'
  204. ccd += 'AFrame=0' + '\n'
  205. ccd += 'ALBA=-150' + '\n'
  206. ccd += 'Zero=0' + '\n'
  207. ccd += 'PMin=' + str(lo_m) + '\n'
  208. ccd += 'PSec=' + str(lo_s) + '\n'
  209. ccd += 'PFrame=' + str(lo_f) + '\n'
  210. ccd += 'PLBA=' + str(leadout_pos) + '\n'
  211. track_number = 1
  212. ccd_tracklist = ''
  213. while track_number <= len(disc_toc_dict):
  214. pmin = int(disc_toc_dict['track' + "{0:02d}".format(track_number)]['indexes']['1']['atime'].split(":")[0])
  215. psec = int(disc_toc_dict['track' + "{0:02d}".format(track_number)]['indexes']['1']['atime'].split(":")[1]) + 2
  216. pframe = int(disc_toc_dict['track' + "{0:02d}".format(track_number)]['indexes']['1']['atime'].split(":")[2])
  217. if psec >= 60:
  218. psec -= 60
  219. pmin += 1
  220. ccd += '' + '\n'
  221. ccd +='[Entry ' + str(track_number + 2) + ']' +'\n'
  222. ccd +='Session=1' +'\n'
  223. ccd +='Point=' + "{0:#0{1}x}".format(track_number,4) +'\n'
  224. ccd +='ADR=0x01' +'\n'
  225. if disc_toc_dict['track' + "{0:02d}".format(track_number)]['mode'] == 'MODE1/2352' or disc_toc_dict['track' + "{0:02d}".format(track_number)]['mode'] == 'MODE2/2352':
  226. ccd +='Control=0x04' +'\n'
  227. elif disc_toc_dict['track' + "{0:02d}".format(track_number)]['mode'] == 'AUDIO':
  228. ccd +='Control=0x00' +'\n'
  229. else:
  230. print("unknown track mode: {}".format(disc_toc_dict['track' + "{0:02d}".format(track_number)]['mode']))
  231. print("exiting...")
  232. quit()
  233. ccd +='TrackNo=0' +'\n'
  234. ccd +='AMin=0' +'\n'
  235. ccd +='ASec=0' +'\n'
  236. ccd +='AFrame=0' +'\n'
  237. ccd +='ALBA=-150' +'\n'
  238. ccd +='Zero=0' +'\n'
  239. ccd +='PMin=' + str(pmin) +'\n'
  240. ccd +='PSec=' + str(psec) +'\n'
  241. ccd +='PFrame=' + str(pframe) +'\n'
  242. ccd +='PLBA=' + str(minsec2frames(disc_toc_dict['track' + "{0:02d}".format(track_number)]['indexes']['1']['atime'])) +'\n' # TODO test again a disck with more indexes than just 00 or 01
  243. ccd_tracklist += '' + '\n'
  244. ccd_tracklist += '[TRACK ' + str(track_number) +']' + '\n'
  245. if disc_toc_dict['track' + "{0:02d}".format(track_number)]['mode'] == 'MODE1/2352':
  246. ccd_tracklist += 'MODE=1' +'\n'
  247. elif disc_toc_dict['track' + "{0:02d}".format(track_number)]['mode'] == 'MODE2/2352':
  248. ccd_tracklist += 'MODE=2' +'\n'
  249. elif disc_toc_dict['track' + "{0:02d}".format(track_number)]['mode'] == 'AUDIO':
  250. ccd_tracklist += 'MODE=0' +'\n'
  251. else:
  252. print("unknown track mode: {}".format(disc_toc_dict['track' + "{0:02d}".format(track_number)]['mode']))
  253. print("exiting...")
  254. quit()
  255. for index in disc_toc_dict['track' + "{0:02d}".format(track_number)]['indexes']:
  256. ccd_tracklist += 'INDEX ' + index + '=' + str(minsec2frames(disc_toc_dict['track' + "{0:02d}".format(track_number)]['indexes'][str(index)]['atime'])) + '\n'
  257. track_number += 1
  258. ccd += ccd_tracklist
  259. return ccd
  260. def list_files(disc_toc_dict):
  261. file_list = []
  262. last_file = disc_toc_dict['track01']['file']
  263. file_list.append(last_file)
  264. for key in disc_toc_dict:
  265. next_file = disc_toc_dict[key]['file']
  266. if next_file != last_file :
  267. file_list.append(next_file)
  268. last_file = next_file
  269. return file_list
  270. def create_imgfile_from_bin():
  271. return 0
  272. def get_total_bin_size(disc_toc_dict, cue_file_location):
  273. total_bin_size = 0
  274. for current_file in list_files(disc_toc_dict):
  275. path = cue_file_location.rsplit('/', 1)[0]
  276. full_file_path = path + '/' + current_file
  277. total_bin_size += os.path.getsize(full_file_path)
  278. return total_bin_size
  279. def generate_sub_file(disc_toc_dict):
  280. # |---- P CHANNEL ----||---- Q CHANNEL ----||---- R CHANNEL ----||---- S CHANNEL ----||---- T CHANNEL ----||---- U CHANNEL ----||---- V CHANNEL ----||---- W CHANNEL ----|
  281. # abcdefghijklmnopqrstuvwx
  282. # 012101001643004739490db4
  283. # '000000000000000000000000410201045343000611565bcd000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
  284. #
  285. # http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-130.pdf
  286. #
  287. #
  288. # THIS SECTION IS CALLED THE Q-CHANNEL
  289. # a = Control field: either 4 or 0, look at the value for Control under the track in question in the ccd
  290. # b = q-Mode field: alyways set to 1
  291. # cd = TNO field: track number
  292. # ef = INDEX field: 00 or 01
  293. # gh = RELATIVE MIN
  294. # ij = RELATIVE SEC
  295. # kl = RELATIVE FRAC
  296. # mn = ZERO: always set to zero
  297. # op = ABSOLUTE MIN
  298. # qr = ABSOLUTE SEC
  299. # st = ABSOLUTE FRAC
  300. # uvwx = CRC
  301. # channels filled with zeros
  302. P = '000000000000000000000000'
  303. R = '000000000000000000000000'
  304. S = '000000000000000000000000'
  305. T = '000000000000000000000000'
  306. U = '000000000000000000000000'
  307. V = '000000000000000000000000'
  308. W = '000000000000000000000000'
  309. # place holders....
  310. time_dict = {}
  311. time_dict['rm'] = 0
  312. time_dict['rs'] = 0
  313. time_dict['rf'] = 0
  314. time_dict['am'] = 0
  315. time_dict['as'] = 2
  316. time_dict['af'] = 0
  317. pregap_mode = False
  318. def mmssff_counter(time_dict, index_number, disc_toc_dict):
  319. nonlocal pregap_mode
  320. if time_dict['af'] == 74:
  321. time_dict['af'] -= 74
  322. if time_dict['as'] == 59:
  323. time_dict['as'] -= 59
  324. time_dict['am'] += 1
  325. else:
  326. time_dict['as'] += 1
  327. else:
  328. time_dict['af'] += 1
  329. #print(disc_toc_dict[track_]['indexes']['1'])
  330. if index_number == '0':
  331. if time_dict['rf'] == 0:
  332. time_dict['rf'] += 74
  333. if time_dict['rs'] == 0:
  334. time_dict['rs'] += 59
  335. time_dict['rm'] -= 1
  336. else:
  337. time_dict['rs'] -= 1
  338. else:
  339. time_dict['rf'] -= 1
  340. else:
  341. if time_dict['rf'] == 74:
  342. time_dict['rf'] -= 74
  343. if time_dict['rs'] == 59:
  344. time_dict['rs'] -= 59
  345. time_dict['rm'] += 1
  346. else:
  347. time_dict['rs'] += 1
  348. else:
  349. time_dict['rf'] += 1
  350. subfile_out = open('./out.sub', 'wb')
  351. crc_table = make_crc_table()
  352. total_frames = (total_bin_size / 2352)
  353. track_number = 1
  354. for track_ in disc_toc_dict:
  355. track_number = track_.split("track")[-1]
  356. for index in disc_toc_dict[track_]['indexes']:
  357. if index == '0':
  358. index_rtime_offset = disc_toc_dict[track_]['indexes']['1']['rtime']
  359. time_dict['rm'] = int(index_rtime_offset.split(":")[0])
  360. time_dict['rs'] = int(index_rtime_offset.split(":")[1])
  361. time_dict['rf'] = int(index_rtime_offset.split(":")[2])
  362. frame = 1
  363. while frame <= disc_toc_dict[track_]['indexes'][index]['frame_size']:
  364. if disc_toc_dict['track' + str(track_number)]['mode'] == 'MODE1/2352' or disc_toc_dict['track' + str(track_number)]['mode'] == 'MODE2/2352':
  365. a = '4'
  366. elif disc_toc_dict['track' + str(track_number)]['mode'] == 'AUDIO':
  367. a = '0'
  368. else:
  369. print("unknown track mode: {}".format(disc_toc_dict['track' + str(track_number)]['mode']))
  370. print("exiting...")
  371. quit()
  372. b = '1'
  373. cd = "{:02d}".format(int(track_number))
  374. ef = "{:02d}".format(int(index))
  375. gh = "{:02d}".format(time_dict['rm']) # just have this count up by 1 for each frame loop!!
  376. ij = "{:02d}".format(time_dict['rs'])
  377. kl = "{:02d}".format(time_dict['rf'])
  378. mn = '00'
  379. op = "{:02d}".format(time_dict['am'])
  380. qr = "{:02d}".format(time_dict['as'])
  381. st = "{:02d}".format(time_dict['af'])
  382. abcdefghijklmnopqrst = a + b + cd + ef + gh + ij + kl + mn + op + qr + st
  383. #print(abcdefghijklmnopqrst)
  384. #print("41020104534300061156 <--- example")
  385. uvwx = "{0:#0{1}x}".format(crc_16(crc_table, bytes.fromhex(abcdefghijklmnopqrst)), 6).split("0x")[1] # "{0:#0{1}x}".format(track_number,4)
  386. Q = abcdefghijklmnopqrst + uvwx
  387. SUBCHANNEL = P + Q + R + S + T + U + V + W
  388. subfile_out.write(bytes.fromhex(SUBCHANNEL))
  389. frame += 1
  390. mmssff_counter(time_dict, index, disc_toc_dict)
  391. subfile_out.close()
  392. def make_crc_table():
  393. # this a simplified version of gen_table() from pycrc/crc_algorithms.py
  394. tbl = []
  395. x = 2
  396. polynomial = ((x**16)+(x**12)+(x**5)+(1)) & 0xffff # 0x1021
  397. for i in range(256):
  398. reg = i
  399. reg = reg << (8)
  400. for bit in range(8):
  401. if reg & (0x8000 << 0) != 0:
  402. reg = (reg << 1) ^ (polynomial << 0)
  403. else:
  404. reg = (reg << 1)
  405. tbl.append((reg >> 0) & 0xffff)
  406. return tbl
  407. def crc_16(table, msg):
  408. crc = 0x0000
  409. for byte in msg:
  410. tbl_idx = ((crc >> 8) ^ byte) & 0xff
  411. crc = (table[tbl_idx] ^ (crc << 8)) & 0xffff
  412. return crc ^ 0xffff
  413. def test_crc16():
  414. print(hex(crc_16(bytes.fromhex("41020104534300061156"))))
  415. print('it should be 0x5bcd')
  416. def append_track_sizes_in_frames(disc_toc, leadout_position_in_frames):
  417. previous_track_beginning = leadout_position_in_frames
  418. for track in sorted(disc_toc, reverse=True):
  419. for index in sorted(disc_toc[track]['indexes'], key=int, reverse=True):
  420. track_beginning = minsec2frames(disc_toc[track]['indexes'][index]['atime'])
  421. index_size_in_frames = previous_track_beginning - track_beginning
  422. disc_toc[track]['indexes'][index]['frame_size'] = index_size_in_frames
  423. previous_track_beginning = track_beginning
  424. return disc_toc
  425. cuefile = os.path.realpath(sys.argv[1])
  426. disc_toc = generate_nested_dict_from_cuefile(cuefile)
  427. total_bin_size = get_total_bin_size(disc_toc, cuefile)
  428. leadout_position_in_frames = bytes2frames(total_bin_size)
  429. disc_toc = append_track_sizes_in_frames(disc_toc, leadout_position_in_frames)
  430. ccd = generate_ccd(disc_toc, leadout_position_in_frames)
  431. f = open("./out.ccd","w") #opens file with name of "test.txt"
  432. f.write(ccd)
  433. f.close()
  434. table = make_crc_table()
  435. sub = generate_sub_file(disc_toc)