"""Lambda function module.""" import logging import json import subprocess import time from typing import List import config from constants import general from lambdacommon.constants import errors from lambdacommon import lambda_exceptions from lambdacommon import s3 from lambdacommon import sentry from lambdacommon import util def exec_and_get_stderr(command: List[str]) -> List[str]: logging.info(f"Exec: {' '.join(command)}") started = time.time() with subprocess.Popen(command, stderr=subprocess.PIPE, universal_newlines=True) as proc: result = proc.stderr.readlines() finished = time.time() logging.info(f'Calculated time: {finished - started} sec') return result def read_lodnorm_json_measurements(lines: List[str]): is_in_filter_output = False lodnorm_output_lines = [] for line in lines: line = line.strip() if is_in_filter_output: lodnorm_output_lines.append(line) if line == '}': break else: if line == '{': is_in_filter_output = True lodnorm_output_lines.append(line) if not lodnorm_output_lines: return None measures_raw_output = ''.join(lodnorm_output_lines) try: return json.loads(measures_raw_output) except ValueError: logging.error(f"Cannot decode JSON measures: {measures_raw_output}") raise def get_measures_command(input_file): loudnorm_measure_command = f"loudnorm=I={config.TARGET_LUFS}:dual_mono=true:TP={config.TARGET_TRUE_PEAK}:LRA={config.TARGET_LRA}:print_format=json" return f"./bin/ffmpeg -i {input_file} -af {loudnorm_measure_command} -f null -" def get_modify_command(input_file, output_file, i, tp, lra, thresh, offset): loudnorm_modify_command = f"loudnorm=I={config.TARGET_LUFS}:{config.TARGET_TRUE_PEAK}:LRA={config.TARGET_LRA}" + \ f":measured_I={i}:measured_TP={tp}:measured_LRA={lra}" + \ f":measured_thresh={thresh}:offset={offset}:linear=true:print_format=json" return f"./bin/ffmpeg -i {input_file} -af {loudnorm_modify_command} -y {output_file}" def handler(event, context): """Lambda entry point. Args: event (dict): Information about uploaded image. context (dict): Environment state. Returns: dict: Dict with validation result or throw exception. { 'status': 'OK', 'message': '', } """ bucket, key = None, None try: bucket, key = util.extract_asset_identifiers(event) if not s3.object_exists(bucket, key): lambda_exceptions.notify_and_raise( general.LAMBDA_NAME, general.AUDIO_VALIDATION_ERROR_STATUS, errors.S3_FILE_NOT_FOUND_CODE, key, bucket, { 'key': key, 'bucket': bucket}) # audio_validation_logic.validate_audio_asset(bucket, key) source = f'https://{bucket}.s3.amazonaws.com/{key}' command = get_measures_command(source) stderr = exec_and_get_stderr(command.split()) logging.info(stderr) measures = read_lodnorm_json_measurements(stderr) logging.info(f"Source measures are: {json.dumps(measures, indent=4, sort_keys=True)}") # { # 'input_i': '-8.71', # 'input_tp': '-2.12', # 'input_lra': '0.40', # 'input_thresh': '-21.76', # 'output_i': '-12.71', # 'output_tp': '-6.13', # 'output_lra': '0.00', # 'output_thresh': '-25.69', # 'normalization_type': 'dynamic', # 'target_offset': '-3.29' # } output = '/tmp/output.wav' command = get_modify_command(input_file=source, output_file=output, i=measures['input_i'], tp=measures['input_tp'], lra=measures['input_lra'], thresh=measures['input_thresh'], offset=measures['target_offset'] ) stderr = exec_and_get_stderr(command.split()) logging.info(stderr) measures = read_lodnorm_json_measurements(stderr) logging.info(f"Final measures are: {json.dumps(measures, indent=4, sort_keys=True)}") logging.info(f"OUTPUT FILE: {output}") # status.send_general_status( # general.LAMBDA_NAME, general.AUDIO_VALIDATION_COMPLETE_STATUS, key, # bucket=bucket) return {'status': 'OK', 'message': ''} except Exception as e: if sentry.sentry_client: sentry.sentry_client.captureException() lambda_exceptions.notify_and_raise( general.LAMBDA_NAME, general.AUDIO_VALIDATION_ERROR_STATUS, errors.AUDIO_VALIDATION_ERROR_CODE, key, bucket, { 'message': str(e)})