Datasets¶
kudio.core.synth
¶
Noisy-speech synthesis: the whole chain between a talker and a file.
A recording is degraded three ways and this class puts all three in. Noise is additive, a room is convolutive, and the channel is neither, so they are applied in the order the signal actually meets them:
Passing rt60= reverberates the clean signal before the noise is mixed
in, which is the order a microphone meets them: the room acts on the talker,
and the noise arrives at the microphone alongside the result. The SNR is
therefore measured against the reverberant speech, because that is what is
actually competing with the noise. Passing channel= runs the finished
mixture through a link -- the wire carries whatever the microphone produced,
noise included, so it comes last.
The channel is not a fourth axis. snr_ratio and rt60 are swept;
channel is not, and it does not appear in the output names. A corpus is
recorded over a phone line or it is not -- that is a property of the corpus,
not a dimension to cross with the others, and four axes already multiply
enough. To compare two links, run the Synthesizer twice into two directories.
The clean file stays the training target, so a model trained on this data is
asked to undo all of it. write_targets=True also writes the
reverberant-but-clean signal -- the room and nothing else -- so a model can be
asked to remove only the noise and leave the room alone, which is a different
and easier problem. manifest records which is which.
Classes:
| Name | Description |
|---|---|
Synthesizer |
Mix clean audio with noise at the requested SNRs. |
Synthesizer
¶
Synthesizer(
clean: Any,
noise: Any,
out_path: str = "",
snr_ratio: Union[float, Sequence[float]] = (-5, 0, 5),
rt60: Optional[Union[float, Sequence[float]]] = None,
drr_db: float = 0.0,
channel: Optional[Union[str, dict]] = None,
write_targets: bool = False,
root: Optional[PathLike] = None,
subtype: str = "PCM_16",
)
Mix clean audio with noise at the requested SNRs.
>>> syx = Synthesizer('data/train/clean', 'data/train/noise',
... out_path='data/train/mixed', snr_ratio=(-5, 0, 5))
>>> syn_list = syx.syn(mode='inc') # every clean x noise x SNR
>>> syn_list = syx.syn(mode='reg') # one output per clean file
>>> syn_list = syx.syn_extra_mode('data/backgroundnoise', background_snr=(5,))
syn returns [(out_path, clean, noise, snr), ...].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
clean
|
Any
|
any input |
required |
noise
|
Any
|
the same, for the noise. |
required |
out_path
|
str
|
output directory for the mixed files (default
|
''
|
snr_ratio
|
Union[float, Sequence[float]]
|
one or more SNR values in dB. |
(-5, 0, 5)
|
rt60
|
Optional[Union[float, Sequence[float]]]
|
one or more reverberation times in seconds. |
None
|
drr_db
|
float
|
direct-to-reverberant ratio for the rooms, in dB. Distance in a readable unit -- +10 and above is a close microphone, 0 is a metre or two, negative is the far end of a hall. |
0.0
|
channel
|
Optional[Union[str, dict]]
|
a link for the finished mixture to travel down: a preset
name ( |
None
|
write_targets
|
bool
|
also write the reverberant-but-clean signal to a
|
False
|
root
|
Optional[PathLike]
|
the folder the clean files' layout is measured from; the
output mirrors that layout. Defaults to clean when it is a directory
and otherwise to the deepest folder the clean files share, so two
talkers' |
None
|
subtype
|
str
|
how the files are written: |
'PCM_16'
|
Methods:
| Name | Description |
|---|---|
waveform |
Load the clean and noise file lists into waveform data. |
syn |
Generate the noisy dataset. |
manifest |
The last run as |
syn_extra_mode |
Two-stage mixing: noise x background first, then everything x clean. |
syn_waves |
Mix one |
Attributes:
| Name | Type | Description |
|---|---|---|
rooms |
Dict[Path, tuple]
|
room per output file, keyed by the output path rather than held in a |
channel |
dict
|
the link, normalised once. Empty dict means no channel, and is |
channels |
Dict[Path, tuple]
|
(spec, seed) per output file, for the same reason |
targets |
Dict[Path, Path]
|
output file -> the reverberant-clean target written beside it |
clipped |
Dict[Path, float]
|
output file -> its peak, for every file of the last run that went |
seed |
Optional[int]
|
the base every per-file draw of the last run came from. Passed back |
clean_root |
Optional[Path]
|
where the output layout is measured from; |
target_sr |
Optional[int]
|
target output rate; |
Source code in kudio/core/synth.py
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 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 | |
rooms
instance-attribute
¶
room per output file, keyed by the output path rather than held in a
list beside synWavesList. Two lists that have to stay in step is
the failure this codebase keeps meeting; a mapping cannot drift.
channel
instance-attribute
¶
channel: dict = channel_spec(channel)
the link, normalised once. Empty dict means no channel, and is
falsy, so every downstream check is if self.channel:.
channels
instance-attribute
¶
(spec, seed) per output file, for the same reason rooms is a
mapping: the seed has to reach the worker that writes that file.
targets
instance-attribute
¶
output file -> the reverberant-clean target written beside it
clipped
instance-attribute
¶
output file -> its peak, for every file of the last run that went past full scale and was clipped there when written
seed
instance-attribute
¶
the base every per-file draw of the last run came from. Passed back
as syn(seed=...) it rebuilds that run, even one given no seed.
clean_root
instance-attribute
¶
where the output layout is measured from; None puts every
output directly in noisyDir
target_sr
instance-attribute
¶
target output rate; None keeps each clean file's own rate
waveform
¶
Load the clean and noise file lists into waveform data.
Source code in kudio/core/synth.py
syn
¶
syn(
mode: str = "regular",
mkdir_parents: bool = True,
is_add_clean: bool = False,
rdn_choice_num: int = 0,
overwrite: bool = True,
is_pool: bool = False,
is_silence: bool = False,
p_silence: float = 0.02,
target_sr: Optional[int] = None,
desired_sample: Optional[int] = None,
seed: Optional[int] = None,
) -> Optional[list]
Generate the noisy dataset.
mode 'reg'/'regular': one output per clean file, the noises and
SNRs taken in turn -- and every noise x SNR combination comes up once
in each len(noise) * len(snr) outputs, whatever the two lengths
are. mode 'inc'/'increment': full cartesian product
clean x noise x SNR.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rdn_choice_num
|
int
|
synthesise this many clean files, drawn at random without replacement, instead of all of them. |
0
|
is_silence
|
bool
|
also write a p_silence share of the outputs again
as noise alone, named |
False
|
target_sr
|
Optional[int]
|
resample the mixture to this rate. |
None
|
desired_sample
|
Optional[int]
|
deprecated alias for target_sr. |
None
|
seed
|
Optional[int]
|
the base of everything random in the run. Each output
file draws its noise crop, its room and its packet loss from the
base and its own path, so a rerun rebuilds the same files whether
or not they went through a process pool. |
None
|
Source code in kudio/core/synth.py
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 | |
manifest
¶
manifest() -> List[Pair]
The last run as kudio.Pair records, rooms included.
syn returns four-tuples and always has; a fifth element that
appeared only when rt60= was passed would be a return shape that
depends on a keyword, which is the bug this codebase has met most
often. The room is carried here instead, where it travels with the
output path it belongs to.
Each row is a file this run wrote and the clean signal it was built
from. A clean file that lost its output name to another one is not
here -- there is no mixture of it to point at -- and syn warns
when that happens. Nor are the noise-only files is_silence=True
adds: there is no speech in them, so the clean file is not their
reference, and they are in what syn returns instead.
Source code in kudio/core/synth.py
syn_extra_mode
¶
syn_extra_mode(
background_path: Any = "../data/backgroundnoise",
background_snr: Union[float, Sequence[float]] = (5,),
seed: Optional[int] = None,
is_pool: bool = True,
) -> list
Two-stage mixing: noise x background first, then everything x clean.
Stage one mixes every noise with every background at background_snr
in a temporary folder. Stage two mixes every clean file with each of
those mixtures, noises and backgrounds at every SNR, and writes a 2%
share again as noise alone. The output folder is emptied first, under
the same guard as syn; seed is as there. Rooms, the link
and resampling are not applied in this mode, and the output is written
flat, one folder for everything.
Names carry dB on both signs, n5dB and 5dB, where
syn writes n5. Corpora on disk already use both spellings,
so neither changes; match on the manifest, which holds each SNR as a
number, rather than on the name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
is_pool
|
bool
|
write stage two through a process pool once it holds
more than 200 files. A pool needs the calling script's top level
behind |
True
|
Source code in kudio/core/synth.py
492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 | |
syn_waves
staticmethod
¶
syn_waves(
synWavesList: Union[tuple, Sequence[tuple]],
isSilence: bool,
target_sr: Optional[int],
num: Optional[int] = None,
rooms: Optional[Dict[Path, tuple]] = None,
channels: Optional[Dict[Path, tuple]] = None,
targets: Optional[Dict[Path, Path]] = None,
seeds: Optional[Dict[Path, int]] = None,
silent: Optional[Set[Path]] = None,
subtype: str = "PCM_16",
) -> float
Mix one (save_dir, clean_file, noise_file, snr) entry to disk.
With num given, synWavesList is the full list and num its
index (multiprocessing form); otherwise it is a single entry.
rooms maps an output path to (rt60, drr_db, seed). The room is
applied to the clean signal before the noise, and the SNR is then
measured against the reverberant result -- the order a microphone meets
them, and the only one in which the requested SNR is the SNR you get.
channels maps an output path to (spec, seed) and is applied to
the finished mixture, since the wire carries the noise too. targets
maps an output path to where the reverberant-clean signal goes; it is
written before the noise arrives and before the channel, so it is the
speech as the room left it and nothing else.
seeds maps an output path to the seed its noise crop is drawn from,
so the crop does not depend on which worker wrote the file; a path
missing from it draws from numpy's global RNG. Outputs in silent are
written as the noise alone; isSilence=True does that to all.
Returns the largest absolute sample written, mixture or target. Past 1.0, a fixed-point subtype clipped it.
Source code in kudio/core/synth.py
756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 | |
kudio.core.dataset
¶
Dataset manifests: record which mixture came from which clean file.
Synthesizer already returns the pairing it wrote. Writing that down beside
the audio is what stops the pairing from being re-derived later by pattern
matching on filenames — the point at which one renamed directory silently
trains a model against the wrong targets.
The manifest is a plain JSON list of objects, so it stays readable and diffable, and any other tool can consume it without importing kudio.
Classes:
| Name | Description |
|---|---|
Pair |
One training example: a degraded file and the clean signal behind it. |
Label |
A named span of a recording, in seconds. |
Functions:
| Name | Description |
|---|---|
save_manifest |
Write pairs to path as JSON. Parent directories are created. |
load_manifest |
Read a manifest written by |
save_labels |
Write labels as JSON, or as an Audacity label track. |
load_labels |
Read labels written by |
split_pairs |
Shuffle once, then cut into |
Pair
dataclass
¶
Pair(
noisy: str,
clean: str,
noise: str = "",
snr_db: Optional[float] = None,
rt60: Optional[float] = None,
channel: Optional[str] = None,
target: Optional[str] = None,
)
One training example: a degraded file and the clean signal behind it.
Frozen, therefore hashable — splits are checked for overlap by putting the pairs in a set, which silently fails to catch anything if they are not.
Methods:
| Name | Description |
|---|---|
exists |
True when both sides are actually on disk. |
Attributes:
| Name | Type | Description |
|---|---|---|
snr_db |
Optional[float]
|
an int when the SNR is a whole number of dB, as it always was; a float |
rt60 |
Optional[float]
|
reverberation time of the room the clean signal was put through, in |
channel |
Optional[str]
|
the link the mixture travelled down, as |
target |
Optional[str]
|
the reverberant-but-clean signal, when one was written. |
snr_db
class-attribute
instance-attribute
¶
an int when the SNR is a whole number of dB, as it always was; a float when it is not, rather than that float cut down to an int
rt60
class-attribute
instance-attribute
¶
reverberation time of the room the clean signal was put through, in
seconds, or None when it was not. Optional and last, so a manifest
written before rooms existed still loads and a manifest written with
them still loads in an older kudio -- see load_manifest.
channel
class-attribute
instance-attribute
¶
the link the mixture travelled down, as kudio.channel_tag
writes it ('telephone', 'mu_law+loss5%x4', ...), or None.
A string rather than the spec dict it came from: this dataclass is
frozen so that a split can be checked for overlap with a set, and a
dict field would quietly make it unhashable again.
target
class-attribute
instance-attribute
¶
the reverberant-but-clean signal, when one was written. clean is
still the dry target; this is the same speech with the room left on,
for training that should remove the noise and leave the room.
Label
dataclass
¶
A named span of a recording, in seconds. end == start is a point.
What kudio.vad finds, what an annotator marks, what a dataset
builder cuts on — all the same shape, so they get one.
save_manifest
¶
save_manifest(
path: PathLike, pairs: Sequence[Pair]
) -> Path
Write pairs to path as JSON. Parent directories are created.
Paths are written with forward slashes, so a manifest made on Windows loads on Linux and macOS too.
Source code in kudio/core/dataset.py
load_manifest
¶
load_manifest(path: PathLike) -> List[Pair]
Read a manifest written by save_manifest.
Extra keys are dropped with a UserWarning rather than raising — a
manifest written by a later version should still load — but a missing
noisy or clean is an error, since there is no example without them.
Source code in kudio/core/dataset.py
save_labels
¶
save_labels(
path: PathLike,
labels: Sequence[Label],
fmt: Optional[str] = None,
) -> Path
Write labels as JSON, or as an Audacity label track.
>>> kudio.save_labels("clip.labels.json", labels)
>>> kudio.save_labels("clip.txt", labels) # Audacity, ELAN, ...
The format follows the suffix unless you say otherwise: .txt / .tsv
means Audacity's tab-separated start end text, anything else means
JSON. Audacity's format is three columns of plain text that half the audio
world can already read, which is worth more than a private format that only
kudio understands.
Source code in kudio/core/dataset.py
load_labels
¶
load_labels(
path: PathLike, fmt: Optional[str] = None
) -> List[Label]
Read labels written by save_labels, or by Audacity.
Audacity follows a label that has a frequency range with a second row,
\<TAB>low<TAB>high. Those rows are skipped: a Label holds
times, not frequencies.
Raises:
| Type | Description |
|---|---|
ValueError
|
on a malformed row, naming the line — a label file is usually hand-edited at some point, and "line 12 has 2 columns" is worth more than an IndexError. |
Source code in kudio/core/dataset.py
split_pairs
¶
split_pairs(
pairs: Sequence[Pair],
val_split: float = 0.1,
test_split: float = 0.1,
seed: Optional[int] = None,
) -> Tuple[List[Pair], List[Pair], List[Pair]]
Shuffle once, then cut into (train, validation, test).
An example listed more than once is cut down to one first, with a
warning, since dealing the copies out would put the same file in two
splits and then score it as if it had been held out. Rows that disagree
about one mixture file raise instead — see _distinct_examples.
Splitting a tiny set by percentage can leave nothing to train on, so when the shares would leave training empty, the validation and test sets are given up instead: an empty validation set is visible, and a run with no training data at all is not a run. Anything above that is split as asked, even when training ends up with a single example.