Compare commits

...

10 Commits

Author SHA1 Message Date
tanhakabir 7d65841f1c fix recursive polling logic 2021-04-16 10:04:33 -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 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
8 changed files with 55 additions and 49 deletions
@@ -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"
}
+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
+1
View File
@@ -173,6 +173,7 @@ class AudioEngine: AudioEngineProtocol {
Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false) { [weak self] (timer: Timer) in
guard let self = self else { return }
guard self.playingStatus != .ended else {
// Log.test("END TIMER")
self.delegate = nil
return
}
+16 -31
View File
@@ -71,6 +71,7 @@ class AudioStreamEngine: AudioEngine {
private var numberOfBuffersScheduledInTotal = 0 {
didSet {
// Log.test(numberOfBuffersScheduledInTotal)
Log.debug("number of buffers scheduled in total: \(numberOfBuffersScheduledInTotal)")
if numberOfBuffersScheduledInTotal == 0 {
pause()
@@ -86,6 +87,7 @@ class AudioStreamEngine: AudioEngine {
private var numberOfBuffersScheduledFromPoll = 0 {
didSet {
if numberOfBuffersScheduledFromPoll > MAX_POLL_BUFFER_COUNT {
// Log.test("🛑 🛑 STOP POLLING")
shouldPollForNextBuffer = false
}
@@ -137,7 +139,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()
}
@@ -165,8 +167,15 @@ class AudioStreamEngine: AudioEngine {
private func pollForNextBuffer() {
guard shouldPollForNextBuffer else { return }
// Log.test("POLL INIT")
pollForNextBufferRecursive()
}
private func pollForNextBufferRecursive() {
// Log.test("POLL")
do {
var nextScheduledBuffer: AVAudioPCMBuffer! = try converter.pullBuffer(withSize: PCM_BUFFER_SIZE)
var nextScheduledBuffer: AVAudioPCMBuffer! = try converter.pullBuffer()
numberOfBuffersScheduledFromPoll += 1
numberOfBuffersScheduledInTotal += 1
@@ -174,16 +183,18 @@ class AudioStreamEngine: AudioEngine {
queue.async { [weak self] in
if #available(iOS 11.0, *) {
// to make sure the pcm buffers are properly free'd from memory we need to nil them after the player has used them
self?.playerNode.scheduleBuffer(nextScheduledBuffer, completionCallbackType: .dataRendered, completionHandler: { (_) in
self?.playerNode.scheduleBuffer(nextScheduledBuffer, completionCallbackType: .dataConsumed, completionHandler: { (_) in
nextScheduledBuffer = nil
self?.numberOfBuffersScheduledInTotal -= 1
self?.pollForNextBufferRecursionHelper()
// Log.test("POLL DATA RENDERED")
self?.pollForNextBufferRecursive()
})
} else {
self?.playerNode.scheduleBuffer(nextScheduledBuffer) {
nextScheduledBuffer = nil
self?.numberOfBuffersScheduledInTotal -= 1
self?.pollForNextBufferRecursionHelper()
// Log.test("POLL OLD")
self?.pollForNextBufferRecursive()
}
}
}
@@ -200,32 +211,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
+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.