Source code for emvoice.signal
"""Load and store audio signals."""
import logging
import math
from typing import Optional
import librosa
import numpy as np
[docs]class BaseSignal:
"""Store a signal.
Parameters
----------
sig: numpy.ndarray
Signal.
sr: int
Sampling rate.
"""
_ts: Optional[np.ndarray] = None
_idx: Optional[np.ndarray] = None
def __init__(self, sig: np.ndarray, sr: int) -> None:
self.logger = logging.getLogger("emvoice.signal.BaseSignal")
self.sig = sig
self.sr = sr
@property
[docs] def idx(self) -> np.ndarray:
"""Sample indices (read-only)."""
if self._idx is None:
self._idx = np.arange(self.sig.shape[0])
return self._idx
@property
[docs] def ts(self) -> np.ndarray:
"""Sample timestamps (read-only)."""
if self._ts is None:
self._ts = librosa.samples_to_time(self.idx, sr=self.sr)
return self._ts
[docs]class AudioSignal(BaseSignal):
"""Load and store an audio signal.
Parameters
----------
sig: numpy.ndarray
Audio signal.
sr: int
Sampling rate.
mono: bool, default=True
Whether the signal has been converted to mono or not.
filename: str, optional
Name of the audio file associated with the signal.
"""
def __init__(
self,
sig: np.ndarray,
sr: int,
mono: bool = True,
filename: Optional[str] = None,
) -> None:
self.logger = logging.getLogger("emvoice.signal.AudioSignal")
self.filename = filename
self.mono = mono
super().__init__(sig, sr)
@classmethod
[docs] def from_file(
cls, filename: str, sr: Optional[float] = None, mono: bool = True
):
"""Load a signal from an audio file.
Parameters
----------
filename: str
Name of the audio file.
File types must be supported by ``soundfile`` or ``audiofile``.
See :func:`librosa.load`.
sr: float, optional, default=None
Sampling rate. If `None`, is detected from the file, otherwise the signal is resampled.
mono: bool, default=True
Whether to convert the signal to mono.
"""
sig, nat_sr = librosa.load(path=filename, sr=sr, mono=mono)
return cls(sig, nat_sr, mono, filename)
[docs]class FormantAudioSignal(AudioSignal):
def __init__(
self,
sig: np.ndarray,
sr: int,
mono: bool,
filename: Optional[str],
preemphasis_from: Optional[float],
):
self.preemphasis_from = preemphasis_from
super().__init__(sig, sr, mono, filename)
@staticmethod
def _calc_preemphasis_coef(preemphasis_from: float, sr: float) -> float:
return math.exp(-2 * math.pi * preemphasis_from * (1 / sr))
@classmethod
[docs] def from_file(
cls,
filename: str,
sr: Optional[float] = None,
mono: bool = True,
preemphasis_from: Optional[float] = 50.0,
):
"""Load a signal from an audio file.
Parameters
----------
filename: str
Name of the audio file.
File types must be supported by ``soundfile`` or ``audiofile``.
See :func:`librosa.load`.
sr: float, optional, default=None
Sampling rate. If `None`, is detected from the file, otherwise the signal is resampled.
mono: bool, default=True
Whether to convert the signal to mono.
preemphasis_from: float, optional, default=50.0
Pre-emphasize the signal from this value onwards (in Hz).
"""
audio_sig_obj = super().from_file(filename, sr, mono)
return cls.from_audio_signal(audio_sig_obj, preemphasis_from)
@classmethod
def from_audio_signal(
cls,
audio_sig_obj: AudioSignal,
preemphasis_from: Optional[float] = 50.0,
):
sig = audio_sig_obj.sig
if preemphasis_from is not None:
pre_coef = cls._calc_preemphasis_coef(
preemphasis_from, audio_sig_obj.sr
)
sig = librosa.effects.preemphasis(sig, coef=pre_coef)
return cls(
sig,
audio_sig_obj.sr,
audio_sig_obj.mono,
audio_sig_obj.filename,
preemphasis_from,
)
@staticmethod
def _preemphasize(sig: np.ndarray, sr: float, preemphasis_from: float):
pre_coef = math.exp(-2 * math.pi * preemphasis_from * (1 / sr))
return librosa.effects.preemphasis(sig, coef=pre_coef)