1 """Stuff to parse Sun and NeXT audio files. 2 3 An audio file consists of a header followed by the data. The structure 4 of the header is as follows. 5 6 +---------------+ 7 | magic word | 8 +---------------+ 9 | header size | 10 +---------------+ 11 | data size | 12 +---------------+ 13 | encoding | 14 +---------------+ 15 | sample rate | 16 +---------------+ 17 | # of channels | 18 +---------------+ 19 | info | 20 | | 21 +---------------+ 22 23 The magic word consists of the 4 characters '.snd'. Apart from the 24 info field, all header fields are 4 bytes in size. They are all 25 32-bit unsigned integers encoded in big-endian byte order. 26 27 The header size really gives the start of the data. 28 The data size is the physical size of the data. From the other 29 parameters the number of frames can be calculated. 30 The encoding gives the way in which audio samples are encoded. 31 Possible values are listed below. 32 The info field currently consists of an ASCII string giving a 33 human-readable description of the audio file. The info field is 34 padded with NUL bytes to the header size. 35 36 Usage. 37 38 Reading audio files: 39 f = sunau.open(file, 'r') 40 where file is either the name of a file or an open file pointer. 41 The open file pointer must have methods read(), seek(), and close(). 42 When the setpos() and rewind() methods are not used, the seek() 43 method is not necessary. 44 45 This returns an instance of a class with the following public methods: 46 getnchannels() -- returns number of audio channels (1 for 47 mono, 2 for stereo) 48 getsampwidth() -- returns sample width in bytes 49 getframerate() -- returns sampling frequency 50 getnframes() -- returns number of audio frames 51 getcomptype() -- returns compression type ('NONE' or 'ULAW') 52 getcompname() -- returns human-readable version of 53 compression type ('not compressed' matches 'NONE') 54 getparams() -- returns a tuple consisting of all of the 55 above in the above order 56 getmarkers() -- returns None (for compatibility with the 57 aifc module) 58 getmark(id) -- raises an error since the mark does not 59 exist (for compatibility with the aifc module) 60 readframes(n) -- returns at most n frames of audio 61 rewind() -- rewind to the beginning of the audio stream 62 setpos(pos) -- seek to the specified position 63 tell() -- return the current position 64 close() -- close the instance (make it unusable) 65 The position returned by tell() and the position given to setpos() 66 are compatible and have nothing to do with the actual position in the 67 file. 68 The close() method is called automatically when the class instance 69 is destroyed. 70 71 Writing audio files: 72 f = sunau.open(file, 'w') 73 where file is either the name of a file or an open file pointer. 74 The open file pointer must have methods write(), tell(), seek(), and 75 close(). 76 77 This returns an instance of a class with the following public methods: 78 setnchannels(n) -- set the number of channels 79 setsampwidth(n) -- set the sample width 80 setframerate(n) -- set the frame rate 81 setnframes(n) -- set the number of frames 82 setcomptype(type, name) 83 -- set the compression type and the 84 human-readable compression type 85 setparams(tuple)-- set all parameters at once 86 tell() -- return current position in output file 87 writeframesraw(data) 88 -- write audio frames without pathing up the 89 file header 90 writeframes(data) 91 -- write audio frames and patch up the file header 92 close() -- patch up the file header and close the 93 output file 94 You should set the parameters before the first writeframesraw or 95 writeframes. The total number of frames does not need to be set, 96 but when it is set to the correct value, the header does not have to 97 be patched up. 98 It is best to first set all parameters, perhaps possibly the 99 compression type, and then write audio frames using writeframesraw. 100 When all frames have been written, either call writeframes('') or 101 close() to patch up the sizes in the header. 102 The close() method is called automatically when the class instance 103 is destroyed. 104 """ 105 106 # from <multimedia/audio_filehdr.h> 107 AUDIO_FILE_MAGIC = 0x2e736e64 108 AUDIO_FILE_ENCODING_MULAW_8 = 1 109 AUDIO_FILE_ENCODING_LINEAR_8 = 2 110 AUDIO_FILE_ENCODING_LINEAR_16 = 3 111 AUDIO_FILE_ENCODING_LINEAR_24 = 4 112 AUDIO_FILE_ENCODING_LINEAR_32 = 5 113 AUDIO_FILE_ENCODING_FLOAT = 6 114 AUDIO_FILE_ENCODING_DOUBLE = 7 115 AUDIO_FILE_ENCODING_ADPCM_G721 = 23 116 AUDIO_FILE_ENCODING_ADPCM_G722 = 24 117 AUDIO_FILE_ENCODING_ADPCM_G723_3 = 25 118 AUDIO_FILE_ENCODING_ADPCM_G723_5 = 26 119 AUDIO_FILE_ENCODING_ALAW_8 = 27 120 121 # from <multimedia/audio_hdr.h> 122 AUDIO_UNKNOWN_SIZE = 0xFFFFFFFFL # ((unsigned)(~0)) 123 124 _simple_encodings = [AUDIO_FILE_ENCODING_MULAW_8, 125 AUDIO_FILE_ENCODING_LINEAR_8, 126 AUDIO_FILE_ENCODING_LINEAR_16, 127 AUDIO_FILE_ENCODING_LINEAR_24, 128 AUDIO_FILE_ENCODING_LINEAR_32, 129 AUDIO_FILE_ENCODING_ALAW_8] 130 131 class Error(Exception): 132 pass 133 134 def _read_u32(file): 135 x = 0L 136 for i in range(4): 137 byte = file.read(1) 138 if byte == '': 139 raise EOFError 140 x = x*256 + ord(byte) 141 return x 142 143 def _write_u32(file, x): 144 data = [] 145 for i in range(4): 146 d, m = divmod(x, 256) 147 data.insert(0, m) 148 x = d 149 for i in range(4): 150 file.write(chr(int(data[i]))) 151 152 class Au_read: 153 154 def __init__(self, f): 155 if type(f) == type(''): 156 import __builtin__ 157 f = __builtin__.open(f, 'rb') 158 self.initfp(f) 159 160 def __del__(self): 161 if self._file: 162 self.close() 163 164 def initfp(self, file): 165 self._file = file 166 self._soundpos = 0 167 magic = int(_read_u32(file)) 168 if magic != AUDIO_FILE_MAGIC: 169 raise Error, 'bad magic number' 170 self._hdr_size = int(_read_u32(file)) 171 if self._hdr_size < 24: 172 raise Error, 'header size too small' 173 if self._hdr_size > 100: 174 raise Error, 'header size ridiculously large' 175 self._data_size = _read_u32(file) 176 if self._data_size != AUDIO_UNKNOWN_SIZE: 177 self._data_size = int(self._data_size) 178 self._encoding = int(_read_u32(file)) 179 if self._encoding not in _simple_encodings: 180 raise Error, 'encoding not (yet) supported' 181 if self._encoding in (AUDIO_FILE_ENCODING_MULAW_8, 182 AUDIO_FILE_ENCODING_ALAW_8): 183 self._sampwidth = 2 184 self._framesize = 1 185 elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_8: 186 self._framesize = self._sampwidth = 1 187 elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_16: 188 self._framesize = self._sampwidth = 2 189 elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_24: 190 self._framesize = self._sampwidth = 3 191 elif self._encoding == AUDIO_FILE_ENCODING_LINEAR_32: 192 self._framesize = self._sampwidth = 4 193 else: 194 raise Error, 'unknown encoding' 195 self._framerate = int(_read_u32(file)) 196 self._nchannels = int(_read_u32(file)) 197 self._framesize = self._framesize * self._nchannels 198 if self._hdr_size > 24: 199 self._info = file.read(self._hdr_size - 24) 200 for i in range(len(self._info)): 201 if self._info[i] == '\0': 202 self._info = self._info[:i] 203 break 204 else: 205 self._info = '' 206 207 def getfp(self): 208 return self._file 209 210 def getnchannels(self): 211 return self._nchannels 212 213 def getsampwidth(self): 214 return self._sampwidth 215 216 def getframerate(self): 217 return self._framerate 218 219 def getnframes(self): 220 if self._data_size == AUDIO_UNKNOWN_SIZE: 221 return AUDIO_UNKNOWN_SIZE 222 if self._encoding in _simple_encodings: 223 return self._data_size / self._framesize 224 return 0 # XXX--must do some arithmetic here 225 226 def getcomptype(self): 227 if self._encoding == AUDIO_FILE_ENCODING_MULAW_8: 228 return 'ULAW' 229 elif self._encoding == AUDIO_FILE_ENCODING_ALAW_8: 230 return 'ALAW' 231 else: 232 return 'NONE' 233 234 def getcompname(self): 235 if self._encoding == AUDIO_FILE_ENCODING_MULAW_8: 236 return 'CCITT G.711 u-law' 237 elif self._encoding == AUDIO_FILE_ENCODING_ALAW_8: 238 return 'CCITT G.711 A-law' 239 else: 240 return 'not compressed' 241 242 def getparams(self): 243 return self.getnchannels(), self.getsampwidth(), \ 244 self.getframerate(), self.getnframes(), \ 245 self.getcomptype(), self.getcompname() 246 247 def getmarkers(self): 248 return None 249 250 def getmark(self, id): 251 raise Error, 'no marks' 252 253 def readframes(self, nframes): 254 if self._encoding in _simple_encodings: 255 if nframes == AUDIO_UNKNOWN_SIZE: 256 data = self._file.read() 257 else: 258 data = self._file.read(nframes * self._framesize * self._nchannels) 259 if self._encoding == AUDIO_FILE_ENCODING_MULAW_8: 260 import audioop 261 data = audioop.ulaw2lin(data, self._sampwidth) 262 return data 263 return None # XXX--not implemented yet 264 265 def rewind(self): 266 self._soundpos = 0 267 self._file.seek(self._hdr_size) 268 269 def tell(self): 270 return self._soundpos 271 272 def setpos(self, pos): 273 if pos < 0 or pos > self.getnframes(): 274 raise Error, 'position not in range' 275 self._file.seek(pos * self._framesize + self._hdr_size) 276 self._soundpos = pos 277 278 def close(self): 279 self._file = None 280 281 class Au_write: 282 283 def __init__(self, f): 284 if type(f) == type(''): 285 import __builtin__ 286 f = __builtin__.open(f, 'wb') 287 self.initfp(f) 288 289 def __del__(self): 290 if self._file: 291 self.close() 292 293 def initfp(self, file): 294 self._file = file 295 self._framerate = 0 296 self._nchannels = 0 297 self._sampwidth = 0 298 self._framesize = 0 299 self._nframes = AUDIO_UNKNOWN_SIZE 300 self._nframeswritten = 0 301 self._datawritten = 0 302 self._datalength = 0 303 self._info = '' 304 self._comptype = 'ULAW' # default is U-law 305 306 def setnchannels(self, nchannels): 307 if self._nframeswritten: 308 raise Error, 'cannot change parameters after starting to write' 309 if nchannels not in (1, 2, 4): 310 raise Error, 'only 1, 2, or 4 channels supported' 311 self._nchannels = nchannels 312 313 def getnchannels(self): 314 if not self._nchannels: 315 raise Error, 'number of channels not set' 316 return self._nchannels 317 318 def setsampwidth(self, sampwidth): 319 if self._nframeswritten: 320 raise Error, 'cannot change parameters after starting to write' 321 if sampwidth not in (1, 2, 4): 322 raise Error, 'bad sample width' 323 self._sampwidth = sampwidth 324 325 def getsampwidth(self): 326 if not self._framerate: 327 raise Error, 'sample width not specified' 328 return self._sampwidth 329 330 def setframerate(self, framerate): 331 if self._nframeswritten: 332 raise Error, 'cannot change parameters after starting to write' 333 self._framerate = framerate 334 335 def getframerate(self): 336 if not self._framerate: 337 raise Error, 'frame rate not set' 338 return self._framerate 339 340 def setnframes(self, nframes): 341 if self._nframeswritten: 342 raise Error, 'cannot change parameters after starting to write' 343 if nframes < 0: 344 raise Error, '# of frames cannot be negative' 345 self._nframes = nframes 346 347 def getnframes(self): 348 return self._nframeswritten 349 350 def setcomptype(self, type, name): 351 if type in ('NONE', 'ULAW'): 352 self._comptype = type 353 else: 354 raise Error, 'unknown compression type' 355 356 def getcomptype(self): 357 return self._comptype 358 359 def getcompname(self): 360 if self._comptype == 'ULAW': 361 return 'CCITT G.711 u-law' 362 elif self._comptype == 'ALAW': 363 return 'CCITT G.711 A-law' 364 else: 365 return 'not compressed' 366 367 def setparams(self, params): 368 nchannels, sampwidth, framerate, nframes, comptype, compname = params 369 self.setnchannels(nchannels) 370 self.setsampwidth(sampwidth) 371 self.setframerate(framerate) 372 self.setnframes(nframes) 373 self.setcomptype(comptype, compname) 374 375 def getparams(self): 376 return self.getnchannels(), self.getsampwidth(), \ 377 self.getframerate(), self.getnframes(), \ 378 self.getcomptype(), self.getcompname() 379 380 def tell(self): 381 return self._nframeswritten 382 383 def writeframesraw(self, data): 384 self._ensure_header_written() 385 nframes = len(data) / self._framesize 386 if self._comptype == 'ULAW': 387 import audioop 388 data = audioop.lin2ulaw(data, self._sampwidth) 389 self._file.write(data) 390 self._nframeswritten = self._nframeswritten + nframes 391 self._datawritten = self._datawritten + len(data) 392 393 def writeframes(self, data): 394 self.writeframesraw(data) 395 if self._nframeswritten != self._nframes or \ 396 self._datalength != self._datawritten: 397 self._patchheader() 398 399 def close(self): 400 self._ensure_header_written() 401 if self._nframeswritten != self._nframes or \ 402 self._datalength != self._datawritten: 403 self._patchheader() 404 self._file.flush() 405 self._file = None 406 407 # 408 # private methods 409 # 410 411 def _ensure_header_written(self): 412 if not self._nframeswritten: 413 if not self._nchannels: 414 raise Error, '# of channels not specified' 415 if not self._sampwidth: 416 raise Error, 'sample width not specified' 417 if not self._framerate: 418 raise Error, 'frame rate not specified' 419 self._write_header() 420 421 def _write_header(self): 422 if self._comptype == 'NONE': 423 if self._sampwidth == 1: 424 encoding = AUDIO_FILE_ENCODING_LINEAR_8 425 self._framesize = 1 426 elif self._sampwidth == 2: 427 encoding = AUDIO_FILE_ENCODING_LINEAR_16 428 self._framesize = 2 429 elif self._sampwidth == 4: 430 encoding = AUDIO_FILE_ENCODING_LINEAR_32 431 self._framesize = 4 432 else: 433 raise Error, 'internal error' 434 elif self._comptype == 'ULAW': 435 encoding = AUDIO_FILE_ENCODING_MULAW_8 436 self._framesize = 1 437 else: 438 raise Error, 'internal error' 439 self._framesize = self._framesize * self._nchannels 440 _write_u32(self._file, AUDIO_FILE_MAGIC) 441 header_size = 25 + len(self._info) 442 header_size = (header_size + 7) & ~7 443 _write_u32(self._file, header_size) 444 if self._nframes == AUDIO_UNKNOWN_SIZE: 445 length = AUDIO_UNKNOWN_SIZE 446 else: 447 length = self._nframes * self._framesize 448 _write_u32(self._file, length) 449 self._datalength = length 450 _write_u32(self._file, encoding) 451 _write_u32(self._file, self._framerate) 452 _write_u32(self._file, self._nchannels) 453 self._file.write(self._info) 454 self._file.write('\0'*(header_size - len(self._info) - 24)) 455 456 def _patchheader(self): 457 self._file.seek(8) 458 _write_u32(self._file, self._datawritten) 459 self._datalength = self._datawritten 460 self._file.seek(0, 2) 461 462 def open(f, mode=None): 463 if mode is None: 464 if hasattr(f, 'mode'): 465 mode = f.mode 466 else: 467 mode = 'rb' 468 if mode in ('r', 'rb'): 469 return Au_read(f) 470 elif mode in ('w', 'wb'): 471 return Au_write(f) 472 else: 473 raise Error, "mode must be 'r', 'rb', 'w', or 'wb'" 474 475 openfp = open 476