55 Commits
v1.5 ... v1.6.3

Author SHA1 Message Date
Jamie Hardt
c966097e7d Nudged version to 1.6.3 2022-01-06 11:45:39 -08:00
Jamie Hardt
35311e394d Merge branch 'master' of https://github.com/iluvcapra/wavinfo 2022-01-06 11:13:43 -08:00
Jamie Hardt
0633a8685c Deleted .idea dir 2022-01-06 11:11:33 -08:00
Jamie Hardt
f65665a06c Delete python-package.yml 2022-01-06 11:08:48 -08:00
Jamie Hardt
261572cff3 Delete python-package-conda.yml 2022-01-06 11:06:47 -08:00
Jamie Hardt
623a8569fd Create python-package.yml 2022-01-06 11:06:31 -08:00
Jamie Hardt
31493f7cf4 Create python-package-conda.yml 2022-01-06 11:05:17 -08:00
Jamie Hardt
4809bb4844 Bumped lxml requirements re CVE-2021-43818 2022-01-06 11:01:28 -08:00
Jamie Hardt
a76f3b1518 Fixed idea interpreter again 2020-12-09 20:16:35 -08:00
Jamie Hardt
b8cb585d50 idea target stuff 2020-12-09 19:47:23 -08:00
Jamie Hardt
84a76f9c74 Update __init__.py
Version 1.6.2
2020-10-19 21:29:40 -07:00
Jamie Hardt
5a1a12e21e Update __init__.py
Version 1.6.1
2020-10-19 21:28:58 -07:00
Jamie Hardt
06835ffe11 Update setup.py
Added 3.9
2020-10-19 21:26:22 -07:00
Jamie Hardt
1b25b8214d Update .travis.yml
Added 3.9
2020-10-19 21:25:44 -07:00
Jamie Hardt
cfc1a451bc Update .travis.yml 2020-10-08 21:37:50 -07:00
Jamie Hardt
0788613ea3 Update setup.py 2020-10-08 21:37:01 -07:00
Jamie Hardt
e9e4b4bcbb Made quick fixes of these to get tests working 2020-10-08 21:31:32 -07:00
Jamie Hardt
c114eb7cf3 Create dolby_parser.py 2020-08-27 13:37:49 -07:00
Jamie Hardt
4576d65da6 Update wave_reader.py
Removed needless sys import
2020-08-21 22:39:52 -07:00
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
Jamie Hardt
24c6871bbc Update pythonpublish.yml 2020-01-05 14:23:55 -08:00
Jamie Hardt
c62dfb7a8a Update .travis.yml 2020-01-05 13:54:49 -08:00
29 changed files with 739 additions and 439 deletions

View File

@@ -16,7 +16,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install setuptools wheel twine pip install setuptools wheel twine lxml
- name: Build and publish - name: Build and publish
env: env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}

3
.idea/.gitignore generated vendored
View File

@@ -1,3 +0,0 @@
# Default ignored files
/workspace.xml

View File

@@ -1,8 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="jamiehardt">
<words>
<w>ident</w>
<w>umid</w>
</words>
</dictionary>
</component>

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

5
.idea/misc.xml generated
View File

@@ -1,4 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <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.7 (wavinfo)" project-jdk-type="Python SDK" />
<component name="PyPackaging">
<option name="earlyReleasesAsUpgrades" value="true" />
</component>
</project> </project>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/wavinfo.iml" filepath="$PROJECT_DIR$/.idea/wavinfo.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

9
.idea/wavinfo.iml generated
View File

@@ -1,11 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4"> <module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" /> <content url="file://$MODULE_DIR$">
<orderEntry type="jdk" jdkName="Python 3.7" jdkType="Python SDK" /> <excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.7 (wavinfo)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="Unittests" />
</component>
</module> </module>

107
.idea/workspace.xml generated Normal file
View File

@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="fb29ad56-fffe-478f-9c0e-1718aa04342e" name="Default Changelist" comment="">
<change beforePath="$PROJECT_DIR$/.idea/dictionaries/jamiehardt.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/dictionaries/jamiehardt.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/wavinfo.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/wavinfo.iml" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FavoritesManager">
<favorites_list name="wavinfo" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Python Script" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
<option name="UPDATE_TYPE" value="MERGE" />
</component>
<component name="ProjectId" id="1VtfP9ukGK4GNIXS2m0hRLG9d7w" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
<option name="showMembers" value="true" />
</component>
<component name="PropertiesComponent">
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
<property name="TODO_SCOPE" value="Project Files" />
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
<property name="settings.editor.selected.configurable" value="com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable" />
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="project-level" UseSingleDictionary="true" transferred="true" />
<component name="SvnConfiguration">
<configuration />
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="fb29ad56-fffe-478f-9c0e-1718aa04342e" name="Default Changelist" comment="" />
<created>1578077284420</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1578077284420</updated>
</task>
<servers />
</component>
<component name="Vcs.Log.History.Properties">
<option name="COLUMN_ORDER">
<list>
<option value="0" />
<option value="2" />
<option value="3" />
<option value="1" />
</list>
</option>
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
<option name="oldMeFiltersMigrated" value="true" />
</component>
<component name="WindowStateProjectService">
<state x="655" y="287" key="#com.intellij.openapi.updateSettings.impl.PluginUpdateInfoDialog" timestamp="1598547965709">
<screen x="0" y="0" width="1920" height="1080" />
</state>
<state x="655" y="287" key="#com.intellij.openapi.updateSettings.impl.PluginUpdateInfoDialog/1920.0.1920.1080/0.0.1920.1080/3840.0.1920.1080@0.0.1920.1080" timestamp="1598547965709" />
<state x="0" y="0" key="#com.intellij.refactoring.rename.AutomaticRenamingDialog" timestamp="1598555737671">
<screen x="0" y="0" width="1920" height="1080" />
</state>
<state x="0" y="0" key="#com.intellij.refactoring.rename.AutomaticRenamingDialog/1920.0.1920.1080/0.0.1920.1080/3840.0.1920.1080@0.0.1920.1080" timestamp="1598555737671" />
<state x="476" y="183" key="SettingsEditor" timestamp="1598548365517">
<screen x="0" y="0" width="1920" height="1080" />
</state>
<state x="476" y="183" key="SettingsEditor/1920.0.1920.1080/0.0.1920.1080/3840.0.1920.1080@0.0.1920.1080" timestamp="1598548365517" />
<state x="114" y="123" width="1706" height="842" key="com.intellij.history.integration.ui.views.FileHistoryDialog" timestamp="1597689482054">
<screen x="0" y="0" width="1920" height="1080" />
</state>
<state x="114" y="123" width="1706" height="842" key="com.intellij.history.integration.ui.views.FileHistoryDialog/1920.0.1920.1080/0.0.1920.1080/3840.0.1920.1080@0.0.1920.1080" timestamp="1597689482054" />
<state x="577" y="376" key="com.intellij.ide.util.TipDialog" timestamp="1601767353495">
<screen x="0" y="0" width="1920" height="1080" />
</state>
<state x="577" y="376" key="com.intellij.ide.util.TipDialog/1920.0.1920.1080/0.0.1920.1080/3840.0.1920.1080@0.0.1920.1080" timestamp="1598550975228" />
<state x="577" y="376" key="com.intellij.ide.util.TipDialog/1920.0.1920.1080/0.0.1920.1080@0.0.1920.1080" timestamp="1601767353495" />
<state x="772" y="468" key="com.intellij.openapi.vcs.update.UpdateOrStatusOptionsDialogupdate-v2" timestamp="1598547948993">
<screen x="0" y="0" width="1920" height="1080" />
</state>
<state x="772" y="468" key="com.intellij.openapi.vcs.update.UpdateOrStatusOptionsDialogupdate-v2/1920.0.1920.1080/0.0.1920.1080/3840.0.1920.1080@0.0.1920.1080" timestamp="1598547948993" />
<state x="632" y="254" width="670" height="676" key="search.everywhere.popup" timestamp="1598554315633">
<screen x="0" y="0" width="1920" height="1080" />
</state>
<state x="632" y="254" width="670" height="676" key="search.everywhere.popup/1920.0.1920.1080/0.0.1920.1080/3840.0.1920.1080@0.0.1920.1080" timestamp="1598554315633" />
</component>
</project>

View File

@@ -2,24 +2,26 @@ dist: xenial
language: python language: python
python: python:
# - "2.7" # - "2.7"
# - "3.5"
- "3.6" - "3.6"
- "3.5"
- "3.7" - "3.7"
- "3.8" - "3.8"
- "3.9"
script: script:
- "gunzip tests/test_files/rf64/*.gz" - "gunzip tests/test_files/rf64/*.gz"
- "python setup.py test" - "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: before_install:
- "sudo apt-get update" - "sudo apt-get update"
- "sudo add-apt-repository universe" - "sudo add-apt-repository universe"
- "sudo apt-get install -y ffmpeg" - "sudo apt-get install -y ffmpeg"
- "pip install pytest" - "pip install pytest"
# - "pip install coverage" - "pip install lxml"
# - "pip install codecov" - "pip install coverage"
# - "pip install pytest-cov==2.5.0" - "pip install codecov"
# - "pip install coverage==4.4" - "pip install pytest-cov==2.5.0"
- "pip install coverage==4.4"
install: install:
- "pip install setuptools" - "pip install setuptools"
# after_success: after_success:
# - "codecov" - "codecov"

View File

@@ -1,33 +1,35 @@
[![Build Status](https://travis-ci.com/iluvcapra/wavinfo.svg?branch=master)](https://travis-ci.com/iluvcapra/wavinfo) [![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) [![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 # 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: `wavinfo` reads:
* __Broadcast-WAVE__ metadata, compliant with [EBU Tech 3285v2 (2011)][ebu], including embedded program * __Broadcast-WAVE__ metadata<sup>[1][ebu]</sup>, including embedded program
loudness and coding history, if extant. This also includes the [SMPTE 330M __UMID__][smpte_330m2011] loudness and coding history, if extant. This also includes the SMPTE UMID<sup>[2][smpte_330m2011]</sup>.
Unique Materials Identifier. * __iXML__ production recorder metadata<sup>[3][ixml]</sup>, including project, scene, and take tags, recorder notes
* [__iXML__ production recorder metadata][ixml], including project, scene, and take tags, recorder notes
and file family information. and file family information.
* Most of the common __RIFF INFO__ metadata fields. * 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 * The __wav format__ is also parsed, so you can access the basic sample rate and channel count
information. information.
In progress: 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. * iXML `STEINBERG` sound library attributes.
* __NetMix__ library attributes.
* Pro Tools __embedded regions__. * Pro Tools __embedded regions__.
[ebu]:https://tech.ebu.ch/docs/tech/tech3285.pdf [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 [smpte_330m2011]:http://standards.smpte.org/content/978-1-61482-678-1/st-330-2011/SEC1.abstract
[ixml]:http://www.ixml.info [ixml]:http://www.ixml.info
[eburf64]:https://tech.ebu.ch/docs/tech/tech3306v1_1.pdf [eburf64]:https://tech.ebu.ch/docs/tech/tech3306v1_1.pdf
[info-tags]:https://exiftool.org/TagNames/RIFF.html#Info
## Demonstration ## Demonstration
@@ -58,10 +60,10 @@ The length of the file in frames (interleaved samples) and bytes is available, a
>>> (48000, 2, 6, 24) >>> (48000, 2, 6, 24)
``` ```
## Platform Lifecycle Stuff
Python 3.5 support is deprecated.
## Other Resources ## Other Resources
* For other file formats and ID3 decoding, look at [audio-metadata](https://github.com/thebigmunch/audio-metadata). * For other file formats and ID3 decoding, look at [audio-metadata](https://github.com/thebigmunch/audio-metadata).

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
lxml>=4.6.5
setuptools~=49.6.0
ear~=2.0.0

View File

@@ -26,12 +26,14 @@ setup(name='wavinfo',
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
'Topic :: Multimedia', 'Topic :: Multimedia',
'Topic :: Multimedia :: Sound/Audio', 'Topic :: Multimedia :: Sound/Audio',
"Programming Language :: Python :: 3.5", # "Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8"], "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10"],
keywords='waveform metadata audio ebu smpte avi library film tv editing editorial', keywords='waveform metadata audio ebu smpte avi library film tv editing editorial',
install_requires=['lxml'], install_requires=['lxml', 'ear'],
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [
'wavinfo = wavinfo.__main__:main' 'wavinfo = wavinfo.__main__:main'

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 from .utils import all_files, ffprobe
import wavinfo import wavinfo
class TestWaveInfo(TestCase): class TestWaveInfo(TestCase):
def test_sanity(self): def test_sanity(self):
for wav_file in all_files(): for wav_file in all_files():
info = wavinfo.WavInfoReader(wav_file) 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): def test_fmt_against_ffprobe(self):
for wav_file in all_files(): for wav_file in all_files():
info = wavinfo.WavInfoReader(wav_file) info = wavinfo.WavInfoReader(wav_file)
ffprobe_info = ffprobe(wav_file) ffprobe_info = ffprobe(wav_file)
self.assertEqual( info.fmt.channel_count , ffprobe_info['streams'][0]['channels'] ) 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.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.bits_per_sample, int(ffprobe_info['streams'][0]['bits_per_raw_sample']))
if info.fmt.audio_format == 1: if info.fmt.audio_format == 1:
self.assertTrue(ffprobe_info['streams'][0]['codec_name'].startswith('pcm') ) self.assertTrue(ffprobe_info['streams'][0]['codec_name'].startswith('pcm'))
byte_rate = int(ffprobe_info['streams'][0]['sample_rate']) \ streams = ffprobe_info['streams'][0]
* ffprobe_info['streams'][0]['channels'] \ byte_rate = int(streams['sample_rate']) * streams['channels'] * int(streams['bits_per_raw_sample']) / 8
* int(ffprobe_info['streams'][0]['bits_per_raw_sample']) / 8 self.assertEqual(info.fmt.byte_rate, byte_rate)
self.assertEqual( info.fmt.byte_rate , byte_rate )
def test_data_against_ffprobe(self): def test_data_against_ffprobe(self):
for wav_file in all_files(): for wav_file in all_files():
info = wavinfo.WavInfoReader(wav_file) info = wavinfo.WavInfoReader(wav_file)
ffprobe_info = ffprobe(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): def test_bext_against_ffprobe(self):
for wav_file in all_files(): for wav_file in all_files():
@@ -41,56 +42,73 @@ class TestWaveInfo(TestCase):
ffprobe_info = ffprobe(wav_file) ffprobe_info = ffprobe(wav_file)
if info.bext: if info.bext:
if 'comment' in ffprobe_info['format']['tags']: if 'comment' in ffprobe_info['format']['tags']:
self.assertEqual( info.bext.description, ffprobe_info['format']['tags']['comment'] ) self.assertEqual(info.bext.description, ffprobe_info['format']['tags']['comment'])
else: else:
self.assertEqual( info.bext.description , '') self.assertEqual(info.bext.description, '')
if 'encoded_by' in ffprobe_info['format']['tags']: 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: else:
self.assertEqual( info.bext.originator, '') self.assertEqual(info.bext.originator, '')
if 'originator_reference' in ffprobe_info['format']['tags']: 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: else:
self.assertEqual( info.bext.originator_ref, '') self.assertEqual(info.bext.originator_ref, '')
# these don't always reflect the bext info # these don't always reflect the bext info
# self.assertEqual( info.bext.originator_date, ffprobe_info['format']['tags']['date'] ) # 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.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.time_reference, int(ffprobe_info['format']['tags']['time_reference']))
if 'coding_history' in ffprobe_info['format']['tags']: 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: else:
self.assertEqual( info.bext.coding_history, '' ) self.assertEqual(info.bext.coding_history, '')
def test_ixml(self): def test_ixml(self):
expected = {'A101_4.WAV': {'project' : 'BMH', 'scene': 'A101', 'take': '4', expected = {'A101_4.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '4',
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124015008231000'}, 'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124015008231000'},
'A101_3.WAV': {'project' : 'BMH', 'scene': 'A101', 'take': '3', 'A101_3.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '3',
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124014008228300'}, 'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124014008228300'},
'A101_2.WAV': {'project' : 'BMH', 'scene': 'A101', 'take': '2', 'A101_2.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '2',
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124004008218600'}, 'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124004008218600'},
'A101_1.WAV': {'project' : 'BMH', 'scene': 'A101', 'take': '1', 'A101_1.WAV': {'project': 'BMH', 'scene': 'A101', 'take': '1',
'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124001008206300'}, 'tape': '18Y12M31', 'family_uid': 'USSDVGR1112089007124001008206300'},
} }
for wav_file in all_files(): for wav_file in all_files():
basename = os.path.basename(wav_file) basename = os.path.basename(wav_file)
if basename in expected: if basename in expected:
info = wavinfo.WavInfoReader(wav_file) info = wavinfo.WavInfoReader(wav_file)
e = expected[basename] e = expected[basename]
self.assertEqual( e['project'], info.ixml.project ) self.assertEqual(e['project'], info.ixml.project)
self.assertEqual( e['scene'], info.ixml.scene ) self.assertEqual(e['scene'], info.ixml.scene)
self.assertEqual( e['take'], info.ixml.take ) self.assertEqual(e['take'], info.ixml.take)
self.assertEqual( e['tape'], info.ixml.tape ) self.assertEqual(e['tape'], info.ixml.tape)
self.assertEqual( e['family_uid'], info.ixml.family_uid ) self.assertEqual(e['family_uid'], info.ixml.family_uid)
for track in info.ixml.track_list: for track in info.ixml.track_list:
self.assertIsNotNone(track.channel_index) self.assertIsNotNone(track.channel_index)
if basename == 'A101_4.WAV' and track.channel_index == '1': 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 import wavinfo
class TestZoomF8(TestCase): class TestZoomF8(TestCase):
pass pass

View File

@@ -4,10 +4,11 @@ import subprocess
from subprocess import PIPE from subprocess import PIPE
import json import json
FFPROBE='ffprobe' FFPROBE = 'ffprobe'
def ffprobe(path): 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: if int(sys.version[0]) < 3:
process = subprocess.Popen(arguments, stdout=PIPE) process = subprocess.Popen(arguments, stdout=PIPE)
process.wait() process.wait()
@@ -27,13 +28,9 @@ def ffprobe(path):
return None return None
def all_files(): def all_files():
for dirpath, _, filenames in os.walk('tests/test_files'): for dirpath, _, filenames in os.walk('tests/test_files'):
for filename in filenames: for filename in filenames:
_, ext = os.path.splitext(filename) _, ext = os.path.splitext(filename)
if ext in ['.wav','.WAV']: if ext in ['.wav', '.WAV']:
yield os.path.join(dirpath, filename) 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 .wave_reader import WavInfoReader
from .riff_parser import WavInfoEOFError from .riff_parser import WavInfoEOFError
__version__ = '1.5' __version__ = '1.6.3'
__author__ = 'Jamie Hardt <jamiehardt@gmail.com>' __author__ = 'Jamie Hardt <jamiehardt@gmail.com>'
__license__ = "MIT" __license__ = "MIT"

View File

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

188
wavinfo/dolby_parser.py Normal file
View File

@@ -0,0 +1,188 @@
# Dolby RMU Metadata per EBU Tech 3285 Supp 6
#
# https://tech.ebu.ch/docs/tech/tech3285s6.pdf
#
from struct import unpack, calcsize
from enum import (Enum, IntEnum)
CHUNK_IDENT = "dbmd"
DOLBY_VERSION = "1.0.0.6"
class _DPPGenericDownMixLevel(Enum):
PLUS_3DB = 0b000
PLUS_1_5DB = 0b001
UNITY = 0b010
MINUS_1_5DB = 0b011
MINUS_3DB = 0b100
MINUS_4_5DB = 0b101
MINUS_6DB = 0b110
MUTE = 0b111
class DPPDolbySurroundEncodingMode(Enum):
RESERVED = 0b11
IN_USE = 0b10
NOT_IN_USE = 0b01
NOT_INDICATED = 0b00
# DPPLoRoDownMixCenterLevel
# DPPLtRtCenterMixLevel
# DPPLtRtSurroundMixLevel
class DolbyMetadataSegmentTypes(IntEnum):
END_MARKER = 0
DOLBY_E_METADATA = 1
DOLBY_DIGITAL_METADATA = 3
DOLBY_DIGITAL_PLUS_METADATA = 7
AUDIO_INFO = 8
class DDPBitStreamMode(Enum):
"""
Dolby Digital Plus `bsmod` field
§ 4.3.2.2
"""
COMPLETE_MAIN = 0b000
MUSIC_AND_EFFECTS = 0b001
VISUALLY_IMPAIRED = 0b010
HEARING_IMPAIRED = 0b011
DIALOGUE_ONLY = 0b100
COMMENTARY = 0b101
EMERGENCY = 0b110
VOICEOVER = 0b111 # if audioconfigmode is 1_0
KARAOKE = 0b1000 # if audioconfigmode is not 1_0
class DDPAudioCodingMode(Enum):
"""
Dolby Digital Plus `acmod` field
§ 4.3.2.3
"""
RESERVED = 0b000
CH_ORD_1_0 = 0b001
CH_ORD_2_0 = 0b010
CH_ORD_3_0 = 0b011
CH_ORD_2_1 = 0b100
CH_ORD_3_1 = 0b101
CH_ORD_2_2 = 0b110
CH_ORD_3_2 = 0b111
class DPPCenterDownMixLevel(Enum):
"""
§ 4.3.3.1
"""
DOWN_3DB = 0b00
DOWN_45DB = 0b01
DOWN_6DB = 0b10
RESERVED = 0b11
class DPPSurroundDownMixLevel(Enum):
"""
Dolby Digital Plus `surmixlev` field
§ 4.3.3.2
"""
DOWN_3DB = 0b00
DOWN_6DB = 0b01
MUTE = 0b10
RESERVED = 0b11
class DPPLanguageCode(Enum):
"""
§ 4.3.4.1 , 4.3.5 (always 0xFF)
"""
# this is removed in https://www.atsc.org/wp-content/uploads/2015/03/A52-201212-17.pdf § 5.4.2.12
# It should just be 0xff
pass
class DPPMixLevel(int):
pass
class DPPDialnormLevel(int):
pass
class DPPRoomTime(Enum):
"""
`roomtyp` 4.3.6.3
"""
NOT_INDICATED = 0b00
LARGE_ROOM_X_CURVE = 0b01
SMALL_ROOM_FLAT_CURVE = 0b10
RESERVED = 0b11
class DPPPreferredDownMixMode(Enum):
"""
§ 4.3.8.1
"""
NOT_INDICATED = 0b00
PRO_LOGIC = 0b01
STEREO = 0b10
PRO_LOGIC_2 = 0b11
# class DPPLtRtCenterMixLevel(_DPPGenericDownMixLevel):
# pass
#
#
# class DPPLtRtSurroundMixLevel(_DPPGenericDownMixLevel):
# pass
#
#
# class DPPSurroundEXMode(_DPPGenericInUseIndicator):
# pass
#
#
# class DPPHeadphoneMode(_DPPGenericInUseIndicator):
# pass
class DPPADConverterType(Enum):
STANDARD = 0
HDCD = 1
class DDPStreamDependency(Enum):
"""
Encodes `ddplus_info1.stream_type` field § 4.3.12.1
"""
INDEPENDENT = 0
DEPENDENT = 1
INDEPENDENT_FROM_DOLBY_DIGITAL = 2
RESERVED = 3
class DDPDataRate(int):
pass
class DPPRFCompressionProfile(Enum):
NONE = 0
FILM_STANDARD = 1
FILM_LIGHT = 2
MUSIC_STANDARD = 3
MUSIC_LIGHT = 4
SPEECH = 5
class DolbyDigitalPlusMetadata:
@classmethod
def parse(cls, binary_data):
binary_format = "<BBxxBBBBBBBBBBBBBxxxBxxxxxH"
assert len(binary_data >= calcsize(binary_format))
fields = unpack(binary_format, binary_data)
class WavDolbyReader:
def __init__(self, dolby_data):
version, remainder = unpack("<U", dolby_data[0]), dolby_data[1:]
## FIXME continues...

View File

@@ -1,40 +1,40 @@
import struct import struct
from collections import namedtuple from collections import namedtuple
from . import riff_parser from . import riff_parser
RF64Context = namedtuple('RF64Context','sample_count bigchunk_table') RF64Context = namedtuple('RF64Context','sample_count bigchunk_table')
def parse_rf64(stream): def parse_rf64(stream, signature = b'RF64'):
#print("starting parse_rf64") # print("starting parse_rf64")
start = stream.tell() start = stream.tell()
assert( stream.read(4) == b'WAVE' ) assert( stream.read(4) == b'WAVE' )
ds64_chunk = riff_parser.parse_chunk(stream) ds64_chunk = riff_parser.parse_chunk(stream)
ds64_field_spec = "<QQQI" ds64_field_spec = "<QQQI"
ds64_fields_size = struct.calcsize(ds64_field_spec) ds64_fields_size = struct.calcsize(ds64_field_spec)
assert(ds64_chunk.ident == b'ds64') assert(ds64_chunk.ident == b'ds64')
ds64_data = ds64_chunk.read_data(stream) ds64_data = ds64_chunk.read_data(stream)
assert(len(ds64_data) >= ds64_fields_size ) 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] ) riff_size, data_size, sample_count, length_lookup_table = struct.unpack( ds64_field_spec , ds64_data[0:ds64_fields_size] )
bigchunk_table = {} bigchunk_table = {}
chunksize64format = "<4sL" chunksize64format = "<4sL"
chunksize64size = struct.calcsize(chunksize64format) chunksize64size = struct.calcsize(chunksize64format)
#print("Found chunks64s:", length_lookup_table) # print("Found chunks64s:", length_lookup_table)
for n in range(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[bigname] = bigsize
bigchunk_table[b'data'] = data_size bigchunk_table[b'data'] = data_size
bigchunk_table[b'RF64'] = riff_size bigchunk_table[signature] = riff_size
stream.seek(start, 0) 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 ) 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')): class ListChunkDescriptor(namedtuple('ListChunkDescriptor', 'signature children')):
def find(self, chunk_path): pass
if len(chunk_path) > 1: # def find(self, chunk_path):
for chunk in self.children: # if len(chunk_path) > 1:
if type(chunk) is ListChunkDescriptor and \ # for chunk in self.children:
chunk.signature is chunk_path[0]: # if type(chunk) is ListChunkDescriptor and \
return chunk.find(chunk_path[1:]) # chunk.signature is chunk_path[0]:
else: # return chunk.find(chunk_path[1:])
for chunk in self.children: # else:
if type(chunk) is ChunkDescriptor and \ # for chunk in self.children:
chunk.ident is chunk_path[0]: # if type(chunk) is ChunkDescriptor and \
return chunk # chunk.ident is chunk_path[0]:
# return chunk
class ChunkDescriptor(namedtuple('ChunkDescriptor', 'ident start length rf64_context')): 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) signature = stream.read(4)
children = [] children = []
while (stream.tell() - start + 8) < length: while stream.tell() - start + 8 < length:
child_chunk = parse_chunk(stream, rf64_context=rf64_context) child_chunk = parse_chunk(stream, rf64_context=rf64_context)
children.append(child_chunk) children.append(child_chunk)
@@ -55,16 +56,16 @@ def parse_chunk(stream, rf64_context=None):
data_size = struct.unpack('<I', size_bytes)[0] data_size = struct.unpack('<I', size_bytes)[0]
if data_size == 0xFFFFFFFF: if data_size == 0xFFFFFFFF:
if rf64_context is None and ident == b'RF64': if rf64_context is None and ident in {b'RF64', b'BW64'}:
rf64_context = parse_rf64(stream=stream) rf64_context = parse_rf64(stream=stream, signature=ident)
data_size = rf64_context.bigchunk_table[ident]
displacement = data_size
if displacement % 2 is not 0:
displacement = displacement + 1
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) return parse_list_chunk(stream=stream, length=data_size, rf64_context=rf64_context)
else: else:
data_start = stream.tell() data_start = stream.tell()

View File

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

View File

@@ -0,0 +1,9 @@
from lxml import etree as ET
from collections import namedtuple
from ear.fileio.bw64 import Bw64Reader

View File

@@ -1,85 +1,89 @@
import struct import struct
import binascii
from .umid_parser import UMIDParser
class WavBextReader: class WavBextReader:
def __init__(self,bext_data,encoding): def __init__(self, bext_data, encoding):
""" """
Read Broadcast-WAV extended metadata. Read Broadcast-WAV extended metadata.
:param best_data: The bytes-like data. :param bext_data: The bytes-like data.
"param encoding: The encoding to use when decoding the text fields of the :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. 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) rest_starts = struct.calcsize(packstring)
unpacked = struct.unpack(packstring, bext_data[:rest_starts]) unpacked = struct.unpack(packstring, bext_data[:rest_starts])
def sanatize_bytes(bytes): def sanitize_bytes(b):
first_null = next( (index for index, byte in enumerate(bytes) if byte == 0 ), None ) first_null = next((index for index, byte in enumerate(b) if byte == 0), None)
if first_null is not None: trimmed = b if first_null is None else b[:first_null]
trimmed = bytes[:first_null]
else:
trimmed = bytes
decoded = trimmed.decode(encoding) decoded = trimmed.decode(encoding)
return decoded return decoded
#: Description. A free-text field up to 256 characters long. #: 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 #: Originator. Usually the name of the encoding application, sometimes
#: a artist name. #: a artist name.
self.originator = sanatize_bytes(unpacked[1]) self.originator = sanitize_bytes(unpacked[1])
#: A unique identifer for the file, a serial number. #: A unique identifier for the file, a serial number.
self.originator_ref = sanatize_bytes(unpacked[2]) self.originator_ref = sanitize_bytes(unpacked[2])
#: Date of the recording, in the format YYY-MM-DD #: 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. #: 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 #: The sample offset of the start of the file relative to an
#: epoch, usually midnight the day of the recording. #: 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 #: A variable-length text field containing a list of processes and
#: and conversions performed on the file. #: 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. #: BEXT version.
self.version = unpacked[6] self.version = unpacked[6]
#: SMPTE 330M UMID of this audio file, 64 bytes are allocated though the UMID #: SMPTE 330M UMID of this audio file, 64 bytes are allocated though the UMID
#: may only be 32 bytes long. #: may only be 32 bytes long.
self.umid = None self.umid = None
#: EBU R128 Integrated loudness, in LUFS. #: EBU R128 Integrated loudness, in LUFS.
self.loudness_value = None self.loudness_value = None
#: EBU R128 Loudness rante, in LUFS. #: EBU R128 Loudness rante, in LUFS.
self.loudness_range = None self.loudness_range = None
#: True peak level, in dBFS TP #: True peak level, in dBFS TP
self.max_true_peak = None self.max_true_peak = None
#: EBU R128 Maximum momentary loudness, in LUFS #: 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. #: EBU R128 Maximum short-term loudness, in LUFS.
self.max_shortterm_loudness = None self.max_shortterm_loudness = None
if self.version > 0: if self.version > 0:
self.umid = unpacked[7] self.umid = unpacked[7]
if self.version > 1: if self.version > 1:
self.loudness_value = unpacked[8] / 100.0 self.loudness_value = unpacked[8] / 100.0
self.loudness_range = unpacked[9] / 100.0 self.loudness_range = unpacked[9] / 100.0
self.max_true_peak = unpacked[10] / 100.0 self.max_true_peak = unpacked[10] / 100.0
self.max_momentary_loudness = unpacked[11] / 100.0 self.max_momentary_loudness = unpacked[11] / 100.0
self.max_shortterm_loudness = unpacked[12] / 100.0 self.max_shortterm_loudness = unpacked[12] / 100.0
def to_dict(self): def to_dict(self):
return {'description': self.description, if self.umid is not None:
'originator': self.originator, umid_parsed = UMIDParser(self.umid)
'originator_ref': self.originator_ref, umid_str = umid_parsed.basic_umid_to_str()
'originator_date': self.originator_date, else:
'originator_time': self.originator_time, umid_str = None
'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
}
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 from .riff_parser import parse_chunk, ListChunkDescriptor
class WavInfoChunkReader: class WavInfoChunkReader:
def __init__(self, f, encoding): def __init__(self, f, encoding):
@@ -9,53 +9,47 @@ class WavInfoChunkReader:
f.seek(0) f.seek(0)
parsed_chunks = parse_chunk(f) parsed_chunks = parse_chunk(f)
list_chunks = [chunk for chunk in parsed_chunks.children \ list_chunks = [chunk for chunk in parsed_chunks.children if type(chunk) is ListChunkDescriptor]
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 #: 'ICOP' Copyright
self.copyright = self._get_field(f,b'ICOP') self.copyright = self._get_field(f, b'ICOP')
#: 'IPRD' Product #: 'IPRD' Product
self.product = self._get_field(f,b'IPRD') self.product = self._get_field(f, b'IPRD')
self.album = self.product
#: 'IGNR' Genre #: 'IGNR' Genre
self.genre = self._get_field(f,b'IGNR') self.genre = self._get_field(f, b'IGNR')
#: 'ISBJ' Supject #: 'ISBJ' Supject
self.subject = self._get_field(f,b'ISBJ') self.subject = self._get_field(f, b'ISBJ')
#: 'IART' Artist, composer, author #: 'IART' Artist, composer, author
self.artist = self._get_field(f,b'IART') self.artist = self._get_field(f, b'IART')
#: 'ICMT' Comment #: 'ICMT' Comment
self.comment = self._get_field(f,b'ICMT') self.comment = self._get_field(f, b'ICMT')
#: 'ISFT' Software, encoding application #: 'ISFT' Software, encoding application
self.software = self._get_field(f,b'ISFT') self.software = self._get_field(f, b'ISFT')
#: 'ICRD' Created date #: 'ICRD' Created date
self.created_date = self._get_field(f,b'ICRD') self.created_date = self._get_field(f, b'ICRD')
#: 'IENG' Engineer #: 'IENG' Engineer
self.engineer = self._get_field(f,b'IENG') self.engineer = self._get_field(f, b'IENG')
#: 'ITCH' Technician #: 'ITCH' Technician
self.technician = self._get_field(f,b'ITCH') self.technician = self._get_field(f, b'ITCH')
#: 'IKEY' Keywords, keyword list #: 'IKEY' Keywords, keyword list
self.keywords = self._get_field(f,b'IKEY') self.keywords = self._get_field(f, b'IKEY')
#: 'INAM' Name, title #: 'INAM' Name, title
self.title = self._get_field(f,b'INAM') self.title = self._get_field(f, b'INAM')
#: 'ISRC' Source #: 'ISRC' Source
self.source = self._get_field(f,b'ISRC') self.source = self._get_field(f, b'ISRC')
#: 'TAPE' Tape #: 'TAPE' Tape
self.tape = self._get_field(f,b'TAPE') self.tape = self._get_field(f, b'TAPE')
#: 'IARL' Archival Location #: 'IARL' Archival Location
self.archival_location = self._get_field(f,b'IARL') self.archival_location = self._get_field(f, b'IARL')
#: 'ISFT' Software
self.software = self._get_field(f,b'ISFT')
#: 'ICSM' Commissioned #: '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): 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),
search = next( ( (chunk.start, chunk.length) for chunk in self.info_chunk.children \ None)
if chunk.ident == field_ident ), None)
if search is not None: if search is not None:
f.seek(search[0]) f.seek(search[0])
@@ -64,32 +58,30 @@ class WavInfoChunkReader:
else: else:
return None return None
def to_dict(self): def to_dict(self):
""" """
A dictionary with all of the key/values read from the INFO scope. A dictionary with all of the key/values read from the INFO scope.
""" """
return {'copyright': self.copyright, return {'copyright': self.copyright,
'product': self.product, 'product': self.product,
'genre': self.genre, 'album': self.album,
'artist': self.artist, 'genre': self.genre,
'comment': self.comment, 'artist': self.artist,
'comment': self.comment,
'software': self.software, 'software': self.software,
'created_date': self.created_date, 'created_date': self.created_date,
'engineer': self.engineer, 'engineer': self.engineer,
'keywords': self.keywords, 'keywords': self.keywords,
'title': self.title, 'title': self.title,
'source': self.source, 'source': self.source,
'tape': self.tape, 'tape': self.tape,
'commissioned': self.commissioned, 'commissioned': self.commissioned,
'software': self.software, 'archival_location': self.archival_location,
'archival_location':self.archival_location, 'subject': self.subject,
'subject': self.subject, 'technician': self.technician
'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,3 @@
#import xml.etree.ElementTree as ET
from lxml import etree as ET from lxml import etree as ET
import io import io
from collections import namedtuple from collections import namedtuple
@@ -6,6 +5,7 @@ from collections import namedtuple
IXMLTrack = namedtuple('IXMLTrack', ['channel_index', 'interleave_index', 'name', 'function']) IXMLTrack = namedtuple('IXMLTrack', ['channel_index', 'interleave_index', 'name', 'function'])
class WavIXMLFormat: class WavIXMLFormat:
""" """
iXML recorder metadata. iXML recorder metadata.
@@ -16,15 +16,9 @@ class WavIXMLFormat:
:param xml: A bytes-like object containing the iXML payload. :param xml: A bytes-like object containing the iXML payload.
""" """
self.source = xml self.source = xml
xmlBytes = io.BytesIO(xml) xml_bytes = io.BytesIO(xml)
try: parser = ET.XMLParser(recover=True)
parser = ET.XMLParser(recover=True) self.parsed = ET.parse(xml_bytes, parser=parser)
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))
def _get_text_value(self, xpath): def _get_text_value(self, xpath):
e = self.parsed.find("./" + xpath) e = self.parsed.find("./" + xpath)
@@ -93,5 +87,3 @@ class WavIXMLFormat:
The name of this file's file family. The name of this file's file family.
""" """
return self._get_text_value("FILE_SET/FAMILY_NAME") return self._get_text_value("FILE_SET/FAMILY_NAME")

View File

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