Source code for cutcutcodec.core.classes.layout

"""The significance of the audio and video frame channels."""

import numbers
import re
import subprocess

from cutcutcodec.core.opti.cache.singleton import MetaSingleton


[docs] class AllLayouts(metaclass=MetaSingleton): """Parse the ``ffmpeg -layouts`` command line. Attributes ---------- individuals : dict[str, str] To each channel canonical name in lower case, associate the description (readonly). layouts : dict[str, tuple[str, ...]] To each layouts name, associate the names of the channels (readonly). """ def __init__(self): doc = "\n" + subprocess.run( ("ffmpeg", "-v", "error", "-layouts"), capture_output=True, check=True, ).stdout.decode() if (match := re.search(r"\n.+:\s+NAME[ \t]+DESCRIPTION", doc)) is None: raise RuntimeError(f"impossible to find invidual channels description in {doc}") ind = match.span() if (match := re.search(r"\n.+:\s+NAME[ \t]+DECOMPOSITION", doc)) is None: raise RuntimeError(f"impossible to find standard channel layouts description in {doc}") lay = match.span() ind, lay = ( (ind[1], lay[0] if lay[0] > ind[1] else len(doc)), (lay[1], ind[0] if ind[0] > lay[1] else len(doc)), ) ind, lay = doc[ind[0]:ind[1]], doc[lay[0]:lay[1]] self._individuals = { m["name"].lower(): m["desc"] for m in re.finditer(r"(?P<name>[a-zA-Z0-9]+)[ \t]+(?P<desc>.+)", ind) } self._layouts = { m["name"].lower(): tuple(c.lower() for c in m["channels"].split("+")) for m in re.finditer(r"(?P<name>[\w\.\-\(\)]+)[ \t]+(?P<channels>[a-zA-Z0-9\+]+)", lay) } @property def individuals(self) -> dict[str, str]: """To each channel canonical name in lower case, associate the description. Examples -------- >>> from pprint import pprint >>> from cutcutcodec.core.classes.layout import AllLayouts >>> pprint(AllLayouts().individuals) # doctest: +ELLIPSIS {'bc': 'back center', ... 'wl': 'wide left', 'wr': 'wide right'} >>> """ return self._individuals.copy() @property def layouts(self) -> dict[str, list[str]]: """To each layouts name, associate the names of the channels. Examples -------- >>> from pprint import pprint >>> from cutcutcodec.core.classes.layout import AllLayouts >>> pprint(AllLayouts().layouts) # doctest: +ELLIPSIS {'2.1': ('fl', 'fr', 'lfe'), ... 'stereo': ('fl', 'fr')} >>> """ return self._layouts.copy()
[docs] class Layout: """An audio profile. Attributes ---------- channels : tuple[tuple[str, str], ...] Each audio channel (readonly) as an ordered dict with: The canonical name of the audio channel and a human description of the audio channel. name : str The canonical name of the audio profile (readonly). """ def __init__(self, name: str | numbers.Integral): """Initialise and create the class. Parameters ---------- name : str or numbers.Integral The canonical name of the audio profile. If an integer is provide, a default profile is assigned. """ if isinstance(name, numbers.Integral): if ( name_ := { 1: "mono", 2: "stereo", 3: "2.1", 4: "quad", 5: "4.1", 6: "5.1", 7: "6.1", 8: "7.1", 10: "5.1.4", 12: "7.1.4", 16: "hexadecagonal", 24: "22.2", }.get(name) ) is None: raise ValueError( f"only 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 16, or 24 audio channels, not {name}", ) name = name_ assert isinstance(name, str), name.__class__.__name__ audio_profile = AllLayouts() name = {"1 channel": "mono", "2 channels": "stereo"}.get(name, name) # debug .wav files if (channels := audio_profile.layouts.get(name, None)) is None: raise ValueError( f"profile audio {name} not in {sorted(AllLayouts().layouts)}", ) self._name = str(name) self._channels = tuple((c, audio_profile.individuals[c]) for c in channels) def __hash__(self): """Make this object able to be used in set or as dict key.""" return hash(self._name) def __eq__(self, other) -> bool: """Make this object able to be used in set or as dict key.""" if not isinstance(other, Layout): return False return other.name == self._name def __len__(self) -> int: """Return the numbers of channels.""" return len(self._channels) def __repr__(self) -> str: """Give a nice representation of the layout.""" return f"{self.__class__.__name__}({self._name!r})" @property def channels(self) -> tuple[tuple[str, str], ...]: """Return the description of each channels. Examples -------- >>> from pprint import pprint >>> from cutcutcodec.core.classes.layout import Layout >>> Layout("stereo").channels (('fl', 'front left'), ('fr', 'front right')) >>> pprint(Layout("5.1").channels) (('fl', 'front left'), ('fr', 'front right'), ('fc', 'front center'), ('lfe', 'low frequency'), ('bl', 'back left'), ('br', 'back right')) >>> """ return self._channels @property def name(self) -> str: """Return the canonical name of the audio profile. Examples -------- >>> from cutcutcodec.core.classes.layout import Layout >>> Layout(2).name 'stereo' >>> """ return self._name