blob: 2d897be80014b2b84509f24ddc0e9110c9d84e20 [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 if chunk.chunkname == 'COMM':
395 self._read_comm_chunk(chunk)
396 self._comm_chunk_read = 1
397 elif chunk.chunkname == 'SSND':
398 self._ssnd_chunk = chunk
399 dummy = chunk.read(8)
400 self._ssnd_seek_needed = 0
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000401 elif chunk.chunkname == 'FVER':
402 self._version = _read_long(chunk)
403 elif chunk.chunkname == 'MARK':
404 self._readmark(chunk)
405 elif chunk.chunkname in _skiplist:
406 pass
407 else:
408 raise Error, 'unrecognized chunk type '+chunk.chunkname
Sjoerd Mullender4150ede1993-08-26 14:12:07 +0000409 formlength = formlength - 8 - chunk.chunksize
410 if chunk.chunksize & 1:
411 formlength = formlength - 1
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)
Guido van Rossum9b3bc711993-06-20 21:02:22 +0000566 # Some files appear to contain invalid counts.
567 # Cope with this by testing for EOF.
568 try:
569 for i in range(nmarkers):
570 id = _read_short(chunk)
571 pos = _read_long(chunk)
572 name = _read_string(chunk)
573 self._markers.append((id, pos, name))
574 except EOFError:
575 print 'Warning: MARK chunk contains only',
576 print len(self._markers),
577 if len(self._markers) == 1: print 'marker',
578 else: print 'markers',
579 print 'instead of', nmarkers
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000580
Guido van Rossumd3166071993-05-24 14:16:22 +0000581class Aifc_write:
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000582 # Variables used in this class:
583 #
584 # These variables are user settable through appropriate methods
585 # of this class:
586 # _file -- the open file with methods write(), close(), tell(), seek()
587 # set through the init() or initfp() method
588 # _comptype -- the AIFF-C compression type ('NONE' in AIFF)
589 # set through the setcomptype() or setparams() method
590 # _compname -- the human-readable AIFF-C compression type
591 # set through the setcomptype() or setparams() method
592 # _nchannels -- the number of audio channels
593 # set through the setnchannels() or setparams() method
594 # _sampwidth -- the number of bytes per audio sample
595 # set through the setsampwidth() or setparams() method
596 # _framerate -- the sampling frequency
597 # set through the setframerate() or setparams() method
598 # _nframes -- the number of audio frames written to the header
599 # set through the setnframes() or setparams() method
600 # _aifc -- whether we're writing an AIFF-C file or an AIFF file
601 # set through the aifc() method, reset through the
602 # aiff() method
603 #
604 # These variables are used internally only:
605 # _version -- the AIFF-C version number
606 # _comp -- the compressor from builtin module cl
607 # _nframeswritten -- the number of audio frames actually written
608 # _datalength -- the size of the audio samples written to the header
609 # _datawritten -- the size of the audio samples actually written
610
611 def init(self, filename):
612 self = self.initfp(builtin.open(filename, 'w'))
613 if filename[-5:] == '.aiff':
614 self._aifc = 0
615 else:
616 self._aifc = 1
617 return self
618
619 def initfp(self, file):
620 self._file = file
621 self._version = _AIFC_version
622 self._comptype = 'NONE'
623 self._compname = 'not compressed'
624 self._comp = None
625 self._nchannels = 0
626 self._sampwidth = 0
627 self._framerate = 0
628 self._nframes = 0
629 self._nframeswritten = 0
630 self._datawritten = 0
Guido van Rossum52fc1f61993-06-17 12:38:10 +0000631 self._datalength = 0
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000632 self._markers = []
633 self._marklength = 0
634 self._aifc = 1 # AIFF-C is default
635 return self
636
637 #
638 # User visible methods.
639 #
640 def aiff(self):
641 if self._nframeswritten:
642 raise Error, 'cannot change parameters after starting to write'
643 self._aifc = 0
644
645 def aifc(self):
646 if self._nframeswritten:
647 raise Error, 'cannot change parameters after starting to write'
648 self._aifc = 1
649
650 def setnchannels(self, nchannels):
651 if self._nframeswritten:
652 raise Error, 'cannot change parameters after starting to write'
Sjoerd Mullender4ab6ff81993-02-05 13:43:44 +0000653 dummy = _convert2(nchannels, _nchannelslist)
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000654 self._nchannels = nchannels
655
656 def getnchannels(self):
657 if not self._nchannels:
658 raise Error, 'number of channels not set'
659 return self._nchannels
660
661 def setsampwidth(self, sampwidth):
662 if self._nframeswritten:
663 raise Error, 'cannot change parameters after starting to write'
Sjoerd Mullender3a997271993-02-04 16:43:28 +0000664 dummy = _convert2(sampwidth, _sampwidthlist)
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000665 self._sampwidth = sampwidth
666
667 def getsampwidth(self):
668 if not self._sampwidth:
669 raise Error, 'sample width not set'
670 return self._sampwidth
671
672 def setframerate(self, framerate):
673 if self._nframeswritten:
674 raise Error, 'cannot change parameters after starting to write'
Sjoerd Mullender3a997271993-02-04 16:43:28 +0000675 dummy = _convert2(framerate, _frameratelist)
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000676 self._framerate = framerate
677
678 def getframerate(self):
679 if not self._framerate:
680 raise Error, 'frame rate not set'
681 return self._framerate
682
683 def setnframes(self, nframes):
684 if self._nframeswritten:
685 raise Error, 'cannot change parameters after starting to write'
686 self._nframes = nframes
687
688 def getnframes(self):
689 return self._nframeswritten
690
691 def setcomptype(self, comptype, compname):
692 if self._nframeswritten:
693 raise Error, 'cannot change parameters after starting to write'
694 if comptype not in ('NONE', 'ULAW', 'ALAW'):
695 raise Error, 'unsupported compression type'
696 self._comptype = comptype
697 self._compname = compname
698
699 def getcomptype(self):
700 return self._comptype
701
702 def getcompname(self):
703 return self._compname
704
705## def setversion(self, version):
706## if self._nframeswritten:
707## raise Error, 'cannot change parameters after starting to write'
708## self._version = version
709
710 def setparams(self, (nchannels, sampwidth, framerate, nframes, comptype, compname)):
711 if self._nframeswritten:
712 raise Error, 'cannot change parameters after starting to write'
713 if comptype not in ('NONE', 'ULAW', 'ALAW'):
714 raise Error, 'unsupported compression type'
Sjoerd Mullender3a997271993-02-04 16:43:28 +0000715 dummy = _convert2(nchannels, _nchannelslist)
716 dummy = _convert2(sampwidth, _sampwidthlist)
717 dummy = _convert2(framerate, _frameratelist)
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000718 self._nchannels = nchannels
719 self._sampwidth = sampwidth
720 self._framerate = framerate
721 self._nframes = nframes
722 self._comptype = comptype
723 self._compname = compname
724
725 def getparams(self):
726 if not self._nchannels or not self._sampwidth or not self._framerate:
727 raise Error, 'not all parameters set'
728 return self._nchannels, self._sampwidth, self._framerate, \
729 self._nframes, self._comptype, self._compname
730
Sjoerd Mullender7564a641993-01-22 14:26:28 +0000731 def setmark(self, id, pos, name):
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000732 if id <= 0:
733 raise Error, 'marker ID must be > 0'
734 if pos < 0:
735 raise Error, 'marker position must be >= 0'
736 if type(name) != type(''):
737 raise Error, 'marker name must be a string'
738 for i in range(len(self._markers)):
739 if id == self._markers[i][0]:
740 self._markers[i] = id, pos, name
741 return
742 self._markers.append((id, pos, name))
743
744 def getmark(self, id):
745 for marker in self._markers:
746 if id == marker[0]:
747 return marker
748 raise Error, 'marker ' + `id` + ' does not exist'
749
750 def getmarkers(self):
751 if len(self._markers) == 0:
752 return None
753 return self._markers
754
755 def writeframesraw(self, data):
Guido van Rossum52fc1f61993-06-17 12:38:10 +0000756 self._ensure_header_written(len(data))
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000757 nframes = len(data) / (self._sampwidth * self._nchannels)
758 if self._comp:
Sjoerd Mullender4ab6ff81993-02-05 13:43:44 +0000759 dummy = self._comp.SetParam(CL.FRAME_BUFFER_SIZE, \
760 len(data))
761 dummy = self._comp.SetParam(CL.COMPRESSED_BUFFER_SIZE,\
Sjoerd Mullender3a997271993-02-04 16:43:28 +0000762 len(data))
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000763 data = self._comp.Compress(nframes, data)
764 self._file.write(data)
765 self._nframeswritten = self._nframeswritten + nframes
766 self._datawritten = self._datawritten + len(data)
767
768 def writeframes(self, data):
769 self.writeframesraw(data)
770 if self._nframeswritten != self._nframes or \
771 self._datalength != self._datawritten:
772 self._patchheader()
773
774 def close(self):
Guido van Rossum52fc1f61993-06-17 12:38:10 +0000775 self._ensure_header_written(0)
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000776 if self._datawritten & 1:
777 # quick pad to even size
778 self._file.write(chr(0))
779 self._datawritten = self._datawritten + 1
780 self._writemarkers()
781 if self._nframeswritten != self._nframes or \
782 self._datalength != self._datawritten or \
783 self._marklength:
784 self._patchheader()
785 if self._comp:
786 self._comp.CloseCompressor()
787 self._comp = None
788 self._file.close()
789 self._file = None
790
791 #
792 # Internal methods.
793 #
Guido van Rossum52fc1f61993-06-17 12:38:10 +0000794 def _ensure_header_written(self, datasize):
795 if not self._nframeswritten:
796 if self._comptype in ('ULAW', 'ALAW'):
797 if not self._sampwidth:
798 self._sampwidth = AL.SAMPLE_16
799 if self._sampwidth != AL.SAMPLE_16:
800 raise Error, 'sample width must be 2 when compressing with ULAW or ALAW'
801 if not self._nchannels:
802 raise Error, '# channels not specified'
803 if not self._sampwidth:
804 raise Error, 'sample width not specified'
805 if not self._framerate:
806 raise Error, 'sampling rate not specified'
807 self._write_header(datasize)
808
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000809 def _write_header(self, initlength):
810 if self._aifc and self._comptype != 'NONE':
811 try:
812 import cl, CL
813 except ImportError:
814 raise Error, 'cannot write compressed AIFF-C files'
815 if self._comptype == 'ULAW':
816 scheme = CL.G711_ULAW
817 elif self._comptype == 'ALAW':
818 scheme = CL.G711_ALAW
819 else:
820 raise Error, 'unsupported compression type'
821 self._comp = cl.OpenCompressor(scheme)
822 params = [CL.ORIGINAL_FORMAT, 0, \
823 CL.BITS_PER_COMPONENT, 0, \
Sjoerd Mullender3a997271993-02-04 16:43:28 +0000824 CL.FRAME_RATE, self._framerate, \
825 CL.FRAME_BUFFER_SIZE, 100, \
826 CL.COMPRESSED_BUFFER_SIZE, 100]
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000827 if self._nchannels == AL.MONO:
828 params[1] = CL.MONO
829 else:
830 params[1] = CL.STEREO_INTERLEAVED
831 if self._sampwidth == AL.SAMPLE_8:
832 params[3] = 8
833 elif self._sampwidth == AL.SAMPLE_16:
834 params[3] = 16
835 else:
836 params[3] = 24
837 self._comp.SetParams(params)
Sjoerd Mullender3a997271993-02-04 16:43:28 +0000838 # the compressor produces a header which we ignore
839 dummy = self._comp.Compress(0, '')
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000840 self._file.write('FORM')
841 if not self._nframes:
842 self._nframes = initlength / (self._nchannels * self._sampwidth)
843 self._datalength = self._nframes * self._nchannels * self._sampwidth
844 if self._datalength & 1:
845 self._datalength = self._datalength + 1
846 if self._aifc and self._comptype in ('ULAW', 'ALAW'):
847 self._datalength = self._datalength / 2
848 if self._datalength & 1:
849 self._datalength = self._datalength + 1
850 self._form_length_pos = self._file.tell()
851 commlength = self._write_form_length(self._datalength)
852 if self._aifc:
853 self._file.write('AIFC')
854 self._file.write('FVER')
855 _write_long(self._file, 4)
856 _write_long(self._file, self._version)
857 else:
858 self._file.write('AIFF')
859 self._file.write('COMM')
860 _write_long(self._file, commlength)
Sjoerd Mullender3a997271993-02-04 16:43:28 +0000861 _write_short(self._file, _convert2(self._nchannels, _nchannelslist))
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000862 self._nframes_pos = self._file.tell()
863 _write_long(self._file, self._nframes)
864 _write_short(self._file, _convert2(self._sampwidth, _sampwidthlist))
865 _write_float(self._file, _convert2(self._framerate, _frameratelist))
866 if self._aifc:
867 self._file.write(self._comptype)
868 _write_string(self._file, self._compname)
869 self._file.write('SSND')
870 self._ssnd_length_pos = self._file.tell()
871 _write_long(self._file, self._datalength + 8)
872 _write_long(self._file, 0)
873 _write_long(self._file, 0)
874
875 def _write_form_length(self, datalength):
876 if self._aifc:
877 commlength = 18 + 5 + len(self._compname)
878 if commlength & 1:
879 commlength = commlength + 1
880 verslength = 12
881 else:
882 commlength = 18
883 verslength = 0
884 _write_long(self._file, 4 + verslength + self._marklength + \
885 8 + commlength + 16 + datalength)
886 return commlength
887
888 def _patchheader(self):
889 curpos = self._file.tell()
890 if self._datawritten & 1:
891 datalength = self._datawritten + 1
892 self._file.write(chr(0))
893 else:
894 datalength = self._datawritten
895 if datalength == self._datalength and \
Sjoerd Mullender7564a641993-01-22 14:26:28 +0000896 self._nframes == self._nframeswritten and \
897 self._marklength == 0:
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000898 self._file.seek(curpos, 0)
899 return
900 self._file.seek(self._form_length_pos, 0)
901 dummy = self._write_form_length(datalength)
902 self._file.seek(self._nframes_pos, 0)
903 _write_long(self._file, self._nframeswritten)
904 self._file.seek(self._ssnd_length_pos, 0)
905 _write_long(self._file, datalength + 8)
906 self._file.seek(curpos, 0)
907 self._nframes = self._nframeswritten
908 self._datalength = datalength
909
910 def _writemarkers(self):
911 if len(self._markers) == 0:
912 return
913 self._file.write('MARK')
914 length = 2
915 for marker in self._markers:
916 id, pos, name = marker
917 length = length + len(name) + 1 + 6
918 if len(name) & 1 == 0:
919 length = length + 1
920 _write_long(self._file, length)
921 self._marklength = length + 8
Sjoerd Mullender7564a641993-01-22 14:26:28 +0000922 _write_short(self._file, len(self._markers))
Sjoerd Mullendereeabe7e1993-01-22 12:53:11 +0000923 for marker in self._markers:
924 id, pos, name = marker
925 _write_short(self._file, id)
926 _write_long(self._file, pos)
927 _write_string(self._file, name)
928
929def open(filename, mode):
930 if mode == 'r':
931 return Aifc_read().init(filename)
932 elif mode == 'w':
933 return Aifc_write().init(filename)
934 else:
935 raise Error, 'mode must be \'r\' or \'w\''
936
937def openfp(filep, mode):
938 if mode == 'r':
939 return Aifc_read().initfp(filep)
940 elif mode == 'w':
941 return Aifc_write().initfp(filep)
942 else:
943 raise Error, 'mode must be \'r\' or \'w\''