| | 1 | | using System; |
| | 2 | | using System.Collections.Generic; |
| | 3 | | using System.Linq; |
| | 4 | |
|
| | 5 | | namespace MusicTheory.Theory.Time; |
| | 6 | |
|
| | 7 | | /// <summary> |
| | 8 | | /// Note シーケンスのレイアウト: ギャップを自動的に Rest で埋め、必要なら正規化。 |
| | 9 | | /// 現状は単声 (モノフォニック) 前提。重なりがある場合は開始順に優先し後続を切り詰めずそのまま並列とする。 |
| | 10 | | /// </summary> |
| | 11 | | public 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 | | { |
| 690 | 20 | | var ordered = notes.OrderBy(n=>n.start).ToList(); |
| 6 | 21 | | var result = new List<(long, IDurationEntity)>(); |
| 6 | 22 | | long cursor = 0; |
| 1380 | 23 | | foreach (var (start, note) in ordered) |
| | 24 | | { |
| 684 | 25 | | if (start > cursor) |
| | 26 | | { |
| 1 | 27 | | var gap = start - cursor; |
| 1 | 28 | | var rest = RestFactory.FromTicks((int)gap); |
| 1 | 29 | | var restSeq = new List<IDurationEntity>{ rest }; |
| 1 | 30 | | if (normalizeRests) |
| 1 | 31 | | restSeq = DurationSequenceUtils.NormalizeRests(restSeq, advancedSplit: advancedSplit, allowSplit: al |
| 4 | 32 | | foreach (var r in restSeq) |
| 1 | 33 | | result.Add((cursor, r)); |
| | 34 | | } |
| 684 | 35 | | result.Add((start, note)); |
| 684 | 36 | | var end = start + note.Duration.Ticks; |
| 1368 | 37 | | if (end > cursor) cursor = end; // 単声前提: 重なる場合は長い方が cursor を進める |
| | 38 | | } |
| | 39 | | // 連続区間に対して連符推論に基づく等分割 (allowSplitTuplets) を適用 |
| 6 | 40 | | if (allowSplitTuplets && result.Count > 0) |
| 5 | 41 | | result = SplitRunsByInferredTuplet(result, extendedTuplets).ToList(); |
| 6 | 42 | | return result; |
| | 43 | | } |
| | 44 | |
|
| | 45 | | private static IEnumerable<(long start, IDurationEntity entity)> SplitRunsByInferredTuplet( |
| | 46 | | IReadOnlyList<(long start, IDurationEntity entity)> timeline, |
| | 47 | | bool extendedTuplets) |
| | 48 | | { |
| 5 | 49 | | var output = new List<(long, IDurationEntity)>(); |
| 5 | 50 | | int i = 0; |
| 10 | 51 | | while (i < timeline.Count) |
| | 52 | | { |
| | 53 | | // 連続ラン収集: start が直前 end と等しいものをひとかたまりにする (重なりや飛びは境界) |
| 5 | 54 | | var run = new List<(long start, IDurationEntity entity)>(); |
| 5 | 55 | | long runStart = timeline[i].start; |
| 5 | 56 | | long cursor = runStart; |
| 5 | 57 | | int j = i; |
| 687 | 58 | | while (j < timeline.Count) |
| | 59 | | { |
| 682 | 60 | | var (s, e) = timeline[j]; |
| 682 | 61 | | var durTicks = e.Duration.Ticks; |
| 682 | 62 | | if (s != cursor) break; // ギャップ or オーバーラップ -> ラン切断 |
| 682 | 63 | | run.Add((s, e)); |
| 682 | 64 | | cursor = s + durTicks; |
| 682 | 65 | | j++; |
| | 66 | | } |
| | 67 | |
|
| | 68 | | // 推論して分割適用 |
| 687 | 69 | | var parts = run.Select(x => x.entity.Duration).ToList(); |
| | 70 | | // 単一要素相当のランは対象外 |
| 5 | 71 | | if (parts.Count == 0) { i = j; continue; } |
| 687 | 72 | | int totalTicks = parts.Sum(p=>p.Ticks); |
| 687 | 73 | | int minTicks = parts.Min(p=>p.Ticks); |
| 5 | 74 | | if (totalTicks < minTicks * 2) |
| | 75 | | { |
| 0 | 76 | | for (int k = i; k < j; k++) output.Add(timeline[k]); |
| 0 | 77 | | i = j; continue; |
| | 78 | | } |
| 5 | 79 | | var inferred = DurationSequenceUtils.InferCompositeTupletFlexible(parts, extendedTuplets); |
| 5 | 80 | | if (inferred.HasValue) |
| | 81 | | { |
| 4 | 82 | | int total = totalTicks; |
| 4 | 83 | | int elemTicks = inferred.Value.Ticks; // 要素長 |
| 4 | 84 | | if (TryChooseGroupTuplet(total, elemTicks, extendedTuplets, out var baseVal, out var dots, out Tuplet? t |
| | 85 | | { |
| 4 | 86 | | var tuplet = tupletNullable!.Value; |
| 4 | 87 | | long segStart = runStart; |
| 40 | 88 | | for (int seg = 0; seg < tuplet.ActualCount; seg++) |
| | 89 | | { |
| 16 | 90 | | var ent = FindEntityAt(run, segStart); |
| 16 | 91 | | var segDur = DurationFactory.Tuplet(baseVal, tuplet, dots); |
| 16 | 92 | | output.Add((segStart, RebuildEntity(ent, segDur))); |
| 16 | 93 | | segStart += elemTicks; |
| | 94 | | } |
| 8 | 95 | | i = j; continue; |
| | 96 | | } |
| | 97 | | // フォールバック: 純粋に elemTicks で等分割 |
| 0 | 98 | | if (elemTicks > 0 && total % elemTicks == 0) |
| | 99 | | { |
| 0 | 100 | | int segCount = total / elemTicks; |
| 0 | 101 | | long segStart = runStart; |
| 0 | 102 | | for (int seg = 0; seg < segCount; seg++) |
| | 103 | | { |
| 0 | 104 | | var ent = FindEntityAt(run, segStart); |
| 0 | 105 | | var segDur = Duration.FromTicks(elemTicks); |
| 0 | 106 | | output.Add((segStart, RebuildEntity(ent, segDur))); |
| 0 | 107 | | segStart += elemTicks; |
| | 108 | | } |
| 0 | 109 | | i = j; continue; |
| | 110 | | } |
| | 111 | | // 決定不能ならそのまま通す |
| 0 | 112 | | for (int k = i; k < j; k++) output.Add(timeline[k]); |
| 0 | 113 | | i = j; continue; |
| | 114 | | } |
| | 115 | |
|
| | 116 | | // 分割しない場合はそのまま出力 |
| 2012 | 117 | | for (int k = i; k < j; k++) output.Add(timeline[k]); |
| 1 | 118 | | i = j; |
| | 119 | | } |
| 5 | 120 | | return output; |
| | 121 | | } |
| | 122 | |
|
| | 123 | | private static IDurationEntity SliceEntity(IDurationEntity entity, int sliceTicks) |
| | 124 | | { |
| 0 | 125 | | var d = Duration.FromTicks(sliceTicks); |
| 0 | 126 | | return entity switch |
| 0 | 127 | | { |
| 0 | 128 | | Note n => new Note(d, n.Pitch, n.Velocity, n.Channel), |
| 0 | 129 | | Rest => new Rest(d), |
| 0 | 130 | | _ => entity |
| 0 | 131 | | }; |
| | 132 | | } |
| | 133 | |
|
| | 134 | | private static IDurationEntity RebuildEntity(IDurationEntity entity, Duration duration) |
| | 135 | | { |
| 16 | 136 | | return entity switch |
| 16 | 137 | | { |
| 16 | 138 | | Note n => new Note(duration, n.Pitch, n.Velocity, n.Channel), |
| 0 | 139 | | Rest => new Rest(duration), |
| 0 | 140 | | _ => entity |
| 16 | 141 | | }; |
| | 142 | | } |
| | 143 | |
|
| | 144 | | private static bool TryChooseGroupTuplet(int totalTicks, int elemTicks, bool extendedTuplets, out BaseNoteValue base |
| | 145 | | { |
| 12 | 146 | | baseVal = default; dots = 0; tuplet = null; |
| 4 | 147 | | if (elemTicks <= 0 || totalTicks <= 0) return false; |
| | 148 | | // 候補連符集合 |
| 4 | 149 | | var tuplets = extendedTuplets |
| 4 | 150 | | ? GenerateTupletsExtended() |
| 4 | 151 | | : GenerateTupletsCommon(); |
| | 152 | | // 優先: normal=4 -> 2 -> その他、かつ ActualCount に好みの順序を付与 (5,3,7...) |
| 336 | 153 | | int PrefNormal(int n) => n==4?0 : n==2?1 : 2; |
| | 154 | | int IndexActual(int a) |
| | 155 | | { |
| 336 | 156 | | int[] pref = new[]{5,3,7,9,11,4,6,8,10,12,2}; |
| 336 | 157 | | int idx = Array.IndexOf(pref, a); |
| 336 | 158 | | return idx >= 0 ? idx : pref.Length + a; |
| | 159 | | } |
| 20 | 160 | | foreach (var t in tuplets |
| 336 | 161 | | .OrderBy(x=>PrefNormal(x.NormalCount)) |
| 340 | 162 | | .ThenBy(x=>IndexActual(x.ActualCount))) |
| | 163 | | { |
| 8 | 164 | | if (totalTicks % t.NormalCount != 0) continue; |
| 8 | 165 | | int baseTicks = totalTicks / t.NormalCount; |
| | 166 | | // elem 一致判定 |
| 16 | 167 | | double ideal = baseTicks * t.Factor; int idealTicks = (int)Math.Round(ideal); |
| 8 | 168 | | if (Math.Abs(ideal - idealTicks) > 0.0001) continue; |
| 7 | 169 | | if (idealTicks != elemTicks) continue; |
| | 170 | | // baseTicks を (baseValue + dots) で表現可能か |
| 12 | 171 | | if (TryFindBaseByTicks(baseTicks, out baseVal, out dots)) { tuplet = t; return true; } |
| | 172 | | } |
| 0 | 173 | | return false; |
| 4 | 174 | | } |
| | 175 | |
|
| | 176 | | private static bool TryFindBaseByTicks(int ticks, out BaseNoteValue baseVal, out int dots) |
| | 177 | | { |
| 44 | 178 | | foreach (BaseNoteValue v in Enum.GetValues(typeof(BaseNoteValue))) |
| | 179 | | { |
| 168 | 180 | | for (int d=0; d<=Duration.MaxDots; d++) |
| | 181 | | { |
| 68 | 182 | | if (Duration.FromBase(v, d).Ticks == ticks) |
| 12 | 183 | | { baseVal = v; dots = d; return true; } |
| | 184 | | } |
| | 185 | | } |
| 0 | 186 | | baseVal = default; dots = 0; return false; |
| 4 | 187 | | } |
| | 188 | |
|
| 1 | 189 | | 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 | | { |
| 3 | 192 | | var list = new List<Tuplet>(); |
| 1557 | 193 | | for (int normal=2; normal<=12; normal++) for (int actual=2; actual<=12; actual++) if (actual!=normal) list.Add(n |
| 3 | 194 | | return list.ToArray(); |
| | 195 | | } |
| | 196 | |
|
| | 197 | | private static IDurationEntity FindEntityAt(List<(long start, IDurationEntity entity)> run, long time) |
| | 198 | | { |
| | 199 | | // run は start 昇順・連続。最後の start<=time の要素を返す。 |
| 16 | 200 | | IDurationEntity ent = run[0].entity; |
| 101 | 201 | | foreach (var (s,e) in run) |
| | 202 | | { |
| 54 | 203 | | if (s > time) break; |
| 28 | 204 | | ent = e; |
| | 205 | | } |
| 16 | 206 | | return ent; |
| | 207 | | } |
| | 208 | | } |