| | 1 | | using System; |
| | 2 | | using System.Collections.Generic; |
| | 3 | | using System.Linq; |
| | 4 | |
|
| | 5 | | namespace MusicTheory.Theory.Time; |
| | 6 | |
|
| | 7 | | /// <summary> |
| | 8 | | /// 基本音符種類 (倍全~128分) - Whole を 1 とする比率は <see cref="BaseNoteValueExtensions"/> を参照。 |
| | 9 | | /// </summary> |
| | 10 | | public 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 | |
|
| | 23 | | public 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> |
| | 42 | | public 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> |
| | 51 | | public 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> |
| | 84 | | public 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> |
| | 200 | | public readonly partial record struct Rest(Duration Duration); |
| | 201 | |
|
| | 202 | | /// <summary>音符 (将来的に Pitch / Velocity / Articulation を拡張)。</summary> |
| | 203 | | public 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 | |
|
| | 208 | | public 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> |
| | 225 | | public static class RestFactory |
| | 226 | | { |
| 1 | 227 | | public static Rest DoubleWhole(int dots=0) => new(DurationFactory.DoubleWhole(dots)); |
| 1 | 228 | | public static Rest Whole(int dots=0) => new(DurationFactory.Whole(dots)); |
| 1 | 229 | | public static Rest Half(int dots=0) => new(DurationFactory.Half(dots)); |
| 9 | 230 | | public static Rest Quarter(int dots=0) => new(DurationFactory.Quarter(dots)); |
| 5 | 231 | | public static Rest Eighth(int dots=0) => new(DurationFactory.Eighth(dots)); |
| 1 | 232 | | public static Rest Sixteenth(int dots=0) => new(DurationFactory.Sixteenth(dots)); |
| 1 | 233 | | public static Rest ThirtySecond(int dots=0) => new(DurationFactory.ThirtySecond(dots)); |
| 1 | 234 | | public static Rest SixtyFourth(int dots=0) => new(DurationFactory.SixtyFourth(dots)); |
| 1 | 235 | | public static Rest OneHundredTwentyEighth(int dots=0) => new(DurationFactory.OneHundredTwentyEighth(dots)); |
| 1 | 236 | | public static Rest Tuplet(BaseNoteValue baseValue, Tuplet tuplet, int dots=0) => new(DurationFactory.Tuplet(baseValu |
| 2 | 237 | | public static Rest FromTicks(int ticks) => new(Duration.FromTicks(ticks)); |
| | 238 | | } |
| | 239 | |
|
| | 240 | | /// <summary>Note / Rest 共通インターフェース。</summary> |
| | 241 | | public interface IDurationEntity { Duration Duration { get; } } |
| | 242 | | public readonly partial record struct Note : IDurationEntity {} |
| | 243 | | public readonly partial record struct Rest : IDurationEntity {} |
| | 244 | |
|
| | 245 | | /// <summary>休符や音符列の正規化ユーティリティ。</summary> |
| | 246 | | public 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 | | } |