import Foundation /// Rechnet Reglerstellung in Verstärkung um. public enum Gain { /// Ohne Overdrive ist bei 100 % Schluss. /// /// Das ist die Stellung, in der das Programm klingt, wie es gemeint war. /// Alles darüber ist ein Eingriff und soll auch einer sein. public static let maximumVolume: Double = 1.0 /// Mit Overdrive: das Vierfache. /// /// Für leise Videokonferenzen und schlecht ausgesteuerte Videos. Darüber /// hinaus bringt es nichts mehr — der Begrenzer drückt dann alles zusammen, /// und was lauter wird, ist vor allem das Grundrauschen der Quelle. public static let overdriveMaximum: Double = 4.0 public static func maximum(overdrive: Bool) -> Double { overdrive ? overdriveMaximum : maximumVolume } /// Verstärkungsfaktor zu einer Reglerstellung von 0 bis 2. /// /// Bewusst nicht linear. Lautstärke wird ungefähr logarithmisch /// wahrgenommen; ein linearer Regler wäre in der unteren Hälfte praktisch /// wirkungslos und in der oberen viel zu empfindlich. /// /// Der Exponent ist hergeleitet, nicht geraten: halbe empfundene Lautstärke /// entspricht rund −10 dB, also Faktor 0,316. Aus 0,5^k = 0,316 folgt /// k ≈ 1,66. Damit klingt der halbe Reglerweg auch halb so laut. /// /// Eine Potenz statt einer Dezibel-Rechnung, weil hier kein Logarithmus je /// Abtastwert anfallen soll — der Faktor wird einmal je Puffer bestimmt, /// aber die Formel steht in einem Echtzeitpfad. public static let taper: Double = 1.66 /// Oberhalb von 100 % gilt die Kennlinie **nicht**. /// /// Dort steht die Zahl für den Faktor selbst: 400 % sind das Vierfache der /// Amplitude. Mit der Kennlinie wären daraus 4^1,66 ≈ 10,4 geworden — eine /// Beschriftung, die etwas anderes verspricht, als der Regler tut. Bei /// genau 100 % stimmen beide Hälften überein, die Kurve hat also keinen /// Sprung, nur einen Knick. public static func factor(forVolume volume: Double) -> Double { let clamped = min(max(volume, 0), overdriveMaximum) guard clamped > 0 else { return 0 } return clamped <= 1 ? pow(clamped, taper) : clamped } } /// Weiche Begrenzung. /// /// Bei zweifacher Verstärkung entstehen Abtastwerte weit über dem gültigen /// Bereich. Ungebremst schneidet die Hardware sie hart ab, und das knackt /// hörbar. Der Tangens hyperbolicus biegt sie stattdessen sanft um: leise /// Signale bleiben unangetastet, laute werden zunehmend gestaucht. public enum SoftClip { /// Ab hier greift die Kennlinie. Darunter bleibt das Signal unverändert — /// eine Begrenzung, die schon bei Zimmerlautstärke arbeitet, klingt matt. public static let threshold: Float = 0.7 public static func apply(_ sample: Float) -> Float { let magnitude = abs(sample) guard magnitude > threshold else { return sample } let excess = magnitude - threshold let headroom = 1 - threshold let compressed = threshold + headroom * tanh(excess / headroom) return sample < 0 ? -compressed : compressed } } /// Merkt sich Lautstärke und Stummschaltung je Programm. /// /// Nach der Bundle-Kennung, nicht nach Prozessnummer: die ändert sich bei /// jedem Start, und eine Einstellung, die einen Neustart des Programms nicht /// überlebt, ist keine. public struct VolumeStore: Sendable { private var volumes: [String: Double] = [:] private var muted: Set = [] private var overdrive: Set = [] public init() {} /// Unbekannte Programme starten auf voller Lautstärke — nicht auf null. /// Ein Programm, das nach dem Start stumm ist, wirkt kaputt, und niemand /// sucht die Ursache im Mixer. public func volume(for bundleID: String) -> Double { volumes[bundleID] ?? 1.0 } public func isMuted(for bundleID: String) -> Bool { muted.contains(bundleID) } public mutating func set(_ volume: Double, for bundleID: String) { volumes[bundleID] = min(max(volume, 0), Gain.maximum(overdrive: isOverdriven(bundleID))) } public func isOverdriven(_ bundleID: String) -> Bool { overdrive.contains(bundleID) } /// Schaltet den erweiterten Bereich frei. /// /// Beim Abschalten wird die Lautstärke zurückgeholt: ein Wert von 340 % /// bliebe sonst wirksam, obwohl der Regler ihn gar nicht mehr darstellen /// kann — der Knopf säße am rechten Anschlag und das Programm wäre trotzdem /// dreifach verstärkt. public mutating func setOverdrive(_ isOn: Bool, for bundleID: String) { if isOn { overdrive.insert(bundleID) } else { overdrive.remove(bundleID) if let volume = volumes[bundleID], volume > Gain.maximumVolume { volumes[bundleID] = Gain.maximumVolume } } } public mutating func setMuted(_ isMuted: Bool, for bundleID: String) { // Getrennt von der Lautstärke: sonst verliert man beim Stummschalten // den eingestellten Wert und muss ihn danach neu suchen. if isMuted { muted.insert(bundleID) } else { muted.remove(bundleID) } } /// Was der Audiopfad tatsächlich anwenden soll. public func effectiveGain(for bundleID: String) -> Double { isMuted(for: bundleID) ? 0 : Gain.factor(forVolume: volume(for: bundleID)) } // MARK: - Sicherung public var snapshot: [String: Double] { volumes } public var mutedIdentifiers: [String] { Array(muted) } public var overdrivenIdentifiers: [String] { Array(overdrive) } public init(volumes: [String: Double], muted: [String], overdrive: [String] = []) { // Der Overdrive zuerst: sonst schneidet das Einsetzen der Lautstärken // alles über 100 % ab, was gerade wiederhergestellt werden soll. self.overdrive = Set(overdrive) let allowed = Set(overdrive) self.volumes = volumes.reduce(into: [:]) { result, entry in let maximum = Gain.maximum(overdrive: allowed.contains(entry.key)) result[entry.key] = min(max(entry.value, 0), maximum) } self.muted = Set(muted) } }