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 20import sys 21import os 22import os.path 23import audiotools 24import audiotools.cue 25import audiotools.ui 26import audiotools.text as _ 27import termios 28from fractions import Fraction 29 30 31def split(progress, source_audiofile, destination_filename, 32 destination_class, compression, metadata, 33 pcm_frames_offset, total_pcm_frames): 34 try: 35 pcmreader = source_audiofile.to_pcm() 36 37 # if PCMReader has seek() 38 # use it to reduce the amount of frames to skip 39 if hasattr(pcmreader, "seek") and callable(pcmreader.seek): 40 pcm_frames_offset -= pcmreader.seek(pcm_frames_offset) 41 42 destination_audiofile = destination_class.from_pcm( 43 str(destination_filename), 44 audiotools.PCMReaderProgress( 45 audiotools.PCMReaderWindow(pcmreader, 46 pcm_frames_offset, 47 total_pcm_frames), 48 total_pcm_frames, 49 progress), 50 compression, 51 total_pcm_frames) 52 53 if metadata is not None: 54 destination_audiofile.set_metadata(metadata) 55 except KeyboardInterrupt: 56 # remove partially split file, if any 57 try: 58 os.unlink(str(destination_filename)) 59 except OSError: 60 pass 61 62 return str(destination_filename) 63 64 65def split_raw(progress, source_filename, 66 sample_rate, channels, channel_mask, bits_per_sample, 67 destination_filename, destination_class, compression, 68 metadata, pcm_frames_offset, total_pcm_frames): 69 f = open(source_filename, "rb") 70 try: 71 # skip initial offset 72 f.seek(pcm_frames_offset * channels * (bits_per_sample // 8)) 73 74 destination_audiofile = destination_class.from_pcm( 75 str(destination_filename), 76 audiotools.PCMReaderProgress( 77 audiotools.PCMReaderHead( 78 audiotools.PCMFileReader(file=f, 79 sample_rate=sample_rate, 80 channels=channels, 81 channel_mask=channel_mask, 82 bits_per_sample=bits_per_sample), 83 total_pcm_frames), 84 total_pcm_frames, 85 progress), 86 compression, 87 total_pcm_frames) 88 89 if metadata is not None: 90 destination_audiofile.set_metadata(metadata) 91 92 return str(destination_filename) 93 except KeyboardInterrupt: 94 # remove partially split file, if any 95 try: 96 os.unlink(str(destination_filename)) 97 except OSError: 98 pass 99 finally: 100 f.close() 101 102 103if (__name__ == '__main__'): 104 import argparse 105 106 parser = argparse.ArgumentParser(description=_.DESCRIPTION_TRACKSPLIT) 107 108 parser.add_argument("--version", 109 action="version", 110 version="Python Audio Tools %s" % (audiotools.VERSION)) 111 112 parser.add_argument("-I", "--interactive", 113 action="store_true", 114 default=False, 115 dest="interactive", 116 help=_.OPT_INTERACTIVE_OPTIONS) 117 118 parser.add_argument("--cue", 119 dest="cuesheet", 120 metavar="FILENAME", 121 help=_.OPT_CUESHEET_TRACKSPLIT) 122 123 parser.add_argument("-V", "--verbose", 124 dest="verbosity", 125 choices=audiotools.VERBOSITY_LEVELS, 126 default=audiotools.DEFAULT_VERBOSITY, 127 help=_.OPT_VERBOSE) 128 129 conversion = parser.add_argument_group(_.OPT_CAT_ENCODING) 130 131 conversion.add_argument("-t", "--type", 132 dest="type", 133 choices=list(sorted(audiotools.TYPE_MAP.keys())) + 134 ["help"], 135 help=_.OPT_TYPE) 136 137 conversion.add_argument("-q", "--quality", 138 dest="quality", 139 help=_.OPT_QUALITY) 140 141 conversion.add_argument("-d", "--dir", 142 dest="dir", 143 default=".", 144 help=_.OPT_DIR) 145 146 conversion.add_argument("--format", 147 default=None, 148 dest="format", 149 help=_.OPT_FORMAT) 150 151 conversion.add_argument("-j", "--joint", 152 type=int, 153 default=audiotools.MAX_JOBS, 154 dest="max_processes", 155 help=_.OPT_JOINT) 156 157 lookup = parser.add_argument_group(_.OPT_CAT_CD_LOOKUP) 158 159 lookup.add_argument("--musicbrainz-server", 160 dest="musicbrainz_server", 161 default=audiotools.MUSICBRAINZ_SERVER, 162 metavar="HOSTNAME") 163 164 lookup.add_argument("--musicbrainz-port", 165 type=int, 166 dest="musicbrainz_port", 167 default=audiotools.MUSICBRAINZ_PORT, 168 metavar="PORT") 169 170 lookup.add_argument("--no-musicbrainz", 171 action="store_false", 172 dest="use_musicbrainz", 173 default=audiotools.MUSICBRAINZ_SERVICE, 174 help=_.OPT_NO_MUSICBRAINZ) 175 176 lookup.add_argument("--freedb-server", 177 dest="freedb_server", 178 default=audiotools.FREEDB_SERVER, 179 metavar="HOSTNAME") 180 181 lookup.add_argument("--freedb-port", 182 type=int, 183 dest="freedb_port", 184 default=audiotools.FREEDB_PORT, 185 metavar="PORT") 186 187 lookup.add_argument("--no-freedb", 188 action="store_false", 189 dest="use_freedb", 190 default=audiotools.FREEDB_SERVICE, 191 help=_.OPT_NO_FREEDB) 192 193 lookup.add_argument("-D", "--default", 194 dest="use_default", 195 action="store_true", 196 default=False, 197 help=_.OPT_DEFAULT) 198 199 metadata = parser.add_argument_group(_.OPT_CAT_METADATA) 200 201 metadata.add_argument("--album-number", 202 dest="album_number", 203 type=int, 204 default=0, 205 help=_.OPT_ALBUM_NUMBER) 206 207 metadata.add_argument("--album-total", 208 dest="album_total", 209 type=int, 210 default=0, 211 help=_.OPT_ALBUM_TOTAL) 212 213 metadata.add_argument("--replay-gain", 214 action="store_true", 215 default=None, 216 dest="add_replay_gain", 217 help=_.OPT_REPLAY_GAIN) 218 219 metadata.add_argument("--no-replay-gain", 220 action="store_false", 221 default=None, 222 dest="add_replay_gain", 223 help=_.OPT_NO_REPLAY_GAIN) 224 225 parser.add_argument("filename", 226 metavar="FILENAME", 227 help=_.OPT_INPUT_FILENAME) 228 229 options = parser.parse_args() 230 231 msg = audiotools.Messenger(options.verbosity == "quiet") 232 233 # ensure interactive mode is available, if selected 234 if options.interactive and (not audiotools.ui.AVAILABLE): 235 audiotools.ui.not_available_message(msg) 236 sys.exit(1) 237 238 # get the AudioFile class we are converted to 239 if options.type == 'help': 240 audiotools.ui.show_available_formats(msg) 241 sys.exit(0) 242 if options.type is not None: 243 AudioType = audiotools.TYPE_MAP[options.type] 244 else: 245 AudioType = audiotools.TYPE_MAP[audiotools.DEFAULT_TYPE] 246 247 # ensure the selected compression is compatible with that class 248 if options.quality == 'help': 249 audiotools.ui.show_available_qualities(msg, AudioType) 250 sys.exit(0) 251 elif options.quality is None: 252 options.quality = audiotools.__default_quality__(AudioType.NAME) 253 elif options.quality not in AudioType.COMPRESSION_MODES: 254 msg.error(_.ERR_UNSUPPORTED_COMPRESSION_MODE % 255 {"quality": options.quality, 256 "type": AudioType.NAME}) 257 sys.exit(1) 258 259 audiofiles = audiotools.open_files([options.filename], 260 sorted=False, 261 messenger=msg) 262 if len(audiofiles) != 1: 263 msg.error(_.ERR_1_FILE_REQUIRED) 264 sys.exit(1) 265 else: 266 audiofile = audiofiles[0] 267 input_filename = audiotools.Filename(audiofile.filename) 268 input_filenames = {input_filename} 269 270 base_directory = options.dir 271 encoded_filenames = [] 272 273 if options.cuesheet is not None: 274 # grab the cuesheet we're using to split tracks 275 # (this overrides an embedded cuesheet) 276 try: 277 cuesheet = audiotools.read_sheet(options.cuesheet) 278 input_filenames.add(audiotools.Filename(options.cuesheet)) 279 except audiotools.SheetException as err: 280 msg.error(err) 281 sys.exit(1) 282 else: 283 cuesheet = audiofile.get_cuesheet() 284 if cuesheet is None: 285 msg.error(_.ERR_TRACKSPLIT_NO_CUESHEET) 286 sys.exit(1) 287 288 if not cuesheet.image_formatted(): 289 msg.error(_.ERR_CUE_INVALID_FORMAT) 290 sys.exit(1) 291 292 cuesheet_offsets = [cuesheet.track_offset(t) for t in 293 cuesheet.track_numbers()] 294 295 cuesheet_lengths = [cuesheet.track_length(t) for t in 296 cuesheet.track_numbers()] 297 298 audiofile_size = audiofile.seconds_length() 299 300 if cuesheet_lengths[-1] is None: 301 # last track length unknown, so ensure there's enough room 302 # based on the total size of the source 303 last_track_size = (audiofile_size - 304 sum(cuesheet_lengths[0:-1]) - 305 cuesheet.pre_gap()) 306 if last_track_size >= 0: 307 cuesheet_lengths.pop() 308 cuesheet_lengths.append(last_track_size) 309 else: 310 msg.error(_.ERR_TRACKSPLIT_OVERLONG_CUESHEET) 311 sys.exit(1) 312 else: 313 # last track length is known, so ensure the size 314 # of all lengths and the pre-gap is the same size as the source 315 if (cuesheet.pre_gap() + sum(cuesheet_lengths)) != audiofile_size: 316 msg.error(_.ERR_TRACKSPLIT_OVERLONG_CUESHEET) 317 sys.exit(1) 318 319 output_track_count = len(cuesheet) 320 321 # use cuesheet to query metadata services for metadata choices 322 try: 323 metadata_choices = audiotools.sheet_metadata_lookup( 324 sheet=cuesheet, 325 total_pcm_frames=audiofile.total_frames(), 326 sample_rate=audiofile.sample_rate(), 327 musicbrainz_server=options.musicbrainz_server, 328 musicbrainz_port=options.musicbrainz_port, 329 freedb_server=options.freedb_server, 330 freedb_port=options.freedb_port, 331 use_musicbrainz=options.use_musicbrainz, 332 use_freedb=options.use_freedb) 333 except KeyboardInterrupt: 334 msg.ansi_clearline() 335 msg.error(_.ERR_CANCELLED) 336 sys.exit(1) 337 338 # populate any empty Album-level metadata fields 339 # with those from original file, if any 340 album_metadata = audiofile.get_metadata() 341 if album_metadata is not None: 342 album_fields = {attr: field for (attr, field) in 343 album_metadata.filled_fields() 344 if (attr in {"album_name", 345 "artist_name", 346 "performer_name", 347 "composer_name", 348 "conductor_name", 349 "media", 350 "catalog", 351 "copyright", 352 "publisher", 353 "year", 354 "date", 355 "album_number", 356 "album_total", 357 "comment"})} 358 359 for c in metadata_choices: 360 for m in c: 361 for (attr, field) in m.empty_fields(): 362 if attr in album_fields: 363 setattr(m, attr, album_fields[attr]) 364 365 # populate any remaining metadata fields 366 # with those from cuesheet, if any 367 cuesheet_metadata = cuesheet.get_metadata() 368 for c in metadata_choices: 369 for (track_metadata, sheet_track) in zip(c, cuesheet): 370 sheet_track_metadata = sheet_track.get_metadata() 371 for (attr, field) in track_metadata.empty_fields(): 372 if ((sheet_track_metadata is not None) and 373 (getattr(sheet_track_metadata, attr) is not None)): 374 setattr(track_metadata, 375 attr, 376 getattr(sheet_track_metadata, attr)) 377 elif ((cuesheet_metadata is not None) and 378 (getattr(cuesheet_metadata, attr) is not None)): 379 setattr(track_metadata, 380 attr, 381 getattr(cuesheet_metadata, attr)) 382 383 # update MetaData with command-line album-number/total, if given 384 if options.album_number != 0: 385 for c in metadata_choices: 386 for m in c: 387 m.album_number = options.album_number 388 389 if options.album_total != 0: 390 for c in metadata_choices: 391 for m in c: 392 m.album_total = options.album_total 393 394 # decide which metadata and output options to use when splitting tracks 395 if options.interactive: 396 # pick choice using interactive widget 397 output_widget = audiotools.ui.OutputFiller( 398 track_labels=[_.LAB_TRACK_X_OF_Y % (i + 1, output_track_count) 399 for i in range(output_track_count)], 400 metadata_choices=metadata_choices, 401 input_filenames=[input_filename for i in 402 range(output_track_count)], 403 output_directory=options.dir, 404 format_string=(options.format if 405 (options.format is not None) else 406 audiotools.FILENAME_FORMAT), 407 output_class=AudioType, 408 quality=options.quality, 409 completion_label=_.LAB_TRACKSPLIT_APPLY) 410 411 loop = audiotools.ui.urwid.MainLoop( 412 output_widget, 413 audiotools.ui.style(), 414 unhandled_input=output_widget.handle_text, 415 pop_ups=True) 416 try: 417 loop.run() 418 msg.ansi_clearscreen() 419 except (termios.error, IOError): 420 msg.error(_.ERR_TERMIOS_ERROR) 421 msg.info(_.ERR_TERMIOS_SUGGESTION) 422 msg.info(audiotools.ui.xargs_suggestion(sys.argv)) 423 sys.exit(1) 424 425 if not output_widget.cancelled(): 426 output_tracks = list(output_widget.output_tracks()) 427 else: 428 sys.exit(0) 429 else: 430 # pick choice without using GUI 431 try: 432 output_tracks = list( 433 audiotools.ui.process_output_options( 434 metadata_choices=metadata_choices, 435 input_filenames=[input_filename for i in 436 range(output_track_count)], 437 output_directory=options.dir, 438 format_string=options.format, 439 output_class=AudioType, 440 quality=options.quality, 441 msg=msg, 442 use_default=options.use_default)) 443 except audiotools.UnsupportedTracknameField as err: 444 err.error_msg(msg) 445 sys.exit(1) 446 except (audiotools.InvalidFilenameFormat, 447 audiotools.OutputFileIsInput, 448 audiotools.DuplicateOutputFile) as err: 449 msg.error(err) 450 sys.exit(1) 451 452 # perform actual track splitting and tagging 453 jobs = zip(cuesheet_offsets, cuesheet_lengths, output_tracks) 454 455 queue = audiotools.ExecProgressQueue(msg) 456 457 if audiofile.seekable(): 458 for (offset, length, (output_class, 459 output_filename, 460 output_quality, 461 output_metadata)) in jobs: 462 try: 463 audiotools.make_dirs(str(output_filename)) 464 except OSError as err: 465 msg.os_error(err) 466 sys.exit(1) 467 468 queue.execute( 469 function=split, 470 progress_text=output_filename.__unicode__(), 471 completion_output=_.LAB_ENCODE % { 472 "source": audiotools.Filename(audiofile.filename), 473 "destination": output_filename}, 474 source_audiofile=audiofile, 475 destination_filename=output_filename, 476 destination_class=output_class, 477 compression=output_quality, 478 metadata=output_metadata, 479 pcm_frames_offset=int(offset * audiofile.sample_rate()), 480 total_pcm_frames=int(length * audiofile.sample_rate())) 481 482 try: 483 encoded_tracks = audiotools.open_files( 484 queue.run(options.max_processes)) 485 except audiotools.EncodingError as err: 486 msg.error(err) 487 sys.exit(1) 488 except KeyboardInterrupt: 489 msg.error(_.ERR_CANCELLED) 490 sys.exit(1) 491 else: 492 import tempfile 493 494 # if input file isn't seekable 495 496 # decode it into a single PCM blob of binary data 497 temp_blob = tempfile.NamedTemporaryFile() 498 cache_progress = audiotools.SingleProgressDisplay( 499 msg, _.LAB_CACHING_FILE) 500 try: 501 audiotools.transfer_framelist_data( 502 audiotools.PCMReaderProgress( 503 audiofile.to_pcm(), 504 audiofile.total_frames(), 505 cache_progress.update), 506 temp_blob.write) 507 except audiotools.DecodingError as err: 508 cache_progress.clear_rows() 509 msg.error(err) 510 temp_blob.close() 511 sys.exit(1) 512 except KeyboardInterrupt: 513 cache_progress.clear_rows() 514 msg.error(_.ERR_CANCELLED) 515 temp_blob.close() 516 sys.exit(1) 517 518 cache_progress.clear_rows() 519 temp_blob.flush() 520 521 # split the blob using multiple jobs 522 for (offset, length, (output_class, 523 output_filename, 524 output_quality, 525 output_metadata)) in jobs: 526 try: 527 audiotools.make_dirs(str(output_filename)) 528 except OSError as err: 529 msg.os_error(err) 530 temp_blob.close() 531 sys.exit(1) 532 533 queue.execute( 534 function=split_raw, 535 progress_text=output_filename.__unicode__(), 536 completion_output=_.LAB_ENCODE % { 537 "source": audiotools.Filename(audiofile.filename), 538 "destination": output_filename}, 539 source_filename=temp_blob.name, 540 sample_rate=audiofile.sample_rate(), 541 channels=audiofile.channels(), 542 channel_mask=int(audiofile.channel_mask()), 543 bits_per_sample=audiofile.bits_per_sample(), 544 destination_filename=output_filename, 545 destination_class=output_class, 546 compression=output_quality, 547 metadata=output_metadata, 548 pcm_frames_offset=int(offset * audiofile.sample_rate()), 549 total_pcm_frames=int(length * audiofile.sample_rate())) 550 551 try: 552 encoded_tracks = audiotools.open_files( 553 queue.run(options.max_processes)) 554 except audiotools.EncodingError as err: 555 msg.error(err) 556 temp_blob.close() 557 sys.exit(1) 558 except KeyboardInterrupt: 559 msg.error(_.ERR_CANCELLED) 560 temp_blob.close() 561 sys.exit(1) 562 563 # then delete the blob when finished 564 temp_blob.close() 565 566 # apply ReplayGain to split tracks, if requested 567 if (output_class.supports_replay_gain() and 568 (options.add_replay_gain if options.add_replay_gain is not None else 569 audiotools.ADD_REPLAYGAIN)): 570 rg_progress = audiotools.ReplayGainProgressDisplay(msg) 571 rg_progress.initial_message() 572 try: 573 # separate encoded files by album_name and album_number 574 for album in audiotools.group_tracks(encoded_tracks): 575 # add ReplayGain to groups of files 576 # belonging to the same album 577 578 audiotools.add_replay_gain(album, rg_progress.update) 579 except ValueError as err: 580 rg_progress.clear_rows() 581 msg.error(err) 582 sys.exit(1) 583 except KeyboardInterrupt: 584 rg_progress.clear_rows() 585 msg.error(_.ERR_CANCELLED) 586 sys.exit(1) 587 rg_progress.final_message() 588