// // MPMediaCenterManager.swift // MusicPlayer // // Created by Mr.Zhou on 2024/4/11. // import Foundation import AVFoundation import MediaPlayer ///播放器播放事件类型 enum MPSideA_PlayerPlayActionType:Int { ///正常播放 case Normal = 0 ///倒计时播放(用户添加倒计时) case CountTime = 1 } ///播放器播放状态 enum MPSideA_PlayerStateType:Int { ///未播放 case Null = 0 ///播放中 case Playing = 1 ///暂停 case Pause = 2 } ///倒计时状态 enum MPSideA_TimerType:Int { ///未启动 case UnActivity = 0 ///运行中 case Playing = 1 ///暂停中 case Suspend = 2 } ///倒计时规格等级 enum MPSideA_CountTimerLevel:Int, CaseIterable { case OFF = 0 case _10 = 1 case _20 = 2 case _30 = 3 case _60 = 4 case _90 = 5 ///标题 var title:String{ switch self { case .OFF: return "OFF" case ._10: return "10" case ._20: return "20" case ._30: return "30" case ._60: return "60" case ._90: return "90" } } ///实际数值(分) var mins:Int{ switch self { case .OFF: return 0 case ._10: return 10 case ._20: return 20 case ._30: return 30 case ._60: return 60 case ._90: return 90 } } } ///多媒体控制器(播放控制器,倒计时控制器,麦克风管理器) class MPSideA_MediaCenterManager { ///控制器单例 static let shared = MPSideA_MediaCenterManager() //MARK: - 各项工具 //音乐播放器 private var player:AVPlayer? //远程控制中心 private var center:MPRemoteCommandCenter? //GCD倒计时器 private var countTimer: DispatchSourceTimer? //监听器(需要获取麦克风权限),通过监听器实时获取周遭分贝 private var monitor:AVAudioRecorder? //监听器设置参数字典 private var monitorSetingsDic:[String : Any]? //GCD监听计时器 private var monitorTimer: DispatchSourceTimer? //MARK: - 实体与状态值 ///音乐实体 private var music:MPSideA_MusicModel? ///获取音乐实体 func getMusic() -> MPSideA_MusicModel?{ return music } ///更改音乐实体 func setMusic(_ music:MPSideA_MusicModel?) { self.music = music } ///播放器播放方法 private var playActionType:MPSideA_PlayerPlayActionType = .Normal ///播放器播放状态(默认未播放) private var playerState:MPSideA_PlayerStateType = .Null ///获取播放器播放状态 func getPlayerState() -> MPSideA_PlayerStateType { return playerState } ///倒计时运行状态(默认未启动) private var countTimeType:MPSideA_TimerType = .UnActivity ///倒计时规格 (默认为OFF) private var countTimerLevel:MPSideA_CountTimerLevel = .OFF ///获取倒计时规格值 func getCountTimerLevel() -> MPSideA_CountTimerLevel { return countTimerLevel } ///监听器计时状态 private var monitorTimerType:MPSideA_TimerType = .UnActivity ///监听器是否活跃 private var isMonitorActivity:Bool = false //隐藏管理器初始化方法 private init() { //初始化时获取上次运行app时存入的最后一首音乐(保存的内容为音乐实体的identifier,具备唯一性) let lastTitle = UserDefaults.standard.string(forKey: "Last") ?? "" let last = MPSideA_MusicModel.fetch(.init(format: "identifier==%@", lastTitle)).first //更新音乐实体(可选性) music = last // 添加观察者,监听播放结束事件 NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying), name: .AVPlayerItemDidPlayToEndTime, object: player?.currentItem) //初始化监听器字典并添加设置参数 monitorSetingsDic = [ //音频格式 AVFormatIDKey:NSNumber(value: kAudioFormatLinearPCM), //录音的声道数,立体声为双声道 AVNumberOfChannelsKey: 2, //录音质量 AVEncoderAudioQualityKey : AVAudioQuality.max.rawValue, //采样位数 AVLinearPCMBitDepthKey:NSNumber(value:16), AVEncoderBitRateKey : 320000, //录音器每秒采集的录音样本数 AVSampleRateKey : 44100.0 ] } //管理器销毁时 deinit { //解除监听 NotificationCenter.default.removeObserver(self) //释放播放器 player = nil //释放倒计时器 countTimer?.cancel() countTimer = nil //释放监听器 monitor = nil monitorTimer?.cancel() monitorTimer = nil } //MARK: - 播放器与倒计时器 /// 启动播放器 /// - Parameters: /// - music: 音乐实体 /// - actionType: 播放方法(正常播放/倒计时播发) /// - countLevel: 如果倒计时播放则填入倒计时规格 func playerStart(_ music:MPSideA_MusicModel, actionType:MPSideA_PlayerPlayActionType, countLevel: MPSideA_CountTimerLevel = .OFF) { //检索倒计时器状态 switch countTimeType { case .UnActivity://未启动计时器 break case .Playing://计时器运行中 if countTimer != nil { //倒计时器具备实例,处于rsume中,可以直接销毁 countTimer!.cancel() countTimer = nil } case .Suspend://计时器暂停中,销毁计时器 if countTimer != nil { //倒计时器具备实例,处于suspend中,可以直接销毁 countTimer!.resume() countTimer!.cancel() countTimer = nil } } //检索当前音乐播放器状态 switch playerState { case .Null://未播放,跳过 break case .Playing://播放中 //销毁播放器 if player != nil { player!.pause() player = nil } //发送停止音乐播放通知 NotificationCenter.notificationKey.post(notificationName: .sideA_stop_music) case .Pause://暂停中 //销毁播放器 if player != nil { player = nil } //发送停止音乐播放通知 NotificationCenter.notificationKey.post(notificationName: .sideA_stop_music) } //更新播放方法 playActionType = actionType //检索播放方法 switch playActionType { case .Normal://正常播放 playMusic(music) case .CountTime://倒计时播放 //更新倒计时规格 countTimerLevel = countLevel countTimerStart(Double(countLevel.mins*60), music: music) } } //倒计时播放时触发 private func countTimerStart(_ totalTimes:TimeInterval, music:MPSideA_MusicModel) { // //检索倒计时器状态 // switch countTimeType { // case .UnActivity://未启动计时器 // break // case .Playing://计时器运行中 // if countTimer != nil { // //倒计时器具备实例,处于rsume中,可以直接销毁 // countTimer!.cancel() // countTimer = nil // } // case .Suspend://计时器暂停中,销毁计时器 // if countTimer != nil { // //倒计时器具备实例,处于suspend中,可以直接销毁 // countTimer!.resume() // countTimer!.cancel() // countTimer = nil // } // } //创建倒计时器队列 let queue = DispatchQueue(label: "com.MPCountTimer.queue") //创建倒计时器 countTimer = DispatchSource.makeTimerSource(queue: queue) //设置计时器的起始时间以及触发事件频率为一秒一次 countTimer!.schedule(deadline: .now(), repeating: .seconds(1)) //将总时间值赋予times var times = totalTimes //计时器设置触发事件 countTimer!.setEventHandler(handler: { [weak self] in //判断时间值是否归零 if times > 0 { //未归0,倒计时-1 times -= 1 //打印倒计时 print(setTimesToMinSeconds(times)) //发布时间值变化通知 NotificationCenter.notificationKey.post(notificationName: .sideA_time_times, object: times) }else { //执行结束事件 self?.playerStop() } }) //启动倒计时器 countTimeType = .Playing countTimer!.resume() print("The CountTimer has started.") //调用音乐播放器 playMusic(music) } //启动播放器 private func playMusic(_ music:MPSideA_MusicModel) { //根据音乐实体类型 self.music = music player_play(self.music!.isLocal ? musicLocal():musicDocument()) } //音乐为本地文件 private func musicLocal() -> URL { //本地文件,mp3格式 let url:URL = .init(fileURLWithPath: Bundle.main.path(forResource: music!.path, ofType: "mp3")!) return url } //音乐为沙盒文件 private func musicDocument() -> URL { var url:URL! //用户上传文件,文件路径设置为沙盒 let directory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! //获取沙盒中的数据 let vedioUrl = URL(fileURLWithPath:URL(fileURLWithPath: directory).appendingPathComponent((music!.path)).path) //检索路径是否安全授权 let authozied = vedioUrl.startAccessingSecurityScopedResource() if authozied == true { //允许安全访问路径,调出文件协调器解码并获取真实文件地址 let fileCoordinator = NSFileCoordinator() fileCoordinator.coordinate(readingItemAt: vedioUrl, options: .withoutChanges, error: nil) { (newUrl) in url = newUrl } }else { //未能获取安全授权,强制获取 url = vedioUrl } //停止安全访问权限 vedioUrl.stopAccessingSecurityScopedResource() return url } //播放音乐 private func player_play(_ url:URL) { //检索真实路径对应的文件是否存在 if FileManager.default.fileExists(atPath: url.path) == true { //当前路径的文件正式存在 //初始化播放器 let playerItem = AVPlayerItem(url: url) //创建AVPlayer player = AVPlayer(playerItem: playerItem) //正式播放 player?.play() playerState = .Playing //启动远程中心并配置相关内容 setCommandCenter(music!) //发送播放器启动通知 NotificationCenter.notificationKey.post(notificationName: .sideA_play_music) //更新最后一次播放内容 music!.lastTime = Date().timeZone() MPSideA_MusicModel.save() UserDefaults.standard.set(music!.identifier, forKey: "Last") }else { //文件不存在,通知用户删除该音乐实体 print("Couldn't find the file.") playerState = .Null music = nil //发送音乐缺失通知 NotificationCenter.notificationKey.post(notificationName: .sideA_null_music) } } ///暂停播放器 func playerPause() { //检索播放状态,是否进行中 guard playerState == .Playing, let player = player else { //未处于播放中 print("Player is not in playing") return } //处于播放中,检索播放方法 switch playActionType { case .Normal://正常播放 player_pause(player) case .CountTime://倒计时播放 //检索倒计时器状态,是否进行中 guard countTimeType == .Playing, let countTimer = countTimer else { //倒计时器未运行 print("CountTimer is not playing") return } //倒计时运行中,暂停倒计时 countTimer.suspend() countTimeType = .Suspend //暂停播放 player_pause(player) } } //暂停播放 private func player_pause(_ player:AVPlayer) { //暂停播放器 player.pause() //切换播放器状态 playerState = .Pause //发布音乐暂停公告 NotificationCenter.notificationKey.post(notificationName: .sideA_pause_music) } ///继续播放器 func playerResume() { //检索播放状态,是否暂停中 guard playerState == .Pause, let player = player else { //未处于暂停中 print("Player is not paused") return } //处于暂停中,检索播放方法 switch playActionType { case .Normal://正常播放 player_resume(player) case .CountTime://倒计时播放 //检索倒计时器状态,是否暂停中 guard countTimeType == .Suspend, let countTimer = countTimer else { //倒计时器未暂停 print("CountTimer is not paused") return } //倒计时暂停中,运行倒计时 countTimer.resume() countTimeType = .Playing //继续播放 player_resume(player) } } //继续播放 private func player_resume(_ player:AVPlayer) { //继续播放器 player.play() //切换播放器状态 playerState = .Playing //发布音乐继续公告 NotificationCenter.notificationKey.post(notificationName: .sideA_resume_music) } ///停止播放器 func playerStop() { //检索播放状态,是否已启动 guard playerState != .Null, let player = player else { //未启动 print("Player is not started") return } //处于启动中,检索播放方法 switch playActionType { case .Normal://正常播放 player_stop(player) case .CountTime://倒计时播放 //检索倒计时器状态,是否已启动 guard countTimeType != .UnActivity, let countTimer = countTimer else { //倒计时器未启动 print("CountTimer is not started") return } //倒计时器已启动,终止倒计时 if countTimeType == .Suspend { countTimer.resume() } countTimer.cancel() self.countTimer = nil //当计时器终止后,将计时等级回正为off countTimerLevel = .OFF //停止音乐播放 player_stop(player) } } //停止播放 private func player_stop(_ player:AVPlayer) { player.pause() self.player = nil playerState = .Null //回正播放类型 playActionType = .Normal //发送停止音乐播放通知 NotificationCenter.notificationKey.post(notificationName: .sideA_stop_music) } //播放器停止 @objc private func playerDidFinishPlaying() { guard playerState == .Playing, let player = player else { return } // 重置播放时间到开始 player.seek(to: CMTime.zero) // 继续播放 player.play() } //MARK: - 远程中心 private func setCommandCenter(_ music:MPSideA_MusicModel) { // 实例化远程控制中心 center = MPRemoteCommandCenter.shared() //设置控制中心各项操作 //播放 center!.playCommand.addTarget(handler: { [weak self] (event) in guard let self = self else { return .noActionableNowPlayingItem} if self.music != nil { return .success }else { return .noActionableNowPlayingItem } }) //暂停 center!.pauseCommand.addTarget(handler: { [weak self] (event) in guard let self = self else { return .noActionableNowPlayingItem} if self.music != nil { return .success }else { return .noActionableNowPlayingItem } }) //设置info字典信息 var info = [String:Any]() //展示标题 info[MPMediaItemPropertyTitle] = music.title ?? "" //设置艺术家 // info[MPMediaItemPropertyArtist] = "" //设置专辑 // info[MPMediaItemPropertyAlbumTitle] = "" //设置歌曲时长 // info[MPMediaItemPropertyPlaybackDuration] = 0 //设置歌曲封面 if let image = UIImage(data: music.cover) { info[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { size in return image }) } //更新远程中心 MPNowPlayingInfoCenter.default().nowPlayingInfo = info } //MARK: - 麦克风监听器 ///开启监听器 func openMonitor(_ decibels:Double) { //优先检索倒计时器和播放器状态 if countTimeType != .UnActivity || playerState != .Null { //有一项处于启动中,终止该事件 playerStop() } //检索监听器计时状态 switch monitorTimerType { case .UnActivity://未启动监听器计时 break case .Playing://监听器计时运行中 if monitorTimer != nil { //倒计时器具备实例,处于rsume中,可以直接销毁 monitorTimer!.cancel() monitorTimer = nil } case .Suspend://监听器计时暂停中 if monitorTimer != nil { //倒计时器具备实例,处于suspend中,可以直接销毁 monitorTimer!.resume() monitorTimer!.cancel() monitorTimer = nil } } //创建计时器队列 let queue = DispatchQueue(label: "com.MPMonitorTimer.queue") //实例化监听器计时 monitorTimer = DispatchSource.makeTimerSource(queue: queue) //设置计时器的起始时间以及触发事件频率为一秒一次 monitorTimer!.schedule(deadline: .now(), repeating: .seconds(1)) //计时器设置触发事件 monitorTimer!.setEventHandler(handler: { [weak self] in guard let self = self else { return } //每秒触发一次监听器,获取当前麦克风分贝值 let currentDecibels = checkDecibels() //根据分贝值大小,判断是否启动音乐播放器 if currentDecibels > decibels { guard let music = self.music else { return } //启动十分钟计时器 playerStart(music, actionType: .CountTime, countLevel: ._10) //停止监听器 stopMonitor() } }) //先删除监听器缓存内容,并释放监听器 monitor?.deleteRecording() monitor = nil var url:URL? let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/play" //生成空路径 if #available(iOS 16.0, *) { url = URL(filePath: path) } else { url = URL(fileURLWithPath: path) } //重新实例化监听器 do { //初始化监听器 monitor = try AVAudioRecorder(url: url!, settings: monitorSetingsDic!) //开启仪表计数功能 monitor!.isMeteringEnabled = true //准备监听 monitor!.prepareToRecord() //开始监听 monitor!.record() //启动监听器计时 monitorTimerType = .Playing isMonitorActivity = true monitorTimer!.resume() //发送开启监听通知 NotificationCenter.notificationKey.post(notificationName: .sideA_open_monitor) print("The monitor has open.") } catch let error { print("Monitor initialization failure:\(error.localizedDescription)") } } ///终止监听器运行 func stopMonitor() { guard let timer = monitorTimer, let monitor = monitor else { return } //停止并销毁计时器和监听器 if monitorTimerType == .Suspend { timer.resume() } timer.cancel() self.monitorTimer = nil monitorTimerType = .UnActivity isMonitorActivity = false monitor.stop() self.monitor = nil //发送关闭监听通知 NotificationCenter.notificationKey.post(notificationName: .sideA_stop_monitor) print("The monitor has stoped") } //检测麦克风音量 private func checkDecibels() -> Double{ // 刷新音量数据 monitor!.updateMeters() let power = monitor!.peakPower(forChannel: 0) //获取分贝 基本在0-1之间 可能超过1 let decibels:Double = pow(Double(10), Double(0.05*power)) print("Current decibels: \(decibels)") return decibels } }