Skip to content

vsmuxtools.utils.source

SourceFilter

Bases: IntEnum

Filter used to index the source file

Source code in vsmuxtools/utils/source.py
class SourceFilter(IntEnum):
    """Filter used to index the source file"""

    AUTO = 0
    """
    LSMASH for m2ts regardless of previewing; FFMS2 if previewing, BESTSOURCE otherwise.
    """
    BESTSOURCE = 1
    """
    Slow initial indexing but guaranteed accuracy.
    https://github.com/vapoursynth/bestsource
    """
    FFMS2 = 2
    """
    Fast, generally good enough.
    https://github.com/FFMS/ffms2
    """
    LSMASH = 3
    """
    Also fast but notably the only thing that isn't affected by a certain m2ts ffmpeg bug.\n
    May be deprecated once a new FFMS2 build is available.\n
    https://github.com/HomeOfAviSynthPlusEvolution/L-SMASH-Works
    """

    BS = BESTSOURCE
    """Alias for BESTSOURCE"""

AUTO class-attribute instance-attribute

AUTO = 0

LSMASH for m2ts regardless of previewing; FFMS2 if previewing, BESTSOURCE otherwise.

BESTSOURCE class-attribute instance-attribute

BESTSOURCE = 1

Slow initial indexing but guaranteed accuracy. https://github.com/vapoursynth/bestsource

FFMS2 class-attribute instance-attribute

FFMS2 = 2

Fast, generally good enough. https://github.com/FFMS/ffms2

LSMASH class-attribute instance-attribute

LSMASH = 3

Also fast but notably the only thing that isn't affected by a certain m2ts ffmpeg bug.

May be deprecated once a new FFMS2 build is available.

https://github.com/HomeOfAviSynthPlusEvolution/L-SMASH-Works

BS class-attribute instance-attribute

BS = BESTSOURCE

Alias for BESTSOURCE

src_file

src_file(file: PathLike | GlobSearch | Sequence[PathLike], trim: Trim | list[Trim] | None = None, preview_sourcefilter: SourceFilter | None = SourceFilter.FFMS2, sourcefilter: SourceFilter = SourceFilter.BESTSOURCE, idx: Callable[[str], VideoNode] | None = None, **kwargs)

Bases: VSObject

Custom FileInfo kind of thing for convenience

Parameters:

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

Either a string based filepath or a Path object

required
trim Trim | list[Trim] | None

A single trim or ordered list, e.g. [(24, 500), (700, None)]. Ends are exclusive.

A negative first start pads frozen frames/silence; later negative starts count from the source end.

None ends keep the remainder; 0 is invalid. Ranges apply after concatenating input files.

None
preview_sourcefilter SourceFilter | None

Source filter to be used when previewing using vspreview. None will make it fall back to sourcefilter.

FFMS2
sourcefilter SourceFilter

Source filter to be used otherwise.

BESTSOURCE
idx Callable[[str], VideoNode] | None

Custom indexer for the input file. Pass a function that takes a string in and returns a vs.VideoNode.

None
Source code in vsmuxtools/utils/source.py
def __init__(
    self,
    file: PathLike | GlobSearch | Sequence[PathLike],
    trim: Trim | list[Trim] | None = None,
    preview_sourcefilter: SourceFilter | None = SourceFilter.FFMS2,
    sourcefilter: SourceFilter = SourceFilter.BESTSOURCE,
    idx: Callable[[str], vs.VideoNode] | None = None,
    **kwargs,
):
    """
    Custom `FileInfo` kind of thing for convenience

    :param file:                    Either a string based filepath or a Path object
    :param trim:                    A single trim or ordered list, e.g. [(24, 500), (700, None)]. Ends are exclusive.\n
                                    A negative first start pads frozen frames/silence; later negative starts count from the source end.\n
                                    None ends keep the remainder; 0 is invalid. Ranges apply after concatenating input files.
    :param preview_sourcefilter:    Source filter to be used when previewing using vspreview.
                                    `None` will make it fall back to `sourcefilter`.
    :param sourcefilter:            Source filter to be used otherwise.
    :param idx:                     Custom indexer for the input file. Pass a function that takes a string in and returns a vs.VideoNode.
    """
    if isinstance(file, Sequence) and not isinstance(file, str) and len(file) == 1:
        file = file[0]

    self.file = (
        [ensure_path_exists(f, self) for f in file]
        if isinstance(file, Sequence) and not isinstance(file, str)
        else ensure_path_exists(file, self)
    )
    self.trim = trim
    self.preview_sourcefilter = preview_sourcefilter
    self.sourcefilter = sourcefilter
    self.idx = idx
    self.idx_args = kwargs

init

init(bits: int | None = 16, matrix: MatrixLike | None = None, transfer: TransferLike | None = None, primaries: PrimariesLike | None = None, chroma_location: ChromaLocationLike | None = None, color_range: ColorRangeLike | None = None, field_based: FieldBasedLike | None = None, dither_type: DitherType = DitherType.RANDOM) -> vs.VideoNode

Getter that calls vstools.initialize_clip on the src clip for convenience

Source code in vsmuxtools/utils/source.py
def init(
    self,
    bits: int | None = 16,
    matrix: MatrixLike | None = None,
    transfer: TransferLike | None = None,
    primaries: PrimariesLike | None = None,
    chroma_location: ChromaLocationLike | None = None,
    color_range: ColorRangeLike | None = None,
    field_based: FieldBasedLike | None = None,
    dither_type: DitherType = DitherType.RANDOM,
) -> vs.VideoNode:
    """
    Getter that calls `vstools.initialize_clip` on the src clip for convenience
    """
    return initialize_clip(
        clip=self.src,
        bits=bits,
        matrix=matrix,
        transfer=transfer,
        primaries=primaries,
        chroma_location=chroma_location,
        color_range=color_range,
        field_based=field_based,
        dither_type=dither_type,
        func=self.init,
    )  # type: ignore # shush this works

init_cut

init_cut(bits: int | None = 16, matrix: MatrixLike | None = None, transfer: TransferLike | None = None, primaries: PrimariesLike | None = None, chroma_location: ChromaLocationLike | None = None, color_range: ColorRangeLike | None = None, field_based: FieldBasedLike | None = None, dither_type: DitherType = DitherType.RANDOM) -> vs.VideoNode

Getter that calls vstools.initialize_clip on the src_cut clip for convenience

Source code in vsmuxtools/utils/source.py
def init_cut(
    self,
    bits: int | None = 16,
    matrix: MatrixLike | None = None,
    transfer: TransferLike | None = None,
    primaries: PrimariesLike | None = None,
    chroma_location: ChromaLocationLike | None = None,
    color_range: ColorRangeLike | None = None,
    field_based: FieldBasedLike | None = None,
    dither_type: DitherType = DitherType.RANDOM,
) -> vs.VideoNode:
    """
    Getter that calls `vstools.initialize_clip` on the src_cut clip for convenience
    """
    return initialize_clip(
        clip=self.src_cut,
        bits=bits,
        matrix=matrix,
        transfer=transfer,
        primaries=primaries,
        chroma_location=chroma_location,
        color_range=color_range,
        field_based=field_based,
        dither_type=dither_type,
        func=self.init,
    )  # type: ignore # shush this works

get_audio

get_audio(track: int = 0, **kwargs) -> vs.AudioNode

Indexes the specified audio track from the input file(s).

Source code in vsmuxtools/utils/source.py
def get_audio(self, track: int = 0, **kwargs) -> vs.AudioNode:
    """
    Indexes the specified audio track from the input file(s).
    """
    file = self.file if isinstance(self.file, list) else [self.file]

    nodes = list[vs.AudioNode]()
    for f in file:
        parsed = ParsedFile.from_file(f, self)
        absolute = parsed.find_tracks(relative_id=track, type=TrackType.AUDIO, error_if_empty=True, caller=self)[0].index
        nodes.append(core.bs.AudioSource(str(f.resolve()), absolute, **kwargs))

    return nodes[0] if len(nodes) == 1 else core.std.AudioSplice(nodes)

get_audio_trimmed

get_audio_trimmed(track: int = 0, **kwargs) -> vs.AudioNode

Applies the source's ordered frame ranges to the indexed audio, padding a negative first start with silence. Requires constant video FPS when trimming. Open ends retain the audio tail; explicit ends use available samples.

Source code in vsmuxtools/utils/source.py
def get_audio_trimmed(self, track: int = 0, **kwargs) -> vs.AudioNode:
    """
    Applies the source's ordered frame ranges to the indexed audio, padding a negative first start with silence.
    Requires constant video FPS when trimming. Open ends retain the audio tail; explicit ends use available samples.
    """
    node = self.get_audio(track, **kwargs)
    if self.trim is None or self.trim == []:
        return node
    clip = self.src
    if clip.fps_num <= 0 or clip.fps_den <= 0:
        raise error("Audio trimming requires a constant, positive video frame rate.", self)
    parts = list[vs.AudioNode]()
    for start, end in _resolve_trims(self.trim, clip.num_frames, self):
        first = f2s(max(0, start), node, clip)
        last = node.num_samples if end is None else min(node.num_samples, f2s(end, node, clip))
        if first >= last:
            raise error(f"Trim range {(start, end)} contains no available audio samples.", self)
        part = node[first:last]
        if start < 0 and (padding := f2s(-start, node, clip)):
            part = core.std.BlankAudio(node, length=padding) + part
        parts.append(part)
    return core.std.AudioSplice(parts)

src

src(filePath: PathLike, preview_sourcefilter: SourceFilter | None = SourceFilter.AUTO, sourcefilter: SourceFilter = SourceFilter.BESTSOURCE, **kwargs: KwargsT) -> vs.VideoNode

Uses lsmas for previewing and bestsource otherwise. Still supports dgi files directly if dgdecodenv is installed to not break existing scripts.

Parameters:

Name Type Description Default
filepath

Path to video or dgi file

required
preview_sourcefilter SourceFilter | None

Source filter to be used when previewing using vspreview. None will make it fall back to sourcefilter.

AUTO
sourcefilter SourceFilter

Source filter to be used otherwise.

BESTSOURCE
kwargs KwargsT

Other arguments you may or may not wanna pass to the indexer.

{}

Returns:

Type Description
VideoNode

Video Node

Source code in vsmuxtools/utils/source.py
def src(
    filePath: PathLike,
    preview_sourcefilter: SourceFilter | None = SourceFilter.AUTO,
    sourcefilter: SourceFilter = SourceFilter.BESTSOURCE,
    **kwargs: KwargsT,
) -> vs.VideoNode:
    """
    Uses lsmas for previewing and bestsource otherwise.
    Still supports dgi files directly if dgdecodenv is installed to not break existing scripts.

    :param filepath:                Path to video or dgi file
    :param preview_sourcefilter:    Source filter to be used when previewing using vspreview.
                                    `None` will make it fall back to `sourcefilter`.
    :param sourcefilter:            Source filter to be used otherwise.
    :param kwargs:                  Other arguments you may or may not wanna pass to the indexer.
    :return:                        Video Node
    """
    filePath = ensure_path_exists(filePath, src)
    dgiFile = filePath.with_suffix(".dgi")
    if filePath.suffix.lower() == ".dgi" or dgiFile.exists():
        if not hasattr(core, "dgdecodenv"):
            raise error("Trying to use a dgi file without dgdecodenv installed.", src)
        return core.lazy.dgdecodenv.DGSource(str(filePath.resolve()) if not dgiFile.exists() else str(dgiFile.resolve()), **kwargs)

    force_lsmas = kwargs.pop("force_lsmas", False)
    if force_lsmas:
        warn("force_lsmas is deprecated!\nPlease switch to using the explicit sourcefilter params.", src, 5)
        preview_sourcefilter = None
        sourcefilter = SourceFilter.LSMASH

    force_bs = kwargs.pop("force_bs", False)
    if force_bs:
        warn("force_bs is deprecated!\nPlease switch to using the explicit sourcefilter params.", src, 5)
        preview_sourcefilter = None
        sourcefilter = SourceFilter.BESTSOURCE

    if preview_sourcefilter is SourceFilter.AUTO:
        preview_sourcefilter = SourceFilter.LSMASH if "m2ts" in filePath.name.lower() else SourceFilter.FFMS2

    if sourcefilter is SourceFilter.AUTO:
        sourcefilter = SourceFilter.LSMASH if "m2ts" in filePath.name.lower() else SourceFilter.BESTSOURCE

    if is_previewing() and preview_sourcefilter is not None:
        return _call_sourcefilter(filePath.resolve(), preview_sourcefilter, **kwargs)
    else:
        return _call_sourcefilter(filePath.resolve(), sourcefilter, **kwargs)

frames_to_samples

frames_to_samples(frame: int | None, sample_rate: AudioNode | int = 48000, fps: VideoNode | Fraction = Fraction(24000, 1001)) -> int

Converts a frame number to a sample number

Parameters:

Name Type Description Default
frame int | None

The frame number

required
sample_rate AudioNode | int

Can be a flat number like 48000 (=48 kHz) or an AudioNode to get the sample rate from

48000
fps VideoNode | Fraction

Can be a Fraction or a VideoNode to get the fps from

Fraction(24000, 1001)

Returns:

Type Description
int

The sample number

Source code in vsmuxtools/utils/source.py
def frames_to_samples(frame: int | None, sample_rate: vs.AudioNode | int = 48000, fps: vs.VideoNode | Fraction = Fraction(24000, 1001)) -> int:
    """
    Converts a frame number to a sample number

    :param frame:           The frame number
    :param sample_rate:     Can be a flat number like 48000 (=48 kHz) or an AudioNode to get the sample rate from
    :param fps:             Can be a Fraction or a VideoNode to get the fps from

    :return:                The sample number
    """
    if not frame:
        return 0
    sample_rate = sample_rate.sample_rate if isinstance(sample_rate, vs.AudioNode) else sample_rate
    fps = Fraction(fps.fps_num, fps.fps_den) if isinstance(fps, vs.VideoNode) else fps
    return int(Fraction(sample_rate * frame, 1) / fps)

generate_svt_av1_keyframes

generate_svt_av1_keyframes(clip: VideoNode, start_frame: int = 0, min_scene_length: int = 129, min_still_scene_length: int = 193, max_scene_length: int = 321) -> list[int]

Run generate_keyframes, and then filter the WWXD keyframe result for SVT-AV1 derived encoders.

A huge contribution to the efficiency of SVT-AV1 derived encoders comes from its referencing system. SVT-AV1 derived encoders by default have a 32 frame hierarchical structure. It will first encode frame 0 as a key frame, followed by frame 32 referencing frame 0. Since later frames will all be referencing these frames, these frames of the lowest temporal layer will be given very good q. After that, the encoder will continue with frame 16, referencing both frame 0 and frame 32. And then after frame 16, it'll be frame 8, frame 4, frame 2, frame 1, in this order. The frames in higher temporal layer will be given a bad q, mostly relying on information in the frame they reference to get a good encoding quality. It's very efficient to allow SVT-AV1 derived encoders to build a full hierarchical structure like this, with lower level frames being given a good q, and higher level frames relying on referencing and saving bitrate. For scene detection, the scenes that gives the best efficiency should have a length of (integer * 32) + 1, followed by scenes that has a length of (integer * 16) + 1, and then 8, and then 4, et cetera.

Additionally, since both frame 0 and frame 32 will be given very good q, if there were a real scene change in the middle of this 32 frame structure, it will be handled well because frame 32 will certainly be given a good enough q to encode the new scene. On the other hand, making too much scenecut in scene detection, resulting in a lot of smaller and incomplete hierarchical strctures in each scene is generally a very bad idea. Since WWXD often has the tendency to place way too much scenecuts in challenging sections, the purpose of this function is to filter the result from WWXD and create scenes that has more efficient hierarchical structure as much as possible.

Source code in vsmuxtools/utils/source.py
def generate_svt_av1_keyframes(
    clip: vs.VideoNode,
    start_frame: int = 0,
    min_scene_length: int = 129,
    min_still_scene_length: int = 193,
    max_scene_length: int = 321,
) -> list[int]:
    """
    Run `generate_keyframes`, and then filter the WWXD keyframe result for SVT-AV1 derived encoders.

    A huge contribution to the efficiency of SVT-AV1 derived encoders comes from its referencing system.
    SVT-AV1 derived encoders by default have a 32 frame hierarchical structure.
    It will first encode frame 0 as a key frame, followed by frame 32 referencing frame 0.
    Since later frames will all be referencing these frames, these frames of the lowest temporal layer will be given very good q.
    After that, the encoder will continue with frame 16, referencing both frame 0 and frame 32.
    And then after frame 16, it'll be frame 8, frame 4, frame 2, frame 1, in this order.
    The frames in higher temporal layer will be given a bad q, mostly relying on information in the frame they reference to get a good encoding quality.
    It's very efficient to allow SVT-AV1 derived encoders to build a full hierarchical structure like this, with lower level frames being given a good q, and higher level frames relying on referencing and saving bitrate.
    For scene detection, the scenes that gives the best efficiency should have a length of `(integer * 32) + 1`, followed by scenes that has a length of `(integer * 16) + 1`, and then `8`, and then `4`, et cetera.

    Additionally, since both frame 0 and frame 32 will be given very good q, if there were a real scene change in the middle of this 32 frame structure, it will be handled well because frame 32 will certainly be given a good enough q to encode the new scene.
    On the other hand, making too much scenecut in scene detection, resulting in a lot of smaller and incomplete hierarchical strctures in each scene is generally a very bad idea.
    Since WWXD often has the tendency to place way too much scenecuts in challenging sections, the purpose of this function is to filter the result from WWXD and create scenes that has more efficient hierarchical structure as much as possible.
    """
    import numpy as np
    from numpy.lib.stride_tricks import sliding_window_view

    frames = generate_keyframes(clip, start_frame)

    if start_frame:
        clip = clip[start_frame:]

    diff_clip = clip.std.PlaneStats(clip[0] + clip, plane=0, prop="Luma")

    frames.append(len(clip))
    head = -1  # Because the result from generate_keyframes doesn't have `0`
    current_frame = 0
    svt_av1_frames = [0]
    while head < len(frames) - 1:
        head += 1

        # Choosing between WWXD selected frames within the limit of min_scene_length and max_scene_length
        if frames[head] - current_frame < min_scene_length:
            if head != len(frames) - 1:
                continue

            else:
                current_frame = frames[head]
                svt_av1_frames.append(current_frame)  # Only to get popped

        elif frames[head] - current_frame <= max_scene_length:
            available_frames = []
            for looka_head in range(head, len(frames)):
                if frames[looka_head] - current_frame <= max_scene_length:
                    available_frames.append(frames[looka_head])
                else:
                    break

            selected_head = None
            for structure in [32, 16, 8, 4, 2]:
                for available_head in range(len(available_frames) - 1, -1, -1):
                    if (available_frames[available_head] - current_frame) % structure == 1:
                        selected_head = available_head
                        break
                if selected_head is not None:
                    break

            if selected_head is None:
                selected_head = len(available_frames) - 1

            head = head + selected_head
            current_frame = frames[head]
            svt_av1_frames.append(current_frame)

        # If WWXD doesn't select anything within max_scene_length, try finding good frames using diffs.
        else:
            selected_frame = None
            diffs = np.array(
                [
                    frame.props["LumaDiff"]
                    for frame in diff_clip[current_frame + min_still_scene_length : current_frame + max_scene_length + 1].frames()
                ]
            )
            windows = sliding_window_view(diffs, 25)
            median = np.median(windows, axis=1).reshape((-1, 1))
            mad = np.median(np.abs(windows - median), axis=1).reshape((-1, 1))
            thr = (median + 3.0 * mad).reshape((-1,))
            thr = np.concatenate((np.full((12,), thr[0]), thr, np.full((12,), thr[-1])))
            motion_frames = np.argwhere(diffs > thr).reshape((-1,))
            motion_frames += current_frame + min_still_scene_length

            if motion_frames.shape[0] != 0:
                for structure in [32, 16, 8]:
                    for frame in motion_frames[::-1]:
                        if (frame - current_frame) % structure == 1:
                            selected_frame = int(frame)
                            break
                    if selected_frame is not None:
                        break

            if selected_frame is None:
                selected_frame = current_frame + max_scene_length

            head -= 1
            current_frame = selected_frame
            svt_av1_frames.append(current_frame)

    svt_av1_frames.pop()

    return svt_av1_frames