// // TSBusinessAudioPlayer.swift // AIRingtone // // Created by 100Years on 2025/3/7. // import AVFoundation class TSBusinessAudioPlayer { static let shared = TSBusinessAudioPlayer() enum PlayerState:Equatable { case play case pause case stop case loading(Float) case volume(Float) case currentTime(Double) } private var audioPlayer: TSAudioPlayer? var stateChangedHandle:((PlayerState) -> Void)? var currentTimeChangedHandle:((Double,Double) -> Void)? var currentPlayerState:PlayerState = .stop var duration:Double{ if let audioPlayer = audioPlayer { return audioPlayer.duration } return 0.0 } var isPlaying:Bool{ if let audioPlayer = audioPlayer { return audioPlayer.isPlaying } return false } var isLoading:Bool{ switch currentPlayerState { case .loading(let float): return float < 1.0 ? true : false default: return false } } var currentTime:Double{ if let audioPlayer = audioPlayer { return audioPlayer.currentTime } return 0.0 } /// 跳转到指定时间 /// - Parameter time: 目标时间(秒) func seek(to time: Double) { audioPlayer?.seek(to: time) } var playProgress:Double{ let playProgress = currentTime / duration // dePrint("TSAudioPlayer playProgress = \(playProgress)") return playProgress } //播放器是否可用 var playerUsable:Bool { if let audioPlayer = audioPlayer { return audioPlayer.playerUsable } return false } var currentURLString:String = "" var currentLocalURL:URL? = nil var currentIndexPath:IndexPath? = nil //加载音乐可能 2-3 秒有结果,停止加载后播放. private var isStopPlayingAfterLoading:Bool = false func isPlayURLString(string:String,localURL:URL? = nil,indexPath:IndexPath? = nil) -> Bool { if currentURLString == string { if let currentIndexPath = currentIndexPath, let indexPath = indexPath, indexPath != currentIndexPath { return false }else if let currentLocalURL = currentLocalURL, let localURL = localURL, currentLocalURL != localURL { return false }else{ return true } } return false } func playUrlString(_ urlString:String?,localURL:URL? = nil,loop:Bool = false,indexPath:IndexPath? = nil) { self.stop() if let urlString = urlString { // if self.currentURLString == urlStrin { // self.play() // return // } self.currentURLString = urlString self.currentLocalURL = localURL self.currentIndexPath = indexPath let palyFile:(URL)->Void = { [weak self] url in guard let self = self else { return } debugPrint("TSAudioPlayer 正在播放url:\(currentURLString)") debugPrint("TSAudioPlayer 正在播放path:\(url)") self.audioPlayer = TSAudioPlayer(url: url) self.audioPlayer?.setLoop(loop) if self.audioPlayer?.volume == 0 { setVolume(volume: 1.0) } self.audioPlayer?.currentTimeChanged = { [weak self] currentTime,duration in guard let self = self else { return } currentTimeChangedHandle?(currentTime,duration) changePlayerState(.currentTime(currentTime)) } self.play() dePrint(self.audioPlayer?.duration) self.audioPlayer?.audioPlayerDidFinishHandle = { [weak self] flag in guard let self = self else { return } if flag == true, self.audioPlayer?.isLooping == false{ stop() } } } isStopPlayingAfterLoading = false if let path = self.currentLocalURL,TSFileManagerTool.fileExists(at: path){ palyFile(path) //播放 }else if let path = TSDownloadManager.getRingLocalURL(urlString: urlString) { // if let path = TSCommonTool.getCachedURLString(from: urlString,missingEx: "mp3") { palyFile(path) //播放 }else{ self.changePlayerState(.loading(0.0)) _ = TSDownloadManager.downloadFile(urlString: urlString,missingEx: "mp3") {[weak self] url, error in guard let self = self else { return } self.changePlayerState(.loading(1.0)) if isStopPlayingAfterLoading == true || currentURLString != urlString{ isStopPlayingAfterLoading = false return } if let url = url { palyFile(url) //播放 }else{ //暂停 self.stop() } } // TSCommonTool.downloadAndCacheFile(from: urlString,missingEx: "mp3") { [weak self] path, error in // guard let self = self else { return } // self.changePlayerState(.loading(1.0)) // // if isStopPlayingAfterLoading == true || currentURLString != urlString{ // isStopPlayingAfterLoading = false // return // } // // if let path = path { // palyFile(URL(fileURLWithPath: path)) //播放 // }else{ // //暂停 // self.stop() // } // } } } } func play() { self.audioPlayer?.play() changePlayerState(.play) } func stop() { self.audioPlayer?.currentTimeChanged = nil isStopPlayingAfterLoading = true currentURLString = "" self.audioPlayer?.stop() changePlayerState(.stop) } func pause() { isStopPlayingAfterLoading = true self.audioPlayer?.pause() changePlayerState(.pause) } func setVolume(volume:Float){ self.audioPlayer?.volume = volume // self.audioPlayer?.setVolume(volume) changePlayerState(.volume(volume)) } func changeAudioSwitch()->Float { let volume:Float = self.audioPlayer?.volume == 0.0 ? 1.0 : 0.0 setVolume(volume: volume) return volume } func changePlayerState(_ state:PlayerState){ if case .currentTime(let time) = state {} else { debugPrint("TSAudioPlayer changePlayerState=\(state)") } currentPlayerState = state kExecuteOnMainThread{ self.stateChangedHandle?(state) // NotificationCenter.default.post(name: .kBusinessAudioStateChange, object: nil, userInfo: ["PlayerState": state]) } } deinit { dePrint("TSAudioPlayer TSBusinessAudioPlayer deinit") } } extension TSBusinessAudioPlayer{ struct AudioFileInfo { let sizeInBytes: UInt64? // 文件大小(字节) let durationInSeconds: Double? // 音频时长(秒) // // 计算属性:格式化显示 // var formattedSize: String { // guard let size = sizeInBytes else { return "未知大小" } // let formatter = ByteCountFormatter() // formatter.allowedUnits = [.useBytes, .useKB, .useMB, .useGB] // return formatter.string(fromByteCount: Int64(size)) // } // // var formattedDuration: String { // guard let duration = durationInSeconds else { return "未知时长" } // let formatter = DateComponentsFormatter() // formatter.unitsStyle = .positional // formatter.allowedUnits = [.hour, .minute, .second] // formatter.zeroFormattingBehavior = .pad // return formatter.string(from: duration) ?? "00:00" // } } static func getAudioFileInfo(path: String) -> AudioFileInfo? { // 1. 检查URL有效性 guard let url = URL(string: path) else { print("无效的URL字符串") return nil } // 2. 检查文件是否存在(仅限本地文件) guard FileManager.default.fileExists(atPath: url.path) else { print("文件不存在或不是本地路径") return nil } // 3. 获取文件大小 let fileSize: UInt64? = { do { let attributes = try FileManager.default.attributesOfItem(atPath: url.path) return attributes[.size] as? UInt64 } catch { print("获取文件大小失败: \(error.localizedDescription)") return nil } }() // 4. 获取音频时长 let duration: Double? = { return getAudioDurationWithAudioFile(url: url) // let asset = AVURLAsset(url: url) // let seconds = Double(CMTimeGetSeconds(asset.duration)) // return seconds.isNaN ? nil : seconds }() return AudioFileInfo(sizeInBytes: fileSize, durationInSeconds: duration) } /// 同步获取音频时长(可能阻塞线程!) /// 使用 AudioFile 同步获取音频时长 static func getAudioDurationWithAudioFile(url: URL) -> TimeInterval? { var audioFile: AudioFileID? let status = AudioFileOpenURL(url as CFURL, .readPermission, 0, &audioFile) guard status == noErr, let file = audioFile else { print("⚠️ 打开音频文件失败: \(status)") return nil } // 获取音频时长(单位:秒) var duration: Float64 = 0 var propertySize = UInt32(MemoryLayout.size(ofValue: duration)) let durationStatus = AudioFileGetProperty( file, kAudioFilePropertyEstimatedDuration, &propertySize, &duration ) AudioFileClose(file) return durationStatus == noErr ? duration : nil } }