< Summary

Information
Class: MusicTheory.Theory.Harmony.ProgressionResult
Assembly: MusicTheory
File(s): /home/runner/work/MusicTheory/MusicTheory/Theory/Harmony/ProgressionAnalyzer.cs
Line coverage
100%
Covered lines: 2
Uncovered lines: 0
Coverable lines: 2
Total lines: 698
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Chords()100%11100%
get_Cadences()100%11100%

File(s)

/home/runner/work/MusicTheory/MusicTheory/Theory/Harmony/ProgressionAnalyzer.cs

#LineLine coverage
 1using System.Collections.Generic;
 2
 3namespace MusicTheory.Theory.Harmony;
 4
 5public readonly record struct ProgressionResult(
 176    List<HarmonyAnalysisResult> Chords,
 17    List<(int indexFrom, CadenceType cadence)> Cadences
 8);
 9
 10public static class ProgressionAnalyzer
 11{
 12    public static ProgressionResult Analyze(IReadOnlyList<int[]> pcsList, Key key, IReadOnlyList<FourPartVoicing?>? voic
 13    {
 14        var chords = new List<HarmonyAnalysisResult>(pcsList.Count);
 15        var cads = new List<(int, CadenceType)>();
 16
 17        FourPartVoicing? prevV = null;
 18        RomanNumeral? prevRn = null;
 19        for (int i = 0; i < pcsList.Count; i++)
 20        {
 21            var pcs = pcsList[i];
 22            var v = voicings != null && i < voicings.Count ? voicings[i] : null;
 23            var res = HarmonyAnalyzer.AnalyzeTriad(pcs, key, v, prevV);
 24            chords.Add(res);
 25            if (prevRn != null && res.Roman != null)
 26            {
 27                var cad = CadenceAnalyzer.Detect(prevRn, res.Roman, key.IsMajor);
 28                if (cad != CadenceType.None)
 29                    cads.Add((i - 1, cad));
 30            }
 31            prevV = v;
 32            prevRn = res.Roman;
 33        }
 34
 35        return new ProgressionResult(chords, cads);
 36    }
 37
 38    // Overload: HarmonyOptions を指定して解析(既定は HarmonyOptions.Default と同等)
 39    public static ProgressionResult Analyze(IReadOnlyList<int[]> pcsList, Key key, HarmonyOptions options, IReadOnlyList
 40    {
 41        var chords = new List<HarmonyAnalysisResult>(pcsList.Count);
 42        var cads = new List<(int, CadenceType)>();
 43
 44        FourPartVoicing? prevV = null;
 45        RomanNumeral? prevRn = null;
 46        for (int i = 0; i < pcsList.Count; i++)
 47        {
 48            var pcs = pcsList[i];
 49            var v = voicings != null && i < voicings.Count ? voicings[i] : null;
 50            var res = HarmonyAnalyzer.AnalyzeTriad(pcs, key, options, v, prevV);
 51            chords.Add(res);
 52            if (prevRn != null && res.Roman != null)
 53            {
 54                var cad = CadenceAnalyzer.Detect(prevRn, res.Roman, key.IsMajor);
 55                if (cad != CadenceType.None)
 56                    cads.Add((i - 1, cad));
 57            }
 58            prevV = v;
 59            prevRn = res.Roman;
 60        }
 61
 62        return new ProgressionResult(chords, cads);
 63    }
 64
 65    /// <summary>
 66    /// Analyze a progression and return per-chord harmony results along with detailed cadence diagnostics.
 67    /// </summary>
 68    /// <remarks>
 69    /// Cadence details include:
 70    /// - Cadence type (Authentic/Plagal/Half/Deceptive)
 71    /// - PAC/IAC approximation flag
 72    /// - Cadential 6-4 flag and generic 6-4 classification (<see cref="SixFourType"/>)
 73    ///
 74    /// Half cadence is suppressed when the previous chord is I64 and the current chord is V (I64→V).
 75    /// In that case, a non-cadential entry is returned with <c>SixFour=Cadential</c>, and the following step will emit 
 76    /// </remarks>
 77    /// <param name="pcsList">Sequence of pitch-class sets (0..11), per chord.</param>
 78    /// <param name="key">Tonic and mode used for analysis.</param>
 79    /// <param name="voicings">Optional four-part voicings aligned to <paramref name="pcsList"/> for inversion/6-4 detec
 80    /// <returns>Tuple of <see cref="ProgressionResult"/> and a list of <see cref="CadenceInfo"/>.</returns>
 81    public static (ProgressionResult result, List<CadenceInfo> cadences) AnalyzeWithDetailedCadences(
 82        IReadOnlyList<int[]> pcsList, Key key, IReadOnlyList<FourPartVoicing?>? voicings = null)
 83    {
 84        var chords = new List<HarmonyAnalysisResult>(pcsList.Count);
 85        var cadInfos = new List<CadenceInfo>();
 86
 87    // vMinus1: 直前 (i-1) のボイシング, vMinus2: さらに一つ前 (i-2)
 88    FourPartVoicing? vMinus1 = null; FourPartVoicing? vMinus2 = null;
 89        RomanNumeral? prevRn = null; string? prevTxt = null; string? prevPrevTxt = null;
 90        static string Head(string? txt)
 91        {
 92            if (string.IsNullOrEmpty(txt)) return string.Empty;
 93            int i = 0;
 94            while (i < txt!.Length)
 95            {
 96                char ch = txt[i];
 97                bool ok = ch == 'b' || ch == '#' || ch == 'i' || ch == 'v' || ch == 'I' || ch == 'V' || ch == '/';
 98                if (!ok) break;
 99                i++;
 100            }
 101            if (i == 0) return txt!;
 102            return txt!.Substring(0, i);
 103        }
 104        for (int i = 0; i < pcsList.Count; i++)
 105        {
 106            var pcs = pcsList[i];
 107            var v = voicings != null && i < voicings.Count ? voicings[i] : null;
 108            var res = HarmonyAnalyzer.AnalyzeTriad(pcs, key, v, vMinus1);
 109            chords.Add(res);
 110            if (prevRn != null && res.Roman != null)
 111            {
 112                var info = CadenceAnalyzer.DetectDetailed(i - 1, prevRn, res.Roman, key.IsMajor, key.TonicMidi % 12, pre
 113                if (info.Type != CadenceType.None)
 114                {
 115                    // Ensure non-cadential 6-4 labels (Passing/Pedal) are not attached to cadence entries
 116                    var info2 = info;
 117                    if (info2.SixFour != SixFourType.None && info2.SixFour != SixFourType.Cadential)
 118                        info2 = new CadenceInfo(info2.IndexFrom, info2.Type, info2.IsPerfectAuthentic, info2.HasCadentia
 119                    cadInfos.Add(info2);
 120                }
 121                else
 122                {
 123                    // Prefer voicing-based classification around x64 when neighbors share the same harmony
 124                    bool around64 = v != null && vMinus1 != null && vMinus2 != null && !string.IsNullOrEmpty(prevTxt) &&
 125                    if (around64)
 126                    {
 127                        var hPrevPrev = Head(prevPrevTxt); // (i-2) の Roman
 128                        var hCurr = Head(res.RomanText);
 129                        if (!string.IsNullOrEmpty(hPrevPrev) && hPrevPrev == hCurr)
 130                        {
 131                            int bPrevPrev = vMinus2!.Value.B % 12;
 132                            int bCurr = v!.Value.B % 12;
 133                            var six = (bPrevPrev == bCurr) ? SixFourType.Pedal : SixFourType.Passing;
 134                            cadInfos.Add(new CadenceInfo(info.IndexFrom, info.Type, info.IsPerfectAuthentic, info.HasCad
 135                            goto NextChord1;
 136                        }
 137                    }
 138                    // Fallback: if neighbors share the same harmony and voicings available, decide Pedal vs Passing by 
 139                    if (v != null && vMinus1 != null && vMinus2 != null)
 140                    {
 141                        var hPrevPrev2 = Head(prevPrevTxt);
 142                        var hCurr2 = Head(res.RomanText);
 143                        if (!string.IsNullOrEmpty(hPrevPrev2) && hPrevPrev2 == hCurr2)
 144                        {
 145                            int bPrevPrev = vMinus2!.Value.B % 12;
 146                            int bCurr = v!.Value.B % 12;
 147                            var six = (bPrevPrev == bCurr) ? SixFourType.Pedal : SixFourType.Passing;
 148                            cadInfos.Add(new CadenceInfo(info.IndexFrom, info.Type, info.IsPerfectAuthentic, info.HasCad
 149                            goto NextChord1;
 150                        }
 151                    }
 152                    if (info.SixFour != SixFourType.None && info.SixFour != SixFourType.Cadential)
 153                        cadInfos.Add(info);
 154                }
 155            NextChord1: ;
 156            }
 157            vMinus2 = vMinus1; vMinus1 = v;
 158            prevPrevTxt = prevTxt;
 159            prevRn = res.Roman;
 160            prevTxt = res.RomanText;
 161        }
 162
 163        return (new ProgressionResult(chords, new List<(int, CadenceType)>()), cadInfos);
 164    }
 165
 166    /// <summary>
 167    /// Analyze a progression and return detailed cadence diagnostics with <see cref="HarmonyOptions"/>.
 168    /// </summary>
 169    /// <remarks>
 170    /// The <see cref="HarmonyOptions.ShowNonCadentialSixFour"/> option controls whether non-cadential 6-4 entries
 171    /// (Passing/Pedal with <c>Type==None</c>) are included in the returned cadence list. When set to <c>false</c>,
 172    /// such entries are suppressed; cadential 6-4 information attached to a proper cadence (e.g., I64→V→I → Authentic)
 173    /// remains part of that cadence entry.
 174    /// </remarks>
 175    /// <param name="pcsList">Sequence of pitch-class sets (0..11), per chord.</param>
 176    /// <param name="key">Tonic and mode used for analysis.</param>
 177    /// <param name="options">Harmony options affecting recognition and display of details.</param>
 178    /// <param name="voicings">Optional four-part voicings aligned to <paramref name="pcsList"/>.</param>
 179    /// <returns>Tuple of <see cref="ProgressionResult"/> and a list of <see cref="CadenceInfo"/>.</returns>
 180    public static (ProgressionResult result, List<CadenceInfo> cadences) AnalyzeWithDetailedCadences(
 181        IReadOnlyList<int[]> pcsList, Key key, HarmonyOptions options, IReadOnlyList<FourPartVoicing?>? voicings = null)
 182    {
 183        var chords = new List<HarmonyAnalysisResult>(pcsList.Count);
 184        var cadInfos = new List<CadenceInfo>();
 185
 186    FourPartVoicing? vMinus1 = null; FourPartVoicing? vMinus2 = null;
 187        RomanNumeral? prevRn = null; string? prevTxt = null; string? prevPrevTxt = null;
 188        static string Head(string? txt)
 189        {
 190            if (string.IsNullOrEmpty(txt)) return string.Empty;
 191            int i = 0;
 192            while (i < txt!.Length)
 193            {
 194                char ch = txt[i];
 195                bool ok = ch == 'b' || ch == '#' || ch == 'i' || ch == 'v' || ch == 'I' || ch == 'V' || ch == '/';
 196                if (!ok) break;
 197                i++;
 198            }
 199            if (i == 0) return txt!;
 200            return txt!.Substring(0, i);
 201        }
 202        for (int i = 0; i < pcsList.Count; i++)
 203        {
 204            var pcs = pcsList[i];
 205            var v = voicings != null && i < voicings.Count ? voicings[i] : null;
 206            var res = HarmonyAnalyzer.AnalyzeTriad(pcs, key, options, v, vMinus1);
 207            chords.Add(res);
 208            if (prevRn != null && res.Roman != null)
 209            {
 210                var info = CadenceAnalyzer.DetectDetailed(i - 1, prevRn, res.Roman, key.IsMajor, key.TonicMidi % 12, pre
 211                if (info.Type != CadenceType.None)
 212                {
 213                    var info2 = info;
 214                    if (info2.SixFour != SixFourType.None && info2.SixFour != SixFourType.Cadential)
 215                        info2 = new CadenceInfo(info2.IndexFrom, info2.Type, info2.IsPerfectAuthentic, info2.HasCadentia
 216
 217                    // Relabel cadential 6-4 (I64) as V64-53 when preferred
 218                    if (options.PreferCadentialSixFourAsDominant && info2.HasCadentialSixFour)
 219                    {
 220                        int idxI64 = i - 2;
 221                        if (idxI64 >= 0 && idxI64 < chords.Count)
 222                        {
 223                            var c = chords[idxI64];
 224                            string? txt = c.RomanText;
 225                            bool looksI64 = !string.IsNullOrEmpty(txt) && txt!.EndsWith("64");
 226                            // Only relabel when it looks like a 6-4 triad on tonic (heuristic: startsWith I/i)
 227                            if (looksI64 && (txt!.StartsWith("I") || txt!.StartsWith("i")))
 228                            {
 229                                var relabeled = new HarmonyAnalysisResult(
 230                                    c.Success,
 231                                    RomanNumeral.V,
 232                                    TonalFunction.Dominant,
 233                                    "V64-53",
 234                                    c.Warnings,
 235                                    c.Errors
 236                                );
 237                                chords[idxI64] = relabeled;
 238                            }
 239                        }
 240                    }
 241                    cadInfos.Add(info2);
 242                }
 243                else
 244                {
 245                    // Prefer voicing-based classification when allowed
 246                    if (options.ShowNonCadentialSixFour)
 247                    {
 248                        bool around64 = v != null && vMinus1 != null && vMinus2 != null && !string.IsNullOrEmpty(prevTxt
 249                        if (around64)
 250                        {
 251                            var hPrevPrev = Head(prevPrevTxt);
 252                            var hCurr = Head(res.RomanText);
 253                            if (!string.IsNullOrEmpty(hPrevPrev) && hPrevPrev == hCurr)
 254                            {
 255                                int bPrevPrev = vMinus2!.Value.B % 12;
 256                                int bCurr = v!.Value.B % 12;
 257                                var six = (bPrevPrev == bCurr) ? SixFourType.Pedal : SixFourType.Passing;
 258                                cadInfos.Add(new CadenceInfo(info.IndexFrom, info.Type, info.IsPerfectAuthentic, info.Ha
 259                                goto NextChord2;
 260                            }
 261                        }
 262                        // Fallback: neighbors share harmony + voicings, decide by bass equality
 263                        if (v != null && vMinus1 != null && vMinus2 != null)
 264                        {
 265                            var hPrevPrev2 = Head(prevPrevTxt);
 266                            var hCurr2 = Head(res.RomanText);
 267                            if (!string.IsNullOrEmpty(hPrevPrev2) && hPrevPrev2 == hCurr2)
 268                            {
 269                                int bPrevPrev = vMinus2!.Value.B % 12;
 270                                int bCurr = v!.Value.B % 12;
 271                                var six = (bPrevPrev == bCurr) ? SixFourType.Pedal : SixFourType.Passing;
 272                                cadInfos.Add(new CadenceInfo(info.IndexFrom, info.Type, info.IsPerfectAuthentic, info.Ha
 273                                goto NextChord2;
 274                            }
 275                        }
 276                        if (info.SixFour != SixFourType.None && info.SixFour != SixFourType.Cadential)
 277                            cadInfos.Add(info);
 278                    }
 279                }
 280            NextChord2: ;
 281            }
 282            vMinus2 = vMinus1; vMinus1 = v;
 283            prevPrevTxt = prevTxt;
 284            prevRn = res.Roman;
 285            prevTxt = res.RomanText;
 286        }
 287
 288        return (new ProgressionResult(chords, new List<(int, CadenceType)>()), cadInfos);
 289    }
 290
 291    public static (ProgressionResult result, List<(int start, int end, Key key)> segments) AnalyzeWithKeyEstimate(
 292        IReadOnlyList<int[]> pcsList, Key initialKey, IReadOnlyList<FourPartVoicing?>? voicings = null, int window = 2)
 293    {
 294        var keys = KeyEstimator.EstimatePerChord(pcsList, initialKey, window);
 295        var chords = new List<HarmonyAnalysisResult>(pcsList.Count);
 296        var cads = new List<(int, CadenceType)>();
 297
 298        FourPartVoicing? prevV = null; RomanNumeral? prevRn = null; Key currKey = initialKey;
 299        for (int i = 0; i < pcsList.Count; i++)
 300        {
 301            currKey = keys[i];
 302            var v = voicings != null && i < voicings.Count ? voicings[i] : null;
 303            var res = HarmonyAnalyzer.AnalyzeTriad(pcsList[i], currKey, v, prevV);
 304            chords.Add(res);
 305            if (prevRn != null && res.Roman != null)
 306            {
 307                var cad = CadenceAnalyzer.Detect(prevRn, res.Roman, currKey.IsMajor);
 308                if (cad != CadenceType.None) cads.Add((i - 1, cad));
 309            }
 310            prevV = v; prevRn = res.Roman;
 311        }
 312
 313        // Build key segments
 314        var segs = new List<(int start, int end, Key key)>();
 315        if (keys.Count > 0)
 316        {
 317            int s = 0;
 318            for (int i = 1; i < keys.Count; i++)
 319            {
 320                if (!SameKey(keys[i - 1], keys[i]))
 321                {
 322                    segs.Add((s, i - 1, keys[i - 1]));
 323                    s = i;
 324                }
 325            }
 326            segs.Add((s, keys.Count - 1, keys[^1]));
 327        }
 328
 329        return (new ProgressionResult(chords, cads), segs);
 330    }
 331
 332    // Overload: HarmonyOptions を併用したキー推定 + 和音解析
 333    public static (ProgressionResult result, List<(int start, int end, Key key)> segments) AnalyzeWithKeyEstimate(
 334        IReadOnlyList<int[]> pcsList, Key initialKey, HarmonyOptions options, IReadOnlyList<FourPartVoicing?>? voicings 
 335    {
 336        var keys = KeyEstimator.EstimatePerChord(pcsList, initialKey, window);
 337        var chords = new List<HarmonyAnalysisResult>(pcsList.Count);
 338        var cads = new List<(int, CadenceType)>();
 339
 340        FourPartVoicing? prevV = null; RomanNumeral? prevRn = null; Key currKey = initialKey;
 341        for (int i = 0; i < pcsList.Count; i++)
 342        {
 343            currKey = keys[i];
 344            var v = voicings != null && i < voicings.Count ? voicings[i] : null;
 345            var res = HarmonyAnalyzer.AnalyzeTriad(pcsList[i], currKey, options, v, prevV);
 346            chords.Add(res);
 347            if (prevRn != null && res.Roman != null)
 348            {
 349                var cad = CadenceAnalyzer.Detect(prevRn, res.Roman, currKey.IsMajor);
 350                if (cad != CadenceType.None) cads.Add((i - 1, cad));
 351            }
 352            prevV = v; prevRn = res.Roman;
 353        }
 354
 355        // Build key segments
 356        var segs = new List<(int start, int end, Key key)>();
 357        if (keys.Count > 0)
 358        {
 359            int s = 0;
 360            for (int i = 1; i < keys.Count; i++)
 361            {
 362                if (!SameKey(keys[i - 1], keys[i]))
 363                {
 364                    segs.Add((s, i - 1, keys[i - 1]));
 365                    s = i;
 366                }
 367            }
 368            segs.Add((s, keys.Count - 1, keys[^1]));
 369        }
 370
 371        return (new ProgressionResult(chords, cads), segs);
 372    }
 373
 374    // Overload exposing the per-chord key trace and allowing estimator Options.
 375    public static (ProgressionResult result, List<(int start, int end, Key key)> segments, List<Key> keys) AnalyzeWithKe
 376        IReadOnlyList<int[]> pcsList, Key initialKey, KeyEstimator.Options options, IReadOnlyList<FourPartVoicing?>? voi
 377    {
 378        var keys = KeyEstimator.EstimatePerChord(pcsList, initialKey, options);
 379        // Enforce MinSwitchIndex: force staying on previous key before this index
 380        if (options.MinSwitchIndex > 0 && keys.Count > 1)
 381        {
 382            if (options.MinSwitchIndex >= keys.Count)
 383            {
 384                // Full lock: keep entire sequence on the initial key
 385                for (int i = 0; i < keys.Count; i++) keys[i] = initialKey;
 386            }
 387            else
 388            {
 389                int limit = Math.Min(options.MinSwitchIndex, keys.Count);
 390                for (int i = 1; i < limit; i++) keys[i] = keys[i - 1];
 391            }
 392        }
 393        var chords = new List<HarmonyAnalysisResult>(pcsList.Count);
 394        var cads = new List<(int, CadenceType)>();
 395
 396        FourPartVoicing? prevV = null; RomanNumeral? prevRn = null; Key currKey = initialKey;
 397        for (int i = 0; i < pcsList.Count; i++)
 398        {
 399            currKey = keys[i];
 400            var v = voicings != null && i < voicings.Count ? voicings[i] : null;
 401            var res = HarmonyAnalyzer.AnalyzeTriad(pcsList[i], currKey, v, prevV);
 402            chords.Add(res);
 403            if (prevRn != null && res.Roman != null)
 404            {
 405                var cad = CadenceAnalyzer.Detect(prevRn, res.Roman, currKey.IsMajor);
 406                if (cad != CadenceType.None) cads.Add((i - 1, cad));
 407            }
 408            prevV = v; prevRn = res.Roman;
 409        }
 410
 411        var segs = new List<(int start, int end, Key key)>();
 412        if (keys.Count > 0)
 413        {
 414            int s = 0;
 415            for (int i = 1; i < keys.Count; i++)
 416            {
 417                if (!SameKey(keys[i - 1], keys[i]))
 418                {
 419                    segs.Add((s, i - 1, keys[i - 1]));
 420                    s = i;
 421                }
 422            }
 423            segs.Add((s, keys.Count - 1, keys[^1]));
 424        }
 425
 426        return (new ProgressionResult(chords, cads), segs, keys);
 427    }
 428
 429    // Overload: KeyEstimator.Options + HarmonyOptions(トレース無し)
 430    public static (ProgressionResult result, List<(int start, int end, Key key)> segments, List<Key> keys) AnalyzeWithKe
 431        IReadOnlyList<int[]> pcsList,
 432        Key initialKey,
 433        KeyEstimator.Options options,
 434        HarmonyOptions harmonyOptions,
 435        IReadOnlyList<FourPartVoicing?>? voicings = null)
 436    {
 437        var keys = KeyEstimator.EstimatePerChord(pcsList, initialKey, options);
 438        // Enforce MinSwitchIndex: force staying on previous key before this index
 439        if (options.MinSwitchIndex > 0 && keys.Count > 1)
 440        {
 441            if (options.MinSwitchIndex >= keys.Count)
 442            {
 443                for (int i = 0; i < keys.Count; i++) keys[i] = initialKey;
 444            }
 445            else
 446            {
 447                int limit = Math.Min(options.MinSwitchIndex, keys.Count);
 448                for (int i = 1; i < limit; i++) keys[i] = keys[i - 1];
 449            }
 450        }
 451        var chords = new List<HarmonyAnalysisResult>(pcsList.Count);
 452        var cads = new List<(int, CadenceType)>();
 453
 454        FourPartVoicing? prevV = null; RomanNumeral? prevRn = null; Key currKey = initialKey;
 455        for (int i = 0; i < pcsList.Count; i++)
 456        {
 457            currKey = keys[i];
 458            var v = voicings != null && i < voicings.Count ? voicings[i] : null;
 459            var res = HarmonyAnalyzer.AnalyzeTriad(pcsList[i], currKey, harmonyOptions, v, prevV);
 460            chords.Add(res);
 461            if (prevRn != null && res.Roman != null)
 462            {
 463                var cad = CadenceAnalyzer.Detect(prevRn, res.Roman, currKey.IsMajor);
 464                if (cad != CadenceType.None) cads.Add((i - 1, cad));
 465            }
 466            prevV = v; prevRn = res.Roman;
 467        }
 468
 469        var segs = new List<(int start, int end, Key key)>();
 470        if (keys.Count > 0)
 471        {
 472            int s = 0;
 473            for (int i = 1; i < keys.Count; i++)
 474            {
 475                if (!SameKey(keys[i - 1], keys[i]))
 476                {
 477                    segs.Add((s, i - 1, keys[i - 1]));
 478                    s = i;
 479                }
 480            }
 481            segs.Add((s, keys.Count - 1, keys[^1]));
 482        }
 483
 484        return (new ProgressionResult(chords, cads), segs, keys);
 485    }
 486
 487    // New overload: expose per-chord key-estimation trace via out parameter.
 488    public static (ProgressionResult result, List<(int start, int end, Key key)> segments, List<Key> keys) AnalyzeWithKe
 489        IReadOnlyList<int[]> pcsList,
 490        Key initialKey,
 491        KeyEstimator.Options options,
 492        out List<KeyEstimator.TraceEntry> trace,
 493        IReadOnlyList<FourPartVoicing?>? voicings = null)
 494    {
 495    var keys = KeyEstimator.EstimatePerChord(pcsList, initialKey, options, out trace, voicings);
 496        // Enforce MinSwitchIndex: force staying on previous key before this index
 497        if (options.MinSwitchIndex > 0 && keys.Count > 1)
 498        {
 499            if (options.MinSwitchIndex >= keys.Count)
 500            {
 501                for (int i = 0; i < keys.Count; i++) keys[i] = initialKey;
 502            }
 503            else
 504            {
 505                int limit = Math.Min(options.MinSwitchIndex, keys.Count);
 506                for (int i = 1; i < limit; i++) keys[i] = keys[i - 1];
 507            }
 508        }
 509        var chords = new List<HarmonyAnalysisResult>(pcsList.Count);
 510        var cads = new List<(int, CadenceType)>();
 511
 512        FourPartVoicing? prevV = null; RomanNumeral? prevRn = null; Key currKey = initialKey;
 513        for (int i = 0; i < pcsList.Count; i++)
 514        {
 515            currKey = keys[i];
 516            var v = voicings != null && i < voicings.Count ? voicings[i] : null;
 517            var res = HarmonyAnalyzer.AnalyzeTriad(pcsList[i], currKey, v, prevV);
 518            chords.Add(res);
 519            if (prevRn != null && res.Roman != null)
 520            {
 521                var cad = CadenceAnalyzer.Detect(prevRn, res.Roman, currKey.IsMajor);
 522                if (cad != CadenceType.None) cads.Add((i - 1, cad));
 523            }
 524            prevV = v; prevRn = res.Roman;
 525        }
 526
 527        var segs = new List<(int start, int end, Key key)>();
 528        if (keys.Count > 0)
 529        {
 530            int s = 0;
 531            for (int i = 1; i < keys.Count; i++)
 532            {
 533                if (!SameKey(keys[i - 1], keys[i]))
 534                {
 535                    segs.Add((s, i - 1, keys[i - 1]));
 536                    s = i;
 537                }
 538            }
 539            segs.Add((s, keys.Count - 1, keys[^1]));
 540        }
 541
 542        return (new ProgressionResult(chords, cads), segs, keys);
 543    }
 544
 545    // Overload: KeyEstimator.Options + HarmonyOptions(トレース有り)
 546    public static (ProgressionResult result, List<(int start, int end, Key key)> segments, List<Key> keys) AnalyzeWithKe
 547        IReadOnlyList<int[]> pcsList,
 548        Key initialKey,
 549        KeyEstimator.Options options,
 550        HarmonyOptions harmonyOptions,
 551        out List<KeyEstimator.TraceEntry> trace,
 552        IReadOnlyList<FourPartVoicing?>? voicings = null)
 553    {
 554        var keys = KeyEstimator.EstimatePerChord(pcsList, initialKey, options, out trace, voicings);
 555        // Enforce MinSwitchIndex: force staying on previous key before this index
 556        if (options.MinSwitchIndex > 0 && keys.Count > 1)
 557        {
 558            if (options.MinSwitchIndex >= keys.Count)
 559            {
 560                for (int i = 0; i < keys.Count; i++) keys[i] = initialKey;
 561            }
 562            else
 563            {
 564                int limit = Math.Min(options.MinSwitchIndex, keys.Count);
 565                for (int i = 1; i < limit; i++) keys[i] = keys[i - 1];
 566            }
 567        }
 568        var chords = new List<HarmonyAnalysisResult>(pcsList.Count);
 569        var cads = new List<(int, CadenceType)>();
 570
 571        FourPartVoicing? prevV = null; RomanNumeral? prevRn = null; Key currKey = initialKey;
 572        for (int i = 0; i < pcsList.Count; i++)
 573        {
 574            currKey = keys[i];
 575            var v = voicings != null && i < voicings.Count ? voicings[i] : null;
 576            var res = HarmonyAnalyzer.AnalyzeTriad(pcsList[i], currKey, harmonyOptions, v, prevV);
 577            chords.Add(res);
 578            if (prevRn != null && res.Roman != null)
 579            {
 580                var cad = CadenceAnalyzer.Detect(prevRn, res.Roman, currKey.IsMajor);
 581                if (cad != CadenceType.None) cads.Add((i - 1, cad));
 582            }
 583            prevV = v; prevRn = res.Roman;
 584        }
 585
 586        var segs = new List<(int start, int end, Key key)>();
 587        if (keys.Count > 0)
 588        {
 589            int s = 0;
 590            for (int i = 1; i < keys.Count; i++)
 591            {
 592                if (!SameKey(keys[i - 1], keys[i]))
 593                {
 594                    segs.Add((s, i - 1, keys[i - 1]));
 595                    s = i;
 596                }
 597            }
 598            segs.Add((s, keys.Count - 1, keys[^1]));
 599        }
 600
 601        return (new ProgressionResult(chords, cads), segs, keys);
 602    }
 603
 604    // Overload: also compute a simple confidence for each key segment (0..1)
 605    // confidence is derived from per-chord trace: avg of (max - secondBest) / max within the segment (where max>0)
 606    public static (ProgressionResult result, List<(int start, int end, Key key, double confidence)> segments, List<Key> 
 607        IReadOnlyList<int[]> pcsList,
 608        Key initialKey,
 609        KeyEstimator.Options options,
 610        out List<KeyEstimator.TraceEntry> trace,
 611        IReadOnlyList<FourPartVoicing?>? voicings,
 612        bool withConfidence)
 613    {
 614        var (res, segs, keys) = AnalyzeWithKeyEstimate(pcsList, initialKey, options, out trace, voicings);
 615        var segsWithConf = new List<(int start, int end, Key key, double confidence)>(segs.Count);
 616        foreach (var s in segs)
 617        {
 618            double sum = 0; int count = 0;
 619            for (int i = s.start; i <= s.end && i < trace.Count; i++)
 620            {
 621                int max = Math.Max(0, trace[i].MaxScore);
 622                int second = Math.Max(0, trace[i].SecondBestScore);
 623                if (max > 0)
 624                {
 625                    sum += Math.Clamp((max - second) / (double)max, 0.0, 1.0);
 626                    count++;
 627                }
 628            }
 629            double conf = count > 0 ? sum / count : 0.0;
 630            segsWithConf.Add((s.start, s.end, s.key, conf));
 631        }
 632        return (res, segsWithConf, keys);
 633    }
 634
 635    // Overload: KeyEstimator.Options + HarmonyOptions(トレース有り + 信頼度)
 636    public static (ProgressionResult result, List<(int start, int end, Key key, double confidence)> segments, List<Key> 
 637        IReadOnlyList<int[]> pcsList,
 638        Key initialKey,
 639        KeyEstimator.Options options,
 640        HarmonyOptions harmonyOptions,
 641        out List<KeyEstimator.TraceEntry> trace,
 642        IReadOnlyList<FourPartVoicing?>? voicings,
 643        bool withConfidence)
 644    {
 645        var (res, segs, keys) = AnalyzeWithKeyEstimate(pcsList, initialKey, options, harmonyOptions, out trace, voicings
 646        var segsWithConf = new List<(int start, int end, Key key, double confidence)>(segs.Count);
 647        foreach (var s in segs)
 648        {
 649            double sum = 0; int count = 0;
 650            for (int i = s.start; i <= s.end && i < trace.Count; i++)
 651            {
 652                int max = Math.Max(0, trace[i].MaxScore);
 653                int second = Math.Max(0, trace[i].SecondBestScore);
 654                if (max > 0)
 655                {
 656                    sum += Math.Clamp((max - second) / (double)max, 0.0, 1.0);
 657                    count++;
 658                }
 659            }
 660            double conf = count > 0 ? sum / count : 0.0;
 661            segsWithConf.Add((s.start, s.end, s.key, conf));
 662        }
 663        return (res, segsWithConf, keys);
 664    }
 665
 666    // New: Modulation-aware segments post-filtering
 667    // - minLength: require at least N chords for a new key segment
 668    // - minConfidence: require average normalized margin >= threshold (0..1) for the segment
 669    public static (ProgressionResult result, List<(int start, int end, Key key, double confidence)> segments, List<Key> 
 670        IReadOnlyList<int[]> pcsList,
 671        Key initialKey,
 672        KeyEstimator.Options options,
 673        out List<KeyEstimator.TraceEntry> trace,
 674        IReadOnlyList<FourPartVoicing?>? voicings,
 675        int minLength = 2,
 676        double minConfidence = 0.2)
 677    {
 678        var (res, segsWithConf, keys) = AnalyzeWithKeyEstimate(pcsList, initialKey, options, out trace, voicings, withCo
 679        var filtered = new List<(int start, int end, Key key, double confidence)>();
 680        foreach (var s in segsWithConf)
 681        {
 682            int len = s.end - s.start + 1;
 683            if (len >= minLength && s.confidence >= minConfidence)
 684            {
 685                filtered.Add(s);
 686            }
 687        }
 688        // Safety: ensure at least one segment is returned to avoid empty results in borderline cases.
 689        // If all segments were filtered out by thresholds, return the first segment as a fallback.
 690        if (filtered.Count == 0 && segsWithConf.Count > 0)
 691        {
 692            filtered.Add(segsWithConf[0]);
 693        }
 694        return (res, filtered, keys);
 695    }
 696
 697    private static bool SameKey(Key a, Key b) => a.IsMajor == b.IsMajor && ((a.TonicMidi % 12 + 12) % 12) == ((b.TonicMi
 698}

Methods/Properties

get_Chords()
get_Cadences()