Files
onyx/Packages/OnyxKit/Sources/AudioProvider/Gain.swift
Scarriffle ed8b095377 Overdrive je Programm: bis 400 %, aber nur auf Ansage
Der Regler ging bisher immer bis 200 %, ohne dass man das wollen musste. Jetzt
endet er bei 100 % — der Stellung, in der ein Programm klingt, wie es gemeint
war —, und ein Blitz in der Zeile gibt den Bereich darüber frei. Je Programm:
ein Browser braucht ihn für eine leise Konferenz, die Musik daneben nicht.

Oberhalb von 100 % gilt die Kennlinie nicht mehr. 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. Unterhalb bleibt alles wie es war.

Beim Abschalten wird die Lautstärke zurückgeholt. Sonst bliebe ein Programm
dreifach verstärkt, während sein Regler am rechten Anschlag steht und 100 %
anzeigt. Dasselbe beim Laden alter Einstellungen: ein gespeichertes 180 % aus
der Zeit ohne Freigabe fällt auf 100 %.

Die 100 %-Markierung im Regler erscheint nur noch mit Overdrive — ohne ihn läge
sie am rechten Anschlag und sagte dort nichts mehr.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 18:02:43 +02:00

149 lines
6.1 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<String> = []
private var overdrive: Set<String> = []
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)
}
}