< Summary

Information
Class: MusicTheory.Theory.Time.TempoMap
Assembly: MusicTheory
File(s): /home/runner/work/MusicTheory/MusicTheory/Theory/Time/TempoMap.cs
Line coverage
67%
Covered lines: 29
Uncovered lines: 14
Coverable lines: 43
Total lines: 93
Line coverage: 67.4%
Branch coverage
34%
Covered branches: 9
Total branches: 26
Branch coverage: 34.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
AddChange(...)50%22100%
GetAt(...)0%4260%
TickToMicroseconds(...)50%44100%
TickToTimeSpan(...)100%210%
MicrosecondsToTick(...)0%4260%
get_Entries()100%210%
RebuildCache()75%44100%
UpperBound(...)75%44100%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Linq;
 4
 5namespace MusicTheory.Theory.Time;
 6
 7/// <summary>
 8/// 可変テンポマップ。Tick -> RealTime (マイクロ秒 / TimeSpan) 変換と逆変換の基本実装。
 9/// MIDI SMF では Tempo MetaEvent(FF 51) がこれに相当するが、
 10/// ライブラリ内で前計算/問い合わせを容易にするため独立構造を持つ。
 11/// </summary>
 12public sealed class TempoMap
 13{
 314    private readonly List<(long tick, Tempo tempo)> _entries = new();
 315    private long[] _ticks = Array.Empty<long>();
 316    private long[] _cumulativeUs = Array.Empty<long>(); // 各エントリ開始 tick 時点での累積マイクロ秒
 1217    public TempoMap(Tempo initial) { _entries.Add((0, initial)); RebuildCache(); }
 18
 19    /// <summary>テンポ変更追加 (同 tick が既に存在する場合置換)。</summary>
 20    public void AddChange(long tick, Tempo tempo)
 21    {
 522        if (tick < 0) throw new ArgumentOutOfRangeException(nameof(tick));
 1223        _entries.RemoveAll(e => e.tick == tick);
 524        _entries.Add((tick, tempo));
 1425        _entries.Sort((a,b)=>a.tick.CompareTo(b.tick));
 526        RebuildCache();
 527    }
 28
 29    /// <summary>対象 tick 時点の有効テンポを取得。</summary>
 30    public Tempo GetAt(long tick)
 31    {
 032        if (tick < 0) throw new ArgumentOutOfRangeException(nameof(tick));
 033        int idx = UpperBound(_ticks, tick) - 1; // 最後の開始 tick
 034        if (idx < 0) idx = 0; if (idx >= _entries.Count) idx = _entries.Count-1;
 035        return _entries[idx].tempo;
 36    }
 37
 38    /// <summary>
 39    /// 0 から指定 tick までの経過時間 (microseconds) を計算。
 40    /// </summary>
 41    public long TickToMicroseconds(long tick)
 42    {
 443        if (tick < 0) throw new ArgumentOutOfRangeException(nameof(tick));
 844        int idx = UpperBound(_ticks, tick) - 1; if (idx < 0) idx = 0;
 445        long baseUs = _cumulativeUs[idx];
 446        var (startTick, tempo) = _entries[idx];
 447        long delta = tick - startTick;
 448        baseUs += delta * tempo.MicrosecondsPerQuarter / Duration.TicksPerQuarter;
 449        return baseUs;
 50    }
 51
 052    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    {
 060        if (microseconds < 0) throw new ArgumentOutOfRangeException(nameof(microseconds));
 061        int idx = UpperBound(_cumulativeUs, microseconds) - 1; if (idx < 0) idx = 0;
 062        long baseUs = _cumulativeUs[idx];
 063        var (startTick, tempo) = _entries[idx];
 064        long remainUs = microseconds - baseUs;
 065        if (remainUs < 0) remainUs = 0;
 066        long ticks = remainUs * Duration.TicksPerQuarter / tempo.MicrosecondsPerQuarter;
 067        return startTick + ticks;
 68    }
 69
 70    /// <summary>内部エントリ列挙 (デバッグ/表示用途)。</summary>
 071    public IReadOnlyList<(long tick, Tempo tempo)> Entries => _entries;
 72
 73    private void RebuildCache()
 74    {
 2375        _ticks = _entries.Select(e=>e.tick).ToArray();
 876        _cumulativeUs = new long[_entries.Count];
 877        if (_entries.Count == 0) return;
 878        _cumulativeUs[0] = 0;
 3079        for (int i=1;i<_entries.Count;i++)
 80        {
 781            var (prevTick, prevTempo) = _entries[i-1];
 782            long deltaTicks = _entries[i].tick - prevTick;
 783            long us = deltaTicks * prevTempo.MicrosecondsPerQuarter / Duration.TicksPerQuarter;
 784            _cumulativeUs[i] = _cumulativeUs[i-1] + us;
 85        }
 886    }
 87
 88    private static int UpperBound(long[] arr, long value)
 89    {
 890        int l=0,r=arr.Length; // first > value
 3291        while (l<r){int m=(l+r)/2; if (arr[m] <= value) l=m+1; else r=m;} return l;
 92    }
 93}