ExtAudioFileOpenURL 和 AVAudioFile 无法读取 .aac 文件

问题描述 投票:0回答:1

我目前正在制作一个快速应用程序,它从 audiostream 记录 aac 文件,并使用 shazamKit 来识别歌曲。流本身以 aac 格式播放音频,这就是为什么我将其下载为 .aac 文件(不是 .m4a 或 mp3),但 shazamKit 读取 .wav 文件,这需要我制作 .aac 到 .wav 转换器功能.

我的代码的录音部分工作正常,但无论我尝试打开这个 .aac 文件(ExtAudioFileOpenURL 或 AVFile),我都会遇到相同的 4 个错误:

2023-07-22 09:58:55.795711+0900 KDVS[46452:2598649]  ReadBytes Failed
2023-07-22 09:58:55.795818+0900 KDVS[46452:2598649]  AACAudioFile::ParseAudioFile failed
2023-07-22 09:58:55.795911+0900 KDVS[46452:2598649]  OpenFromDataSource failed
2023-07-22 09:58:55.795977+0900 KDVS[46452:2598649]  Open failed
2023-07-22 09:58:55.796073+0900 KDVS[46452:2598649] [default]          ExtAudioFile.cpp:210  

我认为文件可能已损坏,但是当我在任何媒体播放器中播放 .aac 文件时,它工作得很好。然后我想也许文件 URL 不正确,所以我写了一个打印语句,如果 (fileExists(at: inputURL) 每次都返回 true,则打印 URL 和 T/F。权限也应该没问题,因为文件位于文档库中

“文件:///Users/johncarraher/Library/Developer/CoreSimulator/Devices/273C3EEA-823C-4A15-A67A-7DE5D5463AB5/data/Containers/Data/Application/A86A9BA4-F2EA-4D10-A93A-5C0F58690E8A/Documents/recording .aac”。

我对音频文件编码不太熟悉,所以我不确定从这里开始,但我认为要么我的文件有点损坏,要么大多数“音频文件”阅读器不支持 .aac 文件。我已在记录流的代码中附加了类,以及我的 aactowav 转换器函数。预先感谢任何可以提供帮助的人。

//
//  aactowav.swift
//  KDVS
//
//  Created by John Carraher on 7/21/23.
//

import Foundation
import AVFoundation

func convertAACtoWAV(inputURL: URL, outputURL: URL) {
    var error: OSStatus = noErr

    var destinationFile: ExtAudioFileRef? = nil

    var sourceFile: ExtAudioFileRef? = nil

    var srcFormat: AudioStreamBasicDescription = AudioStreamBasicDescription()
    var dstFormat: AudioStreamBasicDescription = AudioStreamBasicDescription()

    print("6 About to open \(inputURL) which has a status of \(fileExists(at: inputURL)) which looks like this: \(inputURL as CFURL) as a CFURL")
    
    ExtAudioFileOpenURL(inputURL as CFURL, &sourceFile) //**Line where error comes from**
    print("7")

    var thePropertySize: UInt32 = UInt32(MemoryLayout.stride(ofValue: srcFormat))

    ExtAudioFileGetProperty(sourceFile!,
                            kExtAudioFileProperty_FileDataFormat,
                            &thePropertySize, &srcFormat)

    dstFormat.mSampleRate = 44100 // Set sample rate
    dstFormat.mFormatID = kAudioFormatLinearPCM
    dstFormat.mChannelsPerFrame = 1
    dstFormat.mBitsPerChannel = 16
    dstFormat.mBytesPerPacket = 2 * dstFormat.mChannelsPerFrame
    dstFormat.mBytesPerFrame = 2 * dstFormat.mChannelsPerFrame
    dstFormat.mFramesPerPacket = 1
    dstFormat.mFormatFlags = kLinearPCMFormatFlagIsPacked | kAudioFormatFlagIsSignedInteger

    // Create destination file
    error = ExtAudioFileCreateWithURL(
        outputURL as CFURL,
        kAudioFileWAVEType,
        &dstFormat,
        nil,
        AudioFileFlags.eraseFile.rawValue,
        &destinationFile)
    print("Error 1 in convertAACtoWAV: \(error.description)")

    error = ExtAudioFileSetProperty(sourceFile!,
                                    kExtAudioFileProperty_ClientDataFormat,
                                    thePropertySize,
                                    &dstFormat)
    print("Error 2 in convertAACtoWAV: \(error.description)")

    error = ExtAudioFileSetProperty(destinationFile!,
                                    kExtAudioFileProperty_ClientDataFormat,
                                    thePropertySize,
                                    &dstFormat)
    print("Error 3 in convertAACtoWAV: \(error.description)")

    let bufferByteSize: UInt32 = 32768
    var srcBuffer = [UInt8](repeating: 0, count: Int(bufferByteSize))
    var sourceFrameOffset: ULONG = 0

    while true {
        var fillBufList = AudioBufferList(
            mNumberBuffers: 1,
            mBuffers: AudioBuffer(
                mNumberChannels: 2,
                mDataByteSize: bufferByteSize,
                mData: &srcBuffer
            )
        )
        var numFrames: UInt32 = 0

        if dstFormat.mBytesPerFrame > 0 {
            numFrames = bufferByteSize / dstFormat.mBytesPerFrame
        }

        error = ExtAudioFileRead(sourceFile!, &numFrames, &fillBufList)
        print("Error 4 in convertAACtoWAV: \(error.description)")

        if numFrames == 0 {
            error = noErr
            break
        }

        sourceFrameOffset += numFrames
        error = ExtAudioFileWrite(destinationFile!, numFrames, &fillBufList)
        print("Error 5 in convertAACtoWAV: \(error.description)")
    }

    error = ExtAudioFileDispose(destinationFile!)
    print("Error 6 in convertAACtoWAV: \(error.description)")
    error = ExtAudioFileDispose(sourceFile!)
    print("Error 7 in convertAACtoWAV: \(error.description)")
}

func fileExists(at url: URL) -> Bool {
    let fileManager = FileManager.default
    return fileManager.fileExists(atPath: url.path)
}

//
//  CachingPlayerItem.swift
//  KDVS
//
//  Created by John Carraher on 7/20/23.
//

import Foundation
import AVFoundation

fileprivate extension URL {
    
    func withScheme(_ scheme: String) -> URL? {
        var components = URLComponents(url: self, resolvingAgainstBaseURL: false)
        components?.scheme = scheme
        return components?.url
    }
    
}

@objc protocol CachingPlayerItemDelegate {
    
    /// Is called when the media file is fully downloaded.
    @objc optional func playerItem(_ playerItem: CachingPlayerItem, didFinishDownloadingData data: Data)
    
    /// Is called every time a new portion of data is received.
    @objc optional func playerItem(_ playerItem: CachingPlayerItem, didDownloadBytesSoFar bytesDownloaded: Int, outOf bytesExpected: Int)
    
    /// Is called after initial prebuffering is finished, means
    /// we are ready to play.
    @objc optional func playerItemReadyToPlay(_ playerItem: CachingPlayerItem)
    
    /// Is called when the data being downloaded did not arrive in time to
    /// continue playback.
    @objc optional func playerItemPlaybackStalled(_ playerItem: CachingPlayerItem)
    
    /// Is called on downloading error.
    @objc optional func playerItem(_ playerItem: CachingPlayerItem, downloadingFailedWith error: Error)
}

open class CachingPlayerItem: AVPlayerItem {
    
    class ResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate, URLSessionDataDelegate, URLSessionTaskDelegate {
        
        var playingFromData = false
        var mimeType: String? // is required when playing from Data
        var session: URLSession?
        var mediaData: Data?
        var response: URLResponse?
        var pendingRequests = Set<AVAssetResourceLoadingRequest>()
        weak var owner: CachingPlayerItem?
        var fileURL: URL!
        var outputStream: OutputStream?
        
        func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
            
            if playingFromData {
                
                // Nothing to load.
                
            } else if session == nil {
                
                // If we're playing from a url, we need to download the file.
                // We start loading the file on first request only.
                guard let initialUrl = owner?.url else {
                    fatalError("internal inconsistency")
                }

                startDataRequest(with: initialUrl)
            }
            
            pendingRequests.insert(loadingRequest)
            processPendingRequests()
            return true
            
        }
        
        func startDataRequest(with url: URL) {
                
                var recordingName = "record.mp3"
                if let recording = owner?.recordingName{
                    recordingName = recording
                }
                
                //Find Documents Directory (If it don't exist, don't create it)
                fileURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
                    .appendingPathComponent(recordingName)
                
                // Check if the file already exists
                if FileManager.default.fileExists(atPath: fileURL.path) {
                    do {
                        // Clear the contents of the existing file
                        try Data().write(to: fileURL)
                    } catch {
                        print("Failed to clear existing file data: \(error)")
                    }
                }
                
                let configuration = URLSessionConfiguration.default
                configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
                session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
                session?.dataTask(with: url).resume()
                outputStream = OutputStream(url: fileURL, append: true)
                outputStream?.schedule(in: RunLoop.current, forMode: RunLoop.Mode.default)
                outputStream?.open()
                
            }
        
        func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) {
            pendingRequests.remove(loadingRequest)
        }
        
        // MARK: URLSession delegate
        
        func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
            let bytesWritten = data.withUnsafeBytes{outputStream?.write($0, maxLength: data.count)}
        }
        
        func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
            completionHandler(Foundation.URLSession.ResponseDisposition.allow)
            mediaData = Data()
            self.response = response
            processPendingRequests()
        }
        
        func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
                if let errorUnwrapped = error {
                    owner?.delegate?.playerItem?(owner!, downloadingFailedWith: errorUnwrapped)
                    return
                }
            }
        // MARK: -
        
        func processPendingRequests() {
            
            // get all fullfilled requests
            let requestsFulfilled = Set<AVAssetResourceLoadingRequest>(pendingRequests.compactMap {
                self.fillInContentInformationRequest($0.contentInformationRequest)
                if self.haveEnoughDataToFulfillRequest($0.dataRequest!) {
                    $0.finishLoading()
                    return $0
                }
                return nil
            })
        
            // remove fulfilled requests from pending requests
            _ = requestsFulfilled.map { self.pendingRequests.remove($0) }

        }
        
        func fillInContentInformationRequest(_ contentInformationRequest: AVAssetResourceLoadingContentInformationRequest?) {
            if playingFromData {
                contentInformationRequest?.contentType = self.mimeType
                contentInformationRequest?.contentLength = Int64(mediaData!.count)
                contentInformationRequest?.isByteRangeAccessSupported = true
                return
            }
            
            guard let responseUnwrapped = response else {
                // have no response from the server yet
                return
            }
            
            contentInformationRequest?.contentType = responseUnwrapped.mimeType
            contentInformationRequest?.contentLength = responseUnwrapped.expectedContentLength
            contentInformationRequest?.isByteRangeAccessSupported = true
            
        }
        
        func haveEnoughDataToFulfillRequest(_ dataRequest: AVAssetResourceLoadingDataRequest) -> Bool {
            
            let requestedOffset = Int(dataRequest.requestedOffset)
            let requestedLength = dataRequest.requestedLength
            let currentOffset = Int(dataRequest.currentOffset)
            
            guard let songDataUnwrapped = mediaData,
                songDataUnwrapped.count > currentOffset else {
                return false
            }
            
            let bytesToRespond = min(songDataUnwrapped.count - currentOffset, requestedLength)
            let dataToRespond = songDataUnwrapped.subdata(in: Range(uncheckedBounds: (currentOffset, currentOffset + bytesToRespond)))
            dataRequest.respond(with: dataToRespond)
            
            return songDataUnwrapped.count >= requestedLength + requestedOffset
            
        }
        
        deinit {
            session?.invalidateAndCancel()
        }
        
        
    }
    
    fileprivate let resourceLoaderDelegate = ResourceLoaderDelegate()
    fileprivate let url: URL
    fileprivate let initialScheme: String?
    fileprivate var customFileExtension: String?
    
    
    weak var delegate: CachingPlayerItemDelegate?
    
    func stopDownloading(completion: @escaping () -> Void) {
        resourceLoaderDelegate.session?.invalidateAndCancel()
        completion()
    }
    
    // Function to get the URL of the downloaded file
    func getDownloadedFileURL() -> URL? {
        if resourceLoaderDelegate.playingFromData {
            // If playing from Data, return the URL created for fake data
            return resourceLoaderDelegate.fileURL
        } else {
            // If playing from URL, return the URL of the downloaded file
            return try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
                .appendingPathComponent(recordingName)
        }
    }
    
    open func download() {
        if resourceLoaderDelegate.session == nil {
            resourceLoaderDelegate.startDataRequest(with: url)
        }
    }
    
    private let cachingPlayerItemScheme = "cachingPlayerItemScheme"
    var recordingName = "record.mp3"
    /// Is used for playing remote files.
    convenience init(url: URL, recordingName: String) {
        self.init(url: url, customFileExtension: nil, recordingName: recordingName)
    }
    
    /// Override/append custom file extension to URL path.
    /// This is required for the player to work correctly with the intended file type.
    init(url: URL, customFileExtension: String?, recordingName: String) {
        
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
            let scheme = components.scheme,
            var urlWithCustomScheme = url.withScheme(cachingPlayerItemScheme) else {
            fatalError("Urls without a scheme are not supported")
        }
        self.recordingName = recordingName
        self.url = url
        self.initialScheme = scheme
        
        if let ext = customFileExtension {
            urlWithCustomScheme.deletePathExtension()
            urlWithCustomScheme.appendPathExtension(ext)
            self.customFileExtension = ext
        }
        
        let asset = AVURLAsset(url: urlWithCustomScheme)
        asset.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.main)
        super.init(asset: asset, automaticallyLoadedAssetKeys: nil)
        
        resourceLoaderDelegate.owner = self
        
        addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.new, context: nil)
        
        NotificationCenter.default.addObserver(self, selector: #selector(playbackStalledHandler), name:NSNotification.Name.AVPlayerItemPlaybackStalled, object: self)
        
    }
    
    /// Is used for playing from Data.
    init(data: Data, mimeType: String, fileExtension: String) {
        
        guard let fakeUrl = URL(string: cachingPlayerItemScheme + "://whatever/file.\(fileExtension)") else {
            fatalError("internal inconsistency")
        }
        
        self.url = fakeUrl
        self.initialScheme = nil
        
        resourceLoaderDelegate.mediaData = data
        resourceLoaderDelegate.playingFromData = true
        resourceLoaderDelegate.mimeType = mimeType
        
        let asset = AVURLAsset(url: fakeUrl)
        asset.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.main)
        super.init(asset: asset, automaticallyLoadedAssetKeys: nil)
        resourceLoaderDelegate.owner = self
        
        addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.new, context: nil)
        
        NotificationCenter.default.addObserver(self, selector: #selector(playbackStalledHandler), name:NSNotification.Name.AVPlayerItemPlaybackStalled, object: self)
        
    }
    
    // MARK: KVO
    
    override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        delegate?.playerItemReadyToPlay?(self)
    }
    
    // MARK: Notification hanlers
    
    @objc func playbackStalledHandler() {
        delegate?.playerItemPlaybackStalled?(self)
    }

    // MARK: -
    
    override init(asset: AVAsset, automaticallyLoadedAssetKeys: [String]?) {
        fatalError("not implemented")
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
        removeObserver(self, forKeyPath: "status")
        resourceLoaderDelegate.session?.invalidateAndCancel()
    }
}

swift avfoundation wav aac
1个回答
0
投票

您的

mimeType
似乎未正确接收或处理。我会尝试手动设置它。然后你的其余代码就可以工作了。

func fillInContentInformationRequest(_ contentInformationRequest: AVAssetResourceLoadingContentInformationRequest?) {
            if playingFromData {
                contentInformationRequest?.contentType = self.mimeType
                contentInformationRequest?.contentLength = Int64(mediaData!.count)
                contentInformationRequest?.isByteRangeAccessSupported = true
                return
            }
            
            guard let responseUnwrapped = response else {
                // have no response from the server yet
                return
            }
            
            // contentInformationRequest?.contentType = responseUnwrapped.mimeType
print("responseUnwrapped.mimeType:", responseUnwrapped.mimeType)

contentInformationRequest?.contentType = "audio/aac"

contentInformationRequest?.contentLength = responseUnwrapped.expectedContentLength
            contentInformationRequest?.isByteRangeAccessSupported = true
            
        }
© www.soinside.com 2019 - 2024. All rights reserved.