29 Commits
v1.2 ... v1.5a

Author SHA1 Message Date
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
Jamie Hardt
6c8cc47788 Update README.md
Added note on command-line entry point
2020-01-05 10:10:37 -08:00
Jamie Hardt
a90d3f4b38 Version 1.5
Added command-line entrypoint, more UMID implementation
2020-01-05 10:05:54 -08:00
Jamie Hardt
3ede4de06a Delete pypi_upload.sh 2020-01-04 22:30:15 -08:00
Jamie Hardt
5d4bb13ad6 v1.4.1
to test github action
2020-01-04 22:28:02 -08:00
Jamie Hardt
e132c5846c Create pythonpublish.yml
experimenting with upload workflow
2020-01-04 22:18:15 -08:00
Jamie Hardt
b87b43aab4 Update .gitignore
Ignore .DS_Store
2020-01-03 10:46:26 -08:00
Jamie Hardt
6b42a6bb09 idea files 2020-01-03 10:09:24 -08:00
Jamie Hardt
7a315be242 Update LICENSE
Bump year
2020-01-03 10:06:59 -08:00
Jamie Hardt
eb3e2adc27 Update README.md 2020-01-03 10:04:20 -08:00
Jamie Hardt
3e7faefedb Update .travis.yml 2020-01-03 10:03:56 -08:00
Jamie Hardt
54ce5abe77 Update .travis.yml 2020-01-03 09:40:01 -08:00
Jamie Hardt
2f124b0d56 Update .travis.yml 2020-01-03 09:39:50 -08:00
Jamie Hardt
cd11b0924b Update .travis.yml 2020-01-03 09:31:41 -08:00
Jamie Hardt
ff862aafe9 Update .travis.yml 2020-01-03 09:24:37 -08:00
Jamie Hardt
e0432458cc Update .travis.yml
Attempt to turn codecov back on
2020-01-03 09:03:18 -08:00
Jamie Hardt
b4613ed6f4 Update umid_parser.py
Removed type annotation which seems to die in Python 3.5
2020-01-02 17:38:32 -08:00
Jamie Hardt
8d44d411d7 Update umid_parser.py 2020-01-02 17:26:23 -08:00
Jamie Hardt
6005f79e60 Added basic UMID parser 2020-01-02 17:24:08 -08:00
Jamie Hardt
841b86f3f4 Python 3.8 added to tests
Bumped version and added 3.8 to Travis
2020-01-02 11:55:51 -08:00
Jamie Hardt
5c90d5ff47 Rewrite of chink code
More work on #4
2020-01-02 11:44:42 -08:00
Jamie Hardt
60e329fdb4 Improved exceptions in certain EOF cases
Pursuant to #4
2020-01-02 10:50:36 -08:00
Jamie Hardt
15db4c9ffa Some setup metadata tweaks.. 2019-08-20 16:54:19 -07:00
Jamie Hardt
49ac961b94 Some setup metadata tweaks.. 2019-08-20 16:21:24 -07:00
Jamie Hardt
7fc530b2cd Some documentation tweaks. 2019-08-20 16:11:50 -07:00
Jamie Hardt
4dfc1ab33c Added iXML track list parsing 2019-08-19 11:39:13 -07:00
Jamie Hardt
4770c781b2 Update README.md 2019-06-29 21:55:19 -07:00
Jamie Hardt
45c2aae640 Update wave_ixml_reader.py 2019-06-29 21:50:20 -07:00
21 changed files with 358 additions and 62 deletions

26
.github/workflows/pythonpublish.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Upload Python Package
on:
release:
types: [created]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine lxml
- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*

1
.gitignore vendored
View File

@@ -105,3 +105,4 @@ venv.bak/
# vim swap # vim swap
*.swp *.swp
.DS_Store

3
.idea/.gitignore generated vendored Normal file
View File

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

8
.idea/dictionaries/jamiehardt.xml generated Normal file
View File

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

View File

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

4
.idea/misc.xml generated Normal file
View File

@@ -0,0 +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" />
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?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 Normal file
View File

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

11
.idea/wavinfo.iml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<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="sourceFolder" forTests="false" />
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="Unittests" />
</component>
</module>

View File

@@ -5,19 +5,22 @@ python:
- "3.6" - "3.6"
- "3.5" - "3.5"
- "3.7" - "3.7"
- "3.8"
script: script:
- "gunzip tests/test_files/rf64/*.gz" - "gunzip tests/test_files/rf64/*.gz"
- "python setup.py test" - "python setup.py test"
# - "py.test 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 coverage" - "pip install pytest"
- "pip install codecov" - "pip install lxml"
- "pip install pytest-cov==2.5.0" # - "pip install coverage"
# - "pip install coverage==4.4" # - "pip install codecov"
# - "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,6 +1,6 @@
MIT License MIT License
Copyright (c) 2018 Jamie Hardt Copyright (c) 2020 Jamie Hardt
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,5 +1,4 @@
[![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)
@@ -42,6 +41,12 @@ path = '../tests/test_files/A101_1.WAV'
info = WavInfoReader(path) info = WavInfoReader(path)
``` ```
The package also installs a shell command:
```sh
$ wavinfo test_files/A101_1.WAV
```
### Basic WAV Data ### Basic WAV Data
The length of the file in frames (interleaved samples) and bytes is available, as is the contents of the format chunk. The length of the file in frames (interleaved samples) and bytes is available, as is the contents of the format chunk.
@@ -53,6 +58,10 @@ The length of the file in frames (interleaved samples) and bytes is available, a
>>> (48000, 2, 6, 24) >>> (48000, 2, 6, 24)
``` ```
## Other Resources
* For other file formats and ID3 decoding, look at [audio-metadata](https://github.com/thebigmunch/audio-metadata).

View File

@@ -1,2 +0,0 @@
#!/bin/bash
python3 -m twine upload --repository-url https://upload.pypi.org/legacy/ dist/*

View File

@@ -1,23 +1,40 @@
from setuptools import setup from setuptools import setup
from wavinfo import __author__, __license__, __version__
with open("README.md", "r") as fh: with open("README.md", "r") as fh:
long_description = fh.read() long_description = fh.read()
setup(name='wavinfo', setup(name='wavinfo',
version='1.2', version=__version__,
author='Jamie Hardt', author=__author__,
author_email='jamiehardt@me.com', author_email='jamiehardt@me.com',
description='Probe WAVE Files for iXML, Broadcast-WAVE and other metadata.', description='Probe WAVE Files for iXML, Broadcast-WAVE and other metadata.',
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
long_description=long_description, long_description=long_description,
license=__license__,
url='https://github.com/iluvcapra/wavinfo', url='https://github.com/iluvcapra/wavinfo',
project_urls={
'Source':
'https://github.com/iluvcapra/wavinfo',
'Documentation':
'https://wavinfo.readthedocs.io/',
'Issues':
'https://github.com/iluvcapra/wavinfo/issues',
},
packages=['wavinfo'],
classifiers=['Development Status :: 5 - Production/Stable', classifiers=['Development Status :: 5 - Production/Stable',
'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",
packages=['wavinfo'], "Programming Language :: Python :: 3.8"],
install_requires=['lxml'] keywords='waveform metadata audio ebu smpte avi library film tv editing editorial',
install_requires=['lxml'],
entry_points={
'console_scripts': [
'wavinfo = wavinfo.__main__:main'
]
}
) )

View File

@@ -87,3 +87,10 @@ class TestWaveInfo(TestCase):
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:
self.assertIsNotNone(track.channel_index)
if basename == 'A101_4.WAV' and track.channel_index == '1':
self.assertTrue(track.name == 'MKH516 A')

View File

@@ -1,14 +1,12 @@
# -*- coding: utf-8 -*- """
methods to probe a WAV file for various kinds of production metadata.
# :module:`wavinfo` provides methods to probe a WAV file for
# various kinds of production metadata.
#
#
#
#
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
__version__ = '1.1' __version__ = '1.5'
__author__ = 'Jamie Hardt' __author__ = 'Jamie Hardt <jamiehardt@gmail.com>'
__license__ = "MIT"

32
wavinfo/__main__.py Normal file
View File

@@ -0,0 +1,32 @@
from optparse import OptionParser, OptionGroup
import datetime
from . import WavInfoReader
import sys
import json
def main():
parser = OptionParser()
parser.usage = 'wavinfo [FILE.wav]*'
# parser.add_option('-f', dest='output_format', help='Set the output format',
# default='json',
# metavar='FORMAT')
(options, args) = parser.parse_args(sys.argv)
for arg in args[1:]:
try:
this_file = WavInfoReader(path=arg)
ret_dict = {'file_argument': arg, 'run_date': datetime.datetime.now().isoformat() , 'scopes': {}}
for scope, name, value in this_file.walk():
if scope not in ret_dict['scopes'].keys():
ret_dict['scopes'][scope] = {}
ret_dict['scopes'][scope][name] = value
json.dump(ret_dict, fp=sys.stdout, indent=2)
except Exception as e:
print(e)
if __name__ == "__main__":
main()

View File

@@ -1,70 +1,72 @@
import struct import struct
import pdb
from collections import namedtuple from collections import namedtuple
from .rf64_parser import parse_rf64 from .rf64_parser import parse_rf64
class ListChunkDescriptor(namedtuple('ListChunkDescriptor' , 'signature children')):
def find(chunk_path): class WavInfoEOFError(EOFError):
def __init__(self, identifier, chunk_start):
self.identifier = identifier
self.chunk_start = chunk_start
class ListChunkDescriptor(namedtuple('ListChunkDescriptor', 'signature children')):
def find(self, chunk_path):
if len(chunk_path) > 1: if len(chunk_path) > 1:
for chunk in self.children: for chunk in self.children:
if type(chunk) is ListChunkDescriptor and \ if type(chunk) is ListChunkDescriptor and \
chunk.signature is chunk_path[0]: chunk.signature is chunk_path[0]:
return chunk.find(chunk_path[1:]) return chunk.find(chunk_path[1:])
else: else:
for chunk in self.children: for chunk in self.children:
if type(chunk) is ChunkDescriptor and \ if type(chunk) is ChunkDescriptor and \
chunk.ident is chunk_path[0]: chunk.ident is chunk_path[0]:
return chunk return chunk
class ChunkDescriptor(namedtuple('ChunkDescriptor', 'ident start length rf64_context') ): class ChunkDescriptor(namedtuple('ChunkDescriptor', 'ident start length rf64_context')):
def read_data(self, from_stream): def read_data(self, from_stream):
from_stream.seek(self.start) from_stream.seek(self.start)
return from_stream.read(self.length) return from_stream.read(self.length)
def parse_list_chunk(stream, length, rf64_context =None):
start = stream.tell()
def parse_list_chunk(stream, length, rf64_context=None):
start = stream.tell()
signature = stream.read(4) signature = stream.read(4)
#print("Parsing list chunk with siganture: ", signature)
children = [] children = []
while (stream.tell() - start) < 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)
if child_chunk: children.append(child_chunk)
children.append(child_chunk)
else: stream.seek(start + length)
break
return ListChunkDescriptor(signature=signature, children=children) return ListChunkDescriptor(signature=signature, children=children)
def parse_chunk(stream, rf64_context=None):
ident = stream.read(4)
if len(ident) != 4:
return
sizeb = stream.read(4) def parse_chunk(stream, rf64_context=None):
size = struct.unpack('<I',sizeb)[0] header_start = stream.tell()
ident = stream.read(4)
if size == 0xFFFFFFFF: size_bytes = stream.read(4)
if len(ident) != 4 or len(size_bytes) != 4:
raise WavInfoEOFError(identifier=ident, chunk_start=header_start)
data_size = struct.unpack('<I', size_bytes)[0]
if data_size == 0xFFFFFFFF:
if rf64_context is None and ident == b'RF64': if rf64_context is None and ident == b'RF64':
rf64_context = parse_rf64(stream=stream) rf64_context = parse_rf64(stream=stream)
size = rf64_context.bigchunk_table[ident] data_size = rf64_context.bigchunk_table[ident]
displacement = size displacement = data_size
if displacement % 2 is not 0: if displacement % 2 is not 0:
displacement = displacement + 1 displacement = displacement + 1
if ident in [b'RIFF',b'LIST', b'RF64']: if ident in [b'RIFF', b'LIST', b'RF64']:
#print("Parsing list chunk with ident: ", ident) return parse_list_chunk(stream=stream, length=data_size, rf64_context=rf64_context)
return parse_list_chunk(stream=stream, length=size, rf64_context=rf64_context)
else: else:
start = stream.tell() data_start = stream.tell()
stream.seek(displacement,1) stream.seek(displacement, 1)
#print("Parsing chunk with start=%i, ident=%s" % (start, ident)) return ChunkDescriptor(ident=ident, start=data_start, length=data_size, rf64_context=rf64_context)
return ChunkDescriptor(ident=ident, start=start, length=size, rf64_context=rf64_context)

129
wavinfo/umid_parser.py Normal file
View File

@@ -0,0 +1,129 @@
import struct
from typing import Union
class UMIDParser:
"""
Parse a raw binary SMPTE 330M Universal Materials Identifier
This implementation is based on SMPTE ST 330:2011
"""
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]
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

View File

@@ -1,6 +1,10 @@
#import xml.etree.ElementTree as ET #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
IXMLTrack = namedtuple('IXMLTrack', ['channel_index', 'interleave_index', 'name', 'function'])
class WavIXMLFormat: class WavIXMLFormat:
""" """
@@ -27,6 +31,26 @@ class WavIXMLFormat:
if e is not None: if e is not None:
return e.text return e.text
@property
def raw_xml(self):
"""
The root entity of the iXML document.
"""
return self.parsed
@property
def track_list(self):
"""
A description of each track.
:return: An Iterator
"""
for track in self.parsed.find("./TRACK_LIST").iter():
if track.tag == 'TRACK':
yield IXMLTrack(channel_index=track.xpath('string(CHANNEL_INDEX/text())'),
interleave_index=track.xpath('string(INTERLEAVE_INDEX/text())'),
name=track.xpath('string(NAME/text())'),
function=track.xpath('string(FUNCTION/text())'))
@property @property
def project(self): def project(self):
""" """

View File

@@ -147,9 +147,13 @@ 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()
for key in bext_dict.keys():
yield 'bext', key, bext_dict[key]