1 # FLAC comment support for Mutagen
2 # Copyright 2005 Joe Wreschnig
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of version 2 of the GNU General Public License as
6 # published by the Free Software Foundation.
8 """Read and write FLAC Vorbis comments and stream information.
10 Read more about FLAC at http://flac.sourceforge.net.
12 FLAC supports arbitrary metadata blocks. The two most interesting ones
13 are the FLAC stream information block, and the Vorbis comment block;
14 these are also the only ones Mutagen can currently read.
16 This module does not handle Ogg FLAC files.
18 Based off documentation available at
19 http://flac.sourceforge.net/format.html
22 __all__
= ["FLAC", "Open", "delete"]
25 from cStringIO
import StringIO
26 from _vorbis
import VCommentDict
27 from mutagen
import FileType
28 from mutagen
._util
import insert_bytes
29 from mutagen
.id3
import BitPaddedInt
31 if sys
.version_info
>= (2, 6):
32 from functools
import reduce
34 class error(IOError): pass
35 class FLACNoHeaderError(error
): pass
36 class FLACVorbisError(ValueError, error
): pass
38 def to_int_be(string
):
39 """Convert an arbitrarily-long string to a long using big-endian
41 return reduce(lambda a
, b
: (a
<< 8) + ord(b
), string
, 0L)
43 class MetadataBlock(object):
44 """A generic block of FLAC metadata.
46 This class is extended by specific used as an ancestor for more specific
47 blocks, and also as a container for data blobs of unknown blocks.
50 data -- raw binary data for this block
53 def __init__(self
, data
):
54 """Parse the given data string or file-like as a metadata block.
55 The metadata header should not be included."""
57 if isinstance(data
, str): data
= StringIO(data
)
58 elif not hasattr(data
, 'read'):
60 "StreamInfo requires string data or a file-like")
63 def load(self
, data
): self
.data
= data
.read()
64 def write(self
): return self
.data
66 def writeblocks(blocks
):
67 """Render metadata block as a byte string."""
69 codes
= [[block
.code
, block
.write()] for block
in blocks
]
71 for code
, datum
in codes
:
73 if len(datum
) > 2**24:
74 raise error("block is too long to write")
75 length
= struct
.pack(">I", len(datum
))[-3:]
76 data
.append(byte
+ length
+ datum
)
78 writeblocks
= staticmethod(writeblocks
)
80 def group_padding(blocks
):
81 """Consolidate FLAC padding metadata blocks.
83 The overall size of the rendered blocks does not change, so
84 this adds several bytes of padding for each merged block."""
85 paddings
= filter(lambda x
: isinstance(x
, Padding
), blocks
)
86 map(blocks
.remove
, paddings
)
88 # total padding size is the sum of padding sizes plus 4 bytes
90 size
= sum([padding
.length
for padding
in paddings
])
91 padding
.length
= size
+ 4 * (len(paddings
) - 1)
92 blocks
.append(padding
)
93 group_padding
= staticmethod(group_padding
)
95 class StreamInfo(MetadataBlock
):
96 """FLAC stream information.
98 This contains information about the audio data in the FLAC file.
99 Unlike most stream information objects in Mutagen, changes to this
100 one will rewritten to the file when it is saved. Unless you are
101 actually changing the audio stream itself, don't change any
102 attributes of this block.
105 min_blocksize -- minimum audio block size
106 max_blocksize -- maximum audio block size
107 sample_rate -- audio sample rate in Hz
108 channels -- audio channels (1 for mono, 2 for stereo)
109 bits_per_sample -- bits per sample
110 total_samples -- total samples in file
111 length -- audio length in seconds
116 def __eq__(self
, other
):
117 try: return (self
.min_blocksize
== other
.min_blocksize
and
118 self
.max_blocksize
== other
.max_blocksize
and
119 self
.sample_rate
== other
.sample_rate
and
120 self
.channels
== other
.channels
and
121 self
.bits_per_sample
== other
.bits_per_sample
and
122 self
.total_samples
== other
.total_samples
)
124 __hash__
= MetadataBlock
.__hash
__
126 def load(self
, data
):
127 self
.min_blocksize
= int(to_int_be(data
.read(2)))
128 self
.max_blocksize
= int(to_int_be(data
.read(2)))
129 self
.min_framesize
= int(to_int_be(data
.read(3)))
130 self
.max_framesize
= int(to_int_be(data
.read(3)))
131 # first 16 bits of sample rate
132 sample_first
= to_int_be(data
.read(2))
133 # last 4 bits of sample rate, 3 of channels, first 1 of bits/sample
134 sample_channels_bps
= to_int_be(data
.read(1))
135 # last 4 of bits/sample, 36 of total samples
136 bps_total
= to_int_be(data
.read(5))
138 sample_tail
= sample_channels_bps
>> 4
139 self
.sample_rate
= int((sample_first
<< 4) + sample_tail
)
140 self
.channels
= int(((sample_channels_bps
>> 1) & 7) + 1)
141 bps_tail
= bps_total
>> 36
142 bps_head
= (sample_channels_bps
& 1) << 4
143 self
.bits_per_sample
= int(bps_head
+ bps_tail
+ 1)
144 self
.total_samples
= bps_total
& 0xFFFFFFFFFL
145 self
.length
= self
.total_samples
/ float(self
.sample_rate
)
147 self
.md5_signature
= to_int_be(data
.read(16))
151 f
.write(struct
.pack(">I", self
.min_blocksize
)[-2:])
152 f
.write(struct
.pack(">I", self
.max_blocksize
)[-2:])
153 f
.write(struct
.pack(">I", self
.min_framesize
)[-3:])
154 f
.write(struct
.pack(">I", self
.max_framesize
)[-3:])
156 # first 16 bits of sample rate
157 f
.write(struct
.pack(">I", self
.sample_rate
>> 4)[-2:])
158 # 4 bits sample, 3 channel, 1 bps
159 byte
= (self
.sample_rate
& 0xF) << 4
160 byte
+= ((self
.channels
- 1) & 7) << 1
161 byte
+= ((self
.bits_per_sample
- 1) >> 4) & 1
163 # 4 bits of bps, 4 of sample count
164 byte
= ((self
.bits_per_sample
- 1) & 0xF) << 4
165 byte
+= (self
.total_samples
>> 32) & 0xF
167 # last 32 of sample count
168 f
.write(struct
.pack(">I", self
.total_samples
& 0xFFFFFFFFL
))
170 sig
= self
.md5_signature
172 ">4I", (sig
>> 96) & 0xFFFFFFFFL
, (sig
>> 64) & 0xFFFFFFFFL
,
173 (sig
>> 32) & 0xFFFFFFFFL
, sig
& 0xFFFFFFFFL
))
177 return "FLAC, %.2f seconds, %d Hz" % (self
.length
, self
.sample_rate
)
179 class SeekPoint(tuple):
180 """A single seek point in a FLAC file.
182 Placeholder seek points have first_sample of 0xFFFFFFFFFFFFFFFFL,
183 and byte_offset and num_samples undefined. Seek points must be
184 sorted in ascending order by first_sample number. Seek points must
185 be unique by first_sample number, except for placeholder
186 points. Placeholder points must occur last in the table and there
187 may be any number of them.
190 first_sample -- sample number of first sample in the target frame
191 byte_offset -- offset from first frame to target frame
192 num_samples -- number of samples in target frame
195 def __new__(cls
, first_sample
, byte_offset
, num_samples
):
196 return super(cls
, SeekPoint
).__new
__(cls
, (first_sample
,
197 byte_offset
, num_samples
))
198 first_sample
= property(lambda self
: self
[0])
199 byte_offset
= property(lambda self
: self
[1])
200 num_samples
= property(lambda self
: self
[2])
202 class SeekTable(MetadataBlock
):
203 """Read and write FLAC seek tables.
206 seekpoints -- list of SeekPoint objects
209 __SEEKPOINT_FORMAT
= '>QQH'
210 __SEEKPOINT_SIZE
= struct
.calcsize(__SEEKPOINT_FORMAT
)
214 def __init__(self
, data
):
216 super(SeekTable
, self
).__init
__(data
)
218 def __eq__(self
, other
):
219 try: return (self
.seekpoints
== other
.seekpoints
)
220 except (AttributeError, TypeError): return False
221 __hash__
= MetadataBlock
.__hash
__
223 def load(self
, data
):
225 sp
= data
.read(self
.__SEEKPOINT
_SIZE
)
226 while len(sp
) == self
.__SEEKPOINT
_SIZE
:
227 self
.seekpoints
.append(SeekPoint(
228 *struct
.unpack(self
.__SEEKPOINT
_FORMAT
, sp
)))
229 sp
= data
.read(self
.__SEEKPOINT
_SIZE
)
233 for seekpoint
in self
.seekpoints
:
234 packed
= struct
.pack(self
.__SEEKPOINT
_FORMAT
,
235 seekpoint
.first_sample
, seekpoint
.byte_offset
,
236 seekpoint
.num_samples
)
241 return "<%s seekpoints=%r>" % (type(self
).__name
__, self
.seekpoints
)
243 class VCFLACDict(VCommentDict
):
244 """Read and write FLAC Vorbis comments.
246 FLACs don't use the framing bit at the end of the comment block.
247 So this extends VCommentDict to not use the framing bit.
252 def load(self
, data
, errors
='replace', framing
=False):
253 super(VCFLACDict
, self
).load(data
, errors
=errors
, framing
=framing
)
255 def write(self
, framing
=False):
256 return super(VCFLACDict
, self
).write(framing
=framing
)
258 class CueSheetTrackIndex(tuple):
259 """Index for a track in a cuesheet.
261 For CD-DA, an index_number of 0 corresponds to the track
262 pre-gap. The first index in a track must have a number of 0 or 1,
263 and subsequently, index_numbers must increase by 1. Index_numbers
264 must be unique within a track. And index_offset must be evenly
265 divisible by 588 samples.
268 index_number -- index point number
269 index_offset -- offset in samples from track start
272 def __new__(cls
, index_number
, index_offset
):
273 return super(cls
, CueSheetTrackIndex
).__new
__(cls
,
274 (index_number
, index_offset
))
275 index_number
= property(lambda self
: self
[0])
276 index_offset
= property(lambda self
: self
[1])
278 class CueSheetTrack(object):
279 """A track in a cuesheet.
281 For CD-DA, track_numbers must be 1-99, or 170 for the
282 lead-out. Track_numbers must be unique within a cue sheet. There
283 must be atleast one index in every track except the lead-out track
284 which must have none.
287 track_number -- track number
288 start_offset -- track offset in samples from start of FLAC stream
290 type -- 0 for audio, 1 for digital data
291 pre_emphasis -- true if the track is recorded with pre-emphasis
292 indexes -- list of CueSheetTrackIndex objects
295 def __init__(self
, track_number
, start_offset
, isrc
='', type_
=0,
297 self
.track_number
= track_number
298 self
.start_offset
= start_offset
301 self
.pre_emphasis
= pre_emphasis
304 def __eq__(self
, other
):
305 try: return (self
.track_number
== other
.track_number
and
306 self
.start_offset
== other
.start_offset
and
307 self
.isrc
== other
.isrc
and
308 self
.type == other
.type and
309 self
.pre_emphasis
== other
.pre_emphasis
and
310 self
.indexes
== other
.indexes
)
311 except (AttributeError, TypeError): return False
312 __hash__
= object.__hash
__
315 return ("<%s number=%r, offset=%d, isrc=%r, type=%r, "
316 "pre_emphasis=%r, indexes=%r)>") % (
317 type(self
).__name
__, self
.track_number
, self
.start_offset
,
318 self
.isrc
, self
.type, self
.pre_emphasis
, self
.indexes
)
320 class CueSheet(MetadataBlock
):
321 """Read and write FLAC embedded cue sheets.
323 Number of tracks should be from 1 to 100. There should always be
324 exactly one lead-out track and that track must be the last track
328 media_catalog_number -- media catalog number in ASCII
329 lead_in_samples -- number of lead-in samples
330 compact_disc -- true if the cuesheet corresponds to a compact disc
331 tracks -- list of CueSheetTrack objects
332 lead_out -- lead-out as CueSheetTrack or None if lead-out was not found
335 __CUESHEET_FORMAT
= '>128sQB258xB'
336 __CUESHEET_SIZE
= struct
.calcsize(__CUESHEET_FORMAT
)
337 __CUESHEET_TRACK_FORMAT
= '>QB12sB13xB'
338 __CUESHEET_TRACK_SIZE
= struct
.calcsize(__CUESHEET_TRACK_FORMAT
)
339 __CUESHEET_TRACKINDEX_FORMAT
= '>QB3x'
340 __CUESHEET_TRACKINDEX_SIZE
= struct
.calcsize(__CUESHEET_TRACKINDEX_FORMAT
)
344 media_catalog_number
= ''
345 lead_in_samples
= 88200
348 def __init__(self
, data
):
350 super(CueSheet
, self
).__init
__(data
)
352 def __eq__(self
, other
):
354 return (self
.media_catalog_number
== other
.media_catalog_number
and
355 self
.lead_in_samples
== other
.lead_in_samples
and
356 self
.compact_disc
== other
.compact_disc
and
357 self
.tracks
== other
.tracks
)
358 except (AttributeError, TypeError): return False
359 __hash__
= MetadataBlock
.__hash
__
361 def load(self
, data
):
362 header
= data
.read(self
.__CUESHEET
_SIZE
)
363 media_catalog_number
, lead_in_samples
, flags
, num_tracks
= \
364 struct
.unpack(self
.__CUESHEET
_FORMAT
, header
)
365 self
.media_catalog_number
= media_catalog_number
.rstrip('\0')
366 self
.lead_in_samples
= lead_in_samples
367 self
.compact_disc
= bool(flags
& 0x80)
369 for i
in range(num_tracks
):
370 track
= data
.read(self
.__CUESHEET
_TRACK
_SIZE
)
371 start_offset
, track_number
, isrc_padded
, flags
, num_indexes
= \
372 struct
.unpack(self
.__CUESHEET
_TRACK
_FORMAT
, track
)
373 isrc
= isrc_padded
.rstrip('\0')
374 type_
= (flags
& 0x80) >> 7
375 pre_emphasis
= bool(flags
& 0x40)
377 track_number
, start_offset
, isrc
, type_
, pre_emphasis
)
378 for j
in range(num_indexes
):
379 index
= data
.read(self
.__CUESHEET
_TRACKINDEX
_SIZE
)
380 index_offset
, index_number
= struct
.unpack(
381 self
.__CUESHEET
_TRACKINDEX
_FORMAT
, index
)
383 CueSheetTrackIndex(index_number
, index_offset
))
384 self
.tracks
.append(val
)
389 if self
.compact_disc
: flags |
= 0x80
390 packed
= struct
.pack(
391 self
.__CUESHEET
_FORMAT
, self
.media_catalog_number
,
392 self
.lead_in_samples
, flags
, len(self
.tracks
))
394 for track
in self
.tracks
:
396 track_flags |
= (track
.type & 1) << 7
397 if track
.pre_emphasis
: track_flags |
= 0x40
398 track_packed
= struct
.pack(
399 self
.__CUESHEET
_TRACK
_FORMAT
, track
.start_offset
,
400 track
.track_number
, track
.isrc
, track_flags
,
402 f
.write(track_packed
)
403 for index
in track
.indexes
:
404 index_packed
= struct
.pack(
405 self
.__CUESHEET
_TRACKINDEX
_FORMAT
,
406 index
.index_offset
, index
.index_number
)
407 f
.write(index_packed
)
411 return ("<%s media_catalog_number=%r, lead_in=%r, compact_disc=%r, "
413 type(self
).__name
__, self
.media_catalog_number
,
414 self
.lead_in_samples
, self
.compact_disc
, self
.tracks
)
416 class Picture(MetadataBlock
):
417 """Read and write FLAC embed pictures.
420 type -- picture type (same as types for ID3 APIC frames)
421 mime -- MIME type of the picture
422 desc -- picture's description
423 width -- width in pixels
424 height -- height in pixels
425 depth -- color depth in bits-per-pixel
426 colors -- number of colors for indexed palettes (like GIF),
433 def __init__(self
, data
=None):
442 super(Picture
, self
).__init
__(data
)
444 def __eq__(self
, other
):
445 try: return (self
.type == other
.type and
446 self
.mime
== other
.mime
and
447 self
.desc
== other
.desc
and
448 self
.width
== other
.width
and
449 self
.height
== other
.height
and
450 self
.depth
== other
.depth
and
451 self
.colors
== other
.colors
and
452 self
.data
== other
.data
)
453 except (AttributeError, TypeError): return False
454 __hash__
= MetadataBlock
.__hash
__
456 def load(self
, data
):
457 self
.type, length
= struct
.unpack('>2I', data
.read(8))
458 self
.mime
= data
.read(length
).decode('UTF-8', 'replace')
459 length
, = struct
.unpack('>I', data
.read(4))
460 self
.desc
= data
.read(length
).decode('UTF-8', 'replace')
461 (self
.width
, self
.height
, self
.depth
,
462 self
.colors
, length
) = struct
.unpack('>5I', data
.read(20))
463 self
.data
= data
.read(length
)
467 mime
= self
.mime
.encode('UTF-8')
468 f
.write(struct
.pack('>2I', self
.type, len(mime
)))
470 desc
= self
.desc
.encode('UTF-8')
471 f
.write(struct
.pack('>I', len(desc
)))
473 f
.write(struct
.pack('>5I', self
.width
, self
.height
, self
.depth
,
474 self
.colors
, len(self
.data
)))
479 return "<%s '%s' (%d bytes)>" % (type(self
).__name
__, self
.mime
,
482 class Padding(MetadataBlock
):
483 """Empty padding space for metadata blocks.
485 To avoid rewriting the entire FLAC file when editing comments,
486 metadata is often padded. Padding should occur at the end, and no
487 more than one padding block should be in any FLAC file. Mutagen
488 handles this with MetadataBlock.group_padding.
493 def __init__(self
, data
=""): super(Padding
, self
).__init
__(data
)
494 def load(self
, data
): self
.length
= len(data
.read())
496 try: return "\x00" * self
.length
497 # On some 64 bit platforms this won't generate a MemoryError
498 # or OverflowError since you might have enough RAM, but it
499 # still generates a ValueError. On other 64 bit platforms,
500 # this will still succeed for extremely large values.
501 # Those should never happen in the real world, and if they
502 # do, writeblocks will catch it.
503 except (OverflowError, ValueError, MemoryError):
504 raise error("cannot write %d bytes" % self
.length
)
505 def __eq__(self
, other
):
506 return isinstance(other
, Padding
) and self
.length
== other
.length
507 __hash__
= MetadataBlock
.__hash
__
509 return "<%s (%d bytes)>" % (type(self
).__name
__, self
.length
)
511 class FLAC(FileType
):
512 """A FLAC audio file.
515 info -- stream information (length, bitrate, sample rate)
516 tags -- metadata tags, if any
517 cuesheet -- CueSheet object, if any
518 seektable -- SeekTable object, if any
519 pictures -- list of embedded pictures
522 _mimes
= ["audio/x-flac", "application/x-flac"]
524 METADATA_BLOCKS
= [StreamInfo
, Padding
, None, SeekTable
, VCFLACDict
,
526 """Known metadata block types, indexed by ID."""
528 def score(filename
, fileobj
, header
):
529 return (header
.startswith("fLaC") +
530 filename
.lower().endswith(".flac") * 3)
531 score
= staticmethod(score
)
533 def __read_metadata_block(self
, fileobj
):
534 byte
= ord(fileobj
.read(1))
535 size
= to_int_be(fileobj
.read(3))
537 if (byte
& 0x7F) == VCFLACDict
.code
:
538 # Some jackass is writing broken Metadata block length
539 # for Vorbis comment blocks, and the FLAC reference
540 # implementaton can parse them (mostly by accident),
541 # so we have to too. Instead of parsing the size
542 # given, parse an actual Vorbis comment, leaving
543 # fileobj in the right position.
544 # http://code.google.com/p/mutagen/issues/detail?id=52
545 block
= VCFLACDict(fileobj
)
547 data
= fileobj
.read(size
)
548 if len(data
) != size
:
549 raise error("file said %d bytes, read %d bytes" %(
551 block
= self
.METADATA_BLOCKS
[byte
& 0x7F](data
)
552 except (IndexError, TypeError):
553 block
= MetadataBlock(data
)
554 block
.code
= byte
& 0x7F
556 if block
.code
== VCFLACDict
.code
:
557 if self
.tags
is None:
560 raise FLACVorbisError("> 1 Vorbis comment block found")
561 elif block
.code
== CueSheet
.code
:
562 if self
.cuesheet
is None:
563 self
.cuesheet
= block
565 raise error("> 1 CueSheet block found")
566 elif block
.code
== SeekTable
.code
:
567 if self
.seektable
is None:
568 self
.seektable
= block
570 raise error("> 1 SeekTable block found")
571 self
.metadata_blocks
.append(block
)
572 return not (byte
& 0x80);
575 """Add a Vorbis comment block to the file."""
576 if self
.tags
is None:
577 self
.tags
= VCFLACDict()
578 self
.metadata_blocks
.append(self
.tags
)
579 else: raise FLACVorbisError("a Vorbis comment already exists")
580 add_vorbiscomment
= add_tags
582 def delete(self
, filename
=None):
583 """Remove Vorbis comments from a file.
585 If no filename is given, the one most recently loaded is used.
587 if filename
is None: filename
= self
.filename
588 for s
in list(self
.metadata_blocks
):
589 if isinstance(s
, VCFLACDict
):
590 self
.metadata_blocks
.remove(s
)
595 vc
= property(lambda s
: s
.tags
, doc
="Alias for tags; don't use this.")
597 def load(self
, filename
):
598 """Load file information from a filename."""
600 self
.metadata_blocks
= []
603 self
.seektable
= None
604 self
.filename
= filename
605 fileobj
= open(filename
, "rb")
607 self
.__check
_header
(fileobj
)
608 while self
.__read
_metadata
_block
(fileobj
):
614 self
.metadata_blocks
[0].length
615 except (AttributeError, IndexError):
616 raise FLACNoHeaderError("Stream info block not found")
618 info
= property(lambda s
: s
.metadata_blocks
[0])
620 def add_picture(self
, picture
):
621 """Add a new picture to the file."""
622 self
.metadata_blocks
.append(picture
)
624 def clear_pictures(self
):
625 """Delete all pictures from the file."""
626 self
.metadata_blocks
= filter(lambda b
: b
.code
!= Picture
.code
,
627 self
.metadata_blocks
)
629 def __get_pictures(self
):
630 return filter(lambda b
: b
.code
== Picture
.code
, self
.metadata_blocks
)
631 pictures
= property(__get_pictures
, doc
="List of embedded pictures")
633 def save(self
, filename
=None, deleteid3
=False):
634 """Save metadata blocks to a file.
636 If no filename is given, the one most recently loaded is used.
639 if filename
is None: filename
= self
.filename
640 f
= open(filename
, 'rb+')
642 # Ensure we've got padding at the end, and only at the end.
643 # If adding makes it too large, we'll scale it down later.
644 self
.metadata_blocks
.append(Padding('\x00' * 1020))
645 MetadataBlock
.group_padding(self
.metadata_blocks
)
647 header
= self
.__check
_header
(f
)
648 available
= self
.__find
_audio
_offset
(f
) - header
# "fLaC" and maybe ID3
649 data
= MetadataBlock
.writeblocks(self
.metadata_blocks
)
652 if deleteid3
and header
> 4:
653 available
+= header
- 4
656 if len(data
) > available
:
657 # If we have too much data, see if we can reduce padding.
658 padding
= self
.metadata_blocks
[-1]
659 newlength
= padding
.length
- (len(data
) - available
)
661 padding
.length
= newlength
662 data
= MetadataBlock
.writeblocks(self
.metadata_blocks
)
663 assert len(data
) == available
665 elif len(data
) < available
:
666 # If we have too little data, increase padding.
667 self
.metadata_blocks
[-1].length
+= (available
- len(data
))
668 data
= MetadataBlock
.writeblocks(self
.metadata_blocks
)
669 assert len(data
) == available
671 if len(data
) != available
:
672 # We couldn't reduce the padding enough.
673 diff
= (len(data
) - available
)
674 insert_bytes(f
, diff
, header
)
677 f
.write("fLaC" + data
)
684 if f
.read(3) == "TAG":
688 def __find_audio_offset(self
, fileobj
):
690 while not (byte
& 0x80):
691 byte
= ord(fileobj
.read(1))
692 size
= to_int_be(fileobj
.read(3))
693 if (byte
& 0x7F) == VCFLACDict
.code
:
694 # See comments in read_metadata_block; the size can't
695 # be trusted for Vorbis comment blocks.
699 return fileobj
.tell()
701 def __check_header(self
, fileobj
):
703 header
= fileobj
.read(4)
706 if header
[:3] == "ID3":
707 size
= 14 + BitPaddedInt(fileobj
.read(6)[2:])
708 fileobj
.seek(size
- 4)
709 if fileobj
.read(4) != "fLaC": size
= None
711 raise FLACNoHeaderError(
712 "%r is not a valid FLAC file" % fileobj
.name
)
717 def delete(filename
):
718 """Remove tags from a file."""
719 FLAC(filename
).delete()