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