From cf1acb1986fb2d477ce1e45d114b293b38e5a592 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sat, 10 Oct 2026 23:58:03 +0200 Subject: [PATCH] Apply volume and mute on the avfoundation renderer directly mpv's volume/mute properties are software volume applied before samples reach the AO, and AVSampleBufferAudioRenderer queues ~2s ahead, so slider and mute changes were heard only after the queue drained. With the avfoundation AO active, drive ao-volume/ao-mute (renderer volume, instant) and keep software volume neutral; other AOs keep software volume. Re-apply on every audio reconfig since a new renderer starts at full volume. --- Yattee/Services/Player/MPV/MPVClient.swift | 55 +++++++++++++++++++++- Yattee/Services/Player/MPVBackend.swift | 14 +----- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/Yattee/Services/Player/MPV/MPVClient.swift b/Yattee/Services/Player/MPV/MPVClient.swift index a01b9a22..24456102 100644 --- a/Yattee/Services/Player/MPV/MPVClient.swift +++ b/Yattee/Services/Player/MPV/MPVClient.swift @@ -628,8 +628,6 @@ final class MPVClient: @unchecked Sendable { observeProperty("demuxer-cache-time", format: MPV_FORMAT_DOUBLE) observeProperty("demuxer-cache-state", format: MPV_FORMAT_NODE) observeProperty("speed", format: MPV_FORMAT_DOUBLE) - observeProperty("volume", format: MPV_FORMAT_DOUBLE) - observeProperty("mute", format: MPV_FORMAT_FLAG) observeProperty("core-idle", format: MPV_FORMAT_FLAG) observeProperty("seeking", format: MPV_FORMAT_FLAG) // Cache-related properties for debugging playback start issues @@ -891,6 +889,54 @@ final class MPVClient: @unchecked Sendable { } } + /// Desired volume (0–100) and mute state; only touched on mpvQueue. + private var desiredVolume: Double = 100 + private var desiredMute = false + + /// Set the playback volume (0–100) and mute state. + /// + /// mpv's `volume`/`mute` properties are software volume applied before + /// samples reach the AO, so with avfoundation (AVSampleBufferAudioRenderer + /// queues ~2s ahead) changes are heard only after the queue drains. When + /// avfoundation is active, drive the renderer via `ao-volume`/`ao-mute` + /// instead (instant) and keep software volume neutral. Other AOs + /// (audiounit/coreaudio) keep using software volume. + func setVolume(_ volume: Double, muted: Bool) { + mpvQueue.async { [weak self] in + guard let self else { return } + self.desiredVolume = volume + self.desiredMute = muted + self.applyVolumeUnsafe() + } + } + + /// Must be called on mpvQueue. Re-run on every audio reconfig, since a new + /// AO instance (new renderer) starts at full volume. + private func applyVolumeUnsafe() { + guard let mpv, !isDestroyed else { return } + + var currentAo: String? + if let cString = mpv_get_property_string(mpv, "current-ao") { + currentAo = String(cString: cString) + mpv_free(cString) + } + + var softVolume = desiredVolume + var softMute: Int32 = desiredMute ? 1 : 0 + if currentAo == "avfoundation" { + var aoVolume = desiredVolume + var aoMute: Int32 = desiredMute ? 1 : 0 + let volumeResult = mpv_set_property(mpv, "ao-volume", MPV_FORMAT_DOUBLE, &aoVolume) + let muteResult = mpv_set_property(mpv, "ao-mute", MPV_FORMAT_FLAG, &aoMute) + if volumeResult >= 0 && muteResult >= 0 { + softVolume = 100 + softMute = 0 + } + } + mpv_set_property(mpv, "volume", MPV_FORMAT_DOUBLE, &softVolume) + mpv_set_property(mpv, "mute", MPV_FORMAT_FLAG, &softMute) + } + /// Internal property setter (must be called on mpvQueue). private func setPropertyUnsafe(_ name: String, _ value: Any) { guard let mpv, !isDestroyed else { return } @@ -1452,6 +1498,11 @@ final class MPVClient: @unchecked Sendable { } } + case MPV_EVENT_AUDIO_RECONFIG: + mpvQueue.async { [weak self] in + self?.applyVolumeUnsafe() + } + case MPV_EVENT_LOG_MESSAGE: if let data = event.data { let msg = data.assumingMemoryBound(to: mpv_event_log_message.self).pointee diff --git a/Yattee/Services/Player/MPVBackend.swift b/Yattee/Services/Player/MPVBackend.swift index c46209f5..936e1445 100644 --- a/Yattee/Services/Player/MPVBackend.swift +++ b/Yattee/Services/Player/MPVBackend.swift @@ -48,7 +48,7 @@ final class MPVBackend: PlayerBackend { get { _volume } set { _volume = newValue - mpvClient?.setProperty("volume", Double(newValue * 100)) + mpvClient?.setVolume(Double(newValue * 100), muted: _isMuted) } } @@ -56,7 +56,7 @@ final class MPVBackend: PlayerBackend { get { _isMuted } set { _isMuted = newValue - mpvClient?.setProperty("mute", newValue) + mpvClient?.setVolume(Double(_volume * 100), muted: newValue) } } @@ -1566,16 +1566,6 @@ extension MPVBackend: MPVClientDelegate { _rate = Float(speed) } - case "volume": - if let vol = value as? Double { - _volume = Float(vol / 100) - } - - case "mute": - if let muted = value as? Bool { - _isMuted = muted - } - case "core-idle": // core-idle indicates MPV is processing, but doesn't mean frames are ready break