1# This file is part of audioread.
2# Copyright 2011, Adrian Sampson.
3#
4# Permission is hereby granted, free of charge, to any person obtaining
5# a copy of this software and associated documentation files (the
6# "Software"), to deal in the Software without restriction, including
7# without limitation the rights to use, copy, modify, merge, publish,
8# distribute, sublicense, and/or sell copies of the Software, and to
9# permit persons to whom the Software is furnished to do so, subject to
10# the following conditions:
11#
12# The above copyright notice and this permission notice shall be
13# included in all copies or substantial portions of the Software.
14
15"""Uses standard-library modules to read AIFF, AIFF-C, and WAV files."""
16import wave
17import aifc
18import sunau
19import audioop
20import struct
21import sys
22
23from .exceptions import DecodeError
24
25# Produce two-byte (16-bit) output samples.
26TARGET_WIDTH = 2
27
28# Python 3.4 added support for 24-bit (3-byte) samples.
29if sys.version_info > (3, 4, 0):
30    SUPPORTED_WIDTHS = (1, 2, 3, 4)
31else:
32    SUPPORTED_WIDTHS = (1, 2, 4)
33
34
35class UnsupportedError(DecodeError):
36    """File is not an AIFF, WAV, or Au file."""
37
38
39class BitWidthError(DecodeError):
40    """The file uses an unsupported bit width."""
41
42
43def byteswap(s):
44    """Swaps the endianness of the bytestring s, which must be an array
45    of shorts (16-bit signed integers). This is probably less efficient
46    than it should be.
47    """
48    assert len(s) % 2 == 0
49    parts = []
50    for i in range(0, len(s), 2):
51        chunk = s[i:i + 2]
52        newchunk = struct.pack('<h', *struct.unpack('>h', chunk))
53        parts.append(newchunk)
54    return b''.join(parts)
55
56
57class RawAudioFile(object):
58    """An AIFF, WAV, or Au file that can be read by the Python standard
59    library modules ``wave``, ``aifc``, and ``sunau``.
60    """
61    def __init__(self, filename):
62        self._fh = open(filename, 'rb')
63
64        try:
65            self._file = aifc.open(self._fh)
66        except aifc.Error:
67            # Return to the beginning of the file to try the next reader.
68            self._fh.seek(0)
69        else:
70            self._needs_byteswap = True
71            self._check()
72            return
73
74        try:
75            self._file = wave.open(self._fh)
76        except wave.Error:
77            self._fh.seek(0)
78            pass
79        else:
80            self._needs_byteswap = False
81            self._check()
82            return
83
84        try:
85            self._file = sunau.open(self._fh)
86        except sunau.Error:
87            self._fh.seek(0)
88            pass
89        else:
90            self._needs_byteswap = True
91            self._check()
92            return
93
94        # None of the three libraries could open the file.
95        self._fh.close()
96        raise UnsupportedError()
97
98    def _check(self):
99        """Check that the files' parameters allow us to decode it and
100        raise an error otherwise.
101        """
102        if self._file.getsampwidth() not in SUPPORTED_WIDTHS:
103            self.close()
104            raise BitWidthError()
105
106    def close(self):
107        """Close the underlying file."""
108        self._file.close()
109        self._fh.close()
110
111    @property
112    def channels(self):
113        """Number of audio channels."""
114        return self._file.getnchannels()
115
116    @property
117    def samplerate(self):
118        """Sample rate in Hz."""
119        return self._file.getframerate()
120
121    @property
122    def duration(self):
123        """Length of the audio in seconds (a float)."""
124        return float(self._file.getnframes()) / self.samplerate
125
126    def read_data(self, block_samples=1024):
127        """Generates blocks of PCM data found in the file."""
128        old_width = self._file.getsampwidth()
129
130        while True:
131            data = self._file.readframes(block_samples)
132            if not data:
133                break
134
135            # Make sure we have the desired bitdepth and endianness.
136            data = audioop.lin2lin(data, old_width, TARGET_WIDTH)
137            if self._needs_byteswap and self._file.getcomptype() != 'sowt':
138                # Big-endian data. Swap endianness.
139                data = byteswap(data)
140            yield data
141
142    # Context manager.
143    def __enter__(self):
144        return self
145
146    def __exit__(self, exc_type, exc_val, exc_tb):
147        self.close()
148        return False
149
150    # Iteration.
151    def __iter__(self):
152        return self.read_data()
153