1#!/usr/bin/python 2 3# Audio Tools, a module and set of tools for manipulating audio data 4# Copyright (C) 2007-2014 Brian Langenberger 5 6# This program is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 2 of the License, or 9# (at your option) any later version. 10 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15 16# You should have received a copy of the GNU General Public License 17# along with this program; if not, write to the Free Software 18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 19 20 21import os 22import os.path 23import sys 24import tempfile 25import subprocess 26import audiotools 27import audiotools.toc 28import audiotools.text as _ 29from audiotools.decoders import SameSample 30 31MAX_CPUS = audiotools.MAX_JOBS 32 33 34def convert_to_wave(progress, audiofile, wave_filename): 35 try: 36 if (((audiofile.sample_rate() == 44100) and 37 (audiofile.channels() == 2) and 38 (audiofile.bits_per_sample() == 16))): # already CD quality 39 audiofile.convert(target_path=wave_filename, 40 target_class=audiotools.WaveAudio, 41 progress=progress) 42 43 else: # convert to CD quality 44 pcm = audiotools.PCMReaderProgress( 45 pcmreader=audiotools.PCMConverter( 46 audiofile.to_pcm(), 47 sample_rate=44100, 48 channels=2, 49 channel_mask=audiotools.ChannelMask.from_channels(2), 50 bits_per_sample=16), 51 total_frames=(audiofile.total_frames() * 52 44100 // audiofile.sample_rate()), 53 progress=progress) 54 audiotools.WaveAudio.from_pcm(wave_filename, pcm) 55 pcm.close() 56 except audiotools.EncodingError as err: 57 pass 58 except KeyboardInterrupt: 59 pass 60 61 62def CD_reader_size(audiofile): 63 """returns a (PCMReader, total_pcm_frames) of track in CD format""" 64 65 if (((audiofile.bits_per_sample() == 16) and 66 (audiofile.channels() == 2) and 67 (audiofile.sample_rate() == 44100) and 68 audiofile.lossless())): 69 return (audiofile.to_pcm(), audiofile.total_frames()) 70 else: 71 # if input audio isn't CD quality or lossless, 72 # pull audio data into RAM so PCM frame size 73 # can be calculated exactly 74 pcm_frames = [] 75 audiotools.transfer_data( 76 audiotools.PCMConverter(audiofile.to_pcm(), 77 44100, 2, 0x3, 16).read, 78 pcm_frames.append) 79 return (PCMBuffer(pcm_frames, 44100, 2, 0x3, 16), 80 sum(f.frames for f in pcm_frames)) 81 82 83class PCMBuffer(object): 84 def __init__(self, pcm_frames, 85 sample_rate, channels, channel_mask, bits_per_sample): 86 from collections import deque 87 88 self.pcm_frames = deque(pcm_frames) 89 self.sample_rate = sample_rate 90 self.channels = channels 91 self.channel_mask = channel_mask 92 self.bits_per_sample = bits_per_sample 93 94 def read(self, frames): 95 if self.pcm_frames is not None: 96 try: 97 return self.pcm_frames.popleft() 98 except IndexError: 99 from audiotools.pcm import empty_framelist 100 return empty_framelist(self.channels, self.bits_per_sample) 101 else: 102 raise ValueError() 103 104 def close(self): 105 self.pcm_frames = None 106 107 108if (__name__ == '__main__'): 109 import argparse 110 111 parser = argparse.ArgumentParser(description=_.DESCRIPTION_TRACK2CD) 112 113 parser.add_argument("--version", 114 action="version", 115 version="Python Audio Tools %s" % (audiotools.VERSION)) 116 117 parser.add_argument("-V", "--verbose", 118 dest="verbosity", 119 choices=audiotools.VERBOSITY_LEVELS, 120 default=audiotools.DEFAULT_VERBOSITY, 121 help=_.OPT_VERBOSE) 122 123 parser.add_argument("-c", "--cdrom", 124 dest="dev", 125 default=audiotools.DEFAULT_CDROM) 126 127 parser.add_argument("-s", "--speed", 128 dest="speed", 129 default=20, 130 type=int, 131 help=_.OPT_SPEED) 132 133 parser.add_argument("--cue", 134 dest="cuesheet", 135 metavar="FILENAME", 136 help=_.OPT_CUESHEET_TRACK2CD) 137 138 parser.add_argument("-j", "--joint", 139 type=int, 140 default=MAX_CPUS, 141 dest="max_processes", 142 help=_.OPT_JOINT) 143 144 parser.add_argument("filenames", 145 metavar="FILENAME", 146 nargs="+", 147 help=_.OPT_INPUT_FILENAME) 148 149 options = parser.parse_args() 150 151 msg = audiotools.Messenger(options.verbosity == "quiet") 152 153 if options.max_processes < 1: 154 msg.error(_.ERR_INVALID_JOINT) 155 sys.exit(1) 156 else: 157 max_processes = options.max_processes 158 159 write_offset = audiotools.config.getint_default("System", 160 "cdrom_write_offset", 161 0) 162 163 audiofiles = audiotools.open_files(options.filenames, 164 sorted=False, 165 messenger=msg, 166 warn_duplicates=True) 167 168 if len(audiofiles) < 1: 169 msg.error(_.ERR_FILES_REQUIRED) 170 sys.exit(1) 171 172 if (((len(audiofiles) == 1) and 173 (audiofiles[0].get_cuesheet() is not None))): 174 # writing a single file with an embedded cuesheet 175 # so extract its contents to wave/cue and call cdrdao 176 177 if not audiotools.BIN.can_execute(audiotools.BIN['cdrdao']): 178 msg.error(_.ERR_NO_CDRDAO) 179 msg.info(_.ERR_GET_CDRDAO) 180 sys.exit(1) 181 182 cuesheet = audiofiles[0].get_cuesheet() 183 184 temptoc = tempfile.NamedTemporaryFile(suffix='.toc') 185 tempwav = tempfile.NamedTemporaryFile(suffix='.wav') 186 187 audiotools.toc.write_tocfile( 188 cuesheet, 189 audiotools.Filename(tempwav.name).__unicode__(), 190 temptoc) 191 temptoc.flush() 192 193 progress = audiotools.SingleProgressDisplay( 194 msg, _.LAB_CONVERTING_FILE) 195 196 try: 197 if (((audiofiles[0].sample_rate() == 44100) and 198 (audiofiles[0].channels() == 2) and 199 (audiofiles[0].bits_per_sample() == 16))): 200 # already CD quality, so no additional conversion necessary 201 temptrack = audiotools.WaveAudio.from_pcm( 202 tempwav.name, 203 audiotools.PCMReaderProgress( 204 pcmreader=(audiofiles[0].to_pcm() if 205 (write_offset == 0) else 206 audiotools.PCMReaderWindow( 207 audiofiles[0].to_pcm(), 208 write_offset, 209 audiofiles[0].total_frames())), 210 total_frames=audiofiles[0].total_frames(), 211 progress=progress.update)) 212 213 else: 214 # not CD quality, so convert and adjust total frames as needed 215 temptrack = audiotools.WaveAudio.from_pcm( 216 tempwav.name, 217 audiotools.PCMReaderProgress( 218 pcmreader=audiotools.PCMConverter( 219 pcmreader=(audiofiles[0].to_pcm() if 220 (write_offset == 0) else 221 audiotools.PCMReaderWindow( 222 audiofiles[0].to_pcm(), 223 write_offset, 224 audiofiles[0].total_frames())), 225 sample_rate=44100, 226 channels=2, 227 channel_mask=0x3, 228 bits_per_sample=16), 229 total_frames=(audiofiles[0].total_frames() * 230 44100 // 231 audiofiles[0].sample_rate()), 232 progress=progress.update)) 233 except KeyboardInterrupt: 234 progress.clear_rows() 235 tempwav.close() 236 msg.error(_.ERR_CANCELLED) 237 sys.exit(1) 238 239 progress.clear_rows() 240 241 os.chdir(os.path.dirname(tempwav.name)) 242 cdrdao_args = [audiotools.BIN["cdrdao"], "write"] 243 244 cdrdao_args.append("--device") 245 cdrdao_args.append(options.dev) 246 247 cdrdao_args.append("--speed") 248 cdrdao_args.append(str(options.speed)) 249 250 cdrdao_args.append(temptoc.name) 251 252 if options.verbosity != 'quiet': 253 subprocess.call(cdrdao_args) 254 else: 255 devnull = open(os.devnull, 'wb') 256 sub = subprocess.Popen(cdrdao_args, 257 stdout=devnull, 258 stderr=devnull) 259 sub.wait() 260 devnull.close() 261 262 temptoc.close() 263 tempwav.close() 264 265 elif options.cuesheet is not None: 266 # writing tracks with a cuesheet, 267 # so combine them into a single wave/cue and call cdrdao 268 269 if not audiotools.BIN.can_execute(audiotools.BIN['cdrdao']): 270 msg.error(_.ERR_NO_CDRDAO) 271 msg.info(_.ERR_GET_CDRDAO) 272 sys.exit(1) 273 274 if len({f.sample_rate() for f in audiofiles}) != 1: 275 msg.error(_.ERR_SAMPLE_RATE_MISMATCH) 276 sys.exit(1) 277 278 if len({f.channels() for f in audiofiles}) != 1: 279 msg.error(_.ERR_CHANNEL_COUNT_MISMATCH) 280 sys.exit(1) 281 282 if len({f.bits_per_sample() for f in audiofiles}) != 1: 283 msg.error(_.ERR_BPS_MISMATCH) 284 sys.exit(1) 285 286 try: 287 cuesheet = audiotools.read_sheet(options.cuesheet) 288 except audiotools.SheetException as err: 289 msg.error(err) 290 sys.exit(1) 291 292 # ensure sheet is formatted for CD images 293 if not cuesheet.image_formatted(): 294 msg.error(_.ERR_CUE_INVALID_FORMAT) 295 sys.exit(1) 296 297 # ensure sheet fits the tracks it's given 298 if len(cuesheet) != len(audiofiles): 299 msg.error(_.ERR_CUE_INSUFFICIENT_TRACKS) 300 sys.exit(1) 301 302 for (input_track, cuesheet_track) in zip(audiofiles, 303 cuesheet.track_numbers()): 304 track_length = cuesheet.track_length(cuesheet_track) 305 if ((track_length is not None) and 306 (track_length != input_track.seconds_length())): 307 msg.error(_.ERR_CUE_LENGTH_MISMATCH % (cuesheet_track)) 308 sys.exit(1) 309 310 tempcue = tempfile.NamedTemporaryFile(suffix='.cue') 311 tempwav = tempfile.NamedTemporaryFile(suffix='.wav') 312 313 audiotools.toc.write_tocfile( 314 cuesheet, 315 audiotools.Filename(tempwav.name).__unicode__(), 316 tempcue) 317 tempcue.flush() 318 319 input_frames = (int(audiofiles[0].sample_rate() * cuesheet.pre_gap()) + 320 sum([af.total_frames() for af in audiofiles])) 321 322 if (((audiofiles[0].sample_rate() == 44100) and 323 (audiofiles[0].channels() == 2) and 324 (audiofiles[0].bits_per_sample() == 16))): 325 pcmreader = audiotools.PCMCat( 326 [SameSample( 327 sample=0, 328 total_pcm_frames=int(44100 * cuesheet.pre_gap()), 329 sample_rate=44100, 330 channels=2, 331 channel_mask=0x3, 332 bits_per_sample=16)] + 333 [af.to_pcm() for af in audiofiles]) 334 output_frames = input_frames 335 else: 336 # this presumes a cuesheet and non-CD audio 337 # though theoretically possible, it's difficult to 338 # envision a case in which this happens 339 pcmreader = audiotools.PCMConverter( 340 pcmreader=audiotools.PCMCat( 341 [SameSample( 342 sample=0, 343 total_pcm_frames=int(audiofiles[0].sample_rate() * 344 cuesheet.pre_gap()), 345 channels=audiofiles[0].channels(), 346 channel_mask=int(audiofiles[0].channel_mask()), 347 bits_per_sample=audiofiles[0].bits_per_sample())] + 348 [af.to_pcm() for af in audiofiles]), 349 sample_rate=44100, 350 channels=2, 351 channel_mask=0x3, 352 bits_per_sample=16) 353 output_frames = (input_frames * 44100 // 354 audiofiles[0].sample_rate()) 355 356 progress = audiotools.SingleProgressDisplay( 357 msg, _.LAB_CONVERTING_FILE) 358 359 try: 360 if write_offset == 0: 361 temptrack = audiotools.WaveAudio.from_pcm( 362 tempwav.name, 363 audiotools.PCMReaderProgress( 364 pcmreader=pcmreader, 365 total_frames=output_frames, 366 progress=progress.update)) 367 else: 368 temptrack = audiotools.WaveAudio.from_pcm( 369 tempwav.name, 370 audiotools.PCMReaderProgress( 371 pcmreader=audiotools.PCMReaderWindow(pcmreader, 372 write_offset, 373 input_frames), 374 total_frames=output_frames, 375 progress=progress.update)) 376 except KeyboardInterrupt: 377 progress.clear_rows() 378 tempwav.close() 379 msg.error(_.ERR_CANCELLED) 380 sys.exit(1) 381 382 progress.clear_rows() 383 384 os.chdir(os.path.dirname(tempwav.name)) 385 386 cdrdao_args = [audiotools.BIN["cdrdao"], "write", 387 "--device", options.dev, 388 "--speed", str(options.speed), 389 tempcue.name] 390 391 if options.verbosity != 'quiet': 392 subprocess.call(cdrdao_args) 393 else: 394 devnull = open(os.devnull, 'wb') 395 sub = subprocess.Popen(cdrdao_args, 396 stdout=devnull, 397 stderr=devnull) 398 sub.wait() 399 devnull.close() 400 401 tempcue.close() 402 tempwav.close() 403 else: 404 # writing tracks without a cuesheet, 405 # so extract them to waves and call cdrecord 406 407 if not audiotools.BIN.can_execute(audiotools.BIN['cdrecord']): 408 msg.error(_.ERR_NO_CDRECORD) 409 msg.info(_.ERR_GET_CDRECORD) 410 sys.exit(1) 411 412 exec_args = [audiotools.BIN['cdrecord'], 413 "-pad", 414 "-speed", str(options.speed), 415 "-dev", options.dev, 416 "-dao", "-audio"] 417 418 temp_pool = [] 419 wave_files = [] 420 421 if write_offset == 0: 422 queue = audiotools.ExecProgressQueue(msg) 423 424 for audiofile in audiofiles: 425 if isinstance(audiofile, audiotools.WaveAudio): 426 wave_files.append(audiofile.filename) 427 else: 428 filename = audiotools.Filename(audiofile.filename) 429 f = tempfile.mkstemp(suffix='.wav') 430 temp_pool.append(f) 431 wave_files.append(f[1]) 432 queue.execute( 433 function=convert_to_wave, 434 progress_text=filename.__unicode__(), 435 completion_output=_.LAB_TRACK2CD_CONVERTED % { 436 "filename": filename}, 437 audiofile=audiofile, 438 wave_filename=f[1]) 439 440 try: 441 queue.run(max_processes) 442 except KeyboardInterrupt: 443 msg.error(_.ERR_CANCELLED) 444 for (fd, f) in temp_pool: 445 os.close(fd) 446 os.unlink(f) 447 sys.exit(1) 448 else: 449 # a list of (PCMReader, total_pcm_frames) pairs 450 try: 451 lossless_tracks = [CD_reader_size(t) for t in audiofiles] 452 453 realigned_stream = audiotools.BufferedPCMReader( 454 audiotools.PCMReaderWindow( 455 audiotools.PCMCat([t[0] for t in lossless_tracks]), 456 write_offset, 457 sum([t[1] for t in lossless_tracks]))) 458 459 for audiofile in audiofiles: 460 filename = audiotools.Filename(audiofile.filename) 461 462 progress = audiotools.SingleProgressDisplay( 463 msg, filename.__unicode__()) 464 465 f = tempfile.mkstemp(suffix=".wav") 466 temp_pool.append(f) 467 wave = audiotools.WaveAudio.from_pcm( 468 f[1], 469 audiotools.PCMReaderProgress( 470 audiotools.LimitedPCMReader( 471 realigned_stream, audiofile.total_frames()), 472 audiofile.total_frames(), 473 progress.update)) 474 wave_files.append(wave.filename) 475 progress.clear_rows() 476 msg.info(_.LAB_TRACK2CD_CONVERTED % {"filename": filename}) 477 except KeyboardInterrupt: 478 progress.clear_rows() 479 msg.error(_.ERR_CANCELLED) 480 for (fd, f) in temp_pool: 481 os.close(fd) 482 os.unlink(f) 483 sys.exit(1) 484 485 try: 486 for wave in wave_files: 487 audiotools.open(wave).verify() 488 except (audiotools.UnsupportedFile, 489 audiotools.InvalidFile, 490 IOError): 491 msg.error(_.ERR_TRACK2CD_INVALIDFILE) 492 sys.exit(1) 493 494 exec_args += wave_files 495 496 if options.verbosity != 'quiet': 497 subprocess.call(exec_args) 498 else: 499 devnull = open(os.devnull, 'wb') 500 sub = subprocess.Popen(exec_args, 501 stdout=devnull, 502 stderr=devnull) 503 sub.wait() 504 devnull.close() 505 506 for (fd, f) in temp_pool: 507 os.close(fd) 508 os.unlink(f) 509