Speech activity and pitch¶
kudio.core.vad
¶
Voice activity detection: where in this recording is somebody talking.
kudio.split_on_silence cuts wherever the level drops, which is the
right answer for a clean studio take and the wrong one for anything with a
noise floor — a fan, a road outside, a hiss. Above a certain noise level a
fixed top_db either keeps the whole file or throws away the quiet
consonants, and there is no value in between that works.
This decides per frame instead, on three cheap signals:
- level against the recording's own noise floor, rather than against a fixed threshold, so it adapts to how noisy the file happens to be;
- spectral flatness — speech is harmonic and therefore peaky, broadband noise is flat. This is what separates "a loud hiss" from "a quiet voice". Flatness is taken over the whole band, so noise confined to part of it (a hum, a rumble, a fan with a strong tone) is peaky too and can pass as voice on this test alone;
- zero-crossing rate, which keeps the unvoiced fricatives (s, f, sh) that carry almost no energy and would otherwise be cut off the ends of words — but only where they sit next to something voiced, because on its own a fricative and a hiss are the same measurement.
The consequence of that last rule is worth knowing: a whisper, which has no voiced frames at all, is not detected. Admitting it would mean admitting every noise burst in every file, and a detector that fires on the air conditioning is not a detector.
Then the frame decisions are smoothed: short gaps inside speech are bridged, segments too short to be a word are dropped, and what survives is padded so the cut does not land on the first consonant. Pure numpy — no webrtcvad, no torch.
Functions:
| Name | Description |
|---|---|
vad |
Find the spans of y that contain speech, in seconds. |
vad_split |
The speech segments themselves, as waveforms. |
vad_trim |
Trim to the first and last speech, keeping whatever is between them. |
vad_frames |
|
speech_ratio |
Fraction of the clip that |
FRAME
module-attribute
¶
Analysis frame / hop in seconds. 30 ms sits inside a single phoneme.
THRESHOLD_DB
module-attribute
¶
How far above the estimated noise floor a frame has to be, in dB.
FLATNESS_MAX
module-attribute
¶
Spectral flatness above this is noise-like, not voice. A perfectly even spectrum is 1.0; white noise on these 30 ms frames measures about 0.56, because one frame's spectrum is not flat, and noise confined to a band measures lower still.
FLOOR_REL_DB
module-attribute
¶
A frame this far under the loudest one is silence whatever else says.
ZCR_MIN
module-attribute
¶
Zero-crossing rate above this rescues unvoiced fricatives.
FRICATIVE_REACH
module-attribute
¶
...but only within this many seconds of something actually voiced. A fricative on its own is indistinguishable from broadband noise; a fricative at the edge of a vowel is a consonant.
vad
¶
vad(
y: ndarray,
sr: int,
*,
threshold_db: float = THRESHOLD_DB,
flatness_max: float = FLATNESS_MAX,
min_speech: float = 0.1,
min_silence: float = 0.15,
pad: float = 0.05,
frame: float = FRAME,
hop: float = HOP,
) -> List[Tuple[float, float]]
Find the spans of y that contain speech, in seconds.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
threshold_db
|
float
|
how far above the estimated noise floor a frame must sit. Lower it for a very quiet talker, raise it for a noisy room. |
THRESHOLD_DB
|
flatness_max
|
float
|
spectral flatness above which a loud frame is judged to be noise rather than voice. Raise it towards 1.0 to disable that test — useful for non-speech audio, where "harmonic" means nothing. |
FLATNESS_MAX
|
min_speech
|
float
|
segments shorter than this are dropped (a click, a door). |
0.1
|
min_silence
|
float
|
gaps shorter than this do not split a segment; a stop consonant is a real silence in the middle of a word. |
0.15
|
pad
|
float
|
seconds added to each end, so the cut misses the consonant. |
0.05
|
Returns:
| Type | Description |
|---|---|
List[Tuple[float, float]]
|
non-overlapping |
Raises:
| Type | Description |
|---|---|
FeatureError
|
on non-mono audio, samples that are not finite, or
a non-positive sample rate. A clip with NaN or inf in it is refused,
as |
Source code in kudio/core/vad.py
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | |
vad_split
¶
The speech segments themselves, as waveforms.
The noise-adaptive counterpart of kudio.split_on_silence; takes the
same keyword arguments as vad.
Source code in kudio/core/vad.py
vad_trim
¶
Trim to the first and last speech, keeping whatever is between them.
Returns (trimmed, (start_sample, end_sample)), matching
kudio.trim_silence. A clip with no detected speech comes back
untouched with its full span, rather than as an empty array — deleting the
audio is never the safer reading of "nothing found".
Source code in kudio/core/vad.py
vad_frames
¶
vad_frames(
y: ndarray,
sr: int,
*,
hop_length: int = 256,
n_fft: int = 512,
center: bool = True,
pad: float = 0.0,
**kwargs,
) -> ndarray
vad as one decision per STFT frame, True for speech.
A frame is speech when its centre falls inside a span vad found.
With the default center=True the frames are the ones
kudio.waveform_to_spectrogram and kudio.STFT make with
the same hop_length and n_fft, both of which centre their frames, so
the mask indexes their rows directly:
```pycon
>>> spec = kudio.waveform_to_spectrogram(y) # (frames, bins)
>>> speech = kudio.vad_frames(y, sr) # (frames,) bool
>>> noise_only = spec[~speech]
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pad
|
float
|
seconds added to each end of a span, as in |
0.0
|
center
|
bool
|
Other keyword arguments go to |
True
|
Source code in kudio/core/vad.py
speech_ratio
¶
Fraction of the clip that vad calls speech, in [0, 1].
Useful as a dataset filter: a "speech" file at 0.02 is usually a
mislabelled recording of a room. The floor vad measures against
comes from the quietest 15% of the clip, though, so a clip cut tight
around speech, with no room tone left in it, reads near 0 as well.
Source code in kudio/core/vad.py
kudio.core.pitch
¶
Fundamental frequency, and the voiced/unvoiced decision that goes with it.
A spectrogram shows where the energy is; it does not say what note the speaker is on, and reading harmonic spacing off an image is guesswork the moment the fundamental is weak -- which it is over a phone, through a high-pass, or on any male voice whose f0 sits below the first bin anyone bothers to draw.
>>> track = kudio.f0(y, sr)
>>> track.median_hz, track.voiced_ratio
(118.3, 0.61)
>>> track.f0[track.voiced].min()
94.2
Half of this is the voicing decision. An estimator asked "what is the
pitch of this frame" always answers, including for silence, for a door
slamming and for the s in this -- and a contour drawn through those
answers is a picture of noise with a line through it. PitchTrack.f0
is NaN wherever the frame is unvoiced, so plotting it leaves gaps where
there genuinely is no pitch, and voiced_ratio says how
much of the clip had one at all.
The estimator is pYIN (Mauch & Dixon 2014), librosa's implementation: YIN with a probabilistic voicing decision resolved by a hidden Markov model over the whole clip, rather than a per-frame threshold. That last part is why it costs more than plain YIN -- about a quarter of a second per ten seconds of 16 kHz audio on a laptop, once librosa has warmed up -- and why it does not flicker between voiced and unvoiced twice per syllable the way a thresholded estimator does.
Defaults are speech. fmin=65 to fmax=400 Hz covers a low male
voice through a high female one. Music, children and singing all want a wider
range said out loud -- an octave outside the range is not detected, it is
reported as the nearest wrong answer inside it.
Classes:
| Name | Description |
|---|---|
PitchTrack |
A pitch contour plus the voicing decision that makes it readable. |
Functions:
| Name | Description |
|---|---|
f0 |
Track the fundamental frequency of y with pYIN. |
SPEECH_FMIN
module-attribute
¶
Default search range, in Hz: a low male voice to a high female one.
SPEECH_FMAX
module-attribute
¶
Top of the default search range, in Hz: a high female voice.
PitchTrack
dataclass
¶
PitchTrack(
f0: ndarray,
voiced: ndarray,
voiced_prob: ndarray,
times: ndarray,
sr: int,
fmin: float,
fmax: float,
hop_length: int,
)
A pitch contour plus the voicing decision that makes it readable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
f0
|
ndarray
|
one value per frame, in Hz, |
required |
voiced
|
ndarray
|
the voicing decision, aligned with f0. |
required |
voiced_prob
|
ndarray
|
pYIN's confidence, aligned with f0. |
required |
times
|
ndarray
|
centre time of each frame, in seconds. |
required |
Methods:
| Name | Description |
|---|---|
range_hz |
Percentile range of the voiced f0, in Hz. |
summary |
Everything above as plain numbers, for a report or a CSV row. |
voiced_segments |
Contiguous voiced runs as |
to_labels |
The voiced runs as |
Attributes:
| Name | Type | Description |
|---|---|---|
voiced_ratio |
float
|
Fraction of frames with a pitch. Not the same as speech: an |
median_hz |
float
|
Median f0 over voiced frames; |
semitone_range |
float
|
The same span in semitones -- the unit pitch is actually heard in. |
voiced_ratio
property
¶
Fraction of frames with a pitch. Not the same as speech: an f
or an s is speech and has no pitch.
median_hz
property
¶
Median f0 over voiced frames; nan when nothing was voiced.
The median and not the mean: an octave error is a factor of two, and one of those moves a mean far enough to matter.
semitone_range
property
¶
The same span in semitones -- the unit pitch is actually heard in.
60 Hz of range means something different at the bottom of a male voice than at the top of a female one; twelve semitones is an octave either way.
range_hz
¶
Percentile range of the voiced f0, in Hz.
Percentiles rather than min/max, because a single misjudged frame at the octave sets both extremes.
Source code in kudio/core/pitch.py
summary
¶
Everything above as plain numbers, for a report or a CSV row.
Source code in kudio/core/pitch.py
voiced_segments
¶
Contiguous voiced runs as (start, end) in seconds.
Runs shorter than min_seconds are dropped: at a 10 ms hop a single voiced frame in the middle of a fricative is an estimator hiccup, not a syllable.
Source code in kudio/core/pitch.py
to_labels
¶
The voiced runs as kudio.Label objects.
Saveable with kudio.save_labels, so a pitch track can be
opened as a label track in an editor rather than only read as numbers.
Source code in kudio/core/pitch.py
f0
¶
f0(
y: ndarray,
sr: int,
*,
fmin: float = SPEECH_FMIN,
fmax: float = SPEECH_FMAX,
frame_length: Optional[int] = None,
hop_length: Optional[int] = None,
center: bool = True,
) -> PitchTrack
Track the fundamental frequency of y with pYIN.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fmin
|
float
|
bottom of the search range, in Hz. Nothing below it is found; a voice below it is reported as something wrong above it. |
SPEECH_FMIN
|
fmax
|
float
|
top of the search range. |
SPEECH_FMAX
|
frame_length
|
Optional[int]
|
analysis window. Must hold more than two periods of fmin, so a low fmin needs a long window -- checked, not assumed. Omitted, it is 2048 samples, or the next power of two that is long enough when 2048 is not: 4096 at 88.2 and 96 kHz. |
None
|
hop_length
|
Optional[int]
|
samples between frames; 10 ms when omitted, which is the usual resolution for a pitch contour. |
None
|
center
|
bool
|
frames centred on their time stamp, so |
True
|
Returns:
| Type | Description |
|---|---|
PitchTrack
|
a |
Source code in kudio/core/pitch.py
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | |