├── .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 | [](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 |
--------------------------------------------------------------------------------