Skip to content

extension

do_audio(fileIn, track=0, trims=None, fps=None, num_frames=0, extractor=FFMpeg.Extractor(), trimmer=AutoTrimmer(), encoder=AutoEncoder(), quiet=True, output=None)

One-liner to handle the whole audio processing

Parameters:

Name Type Description Default
fileIn PathLike | src_file | AudioNode

Input file or src_file/FileInfo or AudioNode

required
track int

Audio track number

0
trims Trim | list[Trim] | None

Frame ranges to trim and/or combine, e.g. (24, -24) or [(24, 500), (700, 900)] If your passed src_file has a trim it will use it. Any other trims passed here will overwrite it.

None
fps Fraction | PathLike | None

FPS Fraction used for the conversion to time Will be taken from input if it's a src_file and assume the usual 24 if not. Also accepts a timecode (v2) file.

None
num_frames int

Total number of frames, used for negative numbers in trims Will be taken from input if it's a src_file

0
extractor Extractor

Tool used to extract the audio (Will default to None if an AudioNode gets passed)

Extractor()
trimmer Trimmer | None

Tool used to trim the audio AutoTrimmer means it will choose ffmpeg for lossy and Sox for lossless

AutoTrimmer()
encoder Encoder | None

Tool used to encode the audio AutoEncoder means it won't reencode lossy and choose opus otherwise

AutoEncoder()
quiet bool

Whether the tool output should be visible

True
output PathLike | None

Custom output file or directory, extensions will be automatically added

None

Returns:

Type Description
AudioFile

AudioFile Object containing file path, delays and source

Source code in vsmuxtools/extension/audio.py
def do_audio(
    fileIn: PathLike | src_file | vs.AudioNode,
    track: int = 0,
    trims: Trim | list[Trim] | None = None,
    fps: Fraction | PathLike | None = None,
    num_frames: int = 0,
    extractor: Extractor = FFMpeg.Extractor(),
    trimmer: Trimmer | None = AutoTrimmer(),
    encoder: Encoder | None = AutoEncoder(),
    quiet: bool = True,
    output: PathLike | None = None,
) -> AudioFile:
    """
    One-liner to handle the whole audio processing

    :param fileIn:          Input file or src_file/FileInfo or AudioNode
    :param track:           Audio track number
    :param trims:           Frame ranges to trim and/or combine, e.g. (24, -24) or [(24, 500), (700, 900)]
                            If your passed src_file has a trim it will use it. Any other trims passed here will overwrite it.

    :param fps:             FPS Fraction used for the conversion to time
                            Will be taken from input if it's a src_file and assume the usual 24 if not.
                            Also accepts a timecode (v2) file.

    :param num_frames:      Total number of frames, used for negative numbers in trims
                            Will be taken from input if it's a src_file

    :param extractor:       Tool used to extract the audio (Will default to None if an AudioNode gets passed)
    :param trimmer:         Tool used to trim the audio
                            AutoTrimmer means it will choose ffmpeg for lossy and Sox for lossless

    :param encoder:         Tool used to encode the audio
                            AutoEncoder means it won't reencode lossy and choose opus otherwise

    :param quiet:           Whether the tool output should be visible
    :param output:          Custom output file or directory, extensions will be automatically added
    :return:                AudioFile Object containing file path, delays and source
    """
    if trims is not None:
        if isinstance(fileIn, src_file):
            danger("Other trims passed will overwrite whatever your src_file has!", do_audio, 1)
        if isinstance(fileIn, vs.AudioNode):
            danger("Trims won't be applied if you pass an Audionode. Just do them yourself before this lol.", do_audio, 1)
            trims = None
            trimmer = None

    if isinstance(fileIn, vs.AudioNode):
        extractor = None
        fileIn = export_audionode(fileIn)
    elif isinstance(fileIn, src_file):
        if not trims:
            trims = fileIn.trim
        clip = fileIn.src
        num_frames = clip.num_frames
        fps = Fraction(clip.fps_num, clip.fps_den) if fps is None else fps
        fileIn = fileIn.file

    fps = Fraction(24000, 1001) if fps is None else fps
    return mt_audio(fileIn, track, trims, fps, num_frames, extractor, trimmer, encoder, quiet, output)

export_audionode(node, outfile=None)

Exports an audionode to a wav/w64 file.

Parameters:

Name Type Description Default
node AudioNode

Your audionode

required
outfile PathLike | None

Custom output path if any

None

Returns:

Type Description
Path

Returns path

Source code in vsmuxtools/extension/audio.py
def export_audionode(node: vs.AudioNode, outfile: PathLike | None = None) -> Path:
    """
    Exports an audionode to a wav/w64 file.

    :param node:            Your audionode
    :param outfile:         Custom output path if any

    :return:                Returns path
    """
    if not outfile:
        outfile = uniquify_path(Path(get_workdir(), "exported.wav"))

    outfile = ensure_path(outfile, export_audionode)
    with open(outfile, "wb") as bf:
        audio_async_render(node, bf)
    return outfile

Chapters

Bases: Chapters

Source code in vsmuxtools/extension/chapters.py
class Chapters(Ch):
    def __init__(
        self, chapter_source: src_file | PathLike | GlobSearch | Chapter | list[Chapter], fps: Fraction | PathLike | None = None, _print: bool = True
    ) -> None:
        """
        Convenience class for chapters

        :param chapter_source:      Input either src_file/FileInfo, txt with ogm chapters, xml or (a list of) self defined chapters.
        :param fps:                 Needed for timestamp convertion. Gets the fps from the clip if src_file and otherwise assumes 24000/1001.
                                    Also accepts a timecode (v2) file.
        :param _print:              Prints chapters after parsing and after trimming.
        """
        if isinstance(chapter_source, src_file):
            if isinstance(chapter_source.file, list):
                # I'll make a workaround for this soonish
                raise error("Cannot currently parse chapters when splicing multiple files.", self)
            clip_fps = Fraction(chapter_source.src.fps_num, chapter_source.src.fps_den)
            self.fps = fps if fps else clip_fps
            self.chapters = parse_chapters_bdmv(chapter_source.file, self.fps, chapter_source.src_cut.num_frames, _print)
            if self.chapters and chapter_source.trim:
                self.trim(chapter_source.trim[0], chapter_source.trim[1], chapter_source.src_cut.num_frames)
                if _print:
                    print("After trim:")
                    self.print()
        else:
            super().__init__(chapter_source, fps if fps else Fraction(24000, 1001), _print)

__init__(chapter_source, fps=None, _print=True)

Convenience class for chapters

Parameters:

Name Type Description Default
chapter_source src_file | PathLike | GlobSearch | Chapter | list[Chapter]

Input either src_file/FileInfo, txt with ogm chapters, xml or (a list of) self defined chapters.

required
fps Fraction | PathLike | None

Needed for timestamp convertion. Gets the fps from the clip if src_file and otherwise assumes 24000/1001. Also accepts a timecode (v2) file.

None
_print bool

Prints chapters after parsing and after trimming.

True
Source code in vsmuxtools/extension/chapters.py
def __init__(
    self, chapter_source: src_file | PathLike | GlobSearch | Chapter | list[Chapter], fps: Fraction | PathLike | None = None, _print: bool = True
) -> None:
    """
    Convenience class for chapters

    :param chapter_source:      Input either src_file/FileInfo, txt with ogm chapters, xml or (a list of) self defined chapters.
    :param fps:                 Needed for timestamp convertion. Gets the fps from the clip if src_file and otherwise assumes 24000/1001.
                                Also accepts a timecode (v2) file.
    :param _print:              Prints chapters after parsing and after trimming.
    """
    if isinstance(chapter_source, src_file):
        if isinstance(chapter_source.file, list):
            # I'll make a workaround for this soonish
            raise error("Cannot currently parse chapters when splicing multiple files.", self)
        clip_fps = Fraction(chapter_source.src.fps_num, chapter_source.src.fps_den)
        self.fps = fps if fps else clip_fps
        self.chapters = parse_chapters_bdmv(chapter_source.file, self.fps, chapter_source.src_cut.num_frames, _print)
        if self.chapters and chapter_source.trim:
            self.trim(chapter_source.trim[0], chapter_source.trim[1], chapter_source.src_cut.num_frames)
            if _print:
                print("After trim:")
                self.print()
    else:
        super().__init__(chapter_source, fps if fps else Fraction(24000, 1001), _print)

SubFile dataclass

Bases: SubFile

Utility class representing a subtitle file with various functions to run on.

Parameters:

Name Type Description Default
file PathLike | list[PathLike] | GlobSearch

Can be a string, Path object or GlobSearch. If the GlobSearch returns multiple results or if a list was passed it will merge them.

required
container_delay int

Set a container delay used in the muxing process later.

0
source PathLike | None

The file this sub originates from, will be set by the constructor.

None
encoding

Encoding used for reading and writing the subtitle files.

required
Source code in vsmuxtools/extension/sub.py
@dataclass
class SubFile(MTSubFile):
    """
    Utility class representing a subtitle file with various functions to run on.

    :param file:            Can be a string, Path object or GlobSearch.
                            If the GlobSearch returns multiple results or if a list was passed it will merge them.

    :param container_delay: Set a container delay used in the muxing process later.
    :param source:          The file this sub originates from, will be set by the constructor.
    :param encoding:        Encoding used for reading and writing the subtitle files.
    """

    def truncate_by_video(
        self: SubFileSelf, source: PathLike | VideoTrack | MkvTrack | VideoFile | vs.VideoNode, fps: Fraction | PathLike | None = None
    ) -> SubFileSelf:
        """
        Removes lines that start after the video ends and trims lines that extend past it.

        :param source:      Can be any video file or a VideoNode
        :param fps:         FPS Fraction; Will be parsed from the video by default. Also accepts a timecode (v2) file.
        """
        if isinstance(source, vs.VideoNode):
            frames = source.num_frames
            if not fps:
                fps = Fraction(source.fps_num, source.fps_den)
        else:
            if isinstance(source, VideoTrack) or isinstance(source, MkvTrack) or isinstance(source, VideoFile):
                file = ensure_path_exists(source.file, self)
            else:
                file = ensure_path_exists(source, self)
            # Unused variable, just used to have a simple validation
            track = get_absolute_track(file, 0, TrackType.VIDEO, self)  # noqa: F841
            ffprobe = get_executable("ffprobe")
            args = [ffprobe, "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=r_frame_rate : stream_tags", str(file)]
            out = subprocess.run(args, capture_output=True, text=True)
            frames = 0

            for line in out.stdout.splitlines():
                if "=" not in line:
                    continue
                line = line.strip()
                if "r_frame_rate" in line and not fps:
                    fps = Fraction(line.split("=")[1])
                    debug(f"Parsed FPS from file: {fps}", self)
                elif "NUMBER_OF_FRAMES" in line:
                    line = line.split("=")[1]
                    try:
                        frames = int(line)
                        debug(f"Parsed frames from file: {frames}", self)
                    except:
                        continue

            if not fps or not frames:
                raise error(f"Could not parse frames or fps from file '{file.stem}'!", self)

        cutoff = frame_to_timedelta(frames + 1, fps, compensate=True)

        def filter_lines(lines: LINES):
            removed = 0
            trimmed = 0
            new_list = list[_Line]()
            for line in lines:
                if line.start > cutoff:
                    removed += 1
                    continue
                if line.end > cutoff:
                    line.end = frame_to_timedelta(frames, fps, compensate=True)
                    trimmed += 1
                new_list.append(line)

            if removed or trimmed:
                if removed:
                    debug(f"Removed {removed} line{'s' if removed != 1 else ''} that started past the video", self)
                if trimmed:
                    debug(f"Trimmed {trimmed} line{'s' if trimmed != 1 else ''} that extended past the video", self)

            return new_list

        return self.manipulate_lines(filter_lines)

truncate_by_video(source, fps=None)

Removes lines that start after the video ends and trims lines that extend past it.

Parameters:

Name Type Description Default
source PathLike | VideoTrack | MkvTrack | VideoFile | VideoNode

Can be any video file or a VideoNode

required
fps Fraction | PathLike | None

FPS Fraction; Will be parsed from the video by default. Also accepts a timecode (v2) file.

None
Source code in vsmuxtools/extension/sub.py
def truncate_by_video(
    self: SubFileSelf, source: PathLike | VideoTrack | MkvTrack | VideoFile | vs.VideoNode, fps: Fraction | PathLike | None = None
) -> SubFileSelf:
    """
    Removes lines that start after the video ends and trims lines that extend past it.

    :param source:      Can be any video file or a VideoNode
    :param fps:         FPS Fraction; Will be parsed from the video by default. Also accepts a timecode (v2) file.
    """
    if isinstance(source, vs.VideoNode):
        frames = source.num_frames
        if not fps:
            fps = Fraction(source.fps_num, source.fps_den)
    else:
        if isinstance(source, VideoTrack) or isinstance(source, MkvTrack) or isinstance(source, VideoFile):
            file = ensure_path_exists(source.file, self)
        else:
            file = ensure_path_exists(source, self)
        # Unused variable, just used to have a simple validation
        track = get_absolute_track(file, 0, TrackType.VIDEO, self)  # noqa: F841
        ffprobe = get_executable("ffprobe")
        args = [ffprobe, "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=r_frame_rate : stream_tags", str(file)]
        out = subprocess.run(args, capture_output=True, text=True)
        frames = 0

        for line in out.stdout.splitlines():
            if "=" not in line:
                continue
            line = line.strip()
            if "r_frame_rate" in line and not fps:
                fps = Fraction(line.split("=")[1])
                debug(f"Parsed FPS from file: {fps}", self)
            elif "NUMBER_OF_FRAMES" in line:
                line = line.split("=")[1]
                try:
                    frames = int(line)
                    debug(f"Parsed frames from file: {frames}", self)
                except:
                    continue

        if not fps or not frames:
            raise error(f"Could not parse frames or fps from file '{file.stem}'!", self)

    cutoff = frame_to_timedelta(frames + 1, fps, compensate=True)

    def filter_lines(lines: LINES):
        removed = 0
        trimmed = 0
        new_list = list[_Line]()
        for line in lines:
            if line.start > cutoff:
                removed += 1
                continue
            if line.end > cutoff:
                line.end = frame_to_timedelta(frames, fps, compensate=True)
                trimmed += 1
            new_list.append(line)

        if removed or trimmed:
            if removed:
                debug(f"Removed {removed} line{'s' if removed != 1 else ''} that started past the video", self)
            if trimmed:
                debug(f"Trimmed {trimmed} line{'s' if trimmed != 1 else ''} that extended past the video", self)

        return new_list

    return self.manipulate_lines(filter_lines)