Die Breite folgte der Stellenzahl des aktuellen Werts. Aus „14 KB/s" wird „431 KB/s", und mit der zusätzlichen Stelle wanderte die halbe Leiste. Jetzt wieder feste Breiten, bemessen am breitesten Wert, der vorkommen kann. Der frühere Fehler war nicht die feste Breite, sondern der falsche Bezugswert: gezeichnet wird „63°", bemessen wurde „100 °C" — fünfzehn Punkte für zwei Zeichen, die nie erscheinen. Nachgemessen: zwei Aufnahmen im Abstand von sechs Sekunden mit geänderten Werten ergeben identische Breiten. Der Mixer fasst Hilfsprozesse mit ihrem Programm zusammen. „Google Chrome" und „Google Chrome Helper" standen als zwei Zeilen nebeneinander, mit getrennten Reglern für dasselbe Programm — zieht man den einen, hört man den anderen. Chrome hat je Tab einen Helfer; fünfzehn Regler für einen Browser sind keine Auskunft. Abgegriffen wird weiterhin je Prozess, geregelt aber je Programm. Der Schlüssel der Taps muss deshalb den Prozess mitführen: mehrere Helfer teilen sich eine Kennung und überschrieben sich sonst gegenseitig. Der Pegel einer Zeile ist der lauteste ihrer Prozesse. Die Zeilen sind einzeilig statt zweizeilig: Symbol, Stummschalter, Regler, Wert. Der Name ist weg — bei einem erkennbaren Programmsymbol ist er redundant, und die gewonnene Zeile macht die Liste halb so hoch. Dafür ist das Symbol größer, und wer den Namen braucht, hält den Zeiger darauf. Popover 420 statt 330 Punkte breit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
44 lines
2.0 KiB
Swift
44 lines
2.0 KiB
Swift
import Foundation
|
|
|
|
/// Alle Tonquellen **eines** Programms in einer Zeile.
|
|
///
|
|
/// Chrome gibt seinen Ton über Hilfsprozesse aus — einen je Tab. In der Liste
|
|
/// standen deshalb „Google Chrome" und „Google Chrome Helper" nebeneinander,
|
|
/// und zwar mit getrennten Reglern für dasselbe Programm: zieht man den einen,
|
|
/// hört man den anderen. Fünfzehn Regler für einen Browser sind keine Auskunft,
|
|
/// sondern eine Zumutung.
|
|
public struct AudioProcessGroup: Identifiable, Equatable, Sendable {
|
|
|
|
/// Die Kennung des **Programms**, nicht die des Prozesses. Sie ist der
|
|
/// Schlüssel für Lautstärke, Stummschaltung und Auswahl.
|
|
public let bundleID: String
|
|
public let name: String
|
|
public let processes: [AudioProcess]
|
|
|
|
public var id: String { bundleID }
|
|
|
|
/// Spielt einer, spielt die Gruppe.
|
|
///
|
|
/// Der Ton kommt aus dem Helfer, während der Hauptprozess schweigt — die
|
|
/// Zeile muss trotzdem als spielend gelten, sonst verschwindet sie.
|
|
public var isPlaying: Bool { processes.contains { $0.isPlaying } }
|
|
|
|
public static func group(_ processes: [AudioProcess]) -> [AudioProcessGroup] {
|
|
let byApp = Dictionary(grouping: processes) {
|
|
ProcessNaming.parentBundleID(for: $0.bundleID)
|
|
}
|
|
|
|
return byApp.map { bundleID, members in
|
|
// Der Name kommt vom Programm selbst, wenn es dabei ist. Sonst vom
|
|
// Helfer — dann ist er alles, was da ist.
|
|
let name = members.first { $0.bundleID == bundleID }?.name
|
|
?? ProcessNaming.fallbackName(for: bundleID)
|
|
return AudioProcessGroup(bundleID: bundleID, name: name,
|
|
processes: members.sorted { $0.pid < $1.pid })
|
|
}
|
|
// Nach Namen sortiert: Core Audio sagt keine Reihenfolge zu, und ohne
|
|
// feste Sortierung springen die Zeilen bei jeder Messung umher.
|
|
.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
|
|
}
|
|
}
|