fix: add VUScale.spokenLevel with proper clamping and non-finite handling
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,14 @@ public enum VUScale {
|
|||||||
public static func angle(forFraction fraction: Double) -> Double {
|
public static func angle(forFraction fraction: Double) -> Double {
|
||||||
(-MeterTokens.vuArcDegrees / 2 + MeterTokens.vuArcDegrees * fraction) * .pi / 180
|
(-MeterTokens.vuArcDegrees / 2 + MeterTokens.vuArcDegrees * fraction) * .pi / 180
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Level as VoiceOver should say it: rounded to steps of 3 VU, clamped to the scale, "stil" for non-finite input.
|
||||||
|
public static func spokenLevel(fromDbfs dbfs: Double) -> String {
|
||||||
|
guard dbfs.isFinite else { return "stil" }
|
||||||
|
let stepped = (vu(fromDbfs: dbfs) / 3).rounded() * 3
|
||||||
|
let clamped = min(MeterTokens.vuMax, max(MeterTokens.vuMin, stepped))
|
||||||
|
return "\(Int(clamped)) VU"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Second-order needle: reaches ~99 % in `response` seconds, slight overshoot for damping < 1.
|
/// Second-order needle: reaches ~99 % in `response` seconds, slight overshoot for damping < 1.
|
||||||
|
|||||||
@@ -60,8 +60,7 @@ public struct VUMeterView: View {
|
|||||||
/// Rounded to steps of 3 VU so VoiceOver is not flooded with changes.
|
/// Rounded to steps of 3 VU so VoiceOver is not flooded with changes.
|
||||||
private var spokenValue: String {
|
private var spokenValue: String {
|
||||||
guard isActive else { return "stil" }
|
guard isActive else { return "stil" }
|
||||||
let vu = (VUScale.vu(fromDbfs: levelDbfs) / 3).rounded() * 3
|
return VUScale.spokenLevel(fromDbfs: levelDbfs)
|
||||||
return "\(Int(vu)) VU"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: drawing (design coordinates: MeterTokens.vuWidth × vuHeight)
|
// MARK: drawing (design coordinates: MeterTokens.vuWidth × vuHeight)
|
||||||
|
|||||||
@@ -106,3 +106,26 @@ struct LEDBarTests {
|
|||||||
#expect(LEDBar.litCount(forFraction: -1) == 0)
|
#expect(LEDBar.litCount(forFraction: -1) == 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SpokenLevelTests {
|
||||||
|
@Test func nonFiniteInputReturnsStil() {
|
||||||
|
#expect(VUScale.spokenLevel(fromDbfs: -.infinity) == "stil")
|
||||||
|
#expect(VUScale.spokenLevel(fromDbfs: .nan) == "stil")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func referenceLevel() {
|
||||||
|
#expect(VUScale.spokenLevel(fromDbfs: -20) == "0 VU")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func negativeLevel() {
|
||||||
|
#expect(VUScale.spokenLevel(fromDbfs: -30) == "-9 VU")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func clampedToMinimum() {
|
||||||
|
#expect(VUScale.spokenLevel(fromDbfs: -120) == "-20 VU")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func clampedToMaximum() {
|
||||||
|
#expect(VUScale.spokenLevel(fromDbfs: 10) == "3 VU")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user