Source code for pitch_garden.algorithms.swift_f0
"""A module for the SwiftF0 algorithm."""
from dataclasses import dataclass, replace
from typing import Any, cast
import numpy as np
from ..interfaces.types import Audio
from .base import AlgorithmConfig, BaseAlgorithm, NativeContour, TwoPassPolicy
def _import_swift_f0() -> Any:
"""Import the swift-f0 package lazily.
Returns
-------
out : module
The swift_f0 module.
Raises
------
ImportError
If the swift-f0 package is not installed.
"""
try:
import swift_f0
except ImportError as e:
raise ImportError(
"SwiftF0 requires the swift-f0 package. "
'Install it via `pip install "pitch-garden[lite]"`.'
) from e
return swift_f0
[docs]
@dataclass(frozen=True)
class SwiftF0Config(AlgorithmConfig):
"""Configuration of SwiftF0."""
# The frames whose confidence is greater than or equal to the threshold are
# regarded as voiced. The default value is that of swift-f0.
voicing_threshold: float = 0.5
# The number of CPU threads used by ONNX Runtime for inference. If None, the
# number of physical cores is used, or 1 when running in parallel.
num_threads: int | None = None
[docs]
class SwiftF0Algorithm(BaseAlgorithm):
"""SwiftF0: a fast and accurate monophonic pitch detector.
See https://github.com/lars76/swift-f0 for details.
"""
config_class = SwiftF0Config
is_neural = True
# 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 SwiftF0.
Parameters
----------
config : AlgorithmConfig
The configuration of SwiftF0. The upstream package runs only on CPU.
"""
super().__init__(config)
self.model: Any = None
@classmethod
def get_supported_sample_rates(cls) -> tuple[int, int] | None:
"""Get the sample rate of the model published by swift-f0.
Returns
-------
out : tuple[int, int] | None
The sample rate in Hz as the lower and upper limits.
"""
swift_f0 = _import_swift_f0()
return int(swift_f0.SAMPLE_RATE), int(swift_f0.SAMPLE_RATE)
@classmethod
def get_f0_limits(cls) -> tuple[float, float]:
"""Get the F0 range published by swift-f0.
Returns
-------
out : tuple[float, float]
The lower and upper limits in Hz.
"""
swift_f0 = _import_swift_f0()
return float(swift_f0.FMIN), float(swift_f0.FMAX)
@classmethod
def parallel_config(cls, config: AlgorithmConfig) -> AlgorithmConfig:
"""Use a single thread when running in parallel.
Parameters
----------
config : AlgorithmConfig
The configuration of SwiftF0.
Returns
-------
out : AlgorithmConfig
The adjusted configuration.
"""
if isinstance(config, SwiftF0Config) and config.num_threads is None:
return replace(config, num_threads=1)
return config
def load(self) -> None:
"""Load the ONNX model.
Raises
------
ImportError
If the swift-f0 package is not installed.
"""
if self.model is not None:
return
swift_f0 = _import_swift_f0()
config = cast(SwiftF0Config, self.config)
self.model = swift_f0.SwiftF0(threads=config.num_threads)
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 of the output, which is ignored because the native frame
period of SwiftF0 is fixed to 16 ms.
Returns
-------
out : NativeContour
The estimated contour with 16 ms frame period.
"""
self.load()
config = cast(SwiftF0Config, self.config)
fmin, fmax = f0_range
result = self.model.detect(audio.data, audio.sample_rate, fmin=fmin, fmax=fmax)
candidate = np.asarray(result.pitch_hz, dtype=np.float64)
score = np.asarray(result.confidence, dtype=np.float64)
f0 = np.where(config.voicing_threshold <= score, candidate, 0.0)
return NativeContour(
times=np.asarray(result.timestamps, dtype=np.float64),
f0=f0,
score=score,
candidate=candidate,
)