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 21from audiotools import (AudioFile, InvalidFile) 22 23 24class InvalidMP3(InvalidFile): 25 """raised by invalid files during MP3 initialization""" 26 27 pass 28 29 30class MP3Audio(AudioFile): 31 """an MP3 audio file""" 32 33 from audiotools.text import (COMP_LAME_0, 34 COMP_LAME_6, 35 COMP_LAME_MEDIUM, 36 COMP_LAME_STANDARD, 37 COMP_LAME_EXTREME, 38 COMP_LAME_INSANE) 39 40 SUFFIX = "mp3" 41 NAME = SUFFIX 42 DESCRIPTION = u"MPEG-1 Audio Layer III" 43 DEFAULT_COMPRESSION = "2" 44 # 0 is better quality/lower compression 45 # 9 is worse quality/higher compression 46 COMPRESSION_MODES = ("0", "1", "2", "3", "4", "5", "6", 47 "medium", "standard", "extreme", "insane") 48 COMPRESSION_DESCRIPTIONS = {"0": COMP_LAME_0, 49 "6": COMP_LAME_6, 50 "medium": COMP_LAME_MEDIUM, 51 "standard": COMP_LAME_STANDARD, 52 "extreme": COMP_LAME_EXTREME, 53 "insane": COMP_LAME_INSANE} 54 55 SAMPLE_RATE = ((11025, 12000, 8000, None), # MPEG-2.5 56 (None, None, None, None), # reserved 57 (22050, 24000, 16000, None), # MPEG-2 58 (44100, 48000, 32000, None)) # MPEG-1 59 60 BIT_RATE = ( 61 # MPEG-2.5 62 ( 63 # reserved 64 (None,) * 16, 65 # layer III 66 (None, 8000, 16000, 24000, 32000, 40000, 48000, 56000, 67 64000, 80000, 96000, 112000, 128000, 144000, 160000, None), 68 # layer II 69 (None, 8000, 16000, 24000, 32000, 40000, 48000, 56000, 70 64000, 80000, 96000, 112000, 128000, 144000, 160000, None), 71 # layer I 72 (None, 32000, 48000, 56000, 64000, 80000, 96000, 112000, 73 128000, 144000, 160000, 176000, 192000, 224000, 256000, None), 74 ), 75 # reserved 76 ((None,) * 16, ) * 4, 77 # MPEG-2 78 ( 79 # reserved 80 (None,) * 16, 81 # layer III 82 (None, 8000, 16000, 24000, 32000, 40000, 48000, 56000, 83 64000, 80000, 96000, 112000, 128000, 144000, 160000, None), 84 # layer II 85 (None, 8000, 16000, 24000, 32000, 40000, 48000, 56000, 86 64000, 80000, 96000, 112000, 128000, 144000, 160000, None), 87 # layer I 88 (None, 32000, 48000, 56000, 64000, 80000, 96000, 112000, 89 128000, 144000, 160000, 176000, 192000, 224000, 256000, None)), 90 # MPEG-1 91 ( 92 # reserved 93 (None,) * 16, 94 # layer III 95 (None, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 96 112000, 128000, 160000, 192000, 224000, 256000, 320000, None), 97 # layer II 98 (None, 32000, 48000, 56000, 64000, 80000, 96000, 112000, 99 128000, 160000, 192000, 224000, 256000, 320000, 384000, None), 100 # layer I 101 (None, 32000, 64000, 96000, 128000, 160000, 192000, 224000, 102 256000, 288000, 320000, 352000, 384000, 416000, 448000, None))) 103 104 PCM_FRAMES_PER_MPEG_FRAME = (None, 1152, 1152, 384) 105 106 def __init__(self, filename): 107 """filename is a plain string""" 108 109 AudioFile.__init__(self, filename) 110 111 from audiotools.bitstream import parse 112 113 try: 114 mp3file = open(filename, "rb") 115 except IOError as msg: 116 raise InvalidMP3(str(msg)) 117 118 try: 119 try: 120 header_bytes = MP3Audio.__find_next_mp3_frame__(mp3file) 121 except IOError: 122 from audiotools.text import ERR_MP3_FRAME_NOT_FOUND 123 raise InvalidMP3(ERR_MP3_FRAME_NOT_FOUND) 124 125 (frame_sync, 126 mpeg_id, 127 layer, 128 bit_rate, 129 sample_rate, 130 pad, 131 channels) = parse("11u 2u 2u 1p 4u 2u 1u 1p 2u 6p", 132 False, 133 mp3file.read(4)) 134 135 self.__samplerate__ = self.SAMPLE_RATE[mpeg_id][sample_rate] 136 if self.__samplerate__ is None: 137 from audiotools.text import ERR_MP3_INVALID_SAMPLE_RATE 138 raise InvalidMP3(ERR_MP3_INVALID_SAMPLE_RATE) 139 if channels in (0, 1, 2): 140 self.__channels__ = 2 141 else: 142 self.__channels__ = 1 143 144 first_frame = mp3file.read(self.frame_length(mpeg_id, 145 layer, 146 bit_rate, 147 sample_rate, 148 pad) - 4) 149 150 if ((b"Xing" in first_frame) and 151 (len(first_frame[first_frame.index(b"Xing"): 152 first_frame.index(b"Xing") + 160]) == 160)): 153 # pull length from Xing header, if present 154 self.__pcm_frames__ = ( 155 parse("32p 32p 32u 32p 832p", 156 0, 157 first_frame[first_frame.index(b"Xing"): 158 first_frame.index(b"Xing") + 160])[0] * 159 self.PCM_FRAMES_PER_MPEG_FRAME[layer]) 160 else: 161 # otherwise, bounce through file frames 162 from audiotools.bitstream import BitstreamReader 163 164 reader = BitstreamReader(mp3file, False) 165 self.__pcm_frames__ = 0 166 167 try: 168 (frame_sync, 169 mpeg_id, 170 layer, 171 bit_rate, 172 sample_rate, 173 pad) = reader.parse("11u 2u 2u 1p 4u 2u 1u 9p") 174 175 while frame_sync == 0x7FF: 176 self.__pcm_frames__ += \ 177 self.PCM_FRAMES_PER_MPEG_FRAME[layer] 178 179 reader.skip_bytes(self.frame_length(mpeg_id, 180 layer, 181 bit_rate, 182 sample_rate, 183 pad) - 4) 184 185 (frame_sync, 186 mpeg_id, 187 layer, 188 bit_rate, 189 sample_rate, 190 pad) = reader.parse("11u 2u 2u 1p 4u 2u 1u 9p") 191 except IOError: 192 pass 193 except ValueError as err: 194 raise InvalidMP3(err) 195 finally: 196 mp3file.close() 197 198 def lossless(self): 199 """returns False""" 200 201 return False 202 203 def to_pcm(self): 204 """returns a PCMReader object containing the track's PCM data""" 205 206 from audiotools.decoders import MP3Decoder 207 208 return MP3Decoder(self.filename) 209 210 @classmethod 211 def from_pcm(cls, filename, pcmreader, 212 compression=None, total_pcm_frames=None): 213 """encodes a new file from PCM data 214 215 takes a filename string, PCMReader object, 216 optional compression level string and 217 optional total_pcm_frames integer 218 encodes a new audio file from pcmreader's data 219 at the given filename with the specified compression level 220 and returns a new MP3Audio object""" 221 222 from audiotools import (PCMConverter, 223 BufferedPCMReader, 224 ChannelMask, 225 __default_quality__, 226 EncodingError) 227 from audiotools.encoders import encode_mp3 228 229 if (((compression is None) or 230 (compression not in cls.COMPRESSION_MODES))): 231 compression = __default_quality__(cls.NAME) 232 233 try: 234 if total_pcm_frames is not None: 235 from audiotools import CounterPCMReader 236 pcmreader = CounterPCMReader(pcmreader) 237 238 encode_mp3(filename, 239 BufferedPCMReader( 240 PCMConverter(pcmreader, 241 sample_rate=pcmreader.sample_rate, 242 channels=min(pcmreader.channels, 2), 243 channel_mask=ChannelMask.from_channels( 244 min(pcmreader.channels, 2)), 245 bits_per_sample=16)), 246 compression) 247 248 if ((total_pcm_frames is not None) and 249 (total_pcm_frames != pcmreader.frames_written)): 250 from audiotools.text import ERR_TOTAL_PCM_FRAMES_MISMATCH 251 cls.__unlink__(filename) 252 raise EncodingError(ERR_TOTAL_PCM_FRAMES_MISMATCH) 253 254 return MP3Audio(filename) 255 except (ValueError, IOError) as err: 256 cls.__unlink__(filename) 257 raise EncodingError(str(err)) 258 finally: 259 pcmreader.close() 260 261 def bits_per_sample(self): 262 """returns an integer number of bits-per-sample this track contains""" 263 264 return 16 265 266 def channels(self): 267 """returns an integer number of channels this track contains""" 268 269 return self.__channels__ 270 271 def sample_rate(self): 272 """returns the rate of the track's audio as an integer number of Hz""" 273 274 return self.__samplerate__ 275 276 @classmethod 277 def supports_metadata(cls): 278 """returns True if this audio type supports MetaData""" 279 280 return True 281 282 def get_metadata(self): 283 """returns a MetaData object, or None 284 285 raises IOError if unable to read the file""" 286 287 from audiotools.id3 import ID3CommentPair 288 from audiotools.id3 import read_id3v2_comment 289 from audiotools.id3v1 import ID3v1Comment 290 291 with open(self.filename, "rb") as f: 292 if f.read(3) == b"ID3": 293 id3v2 = read_id3v2_comment(self.filename) 294 295 try: 296 # yes IDv2, yes ID3v1 297 return ID3CommentPair(id3v2, ID3v1Comment.parse(f)) 298 except ValueError: 299 # yes ID3v2, no ID3v1 300 return id3v2 301 else: 302 try: 303 # no ID3v2, yes ID3v1 304 return ID3v1Comment.parse(f) 305 except ValueError: 306 # no ID3v2, no ID3v1 307 return None 308 309 def update_metadata(self, metadata): 310 """takes this track's current MetaData object 311 as returned by get_metadata() and sets this track's metadata 312 with any fields updated in that object 313 314 raises IOError if unable to write the file 315 """ 316 317 import os 318 from audiotools import (TemporaryFile, 319 LimitedFileReader, 320 transfer_data) 321 from audiotools.id3 import (ID3v2Comment, ID3CommentPair) 322 from audiotools.id3v1 import ID3v1Comment 323 from audiotools.bitstream import BitstreamWriter 324 325 if metadata is None: 326 return 327 elif (not (isinstance(metadata, ID3v2Comment) or 328 isinstance(metadata, ID3CommentPair) or 329 isinstance(metadata, ID3v1Comment))): 330 from audiotools.text import ERR_FOREIGN_METADATA 331 raise ValueError(ERR_FOREIGN_METADATA) 332 elif not os.access(self.filename, os.W_OK): 333 raise IOError(self.filename) 334 335 new_mp3 = TemporaryFile(self.filename) 336 337 # get the original MP3 data 338 old_mp3 = open(self.filename, "rb") 339 MP3Audio.__find_last_mp3_frame__(old_mp3) 340 data_end = old_mp3.tell() 341 old_mp3.seek(0, 0) 342 MP3Audio.__find_mp3_start__(old_mp3) 343 data_start = old_mp3.tell() 344 old_mp3 = LimitedFileReader(old_mp3, data_end - data_start) 345 346 # write id3v2 + data + id3v1 to file 347 if isinstance(metadata, ID3CommentPair): 348 metadata.id3v2.build(BitstreamWriter(new_mp3, False)) 349 transfer_data(old_mp3.read, new_mp3.write) 350 metadata.id3v1.build(new_mp3) 351 elif isinstance(metadata, ID3v2Comment): 352 metadata.build(BitstreamWriter(new_mp3, False)) 353 transfer_data(old_mp3.read, new_mp3.write) 354 elif isinstance(metadata, ID3v1Comment): 355 transfer_data(old_mp3.read, new_mp3.write) 356 metadata.build(new_mp3) 357 358 # commit change to disk 359 old_mp3.close() 360 new_mp3.close() 361 362 def set_metadata(self, metadata): 363 """takes a MetaData object and sets this track's metadata 364 365 this metadata includes track name, album name, and so on 366 raises IOError if unable to write the file""" 367 368 from audiotools.id3 import ID3v2Comment 369 from audiotools.id3 import ID3v22Comment 370 from audiotools.id3 import ID3v23Comment 371 from audiotools.id3 import ID3v24Comment 372 from audiotools.id3 import ID3CommentPair 373 from audiotools.id3v1 import ID3v1Comment 374 375 if metadata is None: 376 return self.delete_metadata() 377 378 if (not (isinstance(metadata, ID3v2Comment) or 379 isinstance(metadata, ID3CommentPair) or 380 isinstance(metadata, ID3v1Comment))): 381 from audiotools import config 382 383 DEFAULT_ID3V2 = "id3v2.3" 384 DEFAULT_ID3V1 = "id3v1.1" 385 386 id3v2_class = {"id3v2.2": ID3v22Comment, 387 "id3v2.3": ID3v23Comment, 388 "id3v2.4": ID3v24Comment, 389 "none": None}.get(config.get_default("ID3", 390 "id3v2", 391 DEFAULT_ID3V2), 392 DEFAULT_ID3V2) 393 id3v1_class = {"id3v1.1": ID3v1Comment, 394 "none": None}.get(config.get_default("ID3", 395 "id3v1", 396 DEFAULT_ID3V1), 397 DEFAULT_ID3V1) 398 if (id3v2_class is not None) and (id3v1_class is not None): 399 self.update_metadata( 400 ID3CommentPair.converted(metadata, 401 id3v2_class=id3v2_class, 402 id3v1_class=id3v1_class)) 403 elif id3v2_class is not None: 404 self.update_metadata(id3v2_class.converted(metadata)) 405 elif id3v1_class is not None: 406 self.update_metadata(id3v1_class.converted(metadata)) 407 else: 408 return 409 else: 410 self.update_metadata(metadata) 411 412 def delete_metadata(self): 413 """deletes the track's MetaData 414 415 this removes or unsets tags as necessary in order to remove all data 416 raises IOError if unable to write the file""" 417 418 import os 419 from audiotools import (TemporaryFile, 420 LimitedFileReader, 421 transfer_data) 422 423 # this works a lot like update_metadata 424 # but without any new metadata to set 425 426 if not os.access(self.filename, os.W_OK): 427 raise IOError(self.filename) 428 429 new_mp3 = TemporaryFile(self.filename) 430 431 # get the original MP3 data 432 old_mp3 = open(self.filename, "rb") 433 MP3Audio.__find_last_mp3_frame__(old_mp3) 434 data_end = old_mp3.tell() 435 old_mp3.seek(0, 0) 436 MP3Audio.__find_mp3_start__(old_mp3) 437 data_start = old_mp3.tell() 438 old_mp3 = LimitedFileReader(old_mp3, data_end - data_start) 439 440 # write data to file 441 transfer_data(old_mp3.read, new_mp3.write) 442 443 # commit change to disk 444 old_mp3.close() 445 new_mp3.close() 446 447 def clean(self, output_filename=None): 448 """cleans the file of known data and metadata problems 449 450 output_filename is an optional filename of the fixed file 451 if present, a new AudioFile is written to that path 452 otherwise, only a dry-run is performed and no new file is written 453 454 return list of fixes performed as Unicode strings 455 456 raises IOError if unable to write the file or its metadata 457 raises ValueError if the file has errors of some sort 458 """ 459 460 from audiotools.id3 import total_id3v2_comments 461 from audiotools import transfer_data 462 from audiotools import open as open_audiofile 463 from audiotools.text import CLEAN_REMOVE_DUPLICATE_ID3V2 464 465 with open(self.filename, "rb") as f: 466 if total_id3v2_comments(f) > 1: 467 file_fixes = [CLEAN_REMOVE_DUPLICATE_ID3V2] 468 else: 469 file_fixes = [] 470 471 if output_filename is None: 472 # dry run only 473 metadata = self.get_metadata() 474 if metadata is not None: 475 (metadata, fixes) = metadata.clean() 476 return file_fixes + fixes 477 else: 478 return file_fixes 479 else: 480 # perform complete fix 481 input_f = open(self.filename, "rb") 482 output_f = open(output_filename, "wb") 483 try: 484 transfer_data(input_f.read, output_f.write) 485 finally: 486 input_f.close() 487 output_f.close() 488 489 new_track = open_audiofile(output_filename) 490 metadata = self.get_metadata() 491 if metadata is not None: 492 (metadata, fixes) = metadata.clean() 493 if len(file_fixes + fixes) > 0: 494 # only update metadata if fixes are actually performed 495 new_track.update_metadata(metadata) 496 return file_fixes + fixes 497 else: 498 return file_fixes 499 500 # places mp3file at the position of the next MP3 frame's start 501 @classmethod 502 def __find_next_mp3_frame__(cls, mp3file): 503 from audiotools.id3 import skip_id3v2_comment 504 505 # if we're starting at an ID3v2 header, skip it to save a bunch of time 506 bytes_skipped = skip_id3v2_comment(mp3file) 507 508 # then find the next mp3 frame 509 from audiotools.bitstream import BitstreamReader 510 511 reader = BitstreamReader(mp3file, False) 512 pos = reader.getpos() 513 try: 514 (sync, 515 mpeg_id, 516 layer_description) = reader.parse("11u 2u 2u 1p") 517 except IOError as err: 518 raise err 519 520 while (not ((sync == 0x7FF) and 521 (mpeg_id in (0, 2, 3)) and 522 (layer_description in (1, 2, 3)))): 523 reader.setpos(pos) 524 reader.skip(8) 525 bytes_skipped += 1 526 pos = reader.getpos() 527 try: 528 (sync, 529 mpeg_id, 530 layer_description) = reader.parse("11u 2u 2u 1p") 531 except IOError as err: 532 raise err 533 else: 534 reader.setpos(pos) 535 return bytes_skipped 536 537 @classmethod 538 def __find_mp3_start__(cls, mp3file): 539 """places mp3file at the position of the MP3 file's start""" 540 541 from audiotools.id3 import skip_id3v2_comment 542 543 # if we're starting at an ID3v2 header, skip it to save a bunch of time 544 skip_id3v2_comment(mp3file) 545 546 from audiotools.bitstream import BitstreamReader 547 548 reader = BitstreamReader(mp3file, False) 549 550 # skip over any bytes that aren't a valid MPEG header 551 pos = reader.getpos() 552 (frame_sync, mpeg_id, layer) = reader.parse("11u 2u 2u 1p") 553 while (not ((frame_sync == 0x7FF) and 554 (mpeg_id in (0, 2, 3)) and 555 (layer in (1, 2, 3)))): 556 reader.setpos(pos) 557 reader.skip(8) 558 pos = reader.getpos() 559 reader.setpos(pos) 560 561 @classmethod 562 def __find_last_mp3_frame__(cls, mp3file): 563 """places mp3file at the position of the last MP3 frame's end 564 565 (either the last byte in the file or just before the ID3v1 tag) 566 this may not be strictly accurate if ReplayGain data is present, 567 since APEv2 tags came before the ID3v1 tag, 568 but we're not planning to change that tag anyway 569 """ 570 571 mp3file.seek(-128, 2) 572 if mp3file.read(3) == b'TAG': 573 mp3file.seek(-128, 2) 574 return 575 else: 576 mp3file.seek(0, 2) 577 return 578 579 def frame_length(self, mpeg_id, layer, bit_rate, sample_rate, pad): 580 """returns the total MP3 frame length in bytes 581 582 the given arguments are the header's bit values 583 mpeg_id = 2 bits 584 layer = 2 bits 585 bit_rate = 4 bits 586 sample_rate = 2 bits 587 pad = 1 bit 588 """ 589 590 sample_rate = self.SAMPLE_RATE[mpeg_id][sample_rate] 591 if sample_rate is None: 592 from audiotools.text import ERR_MP3_INVALID_SAMPLE_RATE 593 raise ValueError(ERR_MP3_INVALID_SAMPLE_RATE) 594 bit_rate = self.BIT_RATE[mpeg_id][layer][bit_rate] 595 if bit_rate is None: 596 from audiotools.text import ERR_MP3_INVALID_BIT_RATE 597 raise ValueError(ERR_MP3_INVALID_BIT_RATE) 598 if layer == 3: # layer I 599 return (((12 * bit_rate) // sample_rate) + pad) * 4 600 else: # layer II/III 601 return ((144 * bit_rate) // sample_rate) + pad 602 603 def total_frames(self): 604 """returns the total PCM frames of the track as an integer""" 605 606 return self.__pcm_frames__ 607 608 @classmethod 609 def available(cls, system_binaries): 610 """returns True if all necessary compenents are available 611 to support format""" 612 613 try: 614 from audiotools.decoders import MP3Decoder 615 from audiotools.encoders import encode_mp3 616 617 return True 618 except ImportError: 619 return False 620 621 @classmethod 622 def missing_components(cls, messenger): 623 """given a Messenger object, displays missing binaries or libraries 624 needed to support this format and where to get them""" 625 626 from audiotools.text import (ERR_LIBRARY_NEEDED, 627 ERR_LIBRARY_DOWNLOAD_URL, 628 ERR_PROGRAM_PACKAGE_MANAGER) 629 630 format_ = cls.NAME.decode('ascii') 631 632 # display where to get libmp3lame 633 messenger.info( 634 ERR_LIBRARY_NEEDED % 635 {"library": u"\"libmp3lame\"", 636 "format": format_}) 637 messenger.info( 638 ERR_LIBRARY_DOWNLOAD_URL % 639 {"library": u"mp3lame", 640 "url": "http://lame.sourceforge.net/"}) 641 642 # then display where to get libmpg123 643 messenger.info( 644 ERR_LIBRARY_NEEDED % 645 {"library": u"\"libmpg123\"", 646 "format": format_}) 647 messenger.info( 648 ERR_LIBRARY_DOWNLOAD_URL % 649 {"library": u"mpg123", 650 "url": u"http://www.mpg123.org/"}) 651 652 messenger.info(ERR_PROGRAM_PACKAGE_MANAGER) 653 654 655class MP2Audio(MP3Audio): 656 """an MP2 audio file""" 657 658 from audiotools.text import (COMP_TWOLAME_64, 659 COMP_TWOLAME_384) 660 661 SUFFIX = "mp2" 662 NAME = SUFFIX 663 DESCRIPTION = u"MPEG-1 Audio Layer II" 664 DEFAULT_COMPRESSION = str(192) 665 COMPRESSION_MODES = tuple(map(str, (64, 96, 112, 128, 160, 192, 666 224, 256, 320, 384))) 667 COMPRESSION_DESCRIPTIONS = {"64": COMP_TWOLAME_64, 668 "384": COMP_TWOLAME_384} 669 670 @classmethod 671 def from_pcm(cls, filename, pcmreader, 672 compression=None, total_pcm_frames=None): 673 """encodes a new file from PCM data 674 675 takes a filename string, PCMReader object, 676 optional compression level string and 677 optional total_pcm_frames integer 678 encodes a new audio file from pcmreader's data 679 at the given filename with the specified compression level 680 and returns a new MP2Audio object""" 681 682 from audiotools import (PCMConverter, 683 BufferedPCMReader, 684 ChannelMask, 685 __default_quality__, 686 EncodingError) 687 from audiotools.encoders import encode_mp2 688 import bisect 689 690 if (((compression is None) or 691 (compression not in cls.COMPRESSION_MODES))): 692 compression = __default_quality__(cls.NAME) 693 694 if pcmreader.sample_rate in (32000, 48000, 44100): 695 sample_rate = pcmreader.sample_rate 696 else: 697 sample_rate = [32000, 698 32000, 699 44100, 700 48000][bisect.bisect([32000, 701 44100, 702 48000], 703 pcmreader.sample_rate)] 704 705 if total_pcm_frames is not None: 706 from audiotools import CounterPCMReader 707 pcmreader = CounterPCMReader(pcmreader) 708 709 try: 710 encode_mp2(filename, 711 BufferedPCMReader( 712 PCMConverter(pcmreader, 713 sample_rate=sample_rate, 714 channels=min(pcmreader.channels, 2), 715 channel_mask=ChannelMask.from_channels( 716 min(pcmreader.channels, 2)), 717 bits_per_sample=16)), 718 int(compression)) 719 720 if ((total_pcm_frames is not None) and 721 (total_pcm_frames != pcmreader.frames_written)): 722 from audiotools.text import ERR_TOTAL_PCM_FRAMES_MISMATCH 723 cls.__unlink__(filename) 724 raise EncodingError(ERR_TOTAL_PCM_FRAMES_MISMATCH) 725 726 return MP2Audio(filename) 727 except (ValueError, IOError) as err: 728 cls.__unlink__(filename) 729 raise EncodingError(str(err)) 730 finally: 731 pcmreader.close() 732 733 @classmethod 734 def available(cls, system_binaries): 735 """returns True if all necessary compenents are available 736 to support format""" 737 738 try: 739 from audiotools.decoders import MP3Decoder 740 from audiotools.encoders import encode_mp2 741 742 return True 743 except ImportError: 744 return False 745 746 @classmethod 747 def missing_components(cls, messenger): 748 """given a Messenger object, displays missing binaries or libraries 749 needed to support this format and where to get them""" 750 751 from audiotools.text import (ERR_LIBRARY_NEEDED, 752 ERR_LIBRARY_DOWNLOAD_URL, 753 ERR_PROGRAM_PACKAGE_MANAGER) 754 755 format_ = cls.NAME.decode('ascii') 756 757 # display where to get libtwo,ame 758 messenger.info( 759 ERR_LIBRARY_NEEDED % 760 {"library": u"\"libtwolame\"", 761 "format": format_}) 762 messenger.info( 763 ERR_LIBRARY_DOWNLOAD_URL % 764 {"library": u"twolame", 765 "url": "http://twolame.sourceforge.net/"}) 766 767 # then display where to get libmpg123 768 messenger.info( 769 ERR_LIBRARY_NEEDED % 770 {"library": u"\"libmpg123\"", 771 "format": format_}) 772 messenger.info( 773 ERR_LIBRARY_DOWNLOAD_URL % 774 {"library": u"mpg123", 775 "url": u"http://www.mpg123.org/"}) 776 777 messenger.info(ERR_PROGRAM_PACKAGE_MANAGER) 778