Commit | Line | Data |
---|---|---|
920dae64 AT |
1 | """Stuff to parse AIFF-C and AIFF files. |
2 | ||
3 | Unless explicitly stated otherwise, the description below is true | |
4 | both for AIFF-C files and AIFF files. | |
5 | ||
6 | An AIFF-C file has the following structure. | |
7 | ||
8 | +-----------------+ | |
9 | | FORM | | |
10 | +-----------------+ | |
11 | | <size> | | |
12 | +----+------------+ | |
13 | | | AIFC | | |
14 | | +------------+ | |
15 | | | <chunks> | | |
16 | | | . | | |
17 | | | . | | |
18 | | | . | | |
19 | +----+------------+ | |
20 | ||
21 | An AIFF file has the string "AIFF" instead of "AIFC". | |
22 | ||
23 | A chunk consists of an identifier (4 bytes) followed by a size (4 bytes, | |
24 | big endian order), followed by the data. The size field does not include | |
25 | the size of the 8 byte header. | |
26 | ||
27 | The following chunk types are recognized. | |
28 | ||
29 | FVER | |
30 | <version number of AIFF-C defining document> (AIFF-C only). | |
31 | MARK | |
32 | <# of markers> (2 bytes) | |
33 | list of markers: | |
34 | <marker ID> (2 bytes, must be > 0) | |
35 | <position> (4 bytes) | |
36 | <marker name> ("pstring") | |
37 | COMM | |
38 | <# of channels> (2 bytes) | |
39 | <# of sound frames> (4 bytes) | |
40 | <size of the samples> (2 bytes) | |
41 | <sampling frequency> (10 bytes, IEEE 80-bit extended | |
42 | floating point) | |
43 | in AIFF-C files only: | |
44 | <compression type> (4 bytes) | |
45 | <human-readable version of compression type> ("pstring") | |
46 | SSND | |
47 | <offset> (4 bytes, not used by this program) | |
48 | <blocksize> (4 bytes, not used by this program) | |
49 | <sound data> | |
50 | ||
51 | A pstring consists of 1 byte length, a string of characters, and 0 or 1 | |
52 | byte pad to make the total length even. | |
53 | ||
54 | Usage. | |
55 | ||
56 | Reading AIFF files: | |
57 | f = aifc.open(file, 'r') | |
58 | where file is either the name of a file or an open file pointer. | |
59 | The open file pointer must have methods read(), seek(), and close(). | |
60 | In some types of audio files, if the setpos() method is not used, | |
61 | the seek() method is not necessary. | |
62 | ||
63 | This returns an instance of a class with the following public methods: | |
64 | getnchannels() -- returns number of audio channels (1 for | |
65 | mono, 2 for stereo) | |
66 | getsampwidth() -- returns sample width in bytes | |
67 | getframerate() -- returns sampling frequency | |
68 | getnframes() -- returns number of audio frames | |
69 | getcomptype() -- returns compression type ('NONE' for AIFF files) | |
70 | getcompname() -- returns human-readable version of | |
71 | compression type ('not compressed' for AIFF files) | |
72 | getparams() -- returns a tuple consisting of all of the | |
73 | above in the above order | |
74 | getmarkers() -- get the list of marks in the audio file or None | |
75 | if there are no marks | |
76 | getmark(id) -- get mark with the specified id (raises an error | |
77 | if the mark does not exist) | |
78 | readframes(n) -- returns at most n frames of audio | |
79 | rewind() -- rewind to the beginning of the audio stream | |
80 | setpos(pos) -- seek to the specified position | |
81 | tell() -- return the current position | |
82 | close() -- close the instance (make it unusable) | |
83 | The position returned by tell(), the position given to setpos() and | |
84 | the position of marks are all compatible and have nothing to do with | |
85 | the actual position in the file. | |
86 | The close() method is called automatically when the class instance | |
87 | is destroyed. | |
88 | ||
89 | Writing AIFF files: | |
90 | f = aifc.open(file, 'w') | |
91 | where file is either the name of a file or an open file pointer. | |
92 | The open file pointer must have methods write(), tell(), seek(), and | |
93 | close(). | |
94 | ||
95 | This returns an instance of a class with the following public methods: | |
96 | aiff() -- create an AIFF file (AIFF-C default) | |
97 | aifc() -- create an AIFF-C file | |
98 | setnchannels(n) -- set the number of channels | |
99 | setsampwidth(n) -- set the sample width | |
100 | setframerate(n) -- set the frame rate | |
101 | setnframes(n) -- set the number of frames | |
102 | setcomptype(type, name) | |
103 | -- set the compression type and the | |
104 | human-readable compression type | |
105 | setparams(tuple) | |
106 | -- set all parameters at once | |
107 | setmark(id, pos, name) | |
108 | -- add specified mark to the list of marks | |
109 | tell() -- return current position in output file (useful | |
110 | in combination with setmark()) | |
111 | writeframesraw(data) | |
112 | -- write audio frames without pathing up the | |
113 | file header | |
114 | writeframes(data) | |
115 | -- write audio frames and patch up the file header | |
116 | close() -- patch up the file header and close the | |
117 | output file | |
118 | You should set the parameters before the first writeframesraw or | |
119 | writeframes. The total number of frames does not need to be set, | |
120 | but when it is set to the correct value, the header does not have to | |
121 | be patched up. | |
122 | It is best to first set all parameters, perhaps possibly the | |
123 | compression type, and then write audio frames using writeframesraw. | |
124 | When all frames have been written, either call writeframes('') or | |
125 | close() to patch up the sizes in the header. | |
126 | Marks can be added anytime. If there are any marks, ypu must call | |
127 | close() after all frames have been written. | |
128 | The close() method is called automatically when the class instance | |
129 | is destroyed. | |
130 | ||
131 | When a file is opened with the extension '.aiff', an AIFF file is | |
132 | written, otherwise an AIFF-C file is written. This default can be | |
133 | changed by calling aiff() or aifc() before the first writeframes or | |
134 | writeframesraw. | |
135 | """ | |
136 | ||
137 | import struct | |
138 | import __builtin__ | |
139 | ||
140 | __all__ = ["Error","open","openfp"] | |
141 | ||
142 | class Error(Exception): | |
143 | pass | |
144 | ||
145 | _AIFC_version = 0xA2805140L # Version 1 of AIFF-C | |
146 | ||
147 | _skiplist = 'COMT', 'INST', 'MIDI', 'AESD', \ | |
148 | 'APPL', 'NAME', 'AUTH', '(c) ', 'ANNO' | |
149 | ||
150 | def _read_long(file): | |
151 | try: | |
152 | return struct.unpack('>l', file.read(4))[0] | |
153 | except struct.error: | |
154 | raise EOFError | |
155 | ||
156 | def _read_ulong(file): | |
157 | try: | |
158 | return struct.unpack('>L', file.read(4))[0] | |
159 | except struct.error: | |
160 | raise EOFError | |
161 | ||
162 | def _read_short(file): | |
163 | try: | |
164 | return struct.unpack('>h', file.read(2))[0] | |
165 | except struct.error: | |
166 | raise EOFError | |
167 | ||
168 | def _read_string(file): | |
169 | length = ord(file.read(1)) | |
170 | if length == 0: | |
171 | data = '' | |
172 | else: | |
173 | data = file.read(length) | |
174 | if length & 1 == 0: | |
175 | dummy = file.read(1) | |
176 | return data | |
177 | ||
178 | _HUGE_VAL = 1.79769313486231e+308 # See <limits.h> | |
179 | ||
180 | def _read_float(f): # 10 bytes | |
181 | expon = _read_short(f) # 2 bytes | |
182 | sign = 1 | |
183 | if expon < 0: | |
184 | sign = -1 | |
185 | expon = expon + 0x8000 | |
186 | himant = _read_ulong(f) # 4 bytes | |
187 | lomant = _read_ulong(f) # 4 bytes | |
188 | if expon == himant == lomant == 0: | |
189 | f = 0.0 | |
190 | elif expon == 0x7FFF: | |
191 | f = _HUGE_VAL | |
192 | else: | |
193 | expon = expon - 16383 | |
194 | f = (himant * 0x100000000L + lomant) * pow(2.0, expon - 63) | |
195 | return sign * f | |
196 | ||
197 | def _write_short(f, x): | |
198 | f.write(struct.pack('>h', x)) | |
199 | ||
200 | def _write_long(f, x): | |
201 | f.write(struct.pack('>L', x)) | |
202 | ||
203 | def _write_string(f, s): | |
204 | f.write(chr(len(s))) | |
205 | f.write(s) | |
206 | if len(s) & 1 == 0: | |
207 | f.write(chr(0)) | |
208 | ||
209 | def _write_float(f, x): | |
210 | import math | |
211 | if x < 0: | |
212 | sign = 0x8000 | |
213 | x = x * -1 | |
214 | else: | |
215 | sign = 0 | |
216 | if x == 0: | |
217 | expon = 0 | |
218 | himant = 0 | |
219 | lomant = 0 | |
220 | else: | |
221 | fmant, expon = math.frexp(x) | |
222 | if expon > 16384 or fmant >= 1: # Infinity or NaN | |
223 | expon = sign|0x7FFF | |
224 | himant = 0 | |
225 | lomant = 0 | |
226 | else: # Finite | |
227 | expon = expon + 16382 | |
228 | if expon < 0: # denormalized | |
229 | fmant = math.ldexp(fmant, expon) | |
230 | expon = 0 | |
231 | expon = expon | sign | |
232 | fmant = math.ldexp(fmant, 32) | |
233 | fsmant = math.floor(fmant) | |
234 | himant = long(fsmant) | |
235 | fmant = math.ldexp(fmant - fsmant, 32) | |
236 | fsmant = math.floor(fmant) | |
237 | lomant = long(fsmant) | |
238 | _write_short(f, expon) | |
239 | _write_long(f, himant) | |
240 | _write_long(f, lomant) | |
241 | ||
242 | from chunk import Chunk | |
243 | ||
244 | class Aifc_read: | |
245 | # Variables used in this class: | |
246 | # | |
247 | # These variables are available to the user though appropriate | |
248 | # methods of this class: | |
249 | # _file -- the open file with methods read(), close(), and seek() | |
250 | # set through the __init__() method | |
251 | # _nchannels -- the number of audio channels | |
252 | # available through the getnchannels() method | |
253 | # _nframes -- the number of audio frames | |
254 | # available through the getnframes() method | |
255 | # _sampwidth -- the number of bytes per audio sample | |
256 | # available through the getsampwidth() method | |
257 | # _framerate -- the sampling frequency | |
258 | # available through the getframerate() method | |
259 | # _comptype -- the AIFF-C compression type ('NONE' if AIFF) | |
260 | # available through the getcomptype() method | |
261 | # _compname -- the human-readable AIFF-C compression type | |
262 | # available through the getcomptype() method | |
263 | # _markers -- the marks in the audio file | |
264 | # available through the getmarkers() and getmark() | |
265 | # methods | |
266 | # _soundpos -- the position in the audio stream | |
267 | # available through the tell() method, set through the | |
268 | # setpos() method | |
269 | # | |
270 | # These variables are used internally only: | |
271 | # _version -- the AIFF-C version number | |
272 | # _decomp -- the decompressor from builtin module cl | |
273 | # _comm_chunk_read -- 1 iff the COMM chunk has been read | |
274 | # _aifc -- 1 iff reading an AIFF-C file | |
275 | # _ssnd_seek_needed -- 1 iff positioned correctly in audio | |
276 | # file for readframes() | |
277 | # _ssnd_chunk -- instantiation of a chunk class for the SSND chunk | |
278 | # _framesize -- size of one frame in the file | |
279 | ||
280 | def initfp(self, file): | |
281 | self._version = 0 | |
282 | self._decomp = None | |
283 | self._convert = None | |
284 | self._markers = [] | |
285 | self._soundpos = 0 | |
286 | self._file = Chunk(file) | |
287 | if self._file.getname() != 'FORM': | |
288 | raise Error, 'file does not start with FORM id' | |
289 | formdata = self._file.read(4) | |
290 | if formdata == 'AIFF': | |
291 | self._aifc = 0 | |
292 | elif formdata == 'AIFC': | |
293 | self._aifc = 1 | |
294 | else: | |
295 | raise Error, 'not an AIFF or AIFF-C file' | |
296 | self._comm_chunk_read = 0 | |
297 | while 1: | |
298 | self._ssnd_seek_needed = 1 | |
299 | try: | |
300 | chunk = Chunk(self._file) | |
301 | except EOFError: | |
302 | break | |
303 | chunkname = chunk.getname() | |
304 | if chunkname == 'COMM': | |
305 | self._read_comm_chunk(chunk) | |
306 | self._comm_chunk_read = 1 | |
307 | elif chunkname == 'SSND': | |
308 | self._ssnd_chunk = chunk | |
309 | dummy = chunk.read(8) | |
310 | self._ssnd_seek_needed = 0 | |
311 | elif chunkname == 'FVER': | |
312 | self._version = _read_ulong(chunk) | |
313 | elif chunkname == 'MARK': | |
314 | self._readmark(chunk) | |
315 | elif chunkname in _skiplist: | |
316 | pass | |
317 | else: | |
318 | raise Error, 'unrecognized chunk type '+chunk.chunkname | |
319 | chunk.skip() | |
320 | if not self._comm_chunk_read or not self._ssnd_chunk: | |
321 | raise Error, 'COMM chunk and/or SSND chunk missing' | |
322 | if self._aifc and self._decomp: | |
323 | import cl | |
324 | params = [cl.ORIGINAL_FORMAT, 0, | |
325 | cl.BITS_PER_COMPONENT, self._sampwidth * 8, | |
326 | cl.FRAME_RATE, self._framerate] | |
327 | if self._nchannels == 1: | |
328 | params[1] = cl.MONO | |
329 | elif self._nchannels == 2: | |
330 | params[1] = cl.STEREO_INTERLEAVED | |
331 | else: | |
332 | raise Error, 'cannot compress more than 2 channels' | |
333 | self._decomp.SetParams(params) | |
334 | ||
335 | def __init__(self, f): | |
336 | if type(f) == type(''): | |
337 | f = __builtin__.open(f, 'rb') | |
338 | # else, assume it is an open file object already | |
339 | self.initfp(f) | |
340 | ||
341 | # | |
342 | # User visible methods. | |
343 | # | |
344 | def getfp(self): | |
345 | return self._file | |
346 | ||
347 | def rewind(self): | |
348 | self._ssnd_seek_needed = 1 | |
349 | self._soundpos = 0 | |
350 | ||
351 | def close(self): | |
352 | if self._decomp: | |
353 | self._decomp.CloseDecompressor() | |
354 | self._decomp = None | |
355 | self._file = None | |
356 | ||
357 | def tell(self): | |
358 | return self._soundpos | |
359 | ||
360 | def getnchannels(self): | |
361 | return self._nchannels | |
362 | ||
363 | def getnframes(self): | |
364 | return self._nframes | |
365 | ||
366 | def getsampwidth(self): | |
367 | return self._sampwidth | |
368 | ||
369 | def getframerate(self): | |
370 | return self._framerate | |
371 | ||
372 | def getcomptype(self): | |
373 | return self._comptype | |
374 | ||
375 | def getcompname(self): | |
376 | return self._compname | |
377 | ||
378 | ## def getversion(self): | |
379 | ## return self._version | |
380 | ||
381 | def getparams(self): | |
382 | return self.getnchannels(), self.getsampwidth(), \ | |
383 | self.getframerate(), self.getnframes(), \ | |
384 | self.getcomptype(), self.getcompname() | |
385 | ||
386 | def getmarkers(self): | |
387 | if len(self._markers) == 0: | |
388 | return None | |
389 | return self._markers | |
390 | ||
391 | def getmark(self, id): | |
392 | for marker in self._markers: | |
393 | if id == marker[0]: | |
394 | return marker | |
395 | raise Error, 'marker %r does not exist' % (id,) | |
396 | ||
397 | def setpos(self, pos): | |
398 | if pos < 0 or pos > self._nframes: | |
399 | raise Error, 'position not in range' | |
400 | self._soundpos = pos | |
401 | self._ssnd_seek_needed = 1 | |
402 | ||
403 | def readframes(self, nframes): | |
404 | if self._ssnd_seek_needed: | |
405 | self._ssnd_chunk.seek(0) | |
406 | dummy = self._ssnd_chunk.read(8) | |
407 | pos = self._soundpos * self._framesize | |
408 | if pos: | |
409 | self._ssnd_chunk.seek(pos + 8) | |
410 | self._ssnd_seek_needed = 0 | |
411 | if nframes == 0: | |
412 | return '' | |
413 | data = self._ssnd_chunk.read(nframes * self._framesize) | |
414 | if self._convert and data: | |
415 | data = self._convert(data) | |
416 | self._soundpos = self._soundpos + len(data) / (self._nchannels * self._sampwidth) | |
417 | return data | |
418 | ||
419 | # | |
420 | # Internal methods. | |
421 | # | |
422 | ||
423 | def _decomp_data(self, data): | |
424 | import cl | |
425 | dummy = self._decomp.SetParam(cl.FRAME_BUFFER_SIZE, | |
426 | len(data) * 2) | |
427 | return self._decomp.Decompress(len(data) / self._nchannels, | |
428 | data) | |
429 | ||
430 | def _ulaw2lin(self, data): | |
431 | import audioop | |
432 | return audioop.ulaw2lin(data, 2) | |
433 | ||
434 | def _adpcm2lin(self, data): | |
435 | import audioop | |
436 | if not hasattr(self, '_adpcmstate'): | |
437 | # first time | |
438 | self._adpcmstate = None | |
439 | data, self._adpcmstate = audioop.adpcm2lin(data, 2, | |
440 | self._adpcmstate) | |
441 | return data | |
442 | ||
443 | def _read_comm_chunk(self, chunk): | |
444 | self._nchannels = _read_short(chunk) | |
445 | self._nframes = _read_long(chunk) | |
446 | self._sampwidth = (_read_short(chunk) + 7) / 8 | |
447 | self._framerate = int(_read_float(chunk)) | |
448 | self._framesize = self._nchannels * self._sampwidth | |
449 | if self._aifc: | |
450 | #DEBUG: SGI's soundeditor produces a bad size :-( | |
451 | kludge = 0 | |
452 | if chunk.chunksize == 18: | |
453 | kludge = 1 | |
454 | print 'Warning: bad COMM chunk size' | |
455 | chunk.chunksize = 23 | |
456 | #DEBUG end | |
457 | self._comptype = chunk.read(4) | |
458 | #DEBUG start | |
459 | if kludge: | |
460 | length = ord(chunk.file.read(1)) | |
461 | if length & 1 == 0: | |
462 | length = length + 1 | |
463 | chunk.chunksize = chunk.chunksize + length | |
464 | chunk.file.seek(-1, 1) | |
465 | #DEBUG end | |
466 | self._compname = _read_string(chunk) | |
467 | if self._comptype != 'NONE': | |
468 | if self._comptype == 'G722': | |
469 | try: | |
470 | import audioop | |
471 | except ImportError: | |
472 | pass | |
473 | else: | |
474 | self._convert = self._adpcm2lin | |
475 | self._framesize = self._framesize / 4 | |
476 | return | |
477 | # for ULAW and ALAW try Compression Library | |
478 | try: | |
479 | import cl | |
480 | except ImportError: | |
481 | if self._comptype == 'ULAW': | |
482 | try: | |
483 | import audioop | |
484 | self._convert = self._ulaw2lin | |
485 | self._framesize = self._framesize / 2 | |
486 | return | |
487 | except ImportError: | |
488 | pass | |
489 | raise Error, 'cannot read compressed AIFF-C files' | |
490 | if self._comptype == 'ULAW': | |
491 | scheme = cl.G711_ULAW | |
492 | self._framesize = self._framesize / 2 | |
493 | elif self._comptype == 'ALAW': | |
494 | scheme = cl.G711_ALAW | |
495 | self._framesize = self._framesize / 2 | |
496 | else: | |
497 | raise Error, 'unsupported compression type' | |
498 | self._decomp = cl.OpenDecompressor(scheme) | |
499 | self._convert = self._decomp_data | |
500 | else: | |
501 | self._comptype = 'NONE' | |
502 | self._compname = 'not compressed' | |
503 | ||
504 | def _readmark(self, chunk): | |
505 | nmarkers = _read_short(chunk) | |
506 | # Some files appear to contain invalid counts. | |
507 | # Cope with this by testing for EOF. | |
508 | try: | |
509 | for i in range(nmarkers): | |
510 | id = _read_short(chunk) | |
511 | pos = _read_long(chunk) | |
512 | name = _read_string(chunk) | |
513 | if pos or name: | |
514 | # some files appear to have | |
515 | # dummy markers consisting of | |
516 | # a position 0 and name '' | |
517 | self._markers.append((id, pos, name)) | |
518 | except EOFError: | |
519 | print 'Warning: MARK chunk contains only', | |
520 | print len(self._markers), | |
521 | if len(self._markers) == 1: print 'marker', | |
522 | else: print 'markers', | |
523 | print 'instead of', nmarkers | |
524 | ||
525 | class Aifc_write: | |
526 | # Variables used in this class: | |
527 | # | |
528 | # These variables are user settable through appropriate methods | |
529 | # of this class: | |
530 | # _file -- the open file with methods write(), close(), tell(), seek() | |
531 | # set through the __init__() method | |
532 | # _comptype -- the AIFF-C compression type ('NONE' in AIFF) | |
533 | # set through the setcomptype() or setparams() method | |
534 | # _compname -- the human-readable AIFF-C compression type | |
535 | # set through the setcomptype() or setparams() method | |
536 | # _nchannels -- the number of audio channels | |
537 | # set through the setnchannels() or setparams() method | |
538 | # _sampwidth -- the number of bytes per audio sample | |
539 | # set through the setsampwidth() or setparams() method | |
540 | # _framerate -- the sampling frequency | |
541 | # set through the setframerate() or setparams() method | |
542 | # _nframes -- the number of audio frames written to the header | |
543 | # set through the setnframes() or setparams() method | |
544 | # _aifc -- whether we're writing an AIFF-C file or an AIFF file | |
545 | # set through the aifc() method, reset through the | |
546 | # aiff() method | |
547 | # | |
548 | # These variables are used internally only: | |
549 | # _version -- the AIFF-C version number | |
550 | # _comp -- the compressor from builtin module cl | |
551 | # _nframeswritten -- the number of audio frames actually written | |
552 | # _datalength -- the size of the audio samples written to the header | |
553 | # _datawritten -- the size of the audio samples actually written | |
554 | ||
555 | def __init__(self, f): | |
556 | if type(f) == type(''): | |
557 | filename = f | |
558 | f = __builtin__.open(f, 'wb') | |
559 | else: | |
560 | # else, assume it is an open file object already | |
561 | filename = '???' | |
562 | self.initfp(f) | |
563 | if filename[-5:] == '.aiff': | |
564 | self._aifc = 0 | |
565 | else: | |
566 | self._aifc = 1 | |
567 | ||
568 | def initfp(self, file): | |
569 | self._file = file | |
570 | self._version = _AIFC_version | |
571 | self._comptype = 'NONE' | |
572 | self._compname = 'not compressed' | |
573 | self._comp = None | |
574 | self._convert = None | |
575 | self._nchannels = 0 | |
576 | self._sampwidth = 0 | |
577 | self._framerate = 0 | |
578 | self._nframes = 0 | |
579 | self._nframeswritten = 0 | |
580 | self._datawritten = 0 | |
581 | self._datalength = 0 | |
582 | self._markers = [] | |
583 | self._marklength = 0 | |
584 | self._aifc = 1 # AIFF-C is default | |
585 | ||
586 | def __del__(self): | |
587 | if self._file: | |
588 | self.close() | |
589 | ||
590 | # | |
591 | # User visible methods. | |
592 | # | |
593 | def aiff(self): | |
594 | if self._nframeswritten: | |
595 | raise Error, 'cannot change parameters after starting to write' | |
596 | self._aifc = 0 | |
597 | ||
598 | def aifc(self): | |
599 | if self._nframeswritten: | |
600 | raise Error, 'cannot change parameters after starting to write' | |
601 | self._aifc = 1 | |
602 | ||
603 | def setnchannels(self, nchannels): | |
604 | if self._nframeswritten: | |
605 | raise Error, 'cannot change parameters after starting to write' | |
606 | if nchannels < 1: | |
607 | raise Error, 'bad # of channels' | |
608 | self._nchannels = nchannels | |
609 | ||
610 | def getnchannels(self): | |
611 | if not self._nchannels: | |
612 | raise Error, 'number of channels not set' | |
613 | return self._nchannels | |
614 | ||
615 | def setsampwidth(self, sampwidth): | |
616 | if self._nframeswritten: | |
617 | raise Error, 'cannot change parameters after starting to write' | |
618 | if sampwidth < 1 or sampwidth > 4: | |
619 | raise Error, 'bad sample width' | |
620 | self._sampwidth = sampwidth | |
621 | ||
622 | def getsampwidth(self): | |
623 | if not self._sampwidth: | |
624 | raise Error, 'sample width not set' | |
625 | return self._sampwidth | |
626 | ||
627 | def setframerate(self, framerate): | |
628 | if self._nframeswritten: | |
629 | raise Error, 'cannot change parameters after starting to write' | |
630 | if framerate <= 0: | |
631 | raise Error, 'bad frame rate' | |
632 | self._framerate = framerate | |
633 | ||
634 | def getframerate(self): | |
635 | if not self._framerate: | |
636 | raise Error, 'frame rate not set' | |
637 | return self._framerate | |
638 | ||
639 | def setnframes(self, nframes): | |
640 | if self._nframeswritten: | |
641 | raise Error, 'cannot change parameters after starting to write' | |
642 | self._nframes = nframes | |
643 | ||
644 | def getnframes(self): | |
645 | return self._nframeswritten | |
646 | ||
647 | def setcomptype(self, comptype, compname): | |
648 | if self._nframeswritten: | |
649 | raise Error, 'cannot change parameters after starting to write' | |
650 | if comptype not in ('NONE', 'ULAW', 'ALAW', 'G722'): | |
651 | raise Error, 'unsupported compression type' | |
652 | self._comptype = comptype | |
653 | self._compname = compname | |
654 | ||
655 | def getcomptype(self): | |
656 | return self._comptype | |
657 | ||
658 | def getcompname(self): | |
659 | return self._compname | |
660 | ||
661 | ## def setversion(self, version): | |
662 | ## if self._nframeswritten: | |
663 | ## raise Error, 'cannot change parameters after starting to write' | |
664 | ## self._version = version | |
665 | ||
666 | def setparams(self, (nchannels, sampwidth, framerate, nframes, comptype, compname)): | |
667 | if self._nframeswritten: | |
668 | raise Error, 'cannot change parameters after starting to write' | |
669 | if comptype not in ('NONE', 'ULAW', 'ALAW', 'G722'): | |
670 | raise Error, 'unsupported compression type' | |
671 | self.setnchannels(nchannels) | |
672 | self.setsampwidth(sampwidth) | |
673 | self.setframerate(framerate) | |
674 | self.setnframes(nframes) | |
675 | self.setcomptype(comptype, compname) | |
676 | ||
677 | def getparams(self): | |
678 | if not self._nchannels or not self._sampwidth or not self._framerate: | |
679 | raise Error, 'not all parameters set' | |
680 | return self._nchannels, self._sampwidth, self._framerate, \ | |
681 | self._nframes, self._comptype, self._compname | |
682 | ||
683 | def setmark(self, id, pos, name): | |
684 | if id <= 0: | |
685 | raise Error, 'marker ID must be > 0' | |
686 | if pos < 0: | |
687 | raise Error, 'marker position must be >= 0' | |
688 | if type(name) != type(''): | |
689 | raise Error, 'marker name must be a string' | |
690 | for i in range(len(self._markers)): | |
691 | if id == self._markers[i][0]: | |
692 | self._markers[i] = id, pos, name | |
693 | return | |
694 | self._markers.append((id, pos, name)) | |
695 | ||
696 | def getmark(self, id): | |
697 | for marker in self._markers: | |
698 | if id == marker[0]: | |
699 | return marker | |
700 | raise Error, 'marker %r does not exist' % (id,) | |
701 | ||
702 | def getmarkers(self): | |
703 | if len(self._markers) == 0: | |
704 | return None | |
705 | return self._markers | |
706 | ||
707 | def tell(self): | |
708 | return self._nframeswritten | |
709 | ||
710 | def writeframesraw(self, data): | |
711 | self._ensure_header_written(len(data)) | |
712 | nframes = len(data) / (self._sampwidth * self._nchannels) | |
713 | if self._convert: | |
714 | data = self._convert(data) | |
715 | self._file.write(data) | |
716 | self._nframeswritten = self._nframeswritten + nframes | |
717 | self._datawritten = self._datawritten + len(data) | |
718 | ||
719 | def writeframes(self, data): | |
720 | self.writeframesraw(data) | |
721 | if self._nframeswritten != self._nframes or \ | |
722 | self._datalength != self._datawritten: | |
723 | self._patchheader() | |
724 | ||
725 | def close(self): | |
726 | self._ensure_header_written(0) | |
727 | if self._datawritten & 1: | |
728 | # quick pad to even size | |
729 | self._file.write(chr(0)) | |
730 | self._datawritten = self._datawritten + 1 | |
731 | self._writemarkers() | |
732 | if self._nframeswritten != self._nframes or \ | |
733 | self._datalength != self._datawritten or \ | |
734 | self._marklength: | |
735 | self._patchheader() | |
736 | if self._comp: | |
737 | self._comp.CloseCompressor() | |
738 | self._comp = None | |
739 | self._file.flush() | |
740 | self._file = None | |
741 | ||
742 | # | |
743 | # Internal methods. | |
744 | # | |
745 | ||
746 | def _comp_data(self, data): | |
747 | import cl | |
748 | dummy = self._comp.SetParam(cl.FRAME_BUFFER_SIZE, len(data)) | |
749 | dummy = self._comp.SetParam(cl.COMPRESSED_BUFFER_SIZE, len(data)) | |
750 | return self._comp.Compress(self._nframes, data) | |
751 | ||
752 | def _lin2ulaw(self, data): | |
753 | import audioop | |
754 | return audioop.lin2ulaw(data, 2) | |
755 | ||
756 | def _lin2adpcm(self, data): | |
757 | import audioop | |
758 | if not hasattr(self, '_adpcmstate'): | |
759 | self._adpcmstate = None | |
760 | data, self._adpcmstate = audioop.lin2adpcm(data, 2, | |
761 | self._adpcmstate) | |
762 | return data | |
763 | ||
764 | def _ensure_header_written(self, datasize): | |
765 | if not self._nframeswritten: | |
766 | if self._comptype in ('ULAW', 'ALAW'): | |
767 | if not self._sampwidth: | |
768 | self._sampwidth = 2 | |
769 | if self._sampwidth != 2: | |
770 | raise Error, 'sample width must be 2 when compressing with ULAW or ALAW' | |
771 | if self._comptype == 'G722': | |
772 | if not self._sampwidth: | |
773 | self._sampwidth = 2 | |
774 | if self._sampwidth != 2: | |
775 | raise Error, 'sample width must be 2 when compressing with G7.22 (ADPCM)' | |
776 | if not self._nchannels: | |
777 | raise Error, '# channels not specified' | |
778 | if not self._sampwidth: | |
779 | raise Error, 'sample width not specified' | |
780 | if not self._framerate: | |
781 | raise Error, 'sampling rate not specified' | |
782 | self._write_header(datasize) | |
783 | ||
784 | def _init_compression(self): | |
785 | if self._comptype == 'G722': | |
786 | self._convert = self._lin2adpcm | |
787 | return | |
788 | try: | |
789 | import cl | |
790 | except ImportError: | |
791 | if self._comptype == 'ULAW': | |
792 | try: | |
793 | import audioop | |
794 | self._convert = self._lin2ulaw | |
795 | return | |
796 | except ImportError: | |
797 | pass | |
798 | raise Error, 'cannot write compressed AIFF-C files' | |
799 | if self._comptype == 'ULAW': | |
800 | scheme = cl.G711_ULAW | |
801 | elif self._comptype == 'ALAW': | |
802 | scheme = cl.G711_ALAW | |
803 | else: | |
804 | raise Error, 'unsupported compression type' | |
805 | self._comp = cl.OpenCompressor(scheme) | |
806 | params = [cl.ORIGINAL_FORMAT, 0, | |
807 | cl.BITS_PER_COMPONENT, self._sampwidth * 8, | |
808 | cl.FRAME_RATE, self._framerate, | |
809 | cl.FRAME_BUFFER_SIZE, 100, | |
810 | cl.COMPRESSED_BUFFER_SIZE, 100] | |
811 | if self._nchannels == 1: | |
812 | params[1] = cl.MONO | |
813 | elif self._nchannels == 2: | |
814 | params[1] = cl.STEREO_INTERLEAVED | |
815 | else: | |
816 | raise Error, 'cannot compress more than 2 channels' | |
817 | self._comp.SetParams(params) | |
818 | # the compressor produces a header which we ignore | |
819 | dummy = self._comp.Compress(0, '') | |
820 | self._convert = self._comp_data | |
821 | ||
822 | def _write_header(self, initlength): | |
823 | if self._aifc and self._comptype != 'NONE': | |
824 | self._init_compression() | |
825 | self._file.write('FORM') | |
826 | if not self._nframes: | |
827 | self._nframes = initlength / (self._nchannels * self._sampwidth) | |
828 | self._datalength = self._nframes * self._nchannels * self._sampwidth | |
829 | if self._datalength & 1: | |
830 | self._datalength = self._datalength + 1 | |
831 | if self._aifc: | |
832 | if self._comptype in ('ULAW', 'ALAW'): | |
833 | self._datalength = self._datalength / 2 | |
834 | if self._datalength & 1: | |
835 | self._datalength = self._datalength + 1 | |
836 | elif self._comptype == 'G722': | |
837 | self._datalength = (self._datalength + 3) / 4 | |
838 | if self._datalength & 1: | |
839 | self._datalength = self._datalength + 1 | |
840 | self._form_length_pos = self._file.tell() | |
841 | commlength = self._write_form_length(self._datalength) | |
842 | if self._aifc: | |
843 | self._file.write('AIFC') | |
844 | self._file.write('FVER') | |
845 | _write_long(self._file, 4) | |
846 | _write_long(self._file, self._version) | |
847 | else: | |
848 | self._file.write('AIFF') | |
849 | self._file.write('COMM') | |
850 | _write_long(self._file, commlength) | |
851 | _write_short(self._file, self._nchannels) | |
852 | self._nframes_pos = self._file.tell() | |
853 | _write_long(self._file, self._nframes) | |
854 | _write_short(self._file, self._sampwidth * 8) | |
855 | _write_float(self._file, self._framerate) | |
856 | if self._aifc: | |
857 | self._file.write(self._comptype) | |
858 | _write_string(self._file, self._compname) | |
859 | self._file.write('SSND') | |
860 | self._ssnd_length_pos = self._file.tell() | |
861 | _write_long(self._file, self._datalength + 8) | |
862 | _write_long(self._file, 0) | |
863 | _write_long(self._file, 0) | |
864 | ||
865 | def _write_form_length(self, datalength): | |
866 | if self._aifc: | |
867 | commlength = 18 + 5 + len(self._compname) | |
868 | if commlength & 1: | |
869 | commlength = commlength + 1 | |
870 | verslength = 12 | |
871 | else: | |
872 | commlength = 18 | |
873 | verslength = 0 | |
874 | _write_long(self._file, 4 + verslength + self._marklength + \ | |
875 | 8 + commlength + 16 + datalength) | |
876 | return commlength | |
877 | ||
878 | def _patchheader(self): | |
879 | curpos = self._file.tell() | |
880 | if self._datawritten & 1: | |
881 | datalength = self._datawritten + 1 | |
882 | self._file.write(chr(0)) | |
883 | else: | |
884 | datalength = self._datawritten | |
885 | if datalength == self._datalength and \ | |
886 | self._nframes == self._nframeswritten and \ | |
887 | self._marklength == 0: | |
888 | self._file.seek(curpos, 0) | |
889 | return | |
890 | self._file.seek(self._form_length_pos, 0) | |
891 | dummy = self._write_form_length(datalength) | |
892 | self._file.seek(self._nframes_pos, 0) | |
893 | _write_long(self._file, self._nframeswritten) | |
894 | self._file.seek(self._ssnd_length_pos, 0) | |
895 | _write_long(self._file, datalength + 8) | |
896 | self._file.seek(curpos, 0) | |
897 | self._nframes = self._nframeswritten | |
898 | self._datalength = datalength | |
899 | ||
900 | def _writemarkers(self): | |
901 | if len(self._markers) == 0: | |
902 | return | |
903 | self._file.write('MARK') | |
904 | length = 2 | |
905 | for marker in self._markers: | |
906 | id, pos, name = marker | |
907 | length = length + len(name) + 1 + 6 | |
908 | if len(name) & 1 == 0: | |
909 | length = length + 1 | |
910 | _write_long(self._file, length) | |
911 | self._marklength = length + 8 | |
912 | _write_short(self._file, len(self._markers)) | |
913 | for marker in self._markers: | |
914 | id, pos, name = marker | |
915 | _write_short(self._file, id) | |
916 | _write_long(self._file, pos) | |
917 | _write_string(self._file, name) | |
918 | ||
919 | def open(f, mode=None): | |
920 | if mode is None: | |
921 | if hasattr(f, 'mode'): | |
922 | mode = f.mode | |
923 | else: | |
924 | mode = 'rb' | |
925 | if mode in ('r', 'rb'): | |
926 | return Aifc_read(f) | |
927 | elif mode in ('w', 'wb'): | |
928 | return Aifc_write(f) | |
929 | else: | |
930 | raise Error, "mode must be 'r', 'rb', 'w', or 'wb'" | |
931 | ||
932 | openfp = open # B/W compatibility | |
933 | ||
934 | if __name__ == '__main__': | |
935 | import sys | |
936 | if not sys.argv[1:]: | |
937 | sys.argv.append('/usr/demos/data/audio/bach.aiff') | |
938 | fn = sys.argv[1] | |
939 | f = open(fn, 'r') | |
940 | print "Reading", fn | |
941 | print "nchannels =", f.getnchannels() | |
942 | print "nframes =", f.getnframes() | |
943 | print "sampwidth =", f.getsampwidth() | |
944 | print "framerate =", f.getframerate() | |
945 | print "comptype =", f.getcomptype() | |
946 | print "compname =", f.getcompname() | |
947 | if sys.argv[2:]: | |
948 | gn = sys.argv[2] | |
949 | print "Writing", gn | |
950 | g = open(gn, 'w') | |
951 | g.setparams(f.getparams()) | |
952 | while 1: | |
953 | data = f.readframes(1024) | |
954 | if not data: | |
955 | break | |
956 | g.writeframes(data) | |
957 | g.close() | |
958 | f.close() | |
959 | print "Done." |