File size: 3,714 Bytes
6e802a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# contour_metrics.py: F0 contour diagnostics used by the suite.

from __future__ import annotations

import sys
from pathlib import Path

import numpy as np

REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO))
sys.path.insert(0, str(REPO / "scripts"))


def _f0_semitones(a, sr):
    from score_naturalness_panel import f0_track
    f0, fs = f0_track(a, sr)
    f0 = np.asarray(f0, dtype=float)
    ok = np.isfinite(f0) & (f0 > 0)
    if ok.sum() < 12:
        return None, None, None
    idx = np.arange(len(f0))
    filled = np.interp(idx, idx[ok], f0[ok])
    st = 12.0 * np.log2(filled / np.median(f0[ok]))
    return st, float(fs), ok


def contour_metrics(a, sr) -> dict:
    nan = float("nan")
    st, fs, ok = _f0_semitones(a, sr)
    if st is None or len(st) < 16:
        return {"phrase": nan, "stress": nan, "fast": nan, "prosodic": nan,
                "f0e_corr": nan, "decl": nan}

    x = st - st.mean()
    win = np.hanning(len(x))
    P = np.abs(np.fft.rfft(x * win)) ** 2
    f = np.fft.rfftfreq(len(x), 1.0 / fs)
    band = lambda lo, hi: float(P[(f >= lo) & (f < hi)].sum())
    tot = band(0.2, 20.0) + 1e-12
    phrase, stress, fast = band(0.2, 1.5), band(1.5, 4.0), band(4.0, 20.0)

    hop = max(1, int(round(sr / fs)))
    n = min(len(st), len(a) // hop)
    rms = np.sqrt(np.mean(a[:n * hop].reshape(n, hop) ** 2, axis=1)) + 1e-9
    db = 20.0 * np.log10(rms)
    m = ok[:n] if ok is not None else np.ones(n, bool)
    corr = nan
    if m.sum() >= 12:
        u, v = st[:n][m], db[m]
        if u.std() > 1e-6 and v.std() > 1e-6:
            corr = float(np.corrcoef(u, v)[0, 1])

    t = np.arange(len(st)) / fs
    decl = float(np.polyfit(t, st, 1)[0]) if len(st) > 4 else nan

    return {"phrase": 100 * phrase / tot, "stress": 100 * stress / tot,
            "fast": 100 * fast / tot, "prosodic": 100 * (phrase + stress) / tot,
            "f0e_corr": corr, "decl": decl}


def contour_similarity(a_gen, sr_gen, a_ref, sr_ref) -> float:
    g, _, _ = _f0_semitones(a_gen, sr_gen)
    r, _, _ = _f0_semitones(a_ref, sr_ref)
    if g is None or r is None:
        return float("nan")
    n = min(len(g), len(r), 400)
    if n < 16:
        return float("nan")
    gi = np.interp(np.linspace(0, 1, n), np.linspace(0, 1, len(g)), g)
    ri = np.interp(np.linspace(0, 1, n), np.linspace(0, 1, len(r)), r)
    if gi.std() < 1e-6 or ri.std() < 1e-6:
        return float("nan")
    return float(np.corrcoef(gi, ri)[0, 1])

if __name__ == "__main__":
    import argparse

    import soundfile as sf

    ap = argparse.ArgumentParser()
    ap.add_argument("dirs", nargs="+", help="wav dirs to compare")
    ap.add_argument("--limit", type=int, default=None)
    args = ap.parse_args()

    print(f"{'source':<32} {'n':>3} {'phrase%':>8} {'stress%':>8} {'fast%':>7} "
          f"{'PROSODIC%':>10} {'f0xLoud':>8} {'decl':>7}")
    print("-" * 90)
    for d in args.dirs:
        rows = []
        for w in sorted(Path(d).glob("*.wav"))[:args.limit]:
            a, sr = sf.read(str(w))
            if a.ndim > 1:
                a = a.mean(axis=1)
            rows.append(contour_metrics(a, sr))
        if not rows:
            print(f"{Path(d).name:<32} (no wavs)")
            continue
        mean = lambda k: float(np.nanmean([r[k] for r in rows]))
        print(f"{Path(d).name:<32} {len(rows):>3} {mean('phrase'):>8.1f} "
              f"{mean('stress'):>8.1f} {mean('fast'):>7.1f} {mean('prosodic'):>10.1f} "
              f"{mean('f0e_corr'):>8.2f} {mean('decl'):>7.2f}")
    print("\nprosodic% = pitch motion that is phrase/stress structured (higher = "
          "more expressive intent).\nf0xLoud = stress coupling; decl = "
          "semitones/s drift.")