From 5b71feebfdbc49f3dd6b6c2211e1d1cefbfb7f57 Mon Sep 17 00:00:00 2001 From: Dallas Groot Date: Fri, 10 Apr 2026 17:17:12 -0700 Subject: [PATCH] bug fixes --- iOS/Views/NowPlaying/NowPlayingView.swift | 77 +++++++++-------------- 1 file changed, 30 insertions(+), 47 deletions(-) diff --git a/iOS/Views/NowPlaying/NowPlayingView.swift b/iOS/Views/NowPlaying/NowPlayingView.swift index 97d64c6..749ba88 100644 --- a/iOS/Views/NowPlaying/NowPlayingView.swift +++ b/iOS/Views/NowPlaying/NowPlayingView.swift @@ -1566,15 +1566,15 @@ class ShazamRecognizer: NSObject, ObservableObject, SHSessionDelegate { private var timeoutTask: Task? private var hasDeliveredResult = false - // Tap state - private var tap: Unmanaged? - private var converter: AVAudioConverter? - private var sourceFormat: AVAudioFormat? - private let targetFormat = AVAudioFormat(standardFormatWithSampleRate: 16_000, channels: 1)! - private let analysisQueue = DispatchQueue(label: "com.navidrome.shazam", qos: .userInitiated) + // Tap state — MTAudioProcessingTap is a CF type, ARC manages it directly + private var tap: MTAudioProcessingTap? + private var converter: AVAudioConverter? + private var sourceFormat: AVAudioFormat? + private let targetFormat = AVAudioFormat(standardFormatWithSampleRate: 16_000, channels: 1)! + private let analysisQueue = DispatchQueue(label: "com.navidrome.shazam", qos: .userInitiated) // Legacy mic fallback - private var audioEngine: AVAudioEngine? + private var audioEngine: AVAudioEngine? private override init() { super.init() } @@ -1594,44 +1594,29 @@ class ShazamRecognizer: NSObject, ObservableObject, SHSessionDelegate { DebugLogger.shared.log("Shazam: starting recognition", category: "Audio") - // Prefer tap on AVPlayer (radio / stream / local AVPlayer path) + // Prefer tap on AVPlayer — async track load, then install tap on main actor if let playerItem = AudioPlayer.shared.currentPlayerItem { - if installTap(on: playerItem) { - startTimeout(seconds: 15) - DebugLogger.shared.log("Shazam: tap installed on AVPlayerItem", category: "Audio") - return + Task { @MainActor [weak self] in + guard let self else { return } + if await self.installTap(on: playerItem) { + self.startTimeout(seconds: 15) + DebugLogger.shared.log("Shazam: tap installed on AVPlayerItem", category: "Audio") + } else { + self.startMicFallback() + } } + } else { + startMicFallback() } - - // Fallback: microphone for local engine path - startMicFallback() } // MARK: - MTAudioProcessingTap - private func installTap(on playerItem: AVPlayerItem) -> Bool { - // loadTracks is async (tracks(withMediaType:) deprecated iOS 16) - // We load synchronously from the asset's already-loaded track list if available, - // falling back to a blocking load. Since this is called off the main thread in - // a Task, a brief synchronous wait is acceptable. - let asset = playerItem.asset - var audioTrack: AVAssetTrack? - - // Fast path: tracks already in memory (common for items that are playing) - let loadedTracks = asset.tracks(withMediaType: .audio) - if let first = loadedTracks.first { - audioTrack = first - } else { - // Async load — bridge with a semaphore so we stay synchronous from the caller's view - let sema = DispatchSemaphore(value: 0) - Task { - audioTrack = try? await asset.loadTracks(withMediaType: .audio).first - sema.signal() - } - sema.wait() - } - - guard let audioTrack else { + /// Async because `loadTracks(withMediaType:)` is the non-deprecated API (iOS 16+). + private func installTap(on playerItem: AVPlayerItem) async -> Bool { + // loadTracks is the non-deprecated replacement for tracks(withMediaType:) + guard let audioTrack = try? await playerItem.asset + .loadTracks(withMediaType: .audio).first else { DebugLogger.shared.log("Shazam: no audio track on playerItem", category: "Audio") return false } @@ -1643,14 +1628,13 @@ class ShazamRecognizer: NSObject, ObservableObject, SHSessionDelegate { tapStorageOut.pointee = clientInfo }, finalize: nil, - prepare: { tap, maxFrames, processingFormat in + prepare: { tap, _, processingFormat in let storage = Unmanaged .fromOpaque(MTAudioProcessingTapGetStorage(tap)) .takeUnretainedValue() let format = AVAudioFormat(streamDescription: processingFormat) storage.analysisQueue.async { storage.sourceFormat = format - // AVAudioConverter.init does not throw — no try? needed if let src = format { storage.converter = AVAudioConverter(from: src, to: storage.targetFormat) } @@ -1660,22 +1644,21 @@ class ShazamRecognizer: NSObject, ObservableObject, SHSessionDelegate { process: shazamTapProcess ) - var rawTap: Unmanaged? + // MTAudioProcessingTap is a CF type — Swift bridges it as MTAudioProcessingTap? + var tapOut: MTAudioProcessingTap? let status = MTAudioProcessingTapCreate( kCFAllocatorDefault, &callbacks, - kMTAudioProcessingTapCreationFlag_PostEffects, &rawTap + kMTAudioProcessingTapCreationFlag_PostEffects, &tapOut ) - guard status == noErr, let rawTap else { + guard status == noErr, let tapValue = tapOut else { DebugLogger.shared.log("Shazam: MTAudioProcessingTapCreate failed \(status)", category: "Audio") return false } - // takeRetainedValue transfers ownership (ARC takes over) — assign to our ivar - let tapValue = rawTap.takeRetainedValue() - tap = Unmanaged.passRetained(tapValue) // re-wrap for cleanup later + tap = tapValue // ARC owns it let inputParams = AVMutableAudioMixInputParameters(track: audioTrack) - inputParams.audioTapProcessor = tapValue // passes the already-retained value + inputParams.audioTapProcessor = tapValue let mix = AVMutableAudioMix() mix.inputParameters = [inputParams]