< Summary

Information
Class: MusicTheory.Theory.Time.Rest
Assembly: MusicTheory
File(s): /home/runner/work/MusicTheory/MusicTheory/Theory/Time/Duration.cs
Line coverage
100%
Covered lines: 1
Uncovered lines: 0
Coverable lines: 1
Total lines: 499
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_Duration()100%11100%

File(s)

/home/runner/work/MusicTheory/MusicTheory/Theory/Time/Duration.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Linq;
 4
 5namespace MusicTheory.Theory.Time;
 6
 7/// <summary>
 8/// 基本音符種類 (倍全~128分) - Whole を 1 とする比率は <see cref="BaseNoteValueExtensions"/> を参照。
 9/// </summary>
 10public enum BaseNoteValue
 11{
 12    DoubleWhole, // Breve (2 * Whole)
 13    Whole,
 14    Half,
 15    Quarter,
 16    Eighth,
 17    Sixteenth,
 18    ThirtySecond,
 19    SixtyFourth,
 20    OneHundredTwentyEighth
 21}
 22
 23public static class BaseNoteValueExtensions
 24{
 25    /// <summary>Whole Note を 1.0 (= 1920 ticks/PPQ480*4) とした時の分数 (分子,分母) を返す。</summary>
 26    public static (int num, int den) GetFraction(this BaseNoteValue value) => value switch
 27    {
 28        BaseNoteValue.DoubleWhole => (2,1),
 29        BaseNoteValue.Whole => (1,1),
 30        BaseNoteValue.Half => (1,2),
 31        BaseNoteValue.Quarter => (1,4),
 32        BaseNoteValue.Eighth => (1,8),
 33        BaseNoteValue.Sixteenth => (1,16),
 34        BaseNoteValue.ThirtySecond => (1,32),
 35        BaseNoteValue.SixtyFourth => (1,64),
 36        BaseNoteValue.OneHundredTwentyEighth => (1,128),
 37        _ => throw new ArgumentOutOfRangeException(nameof(value), value, null)
 38    };
 39}
 40
 41/// <summary>連符。ActualCount 個を NormalCount 個分の時間に詰める (例: 三連符 = (3,2))。</summary>
 42public readonly record struct Tuplet(int ActualCount, int NormalCount)
 43{
 44    public double Factor => NormalCount / (double)ActualCount; // 時間倍率
 45    public RationalFactor FactorRational => new(NormalCount, ActualCount);
 46    public static Tuplet Create(int actual, int normal) => new(actual, normal);
 47    public override string ToString() => $"{ActualCount}:{NormalCount}";
 48}
 49
 50/// <summary>有理数係数 (簡約済み)。Whole Note=1 を基準とした比率計算用。</summary>
 51public readonly struct RationalFactor : IEquatable<RationalFactor>
 52{
 53    public int Numerator { get; }
 54    public int Denominator { get; }
 55    public RationalFactor(int numerator, int denominator)
 56    {
 57        if (denominator == 0) throw new DivideByZeroException();
 58        if (numerator == 0) { Numerator = 0; Denominator = 1; return; }
 59        var sign = Math.Sign(numerator) * Math.Sign(denominator);
 60        numerator = Math.Abs(numerator); denominator = Math.Abs(denominator);
 61        var g = Gcd(numerator, denominator);
 62        Numerator = sign * (numerator / g);
 63        Denominator = denominator / g;
 64    }
 65    public static RationalFactor FromFraction(int num, int den) => new(num, den);
 66    public static RationalFactor operator +(RationalFactor a, RationalFactor b)
 67        => new(a.Numerator * b.Denominator + b.Numerator * a.Denominator, a.Denominator * b.Denominator);
 68    public static RationalFactor operator *(RationalFactor a, RationalFactor b)
 69        => new(a.Numerator * b.Numerator, a.Denominator * b.Denominator);
 70    public static RationalFactor operator *(RationalFactor a, int k) => new(a.Numerator * k, a.Denominator);
 71    public static RationalFactor operator /(RationalFactor a, int k) => new(a.Numerator, a.Denominator * k);
 72    public double ToDouble() => Numerator / (double)Denominator;
 73    public bool Equals(RationalFactor other) => Numerator == other.Numerator && Denominator == other.Denominator;
 74    public override bool Equals(object? obj) => obj is RationalFactor rf && Equals(rf);
 75    public override int GetHashCode() => HashCode.Combine(Numerator, Denominator);
 76    public override string ToString() => $"{Numerator}/{Denominator}";
 77    private static int Gcd(int a, int b) { while (b != 0) (a, b) = (b, a % b); return a; }
 78}
 79
 80/// <summary>
 81/// 単一の音価 (基底 + 付点 + 連符) を表す。Tie は別途加算演算で対応。
 82/// Whole Note を 1920 ticks (PPQ=480 * 4/4分) とする。
 83/// </summary>
 84public readonly struct Duration : IEquatable<Duration>, IComparable<Duration>
 85{
 86    public const int TicksPerQuarter = 480;
 87    public const int TicksPerWhole = TicksPerQuarter * 4; // 1920
 88    public const int MaxDots = 3; // 慣例上 3 まで
 89
 90    /// <summary>Whole Note 基準の有理数 (例: 1/4 = Quarter)。</summary>
 91    public RationalFactor WholeFraction { get; }
 92    public int Ticks => (int)((long)WholeFraction.Numerator * TicksPerWhole / WholeFraction.Denominator);
 93
 94    public Duration(RationalFactor fraction) { WholeFraction = fraction; }
 95
 96    public static Duration FromBase(BaseNoteValue value, int dots = 0, Tuplet? tuplet = null)
 97    {
 98        if (dots < 0) throw new ArgumentOutOfRangeException(nameof(dots));
 99        if (dots > MaxDots) throw new ArgumentException($"dots>{MaxDots} は未サポート", nameof(dots));
 100        var (n, d) = value.GetFraction();
 101        var frac = new RationalFactor(n, d);
 102        if (dots > 0)
 103        {
 104            // 付点倍率: (2 - 1/2^dots) = (2*2^dots -1)/2^dots
 105            int pow = 1 << dots; // 2^dots
 106            int numMul = 2 * pow - 1;
 107            frac = frac * new RationalFactor(numMul, pow);
 108        }
 109        if (tuplet.HasValue)
 110        {
 111            // 連符倍率 (Normal/Actual)
 112            var t = tuplet.Value;
 113            frac = frac * t.FactorRational;
 114        }
 115        return new Duration(frac);
 116    }
 117
 118    public static Duration FromTicks(int ticks)
 119    {
 120        if (ticks < 0) throw new ArgumentOutOfRangeException(nameof(ticks));
 121        // fraction = ticks / TicksPerWhole
 122        int num = ticks; int den = TicksPerWhole;
 123        return new Duration(new RationalFactor(num, den));
 124    }
 125
 126    public static Duration operator +(Duration a, Duration b) => new(a.WholeFraction + b.WholeFraction);
 127    public bool Equals(Duration other) => WholeFraction.Equals(other.WholeFraction);
 128    public override bool Equals(object? obj) => obj is Duration d && Equals(d);
 129    public override int GetHashCode() => WholeFraction.GetHashCode();
 130    public override string ToString() => $"{WholeFraction} ({Ticks} ticks)";
 131
 132    /// <summary>単一の (BaseNoteValue, dots, tuplet=null) で表現できるか判定。</summary>
 133    public bool TryAsSimple(out BaseNoteValue baseValue, out int dots)
 134    {
 135        // 試行: 各 base + dots(0..3) を比較
 136        foreach (BaseNoteValue v in Enum.GetValues(typeof(BaseNoteValue)))
 137        {
 138            for (int d = 0; d <= 3; d++)
 139            {
 140                var candidate = FromBase(v, d);
 141                if (candidate.Equals(this)) { baseValue = v; dots = d; return true; }
 142            }
 143        }
 144        baseValue = default; dots = 0; return false;
 145    }
 146
 147    /// <summary>Tuplet を含む表現の探索 (よく使う代表的連符パターンのみ)。</summary>
 148    public bool TryDecomposeFull(out BaseNoteValue baseValue, out int dots, out Tuplet? tuplet, bool extendedTuplets=fal
 149    {
 150        var tuplets = extendedTuplets ? GenerateExtendedTuplets() : GenerateCommonTuplets();
 151        var bases = Enum.GetValues(typeof(BaseNoteValue)).Cast<BaseNoteValue>().ToArray();
 152        var matches = new List<(BaseNoteValue v, int d, Tuplet? t, int baseTicks)>();
 153        foreach (BaseNoteValue v in bases)
 154        {
 155            for (int d = 0; d <= MaxDots; d++)
 156            {
 157                var simple = FromBase(v, d);
 158                if (simple.Equals(this)) { baseValue = v; dots = d; tuplet = null; return true; }
 159                foreach (var t in tuplets)
 160                {
 161                    var cand = FromBase(v, d, t);
 162                    if (cand.Equals(this)) { matches.Add((v, d, t, FromBase(v,0).Ticks)); }
 163                }
 164            }
 165        }
 166        if (matches.Count > 0)
 167        {
 168            int PrefN(Tuplet? tp) => tp.HasValue ? (tp.Value.NormalCount==4?0: tp.Value.NormalCount==2?1:2) : 3;
 169            var chosen = matches
 170                .OrderBy(m => PrefN(m.t))
 171                .ThenBy(m => m.baseTicks)
 172                .ThenBy(m => m.t.HasValue ? m.t.Value.ActualCount : int.MaxValue)
 173                .First();
 174            baseValue = chosen.v; dots = chosen.d; tuplet = chosen.t; return true;
 175        }
 176        baseValue = default; dots = 0; tuplet = null; return false;
 177    }
 178    private static Tuplet[] GenerateCommonTuplets()
 179    {
 180        // 代表的連符 (3,2),(5,4),(7,4),(5,2),(7,8),(9,8)
 181        return new[]{ new Tuplet(3,2), new Tuplet(5,4), new Tuplet(7,4), new Tuplet(5,2), new Tuplet(7,8), new Tuplet(9,
 182    }
 183    private static Tuplet[] GenerateExtendedTuplets()
 184    {
 185        // 2..12 範囲で actual!=normal を列挙
 186        var list = new List<Tuplet>();
 187        for (int normal=2; normal<=12; normal++)
 188            for (int actual=2; actual<=12; actual++)
 189                if (actual!=normal)
 190                    list.Add(new Tuplet(actual, normal));
 191        return list.ToArray();
 192    }
 193
 194    public int CompareTo(Duration other)
 195        => (WholeFraction.Numerator * other.WholeFraction.Denominator)
 196           .CompareTo(other.WholeFraction.Numerator * WholeFraction.Denominator);
 197}
 198
 199/// <summary>休符。Duration ラッパー。</summary>
 35200public readonly partial record struct Rest(Duration Duration);
 201
 202/// <summary>音符 (将来的に Pitch / Velocity / Articulation を拡張)。</summary>
 203public readonly partial record struct Note(Duration Duration, int Pitch = 60, int Velocity = 100, int Channel = 0)
 204{
 205    public Note Tie(Note next) => new(Duration + next.Duration);
 206}
 207
 208public static class DurationFactory
 209{
 210    public static Duration DoubleWhole(int dots=0) => Duration.FromBase(BaseNoteValue.DoubleWhole, dots);
 211    public static Duration Whole(int dots=0) => Duration.FromBase(BaseNoteValue.Whole, dots);
 212    public static Duration Half(int dots=0) => Duration.FromBase(BaseNoteValue.Half, dots);
 213    public static Duration Quarter(int dots=0) => Duration.FromBase(BaseNoteValue.Quarter, dots);
 214    public static Duration Eighth(int dots=0) => Duration.FromBase(BaseNoteValue.Eighth, dots);
 215    public static Duration Sixteenth(int dots=0) => Duration.FromBase(BaseNoteValue.Sixteenth, dots);
 216    public static Duration ThirtySecond(int dots=0) => Duration.FromBase(BaseNoteValue.ThirtySecond, dots);
 217    public static Duration SixtyFourth(int dots=0) => Duration.FromBase(BaseNoteValue.SixtyFourth, dots);
 218    public static Duration OneHundredTwentyEighth(int dots=0) => Duration.FromBase(BaseNoteValue.OneHundredTwentyEighth,
 219    public static Duration Tuplet(BaseNoteValue baseValue, Tuplet tuplet, int dots=0) => Duration.FromBase(baseValue, do
 220}
 221
 222/// <summary>
 223/// 休符生成ヘルパ (Note 用 DurationFactory と対称)。
 224/// </summary>
 225public static class RestFactory
 226{
 227    public static Rest DoubleWhole(int dots=0) => new(DurationFactory.DoubleWhole(dots));
 228    public static Rest Whole(int dots=0) => new(DurationFactory.Whole(dots));
 229    public static Rest Half(int dots=0) => new(DurationFactory.Half(dots));
 230    public static Rest Quarter(int dots=0) => new(DurationFactory.Quarter(dots));
 231    public static Rest Eighth(int dots=0) => new(DurationFactory.Eighth(dots));
 232    public static Rest Sixteenth(int dots=0) => new(DurationFactory.Sixteenth(dots));
 233    public static Rest ThirtySecond(int dots=0) => new(DurationFactory.ThirtySecond(dots));
 234    public static Rest SixtyFourth(int dots=0) => new(DurationFactory.SixtyFourth(dots));
 235    public static Rest OneHundredTwentyEighth(int dots=0) => new(DurationFactory.OneHundredTwentyEighth(dots));
 236    public static Rest Tuplet(BaseNoteValue baseValue, Tuplet tuplet, int dots=0) => new(DurationFactory.Tuplet(baseValu
 237    public static Rest FromTicks(int ticks) => new(Duration.FromTicks(ticks));
 238}
 239
 240/// <summary>Note / Rest 共通インターフェース。</summary>
 241public interface IDurationEntity { Duration Duration { get; } }
 242public readonly partial record struct Note : IDurationEntity {}
 243public readonly partial record struct Rest : IDurationEntity {}
 244
 245/// <summary>休符や音符列の正規化ユーティリティ。</summary>
 246public static class DurationSequenceUtils
 247{
 248    /// <summary>隣接する休符をまとめ、可能なら単純形 (例: 8分+8分=4分) に縮約。</summary>
 249    public static IReadOnlyList<IDurationEntity> NormalizeRests(IEnumerable<IDurationEntity> seq, bool advancedSplit=fal
 250    {
 251        var list = new List<IDurationEntity>();
 252        // 連続休符をランとして収集し、flush 時にまとめて推論。
 253        List<Duration>? restRun = null;
 254        var customTuplets = additionalTuplets?.ToList() ?? new List<Tuplet>();
 255        void FlushPending()
 256        {
 257            if (restRun==null || restRun.Count==0) return;
 258            var sumTicks = restRun.Sum(d=>d.Ticks);
 259            var sumDur = Duration.FromTicks(sumTicks);
 260            // allowSplit: 複数要素から柔軟 tuplets に縮約
 261            if (allowSplit)
 262            {
 263                var inferred = InferCompositeTupletFlexible(restRun, extendedTuplets);
 264                if (inferred.HasValue && inferred.Value.TryDecomposeFull(out var baseVal2, out var dots2, out var tuplet
 265                {
 266                    // 要素ごとに分解して同一長の Rest を並べる (記譜最適化: tuplet 表記の休符)
 267                    int total = restRun.Sum(x=>x.Ticks);
 268                    int elemTicks = total / tuplet2.Value.ActualCount;
 269                    var elemDur = Duration.FromBase(baseVal2, dots2, tuplet2.Value);
 270                    for (int i=0;i<tuplet2.Value.ActualCount;i++)
 271                        list.Add(new Rest(elemDur));
 272                    restRun.Clear(); return;
 273                }
 274            }
 275            // 単純形 / 既存分解
 276            if (sumDur.TryAsSimple(out var b, out var dots))
 277                list.Add(new Rest(Duration.FromBase(b, dots)));
 278            else if (sumDur.TryDecomposeFull(out var b2, out var dots2, out var tp, extendedTuplets) || TryCustomTuplets
 279                list.Add(new Rest(Duration.FromBase(b2, dots2, tp)));
 280            else
 281            {
 282                if (advancedSplit && TrySplitDotted(sumDur, out var parts)) foreach (var p in parts) list.Add(new Rest(p
 283                else list.Add(new Rest(sumDur));
 284            }
 285            restRun.Clear();
 286        }
 287
 288        foreach (var e in seq)
 289        {
 290            if (e is Rest r)
 291            {
 292                restRun ??= new List<Duration>();
 293                restRun.Add(r.Duration);
 294            }
 295            else
 296            {
 297                FlushPending();
 298                list.Add(e);
 299            }
 300        }
 301        FlushPending();
 302        if (mergeTuplets)
 303            list = MergeIntoTuplets(list, extendedTuplets).ToList();
 304        return list;
 305    }
 306
 307    // 付点音価を (基底)+(派生) に分割 (例: dotted quarter = quarter + eighth)。失敗なら false
 308    private static bool TrySplitDotted(Duration dur, out IEnumerable<Duration> parts)
 309    {
 310        parts = Array.Empty<Duration>();
 311        if (dur.TryAsSimple(out var b, out var dots) && dots>0)
 312        {
 313            // 付点展開: base * (1 + 1/2 + 1/4 ...) = base + base/2 + base/4 ...
 314            var (n, d) = b.GetFraction();
 315            var baseDur = Duration.FromBase(b, 0);
 316            var list = new List<Duration>{ baseDur };
 317            var current = baseDur;
 318            for(int i=1;i<=dots;i++)
 319            {
 320                current = new Duration(new RationalFactor(current.WholeFraction.Numerator, current.WholeFraction.Denomin
 321                list.Add(current);
 322            }
 323            parts = list;
 324            return true;
 325        }
 326        return false;
 327    }
 328
 329    private static bool TryCustomTuplets(Duration dur, List<Tuplet> tuplets, out BaseNoteValue baseValue, out int dots, 
 330    {
 331        baseValue = default; dots = 0; tuplet = null;
 332        if (tuplets.Count==0) return false;
 333        foreach (BaseNoteValue v in Enum.GetValues(typeof(BaseNoteValue)))
 334        {
 335            for (int d = 0; d <= Duration.MaxDots; d++)
 336            {
 337                foreach (var t in tuplets)
 338                {
 339                    var candidate = Duration.FromBase(v, d, t);
 340                    if (candidate.Equals(dur)) { baseValue = v; dots = d; tuplet = t; return true; }
 341                }
 342            }
 343        }
 344        return false;
 345    }
 346
 347    // 分割された (基底 + 1/2 + 1/4...) パターンから付点 / 連符へ再統合 (簡易)
 348    private static IEnumerable<IDurationEntity> MergeIntoTuplets(IEnumerable<IDurationEntity> src, bool extendedTuplets)
 349    {
 350        // 現状: 連続 Rest の合算を再度 TryAsSimple / TryDecomposeFull するだけ
 351        Rest? pending = null; var outList = new List<IDurationEntity>();
 352        void Flush()
 353        {
 354            if (!pending.HasValue) return; var d = pending.Value.Duration;
 355            if (d.TryAsSimple(out var b, out var dots)) outList.Add(new Rest(Duration.FromBase(b, dots)));
 356            else if (d.TryDecomposeFull(out var b2, out var dots2, out var tp, extendedTuplets)) outList.Add(new Rest(Du
 357            else outList.Add(pending.Value);
 358            pending = null;
 359        }
 360        foreach (var e in src)
 361        {
 362            if (e is Rest r)
 363            {
 364                pending = pending.HasValue ? new Rest(pending.Value.Duration + r.Duration) : r;
 365            }
 366            else { Flush(); outList.Add(e); }
 367        }
 368        Flush();
 369        return outList;
 370    }
 371
 372    /// <summary>
 373    /// 異種音価の連続 (例: 8分 + 16分 + 16分) が 等分割 n 個で構成される単一連符グループ (例: 3:2 の 8分三連) に縮約できるか推論。
 374    /// 入力は Duration (音符/休符問わず) の列。成功時: 単一 Duration (基底+Tuplet) を返し、失敗時 null。
 375    /// アルゴリズム: 総 tick / n で均等割可能かつ 各部品 tick が baseValue(付点なし) * factor * (normal/actual) に一致するかを検証。
 376    /// </summary>
 377    public static Duration? InferCompositeTuplet(IEnumerable<Duration> parts, bool extendedTuplets=false)
 378    {
 379        var list = parts.ToList(); if (list.Count == 0) return null;
 380        int total = list.Sum(p=>p.Ticks);
 381        // 最小単位 (全要素 gcd) を取得 (混在長を underlying subdivision に展開可能か確認するため)
 382        int gcd = list.Select(p=>p.Ticks).Aggregate(Gcd);
 383        if (gcd == 0) return null;
 384        // underlying subdivision 個数
 385    int unitCount = total / gcd;
 386        // tuplets 候補集合
 387    var tupletsAll = extendedTuplets ? GenerateExtendedTupletsStatic() : GenerateCommonTupletsStatic();
 388    int PrefN(int n) => n==4?0 : n==2?1 : 2;
 389    foreach (var t in tupletsAll
 390                 .OrderBy(tt=>PrefN(tt.NormalCount))
 391                 .ThenBy(tt=>tt.ActualCount)
 392                 .ThenBy(tt=>tt.NormalCount))
 393        {
 394            // 基底音価 ticks = total / normalCount
 395            if (total % t.NormalCount != 0) continue;
 396            int candidateBaseTicks = total / t.NormalCount; // dots 無し想定 (簡易)
 397            // baseValue (+dots) 探索: dots 0..3 で一致するか
 398            foreach (BaseNoteValue v in Enum.GetValues(typeof(BaseNoteValue)))
 399            {
 400                for (int dots=0; dots<=Duration.MaxDots; dots++)
 401                {
 402                    var baseDur = Duration.FromBase(v, dots);
 403                    if (baseDur.Ticks != candidateBaseTicks) continue;
 404                    // 連符適用後の最小単位 ticks = baseDur.Ticks * (normal/actual) / (baseDur 単位) = candidateBaseTicks * t.Factor
 405                    // 連符適用後一要素 (ideal) 長さ = baseDur.Ticks * t.Factor
 406                    double elem = baseDur.Ticks * t.Factor;
 407                    int elemTicks = (int)Math.Round(elem);
 408                    if (Math.Abs(elem - elemTicks) > 0.0001) continue;
 409                    // 元要素合計検証 (冗長だが安全)
 410                    if (list.Sum(p=>p.Ticks) != elemTicks * t.ActualCount) continue;
 411                    // 各要素 tick は elemTicks を超えない & elemTicks の約数の和で構成 (例: 240,120,120 を 160+160+160 の再構成とみなす) -> 要素を el
 412                    int neededUnits = t.ActualCount; int consumedUnits=0; bool feasible=true;
 413                    foreach (var p in list)
 414                    {
 415                        if (p.Ticks > elemTicks) { // 要素が長すぎる場合は elemTicks で割れるかチェック
 416                            if (p.Ticks % elemTicks !=0) { feasible=false; break; }
 417                            consumedUnits += p.Ticks / elemTicks;
 418                        }
 419                        else if (elemTicks % p.Ticks ==0)
 420                        {
 421                            consumedUnits += (elemTicks / p.Ticks); // 小さい音価複数が1要素相当を構成
 422                        }
 423                        else { feasible=false; break; }
 424                        if (consumedUnits > neededUnits){ feasible=false; break; }
 425                    }
 426                    if (!feasible || consumedUnits!=neededUnits) continue;
 427                    return Duration.FromBase(v, dots, t);
 428                }
 429            }
 430        }
 431        return null;
 432    }
 433
 434    private static Tuplet[] GenerateCommonTupletsStatic() => new[]{ new Tuplet(3,2), new Tuplet(5,4), new Tuplet(7,4), n
 435    private static Tuplet[] GenerateExtendedTupletsStatic()
 436    {
 437        var list = new List<Tuplet>();
 438        for (int normal=2; normal<=12; normal++) for (int actual=2; actual<=12; actual++) if (actual!=normal) list.Add(n
 439        return list.ToArray();
 440    }
 441
 442    private static int Gcd(int a, int b){ while (b!=0) (a,b) = (b, a % b); return Math.Abs(a); }
 443
 444    /// <summary>
 445    /// 分割許容版 (allowSplit=true) の連符推論。要素内で自由に境界を切れると仮定し、総時間と候補連符から逆算。
 446    /// 例: 8分(240)+16分(120)+16分(120)=480 を 8分三連 (3:2) (要素長 160) として推論可。
 447    /// 制約: base + dots + tuplet の組み合わせのうち最小 (base値の長い順優先) を返す。
 448    /// </summary>
 449    public static Duration? InferCompositeTupletFlexible(IEnumerable<Duration> parts, bool extendedTuplets=false)
 450    {
 451        var list = parts.ToList(); if (list.Count < 2) return null;
 452        int total = list.Sum(p=>p.Ticks);
 453        var tuplets = extendedTuplets ? GenerateExtendedTupletsStatic() : GenerateCommonTupletsStatic();
 454        var bases = Enum.GetValues(typeof(BaseNoteValue)).Cast<BaseNoteValue>().OrderByDescending(v=>Duration.FromBase(v
 455        int minPart = list.Min(p=>p.Ticks);
 456        var candidates = new List<(Duration dur, Tuplet t, int elemTicks, int baseTicks)>();
 457        foreach (var t in tuplets)
 458        {
 459            if (total % t.NormalCount != 0) continue;
 460            int candidateBaseTicks = total / t.NormalCount;
 461            foreach (var b in bases)
 462            {
 463                for (int dots=0; dots<=Duration.MaxDots; dots++)
 464                {
 465                    var baseDur = Duration.FromBase(b, dots);
 466                    if (baseDur.Ticks != candidateBaseTicks) continue;
 467                    int elemTicks = (int)Math.Round(baseDur.Ticks * t.Factor);
 468                    if (elemTicks * t.ActualCount != total) continue;
 469                    var cand = Duration.FromBase(b, dots, t);
 470                    candidates.Add((cand, t, elemTicks, baseDur.Ticks));
 471                }
 472            }
 473        }
 474        if (candidates.Count > 0)
 475        {
 476            // 優先順位:
 477            // 1) 要素長が minPart 以上
 478            // 2) ActualCount の優先度 [5,3,7,9,11,4,6,8,10,12,2](extended で 5 を優先)
 479            // 3) NormalCount の慣例優先 (4 -> 2 -> その他)
 480            // 4) baseTicks 小さい順
 481            int IndexActual(int a)
 482            {
 483                int[] pref = new[]{5,3,7,9,11,4,6,8,10,12,2};
 484                int idx = Array.IndexOf(pref, a);
 485                return idx >= 0 ? idx : pref.Length + a; // 未掲載は後方
 486            }
 487            int PrefNormal(int n) => n==4?0 : n==2?1 : 2;
 488            var chosen = candidates
 489                .OrderByDescending(c => c.t.ActualCount >= c.t.NormalCount)
 490                .ThenByDescending(c => c.elemTicks >= minPart)
 491                .ThenBy(c => IndexActual(c.t.ActualCount))
 492                .ThenBy(c => PrefNormal(c.t.NormalCount))
 493                .ThenBy(c => c.baseTicks)
 494                .First();
 495            return chosen.dur;
 496        }
 497        return null;
 498    }
 499}

Methods/Properties

get_Duration()