< Summary

Information
Class: MusicTheory.Theory.Harmony.ProgressionAnalyzer
Assembly: MusicTheory
File(s): /home/runner/work/MusicTheory/MusicTheory/Theory/Harmony/ProgressionAnalyzer.cs
Line coverage
70%
Covered lines: 240
Uncovered lines: 99
Coverable lines: 339
Total lines: 698
Line coverage: 70.7%
Branch coverage
70%
Covered branches: 251
Total branches: 358
Branch coverage: 70.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Analyze(...)83.33%1212100%
Analyze(...)83.33%1212100%
AnalyzeWithDetailedCadences(...)89.58%594883.33%
Head()90%2020100%
AnalyzeWithDetailedCadences(...)84.84%876683.05%
Head()90%2020100%
AnalyzeWithKeyEstimate(...)88.88%1818100%
AnalyzeWithKeyEstimate(...)0%342180%
AnalyzeWithKeyEstimate(...)0%812280%
AnalyzeWithKeyEstimate(...)0%812280%
AnalyzeWithKeyEstimate(...)85.71%282892.59%
AnalyzeWithKeyEstimate(...)92.85%282892.59%
AnalyzeWithKeyEstimate(...)100%1010100%
AnalyzeWithKeyEstimate(...)80%101084.61%
AnalyzeWithKeyEstimateAndModulation(...)100%1010100%
SameKey(...)100%22100%

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(
 6    List<HarmonyAnalysisResult> Chords,
 7    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    {
 114        var chords = new List<HarmonyAnalysisResult>(pcsList.Count);
 115        var cads = new List<(int, CadenceType)>();
 16
 117        FourPartVoicing? prevV = null;
 118        RomanNumeral? prevRn = null;
 1019        for (int i = 0; i < pcsList.Count; i++)
 20        {
 421            var pcs = pcsList[i];
 422            var v = voicings != null && i < voicings.Count ? voicings[i] : null;
 423            var res = HarmonyAnalyzer.AnalyzeTriad(pcs, key, v, prevV);
 424            chords.Add(res);
 425            if (prevRn != null && res.Roman != null)
 26            {
 327                var cad = CadenceAnalyzer.Detect(prevRn, res.Roman, key.IsMajor);
 328                if (cad != CadenceType.None)
 229                    cads.Add((i - 1, cad));
 30            }
 431            prevV = v;
 432            prevRn = res.Roman;
 33        }
 34
 135        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    {
 141        var chords = new List<HarmonyAnalysisResult>(pcsList.Count);
 142        var cads = new List<(int, CadenceType)>();
 43
 144        FourPartVoicing? prevV = null;
 145        RomanNumeral? prevRn = null;
 846        for (int i = 0; i < pcsList.Count; i++)
 47        {
 348            var pcs = pcsList[i];
 349            var v = voicings != null && i < voicings.Count ? voicings[i] : null;
 350            var res = HarmonyAnalyzer.AnalyzeTriad(pcs, key, options, v, prevV);
 351            chords.Add(res);
 352            if (prevRn != null && res.Roman != null)
 53            {
 254                var cad = CadenceAnalyzer.Detect(prevRn, res.Roman, key.IsMajor);
 255                if (cad != CadenceType.None)
 256                    cads.Add((i - 1, cad));
 57            }
 358            prevV = v;
 359            prevRn = res.Roman;
 60        }
 61
 162        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    {
 3984        var chords = new List<HarmonyAnalysisResult>(pcsList.Count);
 3985        var cadInfos = new List<CadenceInfo>();
 86
 87    // vMinus1: 直前 (i-1) のボイシング, vMinus2: さらに一つ前 (i-2)
 7888    FourPartVoicing? vMinus1 = null; FourPartVoicing? vMinus2 = null;
 11789        RomanNumeral? prevRn = null; string? prevTxt = null; string? prevPrevTxt = null;
 90        static string Head(string? txt)
 91        {
 2892            if (string.IsNullOrEmpty(txt)) return string.Empty;
 2893            int i = 0;
 8294            while (i < txt!.Length)
 95            {
 6296                char ch = txt[i];
 6297                bool ok = ch == 'b' || ch == '#' || ch == 'i' || ch == 'v' || ch == 'I' || ch == 'V' || ch == '/';
 6298                if (!ok) break;
 5499                i++;
 100            }
 28101            if (i == 0) return txt!;
 28102            return txt!.Substring(0, i);
 103        }
 274104        for (int i = 0; i < pcsList.Count; i++)
 105        {
 98106            var pcs = pcsList[i];
 98107            var v = voicings != null && i < voicings.Count ? voicings[i] : null;
 98108            var res = HarmonyAnalyzer.AnalyzeTriad(pcs, key, v, vMinus1);
 98109            chords.Add(res);
 98110            if (prevRn != null && res.Roman != null)
 111            {
 59112                var info = CadenceAnalyzer.DetectDetailed(i - 1, prevRn, res.Roman, key.IsMajor, key.TonicMidi % 12, pre
 59113                if (info.Type != CadenceType.None)
 114                {
 115                    // Ensure non-cadential 6-4 labels (Passing/Pedal) are not attached to cadence entries
 25116                    var info2 = info;
 25117                    if (info2.SixFour != SixFourType.None && info2.SixFour != SixFourType.Cadential)
 0118                        info2 = new CadenceInfo(info2.IndexFrom, info2.Type, info2.IsPerfectAuthentic, info2.HasCadentia
 25119                    cadInfos.Add(info2);
 120                }
 121                else
 122                {
 123                    // Prefer voicing-based classification around x64 when neighbors share the same harmony
 34124                    bool around64 = v != null && vMinus1 != null && vMinus2 != null && !string.IsNullOrEmpty(prevTxt) &&
 34125                    if (around64)
 126                    {
 13127                        var hPrevPrev = Head(prevPrevTxt); // (i-2) の Roman
 13128                        var hCurr = Head(res.RomanText);
 13129                        if (!string.IsNullOrEmpty(hPrevPrev) && hPrevPrev == hCurr)
 130                        {
 12131                            int bPrevPrev = vMinus2!.Value.B % 12;
 12132                            int bCurr = v!.Value.B % 12;
 12133                            var six = (bPrevPrev == bCurr) ? SixFourType.Pedal : SixFourType.Passing;
 12134                            cadInfos.Add(new CadenceInfo(info.IndexFrom, info.Type, info.IsPerfectAuthentic, info.HasCad
 12135                            goto NextChord1;
 136                        }
 137                    }
 138                    // Fallback: if neighbors share the same harmony and voicings available, decide Pedal vs Passing by 
 22139                    if (v != null && vMinus1 != null && vMinus2 != null)
 140                    {
 1141                        var hPrevPrev2 = Head(prevPrevTxt);
 1142                        var hCurr2 = Head(res.RomanText);
 1143                        if (!string.IsNullOrEmpty(hPrevPrev2) && hPrevPrev2 == hCurr2)
 144                        {
 0145                            int bPrevPrev = vMinus2!.Value.B % 12;
 0146                            int bCurr = v!.Value.B % 12;
 0147                            var six = (bPrevPrev == bCurr) ? SixFourType.Pedal : SixFourType.Passing;
 0148                            cadInfos.Add(new CadenceInfo(info.IndexFrom, info.Type, info.IsPerfectAuthentic, info.HasCad
 0149                            goto NextChord1;
 150                        }
 151                    }
 22152                    if (info.SixFour != SixFourType.None && info.SixFour != SixFourType.Cadential)
 0153                        cadInfos.Add(info);
 154                }
 155            NextChord1: ;
 156            }
 196157            vMinus2 = vMinus1; vMinus1 = v;
 98158            prevPrevTxt = prevTxt;
 98159            prevRn = res.Roman;
 98160            prevTxt = res.RomanText;
 161        }
 162
 39163        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    {
 22183        var chords = new List<HarmonyAnalysisResult>(pcsList.Count);
 22184        var cadInfos = new List<CadenceInfo>();
 185
 44186    FourPartVoicing? vMinus1 = null; FourPartVoicing? vMinus2 = null;
 66187        RomanNumeral? prevRn = null; string? prevTxt = null; string? prevPrevTxt = null;
 188        static string Head(string? txt)
 189        {
 2190            if (string.IsNullOrEmpty(txt)) return string.Empty;
 2191            int i = 0;
 6192            while (i < txt!.Length)
 193            {
 5194                char ch = txt[i];
 5195                bool ok = ch == 'b' || ch == '#' || ch == 'i' || ch == 'v' || ch == 'I' || ch == 'V' || ch == '/';
 5196                if (!ok) break;
 4197                i++;
 198            }
 2199            if (i == 0) return txt!;
 2200            return txt!.Substring(0, i);
 201        }
 162202        for (int i = 0; i < pcsList.Count; i++)
 203        {
 59204            var pcs = pcsList[i];
 59205            var v = voicings != null && i < voicings.Count ? voicings[i] : null;
 59206            var res = HarmonyAnalyzer.AnalyzeTriad(pcs, key, options, v, vMinus1);
 59207            chords.Add(res);
 59208            if (prevRn != null && res.Roman != null)
 209            {
 37210                var info = CadenceAnalyzer.DetectDetailed(i - 1, prevRn, res.Roman, key.IsMajor, key.TonicMidi % 12, pre
 37211                if (info.Type != CadenceType.None)
 212                {
 19213                    var info2 = info;
 19214                    if (info2.SixFour != SixFourType.None && info2.SixFour != SixFourType.Cadential)
 0215                        info2 = new CadenceInfo(info2.IndexFrom, info2.Type, info2.IsPerfectAuthentic, info2.HasCadentia
 216
 217                    // Relabel cadential 6-4 (I64) as V64-53 when preferred
 19218                    if (options.PreferCadentialSixFourAsDominant && info2.HasCadentialSixFour)
 219                    {
 3220                        int idxI64 = i - 2;
 3221                        if (idxI64 >= 0 && idxI64 < chords.Count)
 222                        {
 3223                            var c = chords[idxI64];
 3224                            string? txt = c.RomanText;
 3225                            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)
 3227                            if (looksI64 && (txt!.StartsWith("I") || txt!.StartsWith("i")))
 228                            {
 3229                                var relabeled = new HarmonyAnalysisResult(
 3230                                    c.Success,
 3231                                    RomanNumeral.V,
 3232                                    TonalFunction.Dominant,
 3233                                    "V64-53",
 3234                                    c.Warnings,
 3235                                    c.Errors
 3236                                );
 3237                                chords[idxI64] = relabeled;
 238                            }
 239                        }
 240                    }
 19241                    cadInfos.Add(info2);
 242                }
 243                else
 244                {
 245                    // Prefer voicing-based classification when allowed
 18246                    if (options.ShowNonCadentialSixFour)
 247                    {
 4248                        bool around64 = v != null && vMinus1 != null && vMinus2 != null && !string.IsNullOrEmpty(prevTxt
 4249                        if (around64)
 250                        {
 1251                            var hPrevPrev = Head(prevPrevTxt);
 1252                            var hCurr = Head(res.RomanText);
 1253                            if (!string.IsNullOrEmpty(hPrevPrev) && hPrevPrev == hCurr)
 254                            {
 1255                                int bPrevPrev = vMinus2!.Value.B % 12;
 1256                                int bCurr = v!.Value.B % 12;
 1257                                var six = (bPrevPrev == bCurr) ? SixFourType.Pedal : SixFourType.Passing;
 1258                                cadInfos.Add(new CadenceInfo(info.IndexFrom, info.Type, info.IsPerfectAuthentic, info.Ha
 1259                                goto NextChord2;
 260                            }
 261                        }
 262                        // Fallback: neighbors share harmony + voicings, decide by bass equality
 3263                        if (v != null && vMinus1 != null && vMinus2 != null)
 264                        {
 0265                            var hPrevPrev2 = Head(prevPrevTxt);
 0266                            var hCurr2 = Head(res.RomanText);
 0267                            if (!string.IsNullOrEmpty(hPrevPrev2) && hPrevPrev2 == hCurr2)
 268                            {
 0269                                int bPrevPrev = vMinus2!.Value.B % 12;
 0270                                int bCurr = v!.Value.B % 12;
 0271                                var six = (bPrevPrev == bCurr) ? SixFourType.Pedal : SixFourType.Passing;
 0272                                cadInfos.Add(new CadenceInfo(info.IndexFrom, info.Type, info.IsPerfectAuthentic, info.Ha
 0273                                goto NextChord2;
 274                            }
 275                        }
 3276                        if (info.SixFour != SixFourType.None && info.SixFour != SixFourType.Cadential)
 0277                            cadInfos.Add(info);
 278                    }
 279                }
 280            NextChord2: ;
 281            }
 118282            vMinus2 = vMinus1; vMinus1 = v;
 59283            prevPrevTxt = prevTxt;
 59284            prevRn = res.Roman;
 59285            prevTxt = res.RomanText;
 286        }
 287
 22288        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    {
 6294        var keys = KeyEstimator.EstimatePerChord(pcsList, initialKey, window);
 6295        var chords = new List<HarmonyAnalysisResult>(pcsList.Count);
 6296        var cads = new List<(int, CadenceType)>();
 297
 18298        FourPartVoicing? prevV = null; RomanNumeral? prevRn = null; Key currKey = initialKey;
 56299        for (int i = 0; i < pcsList.Count; i++)
 300        {
 22301            currKey = keys[i];
 22302            var v = voicings != null && i < voicings.Count ? voicings[i] : null;
 22303            var res = HarmonyAnalyzer.AnalyzeTriad(pcsList[i], currKey, v, prevV);
 22304            chords.Add(res);
 22305            if (prevRn != null && res.Roman != null)
 306            {
 16307                var cad = CadenceAnalyzer.Detect(prevRn, res.Roman, currKey.IsMajor);
 29308                if (cad != CadenceType.None) cads.Add((i - 1, cad));
 309            }
 44310            prevV = v; prevRn = res.Roman;
 311        }
 312
 313        // Build key segments
 6314        var segs = new List<(int start, int end, Key key)>();
 6315        if (keys.Count > 0)
 316        {
 6317            int s = 0;
 44318            for (int i = 1; i < keys.Count; i++)
 319            {
 16320                if (!SameKey(keys[i - 1], keys[i]))
 321                {
 10322                    segs.Add((s, i - 1, keys[i - 1]));
 10323                    s = i;
 324                }
 325            }
 6326            segs.Add((s, keys.Count - 1, keys[^1]));
 327        }
 328
 6329        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    {
 0336        var keys = KeyEstimator.EstimatePerChord(pcsList, initialKey, window);
 0337        var chords = new List<HarmonyAnalysisResult>(pcsList.Count);
 0338        var cads = new List<(int, CadenceType)>();
 339
 0340        FourPartVoicing? prevV = null; RomanNumeral? prevRn = null; Key currKey = initialKey;
 0341        for (int i = 0; i < pcsList.Count; i++)
 342        {
 0343            currKey = keys[i];
 0344            var v = voicings != null && i < voicings.Count ? voicings[i] : null;
 0345            var res = HarmonyAnalyzer.AnalyzeTriad(pcsList[i], currKey, options, v, prevV);
 0346            chords.Add(res);
 0347            if (prevRn != null && res.Roman != null)
 348            {
 0349                var cad = CadenceAnalyzer.Detect(prevRn, res.Roman, currKey.IsMajor);
 0350                if (cad != CadenceType.None) cads.Add((i - 1, cad));
 351            }
 0352            prevV = v; prevRn = res.Roman;
 353        }
 354
 355        // Build key segments
 0356        var segs = new List<(int start, int end, Key key)>();
 0357        if (keys.Count > 0)
 358        {
 0359            int s = 0;
 0360            for (int i = 1; i < keys.Count; i++)
 361            {
 0362                if (!SameKey(keys[i - 1], keys[i]))
 363                {
 0364                    segs.Add((s, i - 1, keys[i - 1]));
 0365                    s = i;
 366                }
 367            }
 0368            segs.Add((s, keys.Count - 1, keys[^1]));
 369        }
 370
 0371        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    {
 0378        var keys = KeyEstimator.EstimatePerChord(pcsList, initialKey, options);
 379        // Enforce MinSwitchIndex: force staying on previous key before this index
 0380        if (options.MinSwitchIndex > 0 && keys.Count > 1)
 381        {
 0382            if (options.MinSwitchIndex >= keys.Count)
 383            {
 384                // Full lock: keep entire sequence on the initial key
 0385                for (int i = 0; i < keys.Count; i++) keys[i] = initialKey;
 386            }
 387            else
 388            {
 0389                int limit = Math.Min(options.MinSwitchIndex, keys.Count);
 0390                for (int i = 1; i < limit; i++) keys[i] = keys[i - 1];
 391            }
 392        }
 0393        var chords = new List<HarmonyAnalysisResult>(pcsList.Count);
 0394        var cads = new List<(int, CadenceType)>();
 395
 0396        FourPartVoicing? prevV = null; RomanNumeral? prevRn = null; Key currKey = initialKey;
 0397        for (int i = 0; i < pcsList.Count; i++)
 398        {
 0399            currKey = keys[i];
 0400            var v = voicings != null && i < voicings.Count ? voicings[i] : null;
 0401            var res = HarmonyAnalyzer.AnalyzeTriad(pcsList[i], currKey, v, prevV);
 0402            chords.Add(res);
 0403            if (prevRn != null && res.Roman != null)
 404            {
 0405                var cad = CadenceAnalyzer.Detect(prevRn, res.Roman, currKey.IsMajor);
 0406                if (cad != CadenceType.None) cads.Add((i - 1, cad));
 407            }
 0408            prevV = v; prevRn = res.Roman;
 409        }
 410
 0411        var segs = new List<(int start, int end, Key key)>();
 0412        if (keys.Count > 0)
 413        {
 0414            int s = 0;
 0415            for (int i = 1; i < keys.Count; i++)
 416            {
 0417                if (!SameKey(keys[i - 1], keys[i]))
 418                {
 0419                    segs.Add((s, i - 1, keys[i - 1]));
 0420                    s = i;
 421                }
 422            }
 0423            segs.Add((s, keys.Count - 1, keys[^1]));
 424        }
 425
 0426        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    {
 0437        var keys = KeyEstimator.EstimatePerChord(pcsList, initialKey, options);
 438        // Enforce MinSwitchIndex: force staying on previous key before this index
 0439        if (options.MinSwitchIndex > 0 && keys.Count > 1)
 440        {
 0441            if (options.MinSwitchIndex >= keys.Count)
 442            {
 0443                for (int i = 0; i < keys.Count; i++) keys[i] = initialKey;
 444            }
 445            else
 446            {
 0447                int limit = Math.Min(options.MinSwitchIndex, keys.Count);
 0448                for (int i = 1; i < limit; i++) keys[i] = keys[i - 1];
 449            }
 450        }
 0451        var chords = new List<HarmonyAnalysisResult>(pcsList.Count);
 0452        var cads = new List<(int, CadenceType)>();
 453
 0454        FourPartVoicing? prevV = null; RomanNumeral? prevRn = null; Key currKey = initialKey;
 0455        for (int i = 0; i < pcsList.Count; i++)
 456        {
 0457            currKey = keys[i];
 0458            var v = voicings != null && i < voicings.Count ? voicings[i] : null;
 0459            var res = HarmonyAnalyzer.AnalyzeTriad(pcsList[i], currKey, harmonyOptions, v, prevV);
 0460            chords.Add(res);
 0461            if (prevRn != null && res.Roman != null)
 462            {
 0463                var cad = CadenceAnalyzer.Detect(prevRn, res.Roman, currKey.IsMajor);
 0464                if (cad != CadenceType.None) cads.Add((i - 1, cad));
 465            }
 0466            prevV = v; prevRn = res.Roman;
 467        }
 468
 0469        var segs = new List<(int start, int end, Key key)>();
 0470        if (keys.Count > 0)
 471        {
 0472            int s = 0;
 0473            for (int i = 1; i < keys.Count; i++)
 474            {
 0475                if (!SameKey(keys[i - 1], keys[i]))
 476                {
 0477                    segs.Add((s, i - 1, keys[i - 1]));
 0478                    s = i;
 479                }
 480            }
 0481            segs.Add((s, keys.Count - 1, keys[^1]));
 482        }
 483
 0484        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    {
 8495    var keys = KeyEstimator.EstimatePerChord(pcsList, initialKey, options, out trace, voicings);
 496        // Enforce MinSwitchIndex: force staying on previous key before this index
 8497        if (options.MinSwitchIndex > 0 && keys.Count > 1)
 498        {
 1499            if (options.MinSwitchIndex >= keys.Count)
 500            {
 11501                for (int i = 0; i < keys.Count; i++) keys[i] = initialKey;
 502            }
 503            else
 504            {
 0505                int limit = Math.Min(options.MinSwitchIndex, keys.Count);
 0506                for (int i = 1; i < limit; i++) keys[i] = keys[i - 1];
 507            }
 508        }
 8509        var chords = new List<HarmonyAnalysisResult>(pcsList.Count);
 8510        var cads = new List<(int, CadenceType)>();
 511
 24512        FourPartVoicing? prevV = null; RomanNumeral? prevRn = null; Key currKey = initialKey;
 86513        for (int i = 0; i < pcsList.Count; i++)
 514        {
 35515            currKey = keys[i];
 35516            var v = voicings != null && i < voicings.Count ? voicings[i] : null;
 35517            var res = HarmonyAnalyzer.AnalyzeTriad(pcsList[i], currKey, v, prevV);
 35518            chords.Add(res);
 35519            if (prevRn != null && res.Roman != null)
 520            {
 26521                var cad = CadenceAnalyzer.Detect(prevRn, res.Roman, currKey.IsMajor);
 46522                if (cad != CadenceType.None) cads.Add((i - 1, cad));
 523            }
 70524            prevV = v; prevRn = res.Roman;
 525        }
 526
 8527        var segs = new List<(int start, int end, Key key)>();
 8528        if (keys.Count > 0)
 529        {
 8530            int s = 0;
 70531            for (int i = 1; i < keys.Count; i++)
 532            {
 27533                if (!SameKey(keys[i - 1], keys[i]))
 534                {
 2535                    segs.Add((s, i - 1, keys[i - 1]));
 2536                    s = i;
 537                }
 538            }
 8539            segs.Add((s, keys.Count - 1, keys[^1]));
 540        }
 541
 8542        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    {
 6554        var keys = KeyEstimator.EstimatePerChord(pcsList, initialKey, options, out trace, voicings);
 555        // Enforce MinSwitchIndex: force staying on previous key before this index
 6556        if (options.MinSwitchIndex > 0 && keys.Count > 1)
 557        {
 2558            if (options.MinSwitchIndex >= keys.Count)
 559            {
 28560                for (int i = 0; i < keys.Count; i++) keys[i] = initialKey;
 561            }
 562            else
 563            {
 0564                int limit = Math.Min(options.MinSwitchIndex, keys.Count);
 0565                for (int i = 1; i < limit; i++) keys[i] = keys[i - 1];
 566            }
 567        }
 6568        var chords = new List<HarmonyAnalysisResult>(pcsList.Count);
 6569        var cads = new List<(int, CadenceType)>();
 570
 18571        FourPartVoicing? prevV = null; RomanNumeral? prevRn = null; Key currKey = initialKey;
 44572        for (int i = 0; i < pcsList.Count; i++)
 573        {
 16574            currKey = keys[i];
 16575            var v = voicings != null && i < voicings.Count ? voicings[i] : null;
 16576            var res = HarmonyAnalyzer.AnalyzeTriad(pcsList[i], currKey, harmonyOptions, v, prevV);
 16577            chords.Add(res);
 16578            if (prevRn != null && res.Roman != null)
 579            {
 9580                var cad = CadenceAnalyzer.Detect(prevRn, res.Roman, currKey.IsMajor);
 17581                if (cad != CadenceType.None) cads.Add((i - 1, cad));
 582            }
 32583            prevV = v; prevRn = res.Roman;
 584        }
 585
 6586        var segs = new List<(int start, int end, Key key)>();
 6587        if (keys.Count > 0)
 588        {
 6589            int s = 0;
 32590            for (int i = 1; i < keys.Count; i++)
 591            {
 10592                if (!SameKey(keys[i - 1], keys[i]))
 593                {
 2594                    segs.Add((s, i - 1, keys[i - 1]));
 2595                    s = i;
 596                }
 597            }
 6598            segs.Add((s, keys.Count - 1, keys[^1]));
 599        }
 600
 6601        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    {
 5614        var (res, segs, keys) = AnalyzeWithKeyEstimate(pcsList, initialKey, options, out trace, voicings);
 5615        var segsWithConf = new List<(int start, int end, Key key, double confidence)>(segs.Count);
 24616        foreach (var s in segs)
 617        {
 14618            double sum = 0; int count = 0;
 66619            for (int i = s.start; i <= s.end && i < trace.Count; i++)
 620            {
 26621                int max = Math.Max(0, trace[i].MaxScore);
 26622                int second = Math.Max(0, trace[i].SecondBestScore);
 26623                if (max > 0)
 624                {
 21625                    sum += Math.Clamp((max - second) / (double)max, 0.0, 1.0);
 21626                    count++;
 627                }
 628            }
 7629            double conf = count > 0 ? sum / count : 0.0;
 7630            segsWithConf.Add((s.start, s.end, s.key, conf));
 631        }
 5632        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    {
 1645        var (res, segs, keys) = AnalyzeWithKeyEstimate(pcsList, initialKey, options, harmonyOptions, out trace, voicings
 1646        var segsWithConf = new List<(int start, int end, Key key, double confidence)>(segs.Count);
 4647        foreach (var s in segs)
 648        {
 2649            double sum = 0; int count = 0;
 12650            for (int i = s.start; i <= s.end && i < trace.Count; i++)
 651            {
 5652                int max = Math.Max(0, trace[i].MaxScore);
 5653                int second = Math.Max(0, trace[i].SecondBestScore);
 5654                if (max > 0)
 655                {
 0656                    sum += Math.Clamp((max - second) / (double)max, 0.0, 1.0);
 0657                    count++;
 658                }
 659            }
 1660            double conf = count > 0 ? sum / count : 0.0;
 1661            segsWithConf.Add((s.start, s.end, s.key, conf));
 662        }
 1663        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    {
 4678        var (res, segsWithConf, keys) = AnalyzeWithKeyEstimate(pcsList, initialKey, options, out trace, voicings, withCo
 4679        var filtered = new List<(int start, int end, Key key, double confidence)>();
 20680        foreach (var s in segsWithConf)
 681        {
 6682            int len = s.end - s.start + 1;
 6683            if (len >= minLength && s.confidence >= minConfidence)
 684            {
 4685                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.
 4690        if (filtered.Count == 0 && segsWithConf.Count > 0)
 691        {
 2692            filtered.Add(segsWithConf[0]);
 693        }
 4694        return (res, filtered, keys);
 695    }
 696
 53697    private static bool SameKey(Key a, Key b) => a.IsMajor == b.IsMajor && ((a.TonicMidi % 12 + 12) % 12) == ((b.TonicMi
 698}

Methods/Properties

Analyze(System.Collections.Generic.IReadOnlyList`1<System.Int32[]>,MusicTheory.Theory.Harmony.Key,System.Collections.Generic.IReadOnlyList`1<System.Nullable`1<MusicTheory.Theory.Harmony.FourPartVoicing>>)
Analyze(System.Collections.Generic.IReadOnlyList`1<System.Int32[]>,MusicTheory.Theory.Harmony.Key,MusicTheory.Theory.Harmony.HarmonyOptions,System.Collections.Generic.IReadOnlyList`1<System.Nullable`1<MusicTheory.Theory.Harmony.FourPartVoicing>>)
AnalyzeWithDetailedCadences(System.Collections.Generic.IReadOnlyList`1<System.Int32[]>,MusicTheory.Theory.Harmony.Key,System.Collections.Generic.IReadOnlyList`1<System.Nullable`1<MusicTheory.Theory.Harmony.FourPartVoicing>>)
Head()
AnalyzeWithDetailedCadences(System.Collections.Generic.IReadOnlyList`1<System.Int32[]>,MusicTheory.Theory.Harmony.Key,MusicTheory.Theory.Harmony.HarmonyOptions,System.Collections.Generic.IReadOnlyList`1<System.Nullable`1<MusicTheory.Theory.Harmony.FourPartVoicing>>)
Head()
AnalyzeWithKeyEstimate(System.Collections.Generic.IReadOnlyList`1<System.Int32[]>,MusicTheory.Theory.Harmony.Key,System.Collections.Generic.IReadOnlyList`1<System.Nullable`1<MusicTheory.Theory.Harmony.FourPartVoicing>>,System.Int32)
AnalyzeWithKeyEstimate(System.Collections.Generic.IReadOnlyList`1<System.Int32[]>,MusicTheory.Theory.Harmony.Key,MusicTheory.Theory.Harmony.HarmonyOptions,System.Collections.Generic.IReadOnlyList`1<System.Nullable`1<MusicTheory.Theory.Harmony.FourPartVoicing>>,System.Int32)
AnalyzeWithKeyEstimate(System.Collections.Generic.IReadOnlyList`1<System.Int32[]>,MusicTheory.Theory.Harmony.Key,MusicTheory.Theory.Harmony.KeyEstimator/Options,System.Collections.Generic.IReadOnlyList`1<System.Nullable`1<MusicTheory.Theory.Harmony.FourPartVoicing>>)
AnalyzeWithKeyEstimate(System.Collections.Generic.IReadOnlyList`1<System.Int32[]>,MusicTheory.Theory.Harmony.Key,MusicTheory.Theory.Harmony.KeyEstimator/Options,MusicTheory.Theory.Harmony.HarmonyOptions,System.Collections.Generic.IReadOnlyList`1<System.Nullable`1<MusicTheory.Theory.Harmony.FourPartVoicing>>)
AnalyzeWithKeyEstimate(System.Collections.Generic.IReadOnlyList`1<System.Int32[]>,MusicTheory.Theory.Harmony.Key,MusicTheory.Theory.Harmony.KeyEstimator/Options,System.Collections.Generic.List`1<MusicTheory.Theory.Harmony.KeyEstimator/TraceEntry>&,System.Collections.Generic.IReadOnlyList`1<System.Nullable`1<MusicTheory.Theory.Harmony.FourPartVoicing>>)
AnalyzeWithKeyEstimate(System.Collections.Generic.IReadOnlyList`1<System.Int32[]>,MusicTheory.Theory.Harmony.Key,MusicTheory.Theory.Harmony.KeyEstimator/Options,MusicTheory.Theory.Harmony.HarmonyOptions,System.Collections.Generic.List`1<MusicTheory.Theory.Harmony.KeyEstimator/TraceEntry>&,System.Collections.Generic.IReadOnlyList`1<System.Nullable`1<MusicTheory.Theory.Harmony.FourPartVoicing>>)
AnalyzeWithKeyEstimate(System.Collections.Generic.IReadOnlyList`1<System.Int32[]>,MusicTheory.Theory.Harmony.Key,MusicTheory.Theory.Harmony.KeyEstimator/Options,System.Collections.Generic.List`1<MusicTheory.Theory.Harmony.KeyEstimator/TraceEntry>&,System.Collections.Generic.IReadOnlyList`1<System.Nullable`1<MusicTheory.Theory.Harmony.FourPartVoicing>>,System.Boolean)
AnalyzeWithKeyEstimate(System.Collections.Generic.IReadOnlyList`1<System.Int32[]>,MusicTheory.Theory.Harmony.Key,MusicTheory.Theory.Harmony.KeyEstimator/Options,MusicTheory.Theory.Harmony.HarmonyOptions,System.Collections.Generic.List`1<MusicTheory.Theory.Harmony.KeyEstimator/TraceEntry>&,System.Collections.Generic.IReadOnlyList`1<System.Nullable`1<MusicTheory.Theory.Harmony.FourPartVoicing>>,System.Boolean)
AnalyzeWithKeyEstimateAndModulation(System.Collections.Generic.IReadOnlyList`1<System.Int32[]>,MusicTheory.Theory.Harmony.Key,MusicTheory.Theory.Harmony.KeyEstimator/Options,System.Collections.Generic.List`1<MusicTheory.Theory.Harmony.KeyEstimator/TraceEntry>&,System.Collections.Generic.IReadOnlyList`1<System.Nullable`1<MusicTheory.Theory.Harmony.FourPartVoicing>>,System.Int32,System.Double)
SameKey(MusicTheory.Theory.Harmony.Key,MusicTheory.Theory.Harmony.Key)