Ein Tap mit mutedWhenTapped schaltet die ursprüngliche Ausgabe eines Programms stumm und liefert das Signal stattdessen bei Onyx ab; ein privates Aggregate-Device aus Tap und echtem Ausgabegerät gibt es nach der Verstärkung wieder aus. Das ist der einzige Weg, den macOS ohne eigenen Treiber anbietet. Die Kennlinie ist hergeleitet, nicht geraten: halbe empfundene Lautstärke sind rund −10 dB, also Faktor 0,316. Aus 0,5^k = 0,316 folgt k ≈ 1,66. Die Skala ist damit symmetrisch — 50 % sind −10 dB, 200 % sind +10 dB. Mein erster Ansatz mit der vierten Potenz ergab bei halbem Regler 6 % statt 32 %; der Test hat das gefangen, bevor es an einen Lautsprecher kam. Über 100 % greift ein weicher Begrenzer. Ohne ihn schneidet die Hardware die Werte hart ab, und das knackt hörbar. Er arbeitet erst ab 0,7, weil eine Begrenzung, die schon bei Zimmerlautstärke einsetzt, matt klingt. Der Mixer ist standardmäßig aus, und auch eingeschaltet greift er nur bei ausdrücklich gewählten Programmen ein. Alles andere läuft unverändert vorbei. Der Notausschalter steht sichtbar in der Ansicht, nicht in einem Menü: wenn der Ton weg ist, ist das der Knopf, den man sucht. Drei Fehlerfälle sind eingeplant, weil jeder von ihnen ein stummes Programm bedeutet. Der Wachhund baut Taps neu auf, die nur noch Stille liefern — der bekannte Fall aus Spike C. Ein Wechsel des Ausgabegeräts baut alle Taps neu auf, weil sie am alten Gerät hängen. Und ein fehlgeschlagener Aufbau räumt sich selbst weg, statt in einem halben Zustand zu verharren. Beim Beenden von Onyx wird der Mixer abgebaut: sonst bleiben die abgegriffenen Programme stumm zurück. 178 Tests grün, App-Signatur gültig.
73 lines
3.7 KiB
Swift
73 lines
3.7 KiB
Swift
// swift-tools-version: 6.2
|
|
import PackageDescription
|
|
|
|
let package = Package(
|
|
name: "OnyxKit",
|
|
// Quellsprache ist Englisch, damit die Schlüssel im Code lesbar bleiben.
|
|
// Deutsch kommt als Übersetzung dazu; die Systemsprache entscheidet.
|
|
defaultLocalization: "en",
|
|
platforms: [.macOS(.v26)],
|
|
products: [
|
|
.library(name: "OnyxCore", targets: ["OnyxCore"]),
|
|
.library(name: "OnyxHelperProtocol", targets: ["OnyxHelperProtocol"]),
|
|
.library(name: "OnyxDesign", targets: ["OnyxDesign"]),
|
|
.library(name: "OnyxNotch", targets: ["OnyxNotch"]),
|
|
.library(name: "OnyxWidgetKit", targets: ["OnyxWidgetKit"]),
|
|
.library(name: "OnyxMenuBar", targets: ["OnyxMenuBar"]),
|
|
.library(name: "CalendarProvider", targets: ["CalendarProvider"]),
|
|
.library(name: "WeatherProvider", targets: ["WeatherProvider"]),
|
|
.library(name: "MediaProvider", targets: ["MediaProvider"]),
|
|
.library(name: "MetricsProvider", targets: ["MetricsProvider"]),
|
|
.library(name: "NetworkProvider", targets: ["NetworkProvider"]),
|
|
.library(name: "AudioProvider", targets: ["AudioProvider"]),
|
|
],
|
|
targets: [
|
|
// Der String-Katalog muss ausdrücklich als Ressource stehen — sonst gibt
|
|
// es kein `Bundle.module`, und die Übersetzungen wären zur Laufzeit
|
|
// unauffindbar.
|
|
.target(name: "OnyxCore", resources: [.process("Localizable.xcstrings")]),
|
|
.testTarget(name: "OnyxCoreTests", dependencies: ["OnyxCore"]),
|
|
|
|
// Geteilt zwischen App und privilegiertem Helfer — deshalb ohne jede
|
|
// Abhängigkeit: der Helfer läuft als root und soll so wenig Code wie
|
|
// möglich enthalten.
|
|
.target(name: "OnyxHelperProtocol"),
|
|
.testTarget(name: "OnyxHelperProtocolTests", dependencies: ["OnyxHelperProtocol"]),
|
|
|
|
.target(name: "OnyxDesign"),
|
|
|
|
.target(name: "OnyxNotch", dependencies: ["OnyxDesign"]),
|
|
.testTarget(name: "OnyxNotchTests", dependencies: ["OnyxNotch"]),
|
|
|
|
.target(name: "OnyxWidgetKit", dependencies: ["OnyxDesign"]),
|
|
.testTarget(name: "OnyxWidgetKitTests", dependencies: ["OnyxWidgetKit"]),
|
|
|
|
.target(name: "OnyxMenuBar", dependencies: ["OnyxDesign", "OnyxWidgetKit"]),
|
|
.testTarget(name: "OnyxMenuBarTests", dependencies: ["OnyxMenuBar"]),
|
|
|
|
.target(name: "CalendarProvider", dependencies: ["OnyxDesign", "OnyxWidgetKit"],
|
|
resources: [.process("Localizable.xcstrings")]),
|
|
.testTarget(name: "CalendarProviderTests", dependencies: ["CalendarProvider"]),
|
|
|
|
.target(name: "WeatherProvider", dependencies: ["OnyxDesign", "OnyxWidgetKit"],
|
|
resources: [.process("Localizable.xcstrings")]),
|
|
.testTarget(name: "WeatherProviderTests", dependencies: ["WeatherProvider"]),
|
|
|
|
.target(name: "MediaProvider", dependencies: ["OnyxDesign", "OnyxWidgetKit"],
|
|
resources: [.process("Localizable.xcstrings")]),
|
|
.testTarget(name: "MediaProviderTests", dependencies: ["MediaProvider"]),
|
|
|
|
.target(name: "MetricsProvider", dependencies: ["OnyxDesign", "OnyxWidgetKit", "OnyxMenuBar"],
|
|
resources: [.process("Localizable.xcstrings")]),
|
|
.testTarget(name: "MetricsProviderTests", dependencies: ["MetricsProvider"]),
|
|
|
|
.target(name: "NetworkProvider", dependencies: ["OnyxDesign", "OnyxWidgetKit", "OnyxMenuBar", "MetricsProvider"],
|
|
resources: [.process("Localizable.xcstrings")]),
|
|
.testTarget(name: "NetworkProviderTests", dependencies: ["NetworkProvider"]),
|
|
|
|
.target(name: "AudioProvider", dependencies: ["OnyxDesign", "OnyxWidgetKit"],
|
|
resources: [.process("Localizable.xcstrings")]),
|
|
.testTarget(name: "AudioProviderTests", dependencies: ["AudioProvider"]),
|
|
]
|
|
)
|