Audiomixer zeigt Programme statt Prozesse

Core Audio meldet Prozesse, nicht Programme. Angezeigt wurden deshalb
Kennungen wie „com.apple.WebKit.GPU" und Dutzende Systemdienste, die
theoretisch Ton ausgeben könnten, es aber nie tun.

ProcessNaming fragt erst das laufende Programm, kürzt sonst bekannte
Hilfsprozess-Endungen weg und schlägt das übergeordnete Programm nach —
Chromes Ton kommt aus einem Helfer, angezeigt gehört Chrome. Symbole
gehen denselben Weg, sonst steht neben dem Namen ein Platzhalter.

ProcessVisibility zeigt, was klingt, kürzlich klang oder geregelt wird.
Die Nachlaufzeit von zwei Minuten ist der eigentliche Punkt: ohne sie
verschwindet Spotify beim Pausieren aus der Liste, und man kann die
Lautstärke genau dann nicht mehr einstellen, wenn man es will. Geregelte
Programme bleiben immer — wer etwas leiser gedreht hat, muss es auch
zurückdrehen können.

Die Prozentzahl wandert vom Namen zum Regler. Oben war sie genau die
Breite, die den Namen abgeschnitten hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-08-11 09:54:20 +02:00
parent c71ddb7ce3
commit 28b1b4270e
4 changed files with 265 additions and 24 deletions

View File

@@ -93,6 +93,10 @@ private struct ProcessRow: View {
.foregroundStyle(process.isPlaying ? Onyx.Color.textPrimary
: Onyx.Color.textSecondary)
.lineLimit(1)
// In der Mitte kürzen statt am Ende: bei zwei ähnlichen
// Namen ist das Ende oft das Unterscheidende.
.truncationMode(.middle)
.layoutPriority(1)
if process.isPlaying {
Circle().fill(Onyx.Color.positive).frame(width: 5, height: 5)
@@ -100,11 +104,6 @@ private struct ProcessRow: View {
Spacer(minLength: 0)
if managed {
Text("\(Int(mixer.volume(for: process.bundleID) * 100)) %")
.font(Onyx.Font.metricSmall)
.foregroundStyle(mixer.volume(for: process.bundleID) > 1
? Onyx.Color.warning : Onyx.Color.textTertiary)
Button {
mixer.setMuted(!mixer.isMuted(process.bundleID), for: process.bundleID)
} label: {
@@ -128,30 +127,43 @@ private struct ProcessRow: View {
}
if managed {
Slider(value: Binding(
get: { mixer.volume(for: process.bundleID) },
set: { mixer.setVolume($0, for: process.bundleID) }),
in: 0...Gain.maximumVolume)
.controlSize(.mini)
.disabled(mixer.isMuted(process.bundleID))
.overlay(alignment: .leading) {
// Markierung bei 100 %: darüber wird verstärkt, und
// man soll sehen, wann man diese Grenze überschreitet.
GeometryReader { geometry in
Rectangle()
.fill(Onyx.Color.hairline)
.frame(width: 1, height: 8)
.offset(x: geometry.size.width / Gain.maximumVolume, y: 4)
// Die Prozentzahl steht beim Regler, nicht beim Namen: oben ist
// sie die Breite, die den Namen abschneidet, hier gehört sie
// ohnehin hin.
HStack(spacing: 6) {
Slider(value: Binding(
get: { mixer.volume(for: process.bundleID) },
set: { mixer.setVolume($0, for: process.bundleID) }),
in: 0...Gain.maximumVolume)
.controlSize(.mini)
.disabled(mixer.isMuted(process.bundleID))
.overlay(alignment: .leading) {
// Markierung bei 100 %: darüber wird verstärkt, und
// man soll sehen, wann man diese Grenze überschreitet.
GeometryReader { geometry in
Rectangle()
.fill(Onyx.Color.hairline)
.frame(width: 1, height: 8)
.offset(x: geometry.size.width / Gain.maximumVolume, y: 4)
}
}
}
Text("\(Int(mixer.volume(for: process.bundleID) * 100)) %")
.font(Onyx.Font.metricSmall)
.foregroundStyle(mixer.volume(for: process.bundleID) > 1
? Onyx.Color.warning : Onyx.Color.textTertiary)
// Feste Breite, sonst wandert der Regler beim Ziehen.
.frame(width: 34, alignment: .trailing)
}
}
}
}
@ViewBuilder
private var icon: some View {
if let app = NSRunningApplication(processIdentifier: process.pid),
let image = app.icon {
// Auch Hilfsprozesse bekommen das Symbol ihres Programms sonst steht
// neben Google Chrome" ein leerer Platzhalter.
if let image = ProcessNaming.icon(bundleID: process.bundleID, pid: process.pid) {
Image(nsImage: image).resizable().frame(width: 14, height: 14)
} else {
Image(systemName: "app.dashed")