bug fixes

This commit is contained in:
Dallas Groot 2026-04-10 17:17:12 -07:00
parent ef6124e72e
commit 5b71feebfd

View file

@ -1566,15 +1566,15 @@ class ShazamRecognizer: NSObject, ObservableObject, SHSessionDelegate {
private var timeoutTask: Task<Void, Never>?
private var hasDeliveredResult = false
// Tap state
private var tap: Unmanaged<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)
// 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<ShazamRecognizer>
.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>?
// 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]