Skip to content

Commit

Permalink
Merge pull request #56 from nicholas-fr/master
Browse files Browse the repository at this point in the history
Audio mezzanine JSON metadata and long duration mezzanine audio creation
  • Loading branch information
nicholas-fr authored Apr 7, 2023
2 parents b74de51 + 14bee8d commit b458237
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 3 deletions.
54 changes: 54 additions & 0 deletions adb_update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env python

import argparse
import json
import os
import sys

from pathlib import Path

WAVE_MEZZ_RELEASES_URL = 'https://dash-large-files.akamaized.net/WAVE/Mezzanine/releases/'
FILE_WAV = '.wav'
FILE_JSON = '.json'

adb_json_filename = 'audio_mezzanine_database.json'
adb_json = {'audio':{}}

# Basic argument handling
parser = argparse.ArgumentParser(description="WAVE Audio Mezzanine JSON DB Creator.")
parser.add_argument('path', help="Path to folder containing audio mezzanine files.")

args = parser.parse_args()

if os.path.isdir(str(Path(args.path))):
mezz_path = Path(args.path)
else:
sys.exit('Invalid path to audio mezzanine files: ' + str(args.path))

print('Searching for all audio mezzanine files...')
wav_files = os.listdir(str(mezz_path))
for wav in wav_files:
if wav.startswith('PN') and wav.endswith(FILE_WAV):
print('Audio mezzanine found: ' + wav)
wav_json_file_path = Path(str(mezz_path) + '/' + wav[:-4]+FILE_JSON)
if os.path.isfile(str(wav_json_file_path)):
wav_json_file = open(wav_json_file_path)
wav_json = (json.load(wav_json_file))
wav_json_file.close()
print('Corresponding metadata found: ' + str(wav_json_file_path))
mezz_version = wav_json['Mezzanine']['version']
print('Mezzanine release version: ' + str(mezz_version))
else:
sys.exit('JSON metadata missing for ' + wav + '. Ensure JSON metadata files are in the same folder as their correponding audio mezzanine files.' )

adb_json['audio'][wav[:-4]] = {'path': WAVE_MEZZ_RELEASES_URL+str(mezz_version)+'/'+wav, 'json_path': WAVE_MEZZ_RELEASES_URL+str(mezz_version)+'/'+wav[:-4]+FILE_JSON}

# Save metadata to JSON file
adb_json_filepath = str(mezz_path) + '/' + adb_json_filename
adb_json_file = open(adb_json_filepath, "w")
json.dump(adb_json, adb_json_file, indent=4)
adb_json_file.write('\n')
adb_json_file.close()

print("Audio mezzanine JSON database saved: "+str(adb_json_filepath))
print()
61 changes: 61 additions & 0 deletions audioloop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env python

"""
This program takes an existing WAVE video mezzanine file (MP4) and an existing WAVE audio mezzanine file (WAV).
It loops the audio mezzanine and replaces the video mezzanine file's audio track with the looped audio.
"""

import argparse
import json
import os
import subprocess
import sys

from pathlib import Path

# Basic argument handling
parser = argparse.ArgumentParser(description="WAVE Mezzanine Audio: looping white noise.")
parser.add_argument('input1', help="Source mezzanine file.")
parser.add_argument('input2', help="Source audio file to loop.")

args = parser.parse_args()

# Check that source files are present
if not os.path.isfile(args.input1):
sys.exit("Source file \""+args.input1+"\" does not exist.")
if not os.path.isfile(args.input2):
sys.exit("Source file \""+args.input2+"\" does not exist.")

mezzanine = Path(args.input1)
second_audio = Path(args.input2)
mezzanine_out = Path(str(mezzanine.stem)+'_'+str(second_audio.stem)+str(mezzanine.suffix))

print("Creating new mezzanine file using video from "+str(mezzanine)+" and (looped) audio from "+str(second_audio))

# Detect video mezzanine duration
source_videoproperties = subprocess.check_output(
['ffprobe', '-i', str(mezzanine), '-show_streams', '-select_streams', 'v', '-loglevel', '0', '-print_format', 'json'])
source_videoproperties_json = json.loads(source_videoproperties)
duration = int(source_videoproperties_json['streams'][0]['duration'].split('.')[0])

# Create audio track with the same duration as the video mezzanine by looping audio mezzanine
subprocess.run(['ffmpeg',
'-stream_loop', '-1', '-i', str(second_audio), '-t', str(duration),
'-y',
str(second_audio.stem)+'_looped.wav'])

# Encode and mux new audio track with video mezzanine
# ffmpeg -i <mezzanine_file> -i second_audio_looped.wav -map 0:v -map 1:a -c:v copy -c:a aac -b:a 320k -ac 2 <mezzanine_file_with_new_audio>
subprocess.run(['ffmpeg',
'-i', str(mezzanine),
'-i', str(second_audio.stem)+'_looped.wav',
'-map', '0:v',
'-map', '1:a',
'-c:v','copy',
'-c:a','aac',
'-b:a', '320k', '-ac', '2',
'-y',
str(mezzanine_out)])

os.remove(str(second_audio.stem)+'_looped.wav')
print("Done")
172 changes: 171 additions & 1 deletion audiomezz.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,111 @@
by the CTA WAVE Project.
"""
import argparse
import hashlib
import json
import numpy as np
import os
import re
import scipy.io.wavfile as wav
import sys
import time

from datetime import date
from json import JSONEncoder
from pathlib import Path
from scipy import signal


class MezzanineProperties:
channel_count = 0
bits_per_sample = 0
sample_rate = 0
duration = 0
codec = ''

def __init__(self, channel_count=None, bits_per_sample=None, sample_rate=None, duration=None, codec=None):
if channel_count is not None:
self.channel_count = channel_count
if bits_per_sample is not None:
self.bits_per_sample = bits_per_sample
if sample_rate is not None:
self.sample_rate = sample_rate
if duration is not None:
self.duration = duration
if codec is not None:
self.codec = codec

def json(self):
return {
'channel_count': self.channel_count,
'bits_per_sample': self.bits_per_sample,
'sample_rate': self.sample_rate,
'duration': self.duration,
'codec': self.codec
}


class Mezzanine:
name = ''
URI = ''
version = 0
specification_version = 0
creation_date = 'YYYY-MM-DD'
seed = ''
license = ''
command_line = ''
md5 = ''
properties = MezzanineProperties()

def __init__(self, name=None, uri=None, version=None, specification_version=None,
creation_date=None, seed=None, license=None, cl=None,
md5=None, properties=None):
if name is not None:
self.name = name
if uri is not None:
self.URI = uri
if version is not None:
self.version = version
if specification_version is not None:
self.specification_version = specification_version
if creation_date is not None:
self.creation_date = creation_date
if seed is not None:
self.seed = seed
if license is not None:
self.license = license
if cl is not None:
self.command_line = cl
if md5 is not None:
self.md5 = md5
if properties is not None:
self.properties = properties

def json(self):
properties = self.properties.json()
return {
'Mezzanine': {
'name': self.name,
'URI': self.URI,
'version': self.version,
'specification_version': self.specification_version,
'creation_date': self.creation_date,
'seed': self.seed,
'license': re.sub(' +', ' ', self.license.replace('\n', ' ')),
'command_line': self.command_line,
'md5': self.md5,
'properties': properties
}
}


class MezzanineEncoder(JSONEncoder):
def default(self, o):
if "json" in dir(o):
return o.json()
return JSONEncoder.default(self, o)


def check_channels(ch):
int_ch = int(ch)
if int_ch < 1:
Expand All @@ -29,6 +125,8 @@ def check_channels(ch):
samplerate = 48000 # [Hz]
bw = 7000 # [Hz]
silentstart = 0
mezz_version = 0
mezz_specification_version = 0

# Basic argument handling for the following: -d -f -s -c output
parser = argparse.ArgumentParser(description="Test of audio PN noise and correlation methods")
Expand All @@ -50,6 +148,19 @@ def check_channels(ch):
'--silentstart', required=False, choices=['0', '1'],
help="Determines whether the audio file generated has initial silence (=1) or not (=0). "
"Default: "+str(silentstart))
parser.add_argument(
'--spec-version',
required=False,
type=int,
help="The version of the mezzanine annotation specification that the mezzanine generated will be compliant with. "
"Default: "+str(mezz_specification_version))
parser.add_argument(
'-v', '--version',
required=False,
type=int,
help="The official mezzanine release version that the mezzanine generated are intended for. "
"Default: "+str(mezz_version))

parser.add_argument('output', help="Output file (fname.ftp).")
args = parser.parse_args()

Expand All @@ -63,8 +174,10 @@ def check_channels(ch):

# Set parameters to values provided in arguments
if args.seed is not None:
seed_base = args.seed
seed = int(''.join('{0:03n}'.format(ord(c)) for c in args.seed))
else:
seed_base = output.name
seed = int(''.join('{0:03n}'.format(ord(c)) for c in output.name))
if args.duration is not None:
duration = args.duration
Expand All @@ -74,10 +187,17 @@ def check_channels(ch):
channels = args.channels
if args.silentstart is not None:
silentstart = int(args.silentstart)
if args.spec_version is not None:
mezz_specification_version = args.spec_version
if args.version is not None:
mezz_version = args.version

# Initialise mezzanine properties metadata
mezz_properties = MezzanineProperties(channels, 16, samplerate, duration, 'Signed Linear PCM')

# Generate a binary noise array from a uniform distribution. The array ndata is of type ndarray (1-D)
ss = np.random.SeedSequence(seed)
print('seed base = '+args.seed)
print('seed base = '+seed_base)
print('seed = {}'.format(ss.entropy))
bg = np.random.PCG64(ss) # bg == Bit Generator
gen = np.random.Generator(bg) # gen == instance of generator class
Expand Down Expand Up @@ -112,4 +232,54 @@ def check_channels(ch):
# Write audio generated to wave file
wav.write(output, samplerate, mc_data.astype(np.int16))

# Output metadata
# Import CTA mezzanine license if available
mezz_license = ""
try:
mezz_license_file = open(str(Path('audiomezz_CTA_LICENSE.txt')), encoding="utf-8")
mezz_license = mezz_license_file.read()
mezz_license_file.close()
except OSError:
print("Failed to load mezzanine CTA LICENSE file. Ensure the file is located in the same folder as this script "
"with the name audiomezz_CTA_LICENSE.txt.")

# Calculate MD5 hash
BLOCK_SIZE = 65536
mezz_file_hash = hashlib.md5()
with open(output, 'rb') as mezz_file:
mezz_file_block = mezz_file.read(BLOCK_SIZE)
while len(mezz_file_block) > 0: # Until all data has been read from mezzanine file
mezz_file_hash.update(mezz_file_block) # Update hash
mezz_file_block = mezz_file.read(BLOCK_SIZE) # Read the next block from mezzanine file

mezz_metadata = Mezzanine(output.stem, './'+output.name, mezz_version, mezz_specification_version, date.today().isoformat(),
seed_base, mezz_license, str(Path(__file__).resolve().name)+' '+' '.join(sys.argv[1:]), mezz_file_hash.hexdigest(), mezz_properties)

print()
print("Name: "+mezz_metadata.name)
print("URI: "+mezz_metadata.URI)
print("Version: "+str(mezz_metadata.version))
print("Spec version: "+str(mezz_metadata.specification_version))
print("Creation date: "+mezz_metadata.creation_date)
print("Seed: "+str(mezz_metadata.seed))
print("License: "+mezz_metadata.license)
print("CL used: "+mezz_metadata.command_line)
print("MD5: "+mezz_metadata.md5)
print()
print("Channel count: "+str(mezz_metadata.properties.channel_count))
print("Bits per sample: "+str(mezz_metadata.properties.bits_per_sample))
print("Sample rate: "+str(mezz_metadata.properties.sample_rate))
print("Duration: "+str(mezz_metadata.properties.duration))
print("Codec: "+mezz_metadata.properties.codec)
print()

# Save metadata to JSON file
mezz_metadata_filepath = Path(str(output.parent)+'\\'+str(output.stem)+'.json')
mezz_metadata_file = open(str(mezz_metadata_filepath), "w")
json.dump(mezz_metadata, mezz_metadata_file, indent=4, cls=MezzanineEncoder)
mezz_metadata_file.write('\n')
mezz_metadata_file.close()

print("Mezzanine metadata stored in: "+str(mezz_metadata_filepath))

print("\n End PNFiles at "+str(time.asctime(time.gmtime())))
1 change: 1 addition & 0 deletions audiomezz_CTA_LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
© Consumer Technology Association (CTA)®, licensed under Creative Commons Attribution 4.0 International (CC BY 4.0) (https://creativecommons.org/licenses/by/4.0/) / annotated, encoded and compressed from original.
20 changes: 20 additions & 0 deletions metadata/audio_mezzanine_database.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"audio": {
"PN01": {
"path": "https://dash-large-files.akamaized.net/WAVE/Mezzanine/releases/4/PN01.wav",
"json_path": "https://dash-large-files.akamaized.net/WAVE/Mezzanine/releases/4/PN01.json"
},
"PN02": {
"path": "https://dash-large-files.akamaized.net/WAVE/Mezzanine/releases/4/PN02.wav",
"json_path": "https://dash-large-files.akamaized.net/WAVE/Mezzanine/releases/4/PN02.json"
},
"PN03": {
"path": "https://dash-large-files.akamaized.net/WAVE/Mezzanine/releases/4/PN03.wav",
"json_path": "https://dash-large-files.akamaized.net/WAVE/Mezzanine/releases/4/PN03.json"
},
"PN04": {
"path": "https://dash-large-files.akamaized.net/WAVE/Mezzanine/releases/4/PN04.wav",
"json_path": "https://dash-large-files.akamaized.net/WAVE/Mezzanine/releases/4/PN04.json"
}
}
}
5 changes: 3 additions & 2 deletions mezzanine.py
Original file line number Diff line number Diff line change
Expand Up @@ -983,8 +983,8 @@ def default(self, o):
mezz_file_block = mezz_file.read(BLOCK_SIZE) # Read the next block from mezzanine file

mezz_metadata = Mezzanine(output.stem, mezz_version, mezz_specification_version, date.today().isoformat(), mezz_license,
'./'+output.name, ' '.join(sys.argv), ' '.join(ffmpeg_cl).replace('\t', ''),
mezz_file_hash.hexdigest(), mezz_properties, mezz_source)
'./'+output.name, str(Path(__file__).resolve().name)+' '+' '.join(sys.argv[1:]),
' '.join(ffmpeg_cl).replace('\t', ''), mezz_file_hash.hexdigest(), mezz_properties, mezz_source)

print()
print()
Expand Down Expand Up @@ -1025,6 +1025,7 @@ def default(self, o):
# Save metadata to JSON file
mezz_metadata_file = open(str(mezz_metadata_filepath), "w")
json.dump(mezz_metadata, mezz_metadata_file, indent=4, cls=MezzanineEncoder)
mezz_metadata_file.write('\n')
mezz_metadata_file.close()

print("Mezzanine metadata stored in: "+str(mezz_metadata_filepath))
Expand Down

0 comments on commit b458237

Please sign in to comment.