walston's picture
Upload folder using huggingface_hub
66b8580 verified
Raw
History Blame Contribute Delete
9.83 kB
#!/usr/bin/env python3
# Copyright 2020 Wen-Chin Huang and Tomoki Hayashi
# Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
"""Evaluate MCD between generated and groundtruth audios with SPTK-based mcep."""
import argparse
import fnmatch
import logging
import multiprocessing as mp
import os
from typing import Dict, List, Tuple
import librosa
import numpy as np
import pysptk
import soundfile as sf
from fastdtw import fastdtw
from scipy import spatial
def find_files(
root_dir: str, query: List[str] = ["*.flac", "*.wav"], include_root_dir: bool = True
) -> List[str]:
"""Find files recursively.
Args:
root_dir (str): Root root_dir to find.
query (List[str]): Query to find.
include_root_dir (bool): If False, root_dir name is not included.
Returns:
List[str]: List of found filenames.
"""
files = []
for root, dirnames, filenames in os.walk(root_dir, followlinks=True):
for q in query:
for filename in fnmatch.filter(filenames, q):
files.append(os.path.join(root, filename))
if not include_root_dir:
files = [file_.replace(root_dir + "/", "") for file_ in files]
return files
def sptk_extract(
x: np.ndarray,
fs: int,
n_fft: int = 512,
n_shift: int = 256,
mcep_dim: int = 25,
mcep_alpha: float = 0.41,
is_padding: bool = False,
) -> np.ndarray:
"""Extract SPTK-based mel-cepstrum.
Args:
x (ndarray): 1D waveform array.
fs (int): Sampling rate
n_fft (int): FFT length in point (default=512).
n_shift (int): Shift length in point (default=256).
mcep_dim (int): Dimension of mel-cepstrum (default=25).
mcep_alpha (float): All pass filter coefficient (default=0.41).
is_padding (bool): Whether to pad the end of signal (default=False).
Returns:
ndarray: Mel-cepstrum with the size (N, n_fft).
"""
# perform padding
if is_padding:
n_pad = n_fft - (len(x) - n_fft) % n_shift
x = np.pad(x, (0, n_pad), "reflect")
# get number of frames
n_frame = (len(x) - n_fft) // n_shift + 1
# get window function
win = pysptk.sptk.hamming(n_fft)
# check mcep and alpha
if mcep_dim is None or mcep_alpha is None:
mcep_dim, mcep_alpha = _get_best_mcep_params(fs)
# calculate spectrogram
mcep = [
pysptk.mcep(
x[n_shift * i : n_shift * i + n_fft] * win,
mcep_dim,
mcep_alpha,
eps=1e-6,
etype=1,
)
for i in range(n_frame)
]
return np.stack(mcep)
def _get_basename(path: str) -> str:
return os.path.splitext(os.path.split(path)[-1])[0]
def _get_best_mcep_params(fs: int) -> Tuple[int, float]:
if fs == 16000:
return 23, 0.42
elif fs == 22050:
return 34, 0.45
elif fs == 24000:
return 34, 0.46
elif fs == 44100:
return 39, 0.53
elif fs == 48000:
return 39, 0.55
else:
raise ValueError(f"Not found the setting for {fs}.")
def calculate(
file_list: List[str],
gt_file_list: List[str],
args: argparse.Namespace,
mcd_dict: Dict,
):
"""Calculate MCD."""
for i, gen_path in enumerate(file_list):
corresponding_list = list(
filter(lambda gt_path: _get_basename(gt_path) in gen_path, gt_file_list)
)
assert len(corresponding_list) == 1
gt_path = corresponding_list[0]
gt_basename = _get_basename(gt_path)
# load wav file as int16
gen_x, gen_fs = sf.read(gen_path, dtype="int16")
gt_x, gt_fs = sf.read(gt_path, dtype="int16")
fs = gen_fs
if gen_fs != gt_fs:
gt_x = librosa.resample(gt_x.astype(np.float), gt_fs, gen_fs)
# extract ground truth and converted features
gen_mcep = sptk_extract(
x=gen_x,
fs=fs,
n_fft=args.n_fft,
n_shift=args.n_shift,
mcep_dim=args.mcep_dim,
mcep_alpha=args.mcep_alpha,
)
gt_mcep = sptk_extract(
x=gt_x,
fs=fs,
n_fft=args.n_fft,
n_shift=args.n_shift,
mcep_dim=args.mcep_dim,
mcep_alpha=args.mcep_alpha,
)
# DTW
_, path = fastdtw(gen_mcep, gt_mcep, dist=spatial.distance.euclidean)
twf = np.array(path).T
gen_mcep_dtw = gen_mcep[twf[0]]
gt_mcep_dtw = gt_mcep[twf[1]]
# MCD
diff2sum = np.sum((gen_mcep_dtw - gt_mcep_dtw) ** 2, 1)
mcd = np.mean(10.0 / np.log(10.0) * np.sqrt(2 * diff2sum), 0)
# logging.info(f"{gt_basename} {mcd:.4f}")
mcd_dict[gt_basename] = mcd
def get_parser() -> argparse.Namespace:
"""Get argument parser."""
parser = argparse.ArgumentParser(description="Evaluate Mel-cepstrum distortion.")
parser.add_argument(
"--gen_wavdir_or_wavscp",
type=str,
help="Path of directory or wav.scp for generated waveforms.",
)
parser.add_argument(
"--gt_wavdir_or_wavscp",
type=str,
help="Path of directory or wav.scp for ground truth waveforms.",
)
parser.add_argument(
"--outdir",
type=str,
help="Path of directory to write the results.",
)
# analysis related
parser.add_argument(
"--mcep_dim",
default=None,
type=int,
help=(
"Dimension of mel cepstrum coefficients. "
"If None, automatically set to the best dimension for the sampling."
),
)
parser.add_argument(
"--mcep_alpha",
default=None,
type=float,
help=(
"All pass constant for mel-cepstrum analysis. "
"If None, automatically set to the best dimension for the sampling."
),
)
parser.add_argument(
"--n_fft",
default=1024,
type=int,
help="The number of FFT points.",
)
parser.add_argument(
"--n_shift",
default=256,
type=int,
help="The number of shift points.",
)
parser.add_argument(
"--nj",
default=16,
type=int,
help="Number of parallel jobs.",
)
parser.add_argument(
"--verbose",
default=1,
type=int,
help="Verbosity level. Higher is more logging.",
)
return parser
def main():
"""Run MCD calculation in parallel."""
args = get_parser().parse_args()
# logging info
if args.verbose > 1:
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s",
)
elif args.verbose > 0:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s",
)
else:
logging.basicConfig(
level=logging.WARN,
format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s",
)
logging.warning("Skip DEBUG/INFO messages")
# find files
if os.path.isdir(args.gen_wavdir_or_wavscp):
gen_files = sorted(find_files(args.gen_wavdir_or_wavscp))
else:
with open(args.gen_wavdir_or_wavscp) as f:
gen_files = [line.strip().split(None, 1)[1] for line in f.readlines()]
if gen_files[0].endswith("|"):
raise ValueError("Not supported wav.scp format.")
if os.path.isdir(args.gt_wavdir_or_wavscp):
gt_files = sorted(find_files(args.gt_wavdir_or_wavscp))
else:
with open(args.gt_wavdir_or_wavscp) as f:
gt_files = [line.strip().split(None, 1)[1] for line in f.readlines()]
if gt_files[0].endswith("|"):
raise ValueError("Not supported wav.scp format.")
# Get and divide list
if len(gen_files) == 0:
raise FileNotFoundError("Not found any generated audio files.")
if len(gen_files) > len(gt_files):
raise ValueError(
"#groundtruth files are less than #generated files "
f"(#gen={len(gen_files)} vs. #gt={len(gt_files)}). "
"Please check the groundtruth directory."
)
logging.info("The number of utterances = %d" % len(gen_files))
file_lists = np.array_split(gen_files, args.nj)
file_lists = [f_list.tolist() for f_list in file_lists]
# multi processing
with mp.Manager() as manager:
mcd_dict = manager.dict()
processes = []
for f in file_lists:
p = mp.Process(target=calculate, args=(f, gt_files, args, mcd_dict))
p.start()
processes.append(p)
# wait for all process
for p in processes:
p.join()
# convert to standard list
mcd_dict = dict(mcd_dict)
# calculate statistics
mean_mcd = np.mean(np.array([v for v in mcd_dict.values()]))
std_mcd = np.std(np.array([v for v in mcd_dict.values()]))
logging.info(f"Average: {mean_mcd:.4f} ± {std_mcd:.4f}")
# write results
if args.outdir is None:
if os.path.isdir(args.gen_wavdir_or_wavscp):
args.outdir = args.gen_wavdir_or_wavscp
else:
args.outdir = os.path.dirname(args.gen_wavdir_or_wavscp)
os.makedirs(args.outdir, exist_ok=True)
with open(f"{args.outdir}/utt2mcd", "w") as f:
for utt_id in sorted(mcd_dict.keys()):
mcd = mcd_dict[utt_id]
f.write(f"{utt_id} {mcd:.4f}\n")
with open(f"{args.outdir}/mcd_avg_result.txt", "w") as f:
f.write(f"#utterances: {len(gen_files)}\n")
f.write(f"Average: {mean_mcd:.4f} ± {std_mcd:.4f}")
logging.info("Successfully finished MCD evaluation.")
if __name__ == "__main__":
main()