Compare commits

..

12 Commits

Author SHA1 Message Date
tanhakabir 3d96cc07b2 Merge branch 'master' into issue-88 2021-04-06 21:17:36 -07:00
tanhakabir 17e0ee5dd8 Update example app to include a radio stream link (#89)
* update links

* update example app to show podcast, soundbite, and radio
2021-04-06 21:16:13 -07:00
tanhakabir 9344066468 checkpoint 2021-04-06 20:59:03 -07:00
tanhakabir 0fee2b32e5 move cellular data permissions to new namespace 2021-03-22 23:23:14 -07:00
tanhakabir 97909bacce Release 4.1.0 2021-03-22 23:04:53 -07:00
tanhakabir 30b0189f61 Merge Fix PCM memory leak #87 2021-03-22 23:01:35 -07:00
tanhakabir 5bde849bf0 fix PCM memory leak 2021-03-22 23:00:35 -07:00
tanhakabir b3b519ab4c Revert "audio skips, but reusing same pcm buffers"
This reverts commit f3b62cc756.
2021-03-22 22:49:31 -07:00
tanhakabir f3b62cc756 audio skips, but reusing same pcm buffers 2021-03-22 11:52:29 -07:00
tanhakabir a56d3314ad Update README.md 2021-03-22 11:50:53 -07:00
tanhakabir f75d743cd9 small refractor 2021-03-20 11:59:06 -07:00
tanhakabir f8876d821e remove unnecessary recursion helper 2021-03-20 11:54:56 -07:00
13 changed files with 110 additions and 65 deletions
+4
View File
@@ -17,6 +17,7 @@
A40DBE292391D9CA00F86146 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40DBE282391D9C900F86146 /* Data.swift */; };
A411CE4625F9609D0039E1CD /* SAPlayerFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = A411CE4525F9609D0039E1CD /* SAPlayerFeatures.swift */; };
A41AA0D2238BB9B600A467E1 /* SAPlayingStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = A41AA0D1238BB9B600A467E1 /* SAPlayingStatus.swift */; };
A42157B22609BF8900253D01 /* SAPlayerNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = A42157B12609BF8900253D01 /* SAPlayerNetwork.swift */; };
A4681FC6220113880018AB51 /* SAPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F8D2200E00E0018AB51 /* SAPlayer.swift */; };
A4681FC72201138B0018AB51 /* SAPlayerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F912200E1950018AB51 /* SAPlayerDelegate.swift */; };
A4681FC82201138E0018AB51 /* SAPlayerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F8F2200E1450018AB51 /* SAPlayerPresenter.swift */; };
@@ -103,6 +104,7 @@
A40DBE282391D9C900F86146 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
A411CE4525F9609D0039E1CD /* SAPlayerFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerFeatures.swift; sourceTree = "<group>"; };
A41AA0D1238BB9B600A467E1 /* SAPlayingStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayingStatus.swift; sourceTree = "<group>"; };
A42157B12609BF8900253D01 /* SAPlayerNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerNetwork.swift; sourceTree = "<group>"; };
A4523BC8220A0B3C0079C4BC /* Credited_LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = Credited_LICENSE; sourceTree = "<group>"; };
A4681F802200D0500018AB51 /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = "<group>"; };
A4681F822200D9150018AB51 /* AudioEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioEngine.swift; sourceTree = "<group>"; };
@@ -361,6 +363,7 @@
A411CE4525F9609D0039E1CD /* SAPlayerFeatures.swift */,
A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */,
A4B4CC112223ED2A0045554B /* SAPlayerDownloader.swift */,
A42157B12609BF8900253D01 /* SAPlayerNetwork.swift */,
A4681F912200E1950018AB51 /* SAPlayerDelegate.swift */,
A4681F8F2200E1450018AB51 /* SAPlayerPresenter.swift */,
A4681FBE22010ECF0018AB51 /* LockScreenViewProtocol.swift */,
@@ -548,6 +551,7 @@
A4681FE1220113E70018AB51 /* Constants.swift in Sources */,
A40DBE292391D9CA00F86146 /* Data.swift in Sources */,
A4FBA6B5221B74C900D5A353 /* SALockScreenInfo.swift in Sources */,
A42157B22609BF8900253D01 /* SAPlayerNetwork.swift in Sources */,
A4681FC6220113880018AB51 /* SAPlayer.swift in Sources */,
A4FBA6B7221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift in Sources */,
A4681FC72201138B0018AB51 /* SAPlayerDelegate.swift in Sources */,
@@ -72,8 +72,8 @@
<rect key="frame" x="16" y="60" width="343" height="32"/>
<segments>
<segment title="Soundbite"/>
<segment title="Acquired"/>
<segment title="Y Combinator"/>
<segment title="Podcast"/>
<segment title="Radio"/>
</segments>
<connections>
<action selector="audioSelected:" destination="vXZ-lx-hvc" eventType="valueChanged" id="oYE-yq-348"/>
+7 -7
View File
@@ -13,18 +13,18 @@ struct AudioInfo: Hashable {
var urls: [URL] = [URL(string: "https://www.fesliyanstudios.com/musicfiles/2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com/15SecVersion2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com.mp3")!,
URL(string: "https://chtbl.com/track/18338/traffic.libsyn.com/secure/acquired/acquired_-_armrev_2.mp3?dest-id=376122")!,
URL(string: "https://backtracks.fm/ycombinator/pr/0f685f72-29b1-11e9-9bcf-0ece7a7d2472/111---jake-klamka-and-kevin-hale---y-combinator.mp3?s=1&amp;sd=1&amp;u=1549423185")!]
URL(string: "https://ice6.somafm.com/groovesalad-256-mp3")!]
var url: URL {
switch index {
case 0:
return URL(string: "https://www.fesliyanstudios.com/musicfiles/2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com/15SecVersion2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com.mp3")!
return urls[0]
case 1:
return URL(string: "https://chtbl.com/track/18338/traffic.libsyn.com/secure/acquired/acquired_-_armrev_2.mp3?dest-id=376122")!
return urls[1]
case 2:
return URL(string: "https://backtracks.fm/ycombinator/pr/0f685f72-29b1-11e9-9bcf-0ece7a7d2472/111---jake-klamka-and-kevin-hale---y-combinator.mp3?s=1&amp;sd=1&amp;u=1549423185")!
return urls[2]
default:
return URL(string: "https://www.fesliyanstudios.com/musicfiles/2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com/15SecVersion2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com.mp3")!
return urls[0]
}
}
@@ -33,9 +33,9 @@ struct AudioInfo: Hashable {
case 0:
return "Soundbite"
case 1:
return "Acquired"
return "Podcast"
case 2:
return "Y Combinator"
return "Radio"
default:
return "Soundbite"
}
@@ -68,7 +68,7 @@ class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
SAPlayer.Downloader.allowUsingCellularData = true
SAPlayer.Network.CellularData.allowUsage = true
// SAPlayer.shared.DEBUG_MODE = true
+2 -1
View File
@@ -17,6 +17,7 @@ Thus, using [AudioToolbox](https://developer.apple.com/documentation/audiotoolbo
1. Play locally saved audio with the same API
1. Download audio
1. Queue up downloaded and streamed audio for autoplay
1. Uses only 1-2% CPU for optimal performance for the rest of your app
1. You're able to install taps and any other AVAudioEngine features to do cool things like skipping silences
### Special Features
@@ -33,7 +34,7 @@ iOS 10.0 and higher.
### Running the Example Project
1. Clone repo
2. CD to directory
2. CD to the `Example` folder where the Example app lives
3. Run `pod install` in terminal
4. Build and run
+6 -32
View File
@@ -137,7 +137,7 @@ class AudioStreamEngine: AudioEngine {
Log.info(url)
super.init(url: url, delegate: delegate, engineAudioFormat: AudioEngine.defaultEngineAudioFormat)
do {
converter = try AudioConverter(withRemoteUrl: url, toEngineAudioFormat: AudioEngine.defaultEngineAudioFormat)
converter = try AudioConverter(withRemoteUrl: url, toEngineAudioFormat: AudioEngine.defaultEngineAudioFormat, withPCMBufferSize: PCM_BUFFER_SIZE)
} catch {
delegate?.didError()
}
@@ -149,7 +149,7 @@ class AudioStreamEngine: AudioEngine {
guard let self = self else { return }
guard self.playingStatus != .ended else { return }
self.pollForNextBuffer()
self.pollForNextBufferRecursive()
self.updateNetworkBufferRange()
self.updateNeedle()
self.updateIsPlaying()
@@ -162,11 +162,11 @@ class AudioStreamEngine: AudioEngine {
//Called when
//1. First time audio is finally parsed
//2. When we run to the end of the network buffer and we're waiting again
private func pollForNextBuffer() {
private func pollForNextBufferRecursive() {
guard shouldPollForNextBuffer else { return }
do {
var nextScheduledBuffer: AVAudioPCMBuffer! = try converter.pullBuffer(withSize: PCM_BUFFER_SIZE)
var nextScheduledBuffer: AVAudioPCMBuffer! = try converter.pullBuffer()
numberOfBuffersScheduledFromPoll += 1
numberOfBuffersScheduledInTotal += 1
@@ -177,13 +177,13 @@ class AudioStreamEngine: AudioEngine {
self?.playerNode.scheduleBuffer(nextScheduledBuffer, completionCallbackType: .dataRendered, completionHandler: { (_) in
nextScheduledBuffer = nil
self?.numberOfBuffersScheduledInTotal -= 1
self?.pollForNextBufferRecursionHelper()
self?.pollForNextBufferRecursive()
})
} else {
self?.playerNode.scheduleBuffer(nextScheduledBuffer) {
nextScheduledBuffer = nil
self?.numberOfBuffersScheduledInTotal -= 1
self?.pollForNextBufferRecursionHelper()
self?.pollForNextBufferRecursive()
}
}
}
@@ -200,32 +200,6 @@ class AudioStreamEngine: AudioEngine {
}
}
private func pollForNextBufferRecursionHelper() {
do {
let nextScheduledBuffer = try converter.pullBuffer(withSize: PCM_BUFFER_SIZE)
Log.debug("processed buffer for engine of frame lengthL \(nextScheduledBuffer.frameLength)")
numberOfBuffersScheduledInTotal += 1
queue.async { [weak self] in
self?.playerNode.scheduleBuffer(nextScheduledBuffer) {
self?.numberOfBuffersScheduledInTotal -= 1
self?.pollForNextBufferRecursionHelper()
}
}
} catch ConverterError.reachedEndOfFile {
Log.info(ConverterError.reachedEndOfFile.localizedDescription)
} catch ConverterError.notEnoughData {
shouldPollForNextBuffer = true
Log.debug(ConverterError.notEnoughData.localizedDescription)
} catch ConverterError.superConcerningShouldNeverHappen {
Log.error(ConverterError.superConcerningShouldNeverHappen.localizedDescription)
} catch {
Log.debug(error.localizedDescription)
}
}
private func updateNetworkBufferRange() { //for ui
let range = converter.pollNetworkAudioAvailabilityRange()
isPlayable = (numberOfBuffersScheduledInTotal >= MIN_BUFFERS_TO_BE_PLAYABLE && range.1 > 0) && predictedStreamDuration > 0
+14 -7
View File
@@ -36,8 +36,8 @@ import AudioToolbox
protocol AudioConvertable {
var engineAudioFormat: AVAudioFormat {get}
init(withRemoteUrl url: AudioURL, toEngineAudioFormat: AVAudioFormat) throws
func pullBuffer(withSize size: AVAudioFrameCount) throws -> AVAudioPCMBuffer
init(withRemoteUrl url: AudioURL, toEngineAudioFormat: AVAudioFormat, withPCMBufferSize size: AVAudioFrameCount) throws
func pullBuffer() throws -> AVAudioPCMBuffer
func pollPredictedDuration() -> Duration?
func pollNetworkAudioAvailabilityRange() -> (Needle, Duration)
func seek(_ needle: Needle)
@@ -70,13 +70,20 @@ class AudioConverter: AudioConvertable {
//From protocol
public var engineAudioFormat: AVAudioFormat
let pcmBufferSize: AVAudioFrameCount
//Field
var converter: AudioConverterRef? //set by AudioConverterNew
var currentAudioPacketIndex: AVAudioPacketCount = 0
required init(withRemoteUrl url: AudioURL, toEngineAudioFormat: AVAudioFormat) throws {
// use to store reference to the allocated buffers from the converter to properly deallocate them before the next packet is being converted
var converterBuffer: UnsafeMutableRawPointer?
var converterDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>?
required init(withRemoteUrl url: AudioURL, toEngineAudioFormat: AVAudioFormat, withPCMBufferSize size: AVAudioFrameCount) throws {
self.engineAudioFormat = toEngineAudioFormat
self.pcmBufferSize = size
do {
parser = try AudioParser(withRemoteUrl: url, parsedFileAudioFormatCallback: {
[weak self] (fileAudioFormat: AVAudioFormat) in
@@ -108,17 +115,17 @@ class AudioConverter: AudioConvertable {
}
}
func pullBuffer(withSize size: AVAudioFrameCount) throws -> AVAudioPCMBuffer {
func pullBuffer() throws -> AVAudioPCMBuffer {
guard let converter = converter else {
Log.debug("reader_error trying to read before converter has been created")
throw ConverterError.cannotCreatePCMBufferWithoutConverter
}
guard let pcmBuffer = AVAudioPCMBuffer(pcmFormat: engineAudioFormat, frameCapacity: size) else {
guard let pcmBuffer = AVAudioPCMBuffer(pcmFormat: engineAudioFormat, frameCapacity: pcmBufferSize) else {
Log.monitor(ConverterError.failedToCreatePCMBuffer.errorDescription as Any)
throw ConverterError.failedToCreatePCMBuffer
}
pcmBuffer.frameLength = size
pcmBuffer.frameLength = pcmBufferSize
/**
The whole thing is wrapped in queue.sync() because the converter listener
@@ -127,7 +134,7 @@ class AudioConverter: AudioConvertable {
*/
return try queue.sync { () -> AVAudioPCMBuffer in
let framesPerPacket = engineAudioFormat.streamDescription.pointee.mFramesPerPacket
var numberOfPacketsWeWantTheBufferToFill = size / framesPerPacket
var numberOfPacketsWeWantTheBufferToFill = pcmBuffer.frameLength / framesPerPacket
let context = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
let status = AudioConverterFillComplexBuffer(converter, ConverterListener, context, &numberOfPacketsWeWantTheBufferToFill, pcmBuffer.mutableAudioBufferList, nil)
@@ -65,6 +65,10 @@ func ConverterListener(_ converter: AudioConverterRef, _ packetCount: UnsafeMuta
return ReaderShouldNotHappenError
}
if let lastBuffer = selfAudioConverter.converterBuffer {
lastBuffer.deallocate()
}
// Copy data over (note we've only processing a single packet of data at a time)
var packet = audioPacket.1
let packetByteCount = packet.count //this is not the count of an array
@@ -75,6 +79,12 @@ func ConverterListener(_ converter: AudioConverterRef, _ packetCount: UnsafeMuta
})
ioData.pointee.mBuffers.mDataByteSize = UInt32(packetByteCount)
selfAudioConverter.converterBuffer = ioData.pointee.mBuffers.mData
if let lastDescription = selfAudioConverter.converterDescriptions {
lastDescription.deallocate()
}
// Handle packet descriptions for compressed formats (MP3, AAC, etc)
let fileFormatDescription = fileAudioFormat.streamDescription.pointee
if fileFormatDescription.mFormatID != kAudioFormatLinearPCM {
@@ -86,6 +96,8 @@ func ConverterListener(_ converter: AudioConverterRef, _ packetCount: UnsafeMuta
outPacketDescriptions?.pointee?.pointee.mVariableFramesInPacket = 0
}
selfAudioConverter.converterDescriptions = outPacketDescriptions?.pointee
packetCount.pointee = 1
//we've successfully given a packet to the LPCM buffer now we can process the next audio packet
+6 -3
View File
@@ -29,8 +29,6 @@ protocol AudioDataManagable {
var numberOfQueued: Int { get }
var numberOfActive: Int { get }
var allowCellular: Bool { get set }
func setBackgroundCompletionHandler(_ completionHandler: @escaping () -> ())
func setAllowCellularDownloadPreference(_ preference: Bool)
@@ -53,6 +51,7 @@ protocol AudioDataManagable {
class AudioDataManager: AudioDataManagable {
var allowCellular: Bool = true
var userAgent: String?
static let shared: AudioDataManagable = AudioDataManager()
@@ -104,6 +103,10 @@ class AudioDataManager: AudioDataManagable {
allowCellular = preference
}
func setUserAgent(_ userAgent: String) {
self.userAgent = userAgent
}
func attach(callback: @escaping (_ id: ID, _ progress: Double)->()) {
globalDownloadProgressCallback = callback
}
@@ -128,7 +131,7 @@ extension AudioDataManager {
downloadWorker.stop(withID: url.key) { [weak self] (fetchedData: Data?, totalBytesExpected: Int64?) in
self?.downloadWorker.pauseAllActive()
self?.streamWorker.start(withID: url.key, withRemoteURL: url, withInitialData: fetchedData, andTotalBytesExpectedPreviously: totalBytesExpected)
self?.streamWorker.start(withID: url.key, withRemoteURL: url, withInitialData: fetchedData, andTotalBytesExpectedPreviously: totalBytesExpected, withUserAgent: userAgent)
}
}
@@ -44,7 +44,7 @@ import Foundation
protocol AudioDataStreamable {
//if user taps download then starts to stream
init(progressCallback: @escaping (_ id: ID, _ dto: StreamProgressDTO) -> (), doneCallback: @escaping (_ id: ID, _ error: Error?)->Bool) //Bool is should save or not
func start(withID id: ID, withRemoteURL url: URL, withInitialData data: Data?, andTotalBytesExpectedPreviously previousTotalBytesExpected: Int64?)
func start(withID id: ID, withRemoteURL url: URL, withInitialData data: Data?, andTotalBytesExpectedPreviously previousTotalBytesExpected: Int64?, withUserAgent userAgent: String?)
func pause(withId id: ID)
func resume(withId id: ID)
func stop(withId id: ID)//FIXME: with persistent play we should return a Data so that download can resume
@@ -95,7 +95,7 @@ class AudioStreamWorker:NSObject, AudioDataStreamable {
self.session = URLSession(configuration: config, delegate: self, delegateQueue: nil) //TODO: should we use ephemeral
}
func start(withID id: ID, withRemoteURL url: URL, withInitialData data: Data? = nil, andTotalBytesExpectedPreviously previousTotalBytesExpected: Int64? = nil) {
func start(withID id: ID, withRemoteURL url: URL, withInitialData data: Data? = nil, andTotalBytesExpectedPreviously previousTotalBytesExpected: Int64? = nil, withUserAgent userAgent: String?) {
Log.info("selfID: \(self.id ?? "none"), paramID: \(id) initialData: \(data?.count ?? 0)")
killPreviousTaskIfNeeded()
-9
View File
@@ -100,14 +100,5 @@ extension SAPlayer {
public static func setBackgroundCompletionHandler(_ completionHandler: @escaping () -> ()) {
AudioDataManager.shared.setBackgroundCompletionHandler(completionHandler)
}
/**
Whether downloading audio on cellular data is allowed. By default this is set to `true`.
*/
public static var allowUsingCellularData = true {
didSet {
AudioDataManager.shared.setAllowCellularDownloadPreference(allowUsingCellularData)
}
}
}
}
+53
View File
@@ -0,0 +1,53 @@
//
// SAPlayerNetwork.swift
// SwiftAudioPlayer
//
// Created by Tanha Kabir on 3/22/21.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
extension SAPlayer {
/**
Actions relating to remote requests being made to download or stream audio data.
*/
public struct Network {
/**
Handle permissions for downloading and/or streaming on cellular data.
*/
public struct CellularData {
/**
Whether downloading audio on cellular data is allowed. By default this is set to `true`.
*/
public static var allowUsage = true {
didSet {
AudioDataManager.shared.setAllowCellularDownloadPreference(allowUsage)
}
}
}
}
public struct Request {
public func setUserAgent(with userAgent: String) {
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudioPlayer'
s.version = '4.0.0'
s.version = '4.1.0'
s.summary = 'SwiftAudioPlayer is a Swift based audio player that can handle streaming from a remote location and audio manipulation.'
# This description is used to generate tags and improve search results.