Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 97909bacce | |||
| 30b0189f61 | |||
| 5bde849bf0 | |||
| b3b519ab4c | |||
| f3b62cc756 | |||
| a56d3314ad | |||
| f75d743cd9 | |||
| f8876d821e |
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user