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