From 6bff355515576b0d1be1ea9e3ebb006e720369b5 Mon Sep 17 00:00:00 2001 From: Borewit Date: Wed, 16 Aug 2017 21:46:31 +0200 Subject: [PATCH] Support for RIFF/WAVE format, using ID3v2 tags Related to issue quodlibet/mutagen#207 --- .gitignore | 1 + mutagen/_file.py | 3 +- mutagen/_riff.py | 235 ++++++++++++++++++ mutagen/wave.py | 219 ++++++++++++++++ tests/data/silence-2s-PCM-16000-08-ID3v23.wav | Bin 0 -> 64540 bytes tests/data/silence-2s-PCM-16000-08-notags.wav | Bin 0 -> 64044 bytes tests/data/silence-2s-PCM-44100-16-ID3v23.wav | Bin 0 -> 353342 bytes tests/test___init__.py | 6 + tests/test_wave.py | 113 +++++++++ 9 files changed, 576 insertions(+), 1 deletion(-) create mode 100644 mutagen/_riff.py create mode 100644 mutagen/wave.py create mode 100644 tests/data/silence-2s-PCM-16000-08-ID3v23.wav create mode 100644 tests/data/silence-2s-PCM-16000-08-notags.wav create mode 100644 tests/data/silence-2s-PCM-44100-16-ID3v23.wav create mode 100644 tests/test_wave.py diff --git a/.gitignore b/.gitignore index fbffe1b0..7318eca6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ coverage .cache *.egg-info .coverage +*.iml .idea .hypothesis .pytest_cache diff --git a/mutagen/_file.py b/mutagen/_file.py index 850556e4..2122eb43 100644 --- a/mutagen/_file.py +++ b/mutagen/_file.py @@ -268,10 +268,11 @@ def File(filething, options=None, easy=False): from mutagen.smf import SMF from mutagen.tak import TAK from mutagen.dsf import DSF + from mutagen.wave import WAVE options = [MP3, TrueAudio, OggTheora, OggSpeex, OggVorbis, OggFLAC, FLAC, AIFF, APEv2File, MP4, ID3FileType, WavPack, Musepack, MonkeysAudio, OptimFROG, ASF, OggOpus, AAC, AC3, - SMF, TAK, DSF] + SMF, TAK, DSF, WAVE] if not options: return None diff --git a/mutagen/_riff.py b/mutagen/_riff.py new file mode 100644 index 00000000..f39c351b --- /dev/null +++ b/mutagen/_riff.py @@ -0,0 +1,235 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2017 Borewit +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +"""Resource Interchange File Format (RIFF).""" + +import struct +from abc import abstractmethod +from struct import pack + +from ._compat import text_type + +from mutagen._util import resize_bytes, delete_bytes, MutagenError + + +class error(MutagenError): + pass + + +class InvalidChunk(error): + pass + + +def is_valid_chunk_id(id): + """ is_valid_chunk_id(FOURCC) + + Arguments: + id (FOURCC) + Returns: + true if valid; otherwise false + + Check if argument id is valid FOURCC type. + """ + + assert isinstance(id, text_type) + + if len(id) != 4: + return False + + for i in range(0, 3): + if id[i] < u' ' or id[i] > u'~': + return False + + return True + + +# Assert FOURCC formatted valid +def assert_valid_chunk_id(id): + if not is_valid_chunk_id(id): + raise ValueError("RIFF-chunk-ID must be four ASCII characters.") + + +class _ChunkHeader(): + """ Abstract common RIFF chunk header""" + + # Chunk headers are 8 bytes long (4 for ID and 4 for the size) + HEADER_SIZE = 8 + + @property + @abstractmethod + def _struct(self): + """ must be implemented in order to instantiate """ + return 'xxxx' + + def __init__(self, fileobj, parent_chunk): + self.__fileobj = fileobj + self.parent_chunk = parent_chunk + self.offset = fileobj.tell() + + header = fileobj.read(self.HEADER_SIZE) + if len(header) < self.HEADER_SIZE: + raise InvalidChunk() + + self.id, self.data_size = struct.unpack(self._struct, header) + + try: + self.id = self.id.decode('ascii') + except UnicodeDecodeError: + raise InvalidChunk() + + if not is_valid_chunk_id(self.id): + raise InvalidChunk() + + self.size = self.HEADER_SIZE + self.data_size + self.data_offset = fileobj.tell() + + def read(self): + """Read the chunks data""" + + self.__fileobj.seek(self.data_offset) + return self.__fileobj.read(self.data_size) + + def write(self, data): + """Write the chunk data""" + + if len(data) > self.data_size: + raise ValueError + + self.__fileobj.seek(self.data_offset) + self.__fileobj.write(data) + + def delete(self): + """Removes the chunk from the file""" + + delete_bytes(self.__fileobj, self.size, self.offset) + if self.parent_chunk is not None: + self.parent_chunk._update_size( + self.parent_chunk.data_size - self.size) + + def _update_size(self, data_size): + """Update the size of the chunk""" + + self.__fileobj.seek(self.offset + 4) + self.__fileobj.write(pack('>I', data_size)) + if self.parent_chunk is not None: + size_diff = self.data_size - data_size + self.parent_chunk._update_size( + self.parent_chunk.data_size - size_diff) + self.data_size = data_size + self.size = data_size + self.HEADER_SIZE + + def resize(self, new_data_size): + """Resize the file and update the chunk sizes""" + + resize_bytes( + self.__fileobj, self.data_size, new_data_size, self.data_offset) + self._update_size(new_data_size) + + +class RiffChunkHeader(_ChunkHeader): + """Representation of the RIFF chunk header""" + + @property + def _struct(self): + return '>4sI' # Size in Big-Endian + + def __init__(self, fileobj, parent_chunk=None): + _ChunkHeader.__init__(self, fileobj, parent_chunk) + + +class RiffSubchunk(_ChunkHeader): + """Representation of a RIFF Subchunk""" + + @property + def _struct(self): + return '<4sI' # Size in Little-Endian + + def __init__(self, fileobj, parent_chunk=None): + _ChunkHeader.__init__(self, fileobj, parent_chunk) + + +class RiffFile(object): + """Representation of a RIFF file + + Ref: http://www.johnloomis.org/cpe102/asgn/asgn1/riff.html + """ + + def __init__(self, fileobj): + self._fileobj = fileobj + self.__subchunks = {} + + # Reset read pointer to beginning of RIFF file + fileobj.seek(0) + + # RIFF Files always start with the RIFF chunk + self._riffChunk = RiffChunkHeader(fileobj) + + if (self._riffChunk.id != 'RIFF'): + raise KeyError("Root chunk should be a RIFF chunk.") + + # Read the RIFF file Type + self.fileType = fileobj.read(4).decode('ascii') + + # Load all RIFF subchunks + while True: + try: + chunk = RiffSubchunk(fileobj, self._riffChunk) + except InvalidChunk: + break + # Normalize ID3v2-tag-chunk to lowercase + if chunk.id == 'ID3 ': + chunk.id = 'id3 ' + self.__subchunks[chunk.id] = chunk + + # Calculate the location of the next chunk, + # considering the pad byte + self.__next_offset = chunk.offset + chunk.size + self.__next_offset += self.__next_offset % 2 + fileobj.seek(self.__next_offset) + + def __contains__(self, id_): + """Check if the IFF file contains a specific chunk""" + + assert_valid_chunk_id(id_) + + return id_ in self.__subchunks + + def __getitem__(self, id_): + """Get a chunk from the IFF file""" + + assert_valid_chunk_id(id_) + + try: + return self.__subchunks[id_] + except KeyError: + raise KeyError( + "%r has no %r chunk" % (self._fileobj, id_)) + + def __delitem__(self, id_): + """Remove a chunk from the IFF file""" + + assert_valid_chunk_id(id_) + + self.__subchunks.pop(id_).delete() + + def insert_chunk(self, id_): + """Insert a new chunk at the end of the IFF file""" + + assert isinstance(id_, text_type) + + if not is_valid_chunk_id(id_): + raise KeyError("RIFF key must be four ASCII characters.") + + self.fileobj.seek(self.__next_offset) + self.fileobj.write(pack('>4si', id_.ljust(4).encode('ascii'), 0)) + self.fileobj.seek(self.__next_offset) + chunk = RiffChunkHeader(self.fileobj, self[u'RIFF']) + self[u'RIFF']._update_size(self[u'RIFF'].data_size + chunk.size) + + self.__subchunks[id_] = chunk + self.__next_offset = chunk.offset + chunk.size diff --git a/mutagen/wave.py b/mutagen/wave.py new file mode 100644 index 00000000..3e899380 --- /dev/null +++ b/mutagen/wave.py @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2017 Borewit +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +"""Microsoft WAVE/RIFF audio file/stream information and tags.""" + +import sys +import struct + +from ._compat import endswith, reraise + +from mutagen import StreamInfo, FileType + +from mutagen.id3 import ID3 +from mutagen._riff import RiffFile, InvalidChunk, error +from mutagen.id3._util import ID3NoHeaderError, error as ID3Error +from mutagen._util import loadfile, \ + convert_error, MutagenError + +__all__ = ["WAVE", "Open", "delete"] + + +class error(MutagenError): + """WAVE stream parsing errors.""" + + +class WaveFile(RiffFile): + """Representation of a RIFF/WAVE file""" + + def __init__(self, fileobj): + RiffFile.__init__(self, fileobj) + + if self.fileType != u'WAVE': + raise error("Expected RIFF/WAVE.") + + +class WaveStreamInfo(StreamInfo): + """WaveStreamInfo() + + Microsoft WAVE file information. + + Information is parsed from the 'fmt ' & 'data'chunk of the RIFF/WAVE file + + Attributes: + length (`float`): audio length, in seconds + bitrate (`int`): audio bitrate, in bits per second + channels (`int`): The number of audio channels + sample_rate (`int`): audio sample rate, in Hz + sample_size (`int`): The audio sample size + """ + + length = 0 + bitrate = 0 + channels = 0 + sample_rate = 0 + + SIZE = 16 + + @convert_error(IOError, error) + def __init__(self, fileobj): + """Raises error""" + + waveFile = WaveFile(fileobj) + try: + waveFormatChunk = waveFile[u'fmt '] + except KeyError as e: + raise error(str(e)) + + data = waveFormatChunk.read() + + header = fileobj.read(self.SIZE) + if len(header) < self.SIZE: + raise InvalidChunk() + + # RIFF: http://soundfile.sapp.org/doc/WaveFormat/ + # Python struct.unpack: + # https://docs.python.org/2/library/struct.html#byte-order-size-and-alignment + info = struct.unpack(' 0: + self.length = self.number_of_samples / self.sample_rate + + def pprint(self): + return u"%d channel AIFF @ %d bps, %s Hz, %.2f seconds" % ( + self.channels, self.bitrate, self.sample_rate, self.length) + + +class _WaveID3(ID3): + """A Wave file with ID3v2 tags""" + + print("RIFF/WAVE_WaveID3(ID3)") + + def _pre_load_header(self, fileobj): + try: + fileobj.seek(WaveFile(fileobj)[u'id3 '].data_offset) + except (InvalidChunk, KeyError): + raise ID3NoHeaderError("No ID3 chunk") + + @convert_error(IOError, error) + @loadfile(writable=True) + def save(self, filething, v1=1, v2_version=4, v23_sep='/', padding=None): + """Save ID3v2 data to the Wave/RIFF file""" + + fileobj = filething.fileobj + + wave_file = WaveFile(fileobj) + + if 'id3 ' not in wave_file: + wave_file.insert_chunk(u'id3 ') + + chunk = wave_file[u'id3 '] + + try: + data = self._prepare_data( + fileobj, chunk.data_offset, chunk.data_size, v2_version, + v23_sep, padding) + except ID3Error as e: + reraise(error, e, sys.exc_info()[2]) + + chunk.resize(len(data)) + chunk.write(data) + + @loadfile(writable=True) + def delete(self, filething): + """Completely removes the ID3 chunk from the RIFF/WAVE file""" + + fileobj = filething.fileobj + + waveFile = WaveFile(fileobj) + + if 'id3 ' in waveFile: + try: + waveFile['id3 '].delete() + except ValueError: + pass + + self.clear() + + +@convert_error(IOError, error) +@loadfile(method=False, writable=True) +def delete(filething): + """Completely removes the ID3 chunk from the RIFF file""" + + try: + del RiffFile(filething.fileobj)[u'id3 '] + except KeyError: + pass + + +class WAVE(FileType): + """WAVE(filething) + + A Waveform Audio File Format + (WAVE, or more commonly known as WAV due to its filename extension) + + Arguments: + filething (filething) + + Attributes: + tags (`mutagen.id3.ID3`) + info (`WaveStreamInfo`) + """ + + _mimes = ["audio/wav", "audio/wave"] + + @staticmethod + def score(filename, fileobj, header): + filename = filename.lower() + + return (header.startswith(b"RIFF") * 2 + endswith(filename, b".wav") + + endswith(filename, b".wave")) + + def add_tags(self): + """Add an empty ID3 tag to the file.""" + if self.tags is None: + self.tags = _WaveID3() + else: + raise error("an ID3 tag already exists") + + @convert_error(IOError, error) + @loadfile() + def load(self, filething, **kwargs): + """Load stream and tag information from a file.""" + + fileobj = filething.fileobj + + try: + self.info = WaveStreamInfo(fileobj) + except ValueError as e: + raise error(e) + + fileobj.seek(0, 0) + + try: + self.tags = _WaveID3(fileobj, **kwargs) + except ID3NoHeaderError: + self.tags = None + except ID3Error as e: + raise error(e) + else: + self.tags.filename = self.filename + + +Open = WAVE diff --git a/tests/data/silence-2s-PCM-16000-08-ID3v23.wav b/tests/data/silence-2s-PCM-16000-08-ID3v23.wav new file mode 100644 index 0000000000000000000000000000000000000000..095c93fbdf96debf860ad4019721926e898adbcc GIT binary patch literal 64540 zcmeIu!D}346aetIDQ(uI1)&Ne7Utqj$u<>lD%+h+IyBjCH-nm++w3&m(%md(N}(VW zZ{9qMH}C!>p7kPLJ$e<9#FyL@{11Ku-@G^Pz4^YG-=x~vdE@Vh&-$MZA1~VC^@!-i zTDcCYtA#C&$ASaq^m-nkfvwa0Z;=TzK`=$xM|rw6U#Wt zdqmyeyPM(t%jUy)9w#x2BKC6ZQ4X}3*BMVQvofX`^OK_fYgL#Ag2V=FLug_x)R0TyLxW;rh>; z?`(W{ty_L{fpV)Ie%=mF7K>)t#^B3l+BUP|#k_q|)K8jXx7r{7F?)BJgV(FFUw3~w d`t$t!lel`ecJ}K}hree%s^O^o@$MI2{{xY?e0u-@ literal 0 HcmV?d00001 diff --git a/tests/data/silence-2s-PCM-16000-08-notags.wav b/tests/data/silence-2s-PCM-16000-08-notags.wav new file mode 100644 index 0000000000000000000000000000000000000000..722b4e2951dbc9228eff2807e424cc576cc33f4e GIT binary patch literal 64044 zcmeIuAqs?G0EE%eWZWYdJcnRd!D`Sfh|OkQ@Z6jHvuAjdzA)*{(>QkjWF408abE2w zNtHTxU*eZiP4?~DQa-m4AV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly nK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1YQCU&}$GM literal 0 HcmV?d00001 diff --git a/tests/data/silence-2s-PCM-44100-16-ID3v23.wav b/tests/data/silence-2s-PCM-44100-16-ID3v23.wav new file mode 100644 index 0000000000000000000000000000000000000000..e75cc8c8f3b0a0155f947fdd09e9b93e5795ea02 GIT binary patch literal 353342 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 KHowasT)YDp$)bAz literal 0 HcmV?d00001 diff --git a/tests/test___init__.py b/tests/test___init__.py index 3bd6d923..cfd5764c 100644 --- a/tests/test___init__.py +++ b/tests/test___init__.py @@ -35,6 +35,7 @@ from mutagen.smf import SMF from mutagen.tak import TAK from mutagen.dsf import DSF +from mutagen.wave import WAVE from os import devnull @@ -534,6 +535,11 @@ def test_dict(self): os.path.join(DATA_DIR, '5644800-2ch-s01-silence.dsf'), os.path.join(DATA_DIR, 'with-id3.dsf'), os.path.join(DATA_DIR, 'without-id3.dsf'), + ], + WAVE: [ + os.path.join(DATA_DIR, 'silence-2s-PCM-16000-08-ID3v23.wav'), + os.path.join(DATA_DIR, 'silence-2s-PCM-16000-08-ID3v23.wav'), + os.path.join(DATA_DIR, 'silence-2s-PCM-16000-08-notags.wav'), ] } diff --git a/tests/test_wave.py b/tests/test_wave.py new file mode 100644 index 00000000..104ed21b --- /dev/null +++ b/tests/test_wave.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +import os + +from mutagen.wave import WAVE +from tests import TestCase, DATA_DIR, get_temp_copy + + +class TWave(TestCase): + def setUp(self): + fn_wav_pcm_2s_16000_08_ID3v23 = \ + os.path.join(DATA_DIR, "silence-2s-PCM-16000-08-ID3v23.wav") + self.wav_pcm_2s_16000_08_ID3v23 = \ + WAVE(fn_wav_pcm_2s_16000_08_ID3v23) + + self.tmp_fn_pcm_2s_16000_08_ID3v23 = \ + get_temp_copy(fn_wav_pcm_2s_16000_08_ID3v23) + self.tmp_wav_pcm_2s_16000_08_ID3v23 = \ + WAVE(self.tmp_fn_pcm_2s_16000_08_ID3v23) + + fn_wav_pcm_2s_16000_08_notags = \ + os.path.join(DATA_DIR, "silence-2s-PCM-16000-08-notags.wav") + self.wav_pcm_2s_16000_08_notags = \ + WAVE(fn_wav_pcm_2s_16000_08_notags) + + self.tmp_fn_pcm_2s_16000_08_notag = \ + get_temp_copy(fn_wav_pcm_2s_16000_08_notags) + self.tmp_wav_pcm_2s_16000_08_notag = \ + WAVE(self.tmp_fn_pcm_2s_16000_08_notag) + + fn_wav_pcm_2s_44100_16_ID3v23 = \ + os.path.join(DATA_DIR, "silence-2s-PCM-44100-16-ID3v23.wav") + self.wav_pcm_2s_44100_16_ID3v23 = WAVE(fn_wav_pcm_2s_44100_16_ID3v23) + + def test_channels(self): + self.failUnlessEqual(self.wav_pcm_2s_16000_08_ID3v23.info.channels, 2) + self.failUnlessEqual(self.wav_pcm_2s_44100_16_ID3v23.info.channels, 2) + + def test_sample_rate(self): + self.failUnlessEqual(self.wav_pcm_2s_16000_08_ID3v23.info.sample_rate, + 16000) + self.failUnlessEqual(self.wav_pcm_2s_44100_16_ID3v23.info.sample_rate, + 44100) + + def test_number_of_samples(self): + self.failUnlessEqual(self.wav_pcm_2s_16000_08_ID3v23. + info.number_of_samples, 32000) + self.failUnlessEqual(self.wav_pcm_2s_44100_16_ID3v23. + info.number_of_samples, 88200) + + def test_length(self): + self.failUnlessAlmostEqual(self.wav_pcm_2s_16000_08_ID3v23.info.length, + 2.0, 2) + self.failUnlessAlmostEqual(self.wav_pcm_2s_44100_16_ID3v23.info.length, + 2.0, 2) + + def test_not_my_file(self): + self.failUnlessRaises( + KeyError, WAVE, os.path.join(DATA_DIR, "empty.ogg")) + + def test_pprint(self): + self.wav_pcm_2s_44100_16_ID3v23.pprint() + + def test_mime(self): + self.failUnless("audio/wav" in self.wav_pcm_2s_44100_16_ID3v23.mime) + self.failUnless("audio/wave" in self.wav_pcm_2s_44100_16_ID3v23.mime) + + def test_ID3_tags(self): + id3 = self.wav_pcm_2s_44100_16_ID3v23.tags + self.assertEquals(id3["TALB"], "Quod Libet Test Data") + self.assertEquals(id3["TCON"], "Silence") + self.assertEquals(id3["TIT2"], "Silence") + self.assertEquals(id3["TPE1"], ["piman / jzig"]) # ToDo: split on '/'? + + def test_delete(self): + self.tmp_wav_pcm_2s_16000_08_ID3v23.delete() + + self.failIf(self.tmp_wav_pcm_2s_16000_08_ID3v23.tags) + self.failUnless(WAVE(self.tmp_fn_pcm_2s_16000_08_ID3v23).tags is None) + + def test_save_no_tags(self): + self.tmp_wav_pcm_2s_16000_08_ID3v23.tags = None + self.tmp_wav_pcm_2s_16000_08_ID3v23.save() + self.assertTrue(self.tmp_wav_pcm_2s_16000_08_ID3v23.tags is None) + + def test_add_tags_already_there(self): + self.failUnless(self.tmp_wav_pcm_2s_16000_08_ID3v23.tags) + self.failUnlessRaises(Exception, + self.tmp_wav_pcm_2s_16000_08_ID3v23.add_tags) + + def test_roundtrip(self): + self.failUnlessEqual(self.tmp_wav_pcm_2s_16000_08_ID3v23["TIT2"], + ["Silence"]) + self.tmp_wav_pcm_2s_16000_08_ID3v23.save() + new = WAVE(self.tmp_wav_pcm_2s_16000_08_ID3v23.filename) + self.failUnlessEqual(new["TIT2"], ["Silence"]) + + def test_save_tags(self): + from mutagen.id3 import TIT1 + tags = self.tmp_wav_pcm_2s_16000_08_ID3v23.tags + tags.add(TIT1(encoding=3, text="foobar")) + tags.save() + + new = WAVE(self.tmp_wav_pcm_2s_16000_08_ID3v23.filename) + self.failUnlessEqual(new["TIT1"], ["foobar"]) + + def test_save_without_ID3_chunk(self): + from mutagen.id3 import TIT1 + self.tmp_wav_pcm_2s_16000_08_notag["TIT1"] = TIT1(encoding=3, + text="foobar") + self.tmp_wav_pcm_2s_16000_08_notag.save() + self.failUnless(WAVE(self.tmp_fn_pcm_2s_16000_08_notag)["TIT1"] + == "foobar")