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 unittest
21import audiotools
22import tempfile
23import os
24import os.path
25from hashlib import md5
26import random
27import decimal
28import test_streams
29from io import BytesIO
30import subprocess
31import struct
32
33from test import (parser,
34                  BLANK_PCM_Reader, RANDOM_PCM_Reader,
35                  EXACT_BLANK_PCM_Reader, EXACT_SILENCE_PCM_Reader,
36                  Variable_Reader,
37                  EXACT_RANDOM_PCM_Reader, MD5_Reader,
38                  Join_Reader, FrameCounter,
39                  Combinations,
40                  TEST_COVER1, TEST_COVER2, TEST_COVER3,
41                  HUGE_BMP)
42
43
44def do_nothing(self):
45    pass
46
47
48# add a bunch of decorator metafunctions like LIB_CORE
49# which can be wrapped around individual tests as needed
50for section in parser.sections():
51    for option in parser.options(section):
52        if parser.getboolean(section, option):
53            vars()["%s_%s" % (section.upper(),
54                              option.upper())] = lambda function: function
55        else:
56            vars()["%s_%s" % (section.upper(),
57                              option.upper())] = lambda function: do_nothing
58
59
60class CLOSE_PCM_Reader(audiotools.PCMReader):
61    def __init__(self, pcmreader):
62        audiotools.PCMReader.__init__(
63            self,
64            sample_rate=pcmreader.sample_rate,
65            channels=pcmreader.channels,
66            channel_mask=pcmreader.channel_mask,
67            bits_per_sample=pcmreader.bits_per_sample)
68        self.pcmreader = pcmreader
69        self.closes_called = 0
70
71    def read(self, pcm_frames):
72        return self.pcmreader.read(pcm_frames)
73
74    def close(self):
75        self.closes_called += 1
76        self.pcmreader.close()
77
78
79class ERROR_PCM_Reader(audiotools.PCMReader):
80    def __init__(self, error,
81                 sample_rate=44100, channels=2, bits_per_sample=16,
82                 channel_mask=None, failure_chance=.2, minimum_successes=0):
83        if channel_mask is None:
84            channel_mask = int(audiotools.ChannelMask.from_channels(channels))
85        audiotools.PCMReader.__init__(
86            self,
87            sample_rate=sample_rate,
88            channels=channels,
89            bits_per_sample=bits_per_sample,
90            channel_mask=channel_mask)
91        self.error = error
92
93        # this is so we can generate some "live" PCM data
94        # before erroring out due to our error
95        self.failure_chance = failure_chance
96
97        self.minimum_successes = minimum_successes
98
99        self.frame = audiotools.pcm.from_list([0] * self.channels,
100                                              self.channels,
101                                              self.bits_per_sample,
102                                              True)
103
104    def read(self, pcm_frames):
105        if self.minimum_successes > 0:
106            self.minimum_successes -= 1
107            return audiotools.pcm.from_frames(
108                [self.frame for i in range(pcm_frames)])
109        else:
110            if random.random() <= self.failure_chance:
111                raise self.error
112            else:
113                return audiotools.pcm.from_frames(
114                    [self.frame for i in range(pcm_frames)])
115
116    def close(self):
117        pass
118
119
120class Log:
121    def __init__(self):
122        self.results = []
123
124    def update(self, *args):
125        self.results.append(args)
126
127
128class Filewrapper:
129    def __init__(self, file):
130        self.file = file
131
132    def read(self, bytes):
133        return self.file.read(bytes)
134
135    def tell(self):
136        return self.file.tell()
137
138    def seek(self, pos):
139        self.file.seek(pos)
140
141    def close(self):
142        self.file.close()
143
144
145class AudioFileTest(unittest.TestCase):
146    def setUp(self):
147        self.audio_class = audiotools.AudioFile
148        self.suffix = "." + self.audio_class.SUFFIX
149
150    @FORMAT_AUDIOFILE
151    def test_init(self):
152        if self.audio_class is audiotools.AudioFile:
153            return
154
155        # first check nonexistent files
156        self.assertRaises(audiotools.InvalidFile,
157                          self.audio_class,
158                          "/dev/null/foo.%s" % (self.audio_class.SUFFIX))
159
160        f = tempfile.NamedTemporaryFile(suffix="." + self.audio_class.SUFFIX)
161        try:
162            # then check empty files
163            f.write(b"")
164            f.flush()
165            self.assertEqual(os.path.isfile(f.name), True)
166            self.assertRaises(audiotools.InvalidFile,
167                              self.audio_class,
168                              f.name)
169
170            # then check files with a bit of junk at the beginning
171            f.write(b'\x1aS\xc9\xf0I\xb2"CW\xd6')
172            f.flush()
173            self.assertGreater(os.path.getsize(f.name), 0)
174            self.assertRaises(audiotools.InvalidFile,
175                              self.audio_class,
176                              f.name)
177
178            # finally, check unreadable files
179            original_stat = os.stat(f.name)[0]
180            try:
181                os.chmod(f.name, 0)
182                self.assertRaises(audiotools.InvalidFile,
183                                  self.audio_class,
184                                  f.name)
185            finally:
186                os.chmod(f.name, original_stat)
187        finally:
188            f.close()
189
190    @FORMAT_AUDIOFILE
191    def test_is_type(self):
192        if self.audio_class is audiotools.AudioFile:
193            return
194
195        valid = tempfile.NamedTemporaryFile(suffix=self.suffix)
196        invalid = tempfile.NamedTemporaryFile(suffix=self.suffix)
197        # generate a valid file and check audiotools.file_type
198        self.audio_class.from_pcm(valid.name, BLANK_PCM_Reader(1))
199        with open(valid.name, "rb") as f:
200            self.assertEqual(audiotools.file_type(f), self.audio_class)
201
202        # several invalid files and ensure audiotools.file_type
203        # returns None
204        # (though it's *possible* os.urandom might generate a valid file
205        # by virtue of being random that's extremely unlikely in practice)
206        for i in range(256):
207            self.assertEqual(os.path.getsize(invalid.name), i)
208            with open(invalid.name, "rb") as f:
209                self.assertEqual(audiotools.file_type(f), None)
210            invalid.write(os.urandom(1))
211            invalid.flush()
212
213        valid.close()
214        invalid.close()
215
216    @FORMAT_AUDIOFILE
217    def test_bits_per_sample(self):
218        if self.audio_class is audiotools.AudioFile:
219            return
220
221        temp = tempfile.NamedTemporaryFile(suffix=self.suffix)
222        for bps in (8, 16, 24):
223            track = self.audio_class.from_pcm(
224                temp.name, BLANK_PCM_Reader(1, bits_per_sample=bps))
225            self.assertEqual(track.bits_per_sample(), bps)
226            track2 = audiotools.open(temp.name)
227            self.assertEqual(track2.bits_per_sample(), bps)
228        temp.close()
229
230    @FORMAT_AUDIOFILE_PLACEHOLDER
231    def test_channels(self):
232        self.assertTrue(False)
233
234    @FORMAT_AUDIOFILE_PLACEHOLDER
235    def test_channel_mask(self):
236        self.assertTrue(False)
237
238    @FORMAT_AUDIOFILE_PLACEHOLDER
239    def test_sample_rate(self):
240        self.assertTrue(False)
241
242    @FORMAT_AUDIOFILE_PLACEHOLDER
243    def test_lossless(self):
244        self.assertTrue(False)
245
246    @FORMAT_AUDIOFILE
247    def test_metadata(self):
248        import string
249        from audiotools import PY3
250
251        if self.audio_class is audiotools.AudioFile:
252            return
253
254        dummy_metadata = audiotools.MetaData(
255            **dict([(field, char if PY3 else char.decode("UTF-8")) for
256                    (field, char) in zip(
257                    audiotools.MetaData.FIELDS,
258                    string.ascii_letters)
259                    if field not in audiotools.MetaData.INTEGER_FIELDS] +
260                   [(field, i + 1) for (i, field) in
261                    enumerate(audiotools.MetaData.INTEGER_FIELDS)]))
262        temp = tempfile.NamedTemporaryFile(suffix=self.suffix)
263        try:
264            track = self.audio_class.from_pcm(temp.name,
265                                              BLANK_PCM_Reader(1))
266            track.set_metadata(dummy_metadata)
267            track = audiotools.open(temp.name)
268            metadata = track.get_metadata()
269            if metadata is None:
270                return
271
272            # check that delete_metadata works
273            nonblank_metadata = audiotools.MetaData(
274                track_name=u"Track Name",
275                track_number=1,
276                track_total=2,
277                album_name=u"Album Name")
278            track.set_metadata(nonblank_metadata)
279            self.assertEqual(track.get_metadata(), nonblank_metadata)
280            track.delete_metadata()
281            metadata = track.get_metadata()
282            if metadata is not None:
283                self.assertEqual(
284                    metadata,
285                    audiotools.MetaData())
286
287            track.set_metadata(nonblank_metadata)
288            self.assertEqual(track.get_metadata(), nonblank_metadata)
289
290            old_mode = os.stat(track.filename).st_mode
291            os.chmod(track.filename, 0o400)
292            try:
293                # check IOError on set_metadata()
294                self.assertRaises(IOError,
295                                  track.set_metadata,
296                                  audiotools.MetaData(track_name=u"Foo"))
297
298                # check IOError on delete_metadata()
299                self.assertRaises(IOError,
300                                  track.delete_metadata)
301            finally:
302                os.chmod(track.filename, old_mode)
303
304            os.chmod(track.filename, 0)
305            try:
306                # check IOError on get_metadata()
307                self.assertRaises(IOError,
308                                  track.get_metadata)
309            finally:
310                os.chmod(track.filename, old_mode)
311        finally:
312            temp.close()
313
314    @FORMAT_AUDIOFILE
315    def test_length(self):
316        if self.audio_class is audiotools.AudioFile:
317            return
318
319        temp = tempfile.NamedTemporaryFile(suffix=self.suffix)
320        try:
321            for seconds in [1, 2, 3, 4, 5, 10, 20, 60, 120]:
322                track = self.audio_class.from_pcm(temp.name,
323                                                  BLANK_PCM_Reader(seconds))
324                self.assertEqual(int(track.seconds_length()), seconds)
325        finally:
326            temp.close()
327
328    @FORMAT_AUDIOFILE_PLACEHOLDER
329    def test_pcm(self):
330        self.assertTrue(False)
331
332    @FORMAT_AUDIOFILE_PLACEHOLDER
333    def test_convert(self):
334        self.assertTrue(False)
335
336    @FORMAT_AUDIOFILE
337    def test_context_manager(self):
338        if self.audio_class is audiotools.AudioFile:
339            return
340
341        with tempfile.NamedTemporaryFile(suffix=self.suffix) as temp:
342            track = self.audio_class.from_pcm(temp.name,
343                                              BLANK_PCM_Reader(5))
344            with track.to_pcm() as pcmreader:
345                frame = pcmreader.read(4096)
346                while len(frame) > 0:
347                    frame = pcmreader.read(4096)
348
349    @FORMAT_AUDIOFILE
350    def test_read_leaks(self):
351        # this checks to make sure PCMReader implementations
352        # aren't leaking file handles
353
354        if self.audio_class is audiotools.AudioFile:
355            return
356        elif self.audio_class.NAME == "m4a":
357            # M4A implemented using external programs
358            # so no need to check those
359            return
360
361        # make small temporary file
362        temp = tempfile.NamedTemporaryFile(suffix=self.suffix)
363        track = self.audio_class.from_pcm(temp.name,
364                                          BLANK_PCM_Reader(10))
365
366        # open it a large number of times
367        for i in range(5000):
368            pcmreader = track.to_pcm()
369            pcmreader.close()
370            del(pcmreader)
371
372        temp.close()
373
374    @FORMAT_AUDIOFILE
375    def test_close(self):
376        if self.audio_class is audiotools.AudioFile:
377            return
378
379        pcm_frames = 123456
380
381        # ensure regular encode closes pcmreader
382        with tempfile.NamedTemporaryFile(
383            suffix="." + self.audio_class.SUFFIX) as f:
384            reader = CLOSE_PCM_Reader(EXACT_SILENCE_PCM_Reader(pcm_frames))
385            self.assertEqual(reader.closes_called, 0)
386            track = self.audio_class.from_pcm(f.name,
387                                              reader)
388            self.assertEqual(reader.closes_called, 1)
389
390        # ensure encode closes pcmreader with "total_pcm_frames" set
391        with tempfile.NamedTemporaryFile(
392            suffix="." + self.audio_class.SUFFIX) as f:
393            reader = CLOSE_PCM_Reader(EXACT_SILENCE_PCM_Reader(pcm_frames))
394            self.assertEqual(reader.closes_called, 0)
395            track = self.audio_class.from_pcm(f.name,
396                                              reader,
397                                              total_pcm_frames=pcm_frames)
398            self.assertEqual(reader.closes_called, 1)
399
400        unwritable_path = "/dev/null/foo." + self.audio_class.SUFFIX
401        self.assertFalse(os.access(unwritable_path, os.W_OK))
402
403        # encoding to unwritable file should still close pcmreader
404        reader = CLOSE_PCM_Reader(EXACT_SILENCE_PCM_Reader(pcm_frames))
405        self.assertEqual(reader.closes_called, 0)
406        self.assertRaises(audiotools.EncodingError,
407                          self.audio_class.from_pcm,
408                          unwritable_path,
409                          reader)
410        self.assertEqual(reader.closes_called, 1)
411
412        # encoding to unwritable file with total_pcm_frames
413        # should still close pcmreader
414        reader = CLOSE_PCM_Reader(EXACT_SILENCE_PCM_Reader(pcm_frames))
415        self.assertEqual(reader.closes_called, 0)
416        self.assertRaises(audiotools.EncodingError,
417                          self.audio_class.from_pcm,
418                          unwritable_path,
419                          reader,
420                          total_pcm_frames=pcm_frames)
421        self.assertEqual(reader.closes_called, 1)
422
423        # raising IOError should still close pcmreader
424        reader = CLOSE_PCM_Reader(ERROR_PCM_Reader(IOError("I/O error!"),
425                                                   failure_chance=1.0))
426        self.assertEqual(reader.closes_called, 0)
427        self.assertRaises(audiotools.EncodingError,
428                          self.audio_class.from_pcm,
429                          "error." + self.audio_class.SUFFIX,
430                          reader)
431        self.assertEqual(reader.closes_called, 1)
432        self.assertFalse(os.path.isfile("error." + self.audio_class.SUFFIX))
433
434        # raising ValueError should still close pcmreader
435        reader = CLOSE_PCM_Reader(ERROR_PCM_Reader(ValueError("value error!"),
436                                                   failure_chance=1.0))
437        self.assertEqual(reader.closes_called, 0)
438        self.assertRaises(audiotools.EncodingError,
439                          self.audio_class.from_pcm,
440                          "error." + self.audio_class.SUFFIX,
441                          reader)
442        self.assertEqual(reader.closes_called, 1)
443        self.assertFalse(os.path.isfile("error." + self.audio_class.SUFFIX))
444
445        # raising IOError with total_pcm_frames set
446        # should still close pcmreader
447        reader = CLOSE_PCM_Reader(ERROR_PCM_Reader(IOError("I/O error!"),
448                                                   failure_chance=1.0))
449        self.assertEqual(reader.closes_called, 0)
450        self.assertRaises(audiotools.EncodingError,
451                          self.audio_class.from_pcm,
452                          "error." + self.audio_class.SUFFIX,
453                          reader,
454                          total_pcm_frames=pcm_frames)
455        self.assertEqual(reader.closes_called, 1)
456        self.assertFalse(os.path.isfile("error." + self.audio_class.SUFFIX))
457
458        # raising IOError with total_pcm_frames set
459        # should still close pcmreader
460        reader = CLOSE_PCM_Reader(ERROR_PCM_Reader(ValueError("value error!"),
461                                                   failure_chance=1.0))
462        self.assertEqual(reader.closes_called, 0)
463        self.assertRaises(audiotools.EncodingError,
464                          self.audio_class.from_pcm,
465                          "error." + self.audio_class.SUFFIX,
466                          reader,
467                          total_pcm_frames=pcm_frames)
468        self.assertEqual(reader.closes_called, 1)
469        self.assertFalse(os.path.isfile("error." + self.audio_class.SUFFIX))
470
471    @FORMAT_AUDIOFILE
472    def test_convert_progress(self):
473        if self.audio_class is audiotools.AudioFile:
474            return
475
476        with tempfile.NamedTemporaryFile(suffix=self.suffix) as temp:
477            track = self.audio_class.from_pcm(temp.name,
478                                              BLANK_PCM_Reader(10))
479            if track.lossless():
480                self.assertTrue(
481                    audiotools.pcm_cmp(track.to_pcm(), BLANK_PCM_Reader(10)))
482            for audio_class in audiotools.AVAILABLE_TYPES:
483                with tempfile.NamedTemporaryFile(
484                        suffix="." + audio_class.SUFFIX) as outfile:
485                    log = Log()
486                    track2 = track.convert(outfile.name,
487                                           audio_class,
488                                           progress=log.update)
489                    self.assertGreater(
490                        len(log.results),
491                        0,
492                        "no logging converting %s to %s" %
493                        (self.audio_class.NAME,
494                         audio_class.NAME))
495                    self.assertEqual(len({r[1] for r in log.results}), 1)
496                    for x, y in zip(log.results[1:], log.results):
497                        self.assertGreaterEqual((x[0] - y[0]), 0)
498
499                    if track.lossless() and track2.lossless():
500                        self.assertTrue(
501                            audiotools.pcm_cmp(track.to_pcm(),
502                                               track2.to_pcm()),
503                            "PCM mismatch converting %s to %s" % (
504                                self.audio_class.NAME,
505                                audio_class.NAME))
506
507    @FORMAT_AUDIOFILE
508    def test_track_name(self):
509        import sys
510
511        if self.audio_class is audiotools.AudioFile:
512            return
513
514        format_template = u"Fo\u00f3 %%(%(field)s)s"
515        # first, test the many unicode string fields
516        for field in audiotools.MetaData.FIELDS:
517            if field not in audiotools.MetaData.INTEGER_FIELDS:
518                metadata = audiotools.MetaData()
519                value = u"\u00dcnicode value \u2ec1"
520                setattr(metadata, field, value)
521                format_string = format_template % {u"field": field}
522                track_name = self.audio_class.track_name(
523                    file_path="track",
524                    track_metadata=metadata,
525                    format=(format_string if
526                            (sys.version_info[0] >= 3) else
527                            format_string.encode("UTF-8", "replace")))
528                self.assertGreater(len(track_name), 0)
529                if sys.version_info[0] >= 3:
530                    self.assertEqual(
531                        track_name,
532                        (format_template %
533                         {u"field": u"foo"} %
534                         {u"foo": value}))
535                else:
536                    self.assertEqual(
537                        track_name,
538                        (format_template %
539                         {u"field": u"foo"} %
540                         {u"foo": value}).encode("UTF-8", "replace"))
541
542        # then, check integer fields
543        format_template = (u"Fo\u00f3 %(album_number)d " +
544                           u"%(track_number)2.2d %(album_track_number)s")
545
546        # first, check integers pulled from track metadata
547        for (track_number, album_number, album_track_number) in [
548            (0, 0, u"00"),
549            (1, 0, u"01"),
550            (25, 0, u"25"),
551            (0, 1, u"100"),
552            (1, 1, u"101"),
553            (25, 1, u"125"),
554            (0, 36, u"3600"),
555            (1, 36, u"3601"),
556            (25, 36, u"3625")]:
557            for basepath in [u"track",
558                             u"/foo/bar/track",
559                             u"/f\u00f3o/bar/tr\u00e1ck"]:
560                metadata = audiotools.MetaData(track_number=track_number,
561                                               album_number=album_number)
562
563                if sys.version_info[0] < 3:
564                    track_name = self.audio_class.track_name(
565                        file_path=basepath.encode("UTF-8", "replace"),
566                        track_metadata=metadata,
567                        format=format_template.encode("UTF-8", "replace"))
568
569                    self.assertEqual(
570                        track_name.decode("UTF-8", "replace"),
571                        (format_template % {u"album_number":
572                                            album_number,
573                                            u"track_number":
574                                            track_number,
575                                            u"album_track_number":
576                                            album_track_number}))
577                else:
578                    track_name = self.audio_class.track_name(
579                        file_path=basepath,
580                        track_metadata=metadata,
581                        format=format_template)
582
583                    self.assertEqual(
584                        track_name,
585                        (format_template % {u"album_number":
586                                            album_number,
587                                            u"track_number":
588                                            track_number,
589                                            u"album_track_number":
590                                            album_track_number}))
591
592        # also, check track_total/album_total from metadata
593        format_template = u"Fo\u00f3 %(track_total)d %(album_total)d"
594        for track_total in [0, 1, 25, 99]:
595            for album_total in [0, 1, 25, 99]:
596                metadata = audiotools.MetaData(track_total=track_total,
597                                               album_total=album_total)
598
599                if sys.version_info[0] < 3:
600                    track_name = self.audio_class.track_name(
601                        file_path="track",
602                        track_metadata=metadata,
603                        format=format_template.encode("UTF-8", "replace"))
604
605                    self.assertEqual(
606                        track_name.decode("UTF-8", "replace"),
607                        (format_template % {u"track_total":
608                                            track_total,
609                                            u"album_total":
610                                            album_total}))
611                else:
612                    track_name = self.audio_class.track_name(
613                        file_path="track",
614                        track_metadata=metadata,
615                        format=format_template)
616
617                    self.assertEqual(
618                        track_name,
619                        (format_template % {u"track_total":
620                                            track_total,
621                                            u"album_total":
622                                            album_total}))
623
624        # ensure %(basename)s is set properly
625        format_template = u"Fo\u00f3 %(basename)s"
626        for (path, base) in [(u"track", u"track"),
627                             (u"/foo/bar/track", u"track"),
628                             (u"/f\u00f3o/bar/tr\u00e1ck", u"tr\u00e1ck")]:
629            for metadata in [None, audiotools.MetaData()]:
630                if sys.version_info[0] < 3:
631                    track_name = self.audio_class.track_name(
632                        file_path=path.encode("UTF-8", "replace"),
633                        track_metadata=metadata,
634                        format=format_template.encode("UTF-8", "replace"))
635
636                    self.assertEqual(
637                        track_name.decode("UTF-8", "replace"),
638                        format_template % {u"basename": base})
639                else:
640                    track_name = self.audio_class.track_name(
641                        file_path=path,
642                        track_metadata=metadata,
643                        format=format_template)
644
645                    self.assertEqual(
646                        track_name,
647                        format_template % {u"basename": base})
648
649        # ensure %(suffix)s is set properly
650        format_template = u"Fo\u00f3 %(suffix)s"
651        for path in [u"track",
652                     u"/foo/bar/track",
653                     u"/f\u00f3o/bar/tr\u00e1ck"]:
654            for metadata in [None, audiotools.MetaData()]:
655                if sys.version_info[0] < 3:
656                    track_name = self.audio_class.track_name(
657                        file_path=path.encode("UTF-8", "replace"),
658                        track_metadata=metadata,
659                        format=format_template.encode("UTF-8", "replace"))
660
661                    self.assertEqual(
662                        track_name.decode("UTF-8", "replace"),
663                        (format_template % {
664                         u"suffix":
665                         self.audio_class.SUFFIX.decode('ascii')}))
666                else:
667                    track_name = self.audio_class.track_name(
668                        file_path=path,
669                        track_metadata=metadata,
670                        format=format_template)
671
672                    self.assertEqual(
673                        track_name,
674                        (format_template % {
675                         u"suffix":
676                         self.audio_class.SUFFIX}))
677
678        for metadata in [None, audiotools.MetaData()]:
679            # unsupported template fields raise UnsupportedTracknameField
680            self.assertRaises(audiotools.UnsupportedTracknameField,
681                              self.audio_class.track_name,
682                              "", metadata, "%(foo)s")
683
684            # broken template fields raise InvalidFilenameFormat
685            self.assertRaises(audiotools.InvalidFilenameFormat,
686                              self.audio_class.track_name,
687                              "", metadata, "%")
688
689            self.assertRaises(audiotools.InvalidFilenameFormat,
690                              self.audio_class.track_name,
691                              "", metadata, "%{")
692
693            self.assertRaises(audiotools.InvalidFilenameFormat,
694                              self.audio_class.track_name,
695                              "", metadata, "%[")
696
697            self.assertRaises(audiotools.InvalidFilenameFormat,
698                              self.audio_class.track_name,
699                              "", metadata, "%(")
700
701            self.assertRaises(audiotools.InvalidFilenameFormat,
702                              self.audio_class.track_name,
703                              "", metadata, "%(track_name")
704
705            self.assertRaises(audiotools.InvalidFilenameFormat,
706                              self.audio_class.track_name,
707                              "", metadata, "%(track_name)")
708
709    @FORMAT_AUDIOFILE
710    def test_replay_gain(self):
711        if self.audio_class.supports_replay_gain():
712            # make test file
713            temp_file = tempfile.NamedTemporaryFile(
714                suffix="." + self.audio_class.SUFFIX)
715            track = self.audio_class.from_pcm(
716                temp_file.name,
717                test_streams.Sine16_Stereo(44100, 44100,
718                                           441.0, 0.50,
719                                           4410.0, 0.49, 1.0))
720
721            # ensure get_replay_gain() returns None
722            self.assertEqual(track.get_replay_gain(), None)
723
724            # set dummy gain with set_replay_gain()
725            dummy_gain = audiotools.ReplayGain(
726                track_gain=0.25,
727                track_peak=0.125,
728                album_gain=0.50,
729                album_peak=1.0)
730            track.set_replay_gain(dummy_gain)
731
732            # ensure get_replay_gain() returns dummy gain
733            self.assertEqual(track.get_replay_gain(), dummy_gain)
734
735            # delete gain with delete_replay_gain()
736            track.delete_replay_gain()
737
738            # ensure get_replay_gain() returns None again
739            self.assertEqual(track.get_replay_gain(), None)
740
741            # calling delete_replay_gain() again is okay
742            track.delete_replay_gain()
743            self.assertEqual(track.get_replay_gain(), None)
744
745            # ensure setting replay_gain on unwritable file
746            # raises IOError
747            # FIXME
748
749            # ensure getting replay_gain on unreadable file
750            # raises IOError
751            # FIXME
752
753            temp_file.close()
754
755    @FORMAT_AUDIOFILE
756    def test_read_after_eof(self):
757        if self.audio_class is audiotools.AudioFile:
758            return None
759
760        # build basic file
761        temp_file = tempfile.NamedTemporaryFile(
762            suffix="." + self.audio_class.SUFFIX)
763        try:
764            # build a generic file of silence
765            temp_track = self.audio_class.from_pcm(
766                temp_file.name,
767                EXACT_SILENCE_PCM_Reader(44100))
768
769            # read all the PCM frames from the file
770            pcmreader = temp_track.to_pcm()
771            f = pcmreader.read(4000)
772            while len(f) > 0:
773                f = pcmreader.read(4000)
774
775            self.assertEqual(len(f), 0)
776
777            # then ensure subsequent reads return blank FrameList objects
778            # without triggering an error
779            for i in range(10):
780                f = pcmreader.read(4000)
781                self.assertEqual(len(f), 0)
782
783            pcmreader.close()
784
785            self.assertRaises(ValueError,
786                              pcmreader.read,
787                              4000)
788        finally:
789            temp_file.close()
790
791    @FORMAT_AUDIOFILE
792    def test_invalid_from_pcm(self):
793        if self.audio_class is audiotools.AudioFile:
794            return
795
796        # test our ERROR_PCM_Reader works
797        self.assertRaises(ValueError,
798                          ERROR_PCM_Reader(ValueError("error"),
799                                           failure_chance=1.0).read,
800                          1)
801        self.assertRaises(IOError,
802                          ERROR_PCM_Reader(IOError("error"),
803                                           failure_chance=1.0).read,
804                          1)
805
806        # ensure that our dummy file doesn't exist
807        dummy_filename = "invalid." + self.audio_class.SUFFIX
808        if os.path.isfile(dummy_filename):
809            os.unlink(dummy_filename)
810
811        # a decoder that raises IOError on to_pcm()
812        # should trigger an EncodingError
813        self.assertRaises(audiotools.EncodingError,
814                          self.audio_class.from_pcm,
815                          dummy_filename,
816                          ERROR_PCM_Reader(IOError("I/O Error")))
817
818        # ensure invalid files aren't left lying around
819        self.assertFalse(os.path.isfile(dummy_filename))
820
821        # perform the same check with total_pcm_frames set
822        self.assertRaises(audiotools.EncodingError,
823                          self.audio_class.from_pcm,
824                          dummy_filename,
825                          ERROR_PCM_Reader(IOError("I/O Error")),
826                          total_pcm_frames=44100)
827
828        self.assertFalse(os.path.isfile(dummy_filename))
829
830        # a decoder that raises ValueError on to_pcm()
831        # should trigger an EncodingError
832        self.assertRaises(audiotools.EncodingError,
833                          self.audio_class.from_pcm,
834                          dummy_filename,
835                          ERROR_PCM_Reader(ValueError("Value Error")))
836
837        # and ensure invalid files aren't left lying around
838        self.assertFalse(os.path.isfile(dummy_filename))
839
840        # perform the same check with total_pcm_frames set
841        self.assertRaises(audiotools.EncodingError,
842                          self.audio_class.from_pcm,
843                          dummy_filename,
844                          ERROR_PCM_Reader(ValueError("Value Error")),
845                          total_pcm_frames=44100)
846
847        self.assertFalse(os.path.isfile(dummy_filename))
848
849    @FORMAT_AUDIOFILE
850    def test_total_pcm_frames(self):
851        # all formats take a total_pcm_frames argument to from_pcm()
852        # none are expected to do anything useful with it
853        # but all should raise an exception if the actual amount
854        # of input frames doesn't match
855
856        if self.audio_class is audiotools.AudioFile:
857            return
858
859        temp_name = "test." + self.audio_class.SUFFIX
860
861        if os.path.isfile(temp_name):
862            os.unlink(temp_name)
863
864        # encode a file without the total_pcm_frames argument
865        track = self.audio_class.from_pcm(
866            temp_name,
867            EXACT_SILENCE_PCM_Reader(123456))
868        track.verify()
869
870        if track.lossless():
871            self.assertEqual(track.total_frames(), 123456)
872
873        del(track)
874        os.unlink(temp_name)
875
876        # encode a file with the total_pcm_frames argument
877        track = self.audio_class.from_pcm(
878            temp_name,
879            EXACT_SILENCE_PCM_Reader(234567),
880            total_pcm_frames=234567)
881        track.verify()
882
883        if track.lossless():
884            self.assertEqual(track.total_frames(), 234567)
885
886        del(track)
887        os.unlink(temp_name)
888
889        # check too many total_pcm_frames
890        self.assertRaises(audiotools.EncodingError,
891                          self.audio_class.from_pcm,
892                          temp_name,
893                          EXACT_SILENCE_PCM_Reader(345678),
894                          total_pcm_frames=345679)
895
896        self.assertFalse(os.path.isfile(temp_name))
897
898        # check not enough total_pcm_frames
899        self.assertRaises(audiotools.EncodingError,
900                          self.audio_class.from_pcm,
901                          temp_name,
902                          EXACT_SILENCE_PCM_Reader(345678),
903                          total_pcm_frames=345677)
904
905        self.assertFalse(os.path.isfile(temp_name))
906
907    @FORMAT_AUDIOFILE
908    def test_seekable(self):
909        from hashlib import md5
910        from random import randrange
911
912        if self.audio_class is audiotools.AudioFile:
913            return
914
915        total_pcm_frames = 44100 * 60 * 3
916
917        # create a slightly long file
918        temp_file = tempfile.NamedTemporaryFile(
919            suffix="." + self.audio_class.SUFFIX)
920        try:
921            temp_track = self.audio_class.from_pcm(
922                temp_file.name,
923                EXACT_SILENCE_PCM_Reader(total_pcm_frames),
924                total_pcm_frames=total_pcm_frames)
925
926            if temp_track.seekable():
927                # get a PCMReader of our format
928                with temp_track.to_pcm() as pcmreader:
929                    # hash its data when read to end
930                    raw_data = md5()
931                    frame = pcmreader.read(4096)
932                    while len(frame) > 0:
933                        raw_data.update(frame.to_bytes(False, True))
934                        frame = pcmreader.read(4096)
935
936                    # seeking to negative values should raise ValueError
937                    self.assertRaises(ValueError,
938                                      pcmreader.seek,
939                                      -1)
940
941                    # seeking to offset 0 should always work
942                    # (since it's a very basic rewind)
943                    self.assertEqual(pcmreader.seek(0), 0)
944
945                    # hash its data again and ensure a match
946                    rewound_raw_data = md5()
947                    frame = pcmreader.read(4096)
948                    while len(frame) > 0:
949                        rewound_raw_data.update(frame.to_bytes(False, True))
950                        frame = pcmreader.read(4096)
951                    self.assertEqual(raw_data.digest(),
952                                     rewound_raw_data.digest())
953
954                    # try a bunch of random seeks
955                    # and ensure the offset is always <= the seeked value
956                    for i in range(10):
957                        position = randrange(0, total_pcm_frames)
958                        actual_position = pcmreader.seek(position)
959                        self.assertLessEqual(actual_position, position)
960
961                        # if lossless, ensure seeking works as advertised
962                        # by comparing stream to file window
963                        actual_remaining_frames = 0
964                        desired_remaining_frames = (total_pcm_frames -
965                                                    actual_position)
966                        frame = pcmreader.read(4096)
967                        while len(frame) > 0:
968                            actual_remaining_frames += frame.frames
969                            frame = pcmreader.read(4096)
970
971                        self.assertEqual(actual_remaining_frames,
972                                         desired_remaining_frames)
973
974                    # seeking to some huge value should work
975                    # even if its position doesn't get to the end of the file
976                    for value in [2 ** 31, 2 ** 34, 2 ** 38]:
977                        seeked = pcmreader.seek(value)
978                        self.assertLessEqual(seeked, value,
979                                             "%s > %s" % (seeked, value))
980
981                    # a PCMReader that's closed should raise ValueError
982                    # whenever seek is called
983                    pcmreader.close()
984                    self.assertRaises(ValueError,
985                                      pcmreader.seek,
986                                      0)
987                    for i in range(10):
988                        self.assertRaises(ValueError,
989                                          pcmreader.seek,
990                                          randrange(0, total_pcm_frames))
991            else:
992                # ensure PCMReader has no .seek() method
993                # or that method always returns to the start of the file
994                with temp_track.to_pcm() as pcmreader:
995                    if (hasattr(pcmreader, "seek") and
996                        callable(pcmreader.seek)):
997                        # try a bunch of random seeks
998                        # and ensure the offset is always 0
999                        for i in range(10):
1000                            position = randrange(0, total_pcm_frames)
1001                            self.assertEqual(pcmreader.seek(position), 0)
1002
1003                        # seeking to some huge value should work
1004                        # even if its position doesn't get
1005                        # to the end of the file
1006                        for value in [2 ** 31, 2 ** 34, 2 ** 38]:
1007                            self.assertEqual(pcmreader.seek(value), 0)
1008
1009                        # a PCMReader that's closed should raise ValueError
1010                        # whenever seek is called
1011                        pcmreader.close()
1012                        self.assertRaises(ValueError,
1013                                          pcmreader.seek,
1014                                          0)
1015                        for i in range(10):
1016                            self.assertRaises(ValueError,
1017                                              pcmreader.seek,
1018                                              randrange(0, total_pcm_frames))
1019        finally:
1020            temp_file.close()
1021
1022    # FIXME
1023    @FORMAT_AUDIOFILE_PLACEHOLDER
1024    def test_cuesheet(self):
1025        self.assertTrue(False)
1026
1027    # FIXME
1028    @FORMAT_AUDIOFILE_PLACEHOLDER
1029    def test_verify(self):
1030        self.assertTrue(False)
1031
1032
1033class LosslessFileTest(AudioFileTest):
1034    @FORMAT_LOSSLESS
1035    def test_lossless(self):
1036        if self.audio_class is audiotools.AudioFile:
1037            return
1038
1039        temp = tempfile.NamedTemporaryFile(suffix=self.suffix)
1040        try:
1041            track = self.audio_class.from_pcm(temp.name, BLANK_PCM_Reader(1))
1042            self.assertEqual(track.lossless(), True)
1043            track = audiotools.open(temp.name)
1044            self.assertEqual(track.lossless(), True)
1045        finally:
1046            temp.close()
1047
1048    @FORMAT_LOSSLESS
1049    def test_channels(self):
1050        if self.audio_class is audiotools.AudioFile:
1051            return
1052
1053        temp = tempfile.NamedTemporaryFile(suffix=self.suffix)
1054        try:
1055            for channels in [1, 2, 3, 4, 5, 6]:
1056                track = self.audio_class.from_pcm(
1057                    temp.name, BLANK_PCM_Reader(1,
1058                                                channels=channels,
1059                                                channel_mask=0))
1060            self.assertEqual(track.channels(), channels)
1061            track = audiotools.open(temp.name)
1062            self.assertEqual(track.channels(), channels)
1063        finally:
1064            temp.close()
1065
1066    @FORMAT_LOSSLESS
1067    def test_channel_mask(self):
1068        if self.audio_class is audiotools.AudioFile:
1069            return
1070
1071        temp = tempfile.NamedTemporaryFile(suffix=self.suffix)
1072        try:
1073            for mask in [["front_center"],
1074                         ["front_left",
1075                          "front_right"],
1076                         ["front_left",
1077                          "front_right",
1078                          "front_center"],
1079                         ["front_left",
1080                          "front_right",
1081                          "back_left",
1082                          "back_right"],
1083                         ["front_left",
1084                          "front_right",
1085                          "front_center",
1086                          "back_left",
1087                          "back_right"],
1088                         ["front_left",
1089                          "front_right",
1090                          "front_center",
1091                          "low_frequency",
1092                          "back_left",
1093                          "back_right"]]:
1094                cm = audiotools.ChannelMask.from_fields(
1095                    **dict([(f, True) for f in mask]))
1096                track = self.audio_class.from_pcm(
1097                    temp.name, BLANK_PCM_Reader(1,
1098                                                channels=len(cm),
1099                                                channel_mask=int(cm)))
1100                self.assertEqual(track.channels(), len(cm))
1101                if int(track.channel_mask()) != 0:
1102                    self.assertEqual(track.channel_mask(), cm)
1103                    track = audiotools.open(temp.name)
1104                    self.assertEqual(track.channels(), len(cm))
1105                    self.assertEqual(track.channel_mask(), cm)
1106        finally:
1107            temp.close()
1108
1109    @FORMAT_LOSSLESS
1110    def test_sample_rate(self):
1111        if self.audio_class is audiotools.AudioFile:
1112            return
1113
1114        temp = tempfile.NamedTemporaryFile(suffix=self.suffix)
1115        try:
1116            for rate in [8000, 16000, 22050, 44100, 48000,
1117                         96000, 192000]:
1118                track = self.audio_class.from_pcm(
1119                    temp.name, BLANK_PCM_Reader(1, sample_rate=rate))
1120                self.assertEqual(track.sample_rate(), rate)
1121                track = audiotools.open(temp.name)
1122                self.assertEqual(track.sample_rate(), rate)
1123        finally:
1124            temp.close()
1125
1126    @FORMAT_LOSSLESS
1127    def test_pcm(self):
1128        if self.audio_class is audiotools.AudioFile:
1129            return
1130
1131        temp = tempfile.NamedTemporaryFile(suffix=self.suffix)
1132        temp2 = tempfile.NamedTemporaryFile()
1133        temp_dir = tempfile.mkdtemp()
1134        try:
1135            for total_pcm_frames in [None, 44100]:
1136                for compression in (None,) + self.audio_class.COMPRESSION_MODES:
1137                    # test silence
1138                    reader = MD5_Reader(BLANK_PCM_Reader(1))
1139                    if compression is None:
1140                        track = self.audio_class.from_pcm(
1141                            temp.name,
1142                            reader,
1143                            total_pcm_frames=total_pcm_frames)
1144                    else:
1145                        track = self.audio_class.from_pcm(
1146                            temp.name,
1147                            reader,
1148                            compression,
1149                            total_pcm_frames=total_pcm_frames)
1150                    checksum = md5()
1151                    audiotools.transfer_framelist_data(track.to_pcm(),
1152                                                       checksum.update)
1153                    self.assertEqual(reader.hexdigest(), checksum.hexdigest())
1154
1155                    # test random noise
1156                    reader = MD5_Reader(RANDOM_PCM_Reader(1))
1157                    if compression is None:
1158                        track = self.audio_class.from_pcm(
1159                            temp.name,
1160                            reader,
1161                            total_pcm_frames=total_pcm_frames)
1162                    else:
1163                        track = self.audio_class.from_pcm(
1164                            temp.name,
1165                            reader,
1166                            compression,
1167                            total_pcm_frames=total_pcm_frames)
1168                    checksum = md5()
1169                    audiotools.transfer_framelist_data(track.to_pcm(),
1170                                                       checksum.update)
1171                    self.assertEqual(reader.hexdigest(), checksum.hexdigest())
1172
1173                    # test randomly-sized chunks of silence
1174                    reader = MD5_Reader(Variable_Reader(BLANK_PCM_Reader(10)))
1175                    if compression is None:
1176                        track = self.audio_class.from_pcm(
1177                            temp.name,
1178                            reader,
1179                            total_pcm_frames=(total_pcm_frames * 10)
1180                            if (total_pcm_frames is not None) else None)
1181                    else:
1182                        track = self.audio_class.from_pcm(
1183                            temp.name,
1184                            reader,
1185                            compression,
1186                            total_pcm_frames=(total_pcm_frames * 10)
1187                            if (total_pcm_frames is not None) else None)
1188                    checksum = md5()
1189                    audiotools.transfer_framelist_data(track.to_pcm(),
1190                                                       checksum.update)
1191                    self.assertEqual(reader.hexdigest(), checksum.hexdigest())
1192
1193                    # test randomly-sized chunks of random noise
1194                    reader = MD5_Reader(Variable_Reader(RANDOM_PCM_Reader(10)))
1195                    if compression is None:
1196                        track = self.audio_class.from_pcm(
1197                            temp.name,
1198                            reader,
1199                            total_pcm_frames=(total_pcm_frames * 10)
1200                            if (total_pcm_frames is not None) else None)
1201                    else:
1202                        track = self.audio_class.from_pcm(
1203                            temp.name,
1204                            reader,
1205                            compression,
1206                            total_pcm_frames=(total_pcm_frames * 10)
1207                            if (total_pcm_frames is not None) else None)
1208                    checksum = md5()
1209                    audiotools.transfer_framelist_data(track.to_pcm(),
1210                                                       checksum.update)
1211                    self.assertEqual(reader.hexdigest(), checksum.hexdigest())
1212
1213                    # test PCMReaders that trigger a DecodingError
1214                    self.assertRaises(
1215                        ValueError,
1216                        ERROR_PCM_Reader(ValueError("error"),
1217                                         failure_chance=1.0).read,
1218                        1)
1219                    self.assertRaises(
1220                        IOError,
1221                        ERROR_PCM_Reader(IOError("error"),
1222                                         failure_chance=1.0).read,
1223                        1)
1224                    self.assertRaises(
1225                        audiotools.EncodingError,
1226                        self.audio_class.from_pcm,
1227                        os.path.join(temp_dir, "invalid" + self.suffix),
1228                        ERROR_PCM_Reader(IOError("I/O Error")))
1229
1230                    self.assertEqual(
1231                        os.path.isfile(
1232                            os.path.join(temp_dir,
1233                                         "invalid" + self.suffix)),
1234                        False)
1235
1236                    self.assertRaises(audiotools.EncodingError,
1237                                      self.audio_class.from_pcm,
1238                                      os.path.join(temp_dir,
1239                                                   "invalid" + self.suffix),
1240                                      ERROR_PCM_Reader(IOError("I/O Error")))
1241
1242                    self.assertEqual(
1243                        os.path.isfile(
1244                            os.path.join(temp_dir,
1245                                         "invalid" + self.suffix)),
1246                        False)
1247
1248                    # test unwritable output file
1249                    self.assertRaises(audiotools.EncodingError,
1250                                      self.audio_class.from_pcm,
1251                                      "/dev/null/foo.%s" % (self.suffix),
1252                                      BLANK_PCM_Reader(1))
1253
1254                    # test without suffix
1255                    reader = MD5_Reader(BLANK_PCM_Reader(1))
1256                    if compression is None:
1257                        track = self.audio_class.from_pcm(
1258                            temp2.name,
1259                            reader,
1260                            total_pcm_frames=total_pcm_frames)
1261                    else:
1262                        track = self.audio_class.from_pcm(
1263                            temp2.name,
1264                            reader,
1265                            compression,
1266                            total_pcm_frames=total_pcm_frames)
1267                    checksum = md5()
1268                    audiotools.transfer_framelist_data(track.to_pcm(),
1269                                                       checksum.update)
1270                    self.assertEqual(reader.hexdigest(), checksum.hexdigest())
1271        finally:
1272            temp.close()
1273            temp2.close()
1274            for f in os.listdir(temp_dir):
1275                os.unlink(os.path.join(temp_dir, f))
1276            os.rmdir(temp_dir)
1277
1278    @FORMAT_LOSSLESS
1279    def test_convert(self):
1280        if self.audio_class is audiotools.AudioFile:
1281            return
1282
1283        # check various round-trip options
1284        with tempfile.NamedTemporaryFile(
1285            suffix="." + self.audio_class.SUFFIX) as temp_input:
1286            track = self.audio_class.from_pcm(
1287                temp_input.name,
1288                test_streams.Sine16_Stereo(441000, 44100,
1289                                           8820.0, 0.70, 4410.0, 0.29, 1.0))
1290            for audio_class in audiotools.AVAILABLE_TYPES:
1291                self.assertFalse(os.path.isfile("/dev/null/foo.%s" %
1292                                                (audio_class.SUFFIX)))
1293                self.assertRaises(audiotools.EncodingError,
1294                                  track.convert,
1295                                  "/dev/null/foo.%s" %
1296                                  (audio_class.SUFFIX),
1297                                  audio_class)
1298
1299                with tempfile.NamedTemporaryFile(
1300                    suffix="." + audio_class.SUFFIX) as temp_output:
1301                    track2 = track.convert(temp_output.name, audio_class)
1302                    if track2.lossless():
1303                        self.assertTrue(
1304                            audiotools.pcm_cmp(track.to_pcm(),
1305                                               track2.to_pcm()),
1306                            "error round-tripping %s to %s" %
1307                            (self.audio_class.NAME,
1308                             audio_class.NAME))
1309                    else:
1310                        pcm = track2.to_pcm()
1311                        counter = FrameCounter(pcm.channels,
1312                                               pcm.bits_per_sample,
1313                                               pcm.sample_rate)
1314
1315                        audiotools.transfer_framelist_data(
1316                            pcm, counter.update)
1317
1318                        self.assertEqual(
1319                            int(counter), 10,
1320                            "mismatch encoding %s (%s/%d != %s)" %
1321                            (audio_class.NAME,
1322                             counter,
1323                             int(counter),
1324                             10))
1325
1326                    for compression in audio_class.COMPRESSION_MODES:
1327                        track2 = track.convert(temp_output.name,
1328                                               audio_class,
1329                                               compression)
1330                        if track2.lossless():
1331                            self.assertTrue(
1332                                audiotools.pcm_cmp(track.to_pcm(),
1333                                                   track2.to_pcm()),
1334                                "error round-tripping %s to %s at %s" %
1335                                (self.audio_class.NAME,
1336                                 audio_class.NAME,
1337                                 compression))
1338                        else:
1339                            pcm = track2.to_pcm()
1340                            counter = FrameCounter(
1341                                pcm.channels,
1342                                pcm.bits_per_sample,
1343                                pcm.sample_rate)
1344                            audiotools.transfer_framelist_data(
1345                                pcm, counter.update)
1346                            self.assertEqual(
1347                                int(counter), 10,
1348                                ("mismatch encoding %s " +
1349                                 "at quality %s (%s != %s)") %
1350                                (audio_class.NAME, compression,
1351                                 counter, 10))
1352
1353                        # check some obvious failures
1354                        self.assertRaises(audiotools.EncodingError,
1355                                          track.convert,
1356                                          "/dev/null/foo.%s" %
1357                                          (audio_class.SUFFIX),
1358                                          audio_class,
1359                                          compression)
1360
1361
1362class LossyFileTest(AudioFileTest):
1363    @FORMAT_LOSSY
1364    def test_bits_per_sample(self):
1365        if self.audio_class is audiotools.AudioFile:
1366            return
1367
1368        temp = tempfile.NamedTemporaryFile(suffix=self.suffix)
1369        try:
1370            for bps in (8, 16, 24):
1371                track = self.audio_class.from_pcm(
1372                    temp.name, BLANK_PCM_Reader(1, bits_per_sample=bps))
1373                self.assertEqual(track.bits_per_sample(), 16)
1374                track2 = audiotools.open(temp.name)
1375                self.assertEqual(track2.bits_per_sample(), 16)
1376        finally:
1377            temp.close()
1378
1379    @FORMAT_LOSSY
1380    def test_lossless(self):
1381        if self.audio_class is audiotools.AudioFile:
1382            return
1383
1384        temp = tempfile.NamedTemporaryFile(suffix=self.suffix)
1385        try:
1386            track = self.audio_class.from_pcm(temp.name, BLANK_PCM_Reader(1))
1387            self.assertEqual(track.lossless(), False)
1388            track = audiotools.open(temp.name)
1389            self.assertEqual(track.lossless(), False)
1390        finally:
1391            temp.close()
1392
1393    @FORMAT_LOSSY
1394    def test_channels(self):
1395        if self.audio_class is audiotools.AudioFile:
1396            return
1397
1398        temp = tempfile.NamedTemporaryFile(suffix=self.suffix)
1399        try:
1400            for channels in [1, 2, 3, 4, 5, 6]:
1401                track = self.audio_class.from_pcm(
1402                    temp.name, BLANK_PCM_Reader(1,
1403                                                channels=channels,
1404                                                channel_mask=0))
1405            self.assertEqual(track.channels(), 2)
1406            track = audiotools.open(temp.name)
1407            self.assertEqual(track.channels(), 2)
1408        finally:
1409            temp.close()
1410
1411    @FORMAT_LOSSY
1412    def test_channel_mask(self):
1413        if self.audio_class is audiotools.AudioFile:
1414            return
1415
1416        temp = tempfile.NamedTemporaryFile(suffix=self.suffix)
1417        try:
1418            cm = audiotools.ChannelMask.from_fields(
1419                front_left=True,
1420                front_right=True)
1421            track = self.audio_class.from_pcm(
1422                temp.name, BLANK_PCM_Reader(1,
1423                                            channels=len(cm),
1424                                            channel_mask=int(cm)))
1425            self.assertEqual(track.channels(), len(cm))
1426            self.assertEqual(track.channel_mask(), cm)
1427            track = audiotools.open(temp.name)
1428            self.assertEqual(track.channels(), len(cm))
1429            self.assertEqual(track.channel_mask(), cm)
1430        finally:
1431            temp.close()
1432
1433    @FORMAT_LOSSY
1434    def test_pcm(self):
1435        if self.audio_class is audiotools.AudioFile:
1436            return
1437
1438        temp = tempfile.NamedTemporaryFile(suffix=self.suffix)
1439        temp2 = tempfile.NamedTemporaryFile()
1440        temp_dir = tempfile.mkdtemp()
1441        try:
1442            for total_pcm_frames in [None, 44100 * 5]:
1443                for compression in (None,) + self.audio_class.COMPRESSION_MODES:
1444                    # test silence
1445                    reader = BLANK_PCM_Reader(5)
1446                    if compression is None:
1447                        track = self.audio_class.from_pcm(
1448                            temp.name,
1449                            reader,
1450                            total_pcm_frames=total_pcm_frames)
1451                    else:
1452                        track = self.audio_class.from_pcm(
1453                            temp.name,
1454                            reader,
1455                            compression,
1456                            total_pcm_frames=total_pcm_frames)
1457                    counter = FrameCounter(2, 16, 44100)
1458                    audiotools.transfer_framelist_data(track.to_pcm(),
1459                                                       counter.update)
1460                    self.assertEqual(int(counter), 5,
1461                                     "mismatch encoding %s at quality %s" %
1462                                     (self.audio_class.NAME,
1463                                      compression))
1464
1465                    # test random noise
1466                    reader = RANDOM_PCM_Reader(5)
1467                    if compression is None:
1468                        track = self.audio_class.from_pcm(
1469                            temp.name,
1470                            reader,
1471                            total_pcm_frames=total_pcm_frames)
1472                    else:
1473                        track = self.audio_class.from_pcm(
1474                            temp.name,
1475                            reader,
1476                            compression,
1477                            total_pcm_frames=total_pcm_frames)
1478                    counter = FrameCounter(2, 16, 44100)
1479                    audiotools.transfer_framelist_data(track.to_pcm(),
1480                                                       counter.update)
1481                    self.assertEqual(int(counter), 5,
1482                                     "mismatch encoding %s at quality %s" %
1483                                     (self.audio_class.NAME,
1484                                      compression))
1485
1486                    # test randomly-sized chunks of silence
1487                    reader = Variable_Reader(BLANK_PCM_Reader(5))
1488                    if compression is None:
1489                        track = self.audio_class.from_pcm(
1490                            temp.name,
1491                            reader,
1492                            total_pcm_frames=total_pcm_frames)
1493                    else:
1494                        track = self.audio_class.from_pcm(
1495                            temp.name,
1496                            reader,
1497                            compression,
1498                            total_pcm_frames=total_pcm_frames)
1499
1500                    counter = FrameCounter(2, 16, 44100)
1501                    audiotools.transfer_framelist_data(track.to_pcm(),
1502                                                       counter.update)
1503                    self.assertEqual(int(counter), 5,
1504                                     "mismatch encoding %s at quality %s" %
1505                                     (self.audio_class.NAME,
1506                                      compression))
1507
1508                    # test randomly-sized chunks of random noise
1509                    reader = Variable_Reader(RANDOM_PCM_Reader(5))
1510                    if compression is None:
1511                        track = self.audio_class.from_pcm(
1512                            temp.name,
1513                            reader,
1514                            total_pcm_frames=total_pcm_frames)
1515                    else:
1516                        track = self.audio_class.from_pcm(
1517                            temp.name,
1518                            reader,
1519                            compression,
1520                            total_pcm_frames=total_pcm_frames)
1521
1522                    counter = FrameCounter(2, 16, 44100)
1523                    audiotools.transfer_framelist_data(track.to_pcm(),
1524                                                       counter.update)
1525                    self.assertEqual(int(counter), 5,
1526                                     "mismatch encoding %s at quality %s" %
1527                                     (self.audio_class.NAME,
1528                                      compression))
1529
1530                    # test PCMReaders that trigger a DecodingError
1531                    self.assertRaises(
1532                        ValueError,
1533                        ERROR_PCM_Reader(ValueError("error"),
1534                                         failure_chance=1.0).read,
1535                        1)
1536                    self.assertRaises(
1537                        IOError,
1538                        ERROR_PCM_Reader(IOError("error"),
1539                                         failure_chance=1.0).read,
1540                        1)
1541                    self.assertRaises(audiotools.EncodingError,
1542                                      self.audio_class.from_pcm,
1543                                      os.path.join(temp_dir,
1544                                                   "invalid" + self.suffix),
1545                                      ERROR_PCM_Reader(IOError("I/O Error")))
1546
1547                    self.assertEqual(
1548                        os.path.isfile(
1549                            os.path.join(temp_dir,
1550                                         "invalid" + self.suffix)),
1551                        False)
1552
1553                    self.assertRaises(audiotools.EncodingError,
1554                                      self.audio_class.from_pcm,
1555                                      os.path.join(temp_dir,
1556                                                   "invalid" + self.suffix),
1557                                      ERROR_PCM_Reader(IOError("I/O Error")))
1558
1559                    self.assertEqual(
1560                        os.path.isfile(
1561                            os.path.join(temp_dir,
1562                                         "invalid" + self.suffix)),
1563                        False)
1564
1565                    # test unwritable output file
1566                    self.assertRaises(audiotools.EncodingError,
1567                                      self.audio_class.from_pcm,
1568                                      "/dev/null/foo.%s" % (self.suffix),
1569                                      BLANK_PCM_Reader(1))
1570
1571                    # test without suffix
1572                    reader = BLANK_PCM_Reader(5)
1573                    if compression is None:
1574                        track = self.audio_class.from_pcm(
1575                            temp2.name,
1576                            reader,
1577                            total_pcm_frames=total_pcm_frames)
1578                    else:
1579                        track = self.audio_class.from_pcm(
1580                            temp2.name,
1581                            reader,
1582                            compression,
1583                            total_pcm_frames=total_pcm_frames)
1584
1585                    counter = FrameCounter(2, 16, 44100)
1586                    audiotools.transfer_framelist_data(track.to_pcm(),
1587                                                       counter.update)
1588                    self.assertEqual(int(counter), 5,
1589                                     "mismatch encoding %s at quality %s" %
1590                                     (self.audio_class.NAME,
1591                                      compression))
1592        finally:
1593            temp.close()
1594            temp2.close()
1595            for f in os.listdir(temp_dir):
1596                os.unlink(os.path.join(temp_dir, f))
1597            os.rmdir(temp_dir)
1598
1599    @FORMAT_LOSSY
1600    def test_convert(self):
1601        if self.audio_class is audiotools.AudioFile:
1602            return
1603
1604        # check various round-trip options
1605        temp = tempfile.NamedTemporaryFile(suffix=self.suffix)
1606
1607        track = self.audio_class.from_pcm(
1608            temp.name,
1609            test_streams.Sine16_Stereo(220500, 44100,
1610                                       8820.0, 0.70, 4410.0, 0.29, 1.0))
1611        for audio_class in audiotools.AVAILABLE_TYPES:
1612            temp2 = tempfile.NamedTemporaryFile(
1613                suffix="." + audio_class.SUFFIX)
1614            track2 = track.convert(temp2.name, audio_class)
1615
1616            counter = FrameCounter(2, 16, 44100)
1617            audiotools.transfer_framelist_data(track2.to_pcm(),
1618                                               counter.update)
1619            self.assertEqual(
1620                int(counter), 5,
1621                "mismatch encoding %s" %
1622                (self.audio_class.NAME))
1623
1624            self.assertRaises(audiotools.EncodingError,
1625                              track.convert,
1626                              "/dev/null/foo.%s" %
1627                              (audio_class.SUFFIX),
1628                              audio_class)
1629
1630            for compression in audio_class.COMPRESSION_MODES:
1631                track2 = track.convert(temp2.name,
1632                                       audio_class,
1633                                       compression)
1634
1635                counter = FrameCounter(2, 16, 44100)
1636                audiotools.transfer_framelist_data(track2.to_pcm(),
1637                                                   counter.update)
1638                self.assertEqual(
1639                    int(counter), 5,
1640                    "mismatch encoding %s at quality %s" %
1641                    (self.audio_class.NAME,
1642                     compression))
1643
1644                # check some obvious failures
1645                self.assertRaises(audiotools.EncodingError,
1646                                  track.convert,
1647                                  "/dev/null/foo.%s" %
1648                                  (audio_class.SUFFIX),
1649                                  audio_class,
1650                                  compression)
1651
1652            temp2.close()
1653
1654        temp.close()
1655
1656
1657class TestForeignWaveChunks:
1658    @FORMAT_LOSSLESS
1659    def test_from_wave_close(self):
1660        temp_name = "closed." + self.audio_class.SUFFIX
1661        if os.path.isfile(temp_name):
1662            os.unlink(temp_name)
1663        try:
1664            pcmreader = CLOSE_PCM_Reader(
1665                EXACT_SILENCE_PCM_Reader(304844))
1666
1667            self.assertEqual(pcmreader.closes_called, 0)
1668
1669            # build our audio file using the .from_wave() interface
1670            track = self.audio_class.from_wave(
1671                filename=temp_name,
1672                header=(b'RIFFT\x9b\x12\x00WAVEfmt ' +
1673                        b'\x10\x00\x00\x00\x01\x00\x02\x00D' +
1674                        b'\xac\x00\x00\x10\xb1\x02\x00\x04\x00\x10\x00' +
1675                        b'data0\x9b\x12\x00'),
1676                pcmreader=pcmreader,
1677                footer=b"")
1678
1679            # ensure pcmreader gets closed properly
1680            self.assertEqual(pcmreader.closes_called, 1)
1681        finally:
1682            os.unlink(temp_name)
1683
1684    @FORMAT_LOSSLESS
1685    def test_convert_wave_chunks(self):
1686        import filecmp
1687        from zlib import decompress
1688
1689        self.assertTrue(issubclass(self.audio_class,
1690                                   audiotools.WaveContainer))
1691
1692        # several even-sized chunks
1693        chunks1 = (decompress(b"x\x9c\x0b\xf2ts\xdbQ\xc9\xcb\x10\xee\x18" +
1694                              b"\xe6\x9a\x96[\xa2 \xc0\xc0\xc0\xc0\xc8\xc0" +
1695                              b"\xc4\xe0\xb2\x86\x81A`#\x13\x03\x0b\x83" +
1696                              b"\x00CZ~~\x15\x07P\xbc$\xb5\xb8\xa4$\xb5" +
1697                              b"\xa2$)\xb1\xa8\n\xa4\xae8?757\xbf(\x15!^U" +
1698                              b"\x05\xd40\nF\xc1(\x18\xc1 %\xb1$1\xa0\x94" +
1699                              b"\x97\x01\x00`\xb0\x18\xf7"),
1700                   (220500, 44100, 2, 16, 0x3),
1701                   b"spam\x0c\x00\x00\x00anotherchunk")
1702
1703        # several odd-sized chunks
1704        chunks2 = (decompress(b"x\x9c\x0b\xf2ts\xcbc``\x08w\x0csM\xcb\xcf\xaf" +
1705                              b"\xe2b@\x06i\xb9%\n\x02@\x9a\x11\x08]\xd60" +
1706                              b"\x801#\x03\x07CRbQ\x157H\x1c\x01\x18R\x12K\x12" +
1707                              b"\xf9\x81b\x00\x19\xdd\x0ba"),
1708                   (15, 44100, 1, 8, 0x4),
1709                   b"\x00barz\x0b\x00\x00\x00\x01\x01\x01\x01" +
1710                   b"\x01\x01\x01\x01\x01\x01\x01\x00")
1711
1712        for (header,
1713             (total_frames,
1714              sample_rate,
1715              channels,
1716              bits_per_sample,
1717              channel_mask), footer) in [chunks1, chunks2]:
1718            temp1 = tempfile.NamedTemporaryFile(
1719                suffix="." + self.audio_class.SUFFIX)
1720            try:
1721                # build our audio file from the from_pcm() interface
1722                track = self.audio_class.from_pcm(
1723                    temp1.name,
1724                    EXACT_RANDOM_PCM_Reader(
1725                        pcm_frames=total_frames,
1726                        sample_rate=sample_rate,
1727                        channels=channels,
1728                        bits_per_sample=bits_per_sample,
1729                        channel_mask=channel_mask))
1730
1731                # check has_foreign_wave_chunks
1732                self.assertEqual(track.has_foreign_wave_chunks(), False)
1733            finally:
1734                temp1.close()
1735
1736        for (header,
1737             (total_frames,
1738              sample_rate,
1739              channels,
1740              bits_per_sample,
1741              channel_mask), footer) in [chunks1, chunks2]:
1742            temp1 = tempfile.NamedTemporaryFile(
1743                suffix="." + self.audio_class.SUFFIX)
1744            try:
1745                # build our audio file using the from_wave() interface
1746                track = self.audio_class.from_wave(
1747                    temp1.name,
1748                    header,
1749                    EXACT_RANDOM_PCM_Reader(
1750                        pcm_frames=total_frames,
1751                        sample_rate=sample_rate,
1752                        channels=channels,
1753                        bits_per_sample=bits_per_sample,
1754                        channel_mask=channel_mask),
1755                    footer)
1756
1757                # check has_foreign_wave_chunks
1758                self.assertEqual(track.has_foreign_wave_chunks(), True)
1759
1760                # ensure wave_header_footer returns same header and footer
1761                (track_header,
1762                 track_footer) = track.wave_header_footer()
1763                self.assertEqual(header, track_header)
1764                self.assertEqual(footer, track_footer)
1765
1766                # convert our file to every other WaveContainer format
1767                # (including our own)
1768                for new_class in audiotools.AVAILABLE_TYPES:
1769                    if issubclass(new_class, audiotools.WaveContainer):
1770                        temp2 = tempfile.NamedTemporaryFile(
1771                            suffix="." + new_class.SUFFIX)
1772                        log = Log()
1773                        try:
1774                            track2 = track.convert(temp2.name,
1775                                                   new_class,
1776                                                   progress=log.update)
1777
1778                            # ensure the progress function
1779                            # gets called during conversion
1780                            self.assertGreater(
1781                                len(log.results), 0,
1782                                "no logging converting %s to %s" %
1783                                (self.audio_class.NAME,
1784                                 new_class.NAME))
1785
1786                            self.assertEqual(
1787                                len({r[1] for r in log.results}), 1)
1788                            for x, y in zip(log.results[1:], log.results):
1789                                self.assertGreaterEqual((x[0] - y[0]), 0)
1790
1791                            # ensure newly converted file
1792                            # matches has_foreign_wave_chunks
1793                            self.assertTrue(track2.has_foreign_wave_chunks())
1794
1795                            # ensure newly converted file
1796                            # has same header and footer
1797                            (track2_header,
1798                             track2_footer) = track2.wave_header_footer()
1799                            self.assertEqual(header, track2_header)
1800                            self.assertEqual(footer, track2_footer)
1801
1802                            # ensure newly converted file has same PCM data
1803                            self.assertTrue(
1804                                audiotools.pcm_cmp(
1805                                    track.to_pcm(), track2.to_pcm()))
1806                        finally:
1807                            temp2.close()
1808            finally:
1809                temp1.close()
1810
1811        if os.path.isfile("bad.wav"):
1812            os.unlink("bad.wav")
1813
1814        for (header, footer) in [
1815            # wave header without "RIFF<size>WAVE raises an error
1816            (b"", b""),
1817            (b"FOOZ\x00\x00\x00\x00BARZ", b""),
1818
1819            # invalid total size raises an error
1820            (b"RIFFZ\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01" +
1821             b"\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00" +
1822             b"\x10\x00data2\x00\x00\x00", b""),
1823
1824            # invalid data size raises an error
1825            (b"RIFFV\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01" +
1826             b"\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00" +
1827             b"\x10\x00data6\x00\x00\x00", b""),
1828
1829            # invalid chunk IDs in header raise an error
1830            (b"RIFFb\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01" +
1831             b"\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00" +
1832             b"chn\x00\x04\x00\x00\x00\x01\x02\x03\x04" +
1833             b"data2\x00\x00\x00", b""),
1834
1835            # mulitple fmt chunks raise an error
1836            (b"RIFFn\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01" +
1837             b"\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00" +
1838             b"\x10\x00" +
1839             b"fmt \x10\x00\x00\x00\x01" +
1840             b"\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00" +
1841             b"\x10\x00" +
1842             b"data2\x00\x00\x00", b""),
1843
1844            # data chunk before fmt chunk raises an error
1845            (b"RIFFJ\x00\x00\x00WAVE" +
1846             b"chnk\x04\x00\x00\x00\x01\x02\x03\x04" +
1847             b"data2\x00\x00\x00", b""),
1848
1849            # bytes after data chunk raises an error
1850            (b"RIFFb\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01" +
1851             b"\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00" +
1852             b"chnk\x04\x00\x00\x00\x01\x02\x03\x04" +
1853             b"data3\x00\x00\x00\x01", b""),
1854
1855            # truncated chunks in header raise an error
1856            (b"RIFFb\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01" +
1857             b"\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00" +
1858             b"chnk\x04\x00\x00\x00\x01\x02\x03", b""),
1859
1860            # fmt chunk in footer raises an error
1861            (b"RIFFz\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01" +
1862             b"\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00" +
1863             b"chnk\x04\x00\x00\x00\x01\x02\x03\x04" +
1864             b"data2\x00\x00\x00",
1865             b"fmt \x10\x00\x00\x00\x01" +
1866             b"\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00"),
1867
1868            # data chunk in footer raises an error
1869            (b"RIFFn\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01" +
1870             b"\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00" +
1871             b"chnk\x04\x00\x00\x00\x01\x02\x03\x04" +
1872             b"data2\x00\x00\x00",
1873             b"data\x04\x00\x00\x00\x01\x02\x03\x04"),
1874
1875            # invalid chunk IDs in footer raise an error
1876            (b"RIFFn\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01" +
1877             b"\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00" +
1878             b"chnk\x04\x00\x00\x00\x01\x02\x03\x04" +
1879             b"data2\x00\x00\x00",
1880             b"chn\x00\x04\x00\x00\x00\x01\x02\x03\x04"),
1881
1882            # truncated chunks in footer raise an error
1883            (b"RIFFn\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01" +
1884             b"\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00" +
1885             b"chnk\x04\x00\x00\x00\x01\x02\x03\x04" +
1886             b"data2\x00\x00\x00",
1887             b"chnk\x04\x00\x00\x00\x01\x02\x03")]:
1888            self.assertRaises(audiotools.EncodingError,
1889                              self.audio_class.from_wave,
1890                              "bad.wav",
1891                              header,
1892                              EXACT_BLANK_PCM_Reader(25,
1893                                                     44100,
1894                                                     1,
1895                                                     16,
1896                                                     0x4),
1897                              footer)
1898            self.assertEqual(os.path.isfile("bad.wav"), False)
1899
1900
1901class TestForeignAiffChunks:
1902    @FORMAT_LOSSLESS
1903    def test_from_wave_close(self):
1904        temp_name = "closed." + self.audio_class.SUFFIX
1905        if os.path.isfile(temp_name):
1906            os.unlink(temp_name)
1907        try:
1908            pcmreader = CLOSE_PCM_Reader(
1909                EXACT_SILENCE_PCM_Reader(304844))
1910
1911            self.assertEqual(pcmreader.closes_called, 0)
1912
1913            # build our audio file using the .from_wave() interface
1914            track = self.audio_class.from_aiff(
1915                filename=temp_name,
1916                header=(b'FORM\x00\x12\x9b^AIFFCOMM' +
1917                        b'\x00\x00\x00\x12\x00\x02\x00\x04\xa6\xcc\x00' +
1918                        b'\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00' +
1919                        b'SSND\x00\x12\x9b8\x00\x00\x00\x00\x00\x00\x00\x00'),
1920                pcmreader=pcmreader,
1921                footer=b"")
1922
1923            # ensure pcmreader gets closed properly
1924            self.assertEqual(pcmreader.closes_called, 1)
1925        finally:
1926            os.unlink(temp_name)
1927
1928    @FORMAT_LOSSLESS
1929    def test_convert_aiff_chunks(self):
1930        import filecmp
1931        from zlib import decompress
1932
1933        self.assertTrue(issubclass(self.audio_class,
1934                                   audiotools.AiffContainer))
1935
1936        # several even-sized chunks
1937        chunks1 = (decompress(b"x\x9cs\xf3\x0f\xf2e\xe0\xad<\xe4\xe8\xe9" +
1938                              b"\xe6\xe6\xec\xef\xeb\xcb\xc0\xc0 \xc4\xc0" +
1939                              b"\xc4\xc0\x1c\x1b\xc2 \xe0\xc0\xb7\xc6\x85" +
1940                              b"\x01\x0c\xdc\xfc\xfd\xa3\x80\x14GIjqIIjE" +
1941                              b"\x89\x93c\x10\x88/P\x9c\x9f\x9b\x9a\x9b_" +
1942                              b"\x94\x8a\x10\x8f\x02\x8a\xb30\x8c\x82Q0" +
1943                              b"\nF.\x08\x0e\xf6sa\xe0-\x8d\x80\xf1\x01" +
1944                              b"\xcf\x8c\x17\x18"),
1945                   (220500, 44100, 2, 16, 0x3),
1946                   b"SPAM\x00\x00\x00\x0canotherchunk")
1947
1948        # several odd-sized chunks
1949        chunks2 = (decompress(b"x\x9cs\xf3\x0f\xf2e``\xa8p\xf4tss\xf3" +
1950                              b"\xf7\x8f\x02\xb2\xb9\x18\xe0\xc0\xd9" +
1951                              b"\xdf\x17$+\xc4\xc0\x08$\xf9\x198\x1c" +
1952                              b"\xf8\xd6\xb8@d\x9c\x1c\x83@j\xb9\x19" +
1953                              b"\x11\x80!8\xd8\x0f$+\x0e\xd3\r\x00\x16" +
1954                              b"\xa5\t3"),
1955                   (15, 44100, 1, 8, 0x4),
1956                   b"\x00BAZZ\x00\x00\x00\x0b\x02\x02\x02\x02" +
1957                   b"\x02\x02\x02\x02\x02\x02\x02\x00")
1958
1959        for (header,
1960             (total_frames,
1961              sample_rate,
1962              channels,
1963              bits_per_sample,
1964              channel_mask), footer) in [chunks1, chunks2]:
1965            temp1 = tempfile.NamedTemporaryFile(
1966                suffix="." + self.audio_class.SUFFIX)
1967            try:
1968                # build our audio file from the from_pcm() interface
1969                track = self.audio_class.from_pcm(
1970                    temp1.name,
1971                    EXACT_RANDOM_PCM_Reader(
1972                        pcm_frames=total_frames,
1973                        sample_rate=sample_rate,
1974                        channels=channels,
1975                        bits_per_sample=bits_per_sample,
1976                        channel_mask=channel_mask))
1977
1978                # check has_foreign_aiff_chunks()
1979                self.assertEqual(track.has_foreign_aiff_chunks(), False)
1980            finally:
1981                temp1.close()
1982
1983        for (header,
1984             (total_frames,
1985              sample_rate,
1986              channels,
1987              bits_per_sample,
1988              channel_mask), footer) in [chunks1, chunks2]:
1989            temp1 = tempfile.NamedTemporaryFile(
1990                suffix="." + self.audio_class.SUFFIX)
1991            try:
1992                # build our audio file using from_aiff() interface
1993                track = self.audio_class.from_aiff(
1994                    temp1.name,
1995                    header,
1996                    EXACT_RANDOM_PCM_Reader(
1997                        pcm_frames=total_frames,
1998                        sample_rate=sample_rate,
1999                        channels=channels,
2000                        bits_per_sample=bits_per_sample,
2001                        channel_mask=channel_mask),
2002                    footer)
2003
2004                # check has_foreign_aiff_chunks()
2005                self.assertEqual(track.has_foreign_aiff_chunks(), True)
2006
2007                # ensure aiff_header_footer returns same header and footer
2008                (track_header,
2009                 track_footer) = track.aiff_header_footer()
2010                self.assertEqual(header, track_header)
2011                self.assertEqual(footer, track_footer)
2012
2013                # convert our file to every other AiffContainer format
2014                # (including our own)
2015                for new_class in audiotools.AVAILABLE_TYPES:
2016                    if issubclass(new_class, audiotools.AiffContainer):
2017                        temp2 = tempfile.NamedTemporaryFile(
2018                            suffix="." + new_class.SUFFIX)
2019                        log = Log()
2020                        try:
2021                            track2 = track.convert(temp2.name,
2022                                                   new_class,
2023                                                   progress=log.update)
2024
2025                            # ensure the progress function
2026                            # gets called during conversion
2027                            self.assertGreater(
2028                                len(log.results), 0,
2029                                "no logging converting %s to %s" %
2030                                (self.audio_class.NAME,
2031                                 new_class.NAME))
2032
2033                            self.assertEqual(
2034                                len({r[1] for r in log.results}), 1)
2035                            for x, y in zip(log.results[1:], log.results):
2036                                self.assertGreaterEqual((x[0] - y[0]), 0)
2037
2038                            # ensure newly converted file
2039                            # matches has_foreign_wave_chunks
2040                            self.assertTrue(track2.has_foreign_aiff_chunks())
2041
2042                            # ensure newly converted file
2043                            # has same header and footer
2044                            (track2_header,
2045                             track2_footer) = track2.aiff_header_footer()
2046                            self.assertEqual(header, track2_header)
2047                            self.assertEqual(footer, track2_footer)
2048
2049                            # ensure newly converted file has same PCM data
2050                            self.assertTrue(
2051                                audiotools.pcm_cmp(track.to_pcm(),
2052                                                   track2.to_pcm()))
2053                        finally:
2054                            temp2.close()
2055            finally:
2056                temp1.close()
2057
2058        if os.path.isfile("bad.aiff"):
2059            os.unlink("bad.aiff")
2060
2061        for (header, footer) in [
2062            # aiff header without "FORM<size>AIFF raises an error
2063            (b"", b""),
2064            (b"FOOZ\x00\x00\x00\x00BARZ", b""),
2065
2066            # invalid total size raises an error
2067            (b"FORM\x00\x00\x00tAIFF" +
2068             b"COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" +
2069             b"\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" +
2070             b"SSND\x00\x00\x00:\x00\x00\x00\x00\x00\x00\x00\x00",
2071             b"ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00"),
2072
2073            # invalid SSND size raises an error
2074            (b"FORM\x00\x00\x00rAIFF" +
2075             b"COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" +
2076             b"\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" +
2077             b"SSND\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00",
2078             b"ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00"),
2079
2080            # invalid chunk IDs in header raise an error
2081            (b"FORM\x00\x00\x00~AIFF" +
2082             b"COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" +
2083             b"\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" +
2084             b"CHN\x00\x00\x00\x00\x04\x01\x02\x03\x04" +
2085             b"SSND\x00\x00\x00:\x00\x00\x00\x00\x00\x00\x00\x00",
2086             b"ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00"),
2087
2088            # mulitple COMM chunks raise an error
2089            (b"FORM\x00\x00\x00\x8cAIFF" +
2090             b"COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" +
2091             b"\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" +
2092             b"COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" +
2093             b"\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" +
2094             b"SSND\x00\x00\x00:\x00\x00\x00\x00\x00\x00\x00\x00",
2095             b"ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00"),
2096
2097            # SSND chunk before COMM chunk raises an error
2098            (b"FORM\x00\x00\x00XAIFF" +
2099             b"SSND\x00\x00\x00:\x00\x00\x00\x00\x00\x00\x00\x00",
2100             b"ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00"),
2101
2102            # bytes missing from SSNK chunk raises an error
2103            (b"FORM\x00\x00\x00rAIFF" +
2104             b"COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" +
2105             b"\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" +
2106             b"SSND\x00\x00\x00<\x00\x00\x00\x00\x00\x00",
2107             b"ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00"),
2108
2109            # bytes after SSND chunk raises an error
2110            (b"FORM\x00\x00\x00rAIFF" +
2111             b"COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" +
2112             b"\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" +
2113             b"SSND\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
2114             b"ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00"),
2115
2116            # truncated chunks in header raise an error
2117            (b"FORM\x00\x00\x00rAIFF" +
2118             b"COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00",
2119             b"ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00"),
2120
2121            # COMM chunk in footer raises an error
2122            (b"FORM\x00\x00\x00\x8cAIFF" +
2123             b"COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" +
2124             b"\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" +
2125             b"SSND\x00\x00\x00:\x00\x00\x00\x00\x00\x00\x00\x00",
2126             b"COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" +
2127             b"\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" +
2128             b"ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00"),
2129
2130            # SSND chunk in footer raises an error
2131            (b"FORM\x00\x00\x00rAIFF" +
2132             b"COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" +
2133             b"\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" +
2134             b"SSND\x00\x00\x00:\x00\x00\x00\x00\x00\x00\x00\x00",
2135             b"ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00" +
2136             b"SSND\x00\x00\x00:\x00\x00\x00\x00\x00\x00\x00\x00"),
2137
2138            # invalid chunk IDs in footer raise an error
2139            (b"FORM\x00\x00\x00rAIFF" +
2140             b"COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" +
2141             b"\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" +
2142             b"SSND\x00\x00\x00:\x00\x00\x00\x00\x00\x00\x00\x00",
2143             b"ID3\00\x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00\x00"),
2144
2145            # truncated chunks in footer raise an error
2146            (b"FORM\x00\x00\x00rAIFF" +
2147             b"COMM\x00\x00\x00\x12\x00\x01\x00\x00\x00\x19\x00" +
2148             b"\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00" +
2149             b"SSND\x00\x00\x00:\x00\x00\x00\x00\x00\x00\x00\x00",
2150             b"ID3 \x00\x00\x00\nID3\x02\x00\x00\x00\x00\x00")]:
2151            self.assertRaises(audiotools.EncodingError,
2152                              self.audio_class.from_aiff,
2153                              "bad.aiff",
2154                              header,
2155                              EXACT_BLANK_PCM_Reader(25,
2156                                                     44100,
2157                                                     1,
2158                                                     16,
2159                                                     0x4),
2160                              footer)
2161            self.assertEqual(os.path.isfile("bad.aiff"), False)
2162
2163
2164class AiffFileTest(TestForeignAiffChunks, LosslessFileTest):
2165    def setUp(self):
2166        self.audio_class = audiotools.AiffAudio
2167        self.suffix = "." + self.audio_class.SUFFIX
2168
2169    @FORMAT_AIFF
2170    def test_ieee_extended(self):
2171        from audiotools.bitstream import BitstreamReader, BitstreamRecorder
2172        import audiotools.aiff
2173
2174        for i in range(0, 192000 + 1):
2175            w = BitstreamRecorder(0)
2176            audiotools.aiff.build_ieee_extended(w, float(i))
2177            s = BytesIO(w.data())
2178            self.assertEqual(w.data(), s.getvalue())
2179            self.assertEqual(i, audiotools.aiff.parse_ieee_extended(
2180                BitstreamReader(s, False)))
2181
2182    @FORMAT_AIFF
2183    def test_overlong_file(self):
2184        # trying to generate too large of a file
2185        # should throw an exception right away if total_pcm_frames known
2186        # instead of building it first
2187
2188        self.assertEqual(os.path.isfile("invalid.aiff"), False)
2189
2190        self.assertRaises(audiotools.EncodingError,
2191                          self.audio_class.from_pcm,
2192                          "invalid.aiff",
2193                          EXACT_SILENCE_PCM_Reader(
2194                              pcm_frames=715827883,
2195                              sample_rate=44100,
2196                              channels=2,
2197                              bits_per_sample=24),
2198                          total_pcm_frames=715827883)
2199
2200        self.assertEqual(os.path.isfile("invalid.aiff"), False)
2201
2202    @FORMAT_AIFF
2203    def test_verify(self):
2204        import audiotools.aiff
2205        from test_core import ints_to_bytes, bytes_to_ints
2206
2207        # test truncated file
2208        for aiff_file in ["aiff-8bit.aiff",
2209                          "aiff-1ch.aiff",
2210                          "aiff-2ch.aiff",
2211                          "aiff-6ch.aiff"]:
2212            f = open(aiff_file, 'rb')
2213            aiff_data = f.read()
2214            f.close()
2215
2216            temp = tempfile.NamedTemporaryFile(suffix=".aiff")
2217
2218            try:
2219                # first, check that a truncated comm chunk raises an exception
2220                # at init-time
2221                for i in range(0, 0x25):
2222                    temp.seek(0, 0)
2223                    temp.write(aiff_data[0:i])
2224                    temp.flush()
2225                    self.assertEqual(os.path.getsize(temp.name), i)
2226
2227                    self.assertRaises(audiotools.InvalidFile,
2228                                      audiotools.AiffAudio,
2229                                      temp.name)
2230
2231                # then, check that a truncated ssnd chunk raises an exception
2232                # at read-time
2233                for i in range(0x37, len(aiff_data)):
2234                    temp.seek(0, 0)
2235                    temp.write(aiff_data[0:i])
2236                    temp.flush()
2237                    reader = audiotools.AiffAudio(temp.name).to_pcm()
2238                    self.assertNotEqual(reader, None)
2239                    self.assertRaises(IOError,
2240                                      audiotools.transfer_framelist_data,
2241                                      reader, lambda x: x)
2242            finally:
2243                temp.close()
2244
2245        # test non-ASCII chunk ID
2246        temp = tempfile.NamedTemporaryFile(suffix=".aiff")
2247        try:
2248            f = open("aiff-metadata.aiff", "rb")
2249            aiff_data = bytes_to_ints(f.read())
2250            f.close()
2251            aiff_data[0x89] = 0
2252            temp.seek(0, 0)
2253            temp.write(ints_to_bytes(aiff_data))
2254            temp.flush()
2255            aiff = audiotools.open(temp.name)
2256            self.assertRaises(audiotools.InvalidFile,
2257                              aiff.verify)
2258        finally:
2259            temp.close()
2260
2261        # test no SSND chunk
2262        aiff = audiotools.open("aiff-nossnd.aiff")
2263        self.assertRaises(audiotools.InvalidFile, aiff.verify)
2264
2265        # test convert errors
2266        with tempfile.NamedTemporaryFile(suffix=".aiff") as temp:
2267            with open("aiff-2ch.aiff", "rb") as f:
2268                temp.write(f.read()[0:-10])
2269            temp.flush()
2270            flac = audiotools.open(temp.name)
2271            if os.path.isfile("dummy.wav"):
2272                os.unlink("dummy.wav")
2273            self.assertFalse(os.path.isfile("dummy.wav"))
2274            self.assertRaises(audiotools.EncodingError,
2275                              flac.convert,
2276                              "dummy.wav",
2277                              audiotools.WaveAudio)
2278            self.assertFalse(os.path.isfile("dummy.wav"))
2279
2280        COMM = audiotools.aiff.AIFF_Chunk(
2281            b"COMM",
2282            18,
2283            b'\x00\x01\x00\x00\x00\r\x00\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00')
2284        SSND = audiotools.aiff.AIFF_Chunk(
2285            b"SSND",
2286            34,
2287            b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x00\x03\x00\x02\x00\x01\x00\x00\xff\xff\xff\xfe\xff\xfd\xff\xfe\xff\xff\x00\x00')
2288
2289        # test multiple COMM chunks found
2290        # test multiple SSND chunks found
2291        # test SSND chunk before COMM chunk
2292        # test no SSND chunk
2293        # test no COMM chunk
2294        for chunks in [[COMM, COMM, SSND],
2295                       [COMM, SSND, SSND],
2296                       [SSND, COMM],
2297                       [SSND],
2298                       [COMM]]:
2299            temp = tempfile.NamedTemporaryFile(suffix=".aiff")
2300            try:
2301                audiotools.AiffAudio.aiff_from_chunks(temp, chunks)
2302                self.assertRaises(
2303                    audiotools.InvalidFile,
2304                    audiotools.open(temp.name).verify)
2305            finally:
2306                temp.close()
2307
2308    @FORMAT_AIFF
2309    def test_clean(self):
2310        import audiotools.aiff
2311
2312        COMM = audiotools.aiff.AIFF_Chunk(
2313            b"COMM",
2314            18,
2315            b'\x00\x01\x00\x00\x00\r\x00\x10@\x0e\xacD\x00\x00\x00\x00\x00\x00')
2316        SSND = audiotools.aiff.AIFF_Chunk(
2317            b"SSND",
2318            34,
2319            b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x00\x03\x00\x02\x00\x01\x00\x00\xff\xff\xff\xfe\xff\xfd\xff\xfe\xff\xff\x00\x00')
2320
2321        # test multiple COMM chunks
2322        # test multiple SSND chunks
2323        # test data chunk before fmt chunk
2324        fixed = tempfile.NamedTemporaryFile(suffix=".aiff")
2325        try:
2326            for chunks in [[COMM, COMM, SSND],
2327                           [COMM, SSND, COMM],
2328                           [COMM, SSND, SSND],
2329                           [SSND, COMM],
2330                           [SSND, COMM, COMM]]:
2331                temp = tempfile.NamedTemporaryFile(suffix=".aiff")
2332                audiotools.AiffAudio.aiff_from_chunks(temp, chunks)
2333                temp.flush()
2334                fixes = audiotools.open(temp.name).clean(fixed.name)
2335                temp.close()
2336                aiff = audiotools.open(fixed.name)
2337                chunks = list(aiff.chunks())
2338                self.assertEqual([c.id for c in chunks],
2339                                 [c.id for c in [COMM, SSND]])
2340                self.assertEqual([c.__size__ for c in chunks],
2341                                 [c.__size__ for c in [COMM, SSND]])
2342                self.assertEqual([c.__data__ for c in chunks],
2343                                 [c.__data__ for c in [COMM, SSND]])
2344        finally:
2345            fixed.close()
2346
2347
2348class ALACFileTest(LosslessFileTest):
2349    def setUp(self):
2350        self.audio_class = audiotools.ALACAudio
2351        self.suffix = "." + self.audio_class.SUFFIX
2352
2353        from audiotools.decoders import ALACDecoder
2354        from audiotools.encoders import encode_alac
2355        self.decoder = ALACDecoder
2356        self.encode = encode_alac
2357
2358    @FORMAT_ALAC
2359    def test_init(self):
2360        # check missing file
2361        self.assertRaises(audiotools.m4a.InvalidALAC,
2362                          audiotools.ALACAudio,
2363                          "/dev/null/foo")
2364
2365        # check invalid file
2366        with tempfile.NamedTemporaryFile(suffix=".m4a") as invalid_file:
2367            for c in [b"i", b"n", b"v", b"a", b"l", b"i", b"d",
2368                      b"s", b"t", b"r", b"i", b"n", b"g", b"x", b"x", b"x"]:
2369                invalid_file.write(c)
2370                invalid_file.flush()
2371                self.assertRaises(audiotools.m4a.InvalidALAC,
2372                                  audiotools.ALACAudio,
2373                                  invalid_file.name)
2374
2375        # check some decoder errors,
2376        # mostly to ensure a failed init doesn't make Python explode
2377        self.assertRaises(TypeError, self.decoder)
2378
2379        self.assertRaises(TypeError, self.decoder, None)
2380
2381    @FORMAT_ALAC
2382    def test_bits_per_sample(self):
2383        with tempfile.NamedTemporaryFile(suffix=self.suffix) as temp:
2384            for bps in (16, 24):
2385                track = self.audio_class.from_pcm(
2386                    temp.name, BLANK_PCM_Reader(1, bits_per_sample=bps))
2387                self.assertEqual(track.bits_per_sample(), bps)
2388                track2 = audiotools.open(temp.name)
2389                self.assertEqual(track2.bits_per_sample(), bps)
2390
2391    @FORMAT_ALAC
2392    def test_channel_mask(self):
2393        with tempfile.NamedTemporaryFile(suffix=self.suffix) as temp:
2394            for mask in [["front_center"],
2395                         ["front_left",
2396                          "front_right"]]:
2397                cm = audiotools.ChannelMask.from_fields(
2398                    **dict([(f, True) for f in mask]))
2399                track = self.audio_class.from_pcm(
2400                    temp.name, BLANK_PCM_Reader(1,
2401                                                channels=len(cm),
2402                                                channel_mask=int(cm)))
2403                self.assertEqual(track.channels(), len(cm))
2404                self.assertEqual(track.channel_mask(), cm)
2405                track = audiotools.open(temp.name)
2406                self.assertEqual(track.channels(), len(cm))
2407                self.assertEqual(track.channel_mask(), cm)
2408
2409            for mask in [["front_center",
2410                          "front_left",
2411                          "front_right"],
2412                         ["front_center",
2413                          "front_left",
2414                          "front_right",
2415                          "back_center"],
2416                         ["front_center",
2417                          "front_left",
2418                          "front_right",
2419                          "back_left",
2420                          "back_right"],
2421                         ["front_center",
2422                          "front_left",
2423                          "front_right",
2424                          "back_left",
2425                          "back_right",
2426                          "low_frequency"],
2427                         ["front_center",
2428                          "front_left",
2429                          "front_right",
2430                          "back_left",
2431                          "back_right",
2432                          "back_center",
2433                          "low_frequency"],
2434                         ["front_center",
2435                          "front_left_of_center",
2436                          "front_right_of_center",
2437                          "front_left",
2438                          "front_right",
2439                          "back_left",
2440                          "back_right",
2441                          "low_frequency"]]:
2442                cm = audiotools.ChannelMask.from_fields(
2443                    **dict([(f, True) for f in mask]))
2444                track = self.audio_class.from_pcm(
2445                    temp.name, BLANK_PCM_Reader(1,
2446                                                channels=len(cm),
2447                                                channel_mask=int(cm)))
2448                self.assertEqual(track.channels(), len(cm))
2449                self.assertEqual(track.channel_mask(), cm)
2450                track = audiotools.open(temp.name)
2451                self.assertEqual(track.channels(), len(cm))
2452                self.assertEqual(track.channel_mask(), cm)
2453
2454            # ensure valid channel counts with invalid channel masks
2455            # raise an exception
2456            self.assertRaises(audiotools.UnsupportedChannelMask,
2457                              self.audio_class.from_pcm,
2458                              temp.name,
2459                              BLANK_PCM_Reader(1, channels=4,
2460                                               channel_mask=0x0033))
2461
2462            self.assertRaises(audiotools.UnsupportedChannelMask,
2463                              self.audio_class.from_pcm,
2464                              temp.name,
2465                              BLANK_PCM_Reader(1, channels=5,
2466                                               channel_mask=0x003B))
2467
2468    @FORMAT_ALAC
2469    def test_verify(self):
2470        with open("alac-allframes.m4a", "rb") as f:
2471            alac_data = f.read()
2472
2473        # test truncating the mdat atom triggers IOError
2474        with tempfile.NamedTemporaryFile(suffix='.m4a') as temp:
2475            for i in range(0x16CD, len(alac_data)):
2476                temp.seek(0, 0)
2477                temp.write(alac_data[0:i])
2478                temp.flush()
2479                self.assertEqual(os.path.getsize(temp.name), i)
2480                decoder = audiotools.open(temp.name).to_pcm()
2481                self.assertNotEqual(decoder, None)
2482                self.assertRaises(IOError,
2483                                  audiotools.transfer_framelist_data,
2484                                  decoder, lambda x: x)
2485
2486                self.assertRaises(audiotools.InvalidFile,
2487                                  audiotools.open(temp.name).verify)
2488
2489        # test a truncated file's convert() method raises EncodingError
2490        with tempfile.NamedTemporaryFile(suffix=".m4a") as temp:
2491            with open("alac-allframes.m4a", "rb") as f:
2492                temp.write(f.read()[0:-10])
2493                temp.flush()
2494            alac = audiotools.open(temp.name)
2495            if os.path.isfile("dummy.wav"):
2496                os.unlink("dummy.wav")
2497            self.assertEqual(os.path.isfile("dummy.wav"), False)
2498            self.assertRaises(audiotools.EncodingError,
2499                              alac.convert,
2500                              "dummy.wav",
2501                              audiotools.WaveAudio)
2502            self.assertEqual(os.path.isfile("dummy.wav"), False)
2503
2504    @FORMAT_ALAC
2505    def test_too(self):
2506        # ensure that the 'too' meta atom isn't modified by setting metadata
2507        with tempfile.NamedTemporaryFile(suffix=self.suffix) as temp:
2508            track = self.audio_class.from_pcm(
2509                temp.name,
2510                BLANK_PCM_Reader(1))
2511            metadata = track.get_metadata()
2512            encoder = u"%s" % (metadata[b'ilst'][b'\xa9too'],)
2513            track.set_metadata(audiotools.MetaData(track_name=u"Foo"))
2514            metadata = track.get_metadata()
2515            self.assertEqual(metadata.track_name, u"Foo")
2516            self.assertEqual(u"%s" % (metadata[b'ilst'][b'\xa9too'],), encoder)
2517
2518    def __test_reader__(self, pcmreader, total_pcm_frames, block_size=4096):
2519        if not audiotools.BIN.can_execute(audiotools.BIN["alac"]):
2520            self.assertTrue(
2521                False,
2522                "reference ALAC binary alac(1) required for this test")
2523
2524        temp_file = tempfile.NamedTemporaryFile(suffix=".alac")
2525        self.audio_class.from_pcm(temp_file.name,
2526                                  pcmreader,
2527                                  block_size=block_size)
2528
2529        alac = audiotools.open(temp_file.name)
2530        self.assertGreater(alac.total_frames(), 0)
2531
2532        # first, ensure the ALAC-encoded file
2533        # has the same MD5 signature as pcmreader once decoded
2534        md5sum_decoder = md5()
2535        with alac.to_pcm() as d:
2536            f = d.read(audiotools.FRAMELIST_SIZE)
2537            while len(f) > 0:
2538                md5sum_decoder.update(f.to_bytes(False, True))
2539                f = d.read(audiotools.FRAMELIST_SIZE)
2540        self.assertEqual(md5sum_decoder.digest(), pcmreader.digest())
2541
2542        # then compare our .to_pcm() output
2543        # with that of the ALAC reference decoder
2544        reference = subprocess.Popen([audiotools.BIN["alac"],
2545                                      "-r", temp_file.name],
2546                                     stdout=subprocess.PIPE)
2547        md5sum_reference = md5()
2548        audiotools.transfer_data(reference.stdout.read,
2549                                 md5sum_reference.update)
2550        reference.stdout.close()
2551        self.assertEqual(reference.wait(), 0)
2552        self.assertEqual(md5sum_reference.digest(), pcmreader.digest(),
2553                         "mismatch decoding %s from reference (%s != %s)" %
2554                         (repr(pcmreader),
2555                          md5sum_reference.hexdigest(),
2556                          pcmreader.hexdigest()))
2557
2558        # then, perform test again using from_pcm()
2559        # with total_pcm_frames indicated
2560        pcmreader.reset()
2561
2562        self.audio_class.from_pcm(temp_file.name,
2563                                  pcmreader,
2564                                  total_pcm_frames=total_pcm_frames,
2565                                  block_size=block_size)
2566
2567        alac = audiotools.open(temp_file.name)
2568        self.assertGreater(alac.total_frames(), 0)
2569
2570        # ensure the ALAC-encoded file
2571        # has the same MD5 signature as pcmreader once decoded
2572        md5sum_decoder = md5()
2573        with alac.to_pcm() as d:
2574            f = d.read(audiotools.FRAMELIST_SIZE)
2575            while len(f) > 0:
2576                md5sum_decoder.update(f.to_bytes(False, True))
2577                f = d.read(audiotools.FRAMELIST_SIZE)
2578        self.assertEqual(md5sum_decoder.digest(), pcmreader.digest())
2579
2580        # then compare our .to_pcm() output
2581        # with that of the ALAC reference decoder
2582        reference = subprocess.Popen([audiotools.BIN["alac"],
2583                                      "-r", temp_file.name],
2584                                     stdout=subprocess.PIPE)
2585        md5sum_reference = md5()
2586        audiotools.transfer_data(reference.stdout.read,
2587                                 md5sum_reference.update)
2588        reference.stdout.close()
2589        self.assertEqual(reference.wait(), 0)
2590        self.assertEqual(md5sum_reference.digest(), pcmreader.digest(),
2591                         "mismatch decoding %s from reference (%s != %s)" %
2592                         (repr(pcmreader),
2593                          md5sum_reference.hexdigest(),
2594                          pcmreader.hexdigest()))
2595
2596    def __test_reader_nonalac__(self, pcmreader, total_pcm_frames,
2597                                block_size=4096):
2598        # This is for multichannel testing
2599        # since alac(1) doesn't handle them yet.
2600        # Unfortunately, it relies only on our built-in decoder
2601        # to test correctness.
2602
2603        temp_file = tempfile.NamedTemporaryFile(suffix=".alac")
2604        self.audio_class.from_pcm(temp_file.name,
2605                                  pcmreader,
2606                                  block_size=block_size)
2607
2608        alac = audiotools.open(temp_file.name)
2609        self.assertGreater(alac.total_frames(), 0)
2610
2611        # first, ensure the ALAC-encoded file
2612        # has the same MD5 signature as pcmreader once decoded
2613        md5sum_decoder = md5()
2614        with alac.to_pcm() as d:
2615            f = d.read(audiotools.FRAMELIST_SIZE)
2616            while len(f) > 0:
2617                md5sum_decoder.update(f.to_bytes(False, True))
2618                f = d.read(audiotools.FRAMELIST_SIZE)
2619        self.assertEqual(md5sum_decoder.digest(), pcmreader.digest())
2620
2621        # perform test again with total_pcm_frames indicated
2622        pcmreader.reset()
2623        self.audio_class.from_pcm(temp_file.name,
2624                                  pcmreader,
2625                                  total_pcm_frames=total_pcm_frames,
2626                                  block_size=block_size)
2627
2628        alac = audiotools.open(temp_file.name)
2629        self.assertGreater(alac.total_frames(), 0)
2630
2631        # first, ensure the ALAC-encoded file
2632        # has the same MD5 signature as pcmreader once decoded
2633        md5sum_decoder = md5()
2634        with alac.to_pcm() as d:
2635            f = d.read(audiotools.FRAMELIST_SIZE)
2636            while len(f) > 0:
2637                md5sum_decoder.update(f.to_bytes(False, True))
2638                f = d.read(audiotools.FRAMELIST_SIZE)
2639        self.assertEqual(md5sum_decoder.digest(), pcmreader.digest())
2640
2641        temp_file.close()
2642
2643    def __stream_variations__(self):
2644        for stream in [
2645            test_streams.Silence16_Mono(200000, 44100),
2646            test_streams.Silence16_Mono(200000, 96000),
2647            test_streams.Silence16_Stereo(200000, 44100),
2648            test_streams.Silence16_Stereo(200000, 96000),
2649            test_streams.Silence24_Mono(200000, 44100),
2650            test_streams.Silence24_Mono(200000, 96000),
2651            test_streams.Silence24_Stereo(200000, 44100),
2652            test_streams.Silence24_Stereo(200000, 96000),
2653
2654            test_streams.Sine16_Mono(200000, 48000, 441.0, 0.50, 441.0, 0.49),
2655            test_streams.Sine16_Mono(200000, 96000, 441.0, 0.61, 661.5, 0.37),
2656            test_streams.Sine16_Mono(200000, 44100, 441.0, 0.50, 882.0, 0.49),
2657            test_streams.Sine16_Mono(200000, 44100, 441.0, 0.50, 4410.0, 0.49),
2658            test_streams.Sine16_Mono(200000, 44100, 8820.0, 0.70, 4410.0, 0.29),
2659
2660            test_streams.Sine16_Stereo(200000, 48000, 441.0, 0.50, 441.0, 0.49, 1.0),
2661            test_streams.Sine16_Stereo(200000, 48000, 441.0, 0.61, 661.5, 0.37, 1.0),
2662            test_streams.Sine16_Stereo(200000, 96000, 441.0, 0.50, 882.0, 0.49, 1.0),
2663            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.0),
2664            test_streams.Sine16_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 1.0),
2665            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.50, 441.0, 0.49, 0.5),
2666            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.61, 661.5, 0.37, 2.0),
2667            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.50, 882.0, 0.49, 0.7),
2668            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.3),
2669            test_streams.Sine16_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 0.1),
2670
2671            test_streams.Sine24_Mono(200000, 48000, 441.0, 0.50, 441.0, 0.49),
2672            test_streams.Sine24_Mono(200000, 96000, 441.0, 0.61, 661.5, 0.37),
2673            test_streams.Sine24_Mono(200000, 44100, 441.0, 0.50, 882.0, 0.49),
2674            test_streams.Sine24_Mono(200000, 44100, 441.0, 0.50, 4410.0, 0.49),
2675            test_streams.Sine24_Mono(200000, 44100, 8820.0, 0.70, 4410.0, 0.29),
2676
2677            test_streams.Sine24_Stereo(200000, 48000, 441.0, 0.50, 441.0, 0.49, 1.0),
2678            test_streams.Sine24_Stereo(200000, 48000, 441.0, 0.61, 661.5, 0.37, 1.0),
2679            test_streams.Sine24_Stereo(200000, 96000, 441.0, 0.50, 882.0, 0.49, 1.0),
2680            test_streams.Sine24_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.0),
2681            test_streams.Sine24_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 1.0),
2682            test_streams.Sine24_Stereo(200000, 44100, 441.0, 0.50, 441.0, 0.49, 0.5),
2683            test_streams.Sine24_Stereo(200000, 44100, 441.0, 0.61, 661.5, 0.37, 2.0),
2684            test_streams.Sine24_Stereo(200000, 44100, 441.0, 0.50, 882.0, 0.49, 0.7),
2685            test_streams.Sine24_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.3),
2686            test_streams.Sine24_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 0.1)]:
2687            yield stream
2688
2689    def __multichannel_stream_variations__(self):
2690        for stream in [
2691            test_streams.Simple_Sine(200000, 44100, 0x0007, 16,
2692                                     (6400, 10000),
2693                                     (12800, 20000),
2694                                     (30720, 30000)),
2695            test_streams.Simple_Sine(200000, 44100, 0x0107, 16,
2696                                     (6400, 10000),
2697                                     (12800, 20000),
2698                                     (19200, 30000),
2699                                     (16640, 40000)),
2700            test_streams.Simple_Sine(200000, 44100, 0x0037, 16,
2701                                     (6400, 10000),
2702                                     (8960, 15000),
2703                                     (11520, 20000),
2704                                     (12800, 25000),
2705                                     (14080, 30000)),
2706            test_streams.Simple_Sine(200000, 44100, 0x003F, 16,
2707                                     (6400, 10000),
2708                                     (11520, 15000),
2709                                     (16640, 20000),
2710                                     (21760, 25000),
2711                                     (26880, 30000),
2712                                     (30720, 35000)),
2713            test_streams.Simple_Sine(200000, 44100, 0x013F, 16,
2714                                     (6400, 10000),
2715                                     (11520, 15000),
2716                                     (16640, 20000),
2717                                     (21760, 25000),
2718                                     (26880, 30000),
2719                                     (30720, 35000),
2720                                     (29000, 40000)),
2721            test_streams.Simple_Sine(200000, 44100, 0x00FF, 16,
2722                                     (6400, 10000),
2723                                     (11520, 15000),
2724                                     (16640, 20000),
2725                                     (21760, 25000),
2726                                     (26880, 30000),
2727                                     (30720, 35000),
2728                                     (29000, 40000),
2729                                     (28000, 45000)),
2730
2731            test_streams.Simple_Sine(200000, 44100, 0x0007, 24,
2732                                     (1638400, 10000),
2733                                     (3276800, 20000),
2734                                     (7864320, 30000)),
2735            test_streams.Simple_Sine(200000, 44100, 0x0107, 24,
2736                                     (1638400, 10000),
2737                                     (3276800, 20000),
2738                                     (4915200, 30000),
2739                                     (4259840, 40000)),
2740            test_streams.Simple_Sine(200000, 44100, 0x0037, 24,
2741                                     (1638400, 10000),
2742                                     (2293760, 15000),
2743                                     (2949120, 20000),
2744                                     (3276800, 25000),
2745                                     (3604480, 30000)),
2746            test_streams.Simple_Sine(200000, 44100, 0x003F, 24,
2747                                     (1638400, 10000),
2748                                     (2949120, 15000),
2749                                     (4259840, 20000),
2750                                     (5570560, 25000),
2751                                     (6881280, 30000),
2752                                     (7864320, 35000)),
2753            test_streams.Simple_Sine(200000, 44100, 0x013F, 24,
2754                                     (1638400, 10000),
2755                                     (2949120, 15000),
2756                                     (4259840, 20000),
2757                                     (5570560, 25000),
2758                                     (6881280, 30000),
2759                                     (7864320, 35000),
2760                                     (7000000, 40000)),
2761            test_streams.Simple_Sine(200000, 44100, 0x00FF, 24,
2762                                     (1638400, 10000),
2763                                     (2949120, 15000),
2764                                     (4259840, 20000),
2765                                     (5570560, 25000),
2766                                     (6881280, 30000),
2767                                     (7864320, 35000),
2768                                     (7000000, 40000),
2769                                     (6000000, 45000))]:
2770            yield stream
2771
2772    @FORMAT_ALAC
2773    def test_streams(self):
2774        for g in self.__stream_variations__():
2775            md5sum = md5()
2776            f = g.read(audiotools.FRAMELIST_SIZE)
2777            while len(f) > 0:
2778                md5sum.update(f.to_bytes(False, True))
2779                f = g.read(audiotools.FRAMELIST_SIZE)
2780            self.assertEqual(md5sum.digest(), g.digest())
2781            g.close()
2782
2783        for g in self.__multichannel_stream_variations__():
2784            md5sum = md5()
2785            f = g.read(audiotools.FRAMELIST_SIZE)
2786            while len(f) > 0:
2787                md5sum.update(f.to_bytes(False, True))
2788                f = g.read(audiotools.FRAMELIST_SIZE)
2789            self.assertEqual(md5sum.digest(), g.digest())
2790            g.close()
2791
2792    @FORMAT_ALAC
2793    def test_small_files(self):
2794        for g in [test_streams.Generate01,
2795                  test_streams.Generate02]:
2796            self.__test_reader__(g(44100), 1, block_size=1152)
2797        for g in [test_streams.Generate03,
2798                  test_streams.Generate04]:
2799            self.__test_reader__(g(44100), 5, block_size=1152)
2800
2801    @FORMAT_ALAC
2802    def test_full_scale_deflection(self):
2803        for (bps, fsd) in [(16, test_streams.fsd16),
2804                           (24, test_streams.fsd24)]:
2805            for pattern in [test_streams.PATTERN01,
2806                            test_streams.PATTERN02,
2807                            test_streams.PATTERN03,
2808                            test_streams.PATTERN04,
2809                            test_streams.PATTERN05,
2810                            test_streams.PATTERN06,
2811                            test_streams.PATTERN07]:
2812                self.__test_reader__(
2813                    test_streams.MD5Reader(fsd(pattern, 100)),
2814                    len(pattern) * 100,
2815                    block_size=1152)
2816
2817    @FORMAT_ALAC
2818    def test_sines(self):
2819        for g in self.__stream_variations__():
2820            self.__test_reader__(g, 200000, block_size=1152)
2821
2822        for g in self.__multichannel_stream_variations__():
2823            self.__test_reader_nonalac__(g, 200000, block_size=1152)
2824
2825    @FORMAT_ALAC
2826    def test_wasted_bps(self):
2827        self.__test_reader__(test_streams.WastedBPS16(1000),
2828                             1000,
2829                             block_size=1152)
2830
2831    @FORMAT_ALAC
2832    def test_blocksizes(self):
2833        noise = struct.unpack(">32h", os.urandom(64))
2834
2835        for block_size in [16, 17, 18, 19, 20, 21, 22, 23, 24,
2836                           25, 26, 27, 28, 29, 30, 31, 32, 33]:
2837            self.__test_reader__(
2838                test_streams.MD5Reader(
2839                    test_streams.FrameListReader(noise,
2840                                                 44100, 1, 16)),
2841                len(noise),
2842                block_size=block_size)
2843
2844    @FORMAT_ALAC
2845    def test_noise(self):
2846        for (channels, mask) in [
2847            (1, int(audiotools.ChannelMask.from_channels(1))),
2848            (2, int(audiotools.ChannelMask.from_channels(2)))]:
2849            for bps in [16, 24]:
2850                # the reference decoder can't handle very large block sizes
2851                for blocksize in [32, 4096, 8192]:
2852                    self.__test_reader__(
2853                        MD5_Reader(
2854                            EXACT_RANDOM_PCM_Reader(
2855                                pcm_frames=65536,
2856                                sample_rate=44100,
2857                                channels=channels,
2858                                channel_mask=mask,
2859                                bits_per_sample=bps)),
2860                        65536,
2861                        block_size=blocksize)
2862
2863    @FORMAT_ALAC
2864    def test_fractional(self):
2865        def __perform_test__(block_size, pcm_frames):
2866            self.__test_reader__(
2867                MD5_Reader(
2868                    EXACT_RANDOM_PCM_Reader(
2869                        pcm_frames=pcm_frames,
2870                        sample_rate=44100,
2871                        channels=2,
2872                        bits_per_sample=16)),
2873                pcm_frames,
2874                block_size=block_size)
2875
2876        for pcm_frames in [31, 32, 33, 34, 35, 2046, 2047, 2048, 2049, 2050]:
2877            __perform_test__(33, pcm_frames)
2878
2879        for pcm_frames in [254, 255, 256, 257, 258, 510, 511, 512,
2880                           513, 514, 1022, 1023, 1024, 1025, 1026,
2881                           2046, 2047, 2048, 2049, 2050, 4094, 4095,
2882                           4096, 4097, 4098]:
2883            __perform_test__(256, pcm_frames)
2884
2885        for pcm_frames in [1022, 1023, 1024, 1025, 1026, 2046, 2047,
2886                           2048, 2049, 2050, 4094, 4095, 4096, 4097, 4098]:
2887            __perform_test__(2048, pcm_frames)
2888
2889        for pcm_frames in [1022, 1023, 1024, 1025, 1026, 2046, 2047, 2048,
2890                           2049, 2050, 4094, 4095, 4096, 4097, 4098, 4606,
2891                           4607, 4608, 4609, 4610, 8190, 8191, 8192, 8193,
2892                           8194, 16382, 16383, 16384, 16385, 16386]:
2893            __perform_test__(4608, pcm_frames)
2894
2895    @FORMAT_ALAC
2896    def test_frame_header_variations(self):
2897        self.__test_reader__(test_streams.Sine16_Mono(200000, 96000,
2898                                                      441.0, 0.61, 661.5, 0.37),
2899                             200000,
2900                             block_size=16)
2901
2902        # The alac(1) decoder I'm using as a reference can't handle
2903        # this block size, even though iTunes handles the resulting files
2904        # just fine.  Therefore, it's likely an alac bug beyond my
2905        # capability to fix.
2906        # I don't expect anyone will use anything other than the default
2907        # block size anyway.
2908
2909        # self.__test_reader__(test_streams.Sine16_Mono(200000, 96000,
2910        #                                               441.0, 0.61, 661.5, 0.37),
2911        #                      block_size=65535)
2912
2913        self.__test_reader__(test_streams.Sine16_Mono(200000, 9,
2914                                                      441.0, 0.61, 661.5, 0.37),
2915                             200000,
2916                             block_size=1152)
2917
2918        self.__test_reader__(test_streams.Sine16_Mono(200000, 90,
2919                                                      441.0, 0.61, 661.5, 0.37),
2920                             200000,
2921                             block_size=1152)
2922
2923        self.__test_reader__(test_streams.Sine16_Mono(200000, 90000,
2924                                                      441.0, 0.61, 661.5, 0.37),
2925                             200000,
2926                             block_size=1152)
2927
2928    @FORMAT_ALAC
2929    def test_python_codec(self):
2930        def test_python_reader(pcmreader, total_pcm_frames, block_size=4096):
2931            # ALAC doesn't really have encoding options worth mentioning
2932            from audiotools.py_encoders import encode_mdat
2933
2934            # encode file using Python-based encoder
2935            temp_file = tempfile.NamedTemporaryFile(suffix=".m4a")
2936            audiotools.ALACAudio.from_pcm(
2937                temp_file.name,
2938                pcmreader,
2939                block_size=block_size,
2940                encoding_function=encode_mdat)
2941
2942            # verify contents of file decoded by
2943            # Python-based decoder against contents decoded by
2944            # C-based decoder
2945            from audiotools.py_decoders import ALACDecoder as ALACDecoder1
2946            from audiotools.decoders import ALACDecoder as ALACDecoder2
2947
2948            self.assertTrue(
2949                audiotools.pcm_cmp(
2950                    ALACDecoder1(temp_file.name),
2951                    ALACDecoder2(temp_file.name)))
2952
2953            # test from_pcm() with total_pcm_frames indicated
2954            pcmreader.reset()
2955            audiotools.ALACAudio.from_pcm(
2956                temp_file.name,
2957                pcmreader,
2958                total_pcm_frames=total_pcm_frames,
2959                block_size=block_size,
2960                encoding_function=encode_mdat)
2961
2962            # verify contents of file decoded by
2963            # Python-based decoder against contents decoded by
2964            # C-based decoder
2965            from audiotools.py_decoders import ALACDecoder as ALACDecoder1
2966            from audiotools.decoders import ALACDecoder as ALACDecoder2
2967
2968            self.assertTrue(
2969                audiotools.pcm_cmp(
2970                    ALACDecoder1(temp_file.name),
2971                    ALACDecoder2(temp_file.name)))
2972
2973            temp_file.close()
2974
2975        # test small files
2976        for g in [test_streams.Generate01,
2977                  test_streams.Generate02]:
2978            test_python_reader(g(44100), 1, block_size=1152)
2979        for g in [test_streams.Generate03,
2980                  test_streams.Generate04]:
2981            test_python_reader(g(44100), 5, block_size=1152)
2982
2983        # test full scale deflection
2984        for (bps, fsd) in [(16, test_streams.fsd16),
2985                           (24, test_streams.fsd24)]:
2986            for pattern in [test_streams.PATTERN01,
2987                            test_streams.PATTERN02,
2988                            test_streams.PATTERN03,
2989                            test_streams.PATTERN04,
2990                            test_streams.PATTERN05,
2991                            test_streams.PATTERN06,
2992                            test_streams.PATTERN07]:
2993                test_python_reader(fsd(pattern, 100),
2994                                   len(pattern) * 100,
2995                                   block_size=1152)
2996
2997        # test silence
2998        for g in [test_streams.Silence16_Mono(5000, 48000),
2999                  test_streams.Silence16_Stereo(5000, 48000),
3000                  test_streams.Silence24_Mono(5000, 48000),
3001                  test_streams.Silence24_Stereo(5000, 48000)]:
3002            test_python_reader(g, 5000, block_size=1152)
3003
3004        # test sines
3005        for g in [test_streams.Sine16_Mono(5000, 48000,
3006                                           441.0, 0.50, 441.0, 0.49),
3007                  test_streams.Sine16_Mono(5000, 96000,
3008                                           441.0, 0.61, 661.5, 0.37),
3009                  test_streams.Sine16_Stereo(5000, 48000,
3010                                             441.0, 0.50, 441.0, 0.49, 1.0),
3011                  test_streams.Sine16_Stereo(5000, 96000,
3012                                             441.0, 0.50, 882.0, 0.49, 1.0),
3013                  test_streams.Sine24_Mono(5000, 48000,
3014                                           441.0, 0.50, 441.0, 0.49),
3015                  test_streams.Sine24_Mono(5000, 96000,
3016                                           441.0, 0.61, 661.5, 0.37),
3017                  test_streams.Sine24_Stereo(5000, 48000,
3018                                             441.0, 0.50, 441.0, 0.49, 1.0),
3019                  test_streams.Sine24_Stereo(5000, 96000,
3020                                             441.0, 0.50, 882.0, 0.49, 1.0)]:
3021            test_python_reader(g, 5000, block_size=1152)
3022
3023        for g in [test_streams.Simple_Sine(5000, 44100, 0x0007, 16,
3024                                           (6400, 10000),
3025                                           (12800, 20000),
3026                                           (30720, 30000)),
3027                  test_streams.Simple_Sine(5000, 44100, 0x0107, 16,
3028                                           (6400, 10000),
3029                                           (12800, 20000),
3030                                           (19200, 30000),
3031                                           (16640, 40000)),
3032                  test_streams.Simple_Sine(5000, 44100, 0x0037, 16,
3033                                           (6400, 10000),
3034                                           (8960, 15000),
3035                                           (11520, 20000),
3036                                           (12800, 25000),
3037                                           (14080, 30000)),
3038                  test_streams.Simple_Sine(5000, 44100, 0x003F, 16,
3039                                           (6400, 10000),
3040                                           (11520, 15000),
3041                                           (16640, 20000),
3042                                           (21760, 25000),
3043                                           (26880, 30000),
3044                                           (30720, 35000)),
3045                  test_streams.Simple_Sine(5000, 44100, 0x013F, 16,
3046                                           (6400, 10000),
3047                                           (11520, 15000),
3048                                           (16640, 20000),
3049                                           (21760, 25000),
3050                                           (26880, 30000),
3051                                           (30720, 35000),
3052                                           (29000, 40000)),
3053                  test_streams.Simple_Sine(5000, 44100, 0x00FF, 16,
3054                                           (6400, 10000),
3055                                           (11520, 15000),
3056                                           (16640, 20000),
3057                                           (21760, 25000),
3058                                           (26880, 30000),
3059                                           (30720, 35000),
3060                                           (29000, 40000),
3061                                           (28000, 45000)),
3062
3063                  test_streams.Simple_Sine(5000, 44100, 0x0007, 24,
3064                                           (1638400, 10000),
3065                                           (3276800, 20000),
3066                                           (7864320, 30000)),
3067                  test_streams.Simple_Sine(5000, 44100, 0x0107, 24,
3068                                           (1638400, 10000),
3069                                           (3276800, 20000),
3070                                           (4915200, 30000),
3071                                           (4259840, 40000)),
3072                  test_streams.Simple_Sine(5000, 44100, 0x0037, 24,
3073                                           (1638400, 10000),
3074                                           (2293760, 15000),
3075                                           (2949120, 20000),
3076                                           (3276800, 25000),
3077                                           (3604480, 30000)),
3078                  test_streams.Simple_Sine(5000, 44100, 0x003F, 24,
3079                                           (1638400, 10000),
3080                                           (2949120, 15000),
3081                                           (4259840, 20000),
3082                                           (5570560, 25000),
3083                                           (6881280, 30000),
3084                                           (7864320, 35000)),
3085                  test_streams.Simple_Sine(5000, 44100, 0x013F, 24,
3086                                           (1638400, 10000),
3087                                           (2949120, 15000),
3088                                           (4259840, 20000),
3089                                           (5570560, 25000),
3090                                           (6881280, 30000),
3091                                           (7864320, 35000),
3092                                           (7000000, 40000)),
3093                  test_streams.Simple_Sine(5000, 44100, 0x00FF, 24,
3094                                           (1638400, 10000),
3095                                           (2949120, 15000),
3096                                           (4259840, 20000),
3097                                           (5570560, 25000),
3098                                           (6881280, 30000),
3099                                           (7864320, 35000),
3100                                           (7000000, 40000),
3101                                           (6000000, 45000))]:
3102            test_python_reader(g, 5000, block_size=1152)
3103
3104        # test wasted BPS
3105        test_python_reader(test_streams.WastedBPS16(1000),
3106                           1000,
3107                           block_size=1152)
3108
3109        # test block sizes
3110        noise = struct.unpack(">32h", os.urandom(64))
3111
3112        for block_size in [16, 17, 18, 19, 20, 21, 22, 23, 24,
3113                           25, 26, 27, 28, 29, 30, 31, 32, 33]:
3114            test_python_reader(
3115                test_streams.MD5Reader(
3116                    test_streams.FrameListReader(noise, 44100, 1, 16)),
3117                len(noise),
3118                block_size=block_size)
3119
3120        # test noise
3121        for (channels, mask) in [
3122            (1, int(audiotools.ChannelMask.from_channels(1))),
3123            (2, int(audiotools.ChannelMask.from_channels(2)))]:
3124            for bps in [16, 24]:
3125                # the reference decoder can't handle very large block sizes
3126                for blocksize in [32, 4096, 8192]:
3127                    test_python_reader(
3128                        EXACT_RANDOM_PCM_Reader(
3129                            pcm_frames=4097,
3130                            sample_rate=44100,
3131                            channels=channels,
3132                            channel_mask=mask,
3133                            bits_per_sample=bps),
3134                        4097,
3135                        block_size=blocksize)
3136
3137        # test fractional
3138        for (block_size,
3139             pcm_frames) in [(33, [31, 32, 33, 34, 35, 2046,
3140                                   2047, 2048, 2049, 2050]),
3141                             (256, [254, 255, 256, 257, 258, 510, 511, 512,
3142                                    513, 514, 1022, 1023, 1024, 1025, 1026,
3143                                    2046, 2047, 2048, 2049, 2050, 4094, 4095,
3144                                    4096, 4097, 4098])]:
3145            for frame_count in pcm_frames:
3146                test_python_reader(
3147                    EXACT_RANDOM_PCM_Reader(
3148                        pcm_frames=frame_count,
3149                        sample_rate=44100,
3150                        channels=2,
3151                        bits_per_sample=16),
3152                    frame_count,
3153                    block_size=block_size)
3154
3155        # test frame header variations
3156        test_python_reader(
3157            test_streams.Sine16_Mono(5000, 96000,
3158                                     441.0, 0.61, 661.5, 0.37),
3159            5000,
3160            block_size=16)
3161
3162        test_python_reader(
3163            test_streams.Sine16_Mono(5000, 9,
3164                                     441.0, 0.61, 661.5, 0.37),
3165            5000,
3166            block_size=1152)
3167
3168        test_python_reader(
3169            test_streams.Sine16_Mono(5000, 90,
3170                                     441.0, 0.61, 661.5, 0.37),
3171            5000,
3172            block_size=1152)
3173
3174        test_python_reader(
3175            test_streams.Sine16_Mono(5000, 90000,
3176                                     441.0, 0.61, 661.5, 0.37),
3177            5000,
3178            block_size=1152)
3179
3180
3181class AUFileTest(LosslessFileTest):
3182    def setUp(self):
3183        self.audio_class = audiotools.AuAudio
3184        self.suffix = "." + self.audio_class.SUFFIX
3185
3186    @FORMAT_AU
3187    def test_overlong_file(self):
3188        # trying to generate too large of a file
3189        # should throw an exception right away if total_pcm_frames known
3190        # instead of building it first
3191
3192        self.assertEqual(os.path.isfile("invalid.au"), False)
3193
3194        self.assertRaises(audiotools.EncodingError,
3195                          self.audio_class.from_pcm,
3196                          "invalid.au",
3197                          EXACT_SILENCE_PCM_Reader(
3198                              pcm_frames=715827883,
3199                              sample_rate=44100,
3200                              channels=2,
3201                              bits_per_sample=24),
3202                          total_pcm_frames=715827883)
3203
3204        self.assertEqual(os.path.isfile("invalid.au"), False)
3205
3206    @FORMAT_AU
3207    def test_channel_mask(self):
3208        temp = tempfile.NamedTemporaryFile(suffix=self.suffix)
3209        try:
3210            for mask in [["front_center"],
3211                         ["front_left",
3212                          "front_right"]]:
3213                cm = audiotools.ChannelMask.from_fields(
3214                    **dict([(f, True) for f in mask]))
3215                track = self.audio_class.from_pcm(
3216                    temp.name, BLANK_PCM_Reader(1,
3217                                                channels=len(cm),
3218                                                channel_mask=int(cm)))
3219                self.assertEqual(track.channels(), len(cm))
3220                self.assertEqual(track.channel_mask(), cm)
3221                track = audiotools.open(temp.name)
3222                self.assertEqual(track.channels(), len(cm))
3223                self.assertEqual(track.channel_mask(), cm)
3224
3225            for mask in [["front_left",
3226                          "front_right",
3227                          "front_center"],
3228                         ["front_left",
3229                          "front_right",
3230                          "back_left",
3231                          "back_right"],
3232                         ["front_left",
3233                          "front_right",
3234                          "front_center",
3235                          "back_left",
3236                          "back_right"],
3237                         ["front_left",
3238                          "front_right",
3239                          "front_center",
3240                          "low_frequency",
3241                          "back_left",
3242                          "back_right"]]:
3243                cm = audiotools.ChannelMask.from_fields(
3244                    **dict([(f, True) for f in mask]))
3245                track = self.audio_class.from_pcm(
3246                    temp.name, BLANK_PCM_Reader(1,
3247                                                channels=len(cm),
3248                                                channel_mask=int(cm)))
3249                self.assertEqual(track.channels(), len(cm))
3250                self.assertEqual(track.channel_mask(), 0)
3251                track = audiotools.open(temp.name)
3252                self.assertEqual(track.channels(), len(cm))
3253                self.assertEqual(track.channel_mask(), 0)
3254        finally:
3255            temp.close()
3256
3257    @FORMAT_AU
3258    def test_verify(self):
3259        # test truncated file
3260        with tempfile.NamedTemporaryFile(
3261            suffix="." + self.audio_class.SUFFIX) as temp:
3262            track = self.audio_class.from_pcm(
3263                temp.name,
3264                BLANK_PCM_Reader(1))
3265            with open(temp.name, 'rb') as f:
3266                good_data = f.read()
3267            with open(temp.name, 'wb') as f:
3268                f.write(good_data[0:-10])
3269            reader = track.to_pcm()
3270            self.assertNotEqual(reader, None)
3271            self.assertRaises(IOError,
3272                              audiotools.transfer_framelist_data,
3273                              reader, lambda x: x)
3274
3275        # test convert() error
3276        with tempfile.NamedTemporaryFile(
3277            suffix="." + self.audio_class.SUFFIX) as temp:
3278            track = self.audio_class.from_pcm(
3279                temp.name,
3280                BLANK_PCM_Reader(1))
3281            with open(temp.name, 'rb') as f:
3282                good_data = f.read()
3283            with open(temp.name, 'wb') as f:
3284                f.write(good_data[0:-10])
3285            if os.path.isfile("dummy.wav"):
3286                os.unlink("dummy.wav")
3287            self.assertEqual(os.path.isfile("dummy.wav"), False)
3288            self.assertRaises(audiotools.EncodingError,
3289                              track.convert,
3290                              "dummy.wav",
3291                              audiotools.WaveAudio)
3292            self.assertEqual(os.path.isfile("dummy.wav"), False)
3293
3294
3295class FlacFileTest(TestForeignAiffChunks,
3296                   TestForeignWaveChunks,
3297                   LosslessFileTest):
3298    def setUp(self):
3299        self.audio_class = audiotools.FlacAudio
3300        self.suffix = "." + self.audio_class.SUFFIX
3301
3302        from audiotools.decoders import FlacDecoder
3303        from audiotools.encoders import encode_flac
3304
3305        self.decoder = FlacDecoder
3306        self.encode = encode_flac
3307        self.encode_opts = [{"block_size": 1152,
3308                             "max_lpc_order": 0,
3309                             "min_residual_partition_order": 0,
3310                             "max_residual_partition_order": 3},
3311                            {"block_size": 1152,
3312                             "max_lpc_order": 0,
3313                             "adaptive_mid_side": True,
3314                             "min_residual_partition_order": 0,
3315                             "max_residual_partition_order": 3},
3316                            {"block_size": 1152,
3317                             "max_lpc_order": 0,
3318                             "exhaustive_model_search": True,
3319                             "min_residual_partition_order": 0,
3320                             "max_residual_partition_order": 3},
3321                            {"block_size": 4096,
3322                             "max_lpc_order": 6,
3323                             "min_residual_partition_order": 0,
3324                             "max_residual_partition_order": 4},
3325                            {"block_size": 4096,
3326                             "max_lpc_order": 8,
3327                             "adaptive_mid_side": True,
3328                             "min_residual_partition_order": 0,
3329                             "max_residual_partition_order": 4},
3330                            {"block_size": 4096,
3331                             "max_lpc_order": 8,
3332                             "mid_side": True,
3333                             "min_residual_partition_order": 0,
3334                             "max_residual_partition_order": 5},
3335                            {"block_size": 4096,
3336                             "max_lpc_order": 8,
3337                             "mid_side": True,
3338                             "min_residual_partition_order": 0,
3339                             "max_residual_partition_order": 6},
3340                            {"block_size": 4096,
3341                             "max_lpc_order": 8,
3342                             "mid_side": True,
3343                             "exhaustive_model_search": True,
3344                             "min_residual_partition_order": 0,
3345                             "max_residual_partition_order": 6},
3346                            {"block_size": 4096,
3347                             "max_lpc_order": 12,
3348                             "mid_side": True,
3349                             "exhaustive_model_search": True,
3350                             "min_residual_partition_order": 0,
3351                             "max_residual_partition_order": 6}]
3352
3353    @FORMAT_FLAC
3354    def test_init(self):
3355        # check missing file
3356        self.assertRaises(audiotools.flac.InvalidFLAC,
3357                          audiotools.FlacAudio,
3358                          "/dev/null/foo")
3359
3360        # check invalid file
3361        with tempfile.NamedTemporaryFile(suffix=".flac") as invalid_file:
3362            for c in [b"i", b"n", b"v", b"a", b"l", b"i", b"d",
3363                      b"s", b"t", b"r", b"i", b"n", b"g", b"x", b"x", b"x"]:
3364                invalid_file.write(c)
3365                invalid_file.flush()
3366                self.assertRaises(audiotools.flac.InvalidFLAC,
3367                                  audiotools.FlacAudio,
3368                                  invalid_file.name)
3369
3370        # check some decoder errors,
3371        # mostly to ensure a failed init doesn't make Python explode
3372        self.assertRaises(IOError, self.decoder, None)
3373
3374        self.assertRaises(IOError, self.decoder, "filename")
3375
3376    @FORMAT_FLAC
3377    def test_metadata2(self):
3378        from bz2 import decompress
3379
3380        with tempfile.NamedTemporaryFile(suffix=self.suffix) as temp:
3381            track = self.audio_class.from_pcm(temp.name,
3382                                              BLANK_PCM_Reader(1))
3383
3384            # check that a non-cover image with a description round-trips
3385            m = audiotools.MetaData()
3386            m.add_image(
3387                audiotools.Image.new(
3388                    TEST_COVER1, u'Unicode \u3057\u3066\u307f\u308b', 1))
3389            track.set_metadata(m)
3390
3391            new_track = audiotools.open(track.filename)
3392            m2 = new_track.get_metadata()
3393
3394            self.assertEqual(m.images()[0], m2.images()[0])
3395
3396            orig_md5 = md5()
3397            audiotools.transfer_framelist_data(track.to_pcm(), orig_md5.update)
3398
3399            # add an image too large to fit into a FLAC metadata chunk
3400            metadata = track.get_metadata()
3401            metadata.add_image(
3402                audiotools.Image.new(decompress(HUGE_BMP), u'', 0))
3403
3404            track.update_metadata(metadata)
3405
3406            # ensure that setting the metadata doesn't break the file
3407            new_md5 = md5()
3408            audiotools.transfer_framelist_data(track.to_pcm(), new_md5.update)
3409
3410            self.assertEqual(orig_md5.hexdigest(),
3411                             new_md5.hexdigest())
3412
3413            # ensure that setting fresh oversized metadata
3414            # doesn't break the file
3415            metadata = audiotools.MetaData()
3416            metadata.add_image(
3417                audiotools.Image.new(decompress(HUGE_BMP), u'', 0))
3418
3419            track.set_metadata(metadata)
3420
3421            new_md5 = md5()
3422            audiotools.transfer_framelist_data(track.to_pcm(), new_md5.update)
3423
3424            self.assertEqual(orig_md5.hexdigest(),
3425                             new_md5.hexdigest())
3426
3427            # add a COMMENT block too large to fit into a FLAC metadata chunk
3428            metadata = track.get_metadata()
3429            metadata.comment = u"a" * 16777216
3430
3431            track.update_metadata(metadata)
3432
3433            # ensure that setting the metadata doesn't break the file
3434            new_md5 = md5()
3435            audiotools.transfer_framelist_data(track.to_pcm(), new_md5.update)
3436
3437            self.assertEqual(orig_md5.hexdigest(),
3438                             new_md5.hexdigest())
3439
3440            # ensure that setting fresh oversized metadata
3441            # doesn't break the file
3442            metadata = audiotools.MetaData(comment=u"a" * 16777216)
3443
3444            track.set_metadata(metadata)
3445
3446            new_md5 = md5()
3447            audiotools.transfer_framelist_data(track.to_pcm(), new_md5.update)
3448
3449            self.assertEqual(orig_md5.hexdigest(),
3450                             new_md5.hexdigest())
3451
3452            track.set_metadata(audiotools.MetaData(track_name=u"Testing"))
3453
3454            # ensure that vendor_string isn't modified by setting metadata
3455            metadata = track.get_metadata()
3456            self.assertIsNotNone(metadata)
3457            self.assertEqual(metadata.track_name, u"Testing")
3458            self.assertIsNotNone(
3459                metadata.get_block(audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID))
3460            vorbis_comment = metadata.get_blocks(
3461                audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)
3462            proper_vendor_string = vorbis_comment[0].vendor_string
3463            vorbis_comment[0].vendor_string = u"Different String"
3464            metadata.replace_blocks(audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID,
3465                                    vorbis_comment)
3466            track.set_metadata(metadata)
3467            vendor_string = track.get_metadata().get_block(
3468                audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID).vendor_string
3469            self.assertEqual(vendor_string, proper_vendor_string)
3470
3471            # FIXME - ensure that channel mask isn't modified
3472            # by setting metadata
3473
3474    @FORMAT_FLAC
3475    def test_update_metadata(self):
3476        # build a temporary file
3477        with tempfile.NamedTemporaryFile(suffix=".flac") as temp:
3478            with open("flac-allframes.flac", "rb") as f:
3479                temp.write(f.read())
3480                temp.flush()
3481            flac_file = audiotools.open(temp.name)
3482
3483            # attempt to adjust its metadata with bogus side data fields
3484            metadata = flac_file.get_metadata()
3485            streaminfo = metadata.get_block(
3486                audiotools.flac.Flac_STREAMINFO.BLOCK_ID)
3487
3488            minimum_block_size = streaminfo.minimum_block_size
3489            maximum_block_size = streaminfo.maximum_block_size
3490            minimum_frame_size = streaminfo.minimum_frame_size
3491            maximum_frame_size = streaminfo.maximum_frame_size
3492            sample_rate = streaminfo.sample_rate
3493            channels = streaminfo.channels
3494            bits_per_sample = streaminfo.bits_per_sample
3495            total_samples = streaminfo.total_samples
3496            md5sum = streaminfo.md5sum
3497
3498            streaminfo.minimum_block_size = 1
3499            streaminfo.maximum_block_size = 10
3500            streaminfo.minimum_frame_size = 2
3501            streaminfo.maximum_frame_size = 11
3502            streaminfo.sample_rate = 96000
3503            streaminfo.channels = 4
3504            streaminfo.bits_per_sample = 24
3505            streaminfo.total_samples = 96000
3506            streaminfo.md5sum = b"\x01" * 16
3507
3508            metadata.replace_blocks(audiotools.flac.Flac_STREAMINFO.BLOCK_ID,
3509                                    [streaminfo])
3510
3511            # ensure that set_metadata() restores fields to original values
3512            flac_file.set_metadata(metadata)
3513            metadata = flac_file.get_metadata()
3514            streaminfo = metadata.get_block(
3515                audiotools.flac.Flac_STREAMINFO.BLOCK_ID)
3516
3517            self.assertEqual(minimum_block_size,
3518                             streaminfo.minimum_block_size)
3519            self.assertEqual(maximum_block_size,
3520                             streaminfo.maximum_block_size)
3521            self.assertEqual(minimum_frame_size,
3522                             streaminfo.minimum_frame_size)
3523            self.assertEqual(maximum_frame_size,
3524                             streaminfo.maximum_frame_size)
3525            self.assertEqual(sample_rate,
3526                             streaminfo.sample_rate)
3527            self.assertEqual(channels,
3528                             streaminfo.channels)
3529            self.assertEqual(bits_per_sample,
3530                             streaminfo.bits_per_sample)
3531            self.assertEqual(total_samples,
3532                             streaminfo.total_samples)
3533            self.assertEqual(md5sum,
3534                             streaminfo.md5sum)
3535
3536            # adjust its metadata with new bogus side data files
3537            metadata = flac_file.get_metadata()
3538            streaminfo = metadata.get_block(
3539                audiotools.flac.Flac_STREAMINFO.BLOCK_ID)
3540            streaminfo.minimum_block_size = 1
3541            streaminfo.maximum_block_size = 10
3542            streaminfo.minimum_frame_size = 2
3543            streaminfo.maximum_frame_size = 11
3544            streaminfo.sample_rate = 96000
3545            streaminfo.channels = 4
3546            streaminfo.bits_per_sample = 24
3547            streaminfo.total_samples = 96000
3548            streaminfo.md5sum = b"\x01" * 16
3549
3550            metadata.replace_blocks(audiotools.flac.Flac_STREAMINFO.BLOCK_ID,
3551                                    [streaminfo])
3552
3553            # ensure that update_metadata() uses the bogus side data
3554            flac_file.update_metadata(metadata)
3555            metadata = flac_file.get_metadata()
3556            streaminfo = metadata.get_block(
3557                audiotools.flac.Flac_STREAMINFO.BLOCK_ID)
3558            self.assertEqual(streaminfo.minimum_block_size, 1)
3559            self.assertEqual(streaminfo.maximum_block_size, 10)
3560            self.assertEqual(streaminfo.minimum_frame_size, 2)
3561            self.assertEqual(streaminfo.maximum_frame_size, 11)
3562            self.assertEqual(streaminfo.sample_rate, 96000)
3563            self.assertEqual(streaminfo.channels, 4)
3564            self.assertEqual(streaminfo.bits_per_sample, 24)
3565            self.assertEqual(streaminfo.total_samples, 96000)
3566            self.assertEqual(streaminfo.md5sum, b"\x01" * 16)
3567
3568    @FORMAT_FLAC
3569    def test_verify(self):
3570        from test_core import bytes_to_ints, ints_to_bytes
3571
3572        self.assertEqual(
3573            audiotools.open("flac-allframes.flac").__md5__,
3574            b'\xf5\x3f\x86\x87\x6d\xcd\x77\x83' +
3575            b'\x22\x5c\x93\xba\x8a\x93\x8c\x7d')
3576
3577        with open("flac-allframes.flac", "rb") as f:
3578            flac_data = bytes_to_ints(f.read())
3579
3580        self.assertEqual(audiotools.open("flac-allframes.flac").verify(),
3581                         True)
3582
3583        # try changing the file underfoot
3584        with tempfile.NamedTemporaryFile(suffix=".flac") as temp:
3585            temp.write(ints_to_bytes(flac_data))
3586            temp.flush()
3587            flac_file = audiotools.open(temp.name)
3588            self.assertEqual(flac_file.verify(), True)
3589
3590            for i in range(0, len(flac_data)):
3591                with open(temp.name, "wb") as f:
3592                    f.write(ints_to_bytes(flac_data[0:i]))
3593                self.assertRaises(audiotools.InvalidFile,
3594                                  flac_file.verify)
3595
3596            for i in range(0x2A, len(flac_data)):
3597                for j in range(8):
3598                    new_data = list(flac_data)
3599                    new_data[i] = new_data[i] ^ (1 << j)
3600                    with open(temp.name, "wb") as f:
3601                        f.write(ints_to_bytes(new_data))
3602                    self.assertRaises(audiotools.InvalidFile,
3603                                      flac_file.verify)
3604
3605        # check a FLAC file with a short header
3606        with tempfile.NamedTemporaryFile(suffix=".flac") as temp:
3607            for i in range(0, 0x2A):
3608                temp.seek(0, 0)
3609                temp.write(ints_to_bytes(flac_data[0:i]))
3610                temp.flush()
3611                self.assertEqual(os.path.getsize(temp.name), i)
3612                if i < 4:
3613                    with open(temp.name, "rb") as f:
3614                        self.assertIsNone(audiotools.file_type(f))
3615                with open(temp.name, "rb") as f:
3616                    self.assertRaises(IOError,
3617                                      audiotools.decoders.FlacDecoder,
3618                                      f)
3619
3620        # check a FLAC file that's been truncated
3621        with tempfile.NamedTemporaryFile(suffix=".flac") as temp:
3622            for i in range(0x2A, len(flac_data)):
3623                temp.seek(0, 0)
3624                temp.write(ints_to_bytes(flac_data[0:i]))
3625                temp.flush()
3626                self.assertEqual(os.path.getsize(temp.name), i)
3627                decoder = audiotools.open(temp.name).to_pcm()
3628                self.assertNotEqual(decoder, None)
3629                self.assertRaises(IOError,
3630                                  audiotools.transfer_framelist_data,
3631                                  decoder, lambda x: x)
3632
3633                self.assertRaises(audiotools.InvalidFile,
3634                                  audiotools.open(temp.name).verify)
3635
3636        # test a FLAC file with a single swapped bit
3637        with tempfile.NamedTemporaryFile(suffix=".flac") as temp:
3638            for i in range(0x2A, len(flac_data)):
3639                for j in range(8):
3640                    bytes = flac_data[:]
3641                    bytes[i] ^= (1 << j)
3642                    temp.seek(0, 0)
3643                    temp.write(ints_to_bytes(bytes))
3644                    temp.flush()
3645                    self.assertEqual(len(flac_data),
3646                                     os.path.getsize(temp.name))
3647
3648                    with audiotools.open(temp.name).to_pcm() as decoders:
3649                        try:
3650                            self.assertRaises(
3651                                ValueError,
3652                                audiotools.transfer_framelist_data,
3653                                decoders, lambda x: x)
3654                        except IOError:
3655                            # Randomly swapping bits may send the decoder
3656                            # off the end of the stream before triggering
3657                            # a CRC-16 error.
3658                            # We simply need to catch that case and continue
3659                            continue
3660
3661        # test a FLAC file with an invalid STREAMINFO block
3662        mismatch_streaminfos = [
3663            (4096, 4096, 12, 12, 44101, 0, 15, 80,
3664             b'\xf5?\x86\x87m\xcdw\x83"\\\x93\xba\x8a\x93\x8c}'),
3665            (4096, 4096, 12, 12, 44100, 1, 15, 80,
3666             b'\xf5?\x86\x87m\xcdw\x83"\\\x93\xba\x8a\x93\x8c}'),
3667            (4096, 4096, 12, 12, 44100, 0, 7, 80,
3668             b'\xf5?\x86\x87m\xcdw\x83"\\\x93\xba\x8a\x93\x8c}'),
3669            (4096, 1, 12, 12, 44100, 0, 15, 80,
3670             b'\xf5?\x86\x87m\xcdw\x83"\\\x93\xba\x8a\x93\x8c}'),
3671            (4096, 4096, 12, 12, 44100, 0, 15, 80,
3672             b'\xf5?\x86\x87m\xcdw\x83"\\\x93\xba\x8a\x93\x8d}')]
3673
3674        header = flac_data[0:8]
3675        data = flac_data[0x2A:]
3676
3677        from audiotools.bitstream import BitstreamWriter
3678
3679        for streaminfo in mismatch_streaminfos:
3680            with tempfile.NamedTemporaryFile(suffix=".flac") as temp:
3681                temp.seek(0, 0)
3682                temp.write(ints_to_bytes(header))
3683                BitstreamWriter(temp.file, False).build(
3684                    "16u 16u 24u 24u 20u 3u 5u 36U 16b",
3685                    streaminfo)
3686                temp.write(ints_to_bytes(data))
3687                temp.flush()
3688                with audiotools.open(temp.name).to_pcm() as decoders:
3689                    self.assertRaises(ValueError,
3690                                      audiotools.transfer_framelist_data,
3691                                      decoders, lambda x: x)
3692
3693        # test that convert() from an invalid file also raises an exception
3694        with tempfile.NamedTemporaryFile(suffix=".flac") as temp:
3695            temp.write(ints_to_bytes(flac_data[0:-10]))
3696            temp.flush()
3697            flac = audiotools.open(temp.name)
3698            if os.path.isfile("dummy.wav"):
3699                os.unlink("dummy.wav")
3700            self.assertEqual(os.path.isfile("dummy.wav"), False)
3701            self.assertRaises(audiotools.EncodingError,
3702                              flac.convert,
3703                              "dummy.wav",
3704                              audiotools.WaveAudio)
3705            self.assertEqual(os.path.isfile("dummy.wav"), False)
3706
3707    def __stream_variations__(self):
3708        for stream in [
3709            test_streams.Silence8_Mono(200000, 44100),
3710            test_streams.Silence8_Mono(200000, 96000),
3711            test_streams.Silence8_Stereo(200000, 44100),
3712            test_streams.Silence8_Stereo(200000, 96000),
3713            test_streams.Silence16_Mono(200000, 44100),
3714            test_streams.Silence16_Mono(200000, 96000),
3715            test_streams.Silence16_Stereo(200000, 44100),
3716            test_streams.Silence16_Stereo(200000, 96000),
3717            test_streams.Silence24_Mono(200000, 44100),
3718            test_streams.Silence24_Mono(200000, 96000),
3719            test_streams.Silence24_Stereo(200000, 44100),
3720            test_streams.Silence24_Stereo(200000, 96000),
3721
3722            test_streams.Sine8_Mono(200000, 48000, 441.0, 0.50, 441.0, 0.49),
3723            test_streams.Sine8_Mono(200000, 96000, 441.0, 0.61, 661.5, 0.37),
3724            test_streams.Sine8_Mono(200000, 44100, 441.0, 0.50, 882.0, 0.49),
3725            test_streams.Sine8_Mono(200000, 44100, 441.0, 0.50, 4410.0, 0.49),
3726            test_streams.Sine8_Mono(200000, 44100, 8820.0, 0.70, 4410.0, 0.29),
3727
3728            test_streams.Sine8_Stereo(200000, 48000, 441.0, 0.50, 441.0, 0.49, 1.0),
3729            test_streams.Sine8_Stereo(200000, 48000, 441.0, 0.61, 661.5, 0.37, 1.0),
3730            test_streams.Sine8_Stereo(200000, 96000, 441.0, 0.50, 882.0, 0.49, 1.0),
3731            test_streams.Sine8_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.0),
3732            test_streams.Sine8_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 1.0),
3733            test_streams.Sine8_Stereo(200000, 44100, 441.0, 0.50, 441.0, 0.49, 0.5),
3734            test_streams.Sine8_Stereo(200000, 44100, 441.0, 0.61, 661.5, 0.37, 2.0),
3735            test_streams.Sine8_Stereo(200000, 44100, 441.0, 0.50, 882.0, 0.49, 0.7),
3736            test_streams.Sine8_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.3),
3737            test_streams.Sine8_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 0.1),
3738
3739            test_streams.Sine16_Mono(200000, 48000, 441.0, 0.50, 441.0, 0.49),
3740            test_streams.Sine16_Mono(200000, 96000, 441.0, 0.61, 661.5, 0.37),
3741            test_streams.Sine16_Mono(200000, 44100, 441.0, 0.50, 882.0, 0.49),
3742            test_streams.Sine16_Mono(200000, 44100, 441.0, 0.50, 4410.0, 0.49),
3743            test_streams.Sine16_Mono(200000, 44100, 8820.0, 0.70, 4410.0, 0.29),
3744
3745            test_streams.Sine16_Stereo(200000, 48000, 441.0, 0.50, 441.0, 0.49, 1.0),
3746            test_streams.Sine16_Stereo(200000, 48000, 441.0, 0.61, 661.5, 0.37, 1.0),
3747            test_streams.Sine16_Stereo(200000, 96000, 441.0, 0.50, 882.0, 0.49, 1.0),
3748            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.0),
3749            test_streams.Sine16_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 1.0),
3750            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.50, 441.0, 0.49, 0.5),
3751            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.61, 661.5, 0.37, 2.0),
3752            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.50, 882.0, 0.49, 0.7),
3753            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.3),
3754            test_streams.Sine16_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 0.1),
3755
3756            test_streams.Sine24_Mono(200000, 48000, 441.0, 0.50, 441.0, 0.49),
3757            test_streams.Sine24_Mono(200000, 96000, 441.0, 0.61, 661.5, 0.37),
3758            test_streams.Sine24_Mono(200000, 44100, 441.0, 0.50, 882.0, 0.49),
3759            test_streams.Sine24_Mono(200000, 44100, 441.0, 0.50, 4410.0, 0.49),
3760            test_streams.Sine24_Mono(200000, 44100, 8820.0, 0.70, 4410.0, 0.29),
3761
3762            test_streams.Sine24_Stereo(200000, 48000, 441.0, 0.50, 441.0, 0.49, 1.0),
3763            test_streams.Sine24_Stereo(200000, 48000, 441.0, 0.61, 661.5, 0.37, 1.0),
3764            test_streams.Sine24_Stereo(200000, 96000, 441.0, 0.50, 882.0, 0.49, 1.0),
3765            test_streams.Sine24_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.0),
3766            test_streams.Sine24_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 1.0),
3767            test_streams.Sine24_Stereo(200000, 44100, 441.0, 0.50, 441.0, 0.49, 0.5),
3768            test_streams.Sine24_Stereo(200000, 44100, 441.0, 0.61, 661.5, 0.37, 2.0),
3769            test_streams.Sine24_Stereo(200000, 44100, 441.0, 0.50, 882.0, 0.49, 0.7),
3770            test_streams.Sine24_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.3),
3771            test_streams.Sine24_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 0.1),
3772
3773            test_streams.Simple_Sine(200000, 44100, 0x7, 8,
3774                                     (25, 10000),
3775                                     (50, 20000),
3776                                     (120, 30000)),
3777            test_streams.Simple_Sine(200000, 44100, 0x33, 8,
3778                                     (25, 10000),
3779                                     (50, 20000),
3780                                     (75, 30000),
3781                                     (65, 40000)),
3782            test_streams.Simple_Sine(200000, 44100, 0x37, 8,
3783                                     (25, 10000),
3784                                     (35, 15000),
3785                                     (45, 20000),
3786                                     (50, 25000),
3787                                     (55, 30000)),
3788            test_streams.Simple_Sine(200000, 44100, 0x3F, 8,
3789                                     (25, 10000),
3790                                     (45, 15000),
3791                                     (65, 20000),
3792                                     (85, 25000),
3793                                     (105, 30000),
3794                                     (120, 35000)),
3795
3796            test_streams.Simple_Sine(200000, 44100, 0x7, 16,
3797                                     (6400, 10000),
3798                                     (12800, 20000),
3799                                     (30720, 30000)),
3800            test_streams.Simple_Sine(200000, 44100, 0x33, 16,
3801                                     (6400, 10000),
3802                                     (12800, 20000),
3803                                     (19200, 30000),
3804                                     (16640, 40000)),
3805            test_streams.Simple_Sine(200000, 44100, 0x37, 16,
3806                                     (6400, 10000),
3807                                     (8960, 15000),
3808                                     (11520, 20000),
3809                                     (12800, 25000),
3810                                     (14080, 30000)),
3811            test_streams.Simple_Sine(200000, 44100, 0x3F, 16,
3812                                     (6400, 10000),
3813                                     (11520, 15000),
3814                                     (16640, 20000),
3815                                     (21760, 25000),
3816                                     (26880, 30000),
3817                                     (30720, 35000)),
3818
3819            test_streams.Simple_Sine(200000, 44100, 0x7, 24,
3820                                     (1638400, 10000),
3821                                     (3276800, 20000),
3822                                     (7864320, 30000)),
3823            test_streams.Simple_Sine(200000, 44100, 0x33, 24,
3824                                     (1638400, 10000),
3825                                     (3276800, 20000),
3826                                     (4915200, 30000),
3827                                     (4259840, 40000)),
3828            test_streams.Simple_Sine(200000, 44100, 0x37, 24,
3829                                     (1638400, 10000),
3830                                     (2293760, 15000),
3831                                     (2949120, 20000),
3832                                     (3276800, 25000),
3833                                     (3604480, 30000)),
3834            test_streams.Simple_Sine(200000, 44100, 0x3F, 24,
3835                                     (1638400, 10000),
3836                                     (2949120, 15000),
3837                                     (4259840, 20000),
3838                                     (5570560, 25000),
3839                                     (6881280, 30000),
3840                                     (7864320, 35000))]:
3841            yield stream
3842
3843    @FORMAT_FLAC
3844    def test_streams(self):
3845        for g in self.__stream_variations__():
3846            md5sum = md5()
3847            f = g.read(audiotools.FRAMELIST_SIZE)
3848            while len(f) > 0:
3849                md5sum.update(f.to_bytes(False, True))
3850                f = g.read(audiotools.FRAMELIST_SIZE)
3851            self.assertEqual(md5sum.digest(), g.digest())
3852            g.close()
3853
3854    def __test_reader__(self, pcmreader, **encode_options):
3855        if not audiotools.BIN.can_execute(audiotools.BIN["flac"]):
3856            self.assertTrue(
3857                False,
3858                "reference FLAC binary flac(1) required for this test")
3859
3860        temp_file = tempfile.NamedTemporaryFile(suffix=".flac")
3861        self.encode(temp_file.name,
3862                    audiotools.BufferedPCMReader(pcmreader),
3863                    **encode_options)
3864
3865        self.assertEqual(
3866            subprocess.call([audiotools.BIN["flac"], "-ts", temp_file.name]),
3867            0,
3868            "flac decode error on %s with options %s" %
3869            (repr(pcmreader),
3870             repr(encode_options)))
3871
3872        flac = audiotools.open(temp_file.name)
3873        self.assertGreater(flac.total_frames(), 0)
3874        if hasattr(pcmreader, "digest"):
3875            self.assertEqual(flac.__md5__, pcmreader.digest())
3876
3877        # check FlacDecoder using raw file
3878        md5sum = md5()
3879        d = self.decoder(open(temp_file.name, "rb"))
3880        f = d.read(audiotools.FRAMELIST_SIZE)
3881        while len(f) > 0:
3882            md5sum.update(f.to_bytes(False, True))
3883            f = d.read(audiotools.FRAMELIST_SIZE)
3884        d.close()
3885        self.assertEqual(md5sum.digest(), pcmreader.digest())
3886
3887        # check FlacDecoder using file-like wrapper
3888        md5sum = md5()
3889        d = self.decoder(Filewrapper(open(temp_file.name, "rb")))
3890        f = d.read(audiotools.FRAMELIST_SIZE)
3891        while len(f) > 0:
3892            md5sum.update(f.to_bytes(False, True))
3893            f = d.read(audiotools.FRAMELIST_SIZE)
3894        d.close()
3895        self.assertEqual(md5sum.digest(), pcmreader.digest())
3896
3897        temp_file.close()
3898
3899    @FORMAT_FLAC
3900    def test_small_files(self):
3901        for g in [test_streams.Generate01,
3902                  test_streams.Generate02,
3903                  test_streams.Generate03,
3904                  test_streams.Generate04]:
3905            self.__test_reader__(g(44100),
3906                                 block_size=1152,
3907                                 max_lpc_order=16,
3908                                 min_residual_partition_order=0,
3909                                 max_residual_partition_order=3,
3910                                 mid_side=True,
3911                                 adaptive_mid_side=True,
3912                                 exhaustive_model_search=True)
3913
3914    @FORMAT_FLAC
3915    def test_full_scale_deflection(self):
3916        for (bps, fsd) in [(8, test_streams.fsd8),
3917                           (16, test_streams.fsd16),
3918                           (24, test_streams.fsd24)]:
3919            for pattern in [test_streams.PATTERN01,
3920                            test_streams.PATTERN02,
3921                            test_streams.PATTERN03,
3922                            test_streams.PATTERN04,
3923                            test_streams.PATTERN05,
3924                            test_streams.PATTERN06,
3925                            test_streams.PATTERN07]:
3926                self.__test_reader__(
3927                    test_streams.MD5Reader(fsd(pattern, 100)),
3928                    block_size=1152,
3929                    max_lpc_order=16,
3930                    min_residual_partition_order=0,
3931                    max_residual_partition_order=3,
3932                    mid_side=True,
3933                    adaptive_mid_side=True,
3934                    exhaustive_model_search=True)
3935
3936    @FORMAT_FLAC
3937    def test_sines(self):
3938        import sys
3939
3940        for g in self.__stream_variations__():
3941            self.__test_reader__(g,
3942                                 block_size=1152,
3943                                 max_lpc_order=16,
3944                                 min_residual_partition_order=0,
3945                                 max_residual_partition_order=3,
3946                                 mid_side=True,
3947                                 adaptive_mid_side=True,
3948                                 exhaustive_model_search=True)
3949
3950    @FORMAT_FLAC
3951    def test_wasted_bps(self):
3952        self.__test_reader__(test_streams.WastedBPS16(1000),
3953                             block_size=1152,
3954                             max_lpc_order=16,
3955                             min_residual_partition_order=0,
3956                             max_residual_partition_order=3,
3957                             mid_side=True,
3958                             adaptive_mid_side=True,
3959                             exhaustive_model_search=True)
3960
3961    @FORMAT_FLAC
3962    def test_blocksizes(self):
3963        # FIXME - handle 8bps/24bps also
3964        noise = struct.unpack(">32h", os.urandom(64))
3965
3966        encoding_args = {"min_residual_partition_order": 0,
3967                         "max_residual_partition_order": 6,
3968                         "mid_side": True,
3969                         "adaptive_mid_side": True,
3970                         "exhaustive_model_search": True}
3971        for to_disable in [[],
3972                           ["disable_verbatim_subframes",
3973                            "disable_constant_subframes"],
3974                           ["disable_verbatim_subframes",
3975                            "disable_constant_subframes",
3976                            "disable_fixed_subframes"]]:
3977            for block_size in [16, 17, 18, 19, 20, 21, 22, 23,
3978                               24, 25, 26, 27, 28, 29, 30, 31, 32, 33]:
3979                for lpc_order in [0, 1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17,
3980                                  31, 32]:
3981                    args = encoding_args.copy()
3982                    for disable in to_disable:
3983                        args[disable] = True
3984                    args["block_size"] = block_size
3985                    args["max_lpc_order"] = lpc_order
3986                    self.__test_reader__(
3987                        test_streams.MD5Reader(
3988                            test_streams.FrameListReader(noise,
3989                                                         44100, 1, 16)),
3990                        **args)
3991
3992    @FORMAT_FLAC
3993    def test_frame_header_variations(self):
3994        max_lpc_order = 16
3995
3996        self.__test_reader__(test_streams.Sine16_Mono(200000, 96000,
3997                                                      441.0, 0.61, 661.5, 0.37),
3998                             block_size=max_lpc_order,
3999                             max_lpc_order=max_lpc_order,
4000                             min_residual_partition_order=0,
4001                             max_residual_partition_order=3,
4002                             mid_side=True,
4003                             adaptive_mid_side=True,
4004                             exhaustive_model_search=True)
4005
4006        self.__test_reader__(test_streams.Sine16_Mono(200000, 96000,
4007                                                      441.0, 0.61, 661.5, 0.37),
4008                             block_size=65535,
4009                             max_lpc_order=max_lpc_order,
4010                             min_residual_partition_order=0,
4011                             max_residual_partition_order=3,
4012                             mid_side=True,
4013                             adaptive_mid_side=True,
4014                             exhaustive_model_search=True)
4015
4016        self.__test_reader__(test_streams.Sine16_Mono(200000, 9,
4017                                                      441.0, 0.61, 661.5, 0.37),
4018                             block_size=1152,
4019                             max_lpc_order=max_lpc_order,
4020                             min_residual_partition_order=0,
4021                             max_residual_partition_order=3,
4022                             mid_side=True,
4023                             adaptive_mid_side=True,
4024                             exhaustive_model_search=True)
4025
4026        self.__test_reader__(test_streams.Sine16_Mono(200000, 90,
4027                                                      441.0, 0.61, 661.5, 0.37),
4028                             block_size=1152,
4029                             max_lpc_order=max_lpc_order,
4030                             min_residual_partition_order=0,
4031                             max_residual_partition_order=3,
4032                             mid_side=True,
4033                             adaptive_mid_side=True,
4034                             exhaustive_model_search=True)
4035
4036        self.__test_reader__(test_streams.Sine16_Mono(200000, 90000,
4037                                                      441.0, 0.61, 661.5, 0.37),
4038                             block_size=1152,
4039                             max_lpc_order=max_lpc_order,
4040                             min_residual_partition_order=0,
4041                             max_residual_partition_order=3,
4042                             mid_side=True,
4043                             adaptive_mid_side=True,
4044                             exhaustive_model_search=True)
4045
4046        # the reference encoder's test_streams.sh unit test
4047        # re-does the 9Hz/90Hz/90000Hz tests for some reason
4048        # which I won't repeat here
4049
4050    @FORMAT_FLAC
4051    def test_option_variations(self):
4052        # testing all the option variations
4053        # against all the stream variations
4054        # along with a few extra option variations
4055        # takes a *long* time - so don't panic
4056
4057        for opts in self.encode_opts:
4058            encode_opts = opts.copy()
4059            for disable in [[],
4060                            ["disable_verbatim_subframes",
4061                             "disable_constant_subframes"],
4062                            ["disable_verbatim_subframes",
4063                             "disable_constant_subframes",
4064                             "disable_fixed_subframes"]]:
4065                for extra in [[],
4066                              # FIXME - no analogue for -p option
4067                              ["exhaustive_model_search"]]:
4068                    for d in disable:
4069                        encode_opts[d] = True
4070                    for e in extra:
4071                        encode_opts[e] = True
4072                    for g in self.__stream_variations__():
4073                        self.__test_reader__(g, **encode_opts)
4074
4075    @FORMAT_FLAC
4076    def test_noise_silence(self):
4077        for opts in self.encode_opts:
4078            encode_opts = opts.copy()
4079            for disable in [[],
4080                            ["disable_verbatim_subframes",
4081                             "disable_constant_subframes"],
4082                            ["disable_verbatim_subframes",
4083                             "disable_constant_subframes",
4084                             "disable_fixed_subframes"]]:
4085                for (channels, mask) in [
4086                    (1, audiotools.ChannelMask.from_channels(1)),
4087                    (2, audiotools.ChannelMask.from_channels(2)),
4088                    (4, audiotools.ChannelMask.from_fields(front_left=True,
4089                                                           front_right=True,
4090                                                           back_left=True,
4091                                                           back_right=True)),
4092                    (8, audiotools.ChannelMask(0))]:
4093                    for bps in [8, 16, 24]:
4094                        for extra in [[],
4095                                      # FIXME - no analogue for -p option
4096                                      ["exhaustive_model_search"]]:
4097                            for blocksize in [None, 32, 32768, 65535]:
4098                                for d in disable:
4099                                    encode_opts[d] = True
4100                                for e in extra:
4101                                    encode_opts[e] = True
4102                                if blocksize is not None:
4103                                    encode_opts["block_size"] = blocksize
4104
4105                                self.__test_reader__(
4106                                    MD5_Reader(
4107                                        EXACT_RANDOM_PCM_Reader(
4108                                            pcm_frames=65536,
4109                                            sample_rate=44100,
4110                                            channels=channels,
4111                                            channel_mask=mask,
4112                                            bits_per_sample=bps)),
4113                                    **encode_opts)
4114
4115                                self.__test_reader__(
4116                                    MD5_Reader(
4117                                        EXACT_SILENCE_PCM_Reader(
4118                                            pcm_frames=65536,
4119                                            sample_rate=44100,
4120                                            channels=channels,
4121                                            channel_mask=mask,
4122                                            bits_per_sample=bps)),
4123                                    **encode_opts)
4124
4125    @FORMAT_FLAC
4126    def test_fractional(self):
4127        def __perform_test__(block_size, pcm_frames):
4128            self.__test_reader__(
4129                MD5_Reader(
4130                    EXACT_RANDOM_PCM_Reader(
4131                        pcm_frames=pcm_frames,
4132                        sample_rate=44100,
4133                        channels=2,
4134                        bits_per_sample=16)),
4135                block_size=block_size,
4136                max_lpc_order=8,
4137                min_residual_partition_order=0,
4138                max_residual_partition_order=6)
4139
4140        for pcm_frames in [31, 32, 33, 34, 35, 2046, 2047, 2048, 2049, 2050]:
4141            __perform_test__(33, pcm_frames)
4142
4143        for pcm_frames in [254, 255, 256, 257, 258, 510, 511, 512, 513,
4144                           514, 1022, 1023, 1024, 1025, 1026, 2046, 2047,
4145                           2048, 2049, 2050, 4094, 4095, 4096, 4097, 4098]:
4146            __perform_test__(256, pcm_frames)
4147
4148        for pcm_frames in [1022, 1023, 1024, 1025, 1026, 2046, 2047,
4149                           2048, 2049, 2050, 4094, 4095, 4096, 4097, 4098]:
4150            __perform_test__(2048, pcm_frames)
4151
4152        for pcm_frames in [1022, 1023, 1024, 1025, 1026, 2046, 2047,
4153                           2048, 2049, 2050, 4094, 4095, 4096, 4097,
4154                           4098, 4606, 4607, 4608, 4609, 4610, 8190,
4155                           8191, 8192, 8193, 8194, 16382, 16383, 16384,
4156                           16385, 16386]:
4157            __perform_test__(4608, pcm_frames)
4158
4159    # PCMReaders don't yet support seeking,
4160    # so the seek tests can be skipped
4161
4162    # cuesheets are supported at the metadata level,
4163    # which is tested above
4164
4165    # WAVE and AIFF length fixups are handled by the
4166    # WaveAudio and AIFFAudio classes
4167
4168    # multiple file handling is performed at the tool level
4169
4170    # as is metadata handling
4171
4172    @FORMAT_FLAC
4173    def test_clean(self):
4174        # metadata is tested separately
4175
4176        from audiotools.text import (CLEAN_FLAC_REMOVE_ID3V2,
4177                                     CLEAN_FLAC_REMOVE_ID3V1,
4178                                     CLEAN_FLAC_REORDERED_STREAMINFO,
4179                                     CLEAN_FLAC_POPULATE_MD5,
4180                                     CLEAN_FLAC_ADD_CHANNELMASK,
4181                                     CLEAN_FLAC_FIX_SEEKTABLE,
4182                                     CLEAN_FLAC_ADD_SEEKTABLE)
4183
4184        # check FLAC files with ID3 tags
4185        with open("flac-id3.flac", "rb") as f:
4186            self.assertEqual(f.read(3), b"ID3")
4187        track = audiotools.open("flac-id3.flac")
4188        metadata1 = track.get_metadata()
4189        fixes = track.clean()
4190        self.assertEqual(fixes,
4191                         [CLEAN_FLAC_REMOVE_ID3V2,
4192                          CLEAN_FLAC_REMOVE_ID3V1])
4193        with tempfile.NamedTemporaryFile(suffix=".flac") as temp:
4194            fixes = track.clean(temp.name)
4195            self.assertEqual(fixes,
4196                             [CLEAN_FLAC_REMOVE_ID3V2,
4197                              CLEAN_FLAC_REMOVE_ID3V1])
4198            with open(temp.name, "rb") as f:
4199                self.assertEqual(f.read(4), b"fLaC")
4200            track2 = audiotools.open(temp.name)
4201            self.assertEqual(metadata1, track2.get_metadata())
4202            self.assertTrue(
4203                audiotools.pcm_cmp(track.to_pcm(), track2.to_pcm()))
4204
4205        # check FLAC files with double ID3 tags
4206        f = open("flac-id3-2.flac", "rb")
4207        self.assertEqual(f.read(3), b"ID3")
4208        f.close()
4209        track = audiotools.open("flac-id3-2.flac")
4210        metadata1 = track.get_metadata()
4211        fixes = track.clean()
4212        self.assertEqual(fixes,
4213                         [CLEAN_FLAC_REMOVE_ID3V2])
4214        with tempfile.NamedTemporaryFile(suffix=".flac") as temp:
4215            fixes = track.clean(temp.name)
4216            self.assertEqual(fixes,
4217                             [CLEAN_FLAC_REMOVE_ID3V2])
4218            with open(temp.name, "rb") as f:
4219                self.assertEqual(f.read(4), b"fLaC")
4220            track2 = audiotools.open(temp.name)
4221            self.assertEqual(metadata1, track2.get_metadata())
4222            self.assertTrue(
4223                audiotools.pcm_cmp(track.to_pcm(), track2.to_pcm()))
4224
4225        # check FLAC files with STREAMINFO in the wrong location
4226        with open("flac-disordered.flac", "rb") as f:
4227            self.assertEqual(f.read(5), b"fLaC\x04")
4228        track = audiotools.open("flac-disordered.flac")
4229        metadata1 = track.get_metadata()
4230        fixes = track.clean()
4231        self.assertEqual(fixes,
4232                         [CLEAN_FLAC_REORDERED_STREAMINFO])
4233        with tempfile.NamedTemporaryFile(suffix=".flac") as temp:
4234            fixes = track.clean(temp.name)
4235            self.assertEqual(fixes,
4236                             [CLEAN_FLAC_REORDERED_STREAMINFO])
4237            with open(temp.name, "rb") as f:
4238                self.assertEqual(f.read(5), b"fLaC\x00")
4239            track2 = audiotools.open(temp.name)
4240            self.assertEqual(metadata1, track2.get_metadata())
4241            self.assertTrue(
4242                audiotools.pcm_cmp(track.to_pcm(), track2.to_pcm()))
4243
4244        # check FLAC files with empty MD5 sum
4245        track = audiotools.open("flac-nonmd5.flac")
4246        fixes = []
4247        self.assertEqual(
4248            track.get_metadata().get_block(
4249                audiotools.flac.Flac_STREAMINFO.BLOCK_ID).md5sum, b"\x00" * 16)
4250        fixes = track.clean()
4251        self.assertEqual(fixes, [CLEAN_FLAC_POPULATE_MD5])
4252        with tempfile.NamedTemporaryFile(suffix=".flac") as temp:
4253            fixes = track.clean(temp.name)
4254            self.assertEqual(fixes, [CLEAN_FLAC_POPULATE_MD5])
4255            track2 = audiotools.open(temp.name)
4256            self.assertEqual(
4257                track2.get_metadata().get_block(
4258                    audiotools.flac.Flac_STREAMINFO.BLOCK_ID).md5sum,
4259                b'\xd2\xb1 \x19\x90\x19\xb69' +
4260                b'\xd5\xa7\xe2\xb3F>\x9c\x97')
4261            self.assertTrue(
4262                audiotools.pcm_cmp(track.to_pcm(), track2.to_pcm()))
4263
4264        # check FLAC files with no SEEKTABLE
4265        track = audiotools.open("flac-noseektable.flac")
4266        fixed = []
4267        self.assertFalse(
4268            track.get_metadata().has_block(
4269                audiotools.flac.Flac_SEEKTABLE.BLOCK_ID))
4270        fixes = track.clean()
4271        self.assertEqual(fixes, [CLEAN_FLAC_ADD_SEEKTABLE])
4272        with tempfile.NamedTemporaryFile(suffix=".flac") as temp:
4273            fixes = track.clean(temp.name)
4274            self.assertEqual(fixes, [CLEAN_FLAC_ADD_SEEKTABLE])
4275            track2 = audiotools.open(temp.name)
4276            self.assertTrue(
4277                track2.get_metadata().has_block(
4278                    audiotools.flac.Flac_SEEKTABLE.BLOCK_ID))
4279            self.assertTrue(
4280                audiotools.pcm_cmp(track.to_pcm(), track2.to_pcm()))
4281
4282        # check 24bps/6ch FLAC files without WAVEFORMATEXTENSIBLE_CHANNEL_MASK
4283        for (path, mask) in [("flac-nomask1.flac", 0x3F),
4284                             ("flac-nomask2.flac", 0x3F),
4285                             ("flac-nomask3.flac", 0x3),
4286                             ("flac-nomask4.flac", 0x3)]:
4287            with tempfile.NamedTemporaryFile(suffix=".flac") as no_blocks_file:
4288                with open(path, "rb") as f:
4289                    no_blocks_file.write(f.read())
4290                    no_blocks_file.flush()
4291                track = audiotools.open(no_blocks_file.name)
4292                metadata = track.get_metadata()
4293                for block_id in [1, 2, 4, 5, 6]:
4294                    metadata.replace_blocks(block_id, [])
4295                track.update_metadata(metadata)
4296
4297                for track in [audiotools.open(path),
4298                              audiotools.open(no_blocks_file.name)]:
4299                    fixes = track.clean()
4300                    self.assertEqual(fixes, [CLEAN_FLAC_ADD_CHANNELMASK])
4301
4302                    with tempfile.NamedTemporaryFile(suffix=".flac") as temp:
4303                        fixes = track.clean(temp.name)
4304                        self.assertEqual(
4305                            fixes,
4306                            [CLEAN_FLAC_ADD_CHANNELMASK])
4307                        new_track = audiotools.open(temp.name)
4308                        self.assertEqual(new_track.channel_mask(),
4309                                         track.channel_mask())
4310                        self.assertEqual(int(new_track.channel_mask()), mask)
4311                        metadata = new_track.get_metadata()
4312
4313                        self.assertEqual(
4314                            metadata.get_block(
4315                                audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)[
4316                                u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"][0],
4317                            u"0x%.4X" % (mask))
4318
4319        # check bad seekpoint destinations
4320        track = audiotools.open("flac-seektable.flac")
4321        fixes = track.clean()
4322        self.assertEqual(fixes, [CLEAN_FLAC_FIX_SEEKTABLE])
4323        with tempfile.NamedTemporaryFile(suffix=".flac") as temp:
4324            fixes = track.clean(temp.name)
4325            self.assertEqual(
4326                fixes,
4327                [CLEAN_FLAC_FIX_SEEKTABLE])
4328            new_track = audiotools.open(temp.name)
4329            fixes = new_track.clean()
4330            self.assertEqual(fixes, [])
4331
4332    @FORMAT_FLAC
4333    def test_nonmd5(self):
4334        flac = audiotools.open("flac-nonmd5.flac")
4335        self.assertEqual(flac.__md5__, b"\x00" * 16)
4336        md5sum = md5()
4337
4338        # ensure that a FLAC file with an empty MD5 sum
4339        # decodes without errors
4340        audiotools.transfer_framelist_data(flac.to_pcm(),
4341                                           md5sum.update)
4342        self.assertEqual(md5sum.hexdigest(),
4343                         'd2b120199019b639d5a7e2b3463e9c97')
4344
4345        # ensure that a FLAC file with an empty MD5 sum
4346        # verifies without errors
4347        self.assertEqual(flac.verify(), True)
4348
4349    @FORMAT_FLAC
4350    def test_python_codec(self):
4351        # Python decoder and encoder are far too slow
4352        # to run anything resembling a complete set of tests
4353        # so we'll cut them down to the very basics
4354
4355        def test_python_reader(pcmreader, **encode_options):
4356            from audiotools.py_encoders import encode_flac
4357
4358            # encode file using Python-based encoder
4359            temp_file = tempfile.NamedTemporaryFile(suffix=".flac")
4360            encode_flac(temp_file.name,
4361                        audiotools.BufferedPCMReader(pcmreader),
4362                        **encode_options)
4363
4364            # verify contents of file decoded by
4365            # Python-based decoder against contents decoded by
4366            # C-based decoder
4367            from audiotools.py_decoders import FlacDecoder as FlacDecoder1
4368            from audiotools.decoders import FlacDecoder as FlacDecoder2
4369
4370            self.assertTrue(
4371                audiotools.pcm_cmp(
4372                    FlacDecoder1(temp_file.name, 0),
4373                    FlacDecoder2(open(temp_file.name, "rb"))))
4374
4375            temp_file.close()
4376
4377        # test small files
4378        for g in [test_streams.Generate01,
4379                  test_streams.Generate02,
4380                  test_streams.Generate03,
4381                  test_streams.Generate04]:
4382            test_python_reader(g(44100),
4383                               block_size=1152,
4384                               max_lpc_order=16,
4385                               min_residual_partition_order=0,
4386                               max_residual_partition_order=3,
4387                               mid_side=True,
4388                               adaptive_mid_side=True,
4389                               exhaustive_model_search=True)
4390
4391        # test full-scale deflection
4392        for (bps, fsd) in [(8, test_streams.fsd8),
4393                           (16, test_streams.fsd16),
4394                           (24, test_streams.fsd24)]:
4395            for pattern in [test_streams.PATTERN01,
4396                            test_streams.PATTERN02,
4397                            test_streams.PATTERN03,
4398                            test_streams.PATTERN04,
4399                            test_streams.PATTERN05,
4400                            test_streams.PATTERN06,
4401                            test_streams.PATTERN07]:
4402                test_python_reader(
4403                    fsd(pattern, 100),
4404                    block_size=1152,
4405                    max_lpc_order=16,
4406                    min_residual_partition_order=0,
4407                    max_residual_partition_order=3,
4408                    mid_side=True,
4409                    adaptive_mid_side=True,
4410                    exhaustive_model_search=True)
4411
4412        # test silence
4413        for g in [test_streams.Silence8_Mono(5000, 48000),
4414                  test_streams.Silence8_Stereo(5000, 48000),
4415                  test_streams.Silence16_Mono(5000, 48000),
4416                  test_streams.Silence16_Stereo(5000, 48000),
4417                  test_streams.Silence24_Mono(5000, 48000),
4418                  test_streams.Silence24_Stereo(5000, 48000)]:
4419            test_python_reader(g,
4420                               block_size=1152,
4421                               max_lpc_order=16,
4422                               min_residual_partition_order=0,
4423                               max_residual_partition_order=3,
4424                               mid_side=True,
4425                               adaptive_mid_side=True,
4426                               exhaustive_model_search=True)
4427
4428        # test sines
4429        for g in [test_streams.Sine8_Mono(5000, 48000,
4430                                          441.0, 0.50, 441.0, 0.49),
4431                  test_streams.Sine8_Stereo(5000, 48000,
4432                                            441.0, 0.50, 441.0, 0.49, 1.0),
4433                  test_streams.Sine16_Mono(5000, 48000,
4434                                           441.0, 0.50, 441.0, 0.49),
4435                  test_streams.Sine16_Stereo(5000, 48000,
4436                                             441.0, 0.50, 441.0, 0.49, 1.0),
4437                  test_streams.Sine24_Mono(5000, 48000,
4438                                           441.0, 0.50, 441.0, 0.49),
4439                  test_streams.Sine24_Stereo(5000, 48000,
4440                                             441.0, 0.50, 441.0, 0.49, 1.0),
4441                  test_streams.Simple_Sine(5000, 44100, 0x7, 8,
4442                                           (25, 10000),
4443                                           (50, 20000),
4444                                           (120, 30000)),
4445                  test_streams.Simple_Sine(5000, 44100, 0x33, 8,
4446                                           (25, 10000),
4447                                           (50, 20000),
4448                                           (75, 30000),
4449                                           (65, 40000)),
4450                  test_streams.Simple_Sine(5000, 44100, 0x37, 8,
4451                                           (25, 10000),
4452                                           (35, 15000),
4453                                           (45, 20000),
4454                                           (50, 25000),
4455                                           (55, 30000)),
4456                  test_streams.Simple_Sine(5000, 44100, 0x3F, 8,
4457                                           (25, 10000),
4458                                           (45, 15000),
4459                                           (65, 20000),
4460                                           (85, 25000),
4461                                           (105, 30000),
4462                                           (120, 35000)),
4463                  test_streams.Simple_Sine(5000, 44100, 0x7, 16,
4464                                           (6400, 10000),
4465                                           (12800, 20000),
4466                                           (30720, 30000)),
4467                  test_streams.Simple_Sine(5000, 44100, 0x33, 16,
4468                                           (6400, 10000),
4469                                           (12800, 20000),
4470                                           (19200, 30000),
4471                                           (16640, 40000)),
4472                  test_streams.Simple_Sine(5000, 44100, 0x37, 16,
4473                                           (6400, 10000),
4474                                           (8960, 15000),
4475                                           (11520, 20000),
4476                                           (12800, 25000),
4477                                           (14080, 30000)),
4478                  test_streams.Simple_Sine(5000, 44100, 0x3F, 16,
4479                                           (6400, 10000),
4480                                           (11520, 15000),
4481                                           (16640, 20000),
4482                                           (21760, 25000),
4483                                           (26880, 30000),
4484                                           (30720, 35000)),
4485                  test_streams.Simple_Sine(5000, 44100, 0x7, 24,
4486                                           (1638400, 10000),
4487                                           (3276800, 20000),
4488                                           (7864320, 30000)),
4489                  test_streams.Simple_Sine(5000, 44100, 0x33, 24,
4490                                           (1638400, 10000),
4491                                           (3276800, 20000),
4492                                           (4915200, 30000),
4493                                           (4259840, 40000)),
4494                  test_streams.Simple_Sine(5000, 44100, 0x37, 24,
4495                                           (1638400, 10000),
4496                                           (2293760, 15000),
4497                                           (2949120, 20000),
4498                                           (3276800, 25000),
4499                                           (3604480, 30000)),
4500                  test_streams.Simple_Sine(5000, 44100, 0x3F, 24,
4501                                           (1638400, 10000),
4502                                           (2949120, 15000),
4503                                           (4259840, 20000),
4504                                           (5570560, 25000),
4505                                           (6881280, 30000),
4506                                           (7864320, 35000))]:
4507            test_python_reader(g,
4508                               block_size=1152,
4509                               max_lpc_order=16,
4510                               min_residual_partition_order=0,
4511                               max_residual_partition_order=3,
4512                               mid_side=True,
4513                               adaptive_mid_side=True,
4514                               exhaustive_model_search=True)
4515
4516        # test wasted BPS
4517        test_python_reader(test_streams.WastedBPS16(1000),
4518                           block_size=1152,
4519                           max_lpc_order=16,
4520                           min_residual_partition_order=0,
4521                           max_residual_partition_order=3,
4522                           mid_side=True,
4523                           adaptive_mid_side=True,
4524                           exhaustive_model_search=True)
4525
4526        # test block sizes
4527        noise = struct.unpack(">32h", os.urandom(64))
4528
4529        encoding_args = {"min_residual_partition_order": 0,
4530                         "max_residual_partition_order": 6,
4531                         "mid_side": True,
4532                         "adaptive_mid_side": True,
4533                         "exhaustive_model_search": True}
4534        for block_size in [16, 17, 18, 19, 20, 21, 22, 23,
4535                           24, 25, 26, 27, 28, 29, 30, 31, 32, 33]:
4536            for lpc_order in [0, 1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 31, 32]:
4537                args = encoding_args.copy()
4538                args["block_size"] = block_size
4539                args["max_lpc_order"] = lpc_order
4540                test_python_reader(
4541                    test_streams.FrameListReader(noise, 44100, 1, 16),
4542                    **args)
4543
4544
4545class M4AFileTest(LossyFileTest):
4546    def setUp(self):
4547        self.audio_class = audiotools.M4AAudio
4548        self.suffix = "." + self.audio_class.SUFFIX
4549
4550    @FORMAT_M4A
4551    def test_length(self):
4552        with tempfile.NamedTemporaryFile(suffix=self.suffix) as temp:
4553            for seconds in [1, 2, 3, 4, 5, 10, 20, 60, 120]:
4554                track = self.audio_class.from_pcm(temp.name,
4555                                                  BLANK_PCM_Reader(seconds))
4556                self.assertEqual(int(round(track.seconds_length())), seconds)
4557
4558    @FORMAT_LOSSY
4559    def test_channels(self):
4560        with tempfile.NamedTemporaryFile(suffix=self.suffix) as temp:
4561            for channels in [1, 2, 3, 4, 5, 6]:
4562                track = self.audio_class.from_pcm(
4563                    temp.name, BLANK_PCM_Reader(1,
4564                                                channels=channels,
4565                                                channel_mask=0))
4566            if self.audio_class is audiotools.m4a.M4AAudio_faac:
4567                self.assertEqual(track.channels(), 2)
4568                track = audiotools.open(temp.name)
4569                self.assertEqual(track.channels(), 2)
4570            else:
4571                self.assertEqual(track.channels(), max(2, channels))
4572                track = audiotools.open(temp.name)
4573                self.assertEqual(track.channels(), max(2, channels))
4574
4575    @FORMAT_M4A
4576    def test_too(self):
4577        # ensure that the 'too' meta atom isn't modified by setting metadata
4578        with tempfile.NamedTemporaryFile(suffix=self.suffix) as temp:
4579            track = self.audio_class.from_pcm(
4580                temp.name,
4581                BLANK_PCM_Reader(1))
4582            metadata = track.get_metadata()
4583            encoder = u"%s" % (metadata[b'ilst'][b'\xa9too'],)
4584            track.set_metadata(audiotools.MetaData(track_name=u"Foo"))
4585            metadata = track.get_metadata()
4586            self.assertEqual(metadata.track_name, u"Foo")
4587            self.assertEqual(u"%s" % (metadata[b'ilst'][b'\xa9too'],), encoder)
4588
4589
4590class MP3FileTest(LossyFileTest):
4591    def setUp(self):
4592        self.audio_class = audiotools.MP3Audio
4593        self.suffix = "." + self.audio_class.SUFFIX
4594
4595    @FORMAT_MP3
4596    def test_length(self):
4597        with tempfile.NamedTemporaryFile(suffix=self.suffix) as temp:
4598            for seconds in [1, 2, 3, 4, 5, 10, 20, 60, 120]:
4599                track = self.audio_class.from_pcm(temp.name,
4600                                                  BLANK_PCM_Reader(seconds))
4601                self.assertEqual(int(round(track.seconds_length())), seconds)
4602
4603    @FORMAT_MP3
4604    def test_verify(self):
4605        # test invalid file sent to to_pcm()
4606
4607        # FIXME - mpg123 doesn't generate errors on invalid files
4608        # Ultimately, all of MP3/MP2 decoding needs to be internalized
4609        # so that these sorts of errors can be caught consistently.
4610
4611        # temp = tempfile.NamedTemporaryFile(
4612        #     suffix=self.suffix)
4613        # try:
4614        #     track = self.audio_class.from_pcm(
4615        #         temp.name,
4616        #         BLANK_PCM_Reader(1))
4617        #     good_data = open(temp.name, 'rb').read()
4618        #     f = open(temp.name, 'wb')
4619        #     f.write(good_data[0:100])
4620        #     f.close()
4621        #     reader = track.to_pcm()
4622        #     audiotools.transfer_framelist_data(reader, lambda x: x)
4623        #     self.assertRaises(audiotools.DecodingError,
4624        #                       reader.close)
4625        # finally:
4626        #     temp.close()
4627
4628        # test invalid file send to convert()
4629        # temp = tempfile.NamedTemporaryFile(
4630        #     suffix=self.suffix)
4631        # try:
4632        #     track = self.audio_class.from_pcm(
4633        #         temp.name,
4634        #         BLANK_PCM_Reader(1))
4635        #     good_data = open(temp.name, 'rb').read()
4636        #     f = open(temp.name, 'wb')
4637        #     f.write(good_data[0:100])
4638        #     f.close()
4639        #     if os.path.isfile("dummy.wav"):
4640        #         os.unlink("dummy.wav")
4641        #     self.assertEqual(os.path.isfile("dummy.wav"), False)
4642        #     self.assertRaises(audiotools.EncodingError,
4643        #                       track.convert,
4644        #                       "dummy.wav",
4645        #                       audiotools.WaveAudio)
4646        #     self.assertEqual(os.path.isfile("dummy.wav"), False)
4647        # finally:
4648        #     temp.close()
4649
4650        # # test verify() on invalid files
4651        # temp = tempfile.NamedTemporaryFile(
4652        #     suffix=self.suffix)
4653        # mpeg_data = cStringIO.StringIO()
4654        # frame_header = audiotools.MPEG_Frame_Header("header")
4655        # try:
4656        #     mpx_file = audiotools.open("sine" + self.suffix)
4657        #     self.assertEqual(mpx_file.verify(), True)
4658
4659        #     for (header, data) in mpx_file.mpeg_frames():
4660        #         mpeg_data.write(frame_header.build(header))
4661        #         mpeg_data.write(data)
4662        #     mpeg_data = mpeg_data.getvalue()
4663
4664        #     temp.seek(0, 0)
4665        #     temp.write(mpeg_data)
4666        #     temp.flush()
4667
4668        #     # first, try truncating the file underfoot
4669        #     bad_mpx_file = audiotools.open(temp.name)
4670        #     for i in range(len(mpeg_data)):
4671        #         try:
4672        #             if ((mpeg_data[i] == chr(0xFF)) and
4673        #                 (ord(mpeg_data[i + 1]) & 0xE0)):
4674        #                 # skip sizes that may be the end of a frame
4675        #                 continue
4676        #         except IndexError:
4677        #             continue
4678
4679        #         f = open(temp.name, "wb")
4680        #         f.write(mpeg_data[0:i])
4681        #         f.close()
4682        #         self.assertEqual(os.path.getsize(temp.name), i)
4683        #         self.assertRaises(audiotools.InvalidFile,
4684        #                           bad_mpx_file.verify)
4685
4686        #     # then try swapping some of the header bits
4687        #     for (field, value) in [("sample_rate", 48000),
4688        #                            ("channel", 3)]:
4689        #         temp.seek(0, 0)
4690        #         for (i, (header, data)) in enumerate(mpx_file.mpeg_frames()):
4691        #             if i == 1:
4692        #                 setattr(header, field, value)
4693        #                 temp.write(frame_header.build(header))
4694        #                 temp.write(data)
4695        #             else:
4696        #                 temp.write(frame_header.build(header))
4697        #                 temp.write(data)
4698        #         temp.flush()
4699        #         new_file = audiotools.open(temp.name)
4700        #         self.assertRaises(audiotools.InvalidFile,
4701        #                           new_file.verify)
4702        # finally:
4703        #     temp.close()
4704        pass
4705
4706    @FORMAT_MP3
4707    def test_id3_ladder(self):
4708        with tempfile.NamedTemporaryFile(suffix=self.suffix) as temp_file:
4709            track = self.audio_class.from_pcm(temp_file.name,
4710                                              BLANK_PCM_Reader(5))
4711
4712            dummy_metadata = audiotools.MetaData(track_name=u"Foo")
4713
4714            # ensure that setting particular ID3 variant
4715            # sticks, even through get/set_metadata
4716            track.set_metadata(dummy_metadata)
4717            for new_class in (audiotools.ID3v22Comment,
4718                              audiotools.ID3v23Comment,
4719                              audiotools.ID3v24Comment,
4720                              audiotools.ID3v23Comment,
4721                              audiotools.ID3v22Comment):
4722                metadata = new_class.converted(track.get_metadata())
4723                track.set_metadata(metadata)
4724                metadata = track.get_metadata()
4725                self.assertTrue(isinstance(metadata, new_class))
4726                self.assertEqual(metadata.__class__, new_class([]).__class__)
4727                self.assertEqual(metadata, dummy_metadata)
4728
4729    @FORMAT_MP3
4730    def test_ucs2(self):
4731        with tempfile.NamedTemporaryFile(suffix=self.suffix) as temp_file:
4732            track = self.audio_class.from_pcm(temp_file.name,
4733                                              BLANK_PCM_Reader(5))
4734
4735            # this should be 4 characters long in UCS-4 environments
4736            # if not, we're in a UCS-2 environment
4737            # which is limited to 16 bits anyway
4738            test_string = u'f\U0001d55foo'
4739
4740            # u'\ufffd' is the "not found" character
4741            # this string should result from escaping through UCS-2
4742            test_string_out = u'f\ufffdoo'
4743
4744            if len(test_string) == 4:
4745                self.assertEqual(test_string,
4746                                 test_string.encode('utf-16').decode('utf-16'))
4747                self.assertEqual(test_string.encode('ucs2').decode('ucs2'),
4748                                 test_string_out)
4749
4750                # ID3v2.4 supports UTF-8/UTF-16
4751                metadata = audiotools.ID3v24Comment.converted(
4752                    audiotools.MetaData(track_name=u"Foo"))
4753                track.set_metadata(metadata)
4754                id3 = track.get_metadata()
4755                self.assertEqual(id3, metadata)
4756
4757                metadata.track_name = test_string
4758
4759                track.set_metadata(metadata)
4760                id3 = track.get_metadata()
4761                self.assertEqual(id3, metadata)
4762
4763                metadata.comment = test_string
4764                track.set_metadata(metadata)
4765                id3 = track.get_metadata()
4766                self.assertEqual(id3, metadata)
4767
4768                metadata.add_image(
4769                    audiotools.ID3v24Comment.IMAGE_FRAME.converted(
4770                        audiotools.ID3v24Comment.IMAGE_FRAME_ID,
4771                        audiotools.Image.new(TEST_COVER1,
4772                                             test_string,
4773                                             0)))
4774                track.set_metadata(metadata)
4775                id3 = track.get_metadata()
4776                self.assertEqual(id3.images()[0].description, test_string)
4777
4778                # ID3v2.3 and ID3v2.2 only support UCS-2
4779                for id3_class in (audiotools.ID3v23Comment,
4780                                  audiotools.ID3v22Comment):
4781                    metadata = audiotools.ID3v23Comment.converted(
4782                        audiotools.MetaData(track_name=u"Foo"))
4783                    track.set_metadata(metadata)
4784                    id3 = track.get_metadata()
4785                    self.assertEqual(id3, metadata)
4786
4787                    # ensure that text fields round-trip correctly
4788                    # (i.e. the extra-wide char gets replaced)
4789                    metadata.track_name = test_string
4790
4791                    track.set_metadata(metadata)
4792                    id3 = track.get_metadata()
4793                    self.assertEqual(id3.track_name, test_string_out)
4794
4795                    # ensure that comment blocks round-trip correctly
4796                    metadata.comment = test_string
4797                    track.set_metadata(metadata)
4798                    id3 = track.get_metadata()
4799                    self.assertEqual(id3.track_name, test_string_out)
4800
4801                    # ensure that image comment fields round-trip correctly
4802                    metadata.add_image(
4803                        id3_class.IMAGE_FRAME.converted(
4804                            id3_class.IMAGE_FRAME_ID,
4805                            audiotools.Image.new(TEST_COVER1,
4806                                                 test_string,
4807                                                 0)))
4808                    track.set_metadata(metadata)
4809                    id3 = track.get_metadata()
4810                    self.assertEqual(id3.images()[0].description,
4811                                     test_string_out)
4812
4813    @FORMAT_MP3
4814    def test_clean(self):
4815        # check MP3 file with double ID3 tags
4816
4817        from audiotools.text import CLEAN_REMOVE_DUPLICATE_ID3V2
4818
4819        original_size = os.path.getsize("id3-2.mp3")
4820
4821        track = audiotools.open("id3-2.mp3")
4822        # ensure second ID3 tag is ignored
4823        self.assertEqual(track.get_metadata().track_name, u"Title1")
4824
4825        # ensure duplicate ID3v2 tag is detected and removed
4826        fixes = track.clean()
4827        self.assertEqual(fixes,
4828                         [CLEAN_REMOVE_DUPLICATE_ID3V2])
4829        with tempfile.NamedTemporaryFile(suffix=".mp3") as temp:
4830            fixes = track.clean(temp.name)
4831            self.assertEqual(fixes,
4832                             [CLEAN_REMOVE_DUPLICATE_ID3V2])
4833            track2 = audiotools.open(temp.name)
4834            self.assertEqual(track2.get_metadata(), track.get_metadata())
4835            # ensure new file is exactly one tag smaller
4836            # and the padding is preserved in the old tag
4837            self.assertEqual(os.path.getsize(temp.name),
4838                             original_size - 0x46A)
4839
4840
4841class MP2FileTest(MP3FileTest):
4842    def setUp(self):
4843        self.audio_class = audiotools.MP2Audio
4844        self.suffix = "." + self.audio_class.SUFFIX
4845
4846
4847class OggVerify:
4848    @FORMAT_VORBIS
4849    @FORMAT_OPUS
4850    @FORMAT_OGGFLAC
4851    def test_verify(self):
4852        from test_core import ints_to_bytes, bytes_to_ints
4853
4854        good_file = tempfile.NamedTemporaryFile(suffix=self.suffix)
4855        bad_file = tempfile.NamedTemporaryFile(suffix=self.suffix)
4856        try:
4857            good_track = self.audio_class.from_pcm(
4858                good_file.name,
4859                BLANK_PCM_Reader(1))
4860            good_file.seek(0, 0)
4861            good_file_data = good_file.read()
4862            self.assertEqual(len(good_file_data),
4863                             os.path.getsize(good_file.name))
4864            bad_file.write(good_file_data)
4865            bad_file.flush()
4866
4867            track = audiotools.open(bad_file.name)
4868            self.assertEqual(track.verify(), True)
4869
4870            # first, try truncating the file
4871            for i in range(len(good_file_data)):
4872                with open(bad_file.name, "wb") as f:
4873                    f.write(good_file_data[0:i])
4874                    f.flush()
4875                self.assertEqual(os.path.getsize(bad_file.name), i)
4876                try:
4877                    new_track = self.audio_class(bad_file.name)
4878                    self.assertRaises(audiotools.InvalidFile,
4879                                      new_track.verify)
4880                except audiotools.InvalidFile:
4881                    self.assertTrue(True)
4882
4883            # then, try flipping a bit
4884            for i in range(len(good_file_data)):
4885                for j in range(8):
4886                    bad_file_data = bytes_to_ints(good_file_data)
4887                    bad_file_data[i] = bad_file_data[i] ^ (1 << j)
4888                    with open(bad_file.name, "wb") as f:
4889                        f.write(ints_to_bytes(bad_file_data))
4890                        f.close()
4891                    self.assertEqual(os.path.getsize(bad_file.name),
4892                                     len(good_file_data))
4893                    try:
4894                        new_track = self.audio_class(bad_file.name)
4895                        self.assertRaises(audiotools.InvalidFile,
4896                                          new_track.verify)
4897                    except audiotools.InvalidFile:
4898                        self.assertTrue(True)
4899        finally:
4900            good_file.close()
4901            bad_file.close()
4902
4903        if self.audio_class is audiotools.OpusAudio:
4904            # opusdec doesn't currently reject invalid
4905            # streams like it should
4906            # so the encoding test doesn't work right
4907            # (this is a known bug)
4908            return
4909
4910        with tempfile.NamedTemporaryFile(suffix=self.suffix) as temp:
4911            track = self.audio_class.from_pcm(
4912                temp.name,
4913                BLANK_PCM_Reader(1))
4914            self.assertTrue(track.verify())
4915            with open(temp.name, 'rb') as f:
4916                good_data = f.read()
4917            with open(temp.name, 'wb') as f:
4918                f.write(good_data[0:min(100, len(good_data) - 1)])
4919            if os.path.isfile("dummy.wav"):
4920                os.unlink("dummy.wav")
4921            self.assertEqual(os.path.isfile("dummy.wav"), False)
4922            self.assertRaises(audiotools.EncodingError,
4923                              track.convert,
4924                              "dummy.wav",
4925                              audiotools.WaveAudio)
4926            self.assertEqual(os.path.isfile("dummy.wav"), False)
4927
4928
4929class OggFlacFileTest(OggVerify,
4930                      LosslessFileTest):
4931    def setUp(self):
4932        from audiotools.decoders import OggFlacDecoder
4933
4934        self.audio_class = audiotools.OggFlacAudio
4935        self.suffix = "." + self.audio_class.SUFFIX
4936
4937        self.decoder = OggFlacDecoder
4938
4939    @FORMAT_OGGFLAC
4940    def test_init(self):
4941        # check missing file
4942        self.assertRaises(audiotools.flac.InvalidFLAC,
4943                          audiotools.OggFlacAudio,
4944                          "/dev/null/foo")
4945
4946        # check invalid file
4947        with tempfile.NamedTemporaryFile(suffix=".oga") as invalid_file:
4948            for c in [b"i", b"n", b"v", b"a", b"l", b"i", b"d",
4949                      b"s", b"t", b"r", b"i", b"n", b"g", b"x", b"x", b"x"]:
4950                invalid_file.write(c)
4951                invalid_file.flush()
4952                self.assertRaises(audiotools.flac.InvalidFLAC,
4953                                  audiotools.OggFlacAudio,
4954                                  invalid_file.name)
4955
4956        # check some decoder errors,
4957        # mostly to ensure a failed init doesn't make Python explode
4958        self.assertRaises(TypeError, self.decoder)
4959
4960        self.assertRaises(TypeError, self.decoder, None)
4961
4962        self.assertRaises(ValueError, self.decoder, "/dev/null", -1)
4963
4964
4965class ShortenFileTest(TestForeignWaveChunks,
4966                      TestForeignAiffChunks,
4967                      LosslessFileTest):
4968    def setUp(self):
4969        self.audio_class = audiotools.ShortenAudio
4970        self.suffix = "." + self.audio_class.SUFFIX
4971
4972        from audiotools.decoders import SHNDecoder
4973        from audiotools.encoders import encode_shn
4974        self.decoder = SHNDecoder
4975        self.encode = encode_shn
4976        self.encode_opts = [{"block_size": 4},
4977                            {"block_size": 256},
4978                            {"block_size": 1024}]
4979
4980    @FORMAT_SHORTEN
4981    def test_init(self):
4982        # check invalid file
4983        with tempfile.NamedTemporaryFile(suffix=".shn") as invalid_file:
4984            for c in [b"i", b"n", b"v", b"a", b"l", b"i", b"d",
4985                      b"s", b"t", b"r", b"i", b"n", b"g", b"x", b"x", b"x"]:
4986                invalid_file.write(c)
4987                invalid_file.flush()
4988                self.assertRaises(audiotools.shn.InvalidShorten,
4989                                  audiotools.ShortenAudio,
4990                                  invalid_file.name)
4991
4992        # check some decoder errors,
4993        # mostly to ensure a failed init doesn't make Python explode
4994        self.assertRaises(TypeError, self.decoder)
4995
4996        self.assertRaises(IOError, self.decoder, None)
4997
4998        self.assertRaises(IOError, self.decoder, "filename")
4999
5000    @FORMAT_SHORTEN
5001    def test_bits_per_sample(self):
5002        with tempfile.NamedTemporaryFile(suffix=self.suffix) as temp:
5003            for bps in (8, 16):
5004                track = self.audio_class.from_pcm(
5005                    temp.name, BLANK_PCM_Reader(1, bits_per_sample=bps))
5006                self.assertEqual(track.bits_per_sample(), bps)
5007                track2 = audiotools.open(temp.name)
5008                self.assertEqual(track2.bits_per_sample(), bps)
5009
5010    @FORMAT_SHORTEN
5011    def test_verify(self):
5012        # test changing the file underfoot
5013        with tempfile.NamedTemporaryFile(suffix=".shn") as temp:
5014            with open("shorten-frames.shn", "rb") as f:
5015                shn_data = f.read()
5016                temp.write(shn_data)
5017                temp.flush()
5018            shn_file = audiotools.open(temp.name)
5019            self.assertEqual(shn_file.verify(), True)
5020
5021            for i in range(0, len(shn_data.rstrip(b"\x00"))):
5022                with open(temp.name, "wb") as f:
5023                    f.write(shn_data[0:i])
5024                    f.close()
5025                self.assertRaises(audiotools.InvalidFile,
5026                                  shn_file.verify)
5027
5028            # unfortunately, Shorten doesn't have any checksumming
5029            # or other ways to reliably detect swapped bits
5030
5031        # testing truncating various Shorten files
5032        for (first, last, filename) in [(62, 89, "shorten-frames.shn"),
5033                                        (61, 116, "shorten-lpc.shn")]:
5034
5035            with open(filename, "rb") as f:
5036                shn_data = f.read()
5037
5038            with tempfile.NamedTemporaryFile(suffix=".shn") as temp:
5039                for i in range(0, first):
5040                    temp.seek(0, 0)
5041                    temp.write(shn_data[0:i])
5042                    temp.flush()
5043                    self.assertEqual(os.path.getsize(temp.name), i)
5044                    with open(temp.name, "rb") as f:
5045                        self.assertRaises(IOError,
5046                                          audiotools.decoders.SHNDecoder,
5047                                          f)
5048
5049                for i in range(first, len(shn_data[0:last].rstrip(b"\x00"))):
5050                    temp.seek(0, 0)
5051                    temp.write(shn_data[0:i])
5052                    temp.flush()
5053                    self.assertEqual(os.path.getsize(temp.name), i)
5054                    decoder = audiotools.decoders.SHNDecoder(
5055                        open(temp.name, "rb"))
5056                    self.assertIsNotNone(decoder)
5057                    self.assertRaises(IOError, decoder.pcm_split)
5058                    decoder.close()
5059
5060                    decoder = audiotools.decoders.SHNDecoder(
5061                        open(temp.name, "rb"))
5062                    self.assertIsNotNone(decoder)
5063                    self.assertRaises(IOError,
5064                                      audiotools.transfer_framelist_data,
5065                                      decoder, lambda x: x)
5066
5067        # test running convert() on a truncated file
5068        # triggers EncodingError
5069        with tempfile.NamedTemporaryFile(suffix=".shn") as temp:
5070            with open("shorten-frames.shn", "rb") as f:
5071                temp.write(f.read()[0:-10])
5072                temp.flush()
5073            shn = audiotools.open(temp.name)
5074            if os.path.isfile("dummy.wav"):
5075                os.unlink("dummy.wav")
5076            self.assertEqual(os.path.isfile("dummy.wav"), False)
5077            self.assertRaises(audiotools.EncodingError,
5078                              shn.convert,
5079                              "dummy.wav",
5080                              audiotools.WaveAudio)
5081            self.assertEqual(os.path.isfile("dummy.wav"), False)
5082
5083    def __stream_variations__(self):
5084        for stream in [
5085            test_streams.Silence8_Mono(200000, 44100),
5086            test_streams.Silence8_Mono(200000, 96000),
5087            test_streams.Silence8_Stereo(200000, 44100),
5088            test_streams.Silence8_Stereo(200000, 96000),
5089            test_streams.Silence16_Mono(200000, 44100),
5090            test_streams.Silence16_Mono(200000, 96000),
5091            test_streams.Silence16_Stereo(200000, 44100),
5092            test_streams.Silence16_Stereo(200000, 96000),
5093
5094            test_streams.Sine8_Mono(200000, 48000, 441.0, 0.50, 441.0, 0.49),
5095            test_streams.Sine8_Mono(200000, 96000, 441.0, 0.61, 661.5, 0.37),
5096            test_streams.Sine8_Mono(200000, 44100, 441.0, 0.50, 882.0, 0.49),
5097            test_streams.Sine8_Mono(200000, 44100, 441.0, 0.50, 4410.0, 0.49),
5098            test_streams.Sine8_Mono(200000, 44100, 8820.0, 0.70, 4410.0, 0.29),
5099
5100            test_streams.Sine8_Stereo(200000, 48000, 441.0, 0.50, 441.0, 0.49, 1.0),
5101            test_streams.Sine8_Stereo(200000, 48000, 441.0, 0.61, 661.5, 0.37, 1.0),
5102            test_streams.Sine8_Stereo(200000, 96000, 441.0, 0.50, 882.0, 0.49, 1.0),
5103            test_streams.Sine8_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.0),
5104            test_streams.Sine8_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 1.0),
5105            test_streams.Sine8_Stereo(200000, 44100, 441.0, 0.50, 441.0, 0.49, 0.5),
5106            test_streams.Sine8_Stereo(200000, 44100, 441.0, 0.61, 661.5, 0.37, 2.0),
5107            test_streams.Sine8_Stereo(200000, 44100, 441.0, 0.50, 882.0, 0.49, 0.7),
5108            test_streams.Sine8_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.3),
5109            test_streams.Sine8_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 0.1),
5110
5111            test_streams.Sine16_Mono(200000, 48000, 441.0, 0.50, 441.0, 0.49),
5112            test_streams.Sine16_Mono(200000, 96000, 441.0, 0.61, 661.5, 0.37),
5113            test_streams.Sine16_Mono(200000, 44100, 441.0, 0.50, 882.0, 0.49),
5114            test_streams.Sine16_Mono(200000, 44100, 441.0, 0.50, 4410.0, 0.49),
5115            test_streams.Sine16_Mono(200000, 44100, 8820.0, 0.70, 4410.0, 0.29),
5116            test_streams.Sine16_Stereo(200000, 48000, 441.0, 0.50, 441.0, 0.49, 1.0),
5117            test_streams.Sine16_Stereo(200000, 48000, 441.0, 0.61, 661.5, 0.37, 1.0),
5118            test_streams.Sine16_Stereo(200000, 96000, 441.0, 0.50, 882.0, 0.49, 1.0),
5119            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.0),
5120            test_streams.Sine16_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 1.0),
5121            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.50, 441.0, 0.49, 0.5),
5122            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.61, 661.5, 0.37, 2.0),
5123            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.50, 882.0, 0.49, 0.7),
5124            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.3),
5125            test_streams.Sine16_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 0.1),
5126
5127            test_streams.Simple_Sine(200000, 44100, 0x7, 8,
5128                                     (25, 10000),
5129                                     (50, 20000),
5130                                     (120, 30000)),
5131            test_streams.Simple_Sine(200000, 44100, 0x33, 8,
5132                                     (25, 10000),
5133                                     (50, 20000),
5134                                     (75, 30000),
5135                                     (65, 40000)),
5136            test_streams.Simple_Sine(200000, 44100, 0x37, 8,
5137                                     (25, 10000),
5138                                     (35, 15000),
5139                                     (45, 20000),
5140                                     (50, 25000),
5141                                     (55, 30000)),
5142            test_streams.Simple_Sine(200000, 44100, 0x3F, 8,
5143                                     (25, 10000),
5144                                     (45, 15000),
5145                                     (65, 20000),
5146                                     (85, 25000),
5147                                     (105, 30000),
5148                                     (120, 35000)),
5149
5150            test_streams.Simple_Sine(200000, 44100, 0x7, 16,
5151                                     (6400, 10000),
5152                                     (12800, 20000),
5153                                     (30720, 30000)),
5154            test_streams.Simple_Sine(200000, 44100, 0x33, 16,
5155                                     (6400, 10000),
5156                                     (12800, 20000),
5157                                     (19200, 30000),
5158                                     (16640, 40000)),
5159            test_streams.Simple_Sine(200000, 44100, 0x37, 16,
5160                                     (6400, 10000),
5161                                     (8960, 15000),
5162                                     (11520, 20000),
5163                                     (12800, 25000),
5164                                     (14080, 30000)),
5165            test_streams.Simple_Sine(200000, 44100, 0x3F, 16,
5166                                     (6400, 10000),
5167                                     (11520, 15000),
5168                                     (16640, 20000),
5169                                     (21760, 25000),
5170                                     (26880, 30000),
5171                                     (30720, 35000))]:
5172            yield stream
5173
5174    @FORMAT_SHORTEN
5175    def test_streams(self):
5176        for g in self.__stream_variations__():
5177            md5sum = md5()
5178            f = g.read(audiotools.FRAMELIST_SIZE)
5179            while len(f) > 0:
5180                md5sum.update(f.to_bytes(False, True))
5181                f = g.read(audiotools.FRAMELIST_SIZE)
5182            self.assertEqual(md5sum.digest(), g.digest())
5183            g.close()
5184
5185    def __test_reader__(self, pcmreader, **encode_options):
5186        if not audiotools.BIN.can_execute(audiotools.BIN["shorten"]):
5187            self.assertTrue(False,
5188                            "reference Shorten binary shorten(1) " +
5189                            "required for this test")
5190
5191        temp_file = tempfile.NamedTemporaryFile(suffix=".shn")
5192
5193        # construct a temporary wave file from pcmreader
5194        temp_input_wave_file = tempfile.NamedTemporaryFile(suffix=".wav")
5195        temp_input_wave = audiotools.WaveAudio.from_pcm(
5196            temp_input_wave_file.name, pcmreader)
5197        temp_input_wave.verify()
5198
5199        options = encode_options.copy()
5200        (head, tail) = temp_input_wave.wave_header_footer()
5201        options["is_big_endian"] = False
5202        options["signed_samples"] = (pcmreader.bits_per_sample == 16)
5203        options["header_data"] = head
5204        if len(tail) > 0:
5205            options["footer_data"] = tail
5206
5207        with temp_input_wave.to_pcm() as pcmreader2:
5208            self.encode(temp_file.name,
5209                        pcmreader2,
5210                        **options)
5211
5212        shn = audiotools.open(temp_file.name)
5213        self.assertGreater(shn.total_frames(), 0)
5214
5215        temp_wav_file1 = tempfile.NamedTemporaryFile(suffix=".wav")
5216        temp_wav_file2 = tempfile.NamedTemporaryFile(suffix=".wav")
5217
5218        # first, ensure the Shorten-encoded file
5219        # has the same MD5 signature as pcmreader once decoded
5220        for shndec in [self.decoder(open(temp_file.name, "rb")),
5221                       self.decoder(Filewrapper(open(temp_file.name, "rb")))]:
5222            md5sum = md5()
5223            f = shndec.read(audiotools.FRAMELIST_SIZE)
5224            while len(f) > 0:
5225                md5sum.update(f.to_bytes(False, True))
5226                f = shndec.read(audiotools.FRAMELIST_SIZE)
5227            shndec.close()
5228            self.assertEqual(md5sum.digest(), pcmreader.digest())
5229
5230        # then compare our .to_wave() output
5231        # with that of the Shorten reference decoder
5232        shn.convert(temp_wav_file1.name, audiotools.WaveAudio)
5233        subprocess.call([audiotools.BIN["shorten"],
5234                         "-x", shn.filename, temp_wav_file2.name])
5235
5236        wave = audiotools.WaveAudio(temp_wav_file1.name)
5237        wave.verify()
5238        wave = audiotools.WaveAudio(temp_wav_file2.name)
5239        wave.verify()
5240
5241        self.assertTrue(
5242            audiotools.pcm_cmp(
5243                audiotools.WaveAudio(temp_wav_file1.name).to_pcm(),
5244                audiotools.WaveAudio(temp_wav_file2.name).to_pcm()))
5245
5246        temp_wav_file1.close()
5247        temp_wav_file2.close()
5248
5249        # then perform PCM -> aiff -> Shorten -> PCM testing
5250
5251        # construct a temporary wave file from pcmreader
5252        temp_input_aiff_file = tempfile.NamedTemporaryFile(suffix=".aiff")
5253        temp_input_aiff = temp_input_wave.convert(temp_input_aiff_file.name,
5254                                                  audiotools.AiffAudio)
5255        temp_input_aiff.verify()
5256
5257        options = encode_options.copy()
5258        options["is_big_endian"] = True
5259        options["signed_samples"] = True
5260        (head, tail) = temp_input_aiff.aiff_header_footer()
5261        options["header_data"] = head
5262        if len(tail) > 0:
5263            options["footer_data"] = tail
5264
5265        with temp_input_aiff.to_pcm() as pcmreader2:
5266            self.encode(temp_file.name,
5267                        pcmreader2,
5268                        **options)
5269
5270        shn = audiotools.open(temp_file.name)
5271        self.assertGreater(shn.total_frames(), 0)
5272
5273        temp_aiff_file1 = tempfile.NamedTemporaryFile(suffix=".aiff")
5274        temp_aiff_file2 = tempfile.NamedTemporaryFile(suffix=".aiff")
5275
5276        # first, ensure the Shorten-encoded file
5277        # has the same MD5 signature as pcmreader once decoded
5278        for shndec in [self.decoder(open(temp_file.name, "rb")),
5279                       self.decoder(Filewrapper(open(temp_file.name, "rb")))]:
5280            md5sum = md5()
5281            f = shndec.read(audiotools.BUFFER_SIZE)
5282            while len(f) > 0:
5283                md5sum.update(f.to_bytes(False, True))
5284                f = shndec.read(audiotools.BUFFER_SIZE)
5285            shndec.close()
5286            self.assertEqual(md5sum.digest(), pcmreader.digest())
5287
5288        # then compare our .to_aiff() output
5289        # with that of the Shorten reference decoder
5290        shn.convert(temp_aiff_file1.name, audiotools.AiffAudio)
5291
5292        subprocess.call([audiotools.BIN["shorten"],
5293                         "-x", shn.filename, temp_aiff_file2.name])
5294
5295        aiff = audiotools.AiffAudio(temp_aiff_file1.name)
5296        aiff.verify()
5297        aiff = audiotools.AiffAudio(temp_aiff_file2.name)
5298        aiff.verify()
5299
5300        self.assertTrue(
5301            audiotools.pcm_cmp(
5302                audiotools.AiffAudio(temp_aiff_file1.name).to_pcm(),
5303                audiotools.AiffAudio(temp_aiff_file2.name).to_pcm()))
5304
5305        temp_file.close()
5306        temp_input_aiff_file.close()
5307        temp_input_wave_file.close()
5308        temp_aiff_file1.close()
5309        temp_aiff_file2.close()
5310
5311    @FORMAT_SHORTEN
5312    def test_small_files(self):
5313        for g in [test_streams.Generate01,
5314                  test_streams.Generate02,
5315                  test_streams.Generate03,
5316                  test_streams.Generate04]:
5317            gen = g(44100)
5318            self.__test_reader__(gen, block_size=256)
5319
5320    @FORMAT_SHORTEN
5321    def test_full_scale_deflection(self):
5322        for (bps, fsd) in [(8, test_streams.fsd8),
5323                           (16, test_streams.fsd16)]:
5324            for pattern in [test_streams.PATTERN01,
5325                            test_streams.PATTERN02,
5326                            test_streams.PATTERN03,
5327                            test_streams.PATTERN04,
5328                            test_streams.PATTERN05,
5329                            test_streams.PATTERN06,
5330                            test_streams.PATTERN07]:
5331                stream = test_streams.MD5Reader(fsd(pattern, 100))
5332                self.__test_reader__(
5333                    stream, block_size=256)
5334
5335    @FORMAT_SHORTEN
5336    def test_sines(self):
5337        for g in self.__stream_variations__():
5338            self.__test_reader__(g, block_size=256)
5339
5340    @FORMAT_SHORTEN
5341    def test_blocksizes(self):
5342        noise = struct.unpack(">32h", os.urandom(64))
5343
5344        for block_size in [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
5345                           256, 1024]:
5346            args = {"block_size": block_size}
5347            self.__test_reader__(
5348                test_streams.MD5Reader(
5349                    test_streams.FrameListReader(noise, 44100, 1, 16)), **args)
5350
5351    @FORMAT_SHORTEN
5352    def test_noise(self):
5353        for opts in self.encode_opts:
5354            encode_opts = opts.copy()
5355            for (channels, mask) in [
5356                (1, int(audiotools.ChannelMask.from_channels(1))),
5357                (2, int(audiotools.ChannelMask.from_channels(2))),
5358                (4, int(audiotools.ChannelMask.from_fields(front_left=True,
5359                                                           front_right=True,
5360                                                           back_left=True,
5361                                                           back_right=True))),
5362                (8, int(audiotools.ChannelMask(0)))]:
5363                for bps in [8, 16]:
5364                    self.__test_reader__(
5365                        MD5_Reader(
5366                            EXACT_RANDOM_PCM_Reader(pcm_frames=65536,
5367                                                    sample_rate=44100,
5368                                                    channels=channels,
5369                                                    channel_mask=mask,
5370                                                    bits_per_sample=bps)),
5371                        **encode_opts)
5372
5373    @FORMAT_SHORTEN
5374    def test_python_codec(self):
5375        def test_python_reader(pcmreader, total_pcm_frames, block_size=256):
5376            from audiotools.py_encoders import encode_shn
5377
5378            temp_file = tempfile.NamedTemporaryFile(suffix=".shn")
5379            audiotools.ShortenAudio.from_pcm(
5380                temp_file.name,
5381                pcmreader,
5382                block_size=block_size,
5383                encoding_function=encode_shn)
5384
5385            from audiotools.decoders import SHNDecoder as SHNDecoder1
5386            from audiotools.py_decoders import SHNDecoder as SHNDecoder2
5387
5388            self.assertTrue(audiotools.pcm_cmp(
5389                SHNDecoder1(open(temp_file.name, "rb")),
5390                SHNDecoder2(temp_file.name)))
5391
5392            # try test again, this time with total_pcm_frames indicated
5393            pcmreader.reset()
5394            audiotools.ShortenAudio.from_pcm(
5395                temp_file.name,
5396                pcmreader,
5397                total_pcm_frames=total_pcm_frames,
5398                block_size=block_size,
5399                encoding_function=encode_shn)
5400
5401            self.assertTrue(audiotools.pcm_cmp(
5402                SHNDecoder1(open(temp_file.name, "rb")),
5403                SHNDecoder2(temp_file.name)))
5404
5405            temp_file.close()
5406
5407        # test small files
5408        for g in [test_streams.Generate01,
5409                  test_streams.Generate02]:
5410            test_python_reader(g(44100), 1, block_size=256)
5411
5412        for g in [test_streams.Generate03,
5413                  test_streams.Generate04]:
5414            test_python_reader(g(44100), 5, block_size=256)
5415
5416        # test full scale deflection
5417        for (bps, fsd) in [(8, test_streams.fsd8),
5418                           (16, test_streams.fsd16)]:
5419            for pattern in [test_streams.PATTERN01,
5420                            test_streams.PATTERN02,
5421                            test_streams.PATTERN03,
5422                            test_streams.PATTERN04,
5423                            test_streams.PATTERN05,
5424                            test_streams.PATTERN06,
5425                            test_streams.PATTERN07]:
5426                stream = test_streams.MD5Reader(fsd(pattern, 100))
5427                test_python_reader(stream,
5428                                   len(pattern) * 100,
5429                                   block_size=256)
5430
5431        # test silence
5432        for g in [test_streams.Silence8_Mono(5000, 48000),
5433                  test_streams.Silence8_Stereo(5000, 48000),
5434                  test_streams.Silence16_Mono(5000, 48000),
5435                  test_streams.Silence16_Stereo(5000, 48000)]:
5436            test_python_reader(g, 5000, block_size=256)
5437
5438        # test sines
5439        for g in [test_streams.Sine8_Mono(5000, 48000,
5440                                          441.0, 0.50, 441.0, 0.49),
5441                  test_streams.Sine8_Stereo(5000, 48000,
5442                                            441.0, 0.50, 441.0, 0.49, 1.0),
5443                  test_streams.Sine16_Mono(5000, 48000,
5444                                           441.0, 0.50, 441.0, 0.49),
5445                  test_streams.Sine16_Stereo(5000, 48000,
5446                                             441.0, 0.50, 441.0, 0.49, 1.0),
5447                  test_streams.Simple_Sine(5000, 44100, 0x7, 8,
5448                                           (25, 10000),
5449                                           (50, 20000),
5450                                           (120, 30000)),
5451                  test_streams.Simple_Sine(5000, 44100, 0x33, 8,
5452                                           (25, 10000),
5453                                           (50, 20000),
5454                                           (75, 30000),
5455                                           (65, 40000)),
5456                  test_streams.Simple_Sine(5000, 44100, 0x37, 8,
5457                                           (25, 10000),
5458                                           (35, 15000),
5459                                           (45, 20000),
5460                                           (50, 25000),
5461                                           (55, 30000)),
5462                  test_streams.Simple_Sine(5000, 44100, 0x3F, 8,
5463                                           (25, 10000),
5464                                           (45, 15000),
5465                                           (65, 20000),
5466                                           (85, 25000),
5467                                           (105, 30000),
5468                                           (120, 35000)),
5469                  test_streams.Simple_Sine(5000, 44100, 0x7, 16,
5470                                           (6400, 10000),
5471                                           (12800, 20000),
5472                                           (30720, 30000)),
5473                  test_streams.Simple_Sine(5000, 44100, 0x33, 16,
5474                                           (6400, 10000),
5475                                           (12800, 20000),
5476                                           (19200, 30000),
5477                                           (16640, 40000)),
5478                  test_streams.Simple_Sine(5000, 44100, 0x37, 16,
5479                                           (6400, 10000),
5480                                           (8960, 15000),
5481                                           (11520, 20000),
5482                                           (12800, 25000),
5483                                           (14080, 30000)),
5484                  test_streams.Simple_Sine(5000, 44100, 0x3F, 16,
5485                                           (6400, 10000),
5486                                           (11520, 15000),
5487                                           (16640, 20000),
5488                                           (21760, 25000),
5489                                           (26880, 30000),
5490                                           (30720, 35000))]:
5491            test_python_reader(g, 5000, block_size=256)
5492
5493        # test block sizes
5494        noise = struct.unpack(">32h", os.urandom(64))
5495
5496        for block_size in [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
5497                           256, 1024]:
5498            test_python_reader(
5499                test_streams.FrameListReader(noise, 44100, 1, 16),
5500                len(noise),
5501                block_size=block_size)
5502
5503        # test noise
5504        for block_size in [4, 256, 1024]:
5505            for (channels, mask) in [
5506                (1, int(audiotools.ChannelMask.from_channels(1))),
5507                (2, int(audiotools.ChannelMask.from_channels(2))),
5508                (4, int(audiotools.ChannelMask.from_fields(front_left=True,
5509                                                           front_right=True,
5510                                                           back_left=True,
5511                                                           back_right=True))),
5512                (8, int(audiotools.ChannelMask(0)))]:
5513                for bps in [8, 16]:
5514                    test_python_reader(
5515                        EXACT_RANDOM_PCM_Reader(
5516                            pcm_frames=5000,
5517                            sample_rate=44100,
5518                            channels=channels,
5519                            channel_mask=mask,
5520                            bits_per_sample=bps),
5521                        5000,
5522                        block_size=block_size)
5523
5524
5525class VorbisFileTest(OggVerify, LossyFileTest):
5526    def setUp(self):
5527        self.audio_class = audiotools.VorbisAudio
5528        self.suffix = "." + self.audio_class.SUFFIX
5529
5530    @FORMAT_VORBIS
5531    def test_channels(self):
5532        with tempfile.NamedTemporaryFile(suffix=self.suffix) as temp:
5533            for channels in [1, 2, 3, 4, 5, 6]:
5534                track = self.audio_class.from_pcm(
5535                    temp.name, BLANK_PCM_Reader(1,
5536                                                channels=channels,
5537                                                channel_mask=0))
5538            self.assertEqual(track.channels(), channels)
5539            track = audiotools.open(temp.name)
5540            self.assertEqual(track.channels(), channels)
5541
5542    @FORMAT_VORBIS
5543    def test_big_comment(self):
5544        with tempfile.NamedTemporaryFile(
5545            suffix="." + self.audio_class.SUFFIX) as track_file:
5546            track = self.audio_class.from_pcm(track_file.name,
5547                                              BLANK_PCM_Reader(1))
5548
5549            original_pcm_sum = md5()
5550            audiotools.transfer_framelist_data(track.to_pcm(),
5551                                               original_pcm_sum.update)
5552
5553            comment = audiotools.MetaData(
5554                track_name=u"Name",
5555                track_number=1,
5556                comment=u"abcdefghij" * 13005)
5557            track.set_metadata(comment)
5558            track = audiotools.open(track_file.name)
5559            self.assertEqual(comment, track.get_metadata())
5560
5561            new_pcm_sum = md5()
5562            audiotools.transfer_framelist_data(track.to_pcm(),
5563                                               new_pcm_sum.update)
5564
5565            self.assertEqual(original_pcm_sum.hexdigest(),
5566                             new_pcm_sum.hexdigest())
5567
5568
5569class OpusFileTest(OggVerify, LossyFileTest):
5570    def setUp(self):
5571        self.audio_class = audiotools.OpusAudio
5572        self.suffix = "." + self.audio_class.SUFFIX
5573
5574    @FORMAT_OPUS
5575    def test_channels(self):
5576        # FIXME - test Opus channel assignment
5577        pass
5578
5579    @FORMAT_OPUS
5580    def test_big_comment(self):
5581        with tempfile.NamedTemporaryFile(
5582            suffix="." + self.audio_class.SUFFIX) as track_file:
5583            track = self.audio_class.from_pcm(track_file.name,
5584                                              BLANK_PCM_Reader(1))
5585            original_pcm_sum = md5()
5586            audiotools.transfer_framelist_data(track.to_pcm(),
5587                                               original_pcm_sum.update)
5588
5589            comment = audiotools.MetaData(
5590                track_name=u"Name",
5591                track_number=1,
5592                comment=u"abcdefghij" * 13005)
5593            track.set_metadata(comment)
5594            track = audiotools.open(track_file.name)
5595            self.assertEqual(comment, track.get_metadata())
5596
5597            new_pcm_sum = md5()
5598            audiotools.transfer_framelist_data(track.to_pcm(),
5599                                               new_pcm_sum.update)
5600
5601            self.assertEqual(original_pcm_sum.hexdigest(),
5602                             new_pcm_sum.hexdigest())
5603
5604
5605class WaveFileTest(TestForeignWaveChunks,
5606                   LosslessFileTest):
5607    def setUp(self):
5608        self.audio_class = audiotools.WaveAudio
5609        self.suffix = "." + self.audio_class.SUFFIX
5610
5611    @FORMAT_WAVE
5612    def test_overlong_file(self):
5613        # trying to generate too large of a file
5614        # should throw an exception right away if total_pcm_frames known
5615        # instead of building it first
5616
5617        self.assertEqual(os.path.isfile("invalid.wav"), False)
5618
5619        self.assertRaises(audiotools.EncodingError,
5620                          self.audio_class.from_pcm,
5621                          "invalid.wav",
5622                          EXACT_SILENCE_PCM_Reader(
5623                              pcm_frames=715827883,
5624                              sample_rate=44100,
5625                              channels=2,
5626                              bits_per_sample=24),
5627                          total_pcm_frames=715827883)
5628
5629        self.assertEqual(os.path.isfile("invalid.wav"), False)
5630
5631    @FORMAT_WAVE
5632    def test_verify(self):
5633        # test various truncated files with verify()
5634        for wav_file in ["wav-8bit.wav",
5635                         "wav-1ch.wav",
5636                         "wav-2ch.wav",
5637                         "wav-6ch.wav"]:
5638            with tempfile.NamedTemporaryFile(suffix=".wav") as temp:
5639                with open(wav_file, 'rb') as f:
5640                    wav_data = f.read()
5641                temp.write(wav_data)
5642                temp.flush()
5643                wave = audiotools.open(temp.name)
5644
5645                # try changing the file out from under it
5646                for i in range(0, len(wav_data)):
5647                    with open(temp.name, 'wb') as f:
5648                        f.write(wav_data[0:i])
5649                    self.assertEqual(os.path.getsize(temp.name), i)
5650                    self.assertRaises(audiotools.InvalidFile,
5651                                      wave.verify)
5652
5653        # test running convert() on a truncated file
5654        # triggers EncodingError
5655        # FIXME - truncate file underfoot
5656        # temp = tempfile.NamedTemporaryFile(suffix=".flac")
5657        # try:
5658        #     temp.write(open("wav-2ch.wav", "rb").read()[0:-10])
5659        #     temp.flush()
5660        #     flac = audiotools.open(temp.name)
5661        #     if os.path.isfile("dummy.wav"):
5662        #         os.unlink("dummy.wav")
5663        #     self.assertEqual(os.path.isfile("dummy.wav"), False)
5664        #     self.assertRaises(audiotools.EncodingError,
5665        #                       flac.convert,
5666        #                       "dummy.wav",
5667        #                       audiotools.WaveAudio)
5668        #     self.assertEqual(os.path.isfile("dummy.wav"), False)
5669        # finally:
5670        #     temp.close()
5671
5672        # test other truncated file combinations
5673        for (fmt_size, wav_file) in [(0x24, "wav-8bit.wav"),
5674                                     (0x24, "wav-1ch.wav"),
5675                                     (0x24, "wav-2ch.wav"),
5676                                     (0x3C, "wav-6ch.wav")]:
5677            f = open(wav_file, 'rb')
5678            wav_data = f.read()
5679            f.close()
5680
5681            with tempfile.NamedTemporaryFile(suffix=".wav") as temp:
5682                # first, check that a truncated fmt chunk raises an exception
5683                # at init-time
5684                for i in range(0, fmt_size + 8):
5685                    temp.seek(0, 0)
5686                    temp.write(wav_data[0:i])
5687                    temp.flush()
5688                    self.assertEqual(os.path.getsize(temp.name), i)
5689
5690                    self.assertRaises(audiotools.InvalidFile,
5691                                      audiotools.WaveAudio,
5692                                      temp.name)
5693
5694        # test for non-ASCII chunk IDs
5695        from struct import pack
5696        from test_core import bytes_to_ints, ints_to_bytes
5697
5698        chunks = list(audiotools.open("wav-2ch.wav").chunks()) + \
5699            [audiotools.wav.RIFF_Chunk(b"fooz",
5700                                       10,
5701                                       b"\x00" * 10)]
5702        with tempfile.NamedTemporaryFile(suffix=".wav") as temp:
5703            audiotools.WaveAudio.wave_from_chunks(temp.name,
5704                                                  iter(chunks))
5705            with open(temp.name, 'rb') as f:
5706                wav_data = bytes_to_ints(f.read())
5707            wav_data[-15] = 0
5708            temp.seek(0, 0)
5709            temp.write(ints_to_bytes(wav_data))
5710            temp.flush()
5711            self.assertRaises(audiotools.InvalidFile,
5712                              audiotools.open(temp.name).verify)
5713
5714        FMT = audiotools.wav.RIFF_Chunk(
5715            b"fmt ",
5716            16,
5717            b'\x01\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00')
5718
5719        DATA = audiotools.wav.RIFF_Chunk(
5720            b"data",
5721            26,
5722            b'\x00\x00\x01\x00\x02\x00\x03\x00\x02\x00\x01\x00\x00\x00\xff\xff\xfe\xff\xfd\xff\xfe\xff\xff\xff\x00\x00')
5723
5724        # test multiple fmt chunks
5725        with tempfile.NamedTemporaryFile(suffix=".wav") as temp:
5726            for chunks in [[FMT, FMT, DATA],
5727                           [FMT, DATA, FMT]]:
5728                audiotools.WaveAudio.wave_from_chunks(temp.name, chunks)
5729                self.assertRaises(
5730                    audiotools.InvalidFile,
5731                    audiotools.open(temp.name).verify)
5732
5733        # test multiple data chunks
5734        with tempfile.NamedTemporaryFile(suffix=".wav") as temp:
5735            audiotools.WaveAudio.wave_from_chunks(temp.name, [FMT, DATA, DATA])
5736            self.assertRaises(
5737                audiotools.InvalidFile,
5738                audiotools.open(temp.name).verify)
5739
5740        # test data chunk before fmt chunk
5741        with tempfile.NamedTemporaryFile(suffix=".wav") as temp:
5742            audiotools.WaveAudio.wave_from_chunks(temp.name, [DATA, FMT])
5743            self.assertRaises(
5744                audiotools.InvalidFile,
5745                audiotools.open(temp.name).verify)
5746
5747        # test no fmt chunk
5748        with tempfile.NamedTemporaryFile(suffix=".wav") as temp:
5749            audiotools.WaveAudio.wave_from_chunks(temp.name, [DATA])
5750            self.assertRaises(
5751                audiotools.InvalidFile,
5752                audiotools.open(temp.name).verify)
5753
5754        # test no data chunk
5755        with tempfile.NamedTemporaryFile(suffix=".wav") as temp:
5756            audiotools.WaveAudio.wave_from_chunks(temp.name, [FMT])
5757            self.assertRaises(
5758                audiotools.InvalidFile,
5759                audiotools.open(temp.name).verify)
5760
5761    @FORMAT_WAVE
5762    def test_clean(self):
5763        FMT = audiotools.wav.RIFF_Chunk(
5764            b"fmt ",
5765            16,
5766            b'\x01\x00\x01\x00D\xac\x00\x00\x88X\x01\x00\x02\x00\x10\x00')
5767
5768        DATA = audiotools.wav.RIFF_Chunk(
5769            b"data",
5770            26,
5771            b'\x00\x00\x01\x00\x02\x00\x03\x00\x02\x00\x01\x00\x00\x00\xff\xff\xfe\xff\xfd\xff\xfe\xff\xff\xff\x00\x00')
5772
5773        # test multiple fmt chunks
5774        # test multiple data chunks
5775        # test data chunk before fmt chunk
5776        temp = tempfile.NamedTemporaryFile(suffix=".wav")
5777        fixed = tempfile.NamedTemporaryFile(suffix=".wav")
5778        try:
5779            for chunks in [[FMT, FMT, DATA],
5780                           [FMT, DATA, FMT],
5781                           [FMT, DATA, DATA],
5782                           [DATA, FMT],
5783                           [DATA, FMT, FMT]]:
5784                audiotools.WaveAudio.wave_from_chunks(temp.name, chunks)
5785                fixes = audiotools.open(temp.name).clean(fixed.name)
5786                wave = audiotools.open(fixed.name)
5787                chunks = list(wave.chunks())
5788                self.assertEqual([c.id for c in chunks],
5789                                 [c.id for c in [FMT, DATA]])
5790                self.assertEqual([c.__size__ for c in chunks],
5791                                 [c.__size__ for c in [FMT, DATA]])
5792                self.assertEqual([c.__data__ for c in chunks],
5793                                 [c.__data__ for c in [FMT, DATA]])
5794        finally:
5795            temp.close()
5796            fixed.close()
5797
5798        # test converting 24bps file to WAVEFORMATEXTENSIBLE
5799        # FIXME
5800
5801
5802class WavPackFileTest(TestForeignWaveChunks,
5803                      LosslessFileTest):
5804    def setUp(self):
5805        self.audio_class = audiotools.WavPackAudio
5806        self.suffix = "." + self.audio_class.SUFFIX
5807
5808        from audiotools.decoders import WavPackDecoder
5809        from audiotools.encoders import encode_wavpack
5810
5811        self.decoder = WavPackDecoder
5812        self.encode = encode_wavpack
5813        self.encode_opts = [{"block_size": 44100,
5814                             "false_stereo": True,
5815                             "wasted_bits": True,
5816                             "joint_stereo": False,
5817                             "correlation_passes": 0},
5818                            {"block_size": 44100,
5819                             "false_stereo": True,
5820                             "wasted_bits": True,
5821                             "joint_stereo": True,
5822                             "correlation_passes": 0},
5823                            {"block_size": 44100,
5824                             "false_stereo": True,
5825                             "wasted_bits": True,
5826                             "joint_stereo": True,
5827                             "correlation_passes": 1},
5828                            {"block_size": 44100,
5829                             "false_stereo": True,
5830                             "wasted_bits": True,
5831                             "joint_stereo": True,
5832                             "correlation_passes": 2},
5833                            {"block_size": 44100,
5834                             "false_stereo": True,
5835                             "wasted_bits": True,
5836                             "joint_stereo": True,
5837                             "correlation_passes": 5},
5838                            {"block_size": 44100,
5839                             "false_stereo": True,
5840                             "wasted_bits": True,
5841                             "joint_stereo": True,
5842                             "correlation_passes": 10},
5843                            {"block_size": 44100,
5844                             "false_stereo": True,
5845                             "wasted_bits": True,
5846                             "joint_stereo": True,
5847                             "correlation_passes": 16}]
5848
5849    @FORMAT_WAVPACK
5850    def test_init(self):
5851        # check missing file
5852        self.assertRaises(audiotools.wavpack.InvalidWavPack,
5853                          audiotools.WavPackAudio,
5854                          "/dev/null/foo")
5855
5856        # check invalid file
5857        with tempfile.NamedTemporaryFile(suffix=".wv") as invalid_file:
5858            for c in [b"i", b"n", b"v", b"a", b"l", b"i", b"d",
5859                      b"s", b"t", b"r", b"i", b"n", b"g", b"x", b"x", b"x"]:
5860                invalid_file.write(c)
5861                invalid_file.flush()
5862                self.assertRaises(audiotools.wavpack.InvalidWavPack,
5863                                  audiotools.WavPackAudio,
5864                                  invalid_file.name)
5865
5866        # check some decoder errors,
5867        # mostly to ensure a failed init doesn't make Python explode
5868        self.assertRaises(TypeError, self.decoder)
5869
5870        self.assertRaises(IOError, self.decoder, None)
5871
5872        self.assertRaises(IOError, self.decoder, "filename")
5873
5874    @FORMAT_WAVPACK
5875    def test_verify(self):
5876        # test truncating a WavPack file causes verify()
5877        # to raise InvalidFile as necessary
5878        with open("wavpack-combo.wv", "rb") as f:
5879            wavpackdata = f.read()
5880        with tempfile.NamedTemporaryFile(
5881            suffix="." + self.audio_class.SUFFIX) as temp:
5882            self.assertEqual(audiotools.open("wavpack-combo.wv").verify(),
5883                             True)
5884            temp.write(wavpackdata)
5885            temp.flush()
5886            test_wavpack = audiotools.open(temp.name)
5887            for i in range(0, 0x20B):
5888                with open(temp.name, "wb") as f:
5889                    f.write(wavpackdata[0:i])
5890                self.assertEqual(os.path.getsize(temp.name), i)
5891                self.assertRaises(audiotools.InvalidFile,
5892                                  test_wavpack.verify)
5893
5894                # Swapping random bits doesn't affect WavPack's decoding
5895                # in many instances - which is surprising since I'd
5896                # expect its adaptive routines to be more susceptible
5897                # to values being out-of-whack during decorrelation.
5898                # This resilience may be related to its hybrid mode,
5899                # but it doesn't inspire confidence.
5900
5901        # test truncating a WavPack file causes the WavPackDecoder
5902        # to raise IOError as necessary
5903        from audiotools.decoders import WavPackDecoder
5904        from test_core import ints_to_bytes, bytes_to_ints
5905
5906        with open("silence.wv", "rb") as f:
5907            wavpack_data = bytes_to_ints(f.read())
5908
5909        with tempfile.NamedTemporaryFile(suffix=".wv") as temp:
5910            for i in range(0, len(wavpack_data)):
5911                temp.seek(0, 0)
5912                temp.write(ints_to_bytes(wavpack_data[0:i]))
5913                temp.flush()
5914                self.assertEqual(os.path.getsize(temp.name), i)
5915                f = open(temp.name, "rb")
5916                try:
5917                    decoder = WavPackDecoder(f)
5918                except IOError:
5919                    # chopping off the first few bytes might trigger
5920                    # an IOError at init-time, which is ok
5921                    f.close()
5922                    continue
5923                self.assertIsNotNone(decoder)
5924                self.assertRaises(IOError,
5925                                  audiotools.transfer_framelist_data,
5926                                  decoder, lambda f: f)
5927
5928        # test a truncated WavPack file's convert() method
5929        # generates EncodingErrors
5930        with tempfile.NamedTemporaryFile(
5931            suffix="." + self.audio_class.SUFFIX) as temp:
5932            with open("wavpack-combo.wv", "rb") as f:
5933                temp.write(f.read())
5934                temp.flush()
5935            wavpack = audiotools.open(temp.name)
5936            with open(temp.name, "wb") as w:
5937                with open("wavpack-combo.wv", "rb") as r:
5938                    w.write(r.read()[0:-0x20B])
5939
5940            if os.path.isfile("dummy.wav"):
5941                os.unlink("dummy.wav")
5942            self.assertFalse(os.path.isfile("dummy.wav"))
5943            self.assertRaises(audiotools.EncodingError,
5944                              wavpack.convert,
5945                              "dummy.wav",
5946                              audiotools.WaveAudio)
5947            self.assertFalse(os.path.isfile("dummy.wav"))
5948
5949    def __stream_variations__(self):
5950        for stream in [
5951            test_streams.Silence8_Mono(200000, 44100),
5952            test_streams.Silence8_Mono(200000, 96000),
5953            test_streams.Silence8_Stereo(200000, 44100),
5954            test_streams.Silence8_Stereo(200000, 96000),
5955            test_streams.Silence16_Mono(200000, 44100),
5956            test_streams.Silence16_Mono(200000, 96000),
5957            test_streams.Silence16_Stereo(200000, 44100),
5958            test_streams.Silence16_Stereo(200000, 96000),
5959            test_streams.Silence24_Mono(200000, 44100),
5960            test_streams.Silence24_Mono(200000, 96000),
5961            test_streams.Silence24_Stereo(200000, 44100),
5962            test_streams.Silence24_Stereo(200000, 96000),
5963
5964            test_streams.Sine8_Mono(200000, 48000, 441.0, 0.50, 441.0, 0.49),
5965            test_streams.Sine8_Mono(200000, 96000, 441.0, 0.61, 661.5, 0.37),
5966            test_streams.Sine8_Mono(200000, 44100, 441.0, 0.50, 882.0, 0.49),
5967            test_streams.Sine8_Mono(200000, 44100, 441.0, 0.50, 4410.0, 0.49),
5968            test_streams.Sine8_Mono(200000, 44100, 8820.0, 0.70, 4410.0, 0.29),
5969
5970            test_streams.Sine8_Stereo(200000, 48000, 441.0, 0.50, 441.0, 0.49, 1.0),
5971            test_streams.Sine8_Stereo(200000, 48000, 441.0, 0.61, 661.5, 0.37, 1.0),
5972            test_streams.Sine8_Stereo(200000, 96000, 441.0, 0.50, 882.0, 0.49, 1.0),
5973            test_streams.Sine8_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.0),
5974            test_streams.Sine8_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 1.0),
5975            test_streams.Sine8_Stereo(200000, 44100, 441.0, 0.50, 441.0, 0.49, 0.5),
5976            test_streams.Sine8_Stereo(200000, 44100, 441.0, 0.61, 661.5, 0.37, 2.0),
5977            test_streams.Sine8_Stereo(200000, 44100, 441.0, 0.50, 882.0, 0.49, 0.7),
5978            test_streams.Sine8_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.3),
5979            test_streams.Sine8_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 0.1),
5980
5981            test_streams.Sine16_Mono(200000, 48000, 441.0, 0.50, 441.0, 0.49),
5982            test_streams.Sine16_Mono(200000, 96000, 441.0, 0.61, 661.5, 0.37),
5983            test_streams.Sine16_Mono(200000, 44100, 441.0, 0.50, 882.0, 0.49),
5984            test_streams.Sine16_Mono(200000, 44100, 441.0, 0.50, 4410.0, 0.49),
5985            test_streams.Sine16_Mono(200000, 44100, 8820.0, 0.70, 4410.0, 0.29),
5986
5987            test_streams.Sine16_Stereo(200000, 48000, 441.0, 0.50, 441.0, 0.49, 1.0),
5988            test_streams.Sine16_Stereo(200000, 48000, 441.0, 0.61, 661.5, 0.37, 1.0),
5989            test_streams.Sine16_Stereo(200000, 96000, 441.0, 0.50, 882.0, 0.49, 1.0),
5990            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.0),
5991            test_streams.Sine16_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 1.0),
5992            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.50, 441.0, 0.49, 0.5),
5993            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.61, 661.5, 0.37, 2.0),
5994            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.50, 882.0, 0.49, 0.7),
5995            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.3),
5996            test_streams.Sine16_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 0.1),
5997
5998            test_streams.Sine24_Mono(200000, 48000, 441.0, 0.50, 441.0, 0.49),
5999            test_streams.Sine24_Mono(200000, 96000, 441.0, 0.61, 661.5, 0.37),
6000            test_streams.Sine24_Mono(200000, 44100, 441.0, 0.50, 882.0, 0.49),
6001            test_streams.Sine24_Mono(200000, 44100, 441.0, 0.50, 4410.0, 0.49),
6002            test_streams.Sine24_Mono(200000, 44100, 8820.0, 0.70, 4410.0, 0.29),
6003
6004            test_streams.Sine24_Stereo(200000, 48000, 441.0, 0.50, 441.0, 0.49, 1.0),
6005            test_streams.Sine24_Stereo(200000, 48000, 441.0, 0.61, 661.5, 0.37, 1.0),
6006            test_streams.Sine24_Stereo(200000, 96000, 441.0, 0.50, 882.0, 0.49, 1.0),
6007            test_streams.Sine24_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.0),
6008            test_streams.Sine24_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 1.0),
6009            test_streams.Sine24_Stereo(200000, 44100, 441.0, 0.50, 441.0, 0.49, 0.5),
6010            test_streams.Sine24_Stereo(200000, 44100, 441.0, 0.61, 661.5, 0.37, 2.0),
6011            test_streams.Sine24_Stereo(200000, 44100, 441.0, 0.50, 882.0, 0.49, 0.7),
6012            test_streams.Sine24_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.3),
6013            test_streams.Sine24_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 0.1),
6014
6015            test_streams.Simple_Sine(200000, 44100, 0x7, 8,
6016                                     (25, 10000),
6017                                     (50, 20000),
6018                                     (120, 30000)),
6019            test_streams.Simple_Sine(200000, 44100, 0x33, 8,
6020                                     (25, 10000),
6021                                     (50, 20000),
6022                                     (75, 30000),
6023                                     (65, 40000)),
6024            test_streams.Simple_Sine(200000, 44100, 0x37, 8,
6025                                     (25, 10000),
6026                                     (35, 15000),
6027                                     (45, 20000),
6028                                     (50, 25000),
6029                                     (55, 30000)),
6030            test_streams.Simple_Sine(200000, 44100, 0x3F, 8,
6031                                     (25, 10000),
6032                                     (45, 15000),
6033                                     (65, 20000),
6034                                     (85, 25000),
6035                                     (105, 30000),
6036                                     (120, 35000)),
6037
6038            test_streams.Simple_Sine(200000, 44100, 0x7, 16,
6039                                     (6400, 10000),
6040                                     (12800, 20000),
6041                                     (30720, 30000)),
6042            test_streams.Simple_Sine(200000, 44100, 0x33, 16,
6043                                     (6400, 10000),
6044                                     (12800, 20000),
6045                                     (19200, 30000),
6046                                     (16640, 40000)),
6047            test_streams.Simple_Sine(200000, 44100, 0x37, 16,
6048                                     (6400, 10000),
6049                                     (8960, 15000),
6050                                     (11520, 20000),
6051                                     (12800, 25000),
6052                                     (14080, 30000)),
6053            test_streams.Simple_Sine(200000, 44100, 0x3F, 16,
6054                                     (6400, 10000),
6055                                     (11520, 15000),
6056                                     (16640, 20000),
6057                                     (21760, 25000),
6058                                     (26880, 30000),
6059                                     (30720, 35000)),
6060
6061            test_streams.Simple_Sine(200000, 44100, 0x7, 24,
6062                                     (1638400, 10000),
6063                                     (3276800, 20000),
6064                                     (7864320, 30000)),
6065            test_streams.Simple_Sine(200000, 44100, 0x33, 24,
6066                                     (1638400, 10000),
6067                                     (3276800, 20000),
6068                                     (4915200, 30000),
6069                                     (4259840, 40000)),
6070            test_streams.Simple_Sine(200000, 44100, 0x37, 24,
6071                                     (1638400, 10000),
6072                                     (2293760, 15000),
6073                                     (2949120, 20000),
6074                                     (3276800, 25000),
6075                                     (3604480, 30000)),
6076            test_streams.Simple_Sine(200000, 44100, 0x3F, 24,
6077                                     (1638400, 10000),
6078                                     (2949120, 15000),
6079                                     (4259840, 20000),
6080                                     (5570560, 25000),
6081                                     (6881280, 30000),
6082                                     (7864320, 35000))]:
6083            yield stream
6084
6085    def __test_reader__(self, pcmreader, total_pcm_frames, **encode_options):
6086        if not audiotools.BIN.can_execute(audiotools.BIN["wvunpack"]):
6087            self.assertTrue(False,
6088                            "reference WavPack binary wvunpack(1) " +
6089                            "required for this test")
6090
6091        temp_file = tempfile.NamedTemporaryFile(suffix=".wv")
6092
6093        self.encode(temp_file.name,
6094                    audiotools.BufferedPCMReader(pcmreader),
6095                    **encode_options)
6096
6097        devnull = open(os.devnull, "wb")
6098
6099        sub = subprocess.Popen([audiotools.BIN["wvunpack"],
6100                                "-vmq", temp_file.name],
6101                               stdout=devnull,
6102                               stderr=devnull)
6103
6104        self.assertEqual(sub.wait(), 0,
6105                         "wvunpack decode error on %s with options %s" %
6106                         (repr(pcmreader),
6107                          repr(encode_options)))
6108
6109        for wavpack in [self.decoder(open(temp_file.name, "rb")),
6110                        self.decoder(Filewrapper(open(temp_file.name, "rb")))]:
6111            self.assertEqual(wavpack.sample_rate, pcmreader.sample_rate)
6112            self.assertEqual(wavpack.bits_per_sample, pcmreader.bits_per_sample)
6113            self.assertEqual(wavpack.channels, pcmreader.channels)
6114            self.assertEqual(wavpack.channel_mask, pcmreader.channel_mask)
6115
6116            md5sum = md5()
6117            f = wavpack.read(audiotools.FRAMELIST_SIZE)
6118            while len(f) > 0:
6119                md5sum.update(f.to_bytes(False, True))
6120                f = wavpack.read(audiotools.FRAMELIST_SIZE)
6121            wavpack.close()
6122            self.assertEqual(md5sum.digest(), pcmreader.digest())
6123
6124        # perform test again with total_pcm_frames indicated
6125        pcmreader.reset()
6126
6127        self.encode(temp_file.name,
6128                    audiotools.BufferedPCMReader(pcmreader),
6129                    total_pcm_frames=total_pcm_frames,
6130                    **encode_options)
6131
6132        sub = subprocess.Popen([audiotools.BIN["wvunpack"],
6133                                "-vmq", temp_file.name],
6134                               stdout=devnull,
6135                               stderr=devnull)
6136
6137        devnull.close()
6138
6139        self.assertEqual(sub.wait(), 0,
6140                         "wvunpack decode error on %s with options %s" %
6141                         (repr(pcmreader),
6142                          repr(encode_options)))
6143
6144        for wavpack in [self.decoder(open(temp_file.name, "rb")),
6145                        self.decoder(Filewrapper(open(temp_file.name, "rb")))]:
6146            self.assertEqual(wavpack.sample_rate, pcmreader.sample_rate)
6147            self.assertEqual(wavpack.bits_per_sample, pcmreader.bits_per_sample)
6148            self.assertEqual(wavpack.channels, pcmreader.channels)
6149            self.assertEqual(wavpack.channel_mask, pcmreader.channel_mask)
6150
6151            md5sum = md5()
6152            f = wavpack.read(audiotools.FRAMELIST_SIZE)
6153            while len(f) > 0:
6154                md5sum.update(f.to_bytes(False, True))
6155                f = wavpack.read(audiotools.FRAMELIST_SIZE)
6156            wavpack.close()
6157            self.assertEqual(md5sum.digest(), pcmreader.digest())
6158
6159        temp_file.close()
6160
6161    @FORMAT_WAVPACK
6162    def test_small_files(self):
6163        for opts in self.encode_opts:
6164            for g in [test_streams.Generate01,
6165                      test_streams.Generate02]:
6166                self.__test_reader__(g(44100), 1, **opts)
6167            for g in [test_streams.Generate03,
6168                      test_streams.Generate04]:
6169                self.__test_reader__(g(44100), 5, **opts)
6170
6171    @FORMAT_WAVPACK
6172    def test_full_scale_deflection(self):
6173        for opts in self.encode_opts:
6174            for (bps, fsd) in [(8, test_streams.fsd8),
6175                               (16, test_streams.fsd16),
6176                               (24, test_streams.fsd24)]:
6177                for pattern in [test_streams.PATTERN01,
6178                                test_streams.PATTERN02,
6179                                test_streams.PATTERN03,
6180                                test_streams.PATTERN04,
6181                                test_streams.PATTERN05,
6182                                test_streams.PATTERN06,
6183                                test_streams.PATTERN07]:
6184                    self.__test_reader__(
6185                        test_streams.MD5Reader(fsd(pattern, 100)),
6186                        len(pattern) * 100,
6187                        **opts)
6188
6189    @FORMAT_WAVPACK
6190    def test_wasted_bps(self):
6191        for opts in self.encode_opts:
6192            self.__test_reader__(test_streams.WastedBPS16(1000),
6193                                 1000,
6194                                 **opts)
6195
6196    @FORMAT_WAVPACK
6197    def test_blocksizes(self):
6198        noise = struct.unpack(">32h", os.urandom(64))
6199
6200        opts = {"false_stereo": False,
6201                "wasted_bits": False,
6202                "joint_stereo": False}
6203        for block_size in [16, 17, 18, 19, 20, 21, 22, 23,
6204                           24, 25, 26, 27, 28, 29, 30, 31, 32, 33]:
6205            for decorrelation_passes in [0, 1, 5]:
6206                opts_copy = opts.copy()
6207                opts_copy["block_size"] = block_size
6208                opts_copy["correlation_passes"] = decorrelation_passes
6209                self.__test_reader__(
6210                    test_streams.MD5Reader(
6211                        test_streams.FrameListReader(noise,
6212                                                     44100, 1, 16)),
6213                    len(noise),
6214                    **opts_copy)
6215
6216    @FORMAT_WAVPACK
6217    def test_silence(self):
6218        for opts in self.encode_opts:
6219            for (channels, mask) in [
6220                (1, audiotools.ChannelMask.from_channels(1)),
6221                (2, audiotools.ChannelMask.from_channels(2)),
6222                (4, audiotools.ChannelMask.from_fields(front_left=True,
6223                                                       front_right=True,
6224                                                       back_left=True,
6225                                                       back_right=True)),
6226                (8, audiotools.ChannelMask(0))]:
6227                for bps in [8, 16, 24]:
6228                    opts_copy = opts.copy()
6229                    for block_size in [44100, 32, 32768, 65535,
6230                                       16777215]:
6231                        opts_copy['block_size'] = block_size
6232
6233                        self.__test_reader__(
6234                            MD5_Reader(
6235                                EXACT_SILENCE_PCM_Reader(
6236                                    pcm_frames=65536,
6237                                    sample_rate=44100,
6238                                    channels=channels,
6239                                    channel_mask=mask,
6240                                    bits_per_sample=bps)),
6241                            65536,
6242                            **opts_copy)
6243
6244    @FORMAT_WAVPACK
6245    def test_noise(self):
6246        for opts in self.encode_opts:
6247            for (channels, mask) in [
6248                (1, audiotools.ChannelMask.from_channels(1)),
6249                (2, audiotools.ChannelMask.from_channels(2)),
6250                (4, audiotools.ChannelMask.from_fields(front_left=True,
6251                                                       front_right=True,
6252                                                       back_left=True,
6253                                                       back_right=True)),
6254                (8, audiotools.ChannelMask(0))]:
6255                for bps in [8, 16, 24]:
6256                    opts_copy = opts.copy()
6257                    for block_size in [44100, 32, 32768, 65535,
6258                                       16777215]:
6259                        opts_copy['block_size'] = block_size
6260
6261                        self.__test_reader__(
6262                            MD5_Reader(
6263                                EXACT_RANDOM_PCM_Reader(
6264                                    pcm_frames=65536,
6265                                    sample_rate=44100,
6266                                    channels=channels,
6267                                    channel_mask=mask,
6268                                    bits_per_sample=bps)),
6269                            65536,
6270                            **opts_copy)
6271
6272    @FORMAT_WAVPACK
6273    def test_fractional(self):
6274        def __perform_test__(block_size, pcm_frames):
6275            self.__test_reader__(
6276                MD5_Reader(
6277                    EXACT_RANDOM_PCM_Reader(
6278                        pcm_frames=pcm_frames,
6279                        sample_rate=44100,
6280                        channels=2,
6281                        bits_per_sample=16)),
6282                pcm_frames,
6283                block_size=block_size,
6284                correlation_passes=5,
6285                false_stereo=False,
6286                wasted_bits=False,
6287                joint_stereo=False)
6288
6289        for pcm_frames in [31, 32, 33, 34, 35, 2046, 2047, 2048, 2049, 2050]:
6290            __perform_test__(33, pcm_frames)
6291
6292        for pcm_frames in [254, 255, 256, 257, 258, 510, 511, 512, 513,
6293                           514, 1022, 1023, 1024, 1025, 1026, 2046, 2047,
6294                           2048, 2049, 2050, 4094, 4095, 4096, 4097, 4098]:
6295            __perform_test__(256, pcm_frames)
6296
6297        for pcm_frames in [1022, 1023, 1024, 1025, 1026, 2046, 2047,
6298                           2048, 2049, 2050, 4094, 4095, 4096, 4097, 4098]:
6299            __perform_test__(2048, pcm_frames)
6300
6301        for pcm_frames in [1022, 1023, 1024, 1025, 1026, 2046, 2047,
6302                           2048, 2049, 2050, 4094, 4095, 4096, 4097,
6303                           4098, 4606, 4607, 4608, 4609, 4610, 8190,
6304                           8191, 8192, 8193, 8194, 16382, 16383, 16384,
6305                           16385, 16386]:
6306            __perform_test__(4608, pcm_frames)
6307
6308        for pcm_frames in [44098, 44099, 44100, 44101, 44102, 44103,
6309                           88198, 88199, 88200, 88201, 88202, 88203]:
6310            __perform_test__(44100, pcm_frames)
6311
6312    @FORMAT_WAVPACK
6313    def test_multichannel(self):
6314        def __permutations__(executables, options, total):
6315            if total == 0:
6316                yield []
6317            else:
6318                for (executable, option) in zip(executables,
6319                                                options):
6320                    for permutation in __permutations__(executables,
6321                                                        options,
6322                                                        total - 1):
6323                        yield [executable(**option)] + permutation
6324
6325        # test a mix of identical and non-identical channels
6326        # using different decorrelation, joint stereo and false stereo options
6327        combos = 0
6328        for (false_stereo, joint_stereo) in [(False, False),
6329                                             (False, True),
6330                                             (True, False),
6331                                             (True, True)]:
6332            for (channels, mask) in [(2, 0x3), (3, 0x7), (4, 0x33),
6333                                     (5, 0x3B), (6, 0x3F)]:
6334                for readers in __permutations__(
6335                    [EXACT_BLANK_PCM_Reader,
6336                     EXACT_RANDOM_PCM_Reader,
6337                     test_streams.Sine16_Mono],
6338                    [{"pcm_frames": 100,
6339                      "sample_rate": 44100,
6340                      "channels": 1,
6341                      "bits_per_sample": 16},
6342                     {"pcm_frames": 100,
6343                      "sample_rate": 44100,
6344                      "channels": 1,
6345                      "bits_per_sample": 16},
6346                     {"pcm_frames": 100,
6347                      "sample_rate": 44100,
6348                      "f1": 441.0,
6349                      "a1": 0.61,
6350                      "f2": 661.5,
6351                      "a2": 0.37}], channels):
6352                    joined = MD5_Reader(Join_Reader(readers, mask))
6353                    self.__test_reader__(joined,
6354                                         100,
6355                                         block_size=44100,
6356                                         false_stereo=false_stereo,
6357                                         joint_stereo=joint_stereo,
6358                                         correlation_passes=1,
6359                                         wasted_bits=False)
6360
6361    @FORMAT_WAVPACK
6362    def test_sines(self):
6363        for opts in self.encode_opts:
6364            for g in self.__stream_variations__():
6365                self.__test_reader__(g, 200000, **opts)
6366
6367    @FORMAT_WAVPACK
6368    def test_option_variations(self):
6369        for block_size in [11025, 22050, 44100, 88200, 176400]:
6370            for false_stereo in [False, True]:
6371                for wasted_bits in [False, True]:
6372                    for joint_stereo in [False, True]:
6373                        for decorrelation_passes in [0, 1, 2, 5, 10, 16]:
6374                            self.__test_reader__(
6375                                test_streams.Sine16_Stereo(200000,
6376                                                           48000,
6377                                                           441.0,
6378                                                           0.50,
6379                                                           441.0,
6380                                                           0.49,
6381                                                           1.0),
6382                                200000,
6383                                block_size=block_size,
6384                                false_stereo=false_stereo,
6385                                wasted_bits=wasted_bits,
6386                                joint_stereo=joint_stereo,
6387                                correlation_passes=decorrelation_passes)
6388
6389    @FORMAT_WAVPACK
6390    def test_python_codec(self):
6391        def test_python_reader(pcmreader, total_pcm_frames, **encode_options):
6392            from audiotools.py_encoders import encode_wavpack
6393
6394            # encode file using Python-based encoder
6395            temp_file = tempfile.NamedTemporaryFile(suffix=".wv")
6396
6397            encode_wavpack(temp_file.name,
6398                           audiotools.BufferedPCMReader(pcmreader),
6399                           **encode_options)
6400
6401            # verify contents of file decoded by
6402            # Python-based decoder against contents decoded by
6403            # C-based decoder
6404            from audiotools.py_decoders import WavPackDecoder as WavPackDecoder1
6405            from audiotools.decoders import WavPackDecoder as WavPackDecoder2
6406
6407            self.assertTrue(
6408                audiotools.pcm_cmp(
6409                    WavPackDecoder1(temp_file.name),
6410                    WavPackDecoder2(open(temp_file.name, "rb"))))
6411
6412            # redo test with total_pcm_frames indicated
6413            pcmreader.reset()
6414
6415            encode_wavpack(temp_file.name,
6416                           audiotools.BufferedPCMReader(pcmreader),
6417                           total_pcm_frames=total_pcm_frames,
6418                           **encode_options)
6419
6420            # verify contents of file decoded by
6421            # Python-based decoder against contents decoded by
6422            # C-based decoder
6423            from audiotools.py_decoders import WavPackDecoder as WavPackDecoder1
6424            from audiotools.decoders import WavPackDecoder as WavPackDecoder2
6425
6426            self.assertTrue(
6427                audiotools.pcm_cmp(
6428                    WavPackDecoder1(temp_file.name),
6429                    WavPackDecoder2(open(temp_file.name, "rb"))))
6430
6431            temp_file.close()
6432
6433        # test small files
6434        for opts in self.encode_opts:
6435            for g in [test_streams.Generate01,
6436                      test_streams.Generate02]:
6437                test_python_reader(g(44100), 1, **opts)
6438            for g in [test_streams.Generate03,
6439                      test_streams.Generate04]:
6440                test_python_reader(g(44100), 5, **opts)
6441
6442        # test full scale deflection
6443        for opts in self.encode_opts:
6444            for (bps, fsd) in [(8, test_streams.fsd8),
6445                               (16, test_streams.fsd16),
6446                               (24, test_streams.fsd24)]:
6447                for pattern in [test_streams.PATTERN01,
6448                                test_streams.PATTERN02,
6449                                test_streams.PATTERN03,
6450                                test_streams.PATTERN04,
6451                                test_streams.PATTERN05,
6452                                test_streams.PATTERN06,
6453                                test_streams.PATTERN07]:
6454                    test_python_reader(fsd(pattern, 100),
6455                                       len(pattern) * 100,
6456                                       **opts)
6457
6458        # test wasted BPS
6459        for opts in self.encode_opts:
6460            test_python_reader(test_streams.WastedBPS16(1000),
6461                               1000,
6462                               **opts)
6463
6464        # test block sizes
6465        noise = struct.unpack(">32h", os.urandom(64))
6466
6467        opts = {"false_stereo": False,
6468                "wasted_bits": False,
6469                "joint_stereo": False}
6470        for block_size in [16, 17, 18, 19, 20, 21, 22, 23,
6471                           24, 25, 26, 27, 28, 29, 30, 31, 32, 33]:
6472            for decorrelation_passes in [0, 1, 5]:
6473                opts_copy = opts.copy()
6474                opts_copy["block_size"] = block_size
6475                opts_copy["correlation_passes"] = decorrelation_passes
6476                test_python_reader(
6477                    test_streams.FrameListReader(noise,
6478                                                 44100, 1, 16),
6479                    len(noise),
6480                    **opts_copy)
6481
6482        # test silence
6483        for opts in self.encode_opts:
6484            for (channels, mask) in [
6485                (1, audiotools.ChannelMask.from_channels(1)),
6486                (2, audiotools.ChannelMask.from_channels(2))]:
6487                opts_copy = opts.copy()
6488                opts_copy['block_size'] = 4095
6489                test_python_reader(
6490                    EXACT_SILENCE_PCM_Reader(
6491                        pcm_frames=4096,
6492                        sample_rate=44100,
6493                        channels=channels,
6494                        channel_mask=mask,
6495                        bits_per_sample=16),
6496                    4096,
6497                    **opts_copy)
6498
6499        # test noise
6500        for opts in self.encode_opts:
6501            for (channels, mask) in [
6502                (1, audiotools.ChannelMask.from_channels(1)),
6503                (2, audiotools.ChannelMask.from_channels(2))]:
6504                opts_copy = opts.copy()
6505                opts_copy['block_size'] = 4095
6506                test_python_reader(
6507                    EXACT_RANDOM_PCM_Reader(
6508                        pcm_frames=4096,
6509                        sample_rate=44100,
6510                        channels=channels,
6511                        channel_mask=mask,
6512                        bits_per_sample=16),
6513                    4096,
6514                    **opts_copy)
6515
6516        # test fractional
6517        for (block_size,
6518             pcm_frames_list) in [(33, [31, 32, 33, 34, 35, 2046,
6519                                        2047, 2048, 2049, 2050]),
6520                                  (256, [254, 255, 256, 257, 258, 510,
6521                                         511, 512, 513, 514, 1022, 1023,
6522                                         1024, 1025, 1026, 2046, 2047, 2048,
6523                                         2049, 2050, 4094, 4095, 4096, 4097,
6524                                         4098])]:
6525            for pcm_frames in pcm_frames_list:
6526                test_python_reader(
6527                    EXACT_RANDOM_PCM_Reader(
6528                        pcm_frames=pcm_frames,
6529                        sample_rate=44100,
6530                        channels=2,
6531                        bits_per_sample=16),
6532                    pcm_frames,
6533                    block_size=block_size,
6534                    correlation_passes=5,
6535                    false_stereo=False,
6536                    wasted_bits=False,
6537                    joint_stereo=False)
6538
6539        # test sines
6540        for opts in self.encode_opts:
6541            for g in [test_streams.Sine8_Mono(5000, 48000,
6542                                              441.0, 0.50, 441.0, 0.49),
6543                      test_streams.Sine8_Stereo(5000, 48000,
6544                                                441.0, 0.50, 441.0, 0.49, 1.0),
6545                      test_streams.Sine16_Mono(5000, 48000,
6546                                               441.0, 0.50, 441.0, 0.49),
6547                      test_streams.Sine16_Stereo(5000, 48000,
6548                                                 441.0, 0.50, 441.0, 0.49, 1.0),
6549                      test_streams.Sine24_Mono(5000, 48000,
6550                                               441.0, 0.50, 441.0, 0.49),
6551                      test_streams.Sine24_Stereo(5000, 48000,
6552                                                 441.0, 0.50, 441.0, 0.49, 1.0),
6553                      test_streams.Simple_Sine(5000, 44100, 0x7, 8,
6554                                               (25, 10000),
6555                                               (50, 20000),
6556                                               (120, 30000)),
6557                      test_streams.Simple_Sine(5000, 44100, 0x33, 8,
6558                                               (25, 10000),
6559                                               (50, 20000),
6560                                               (75, 30000),
6561                                               (65, 40000)),
6562                      test_streams.Simple_Sine(5000, 44100, 0x37, 8,
6563                                               (25, 10000),
6564                                               (35, 15000),
6565                                               (45, 20000),
6566                                               (50, 25000),
6567                                               (55, 30000)),
6568                      test_streams.Simple_Sine(5000, 44100, 0x3F, 8,
6569                                               (25, 10000),
6570                                               (45, 15000),
6571                                               (65, 20000),
6572                                               (85, 25000),
6573                                               (105, 30000),
6574                                               (120, 35000)),
6575
6576                      test_streams.Simple_Sine(5000, 44100, 0x7, 16,
6577                                               (6400, 10000),
6578                                               (12800, 20000),
6579                                               (30720, 30000)),
6580                      test_streams.Simple_Sine(5000, 44100, 0x33, 16,
6581                                               (6400, 10000),
6582                                               (12800, 20000),
6583                                               (19200, 30000),
6584                                               (16640, 40000)),
6585                      test_streams.Simple_Sine(5000, 44100, 0x37, 16,
6586                                               (6400, 10000),
6587                                               (8960, 15000),
6588                                               (11520, 20000),
6589                                               (12800, 25000),
6590                                               (14080, 30000)),
6591                      test_streams.Simple_Sine(5000, 44100, 0x3F, 16,
6592                                               (6400, 10000),
6593                                               (11520, 15000),
6594                                               (16640, 20000),
6595                                               (21760, 25000),
6596                                               (26880, 30000),
6597                                               (30720, 35000)),
6598
6599                      test_streams.Simple_Sine(5000, 44100, 0x7, 24,
6600                                               (1638400, 10000),
6601                                               (3276800, 20000),
6602                                               (7864320, 30000)),
6603                      test_streams.Simple_Sine(5000, 44100, 0x33, 24,
6604                                               (1638400, 10000),
6605                                               (3276800, 20000),
6606                                               (4915200, 30000),
6607                                               (4259840, 40000)),
6608                      test_streams.Simple_Sine(5000, 44100, 0x37, 24,
6609                                               (1638400, 10000),
6610                                               (2293760, 15000),
6611                                               (2949120, 20000),
6612                                               (3276800, 25000),
6613                                               (3604480, 30000)),
6614                      test_streams.Simple_Sine(5000, 44100, 0x3F, 24,
6615                                               (1638400, 10000),
6616                                               (2949120, 15000),
6617                                               (4259840, 20000),
6618                                               (5570560, 25000),
6619                                               (6881280, 30000),
6620                                               (7864320, 35000))]:
6621                test_python_reader(g, 5000, **opts)
6622
6623
6624class TTAFileTest(LosslessFileTest):
6625    def setUp(self):
6626        self.audio_class = audiotools.TrueAudio
6627        self.suffix = "." + self.audio_class.SUFFIX
6628
6629        from audiotools.decoders import TTADecoder
6630
6631        self.decoder = TTADecoder
6632        self.encode = audiotools.TrueAudio.from_pcm
6633
6634    @FORMAT_TTA
6635    def test_init(self):
6636        # check missing file
6637        self.assertRaises(audiotools.tta.InvalidTTA,
6638                          audiotools.TrueAudio,
6639                          "/dev/null/foo")
6640
6641        # check invalid file
6642        with tempfile.NamedTemporaryFile(suffix=".tta") as invalid_file:
6643            for c in [b"i", b"n", b"v", b"a", b"l", b"i", b"d",
6644                      b"s", b"t", b"r", b"i", b"n", b"g", b"x", b"x", b"x"]:
6645                invalid_file.write(c)
6646                invalid_file.flush()
6647                self.assertRaises(audiotools.tta.InvalidTTA,
6648                                  audiotools.TrueAudio,
6649                                  invalid_file.name)
6650
6651        # check some decoder errors
6652        self.assertRaises(TypeError, self.decoder)
6653
6654        self.assertRaises(IOError, self.decoder, None)
6655
6656        self.assertRaises(IOError, self.decoder, "filename")
6657
6658    @FORMAT_TTA
6659    def test_verify(self):
6660        from test_core import ints_to_bytes, bytes_to_ints
6661
6662        with open("trueaudio.tta", "rb") as f:
6663            tta_data = f.read()
6664
6665        self.assertTrue(audiotools.open("trueaudio.tta").verify())
6666
6667        # try changing the file underfoot
6668        with tempfile.NamedTemporaryFile(suffix=".tta") as temp:
6669            temp.write(tta_data)
6670            temp.flush()
6671            tta_file = audiotools.open(temp.name)
6672            self.assertTrue(tta_file.verify())
6673
6674            for i in range(0, len(tta_data)):
6675                with open(temp.name, "wb") as f:
6676                    f.write(tta_data[0:i])
6677                self.assertRaises(audiotools.InvalidFile,
6678                                  tta_file.verify)
6679
6680            for i in range(0x2A, len(tta_data)):
6681                for j in range(8):
6682                    new_data = bytes_to_ints(tta_data)
6683                    new_data[i] = new_data[i] ^ (1 << j)
6684                    with open(temp.name, "wb") as f:
6685                        f.write(ints_to_bytes(new_data))
6686                    self.assertRaises(audiotools.InvalidFile,
6687                                      tta_file.verify)
6688
6689        # check a TTA file with a short header
6690        with tempfile.NamedTemporaryFile(suffix=".tta") as temp:
6691            for i in range(0, 18):
6692                temp.seek(0, 0)
6693                temp.write(tta_data[0:i])
6694                temp.flush()
6695                self.assertEqual(os.path.getsize(temp.name), i)
6696                if i < 4:
6697                    with open(temp.name, "rb") as f:
6698                        self.assertIsNone(audiotools.file_type(f))
6699                with open(temp.name, "rb") as f:
6700                    self.assertRaises(IOError,
6701                                      audiotools.decoders.TTADecoder,
6702                                      f)
6703
6704        # check a TTA file that's been truncated
6705        with tempfile.NamedTemporaryFile(suffix=".tta") as temp:
6706            for i in range(30, len(tta_data)):
6707                temp.seek(0, 0)
6708                temp.write(tta_data[0:i])
6709                temp.flush()
6710                self.assertEqual(os.path.getsize(temp.name), i)
6711                decoder = audiotools.open(temp.name).to_pcm()
6712                self.assertNotEqual(decoder, None)
6713                self.assertRaises(IOError,
6714                                  audiotools.transfer_framelist_data,
6715                                  decoder, lambda x: x)
6716
6717                self.assertRaises(audiotools.InvalidFile,
6718                                  audiotools.open(temp.name).verify)
6719
6720        # check a TTA file with a single swapped bit
6721        with tempfile.NamedTemporaryFile(suffix=".tta") as temp:
6722            for i in range(0x30, len(tta_data)):
6723                for j in range(8):
6724                    bytes = bytes_to_ints(tta_data)
6725                    bytes[i] ^= (1 << j)
6726                    temp.seek(0, 0)
6727                    temp.write(ints_to_bytes(bytes))
6728                    temp.flush()
6729                    self.assertEqual(len(tta_data),
6730                                     os.path.getsize(temp.name))
6731
6732                    with audiotools.open(temp.name).to_pcm() as decoders:
6733                        try:
6734                            self.assertRaises(
6735                                ValueError,
6736                                audiotools.transfer_framelist_data,
6737                                decoders, lambda x: x)
6738                        except IOError:
6739                            # Randomly swapping bits may send the decoder
6740                            # off the end of the stream before triggering
6741                            # a CRC-16 error.
6742                            # We simply need to catch that case and continue
6743                            continue
6744
6745    def __stream_variations__(self):
6746        for stream in [
6747            test_streams.Silence8_Mono(200000, 44100),
6748            test_streams.Silence8_Mono(200000, 96000),
6749            test_streams.Silence8_Stereo(200000, 44100),
6750            test_streams.Silence8_Stereo(200000, 96000),
6751            test_streams.Silence16_Mono(200000, 44100),
6752            test_streams.Silence16_Mono(200000, 96000),
6753            test_streams.Silence16_Stereo(200000, 44100),
6754            test_streams.Silence16_Stereo(200000, 96000),
6755            test_streams.Silence24_Mono(200000, 44100),
6756            test_streams.Silence24_Mono(200000, 96000),
6757            test_streams.Silence24_Stereo(200000, 44100),
6758            test_streams.Silence24_Stereo(200000, 96000),
6759
6760            test_streams.Sine8_Mono(200000, 48000, 441.0, 0.50, 441.0, 0.49),
6761            test_streams.Sine8_Mono(200000, 96000, 441.0, 0.61, 661.5, 0.37),
6762            test_streams.Sine8_Mono(200000, 44100, 441.0, 0.50, 882.0, 0.49),
6763            test_streams.Sine8_Mono(200000, 44100, 441.0, 0.50, 4410.0, 0.49),
6764            test_streams.Sine8_Mono(200000, 44100, 8820.0, 0.70, 4410.0, 0.29),
6765
6766            test_streams.Sine8_Stereo(200000, 48000, 441.0, 0.50, 441.0, 0.49, 1.0),
6767            test_streams.Sine8_Stereo(200000, 48000, 441.0, 0.61, 661.5, 0.37, 1.0),
6768            test_streams.Sine8_Stereo(200000, 96000, 441.0, 0.50, 882.0, 0.49, 1.0),
6769            test_streams.Sine8_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.0),
6770            test_streams.Sine8_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 1.0),
6771            test_streams.Sine8_Stereo(200000, 44100, 441.0, 0.50, 441.0, 0.49, 0.5),
6772            test_streams.Sine8_Stereo(200000, 44100, 441.0, 0.61, 661.5, 0.37, 2.0),
6773            test_streams.Sine8_Stereo(200000, 44100, 441.0, 0.50, 882.0, 0.49, 0.7),
6774            test_streams.Sine8_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.3),
6775            test_streams.Sine8_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 0.1),
6776
6777            test_streams.Sine16_Mono(200000, 48000, 441.0, 0.50, 441.0, 0.49),
6778            test_streams.Sine16_Mono(200000, 96000, 441.0, 0.61, 661.5, 0.37),
6779            test_streams.Sine16_Mono(200000, 44100, 441.0, 0.50, 882.0, 0.49),
6780            test_streams.Sine16_Mono(200000, 44100, 441.0, 0.50, 4410.0, 0.49),
6781            test_streams.Sine16_Mono(200000, 44100, 8820.0, 0.70, 4410.0, 0.29),
6782
6783            test_streams.Sine16_Stereo(200000, 48000, 441.0, 0.50, 441.0, 0.49, 1.0),
6784            test_streams.Sine16_Stereo(200000, 48000, 441.0, 0.61, 661.5, 0.37, 1.0),
6785            test_streams.Sine16_Stereo(200000, 96000, 441.0, 0.50, 882.0, 0.49, 1.0),
6786            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.0),
6787            test_streams.Sine16_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 1.0),
6788            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.50, 441.0, 0.49, 0.5),
6789            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.61, 661.5, 0.37, 2.0),
6790            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.50, 882.0, 0.49, 0.7),
6791            test_streams.Sine16_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.3),
6792            test_streams.Sine16_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 0.1),
6793
6794            test_streams.Sine24_Mono(200000, 48000, 441.0, 0.50, 441.0, 0.49),
6795            test_streams.Sine24_Mono(200000, 96000, 441.0, 0.61, 661.5, 0.37),
6796            test_streams.Sine24_Mono(200000, 44100, 441.0, 0.50, 882.0, 0.49),
6797            test_streams.Sine24_Mono(200000, 44100, 441.0, 0.50, 4410.0, 0.49),
6798            test_streams.Sine24_Mono(200000, 44100, 8820.0, 0.70, 4410.0, 0.29),
6799
6800            test_streams.Sine24_Stereo(200000, 48000, 441.0, 0.50, 441.0, 0.49, 1.0),
6801            test_streams.Sine24_Stereo(200000, 48000, 441.0, 0.61, 661.5, 0.37, 1.0),
6802            test_streams.Sine24_Stereo(200000, 96000, 441.0, 0.50, 882.0, 0.49, 1.0),
6803            test_streams.Sine24_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.0),
6804            test_streams.Sine24_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 1.0),
6805            test_streams.Sine24_Stereo(200000, 44100, 441.0, 0.50, 441.0, 0.49, 0.5),
6806            test_streams.Sine24_Stereo(200000, 44100, 441.0, 0.61, 661.5, 0.37, 2.0),
6807            test_streams.Sine24_Stereo(200000, 44100, 441.0, 0.50, 882.0, 0.49, 0.7),
6808            test_streams.Sine24_Stereo(200000, 44100, 441.0, 0.50, 4410.0, 0.49, 1.3),
6809            test_streams.Sine24_Stereo(200000, 44100, 8820.0, 0.70, 4410.0, 0.29, 0.1),
6810
6811            test_streams.Simple_Sine(200000, 44100, 0x7, 8,
6812                                     (25, 10000),
6813                                     (50, 20000),
6814                                     (120, 30000)),
6815            test_streams.Simple_Sine(200000, 44100, 0x33, 8,
6816                                     (25, 10000),
6817                                     (50, 20000),
6818                                     (75, 30000),
6819                                     (65, 40000)),
6820            test_streams.Simple_Sine(200000, 44100, 0x37, 8,
6821                                     (25, 10000),
6822                                     (35, 15000),
6823                                     (45, 20000),
6824                                     (50, 25000),
6825                                     (55, 30000)),
6826            test_streams.Simple_Sine(200000, 44100, 0x3F, 8,
6827                                     (25, 10000),
6828                                     (45, 15000),
6829                                     (65, 20000),
6830                                     (85, 25000),
6831                                     (105, 30000),
6832                                     (120, 35000)),
6833
6834            test_streams.Simple_Sine(200000, 44100, 0x7, 16,
6835                                     (6400, 10000),
6836                                     (12800, 20000),
6837                                     (30720, 30000)),
6838            test_streams.Simple_Sine(200000, 44100, 0x33, 16,
6839                                     (6400, 10000),
6840                                     (12800, 20000),
6841                                     (19200, 30000),
6842                                     (16640, 40000)),
6843            test_streams.Simple_Sine(200000, 44100, 0x37, 16,
6844                                     (6400, 10000),
6845                                     (8960, 15000),
6846                                     (11520, 20000),
6847                                     (12800, 25000),
6848                                     (14080, 30000)),
6849            test_streams.Simple_Sine(200000, 44100, 0x3F, 16,
6850                                     (6400, 10000),
6851                                     (11520, 15000),
6852                                     (16640, 20000),
6853                                     (21760, 25000),
6854                                     (26880, 30000),
6855                                     (30720, 35000)),
6856
6857            test_streams.Simple_Sine(200000, 44100, 0x7, 24,
6858                                     (1638400, 10000),
6859                                     (3276800, 20000),
6860                                     (7864320, 30000)),
6861            test_streams.Simple_Sine(200000, 44100, 0x33, 24,
6862                                     (1638400, 10000),
6863                                     (3276800, 20000),
6864                                     (4915200, 30000),
6865                                     (4259840, 40000)),
6866            test_streams.Simple_Sine(200000, 44100, 0x37, 24,
6867                                     (1638400, 10000),
6868                                     (2293760, 15000),
6869                                     (2949120, 20000),
6870                                     (3276800, 25000),
6871                                     (3604480, 30000)),
6872            test_streams.Simple_Sine(200000, 44100, 0x3F, 24,
6873                                     (1638400, 10000),
6874                                     (2949120, 15000),
6875                                     (4259840, 20000),
6876                                     (5570560, 25000),
6877                                     (6881280, 30000),
6878                                     (7864320, 35000))]:
6879            yield stream
6880
6881    def __test_reader__(self, pcmreader, total_pcm_frames):
6882        if not audiotools.BIN.can_execute(audiotools.BIN["tta"]):
6883            self.assertTrue(
6884                False,
6885                "reference TrueAudio binary tta(1) required for this test")
6886
6887        devnull = open(os.devnull, "wb")
6888        temp_tta_file = tempfile.NamedTemporaryFile(suffix=".tta")
6889        self.encode(temp_tta_file.name, pcmreader)
6890
6891        if (pcmreader.bits_per_sample > 8) and (pcmreader.channels <= 6):
6892            # reference decoder doesn't like 8 bit .wav files?!
6893            # or files with too many channels?
6894
6895            temp_wav_file = tempfile.NamedTemporaryFile(suffix=".wav")
6896            sub = subprocess.Popen([audiotools.BIN["tta"],
6897                                    "-d", temp_tta_file.name,
6898                                    temp_wav_file.name],
6899                                   stdout=devnull,
6900                                   stderr=devnull)
6901            self.assertEqual(sub.wait(), 0,
6902                             "tta decode error on %s" % (repr(pcmreader)))
6903        else:
6904            temp_wav_file = None
6905
6906        for tta in [self.decoder(open(temp_tta_file.name, "rb")),
6907                    self.decoder(Filewrapper(open(temp_tta_file.name, "rb")))]:
6908            self.assertEqual(tta.sample_rate, pcmreader.sample_rate)
6909            self.assertEqual(tta.bits_per_sample, pcmreader.bits_per_sample)
6910            self.assertEqual(tta.channels, pcmreader.channels)
6911
6912            md5sum = md5()
6913            f = tta.read(audiotools.FRAMELIST_SIZE)
6914            while len(f) > 0:
6915                md5sum.update(f.to_bytes(False, True))
6916                f = tta.read(audiotools.FRAMELIST_SIZE)
6917            tta.close()
6918            self.assertEqual(md5sum.digest(), pcmreader.digest())
6919
6920            if temp_wav_file is not None:
6921                wav_md5sum = md5()
6922                audiotools.transfer_framelist_data(
6923                    audiotools.WaveAudio(temp_wav_file.name).to_pcm(),
6924                    wav_md5sum.update)
6925                self.assertEqual(md5sum.digest(), wav_md5sum.digest())
6926
6927        if temp_wav_file is not None:
6928            temp_wav_file.close()
6929
6930        # perform test again with total_pcm_frames indicated
6931        pcmreader.reset()
6932        self.encode(temp_tta_file.name,
6933                    pcmreader,
6934                    total_pcm_frames=total_pcm_frames)
6935
6936        if (pcmreader.bits_per_sample > 8) and (pcmreader.channels <= 6):
6937            # reference decoder doesn't like 8 bit .wav files?!
6938            # or files with too many channels?
6939            temp_wav_file = tempfile.NamedTemporaryFile(suffix=".wav")
6940            sub = subprocess.Popen([audiotools.BIN["tta"],
6941                                    "-d", temp_tta_file.name,
6942                                    temp_wav_file.name],
6943                                   stdout=devnull,
6944                                   stderr=devnull)
6945            self.assertEqual(sub.wait(), 0,
6946                             "tta decode error on %s" % (repr(pcmreader)))
6947        else:
6948            temp_wav_file = None
6949
6950        for tta in [self.decoder(open(temp_tta_file.name, "rb")),
6951                    self.decoder(Filewrapper(open(temp_tta_file.name, "rb")))]:
6952            self.assertEqual(tta.sample_rate, pcmreader.sample_rate)
6953            self.assertEqual(tta.bits_per_sample, pcmreader.bits_per_sample)
6954            self.assertEqual(tta.channels, pcmreader.channels)
6955
6956            md5sum = md5()
6957            f = tta.read(audiotools.FRAMELIST_SIZE)
6958            while len(f) > 0:
6959                md5sum.update(f.to_bytes(False, True))
6960                f = tta.read(audiotools.FRAMELIST_SIZE)
6961            tta.close()
6962            self.assertEqual(md5sum.digest(), pcmreader.digest())
6963            temp_tta_file.close()
6964
6965            if temp_wav_file is not None:
6966                wav_md5sum = md5()
6967                audiotools.transfer_framelist_data(
6968                    audiotools.WaveAudio(temp_wav_file.name).to_pcm(),
6969                    wav_md5sum.update)
6970                self.assertEqual(md5sum.digest(), wav_md5sum.digest())
6971
6972        if temp_wav_file is not None:
6973            temp_wav_file.close()
6974
6975        devnull.close()
6976
6977    @FORMAT_TTA
6978    def test_small_files(self):
6979        for g in [test_streams.Generate01,
6980                  test_streams.Generate02]:
6981            self.__test_reader__(g(44100), 1)
6982        for g in [test_streams.Generate03,
6983                  test_streams.Generate04]:
6984            self.__test_reader__(g(44100), 5)
6985
6986    @FORMAT_TTA
6987    def test_full_scale_deflection(self):
6988        for (bps, fsd) in [(8, test_streams.fsd8),
6989                           (16, test_streams.fsd16),
6990                           (24, test_streams.fsd24)]:
6991            for pattern in [test_streams.PATTERN01,
6992                            test_streams.PATTERN02,
6993                            test_streams.PATTERN03,
6994                            test_streams.PATTERN04,
6995                            test_streams.PATTERN05,
6996                            test_streams.PATTERN06,
6997                            test_streams.PATTERN07]:
6998                self.__test_reader__(
6999                    test_streams.MD5Reader(fsd(pattern, 100)),
7000                    len(pattern) * 100)
7001
7002    @FORMAT_TTA
7003    def test_wasted_bps(self):
7004        self.__test_reader__(test_streams.WastedBPS16(1000), 1000)
7005
7006    @FORMAT_TTA
7007    def test_silence(self):
7008        for (channels, mask) in [
7009            (1, audiotools.ChannelMask.from_channels(1)),
7010            (2, audiotools.ChannelMask.from_channels(2)),
7011            (4, audiotools.ChannelMask.from_fields(front_left=True,
7012                                                   front_right=True,
7013                                                   back_left=True,
7014                                                   back_right=True)),
7015            (8, audiotools.ChannelMask(0))]:
7016            for bps in [8, 16, 24]:
7017                self.__test_reader__(
7018                    MD5_Reader(
7019                        EXACT_SILENCE_PCM_Reader(
7020                            pcm_frames=65536,
7021                            sample_rate=44100,
7022                            channels=channels,
7023                            channel_mask=mask,
7024                            bits_per_sample=bps)),
7025                    65536)
7026
7027    @FORMAT_TTA
7028    def test_noise(self):
7029        for (channels, mask) in [
7030            (1, audiotools.ChannelMask.from_channels(1)),
7031            (2, audiotools.ChannelMask.from_channels(2)),
7032            (4, audiotools.ChannelMask.from_fields(front_left=True,
7033                                                   front_right=True,
7034                                                   back_left=True,
7035                                                   back_right=True)),
7036            (8, audiotools.ChannelMask(0))]:
7037            for bps in [8, 16, 24]:
7038                self.__test_reader__(
7039                    MD5_Reader(
7040                        EXACT_RANDOM_PCM_Reader(
7041                            pcm_frames=65536,
7042                            sample_rate=44100,
7043                            channels=channels,
7044                            channel_mask=mask,
7045                            bits_per_sample=bps)),
7046                    65536)
7047
7048    @FORMAT_TTA
7049    def test_sines(self):
7050        for g in self.__stream_variations__():
7051            self.__test_reader__(g, 200000)
7052
7053    @FORMAT_TTA
7054    def test_multichannel(self):
7055        def __permutations__(executables, options, total):
7056            if total == 0:
7057                yield []
7058            else:
7059                for (executable, option) in zip(executables,
7060                                                options):
7061                    for permutation in __permutations__(executables,
7062                                                        options,
7063                                                        total - 1):
7064                        yield [executable(**option)] + permutation
7065
7066        for (channels, mask) in [(2, 0x3), (3, 0x7), (4, 0x33),
7067                                 (5, 0x3B), (6, 0x3F)]:
7068            for readers in __permutations__(
7069                [EXACT_BLANK_PCM_Reader,
7070                 EXACT_RANDOM_PCM_Reader,
7071                 test_streams.Sine16_Mono],
7072                [{"pcm_frames": 100,
7073                  "sample_rate": 44100,
7074                  "channels": 1,
7075                  "bits_per_sample": 16},
7076                 {"pcm_frames": 100,
7077                  "sample_rate": 44100,
7078                  "channels": 1,
7079                  "bits_per_sample": 16},
7080                 {"pcm_frames": 100,
7081                  "sample_rate": 44100,
7082                  "f1": 441.0,
7083                  "a1": 0.61,
7084                  "f2": 661.5,
7085                  "a2": 0.37}], channels):
7086                    self.__test_reader__(
7087                        MD5_Reader(Join_Reader(readers, mask)),
7088                        100)
7089
7090    @FORMAT_TTA
7091    def test_fractional(self):
7092        for pcm_frames in [46078, 46079, 46080, 46081, 46082]:
7093            self.__test_reader__(
7094                MD5_Reader(
7095                    EXACT_RANDOM_PCM_Reader(
7096                        pcm_frames=pcm_frames,
7097                        sample_rate=44100,
7098                        channels=2,
7099                        bits_per_sample=16)),
7100                pcm_frames)
7101
7102    @FORMAT_TTA
7103    def test_python_codec(self):
7104        def test_python_reader(pcmreader, pcm_frames):
7105            if not audiotools.BIN.can_execute(audiotools.BIN["tta"]):
7106                self.assertTrue(
7107                    False,
7108                    "reference TrueAudio binary tta(1) required for this test")
7109
7110            from audiotools.py_encoders import encode_tta
7111            from audiotools.py_decoders import TTADecoder as TTADecoder1
7112            from audiotools.decoders import TTADecoder as TTADecoder2
7113
7114            devnull = open(os.devnull, "wb")
7115
7116            # encode file using Python-based encoder
7117            temp_tta_file = tempfile.NamedTemporaryFile(suffix=".tta")
7118
7119            self.encode(temp_tta_file.name,
7120                        pcmreader,
7121                        encoding_function=encode_tta)
7122
7123            # verify against output of Python encoder
7124            # against reference tta decoder
7125            if (pcmreader.bits_per_sample > 8) and (pcmreader.channels <= 6):
7126                # reference decoder doesn't like 8 bit .wav files?!
7127                # or files with too many channels?
7128                temp_wav_file = tempfile.NamedTemporaryFile(suffix=".wav")
7129                sub = subprocess.Popen([audiotools.BIN["tta"],
7130                                        "-d", temp_tta_file.name,
7131                                        temp_wav_file.name],
7132                                       stdout=devnull,
7133                                       stderr=devnull)
7134                self.assertEqual(sub.wait(), 0,
7135                                 "tta decode error on %s" % (repr(pcmreader)))
7136
7137                self.assertTrue(
7138                    audiotools.pcm_cmp(
7139                        TTADecoder2(open(temp_tta_file.name, "rb")),
7140                        audiotools.WaveAudio(temp_wav_file.name).to_pcm()))
7141
7142            # verify contents of file decoded by
7143            # Python-based decoder against contents decoded by
7144            # C-based decoder
7145            self.assertTrue(
7146                audiotools.pcm_cmp(
7147                    TTADecoder1(temp_tta_file.name),
7148                    TTADecoder2(open(temp_tta_file.name, "rb"))))
7149
7150            # perform tests again with total_pcm_frames indicated
7151            pcmreader.reset()
7152
7153            self.encode(temp_tta_file.name,
7154                        pcmreader,
7155                        total_pcm_frames=pcm_frames,
7156                        encoding_function=encode_tta)
7157
7158            # verify against output of Python encoder
7159            # against reference tta decoder
7160            if (pcmreader.bits_per_sample > 8) and (pcmreader.channels <= 6):
7161                # reference decoder doesn't like 8 bit .wav files?!
7162                # or files with too many channels?
7163                temp_wav_file = tempfile.NamedTemporaryFile(suffix=".wav")
7164                sub = subprocess.Popen([audiotools.BIN["tta"],
7165                                        "-d", temp_tta_file.name,
7166                                        temp_wav_file.name],
7167                                       stdout=devnull,
7168                                       stderr=devnull)
7169                self.assertEqual(sub.wait(), 0,
7170                                 "tta decode error on %s" % (repr(pcmreader)))
7171
7172                self.assertTrue(
7173                    audiotools.pcm_cmp(
7174                        TTADecoder2(open(temp_tta_file.name, "rb")),
7175                        audiotools.WaveAudio(temp_wav_file.name).to_pcm()))
7176
7177            # verify contents of file decoded by
7178            # Python-based decoder against contents decoded by
7179            # C-based decoder
7180            self.assertTrue(
7181                audiotools.pcm_cmp(
7182                    TTADecoder1(temp_tta_file.name),
7183                    TTADecoder2(open(temp_tta_file.name, "rb"))))
7184
7185            temp_tta_file.close()
7186
7187            devnull.close()
7188
7189        # test small files
7190        for g in [test_streams.Generate01,
7191                  test_streams.Generate02]:
7192            test_python_reader(g(44100), 1)
7193        for g in [test_streams.Generate03,
7194                  test_streams.Generate04]:
7195            test_python_reader(g(44100), 5)
7196
7197        # test full-scale deflection
7198        for (bps, fsd) in [(8, test_streams.fsd8),
7199                           (16, test_streams.fsd16),
7200                           (24, test_streams.fsd24)]:
7201            for pattern in [test_streams.PATTERN01,
7202                            test_streams.PATTERN02,
7203                            test_streams.PATTERN03,
7204                            test_streams.PATTERN04,
7205                            test_streams.PATTERN05,
7206                            test_streams.PATTERN06,
7207                            test_streams.PATTERN07]:
7208                test_python_reader(fsd(pattern, 100), len(pattern) * 100)
7209
7210        # test silence
7211        for g in [test_streams.Silence8_Mono(5000, 48000),
7212                  test_streams.Silence8_Stereo(5000, 48000),
7213                  test_streams.Silence16_Mono(5000, 48000),
7214                  test_streams.Silence16_Stereo(5000, 48000),
7215                  test_streams.Silence24_Mono(5000, 48000),
7216                  test_streams.Silence24_Stereo(5000, 48000)]:
7217            test_python_reader(g, 5000)
7218
7219        # test sines
7220        for g in [test_streams.Sine8_Mono(5000, 48000,
7221                                          441.0, 0.50, 441.0, 0.49),
7222                  test_streams.Sine8_Stereo(5000, 48000,
7223                                            441.0, 0.50, 441.0, 0.49, 1.0),
7224                  test_streams.Sine16_Mono(5000, 48000,
7225                                           441.0, 0.50, 441.0, 0.49),
7226                  test_streams.Sine16_Stereo(5000, 48000,
7227                                             441.0, 0.50, 441.0, 0.49, 1.0),
7228                  test_streams.Sine24_Mono(5000, 48000,
7229                                           441.0, 0.50, 441.0, 0.49),
7230                  test_streams.Sine24_Stereo(5000, 48000,
7231                                             441.0, 0.50, 441.0, 0.49, 1.0),
7232                  test_streams.Simple_Sine(5000, 44100, 0x7, 8,
7233                                           (25, 10000),
7234                                           (50, 20000),
7235                                           (120, 30000)),
7236                  test_streams.Simple_Sine(5000, 44100, 0x33, 8,
7237                                           (25, 10000),
7238                                           (50, 20000),
7239                                           (75, 30000),
7240                                           (65, 40000)),
7241                  test_streams.Simple_Sine(5000, 44100, 0x37, 8,
7242                                           (25, 10000),
7243                                           (35, 15000),
7244                                           (45, 20000),
7245                                           (50, 25000),
7246                                           (55, 30000)),
7247                  test_streams.Simple_Sine(5000, 44100, 0x3F, 8,
7248                                           (25, 10000),
7249                                           (45, 15000),
7250                                           (65, 20000),
7251                                           (85, 25000),
7252                                           (105, 30000),
7253                                           (120, 35000)),
7254                  test_streams.Simple_Sine(5000, 44100, 0x7, 16,
7255                                           (6400, 10000),
7256                                           (12800, 20000),
7257                                           (30720, 30000)),
7258                  test_streams.Simple_Sine(5000, 44100, 0x33, 16,
7259                                           (6400, 10000),
7260                                           (12800, 20000),
7261                                           (19200, 30000),
7262                                           (16640, 40000)),
7263                  test_streams.Simple_Sine(5000, 44100, 0x37, 16,
7264                                           (6400, 10000),
7265                                           (8960, 15000),
7266                                           (11520, 20000),
7267                                           (12800, 25000),
7268                                           (14080, 30000)),
7269                  test_streams.Simple_Sine(5000, 44100, 0x3F, 16,
7270                                           (6400, 10000),
7271                                           (11520, 15000),
7272                                           (16640, 20000),
7273                                           (21760, 25000),
7274                                           (26880, 30000),
7275                                           (30720, 35000)),
7276                  test_streams.Simple_Sine(5000, 44100, 0x7, 24,
7277                                           (1638400, 10000),
7278                                           (3276800, 20000),
7279                                           (7864320, 30000)),
7280                  test_streams.Simple_Sine(5000, 44100, 0x33, 24,
7281                                           (1638400, 10000),
7282                                           (3276800, 20000),
7283                                           (4915200, 30000),
7284                                           (4259840, 40000)),
7285                  test_streams.Simple_Sine(5000, 44100, 0x37, 24,
7286                                           (1638400, 10000),
7287                                           (2293760, 15000),
7288                                           (2949120, 20000),
7289                                           (3276800, 25000),
7290                                           (3604480, 30000)),
7291                  test_streams.Simple_Sine(5000, 44100, 0x3F, 24,
7292                                           (1638400, 10000),
7293                                           (2949120, 15000),
7294                                           (4259840, 20000),
7295                                           (5570560, 25000),
7296                                           (6881280, 30000),
7297                                           (7864320, 35000))]:
7298            test_python_reader(g, 5000)
7299
7300        # test wasted BPS
7301        test_python_reader(test_streams.WastedBPS16(1000), 1000)
7302
7303        # test fractional blocks
7304        for pcm_frames in [46078, 46079, 46080, 46081, 46082]:
7305            test_python_reader(
7306                MD5_Reader(
7307                    EXACT_RANDOM_PCM_Reader(
7308                        pcm_frames=pcm_frames,
7309                        sample_rate=44100,
7310                        channels=2,
7311                        bits_per_sample=16)),
7312                pcm_frames)
7313
7314    @FORMAT_TTA
7315    def test_clean(self):
7316        # check TTA file with double ID3 tags
7317
7318        from audiotools.text import CLEAN_REMOVE_DUPLICATE_ID3V2
7319
7320        original_size = os.path.getsize("tta-id3-2.tta")
7321
7322        track = audiotools.open("tta-id3-2.tta")
7323
7324        # ensure second ID3 tag is ignored
7325        self.assertEqual(track.get_metadata().track_name, u"Title1")
7326
7327        # ensure duplicate ID3v2 tag is detected and removed
7328        fixes = track.clean()
7329        self.assertEqual(fixes,
7330                         [CLEAN_REMOVE_DUPLICATE_ID3V2])
7331        temp = tempfile.NamedTemporaryFile(suffix=".tta")
7332        try:
7333            fixes = track.clean(temp.name)
7334            self.assertEqual(fixes,
7335                             [CLEAN_REMOVE_DUPLICATE_ID3V2])
7336            track2 = audiotools.open(temp.name)
7337            self.assertEqual(track2.get_metadata(), track.get_metadata())
7338            # ensure new file is exactly one tag smaller
7339            # and the padding is preserved in the old tag
7340            self.assertEqual(os.path.getsize(temp.name),
7341                             original_size - 0x46A)
7342        finally:
7343            temp.close()
7344
7345
7346class SineStreamTest(unittest.TestCase):
7347    @FORMAT_SINES
7348    def test_init(self):
7349        from audiotools.decoders import Sine_Mono
7350        from audiotools.decoders import Sine_Stereo
7351        from audiotools.decoders import Sine_Simple
7352
7353        # ensure that failed inits don't make Python explode
7354        self.assertRaises(ValueError, Sine_Mono,
7355                          -1, 4000, 44100, 1.0, 1.0, 1.0, 1.0)
7356        self.assertRaises(ValueError, Sine_Mono,
7357                          16, -1, 44100, 1.0, 1.0, 1.0, 1.0)
7358        self.assertRaises(ValueError, Sine_Mono,
7359                          16, 4000, -1, 1.0, 1.0, 1.0, 1.0)
7360
7361        self.assertRaises(ValueError, Sine_Stereo,
7362                          -1, 4000, 44100, 1.0, 1.0, 1.0, 1.0, 1.0)
7363        self.assertRaises(ValueError, Sine_Stereo,
7364                          16, -1, 44100, 1.0, 1.0, 1.0, 1.0, 1.0)
7365        self.assertRaises(ValueError, Sine_Stereo,
7366                          16, 4000, -1, 1.0, 1.0, 1.0, 1.0, 1.0)
7367
7368        self.assertRaises(ValueError, Sine_Simple,
7369                          -1, 4000, 44100, 100, 100)
7370        self.assertRaises(ValueError, Sine_Simple,
7371                          16, -1, 44100, 100, 100)
7372        self.assertRaises(ValueError, Sine_Simple,
7373                          16, 4000, -1, 100, 100)
7374