34 Commits
v1.5a ... v1.6

Author SHA1 Message Date
Jamie Hardt
65994db36d Added 3.8 to idea 2020-08-17 11:27:30 -07:00
Jamie Hardt
1a3417d3e5 Merge pull request #8 from elibroftw/master
Updated Code
2020-08-17 11:15:50 -07:00
Elijah Lopez
7589d5fb82 Add metadata tests 2020-08-14 15:19:28 -04:00
Elijah Lopez
6014d1d48b Update utils.py 2020-08-14 15:05:32 -04:00
Elijah Lopez
f8bf6cb4a0 Add tests 2020-08-14 15:03:06 -04:00
Elijah Lopez
6d8e717f42 Update wave_info_reader.py 2020-08-14 14:51:31 -04:00
Elijah Lopez
ba232605db fix bugs 2020-08-14 14:48:49 -04:00
Elijah Lopez
9a90a0c310 Update wave_reader.py 2020-08-14 14:34:17 -04:00
Elijah Lopez
add390c0a0 Formatting, refactoring, __repr__ 2020-08-14 09:07:56 -04:00
Jamie Hardt
7351623e3a Update riff_parser.py
Pass file signature to parse_rf64()
2020-08-07 22:56:31 -07:00
Jamie Hardt
c23ca4bded Update rf64_parser.py
Parameterize the file magic number.
2020-08-07 22:55:37 -07:00
Jamie Hardt
8fe799b211 Update riff_parser.py
Added `BW64` identifier, which is apparently what certain ITU/EBU big WAV files use.
2020-08-07 22:52:28 -07:00
Jamie Hardt
18ebd22ec1 Update wave_info_reader.py
Removed redundant "ISFT" fourcc
2020-06-24 18:30:14 -07:00
Jamie Hardt
cf8aa36fc3 Update README.md 2020-01-06 09:16:24 -08:00
Jamie Hardt
12d16a472f Update README.md 2020-01-06 09:12:24 -08:00
Jamie Hardt
39210738e3 Update umid_parser.py
Can't figure out how these are formatted as string yet so will just output raw hex
2020-01-06 08:58:32 -08:00
Jamie Hardt
966da7c4a2 Merge branch 'master' of https://github.com/iluvcapra/wavinfo 2020-01-06 08:27:38 -08:00
Jamie Hardt
5a30ce3afc UMID Implementation 2020-01-06 08:27:34 -08:00
Jamie Hardt
1b9547e8c2 Style tweaks
Style stuff
2020-01-06 08:27:26 -08:00
Jamie Hardt
1507898b9e Update jamiehardt.xml 2020-01-06 08:23:56 -08:00
Jamie Hardt
4f2a6689f5 Update README.md 2020-01-05 19:41:52 -08:00
Jamie Hardt
597acb2122 Update README.md 2020-01-05 19:28:54 -08:00
Jamie Hardt
9d9592e9e1 Update wave_reader.py
output INFO and test for if these aren't present
2020-01-05 19:20:12 -08:00
Jamie Hardt
b0c5a7de72 Update wave_bext_reader.py
Print UMID in to_dict
2020-01-05 19:19:56 -08:00
Jamie Hardt
c36b53c5c5 Update wave_ixml_reader.py
Removed dead code from iXML parser
2020-01-05 17:20:05 -08:00
Jamie Hardt
25485d9601 Update riff_parser.py
Removed code that isn't being used.
2020-01-05 17:14:09 -08:00
Jamie Hardt
ffa51eaff4 Tests for walking metadata 2020-01-05 17:10:33 -08:00
Jamie Hardt
93a9ca0fd3 Update README.md 2020-01-05 16:45:45 -08:00
Jamie Hardt
b930fc6d6e Update README.md 2020-01-05 16:43:07 -08:00
Jamie Hardt
5f7803fd00 Update .travis.yml 2020-01-05 15:00:02 -08:00
Jamie Hardt
e37a37221b Update README.md 2020-01-05 14:56:23 -08:00
Jamie Hardt
d0b0b06ecb Update README.md 2020-01-05 14:38:31 -08:00
Jamie Hardt
5824406ae6 Update umid_parser.py 2020-01-05 14:29:46 -08:00
Jamie Hardt
dbbc0683f5 Update .travis.yml 2020-01-05 14:28:36 -08:00
19 changed files with 420 additions and 402 deletions

View File

@@ -1,6 +1,7 @@
<component name="ProjectDictionaryState">
<dictionary name="jamiehardt">
<words>
<w>bext</w>
<w>ident</w>
<w>umid</w>
</words>

2
.idea/misc.xml generated
View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (wavinfo)" project-jdk-type="Python SDK" />
</project>

5
.idea/wavinfo.iml generated
View File

@@ -2,10 +2,7 @@
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.7" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="Python 3.8 (wavinfo)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="Unittests" />
</component>
</module>

View File

@@ -2,25 +2,25 @@ dist: xenial
language: python
python:
# - "2.7"
- "3.6"
- "3.5"
- "3.6"
- "3.7"
- "3.8"
script:
- "gunzip tests/test_files/rf64/*.gz"
- "python setup.py test"
# - "python -m pytest tests/ -v --cov wavinfo --cov-report term-missing"
- "python -m pytest tests/ -v --cov wavinfo --cov-report term-missing"
before_install:
- "sudo apt-get update"
- "sudo add-apt-repository universe"
- "sudo apt-get install -y ffmpeg"
- "pip install pytest"
- "pip install lxml"
# - "pip install coverage"
# - "pip install codecov"
# - "pip install pytest-cov==2.5.0"
# - "pip install coverage==4.4"
- "pip install coverage"
- "pip install codecov"
- "pip install pytest-cov==2.5.0"
- "pip install coverage==4.4"
install:
- "pip install setuptools"
# after_success:
# - "codecov"
after_success:
- "codecov"

View File

@@ -1,33 +1,35 @@
[![Build Status](https://travis-ci.com/iluvcapra/wavinfo.svg?branch=master)](https://travis-ci.com/iluvcapra/wavinfo)
[![codecov](https://codecov.io/gh/iluvcapra/wavinfo/branch/master/graph/badge.svg)](https://codecov.io/gh/iluvcapra/wavinfo)
[![Documentation Status](https://readthedocs.org/projects/wavinfo/badge/?version=latest)](https://wavinfo.readthedocs.io/en/latest/?badge=latest) ![](https://img.shields.io/github/license/iluvcapra/wavinfo.svg) ![](https://img.shields.io/pypi/pyversions/wavinfo.svg) [![](https://img.shields.io/pypi/v/wavinfo.svg)](https://pypi.org/project/wavinfo/) ![](https://img.shields.io/pypi/wheel/wavinfo.svg)
<!-- ![Test](https://github.com/iluvcapra/wavinfo/workflows/Upload%20Python%20Package/badge.svg) -->
# wavinfo
The `wavinfo` package allows you to probe WAVE and [RF64/WAVE files][eburf64] and extract extended metadata, with an emphasis on film, video and professional music production metadata.
The `wavinfo` package allows you to probe WAVE and [RF64/WAVE files][eburf64] and extract extended metadata, with an emphasis on film, video and professional music production metadata.
`wavinfo` reads:
* __Broadcast-WAVE__ metadata, compliant with [EBU Tech 3285v2 (2011)][ebu], including embedded program
loudness and coding history, if extant. This also includes the [SMPTE 330M __UMID__][smpte_330m2011]
Unique Materials Identifier.
* [__iXML__ production recorder metadata][ixml], including project, scene, and take tags, recorder notes
* __Broadcast-WAVE__ metadata<sup>[1][ebu]</sup>, including embedded program
loudness and coding history, if extant. This also includes the SMPTE UMID<sup>[2][smpte_330m2011]</sup>.
* __iXML__ production recorder metadata<sup>[3][ixml]</sup>, including project, scene, and take tags, recorder notes
and file family information.
* Most of the common __RIFF INFO__ metadata fields.
* The __wav format__ is also parsed, so you can access the basic sample rate and channel count
* Most of the common __RIFF INFO__<sup>[4][info-tags]</sup> metadata fields.
* The __wav format__ is also parsed, so you can access the basic sample rate and channel count
information.
In progress:
* ADM metadata consilient with the output of the __Dolby RMU__, perhaps later fully complaint with [ITU BS.2076-2][adm].
* iXML `STEINBERG` sound library attributes.
* __NetMix__ library attributes.
* Pro Tools __embedded regions__.
[ebu]:https://tech.ebu.ch/docs/tech/tech3285.pdf
[adm]:https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.2076-2-201910-I!!PDF-E.pdf
[smpte_330m2011]:http://standards.smpte.org/content/978-1-61482-678-1/st-330-2011/SEC1.abstract
[ixml]:http://www.ixml.info
[eburf64]:https://tech.ebu.ch/docs/tech/tech3306v1_1.pdf
[info-tags]:https://exiftool.org/TagNames/RIFF.html#Info
## Demonstration
@@ -58,10 +60,10 @@ The length of the file in frames (interleaved samples) and bytes is available, a
>>> (48000, 2, 6, 24)
```
## Platform Lifecycle Stuff
Python 3.5 support is deprecated.
## Other Resources
* For other file formats and ID3 decoding, look at [audio-metadata](https://github.com/thebigmunch/audio-metadata).

Binary file not shown.

25
tests/test_walk.py Normal file
View File

@@ -0,0 +1,25 @@
import unittest
import wavinfo
class TestWalk(unittest.TestCase):
def test_walk_metadata(self):
test_file = 'tests/test_files/protools/PT A101_4.A1.wav'
info = wavinfo.WavInfoReader(test_file)
tested_data , tested_format = False, False
for scope, key, value in info.walk():
if scope == 'fmt':
if key == 'channel_count':
tested_format = True
self.assertEqual(value, 2)
if scope == 'data':
if key == 'frame_count':
tested_data = True
self.assertEqual(value, 144140)
self.assertTrue(tested_data and tested_format)
if __name__ == '__main__':
unittest.main()

View File

@@ -6,34 +6,35 @@ from unittest import TestCase
from .utils import all_files, ffprobe
import wavinfo
class TestWaveInfo(TestCase):
def test_sanity(self):
for wav_file in all_files():
info = wavinfo.WavInfoReader(wav_file)
self.assertTrue(info is not None)
self.assertEqual(info.__repr__(), 'WavInfoReader(%s, %s, %s)'.format(wav_file, 'latin_1', 'ascii'))
self.assertIsNotNone(info)
def test_fmt_against_ffprobe(self):
for wav_file in all_files():
info = wavinfo.WavInfoReader(wav_file)
ffprobe_info = ffprobe(wav_file)
self.assertEqual( info.fmt.channel_count , ffprobe_info['streams'][0]['channels'] )
self.assertEqual( info.fmt.sample_rate , int(ffprobe_info['streams'][0]['sample_rate']) )
self.assertEqual( info.fmt.bits_per_sample, int(ffprobe_info['streams'][0]['bits_per_raw_sample']) )
self.assertEqual(info.fmt.channel_count, ffprobe_info['streams'][0]['channels'])
self.assertEqual(info.fmt.sample_rate, int(ffprobe_info['streams'][0]['sample_rate']))
self.assertEqual(info.fmt.bits_per_sample, int(ffprobe_info['streams'][0]['bits_per_raw_sample']))
if info.fmt.audio_format == 1:
self.assertTrue(ffprobe_info['streams'][0]['codec_name'].startswith('pcm') )
byte_rate = int(ffprobe_info['streams'][0]['sample_rate']) \
* ffprobe_info['streams'][0]['channels'] \
* int(ffprobe_info['streams'][0]['bits_per_raw_sample']) / 8
self.assertEqual( info.fmt.byte_rate , byte_rate )
self.assertTrue(ffprobe_info['streams'][0]['codec_name'].startswith('pcm'))
streams = ffprobe_info['streams'][0]
byte_rate = int(streams['sample_rate']) * streams['channels'] * int(streams['bits_per_raw_sample']) / 8
self.assertEqual(info.fmt.byte_rate, byte_rate)
def test_data_against_ffprobe(self):
for wav_file in all_files():
info = wavinfo.WavInfoReader(wav_file)
ffprobe_info = ffprobe(wav_file)
self.assertEqual( info.data.frame_count, int(ffprobe_info['streams'][0]['duration_ts'] ))
self.assertEqual(info.data.frame_count, int(ffprobe_info['streams'][0]['duration_ts']))
def test_bext_against_ffprobe(self):
for wav_file in all_files():
@@ -41,56 +42,73 @@ class TestWaveInfo(TestCase):
ffprobe_info = ffprobe(wav_file)
if info.bext:
if 'comment' in ffprobe_info['format']['tags']:
self.assertEqual( info.bext.description, ffprobe_info['format']['tags']['comment'] )
else:
self.assertEqual( info.bext.description , '')
self.assertEqual(info.bext.description, ffprobe_info['format']['tags']['comment'])
else:
self.assertEqual(info.bext.description, '')
if 'encoded_by' in ffprobe_info['format']['tags']:
self.assertEqual( info.bext.originator, ffprobe_info['format']['tags']['encoded_by'] )
self.assertEqual(info.bext.originator, ffprobe_info['format']['tags']['encoded_by'])
else:
self.assertEqual( info.bext.originator, '')
self.assertEqual(info.bext.originator, '')
if 'originator_reference' in ffprobe_info['format']['tags']:
self.assertEqual( info.bext.originator_ref, ffprobe_info['format']['tags']['originator_reference'] )
self.assertEqual(info.bext.originator_ref, ffprobe_info['format']['tags']['originator_reference'])
else:
self.assertEqual( info.bext.originator_ref, '')
self.assertEqual(info.bext.originator_ref, '')
# these don't always reflect the bext info
# self.assertEqual( info.bext.originator_date, ffprobe_info['format']['tags']['date'] )
# self.assertEqual( info.bext.originator_time, ffprobe_info['format']['tags']['creation_time'] )
self.assertEqual( info.bext.time_reference, int(ffprobe_info['format']['tags']['time_reference']) )
# self.assertEqual(info.bext.originator_date, ffprobe_info['format']['tags']['date'])
# self.assertEqual(info.bext.originator_time, ffprobe_info['format']['tags']['creation_time'])
self.assertEqual(info.bext.time_reference, int(ffprobe_info['format']['tags']['time_reference']))
if 'coding_history' in ffprobe_info['format']['tags']:
self.assertEqual( info.bext.coding_history, ffprobe_info['format']['tags']['coding_history'] )
self.assertEqual(info.bext.coding_history, ffprobe_info['format']['tags']['coding_history'])
else:
self.assertEqual( info.bext.coding_history, '' )
self.assertEqual(info.bext.coding_history, '')
def test_ixml(self):
expected = {'A101_4.WAV': {'project' : 'BMH', 'scene': 'A101', 'take': '4',
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124015008231000'},
'A101_3.WAV': {'project' : 'BMH', 'scene': 'A101', 'take': '3',
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124014008228300'},
'A101_2.WAV': {'project' : 'BMH', 'scene': 'A101', 'take': '2',
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124004008218600'},
'A101_1.WAV': {'project' : 'BMH', 'scene': 'A101', 'take': '1',
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124001008206300'},
}
expected = {'A101_4.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '4',
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124015008231000'},
'A101_3.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '3',
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124014008228300'},
'A101_2.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '2',
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124004008218600'},
'A101_1.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '1',
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124001008206300'},
}
for wav_file in all_files():
basename = os.path.basename(wav_file)
basename = os.path.basename(wav_file)
if basename in expected:
info = wavinfo.WavInfoReader(wav_file)
e = expected[basename]
self.assertEqual( e['project'], info.ixml.project )
self.assertEqual( e['scene'], info.ixml.scene )
self.assertEqual( e['take'], info.ixml.take )
self.assertEqual( e['tape'], info.ixml.tape )
self.assertEqual( e['family_uid'], info.ixml.family_uid )
self.assertEqual(e['project'], info.ixml.project)
self.assertEqual(e['scene'], info.ixml.scene)
self.assertEqual(e['take'], info.ixml.take)
self.assertEqual(e['tape'], info.ixml.tape)
self.assertEqual(e['family_uid'], info.ixml.family_uid)
for track in info.ixml.track_list:
self.assertIsNotNone(track.channel_index)
if basename == 'A101_4.WAV' and track.channel_index == '1':
self.assertTrue(track.name == 'MKH516 A')
self.assertEqual(track.name, 'MKH516 A')
def test_metadata(self):
file_with_metadata = 'tests/test_files/sound_grinder_pro/new_camera bumb 1.wav'
self.assertTrue(os.path.exists(file_with_metadata))
info = wavinfo.WavInfoReader(file_with_metadata).info
self.assertEqual(info.title, 'camera bumb 1')
self.assertEqual(info.artist, 'Jamie Hardt')
self.assertEqual(info.copyright, '© 2010 Jamie Hardt')
self.assertEqual(info.product, 'Test Sounds') # album
self.assertEqual(info.album, info.product)
self.assertEqual(info.comment, 'Comments')
self.assertEqual(info.software, 'Sound Grinder Pro')
self.assertEqual(info.created_date, '2010-12-28')
self.assertEqual(info.engineer, 'JPH')
self.assertEqual(info.keywords, 'Sound Effect, movement, microphone, bump')
self.assertEqual(info.title, 'camera bumb 1')
self.assertEqual(type(info.to_dict()), dict)
self.assertEqual(type(info.__repr__()), str)

View File

@@ -8,5 +8,6 @@ from unittest import TestCase
import wavinfo
class TestZoomF8(TestCase):
pass

View File

@@ -4,10 +4,11 @@ import subprocess
from subprocess import PIPE
import json
FFPROBE='ffprobe'
FFPROBE = 'ffprobe'
def ffprobe(path):
arguments = [ FFPROBE , "-of", "json" , "-show_format", "-show_streams", path ]
arguments = [FFPROBE, "-of", "json", "-show_format", "-show_streams", path]
if int(sys.version[0]) < 3:
process = subprocess.Popen(arguments, stdout=PIPE)
process.wait()
@@ -27,13 +28,9 @@ def ffprobe(path):
return None
def all_files():
for dirpath, _, filenames in os.walk('tests/test_files'):
for filename in filenames:
_, ext = os.path.splitext(filename)
if ext in ['.wav','.WAV']:
if ext in ['.wav', '.WAV']:
yield os.path.join(dirpath, filename)

View File

@@ -7,6 +7,6 @@ Go to the documentation for wavinfo.WavInfoReader for more information.
from .wave_reader import WavInfoReader
from .riff_parser import WavInfoEOFError
__version__ = '1.5'
__version__ = '1.6'
__author__ = 'Jamie Hardt <jamiehardt@gmail.com>'
__license__ = "MIT"
__license__ = "MIT"

View File

@@ -4,6 +4,7 @@ from . import WavInfoReader
import sys
import json
def main():
parser = OptionParser()
@@ -28,5 +29,6 @@ def main():
except Exception as e:
print(e)
if __name__ == "__main__":
main()

View File

@@ -1,40 +1,40 @@
import struct
from collections import namedtuple
from . import riff_parser
from . import riff_parser
RF64Context = namedtuple('RF64Context','sample_count bigchunk_table')
def parse_rf64(stream):
#print("starting parse_rf64")
def parse_rf64(stream, signature = b'RF64'):
# print("starting parse_rf64")
start = stream.tell()
assert( stream.read(4) == b'WAVE' )
assert( stream.read(4) == b'WAVE' )
ds64_chunk = riff_parser.parse_chunk(stream)
ds64_field_spec = "<QQQI"
ds64_fields_size = struct.calcsize(ds64_field_spec)
assert(ds64_chunk.ident == b'ds64')
ds64_data = ds64_chunk.read_data(stream)
assert(len(ds64_data) >= ds64_fields_size )
#print("Read ds64 chunk: len()",len(ds64_data))
# print("Read ds64 chunk: len()",len(ds64_data))
riff_size, data_size, sample_count, length_lookup_table = struct.unpack( ds64_field_spec , ds64_data[0:ds64_fields_size] )
bigchunk_table = {}
chunksize64format = "<4sL"
chunksize64size = struct.calcsize(chunksize64format)
#print("Found chunks64s:", length_lookup_table)
# print("Found chunks64s:", length_lookup_table)
for n in range(length_lookup_table):
bigname, bigsize = struct.unpack_from( chunksize64format , ds64data, offset= ds64_fields_size )
bigname, bigsize = struct.unpack_from( chunksize64format , ds64_data, offset= ds64_fields_size )
bigchunk_table[bigname] = bigsize
bigchunk_table[b'data'] = data_size
bigchunk_table[b'RF64'] = riff_size
bigchunk_table[signature] = riff_size
stream.seek(start, 0)
#print("returning from parse_rf64, context: ",RF64Context( sample_count=sample_count, bigchunk_table=bigchunk_table ) )
# print("returning from parse_rf64, context: ", RF64Context(sample_count=sample_count, bigchunk_table=bigchunk_table))
return RF64Context( sample_count=sample_count, bigchunk_table=bigchunk_table )

View File

@@ -11,17 +11,18 @@ class WavInfoEOFError(EOFError):
class ListChunkDescriptor(namedtuple('ListChunkDescriptor', 'signature children')):
def find(self, chunk_path):
if len(chunk_path) > 1:
for chunk in self.children:
if type(chunk) is ListChunkDescriptor and \
chunk.signature is chunk_path[0]:
return chunk.find(chunk_path[1:])
else:
for chunk in self.children:
if type(chunk) is ChunkDescriptor and \
chunk.ident is chunk_path[0]:
return chunk
pass
# def find(self, chunk_path):
# if len(chunk_path) > 1:
# for chunk in self.children:
# if type(chunk) is ListChunkDescriptor and \
# chunk.signature is chunk_path[0]:
# return chunk.find(chunk_path[1:])
# else:
# for chunk in self.children:
# if type(chunk) is ChunkDescriptor and \
# chunk.ident is chunk_path[0]:
# return chunk
class ChunkDescriptor(namedtuple('ChunkDescriptor', 'ident start length rf64_context')):
@@ -35,7 +36,7 @@ def parse_list_chunk(stream, length, rf64_context=None):
signature = stream.read(4)
children = []
while (stream.tell() - start + 8) < length:
while stream.tell() - start + 8 < length:
child_chunk = parse_chunk(stream, rf64_context=rf64_context)
children.append(child_chunk)
@@ -55,16 +56,16 @@ def parse_chunk(stream, rf64_context=None):
data_size = struct.unpack('<I', size_bytes)[0]
if data_size == 0xFFFFFFFF:
if rf64_context is None and ident == b'RF64':
rf64_context = parse_rf64(stream=stream)
data_size = rf64_context.bigchunk_table[ident]
displacement = data_size
if displacement % 2 is not 0:
displacement = displacement + 1
if rf64_context is None and ident in {b'RF64', b'BW64'}:
rf64_context = parse_rf64(stream=stream, signature=ident)
if ident in [b'RIFF', b'LIST', b'RF64']:
data_size = rf64_context.bigchunk_table[ident]
displacement = data_size
if displacement % 2:
displacement += 1
if ident in {b'RIFF', b'LIST', b'RF64', b'BW64'}:
return parse_list_chunk(stream=stream, length=data_size, rf64_context=rf64_context)
else:
data_start = stream.tell()

View File

@@ -1,5 +1,11 @@
import struct
from typing import Union
import binascii
from functools import reduce
def binary_to_string(binary_value):
return reduce(lambda val, el: val + "{:02x}".format(el), binary_value, '')
class UMIDParser:
"""
@@ -9,121 +15,109 @@ class UMIDParser:
"""
def __init__(self, raw_umid: bytearray):
self.raw_umid = raw_umid
@classmethod
def binary_to_string(cls, binary_value):
result_str = ''
for n in range(len(binary_value)):
result_str = f'{binary_value[n]:x}' + result_str
return result_str
@property
def universal_label(self) -> bytearray:
return self.raw_umid[0:12]
@property
def basic_umid(self):
return self.raw_umid[0:32]
#
# @property
# def universal_label(self) -> bytearray:
# return self.raw_umid[0:12]
#
# @property
# def basic_umid(self):
# return self.raw_umid[0:32]
def basic_umid_to_str(self):
return "%024x-%06x-%032x" % (self.binary_to_string(self.universal_label),
self.binary_to_string(self.instance_number),
self.binary_to_string(self.material_number))
@property
def universal_label_is_valid(self) -> bool:
valid_preamble = b'\x06\x0a\x2b\x34\x01\x01\x01\x05\x01\x01'
return self.universal_label[0:len(valid_preamble)] == valid_preamble
@property
def material_type(self) -> str:
material_byte = self.raw_umid[10]
if material_byte == 0x1:
return 'picture'
elif material_byte == 0x2:
return 'audio'
elif material_byte == 0x3:
return 'data'
elif material_byte == 0x4:
return 'other'
elif material_byte == 0x5:
return 'picture_single_component'
elif material_byte == 0x6:
return 'picture_multiple_component'
elif material_byte == 0x7:
return 'audio_single_component'
elif material_byte == 0x9:
return 'audio_multiple_component'
elif material_byte == 0xb:
return 'auxiliary_single_component'
elif material_byte == 0xc:
return 'auxiliary_multiple_component'
elif material_byte == 0xd:
return 'mixed_components'
elif material_byte == 0xf:
return 'not_identified'
else:
return 'not_recognized'
@property
def material_number_creation_method(self) -> str:
method_byte = self.raw_umid[11]
method_byte = (method_byte << 4) & 0xf
if method_byte == 0x0:
return 'undefined'
elif method_byte == 0x1:
return 'smpte'
elif method_byte == 0x2:
return 'uuid'
elif method_byte == 0x3:
return 'masked'
elif method_byte == 0x4:
return 'ieee1394'
elif 0x5 <= method_byte <= 0x7:
return 'reserved_undefined'
else:
return 'unrecognized'
@property
def instance_number_creation_method(self) -> str:
method_byte = self.raw_umid[11]
method_byte = method_byte & 0xf
if method_byte == 0x0:
return 'undefined'
elif method_byte == 0x01:
return 'local_registration'
elif method_byte == 0x02:
return '24_bit_prs'
elif method_byte == 0x03:
return 'copy_number_and_16_bit_prs'
elif 0x04 <= method_byte <= 0x0e:
return 'reserved_undefined'
elif method_byte == 0x0f:
return 'live_stream'
else:
return 'unrecognized'
@property
def indicated_length(self) -> str:
if self.raw_umid[12] == 0x13:
return 'basic'
elif self.raw_umid[12] == 0x33:
return 'extended'
@property
def instance_number(self) -> bytearray:
return self.raw_umid[13:3]
@property
def material_number(self) -> bytearray:
return self.raw_umid[16:16]
@property
def source_pack(self) -> Union[bytearray, None]:
if self.indicated_length == 'extended':
return self.raw_umid[32:32]
else:
return None
return binary_to_string(self.raw_umid[0:32])
#
# @property
# def universal_label_is_valid(self) -> bool:
# valid_preamble = b'\x06\x0a\x2b\x34\x01\x01\x01\x05\x01\x01'
# return self.universal_label[0:len(valid_preamble)] == valid_preamble
#
# @property
# def material_type(self) -> str:
# material_byte = self.raw_umid[10]
# if material_byte == 0x1:
# return 'picture'
# elif material_byte == 0x2:
# return 'audio'
# elif material_byte == 0x3:
# return 'data'
# elif material_byte == 0x4:
# return 'other'
# elif material_byte == 0x5:
# return 'picture_single_component'
# elif material_byte == 0x6:
# return 'picture_multiple_component'
# elif material_byte == 0x7:
# return 'audio_single_component'
# elif material_byte == 0x9:
# return 'audio_multiple_component'
# elif material_byte == 0xb:
# return 'auxiliary_single_component'
# elif material_byte == 0xc:
# return 'auxiliary_multiple_component'
# elif material_byte == 0xd:
# return 'mixed_components'
# elif material_byte == 0xf:
# return 'not_identified'
# else:
# return 'not_recognized'
#
# @property
# def material_number_creation_method(self) -> str:
# method_byte = self.raw_umid[11]
# method_byte = (method_byte << 4) & 0xf
# if method_byte == 0x0:
# return 'undefined'
# elif method_byte == 0x1:
# return 'smpte'
# elif method_byte == 0x2:
# return 'uuid'
# elif method_byte == 0x3:
# return 'masked'
# elif method_byte == 0x4:
# return 'ieee1394'
# elif 0x5 <= method_byte <= 0x7:
# return 'reserved_undefined'
# else:
# return 'unrecognized'
#
# @property
# def instance_number_creation_method(self) -> str:
# method_byte = self.raw_umid[11]
# method_byte = method_byte & 0xf
# if method_byte == 0x0:
# return 'undefined'
# elif method_byte == 0x01:
# return 'local_registration'
# elif method_byte == 0x02:
# return '24_bit_prs'
# elif method_byte == 0x03:
# return 'copy_number_and_16_bit_prs'
# elif 0x04 <= method_byte <= 0x0e:
# return 'reserved_undefined'
# elif method_byte == 0x0f:
# return 'live_stream'
# else:
# return 'unrecognized'
#
# @property
# def indicated_length(self) -> str:
# if self.raw_umid[12] == 0x13:
# return 'basic'
# elif self.raw_umid[12] == 0x33:
# return 'extended'
#
# @property
# def instance_number(self) -> bytearray:
# return self.raw_umid[13:3]
#
# @property
# def material_number(self) -> bytearray:
# return self.raw_umid[16:16]
#
# @property
# def source_pack(self) -> Union[bytearray, None]:
# if self.indicated_length == 'extended':
# return self.raw_umid[32:32]
# else:
# return None

View File

@@ -1,85 +1,89 @@
import struct
import binascii
from .umid_parser import UMIDParser
class WavBextReader:
def __init__(self,bext_data,encoding):
def __init__(self, bext_data, encoding):
"""
Read Broadcast-WAV extended metadata.
:param best_data: The bytes-like data.
"param encoding: The encoding to use when decoding the text fields of the
:param bext_data: The bytes-like data.
:param encoding: The encoding to use when decoding the text fields of the
BEXT metadata scope. According to EBU Rec 3285 this shall be ASCII.
"""
packstring = "<256s"+ "32s" + "32s" + "10s" + "8s" + "QH" + "64s" + "hhhhh" + "180s"
packstring = "<256s" + "32s" + "32s" + "10s" + "8s" + "QH" + "64s" + "hhhhh" + "180s"
rest_starts = struct.calcsize(packstring)
unpacked = struct.unpack(packstring, bext_data[:rest_starts])
def sanatize_bytes(bytes):
first_null = next( (index for index, byte in enumerate(bytes) if byte == 0 ), None )
if first_null is not None:
trimmed = bytes[:first_null]
else:
trimmed = bytes
def sanitize_bytes(b):
first_null = next((index for index, byte in enumerate(b) if byte == 0), None)
trimmed = b if first_null is None else b[:first_null]
decoded = trimmed.decode(encoding)
return decoded
#: Description. A free-text field up to 256 characters long.
self.description = sanatize_bytes(unpacked[0])
self.description = sanitize_bytes(unpacked[0])
#: Originator. Usually the name of the encoding application, sometimes
#: a artist name.
self.originator = sanatize_bytes(unpacked[1])
#: A unique identifer for the file, a serial number.
self.originator_ref = sanatize_bytes(unpacked[2])
self.originator = sanitize_bytes(unpacked[1])
#: A unique identifier for the file, a serial number.
self.originator_ref = sanitize_bytes(unpacked[2])
#: Date of the recording, in the format YYY-MM-DD
self.originator_date = sanatize_bytes(unpacked[3])
self.originator_date = sanitize_bytes(unpacked[3])
#: Time of the recording, in the format HH:MM:SS.
self.originator_time = sanatize_bytes(unpacked[4])
self.originator_time = sanitize_bytes(unpacked[4])
#: The sample offset of the start of the file relative to an
#: epoch, usually midnight the day of the recording.
self.time_reference = unpacked[5]
self.time_reference = unpacked[5]
#: A variable-length text field containing a list of processes and
#: and conversions performed on the file.
self.coding_history = sanatize_bytes(bext_data[rest_starts:])
self.coding_history = sanitize_bytes(bext_data[rest_starts:])
#: BEXT version.
self.version = unpacked[6]
self.version = unpacked[6]
#: SMPTE 330M UMID of this audio file, 64 bytes are allocated though the UMID
#: may only be 32 bytes long.
self.umid = None
self.umid = None
#: EBU R128 Integrated loudness, in LUFS.
self.loudness_value = None
self.loudness_value = None
#: EBU R128 Loudness rante, in LUFS.
self.loudness_range = None
self.loudness_range = None
#: True peak level, in dBFS TP
self.max_true_peak = None
self.max_true_peak = None
#: EBU R128 Maximum momentary loudness, in LUFS
self.max_momentary_loudness = None
self.max_momentary_loudness = None
#: EBU R128 Maximum short-term loudness, in LUFS.
self.max_shortterm_loudness = None
self.max_shortterm_loudness = None
if self.version > 0:
self.umid = unpacked[7]
if self.version > 1:
self.loudness_value = unpacked[8] / 100.0
self.loudness_range = unpacked[9] / 100.0
self.max_true_peak = unpacked[10] / 100.0
self.max_momentary_loudness = unpacked[11] / 100.0
self.max_shortterm_loudness = unpacked[12] / 100.0
self.loudness_value = unpacked[8] / 100.0
self.loudness_range = unpacked[9] / 100.0
self.max_true_peak = unpacked[10] / 100.0
self.max_momentary_loudness = unpacked[11] / 100.0
self.max_shortterm_loudness = unpacked[12] / 100.0
def to_dict(self):
return {'description': self.description,
'originator': self.originator,
'originator_ref': self.originator_ref,
'originator_date': self.originator_date,
'originator_time': self.originator_time,
'time_reference': self.time_reference,
'version': self.version,
'coding_history': self.coding_history,
'loudness_value': self.loudness_value,
'loudness_range': self.loudness_range,
'max_true_peak': self.max_true_peak,
'max_momentary_loudness': self.max_momentary_loudness,
'max_shortterm_loudness': self.max_shortterm_loudness
}
if self.umid is not None:
umid_parsed = UMIDParser(self.umid)
umid_str = umid_parsed.basic_umid_to_str()
else:
umid_str = None
return {'description': self.description,
'originator': self.originator,
'originator_ref': self.originator_ref,
'originator_date': self.originator_date,
'originator_time': self.originator_time,
'time_reference': self.time_reference,
'version': self.version,
'umid': umid_str,
'coding_history': self.coding_history,
'loudness_value': self.loudness_value,
'loudness_range': self.loudness_range,
'max_true_peak': self.max_true_peak,
'max_momentary_loudness': self.max_momentary_loudness,
'max_shortterm_loudness': self.max_shortterm_loudness
}

View File

@@ -1,6 +1,6 @@
from .riff_parser import parse_chunk, ListChunkDescriptor
class WavInfoChunkReader:
def __init__(self, f, encoding):
@@ -9,53 +9,47 @@ class WavInfoChunkReader:
f.seek(0)
parsed_chunks = parse_chunk(f)
list_chunks = [chunk for chunk in parsed_chunks.children \
if type(chunk) is ListChunkDescriptor]
list_chunks = [chunk for chunk in parsed_chunks.children if type(chunk) is ListChunkDescriptor]
self.info_chunk = next((chunk for chunk in list_chunks if chunk.signature == b'INFO'), None)
self.info_chunk = next((chunk for chunk in list_chunks \
if chunk.signature == b'INFO'), None)
#: 'ICOP' Copyright
self.copyright = self._get_field(f,b'ICOP')
self.copyright = self._get_field(f, b'ICOP')
#: 'IPRD' Product
self.product = self._get_field(f,b'IPRD')
self.product = self._get_field(f, b'IPRD')
self.album = self.product
#: 'IGNR' Genre
self.genre = self._get_field(f,b'IGNR')
self.genre = self._get_field(f, b'IGNR')
#: 'ISBJ' Supject
self.subject = self._get_field(f,b'ISBJ')
self.subject = self._get_field(f, b'ISBJ')
#: 'IART' Artist, composer, author
self.artist = self._get_field(f,b'IART')
self.artist = self._get_field(f, b'IART')
#: 'ICMT' Comment
self.comment = self._get_field(f,b'ICMT')
self.comment = self._get_field(f, b'ICMT')
#: 'ISFT' Software, encoding application
self.software = self._get_field(f,b'ISFT')
self.software = self._get_field(f, b'ISFT')
#: 'ICRD' Created date
self.created_date = self._get_field(f,b'ICRD')
self.created_date = self._get_field(f, b'ICRD')
#: 'IENG' Engineer
self.engineer = self._get_field(f,b'IENG')
self.engineer = self._get_field(f, b'IENG')
#: 'ITCH' Technician
self.technician = self._get_field(f,b'ITCH')
self.technician = self._get_field(f, b'ITCH')
#: 'IKEY' Keywords, keyword list
self.keywords = self._get_field(f,b'IKEY')
self.keywords = self._get_field(f, b'IKEY')
#: 'INAM' Name, title
self.title = self._get_field(f,b'INAM')
self.title = self._get_field(f, b'INAM')
#: 'ISRC' Source
self.source = self._get_field(f,b'ISRC')
self.source = self._get_field(f, b'ISRC')
#: 'TAPE' Tape
self.tape = self._get_field(f,b'TAPE')
self.tape = self._get_field(f, b'TAPE')
#: 'IARL' Archival Location
self.archival_location = self._get_field(f,b'IARL')
#: 'ISFT' Software
self.software = self._get_field(f,b'ISFT')
self.archival_location = self._get_field(f, b'IARL')
#: 'ICSM' Commissioned
self.commissioned = self._get_field(f,b'ICMS')
self.commissioned = self._get_field(f, b'ICMS')
def _get_field(self, f, field_ident):
search = next( ( (chunk.start, chunk.length) for chunk in self.info_chunk.children \
if chunk.ident == field_ident ), None)
search = next(((chunk.start, chunk.length) for chunk in self.info_chunk.children if chunk.ident == field_ident),
None)
if search is not None:
f.seek(search[0])
@@ -64,32 +58,30 @@ class WavInfoChunkReader:
else:
return None
def to_dict(self):
"""
A dictionary with all of the key/values read from the INFO scope.
"""
return {'copyright': self.copyright,
'product': self.product,
'genre': self.genre,
'artist': self.artist,
'comment': self.comment,
return {'copyright': self.copyright,
'product': self.product,
'album': self.album,
'genre': self.genre,
'artist': self.artist,
'comment': self.comment,
'software': self.software,
'created_date': self.created_date,
'engineer': self.engineer,
'keywords': self.keywords,
'title': self.title,
'source': self.source,
'tape': self.tape,
'title': self.title,
'source': self.source,
'tape': self.tape,
'commissioned': self.commissioned,
'software': self.software,
'archival_location':self.archival_location,
'subject': self.subject,
'technician':self.technician
'archival_location': self.archival_location,
'subject': self.subject,
'technician': self.technician
}
def __repr__(self):
return_val = self.to_dict()
return_val.update({'encoding': self.encoding})
return str(return_val)

View File

@@ -1,4 +1,4 @@
#import xml.etree.ElementTree as ET
# import xml.etree.ElementTree as ET
from lxml import etree as ET
import io
from collections import namedtuple
@@ -6,6 +6,7 @@ from collections import namedtuple
IXMLTrack = namedtuple('IXMLTrack', ['channel_index', 'interleave_index', 'name', 'function'])
class WavIXMLFormat:
"""
iXML recorder metadata.
@@ -16,15 +17,9 @@ class WavIXMLFormat:
:param xml: A bytes-like object containing the iXML payload.
"""
self.source = xml
xmlBytes = io.BytesIO(xml)
try:
parser = ET.XMLParser(recover=True)
self.parsed = ET.parse(xmlBytes, parser=parser)
except ET.ParseError as err:
print("Error parsing iXML: " + str(err))
decoded = xml.decode(encoding='utf_8_sig')
print(decoded)
self.parsed = ET.parse(io.StringIO(decoded))
xml_bytes = io.BytesIO(xml)
parser = ET.XMLParser(recover=True)
self.parsed = ET.parse(xml_bytes, parser=parser)
def _get_text_value(self, xpath):
e = self.parsed.find("./" + xpath)
@@ -93,5 +88,3 @@ class WavIXMLFormat:
The name of this file's file family.
"""
return self._get_text_value("FILE_SET/FAMILY_NAME")

View File

@@ -4,10 +4,7 @@ import os
import sys
from collections import namedtuple
if sys.version[0] == '3':
import pathlib
else:
import urlparse, urllib
import pathlib
from .riff_parser import parse_chunk, ChunkDescriptor, ListChunkDescriptor
from .wave_ixml_reader import WavIXMLFormat
@@ -15,12 +12,14 @@ from .wave_bext_reader import WavBextReader
from .wave_info_reader import WavInfoChunkReader
#: Calculated statistics about the audio data.
WavDataDescriptor = namedtuple('WavDataDescriptor','byte_count frame_count')
WavDataDescriptor = namedtuple('WavDataDescriptor', 'byte_count frame_count')
#: The format of the audio samples.
WavAudioFormat = namedtuple('WavAudioFormat','audio_format channel_count sample_rate byte_rate block_align bits_per_sample')
WavAudioFormat = namedtuple('WavAudioFormat',
'audio_format channel_count sample_rate byte_rate block_align bits_per_sample')
class WavInfoReader():
class WavInfoReader:
"""
Parse a WAV audio file for metadata.
"""
@@ -36,15 +35,17 @@ class WavInfoReader():
:param bext_encoding: The text encoding to use when decoding the string
fields of the Broadcast-WAV extension. Per EBU 3285 this is ASCII
but this parameter is available to you if you encounter a werido.
but this parameter is available to you if you encounter a weirdo.
"""
absolute_path = os.path.abspath(path)
if sys.version[0] == '3':
#: `file://` url for the file.
self.url = pathlib.Path(absolute_path).as_uri()
else:
self.url = urlparse.urljoin('file:', urllib.pathname2url(absolute_path))
#: `file://` url for the file.
self.url = pathlib.Path(absolute_path).as_uri()
# for __repr__()
self.path = absolute_path
self.info_encoding = info_encoding
self.bext_encoding = bext_encoding
with open(path, 'rb') as f:
chunks = parse_chunk(f)
@@ -53,42 +54,31 @@ class WavInfoReader():
f.seek(0)
#: :class:`wavinfo.wave_reader.WavAudioFormat`
self.fmt = self._get_format(f)
self.fmt = self._get_format(f)
#: :class:`wavinfo.wave_bext_reader.WavBextReader` with Broadcast-WAV metadata
self.bext = self._get_bext(f, encoding=bext_encoding)
self.bext = self._get_bext(f, encoding=bext_encoding)
#: :class:`wavinfo.wave_ixml_reader.WavIXMLFormat` with iXML metadata
self.ixml = self._get_ixml(f)
self.ixml = self._get_ixml(f)
#: :class:`wavinfo.wave_info_reader.WavInfoChunkReader` with RIFF INFO metadata
self.info = self._get_info(f, encoding=info_encoding)
self.data = self._describe_data(f)
self.info = self._get_info(f, encoding=info_encoding)
self.data = self._describe_data()
def _find_chunk_data(self, ident, from_stream, default_none=False):
chunk_descriptor = None
top_chunks = (chunk for chunk in self.main_list if type(chunk) is ChunkDescriptor)
top_chunks = (chunk for chunk in self.main_list if type(chunk) is ChunkDescriptor and chunk.ident == ident)
chunk_descriptor = next(top_chunks, None) if default_none else next(top_chunks)
return chunk_descriptor.read_data(from_stream) if chunk_descriptor else None
if default_none:
chunk_descriptor = next((chunk for chunk in top_chunks if chunk.ident == ident),None)
else:
chunk_descriptor = next((chunk for chunk in top_chunks if chunk.ident == ident))
if chunk_descriptor:
return chunk_descriptor.read_data(from_stream)
else:
return None
def _describe_data(self,f):
def _describe_data(self):
data_chunk = next(c for c in self.main_list if c.ident == b'data')
return WavDataDescriptor(byte_count= data_chunk.length,
frame_count= int(data_chunk.length / self.fmt.block_align))
return WavDataDescriptor(byte_count=data_chunk.length,
frame_count=int(data_chunk.length / self.fmt.block_align))
def _get_format(self,f):
fmt_data = self._find_chunk_data(b'fmt ',f)
def _get_format(self, f):
fmt_data = self._find_chunk_data(b'fmt ', f)
# The format chunk is
# audio_format U16
@@ -102,42 +92,34 @@ class WavInfoReader():
unpacked = struct.unpack(packstring, fmt_data[:rest_starts])
#0x0001 WAVE_FORMAT_PCM PCM
#0x0003 WAVE_FORMAT_IEEE_FLOAT IEEE float
#0x0006 WAVE_FORMAT_ALAW 8-bit ITU-T G.711 A-law
#0x0007 WAVE_FORMAT_MULAW 8-bit ITU-T G.711 µ-law
#0xFFFE WAVE_FORMAT_EXTENSIBLE Determined by SubFormat
# 0x0001 WAVE_FORMAT_PCM PCM
# 0x0003 WAVE_FORMAT_IEEE_FLOAT IEEE float
# 0x0006 WAVE_FORMAT_ALAW 8-bit ITU-T G.711 A-law
# 0x0007 WAVE_FORMAT_MULAW 8-bit ITU-T G.711 µ-law
# 0xFFFE WAVE_FORMAT_EXTENSIBLE Determined by SubFormat
#https://sno.phy.queensu.ca/~phil/exiftool/TagNames/RIFF.html
return WavAudioFormat(audio_format = unpacked[0],
channel_count = unpacked[1],
sample_rate = unpacked[2],
byte_rate = unpacked[3],
block_align = unpacked[4],
bits_per_sample = unpacked[5]
)
# https://sno.phy.queensu.ca/~phil/exiftool/TagNames/RIFF.html
return WavAudioFormat(audio_format=unpacked[0],
channel_count=unpacked[1],
sample_rate=unpacked[2],
byte_rate=unpacked[3],
block_align=unpacked[4],
bits_per_sample=unpacked[5]
)
def _get_info(self, f, encoding):
finder = (chunk.signature for chunk in self.main_list \
if type(chunk) is ListChunkDescriptor)
finder = (chunk.signature for chunk in self.main_list if type(chunk) is ListChunkDescriptor)
if b'INFO' in finder:
return WavInfoChunkReader(f, encoding)
def _get_bext(self, f, encoding):
bext_data = self._find_chunk_data(b'bext',f,default_none=True)
if bext_data:
return WavBextReader(bext_data, encoding)
else:
return None
bext_data = self._find_chunk_data(b'bext', f, default_none=True)
return WavBextReader(bext_data, encoding) if bext_data else None
def _get_ixml(self,f):
ixml_data = self._find_chunk_data(b'iXML',f,default_none=True)
if ixml_data is None:
return None
ixml_string = ixml_data.rstrip(b'\0')
return WavIXMLFormat(ixml_string)
def _get_ixml(self, f):
ixml_data = self._find_chunk_data(b'iXML', f, default_none=True)
return None if ixml_data is None else WavIXMLFormat(ixml_data.rstrip(b'\0'))
def walk(self):
"""
@@ -147,13 +129,22 @@ class WavInfoReader():
metadata field, and the value.
"""
scopes = ('fmt', 'data') #'bext', 'ixml', 'info')
scopes = ('fmt', 'data') # 'bext', 'ixml', 'info')
for scope in scopes:
attr = self.__getattribute__(scope)
for field in attr._fields:
yield scope, field, attr.__getattribute__(field)
bext_dict = self.bext.to_dict()
for key in bext_dict.keys():
yield 'bext', key, bext_dict[key]
if self.bext is not None:
bext_dict = (self.bext or {}).to_dict()
for key in bext_dict.keys():
yield 'bext', key, bext_dict[key]
if self.info is not None:
info_dict = self.info.to_dict()
for key in info_dict.keys():
yield 'info', key, info_dict[key]
def __repr__(self):
return 'WavInfoReader(%s, %s, %s)'.format(self.path, self.info_encoding, self.bext_encoding)