Source code for pitch_garden.algorithms.straight

"""A module for the F0 extraction of STRAIGHT."""

import sys
from dataclasses import dataclass
from typing import Any, cast

import numpy as np

from ..interfaces.types import Audio
from .base import AlgorithmConfig, BaseAlgorithm, NativeContour, TwoPassPolicy


def _import_pylstraight() -> Any:
    """Import the pylstraight package lazily.

    Returns
    -------
    out : module
        The pylstraight module.

    Raises
    ------
    ImportError
        If the pylstraight package is not installed.

    """
    try:
        import pylstraight
    except ImportError as e:
        raise ImportError(
            "STRAIGHT requires the pylstraight package. "
            'Install it via `pip install "pitch-garden[lite]"`.'
        ) from e
    return pylstraight


[docs] @dataclass(frozen=True) class StraightConfig(AlgorithmConfig): """Configuration of STRAIGHT. The default search range covers both speech and singing voice. """ fmin: float | None = 40.0 fmax: float | None = 800.0 # The seed of the random noise added inside pylstraight, which makes the # results reproducible. If None, the results vary slightly at every call. seed: int | None = 0
[docs] class StraightAlgorithm(BaseAlgorithm): """STRAIGHT: the F0 extraction based on multiple cues (Kawahara et al., 2005). This uses pylstraight, a Python port of Legacy STRAIGHT. See https://github.com/takenori-y/pylstraight for details. The voicing decision is made inside the algorithm, and thus the voicing score is not provided. Note that the voicing decision relies on the contrast with silent segments. """ config_class = StraightConfig # The setting chosen on the speech of pitch-benchmark (examples/pitch_benchmark). two_pass_policy = TwoPassPolicy(enabled=False) def __init__(self, config: AlgorithmConfig) -> None: """Initialize STRAIGHT. Parameters ---------- config : AlgorithmConfig The configuration of STRAIGHT. """ super().__init__(config) self.pyls: Any = None @classmethod def get_supported_sample_rates(cls) -> tuple[int, int] | None: """Get the sample rates that pylstraight accepts. Returns ------- out : tuple[int, int] | None The lower and upper limits in Hz, where the upper one is unlimited. """ return int(_import_pylstraight().min_fs), sys.maxsize @classmethod def get_f0_limits(cls) -> tuple[float, float]: """Get the F0 range that pylstraight accepts. The upper limit also has to be lower than or equal to the Nyquist frequency. Returns ------- out : tuple[float, float] The lower and upper limits in Hz. """ return float(_import_pylstraight().min_f0), float("inf") def load(self) -> None: """Import pylstraight. Raises ------ ImportError If the pylstraight package is not installed. """ if self.pyls is None: self.pyls = _import_pylstraight() def estimate( self, audio: Audio, f0_range: tuple[float, float], frame_period: float ) -> NativeContour: """Estimate the F0 contour. Parameters ---------- audio : Audio The input audio. f0_range : tuple[float, float] The F0 search range in Hz. frame_period : float The frame period in milliseconds. Returns ------- out : NativeContour The estimated contour. Raises ------ ValueError If the search range is invalid for pylstraight, e.g., above the Nyquist frequency or too narrow. """ self.load() config = cast(StraightConfig, self.config) f0 = self.pyls.extract_f0( audio.data, audio.sample_rate, frame_shift=frame_period, f0_range=f0_range, seed=config.seed, ) f0 = np.asarray(f0, dtype=np.float64) times = np.arange(len(f0)) * frame_period / 1000 return NativeContour(times=times, f0=f0)