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:
@@ -52,6 +52,38 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"mixer.input.meterOff": {
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Mikrofonpegel ausschalten"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Stop showing the microphone level"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mixer.input.meterOn": {
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Mikrofonpegel anzeigen — bei Bluetooth-Hörern schaltet macOS dabei auf Telefonqualität um"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Show microphone level — with Bluetooth headsets macOS switches to call quality"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"mixer.name": {
|
"mixer.name": {
|
||||||
"localizations": {
|
"localizations": {
|
||||||
"de": {
|
"de": {
|
||||||
|
|||||||
@@ -495,6 +495,9 @@ private struct SystemSection: View {
|
|||||||
selected: system.inputDeviceID,
|
selected: system.inputDeviceID,
|
||||||
volume: system.inputVolume,
|
volume: system.inputVolume,
|
||||||
level: system.levels.input,
|
level: system.levels.input,
|
||||||
|
// Der Mikrofonpegel nur auf Knopfdruck — siehe SystemLevels.
|
||||||
|
metering: MeteringButton(isOn: system.levels.isListeningToInput,
|
||||||
|
toggle: { system.levels.toggleInputMetering() }),
|
||||||
muted: system.inputMuted,
|
muted: system.inputMuted,
|
||||||
adjustable: system.inputIsAdjustable,
|
adjustable: system.inputIsAdjustable,
|
||||||
compact: compact,
|
compact: compact,
|
||||||
@@ -528,6 +531,8 @@ private struct SystemRow: View {
|
|||||||
let selected: AudioObjectID
|
let selected: AudioObjectID
|
||||||
let volume: Double
|
let volume: Double
|
||||||
let level: Double
|
let level: Double
|
||||||
|
/// Nur die Eingabezeile hat einen — die Ausgabe misst ohne Nebenwirkung.
|
||||||
|
var metering: MeteringButton?
|
||||||
let muted: Bool
|
let muted: Bool
|
||||||
let adjustable: Bool
|
let adjustable: Bool
|
||||||
let compact: Bool
|
let compact: Bool
|
||||||
@@ -578,6 +583,7 @@ private struct SystemRow: View {
|
|||||||
if adjustable {
|
if adjustable {
|
||||||
SystemVolumeBar(volume: volume, level: level,
|
SystemVolumeBar(volume: volume, level: level,
|
||||||
isActive: !muted, onChange: onVolume)
|
isActive: !muted, onChange: onVolume)
|
||||||
|
if let metering { metering }
|
||||||
Text("\(Int(volume * 100)) %")
|
Text("\(Int(volume * 100)) %")
|
||||||
.font(Onyx.Font.metricSmall).monospacedDigit()
|
.font(Onyx.Font.metricSmall).monospacedDigit()
|
||||||
.foregroundStyle(Onyx.Color.textTertiary)
|
.foregroundStyle(Onyx.Color.textTertiary)
|
||||||
@@ -598,6 +604,28 @@ private struct SystemRow: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Der Knopf, der den Mikrofonpegel anschaltet.
|
||||||
|
///
|
||||||
|
/// **Ein Knopf und kein Automatismus**, weil ein offener Eingangsstrom bei
|
||||||
|
/// Bluetooth-Hörern nicht folgenlos ist: macOS schaltet sie ins Telefonprofil,
|
||||||
|
/// die Abtastrate halbiert sich und die Wiedergabe wird hörbar lauter. Wer nur
|
||||||
|
/// die Lautstärke regeln will, hat das nicht bestellt.
|
||||||
|
struct MeteringButton: View {
|
||||||
|
let isOn: Bool
|
||||||
|
let toggle: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: toggle) {
|
||||||
|
Image(systemName: isOn ? "waveform.circle.fill" : "waveform.circle")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.frame(width: 14, height: 14)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.foregroundStyle(isOn ? Onyx.Color.accent : Onyx.Color.textTertiary)
|
||||||
|
.help(Text(isOn ? "mixer.input.meterOff" : "mixer.input.meterOn", bundle: .module))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Wie der Programmregler, nur ohne Verstärkung und ohne Pegel: die
|
/// Wie der Programmregler, nur ohne Verstärkung und ohne Pegel: die
|
||||||
/// Systemlautstärke geht von null bis hundert, mehr gibt Core Audio nicht her.
|
/// Systemlautstärke geht von null bis hundert, mehr gibt Core Audio nicht her.
|
||||||
private struct SystemVolumeBar: View {
|
private struct SystemVolumeBar: View {
|
||||||
|
|||||||
@@ -224,9 +224,14 @@ public final class SystemLevels {
|
|||||||
public private(set) var output: Double = 0
|
public private(set) var output: Double = 0
|
||||||
public private(set) var input: Double = 0
|
public private(set) var input: Double = 0
|
||||||
|
|
||||||
/// Ob gerade tatsächlich das Mikrofon gelesen wird — für den Hinweis in der
|
/// Ob gerade tatsächlich das Mikrofon gelesen wird.
|
||||||
/// Oberfläche. Wer den orangen Punkt sieht, soll in Onyx nachlesen können,
|
///
|
||||||
/// warum er da ist.
|
/// **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
|
public private(set) var isListeningToInput = false
|
||||||
|
|
||||||
/// Prozesse, deren Originalton der Mixer bereits stummgeschaltet hat.
|
/// Prozesse, deren Originalton der Mixer bereits stummgeschaltet hat.
|
||||||
@@ -248,7 +253,7 @@ public final class SystemLevels {
|
|||||||
public func addConsumer() {
|
public func addConsumer() {
|
||||||
consumers += 1
|
consumers += 1
|
||||||
guard consumers == 1 else { return }
|
guard consumers == 1 else { return }
|
||||||
rebuild()
|
startOutputMeter()
|
||||||
let timer = Timer(timeInterval: 1.0 / 15, repeats: true) { [weak self] _ in
|
let timer = Timer(timeInterval: 1.0 / 15, repeats: true) { [weak self] _ in
|
||||||
Task { @MainActor in self?.sample() }
|
Task { @MainActor in self?.sample() }
|
||||||
}
|
}
|
||||||
@@ -262,19 +267,34 @@ public final class SystemLevels {
|
|||||||
timer?.invalidate()
|
timer?.invalidate()
|
||||||
timer = nil
|
timer = nil
|
||||||
outputMeter.stop()
|
outputMeter.stop()
|
||||||
inputMeter.stop()
|
// Das Mikrofon geht in jedem Fall mit aus — auch wenn es noch
|
||||||
isListeningToInput = false
|
// eingeschaltet war. Es soll nie länger offen sein als die Ansicht.
|
||||||
|
stopInputMetering()
|
||||||
output = 0
|
output = 0
|
||||||
input = 0
|
input = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private func rebuild() {
|
/// Schaltet die Mikrofonmessung an oder aus.
|
||||||
if let uid = Self.defaultDeviceUID(kAudioHardwarePropertyDefaultOutputDevice) {
|
public func toggleInputMetering() {
|
||||||
outputMeter.start(excluding: mutedProcesses(), outputDeviceUID: uid)
|
if isListeningToInput {
|
||||||
}
|
stopInputMetering()
|
||||||
|
} else {
|
||||||
inputMeter.start(device: Self.defaultDevice(kAudioHardwarePropertyDefaultInputDevice))
|
inputMeter.start(device: Self.defaultDevice(kAudioHardwarePropertyDefaultInputDevice))
|
||||||
isListeningToInput = inputMeter.isRunning
|
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() {
|
private func sample() {
|
||||||
// Ändert sich, welche Programme der Mixer abgreift, muss der Tap neu
|
// Ändert sich, welche Programme der Mixer abgreift, muss der Tap neu
|
||||||
@@ -290,7 +310,7 @@ public final class SystemLevels {
|
|||||||
// Schnell hoch, langsam runter — wie bei den Programmzeilen, damit
|
// Schnell hoch, langsam runter — wie bei den Programmzeilen, damit
|
||||||
// beide Balken dieselbe Sprache sprechen.
|
// beide Balken dieselbe Sprache sprechen.
|
||||||
output = decay(output, toward: Double(outputMeter.peak.take()))
|
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 {
|
private func decay(_ current: Double, toward peak: Double) -> Double {
|
||||||
|
|||||||
Reference in New Issue
Block a user