# Silent-object and silent-height checks, from Sony QC's blank-object/height scanner # (ventura_1_1.py analyze_blank_objects_auto): each channel's peak sample level over the whole # track is compared against the silence threshold. Any silent OBJECT channel warns. Silent bed # HEIGHT channels warn only when no object is active (Sony's gating): with active objects the # height content can legitimately live in the objects, but silent heights with no active objects # means zero height content anywhere — effectively a flat mix in an Atmos container. Sony # classifies channels positionally from MediaInfo; we classify from the parsed ADM instead — the # same channels on a standard master. import math from typing import Final import numpy as np import numpy.typing as npt from pydantic import BaseModel, ConfigDict from src.atmos.models import ( SILENCE_FLOOR_DBFS, AtmosValidationBuilder, AtmosValidationFindingCode, ) # Sony's comparison is inclusive: a peak exactly at the threshold IS silent. SILENT_OBJECT_WARN_DBFS: Final = -120.0 class SilentObjectMeasurement(BaseModel): model_config = ConfigDict(frozen=True) object_track_count: int silent_object_track_indices: tuple[int, ...] height_track_count: int silent_height_track_indices: tuple[int, ...] def to_silent_object_measurement( channel_peaks: npt.NDArray[np.float64], object_track_indices: tuple[int, ...], bed_height_track_indices: tuple[int, ...], ) -> SilentObjectMeasurement: # channel_peaks holds each source channel's max |sample| over the whole track. return SilentObjectMeasurement( object_track_count=len(object_track_indices), silent_object_track_indices=_silent_track_indices(channel_peaks, object_track_indices), height_track_count=len(bed_height_track_indices), silent_height_track_indices=_silent_track_indices(channel_peaks, bed_height_track_indices), ) def _silent_track_indices(channel_peaks: npt.NDArray[np.float64], track_indices: tuple[int, ...]) -> tuple[int, ...]: return tuple( track_index for track_index in track_indices if _peak_dbfs(float(channel_peaks[track_index])) <= SILENT_OBJECT_WARN_DBFS ) def _peak_dbfs(peak: float) -> float: if peak <= 0.0: return SILENCE_FLOOR_DBFS return 20.0 * math.log10(peak) def apply_silent_object_measurement( silent_object_measurement: SilentObjectMeasurement, atmos_validation_builder: AtmosValidationBuilder, ) -> None: atmos_validation_builder.update_metadata( object_track_count=silent_object_measurement.object_track_count, silent_object_track_indices=silent_object_measurement.silent_object_track_indices, height_track_count=silent_object_measurement.height_track_count, silent_height_track_indices=silent_object_measurement.silent_height_track_indices, ) _check_silent_objects(silent_object_measurement, atmos_validation_builder) _check_silent_heights(silent_object_measurement, atmos_validation_builder) def _check_silent_objects( silent_object_measurement: SilentObjectMeasurement, atmos_validation_builder: AtmosValidationBuilder, ) -> None: if silent_object_measurement.silent_object_track_indices: atmos_validation_builder.warning( AtmosValidationFindingCode.SILENT_OBJECT, f"{len(silent_object_measurement.silent_object_track_indices)} of " f"{silent_object_measurement.object_track_count} object channels are silent " f"(peak at or below {SILENT_OBJECT_WARN_DBFS} dBFS): source channel index " f"{', '.join(str(i) for i in silent_object_measurement.silent_object_track_indices)}", ) def _check_silent_heights( silent_object_measurement: SilentObjectMeasurement, atmos_validation_builder: AtmosValidationBuilder, ) -> None: if not silent_object_measurement.silent_height_track_indices: return # Sony's gating: with at least one active object the height content can live in the objects, so # silent bed heights are a mixing choice, not a defect. has_active_objects = silent_object_measurement.object_track_count > len( silent_object_measurement.silent_object_track_indices ) if has_active_objects: return atmos_validation_builder.warning( AtmosValidationFindingCode.SILENT_HEIGHT, f"{len(silent_object_measurement.silent_height_track_indices)} of " f"{silent_object_measurement.height_track_count} bed height channels are silent with no " f"active objects — the master carries no height content: source channel index " f"{', '.join(str(i) for i in silent_object_measurement.silent_height_track_indices)}", )