| | 1 | | using System; |
| | 2 | | using System.Collections.Generic; |
| | 3 | | using System.Linq; |
| | 4 | |
|
| | 5 | | namespace MusicTheory.Theory.Time; |
| | 6 | |
|
| | 7 | | /// <summary> |
| | 8 | | /// 音価虫眼鏡ツール: 四分音符[*1] を基準に、定義済みの代表音価ステップに沿って拡大(+) / 縮小(-) する。 |
| | 9 | | /// 例: 8分三連符[*0.33] → 付点16分[*0.375] → 8分[*0.5] → 4分三連符[*0.66] → 付点8分[*0.75] → 4分[*1] → ... |
| | 10 | | /// </summary> |
| | 11 | | public static class NoteValueZoom |
| | 12 | | { |
| 134 | 13 | | public readonly record struct Entry(string Label, Func<Duration> Factory) |
| | 14 | | { |
| 106 | 15 | | public Duration Create() => Factory(); |
| 0 | 16 | | public double FactorToQuarter => Create().Ticks / (double)Duration.TicksPerQuarter; // 四分音符=1.0 |
| 98 | 17 | | public int Ticks => Create().Ticks; |
| | 18 | | } |
| | 19 | |
|
| | 20 | | // 昇順(小→大)。四分音符[*1] を中心に、画像で示されたリストに準拠。 |
| 1 | 21 | | private static readonly Entry[] _entries = new[] |
| 1 | 22 | | { |
| 9 | 23 | | new Entry("32分音符[*0.125]", () => DurationFactory.ThirtySecond()), |
| 7 | 24 | | new Entry("16分三連符[*0.165]", () => DurationFactory.Tuplet(BaseNoteValue.Sixteenth, new Tuplet(3,2))), |
| 8 | 25 | | new Entry("16分音符[*0.25]", () => DurationFactory.Sixteenth()), |
| 7 | 26 | | new Entry("8分三連符[*0.33]", () => DurationFactory.Tuplet(BaseNoteValue.Eighth, new Tuplet(3,2))), |
| 7 | 27 | | new Entry("付点16分音符[*0.375]", () => DurationFactory.Sixteenth(1)), |
| 7 | 28 | | new Entry("8分音符[*0.5]", () => DurationFactory.Eighth()), |
| 7 | 29 | | new Entry("4分三連符[*0.66]", () => DurationFactory.Tuplet(BaseNoteValue.Quarter, new Tuplet(3,2))), |
| 8 | 30 | | new Entry("付点8分音符[*0.75]", () => DurationFactory.Eighth(1)), |
| 8 | 31 | | new Entry("4分音符[*1]", () => DurationFactory.Quarter()), |
| 8 | 32 | | new Entry("2分三連符[*1.32]", () => DurationFactory.Tuplet(BaseNoteValue.Half, new Tuplet(3,2))), |
| 7 | 33 | | new Entry("付点4分音符[*1.5]", () => DurationFactory.Quarter(1)), |
| 7 | 34 | | new Entry("2分音符[*2]", () => DurationFactory.Half()), |
| 7 | 35 | | new Entry("付点2分音符[*3]", () => DurationFactory.Half(1)), |
| 9 | 36 | | new Entry("全音符[*4]", () => DurationFactory.Whole()), |
| 1 | 37 | | }; |
| | 38 | |
|
| 8 | 39 | | public static IReadOnlyList<Entry> Entries => _entries; |
| | 40 | |
|
| | 41 | | /// <summary>四分音符[*1]基準の倍率(ticks/480)で現在位置に最も近いインデックスを返す(同距離は「大きい方」を優先)。</summary> |
| | 42 | | public static int FindNearestIndex(Duration current) |
| | 43 | | { |
| 6 | 44 | | var ticks = current.Ticks; |
| 12 | 45 | | int bestIdx = 0; int bestDiff = Math.Abs(_entries[0].Ticks - ticks); |
| 168 | 46 | | for (int i=1;i<_entries.Length;i++) |
| | 47 | | { |
| 78 | 48 | | int diff = Math.Abs(_entries[i].Ticks - ticks); |
| 78 | 49 | | if (diff < bestDiff || (diff==bestDiff && _entries[i].Ticks > _entries[bestIdx].Ticks)) |
| 39 | 50 | | (bestIdx, bestDiff) = (i, diff); |
| | 51 | | } |
| 6 | 52 | | return bestIdx; |
| | 53 | | } |
| | 54 | |
|
| | 55 | | /// <summary>ズーム(delta>0:大きく, delta<0:小さく)。範囲外は端にクランプ。</summary> |
| | 56 | | public static Duration Zoom(Duration current, int delta) |
| | 57 | | { |
| 4 | 58 | | int idx = FindNearestIndex(current); |
| 4 | 59 | | int next = Math.Clamp(idx + delta, 0, _entries.Length - 1); |
| 4 | 60 | | return _entries[next].Create(); |
| | 61 | | } |
| | 62 | |
|
| 2 | 63 | | public static Duration ZoomIn(Duration current) => Zoom(current, +1); |
| 2 | 64 | | public static Duration ZoomOut(Duration current) => Zoom(current, -1); |
| | 65 | | } |