問題
指揮者タイムアウト後の Internal モード復帰で、位相継承が正しく機能しない。
現状
`MetronomeViewModel.followerTimedOut()` → `MetronomeEngine.setInternalFromExternal(lastBeatHostTime:beatIndex:bpm:)` が呼ばれる。
`setInternalFromExternal` は:
```swift
nextBeatHostTime = lastBeatHostTime + subIntervalMach * UInt64(subsPerBeat)
```
つまり「最終ビート + 1拍」を次のビートとして設定する。
バグ
タイムアウトは 1.5拍後 に発火する。
発火時点では `lastBeatHostTime + 1拍` は 必ず過去時刻。
その直後 `scheduleUpcoming()` が走り:
```swift
if nextBeatHostTime < now {
if syncedNowProvider != nil {
calculateNextBeatFromSyncedClock() // ← 外部位相を捨てて再量子化
}
}
```
結果、指揮者の位相は継承されず、grid 上の次のタイミングにジャンプする。
改善方針
復帰時点で `lastBeatHostTime + n * beatInterval >= now` となる最初の未来ビートを計算:
```swift
var next = lastBeatHostTime + subIntervalMach * UInt64(subsPerBeat)
let now = mach_absolute_time()
while next < now {
next += subIntervalMach * UInt64(subsPerBeat)
currentSubBeat = (currentSubBeat + subsPerBeat) % totalSubs
}
nextBeatHostTime = next
```
加えて、この経路では `calculateNextBeatFromSyncedClock()` に潰されないように scheduleUpcoming のガード条件を見直す必要あり。
重要度
High
問題
指揮者タイムアウト後の Internal モード復帰で、位相継承が正しく機能しない。
現状
`MetronomeViewModel.followerTimedOut()` → `MetronomeEngine.setInternalFromExternal(lastBeatHostTime:beatIndex:bpm:)` が呼ばれる。
`setInternalFromExternal` は:
```swift
nextBeatHostTime = lastBeatHostTime + subIntervalMach * UInt64(subsPerBeat)
```
つまり「最終ビート + 1拍」を次のビートとして設定する。
バグ
タイムアウトは 1.5拍後 に発火する。
発火時点では `lastBeatHostTime + 1拍` は 必ず過去時刻。
その直後 `scheduleUpcoming()` が走り:
```swift
if nextBeatHostTime < now {
if syncedNowProvider != nil {
calculateNextBeatFromSyncedClock() // ← 外部位相を捨てて再量子化
}
}
```
結果、指揮者の位相は継承されず、grid 上の次のタイミングにジャンプする。
改善方針
復帰時点で `lastBeatHostTime + n * beatInterval >= now` となる最初の未来ビートを計算:
```swift
var next = lastBeatHostTime + subIntervalMach * UInt64(subsPerBeat)
let now = mach_absolute_time()
while next < now {
next += subIntervalMach * UInt64(subsPerBeat)
currentSubBeat = (currentSubBeat + subsPerBeat) % totalSubs
}
nextBeatHostTime = next
```
加えて、この経路では `calculateNextBeatFromSyncedClock()` に潰されないように scheduleUpcoming のガード条件を見直す必要あり。
重要度
High