├── .gitignore ├── APlay.podspec ├── APlay.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── APlay.xcscheme ├── APlay ├── APlay.h ├── APlay.swift ├── BuildInComponents │ ├── AVAduioSessionWorkaround │ │ ├── AVAudioSession+Workaround.h │ │ └── AVAudioSession+Workaround.m │ ├── AudioDecoder │ │ └── DefaultAudioDecoder.swift │ ├── Configuration │ │ └── Configuration.swift │ ├── Logger │ │ └── InternalLogger.swift │ ├── NowPlaying │ │ └── NowPlayingInfo.swift │ ├── Players │ │ ├── APlayer.swift │ │ └── AUPlayer.swift │ ├── StreamProvider │ │ └── Streamer.swift │ └── TagParser │ │ ├── FlacParser.swift │ │ ├── ID3Parser.swift │ │ └── TagParser+Extension.swift ├── Composer.swift ├── Info.plist ├── Protocols │ ├── AudioDecoderCompatible.swift │ ├── ConfigurationCompatible.swift │ ├── LoggerCompatible.swift │ ├── MetadataPaserCompatible.swift │ ├── PlayerCompatible.swift │ └── StreamProviderCompatible.swift ├── Utils │ ├── APlay+Debug.swift │ ├── APlay+Extensions.swift │ └── APlay+Typealias.swift └── Vendor │ ├── Delegated.swift │ ├── GCDTimer.swift │ ├── PlayList.swift │ ├── RunloopQueue.swift │ └── Uroboros.swift ├── APlayDemo ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── ViewController.swift └── a.m4a ├── ChangeLog.md ├── LICENSE ├── README.md ├── fastlane ├── Fastfile └── README.md └── generate_docs.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | 70 | *.zip 71 | .DS_Store 72 | 73 | docs/ 74 | -------------------------------------------------------------------------------- /APlay.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'APlay' 3 | s.version = '1.2.1' 4 | s.summary = 'A Better(Maybe) iOS Audio Stream & Play Swift Framework.' 5 | s.swift_version = '5.0' 6 | s.description = <<-DESC 7 | A Better(Maybe) iOS Audio Stream & Play Swift Framework 8 | DESC 9 | 10 | s.homepage = 'https://github.com/CodeEagle/APlay' 11 | s.license = { :type => 'MIT', :file => 'LICENSE' } 12 | s.author = { 'CodeEagle' => 'stasura@hotmail.com' } 13 | s.source = { :git => 'https://github.com/CodeEagle/APlay.git', :tag => s.version.to_s } 14 | 15 | s.platform = :ios 16 | s.ios.deployment_target = '8.0' 17 | 18 | s.exclude_files = 'APlay/Info.plist' 19 | s.source_files = 'APlay/**/*' 20 | end 21 | -------------------------------------------------------------------------------- /APlay.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /APlay.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /APlay.xcodeproj/xcshareddata/xcschemes/APlay.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 47 | 48 | 54 | 55 | 56 | 57 | 58 | 59 | 65 | 66 | 72 | 73 | 74 | 75 | 77 | 78 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /APlay/APlay.h: -------------------------------------------------------------------------------- 1 | // 2 | // APlay.h 3 | // APlay 4 | // 5 | // Created by lincoln on 2018/4/23. 6 | // Copyright © 2018年 SelfStudio. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for APlay. 12 | FOUNDATION_EXPORT double APlayVersionNumber; 13 | 14 | //! Project version string for APlay. 15 | FOUNDATION_EXPORT const unsigned char APlayVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | #import "AVAudioSession+Workaround.h" 19 | 20 | -------------------------------------------------------------------------------- /APlay/APlay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APlay.swift 3 | // APlay 4 | // 5 | // Created by lincoln on 2018/5/8. 6 | // Copyright © 2018年 SelfStudio. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AVFoundation 11 | #if canImport(UIKit) 12 | import UIKit 13 | #endif 14 | 15 | /// A public class for control audio playback 16 | public final class APlay { 17 | /// Current framework version 18 | public static var version: String = "0.0.4" 19 | 20 | /// Loop pattern for playback list 21 | public var loopPattern: PlayList.LoopPattern { 22 | get { return playlist.loopPattern } 23 | set { 24 | playlist.loopPattern = newValue 25 | eventPipeline.call(.playModeChanged(newValue)) 26 | } 27 | } 28 | 29 | /// Event callback for audio playback 30 | public private(set) var eventPipeline = Delegated() 31 | /// Metadatas for current audio 32 | public private(set) lazy var metadatas: [MetadataParser.Item] = [] 33 | /// Player Configuration 34 | public let config: ConfigurationCompatible 35 | 36 | private let _player: PlayerCompatible 37 | private let _nowPlayingInfo: NowPlayingInfo 38 | 39 | private var _state: State = .idle 40 | private var _playlist: PlayList 41 | private var _propertiesQueue = DispatchQueue(concurrentName: "APlay.properties") 42 | 43 | private lazy var __isSteamerEndEncounted = false 44 | private lazy var __isDecoderEndEncounted = false 45 | private lazy var __isCalledDelayPaused = false 46 | private lazy var __isFlagReseted = false 47 | private lazy var __lastDelta: Float = -1 48 | private lazy var __lastDeltaHitCount: Int = 0 49 | private var __currentComposer: Composer? 50 | private let _maxOpenRestry = 5 51 | private lazy var _currentOpenRestry = 0 52 | 53 | private lazy var _obs: [NSObjectProtocol] = [] 54 | 55 | deinit { 56 | destroy() 57 | _obs.forEach({ NotificationCenter.default.removeObserver($0) }) 58 | config.endBackgroundTask(isToDownloadImage: false) 59 | debug_log("\(self) \(#function)") 60 | } 61 | 62 | public init(configuration: ConfigurationCompatible = Configuration()) { 63 | config = configuration 64 | 65 | if #available(iOS 11.0, *) { 66 | _player = APlayer(config: config) 67 | } else { 68 | _player = AUPlayer(config: config) 69 | } 70 | 71 | _playlist = PlayList(pipeline: eventPipeline) 72 | 73 | _nowPlayingInfo = NowPlayingInfo(config: config) 74 | 75 | addInteruptOb() 76 | 77 | _player.eventPipeline.delegate(to: self) { obj, event in 78 | switch event { 79 | case let .state(state): 80 | let stateValue: State 81 | switch state { 82 | case .idle: stateValue = .idle 83 | case .running: 84 | stateValue = .playing 85 | obj._currentOpenRestry = 0 86 | case .paused: stateValue = .paused 87 | } 88 | obj.state = stateValue 89 | obj.eventPipeline.call(.state(stateValue)) 90 | case let .playback(time): 91 | // synchronize playback time for first time since reset 92 | if obj._isFlagReseted { 93 | obj._isFlagReseted = false 94 | obj._nowPlayingInfo.play(elapsedPlayback: time) 95 | debug_log("NowPlayingInfo:\(obj._nowPlayingInfo.info)") 96 | } 97 | obj.eventPipeline.call(.playback(time)) 98 | case let .error(error): 99 | if case let APlay.Error.open(value) = error, obj._currentOpenRestry < obj._maxOpenRestry { 100 | obj._currentOpenRestry += 1 101 | obj.seek(to: 0) 102 | debug_log("reopen by using seek, \(value)") 103 | return 104 | } 105 | let state = APlay.State.error(error) 106 | obj.state = state 107 | obj.eventPipeline.call(.state(state)) 108 | case let .unknown(error): 109 | let state = APlay.State.unknown(error) 110 | obj.state = state 111 | obj.eventPipeline.call(.state(state)) 112 | } 113 | } 114 | } 115 | } 116 | 117 | // MARK: - Public API 118 | 119 | public extension APlay { 120 | /// play with a autoclosure 121 | /// 122 | /// - Parameter url: a autoclosure to produce URL 123 | func play(_ url: @autoclosure () -> URL) { 124 | let u = url() 125 | let urls = [u] 126 | playlist.changeList(to: urls, at: 0) 127 | _play(u) 128 | } 129 | 130 | /// play whit variable parametric 131 | /// 132 | /// - Parameter urls: variable parametric URL input 133 | @inline(__always) 134 | func play(_ urls: URL..., at index: Int = 0) { play(urls, at: index) } 135 | 136 | /// play whit URL array 137 | /// 138 | /// - Parameter urls: URL array 139 | func play(_ urls: [URL], at index: Int = 0) { 140 | playlist.changeList(to: urls, at: index) 141 | guard let url = playlist.currentList[ap_safe: index] else { 142 | let msg = "Can not found item at \(index) in list \(urls)" 143 | eventPipeline.call(.error(.playItemNotFound(msg))) 144 | return 145 | } 146 | _play(url) 147 | } 148 | 149 | func play(at index: Int) { 150 | guard let url = playlist.play(at: index) else { 151 | let msg = "Can not found item at \(index) in list \(playlist.list)" 152 | eventPipeline.call(.error(.playItemNotFound(msg))) 153 | return 154 | } 155 | _play(url) 156 | } 157 | 158 | /// toggle play/pause for player 159 | func toggle() { 160 | _player.toggle() 161 | switch _player.state { 162 | case .running: _state = .playing 163 | case .paused: _state = .paused 164 | case .idle: _state = .idle 165 | } 166 | } 167 | 168 | /// resume playback 169 | func resume() { 170 | _player.resume() 171 | _nowPlayingInfo.play(elapsedPlayback: _player.currentTime()) 172 | } 173 | 174 | /// pause playback 175 | func pause() { 176 | _player.pause() 177 | _nowPlayingInfo.pause(elapsedPlayback: _player.currentTime()) 178 | } 179 | 180 | /// Seek to specific time 181 | /// 182 | /// - Parameter time: TimeInterval 183 | func seek(to time: TimeInterval) { 184 | resetFlag(clearNowPlayingInfo: false) 185 | guard let current = _currentComposer else { return } 186 | var maybeTime = time 187 | let p = current.position(for: &maybeTime) 188 | current.destroy() 189 | let com = createComposer() 190 | _player.startTime = Float(maybeTime) 191 | com.play(current.url, position: p, info: current.streamInfo) 192 | _currentComposer = com 193 | _nowPlayingInfo.play(elapsedPlayback: Float(maybeTime)) 194 | eventPipeline.call(.duration(_nowPlayingInfo.duration)) 195 | } 196 | 197 | /// play next song in list 198 | func next() { 199 | guard let url = playlist.nextURL() else { return } 200 | _play(url) 201 | indexChanged() 202 | } 203 | 204 | /// play previous song in list 205 | func previous() { 206 | guard let url = playlist.previousURL() else { return } 207 | _play(url) 208 | indexChanged() 209 | } 210 | 211 | /// destroy player 212 | func destroy() { 213 | _currentComposer?.destroy() 214 | _player.destroy() 215 | } 216 | 217 | /// whether current song support seek 218 | func seekable() -> Bool { 219 | return _currentComposer?.seekable() ?? false 220 | } 221 | 222 | func metadataUpdate(title: String? = nil, album: String? = nil, artist: String? = nil, cover: UIImage? = nil) { 223 | if let value = title { _nowPlayingInfo.name = value } 224 | if let value = artist { _nowPlayingInfo.artist = value } 225 | if let value = album { _nowPlayingInfo.album = value } 226 | if let value = cover { _nowPlayingInfo.artwork = value } 227 | _nowPlayingInfo.update() 228 | } 229 | } 230 | 231 | // MARK: - Private Utils 232 | 233 | private extension APlay { 234 | 235 | // MARK: Playback 236 | 237 | func _play(_ url: URL) { 238 | resetFlag() 239 | _currentComposer?.destroy() 240 | let com = createComposer() 241 | com.play(url) 242 | _currentComposer = com 243 | _nowPlayingInfo.play(elapsedPlayback: 0) 244 | } 245 | 246 | func resetFlag(clearNowPlayingInfo: Bool = true) { 247 | _isSteamerEndEncounted = false 248 | _isDecoderEndEncounted = false 249 | _lastDelta = -1 250 | _lastDeltaHitCount = 0 251 | _player.startTime = 0 252 | _isFlagReseted = true 253 | config.logger.reset() 254 | if clearNowPlayingInfo { _nowPlayingInfo.remove() } 255 | } 256 | 257 | func checkPlayEnded() { 258 | if _isSteamerEndEncounted == false { 259 | // bad network condition, show waiting 260 | eventPipeline.call(.waitForStreaming) 261 | return 262 | } 263 | guard let dur = _currentComposer?.duration else { return } 264 | let currentTime = _player.currentTime() 265 | let delta = abs(currentTime - dur) 266 | let deltaThreshold: Float = 0.02 267 | let lastDeltaThreshold: Float = 1 268 | let lastDeltaHitThreshold = 2 269 | if delta <= deltaThreshold { 270 | pauseAll(after: delta) 271 | } else { 272 | if _lastDelta != delta { 273 | _lastDelta = delta 274 | } else if _lastDelta <= deltaThreshold { 275 | pauseAll(after: delta) 276 | } else { 277 | _lastDeltaHitCount += 1 278 | if _lastDeltaHitCount > lastDeltaHitThreshold, _lastDelta <= lastDeltaThreshold { 279 | pauseAll(after: _lastDelta) 280 | } 281 | } 282 | } 283 | } 284 | 285 | func pauseAll(after time: Float) { 286 | guard _isCalledDelayPaused == false else { return } 287 | _isCalledDelayPaused = true 288 | let delay = DispatchTimeInterval.milliseconds(Int(floor(time * 1000))) 289 | DispatchQueue.main.asyncAfter(deadline: .now() + delay) { 290 | if let dur = self._currentComposer?.duration { 291 | self.eventPipeline.call(.playback(dur)) 292 | } 293 | self._player.pause() 294 | self._currentComposer?.pause() 295 | self.eventPipeline.call(.playEnded) 296 | self._isCalledDelayPaused = false 297 | self.next() 298 | } 299 | } 300 | 301 | func indexChanged() { 302 | guard let index = playlist.playingIndex else { return } 303 | eventPipeline.call(.playingIndexChanged(index)) 304 | } 305 | 306 | // MARK: Composer 307 | 308 | func createComposer() -> Composer { 309 | let com = Composer(player: _player, config: config) 310 | com.eventPipeline.delegate(to: self) { obj, event in 311 | switch event { 312 | case let .seekable(value): 313 | obj.eventPipeline.call(.seekable(value)) 314 | case let .buffering(p): 315 | obj.eventPipeline.call(.buffering(p)) 316 | case .streamerEndEncountered: 317 | obj._isSteamerEndEncounted = true 318 | obj.eventPipeline.call(.streamerEndEncountered) 319 | case let .duration(value): 320 | obj.eventPipeline.call(.duration(value)) 321 | obj._nowPlayingInfo.duration = value 322 | obj._nowPlayingInfo.update() 323 | case let .error(err): 324 | obj.eventPipeline.call(.error(err)) 325 | case .decoderEmptyEncountered: 326 | obj.checkPlayEnded() 327 | case let .unknown(error): 328 | let state = APlay.State.unknown(error) 329 | obj.state = state 330 | obj.eventPipeline.call(.state(state)) 331 | case let .flac(value): 332 | obj.eventPipeline.call(.flac(value)) 333 | case let .metadata(values): 334 | obj.metadatas = values 335 | obj.eventPipeline.call(.metadata(values)) 336 | guard obj.config.isAutoFillID3InfoToNowPlayingCenter else { return } 337 | for val in values { 338 | switch val { 339 | case let .album(text): obj._nowPlayingInfo.album = text 340 | case let .artist(text): obj._nowPlayingInfo.artist = text 341 | case let .title(text): obj._nowPlayingInfo.name = text 342 | #if canImport(UIKit) 343 | case let .cover(cov): obj._nowPlayingInfo.artwork = UIImage(data: cov) 344 | #endif 345 | default: break 346 | } 347 | } 348 | obj._nowPlayingInfo.update() 349 | } 350 | } 351 | return com 352 | } 353 | 354 | private func addInteruptOb() { 355 | config.logger.log("config.isAutoHandlingInterruptEvent: \(config.isAutoHandlingInterruptEvent)", to: .player) 356 | guard config.isAutoHandlingInterruptEvent else { return } 357 | /// RouteChange 358 | 359 | let note1 = NotificationCenter.default.addObserver(forName: AVAudioSession.routeChangeNotification, object: nil, queue: .main) {[weak self] (note) in 360 | let interuptionDict = note.userInfo 361 | // "Headphone/Line was pulled. Stopping player...." 362 | self?.config.logger.log("routeChange: \(interuptionDict ?? [:])", to: .player) 363 | if let routeChangeReason = interuptionDict?[AVAudioSessionRouteChangeReasonKey] as? UInt, routeChangeReason == AVAudioSession.RouteChangeReason.oldDeviceUnavailable.rawValue { 364 | self?.config.logger.log("routeChange pause", to: .player) 365 | self?.pause() 366 | } 367 | } 368 | 369 | var playingStateBeforeInterrupte = state.isPlaying 370 | let note2 = NotificationCenter.default.addObserver(forName: AVAudioSession.interruptionNotification, object: nil, queue: .main) { [weak self](note) -> Void in 371 | guard let sself = self else { return } 372 | let info = note.userInfo 373 | sself.config.logger.log("interruption event \(info ?? [:])", to: .player) 374 | guard let type = info?[AVAudioSessionInterruptionTypeKey] as? UInt else { return } 375 | if type == AVAudioSession.InterruptionType.began.rawValue { 376 | // 中断开始 377 | playingStateBeforeInterrupte = sself.state.isPlaying 378 | if playingStateBeforeInterrupte == true { sself.pause() } 379 | } else { 380 | // 中断结束 381 | guard let options = info?[AVAudioSessionInterruptionOptionKey] as? UInt, options == AVAudioSession.InterruptionOptions.shouldResume.rawValue, playingStateBeforeInterrupte == true else { return } 382 | sself.resume() 383 | } 384 | } 385 | _obs = [note1, note2] 386 | } 387 | 388 | } 389 | 390 | // MARK: - Thread Safe 391 | 392 | extension APlay { 393 | /// playback list 394 | public var playlist: PlayList { 395 | get { return _propertiesQueue.sync { _playlist } } 396 | set { _propertiesQueue.async(flags: .barrier) { self._playlist = newValue } } 397 | } 398 | 399 | /// playback state 400 | public var state: State { 401 | get { return _propertiesQueue.sync { _state } } 402 | set { _propertiesQueue.async(flags: .barrier) { self._state = newValue } } 403 | } 404 | 405 | /// duration for current song 406 | public var duration: Int { 407 | return _nowPlayingInfo.duration 408 | } 409 | 410 | private var _isSteamerEndEncounted: Bool { 411 | get { return _propertiesQueue.sync { __isSteamerEndEncounted } } 412 | set { _propertiesQueue.async(flags: .barrier) { self.__isSteamerEndEncounted = newValue } } 413 | } 414 | 415 | private var _isDecoderEndEncounted: Bool { 416 | get { return _propertiesQueue.sync { __isDecoderEndEncounted } } 417 | set { _propertiesQueue.async(flags: .barrier) { self.__isDecoderEndEncounted = newValue } } 418 | } 419 | 420 | private var _isCalledDelayPaused: Bool { 421 | get { return _propertiesQueue.sync { __isCalledDelayPaused } } 422 | set { _propertiesQueue.async(flags: .barrier) { self.__isCalledDelayPaused = newValue } } 423 | } 424 | 425 | private var _isFlagReseted: Bool { 426 | get { return _propertiesQueue.sync { __isFlagReseted } } 427 | set { _propertiesQueue.async(flags: .barrier) { self.__isFlagReseted = newValue } } 428 | } 429 | 430 | private var _lastDelta: Float { 431 | get { return _propertiesQueue.sync { __lastDelta } } 432 | set { _propertiesQueue.async(flags: .barrier) { self.__lastDelta = newValue } } 433 | } 434 | 435 | private var _lastDeltaHitCount: Int { 436 | get { return _propertiesQueue.sync { __lastDeltaHitCount } } 437 | set { _propertiesQueue.async(flags: .barrier) { self.__lastDeltaHitCount = newValue } } 438 | } 439 | 440 | private var _currentComposer: Composer? { 441 | get { return _propertiesQueue.sync { __currentComposer } } 442 | set { _propertiesQueue.async(flags: .barrier) { self.__currentComposer = newValue } } 443 | } 444 | } 445 | 446 | // MARK: - Enums 447 | 448 | public extension APlay { 449 | /// Event for playback 450 | /// 451 | /// - state: player state 452 | /// - buffering: buffer event with progress 453 | /// - waitForStreaming: bad network detech, waiting for more data to come 454 | /// - streamerEndEncountered: stream end 455 | /// - playEnded: playback complete 456 | /// - playback: playback with current time 457 | /// - duration: song duration 458 | /// - seekable: seekable event 459 | /// - playlistChanged: playlist changed 460 | /// - playModeChanged: loop pattern changed 461 | /// - error: error 462 | /// - metadata: song matadata 463 | /// - flac: flac metadata 464 | enum Event { 465 | case state(State) 466 | case buffering(Float) 467 | case waitForStreaming 468 | case streamerEndEncountered 469 | case playEnded 470 | case playback(Float) 471 | case duration(Int) 472 | case seekable(Bool) 473 | case playingIndexChanged(Int) 474 | case playlistChanged([URL], Int) 475 | case playModeChanged(PlayList.LoopPattern) 476 | case error(APlay.Error) 477 | case metadata([MetadataParser.Item]) 478 | case flac(FlacMetadata) 479 | } 480 | 481 | /// Player State 482 | /// 483 | /// - idle: init state 484 | /// - playing: playing 485 | /// - paused: paused 486 | /// - error: error 487 | /// - unknown: exception 488 | enum State { 489 | case idle 490 | case playing 491 | case paused 492 | case error(APlay.Error) 493 | case unknown(Swift.Error) 494 | 495 | public var isPlaying: Bool { 496 | switch self { 497 | case .playing: return true 498 | default: return false 499 | } 500 | } 501 | } 502 | 503 | /// Error for APlay 504 | /// 505 | /// - none: init state 506 | /// - open: error when opening stream 507 | /// - openedAlready: try to reopen a stream 508 | /// - streamParse: parser error 509 | /// - network: network error 510 | /// - networkPermission: network permission result 511 | /// - reachMaxRetryTime: reach max retry time error 512 | /// - networkStatusCode: networ reponse with status code 513 | /// - parser: parser error with OSStatus 514 | /// - player: player error 515 | enum Error: Swift.Error { 516 | case none, open(String), openedAlready(String), streamParse(String), network(String), networkPermission(String), reachMaxRetryTime, networkStatusCode(Int), parser(OSStatus), player(String), playItemNotFound(String) 517 | } 518 | } 519 | -------------------------------------------------------------------------------- /APlay/BuildInComponents/AVAduioSessionWorkaround/AVAudioSession+Workaround.h: -------------------------------------------------------------------------------- 1 | // 2 | // AVAudioSession+Workaround.h 3 | // APlay 4 | // 5 | // Created by lincoln on 2018/6/8. 6 | // Copyright © 2018年 SelfStudio. All rights reserved. 7 | // 8 | 9 | #import 10 | @interface AVAduioSessionWorkaround: NSObject 11 | + (NSError* __nullable) setPlaybackCategory; 12 | @end 13 | -------------------------------------------------------------------------------- /APlay/BuildInComponents/AVAduioSessionWorkaround/AVAudioSession+Workaround.m: -------------------------------------------------------------------------------- 1 | // 2 | // a.m 3 | // APlay 4 | // 5 | // Created by lincoln on 2018/6/8. 6 | // Copyright © 2018年 SelfStudio. All rights reserved. 7 | // 8 | 9 | #import "AVAudioSession+Workaround.h" 10 | @import AVFoundation; 11 | @implementation AVAduioSessionWorkaround 12 | +(NSError*) setPlaybackCategory { 13 | NSError *error = nil; 14 | [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error: &error]; 15 | return error; 16 | } 17 | @end 18 | -------------------------------------------------------------------------------- /APlay/BuildInComponents/Configuration/Configuration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StreamConfiguration.swift 3 | // FreePlayer 4 | // 5 | // Created by Lincoln Law on 2017/2/19. 6 | // Copyright © 2017年 Lincoln Law. All rights reserved. 7 | // 8 | 9 | import AudioToolbox 10 | import AVFoundation 11 | #if os(iOS) 12 | import UIKit 13 | #endif 14 | 15 | extension APlay { 16 | /// Configuration for APlay 17 | public final class Configuration: ConfigurationCompatible { 18 | /// 播放器歌曲默认图像 19 | public var defaultCoverImage: UIImage? 20 | /** 缓存目录 */ 21 | public let cacheDirectory: String 22 | /// 网络 session 23 | public let session: URLSession 24 | public let logPolicy: Logger.Policy 25 | /// 校验下载文件完整性 26 | public let httpFileCompletionValidator: HttpFileValidationPolicy 27 | /** 远程 Wave 文件的预缓冲大小(先缓冲到10%再播放) */ 28 | public let preBufferWaveFormatPercentageBeforePlay: Float 29 | /** 每个解码的大小 */ 30 | public let decodeBufferSize: UInt 31 | /** 监控播放器,超时没播放则🚔 */ 32 | public let startupWatchdogPeriod: UInt 33 | /** 磁盘最大缓存数(bytes) */ 34 | public let maxDiskCacheSize: UInt32 35 | /** 最大解码数(bytes) */ 36 | public let maxDecodedByteCount: UInt32 37 | /** 自定义 UA */ 38 | public let userAgent: String 39 | /** 缓存命名策略 */ 40 | public let cacheNaming: CacheFileNamingPolicy 41 | /** 缓存策略 */ 42 | public let cachePolicy: CachePolicy 43 | /** 代理策略 */ 44 | public let proxyPolicy: ProxyPolicy 45 | /** 网络策略 */ 46 | public let networkPolicy: NetworkPolicy 47 | /** 自定义 http header 字典 */ 48 | public let predefinedHttpHeaderValues: [String: String] 49 | /** 自动控制 AudioSession */ 50 | public let isEnabledAutomaticAudioSessionHandling: Bool 51 | /** 远程连接最大重试次数 默认5次*/ 52 | public let maxRemoteStreamOpenRetry: UInt 53 | /** 自动填充ID3的信息到 NowPlayingCenter */ 54 | public let isAutoFillID3InfoToNowPlayingCenter: Bool 55 | /** 自动处理中断事件 */ 56 | public let isAutoHandlingInterruptEvent: Bool 57 | /// If YES then volume control will be enabled on iOS 58 | public let isEnabledVolumeMixer: Bool 59 | /// A pointer to a 0 terminated array of band frequencies (iOS 5.0 and later, OSX 10.9 and later) 60 | public let equalizerBandFrequencies: [Float] 61 | /// logger 62 | public let logger: LoggerCompatible 63 | 64 | /// streamer factory 65 | public private(set) var streamerBuilder: StreamerBuilder = { Streamer(config: $0) } 66 | 67 | /// audio decoder factory 68 | public private(set) var audioDecoderBuilder: AudioDecoderBuilder = { DefaultAudioDecoder(config: $0) } 69 | 70 | /// metadata parser factory 71 | public private(set) var metadataParserBuilder: MetadataParserBuilder = { 72 | type, config in 73 | if type == .flac { return FlacParser(config: config) } 74 | else if type == .mp3 { return ID3Parser(config: config) } 75 | else { return nil } 76 | } 77 | 78 | #if os(iOS) 79 | private lazy var _backgroundTask = UIBackgroundTaskIdentifier.invalid 80 | #endif 81 | 82 | #if DEBUG 83 | deinit { 84 | debug_log("\(self) \(#function)") 85 | } 86 | #endif 87 | 88 | public init(defaultCoverImage: UIImage? = nil, 89 | proxyPolicy: ProxyPolicy = .system, 90 | logPolicy: Logger.Policy = Logger.Policy.defaultPolicy, 91 | httpFileCompletionValidator: HttpFileValidationPolicy = .notValidate, 92 | preBufferWaveFormatPercentageBeforePlay: Float = 0.1, 93 | decodeBufferSize: UInt = 8192, 94 | startupWatchdogPeriod: UInt = 30, 95 | maxDiskCacheSize: UInt32 = 256_435_456, 96 | maxDecodedByteCount: UInt32 = Configuration.defaultMaxDecodedByteCount, 97 | userAgent: String = Configuration.defaultUA, 98 | cacheNaming: CacheFileNamingPolicy = CacheFileNamingPolicy.defaultPolicy, 99 | cachePolicy: CachePolicy = .enable([]), 100 | cacheDirectory: String = Configuration.defaultCachedDirectory, 101 | networkPolicy: NetworkPolicy = .noRestrict, 102 | predefinedHttpHeaderValues: [String: String] = [:], 103 | automaticAudioSessionHandlingEnabled: Bool = true, 104 | maxRemoteStreamOpenRetry: UInt = 5, 105 | equalizerBandFrequencies: [Float] = [50, 100, 200, 400, 800, 1600, 2600, 16000], 106 | autoFillID3InfoToNowPlayingCenter: Bool = true, 107 | autoHandlingInterruptEvent: Bool = true, 108 | enableVolumeMixer: Bool = true, 109 | sessionBuilder: SessionBuilder? = nil, 110 | sessionDelegateBuilder: SessionDelegateBuilder? = nil, 111 | loggerBuilder: LoggerBuilder? = nil, 112 | streamerBuilder: StreamerBuilder? = nil, 113 | audioDecoderBuilder: AudioDecoderBuilder? = nil, 114 | metadataParserBuilder: MetadataParserBuilder? = nil) { 115 | self.defaultCoverImage = defaultCoverImage 116 | self.proxyPolicy = proxyPolicy 117 | self.logPolicy = logPolicy 118 | self.httpFileCompletionValidator = httpFileCompletionValidator 119 | self.preBufferWaveFormatPercentageBeforePlay = preBufferWaveFormatPercentageBeforePlay 120 | self.decodeBufferSize = decodeBufferSize 121 | self.startupWatchdogPeriod = startupWatchdogPeriod 122 | self.maxDiskCacheSize = maxDiskCacheSize 123 | self.maxDecodedByteCount = maxDecodedByteCount 124 | self.userAgent = userAgent 125 | self.cacheNaming = cacheNaming 126 | self.cachePolicy = cachePolicy 127 | self.cacheDirectory = cacheDirectory 128 | self.networkPolicy = networkPolicy 129 | self.predefinedHttpHeaderValues = predefinedHttpHeaderValues 130 | isEnabledAutomaticAudioSessionHandling = automaticAudioSessionHandlingEnabled 131 | self.maxRemoteStreamOpenRetry = maxRemoteStreamOpenRetry 132 | self.equalizerBandFrequencies = equalizerBandFrequencies 133 | isEnabledVolumeMixer = enableVolumeMixer 134 | isAutoFillID3InfoToNowPlayingCenter = autoFillID3InfoToNowPlayingCenter 135 | isAutoHandlingInterruptEvent = autoHandlingInterruptEvent 136 | 137 | logger = loggerBuilder?(logPolicy) ?? APlay.InternalLogger(policy: logPolicy) 138 | 139 | if let builder = streamerBuilder { self.streamerBuilder = builder } 140 | if let builder = audioDecoderBuilder { self.audioDecoderBuilder = builder } 141 | if let builder = metadataParserBuilder { self.metadataParserBuilder = builder } 142 | 143 | // config session 144 | if let builder = sessionBuilder { 145 | session = builder(proxyPolicy) 146 | } else if case let Configuration.ProxyPolicy.custom(info) = proxyPolicy { 147 | let configure = URLSessionConfiguration.default 148 | let enableKey = kCFNetworkProxiesHTTPEnable as String 149 | let hostKey = kCFNetworkProxiesHTTPProxy as String 150 | let portKey = kCFNetworkProxiesHTTPPort as String 151 | configure.connectionProxyDictionary = [ 152 | enableKey: 1, 153 | hostKey: info.host, 154 | portKey: info.port, 155 | ] 156 | let delegate = sessionDelegateBuilder?(proxyPolicy) ?? SessionDelegate(policy: proxyPolicy) 157 | session = URLSession(configuration: configure, delegate: delegate, delegateQueue: .main) 158 | } else { 159 | session = URLSession(configuration: URLSessionConfiguration.default) 160 | } 161 | } 162 | 163 | /// Start background task 164 | /// 165 | /// - Parameter isToDownloadImage: Bool 166 | public func startBackgroundTask(isToDownloadImage: Bool = false) { 167 | #if os(iOS) 168 | if isToDownloadImage { 169 | guard _backgroundTask != UIBackgroundTaskIdentifier.invalid else { return } 170 | } 171 | if isEnabledAutomaticAudioSessionHandling { 172 | do { 173 | let instance = AVAudioSession.sharedInstance() 174 | if #available(iOS 11.0, *) { 175 | try instance.setCategory(.playback, mode: .default, policy: AVAudioSession.RouteSharingPolicy.longForm) 176 | } else if #available(iOS 10.0, *) { 177 | try instance.setCategory(.playback, mode: .default) 178 | } else { 179 | if let error = AVAduioSessionWorkaround.setPlaybackCategory() { 180 | throw error 181 | } 182 | } 183 | try instance.setActive(true) 184 | } catch { 185 | debug_log("error: \(error)") 186 | } 187 | } 188 | endBackgroundTask(isToDownloadImage: isToDownloadImage) 189 | _backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: { [weak self] in 190 | self?.endBackgroundTask(isToDownloadImage: isToDownloadImage) 191 | }) 192 | #elseif os(macOS) 193 | print("tbd") 194 | // do { 195 | // if #available(iOS 11.0, *) { 196 | // try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, policy: AVAudioSession.RouteSharingPolicy.longForm) 197 | // } else if #available(iOS 10.0, *) { 198 | // try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default) 199 | // } else { 200 | // if let error = AVAduioSessionWorkaround.setPlaybackCategory() { 201 | // throw error 202 | // } 203 | // } 204 | // } catch { 205 | // debug_log("error: \(error)") 206 | // } 207 | #endif 208 | } 209 | 210 | /// Stop background task 211 | /// 212 | /// - Parameter isToDownloadImage: Bool 213 | public func endBackgroundTask(isToDownloadImage: Bool) { 214 | #if os(iOS) 215 | if isToDownloadImage { return } 216 | guard _backgroundTask != UIBackgroundTaskIdentifier.invalid else { return } 217 | UIApplication.shared.endBackgroundTask(_backgroundTask) 218 | _backgroundTask = UIBackgroundTaskIdentifier.invalid 219 | #endif 220 | } 221 | } 222 | } 223 | 224 | // MARK: - Models 225 | 226 | extension APlay.Configuration { 227 | /// Default cache directory for player 228 | public static var defaultCachedDirectory: String { 229 | let base = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first! 230 | let target = "\(base)/APlay/Tmp" 231 | let fs = FileManager.default 232 | guard fs.fileExists(atPath: target) == false else { return target } 233 | try? fs.createDirectory(atPath: target, withIntermediateDirectories: true, attributes: nil) 234 | return target 235 | } 236 | 237 | /// Default User-Agent for network streaming 238 | public static var defaultUA: String { 239 | var osStr = "" 240 | #if os(iOS) 241 | let rversion = UIDevice.current.systemVersion 242 | osStr = "iOS \(rversion)" 243 | #elseif os(OSX) 244 | // No need to be so concervative with the cache sizes 245 | osStr = "macOS" 246 | #endif 247 | return "APlay/\(APlay.version) \(osStr)" 248 | } 249 | 250 | /// Default size for decoeded data 251 | public static var defaultMaxDecodedByteCount: UInt32 { 252 | let is64Bit = MemoryLayout.size == MemoryLayout.size 253 | return (is64Bit ? 4 : 2) * 1_048_576 254 | } 255 | 256 | /// Validate policy for remote file 257 | /// 258 | /// - notValidate: not validate 259 | /// - validateHeader->Bool: validate with header info, validator 260 | public enum HttpFileValidationPolicy { 261 | case notValidate 262 | case validateHeader(keys: [String], validator: ((URL, String, [String: Any]) -> Bool)) 263 | 264 | var keys: [String] { 265 | switch self { 266 | case .notValidate: return [] 267 | case let .validateHeader(keys: keys, _): return keys 268 | } 269 | } 270 | } 271 | 272 | /// Naming policy for cached file 273 | /// 274 | /// - `default`: will use `url.path.replacingOccurrences(of: "/", with: "_")` for naming url 275 | /// - custom->String: custom policy 276 | public enum CacheFileNamingPolicy { 277 | /// default is url.path.hashValue 278 | case `default` 279 | case custom((URL) -> String) 280 | 281 | func name(for url: URL) -> String { 282 | switch self { 283 | case .default: return url.path.replacingOccurrences(of: "/", with: "_") 284 | case let .custom(block): return block(url) 285 | } 286 | } 287 | 288 | /// A default implementation for custom((URL) -> String) 289 | public static var defaultPolicy: CacheFileNamingPolicy { 290 | return .custom({ (url) -> String in 291 | let raw = url.path 292 | guard let dat = raw.data(using: .utf8) else { return raw } 293 | let sub = dat.base64EncodedString() 294 | return sub 295 | }) 296 | } 297 | } 298 | 299 | /// Cache Plolicy 300 | /// 301 | /// - enable: enable with extra folders 302 | /// - disable: disable cache on disk 303 | public enum CachePolicy { 304 | case enable([String]) 305 | case disable 306 | 307 | var isEnabled: Bool { 308 | switch self { 309 | case .disable: return false 310 | default: return true 311 | } 312 | } 313 | 314 | var cachedFolder: [String]? { 315 | switch self { 316 | case let .enable(values): return values 317 | default: return nil 318 | } 319 | } 320 | } 321 | 322 | /// Network policy for accessing remote resources 323 | public enum NetworkPolicy { 324 | public typealias PermissionHandler = (URL, (@escaping (Bool) -> Void)) -> Void 325 | case noRestrict 326 | case requiredPermission(PermissionHandler) 327 | func requestPermission(for url: URL, handler: @escaping (Bool) -> Void) { 328 | switch self { 329 | case .noRestrict: handler(true) 330 | case let .requiredPermission(closure): closure(url, handler) 331 | } 332 | } 333 | } 334 | 335 | /// Proxy Policy 336 | /// 337 | /// - system: using system proxy 338 | /// - custom: using custom proxy with config 339 | public enum ProxyPolicy { 340 | case system 341 | case custom(Info) 342 | 343 | /// Custom proxy info 344 | public struct Info { 345 | /** 使用自定义代理 用户名 */ 346 | public let username: String 347 | /** 使用自定义代理 密码 */ 348 | public let password: String 349 | /** 使用自定义代理 Host */ 350 | public let host: String 351 | /** 使用自定义代理 Port */ 352 | public let port: UInt 353 | /** 使用自定义代理 authenticationScheme, kCFHTTPAuthenticationSchemeBasic... */ 354 | public let scheme: AuthenticationScheme 355 | /** 代理 Https */ 356 | public let isProxyingHttps: Bool 357 | 358 | public init(username: String, password: String, host: String, port: UInt, scheme: AuthenticationScheme, proxyingHttps: Bool = false) { 359 | self.username = username 360 | self.password = password 361 | self.host = host 362 | self.port = port 363 | self.scheme = scheme 364 | isProxyingHttps = proxyingHttps 365 | } 366 | 367 | /// Authentication scheme 368 | public enum AuthenticationScheme { 369 | case digest, basic 370 | var name: CFString { 371 | switch self { 372 | case .digest: return kCFHTTPAuthenticationSchemeDigest 373 | case .basic: return kCFHTTPAuthenticationSchemeBasic 374 | } 375 | } 376 | } 377 | } 378 | } 379 | } 380 | 381 | // MARK: - SessionDelegate 382 | 383 | extension APlay.Configuration { 384 | private final class SessionDelegate: NSObject, URLSessionDelegate, URLSessionTaskDelegate { 385 | private let _proxyPolicy: APlay.Configuration.ProxyPolicy 386 | 387 | init(policy: APlay.Configuration.ProxyPolicy) { 388 | _proxyPolicy = policy 389 | } 390 | 391 | public func urlSession(_: URLSession, task _: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { 392 | var credential: URLCredential? 393 | if case let APlay.Configuration.ProxyPolicy.custom(info) = _proxyPolicy { 394 | credential = URLCredential(user: info.username, password: info.password, persistence: .forSession) 395 | } 396 | completionHandler(.useCredential, credential) 397 | } 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /APlay/BuildInComponents/Logger/InternalLogger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InternalLogger.swift 3 | // APlay 4 | // 5 | // Created by Lincoln Law on 2017/2/22. 6 | // Copyright © 2017年 Lincoln Law. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension APlay { 12 | final class InternalLogger { 13 | var currentFile: String { return _filePath } 14 | var isLoggedToConsole: Bool = true 15 | private let _policy: Logger.Policy 16 | private lazy var _filePath: String = "" 17 | 18 | private var _date = "" 19 | private var _fileHandler: FileHandle? 20 | private var _logQueue = DispatchQueue(concurrentName: "Logger") 21 | private var _openTime = "" 22 | private var _lastRead: UInt64 = 0 23 | private var _totalSize: UInt64 = 0 24 | 25 | deinit { 26 | guard let fileHandle = _fileHandler else { return } 27 | _logQueue.sync { fileHandle.closeFile() } 28 | } 29 | 30 | init(policy: Logger.Policy) { 31 | _policy = policy 32 | guard let dir = _policy.folder else { return } 33 | let total = dateTime() 34 | _date = total.0 35 | _openTime = total.1 36 | _filePath = "\(dir)/\(_date).log" 37 | 38 | let u = URL(fileURLWithPath: _filePath) 39 | if access(_filePath.withCString({ $0 }), F_OK) == -1 { // file not exists 40 | FileManager.default.createFile(atPath: _filePath, contents: nil, attributes: nil) 41 | } 42 | _fileHandler = try? FileHandle(forWritingTo: u) 43 | if let fileHandle = _fileHandler { 44 | _lastRead = fileHandle.seekToEndOfFile() 45 | } 46 | _totalSize = _lastRead 47 | } 48 | 49 | private func dateTime() -> (String, String) { 50 | var rawTime = time_t() 51 | time(&rawTime) 52 | var timeinfo = tm() 53 | localtime_r(&rawTime, &timeinfo) 54 | 55 | var curTime = timeval() 56 | gettimeofday(&curTime, nil) 57 | let milliseconds = curTime.tv_usec / 1000 58 | return ("\(Int(timeinfo.tm_year) + 1900)-\(Int(timeinfo.tm_mon + 1))-\(Int(timeinfo.tm_mday))", "\(Int(timeinfo.tm_hour)):\(Int(timeinfo.tm_min)):\(Int(milliseconds))") 59 | } 60 | } 61 | } 62 | 63 | extension APlay.InternalLogger: LoggerCompatible { 64 | func reset() { 65 | _openTime = dateTime().1 66 | let msg = "🎹:APlay[\(APlay.version)]@\(_openTime)\(Logger.lineSeperator)" 67 | log(msg, to: .audioDecoder) 68 | } 69 | 70 | func cleanAllLogs() { 71 | guard let dir = _policy.folder else { return } 72 | let fm = FileManager.default 73 | try? fm.removeItem(atPath: dir) 74 | if fm.fileExists(atPath: dir) == false { 75 | try? fm.createDirectory(atPath: dir, withIntermediateDirectories: false, attributes: nil) 76 | } 77 | } 78 | 79 | func log(_ msg: String, to channel: Logger.Channel, method: String) { 80 | guard let fileHandler = _fileHandler else { return } 81 | let total = "\(channel.symbole)\(method):\(msg)\(Logger.lineSeperator)" 82 | 83 | if isLoggedToConsole { print(total) } 84 | 85 | _logQueue.async(flags: .barrier) { 86 | guard let data = total.data(using: .utf8) else { return } 87 | self._totalSize += UInt64(data.count) 88 | fileHandler.write(data) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /APlay/BuildInComponents/NowPlaying/NowPlayingInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FreePlayer+MPPlayingCenter.swift 3 | // FreePlayer 4 | // 5 | // Created by Lincoln Law on 2017/3/1. 6 | // Copyright © 2017年 Lincoln Law. All rights reserved. 7 | // 8 | #if os(iOS) 9 | import MediaPlayer 10 | #endif 11 | extension APlay { 12 | final class NowPlayingInfo { 13 | var name = "" 14 | var artist = "" 15 | var album = "" 16 | var artwork: UIImage? 17 | var duration = 0 18 | var playbackRate: Float = 0 19 | var playbackTime: Float = 0 20 | private var _queue: DispatchQueue = DispatchQueue(concurrentName: "NowPlayingInfo") 21 | private var _coverTask: URLSessionDataTask? 22 | private unowned var _config: ConfigurationCompatible 23 | 24 | #if DEBUG 25 | deinit { 26 | debug_log("\(self) \(#function)") 27 | } 28 | #endif 29 | 30 | init(config: ConfigurationCompatible) { 31 | _config = config 32 | } 33 | 34 | var info: [String: Any] { 35 | return _queue.sync(execute: { () -> [String: Any] in 36 | var map = [String: Any]() 37 | map[MPMediaItemPropertyTitle] = name 38 | map[MPMediaItemPropertyArtist] = artist 39 | map[MPMediaItemPropertyAlbumTitle] = album 40 | map[MPNowPlayingInfoPropertyElapsedPlaybackTime] = Double(playbackTime) 41 | map[MPNowPlayingInfoPropertyPlaybackRate] = Double(playbackRate) 42 | map[MPMediaItemPropertyPlaybackDuration] = duration 43 | #if os(iOS) 44 | if let image = artwork { 45 | map[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(image: image) 46 | } 47 | #endif 48 | return map 49 | }) 50 | } 51 | 52 | func play(elapsedPlayback: Float) { 53 | playbackTime = elapsedPlayback 54 | playbackRate = 1 55 | update() 56 | } 57 | 58 | func pause(elapsedPlayback: Float) { 59 | playbackTime = elapsedPlayback 60 | playbackRate = 0 61 | update() 62 | } 63 | 64 | func image(with url: String?) { 65 | guard let u = url, let r = URL(string: u) else { return } 66 | _coverTask?.cancel() 67 | DispatchQueue.global(qos: .utility).async { 68 | let request = URLRequest(url: r, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 20) 69 | if let d = URLCache.shared.cachedResponse(for: request)?.data, let image = UIImage(data: d) { 70 | self._queue.sync { self.artwork = image } 71 | self.update() 72 | return 73 | } 74 | self._config.networkPolicy.requestPermission(for: r, handler: { [unowned self] success in 75 | guard success else { return } 76 | self.doRequest(request) 77 | }) 78 | } 79 | } 80 | 81 | func update() { 82 | #if os(iOS) 83 | DispatchQueue.main.async { 84 | let nowPlayingInfo = self.info 85 | MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo 86 | } 87 | #endif 88 | } 89 | 90 | func remove() { 91 | _queue.async(flags: .barrier) { 92 | self.name = "" 93 | self.artist = "" 94 | self.album = "" 95 | self.artwork = self._config.defaultCoverImage 96 | self.duration = 0 97 | self.playbackRate = 0 98 | self.playbackTime = 0 99 | } 100 | #if os(iOS) 101 | DispatchQueue.main.async { 102 | MPNowPlayingInfoCenter.default().nowPlayingInfo = nil 103 | } 104 | #endif 105 | } 106 | 107 | private func doRequest(_ request: URLRequest) { 108 | _config.startBackgroundTask(isToDownloadImage: true) 109 | let task = _config.session.dataTask(with: request, completionHandler: { [weak self] data, resp, _ in 110 | if let r = resp, let d = data { 111 | let cre = CachedURLResponse(response: r, data: d) 112 | URLCache.shared.storeCachedResponse(cre, for: request) 113 | } 114 | guard let sself = self, let d = data, let image = UIImage(data: d) else { return } 115 | sself._queue.sync { sself.artwork = image } 116 | sself.update() 117 | }) 118 | task.resume() 119 | _coverTask = task 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /APlay/BuildInComponents/Players/APlayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APlayer.swift 3 | // APlay 4 | // 5 | // Created by lincoln on 2018/6/29. 6 | // Copyright © 2018年 SelfStudio. All rights reserved. 7 | // 8 | 9 | import AVFoundation 10 | 11 | @available(iOS 11.0, *) 12 | final class APlayer: PlayerCompatible { 13 | var readClosure: (UInt32, UnsafeMutablePointer) -> (UInt32, Bool) = { _, _ in (0, false) } 14 | 15 | var eventPipeline: Delegated = Delegated() 16 | 17 | var startTime: Float = 0 { 18 | didSet { 19 | _stateQueue.async(flags: .barrier) { self._progress = 0 } 20 | } 21 | } 22 | 23 | fileprivate lazy var _progress: Float = 0 24 | private lazy var _volume: Float = 1 25 | 26 | private(set) lazy var asbd = AudioStreamBasicDescription() 27 | 28 | private(set) var state: Player.State { 29 | get { return _stateQueue.sync { _state } } 30 | set { 31 | _stateQueue.async(flags: .barrier) { 32 | self._state = newValue 33 | self.eventPipeline.call(.state(newValue)) 34 | } 35 | } 36 | } 37 | 38 | private lazy var _state: Player.State = .idle 39 | private lazy var _stateQueue = DispatchQueue(concurrentName: "AUPlayer.state") 40 | 41 | private lazy var _playbackTimer: GCDTimer = { 42 | GCDTimer(interval: .seconds(1), callback: { [weak self] _ in 43 | guard let sself = self else { return } 44 | sself.eventPipeline.call(.playback(sself.currentTime())) 45 | }) 46 | }() 47 | 48 | private lazy var _buffers: UnsafeMutablePointer = { 49 | let size = Player.minimumBufferSize 50 | return UnsafeMutablePointer.uint8Pointer(of: size) 51 | }() 52 | 53 | private lazy var audioBufferList: AudioBufferList = { 54 | let buf = AudioBuffer() 55 | var list = AudioBufferList(mNumberBuffers: 1, mBuffers: buf) 56 | return list 57 | }() 58 | 59 | private var _player: AudioUnit? = { 60 | #if os(OSX) 61 | let subType = kAudioUnitSubType_DefaultOutput 62 | #else 63 | let subType = kAudioUnitSubType_RemoteIO 64 | #endif 65 | var player: AudioUnit? 66 | var componentDesc = AudioComponentDescription(componentType: kAudioUnitType_Output, componentSubType: subType, componentManufacturer: kAudioUnitManufacturer_Apple, componentFlags: 0, componentFlagsMask: 0) 67 | guard let audioComponent = AudioComponentFindNext(nil, &componentDesc) else { fatalError("player create failure") } 68 | AudioComponentInstanceNew(audioComponent, &player).check() 69 | return player 70 | }() 71 | 72 | private let _engine = AVAudioEngine() 73 | /// 74 | private let _eq = AVAudioUnitEQ() 75 | fileprivate var _renderBlock: AVAudioEngineManualRenderingBlock? 76 | 77 | private unowned let _config: ConfigurationCompatible 78 | 79 | deinit { 80 | debug_log("\(self) \(#function)") 81 | } 82 | 83 | init(config: ConfigurationCompatible) { 84 | _config = config 85 | _engine.attach(_eq) 86 | // Avoid requesting microphone permission, set rendering mode first before connect 87 | let format = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 2)! 88 | _engine.stop() 89 | try? _engine.enableManualRenderingMode(.realtime, format: format, maximumFrameCount: Player.maxFramesPerSlice) 90 | _engine.connect(_engine.inputNode, to: _eq, format: nil) 91 | _engine.connect(_eq, to: _engine.mainMixerNode, format: nil) 92 | } 93 | } 94 | 95 | // MARK: - Create Player 96 | 97 | @available(iOS 11.0, *) 98 | private extension APlayer { 99 | private func updatePlayerConfig() throws { 100 | guard let unit = _player else { return } 101 | let s = MemoryLayout.size(ofValue: asbd) 102 | // set stream format for input bus 103 | try AudioUnitSetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, Player.Bus.output, &asbd, UInt32(s)).throwCheck() 104 | 105 | let fSize = MemoryLayout.size(ofValue: Player.maxFramesPerSlice) 106 | try AudioUnitSetProperty(unit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &Player.maxFramesPerSlice, UInt32(fSize)).throwCheck() 107 | // render callback 108 | let pointer = UnsafeMutableRawPointer.from(object: self) 109 | var callbackStruct = AURenderCallbackStruct(inputProc: renderCallback, inputProcRefCon: pointer) 110 | let callbackSize = MemoryLayout.size(ofValue: callbackStruct) 111 | try AudioUnitSetProperty(unit, kAudioUnitProperty_SetRenderCallback, 112 | kAudioUnitScope_Output, Player.Bus.output, &callbackStruct, 113 | UInt32(callbackSize)).throwCheck() 114 | } 115 | } 116 | 117 | // MARK: - PlayerCompatible 118 | 119 | @available(iOS 11.0, *) 120 | extension APlayer { 121 | func destroy() { 122 | _engine.stop() 123 | _playbackTimer.invalidate() 124 | pause() 125 | readClosure = { _, _ in (0, false) } 126 | eventPipeline.removeDelegate() 127 | } 128 | 129 | func pause() { 130 | guard state == .running, let unit = _player else { return } 131 | AudioOutputUnitStop(unit).check() 132 | state = .paused 133 | _playbackTimer.pause() 134 | } 135 | 136 | func resume() { 137 | guard state != .running, let unit = _player else { return } 138 | do { 139 | try AudioOutputUnitStart(unit).throwCheck() 140 | state = .running 141 | _config.startBackgroundTask(isToDownloadImage: false) 142 | _playbackTimer.resume() 143 | } catch { 144 | state = .idle 145 | debug_log(error.localizedDescription) 146 | } 147 | } 148 | 149 | func toggle() { 150 | state == .paused ? resume() : pause() 151 | } 152 | 153 | func currentTime() -> Float { 154 | return _stateQueue.sync { _progress / Float(asbd.mSampleRate) + startTime } 155 | } 156 | 157 | var volume: Float { 158 | get { return _volume } 159 | set { 160 | _volume = newValue 161 | #if os(iOS) 162 | if let unit = _player { 163 | AudioUnitSetParameter(unit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Output, 0, _volume, 0) 164 | } 165 | #else 166 | if let unit = _player { 167 | AudioUnitSetParameter(unit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Output, 0, _volume, 0) 168 | } 169 | #endif 170 | } 171 | } 172 | 173 | func setup(_ value: AudioStreamBasicDescription) { 174 | do { 175 | asbd = value 176 | try updatePlayerConfig() 177 | let format = AVAudioFormat(streamDescription: &asbd)! 178 | _engine.stop() 179 | try _engine.enableManualRenderingMode(.realtime, format: format, maximumFrameCount: Player.maxFramesPerSlice) 180 | _renderBlock = _engine.manualRenderingBlock 181 | 182 | _engine.inputNode.setManualRenderingInputPCMFormat(format) { [weak self] (frameCount) -> UnsafePointer? in 183 | guard let sself = self else { return nil } 184 | let bytesPerFrame = sself.asbd.mBytesPerFrame 185 | let size = bytesPerFrame * frameCount 186 | 187 | let (readSize, _) = sself.readClosure(size, sself._buffers) 188 | 189 | var totalReadFrame: UInt32 = frameCount 190 | if readSize != size { 191 | totalReadFrame = readSize / bytesPerFrame 192 | memset(sself._buffers.advanced(by: Int(readSize)), 0, Int(size - readSize)) 193 | } 194 | sself.audioBufferList.mBuffers.mData = UnsafeMutableRawPointer(sself._buffers) 195 | sself.audioBufferList.mBuffers.mNumberChannels = sself.asbd.mChannelsPerFrame 196 | sself.audioBufferList.mBuffers.mDataByteSize = size 197 | 198 | sself._stateQueue.async(flags: .barrier) { sself._progress += Float(totalReadFrame) } 199 | return withUnsafePointer(to: &sself.audioBufferList, { $0 }) 200 | } 201 | 202 | _engine.prepare() 203 | try _engine.start() 204 | } catch { 205 | eventPipeline.call(Player.Event.unknown(error)) 206 | } 207 | } 208 | } 209 | 210 | /// renderCallback 211 | /// 212 | /// - Parameters: 213 | /// - userInfo: Your context (aka, user info) pointer. 214 | /// - ioActionFlags: A bit field describing the purpose of the call. It’s often blank (0), and you can look up the possible values as the AudioUnitRenderActionFlag’s enum in the documentation or AUComponent.h. 215 | /// - inTimeStamp: An AudioTimeStamp structure that indicates the timing of this call relative to other calls to your render callback. 216 | /// - inBusNumber: Which bus (aka, element) of the Audio Unit is requesting audio data. 217 | /// - inNumberFrames: The number of frames to be rendered. Notice that this variable is prefixed as “in” instead of “io.”That indicates that this isn’t a case when you can render fewer frames and indicate that situation by passing back the number of frames actually rendered.Your callback must provide exactly the requested number of frames. 218 | /// - ioData: An AudioBufferList struct to be filled with data.You write your sam- ples into the mData members of the AudioBuffers contained in this struct.The list has a count of how many AudioBuffers are present, and each AudioBuffer has members for its channel count and byte size. Combined with inNumberFrames, you can figure out how much data can be safely written to these data buffers. 219 | /// - Returns: OSStatus 220 | @available(iOS 11.0, *) private func renderCallback(userInfo: UnsafeMutableRawPointer, ioActionFlags: UnsafeMutablePointer, inTimeStamp _: UnsafePointer, inBusNumber _: UInt32, inNumberFrames: UInt32, ioData: UnsafeMutablePointer?) -> OSStatus { 221 | let sself = userInfo.to(object: APlayer.self) 222 | var status = noErr 223 | _ = sself._renderBlock?(inNumberFrames, ioData!, &status) 224 | return status 225 | } 226 | -------------------------------------------------------------------------------- /APlay/BuildInComponents/Players/AUPlayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AUPlayer.swift 3 | // VinylPlayer 4 | // 5 | // Created by lincolnlaw on 2017/9/1. 6 | // Copyright © 2017年 lincolnlaw. All rights reserved. 7 | // 8 | 9 | import AudioUnit 10 | import Foundation 11 | 12 | final class AUPlayer: PlayerCompatible { 13 | var readClosure: (UInt32, UnsafeMutablePointer) -> (UInt32, Bool) = { _, _ in (0, false) } 14 | 15 | var eventPipeline: Delegated = Delegated() 16 | 17 | var startTime: Float = 0 { 18 | didSet { 19 | _stateQueue.async(flags: .barrier) { self._progress = 0 } 20 | } 21 | } 22 | private(set) lazy var asbd = AudioStreamBasicDescription() 23 | 24 | private(set) var state: Player.State { 25 | get { return _stateQueue.sync { _state } } 26 | set { 27 | _stateQueue.async(flags: .barrier) { 28 | self._state = newValue 29 | self.eventPipeline.call(.state(newValue)) 30 | } 31 | } 32 | } 33 | 34 | private lazy var _converterNodes: [AUNode] = [] 35 | private lazy var _volume: Float = 1 36 | 37 | private lazy var _audioGraph: AUGraph? = nil 38 | 39 | private lazy var _equalizerEnabled = false 40 | private lazy var _equalizerOn = false 41 | 42 | private lazy var maxSizeForNextRead = 0 43 | private lazy var _cachedSize = 0 44 | 45 | private lazy var _eqNode: AUNode = 0 46 | private lazy var _mixerNode: AUNode = 0 47 | private lazy var _outputNode: AUNode = 0 48 | 49 | private lazy var _eqInputNode: AUNode = 0 50 | private lazy var _eqOutputNode: AUNode = 0 51 | private lazy var _mixerInputNode: AUNode = 0 52 | private lazy var _mixerOutputNode: AUNode = 0 53 | 54 | private lazy var _eqUnit: AudioUnit? = nil 55 | private lazy var _mixerUnit: AudioUnit? = nil 56 | private lazy var _outputUnit: AudioUnit? = nil 57 | 58 | private lazy var _audioConverterRef: AudioConverterRef? = nil 59 | 60 | private lazy var _eqBandCount: UInt32 = 0 61 | 62 | private lazy var _playbackTimer: GCDTimer = { 63 | GCDTimer(interval: .seconds(1), callback: { [weak self] _ in 64 | guard let sself = self else { return } 65 | sself.eventPipeline.call(.playback(sself.currentTime())) 66 | }) 67 | }() 68 | 69 | private lazy var _state: Player.State = .idle 70 | private unowned let _config: ConfigurationCompatible 71 | 72 | fileprivate lazy var _stateQueue = DispatchQueue(concurrentName: "AUPlayer.state") 73 | 74 | fileprivate let _pcmBufferFrameSizeInBytes: UInt32 = Player.canonical.mBytesPerFrame 75 | 76 | fileprivate lazy var _progress: Float = 0 77 | 78 | fileprivate lazy var _currentIndex = 0 79 | fileprivate lazy var _pageSize = Player.maxReadPerSlice 80 | fileprivate func increaseBufferIndex() { 81 | _stateQueue.sync { 82 | _currentIndex = (_currentIndex + 1) % Int(Player.minimumBufferCount) 83 | } 84 | } 85 | 86 | fileprivate lazy var _buffers: UnsafeMutablePointer = { 87 | let size = Player.minimumBufferSize 88 | return UnsafeMutablePointer.uint8Pointer(of: size) 89 | }() 90 | 91 | #if DEBUG 92 | private lazy var _fakeConsumer: GCDTimer = { 93 | GCDTimer(interval: .seconds(1), callback: { [weak self] _ in 94 | guard let sself = self else { return } 95 | let size = sself._pcmBufferFrameSizeInBytes * 4096 96 | let raw = sself._buffers.advanced(by: sself._currentIndex * sself._pageSize) 97 | sself.increaseBufferIndex() 98 | _ = sself.readClosure(size, raw) 99 | }) 100 | }() 101 | #endif 102 | 103 | deinit { 104 | free(_buffers) 105 | debug_log("\(self) \(#function)") 106 | } 107 | 108 | init(config: ConfigurationCompatible) { _config = config } 109 | 110 | func setup(_ asbd: AudioStreamBasicDescription) { 111 | updateAudioGraph(asbd: asbd) 112 | } 113 | 114 | func destroy() { 115 | #if DEBUG 116 | if runProfile { 117 | _fakeConsumer.invalidate() 118 | } 119 | #endif 120 | _playbackTimer.invalidate() 121 | pause() 122 | readClosure = { _, _ in (0, false) } 123 | eventPipeline.removeDelegate() 124 | } 125 | } 126 | 127 | // MARK: - Open API 128 | 129 | extension AUPlayer { 130 | func pause() { 131 | #if DEBUG 132 | if runProfile { 133 | _fakeConsumer.pause() 134 | return 135 | } 136 | #endif 137 | guard let audioGraph = _audioGraph else { return } 138 | let result = AUGraphStop(audioGraph) 139 | guard result == noErr else { 140 | let msg = result.check() ?? "\(result)" 141 | eventPipeline.call(.error(.player(msg))) 142 | return 143 | } 144 | state = .paused 145 | _playbackTimer.pause() 146 | } 147 | 148 | func resume() { 149 | #if DEBUG 150 | if runProfile { 151 | _fakeConsumer.resume() 152 | return 153 | } 154 | #endif 155 | guard let graph = _audioGraph else { return } 156 | if state == .paused { 157 | state = .running 158 | let result = AUGraphStart(graph) 159 | guard result == noErr else { 160 | let msg = result.check() ?? "\(result)" 161 | eventPipeline.call(.error(.player(msg))) 162 | return 163 | } 164 | _config.startBackgroundTask(isToDownloadImage: false) 165 | _playbackTimer.resume() 166 | } else if state == .idle { 167 | _progress = 0 168 | if audioGraphIsRunning() { return } 169 | do { 170 | try AUGraphStart(graph).throwCheck() 171 | _config.startBackgroundTask(isToDownloadImage: false) 172 | state = .running 173 | _playbackTimer.resume() 174 | } catch { 175 | if let err = error as? APlay.Error { 176 | eventPipeline.call(.error(err)) 177 | } else { 178 | eventPipeline.call(.unknown(error)) 179 | } 180 | } 181 | } 182 | } 183 | 184 | func currentTime() -> Float { 185 | return _stateQueue.sync { _progress / Float(asbd.mSampleRate) + startTime } 186 | } 187 | 188 | func toggle() { 189 | state == .paused ? resume() : pause() 190 | } 191 | 192 | var volume: Float { 193 | get { return _volume } 194 | set { 195 | _volume = newValue 196 | #if os(iOS) 197 | if let unit = _mixerUnit { 198 | AudioUnitSetParameter(unit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Output, 0, _volume, 0) 199 | } 200 | #else 201 | if let unit = _mixerUnit { 202 | AudioUnitSetParameter(unit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Output, 0, _volume, 0) 203 | } else if let unit = _outputUnit { 204 | AudioUnitSetParameter(unit, kHALOutputParam_Volume, kAudioUnitScope_Output, AUPlayer.Bus.output, _volume, 0) 205 | } 206 | #endif 207 | } 208 | } 209 | } 210 | 211 | // MARK: - Create 212 | 213 | private extension AUPlayer { 214 | func updateAudioGraph(asbd: AudioStreamBasicDescription) { 215 | self.asbd = asbd 216 | let volumeBefore = volume 217 | if _audioGraph != nil { 218 | volume = 0 219 | pause() 220 | connectGraph() 221 | resume() 222 | volume = volumeBefore 223 | return 224 | } 225 | do { 226 | try NewAUGraph(&_audioGraph).throwCheck() 227 | guard let graph = _audioGraph else { return } 228 | try AUGraphOpen(graph).throwCheck() 229 | createEqUnit() 230 | createMixerUnit() 231 | createOutputUnit() 232 | connectGraph() 233 | try AUGraphInitialize(graph).throwCheck() 234 | volume = volumeBefore 235 | } catch { 236 | if let e = error as? APlay.Error { 237 | eventPipeline.call(.error(e)) 238 | } else { 239 | eventPipeline.call(.unknown(error)) 240 | } 241 | } 242 | } 243 | 244 | private func createEqUnit() { 245 | #if os(OSX) 246 | guard #available(OSX 10.9, *) else { return } 247 | #endif 248 | let _options = _config 249 | guard let value = _options.equalizerBandFrequencies[ap_safe: 0], value != 0, let audioGraph = _audioGraph else { return } 250 | do { 251 | try AUGraphAddNode(audioGraph, &AUPlayer.nbandUnit, &_eqNode).throwCheck() 252 | try AUGraphNodeInfo(audioGraph, _eqNode, nil, &_eqUnit).throwCheck() 253 | guard let eqUnit = _eqUnit else { return } 254 | let size = MemoryLayout.size(ofValue: Player.maxFramesPerSlice) 255 | try AudioUnitSetProperty(eqUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &Player.maxFramesPerSlice, UInt32(size)).throwCheck() 256 | _eqBandCount = UInt32(_options.equalizerBandFrequencies.count) 257 | let eqBandSize = UInt32(MemoryLayout.size(ofValue: _eqBandCount)) 258 | try AudioUnitSetProperty(eqUnit, kAUNBandEQProperty_NumberOfBands, kAudioUnitScope_Global, 0, &_eqBandCount, eqBandSize).throwCheck() 259 | let count = Int(_eqBandCount) 260 | for i in 0 ..< count { 261 | let value = _options.equalizerBandFrequencies[i] 262 | let index = UInt32(i) 263 | try AudioUnitSetParameter(eqUnit, kAUNBandEQParam_Frequency + index, kAudioUnitScope_Global, 0, value, 0).throwCheck() 264 | try AudioUnitSetParameter(eqUnit, kAUNBandEQParam_BypassBand + index, kAudioUnitScope_Global, 0, 0, 0).throwCheck() 265 | // try AudioUnitSetParameter(eqUnit, kAUNBandEQParam_Gain + index, kAudioUnitScope_Global, 0, gain, 0).throwCheck() 266 | } 267 | 268 | } catch let APlay.Error.player(err) { 269 | eventPipeline.call(.error(.player(err))) 270 | } catch { 271 | eventPipeline.call(.unknown(error)) 272 | } 273 | } 274 | 275 | private func createMixerUnit() { 276 | let _options = _config 277 | guard _options.isEnabledVolumeMixer, let graph = _audioGraph else { return } 278 | do { 279 | try AUGraphAddNode(graph, &AUPlayer.mixer, &_mixerNode).throwCheck() 280 | try AUGraphNodeInfo(graph, _mixerNode, &AUPlayer.mixer, &_mixerUnit).throwCheck() 281 | guard let mixerUnit = _mixerUnit else { return } 282 | let size = UInt32(MemoryLayout.size(ofValue: Player.maxFramesPerSlice)) 283 | try AudioUnitSetProperty(mixerUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &Player.maxFramesPerSlice, size).throwCheck() 284 | var busCount: UInt32 = 1 285 | let busCountSize = UInt32(MemoryLayout.size(ofValue: busCount)) 286 | try AudioUnitSetProperty(mixerUnit, kAudioUnitProperty_ElementCount, kAudioUnitScope_Input, 0, &busCount, busCountSize).throwCheck() 287 | var graphSampleRate: Float64 = 44100 288 | let graphSampleRateSize = UInt32(MemoryLayout.size(ofValue: graphSampleRate)) 289 | try AudioUnitSetProperty(mixerUnit, kAudioUnitProperty_SampleRate, kAudioUnitScope_Output, 0, &graphSampleRate, graphSampleRateSize).throwCheck() 290 | try AudioUnitSetParameter(mixerUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, 0, 1, 0).throwCheck() 291 | } catch let APlay.Error.player(err) { 292 | eventPipeline.call(.error(.player(err))) 293 | } catch { 294 | eventPipeline.call(.unknown(error)) 295 | } 296 | } 297 | 298 | private func createOutputUnit() { 299 | guard let audioGraph = _audioGraph else { return } 300 | do { 301 | try AUGraphAddNode(audioGraph, &AUPlayer.outputUnit, &_outputNode).throwCheck() 302 | try AUGraphNodeInfo(audioGraph, _outputNode, &AUPlayer.outputUnit, &_outputUnit).throwCheck() 303 | guard let unit = _outputUnit else { return } 304 | let s = MemoryLayout.size(ofValue: Player.canonical) 305 | try AudioUnitSetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, Player.Bus.output, &Player.canonical, UInt32(s)).throwCheck() 306 | } catch let APlay.Error.player(err) { 307 | eventPipeline.call(.error(.player(err))) 308 | } catch { 309 | eventPipeline.call(.unknown(error)) 310 | } 311 | } 312 | 313 | private func connectGraph() { 314 | guard let audioGraph = _audioGraph else { return } 315 | AUGraphClearConnections(audioGraph) 316 | for node in _converterNodes { 317 | AUGraphRemoveNode(audioGraph, node).check() 318 | } 319 | _converterNodes.removeAll() 320 | var nodes: [AUNode] = [] 321 | var units: [AudioUnit] = [] 322 | if let unit = _eqUnit { 323 | if _equalizerEnabled { 324 | nodes.append(_eqNode) 325 | units.append(unit) 326 | _equalizerOn = true 327 | } else { 328 | _equalizerOn = false 329 | } 330 | } else { 331 | _equalizerOn = false 332 | } 333 | 334 | if let unit = _mixerUnit { 335 | nodes.append(_mixerNode) 336 | units.append(unit) 337 | } 338 | 339 | if let unit = _outputUnit { 340 | nodes.append(_outputNode) 341 | units.append(unit) 342 | } 343 | if let node = nodes.first, let unit = units.first { 344 | setOutputCallback(for: node, unit: unit) 345 | } else { 346 | #if DEBUG 347 | fatalError("Output Callback Not Set!!!!!!!") 348 | #endif 349 | } 350 | for i in 0 ..< nodes.count - 1 { 351 | let node = nodes[i] 352 | let nextNode = nodes[i + 1] 353 | let unit = units[i] 354 | let nextUnit = units[i + 1] 355 | connect(node: node, destNode: nextNode, unit: unit, destUnit: nextUnit) 356 | } 357 | } 358 | 359 | func setOutputCallback(for node: AUNode, unit: AudioUnit) { 360 | var status: OSStatus = noErr 361 | let pointer = UnsafeMutableRawPointer.from(object: self) 362 | var callbackStruct = AURenderCallbackStruct(inputProc: renderCallback, inputProcRefCon: pointer) 363 | let sizeOfASBD = MemoryLayout.size(ofValue: asbd) 364 | status = AudioUnitSetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &asbd, UInt32(sizeOfASBD)) 365 | guard let audioGraph = _audioGraph else { 366 | #if DEBUG 367 | fatalError("Output Callback Not Set!!!!!!!") 368 | #else 369 | return 370 | #endif 371 | } 372 | do { 373 | if status == noErr { 374 | try AUGraphSetNodeInputCallback(audioGraph, node, 0, &callbackStruct).throwCheck() 375 | } else { 376 | var format: AudioStreamBasicDescription = AudioStreamBasicDescription() 377 | var size = UInt32(MemoryLayout.size) 378 | try AudioUnitGetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &format, &size).throwCheck() 379 | let converterNode = createConverterNode(for: asbd, destFormat: format) 380 | guard let c = converterNode else { 381 | #if DEBUG 382 | fatalError("Output Callback Not Set!!!!!!!") 383 | #else 384 | return 385 | #endif 386 | } 387 | try AUGraphSetNodeInputCallback(audioGraph, c, 0, &callbackStruct).throwCheck() 388 | try AUGraphConnectNodeInput(audioGraph, c, 0, node, 0).throwCheck() 389 | } 390 | } catch let APlay.Error.player(err) { 391 | eventPipeline.call(.error(.player(err))) 392 | } catch { 393 | eventPipeline.call(.unknown(error)) 394 | } 395 | } 396 | 397 | func connect(node: AUNode, destNode: AUNode, unit: AudioUnit, destUnit: AudioUnit) { 398 | guard let audioGraph = _audioGraph else { return } 399 | var status: OSStatus = noErr 400 | var needConverter = false 401 | var srcFormat: AudioStreamBasicDescription = AudioStreamBasicDescription() 402 | var desFormat: AudioStreamBasicDescription = AudioStreamBasicDescription() 403 | var size = UInt32(MemoryLayout.size) 404 | do { 405 | try AudioUnitGetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &srcFormat, &size).throwCheck() 406 | try AudioUnitGetProperty(destUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &desFormat, &size).throwCheck() 407 | 408 | needConverter = memcmp(&srcFormat, &desFormat, MemoryLayout.size(ofValue: srcFormat)) != 0 409 | if needConverter { 410 | status = AudioUnitSetProperty(destUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &srcFormat, UInt32(MemoryLayout.size(ofValue: srcFormat))) 411 | needConverter = status != noErr 412 | } 413 | if needConverter { 414 | if let convertNode = createConverterNode(for: srcFormat, destFormat: desFormat) { 415 | try AUGraphConnectNodeInput(audioGraph, node, 0, convertNode, 0).throwCheck() 416 | try AUGraphConnectNodeInput(audioGraph, convertNode, 0, destNode, 0).throwCheck() 417 | } 418 | 419 | } else { 420 | try AUGraphConnectNodeInput(audioGraph, node, 0, destNode, 0).throwCheck() 421 | } 422 | } catch let APlay.Error.player(err) { 423 | eventPipeline.call(.error(.player(err))) 424 | } catch { 425 | eventPipeline.call(.unknown(error)) 426 | } 427 | } 428 | 429 | func createConverterNode(for format: AudioStreamBasicDescription, destFormat: AudioStreamBasicDescription) -> AUNode? { 430 | guard let audioGraph = _audioGraph else { return nil } 431 | var convertNode = AUNode() 432 | var convertUnit: AudioUnit? 433 | do { 434 | try AUGraphAddNode(audioGraph, &AUPlayer.convertUnit, &convertNode).throwCheck() 435 | try AUGraphNodeInfo(audioGraph, convertNode, &AUPlayer.mixer, &convertUnit).throwCheck() 436 | guard let unit = convertUnit else { return nil } 437 | var srcFormat = format 438 | try AudioUnitSetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &srcFormat, UInt32(MemoryLayout.size(ofValue: format))).throwCheck() 439 | var desFormat = destFormat 440 | try AudioUnitSetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &desFormat, UInt32(MemoryLayout.size(ofValue: destFormat))).throwCheck() 441 | try AudioUnitSetProperty(unit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &Player.maxFramesPerSlice, UInt32(MemoryLayout.size(ofValue: Player.maxFramesPerSlice))).throwCheck() 442 | _converterNodes.append(convertNode) 443 | return convertNode 444 | } catch let APlay.Error.player(err) { 445 | eventPipeline.call(.error(.player(err))) 446 | return nil 447 | } catch { 448 | eventPipeline.call(.unknown(error)) 449 | return nil 450 | } 451 | } 452 | 453 | private func audioGraphIsRunning() -> Bool { 454 | guard let graph = _audioGraph else { return false } 455 | var isRuning: DarwinBoolean = false 456 | guard AUGraphIsRunning(graph, &isRuning) == noErr else { return false } 457 | return isRuning.boolValue 458 | } 459 | } 460 | 461 | // MARK: - Model 462 | 463 | extension AUPlayer { 464 | static var outputUnit: AudioComponentDescription = { 465 | #if os(OSX) 466 | let subType = kAudioUnitSubType_DefaultOutput 467 | #else 468 | let subType = kAudioUnitSubType_RemoteIO 469 | #endif 470 | let component = AudioComponentDescription(componentType: kAudioUnitType_Output, componentSubType: subType, componentManufacturer: kAudioUnitManufacturer_Apple, componentFlags: 0, componentFlagsMask: 0) 471 | return component 472 | }() 473 | 474 | static var canonicalSize: UInt32 = { 475 | UInt32(MemoryLayout.size(ofValue: Player.canonical)) 476 | }() 477 | 478 | static var convertUnit: AudioComponentDescription = { 479 | let component = AudioComponentDescription(componentType: kAudioUnitType_FormatConverter, componentSubType: kAudioUnitSubType_AUConverter, componentManufacturer: kAudioUnitManufacturer_Apple, componentFlags: 0, componentFlagsMask: 0) 480 | return component 481 | }() 482 | 483 | static var mixer: AudioComponentDescription = { 484 | let component = AudioComponentDescription(componentType: kAudioUnitType_Mixer, componentSubType: kAudioUnitSubType_MultiChannelMixer, componentManufacturer: kAudioUnitManufacturer_Apple, componentFlags: 0, componentFlagsMask: 0) 485 | return component 486 | }() 487 | 488 | static func record() -> AudioStreamBasicDescription { 489 | var component = AudioStreamBasicDescription() 490 | component.mFormatID = kAudioFormatMPEG4AAC 491 | component.mFormatFlags = AudioFormatFlags(MPEG4ObjectID.AAC_LC.rawValue) 492 | component.mChannelsPerFrame = Player.canonical.mChannelsPerFrame 493 | component.mSampleRate = Player.canonical.mSampleRate 494 | return component 495 | } 496 | 497 | static var nbandUnit: AudioComponentDescription = { 498 | let component = AudioComponentDescription(componentType: kAudioUnitType_Effect, componentSubType: kAudioUnitSubType_NBandEQ, componentManufacturer: kAudioUnitManufacturer_Apple, componentFlags: 0, componentFlagsMask: 0) 499 | return component 500 | }() 501 | } 502 | 503 | // MARK: - AURenderCallback 504 | 505 | /// renderCallback 506 | private func renderCallback(userInfo: UnsafeMutableRawPointer, ioActionFlags: UnsafeMutablePointer, inTimeStamp _: UnsafePointer, inBusNumber _: UInt32, inNumberFrames: UInt32, ioData: UnsafeMutablePointer?) -> OSStatus { 507 | let sself = userInfo.to(object: AUPlayer.self) 508 | 509 | let size = sself._pcmBufferFrameSizeInBytes * inNumberFrames 510 | ioData?.pointee.mBuffers.mNumberChannels = 2 511 | ioData?.pointee.mBuffers.mDataByteSize = size 512 | let raw = sself._buffers.advanced(by: sself._currentIndex * sself._pageSize) 513 | 514 | sself.increaseBufferIndex() 515 | let (readSize, _) = sself.readClosure(size, raw) 516 | var totalReadFrame: UInt32 = inNumberFrames 517 | ioActionFlags.pointee = AudioUnitRenderActionFlags.unitRenderAction_PreRender 518 | if readSize == 0 { 519 | ioActionFlags.pointee = AudioUnitRenderActionFlags.unitRenderAction_OutputIsSilence 520 | memset(raw, 0, Int(size)) 521 | return noErr 522 | } else if readSize != size { 523 | totalReadFrame = readSize / sself._pcmBufferFrameSizeInBytes 524 | let left = size - readSize 525 | memset(raw.advanced(by: Int(readSize)), 0, Int(left)) 526 | } 527 | ioData?.pointee.mBuffers.mData = UnsafeMutableRawPointer(raw) 528 | 529 | sself._stateQueue.async(flags: .barrier) { sself._progress += Float(totalReadFrame) } 530 | return noErr 531 | } 532 | -------------------------------------------------------------------------------- /APlay/BuildInComponents/TagParser/FlacParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlacParser.swift 3 | // APlay 4 | // 5 | // Created by lincoln on 2018/5/29. 6 | // Copyright © 2018年 SelfStudio. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | import Foundation 11 | 12 | final class FlacParser { 13 | private lazy var _outputStream = Delegated() 14 | private lazy var _data = Data() 15 | private lazy var _backupHeaderData = Data() 16 | private lazy var _isHeaderParserd = false 17 | private lazy var _state: MetadataParser.State = .initial 18 | private lazy var _queue = DispatchQueue(concurrentName: "FlacParser") 19 | private var _flacMetadata: FlacMetadata? 20 | init(config _: ConfigurationCompatible) {} 21 | } 22 | 23 | extension FlacParser: MetadataParserCompatible { 24 | var outputStream: Delegated { 25 | return _outputStream 26 | } 27 | 28 | func acceptInput(data: UnsafeMutablePointer, count: UInt32) { 29 | guard _state.isNeedData else { return } 30 | _queue.async(flags: .barrier) { self.appendTagData(data, count: count) } 31 | _queue.sync { 32 | if _state == .initial, _data.count < 4 { return } 33 | parse() 34 | } 35 | } 36 | } 37 | 38 | // MARK: - Private 39 | 40 | extension FlacParser { 41 | private func appendTagData(_ data: UnsafeMutablePointer, count: UInt32) { 42 | let bytesSize = Int(count) 43 | let raw = UnsafeMutablePointer.uint8Pointer(of: bytesSize) 44 | defer { free(raw) } 45 | memcpy(raw, data, bytesSize) 46 | let dat = Data(bytes: raw, count: bytesSize) 47 | _data.append(dat) 48 | _backupHeaderData.append(dat) 49 | } 50 | 51 | private func parse() { 52 | if _state == .initial { 53 | guard let head = String(data: _data[0 ..< 4], encoding: .ascii), head == FlacMetadata.tag else { 54 | _state = .error("Not a flac file") 55 | return 56 | } 57 | _data = _data.advanced(by: 4) 58 | _state = .parsering 59 | } 60 | var hasBlock = true 61 | while hasBlock { 62 | guard _data.count >= FlacMetadata.Header.size else { return } 63 | let bytes = _data[0 ..< FlacMetadata.Header.size] 64 | let header = FlacMetadata.Header(bytes: bytes) 65 | let blockSize = Int(header.metadataBlockDataSize) 66 | let blockLengthPosition = FlacMetadata.Header.size + blockSize 67 | guard _data.count >= blockLengthPosition else { return } 68 | _data = _data.advanced(by: FlacMetadata.Header.size) 69 | switch header.blockType { 70 | case .streamInfo: 71 | let streamInfo = FlacMetadata.StreamInfo(data: _data, header: header) 72 | _flacMetadata = FlacMetadata(streamInfo: streamInfo) 73 | case .seektable: 74 | let tables = FlacMetadata.SeekTable(bytes: _data, header: header) 75 | _flacMetadata?.seekTable = tables 76 | case .padding: 77 | let padding = FlacMetadata.Padding(header: header, length: UInt32(header.metadataBlockDataSize)) 78 | _flacMetadata?.addPadding(padding) 79 | case .application: 80 | let app = FlacMetadata.Application(bytes: _data, header: header) 81 | _flacMetadata?.application = app 82 | case .cueSheet: 83 | let cue = FlacMetadata.CUESheet(bytes: _data, header: header) 84 | _flacMetadata?.cueSheet = cue 85 | case .vorbisComments: 86 | let comment = FlacMetadata.VorbisComments(bytes: _data, header: header) 87 | _flacMetadata?.vorbisComments = comment 88 | case .picture: 89 | let picture = FlacMetadata.Picture(bytes: _data, header: header) 90 | _flacMetadata?.picture = picture 91 | case .undifined: print("Flac metadta header error, undifined block type") 92 | } 93 | _data = _data.advanced(by: blockSize) 94 | hasBlock = header.isLastMetadataBlock == false 95 | if hasBlock == false { 96 | _state = .complete 97 | if var value = _flacMetadata { 98 | _outputStream.call(.tagSize(value.totalSize())) 99 | if let meta = value.vorbisComments?.asMetadata() { 100 | _outputStream.call(.metadata(meta)) 101 | } 102 | let size = value.totalSize() 103 | value.headerData = Data(_backupHeaderData[0 ..< Int(size)]) 104 | _backupHeaderData = Data() 105 | _outputStream.call(.flac(value)) 106 | } 107 | _outputStream.call(.end) 108 | } 109 | } 110 | } 111 | } 112 | 113 | // MARK: - FlacMetadata 114 | 115 | /// https://xiph.org/flac/format.html#metadata_block_data 116 | public struct FlacMetadata { 117 | static let tag = "fLaC" 118 | public let streamInfo: StreamInfo 119 | fileprivate(set) var headerData: Data = Data() 120 | public fileprivate(set) var vorbisComments: VorbisComments? 121 | public fileprivate(set) var picture: Picture? 122 | public fileprivate(set) var application: Application? 123 | public fileprivate(set) var seekTable: SeekTable? 124 | public fileprivate(set) var cueSheet: CUESheet? 125 | public fileprivate(set) var paddings: [Padding]? 126 | 127 | init(streamInfo: StreamInfo) { self.streamInfo = streamInfo } 128 | 129 | mutating func addPadding(_ padding: Padding) { 130 | var value = paddings ?? [Padding]() 131 | value.append(padding) 132 | paddings = value 133 | } 134 | 135 | func totalSize() -> UInt32 { 136 | var total: UInt32 = 4 137 | let headers = [streamInfo.header, vorbisComments?.header, picture?.header, application?.header, seekTable?.header, cueSheet?.header].compactMap({ $0 }) + (paddings?.compactMap({ $0.header }) ?? []) 138 | total += headers.reduce(0, { $0 + $1.metadataBlockDataSize }) 139 | total += UInt32(headers.count * Header.size) 140 | return total 141 | } 142 | 143 | func nearestOffset(for time: TimeInterval) -> (TimeInterval, UInt64)? { 144 | guard let table = seekTable, table.points.count > 0 else { return nil } 145 | var delta: TimeInterval = 999 146 | var targetTime: TimeInterval = time 147 | var offset: UInt64 = 0 148 | let sampleRate = TimeInterval(streamInfo.sampleRate) 149 | for point in table.points { 150 | let pointTime = TimeInterval(point.sampleNumber) / sampleRate 151 | let pointDelta = abs(time - pointTime) 152 | if pointDelta < delta { 153 | delta = pointDelta 154 | targetTime = pointTime 155 | offset = point.streamOffset 156 | } 157 | } 158 | return (targetTime, offset) 159 | } 160 | 161 | public struct Header { 162 | static let size = 4 163 | public let isLastMetadataBlock: Bool 164 | public let blockType: BlockType 165 | public let metadataBlockDataSize: UInt32 166 | 167 | public enum BlockType: UInt8 { 168 | case streamInfo 169 | case padding 170 | case application 171 | case seektable 172 | case vorbisComments 173 | case cueSheet 174 | case picture 175 | case undifined 176 | 177 | init?(bytes: UInt8) { 178 | let type = bytes & 0x7F 179 | if let value = BlockType(rawValue: type) { self = value } 180 | else { return nil } 181 | } 182 | } 183 | 184 | public init(bytes: Data) { 185 | var data = bytes.advanced(by: 0) 186 | isLastMetadataBlock = (data[0] & 0x80) != 0 187 | let type = BlockType(bytes: data[0]) ?? .undifined 188 | blockType = type 189 | data = data.advanced(by: 1) 190 | metadataBlockDataSize = Array(data[0 ..< 3]).unpack() 191 | } 192 | } 193 | 194 | public struct StreamInfo { 195 | public let header: Header 196 | public let minimumBlockSize: UInt32 197 | public let maximumBlockSize: UInt32 198 | public let minimumFrameSize: UInt32 199 | public let maximumFrameSize: UInt32 200 | public let sampleRate: UInt32 201 | public let channels: UInt32 202 | public let bitsPerSample: UInt32 203 | public let totalSamples: UInt64 204 | public let md5: String 205 | 206 | // https://github.com/xiph/flac/blob/64b7142a3601717a533cd0d7e6ef19f8aaba3db8/src/libFLAC/metadata_iterators.c#L2177-L2200 207 | init(data: Data, header: Header) { 208 | self.header = header 209 | let point0 = data.startIndex 210 | let point2 = point0 + 2 211 | let point4 = point2 + 2 212 | let point7 = point4 + 3 213 | let point10 = point7 + 3 214 | let point12 = point10 + 2 215 | let point13 = point12 + 1 216 | let point14 = point13 + 1 217 | let point18 = point14 + 4 218 | let point34 = point18 + 16 219 | 220 | minimumBlockSize = Array(data[point0 ..< point2]).unpack() 221 | maximumBlockSize = Array(data[point2 ..< point4]).unpack() 222 | minimumFrameSize = Array(data[point4 ..< point7]).unpack() 223 | maximumFrameSize = Array(data[point7 ..< point10]).unpack() 224 | 225 | let a = Array(data[point10 ..< point12]).unpack() << 4 226 | sampleRate = a | (UInt32(data[point12]) & 0xF0) >> 4 227 | channels = (UInt32(data[point12]) & 0x0E) >> 1 + 1 228 | bitsPerSample = (UInt32(data[point12]) & 0x01) << 4 | (UInt32(data[point13]) & 0xF0) >> 4 + 1 229 | totalSamples = (UInt64(data[point13]) & 0x0F) << 32 | Array(data[point14 ..< point18]).unpackUInt64() 230 | md5 = data[point18 ..< point34].compactMap({ Optional(String(format: "%02x", $0)) }).joined() 231 | } 232 | } 233 | // https://github.com/xiph/flac/blob/64b7142a3601717a533cd0d7e6ef19f8aaba3db8/src/libFLAC/metadata_iterators.c#L2303-L2353 234 | public struct VorbisComments { 235 | public let header: Header 236 | public let vendor: String 237 | public let metadata: [Field: String] 238 | public let rawMeta: [String] 239 | 240 | init(bytes: Data, header: Header) { 241 | self.header = header 242 | var data = bytes.advanced(by: 0) 243 | let vendorLength = Int(Array(data[0 ..< 4]).unpack(isLittleEndian: true)) 244 | data = data.advanced(by: 4) 245 | let vendorData = data[0 ..< vendorLength] 246 | vendor = String(data: vendorData, encoding: .utf8) ?? "" 247 | data = data.advanced(by: vendorLength) 248 | let commentsCount = Array(data[0 ..< 4]).unpack(isLittleEndian: true) 249 | data = data.advanced(by: 4) 250 | var map: [Field: String] = [:] 251 | var metas: [String] = [] 252 | for _ in 0 ..< commentsCount { 253 | let length = Int(Array(data[0 ..< 4]).unpack(isLittleEndian: true)) 254 | data = data.advanced(by: 4) 255 | let strData = data[0 ..< length] 256 | guard let value = String(data: strData, encoding: .utf8) else { continue } 257 | data = data.advanced(by: length) 258 | metas.append(value) 259 | let kv = value.split(separator: "=") 260 | if kv.count == 2, let key = Field(rawValue: String(kv[0])) { 261 | map[key] = String(kv[1]) 262 | } 263 | } 264 | rawMeta = metas 265 | metadata = map 266 | } 267 | 268 | public func asMetadata() -> [MetadataParser.Item] { 269 | var ret: [MetadataParser.Item] = [] 270 | for (key, value) in metadata { 271 | switch key { 272 | case .album: ret.append(.album(value)) 273 | case .title: ret.append(.title(value)) 274 | case .trackNumber: ret.append(.track(value)) 275 | case .atrist: ret.append(.artist(value)) 276 | case .genre: ret.append(.genre(value)) 277 | case .date: ret.append(.year(value)) 278 | case .contact: ret.append(.comment(value)) 279 | default: break 280 | } 281 | } 282 | return ret 283 | } 284 | 285 | public enum Field: String { 286 | case title = "TITLE" 287 | case version = "VERSION" 288 | case album = "ALBUM" 289 | case trackNumber = "TRACKNUMBER" 290 | case atrist = "ARTIST" 291 | case performer = "PERFORMER" 292 | case copyright = "COPYRIGHT" 293 | case license = "LICENSE" 294 | case organization = "ORGANIZATION" 295 | case description = "DESCRIPTION" 296 | case genre = "GENRE" 297 | case date = "DATE" 298 | case location = "LOCATION" 299 | case contact = "CONTACT" 300 | case isrc = "ISRC" 301 | } 302 | } 303 | 304 | public struct Picture { 305 | public let header: Header 306 | public let type: MetadataParser.PictureType 307 | public let mimeType: String 308 | public let desc: String 309 | public let size: CGSize 310 | public let colorDepth: UInt32 311 | /// For indexed-color pictures (e.g. GIF), the number of colors used, or 0 for non-indexed pictures. 312 | public let colorUsed: UInt32 313 | public let length: UInt32 314 | public let picData: Data 315 | 316 | init(bytes: Data, header: Header) { 317 | self.header = header 318 | var data = bytes.advanced(by: 0) 319 | let value = Array(data[0 ..< 4]).unpack() 320 | type = MetadataParser.PictureType(rawValue: UInt8(value)) ?? .undifined 321 | data = data.advanced(by: 4) 322 | let mimeTypeLength = Int(Array(data[0 ..< 4]).unpack()) 323 | data = data.advanced(by: 4) 324 | let mimeTypeData = data[0 ..< mimeTypeLength] 325 | mimeType = String(data: mimeTypeData, encoding: .ascii) ?? "" 326 | data = data.advanced(by: mimeTypeLength) 327 | let descLength = Int(Array(data[0 ..< 4]).unpack()) 328 | data = data.advanced(by: 4) 329 | if descLength > 0 { 330 | let descData = data[0 ..< descLength] 331 | desc = String(data: descData, encoding: .utf8) ?? "" 332 | data = data.advanced(by: descLength) 333 | } else { 334 | desc = "" 335 | } 336 | let width = Array(data[0 ..< 4]).unpack() 337 | data = data.advanced(by: 4) 338 | let height = Array(data[0 ..< 4]).unpack() 339 | data = data.advanced(by: 4) 340 | size = CGSize(width: CGFloat(width), height: CGFloat(height)) 341 | colorDepth = Array(data[0 ..< 4]).unpack() 342 | data = data.advanced(by: 4) 343 | colorUsed = Array(data[0 ..< 4]).unpack() 344 | data = data.advanced(by: 4) 345 | length = Array(data[0 ..< 4]).unpack() 346 | data = data.advanced(by: 4) 347 | picData = data[0 ..< Int(length)] 348 | } 349 | } 350 | 351 | public struct Padding { 352 | public let header: Header 353 | /// in bytes 354 | public let length: UInt32 355 | } 356 | 357 | public struct CUESheet { 358 | public let header: Header 359 | public let mediaCatalogNumber: String 360 | public let leadIn: UInt64 361 | public let isCD: Bool 362 | public let tracks: [Track] 363 | 364 | init(bytes: Data, header: Header) { 365 | self.header = header 366 | 367 | var data = bytes.advanced(by: 0) 368 | mediaCatalogNumber = String(data: data[0 ..< 128], encoding: .ascii)?.trimZeroTerminator() ?? "" 369 | data = data.advanced(by: 128) 370 | leadIn = Array(data[0 ..< 8]).unpackUInt64() 371 | data = data.advanced(by: 8) 372 | isCD = (UInt32(data[0]) & 0x80) != 0 373 | data = data.advanced(by: 258 + 1) 374 | let tracksCount = data[0] 375 | data = data.advanced(by: 1) 376 | var tracks: [Track] = [] 377 | for _ in 0 ..< tracksCount { 378 | let offset = Array(data[0 ..< 8]).unpackUInt64() 379 | data = data.advanced(by: 8) 380 | let number = data[0] 381 | data = data.advanced(by: 1) 382 | let isrc = String(data: data[0 ..< 12], encoding: .ascii)?.trimZeroTerminator() ?? "" 383 | data = data.advanced(by: 12) 384 | let isAudio = UInt32(data[0]) & 0x80 == 0 385 | let isPreEmphasis = UInt32(data[0]) & 0x70 != 0 386 | data = data.advanced(by: 1 + 13) 387 | let numberOfIndexPoints = data[0] 388 | data = data.advanced(by: 1) 389 | var indexPoints: [Track.Index] = [] 390 | if numberOfIndexPoints > 0 { 391 | for _ in 0 ..< numberOfIndexPoints { 392 | let size = Track.Index.size 393 | let pointData = data[0 ..< size] 394 | data = data.advanced(by: size) 395 | let offset = Array(pointData[0 ..< 8]).unpackUInt64() 396 | let number = pointData[8] 397 | let idx = Track.Index(offset: offset, number: number) 398 | indexPoints.append(idx) 399 | } 400 | } 401 | let track = Track(offset: offset, number: number, isrc: isrc, isAudio: isAudio, isPreEmphasis: isPreEmphasis, numberOfIndexPoints: numberOfIndexPoints, indexPoints: indexPoints) 402 | tracks.append(track) 403 | } 404 | self.tracks = tracks 405 | } 406 | 407 | public struct Track { 408 | public let offset: UInt64 409 | public let number: UInt8 410 | public let isrc: String 411 | public let isAudio: Bool 412 | public let isPreEmphasis: Bool 413 | public let numberOfIndexPoints: UInt8 414 | public let indexPoints: [Index] 415 | 416 | public struct Index { 417 | static let size = 8 + 1 + 3 418 | public let offset: UInt64 419 | public let number: UInt8 420 | } 421 | } 422 | } 423 | 424 | public struct Application { 425 | public let header: Header 426 | public let name: String 427 | public let data: Data 428 | 429 | init(bytes: Data, header: Header) { 430 | self.header = header 431 | let point0 = bytes.startIndex 432 | let point4 = point0 + 4 433 | let value = Array(bytes[point0 ..< point4]).unpack() 434 | name = String(from: value)?.trimZeroTerminator() ?? "\(value)" 435 | data = Data(bytes[point4 ..< Int(header.metadataBlockDataSize)]) 436 | } 437 | } 438 | 439 | public struct SeekTable { 440 | public let header: Header 441 | public let points: [SeekPoint] 442 | 443 | init(bytes: Data, header: Header) { 444 | self.header = header 445 | let size = Int(header.metadataBlockDataSize) 446 | let totalPoints = Int(size / 18) 447 | var pointTable: [SeekPoint] = [] 448 | let startIndex = bytes.startIndex 449 | for i in 0 ..< totalPoints { 450 | let offset = i * 18 + startIndex 451 | let end = offset + 18 452 | let point = SeekPoint(bytes: bytes[offset ..< end]) 453 | pointTable.append(point) 454 | } 455 | points = pointTable.sorted(by: { $0.sampleNumber < $1.sampleNumber }) 456 | } 457 | 458 | public struct SeekPoint: Hashable, CustomStringConvertible { 459 | private static let placeHolder: UInt64 = 0xFFFF_FFFF_FFFF_FFFF 460 | public let sampleNumber: UInt64 461 | public let streamOffset: UInt64 462 | public let frameSamples: UInt32 463 | 464 | init(bytes: Data) { 465 | let point0 = bytes.startIndex 466 | let point8 = point0 + 8 467 | let point16 = point8 + 8 468 | let point18 = point16 + 2 469 | sampleNumber = bytes[point0 ..< point8].compactMap({ $0 }).unpackUInt64() 470 | streamOffset = bytes[point8 ..< point16].compactMap({ $0 }).unpackUInt64() 471 | frameSamples = bytes[point16 ..< point18].compactMap({ $0 }).unpack() 472 | } 473 | 474 | public var description: String { 475 | let clz = "\(type(of: self))" 476 | if sampleNumber == SeekPoint.placeHolder { return "\(clz).PlaceHolder" } 477 | return "\(clz)(sampleNumber: \(sampleNumber), streamOffset:\(streamOffset), frameSamples:\(frameSamples))" 478 | } 479 | } 480 | } 481 | } 482 | -------------------------------------------------------------------------------- /APlay/BuildInComponents/TagParser/ID3Parser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ID3Parser.swift 3 | // APlay 4 | // 5 | // Created by lincoln on 2018/6/6. 6 | // Copyright © 2018年 SelfStudio. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | final class ID3Parser { 11 | private lazy var _outputStream = Delegated() 12 | private lazy var _data = Data() 13 | private lazy var _queue = DispatchQueue(concurrentName: "ID3Parser") 14 | // parser stuff 15 | private lazy var _nextParseStartAt = 0 16 | private lazy var _metadatas: [Version: [MetadataParser.Item]] = [:] 17 | private lazy var _isSkippedExtendedHeader = false 18 | private unowned let _config: ConfigurationCompatible 19 | 20 | private var _v2Info: ID3V2? { 21 | didSet { 22 | guard let info = _v2Info else { return } 23 | debug_log("\(info)") 24 | } 25 | } 26 | 27 | private var _v2State = MetadataParser.State.initial { didSet { dispatchEvent() } } 28 | private var _v1State = MetadataParser.State.initial { didSet { dispatchEvent() } } 29 | 30 | init(config: ConfigurationCompatible) { 31 | _config = config 32 | } 33 | } 34 | 35 | // MARK: - State Handler 36 | 37 | private extension ID3Parser { 38 | func dispatchEvent() { 39 | guard _v1State.isDone, _v2State.isDone else { return } 40 | _data = Data() 41 | var size = _v2Info?.size ?? 0 42 | if case MetadataParser.State.complete = _v1State { 43 | size += 128 44 | } 45 | outputStream.call(.tagSize(size)) 46 | var metas: [MetadataParser.Item] = _metadatas[.v1] ?? (_metadatas[.v11] ?? []) 47 | for (ver, list) in _metadatas { 48 | guard ver != .v1, ver != .v11 else { continue } 49 | metas.append(contentsOf: list) 50 | } 51 | outputStream.call(.metadata(metas)) 52 | debug_log("ID3Parser parse done, read \(_nextParseStartAt) bytes") 53 | } 54 | } 55 | 56 | // MARK: - MetadataParserCompatible 57 | 58 | extension ID3Parser: MetadataParserCompatible { 59 | var outputStream: Delegated { return _outputStream } 60 | 61 | func acceptInput(data: UnsafeMutablePointer, count: UInt32) { 62 | guard _v2State.isNeedData else { return } 63 | _queue.async(flags: .barrier) { self.appendTagData(data, count: count) } 64 | _queue.sync { 65 | if _v2State == .initial, _data.count < 10 { return } 66 | parse() 67 | } 68 | } 69 | 70 | func parseID3V1Tag(at url: URL) { 71 | guard _v1State.isDone == false else { return } 72 | let scheme = url.scheme?.lowercased() 73 | let isLocal = scheme == "file" 74 | _queue.async(flags: .barrier) { 75 | isLocal ? self.processingID3V1FromLocal(url: url) : self.processingID3V1FromRemote(url: url) 76 | } 77 | } 78 | } 79 | 80 | // MARK: - ID3v2 Parse 81 | 82 | extension ID3Parser { 83 | private func appendTagData(_ data: UnsafeMutablePointer, count: UInt32) { 84 | if let size = _v2Info?.size, _data.count >= size, _v2State != .initial { return } 85 | let bytesSize = Int(count) 86 | let raw = UnsafeMutablePointer.uint8Pointer(of: bytesSize) 87 | defer { free(raw) } 88 | memcpy(raw, data, bytesSize) 89 | let dat = Data(bytes: raw, count: bytesSize) 90 | _data.append(dat) 91 | } 92 | 93 | /// , , 94 | private func parse() { 95 | // Parser ID3V2 Header 96 | if _v2State == .initial { 97 | guard let header = String(data: _data[ID3V2.header], encoding: .ascii), header == ID3V2.tag else { 98 | _v2State = .error("Not A ID3V2 File") 99 | return 100 | } 101 | let headerCount = ID3V2.header.count 102 | let length = ID3V2.headerFrameLength - headerCount 103 | _data = _data.advanced(by: headerCount) 104 | _v2Info = ID3V2(_data[0 ..< length]) 105 | _data = _data.advanced(by: length) 106 | _nextParseStartAt += ID3V2.headerFrameLength 107 | _v2State = .parsering 108 | } 109 | guard _v2State == .parsering, let info = _v2Info else { return } 110 | // Skip Extended Header 111 | if info.version >= 3, _isSkippedExtendedHeader == false, info.hasExtendedHeader { 112 | guard _data.count >= 4 else { return } 113 | let size = Int(_data[0 ..< 4].compactMap({ $0 }).unpack()) 114 | guard _data.count >= size else { return } 115 | _data = _data.advanced(by: size) 116 | } 117 | // Start Parse Tag 118 | while true { 119 | // The remaining buffer in not enough for a frame, consider it as padding, parse complete 120 | guard _data.count >= info.minimumByteForFrame, _nextParseStartAt < Int(info.size) else { 121 | _v2State = .complete 122 | break 123 | } 124 | // Retrieve Frame Name And Calculate Frame Size 125 | var frameSize: Int = 0 126 | let frameNameData: Data 127 | var readlength = 0 128 | if info.version >= 3 { 129 | frameNameData = _data[0 ..< 4] 130 | readlength = 4 131 | } else { 132 | frameNameData = _data[0 ..< 3] 133 | readlength = 3 134 | } 135 | guard let frameName = String(data: frameNameData, encoding: .utf8)?.trimZeroTerminator() else { 136 | _v2State = .error("pasering wrong frame") 137 | break 138 | } 139 | 140 | let pos = readlength 141 | if info.version == 4 { 142 | frameSize = Int([_data[pos] & 0x7F, _data[pos + 1] & 0x7F, _data[pos + 2] & 0x7F, _data[pos + 3] & 0x7F].unpack(offsetSize: 7)) 143 | readlength += 6 144 | } else if info.version == 3 { 145 | /* 146 | Frame ID $xx xx xx xx (four characters) 147 | Size $xx xx xx xx 148 | Flags $xx xx 149 | */ 150 | frameSize = Int([_data[pos], _data[pos + 1], _data[pos + 2], _data[pos + 3]].unpack()) 151 | // skip 2 byte for flags 152 | readlength += 6 153 | } else { 154 | /* 155 | Frame size $xx xx xx 156 | */ 157 | frameSize = Int([_data[pos], _data[pos + 1], _data[pos + 2]].unpack()) 158 | readlength += 3 159 | } 160 | 161 | // Maybe just padding, add minimum frame size and continue parsing 162 | guard frameSize > 0 else { 163 | let realLength: Int 164 | if info.version >= 3 { 165 | // No flags bytes, minus 2 bytes 166 | realLength = readlength - 2 167 | } else { 168 | realLength = readlength 169 | } 170 | _data = _data.advanced(by: realLength) 171 | _nextParseStartAt += realLength 172 | continue 173 | } 174 | 175 | // Make sure data size is enough for parsing 176 | guard _data.count >= readlength + frameSize else { 177 | if frameName == "", Int(info.size) - _nextParseStartAt < info.minimumByteForFrame { 178 | _v2State = .complete 179 | } 180 | break 181 | } 182 | 183 | // If frame name is empty..., maybe is end of file 184 | if frameName == "", Int(info.size) - _nextParseStartAt < info.minimumByteForFrame { 185 | _v2State = .complete 186 | break 187 | } 188 | _data = _data.advanced(by: readlength) 189 | _nextParseStartAt += readlength 190 | readlength = 0 191 | 192 | // Text encoding is counted in frame size 193 | let encodingIndex = _data[0] 194 | readlength = 1 195 | 196 | if frameName == "APIC" || frameName == "PIC" { 197 | /* 198 |
199 | Text encoding $xx 200 | MIME type $00 201 | Picture type $xx 202 | Description $00 (00) 203 | Picture data 204 | */ 205 | let mimeTypeStartIndex = readlength 206 | var mimeTypeEndIndex = mimeTypeStartIndex 207 | while _data[mimeTypeEndIndex] != 0 { 208 | mimeTypeEndIndex += 1 209 | } 210 | 211 | guard let mimeType = String(data: _data[mimeTypeStartIndex ..< mimeTypeEndIndex], encoding: .utf8) else { 212 | _v2State = .error("Not retreive mime type for image") 213 | break 214 | } 215 | 216 | let picType = MetadataParser.PictureType(rawValue: _data[mimeTypeEndIndex]) ?? .undifined 217 | readlength = mimeTypeEndIndex + 1 218 | // skip desc 219 | while true { 220 | defer { readlength += 1 } 221 | guard _data[readlength] == 0, _data[readlength + 1] != 0 else { continue } 222 | break 223 | } 224 | let data = Data(_data[readlength ..< readlength + frameSize]) 225 | var meta = _metadatas[info.ver] ?? [] 226 | meta.append(.cover(data)) 227 | _metadatas[info.ver] = meta 228 | _config.logger.log("(\(frameName))(\(mimeType))(\(picType))(\(frameSize)bytes)", to: .metadataParser) 229 | } else { 230 | if let encoding = StringEncoding(rawValue: encodingIndex) { 231 | var d = _data[readlength ..< frameSize].compactMap({ $0 }) 232 | if let text = CFStringCreateWithBytes(kCFAllocatorDefault, &d, d.count, encoding.enc, encoding.isExternalRepresentation) as String?, text.isEmpty == false { 233 | let value = text.trimZeroTerminator() 234 | var meta = _metadatas[info.ver] ?? [] 235 | switch frameName { 236 | case "TIT2", "TT2": meta.append(.title(value)) 237 | case "TALB", "TAL": meta.append(.album(value)) 238 | case "TPE1", "TP1": meta.append(.artist(value)) 239 | case "TRCK", "TRK": meta.append(.track(value)) 240 | case "COMM", "COM": meta.append(.comment(value)) 241 | case "TDAT", "TDA": meta.append(.year(value)) 242 | default: meta.append(.other([frameName: text])) 243 | } 244 | _metadatas[info.ver] = meta 245 | _config.logger.log("(\(frameName))(\(frameSize)bytes)=\"\(value)\"", to: .metadataParser) 246 | } 247 | } else { 248 | _config.logger.log("(\(frameName))(\(frameSize)bytes)", to: .metadataParser) 249 | } 250 | } 251 | _data = _data.advanced(by: frameSize) 252 | _nextParseStartAt += frameSize 253 | } 254 | } 255 | } 256 | 257 | // MARK: - ID3V1 Logic 258 | 259 | private extension ID3Parser { 260 | func processingID3V1FromRemote(url: URL) { 261 | var request = URLRequest(url: url, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: 60) 262 | request.setValue("bytes=-128", forHTTPHeaderField: "Range") 263 | _config.session.dataTask(with: request) { [weak self] data, _, _ in 264 | guard let d = data, let sself = self else { return } 265 | sself.processingID3V1(data: d) 266 | }.resume() 267 | } 268 | 269 | func processingID3V1FromLocal(url: URL) { 270 | let name = url.asCFunctionString() 271 | guard let file = fopen(name, "r") else { 272 | _v1State = .error("can not open file at \(url)") 273 | return 274 | } 275 | defer { fclose(file) } 276 | fseek(file, -128, SEEK_END) 277 | var bytes: [UInt8] = Array(repeating: 0, count: 128) 278 | fread(&bytes, 1, 128, file) 279 | processingID3V1(data: Data(bytes)) 280 | } 281 | 282 | // Ref: 283 | func processingID3V1(data: Data) { 284 | func emptyV1Tag() { 285 | _v1State = .error("not validate id3v1 tag") 286 | } 287 | guard data.count == 128 else { 288 | emptyV1Tag() 289 | return 290 | } 291 | let isVersion11 = data[ID3V1.flagv11] == 0 292 | let version = isVersion11 ? Version.v11 : Version.v1 293 | let header = String(data: data[ID3V1.header], encoding: .utf8) 294 | guard header == ID3V1.tag else { 295 | emptyV1Tag() 296 | return 297 | } 298 | let enc: String.Encoding = .isoLatin1 299 | let basic: [Range] = [ID3V1.title, ID3V1.artist, ID3V1.album, ID3V1.year] 300 | var metas: [MetadataParser.Item] = [] 301 | for (index, range) in basic.enumerated() { 302 | let sub = data[range] 303 | guard let content = String(data: sub, encoding: enc)?.trimZeroTerminator(), content.isEmpty == false else { continue } 304 | switch index { 305 | case 0: metas.append(.title(content)) 306 | case 1: metas.append(.artist(content)) 307 | case 2: metas.append(.album(content)) 308 | case 3: metas.append(.year(content)) 309 | default: break 310 | } 311 | } 312 | if isVersion11 { 313 | let sub = data[ID3V1.commentv11] 314 | if let comment = String(data: sub, encoding: enc)?.trimZeroTerminator(), comment.isEmpty == false { 315 | metas.append(.comment(comment)) 316 | } 317 | let trackData = data[ID3V1.trackv11] 318 | metas.append(.track("\(trackData)")) 319 | } else { 320 | let sub = data[ID3V1.comment] 321 | if let comment = String(data: sub, encoding: enc)?.trimZeroTerminator(), comment.isEmpty == false { 322 | metas.append(.comment(comment)) 323 | } 324 | } 325 | let genre = data[ID3V1.genre] 326 | let style: String 327 | if let value = MetadataParser.genre[ap_safe: Int(genre)] { 328 | style = value 329 | } else { 330 | style = "\(genre)" 331 | } 332 | metas.append(.genre(style)) 333 | _metadatas[version] = metas 334 | _v1State = .complete 335 | } 336 | } 337 | 338 | // MARK: - Models 339 | 340 | private extension ID3Parser { 341 | enum Version: Int { 342 | case v1 343 | case v11 344 | case v22 345 | case v23 346 | case v24 347 | } 348 | 349 | enum StringEncoding: UInt8 { 350 | case isoLatin1 351 | case utf16WithExternalRepresentation 352 | case utf16be 353 | case utf8 354 | 355 | var isExternalRepresentation: Bool { 356 | switch self { 357 | case .utf16WithExternalRepresentation: return true 358 | default: return false 359 | } 360 | } 361 | 362 | var enc: CFStringEncoding { 363 | var encoding: CFStringBuiltInEncodings 364 | switch self { 365 | case .isoLatin1: encoding = .isoLatin1 366 | case .utf16WithExternalRepresentation: encoding = .UTF16 367 | case .utf16be: encoding = .UTF16BE 368 | case .utf8: encoding = .UTF8 369 | } 370 | return encoding.rawValue 371 | } 372 | } 373 | 374 | struct ID3V2 { 375 | static let headerFrameLength = 10 376 | static let tag: String = "ID3" 377 | static var header: Range { return Range(0 ... 2) } 378 | static var size: Range { return Range(6 ... 9) } 379 | 380 | let ver: Version 381 | let version: UInt8 382 | let reversion: UInt8 383 | let flags: Flag 384 | var hasExtendedHeader: Bool { 385 | return flags.contains(.extendedHeader) 386 | } 387 | 388 | let size: UInt32 389 | 390 | var minimumByteForFrame: Int { 391 | // A tag must contain at least one frame. A frame must be at least 1 byte big, excluding the 6-byte header. 392 | if version == 2 { return 7 } 393 | /// A tag must contain at least one frame. A frame must be at least 1 byte big, excluding the 10-byteheader. 394 | else if version == 3 || version == 4 { return 11 } 395 | return 7 396 | } 397 | 398 | init(_ bytes: Data) { 399 | var data = bytes 400 | let rawVersion = data[0] 401 | reversion = data[1] 402 | let bit7HasData = (data[2] & 0b1000_0000) != 0 403 | let bit6HasData = (data[2] & 0b0100_0000) != 0 404 | var rawFlags: Flag = [] 405 | var rawSize = [data[3] & 0x7F, data[4] & 0x7F, data[5] & 0x7F, data[6] & 0x7F].unpack(offsetSize: 7) 406 | if bit7HasData { rawFlags = rawFlags.union(.unsynchronisation) } 407 | if rawVersion == 2 { 408 | if bit6HasData { rawFlags = rawFlags.union(.compression) } 409 | } else if rawVersion == 3 || rawVersion == 4 { 410 | if bit6HasData { rawFlags = rawFlags.union(.extendedHeader) } 411 | let bit5HasData = (data[2] & 0b0010_0000) != 0 412 | if bit5HasData { rawFlags = rawFlags.union(.experimentalIndicator) } 413 | if rawVersion == 4 { 414 | let bit4HasData = (data[2] & 0b0001_0000) != 0 415 | if bit4HasData { rawFlags = rawFlags.union(.footerPresent) } 416 | } 417 | if rawFlags.contains(.footerPresent) { 418 | rawSize += 10 // footer size 419 | } 420 | } 421 | version = rawVersion 422 | ver = Version(rawValue: Int(version)) ?? .v23 423 | size = rawSize + 10 // 10 for header bytes 424 | flags = rawFlags 425 | } 426 | 427 | struct Flag: OptionSet { 428 | let rawValue: Int 429 | static let unsynchronisation = Flag(rawValue: 1 << 0) 430 | static let compression = Flag(rawValue: 1 << 1) 431 | // 2.3 432 | static let extendedHeader = Flag(rawValue: 1 << 2) 433 | static let experimentalIndicator = Flag(rawValue: 1 << 3) 434 | // 2.4 435 | static let footerPresent = Flag(rawValue: 1 << 4) 436 | } 437 | } 438 | 439 | struct ID3V1 { 440 | static let tag: String = "TAG" 441 | static var header: Range { return Range(0 ... 2) } 442 | static var title: Range { return Range(3 ... 32) } 443 | static var artist: Range { return Range(33 ... 62) } 444 | static var album: Range { return Range(63 ... 92) } 445 | static var year: Range { return Range(93 ... 96) } 446 | static var comment: Range { return Range(97 ... 126) } 447 | static var commentv11: Range { return Range(97 ... 124) } 448 | static var flagv11: Int { return 125 } 449 | static var trackv11: Int { return 126 } 450 | static var genre: Int { return 127 } 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /APlay/BuildInComponents/TagParser/TagParser+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConstantAndExt.swift 3 | // APlay 4 | // 5 | // Created by lincoln on 2018/5/31. 6 | // Copyright © 2018年 SelfStudio. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | func trimZeroTerminator() -> String { return replacingOccurrences(of: "\0", with: "") } 13 | } 14 | 15 | extension Array where Element == UInt8 { 16 | func unpack(offsetSize: Int = 8, isLittleEndian: Bool = false) -> UInt32 { 17 | precondition(count <= 4, "Array count can not larger than 4") 18 | var ret: UInt32 = 0 19 | for i in 0 ..< count { 20 | let index = isLittleEndian ? (count - i - 1) : i 21 | ret = (ret << offsetSize) | UInt32(self[index]) 22 | } 23 | return ret 24 | } 25 | 26 | func unpackUInt64(isLittleEndian: Bool = false) -> UInt64 { 27 | precondition(count <= 8, "Array count can not larger than 8") 28 | var ret: UInt64 = 0 29 | for i in 0 ..< count { 30 | let index = isLittleEndian ? (count - i - 1) : i 31 | ret = (ret << 8) | UInt64(self[index]) 32 | } 33 | return ret 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /APlay/Composer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Composer.swift 3 | // APlayer 4 | // 5 | // Created by lincoln on 2018/4/16. 6 | // Copyright © 2018年 SelfStudio. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | #if canImport(UIKit) 11 | import UIKit 12 | #endif 13 | 14 | final class Composer { 15 | lazy var eventPipeline: Delegated = Delegated() 16 | private(set) var isRunning: Bool { 17 | get { return _queue.sync { _isRuning } } 18 | set { _queue.async(flags: .barrier) { self._isRuning = newValue } } 19 | } 20 | 21 | private weak var _player: PlayerCompatible? 22 | private let _streamer: StreamProviderCompatible 23 | private let _decoder: AudioDecoderCompatible 24 | private let _ringBuffer = Uroboros(capacity: 2 << 21) // 2MB 25 | private lazy var _queue = DispatchQueue(concurrentName: "Composer") 26 | private lazy var _isRuning = false 27 | private lazy var __isDoubleChecked = false 28 | 29 | private var _isDoubleChecked: Bool { 30 | get { return _queue.sync { __isDoubleChecked } } 31 | set { _queue.async(flags: .barrier) { self.__isDoubleChecked = newValue } } 32 | } 33 | 34 | private unowned let _config: ConfigurationCompatible 35 | #if DEBUG 36 | private static var count = 0 37 | private let _id: Int 38 | deinit { 39 | debug_log("\(self) \(#function)") 40 | } 41 | #endif 42 | 43 | init(player: PlayerCompatible, config: ConfigurationCompatible) { 44 | #if DEBUG 45 | _id = Composer.count 46 | Composer.count = Composer.count &+ 1 47 | #endif 48 | _config = config 49 | _streamer = config.streamerBuilder(config) 50 | _decoder = config.audioDecoderBuilder(config) 51 | _player = player 52 | _streamer.outputPipeline.delegate(to: self) { sself, value in 53 | switch value { 54 | case let .flac(value): 55 | sself._decoder.info.flacMetadata = value 56 | sself.eventPipeline.call(.flac(value)) 57 | case let .unknown(error): 58 | sself.eventPipeline.call(.unknown(error)) 59 | case .readyForRead: 60 | sself.prepare() 61 | case let .hasBytesAvailable(data, count, isFirstPacket): 62 | let bufProgress = sself._streamer.bufferingProgress 63 | sself.eventPipeline.call(.buffering(bufProgress)) 64 | if sself._streamer.info.isRemoteWave { 65 | let targetPercentage = sself._config.preBufferWaveFormatPercentageBeforePlay 66 | if bufProgress > targetPercentage { 67 | DispatchQueue.main.async { 68 | sself._player?.resume() 69 | } 70 | } 71 | } 72 | sself._decoder.info.fileHint = sself._streamer.info.fileHint 73 | sself._decoder.inputStream.call((data, count, isFirstPacket)) 74 | case .endEncountered: 75 | sself.eventPipeline.call(.streamerEndEncountered) 76 | case let .metadataSize(size): 77 | sself._decoder.info.metadataSize = UInt(size) 78 | case let .errorOccurred(error): 79 | sself.eventPipeline.call(.error(error)) 80 | case let .metadata(map): 81 | sself.modifyMetadata(of: map) 82 | } 83 | } 84 | 85 | _decoder.outputStream.delegate(to: self) { sself, value in 86 | switch value { 87 | case let .seekable(value): 88 | sself.eventPipeline.call(.seekable(value)) 89 | case .empty: 90 | sself.eventPipeline.call(.decoderEmptyEncountered) 91 | case let .output(item): 92 | DispatchQueue.main.async { 93 | if let player = sself._player { 94 | let dstFormat = sself._decoder.info.dstFormat 95 | let srcFormat = sself._decoder.info.srcFormat 96 | if srcFormat.isLinearPCM, player.asbd != srcFormat { 97 | player.setup(srcFormat) 98 | debug_log("⛑ 0 set asbd") 99 | } else if dstFormat != player.asbd { 100 | player.setup(dstFormat) 101 | debug_log("⛑ 1 set asbd") 102 | } 103 | } 104 | } 105 | sself._ringBuffer.write(data: item.0, amount: item.1) 106 | case let .error(err): 107 | sself.eventPipeline.call(.error(err)) 108 | if case APlay.Error.parser = err { 109 | sself._player?.pause() 110 | sself._decoder.pause() 111 | } 112 | case .bitrate: 113 | sself.updateDuration() 114 | } 115 | } 116 | } 117 | 118 | private func updateDuration() { 119 | DispatchQueue.main.async { 120 | let d = Int(ceil(self.duration)) 121 | self.eventPipeline.call(.duration(d)) 122 | } 123 | } 124 | 125 | private func prepare() { 126 | do { 127 | eventPipeline.call(.buffering(0)) 128 | try _decoder.prepare(for: _streamer, at: _streamer.position) 129 | } catch { 130 | guard let e = error as? APlay.Error else { 131 | eventPipeline.call(.unknown(error)) 132 | return 133 | } 134 | eventPipeline.call(.error(e)) 135 | } 136 | } 137 | 138 | private func modifyMetadata(of data: [MetadataParser.Item]) { 139 | var ori = data 140 | for (index, item) in data.enumerated() { 141 | guard case let MetadataParser.Item.title(value) = item else { continue } 142 | if value.isEmpty { 143 | ori.remove(at: index) 144 | break 145 | } else { 146 | eventPipeline.call(.metadata(ori)) 147 | return 148 | } 149 | } 150 | let title = _streamer.info.fileName 151 | ori.append(MetadataParser.Item.title(title)) 152 | eventPipeline.call(.metadata(ori)) 153 | } 154 | } 155 | 156 | extension Composer { 157 | var duration: Float { 158 | let _srcFormat = _decoder.info.srcFormat 159 | let framesPerPacket = _srcFormat.mFramesPerPacket 160 | let rate = _srcFormat.mSampleRate 161 | if _decoder.info.audioDataPacketCount > 0, framesPerPacket > 0 { 162 | return Float(_decoder.info.audioDataPacketCount) * Float(framesPerPacket) / Float(rate) 163 | } 164 | // Not enough data provided by the format, use bit rate based estimation 165 | var audioFileLength: UInt = 0 166 | let _audioDataByteCount = _decoder.info.audioDataByteCount 167 | let _metaDataSizeInBytes = _decoder.info.metadataSize 168 | let contentLength = _streamer.contentLength 169 | if _audioDataByteCount > 0 { 170 | audioFileLength = _audioDataByteCount 171 | } else { 172 | // FIXME: May minus more bytes 173 | /// http://www.beaglebuddy.com/content/pages/javadocs/index.html 174 | if contentLength > _metaDataSizeInBytes { 175 | audioFileLength = contentLength - _metaDataSizeInBytes 176 | } 177 | } 178 | if audioFileLength > 0 { 179 | let bitrate = Float(_decoder.info.bitrate) 180 | // 总播放时间 = 文件大小 * 8 / 比特率 181 | let rate = ceil(bitrate / 1000) * 1000 * 0.125 182 | if rate > 0 { 183 | let length = Float(audioFileLength) 184 | let dur = floor(length / rate) 185 | return dur 186 | } 187 | } 188 | return 0 189 | } 190 | 191 | var streamInfo: AudioDecoder.Info { return _decoder.info } 192 | 193 | var url: URL { return _streamer.info.url } 194 | 195 | func play(_ url: URL, position: StreamProvider.Position = 0, info: AudioDecoder.Info? = nil) { 196 | eventPipeline.toggle(enable: true) 197 | if let value = info { _decoder.info.update(from: value) } 198 | _decoder.resume() 199 | _streamer.open(url: url, at: position) 200 | _player?.setup(Player.canonical) 201 | _player?.readClosure = { [weak self] size, pointer in 202 | guard let sself = self else { return (0, false) } 203 | let (readSize, isFirstData) = sself._ringBuffer.read(amount: size, into: pointer) 204 | if sself._decoder.info.srcFormat.isLinearPCM, readSize == 0 { 205 | sself._decoder.outputStream.call(.empty) 206 | } 207 | return (readSize, isFirstData) 208 | } 209 | if _streamer.info.isRemoteWave == false { 210 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: { 211 | self._player?.resume() 212 | }) 213 | } 214 | isRunning = true 215 | _config.startBackgroundTask(isToDownloadImage: false) 216 | } 217 | 218 | func position(for time: inout TimeInterval) -> StreamProvider.Position { 219 | let d = duration 220 | guard d > 0 else { return 0 } 221 | var finalTime = time 222 | if time > TimeInterval(d) { finalTime = TimeInterval(d) - 1 } 223 | let percentage = Float(finalTime) / d 224 | // more accuracy using `_decoder.streamInfo.metadataSize` then `streamerinfo.dataOffset`, may id3v2 and id3v1 tag both exist. 225 | var dataOffset = percentage * Float(_streamer.contentLength - _decoder.info.metadataSize) 226 | 227 | let fileHint = streamInfo.fileHint 228 | if fileHint == .wave { 229 | let blockSize = Float(streamInfo.waveSubchunk1Size) 230 | let min = Int(dataOffset / blockSize) 231 | dataOffset = Float(min) * blockSize 232 | } else if fileHint == .flac, let flac = streamInfo.flacMetadata { 233 | // https://github.com/xiph/flac/blob/01eb19708c11f6aae1013e7c9c29c83efda33bfb/src/libFLAC/stream_decoder.c#L2990-L3198 234 | // consider no seektable condition 235 | if let (targetTime, offset) = flac.nearestOffset(for: time) { 236 | dataOffset = Float(offset) 237 | debug_log("flac seek: support to \(time), real time:\(targetTime)") 238 | time = targetTime 239 | } 240 | } 241 | let seekByteOffset = Float(streamInfo.dataOffset) + dataOffset 242 | return StreamProvider.Position(UInt(seekByteOffset)) 243 | } 244 | 245 | func resume() { 246 | _decoder.resume() 247 | _streamer.resume() 248 | } 249 | 250 | func pause() { 251 | _decoder.pause() 252 | _streamer.pause() 253 | } 254 | 255 | func destroy() { 256 | _ringBuffer.clear() 257 | eventPipeline.toggle(enable: false) 258 | _decoder.destroy() 259 | _streamer.destroy() 260 | } 261 | 262 | func seekable() -> Bool { 263 | return _decoder.seekable() 264 | } 265 | } 266 | 267 | extension Composer { 268 | enum Event { 269 | case buffering(Float) 270 | case streamerEndEncountered 271 | case decoderEmptyEncountered 272 | case error(APlay.Error) 273 | case unknown(Error) 274 | case duration(Int) 275 | case seekable(Bool) 276 | case metadata([MetadataParser.Item]) 277 | case flac(FlacMetadata) 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /APlay/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.2.1 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /APlay/Protocols/AudioDecoderCompatible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AudioDecoderCompatible.swift 3 | // APlayer 4 | // 5 | // Created by lincoln on 2018/4/16. 6 | // Copyright © 2018年 SelfStudio. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - AudioDecoderCompatible 12 | 13 | /// Protocol for Audio Decoder 14 | public protocol AudioDecoderCompatible: AnyObject { 15 | func prepare(for provider: StreamProviderCompatible, at: StreamProvider.Position) throws 16 | func pause() 17 | func resume() 18 | func destroy() 19 | func seekable() -> Bool 20 | 21 | var info: AudioDecoder.Info { get } 22 | var outputStream: Delegated { get } 23 | var inputStream: Delegated { get } 24 | 25 | init(config: ConfigurationCompatible) 26 | } 27 | 28 | // MARK: - AudioDecoder 29 | 30 | public struct AudioDecoder { 31 | /// AudioDecoder Event 32 | /// 33 | /// - error: error 34 | /// - output: output data 35 | /// - bitrate: bitrate available 36 | /// - seekable: seekable value available 37 | /// - empty: empty data encounted 38 | public enum Event { 39 | case error(APlay.Error) 40 | case output(AudioOutput) 41 | case bitrate(UInt32) 42 | case seekable(Bool) 43 | case empty 44 | } 45 | 46 | // MARK: - AudioInput 47 | 48 | /// (UnsafePointer, UInt32, Bool) 49 | public typealias AudioInput = (UnsafePointer, UInt32, Bool) 50 | 51 | // MARK: - AudioOutput 52 | 53 | /// (UnsafePointer, UInt32, Bool) 54 | public typealias AudioOutput = (UnsafeRawPointer, UInt32) 55 | 56 | // MARK: - Decoder Info 57 | 58 | /// Decoder Info 59 | public final class Info { 60 | private static let maxBitrateSample = 50 61 | public lazy var srcFormat = AudioStreamBasicDescription() 62 | public lazy var dstFormat = Player.canonical 63 | public lazy var audioDataByteCount: UInt = 0 64 | public lazy var dataOffset: UInt = 0 65 | public lazy var sampleRate: Float64 = 0 66 | public lazy var packetDuration: Double = 0 67 | public lazy var packetBufferSize: UInt32 = 0 68 | public lazy var fileHint: AudioFileType = .mp3 69 | public lazy var bitrate: UInt32 = 0 70 | public lazy var audioDataPacketCount: UInt = 0 71 | public lazy var parseFlags: AudioFileStreamParseFlags = .discontinuity 72 | public lazy var metadataSize: UInt = 0 73 | public lazy var waveSubchunk1Size: UInt32 = 0 74 | public var flacMetadata: FlacMetadata? 75 | var isUpdated = false 76 | private lazy var bitrateIndexArray: [Double] = [] 77 | private var isUpdatedOnce = false 78 | 79 | public init() {} 80 | 81 | func infoUpdated() { isUpdatedOnce = true } 82 | 83 | func reset() { 84 | srcFormat = AudioStreamBasicDescription() 85 | audioDataByteCount = 0 86 | dataOffset = 0 87 | sampleRate = 0 88 | packetDuration = 0 89 | packetBufferSize = 0 90 | fileHint = .mp3 91 | bitrate = 0 92 | audioDataPacketCount = 0 93 | parseFlags = .discontinuity 94 | metadataSize = 0 95 | waveSubchunk1Size = 0 96 | isUpdated = false 97 | bitrateIndexArray = [] 98 | flacMetadata = nil 99 | } 100 | 101 | func update(from info: Info) { 102 | isUpdated = true 103 | srcFormat = info.srcFormat 104 | dstFormat = info.dstFormat 105 | audioDataByteCount = info.audioDataByteCount 106 | dataOffset = info.dataOffset 107 | sampleRate = info.sampleRate 108 | packetDuration = info.packetDuration 109 | packetBufferSize = info.packetBufferSize 110 | fileHint = info.fileHint 111 | bitrate = info.bitrate 112 | audioDataPacketCount = info.audioDataPacketCount 113 | parseFlags = .discontinuity 114 | metadataSize = info.metadataSize 115 | bitrateIndexArray = info.bitrateIndexArray 116 | waveSubchunk1Size = info.waveSubchunk1Size 117 | flacMetadata = info.flacMetadata 118 | infoUpdated() 119 | } 120 | 121 | func calculate(packet: AudioStreamPacketDescription) -> Bool { 122 | if bitrate == 0, packetDuration > 0, bitrateIndexArray.count < Info.maxBitrateSample { 123 | let value = Double(8 * packet.mDataByteSize) / packetDuration 124 | bitrateIndexArray.append(value) 125 | if bitrateIndexArray.count >= Info.maxBitrateSample { 126 | bitrate = UInt32(bitrateIndexArray.reduce(0, +)) / UInt32(Info.maxBitrateSample) 127 | return true 128 | } 129 | } 130 | return false 131 | } 132 | 133 | func seekable() -> Bool { 134 | guard isUpdatedOnce else { return false } 135 | if fileHint == .flac { 136 | guard let count = flacMetadata?.seekTable?.points.count else { return false } 137 | return count > 0 138 | } 139 | return true 140 | } 141 | } 142 | 143 | // MARK: - AudioFileType 144 | 145 | /// A wrap for AudioFileTypeID 146 | public struct AudioFileType: RawRepresentable, Hashable { 147 | public typealias RawValue = String 148 | public var rawValue: String 149 | 150 | public init(rawValue: RawValue) { self.rawValue = rawValue } 151 | 152 | public init(_ value: RawValue) { rawValue = value } 153 | 154 | public init?(value: AudioFileTypeID) { 155 | guard let result = String(from: value) else { return nil } 156 | self.init(rawValue: result) 157 | } 158 | // https://developer.apple.com/documentation/audiotoolbox/1576497-anonymous?language=objc 159 | public static let aiff = AudioFileType("AIFF") 160 | public static let aifc = AudioFileType("AIFC") 161 | public static let wave = AudioFileType("WAVE") 162 | public static let rf64 = AudioFileType("RF64") 163 | public static let soundDesigner2 = AudioFileType("Sd2f") 164 | public static let next = AudioFileType("NeXT") 165 | public static let mp3 = AudioFileType("MPG3") 166 | public static let mp2 = AudioFileType("MPG2") 167 | public static let mp1 = AudioFileType("MPG1") 168 | public static let ac3 = AudioFileType("ac-3") 169 | public static let aacADTS = AudioFileType("adts") 170 | public static let mp4 = AudioFileType("mp4f") 171 | public static let m4a = AudioFileType("m4af") 172 | public static let m4b = AudioFileType("m4bf") 173 | public static let caf = AudioFileType("caff") 174 | public static let k3gp = AudioFileType("3gpp") 175 | public static let k3gp2 = AudioFileType("3gp2") 176 | public static let amr = AudioFileType("amrf") 177 | public static let flac = AudioFileType("flac") 178 | public static let opus = AudioFileType("opus") 179 | 180 | private static var map: [AudioFileType: AudioFileTypeID] = [:] 181 | 182 | public var audioFileTypeID: AudioFileTypeID { 183 | let value: AudioFileTypeID 184 | if let result = AudioFileType.map[self] { 185 | value = result 186 | } else { 187 | value = rawValue.audioFileTypeID() 188 | AudioFileType.map[self] = value 189 | } 190 | return value 191 | } 192 | } 193 | } 194 | 195 | extension String { 196 | init?(from value: UInt32) { 197 | var bigEndian = value.bigEndian 198 | let count = MemoryLayout.size 199 | let bytePtr = withUnsafePointer(to: &bigEndian) { 200 | $0.withMemoryRebound(to: UInt8.self, capacity: count) { 201 | UnsafeBufferPointer(start: $0, count: count) 202 | } 203 | } 204 | self.init(data: Data(buffer: bytePtr), encoding: .utf8) 205 | } 206 | 207 | fileprivate func audioFileTypeID() -> AudioFileTypeID { 208 | let offsetSize = 8 209 | let array: [UInt8] = Array(utf8) 210 | let total = array.count 211 | var totalSize: UInt32 = 0 212 | for i in 0 ..< total { 213 | totalSize += UInt32(array[i]) << (offsetSize * ((total - 1) - i)) 214 | } 215 | return totalSize 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /APlay/Protocols/ConfigurationCompatible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurationCompatible.swift 3 | // APlay 4 | // 5 | // Created by lincoln on 2018/6/13. 6 | // Copyright © 2018年 SelfStudio. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | // Using __`unowned let`__ to avoid retain cycle 12 | /// Protocol for APlay Configuration 13 | public protocol ConfigurationCompatible: AnyObject { 14 | var defaultCoverImage: UIImage? { get set } 15 | var session: URLSession { get } 16 | var streamerBuilder: (ConfigurationCompatible) -> StreamProviderCompatible { get } 17 | var audioDecoderBuilder: (ConfigurationCompatible) -> AudioDecoderCompatible { get } 18 | var metadataParserBuilder: (AudioFileType, ConfigurationCompatible) -> MetadataParserCompatible? { get } 19 | var httpFileCompletionValidator: APlay.Configuration.HttpFileValidationPolicy { get } 20 | var preBufferWaveFormatPercentageBeforePlay: Float { get } 21 | var decodeBufferSize: UInt { get } 22 | var startupWatchdogPeriod: UInt { get } 23 | var maxDiskCacheSize: UInt32 { get } 24 | var maxDecodedByteCount: UInt32 { get } 25 | var maxRemoteStreamOpenRetry: UInt { get } 26 | var userAgent: String { get } 27 | var cacheDirectory: String { get } 28 | var cacheNaming: APlay.Configuration.CacheFileNamingPolicy { get } 29 | var cachePolicy: APlay.Configuration.CachePolicy { get } 30 | var proxyPolicy: APlay.Configuration.ProxyPolicy { get } 31 | var networkPolicy: APlay.Configuration.NetworkPolicy { get } 32 | var predefinedHttpHeaderValues: [String: String] { get } 33 | var isEnabledAutomaticAudioSessionHandling: Bool { get } 34 | var isEnabledVolumeMixer: Bool { get } 35 | var equalizerBandFrequencies: [Float] { get } 36 | var logger: LoggerCompatible { get } 37 | var isAutoFillID3InfoToNowPlayingCenter: Bool { get } 38 | var isAutoHandlingInterruptEvent: Bool { get } 39 | 40 | func startBackgroundTask(isToDownloadImage: Bool) 41 | func endBackgroundTask(isToDownloadImage: Bool) 42 | } 43 | -------------------------------------------------------------------------------- /APlay/Protocols/LoggerCompatible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoggerCompatible.swift 3 | // APlay 4 | // 5 | // Created by lincoln on 2018/6/13. 6 | // Copyright © 2018年 SelfStudio. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Protocol for logger 12 | public protocol LoggerCompatible { 13 | var currentFile: String { get } 14 | var isLoggedToConsole: Bool { get } 15 | func log(_ msg: String, to channel: Logger.Channel, method: String) 16 | func cleanAllLogs() 17 | func reset() 18 | init(policy: Logger.Policy) 19 | } 20 | 21 | extension LoggerCompatible { 22 | func log(_ msg: String, to channel: Logger.Channel, func method: String = #function) { 23 | log(msg, to: channel, method: method) 24 | } 25 | } 26 | 27 | public struct Logger { 28 | /// Line seperator 29 | public static let lineSeperator = "\n\u{FEFF}\u{FEFF}" 30 | 31 | /// Logger Policy 32 | /// 33 | /// - disable: disable 34 | /// - persistentInFolder: log in certain folder 35 | public enum Policy: Hashable { 36 | case disable 37 | case persistentInFolder(String) 38 | 39 | /// Is disabled log 40 | public var isDisabled: Bool { 41 | switch self { 42 | case .disable: return true 43 | default: return false 44 | } 45 | } 46 | 47 | /// Folder to store logs 48 | public var folder: String? { 49 | switch self { 50 | case let .persistentInFolder(path): return path 51 | default: return nil 52 | } 53 | } 54 | 55 | /// Default policy for APlay 56 | public static var defaultPolicy: Policy { 57 | let cache = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first! 58 | let dir = "\(cache)/APlay/Log" 59 | let fm = FileManager.default 60 | if fm.fileExists(atPath: dir) == false { 61 | try? fm.createDirectory(atPath: dir, withIntermediateDirectories: true, attributes: nil) 62 | } 63 | return Logger.Policy.persistentInFolder(dir) 64 | } 65 | } 66 | 67 | /// Log channel 68 | public enum Channel: CaseIterable { 69 | case audioDecoder, streamProvider, metadataParser, player 70 | var symbole: String { 71 | switch self { 72 | case .audioDecoder: return "🌈" 73 | case .streamProvider: return "🌊" 74 | case .metadataParser: return "⚡️" 75 | case .player: return "🍵" 76 | } 77 | } 78 | 79 | func log(msg: String, method: String = #function) { 80 | let total = "\(symbole)[\(method)] \(msg)" 81 | #if DEBUG 82 | print(total) 83 | #endif 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /APlay/Protocols/MetadataPaserCompatible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetadataParserCompatible.swift 3 | // APlay 4 | // 5 | // Created by lincoln on 2018/5/11. 6 | // Copyright © 2018年 SelfStudio. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | /// Protocol for metadata parser 11 | public protocol MetadataParserCompatible: AnyObject { 12 | var outputStream: Delegated { get } 13 | func acceptInput(data: UnsafeMutablePointer, count: UInt32) 14 | func parseID3V1Tag(at url: URL) 15 | init(config: ConfigurationCompatible) 16 | } 17 | 18 | extension MetadataParserCompatible { func parseID3V1Tag(at _: URL) {} } 19 | 20 | // MARK: - MetadataParser 21 | 22 | public struct MetadataParser { 23 | public enum Event { 24 | case end 25 | case tagSize(UInt32) 26 | case metadata([Item]) 27 | case flac(FlacMetadata) 28 | } 29 | 30 | public enum Item { 31 | case artist(String) 32 | case title(String) 33 | case cover(Data) 34 | case album(String) 35 | case genre(String) 36 | case track(String) 37 | case year(String) 38 | case comment(String) 39 | case other([String: String]) 40 | } 41 | 42 | public enum PictureType: UInt8 { 43 | /** Other */ 44 | case other 45 | /** 32x32 pixels 'file icon' (PNG only) */ 46 | case fileIconStandard 47 | /** Other file icon */ 48 | case fileIcon 49 | /** Cover (front) */ 50 | case frontCover 51 | /** Cover (back) */ 52 | case backCover 53 | /** Leaflet page */ 54 | case leafletPage 55 | /** Media (e.g. label side of CD) */ 56 | case media 57 | /** Lead artist/lead performer/soloist */ 58 | case leadArtist 59 | /** Artist/performer */ 60 | case artist 61 | /** Conductor */ 62 | case conductor 63 | /** Band/Orchestra */ 64 | case band 65 | /** Composer */ 66 | case composer 67 | /** Lyricist/text writer */ 68 | case lyricist 69 | /** Recording Location */ 70 | case recordingLocation 71 | /** During recording */ 72 | case duringRecording 73 | /** During performance */ 74 | case duringPerformance 75 | /** Movie/video screen capture */ 76 | case videoScreenCapture 77 | /** A bright coloured fish */ 78 | case fish 79 | /** Illustration */ 80 | case illustration 81 | /** Band/artist logotype */ 82 | case bandLogotype 83 | /** Publisher/Studio logotype */ 84 | case publisherLogotype 85 | /// undifined 86 | case undifined 87 | } 88 | } 89 | 90 | // MARK: Internal 91 | 92 | extension MetadataParser { 93 | enum State: Equatable { 94 | case initial 95 | case parsering 96 | case complete 97 | case error(String) 98 | 99 | var isDone: Bool { 100 | switch self { 101 | case .complete, .error: return true 102 | default: return false 103 | } 104 | } 105 | 106 | var isNeedData: Bool { 107 | switch self { 108 | case .complete, .error: return false 109 | default: return true 110 | } 111 | } 112 | } 113 | 114 | // http://mutagen-specs.readthedocs.io/en/latest/id3/id3v1-genres.html 115 | static let genre: [String] = ["Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", "Alt. Rock", "Bass", "Soul", "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta Rap", "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", "Cabaret", "New Wave", "Psychedelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock", "Folk", "Folk-Rock", "National Folk", "Swing", "Fast-Fusion", "Bebop", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A Cappella", "Euro-House", "Dance Hall", "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", "BritPop", "Afro-Punk", "Polsk Punk", "Beat", "Christian Gangsta Rap", "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian", "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop", "Synthpop", "Abstract", "Art Rock", "Baroque", "Bhangra", "Big Beat", "Breakbeat", "Chillout", "Downtempo", "Dub", "EBM", "Eclectic", "Electro", "Electroclash", "Emo", "Experimental", "Garage", "Global", "IDM", "Illbient", "Industro-Goth", "Jam Band", "Krautrock", "Leftfield", "Lounge", "Math Rock", "New Romantic", "Nu-Breakz", "Post-Punk", "Post-Rock", "Psytrance", "Shoegaze", "Space Rock", "Trop Rock", "World Music", "Neoclassical", "Audiobook", "Audio Theatre", "Neue Deutsche Welle", "Podcast", "Indie Rock", "G-Funk", "Dubstep", "Garage Rock", "Psybient"] 116 | } 117 | -------------------------------------------------------------------------------- /APlay/Protocols/PlayerCompatible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayerCompatible.swift 3 | // APlay 4 | // 5 | // Created by lincoln on 2018/7/2. 6 | // Copyright © 2018年 SelfStudio. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | protocol PlayerCompatible: AnyObject { 11 | var readClosure: (UInt32, UnsafeMutablePointer) -> (UInt32, Bool) { get set } 12 | var eventPipeline: Delegated { get } 13 | var startTime: Float { get set } 14 | var asbd: AudioStreamBasicDescription { get } 15 | var state: Player.State { get } 16 | var volume: Float { get set } 17 | 18 | func destroy() 19 | func pause() 20 | func resume() 21 | func toggle() 22 | 23 | func setup(_: AudioStreamBasicDescription) 24 | 25 | func currentTime() -> Float 26 | 27 | init(config: ConfigurationCompatible) 28 | } 29 | 30 | struct Player { 31 | static var maxFramesPerSlice: UInt32 = 4096 32 | 33 | static var ringBufferSize: UInt32 = 1024 * 1024 * 2 34 | 35 | static let maxReadPerSlice: Int = Int(maxFramesPerSlice * canonical.mBytesPerPacket) 36 | static let minimumBufferCount: Int = 1 37 | static let minimumBufferSize: Int = maxReadPerSlice * minimumBufferCount 38 | 39 | static var canonical: AudioStreamBasicDescription = { 40 | var bytesPerSample = UInt32(MemoryLayout.size) 41 | if #available(iOS 8.0, *) { 42 | bytesPerSample = UInt32(MemoryLayout.size) 43 | } 44 | let flags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked 45 | let component = AudioStreamBasicDescription(mSampleRate: 44100, mFormatID: kAudioFormatLinearPCM, mFormatFlags: flags, mBytesPerPacket: bytesPerSample * 2, mFramesPerPacket: 1, mBytesPerFrame: bytesPerSample * 2, mChannelsPerFrame: 2, mBitsPerChannel: 8 * bytesPerSample, mReserved: 0) 46 | return component 47 | }() 48 | 49 | enum State { case idle, running, paused } 50 | 51 | enum Event { 52 | case playback(Float) 53 | case state(State) 54 | case error(APlay.Error) 55 | case unknown(Error) 56 | } 57 | 58 | struct Bus { 59 | static let output: UInt32 = 0 60 | static let input: UInt32 = 1 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /APlay/Protocols/StreamProviderCompatible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StreamProvider.swift 3 | // APlay 4 | // 5 | // Created by lincoln on 2018/5/11. 6 | // Copyright © 2018年 SelfStudio. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | /// Protocol for stream provider 11 | public protocol StreamProviderCompatible: AnyObject { 12 | var outputPipeline: Delegated { get } 13 | var position: StreamProvider.Position { get } 14 | var contentLength: UInt { get } 15 | var info: StreamProvider.URLInfo { get } 16 | var bufferingProgress: Float { get } 17 | 18 | func open(url: URL, at position: StreamProvider.Position) 19 | func destroy() 20 | func pause() 21 | func resume() 22 | init(config: ConfigurationCompatible) 23 | } 24 | 25 | extension StreamProviderCompatible { 26 | @inline(__always) 27 | public func open(url: URL) { return open(url: url, at: 0) } 28 | } 29 | 30 | public struct StreamProvider { 31 | public enum Event { 32 | case readyForRead 33 | case hasBytesAvailable(UnsafePointer, UInt32, Bool) 34 | case endEncountered 35 | case errorOccurred(APlay.Error) 36 | case metadata([MetadataParser.Item]) 37 | case metadataSize(UInt32) 38 | case flac(FlacMetadata) 39 | case unknown(Error) 40 | } 41 | 42 | public typealias Position = UInt 43 | 44 | public enum URLInfo { 45 | case remote(URL, AudioFileType) 46 | case local(URL, AudioFileType) 47 | case unknown(URL) 48 | 49 | public static let none = URLInfo.unknown(URL(string: "https://URLInfo.none")!) 50 | 51 | public var isRemote: Bool { if case .remote = self { return true }; return false } 52 | 53 | public var url: URL { 54 | switch self { 55 | case let .remote(url, _): return url 56 | case let .local(url, _): return url 57 | case let .unknown(url): return url 58 | } 59 | } 60 | 61 | public var fileHint: AudioFileType { 62 | switch self { 63 | case let .remote(_, hint): return hint 64 | case let .local(_, hint): return hint 65 | default: return .mp3 66 | } 67 | } 68 | 69 | public var isWave: Bool { 70 | switch self { 71 | case let .remote(_, hint): return hint == .wave 72 | case let .local(_, hint): return hint == .wave 73 | default: return false 74 | } 75 | } 76 | 77 | public var isRemoteWave: Bool { 78 | switch self { 79 | case let .remote(_, hint): return hint == .wave 80 | default: return false 81 | } 82 | } 83 | 84 | public var isLocalWave: Bool { 85 | switch self { 86 | case let .local(_, hint): return hint == .wave 87 | default: return false 88 | } 89 | } 90 | 91 | public var fileName: String { 92 | var coms = url.lastPathComponent.split(separator: ".") 93 | coms.removeLast() 94 | return coms.joined(separator: ".") 95 | } 96 | 97 | public init(url: URL) { 98 | guard let scheme = url.scheme?.lowercased() else { 99 | self = .unknown(url) 100 | return 101 | } 102 | if scheme == "file" { 103 | let localFileHint = URLInfo.localFileHit(from: url) 104 | let pathExtensionHint = URLInfo.fileHint(from: url.pathExtension) 105 | if localFileHint != .mp3 { 106 | self = .local(url, localFileHint) 107 | } else if pathExtensionHint != .mp3 { 108 | self = .local(url, pathExtensionHint) 109 | } else { 110 | self = .local(url, .mp3) 111 | } 112 | } else { 113 | self = .remote(url, URLInfo.fileHint(from: url.pathExtension)) 114 | } 115 | } 116 | 117 | static func isWave(for url: URL) -> Bool { 118 | return fileHint(from: url.pathExtension) == .wave 119 | } 120 | 121 | func localContentLength() -> UInt { 122 | guard case let URLInfo.local(url, _) = self else { return 0 } 123 | let name = url.asCFunctionString() 124 | var buff = stat() 125 | if stat(name, &buff) != 0 { return 0 } 126 | let size = buff.st_size 127 | return UInt(size) 128 | } 129 | 130 | private static func localFileHit(from url: URL) -> AudioFileType { 131 | let name = url.asCFunctionString() 132 | let tagSize = 4 133 | guard let fd = fopen(name, "r") else { return .mp3 } 134 | defer { fclose(fd) } 135 | var buffer = UnsafeMutablePointer.uint8Pointer(of: tagSize) 136 | defer { free(buffer) } 137 | fseek(fd, 8, SEEK_SET) 138 | fread(buffer, 1, tagSize, fd) 139 | var d = Data(bytes: buffer, count: tagSize) 140 | var value = String(data: d, encoding: .utf8) 141 | if value?.lowercased() == "wave" { return .wave } 142 | fseek(fd, 0, SEEK_SET) 143 | fread(buffer, 1, tagSize, fd) 144 | d = Data(bytes: buffer, count: tagSize) 145 | value = String(data: d, encoding: .utf8) 146 | if value?.lowercased() == "flac" { return .flac } 147 | return .mp3 148 | } 149 | 150 | /// Get fileHint from fileformat, file extension or content type, 151 | /// 152 | /// - Parameter value: fileformat, file extension or content type 153 | /// - Returns: AudioFileTypeID, default value is `kAudioFileMP3Type` 154 | static func fileHint(from value: String) -> AudioFileType { 155 | switch value.lowercased() { 156 | case "flac": return .flac 157 | case "mp3", "mpg3", "audio/mpeg", "audio/mp3": return .mp3 158 | case "wav", "wave", "audio/x-wav": return .wave 159 | case "aifc", "audio/x-aifc": return .aifc 160 | case "aiff", "audio/x-aiff": return .aiff 161 | case "m4a", "audio/x-m4a": return .m4a 162 | case "mp4", "mp4f", "mpg4", "audio/mp4", "video/mp4": return .mp4 163 | case "caf", "caff", "audio/x-caf": return .caf 164 | case "aac", "adts", "aacp", "audio/aac", "audio/aacp": return .aacADTS 165 | case "opus", "audio/opus": return .opus 166 | default: return .mp3 167 | } 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /APlay/Utils/APlay+Debug.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APlay+Debug.swift 3 | // APlay 4 | // 5 | // Created by lincoln on 2018/6/13. 6 | // Copyright © 2018年 SelfStudio. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | #if DEBUG 12 | let runProfile = false 13 | #endif 14 | 15 | func debug_log(_ msg: String) { 16 | #if DEBUG 17 | var message = msg 18 | if message.contains("deinit") { message = "❌ \(msg)" } 19 | print("🐛 [Debug]", message) 20 | #endif 21 | } 22 | -------------------------------------------------------------------------------- /APlay/Utils/APlay+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // APlay 4 | // 5 | // Created by Lincoln Law on 2017/2/20. 6 | // Copyright © 2017年 Lincoln Law. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Add Equatable support for AudioStreamBasicDescription 12 | extension AudioStreamBasicDescription: Equatable { 13 | /// whether current mFormatID equal to kAudioFormatLinearPCM 14 | public var isLinearPCM: Bool { 15 | return mFormatID == kAudioFormatLinearPCM 16 | } 17 | 18 | public static func == (source: AudioStreamBasicDescription, dst: AudioStreamBasicDescription) -> Bool { 19 | return dst.mFormatID == source.mFormatID && 20 | dst.mSampleRate == source.mSampleRate && 21 | dst.mBytesPerPacket == source.mBytesPerPacket && 22 | dst.mFormatFlags == source.mFormatFlags && 23 | dst.mBytesPerPacket == source.mBytesPerPacket && 24 | dst.mBitsPerChannel == source.mBitsPerChannel && 25 | dst.mFramesPerPacket == source.mFramesPerPacket && 26 | dst.mChannelsPerFrame == source.mChannelsPerFrame && 27 | dst.mReserved == source.mReserved 28 | } 29 | } 30 | 31 | extension Array { 32 | subscript(ap_safe index: Int) -> Element? { 33 | return indices ~= index ? self[index] : nil 34 | } 35 | } 36 | 37 | extension DispatchQueue { 38 | convenience init(name: String) { 39 | self.init(label: "com.SelfStudio.APlay.\(name)") 40 | } 41 | 42 | convenience init(concurrentName: String) { 43 | self.init(label: "com.SelfStudio.APlay.\(concurrentName)", attributes: .concurrent) 44 | } 45 | } 46 | 47 | extension URL { 48 | func asCFunctionString() -> String { 49 | var name = absoluteString.replacingOccurrences(of: "file://", with: "") 50 | if let value = name.removingPercentEncoding { name = value } 51 | return name 52 | } 53 | } 54 | 55 | extension UnsafeMutableRawPointer { 56 | func to(object _: T.Type) -> T { 57 | return Unmanaged.fromOpaque(self).takeUnretainedValue() 58 | } 59 | 60 | static func from(object: T) -> UnsafeMutableRawPointer { 61 | return Unmanaged.passUnretained(object).toOpaque() 62 | } 63 | } 64 | 65 | extension UnsafeMutablePointer where Pointee == UInt8 { 66 | static func uint8Pointer(of size: Int) -> UnsafeMutablePointer { 67 | let alignment = MemoryLayout.alignment 68 | return UnsafeMutableRawPointer.allocate(byteCount: size, alignment: alignment).bindMemory(to: UInt8.self, capacity: size) 69 | } 70 | } 71 | 72 | extension AudioFileStreamParseFlags { 73 | static let continuity = AudioFileStreamParseFlags(rawValue: 0) 74 | } 75 | 76 | extension OSStatus { 77 | static let empty: OSStatus = 101 78 | func readableMessage(from raw: String) -> String { 79 | var result = "" 80 | switch raw { 81 | case "wht?": result = "Audio File Unspecified" 82 | case "typ?": result = "Audio File Unsupported File Type" 83 | case "fmt?": result = "Audio File Unsupported Data Format" 84 | case "pty?": result = "Audio File Unsupported Property" 85 | case "!siz": result = "Audio File Bad Property Size" 86 | case "prm?": result = "Audio File Permissions Error" 87 | case "optm": result = "Audio File Not Optimized" 88 | case "chk?": result = "Audio File Invalid Chunk" 89 | case "off?": result = "Audio File Does Not Allow 64Bit Data Size" 90 | case "pck?": result = "Audio File Invalid Packet Offset" 91 | case "dta?": result = "Audio File Invalid File" 92 | case "op??", "0x6F703F3F": result = "Audio File Operation Not Supported" 93 | case "!pkd": result = "Audio Converter Err Requires Packet Descriptions Error" 94 | case "-38": result = "Audio File Not Open" 95 | case "-39": result = "Audio File End Of File Error" 96 | case "-40": result = "Audio File Position Error" 97 | case "-43": result = "Audio File File Not Found" 98 | default: result = "" 99 | } 100 | result = "\(result)(\(raw))" 101 | return result 102 | } 103 | 104 | @discardableResult func check(operation: String = "", file: String = #file, method: String = #function, line: Int = #line) -> String? { 105 | guard self != noErr else { return nil } 106 | var result: String = "" 107 | var char = Int(bigEndian) 108 | 109 | for _ in 0 ..< 4 { 110 | guard isprint(Int32(char & 255)) == 1 else { 111 | result = "\(self)" 112 | break 113 | } 114 | // UnicodeScalar(char&255) will get optional 115 | let raw = String(describing: UnicodeScalar(UInt8(char & 255))) 116 | result += raw 117 | char = char / 256 118 | } 119 | let humanMsg = readableMessage(from: result) 120 | let msg = "\n{\n file: \(file):\(line),\n function: \(method),\n operation: \(operation),\n message: \(humanMsg)\n}" 121 | #if DEBUG 122 | debug_log(msg) 123 | #endif 124 | return msg 125 | } 126 | 127 | func throwCheck(file: String = #file, method: String = #function, line: Int = #line) throws { 128 | guard let msg = check(file: file, method: method, line: line) else { return } 129 | throw APlay.Error.player(msg) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /APlay/Utils/APlay+Typealias.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Typealias.swift 3 | // APlayer 4 | // 5 | // Created by lincoln on 2018/4/3. 6 | // Copyright © 2018年 SelfStudio. All rights reserved. 7 | // 8 | 9 | import AudioToolbox 10 | import AVFoundation 11 | import CoreAudio 12 | import Foundation 13 | 14 | /// AVFoundation.AudioFileTypeID 15 | public typealias AudioFileTypeID = AVFoundation.AudioFileTypeID 16 | 17 | /// AVFoundation.AVAudioSession 18 | public typealias AVAudioSession = AVFoundation.AVAudioSession 19 | 20 | /// AVFoundation.AVAudioEngine 21 | public typealias AVAudioEngine = AVFoundation.AVAudioEngine 22 | 23 | /// CoreAudio.AudioStreamPacketDescription 24 | public typealias AudioStreamPacketDescription = CoreAudio.AudioStreamPacketDescription 25 | 26 | /// CoreAudio.AudioStreamBasicDescription 27 | public typealias AudioStreamBasicDescription = CoreAudio.AudioStreamBasicDescription 28 | 29 | /// AudioToolbox.AudioFileStreamParseFlags 30 | public typealias AudioFileStreamParseFlags = AudioToolbox.AudioFileStreamParseFlags 31 | 32 | /// AudioToolbox.AudioBuffer 33 | public typealias AudioBuffer = AudioToolbox.AudioBuffer 34 | 35 | /// AudioToolbox.AudioConverterRef 36 | public typealias AudioConverterRef = AudioToolbox.AudioConverterRef 37 | 38 | /// AudioToolbox.AudioBufferList 39 | public typealias AudioBufferList = AudioToolbox.AudioBufferList 40 | 41 | /// AudioToolbox.AudioConverterComplexInputDataProc 42 | public typealias AudioConverterComplexInputDataProc = AudioToolbox.AudioConverterComplexInputDataProc 43 | 44 | /// CoreAudio.kAudioFormatFLAC 45 | public let kAudioFormatFLAC = CoreAudio.kAudioFormatFLAC 46 | 47 | /// CoreAudio.kAudioFormatLinearPCM 48 | public let kAudioFormatLinearPCM = CoreAudio.kAudioFormatLinearPCM 49 | 50 | /// CoreAudio.kAudioFormatFlagIsSignedInteger 51 | public let kAudioFormatFlagIsSignedInteger = CoreAudio.kAudioFormatFlagIsSignedInteger 52 | 53 | /// CoreAudio.kAudioFormatFlagsNativeEndian 54 | public let kAudioFormatFlagsNativeEndian = CoreAudio.kAudioFormatFlagsNativeEndian 55 | 56 | /// CoreAudio.kAudioFormatFlagIsPacked 57 | public let kAudioFormatFlagIsPacked = CoreAudio.kAudioFormatFlagIsPacked 58 | 59 | /// AudioDecoder.AudioFileType 60 | public typealias AudioFileType = AudioDecoder.AudioFileType 61 | 62 | /// (ConfigurationCompatible) -> StreamProviderCompatible 63 | public typealias StreamerBuilder = (ConfigurationCompatible) -> StreamProviderCompatible 64 | 65 | /// (ConfigurationCompatible) -> AudioDecoderCompatible 66 | public typealias AudioDecoderBuilder = (ConfigurationCompatible) -> AudioDecoderCompatible 67 | 68 | /// (Logger.Policy) -> LoggerCompatible 69 | public typealias LoggerBuilder = (Logger.Policy) -> LoggerCompatible 70 | 71 | /// (AudioFileType, ConfigurationCompatible) -> MetadataParserCompatible? 72 | public typealias MetadataParserBuilder = (AudioFileType, ConfigurationCompatible) -> MetadataParserCompatible? 73 | 74 | /// (APlay.Configuration.ProxyPolicy) -> URLSessionDelegate 75 | public typealias SessionDelegateBuilder = (APlay.Configuration.ProxyPolicy) -> URLSessionDelegate 76 | 77 | /// (APlay.Configuration.ProxyPolicy) -> URLSession 78 | public typealias SessionBuilder = (APlay.Configuration.ProxyPolicy) -> URLSession 79 | -------------------------------------------------------------------------------- /APlay/Vendor/Delegated.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Delegated.swift 3 | // Delegated 4 | // 5 | // Created by Oleg Dreyman on 3/11/18. 6 | // Copyright © 2018 Delegated. All rights reserved. 7 | // 8 | 9 | /// [Delegated](https://github.com/dreymonde/Delegated) is a super small package that solves the retain cycle problem when dealing with closure-based delegation 10 | public final class Delegated { 11 | private(set) var callback: ((Input) -> Output?)? 12 | 13 | private var _isEnabled = true 14 | 15 | public init() {} 16 | 17 | public func delegate(to target: Target, 18 | with callback: @escaping (Target, Input) -> Output) { 19 | self.callback = { [weak target] input in 20 | guard let target = target else { 21 | return nil 22 | } 23 | return callback(target, input) 24 | } 25 | } 26 | 27 | public func call(_ input: Input) -> Output? { 28 | guard _isEnabled else { return nil } 29 | return callback?(input) 30 | } 31 | 32 | public var isDelegateSet: Bool { 33 | return callback != nil 34 | } 35 | } 36 | 37 | extension Delegated { 38 | public func stronglyDelegate(to target: Target, 39 | with callback: @escaping (Target, Input) -> Output) { 40 | self.callback = { input in 41 | callback(target, input) 42 | } 43 | } 44 | 45 | public func manuallyDelegate(with callback: @escaping (Input) -> Output) { 46 | self.callback = callback 47 | } 48 | 49 | public func removeDelegate() { 50 | callback = nil 51 | } 52 | 53 | public func toggle(enable: Bool) { 54 | _isEnabled = enable 55 | } 56 | } 57 | 58 | extension Delegated where Input == Void { 59 | public func delegate(to target: Target, 60 | with callback: @escaping (Target) -> Output) { 61 | delegate(to: target, with: { target, _ in callback(target) }) 62 | } 63 | 64 | public func stronglyDelegate(to target: Target, 65 | with callback: @escaping (Target) -> Output) { 66 | stronglyDelegate(to: target, with: { target, _ in callback(target) }) 67 | } 68 | } 69 | 70 | extension Delegated where Input == Void { 71 | public func call() -> Output? { 72 | guard _isEnabled else { return nil } 73 | return call(()) 74 | } 75 | } 76 | 77 | extension Delegated where Output == Void { 78 | public func call(_ input: Input) { 79 | guard _isEnabled else { return } 80 | callback?(input) 81 | } 82 | } 83 | 84 | extension Delegated where Input == Void, Output == Void { 85 | public func call() { 86 | guard _isEnabled else { return } 87 | call(()) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /APlay/Vendor/GCDTimer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GCDTimer.swift 3 | // APlay 4 | // 5 | // Created by lincoln on 2018/5/23. 6 | // Copyright © 2018年 SelfStudio. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class GCDTimer { 12 | private static var count = 0 13 | private(set) var index = 0 14 | private lazy var _timerQueue = DispatchQueue(label: "GCDTimer", qos: .userInitiated) 15 | private lazy var _timer: DispatchSourceTimer? = nil 16 | private lazy var _stateQueue = DispatchQueue(name: "GCDTimer.State") 17 | private lazy var _isStopped = false 18 | private lazy var _action: ((GCDTimer) -> Void)? = nil 19 | private var _name: String 20 | 21 | deinit { 22 | _timer?.setEventHandler {} 23 | _timer?.cancel() 24 | let isStopped = _stateQueue.sync { _isStopped } 25 | if isStopped { _timer?.resume() } 26 | debug_log("\(self)[\(_name)] \(#function)") 27 | } 28 | 29 | init(interval: DispatchTimeInterval, callback: @escaping (GCDTimer) -> Void, name: String = #file) { 30 | _name = name.components(separatedBy: "/").last ?? name 31 | GCDTimer.count = GCDTimer.count &+ 1 32 | index = GCDTimer.count 33 | _action = callback 34 | let timer = DispatchSource.makeTimerSource(flags: DispatchSource.TimerFlags(rawValue: 0), queue: _timerQueue) 35 | timer.schedule(deadline: .now() + interval, repeating: interval) 36 | timer.setEventHandler(handler: { [weak self] in 37 | guard let sself = self else { return } 38 | let isStopped = sself._stateQueue.sync { sself._isStopped } 39 | guard isStopped == false else { return } 40 | sself._action?(sself) 41 | }) 42 | _isStopped = true 43 | _timer = timer 44 | } 45 | 46 | func invalidate() { 47 | _action = nil 48 | _timer?.setEventHandler(handler: nil) 49 | pause() 50 | } 51 | 52 | func pause() { 53 | _stateQueue.sync { 54 | guard _isStopped == false else { return } 55 | _timer?.suspend() 56 | _isStopped = true 57 | } 58 | } 59 | 60 | func resume() { 61 | _stateQueue.sync { 62 | guard _isStopped == true else { return } 63 | _timer?.resume() 64 | _isStopped = false 65 | } 66 | } 67 | } 68 | 69 | // MARK: - Hashable 70 | 71 | extension GCDTimer: Hashable { 72 | func hash(into hasher: inout Hasher) { 73 | hasher.combine(index) 74 | } 75 | 76 | static func == (lhs: GCDTimer, rhs: GCDTimer) -> Bool { 77 | return lhs.index == rhs.index 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /APlay/Vendor/PlayList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayList.swift 3 | // APlay 4 | // 5 | // Created by lincoln on 2018/5/23. 6 | // Copyright © 2018年 SelfStudio. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A List for APlay 12 | public final class PlayList { 13 | public private(set) lazy var playingIndex: Int? = nil 14 | public private(set) lazy var list: [URL] = [] 15 | 16 | var loopPattern: LoopPattern = .order { didSet { updateRandomList() } } 17 | 18 | private lazy var _randomList: [URL] = [] 19 | public var randomList: [URL] { 20 | if loopPattern == .random { return _randomList } 21 | return [] 22 | } 23 | 24 | public var currentList: [URL] { 25 | if loopPattern == .random { return _randomList } 26 | return list 27 | } 28 | 29 | private unowned let _pipeline: Delegated 30 | 31 | #if DEBUG 32 | deinit { 33 | debug_log("\(self) \(#function)") 34 | } 35 | #endif 36 | 37 | init(pipeline: Delegated) { 38 | _pipeline = pipeline 39 | } 40 | 41 | public func changeList(to value: [URL], at index: Int) { 42 | list = value 43 | updateRandomList() 44 | playingIndex = index 45 | let list = loopPattern == .random ? _randomList : value 46 | _pipeline.call(.playlistChanged(list, index)) 47 | _pipeline.call(.playingIndexChanged(index)) 48 | } 49 | 50 | public func nextURL() -> URL? { 51 | guard list.count > 0 else { return nil } 52 | switch loopPattern { 53 | case .order: return _nextURL(pattern: .order) 54 | case .random: return _nextURL(pattern: .random) 55 | case .single: return _nextURL(pattern: .single) 56 | case let .stopWhenAllPlayed(mode): return _nextURL(pattern: mode) 57 | } 58 | } 59 | 60 | public func previousURL() -> URL? { 61 | guard list.count > 0 else { return nil } 62 | switch loopPattern { 63 | case .order: return _previousURL(pattern: .order) 64 | case .random: return _previousURL(pattern: .random) 65 | case .single: return _previousURL(pattern: .single) 66 | case let .stopWhenAllPlayed(mode): return _previousURL(pattern: mode) 67 | } 68 | } 69 | 70 | private func _nextURL(pattern: LoopPattern) -> URL? { 71 | var index = 0 72 | switch pattern { 73 | case .order: 74 | if let idx = playingIndex { index = idx + 1 } 75 | if index >= list.count { 76 | if loopPattern.isGonnaStopAtEndOfList { 77 | return nil 78 | } 79 | index = 0 80 | } 81 | playingIndex = index 82 | let url = list[index] 83 | return url 84 | case .random: 85 | if let idx = playingIndex { index = idx + 1 } 86 | if index >= list.count { 87 | if loopPattern.isGonnaStopAtEndOfList { 88 | return nil 89 | } 90 | index = 0 91 | } 92 | playingIndex = index 93 | let url = _randomList[index] 94 | return url 95 | case .single: 96 | if loopPattern.isGonnaStopAtEndOfList { 97 | return nil 98 | } 99 | if let idx = playingIndex { index = idx } 100 | playingIndex = index 101 | let url = list[index] 102 | return url 103 | case let .stopWhenAllPlayed(mode): 104 | if let idx = playingIndex, idx == list.count - 1 { return nil } 105 | switch mode { 106 | case .order: return _nextURL(pattern: .order) 107 | case .random: return _nextURL(pattern: .random) 108 | case .single: return _nextURL(pattern: .single) 109 | case let .stopWhenAllPlayed(mode2): return _nextURL(pattern: mode2) 110 | } 111 | } 112 | } 113 | 114 | private func _previousURL(pattern: LoopPattern) -> URL? { 115 | switch pattern { 116 | case .order: 117 | var index = 0 118 | if let idx = playingIndex { index = idx } 119 | if index == 0 { index = list.count - 1 } 120 | else { index -= 1 } 121 | playingIndex = index 122 | return list[ap_safe: index] 123 | case .random: 124 | var index = 0 125 | if let idx = playingIndex { index = idx } 126 | if index == 0 { index = _randomList.count - 1 } 127 | else { index -= 1 } 128 | playingIndex = index 129 | return _randomList[ap_safe: index] 130 | case .single: return _nextURL(pattern: .single) 131 | case let .stopWhenAllPlayed(mode): return _previousURL(pattern: mode) 132 | } 133 | } 134 | 135 | private func updateRandomList() { 136 | if loopPattern == .random { 137 | _randomList = list.shuffled() 138 | } else { 139 | _randomList = [] 140 | } 141 | } 142 | 143 | func play(at index: Int) -> URL? { 144 | guard let url = list[ap_safe: index] else { return nil } 145 | if loopPattern == .random { 146 | if let idx = _randomList.firstIndex(of: url) { 147 | playingIndex = idx 148 | return url 149 | } 150 | } 151 | playingIndex = index 152 | _pipeline.call(.playingIndexChanged(index)) 153 | return url 154 | } 155 | } 156 | 157 | // MARK: - Enums 158 | 159 | extension PlayList { 160 | public indirect enum LoopPattern: Equatable { 161 | case single 162 | case order 163 | case random 164 | case stopWhenAllPlayed(LoopPattern) 165 | 166 | var isGonnaStopAtEndOfList: Bool { 167 | switch self { 168 | case .stopWhenAllPlayed: return true 169 | default: return false 170 | } 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /APlay/Vendor/RunloopQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunloopQueue.swift 3 | // RunloopQueue 4 | // 5 | // Created by Daniel Kennett on 2017-02-14. 6 | // For license information, see LICENSE.md. 7 | // 8 | 9 | import Foundation 10 | 11 | /// [RunloopQueue](https://github.com/Cascable/runloop-queue) is a serial queue based on CFRunLoop, running on the background thread. 12 | public final class RunloopQueue { 13 | 14 | // MARK: - Code That Runs On The Main/Creating Thread 15 | 16 | private let thread: RunloopQueueThread 17 | /// Init a new queue with the given name. 18 | /// 19 | /// - Parameter name: The name of the queue. 20 | public init(named name: String?) { 21 | thread = RunloopQueueThread() 22 | thread.name = name 23 | startRunloop() 24 | } 25 | 26 | deinit { 27 | let runloop = self.runloop 28 | sync { CFRunLoopStop(runloop) } 29 | debug_log("\(self) \(#function), \(thread.name ?? "")") 30 | } 31 | 32 | /// Returns `true` if the queue is running, otherwise `false`. Once stopped, a queue cannot be restarted. 33 | public var running: Bool { return true } 34 | 35 | /// Execute a block of code in an asynchronous manner. Will return immediately. 36 | /// 37 | /// - Parameter block: The block of code to execute. 38 | public func async(_ block: @escaping () -> Void) { 39 | CFRunLoopPerformBlock(runloop, CFRunLoopMode.defaultMode.rawValue, block) 40 | thread.awake() 41 | } 42 | 43 | /// Execute a block of code in a synchronous manner. Will return when the code has executed. 44 | /// 45 | /// It's important to be careful with `sync()` to avoid deadlocks. In particular, calling `sync()` from inside 46 | /// a block previously passed to `sync()` will deadlock if the second call is made from a different thread. 47 | /// 48 | /// - Parameter block: The block of code to execute. 49 | public func sync(_ block: @escaping () -> Void) { 50 | if isRunningOnQueue() { 51 | block() 52 | return 53 | } 54 | 55 | let conditionLock = NSConditionLock(condition: 0) 56 | 57 | CFRunLoopPerformBlock(runloop, CFRunLoopMode.defaultMode.rawValue) { 58 | conditionLock.lock() 59 | block() 60 | conditionLock.unlock(withCondition: 1) 61 | } 62 | 63 | thread.awake() 64 | conditionLock.lock(whenCondition: 1) 65 | conditionLock.unlock() 66 | } 67 | 68 | /// Query if the caller is running on this queue. 69 | /// 70 | /// - Returns: `true` if the caller is running on this queue, otherwise `false`. 71 | public func isRunningOnQueue() -> Bool { 72 | return CFEqual(CFRunLoopGetCurrent(), runloop) 73 | } 74 | 75 | // MARK: - Code That Runs On The Background Thread 76 | 77 | private var runloop: CFRunLoop! 78 | private func startRunloop() { 79 | let conditionLock = NSConditionLock(condition: 0) 80 | 81 | thread.start { 82 | [weak self] runloop in 83 | // This is on the background thread. 84 | 85 | conditionLock.lock() 86 | defer { conditionLock.unlock(withCondition: 1) } 87 | 88 | guard let `self` = self else { return } 89 | self.runloop = runloop 90 | } 91 | 92 | conditionLock.lock(whenCondition: 1) 93 | conditionLock.unlock() 94 | } 95 | } 96 | 97 | private class RunloopQueueThread: Thread { 98 | // Required to keep the runloop running when nothing is going on. 99 | private let runloopSource: CFRunLoopSource 100 | private var currentRunloop: CFRunLoop? 101 | 102 | override init() { 103 | var sourceContext = CFRunLoopSourceContext() 104 | runloopSource = CFRunLoopSourceCreate(nil, 0, &sourceContext) 105 | } 106 | 107 | /// The callback to be called once the runloop has started executing. Will be called on the runloop's own thread. 108 | var whenReadyCallback: ((CFRunLoop) -> Void)? 109 | 110 | func start(whenReady call: @escaping (CFRunLoop) -> Void) { 111 | whenReadyCallback = call 112 | start() 113 | } 114 | 115 | func awake() { 116 | guard let runloop = currentRunloop else { return } 117 | if CFRunLoopIsWaiting(runloop) { 118 | CFRunLoopSourceSignal(runloopSource) 119 | CFRunLoopWakeUp(runloop) 120 | } 121 | } 122 | 123 | override func main() { 124 | let strongSelf = self 125 | let runloop = CFRunLoopGetCurrent()! 126 | currentRunloop = runloop 127 | 128 | CFRunLoopAddSource(runloop, runloopSource, .commonModes) 129 | 130 | let observer = CFRunLoopObserverCreateWithHandler(nil, CFRunLoopActivity.entry.rawValue, false, 0) { 131 | _, _ in 132 | strongSelf.whenReadyCallback?(runloop) 133 | } 134 | 135 | CFRunLoopAddObserver(runloop, observer, .commonModes) 136 | CFRunLoopRun() 137 | CFRunLoopRemoveObserver(runloop, observer, .commonModes) 138 | CFRunLoopRemoveSource(runloop, runloopSource, .commonModes) 139 | 140 | currentRunloop = nil 141 | } 142 | } 143 | 144 | public extension RunloopQueue { 145 | /// Schedules the given stream into the queue. 146 | /// 147 | /// - Parameter stream: The stream to schedule. 148 | 149 | func schedule(_ stream: Stream) { 150 | if let input = stream as? InputStream { 151 | sync { CFReadStreamScheduleWithRunLoop(input as CFReadStream, self.runloop, .commonModes) } 152 | } else if let output = stream as? OutputStream { 153 | sync { CFWriteStreamScheduleWithRunLoop(output as CFWriteStream, self.runloop, .commonModes) } 154 | } 155 | } 156 | 157 | /// Removes the given stream from the queue. 158 | /// 159 | /// - Parameter stream: The stream to remove. 160 | 161 | func unschedule(_ stream: Stream) { 162 | if let input = stream as? InputStream { 163 | CFReadStreamUnscheduleFromRunLoop(input as CFReadStream, runloop, .commonModes) 164 | } else if let output = stream as? OutputStream { 165 | sync { CFWriteStreamUnscheduleFromRunLoop(output as CFWriteStream, self.runloop, .commonModes) } 166 | } 167 | } 168 | 169 | func addTimer(_ value: CFRunLoopTimer) { 170 | CFRunLoopAddTimer(CFRunLoopGetCurrent(), value, .commonModes) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /APlay/Vendor/Uroboros.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Uroboros.swift 3 | // APlayer 4 | // 5 | // Created by lincoln on 2018/4/25. 6 | // Copyright © 2018年 SelfStudio. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A Circle Buffer Implementation for APlay 12 | public final class Uroboros { 13 | /// Basic type of buffer, UInt8 14 | public typealias Byte = UInt8 15 | 16 | private lazy var _start: UInt32 = 0 17 | private var start: UInt32 { 18 | get { return _propertiesQueue.sync { _start } } 19 | set { _propertiesQueue.sync { _start = newValue } } 20 | } 21 | 22 | private lazy var _end: UInt32 = 0 23 | private var end: UInt32 { 24 | get { return _propertiesQueue.sync { _end } } 25 | set { _propertiesQueue.sync { _end = newValue } } 26 | } 27 | 28 | private var _availableData: UInt32 = 0 29 | public var availableData: UInt32 { 30 | get { return _propertiesQueue.sync { _availableData } } 31 | set { _propertiesQueue.sync { _availableData = newValue } } 32 | } 33 | 34 | private var _availableSpace: UInt32 = 0 35 | public var availableSpace: UInt32 { 36 | get { return _propertiesQueue.sync { _availableSpace } } 37 | set { _propertiesQueue.sync { _availableSpace = newValue } } 38 | } 39 | 40 | private var _requiredSpace: UInt32 = 0 41 | /// required space for next write action 42 | private var requiredSpace: UInt32 { 43 | get { return _propertiesQueue.sync { _requiredSpace } } 44 | set { _propertiesQueue.sync { _requiredSpace = newValue } } 45 | } 46 | 47 | /// capacity of uroboros 48 | public var capacity: UInt32 { return UInt32(_body.capacity) } 49 | /// The base address of the buffer. 50 | private var baseAddress: UnsafeMutablePointer? { return _body.baseAddress } 51 | /// The end address of the buffer 52 | private var endAddress: UnsafeMutablePointer? { return baseAddress?.advanced(by: Int(end)) } 53 | /// The start address of the buffer 54 | public var startAddress: UnsafeMutablePointer? { return baseAddress?.advanced(by: Int(start)) } 55 | /// Queue for write action 56 | private let _writeQueue = DispatchQueue(label: "Uroboros.Write") 57 | private let _readQueue = DispatchQueue(label: "Uroboros.Read") 58 | /// Queue for properties I/O 59 | private let _propertiesQueue = DispatchQueue(label: "Uroboros.Properties") 60 | /// Semaphore for stop/continue write action 61 | private lazy var _semaphore = DispatchSemaphore(value: 0) 62 | /// Store content 63 | private var _body: UroborosBody 64 | 65 | private var _name: String 66 | 67 | private var _deliveryingFirstPacket = true 68 | 69 | #if DEBUG 70 | deinit { 71 | debug_log("\(self)[\(_name)] \(#function)") 72 | } 73 | #endif 74 | 75 | /// Init uroboros 76 | /// 77 | /// - Parameter count: size you want for buffer 78 | public init(capacity count: UInt32, name: String = #file) { 79 | _name = name.components(separatedBy: "/").last ?? name 80 | _body = UroborosBody(capacity: count) 81 | availableSpace = count 82 | } 83 | 84 | /// Store data into uroboros 85 | /// 86 | /// - Parameters: 87 | /// - data: data being stored 88 | /// - amount: size of bytes 89 | public func write(data: UnsafeRawPointer, amount: UInt32) { 90 | guard amount > 0 else { return } 91 | _writeQueue.sync { 92 | func checkSpace() { 93 | guard amount > availableSpace else { return } 94 | requiredSpace = amount 95 | _semaphore.wait() 96 | } 97 | checkSpace() 98 | let intCount = Int(amount) 99 | let targetLocation = end + amount 100 | if targetLocation > capacity { 101 | let secondPart = Int(targetLocation - capacity) 102 | let firstPart = intCount - secondPart 103 | memcpy(endAddress, data, firstPart) 104 | memcpy(baseAddress, data.advanced(by: firstPart), secondPart) 105 | } else { 106 | memcpy(endAddress, data, intCount) 107 | } 108 | commitWrite(count: amount) 109 | } 110 | } 111 | 112 | /// Get data form uroboros 113 | /// 114 | /// - Parameters: 115 | /// - amount: The number of bytes to retreive 116 | /// - data: The bytes to retreive buffer 117 | /// - commitRead: Can read data without commit 118 | /// - Returns: size for this time read 119 | @discardableResult public func readInQueue(amount: UInt32, into data: UnsafeMutableRawPointer, commitRead: Bool = true) -> (UInt32, Bool) { 120 | return _readQueue.sync { 121 | return self.read(amount: amount, into: data, commitRead: commitRead) 122 | } 123 | } 124 | 125 | /// Get data form uroboros 126 | /// 127 | /// - Parameters: 128 | /// - amount: The number of bytes to retreive 129 | /// - data: The bytes to retreive buffer 130 | /// - commitRead: Can read data without commit 131 | /// - Returns: size for this time read 132 | @discardableResult public func read(amount: UInt32, into data: UnsafeMutableRawPointer, commitRead: Bool = true) -> (UInt32, Bool) { 133 | if amount == 0 || availableData == 0 { return (0, false) } 134 | let read = _propertiesQueue.sync { 135 | return _availableData < amount ? _availableData : amount 136 | } 137 | let intCount = Int(read) 138 | let targetLocation = Int(_start) + intCount 139 | if targetLocation > capacity { 140 | let secondPartLength = targetLocation - Int(capacity) 141 | let firstPartLength = intCount - secondPartLength 142 | memcpy(data, startAddress, firstPartLength) 143 | memcpy(data.advanced(by: firstPartLength), baseAddress, secondPartLength) 144 | } else { 145 | memcpy(data, startAddress, intCount) 146 | } 147 | if commitRead { self.commitRead(count: read) } 148 | let value = _deliveryingFirstPacket 149 | _deliveryingFirstPacket = false 150 | return (read, value) 151 | } 152 | 153 | // MARK: - Private Functions 154 | 155 | /// Commit a read into the buffer, moving the `start` position 156 | public func commitRead(count: UInt32) { 157 | _propertiesQueue.sync { 158 | _start = (_start + count) % capacity 159 | if _availableData >= count { 160 | _availableData -= count 161 | } else { 162 | _availableData = 0 163 | } 164 | _availableSpace += count 165 | guard _availableSpace >= _requiredSpace, _requiredSpace > 0 else { return } 166 | _requiredSpace = 0 167 | _semaphore.signal() 168 | } 169 | } 170 | 171 | /// Commit a write into the buffer, moving the `end` position 172 | private func commitWrite(count: UInt32) { 173 | _propertiesQueue.sync { 174 | _end = (_end + count) % capacity 175 | _availableData += count 176 | _availableSpace -= count 177 | } 178 | } 179 | 180 | /// Reset to empty 181 | public func clear() { 182 | let data = availableData 183 | guard data > 0 else { return } 184 | commitRead(count: data) 185 | } 186 | } 187 | 188 | // MARK: - Uroboros Types 189 | 190 | extension Uroboros { 191 | /// Storage for `Uroboros` 192 | private final class UroborosBody { 193 | let capacity: UInt32 194 | 195 | /// Pointer to our allocated memory 196 | private(set) var storagePointer: UnsafeMutableRawPointer! 197 | 198 | /// Base address of the storage, as mapped to UInt8 199 | private(set) var baseAddress: UnsafeMutablePointer? 200 | 201 | init(capacity count: UInt32) { 202 | capacity = count 203 | let intCount = Int(count) 204 | let alignment = MemoryLayout.alignment 205 | storagePointer = UnsafeMutableRawPointer.allocate(byteCount: intCount, alignment: alignment) 206 | baseAddress = storagePointer.bindMemory(to: Byte.self, capacity: intCount) 207 | assert(baseAddress != nil, "UroborosBody cant not be nil") 208 | } 209 | 210 | deinit { 211 | if let base = baseAddress { 212 | base.deinitialize(count: Int(capacity)) 213 | } 214 | storagePointer.deallocate() 215 | _fixLifetime(self) 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /APlayDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // APlayDemo 4 | // 5 | // Created by Lincoln on 2019/1/21. 6 | // Copyright © 2019 SelfStudio. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /APlayDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /APlayDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /APlayDemo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /APlayDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /APlayDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.2.1 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /APlayDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // APlayDemo 4 | // 5 | // Created by Lincoln on 2019/1/21. 6 | // Copyright © 2019 SelfStudio. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import APlay 11 | 12 | class ViewController: UIViewController { 13 | 14 | private lazy var config: APlay.Configuration = { 15 | let c = APlay.Configuration(cachePolicy: .disable) 16 | return c 17 | }() 18 | private lazy var player: APlay = { 19 | return APlay(configuration: self.config) 20 | }() 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | // player.loopPattern = .stopWhenAllPlayed(.order) 25 | // player.eventPipeline.delegate(to: self) { (target, event) in 26 | // switch event { 27 | // case .playEnded: 28 | // self.player.pause() 29 | // DispatchQueue.main.async { 30 | // let vc = ViewControllerB() 31 | // self.show(vc, sender: nil) 32 | // } 33 | // default: break 34 | // } 35 | // } 36 | player.loopPattern = .random 37 | player.play(URL(fileURLWithPath: Bundle.main.path(forResource: "a", ofType: "m4a")!), URL(string: "https://umemore.shaunwill.cn/game/emotion/game_bgmusic.mp3")!) 38 | // DispatchQueue.main.asyncAfter(deadline: .now() + 3) { 39 | // self.player.seek(to: 60) 40 | // } 41 | // 42 | // DispatchQueue.main.asyncAfter(deadline: .now() + 10) { 43 | // self.player.seek(to: 180) 44 | // } 45 | } 46 | } 47 | 48 | class ViewControllerB: UIViewController { 49 | 50 | private let player = APlay() 51 | 52 | override func viewDidLoad() { 53 | super.viewDidLoad() 54 | player.loopPattern = .stopWhenAllPlayed(.order) 55 | player.eventPipeline.delegate(to: self) { (target, event) in 56 | switch event { 57 | case .playEnded: print("end") 58 | default: break 59 | } 60 | } 61 | player.play(URL(string: "https://umemore.shaunwill.cn/game/emotion/game_bgmusic.mp3")!) 62 | } 63 | } 64 | 65 | 66 | -------------------------------------------------------------------------------- /APlayDemo/a.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeEagle/APlay/e37afb6c2110b6149b37b291dd66e62ee6ebb539/APlayDemo/a.m4a -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | v0.0.5 2 | --- 3 | >2019.01.14 4 | 5 | 1. Support `stopWhenAllPlayed` mode 6 | 2. Add interruption handler, fixed auto fill `metadata` bug 7 | 8 | v0.0.4 9 | --- 10 | >2018.10.14 11 | 12 | 1. Change playlist also triiger event `playingIndexChanged` 13 | 2. Alter `PlayList` init method to internal 14 | 3. Fixed reconnect not working 15 | 16 | v0.0.3 17 | --- 18 | >2018.07.20 19 | 20 | 1. Add support for play a list at certain index 21 | 2. Change defaultCoverImage to allow modify on runtime 22 | 3. Change Connenct node location to avoid requst permission that using microphone 23 | 4. Support to set metadata on the fly 24 | 5. Change verbose log at debug level 25 | 6. Support to play at certain index and output index changed event 26 | 7. Fixed resume/pause Not set the right value in NowPlayingCenter 27 | 8. change fraquency of decode timer 28 | 9. Fixed glitches, format code, avoid memcpy when output decoded audio 29 | 30 | 31 | v0.0.2 32 | --- 33 | >2018.07.13 34 | 35 | 0. Changed optimization mode for release, or it will cause a cpu halt bug 36 | 1. Remove debug log in release mode 37 | 2. Remove repeated reset() function log 38 | 39 | v0.0.1 40 | --- 41 | >2018.07.09 42 | 43 | First Release. 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 CodeEagle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | APlay 2 | --- 3 | A Better(Maybe) iOS Audio Stream & Play Swift Framework 4 | 5 | 6 | Usage 7 | --- 8 | ```Swift 9 | import APlay 10 | ... 11 | let url = URL(string: "path/to/audio/resource")! 12 | let player = APlay() 13 | player.eventPipeline.delegate(to: self, with: { (target, event) in 14 | // event handling 15 | }) 16 | player.play(url) 17 | ... 18 | ``` 19 | 20 | ⚠️⚠️⚠️ Known issue 21 | --- 22 | This project can only run in `DEBUG` mode,cause optimization mode will pause the decode loop. 23 | 24 | if install with CocoaPods, add this block of code in your podfile 25 | ```ruby 26 | post_install do |installer| 27 | installer.pods_project.targets.each do |target| 28 | target.build_configurations.each do |config| 29 | swiftPods = ['APlay'] 30 | if swiftPods.include?(target.name) 31 | config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-Onone' 32 | end 33 | end 34 | end 35 | end 36 | ``` 37 | 38 | Docs 39 | --- 40 | Run `./generate_docs.sh` 41 | 42 | Features 43 | --- 44 | - [x] CPU-friendly design to avoid excessive peaks 45 | 46 | - [x] Support seek on WAVE, and FLAC(with seektable) 47 | 48 | - [x] Support all type of audio format(MP3, WAVE, FLAC, etc...) that iOS already support(Not fully tested) 49 | 50 | - [x] Digest(Tested), Basic(not tested) proxy support 51 | 52 | - [x] Multiple protocols supported: ShoutCast, standard HTTP, local files 53 | 54 | - [x] Prepared for tough network conditions: restart on failures,restart on not full content streamed when end of stream 55 | 56 | - [x] Metadata support: ShoutCast metadata, ID3V1, ID3v1.1, ID3v2.2, ID3v2.3, ID3v2.4, FLAC metadata 57 | 58 | - [x] Local disk storing: user can add folders for local resource loading 59 | 60 | - [x] Playback can start immediately without needing to wait for buffering 61 | 62 | - [x] Support cached the stream contents to a file 63 | 64 | - [x] Custom logging module and logging into file supported 65 | 66 | - [x] Open protocols to support customizing. `AudioDecoderCompatible`, `ConfigurationCompatible`, `LoggerCompatible`... 67 | 68 | Installation 69 | --- 70 | [Carthage](https://github.com/Carthage/Carthage) `github "CodeEagle/APlay"` 71 | 72 | [CocoaPods](https://cocoapods.org/) `pod 'APlay'` 73 | 74 | Todo 75 | --- 76 | - [ ] Airplay2 support(Maybe not) 77 | - [ ] AudioEffectUint support 78 | 79 | Sponsor 80 | --- 81 | [![Powered by DartNode](https://dartnode.com/branding/DN-Open-Source-sm.png)](https://dartnode.com "Powered by DartNode - Free VPS for Open Source") 82 | 83 | License 84 | --- 85 | [License](LICENSE) 86 | 87 | Contact 88 | --- 89 | [Github](https://github.com/CodeEagle), [Twitter](https://twitter.com/_SelfStudio) 90 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | fastlane_version "2.58.0" 2 | 3 | default_platform :ios 4 | 5 | platform :ios do 6 | before_all do 7 | 8 | end 9 | desc "Lint" 10 | lane :lint do 11 | pod_lib_lint( 12 | allow_warnings: true, 13 | sources: ["https://github.com/CocoaPods/Specs.git"] 14 | ) 15 | end 16 | 17 | desc "Release new version" 18 | lane :release do |options| 19 | target_version = options[:version] 20 | raise "The version is missed. Use `fastlane release version:{version_number}`.`" if target_version.nil? 21 | 22 | ensure_git_branch 23 | ensure_git_status_clean 24 | 25 | # test_all_schemes 26 | lint 27 | 28 | increment_version_number(version_number: target_version) 29 | version_bump_podspec(path: "APlay.podspec", version_number: target_version) 30 | 31 | changelog = changelog_from_git_commits 32 | 33 | Action.sh("git add -A") 34 | Actions.sh("git commit -am \"Bumped to version #{target_version}\"") 35 | 36 | Actions.sh("git tag -a #{target_version} -m ''") 37 | 38 | push_to_git_remote 39 | 40 | pod_push(allow_warnings: true) 41 | end 42 | 43 | after_all do |lane| 44 | 45 | end 46 | 47 | error do |lane, exception| 48 | 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | Install _fastlane_ using 12 | ``` 13 | [sudo] gem install fastlane -NV 14 | ``` 15 | or alternatively using `brew cask install fastlane` 16 | 17 | # Available Actions 18 | ## iOS 19 | ### ios lint 20 | ``` 21 | fastlane ios lint 22 | ``` 23 | Lint 24 | ### ios release 25 | ``` 26 | fastlane ios release 27 | ``` 28 | Release new version 29 | 30 | ---- 31 | 32 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 33 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 34 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 35 | -------------------------------------------------------------------------------- /generate_docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # git describe --tags `git rev-list --tags --max-count=1` 4 | VERSION="0.0.1" 5 | 6 | jazzy \ 7 | --clean \ 8 | --author CodeEagle \ 9 | --author_url https://selfstudio.app \ 10 | --github_url https://github.com/CodeEagle/APlay \ 11 | --github-file-prefix https://github.com/CodeEagle/APlay/tree/v$VERSION \ 12 | --module-version $VERSION \ 13 | --xcodebuild-arguments -scheme,APlay \ 14 | --module APlay \ 15 | --output docs/ 16 | --------------------------------------------------------------------------------