This commit is contained in:
Jamie Hardt
2019-01-01 11:47:09 -08:00
parent 4713158e00
commit feb8753ff9
10 changed files with 508 additions and 23 deletions

View File

@@ -4,6 +4,7 @@
# wavinfo
The `wavinfo` package allows you to probe WAVE files and extract extended metadata, with an emphasis on
production metadata.
@@ -23,4 +24,93 @@ This module is presently under construction and not sutiable for production at t
[ebu]:https://tech.ebu.ch/docs/tech/tech3285.pdf
[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.infoi
## Demonstration
The entry point for wavinfo is the WavInfoReader class.
```python
from wavinfo import WavInfoReader
path = '../tests/test_files/A101_1.WAV'
info = WavInfoReader(path)
```
### Basic WAV Data
The length of the file in frames (interleaved samples) and bytes is available, as is the contents of the format chunk.
```python
(info.data.frame_count, info.data.byte_count)
>>> (240239, 1441434)
(info.fmt.sample_rate, info.fmt.channel_count, info.fmt.block_align, info.fmt.bits_per_sample)
>>> (48000, 2, 6, 24)
```
## Broadcast WAV Extension
```python
print(info.bext.description)
print("----------")
print("Originator:", info.bext.originator)
print("Originator Ref:", info.bext.originator_ref)
print("Originator Date:", info.bext.originator_date)
print("Originator Time:", info.bext.originator_time)
print("Time Reference:", info.bext.time_reference)
print(info.bext.coding_history)
```
sSPEED=023.976-ND
sTAKE=1
sUBITS=$12311801
sSWVER=2.67
sPROJECT=BMH
sSCENE=A101
sFILENAME=A101_1.WAV
sTAPE=18Y12M31
sTRK1=MKH516 A
sTRK2=Boom
sNOTE=
----------
Originator: Sound Dev: 702T S#GR1112089007
Originator Ref: USSDVGR1112089007124001008206301
Originator Date: 2018-12-31
Originator Time: 12:40:00
Time Reference: 2190940753
A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch
## iXML Production Recorder Metadata
```python
print("iXML Project:", info.ixml.project)
print("iXML Scene:", info.ixml.scene)
print("iXML Take:", info.ixml.take)
print("iXML Tape:", info.ixml.tape)
print("iXML File Family Name:", info.ixml.family_name)
print("iXML File Family UID:", info.ixml.family_uid)
```
iXML Project: BMH
iXML Scene: A101
iXML Take: 1
iXML Tape: 18Y12M31
iXML File Family Name: None
iXML File Family UID: USSDVGR1112089007124001008206300
A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch

104
demo.md Normal file
View File

@@ -0,0 +1,104 @@
# `wavinfo` Demonstration
The entry point for wavinfo is the WavInfoReader class.
```python
from wavinfo import WavInfoReader
path = '../tests/test_files/A101_1.WAV'
info = WavInfoReader(path)
```
## Basic WAV Data
The length of the file in frames (interleaved samples) and bytes is available, as is the contents of the format chunk.
```python
(info.data.frame_count, info.data.byte_count)
```
(240239, 1441434)
```python
(info.fmt.sample_rate, info.fmt.channel_count, info.fmt.block_align, info.fmt.bits_per_sample)
```
(48000, 2, 6, 24)
## Broadcast WAV Extension
```python
print(info.bext.description)
print("----------")
print("Originator:", info.bext.originator)
print("Originator Ref:", info.bext.originator_ref)
print("Originator Date:", info.bext.originator_date)
print("Originator Time:", info.bext.originator_time)
print("Time Reference:", info.bext.time_reference)
print(info.bext.coding_history)
```
sSPEED=023.976-ND
sTAKE=1
sUBITS=$12311801
sSWVER=2.67
sPROJECT=BMH
sSCENE=A101
sFILENAME=A101_1.WAV
sTAPE=18Y12M31
sTRK1=MKH516 A
sTRK2=Boom
sNOTE=
----------
Originator: Sound Dev: 702T S#GR1112089007
Originator Ref: USSDVGR1112089007124001008206301
Originator Date: 2018-12-31
Originator Time: 12:40:00
Time Reference: 2190940753
A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch
## iXML Production Recorder Metadata
```python
print("iXML Project:", info.ixml.project)
print("iXML Scene:", info.ixml.scene)
print("iXML Take:", info.ixml.take)
print("iXML Tape:", info.ixml.tape)
print("iXML File Family Name:", info.ixml.family_name)
print("iXML File Family UID:", info.ixml.family_uid)
```
iXML Project: BMH
iXML Scene: A101
iXML Take: 1
iXML Tape: 18Y12M31
iXML File Family Name: None
iXML File Family UID: USSDVGR1112089007124001008206300
A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch
```python
```

189
examples/demo.ipynb Normal file
View File

@@ -0,0 +1,189 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# `wavinfo` Demonstration\n",
"\n",
"The entry point for wavinfo is the WavInfoReader class."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"from wavinfo import WavInfoReader\n",
"\n",
"path = '../tests/test_files/A101_1.WAV'\n",
"\n",
"info = WavInfoReader(path)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Basic WAV Data\n",
"\n",
"The length of the file in frames (interleaved samples) and bytes is available, as is the contents of the format chunk."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(240239, 1441434)"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"(info.data.frame_count, info.data.byte_count)"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(48000, 2, 6, 24)"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"(info.fmt.sample_rate, info.fmt.channel_count, info.fmt.block_align, info.fmt.bits_per_sample)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Broadcast WAV Extension"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"sSPEED=023.976-ND\r\n",
"sTAKE=1\r\n",
"sUBITS=$12311801\r\n",
"sSWVER=2.67\r\n",
"sPROJECT=BMH\r\n",
"sSCENE=A101\r\n",
"sFILENAME=A101_1.WAV\r\n",
"sTAPE=18Y12M31\r\n",
"sTRK1=MKH516 A\r\n",
"sTRK2=Boom\r\n",
"sNOTE=\r\n",
"\n",
"----------\n",
"Originator: Sound Dev: 702T S#GR1112089007\n",
"Originator Ref: USSDVGR1112089007124001008206301\n",
"Originator Date: 2018-12-31\n",
"Originator Time: 12:40:00\n",
"Time Reference: 2190940753\n",
"A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\r\n",
"\n"
]
}
],
"source": [
"print(info.bext.description)\n",
"print(\"----------\")\n",
"print(\"Originator:\", info.bext.originator)\n",
"print(\"Originator Ref:\", info.bext.originator_ref)\n",
"print(\"Originator Date:\", info.bext.originator_date)\n",
"print(\"Originator Time:\", info.bext.originator_time)\n",
"print(\"Time Reference:\", info.bext.time_reference)\n",
"print(info.bext.coding_history)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## iXML Production Recorder Metadata"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"iXML Project: BMH\n",
"iXML Scene: A101\n",
"iXML Take: 1\n",
"iXML Tape: 18Y12M31\n",
"iXML File Family Name: None\n",
"iXML File Family UID: USSDVGR1112089007124001008206300\n",
"A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\r\n",
"\n"
]
}
],
"source": [
"print(\"iXML Project:\", info.ixml.project)\n",
"print(\"iXML Scene:\", info.ixml.scene)\n",
"print(\"iXML Take:\", info.ixml.take)\n",
"print(\"iXML Tape:\", info.ixml.tape)\n",
"print(\"iXML File Family Name:\", info.ixml.family_name)\n",
"print(\"iXML File Family UID:\", info.ixml.family_uid)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.2"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@@ -57,7 +57,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"WavBextFormat(description='sSPEED=023.976-ND\\r\\nsTAKE=1\\r\\nsUBITS=$12311801\\r\\nsSWVER=2.67\\r\\nsPROJECT=BMH\\r\\nsSCENE=A101\\r\\nsFILENAME=A101_1.WAV\\r\\nsTAPE=18Y12M31\\r\\nsTRK1=MKH516 A\\r\\nsTRK2=Boom\\r\\nsNOTE=\\r\\n', originator='Sound Dev: 702T S#GR1112089007', originator_ref='USSDVGR1112089007124001008206301', originator_date='2018-12-31', originator_time='12:40:00', time_reference=2190940753, version=1, umid=b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00', loudness_value=0, loudness_range=0, max_true_peak=0, max_momentary_loudness=0, max_shortterm_loudness=0, coding_history='A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\\r\\n')\n"
"WavBextFormat(description='sSPEED=023.976-ND\\r\\nsTAKE=1\\r\\nsUBITS=$12311801\\r\\nsSWVER=2.67\\r\\nsPROJECT=BMH\\r\\nsSCENE=A101\\r\\nsFILENAME=A101_1.WAV\\r\\nsTAPE=18Y12M31\\r\\nsTRK1=MKH516 A\\r\\nsTRK2=Boom\\r\\nsNOTE=\\r\\n', originator='Sound Dev: 702T S#GR1112089007', originator_ref='USSDVGR1112089007124001008206301', originator_date='2018-12-31', originator_time='12:40:00', time_reference=2190940753, version=1, umid=b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00', loudness_value=0.0, loudness_range=0.0, max_true_peak=0.0, max_momentary_loudness=0.0, max_shortterm_loudness=0.0, coding_history='A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\\r\\n')\n"
]
}
],
@@ -138,7 +138,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"WavBextFormat(description='dUBITS=12311804\\r\\ndSCENE=A101\\r\\ndTAKE=4\\r\\ndTAPE=18Y12M31\\r\\ndFRAMERATE=23.976ND\\r\\ndSPEED=023.976-NDF\\r\\ndTRK1=MKH516 A\\r\\ndTRK2=Boom\\r\\n', originator='Sound Dev: 702T S#GR1112089007', originator_ref='aa4CKtcd13Vk', originator_date='2018-12-31', originator_time='12:40:07', time_reference=2191709524, version=0, umid=b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00', loudness_value=0, loudness_range=0, max_true_peak=0, max_momentary_loudness=0, max_shortterm_loudness=0, coding_history='A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\\r\\n')\n"
"WavBextFormat(description='dUBITS=12311804\\r\\ndSCENE=A101\\r\\ndTAKE=4\\r\\ndTAPE=18Y12M31\\r\\ndFRAMERATE=23.976ND\\r\\ndSPEED=023.976-NDF\\r\\ndTRK1=MKH516 A\\r\\ndTRK2=Boom\\r\\n', originator='Sound Dev: 702T S#GR1112089007', originator_ref='aa4CKtcd13Vk', originator_date='2018-12-31', originator_time='12:40:07', time_reference=2191709524, version=0, umid=b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00', loudness_value=0.0, loudness_range=0.0, max_true_peak=0.0, max_momentary_loudness=0.0, max_shortterm_loudness=0.0, coding_history='A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\\r\\n')\n"
]
}
],

2
tests/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from . import test_wave_parsing

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
SOUND REPORT
Can't render this file because it contains an unexpected character in line 1 and column 53.

View File

@@ -0,0 +1,99 @@
import os.path
import json
import subprocess
from unittest import TestCase
import wavinfo
FFPROBE='/usr/local/bin/ffprobe'
def ffprobe(path):
arguments = [ FFPROBE , "-of", "json" , "-show_format", "-show_streams", path ]
process = subprocess.run(arguments, stdin=None, capture_output=True)
if process.returncode == 0:
return json.loads(process.stdout)
else:
return None
class TestWaveInfo(TestCase):
def all_files(self):
for dirpath, dirnames, filenames in os.walk('tests/test_files'):
for filename in filenames:
name, ext = os.path.splitext(filename)
if ext == '.wav':
yield os.path.join(dirpath, filename)
def test_sanity(self):
for wav_file in self.all_files():
info = wavinfo.WavInfoReader(wav_file)
self.assertTrue(info is not None)
def test_fmt_against_ffprobe(self):
for wav_file in self.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']) )
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 )
def test_data_against_ffprobe(self):
for wav_file in self.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'] ))
def test_bext_against_ffprobe(self):
for wav_file in self.all_files():
info = wavinfo.WavInfoReader(wav_file)
ffprobe_info = ffprobe(wav_file)
self.assertEqual( info.bext.description, ffprobe_info['format']['tags']['comment'] )
self.assertEqual( info.bext.originator, ffprobe_info['format']['tags']['encoded_by'] )
self.assertEqual( info.bext.originator_ref, ffprobe_info['format']['tags']['originator_reference'] )
# 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.coding_history, ffprobe_info['format']['tags']['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'},
}
for wav_file in self.all_files():
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 )

View File

@@ -125,11 +125,11 @@ class WavInfoReader():
time_reference = unpacked[5],
version = unpacked[6],
umid = unpacked[7],
loudness_value = unpacked[8],
loudness_range = unpacked[9],
max_true_peak = unpacked[10],
max_momentary_loudness = unpacked[11],
max_shortterm_loudness = unpacked[12],
loudness_value = unpacked[8] / 100.0,
loudness_range = unpacked[9] / 100.0,
max_true_peak = unpacked[10] / 100.0,
max_momentary_loudness = unpacked[11] / 100.0,
max_shortterm_loudness = unpacked[12] / 100.0,
coding_history = bext_data[rest_starts:].decode('ascii').rstrip('\0')
)