blob: 8c04ea30b3e06f5b18cfecd8196caad104a82e2d [file] [log] [blame]
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +00001# 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# if 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# or
59# f = aifc.openfp(filep, 'r')
60# where file is the name of a file and filep is an open file pointer.
61# The open file pointer must have methods read(), seek(), and
62# close(). In some types of audio files, if the setpos() method is
63# not used, the seek() method is not necessary.
64#
65# This returns an instance of a class with the following public methods:
66# getnchannels() -- returns number of audio channels (1 for
67# mono, 2 for stereo)
68# getsampwidth() -- returns sample width in bytes
69# getframerate() -- returns sampling frequency
70# getnframes() -- returns number of audio frames
71# getcomptype() -- returns compression type ('NONE' for AIFF files)
72# getcompname() -- returns human-readable version of
73# compression type ('not compressed' for AIFF files)
74# getparams() -- returns a tuple consisting of all of the
75# above in the above order
76# getmarkers() -- get the list of marks in the audio file or None
77# if there are no marks
78# getmark(id) -- get mark with the specified id (raises an error
79# if the mark does not exist)
80# readframes(n) -- returns at most n frames of audio
81# rewind() -- rewind to the beginning of the audio stream
82# setpos(pos) -- seek to the specified position
83# tell() -- return the current position
Guido van Rossum17ed1ae1993-06-01 13:21:04 +000084# close() -- close the instance (make it unusable)
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +000085# The position returned by tell(), the position given to setpos() and
86# the position of marks are all compatible and have nothing to do with
87# the actual postion in the file.
88#
89# Writing AIFF files:
90# f = aifc.open(file, 'w')
91# or
92# f = aifc.openfp(filep, 'w')
93# where file is the name of a file and filep is an open file pointer.
94# The open file pointer must have methods write(), tell(), seek(), and
95# close().
96#
97# This returns an instance of a class with the following public methods:
98# aiff() -- create an AIFF file (AIFF-C default)
99# aifc() -- create an AIFF-C file
100# setnchannels(n) -- set the number of channels
101# setsampwidth(n) -- set the sample width
102# setframerate(n) -- set the frame rate
103# setnframes(n) -- set the number of frames
104# setcomptype(type, name)
105# -- set the compression type and the
106# human-readable compression type
107# setparams(nchannels, sampwidth, framerate, nframes, comptype, compname)
108# -- set all parameters at once
109# setmark(id, pos, name)
110# -- add specified mark to the list of marks
111# tell() -- return current position in output file (useful
112# in combination with setmark())
113# writeframesraw(data)
114# -- write audio frames without pathing up the
115# file header
116# writeframes(data)
117# -- write audio frames and patch up the file header
118# close() -- patch up the file header and close the
119# output file
120# You should set the parameters before the first writeframesraw or
121# writeframes. The total number of frames does not need to be set,
122# but when it is set to the correct value, the header does not have to
123# be patched up.
124# It is best to first set all parameters, perhaps possibly the
125# compression type, and the write audio frames using writeframesraw.
126# When all frames have been written, either call writeframes('') or
127# close() to patch up the sizes in the header.
128# Marks can be added anytime. If there are any marks, ypu must call
129# close() after all frames have been written.
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
136import builtin
137import AL
138try:
139 import CL
140except ImportError:
141 pass
142
143Error = 'aifc.Error'
144
145_AIFC_version = 0xA2805140 # Version 1 of AIFF-C
146
147_skiplist = 'COMT', 'INST', 'MIDI', 'AESD', \
148 'APPL', 'NAME', 'AUTH', '(c) ', 'ANNO'
149
150_nchannelslist = [(1, AL.MONO), (2, AL.STEREO)]
151_sampwidthlist = [(8, AL.SAMPLE_8), (16, AL.SAMPLE_16), (24, AL.SAMPLE_24)]
152_frameratelist = [(48000, AL.RATE_48000), \
153 (44100, AL.RATE_44100), \
154 (32000, AL.RATE_32000), \
155 (22050, AL.RATE_22050), \
156 (16000, AL.RATE_16000), \
157 (11025, AL.RATE_11025), \
158 ( 8000, AL.RATE_8000)]
159
160def _convert1(value, list):
161 for t in list:
162 if value == t[0]:
163 return t[1]
164 raise Error, 'unknown parameter value'
165
166def _convert2(value, list):
167 for t in list:
168 if value == t[1]:
169 return t[0]
170 raise Error, 'unknown parameter value'
171
172def _read_long(file):
173 x = 0L
174 for i in range(4):
175 byte = file.read(1)
176 if byte == '':
177 raise EOFError
178 x = x*256 + ord(byte)
179 if x >= 0x80000000L:
180 x = x - 0x100000000L
181 return int(x)
182
183def _read_ulong(file):
184 x = 0L
185 for i in range(4):
186 byte = file.read(1)
187 if byte == '':
188 raise EOFError
189 x = x*256 + ord(byte)
190 return x
191
192def _read_short(file):
193 x = 0
194 for i in range(2):
195 byte = file.read(1)
196 if byte == '':
197 raise EOFError
198 x = x*256 + ord(byte)
199 if x >= 0x8000:
200 x = x - 0x10000
201 return x
202
203def _read_string(file):
204 length = ord(file.read(1))
205 data = file.read(length)
206 if length & 1 == 0:
207 dummy = file.read(1)
208 return data
209
210_HUGE_VAL = 1.79769313486231e+308 # See <limits.h>
211
212def _read_float(f): # 10 bytes
213 import math
214 expon = _read_short(f) # 2 bytes
215 sign = 1
216 if expon < 0:
217 sign = -1
218 expon = expon + 0x8000
219 himant = _read_ulong(f) # 4 bytes
220 lomant = _read_ulong(f) # 4 bytes
221 if expon == himant == lomant == 0:
222 f = 0.0
223 elif expon == 0x7FFF:
224 f = _HUGE_VAL
225 else:
226 expon = expon - 16383
227 f = (himant * 0x100000000L + lomant) * pow(2.0, expon - 63)
228 return sign * f
229
230def _write_short(f, x):
231 d, m = divmod(x, 256)
232 f.write(chr(d))
233 f.write(chr(m))
234
235def _write_long(f, x):
236 if x < 0:
237 x = x + 0x100000000L
238 data = []
239 for i in range(4):
240 d, m = divmod(x, 256)
241 data.insert(0, m)
242 x = d
243 for i in range(4):
244 f.write(chr(int(data[i])))
245
246def _write_string(f, s):
247 f.write(chr(len(s)))
248 f.write(s)
249 if len(s) & 1 == 0:
250 f.write(chr(0))
251
252def _write_float(f, x):
253 import math
254 if x < 0:
255 sign = 0x8000
256 x = x * -1
257 else:
258 sign = 0
259 if x == 0:
260 expon = 0
261 himant = 0
262 lomant = 0
263 else:
264 fmant, expon = math.frexp(x)
265 if expon > 16384 or fmant >= 1: # Infinity or NaN
266 expon = sign|0x7FFF
267 himant = 0
268 lomant = 0
269 else: # Finite
270 expon = expon + 16382
271 if expon < 0: # denormalized
272 fmant = math.ldexp(fmant, expon)
273 expon = 0
274 expon = expon | sign
275 fmant = math.ldexp(fmant, 32)
276 fsmant = math.floor(fmant)
277 himant = long(fsmant)
278 fmant = math.ldexp(fmant - fsmant, 32)
279 fsmant = math.floor(fmant)
280 lomant = long(fsmant)
281 _write_short(f, expon)
282 _write_long(f, himant)
283 _write_long(f, lomant)
284
Guido van Rossumd3166071993-05-24 14:16:22 +0000285class Chunk:
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000286 def init(self, file):
287 self.file = file
288 self.chunkname = self.file.read(4)
289 if len(self.chunkname) < 4:
290 raise EOFError
291 self.chunksize = _read_long(self.file)
292 self.size_read = 0
293 self.offset = self.file.tell()
294 return self
295
296 def rewind(self):
297 self.file.seek(self.offset, 0)
298 self.size_read = 0
299
300 def setpos(self, pos):
301 if pos < 0 or pos > self.chunksize:
302 raise RuntimeError
303 self.file.seek(self.offset + pos, 0)
304 self.size_read = pos
305
306 def read(self, length):
307 if self.size_read >= self.chunksize:
308 return ''
309 if length > self.chunksize - self.size_read:
310 length = self.chunksize - self.size_read
311 data = self.file.read(length)
312 self.size_read = self.size_read + len(data)
313 return data
314
315 def skip(self):
316 try:
317 self.file.seek(self.chunksize - self.size_read, 1)
318 except RuntimeError:
319 while self.size_read < self.chunksize:
320 dummy = self.read(8192)
321 if not dummy:
322 raise EOFError
323 if self.chunksize & 1:
324 dummy = self.read(1)
325
Guido van Rossumd3166071993-05-24 14:16:22 +0000326class Aifc_read:
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000327 # Variables used in this class:
328 #
329 # These variables are available to the user though appropriate
330 # methods of this class:
331 # _file -- the open file with methods read(), close(), and seek()
332 # set through the init() ir initfp() method
333 # _nchannels -- the number of audio channels
334 # available through the getnchannels() method
335 # _nframes -- the number of audio frames
336 # available through the getnframes() method
337 # _sampwidth -- the number of bytes per audio sample
338 # available through the getsampwidth() method
339 # _framerate -- the sampling frequency
340 # available through the getframerate() method
341 # _comptype -- the AIFF-C compression type ('NONE' if AIFF)
342 # available through the getcomptype() method
343 # _compname -- the human-readable AIFF-C compression type
344 # available through the getcomptype() method
345 # _markers -- the marks in the audio file
346 # available through the getmarkers() and getmark()
347 # methods
348 # _soundpos -- the position in the audio stream
349 # available through the tell() method, set through the
350 # tell() method
351 #
352 # These variables are used internally only:
353 # _version -- the AIFF-C version number
354 # _decomp -- the decompressor from builtin module cl
355 # _comm_chunk_read -- 1 iff the COMM chunk has been read
356 # _aifc -- 1 iff reading an AIFF-C file
357 # _ssnd_seek_needed -- 1 iff positioned correctly in audio
358 # file for readframes()
359 # _ssnd_chunk -- instantiation of a chunk class for the SSND chunk
Sjoerd Mullender3a997271993-02-04 16:43:28 +0000360 # _framesize -- size of one frame in the file
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000361 def initfp(self, file):
362 self._file = file
363 self._version = 0
364 self._decomp = None
365 self._markers = []
366 self._soundpos = 0
367 form = self._file.read(4)
368 if form != 'FORM':
369 raise Error, 'file does not start with FORM id'
370 formlength = _read_long(self._file)
371 if formlength <= 0:
372 raise Error, 'invalid FORM chunk data size'
373 formdata = self._file.read(4)
374 formlength = formlength - 4
375 if formdata == 'AIFF':
376 self._aifc = 0
377 elif formdata == 'AIFC':
378 self._aifc = 1
379 else:
380 raise Error, 'not an AIFF or AIFF-C file'
381 self._comm_chunk_read = 0
Sjoerd Mullender7564a641993-01-22 14:26:28 +0000382 while formlength > 0:
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000383 self._ssnd_seek_needed = 1
Sjoerd Mullender8d733a01993-01-29 12:01:00 +0000384 #DEBUG: SGI's soundfiler has a bug. There should
385 # be no need to check for EOF here.
386 try:
387 chunk = Chunk().init(self._file)
388 except EOFError:
389 if formlength == 8:
390 print 'Warning: FORM chunk size too large'
391 formlength = 0
392 break
393 raise EOFError # different error, raise exception
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000394 formlength = formlength - 8 - chunk.chunksize
395 if chunk.chunksize & 1:
396 formlength = formlength - 1
397 if chunk.chunkname == 'COMM':
398 self._read_comm_chunk(chunk)
399 self._comm_chunk_read = 1
400 elif chunk.chunkname == 'SSND':
401 self._ssnd_chunk = chunk
402 dummy = chunk.read(8)
403 self._ssnd_seek_needed = 0
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000404 elif chunk.chunkname == 'FVER':
405 self._version = _read_long(chunk)
406 elif chunk.chunkname == 'MARK':
407 self._readmark(chunk)
408 elif chunk.chunkname in _skiplist:
409 pass
410 else:
411 raise Error, 'unrecognized chunk type '+chunk.chunkname
Sjoerd Mullender93f07401993-01-26 09:24:37 +0000412 if formlength > 0:
413 chunk.skip()
Sjoerd Mullender7564a641993-01-22 14:26:28 +0000414 if not self._comm_chunk_read or not self._ssnd_chunk:
415 raise Error, 'COMM chunk and/or SSND chunk missing'
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000416 if self._aifc and self._decomp:
417 params = [CL.ORIGINAL_FORMAT, 0, \
418 CL.BITS_PER_COMPONENT, 0, \
419 CL.FRAME_RATE, self._framerate]
420 if self._nchannels == AL.MONO:
421 params[1] = CL.MONO
422 else:
423 params[1] = CL.STEREO_INTERLEAVED
424 if self._sampwidth == AL.SAMPLE_8:
425 params[3] = 8
426 elif self._sampwidth == AL.SAMPLE_16:
427 params[3] = 16
428 else:
429 params[3] = 24
430 self._decomp.SetParams(params)
431 return self
432
433 def init(self, filename):
434 return self.initfp(builtin.open(filename, 'r'))
435
436 #
437 # User visible methods.
438 #
439 def getfp(self):
440 return self._file
441
442 def rewind(self):
443 self._ssnd_seek_needed = 1
444 self._soundpos = 0
445
446 def close(self):
447 if self._decomp:
448 self._decomp.CloseDecompressor()
449 self._decomp = None
450 self._file.close()
451 self._file = None
452
453 def tell(self):
454 return self._soundpos
455
456 def getnchannels(self):
457 return self._nchannels
458
459 def getnframes(self):
460 return self._nframes
461
462 def getsampwidth(self):
463 return self._sampwidth
464
465 def getframerate(self):
466 return self._framerate
467
468 def getcomptype(self):
469 return self._comptype
470
471 def getcompname(self):
472 return self._compname
473
474## def getversion(self):
475## return self._version
476
477 def getparams(self):
478 return self._nchannels, self._sampwidth, self._framerate, \
479 self._nframes, self._comptype, self._compname
480
481 def getmarkers(self):
482 if len(self._markers) == 0:
483 return None
484 return self._markers
485
486 def getmark(self, id):
487 for marker in self._markers:
488 if id == marker[0]:
489 return marker
490 raise Error, 'marker ' + `id` + ' does not exist'
491
492 def setpos(self, pos):
493 if pos < 0 or pos > self._nframes:
494 raise Error, 'position not in range'
495 self._soundpos = pos
496 self._ssnd_seek_needed = 1
497
498 def readframes(self, nframes):
499 if self._ssnd_seek_needed:
500 self._ssnd_chunk.rewind()
501 dummy = self._ssnd_chunk.read(8)
Sjoerd Mullender3a997271993-02-04 16:43:28 +0000502 pos = self._soundpos * self._framesize
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000503 if pos:
504 self._ssnd_chunk.setpos(pos + 8)
505 self._ssnd_seek_needed = 0
Sjoerd Mullender8d733a01993-01-29 12:01:00 +0000506 if nframes == 0:
507 return ''
Sjoerd Mullender3a997271993-02-04 16:43:28 +0000508 data = self._ssnd_chunk.read(nframes * self._framesize)
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000509 if self._decomp and data:
Sjoerd Mullender4ab6ff81993-02-05 13:43:44 +0000510 dummy = self._decomp.SetParam(CL.FRAME_BUFFER_SIZE, \
511 len(data) * 2)
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000512 data = self._decomp.Decompress(len(data) / self._nchannels, data)
513 self._soundpos = self._soundpos + len(data) / (self._nchannels * self._sampwidth)
514 return data
515
516 #
517 # Internal methods.
518 #
519 def _read_comm_chunk(self, chunk):
520 nchannels = _read_short(chunk)
521 self._nchannels = _convert1(nchannels, _nchannelslist)
522 self._nframes = _read_long(chunk)
523 sampwidth = _read_short(chunk)
524 self._sampwidth = _convert1(sampwidth, _sampwidthlist)
525 framerate = _read_float(chunk)
526 self._framerate = _convert1(framerate, _frameratelist)
Sjoerd Mullender3a997271993-02-04 16:43:28 +0000527 self._framesize = self._nchannels * self._sampwidth
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000528 if self._aifc:
529 #DEBUG: SGI's soundeditor produces a bad size :-(
530 kludge = 0
531 if chunk.chunksize == 18:
532 kludge = 1
533 print 'Warning: bad COMM chunk size'
534 chunk.chunksize = 23
535 #DEBUG end
536 self._comptype = chunk.read(4)
537 #DEBUG start
538 if kludge:
539 length = ord(chunk.file.read(1))
540 if length & 1 == 0:
541 length = length + 1
542 chunk.chunksize = chunk.chunksize + length
543 chunk.file.seek(-1, 1)
544 #DEBUG end
545 self._compname = _read_string(chunk)
546 if self._comptype != 'NONE':
547 try:
548 import cl, CL
549 except ImportError:
550 raise Error, 'cannot read compressed AIFF-C files'
551 if self._comptype == 'ULAW':
552 scheme = CL.G711_ULAW
Sjoerd Mullender3a997271993-02-04 16:43:28 +0000553 self._framesize = self._framesize / 2
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000554 elif self._comptype == 'ALAW':
555 scheme = CL.G711_ALAW
Sjoerd Mullender3a997271993-02-04 16:43:28 +0000556 self._framesize = self._framesize / 2
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000557 else:
558 raise Error, 'unsupported compression type'
559 self._decomp = cl.OpenDecompressor(scheme)
560 else:
561 self._comptype = 'NONE'
562 self._compname = 'not compressed'
563
564 def _readmark(self, chunk):
565 nmarkers = _read_short(chunk)
566 for i in range(nmarkers):
567 id = _read_short(chunk)
568 pos = _read_long(chunk)
569 name = _read_string(chunk)
570 self._markers.append((id, pos, name))
571
Guido van Rossumd3166071993-05-24 14:16:22 +0000572class Aifc_write:
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000573 # Variables used in this class:
574 #
575 # These variables are user settable through appropriate methods
576 # of this class:
577 # _file -- the open file with methods write(), close(), tell(), seek()
578 # set through the init() or initfp() method
579 # _comptype -- the AIFF-C compression type ('NONE' in AIFF)
580 # set through the setcomptype() or setparams() method
581 # _compname -- the human-readable AIFF-C compression type
582 # set through the setcomptype() or setparams() method
583 # _nchannels -- the number of audio channels
584 # set through the setnchannels() or setparams() method
585 # _sampwidth -- the number of bytes per audio sample
586 # set through the setsampwidth() or setparams() method
587 # _framerate -- the sampling frequency
588 # set through the setframerate() or setparams() method
589 # _nframes -- the number of audio frames written to the header
590 # set through the setnframes() or setparams() method
591 # _aifc -- whether we're writing an AIFF-C file or an AIFF file
592 # set through the aifc() method, reset through the
593 # aiff() method
594 #
595 # These variables are used internally only:
596 # _version -- the AIFF-C version number
597 # _comp -- the compressor from builtin module cl
598 # _nframeswritten -- the number of audio frames actually written
599 # _datalength -- the size of the audio samples written to the header
600 # _datawritten -- the size of the audio samples actually written
601
602 def init(self, filename):
603 self = self.initfp(builtin.open(filename, 'w'))
604 if filename[-5:] == '.aiff':
605 self._aifc = 0
606 else:
607 self._aifc = 1
608 return self
609
610 def initfp(self, file):
611 self._file = file
612 self._version = _AIFC_version
613 self._comptype = 'NONE'
614 self._compname = 'not compressed'
615 self._comp = None
616 self._nchannels = 0
617 self._sampwidth = 0
618 self._framerate = 0
619 self._nframes = 0
620 self._nframeswritten = 0
621 self._datawritten = 0
Guido van Rossum52fc1f61993-06-17 12:38:10 +0000622 self._datalength = 0
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000623 self._markers = []
624 self._marklength = 0
625 self._aifc = 1 # AIFF-C is default
626 return self
627
628 #
629 # User visible methods.
630 #
631 def aiff(self):
632 if self._nframeswritten:
633 raise Error, 'cannot change parameters after starting to write'
634 self._aifc = 0
635
636 def aifc(self):
637 if self._nframeswritten:
638 raise Error, 'cannot change parameters after starting to write'
639 self._aifc = 1
640
641 def setnchannels(self, nchannels):
642 if self._nframeswritten:
643 raise Error, 'cannot change parameters after starting to write'
Sjoerd Mullender4ab6ff81993-02-05 13:43:44 +0000644 dummy = _convert2(nchannels, _nchannelslist)
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000645 self._nchannels = nchannels
646
647 def getnchannels(self):
648 if not self._nchannels:
649 raise Error, 'number of channels not set'
650 return self._nchannels
651
652 def setsampwidth(self, sampwidth):
653 if self._nframeswritten:
654 raise Error, 'cannot change parameters after starting to write'
Sjoerd Mullender3a997271993-02-04 16:43:28 +0000655 dummy = _convert2(sampwidth, _sampwidthlist)
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000656 self._sampwidth = sampwidth
657
658 def getsampwidth(self):
659 if not self._sampwidth:
660 raise Error, 'sample width not set'
661 return self._sampwidth
662
663 def setframerate(self, framerate):
664 if self._nframeswritten:
665 raise Error, 'cannot change parameters after starting to write'
Sjoerd Mullender3a997271993-02-04 16:43:28 +0000666 dummy = _convert2(framerate, _frameratelist)
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000667 self._framerate = framerate
668
669 def getframerate(self):
670 if not self._framerate:
671 raise Error, 'frame rate not set'
672 return self._framerate
673
674 def setnframes(self, nframes):
675 if self._nframeswritten:
676 raise Error, 'cannot change parameters after starting to write'
677 self._nframes = nframes
678
679 def getnframes(self):
680 return self._nframeswritten
681
682 def setcomptype(self, comptype, compname):
683 if self._nframeswritten:
684 raise Error, 'cannot change parameters after starting to write'
685 if comptype not in ('NONE', 'ULAW', 'ALAW'):
686 raise Error, 'unsupported compression type'
687 self._comptype = comptype
688 self._compname = compname
689
690 def getcomptype(self):
691 return self._comptype
692
693 def getcompname(self):
694 return self._compname
695
696## def setversion(self, version):
697## if self._nframeswritten:
698## raise Error, 'cannot change parameters after starting to write'
699## self._version = version
700
701 def setparams(self, (nchannels, sampwidth, framerate, nframes, comptype, compname)):
702 if self._nframeswritten:
703 raise Error, 'cannot change parameters after starting to write'
704 if comptype not in ('NONE', 'ULAW', 'ALAW'):
705 raise Error, 'unsupported compression type'
Sjoerd Mullender3a997271993-02-04 16:43:28 +0000706 dummy = _convert2(nchannels, _nchannelslist)
707 dummy = _convert2(sampwidth, _sampwidthlist)
708 dummy = _convert2(framerate, _frameratelist)
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000709 self._nchannels = nchannels
710 self._sampwidth = sampwidth
711 self._framerate = framerate
712 self._nframes = nframes
713 self._comptype = comptype
714 self._compname = compname
715
716 def getparams(self):
717 if not self._nchannels or not self._sampwidth or not self._framerate:
718 raise Error, 'not all parameters set'
719 return self._nchannels, self._sampwidth, self._framerate, \
720 self._nframes, self._comptype, self._compname
721
Sjoerd Mullender7564a641993-01-22 14:26:28 +0000722 def setmark(self, id, pos, name):
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000723 if id <= 0:
724 raise Error, 'marker ID must be > 0'
725 if pos < 0:
726 raise Error, 'marker position must be >= 0'
727 if type(name) != type(''):
728 raise Error, 'marker name must be a string'
729 for i in range(len(self._markers)):
730 if id == self._markers[i][0]:
731 self._markers[i] = id, pos, name
732 return
733 self._markers.append((id, pos, name))
734
735 def getmark(self, id):
736 for marker in self._markers:
737 if id == marker[0]:
738 return marker
739 raise Error, 'marker ' + `id` + ' does not exist'
740
741 def getmarkers(self):
742 if len(self._markers) == 0:
743 return None
744 return self._markers
745
746 def writeframesraw(self, data):
Guido van Rossum52fc1f61993-06-17 12:38:10 +0000747 self._ensure_header_written(len(data))
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000748 nframes = len(data) / (self._sampwidth * self._nchannels)
749 if self._comp:
Sjoerd Mullender4ab6ff81993-02-05 13:43:44 +0000750 dummy = self._comp.SetParam(CL.FRAME_BUFFER_SIZE, \
751 len(data))
752 dummy = self._comp.SetParam(CL.COMPRESSED_BUFFER_SIZE,\
Sjoerd Mullender3a997271993-02-04 16:43:28 +0000753 len(data))
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000754 data = self._comp.Compress(nframes, data)
755 self._file.write(data)
756 self._nframeswritten = self._nframeswritten + nframes
757 self._datawritten = self._datawritten + len(data)
758
759 def writeframes(self, data):
760 self.writeframesraw(data)
761 if self._nframeswritten != self._nframes or \
762 self._datalength != self._datawritten:
763 self._patchheader()
764
765 def close(self):
Guido van Rossum52fc1f61993-06-17 12:38:10 +0000766 self._ensure_header_written(0)
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000767 if self._datawritten & 1:
768 # quick pad to even size
769 self._file.write(chr(0))
770 self._datawritten = self._datawritten + 1
771 self._writemarkers()
772 if self._nframeswritten != self._nframes or \
773 self._datalength != self._datawritten or \
774 self._marklength:
775 self._patchheader()
776 if self._comp:
777 self._comp.CloseCompressor()
778 self._comp = None
779 self._file.close()
780 self._file = None
781
782 #
783 # Internal methods.
784 #
Guido van Rossum52fc1f61993-06-17 12:38:10 +0000785 def _ensure_header_written(self, datasize):
786 if not self._nframeswritten:
787 if self._comptype in ('ULAW', 'ALAW'):
788 if not self._sampwidth:
789 self._sampwidth = AL.SAMPLE_16
790 if self._sampwidth != AL.SAMPLE_16:
791 raise Error, 'sample width must be 2 when compressing with ULAW or ALAW'
792 if not self._nchannels:
793 raise Error, '# channels not specified'
794 if not self._sampwidth:
795 raise Error, 'sample width not specified'
796 if not self._framerate:
797 raise Error, 'sampling rate not specified'
798 self._write_header(datasize)
799
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000800 def _write_header(self, initlength):
801 if self._aifc and self._comptype != 'NONE':
802 try:
803 import cl, CL
804 except ImportError:
805 raise Error, 'cannot write compressed AIFF-C files'
806 if self._comptype == 'ULAW':
807 scheme = CL.G711_ULAW
808 elif self._comptype == 'ALAW':
809 scheme = CL.G711_ALAW
810 else:
811 raise Error, 'unsupported compression type'
812 self._comp = cl.OpenCompressor(scheme)
813 params = [CL.ORIGINAL_FORMAT, 0, \
814 CL.BITS_PER_COMPONENT, 0, \
Sjoerd Mullender3a997271993-02-04 16:43:28 +0000815 CL.FRAME_RATE, self._framerate, \
816 CL.FRAME_BUFFER_SIZE, 100, \
817 CL.COMPRESSED_BUFFER_SIZE, 100]
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000818 if self._nchannels == AL.MONO:
819 params[1] = CL.MONO
820 else:
821 params[1] = CL.STEREO_INTERLEAVED
822 if self._sampwidth == AL.SAMPLE_8:
823 params[3] = 8
824 elif self._sampwidth == AL.SAMPLE_16:
825 params[3] = 16
826 else:
827 params[3] = 24
828 self._comp.SetParams(params)
Sjoerd Mullender3a997271993-02-04 16:43:28 +0000829 # the compressor produces a header which we ignore
830 dummy = self._comp.Compress(0, '')
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000831 self._file.write('FORM')
832 if not self._nframes:
833 self._nframes = initlength / (self._nchannels * self._sampwidth)
834 self._datalength = self._nframes * self._nchannels * self._sampwidth
835 if self._datalength & 1:
836 self._datalength = self._datalength + 1
837 if self._aifc and self._comptype in ('ULAW', 'ALAW'):
838 self._datalength = self._datalength / 2
839 if self._datalength & 1:
840 self._datalength = self._datalength + 1
841 self._form_length_pos = self._file.tell()
842 commlength = self._write_form_length(self._datalength)
843 if self._aifc:
844 self._file.write('AIFC')
845 self._file.write('FVER')
846 _write_long(self._file, 4)
847 _write_long(self._file, self._version)
848 else:
849 self._file.write('AIFF')
850 self._file.write('COMM')
851 _write_long(self._file, commlength)
Sjoerd Mullender3a997271993-02-04 16:43:28 +0000852 _write_short(self._file, _convert2(self._nchannels, _nchannelslist))
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000853 self._nframes_pos = self._file.tell()
854 _write_long(self._file, self._nframes)
855 _write_short(self._file, _convert2(self._sampwidth, _sampwidthlist))
856 _write_float(self._file, _convert2(self._framerate, _frameratelist))
857 if self._aifc:
858 self._file.write(self._comptype)
859 _write_string(self._file, self._compname)
860 self._file.write('SSND')
861 self._ssnd_length_pos = self._file.tell()
862 _write_long(self._file, self._datalength + 8)
863 _write_long(self._file, 0)
864 _write_long(self._file, 0)
865
866 def _write_form_length(self, datalength):
867 if self._aifc:
868 commlength = 18 + 5 + len(self._compname)
869 if commlength & 1:
870 commlength = commlength + 1
871 verslength = 12
872 else:
873 commlength = 18
874 verslength = 0
875 _write_long(self._file, 4 + verslength + self._marklength + \
876 8 + commlength + 16 + datalength)
877 return commlength
878
879 def _patchheader(self):
880 curpos = self._file.tell()
881 if self._datawritten & 1:
882 datalength = self._datawritten + 1
883 self._file.write(chr(0))
884 else:
885 datalength = self._datawritten
886 if datalength == self._datalength and \
Sjoerd Mullender7564a641993-01-22 14:26:28 +0000887 self._nframes == self._nframeswritten and \
888 self._marklength == 0:
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000889 self._file.seek(curpos, 0)
890 return
891 self._file.seek(self._form_length_pos, 0)
892 dummy = self._write_form_length(datalength)
893 self._file.seek(self._nframes_pos, 0)
894 _write_long(self._file, self._nframeswritten)
895 self._file.seek(self._ssnd_length_pos, 0)
896 _write_long(self._file, datalength + 8)
897 self._file.seek(curpos, 0)
898 self._nframes = self._nframeswritten
899 self._datalength = datalength
900
901 def _writemarkers(self):
902 if len(self._markers) == 0:
903 return
904 self._file.write('MARK')
905 length = 2
906 for marker in self._markers:
907 id, pos, name = marker
908 length = length + len(name) + 1 + 6
909 if len(name) & 1 == 0:
910 length = length + 1
911 _write_long(self._file, length)
912 self._marklength = length + 8
Sjoerd Mullender7564a641993-01-22 14:26:28 +0000913 _write_short(self._file, len(self._markers))
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000914 for marker in self._markers:
915 id, pos, name = marker
916 _write_short(self._file, id)
917 _write_long(self._file, pos)
918 _write_string(self._file, name)
919
920def open(filename, mode):
921 if mode == 'r':
922 return Aifc_read().init(filename)
923 elif mode == 'w':
924 return Aifc_write().init(filename)
925 else:
926 raise Error, 'mode must be \'r\' or \'w\''
927
928def openfp(filep, mode):
929 if mode == 'r':
930 return Aifc_read().initfp(filep)
931 elif mode == 'w':
932 return Aifc_write().initfp(filep)
933 else:
934 raise Error, 'mode must be \'r\' or \'w\''