1# -*- coding: utf-8 -*-
2
3import os
4
5from tests import TestCase, DATA_DIR, get_temp_copy
6from mutagen._compat import cBytesIO, text_type, xrange
7from mutagen.mp3 import MP3, error as MP3Error, delete, MPEGInfo, EasyMP3, \
8    BitrateMode, iter_sync
9from mutagen.mp3._util import XingHeader, XingHeaderError, VBRIHeader, \
10    VBRIHeaderError, LAMEHeader, LAMEError
11from mutagen.id3 import ID3
12
13
14class TMP3Util(TestCase):
15
16    def test_find_sync(self):
17
18        def get_syncs(fileobj, max_read):
19            start = fileobj.tell()
20            pos = []
21            for i in iter_sync(fileobj, max_read):
22                pos.append(fileobj.tell() - start)
23            return pos
24
25        self.assertEqual(get_syncs(cBytesIO(b"abc"), 100), [])
26        self.assertEqual(get_syncs(cBytesIO(b""), 100), [])
27        self.assertEqual(get_syncs(cBytesIO(b"a\xff\xe0"), 1), [])
28
29        self.assertEqual(get_syncs(cBytesIO(b"a\xff\xc0\xff\xe0"), 100), [3])
30        self.assertEqual(
31            get_syncs(cBytesIO(b"a\xff\xe0\xff\xe0\xff\xe0"), 100), [1, 3, 5])
32
33        for i in xrange(400):
34            fileobj = cBytesIO(b"\x00" * i + b"\xff\xe0")
35            self.assertEqual(get_syncs(fileobj, 100 + i), [i])
36
37
38class TMP3(TestCase):
39    silence = os.path.join(DATA_DIR, 'silence-44-s.mp3')
40    silence_nov2 = os.path.join(DATA_DIR, 'silence-44-s-v1.mp3')
41    silence_mpeg2 = os.path.join(DATA_DIR, 'silence-44-s-mpeg2.mp3')
42    silence_mpeg25 = os.path.join(DATA_DIR, 'silence-44-s-mpeg25.mp3')
43    lame = os.path.join(DATA_DIR, 'lame.mp3')
44    lame_peak = os.path.join(DATA_DIR, 'lame-peak.mp3')
45    lame_broken_short = os.path.join(DATA_DIR, 'lame397v9short.mp3')
46
47    def setUp(self):
48        self.filename = get_temp_copy(
49            os.path.join(DATA_DIR, "silence-44-s.mp3"))
50
51        self.mp3 = MP3(self.filename)
52        self.mp3_2 = MP3(self.silence_nov2)
53        self.mp3_3 = MP3(self.silence_mpeg2)
54        self.mp3_4 = MP3(self.silence_mpeg25)
55        self.mp3_lame = MP3(self.lame)
56        self.mp3_lame_peak = MP3(self.lame_peak)
57
58    def test_lame_broken_short(self):
59        # lame <=3.97 wrote broken files
60        f = MP3(self.lame_broken_short)
61        assert f.info.encoder_info == "LAME 3.97.0"
62        assert f.info.encoder_settings == "-V 9"
63        assert f.info.length == 0.0
64        assert f.info.bitrate == 40000
65        assert f.info.bitrate_mode == 2
66        assert f.info.sample_rate == 24000
67
68    def test_mode(self):
69        from mutagen.mp3 import JOINTSTEREO
70        self.failUnlessEqual(self.mp3.info.mode, JOINTSTEREO)
71        self.failUnlessEqual(self.mp3_2.info.mode, JOINTSTEREO)
72        self.failUnlessEqual(self.mp3_3.info.mode, JOINTSTEREO)
73        self.failUnlessEqual(self.mp3_4.info.mode, JOINTSTEREO)
74
75    def test_replaygain(self):
76        self.assertEqual(self.mp3_3.info.track_gain, 51.0)
77        self.assertEqual(self.mp3_4.info.track_gain, 51.0)
78        self.assertEqual(self.mp3_lame.info.track_gain, 6.0)
79        self.assertAlmostEqual(self.mp3_lame_peak.info.track_gain, 6.8, 1)
80        self.assertAlmostEqual(self.mp3_lame_peak.info.track_peak, 0.21856, 4)
81
82        self.assertTrue(self.mp3.info.track_gain is None)
83        self.assertTrue(self.mp3.info.track_peak is None)
84        self.assertTrue(self.mp3.info.album_gain is None)
85
86    def test_channels(self):
87        self.assertEqual(self.mp3.info.channels, 2)
88        self.assertEqual(self.mp3_2.info.channels, 2)
89        self.assertEqual(self.mp3_3.info.channels, 2)
90        self.assertEqual(self.mp3_4.info.channels, 2)
91
92    def test_encoder_info(self):
93        self.assertEqual(self.mp3.info.encoder_info, u"")
94        self.assertTrue(isinstance(self.mp3.info.encoder_info, text_type))
95        self.assertEqual(self.mp3_2.info.encoder_info, u"")
96        self.assertEqual(self.mp3_3.info.encoder_info, u"LAME 3.98.1+")
97        self.assertEqual(self.mp3_4.info.encoder_info, u"LAME 3.98.1+")
98        self.assertTrue(isinstance(self.mp3_4.info.encoder_info, text_type))
99
100    def test_bitrate_mode(self):
101        self.failUnlessEqual(self.mp3.info.bitrate_mode, BitrateMode.UNKNOWN)
102        self.failUnlessEqual(self.mp3_2.info.bitrate_mode, BitrateMode.UNKNOWN)
103        self.failUnlessEqual(self.mp3_3.info.bitrate_mode, BitrateMode.VBR)
104        self.failUnlessEqual(self.mp3_4.info.bitrate_mode, BitrateMode.VBR)
105
106    def test_id3(self):
107        self.failUnlessEqual(self.mp3.tags, ID3(self.silence))
108        self.failUnlessEqual(self.mp3_2.tags, ID3(self.silence_nov2))
109
110    def test_length(self):
111        self.assertAlmostEqual(self.mp3.info.length, 3.77, 2)
112        self.assertAlmostEqual(self.mp3_2.info.length, 3.77, 2)
113        self.assertAlmostEqual(self.mp3_3.info.length, 3.68475, 4)
114        self.assertAlmostEqual(self.mp3_4.info.length, 3.68475, 4)
115
116    def test_version(self):
117        self.failUnlessEqual(self.mp3.info.version, 1)
118        self.failUnlessEqual(self.mp3_2.info.version, 1)
119        self.failUnlessEqual(self.mp3_3.info.version, 2)
120        self.failUnlessEqual(self.mp3_4.info.version, 2.5)
121
122    def test_layer(self):
123        self.failUnlessEqual(self.mp3.info.layer, 3)
124        self.failUnlessEqual(self.mp3_2.info.layer, 3)
125        self.failUnlessEqual(self.mp3_3.info.layer, 3)
126        self.failUnlessEqual(self.mp3_4.info.layer, 3)
127
128    def test_bitrate(self):
129        self.failUnlessEqual(self.mp3.info.bitrate, 32000)
130        self.failUnlessEqual(self.mp3_2.info.bitrate, 32000)
131        self.failUnlessEqual(self.mp3_3.info.bitrate, 17783)
132        self.failUnlessEqual(self.mp3_4.info.bitrate, 8900)
133
134    def test_notmp3(self):
135        self.failUnlessRaises(
136            MP3Error, MP3, os.path.join(DATA_DIR, 'empty.ofr'))
137
138        self.failUnlessRaises(
139            MP3Error, MP3, os.path.join(DATA_DIR, 'emptyfile.mp3'))
140
141    def test_too_short(self):
142        self.failUnlessRaises(
143            MP3Error, MP3, os.path.join(DATA_DIR, 'too-short.mp3'))
144
145    def test_sketchy(self):
146        self.failIf(self.mp3.info.sketchy)
147        self.failIf(self.mp3_2.info.sketchy)
148        self.failIf(self.mp3_3.info.sketchy)
149        self.failIf(self.mp3_4.info.sketchy)
150
151    def test_sketchy_notmp3(self):
152        notmp3 = MP3(os.path.join(DATA_DIR, "silence-44-s.flac"))
153        self.failUnless(notmp3.info.sketchy)
154        self.assertTrue(u"sketchy" in notmp3.info.pprint())
155
156    def test_pprint(self):
157        self.failUnless(self.mp3.pprint())
158
159    def test_info_pprint(self):
160        res = self.mp3.info.pprint()
161        self.assertTrue(res)
162        self.assertTrue(isinstance(res, text_type))
163        self.assertTrue(res.startswith(u"MPEG 1 layer 3"))
164
165    def test_pprint_no_tags(self):
166        self.mp3.tags = None
167        self.failUnless(self.mp3.pprint())
168
169    def test_xing(self):
170        mp3 = MP3(os.path.join(DATA_DIR, "xing.mp3"))
171        self.assertAlmostEqual(mp3.info.length, 2.052, 3)
172        self.assertEqual(mp3.info.bitrate, 32000)
173
174    def test_vbri(self):
175        mp3 = MP3(os.path.join(DATA_DIR, "vbri.mp3"))
176        self.assertAlmostEqual(mp3.info.length, 222.19755, 3)
177        self.assertEqual(mp3.info.bitrate, 233260)
178
179    def test_empty_xing(self):
180        mp3 = MP3(os.path.join(DATA_DIR, "bad-xing.mp3"))
181        self.assertEqual(mp3.info.length, 0)
182        self.assertEqual(mp3.info.bitrate, 48000)
183
184    def test_delete(self):
185        self.mp3.delete()
186        self.failIf(self.mp3.tags)
187        self.failUnless(MP3(self.filename).tags is None)
188
189    def test_module_delete(self):
190        delete(self.filename)
191        self.failUnless(MP3(self.filename).tags is None)
192
193    def test_save(self):
194        self.mp3["TIT1"].text = ["foobar"]
195        self.mp3.save()
196        self.failUnless(MP3(self.filename)["TIT1"] == "foobar")
197
198    def test_save_padding(self):
199        self.mp3.save(padding=lambda x: 42)
200        self.assertEqual(MP3(self.filename).tags._padding, 42)
201
202    def test_load_non_id3(self):
203        filename = os.path.join(DATA_DIR, "apev2-lyricsv2.mp3")
204        from mutagen.apev2 import APEv2
205        mp3 = MP3(filename, ID3=APEv2)
206        self.failUnless("replaygain_track_peak" in mp3.tags)
207
208    def test_add_tags(self):
209        mp3 = MP3(os.path.join(DATA_DIR, "xing.mp3"))
210        self.failIf(mp3.tags)
211        mp3.add_tags()
212        self.failUnless(isinstance(mp3.tags, ID3))
213
214    def test_add_tags_already_there(self):
215        mp3 = MP3(os.path.join(DATA_DIR, "silence-44-s.mp3"))
216        self.failUnless(mp3.tags)
217        self.failUnlessRaises(Exception, mp3.add_tags)
218
219    def test_save_no_tags(self):
220        self.mp3.tags = None
221        self.mp3.save()
222        self.assertTrue(self.mp3.tags is None)
223
224    def test_mime(self):
225        self.failUnless("audio/mp3" in self.mp3.mime)
226        # XXX
227        self.mp3.info.layer = 2
228        self.failIf("audio/mp3" in self.mp3.mime)
229        self.failUnless("audio/mp2" in self.mp3.mime)
230
231    def tearDown(self):
232        os.unlink(self.filename)
233
234
235class TMPEGInfo(TestCase):
236
237    def test_not_real_file(self):
238        filename = os.path.join(DATA_DIR, "silence-44-s-v1.mp3")
239        with open(filename, "rb") as h:
240            fileobj = cBytesIO(h.read(20))
241        self.failUnlessRaises(MP3Error, MPEGInfo, fileobj)
242
243    def test_empty(self):
244        fileobj = cBytesIO(b"")
245        self.failUnlessRaises(MP3Error, MPEGInfo, fileobj)
246
247    def test_xing_unknown_framecount(self):
248        frame = (
249            b'\xff\xfb\xe4\x0c\x00\x0f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00'
250            b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
251            b'\x00\x00\x00\x00Info\x00\x00\x00\x02\x00\xb4V@\x00\xb4R\x80\x00'
252            b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
253        )
254        fileobj = cBytesIO(frame)
255        info = MPEGInfo(fileobj)
256        assert info.bitrate == 320000
257        assert info.length > 0
258
259
260class TEasyMP3(TestCase):
261
262    def setUp(self):
263        self.filename = get_temp_copy(
264            os.path.join(DATA_DIR, "silence-44-s.mp3"))
265        self.mp3 = EasyMP3(self.filename)
266
267    def test_artist(self):
268        self.failUnless("artist" in self.mp3)
269
270    def test_no_composer(self):
271        self.failIf("composer" in self.mp3)
272
273    def test_length(self):
274        # https://github.com/quodlibet/mutagen/issues/125
275        # easyid3, normal id3 and mpeg loading without tags should skip
276        # the tags and get the right offset of the first frame
277        easy = self.mp3.info
278        noneasy = MP3(self.filename).info
279        with open(self.filename, "rb") as h:
280            nonid3 = MPEGInfo(h)
281
282        self.failUnlessEqual(easy.length, noneasy.length)
283        self.failUnlessEqual(noneasy.length, nonid3.length)
284
285    def tearDown(self):
286        os.unlink(self.filename)
287
288
289class TXingHeader(TestCase):
290
291    def test_valid_info_header(self):
292        data = (b'Info\x00\x00\x00\x0f\x00\x00:>\x00\xed\xbd8\x00\x03\x05\x07'
293                b'\n\r\x0f\x12\x14\x17\x1a\x1c\x1e"$&)+.1359;=@CEGJLORTVZ\\^ac'
294                b'fikmqsux{}\x80\x82\x84\x87\x8a\x8c\x8e\x92\x94\x96\x99\x9c'
295                b'\x9e\xa1\xa3\xa5\xa9\xab\xad\xb0\xb3\xb5\xb8\xba\xbd\xc0\xc2'
296                b'\xc4\xc6\xca\xcc\xce\xd1\xd4\xd6\xd9\xdb\xdd\xe1\xe3\xe5\xe8'
297                b'\xeb\xed\xf0\xf2\xf5\xf8\xfa\xfc\x00\x00\x009')
298
299        fileobj = cBytesIO(data)
300        xing = XingHeader(fileobj)
301        self.assertEqual(xing.bytes, 15580472)
302        self.assertEqual(xing.frames, 14910)
303        self.assertEqual(xing.vbr_scale, 57)
304        self.assertTrue(xing.toc)
305        self.assertEqual(len(xing.toc), 100)
306        self.assertEqual(sum(xing.toc), 12625)  # only for coverage..
307        self.assertEqual(xing.is_info, True)
308
309        XingHeader(cBytesIO(data.replace(b'Info', b'Xing')))
310
311    def test_invalid(self):
312        self.assertRaises(XingHeaderError, XingHeader, cBytesIO(b""))
313        self.assertRaises(XingHeaderError, XingHeader, cBytesIO(b"Xing"))
314        self.assertRaises(XingHeaderError, XingHeader, cBytesIO(b"aaaa"))
315
316    def test_get_offset(self):
317        mp3 = MP3(os.path.join(DATA_DIR, "silence-44-s.mp3"))
318        self.assertEqual(XingHeader.get_offset(mp3.info), 36)
319
320
321class TVBRIHeader(TestCase):
322
323    def test_valid(self):
324        # parts of the trailing toc zeroed...
325        data = (b'VBRI\x00\x01\t1\x00d\x00\x0c\xb05\x00\x00\x049\x00\x87\x00'
326                b'\x01\x00\x02\x00\x08\n0\x19H\x18\xe0\x18x\x18\xe0\x18x\x19H'
327                b'\x18\xe0\x19H\x18\xe0\x18\xe0\x18x' + b'\x00' * 300)
328
329        fileobj = cBytesIO(data)
330        vbri = VBRIHeader(fileobj)
331        self.assertEqual(vbri.bytes, 831541)
332        self.assertEqual(vbri.frames, 1081)
333        self.assertEqual(vbri.quality, 100)
334        self.assertEqual(vbri.version, 1)
335        self.assertEqual(vbri.toc_frames, 8)
336        self.assertTrue(vbri.toc)
337        self.assertEqual(len(vbri.toc), 135)
338        self.assertEqual(sum(vbri.toc), 72656)
339
340    def test_invalid(self):
341        self.assertRaises(VBRIHeaderError, VBRIHeader, cBytesIO(b""))
342        self.assertRaises(VBRIHeaderError, VBRIHeader, cBytesIO(b"VBRI"))
343        self.assertRaises(VBRIHeaderError, VBRIHeader, cBytesIO(b"Xing"))
344
345    def test_get_offset(self):
346        mp3 = MP3(os.path.join(DATA_DIR, "silence-44-s.mp3"))
347        self.assertEqual(VBRIHeader.get_offset(mp3.info), 36)
348
349
350class TLAMEHeader(TestCase):
351
352    def test_version(self):
353
354        def parse(data):
355            data = cBytesIO(data + b"\x00" * (20 - len(data)))
356            return tuple(LAMEHeader.parse_version(data)[1:])
357
358        self.assertEqual(parse(b"LAME3.80"), (u"3.80", False))
359        self.assertEqual(parse(b"LAME3.80 "), (u"3.80", False))
360        self.assertEqual(parse(b"LAME3.88 (beta)"), (u"3.88 (beta)", False))
361        self.assertEqual(parse(b"LAME3.90 (alpha)"), (u"3.90 (alpha)", False))
362        self.assertEqual(parse(b"LAME3.90 "), (u"3.90.0+", True))
363        self.assertEqual(parse(b"LAME3.96a"), (u"3.96 (alpha)", True))
364        self.assertEqual(parse(b"LAME3.96b"), (u"3.96 (beta)", True))
365        self.assertEqual(parse(b"LAME3.96x"), (u"3.96 (?)", True))
366        self.assertEqual(parse(b"LAME3.98 "), (u"3.98.0", True))
367        self.assertEqual(parse(b"LAME3.96r"), (u"3.96.1+", True))
368        self.assertEqual(parse(b"L3.99r"), (u"3.99.1+", True))
369        self.assertEqual(parse(b"LAME3100r"), (u"3.100.1+", True))
370        self.assertEqual(parse(b"LAME3.90.\x03\xbe\x00"), (u"3.90.0+", True))
371        self.assertEqual(parse(b"LAME3.100"), (u"3.100.0+", True))
372
373    def test_invalid(self):
374
375        def parse(data):
376            data = cBytesIO(data + b"\x00" * (20 - len(data)))
377            return LAMEHeader.parse_version(data)
378
379        self.assertRaises(LAMEError, parse, b"")
380        self.assertRaises(LAMEError, parse, b"LAME")
381        self.assertRaises(LAMEError, parse, b"LAME3.9999")
382
383    def test_real(self):
384        with open(os.path.join(DATA_DIR, "lame.mp3"), "rb") as h:
385            h.seek(36, 0)
386            xing = XingHeader(h)
387            self.assertEqual(xing.lame_version_desc, u"3.99.1+")
388            self.assertTrue(xing.lame_header)
389            self.assertEqual(xing.lame_header.track_gain_adjustment, 6.0)
390            assert xing.get_encoder_settings() == u"-V 2"
391
392    def test_settings(self):
393        with open(os.path.join(DATA_DIR, "lame.mp3"), "rb") as h:
394            h.seek(36, 0)
395            xing = XingHeader(h)
396        header = xing.lame_header
397
398        def s(major, minor, **kwargs):
399            old = vars(header)
400            for key, value in kwargs.items():
401                assert hasattr(header, key)
402                setattr(header, key, value)
403            r = header.guess_settings(major, minor)
404            header.__dict__.update(old)
405            return r
406
407        assert s(3, 99) == "-V 2"
408        assert s(3, 98) == "-V 2"
409        assert s(3, 97) == "-V 2 --vbr-new"
410        assert s(3, 96) == "-V 2 --vbr-new"
411        assert s(3, 95) == "-V 2 --vbr-new"
412        assert s(3, 94) == "-V 2 --vbr-new"
413        assert s(3, 93) == "-V 2 --vbr-new"
414        assert s(3, 92) == "-V 2 --vbr-new"
415        assert s(3, 91) == "-V 2 --vbr-new"
416        assert s(3, 90) == "-V 2 --vbr-new"
417        assert s(3, 89) == ""
418
419        assert s(3, 91, vbr_method=2) == "--alt-preset 32"
420        assert s(3, 91, vbr_method=2, bitrate=255) == "--alt-preset 255+"
421        assert s(3, 99, vbr_method=2, preset_used=128) == "--preset 128"
422        assert s(3, 99, vbr_method=2, preset_used=0, bitrate=48) == "--abr 48"
423        assert \
424            s(3, 94, vbr_method=2, preset_used=0, bitrate=255) == "--abr 255+"
425        assert s(3, 99, vbr_method=3) == "-V 2 --vbr-old"
426        assert s(3, 94, vbr_method=3) == "-V 2"
427        assert s(3, 99, vbr_method=1, preset_used=1003) == "--preset insane"
428        assert s(3, 93, vbr_method=3, preset_used=1001) == "--preset standard"
429        assert s(3, 93, vbr_method=3, preset_used=1002) == "--preset extreme"
430        assert s(3, 93, vbr_method=3, preset_used=1004) == \
431            "--preset fast standard"
432        assert s(3, 93, vbr_method=3, preset_used=1005) == \
433            "--preset fast extreme"
434        assert s(3, 93, vbr_method=3, preset_used=1006) == "--preset medium"
435        assert s(3, 93, vbr_method=3, preset_used=1007) == \
436            "--preset fast medium"
437        assert s(3, 92, vbr_method=3) == "-V 2"
438        assert s(3, 92, vbr_method=1, preset_used=0, bitrate=254) == "-b 254"
439        assert s(3, 92, vbr_method=1, preset_used=0, bitrate=255) == "-b 255+"
440
441        def skey(major, minor, args):
442            keys = ["vbr_quality", "quality", "vbr_method", "lowpass_filter",
443                    "ath_type"]
444            return s(major, minor, **dict(zip(keys, args)))
445
446        assert skey(3, 91, (1, 2, 4, 19500, 3)) == "--preset r3mix"
447        assert skey(3, 91, (2, 2, 3, 19000, 4)) == "--alt-preset standard"
448        assert skey(3, 91, (2, 2, 3, 19500, 2)) == "--alt-preset extreme"
449
450    def test_length(self):
451        mp3 = MP3(os.path.join(DATA_DIR, "lame.mp3"))
452        self.assertAlmostEqual(mp3.info.length, 0.06160, 4)
453