< Summary

Information
Class: MusicTheory.Theory.Time.SequenceLayout
Assembly: MusicTheory
File(s): /home/runner/work/MusicTheory/MusicTheory/Theory/Time/SequenceLayout.cs
Line coverage
78%
Covered lines: 90
Uncovered lines: 24
Coverable lines: 114
Total lines: 208
Line coverage: 78.9%
Branch coverage
71%
Covered branches: 63
Total branches: 88
Branch coverage: 71.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
InsertRests(...)100%1414100%
SplitRunsByInferredTuplet(...)53.57%442872.91%
SliceEntity(...)0%2040%
RebuildEntity(...)25%5466.66%
TryChooseGroupTuplet(...)81.25%161693.75%
PrefNormal()100%44100%
IndexActual()50%22100%
TryFindBaseByTicks(...)83.33%6683.33%
GenerateTupletsCommon()100%11100%
GenerateTupletsExtended()100%66100%
FindEntityAt(...)100%44100%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Linq;
 4
 5namespace MusicTheory.Theory.Time;
 6
 7/// <summary>
 8/// Note シーケンスのレイアウト: ギャップを自動的に Rest で埋め、必要なら正規化。
 9/// 現状は単声 (モノフォニック) 前提。重なりがある場合は開始順に優先し後続を切り詰めずそのまま並列とする。
 10/// </summary>
 11public static class SequenceLayout
 12{
 13    public static IReadOnlyList<(long start, IDurationEntity entity)> InsertRests(
 14        IEnumerable<(long start, Note note)> notes,
 15        bool normalizeRests = true,
 16        bool advancedSplit = false,
 17        bool allowSplitTuplets = false,
 18        bool extendedTuplets = false)
 19    {
 69020        var ordered = notes.OrderBy(n=>n.start).ToList();
 621        var result = new List<(long, IDurationEntity)>();
 622        long cursor = 0;
 138023        foreach (var (start, note) in ordered)
 24        {
 68425            if (start > cursor)
 26            {
 127                var gap = start - cursor;
 128                var rest = RestFactory.FromTicks((int)gap);
 129                var restSeq = new List<IDurationEntity>{ rest };
 130                if (normalizeRests)
 131                    restSeq = DurationSequenceUtils.NormalizeRests(restSeq, advancedSplit: advancedSplit, allowSplit: al
 432                foreach (var r in restSeq)
 133                    result.Add((cursor, r));
 34            }
 68435            result.Add((start, note));
 68436            var end = start + note.Duration.Ticks;
 136837            if (end > cursor) cursor = end; // 単声前提: 重なる場合は長い方が cursor を進める
 38        }
 39        // 連続区間に対して連符推論に基づく等分割 (allowSplitTuplets) を適用
 640        if (allowSplitTuplets && result.Count > 0)
 541            result = SplitRunsByInferredTuplet(result, extendedTuplets).ToList();
 642        return result;
 43    }
 44
 45    private static IEnumerable<(long start, IDurationEntity entity)> SplitRunsByInferredTuplet(
 46        IReadOnlyList<(long start, IDurationEntity entity)> timeline,
 47        bool extendedTuplets)
 48    {
 549        var output = new List<(long, IDurationEntity)>();
 550        int i = 0;
 1051        while (i < timeline.Count)
 52        {
 53            // 連続ラン収集: start が直前 end と等しいものをひとかたまりにする (重なりや飛びは境界)
 554            var run = new List<(long start, IDurationEntity entity)>();
 555            long runStart = timeline[i].start;
 556            long cursor = runStart;
 557            int j = i;
 68758            while (j < timeline.Count)
 59            {
 68260                var (s, e) = timeline[j];
 68261                var durTicks = e.Duration.Ticks;
 68262                if (s != cursor) break; // ギャップ or オーバーラップ -> ラン切断
 68263                run.Add((s, e));
 68264                cursor = s + durTicks;
 68265                j++;
 66            }
 67
 68            // 推論して分割適用
 68769            var parts = run.Select(x => x.entity.Duration).ToList();
 70            // 単一要素相当のランは対象外
 571            if (parts.Count == 0) { i = j; continue; }
 68772            int totalTicks = parts.Sum(p=>p.Ticks);
 68773            int minTicks = parts.Min(p=>p.Ticks);
 574            if (totalTicks < minTicks * 2)
 75            {
 076                for (int k = i; k < j; k++) output.Add(timeline[k]);
 077                i = j; continue;
 78            }
 579            var inferred = DurationSequenceUtils.InferCompositeTupletFlexible(parts, extendedTuplets);
 580            if (inferred.HasValue)
 81            {
 482                int total = totalTicks;
 483                int elemTicks = inferred.Value.Ticks; // 要素長
 484                if (TryChooseGroupTuplet(total, elemTicks, extendedTuplets, out var baseVal, out var dots, out Tuplet? t
 85                {
 486                    var tuplet = tupletNullable!.Value;
 487                    long segStart = runStart;
 4088                    for (int seg = 0; seg < tuplet.ActualCount; seg++)
 89                    {
 1690                        var ent = FindEntityAt(run, segStart);
 1691                        var segDur = DurationFactory.Tuplet(baseVal, tuplet, dots);
 1692                        output.Add((segStart, RebuildEntity(ent, segDur)));
 1693                        segStart += elemTicks;
 94                    }
 895                    i = j; continue;
 96                }
 97                // フォールバック: 純粋に elemTicks で等分割
 098                if (elemTicks > 0 && total % elemTicks == 0)
 99                {
 0100                    int segCount = total / elemTicks;
 0101                    long segStart = runStart;
 0102                    for (int seg = 0; seg < segCount; seg++)
 103                    {
 0104                        var ent = FindEntityAt(run, segStart);
 0105                        var segDur = Duration.FromTicks(elemTicks);
 0106                        output.Add((segStart, RebuildEntity(ent, segDur)));
 0107                        segStart += elemTicks;
 108                    }
 0109                    i = j; continue;
 110                }
 111                // 決定不能ならそのまま通す
 0112                for (int k = i; k < j; k++) output.Add(timeline[k]);
 0113                i = j; continue;
 114            }
 115
 116            // 分割しない場合はそのまま出力
 2012117            for (int k = i; k < j; k++) output.Add(timeline[k]);
 1118            i = j;
 119        }
 5120        return output;
 121    }
 122
 123    private static IDurationEntity SliceEntity(IDurationEntity entity, int sliceTicks)
 124    {
 0125        var d = Duration.FromTicks(sliceTicks);
 0126        return entity switch
 0127        {
 0128            Note n => new Note(d, n.Pitch, n.Velocity, n.Channel),
 0129            Rest => new Rest(d),
 0130            _ => entity
 0131        };
 132    }
 133
 134    private static IDurationEntity RebuildEntity(IDurationEntity entity, Duration duration)
 135    {
 16136        return entity switch
 16137        {
 16138            Note n => new Note(duration, n.Pitch, n.Velocity, n.Channel),
 0139            Rest => new Rest(duration),
 0140            _ => entity
 16141        };
 142    }
 143
 144    private static bool TryChooseGroupTuplet(int totalTicks, int elemTicks, bool extendedTuplets, out BaseNoteValue base
 145    {
 12146        baseVal = default; dots = 0; tuplet = null;
 4147        if (elemTicks <= 0 || totalTicks <= 0) return false;
 148        // 候補連符集合
 4149        var tuplets = extendedTuplets
 4150            ? GenerateTupletsExtended()
 4151            : GenerateTupletsCommon();
 152        // 優先: normal=4 -> 2 -> その他、かつ ActualCount に好みの順序を付与 (5,3,7...)
 336153        int PrefNormal(int n) => n==4?0 : n==2?1 : 2;
 154        int IndexActual(int a)
 155        {
 336156            int[] pref = new[]{5,3,7,9,11,4,6,8,10,12,2};
 336157            int idx = Array.IndexOf(pref, a);
 336158            return idx >= 0 ? idx : pref.Length + a;
 159        }
 20160        foreach (var t in tuplets
 336161                     .OrderBy(x=>PrefNormal(x.NormalCount))
 340162                     .ThenBy(x=>IndexActual(x.ActualCount)))
 163        {
 8164            if (totalTicks % t.NormalCount != 0) continue;
 8165            int baseTicks = totalTicks / t.NormalCount;
 166            // elem 一致判定
 16167            double ideal = baseTicks * t.Factor; int idealTicks = (int)Math.Round(ideal);
 8168            if (Math.Abs(ideal - idealTicks) > 0.0001) continue;
 7169            if (idealTicks != elemTicks) continue;
 170            // baseTicks を (baseValue + dots) で表現可能か
 12171            if (TryFindBaseByTicks(baseTicks, out baseVal, out dots)) { tuplet = t; return true; }
 172        }
 0173        return false;
 4174    }
 175
 176    private static bool TryFindBaseByTicks(int ticks, out BaseNoteValue baseVal, out int dots)
 177    {
 44178        foreach (BaseNoteValue v in Enum.GetValues(typeof(BaseNoteValue)))
 179        {
 168180            for (int d=0; d<=Duration.MaxDots; d++)
 181            {
 68182                if (Duration.FromBase(v, d).Ticks == ticks)
 12183                { baseVal = v; dots = d; return true; }
 184            }
 185        }
 0186        baseVal = default; dots = 0; return false;
 4187    }
 188
 1189    private static Tuplet[] GenerateTupletsCommon() => new[]{ new Tuplet(3,2), new Tuplet(5,4), new Tuplet(7,4), new Tup
 190    private static Tuplet[] GenerateTupletsExtended()
 191    {
 3192        var list = new List<Tuplet>();
 1557193        for (int normal=2; normal<=12; normal++) for (int actual=2; actual<=12; actual++) if (actual!=normal) list.Add(n
 3194        return list.ToArray();
 195    }
 196
 197    private static IDurationEntity FindEntityAt(List<(long start, IDurationEntity entity)> run, long time)
 198    {
 199        // run は start 昇順・連続。最後の start<=time の要素を返す。
 16200        IDurationEntity ent = run[0].entity;
 101201        foreach (var (s,e) in run)
 202        {
 54203            if (s > time) break;
 28204            ent = e;
 205        }
 16206        return ent;
 207    }
 208}