def __init__(self, fileobj, info): pages = [] complete = False while not complete: page = OggPage(fileobj) if page.serial == info.serial: pages.append(page) complete = page.complete or (len(page.packets) > 1) data = OggPage.to_packets(pages)[0][7:] super(OggTheoraCommentDict, self).__init__(data + "\x01")
def scan_file(self): fileobj = open(self.filename, "rb") try: try: while True: OggPage(fileobj) except EOFError: pass finally: fileobj.close()
def __init__(self, fileobj, info): pages = [] complete = False while not complete: page = OggPage(fileobj) if page.serial == info.serial: pages.append(page) complete = page.complete or (len(page.packets) > 1) data = OggPage.to_packets(pages)[0] + b"\x01" super(OggSpeexVComment, self).__init__(data, framing=False)
def __init__(self, fileobj): page = OggPage(fileobj) while not page.packets[0].startswith(b"\x80theora"): page = OggPage(fileobj) if not page.first: raise OggTheoraHeaderError( "page has ID header, but doesn't start a stream") data = page.packets[0] vmaj, vmin = struct.unpack("2B", data[7:9]) if (vmaj, vmin) != (3, 2): raise OggTheoraHeaderError("found Theora version %d.%d != 3.2" % (vmaj, vmin)) fps_num, fps_den = struct.unpack(">2I", data[22:30]) if not fps_den: raise OggTheoraHeaderError("fps_den is equal to zero") self.fps = fps_num / float(fps_den) self.bitrate = cdata.uint_be(b"\x00" + data[37:40]) self.granule_shift = (cdata.ushort_be(data[40:42]) >> 5) & 0x1F self.serial = page.serial
def load(self, data): # Ogg expects file objects that don't raise on read if isinstance(data, StrictFileObject): data = data._fileobj page = OggPage(data) while not page.packets[0].startswith("\x7FFLAC"): page = OggPage(data) major, minor, self.packets, flac = struct.unpack( ">BBH4s", page.packets[0][5:13]) if flac != "fLaC": raise OggFLACHeaderError("invalid FLAC marker (%r)" % flac) elif (major, minor) != (1, 0): raise OggFLACHeaderError("unknown mapping version: %d.%d" % (major, minor)) self.serial = page.serial # Skip over the block header. stringobj = StrictFileObject(StringIO(page.packets[0][17:])) super(OggFLACStreamInfo, self).load(stringobj)
def _inject(self, fileobj): """Write tag data into the Theora comment packet/page.""" fileobj.seek(0) page = OggPage(fileobj) while not page.packets[0].startswith("\x81theora"): page = OggPage(fileobj) old_pages = [page] while not (old_pages[-1].complete or len(old_pages[-1].packets) > 1): page = OggPage(fileobj) if page.serial == old_pages[0].serial: old_pages.append(page) packets = OggPage.to_packets(old_pages, strict=False) packets[0] = "\x81theora" + self.write(framing=False) new_pages = OggPage.from_packets(packets, old_pages[0].sequence) OggPage.replace(fileobj, old_pages, new_pages)
def main(argv): from mutagen.ogg import OggPage parser = OptionParser( usage="%prog [options] filename.ogg ...", description="Split Ogg logical streams using Mutagen.", version="Mutagen %s" % ".".join(map(str, mutagen.version))) parser.add_option("--extension", dest="extension", default="ogg", metavar='ext', help="use this extension (default 'ogg')") parser.add_option("--pattern", dest="pattern", default="%(base)s-%(stream)d.%(ext)s", metavar='pattern', help="name files using this pattern") parser.add_option("--m3u", dest="m3u", action="store_true", default=False, help="generate an m3u (playlist) file") (options, args) = parser.parse_args(argv[1:]) if not args: raise SystemExit(parser.print_help() or 1) format = {'ext': options.extension} for filename in args: with _sig.block(): fileobjs = {} format["base"] = os.path.splitext(os.path.basename(filename))[0] with open(filename, "rb") as fileobj: if options.m3u: m3u = open(format["base"] + ".m3u", "w") fileobjs["m3u"] = m3u else: m3u = None while True: try: page = OggPage(fileobj) except EOFError: break else: format["stream"] = page.serial if page.serial not in fileobjs: new_filename = options.pattern % format new_fileobj = open(new_filename, "wb") fileobjs[page.serial] = new_fileobj if m3u: m3u.write(new_filename + "\r\n") fileobjs[page.serial].write(page.write()) for f in fileobjs.values(): f.close()
def __init__(self, fileobj): page = OggPage(fileobj) while not page.packets[0].startswith(b"OpusHead"): page = OggPage(fileobj) self.serial = page.serial if not page.first: raise OggOpusHeaderError( "page has ID header, but doesn't start a stream") (version, self.channels, pre_skip, orig_sample_rate, output_gain, channel_map) = struct.unpack("<BBHIhB", page.packets[0][8:19]) self.__pre_skip = pre_skip # only the higher 4 bits change on incombatible changes major = version >> 4 if major != 0: raise OggOpusHeaderError("version %r unsupported" % major)
def __init__(self, fileobj, info): pages = [] complete = False while not complete: page = OggPage(fileobj) if page.serial == info.serial: pages.append(page) complete = page.complete or (len(page.packets) > 1) data = OggPage.to_packets(pages)[0][7:] # Strip off "\x03vorbis". super(OggVCommentDict, self).__init__(data) self._padding = len(data) - self._size
def test_unsupported_version(self): page = OggPage(open(self.filename, "rb")) data = bytearray(page.packets[0]) data[8] = 0x03 page.packets[0] = bytes(data) OggOpusInfo(BytesIO(page.write())) data[8] = 0x10 page.packets[0] = bytes(data) self.failUnlessRaises(IOError, OggOpusInfo, BytesIO(page.write()))
def __init__(self, fileobj, info): # data should be pointing at the start of an Ogg page, after # the first FLAC page. pages = [] complete = False while not complete: page = OggPage(fileobj) if page.serial == info.serial: pages.append(page) complete = page.complete or (len(page.packets) > 1) comment = cBytesIO(OggPage.to_packets(pages)[0][4:]) super(OggFLACVComment, self).__init__(comment, framing=False)
def __init__(self, fileobj): page = OggPage(fileobj) while not page.packets[0].startswith("\x01vorbis"): page = OggPage(fileobj) if not page.first: raise OggVorbisHeaderError( "page has ID header, but doesn't start a stream") (self.channels, self.sample_rate, max_bitrate, nominal_bitrate, min_bitrate) = struct.unpack("<B4I", page.packets[0][11:28]) self.serial = page.serial if nominal_bitrate == 0: self.bitrate = (max_bitrate + min_bitrate) // 2 elif max_bitrate and max_bitrate < nominal_bitrate: # If the max bitrate is less than the nominal, we know # the nominal is wrong. self.bitrate = max_bitrate elif min_bitrate > nominal_bitrate: self.bitrate = min_bitrate else: self.bitrate = nominal_bitrate
def load(self, data, info, errors='replace'): # data should be pointing at the start of an Ogg page, after # the first FLAC page. pages = [] complete = False while not complete: page = OggPage(data) if page.serial == info.serial: pages.append(page) complete = page.complete or (len(page.packets) > 1) comment = StringIO(OggPage.to_packets(pages)[0][4:]) super(OggFLACVComment, self).load(comment, errors=errors)
def test_preserve_non_padding(self): self.audio["FOO"] = ["BAR"] self.audio.save() extra_data = b"\xde\xad\xbe\xef" with open(self.filename, "r+b") as fobj: OggPage(fobj) # header page = OggPage(fobj) data = OggPage.to_packets([page])[0] data = data.rstrip(b"\x00") + b"\x01" + extra_data new_pages = OggPage.from_packets([data], page.sequence) OggPage.replace(fobj, [page], new_pages) OggOpus(self.filename).save() with open(self.filename, "rb") as fobj: OggPage(fobj) # header page = OggPage(fobj) data = OggPage.to_packets([page])[0] self.assertTrue(data.endswith(b"\x01" + extra_data))
def test_replace_continued(self): # take a partial packet and replace it with a new page # replace() should make it spanning again fileobj = BytesIO() pages = [OggPage(), OggPage()] pages[0].serial = 1 pages[0].sequence = 0 pages[0].complete = False pages[0].packets = [b"foo"] pages[1].serial = 1 pages[1].sequence = 1 pages[1].continued = True pages[1].packets = [b"bar"] fileobj = BytesIO() for page in pages: fileobj.write(page.write()) fileobj.seek(0, 0) pages_from_file = [OggPage(fileobj), OggPage(fileobj)] self.assertEqual(OggPage.to_packets(pages_from_file), [b"foobar"]) packets_part = OggPage.to_packets([pages_from_file[0]]) self.assertEqual(packets_part, [b"foo"]) new_pages = OggPage.from_packets([b"quuux"]) OggPage.replace(fileobj, [pages_from_file[0]], new_pages) fileobj.seek(0, 0) written = OggPage.to_packets([OggPage(fileobj), OggPage(fileobj)]) self.assertEquals(written, [b"quuuxbar"])
def test_replace_fast_path(self): # create interleaved pages fileobj = BytesIO() pages = [OggPage(), OggPage(), OggPage()] pages[0].serial = 42 pages[0].sequence = 0 pages[0].packets = [b"foo"] pages[1].serial = 24 pages[1].sequence = 0 pages[1].packets = [b"bar"] pages[2].serial = 42 pages[2].sequence = 1 pages[2].packets = [b"baz"] for page in pages: fileobj.write(page.write()) fileobj.seek(0, 0) pages_from_file = [OggPage(fileobj), OggPage(fileobj), OggPage(fileobj)] old_pages = [pages_from_file[0], pages_from_file[2]] packets = OggPage.to_packets(old_pages, strict=True) self.assertEqual(packets, [b"foo", b"baz"]) new_packets = [b"111", b"222"] new_pages = OggPage._from_packets_try_preserve(new_packets, old_pages) self.assertEqual(len(new_pages), 2) # remove insert_bytes, so we can be sure the fast path was taken old_insert_bytes = _util.insert_bytes _util.insert_bytes = None try: OggPage.replace(fileobj, old_pages, new_pages) finally: _util.insert_bytes = old_insert_bytes # validate that the new data was written and the other pages # are untouched fileobj.seek(0, 0) pages_from_file = [OggPage(fileobj), OggPage(fileobj), OggPage(fileobj)] packets = OggPage.to_packets( [pages_from_file[0], pages_from_file[2]], strict=True) self.assertEqual(packets, [b"111", b"222"]) packets = OggPage.to_packets([pages_from_file[1]], strict=True) self.assertEqual(packets, [b"bar"])
def _inject(self, fileobj, padding_func): """Write tag data into the Speex comment packet/page.""" fileobj.seek(0) # Find the first header page, with the stream info. # Use it to get the serial number. page = OggPage(fileobj) while not page.packets[0].startswith(b"Speex "): page = OggPage(fileobj) # Look for the next page with that serial number, it'll start # the comment packet. serial = page.serial page = OggPage(fileobj) while page.serial != serial: page = OggPage(fileobj) # Then find all the pages with the comment packet. old_pages = [page] while not (old_pages[-1].complete or len(old_pages[-1].packets) > 1): page = OggPage(fileobj) if page.serial == old_pages[0].serial: old_pages.append(page) packets = OggPage.to_packets(old_pages, strict=False) content_size = get_size(fileobj) - len(packets[0]) # approx vcomment_data = self.write(framing=False) padding_left = len(packets[0]) - len(vcomment_data) info = PaddingInfo(padding_left, content_size) new_padding = info._get_padding(padding_func) # Set the new comment packet. packets[0] = vcomment_data + b"\x00" * new_padding new_pages = OggPage._from_packets_try_preserve(packets, old_pages) OggPage.replace(fileobj, old_pages, new_pages)
def sample_rate(self) -> int: # total_samples = file_info.sample_rate * file_info.length file = self._f info = file.info try: return info.sample_rate except AttributeError: if not isinstance(file, OggOpus): raise with self.path.open('rb') as f: while not (page := OggPage(f)).packets[0].startswith(b'OpusHead'): pass return struct.unpack('<I', page.packets[0][12:16])[0]
def test_renumber_extradata(self): fileobj = BytesIO() for page in self.pages: fileobj.write(page.write()) fileobj.write(b"left over data") fileobj.seek(0) # Trying to rewrite should raise an error... self.failUnlessRaises(Exception, OggPage.renumber, fileobj, 1, 10) fileobj.seek(0) # But the already written data should remain valid, pages = [OggPage(fileobj) for i in range(3)] self.failUnlessEqual([page.sequence for page in pages], [10, 11, 12]) # And the garbage that caused the error should be okay too. self.failUnlessEqual(fileobj.read(), b"left over data")
def __init__(self, fileobj, info): pages = [] complete = False while not complete: page = OggPage(fileobj) if page.serial == info.serial: pages.append(page) complete = page.complete or (len(page.packets) > 1) packets = OggPage.to_packets(pages) if not packets: raise error("Missing metadata packet") data = packets[0][7:] super(OggTheoraCommentDict, self).__init__(data, framing=False) self._padding = len(data) - self._size
def _inject(self, fileobj): """Write tag data into the Vorbis comment packet/page.""" # Find the old pages in the file; we'll need to remove them, # plus grab any stray setup packet data out of them. fileobj.seek(0) page = OggPage(fileobj) while not page.packets[0].startswith(b"\x03vorbis"): page = OggPage(fileobj) old_pages = [page] while not (old_pages[-1].complete or len(old_pages[-1].packets) > 1): page = OggPage(fileobj) if page.serial == old_pages[0].serial: old_pages.append(page) packets = OggPage.to_packets(old_pages, strict=False) # Set the new comment packet. packets[0] = b"\x03vorbis" + self.write() new_pages = OggPage.from_packets(packets, old_pages[0].sequence) OggPage.replace(fileobj, old_pages, new_pages)
def test_find_last_last_empty(self): # https://github.com/quodlibet/mutagen/issues/308 pages = [OggPage() for i in xrange(10)] for i, page in enumerate(pages): page.sequence = i page.position = i pages[-1].last = True pages[-1].position = -1 data = BytesIO(b"".join([page.write() for page in pages])) page = OggPage.find_last(data, pages[-1].serial, finishing=True) assert page is not None assert page.position == 8 page = OggPage.find_last(data, pages[-1].serial, finishing=False) assert page is not None assert page.position == -1
def bitrate(self) -> int: # Bitrate is variable for lossless formats, so the reported value will be an average # bit_rate = file_info.sample_rate * file_info.bits_per_sample * file_info.channels # not accurate for VBR info = self._f.info try: return info.bitrate except AttributeError: if self.file_type != 'ogg': raise with self.path.open('rb') as f: while (page := OggPage(f)).position == 0: pass f.seek(0, 2) # End of the file return int((f.tell() - page.offset) * 8 / info.length)
def __init__(self, fileobj): page = OggPage(fileobj) while not page.packets[0].startswith(b"\x7FFLAC"): page = OggPage(fileobj) major, minor, self.packets, flac = struct.unpack( ">BBH4s", page.packets[0][5:13]) if flac != b"fLaC": raise OggFLACHeaderError("invalid FLAC marker (%r)" % flac) elif (major, minor) != (1, 0): raise OggFLACHeaderError( "unknown mapping version: %d.%d" % (major, minor)) self.serial = page.serial # Skip over the block header. stringobj = cBytesIO(page.packets[0][17:]) try: flac_info = FLACStreamInfo(stringobj) except FLACError as e: raise OggFLACHeaderError(e) for attr in ["min_blocksize", "max_blocksize", "sample_rate", "channels", "bits_per_sample", "total_samples", "length"]: setattr(self, attr, getattr(flac_info, attr))
def test_replace(self): # create interleaved pages fileobj = BytesIO() pages = [OggPage(), OggPage(), OggPage()] pages[0].serial = 42 pages[0].sequence = 0 pages[0].packets = [b"foo"] pages[1].serial = 24 pages[1].sequence = 0 pages[1].packets = [b"bar"] pages[2].serial = 42 pages[2].sequence = 1 pages[2].packets = [b"baz"] for page in pages: fileobj.write(page.write()) fileobj.seek(0, 0) pages_from_file = [ OggPage(fileobj), OggPage(fileobj), OggPage(fileobj) ] old_pages = [pages_from_file[0], pages_from_file[2]] packets = OggPage.to_packets(old_pages, strict=True) self.assertEqual(packets, [b"foo", b"baz"]) new_packets = [b"1111", b"2222"] new_pages = OggPage.from_packets(new_packets, sequence=old_pages[0].sequence) self.assertEqual(len(new_pages), 1) OggPage.replace(fileobj, old_pages, new_pages) fileobj.seek(0, 0) first = OggPage(fileobj) self.assertEqual(first.serial, 42) self.assertEqual(OggPage.to_packets([first], strict=True), [b"1111", b"2222"]) second = OggPage(fileobj) self.assertEqual(second.serial, 24) self.assertEqual(OggPage.to_packets([second], strict=True), [b"bar"])
def test_at_least_one_audio_page(self): page = OggPage(self.fileobj) while not page.last: page = OggPage(self.fileobj) self.failUnless(page.last)
def test_streaminfo_bad_version(self): page = OggPage(open(self.filename, "rb")).write() page = page.replace(b"\x01\x00", b"\x02\x00", 1) self.failUnlessRaises(IOError, OggFLACStreamInfo, cBytesIO(page))
def test_streaminfo_too_short(self): page = OggPage(open(self.filename, "rb")).write() self.failUnlessRaises(OggError, OggFLACStreamInfo, cBytesIO(page[:10]))
def test_streaminfo_bad_marker(self): page = OggPage(open(self.filename, "rb")).write() page = page.replace(b"fLaC", b"!fLa", 1) self.failUnlessRaises(IOError, OggFLACStreamInfo, cBytesIO(page))
def test_theora_not_first_page(self): page = OggPage(open(self.filename, "rb")) page.first = False fileobj = cBytesIO(page.write()) self.failUnlessRaises(IOError, OggTheoraInfo, fileobj)