UMID Implementation

This commit is contained in:
Jamie Hardt
2020-01-06 08:27:34 -08:00
parent 1b9547e8c2
commit 5a30ce3afc
4 changed files with 185 additions and 203 deletions

Binary file not shown.

View File

@@ -1,5 +1,10 @@
import struct
from typing import Union from typing import Union
import binascii
def binary_to_string(binary_value):
return str(binascii.hexlify(binary_value), encoding='ascii')
class UMIDParser: class UMIDParser:
""" """
@@ -9,121 +14,111 @@ 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 = '{:x}'.format(binary_value[n]) + 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:13]) + '-' + binary_to_string(self.raw_umid[13:3])
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

@@ -1,21 +1,22 @@
import struct import struct
import binascii 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 best_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 sanatize_bytes(bytes):
first_null = next( (index for index, byte in enumerate(bytes) if byte == 0 ), None ) first_null = next((index for index, byte in enumerate(bytes) if byte == 0), None)
if first_null is not None: if first_null is not None:
trimmed = bytes[:first_null] trimmed = bytes[:first_null]
else: else:
@@ -25,68 +26,67 @@ class WavBextReader:
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 = sanatize_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 = sanatize_bytes(unpacked[1])
#: A unique identifer for the file, a serial number. #: A unique identifer for the file, a serial number.
self.originator_ref = sanatize_bytes(unpacked[2]) self.originator_ref = sanatize_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 = sanatize_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 = sanatize_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 = sanatize_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 umid_to_str(self):
if self.umid:
return str(binascii.hexlify(self.umid), encoding='ascii')
else:
return None
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,
'umid': self.umid_to_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
}
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,48 @@ 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 \ self.info_chunk = next((chunk for chunk in list_chunks if chunk.signature == b'INFO'), None)
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')
#: '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 #: 'ISFT' Software
self.software = self._get_field(f,b'ISFT') 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 +59,24 @@ 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, 'genre': self.genre,
'artist': self.artist, 'artist': self.artist,
'comment': self.comment, '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
} }