"""Lambda to begin step machine execution upon S3 trigger.""" import json import os from common.helpers.bulk_asset import load_bulk_asset_json from common.lambda_exceptions import TranscodingException import config from constants.audio import CD_AUDIO from constants.audio import DAT_AUDIO from constants.audio import GOOD_16_SAMPLERATES from constants.audio import GOOD_24_SAMPLERATES from constants.audio import GOOD_AUDIO_SPECS from constants.audio import GOOD_CHANNELS from constants.audio import GOOD_FORMATS from constants.audio import GOOD_SUBTYPES from constants.audio import NEW_ROOT_SUBFOLDER from constants.audio import PCM_16_SUBTYPE from constants.audio import PCM_24_SUBTYPE from constants.audio import PRO_AUDIO from constants.audio import WAVE_PCM_FORMAT import soundfile as sf from util.audio_utils import get_audio_stream_from_s3 from util.audio_utils import process_audio from util.audio_utils import save_audio_to_s3 from util.audio_utils import validate_file_key def handler(event, context): """Lambda handler entry point.""" correlation_id = event.get('correlation_id') logger = config.get_current_logger(correlation_id) # Show event content logger.info(f'pre_process_audio received: {event}') # DDEX_INGESTER_INTEGRATION: Skip ddex-ingester context JSON, load basic # JSON elements asset = load_bulk_asset_json(event) try: pre_process_audio(asset, correlation_id) except Exception as e: msg = f'Fatal audio pre-processing error for ' \ f'UPC {asset.product.upc} ' \ f'Product ID {asset.product.product_id} ' \ f'TUID {asset.product.track.tuid}: {str(e)}' logger.error(msg) raise e asset.product.track.key = os.path.join(NEW_ROOT_SUBFOLDER, asset.product.track.bucket, asset.product.track.key) asset.product.track.bucket = config.STAGING_S3_BUCKET return json.loads(json.dumps(asset, default=lambda s: vars(s))) def audio_spec_check(audio_file, file_name, correlation_id): """Check if an audio file is within Orchard specifications.""" logger = config.get_current_logger(correlation_id) target_format = audio_file.format target_subtype = audio_file.subtype target_samplerate = audio_file.samplerate target_channels = audio_file.channels audio_file_params = ( target_format, target_subtype, target_samplerate, target_channels ) # Default - no op wav = None # File is in spec if audio_file_params in GOOD_AUDIO_SPECS: return wav # Check for bad format / container (not WAV / FLAC) if target_format not in GOOD_FORMATS: logger.info(f'Transcoding {file_name} from {audio_file.format} ' f'to {WAVE_PCM_FORMAT}') target_format = WAVE_PCM_FORMAT # Check for bad subtype (not 16-bit or 24-bit) if target_subtype not in GOOD_SUBTYPES: logger.info(f'Transcoding {file_name} from {audio_file.subtype} ' f'to {PCM_24_SUBTYPE}') target_subtype = PCM_24_SUBTYPE # Check for bad num of channels (stereo) if target_channels not in GOOD_CHANNELS: msg = f'Transcoding error: non-stereo file. {file_name} has ' \ f'{audio_file.channels} channels.' logger.info(msg) raise TranscodingException(msg) # Up/Downsample out-of-bounds if target_samplerate < CD_AUDIO: # Boost samplerate to CD logger.info(f'Transcoding {file_name} from ' f'{audio_file.samplerate} to {CD_AUDIO}') target_samplerate = CD_AUDIO elif target_samplerate > PRO_AUDIO: # Downsample to pro audio logger.info(f'Transcoding {file_name} from ' f'{audio_file.samplerate} to {PRO_AUDIO}') target_samplerate = PRO_AUDIO # Check for bad samplerate / subtype combos if target_subtype == PCM_16_SUBTYPE: # Bad 16-bit samplerate if target_samplerate not in GOOD_16_SAMPLERATES: # But good 24-bit samplerate if target_samplerate in GOOD_24_SAMPLERATES: # Boost subtype to 24-bit logger.info(f'{audio_file.subtype} file found with ' f'{target_samplerate} samplerate. Transcoding ' f'{file_name} from {audio_file.subtype} ' f'to {PCM_24_SUBTYPE}') target_subtype = PCM_24_SUBTYPE else: # Resample to 48Khz logger.info(f'Transcoding {file_name} from ' f'{audio_file.samplerate} to {DAT_AUDIO}') target_samplerate = DAT_AUDIO # Transform wav file. wav = process_audio( audio_file, audio_format=target_format, subtype=target_subtype, channels=target_channels, samplerate=target_samplerate) return wav def pre_process_audio(asset, correlation_id): """Pre-process an audio file.""" product = asset.product bucket = product.track.bucket key = product.track.key file_name = os.path.split(key)[1] logger = config.get_current_logger(correlation_id) # Check if file is valid audio file_type logger.info(f'Validating audio file: {bucket}/{key}') validate_file_key(bucket, key) # Get file as byte stream logger.info(f'Converting file to byte object: {bucket}/{key}') audio_stream = get_audio_stream_from_s3(bucket, key) # Get stream as wav logger.info(f'Opening audio file: {bucket}/{key}') audio_file = sf.SoundFile(audio_stream) # Default no-op for transcoding wav = None # Check if audio is a bad format / container wav = audio_spec_check(audio_file, file_name, correlation_id) logger.info(f'Saving audio file: {bucket}/{key}') new_target_key = os.path.join(NEW_ROOT_SUBFOLDER, bucket, key) save_audio_to_s3( wav, config.STAGING_S3_BUCKET, new_target_key, bucket, key, correlation_id )