Das Mikrofon nur noch auf Knopfdruck

Ein offener Eingangsstrom ist bei Bluetooth-Hörern kein stiller Mitleser.
Gemessen an AirPods Pro:

  vorher    48000 Hz, Gerätelautstärke 0.0
  mit Mikro 24000 Hz, Gerätelautstärke 0.6
  danach    48000 Hz, Gerätelautstärke 0.0

macOS schaltet sie ins Telefonprofil: halbe Abtastrate, und der
Lautstärkeregler des Geräts springt. Die Musik wird hörbar lauter und
schlechter — beim Öffnen des Popovers, und beim Schließen wieder zurück.
Dasselbe passiert, wenn am Telefon eine App das Mikrofon greift.

Das darf nicht passieren, weil jemand ein Lautstärkefenster aufmacht. Der
Mikrofonpegel hängt deshalb an einem Knopf in der Eingabezeile, mit dem
Hinweis im Tooltip, was er auslöst. Beim Schließen des Popovers geht er in
jedem Fall wieder aus.

Der Ausgangspegel bleibt automatisch: der läuft über einen Process Tap und
fasst das Gerät nicht an.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-08-18 09:32:19 +02:00
parent 4dee12e4b9
commit 286d886e9e
3 changed files with 92 additions and 12 deletions

View File

@@ -224,9 +224,14 @@ public final class SystemLevels {
public private(set) var output: Double = 0
public private(set) var input: Double = 0
/// Ob gerade tatsächlich das Mikrofon gelesen wird für den Hinweis in der
/// Oberfläche. Wer den orangen Punkt sieht, soll in Onyx nachlesen können,
/// warum er da ist.
/// Ob gerade tatsächlich das Mikrofon gelesen wird.
///
/// **Nur auf Knopfdruck.** Ein Eingangsstrom ist bei Bluetooth-Hörern kein
/// stiller Mitleser: macOS schaltet sie dabei ins Telefonprofil. Gemessen
/// an AirPods Pro fällt die Abtastrate von 48 auf 24 kHz, und der
/// Lautstärkeregler des Geräts springt von 0,0 auf 0,6 die Musik wird
/// hörbar lauter und schlechter. Das darf nicht passieren, weil jemand ein
/// Lautstärkefenster aufmacht.
public private(set) var isListeningToInput = false
/// Prozesse, deren Originalton der Mixer bereits stummgeschaltet hat.
@@ -248,7 +253,7 @@ public final class SystemLevels {
public func addConsumer() {
consumers += 1
guard consumers == 1 else { return }
rebuild()
startOutputMeter()
let timer = Timer(timeInterval: 1.0 / 15, repeats: true) { [weak self] _ in
Task { @MainActor in self?.sample() }
}
@@ -262,18 +267,33 @@ public final class SystemLevels {
timer?.invalidate()
timer = nil
outputMeter.stop()
inputMeter.stop()
isListeningToInput = false
// Das Mikrofon geht in jedem Fall mit aus auch wenn es noch
// eingeschaltet war. Es soll nie länger offen sein als die Ansicht.
stopInputMetering()
output = 0
input = 0
}
private func rebuild() {
if let uid = Self.defaultDeviceUID(kAudioHardwarePropertyDefaultOutputDevice) {
outputMeter.start(excluding: mutedProcesses(), outputDeviceUID: uid)
/// Schaltet die Mikrofonmessung an oder aus.
public func toggleInputMetering() {
if isListeningToInput {
stopInputMetering()
} else {
inputMeter.start(device: Self.defaultDevice(kAudioHardwarePropertyDefaultInputDevice))
isListeningToInput = inputMeter.isRunning
}
inputMeter.start(device: Self.defaultDevice(kAudioHardwarePropertyDefaultInputDevice))
isListeningToInput = inputMeter.isRunning
}
private func stopInputMetering() {
inputMeter.stop()
isListeningToInput = false
input = 0
}
private func startOutputMeter() {
guard let uid = Self.defaultDeviceUID(kAudioHardwarePropertyDefaultOutputDevice)
else { return }
outputMeter.start(excluding: mutedProcesses(), outputDeviceUID: uid)
}
private func sample() {
@@ -290,7 +310,7 @@ public final class SystemLevels {
// Schnell hoch, langsam runter wie bei den Programmzeilen, damit
// beide Balken dieselbe Sprache sprechen.
output = decay(output, toward: Double(outputMeter.peak.take()))
input = decay(input, toward: Double(inputMeter.peak.take()))
input = isListeningToInput ? decay(input, toward: Double(inputMeter.peak.take())) : 0
}
private func decay(_ current: Double, toward peak: Double) -> Double {