| | 1 | | using System; |
| | 2 | | using System.Collections.Generic; |
| | 3 | | using System.Linq; |
| | 4 | |
|
| | 5 | | namespace MusicTheory.Theory.Time; |
| | 6 | |
|
| | 7 | | /// <summary> |
| | 8 | | /// 可変テンポマップ。Tick -> RealTime (マイクロ秒 / TimeSpan) 変換と逆変換の基本実装。 |
| | 9 | | /// MIDI SMF では Tempo MetaEvent(FF 51) がこれに相当するが、 |
| | 10 | | /// ライブラリ内で前計算/問い合わせを容易にするため独立構造を持つ。 |
| | 11 | | /// </summary> |
| | 12 | | public sealed class TempoMap |
| | 13 | | { |
| 3 | 14 | | private readonly List<(long tick, Tempo tempo)> _entries = new(); |
| 3 | 15 | | private long[] _ticks = Array.Empty<long>(); |
| 3 | 16 | | private long[] _cumulativeUs = Array.Empty<long>(); // 各エントリ開始 tick 時点での累積マイクロ秒 |
| 12 | 17 | | public TempoMap(Tempo initial) { _entries.Add((0, initial)); RebuildCache(); } |
| | 18 | |
|
| | 19 | | /// <summary>テンポ変更追加 (同 tick が既に存在する場合置換)。</summary> |
| | 20 | | public void AddChange(long tick, Tempo tempo) |
| | 21 | | { |
| 5 | 22 | | if (tick < 0) throw new ArgumentOutOfRangeException(nameof(tick)); |
| 12 | 23 | | _entries.RemoveAll(e => e.tick == tick); |
| 5 | 24 | | _entries.Add((tick, tempo)); |
| 14 | 25 | | _entries.Sort((a,b)=>a.tick.CompareTo(b.tick)); |
| 5 | 26 | | RebuildCache(); |
| 5 | 27 | | } |
| | 28 | |
|
| | 29 | | /// <summary>対象 tick 時点の有効テンポを取得。</summary> |
| | 30 | | public Tempo GetAt(long tick) |
| | 31 | | { |
| 0 | 32 | | if (tick < 0) throw new ArgumentOutOfRangeException(nameof(tick)); |
| 0 | 33 | | int idx = UpperBound(_ticks, tick) - 1; // 最後の開始 tick |
| 0 | 34 | | if (idx < 0) idx = 0; if (idx >= _entries.Count) idx = _entries.Count-1; |
| 0 | 35 | | return _entries[idx].tempo; |
| | 36 | | } |
| | 37 | |
|
| | 38 | | /// <summary> |
| | 39 | | /// 0 から指定 tick までの経過時間 (microseconds) を計算。 |
| | 40 | | /// </summary> |
| | 41 | | public long TickToMicroseconds(long tick) |
| | 42 | | { |
| 4 | 43 | | if (tick < 0) throw new ArgumentOutOfRangeException(nameof(tick)); |
| 8 | 44 | | int idx = UpperBound(_ticks, tick) - 1; if (idx < 0) idx = 0; |
| 4 | 45 | | long baseUs = _cumulativeUs[idx]; |
| 4 | 46 | | var (startTick, tempo) = _entries[idx]; |
| 4 | 47 | | long delta = tick - startTick; |
| 4 | 48 | | baseUs += delta * tempo.MicrosecondsPerQuarter / Duration.TicksPerQuarter; |
| 4 | 49 | | return baseUs; |
| | 50 | | } |
| | 51 | |
|
| 0 | 52 | | public TimeSpan TickToTimeSpan(long tick) => TimeSpan.FromMilliseconds(TickToMicroseconds(tick) / 1000.0); |
| | 53 | |
|
| | 54 | | /// <summary> |
| | 55 | | /// microseconds から tick への概算逆変換 (現在のテンポ区間を前から積分)。 |
| | 56 | | /// 完全逆写像ではなく、テンポ境界を跨ぐ場合は境界単位で探索。 |
| | 57 | | /// </summary> |
| | 58 | | public long MicrosecondsToTick(long microseconds) |
| | 59 | | { |
| 0 | 60 | | if (microseconds < 0) throw new ArgumentOutOfRangeException(nameof(microseconds)); |
| 0 | 61 | | int idx = UpperBound(_cumulativeUs, microseconds) - 1; if (idx < 0) idx = 0; |
| 0 | 62 | | long baseUs = _cumulativeUs[idx]; |
| 0 | 63 | | var (startTick, tempo) = _entries[idx]; |
| 0 | 64 | | long remainUs = microseconds - baseUs; |
| 0 | 65 | | if (remainUs < 0) remainUs = 0; |
| 0 | 66 | | long ticks = remainUs * Duration.TicksPerQuarter / tempo.MicrosecondsPerQuarter; |
| 0 | 67 | | return startTick + ticks; |
| | 68 | | } |
| | 69 | |
|
| | 70 | | /// <summary>内部エントリ列挙 (デバッグ/表示用途)。</summary> |
| 0 | 71 | | public IReadOnlyList<(long tick, Tempo tempo)> Entries => _entries; |
| | 72 | |
|
| | 73 | | private void RebuildCache() |
| | 74 | | { |
| 23 | 75 | | _ticks = _entries.Select(e=>e.tick).ToArray(); |
| 8 | 76 | | _cumulativeUs = new long[_entries.Count]; |
| 8 | 77 | | if (_entries.Count == 0) return; |
| 8 | 78 | | _cumulativeUs[0] = 0; |
| 30 | 79 | | for (int i=1;i<_entries.Count;i++) |
| | 80 | | { |
| 7 | 81 | | var (prevTick, prevTempo) = _entries[i-1]; |
| 7 | 82 | | long deltaTicks = _entries[i].tick - prevTick; |
| 7 | 83 | | long us = deltaTicks * prevTempo.MicrosecondsPerQuarter / Duration.TicksPerQuarter; |
| 7 | 84 | | _cumulativeUs[i] = _cumulativeUs[i-1] + us; |
| | 85 | | } |
| 8 | 86 | | } |
| | 87 | |
|
| | 88 | | private static int UpperBound(long[] arr, long value) |
| | 89 | | { |
| 8 | 90 | | int l=0,r=arr.Length; // first > value |
| 32 | 91 | | while (l<r){int m=(l+r)/2; if (arr[m] <= value) l=m+1; else r=m;} return l; |
| | 92 | | } |
| | 93 | | } |