| | 1 | | using System; |
| | 2 | | using System.Collections.Generic; |
| | 3 | | using System.Linq; |
| | 4 | |
|
| | 5 | | namespace MusicTheory.Theory.Time; |
| | 6 | |
|
| | 7 | | /// <summary> |
| | 8 | | /// 拍子記号。例: 4/4, 3/8 など。PPQ=Duration.TicksPerQuarter を前提に ticks 計算。 |
| | 9 | | /// </summary> |
| | 10 | | public readonly struct TimeSignature : IEquatable<TimeSignature> |
| | 11 | | { |
| | 12 | | public int Numerator { get; } |
| | 13 | | public int Denominator { get; } // (1,2,4,8,16,...) |
| | 14 | | public TimeSignature(int numerator, int denominator) |
| | 15 | | { |
| | 16 | | if (numerator <= 0) throw new ArgumentOutOfRangeException(nameof(numerator)); |
| | 17 | | if (denominator <= 0 || (denominator & (denominator - 1)) != 0) throw new ArgumentException("Denominator must be |
| | 18 | | Numerator = numerator; Denominator = denominator; |
| | 19 | | } |
| | 20 | | public int TicksPerBeat => Duration.TicksPerQuarter * 4 / Denominator; |
| | 21 | | public int TicksPerBar => TicksPerBeat * Numerator; |
| | 22 | | public bool Equals(TimeSignature other) => Numerator == other.Numerator && Denominator == other.Denominator; |
| | 23 | | public override bool Equals(object? obj) => obj is TimeSignature ts && Equals(ts); |
| | 24 | | public override int GetHashCode() => HashCode.Combine(Numerator, Denominator); |
| | 25 | | public override string ToString() => $"{Numerator}/{Denominator}"; |
| | 26 | | } |
| | 27 | |
|
| | 28 | | /// <summary>小節位置 (bar, beat, tickWithinBeat)。bar, beat は 0 基点。</summary> |
| | 29 | | public readonly struct BarBeatTick : IEquatable<BarBeatTick> |
| | 30 | | { |
| | 31 | | public int Bar { get; } |
| | 32 | | public int Beat { get; } |
| | 33 | | public int TickWithinBeat { get; } |
| | 34 | | public BarBeatTick(int bar, int beat, int tickWithinBeat) |
| | 35 | | { if (bar<0||beat<0||tickWithinBeat<0) throw new ArgumentOutOfRangeException(); Bar=bar; Beat=beat; TickWithinBeat=t |
| | 36 | | public bool Equals(BarBeatTick other) => Bar==other.Bar && Beat==other.Beat && TickWithinBeat==other.TickWithinBeat; |
| | 37 | | public override bool Equals(object? obj)=> obj is BarBeatTick bbt && Equals(bbt); |
| | 38 | | public override int GetHashCode()=> HashCode.Combine(Bar,Beat,TickWithinBeat); |
| | 39 | | public override string ToString()=> $"{Bar}:{Beat}+{TickWithinBeat}"; |
| | 40 | | } |
| | 41 | |
|
| | 42 | | /// <summary>絶対 tick と拍子変換用。可変拍子対応は将来的に (マップを保持) 拡張。</summary> |
| | 43 | | public readonly struct TimePosition |
| | 44 | | { |
| | 45 | | public TimeSignature Signature { get; } |
| | 46 | | public long AbsoluteTicks { get; } |
| | 47 | | public BarBeatTick BarBeatTick { get; } |
| | 48 | | public TimePosition(TimeSignature sig, long absoluteTicks) |
| | 49 | | { |
| | 50 | | if (absoluteTicks < 0) throw new ArgumentOutOfRangeException(nameof(absoluteTicks)); |
| | 51 | | Signature = sig; AbsoluteTicks = absoluteTicks; |
| | 52 | | var tpBar = sig.TicksPerBar; |
| | 53 | | var bar = (int)(absoluteTicks / tpBar); |
| | 54 | | var rem = (int)(absoluteTicks % tpBar); |
| | 55 | | var beat = rem / sig.TicksPerBeat; |
| | 56 | | var tickInBeat = rem % sig.TicksPerBeat; |
| | 57 | | BarBeatTick = new BarBeatTick(bar, beat, tickInBeat); |
| | 58 | | } |
| | 59 | | private TimePosition(TimeSignature sig, long abs, BarBeatTick bbt) |
| | 60 | | { Signature=sig; AbsoluteTicks=abs; BarBeatTick=bbt; } |
| | 61 | | public static TimePosition FromBarBeatTick(TimeSignature sig, BarBeatTick bbt) |
| | 62 | | { |
| | 63 | | if (bbt.Beat >= sig.Numerator) throw new ArgumentOutOfRangeException(nameof(bbt),"Beat >= numerator"); |
| | 64 | | if (bbt.TickWithinBeat >= sig.TicksPerBeat) throw new ArgumentOutOfRangeException(nameof(bbt),"TickWithinBeat >= |
| | 65 | | long abs = (long)bbt.Bar * sig.TicksPerBar + bbt.Beat * sig.TicksPerBeat + bbt.TickWithinBeat; |
| | 66 | | return new TimePosition(sig, abs, bbt); |
| | 67 | | } |
| | 68 | | public TimePosition Add(Duration d) => new(Signature, AbsoluteTicks + d.Ticks); |
| | 69 | | public override string ToString() => $"{BarBeatTick} ({AbsoluteTicks} ticks)"; |
| | 70 | | } |
| | 71 | |
|
| | 72 | | /// <summary> |
| | 73 | | /// Tuplet パターン生成ユーティリティ。 |
| | 74 | | /// </summary> |
| | 75 | | public static class TupletPatterns |
| | 76 | | { |
| | 77 | | /// <summary>任意範囲で n:k 連符 (n!=k) を生成 (既約化は行わず生の組)。</summary> |
| | 78 | | public static IEnumerable<Tuplet> Generate(int maxActual=13, int maxNormal=12) |
| | 79 | | { |
| | 80 | | for (int normal=2; normal<=maxNormal; normal++) |
| | 81 | | for (int actual=2; actual<=maxActual; actual++) |
| | 82 | | if (actual!=normal) |
| | 83 | | yield return new Tuplet(actual, normal); |
| | 84 | | } |
| | 85 | | } |
| | 86 | |
|
| | 87 | | /// <summary>MIDI 非依存の簡易イベント。Status は 0x9x / 0x8x 相当を想定。</summary> |
| | 88 | | public readonly struct MidiNoteEvent |
| | 89 | | { |
| | 90 | | public int Channel { get; } |
| | 91 | | public int Pitch { get; } |
| | 92 | | public int Velocity { get; } |
| | 93 | | public long Tick { get; } |
| | 94 | | public bool IsNoteOn { get; } |
| | 95 | | public MidiNoteEvent(int channel, int pitch, int velocity, long tick, bool isNoteOn) |
| | 96 | | { Channel=channel; Pitch=pitch; Velocity=velocity; Tick=tick; IsNoteOn=isNoteOn; } |
| | 97 | | public override string ToString()=> $"{(IsNoteOn?"On":"Off")}[ch{Channel}] p{Pitch} v{Velocity} @{Tick}"; |
| | 98 | | } |
| | 99 | |
|
| | 100 | | /// <summary>Note から NoteOn/NoteOff イベント列を生成するヘルパ。</summary> |
| | 101 | | public static class MidiNoteBuilder |
| | 102 | | { |
| | 103 | | /// <summary>シンプルな単音生成。</summary> |
| | 104 | | public static IEnumerable<MidiNoteEvent> BuildSingle(int channel, int pitch, int velocity, long startTick, Duration |
| | 105 | | { |
| | 106 | | yield return new MidiNoteEvent(channel, pitch, velocity, startTick, true); |
| | 107 | | yield return new MidiNoteEvent(channel, pitch, 0, startTick + dur.Ticks, false); |
| | 108 | | } |
| | 109 | |
|
| | 110 | | /// <summary>シーケンス (pitch, start, duration, velocity 可変) からソート済イベントを生成。</summary> |
| | 111 | | public static IEnumerable<MidiNoteEvent> BuildMany(IEnumerable<(int pitch,long start,Duration dur,int velocity,int c |
| | 112 | | => notes.SelectMany(n => BuildSingle(n.channel, n.pitch, n.velocity, n.start, n.dur)) |
| | 113 | | .OrderBy(e=>e.Tick).ThenBy(e=>!e.IsNoteOn); // 同 tick では NoteOff 先行で重なり制御可 (用途で逆も) |
| | 114 | | } |
| | 115 | |
|
| | 116 | | /// <summary>拍子変更マップ (単純: ソート済境界 + 検索)。</summary> |
| | 117 | | public sealed class TimeSignatureMap |
| | 118 | | { |
| | 119 | | private readonly List<(long tick, TimeSignature sig)> _entries = new(); |
| | 120 | | public TimeSignatureMap(TimeSignature initial) { _entries.Add((0, initial)); } |
| | 121 | | public void AddChange(long tick, TimeSignature sig) |
| | 122 | | { |
| | 123 | | if (tick < 0) throw new ArgumentOutOfRangeException(nameof(tick)); |
| | 124 | | if (_entries.Any(e=>e.tick==tick)) _entries.RemoveAll(e=>e.tick==tick); |
| | 125 | | _entries.Add((tick, sig)); |
| | 126 | | _entries.Sort((a,b)=>a.tick.CompareTo(b.tick)); |
| | 127 | | } |
| | 128 | | public TimeSignature GetAt(long tick) |
| | 129 | | { |
| | 130 | | TimeSignature current = _entries[0].sig; |
| | 131 | | foreach (var e in _entries) |
| | 132 | | { |
| | 133 | | if (e.tick > tick) break; |
| | 134 | | current = e.sig; |
| | 135 | | } |
| | 136 | | return current; |
| | 137 | | } |
| | 138 | | public TimePosition ToPosition(long tick) |
| | 139 | | => new(GetAt(tick), tick); |
| | 140 | | } |
| | 141 | |
|
| | 142 | | /// <summary>テンポ (microseconds per quarter note)。</summary> |
| | 143 | | public readonly struct Tempo : IEquatable<Tempo> |
| | 144 | | { |
| | 145 | | public int MicrosecondsPerQuarter { get; } |
| | 146 | | public Tempo(int usPerQuarter) |
| | 147 | | { if (usPerQuarter<=0) throw new ArgumentOutOfRangeException(nameof(usPerQuarter)); MicrosecondsPerQuarter=usPerQuar |
| | 148 | | public double Bpm => 60_000_000.0 / MicrosecondsPerQuarter; |
| | 149 | | public static Tempo FromBpm(double bpm) => new((int)(60_000_000 / bpm)); |
| | 150 | | public bool Equals(Tempo other)=> MicrosecondsPerQuarter==other.MicrosecondsPerQuarter; |
| | 151 | | public override bool Equals(object? o)=> o is Tempo t && Equals(t); |
| | 152 | | public override int GetHashCode()=> MicrosecondsPerQuarter; |
| | 153 | | public override string ToString()=>$"{Bpm:F2} BPM"; |
| | 154 | | } |
| | 155 | |
|
| | 156 | | public readonly struct TempoChange { public long Tick { get; } public Tempo Tempo { get; } public TempoChange(long t, Te |
| | 157 | |
|
| | 158 | | /// <summary>メタイベント (簡易)。</summary> |
| | 159 | | public abstract record MetaEvent(long Tick); |
| | 160 | | public sealed record TempoEvent(long Tick, Tempo Tempo): MetaEvent(Tick); |
| | 161 | | public sealed record TimeSignatureEvent(long Tick, TimeSignature Signature): MetaEvent(Tick); |
| | 162 | | public sealed record KeySignatureEvent(long Tick, int SharpsFlats, bool IsMinor): MetaEvent(Tick); // SharpsFlats: -7..+ |
| | 163 | | public sealed record TextEvent(long Tick, string Text): MetaEvent(Tick); |
| | 164 | | public sealed record TrackNameEvent(long Tick, string Name): MetaEvent(Tick); |
| 4 | 165 | | public sealed record MarkerEvent(long Tick, string Label): MetaEvent(Tick); |
| | 166 | |
|
| | 167 | | /// <summary>MIDI トラック的コンテナ (ノートイベント + メタイベント)。</summary> |
| | 168 | | public sealed class MidiTrack |
| | 169 | | { |
| | 170 | | private readonly List<MidiNoteEvent> _notes = new(); |
| | 171 | | private readonly List<MetaEvent> _meta = new(); |
| | 172 | | public IReadOnlyList<MidiNoteEvent> Notes => _notes; |
| | 173 | | public IReadOnlyList<MetaEvent> MetaEvents => _meta; |
| | 174 | | public void AddNote(MidiNoteEvent e) => _notes.Add(e); |
| | 175 | | public void AddMeta(MetaEvent e) => _meta.Add(e); |
| | 176 | | public void AddNoteRange(IEnumerable<MidiNoteEvent> events) => _notes.AddRange(events); |
| | 177 | | public void Sort() |
| | 178 | | { |
| | 179 | | _notes.Sort((a,b)=> a.Tick==b.Tick ? (a.IsNoteOn==b.IsNoteOn ? 0 : (a.IsNoteOn ? 1:-1)) : a.Tick.CompareTo(b.Tic |
| | 180 | | _meta.Sort((a,b)=> a.Tick.CompareTo(b.Tick)); |
| | 181 | | } |
| | 182 | | } |
| | 183 | |
|
| | 184 | | /// <summary>クオンタイズ & スイングユーティリティ。</summary> |
| | 185 | | public static class Quantize |
| | 186 | | { |
| | 187 | | /// <summary>指定分解能 (ticks) に丸め (最近傍)。</summary> |
| | 188 | | public static long ToGrid(long tick, int gridTicks) |
| | 189 | | { |
| | 190 | | if (gridTicks<=0) return tick; |
| | 191 | | long q = tick / gridTicks; |
| | 192 | | long r = tick % gridTicks; |
| | 193 | | return r < gridTicks/2 ? q*gridTicks : (q+1)*gridTicks; |
| | 194 | | } |
| | 195 | | /// <summary>スイング (2つ目の8分を遅らせる) Ratio=0..1 (0=ストレート, 0.5=三連近似)。</summary> |
| | 196 | | public static long ApplySwing(long tick, int subdivisionTicks, double ratio) |
| | 197 | | { |
| | 198 | | if (ratio<=0) return tick; |
| | 199 | | int pair = subdivisionTicks*2; |
| | 200 | | long inPair = tick % pair; |
| | 201 | | if (inPair < subdivisionTicks) return tick; // 1つ目はそのまま |
| | 202 | | double delay = subdivisionTicks * ratio; |
| | 203 | | return tick + (long)delay; |
| | 204 | | } |
| | 205 | |
|
| | 206 | | /// <summary> |
| | 207 | | /// 強拍優先クオンタイズ: 四捨五入結果と隣接強拍のどちらが近いか + バイアス係数で決める。 |
| | 208 | | /// strongBeatsTicksWithinBar は 拍子内 (0..TicksPerBar) の強拍 tick (0 基点) リスト。 |
| | 209 | | /// bias (0..1) 高いほど強拍へ吸着しやすい。 |
| | 210 | | /// </summary> |
| | 211 | | public static long ToGridStrongBeat(long tick, int gridTicks, int ticksPerBar, IReadOnlyList<int> strongBeatsTicksWi |
| | 212 | | { |
| | 213 | | if (gridTicks<=0 || strongBeatsTicksWithinBar.Count==0) return ToGrid(tick, gridTicks); |
| | 214 | | long rounded = ToGrid(tick, gridTicks); |
| | 215 | | long barIndex = tick / ticksPerBar; |
| | 216 | | long posInBar = tick % ticksPerBar; |
| | 217 | | int nearestStrong = strongBeatsTicksWithinBar.OrderBy(b => Math.Abs(b - posInBar)).First(); |
| | 218 | | long strongTick = barIndex * ticksPerBar + nearestStrong; |
| | 219 | | long distRounded = Math.Abs(rounded - tick); |
| | 220 | | long distStrong = Math.Abs(strongTick - tick); |
| | 221 | | if (distStrong * (1.0 - bias) < distRounded) return strongTick; // バイアス適用 |
| | 222 | | return rounded; |
| | 223 | | } |
| | 224 | |
|
| | 225 | | /// <summary> |
| | 226 | | /// グルーヴテンプレート適用: pattern[i]=tick オフセット (±) を 1 サイクル (pattern.Length グリッド) で繰返し適用。 |
| | 227 | | /// 先にグリッドに丸めた後、オフセットを加算。 |
| | 228 | | /// </summary> |
| | 229 | | public static long ApplyGroove(long tick, int gridTicks, IReadOnlyList<int> pattern) |
| | 230 | | { |
| | 231 | | if (gridTicks<=0 || pattern.Count==0) return tick; |
| | 232 | | long baseGrid = ToGrid(tick, gridTicks); |
| | 233 | | long index = (baseGrid / gridTicks) % pattern.Count; |
| | 234 | | return baseGrid + pattern[(int)index]; |
| | 235 | | } |
| | 236 | | } |