Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b89d3d953f | |||
| 4951b54ede | |||
| 2337cd3844 | |||
| f8f836125d | |||
| d24bca48a2 | |||
| 1916a0628a | |||
| 579fd26846 |
@@ -19,6 +19,7 @@ enum AudioContent {
|
||||
case remoteWave
|
||||
case local
|
||||
case localWave
|
||||
case loopBeatFlac
|
||||
case custom(String)
|
||||
|
||||
var title: String {
|
||||
@@ -49,6 +50,8 @@ enum AudioContent {
|
||||
return "Jazzy Frenchy"
|
||||
case .nonOptimized:
|
||||
return "Jazzy Frenchy"
|
||||
case .loopBeatFlac:
|
||||
return "Beat loop"
|
||||
case .custom(let url):
|
||||
return url
|
||||
}
|
||||
@@ -82,6 +85,8 @@ enum AudioContent {
|
||||
return "Music by: bensound.com - m4a optimized"
|
||||
case .nonOptimized:
|
||||
return "Music by: bensound.com - m4a non-optimized"
|
||||
case .loopBeatFlac:
|
||||
return "Remote flac"
|
||||
case .custom:
|
||||
return ""
|
||||
}
|
||||
@@ -117,6 +122,8 @@ enum AudioContent {
|
||||
return URL(fileURLWithPath: path)
|
||||
case .remoteWave:
|
||||
return URL(string: "https://github.com/dimitris-c/sample-audio/raw/main/5-MB-WAV.wav")!
|
||||
case .loopBeatFlac:
|
||||
return URL(string: "https://github.com/dimitris-c/sample-audio/raw/main/drumbeat-loop.flac")!
|
||||
case .custom(let url):
|
||||
return URL(string: url)!
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public class AudioPlayerModel {
|
||||
}
|
||||
|
||||
private let radioTracks: [AudioContent] = [.offradio, .enlefko, .pepper966, .kosmos, .kosmosJazz, .radiox]
|
||||
private let audioTracks: [AudioContent] = [.khruangbin, .piano, .optimized, .nonOptimized, .remoteWave, .local, .localWave]
|
||||
private let audioTracks: [AudioContent] = [.khruangbin, .piano, .optimized, .nonOptimized, .remoteWave, .local, .localWave, .loopBeatFlac]
|
||||
|
||||
func audioTracksProvider() -> [AudioPlaylist] {
|
||||
[
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'AudioStreaming'
|
||||
s.version = '1.2.3'
|
||||
s.version = '1.2.5'
|
||||
s.license = 'MIT'
|
||||
s.summary = 'An AudioPlayer/Streaming library for iOS written in Swift using AVAudioEngine.'
|
||||
s.homepage = 'https://github.com/dimitris-c/AudioStreaming'
|
||||
|
||||
@@ -833,7 +833,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.3;
|
||||
MARKETING_VERSION = 1.2.5;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
@@ -865,7 +865,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.3;
|
||||
MARKETING_VERSION = 1.2.5;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os
|
||||
|
||||
protocol Lock {
|
||||
func lock()
|
||||
@@ -14,24 +15,96 @@ protocol Lock {
|
||||
|
||||
// Execute a closure while acquiring a lock
|
||||
func withLock(body: () -> Void)
|
||||
|
||||
func deallocate()
|
||||
}
|
||||
|
||||
/// A wrapper for `os_unfair_lock`
|
||||
/// - Tag: UnfairLock
|
||||
final class UnfairLock: Lock {
|
||||
@usableFromInline let unfairLock: UnsafeMutablePointer<os_unfair_lock>
|
||||
|
||||
var unfairLock: Lock
|
||||
|
||||
init() {
|
||||
if #available(iOS 16.0, *), #available(macOS 13.0, *) {
|
||||
unfairLock = OSStorageLock()
|
||||
} else {
|
||||
unfairLock = UnfairStorageLock()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
deallocate()
|
||||
}
|
||||
|
||||
func deallocate() {
|
||||
unfairLock.deallocate()
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func withLock<Result>(body: () throws -> Result) rethrows -> Result {
|
||||
try unfairLock.withLock(body: body)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func withLock(body: () -> Void) {
|
||||
unfairLock.withLock(body: body)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func lock() {
|
||||
unfairLock.lock()
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func unlock() {
|
||||
unfairLock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
@available(macOS 13, *)
|
||||
private class OSStorageLock: Lock {
|
||||
@usableFromInline
|
||||
let osLock = OSAllocatedUnfairLock()
|
||||
|
||||
@inlinable
|
||||
func lock() {
|
||||
osLock.lock()
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func unlock() {
|
||||
osLock.unlock()
|
||||
}
|
||||
|
||||
func withLock<Result>(body: () throws -> Result) rethrows -> Result {
|
||||
try osLock.withLockUnchecked(body)
|
||||
}
|
||||
|
||||
func withLock(body: () -> Void) {
|
||||
osLock.withLockUnchecked(body)
|
||||
}
|
||||
|
||||
func deallocate() {} // no-op
|
||||
}
|
||||
|
||||
private class UnfairStorageLock: Lock {
|
||||
|
||||
@usableFromInline
|
||||
let unfairLock: UnsafeMutablePointer<os_unfair_lock>
|
||||
|
||||
init() {
|
||||
unfairLock = .allocate(capacity: 1)
|
||||
unfairLock.initialize(to: os_unfair_lock())
|
||||
}
|
||||
|
||||
deinit {
|
||||
func deallocate() {
|
||||
unfairLock.deinitialize(count: 1)
|
||||
unfairLock.deallocate()
|
||||
}
|
||||
|
||||
@inlinable
|
||||
@inline(__always)
|
||||
func withLock<Result>(body: () throws -> Result) rethrows -> Result {
|
||||
os_unfair_lock_lock(unfairLock)
|
||||
defer { os_unfair_lock_unlock(unfairLock) }
|
||||
@@ -39,7 +112,6 @@ final class UnfairLock: Lock {
|
||||
}
|
||||
|
||||
@inlinable
|
||||
@inline(__always)
|
||||
func withLock(body: () -> Void) {
|
||||
os_unfair_lock_lock(unfairLock)
|
||||
defer { os_unfair_lock_unlock(unfairLock) }
|
||||
@@ -47,13 +119,11 @@ final class UnfairLock: Lock {
|
||||
}
|
||||
|
||||
@inlinable
|
||||
@inline(__always)
|
||||
func lock() {
|
||||
os_unfair_lock_lock(unfairLock)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
@inline(__always)
|
||||
func unlock() {
|
||||
os_unfair_lock_unlock(unfairLock)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public struct AudioPlayerConfiguration: Equatable {
|
||||
bufferSizeInSeconds: 10,
|
||||
secondsRequiredToStartPlaying: 1,
|
||||
gracePeriodAfterSeekInSeconds: 0.5,
|
||||
secondsRequiredToStartPlayingAfterBufferUnderrun: 1,
|
||||
secondsRequiredToStartPlayingAfterBufferUnderrun: 7,
|
||||
enableLogs: false)
|
||||
/// Initializes the configuration for the `AudioPlayer`
|
||||
///
|
||||
|
||||
@@ -13,11 +13,11 @@ extension AudioPlayer {
|
||||
|
||||
static let initial = InternalState([])
|
||||
static let running = InternalState(rawValue: 1)
|
||||
static let playing = InternalState(rawValue: 1 << 1 | InternalState.running.rawValue)
|
||||
static let rebuffering = InternalState(rawValue: 1 << 2 | InternalState.running.rawValue)
|
||||
static let waitingForData = InternalState(rawValue: 1 << 3 | InternalState.running.rawValue)
|
||||
static let waitingForDataAfterSeek = InternalState(rawValue: 1 << 4 | InternalState.running.rawValue)
|
||||
static let paused = InternalState(rawValue: 1 << 5 | InternalState.running.rawValue)
|
||||
static let playing = InternalState(rawValue: (1 << 1) | InternalState.running.rawValue)
|
||||
static let rebuffering = InternalState(rawValue: (1 << 2) | InternalState.running.rawValue)
|
||||
static let waitingForData = InternalState(rawValue: (1 << 3) | InternalState.running.rawValue)
|
||||
static let waitingForDataAfterSeek = InternalState(rawValue: (1 << 4) | InternalState.running.rawValue)
|
||||
static let paused = InternalState(rawValue: (1 << 5) | InternalState.running.rawValue)
|
||||
static let stopped = InternalState(rawValue: 1 << 9)
|
||||
static let pendingNext = InternalState(rawValue: 1 << 10)
|
||||
static let disposed = InternalState(rawValue: 1 << 30)
|
||||
|
||||
@@ -20,9 +20,9 @@ final class AudioRendererContext {
|
||||
|
||||
let packetsSemaphore = DispatchSemaphore(value: 0)
|
||||
|
||||
let framesRequiredToStartPlaying: UInt32
|
||||
let framesRequiredAfterRebuffering: UInt32
|
||||
let framesRequiredForDataAfterSeekPlaying: UInt32
|
||||
let framesRequiredToStartPlaying: Double
|
||||
let framesRequiredAfterRebuffering: Double
|
||||
let framesRequiredForDataAfterSeekPlaying: Double
|
||||
|
||||
let waitingForDataAfterSeekFrameCount = Atomic<Int32>(0)
|
||||
|
||||
@@ -33,9 +33,9 @@ final class AudioRendererContext {
|
||||
|
||||
let canonicalStream = outputAudioFormat.basicStreamDescription
|
||||
|
||||
framesRequiredToStartPlaying = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.secondsRequiredToStartPlaying)
|
||||
framesRequiredAfterRebuffering = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.secondsRequiredToStartPlayingAfterBufferUnderrun)
|
||||
framesRequiredForDataAfterSeekPlaying = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.gracePeriodAfterSeekInSeconds)
|
||||
framesRequiredToStartPlaying = Double(canonicalStream.mSampleRate) * Double(configuration.secondsRequiredToStartPlaying)
|
||||
framesRequiredAfterRebuffering = Double(canonicalStream.mSampleRate) * Double(configuration.secondsRequiredToStartPlayingAfterBufferUnderrun)
|
||||
framesRequiredForDataAfterSeekPlaying = Double(canonicalStream.mSampleRate) * Double(configuration.gracePeriodAfterSeekInSeconds)
|
||||
|
||||
let dataByteSize = Int(canonicalStream.mSampleRate * configuration.bufferSizeInSeconds) * Int(canonicalStream.mBytesPerFrame)
|
||||
inOutAudioBufferList = allocateBufferList(dataByteSize: dataByteSize)
|
||||
|
||||
@@ -228,8 +228,9 @@ final class AudioFileStreamProcessor {
|
||||
processAudioDataPacketCount(entry: entry, fileStream: fileStream)
|
||||
case kAudioFileStreamProperty_ReadyToProducePackets:
|
||||
// check converter for discontinuous stream
|
||||
processReadyToProducePackets(entry: entry, fileStream: fileStream)
|
||||
assignMagicCookieToConverterIfNeeded()
|
||||
processPacketUpperBoundAndMaxPacketSize(entry: entry, fileStream: fileStream)
|
||||
processReadyToProducePackets(entry: entry, fileStream: fileStream)
|
||||
case kAudioFileStreamProperty_FormatList:
|
||||
processFormatList(entry: entry, fileStream: fileStream)
|
||||
default:
|
||||
@@ -242,7 +243,7 @@ final class AudioFileStreamProcessor {
|
||||
private func processDataOffset(entry: AudioEntry, fileStream: AudioFileStreamID) {
|
||||
var offset: UInt64 = 0
|
||||
fileStreamGetProperty(value: &offset, fileStream: fileStream, propertyId: kAudioFileStreamProperty_DataOffset)
|
||||
entry.lock.lock(); defer { playerContext.audioReadingEntry?.lock.unlock() }
|
||||
entry.lock.lock(); defer { entry.lock.unlock() }
|
||||
entry.audioStreamState.processedDataFormat = true
|
||||
entry.audioStreamState.dataOffset = offset
|
||||
}
|
||||
@@ -253,7 +254,9 @@ final class AudioFileStreamProcessor {
|
||||
AudioFileStreamGetProperty(fileStream, kAudioFileStreamProperty_AudioDataPacketCount, &packetCountSize, &packetCount)
|
||||
entry.lock.lock(); defer { entry.lock.unlock() }
|
||||
entry.audioStreamState.dataPacketCount = Double(packetCount)
|
||||
if entry.audioStreamFormat.mFormatID != kAudioFormatLinearPCM {
|
||||
let entryFormatID = entry.audioStreamFormat.mFormatID
|
||||
let isFLAC = entryFormatID == kAudioFormatFLAC
|
||||
if entryFormatID != kAudioFormatLinearPCM && !isFLAC {
|
||||
discontinuous = true
|
||||
}
|
||||
}
|
||||
@@ -370,6 +373,11 @@ final class AudioFileStreamProcessor {
|
||||
guard let entry = playerContext.audioReadingEntry else { return }
|
||||
guard entry.audioStreamState.processedDataFormat else { return }
|
||||
|
||||
guard let converter = audioConverter else {
|
||||
Logger.error("Couldn't find audio converter", category: .audioRendering)
|
||||
return
|
||||
}
|
||||
|
||||
if let playingEntry = playerContext.audioPlayingEntry,
|
||||
playingEntry.seekRequest.requested, playingEntry.calculatedBitrate() > 0
|
||||
{
|
||||
@@ -380,25 +388,24 @@ final class AudioFileStreamProcessor {
|
||||
return
|
||||
}
|
||||
|
||||
guard let converter = audioConverter else {
|
||||
Logger.error("Couldn't find audio converter", category: .audioRendering)
|
||||
return
|
||||
}
|
||||
|
||||
// reset discontinuity
|
||||
discontinuous = false
|
||||
|
||||
var convertInfo = AudioConvertInfo(done: false,
|
||||
numberOfPackets: inNumberPackets,
|
||||
packDescription: inPacketDescriptions)
|
||||
var convertInfo = AudioConvertInfo(
|
||||
done: false,
|
||||
numberOfPackets: inNumberPackets,
|
||||
packDescription: inPacketDescriptions
|
||||
)
|
||||
convertInfo.audioBuffer.mData = UnsafeMutableRawPointer(mutating: inInputData)
|
||||
convertInfo.audioBuffer.mDataByteSize = inNumberBytes
|
||||
if let playingAudioStreamFormat = playerContext.audioPlayingEntry?.audioStreamFormat {
|
||||
convertInfo.audioBuffer.mNumberChannels = playingAudioStreamFormat.mChannelsPerFrame
|
||||
}
|
||||
|
||||
updateProcessedPackets(inPacketDescriptions: inPacketDescriptions,
|
||||
inNumberPackets: inNumberPackets)
|
||||
updateProcessedPackets(
|
||||
inPacketDescriptions: inPacketDescriptions,
|
||||
inNumberPackets: inNumberPackets
|
||||
)
|
||||
|
||||
var status: OSStatus = noErr
|
||||
packetProcess: while status == noErr {
|
||||
@@ -406,7 +413,7 @@ final class AudioFileStreamProcessor {
|
||||
let bufferContext = rendererContext.bufferContext
|
||||
var used = bufferContext.frameUsedCount
|
||||
var start = bufferContext.frameStartIndex
|
||||
var end = bufferContext.end
|
||||
var end = (bufferContext.frameStartIndex + bufferContext.frameUsedCount) % bufferContext.totalFrameCount
|
||||
|
||||
var framesLeftInBuffer = bufferContext.totalFrameCount - used
|
||||
rendererContext.lock.unlock()
|
||||
|
||||
@@ -64,29 +64,30 @@ final class AudioPlayerRenderProcessor: NSObject {
|
||||
let frameSizeInBytes = bufferContext.sizeInBytes
|
||||
let used = bufferContext.frameUsedCount
|
||||
let start = bufferContext.frameStartIndex
|
||||
let end = bufferContext.end
|
||||
let end = (bufferContext.frameStartIndex + bufferContext.frameUsedCount) % bufferContext.totalFrameCount
|
||||
let signal = rendererContext.waiting.value && used < bufferContext.totalFrameCount / 2
|
||||
|
||||
if let playingEntry = playingEntry {
|
||||
playingEntry.lock.lock()
|
||||
let framesState = playingEntry.framesState
|
||||
playingEntry.lock.unlock()
|
||||
|
||||
if state == .waitingForData {
|
||||
var requiredFramesToStart = rendererContext.framesRequiredToStartPlaying
|
||||
if framesState.lastFrameQueued >= 0 {
|
||||
requiredFramesToStart = min(requiredFramesToStart, UInt32(playingEntry.framesState.lastFrameQueued))
|
||||
requiredFramesToStart = min(requiredFramesToStart, Double(playingEntry.framesState.lastFrameQueued))
|
||||
}
|
||||
if let readingEntry = readingEntry, readingEntry === playingEntry,
|
||||
framesState.queued < requiredFramesToStart
|
||||
|
||||
if readingEntry === playingEntry, framesState.queued < Int(requiredFramesToStart)
|
||||
{
|
||||
waitForBuffer = true
|
||||
}
|
||||
} else if state == .rebuffering {
|
||||
var requiredFramesToStart = rendererContext.framesRequiredAfterRebuffering
|
||||
if framesState.lastFrameQueued >= 0 {
|
||||
requiredFramesToStart = min(requiredFramesToStart, UInt32(framesState.lastFrameQueued - framesState.queued))
|
||||
requiredFramesToStart = min(requiredFramesToStart, Double(framesState.lastFrameQueued - framesState.queued))
|
||||
}
|
||||
if used < requiredFramesToStart {
|
||||
if used < Int(requiredFramesToStart) {
|
||||
waitForBuffer = true
|
||||
}
|
||||
} else if state == .waitingForDataAfterSeek {
|
||||
@@ -102,7 +103,7 @@ final class AudioPlayerRenderProcessor: NSObject {
|
||||
rendererContext.lock.unlock()
|
||||
|
||||
var totalFramesCopied: UInt32 = 0
|
||||
if used > 0 && !waitForBuffer && state.contains(.running) && state != .paused {
|
||||
if used > 0 && !waitForBuffer && playingEntry != nil && state.contains(.running) && state != .paused {
|
||||
if end > start {
|
||||
let framesToCopy = min(inNumberFrames, used)
|
||||
bufferList.mBuffers.mNumberChannels = 2
|
||||
@@ -162,6 +163,7 @@ final class AudioPlayerRenderProcessor: NSObject {
|
||||
bufferContext.frameUsedCount -= totalFramesCopied
|
||||
rendererContext.lock.unlock()
|
||||
}
|
||||
|
||||
if playerContext.internalState != .playing {
|
||||
playerContext.setInternalState(to: .playing, when: { state -> Bool in
|
||||
state.contains(.running) && state != .paused
|
||||
@@ -175,7 +177,7 @@ final class AudioPlayerRenderProcessor: NSObject {
|
||||
memset(mData + Int(totalFramesCopied * frameSizeInBytes), 0, Int(delta * frameSizeInBytes))
|
||||
}
|
||||
|
||||
if playingEntry != nil || AudioPlayer.InternalState.waiting.contains(state) {
|
||||
if !(playingEntry == nil || state == .waitingForDataAfterSeek || state == .waitingForData || state == .rebuffering) {
|
||||
if playerContext.internalState != .rebuffering {
|
||||
playerContext.setInternalState(to: .rebuffering, when: { state -> Bool in
|
||||
state.contains(.running) && state != .paused
|
||||
@@ -184,7 +186,7 @@ final class AudioPlayerRenderProcessor: NSObject {
|
||||
} else if state == .waitingForDataAfterSeek {
|
||||
if totalFramesCopied == 0 {
|
||||
rendererContext.waitingForDataAfterSeekFrameCount.write { $0 += Int32(inNumberFrames - totalFramesCopied) }
|
||||
if rendererContext.waitingForDataAfterSeekFrameCount.value > rendererContext.framesRequiredForDataAfterSeekPlaying {
|
||||
if rendererContext.waitingForDataAfterSeekFrameCount.value > Int(rendererContext.framesRequiredForDataAfterSeekPlaying) {
|
||||
if playerContext.internalState != .playing {
|
||||
playerContext.setInternalState(to: .playing) { state -> Bool in
|
||||
state.contains(.running) && state != .playing
|
||||
|
||||
@@ -5,10 +5,8 @@
|
||||
|
||||
import AVFoundation
|
||||
|
||||
private let outputChannels: UInt32 = 2
|
||||
|
||||
enum UnitDescriptions {
|
||||
static var output: AudioComponentDescription = {
|
||||
static let output: AudioComponentDescription = {
|
||||
var desc = AudioComponentDescription()
|
||||
desc.componentType = kAudioUnitType_Output
|
||||
#if os(iOS)
|
||||
|
||||
@@ -33,6 +33,7 @@ let fileTypesFromMimeType: [String: AudioFileTypeID] =
|
||||
"video/3gpp": kAudioFile3GPType,
|
||||
"audio/3gp2": kAudioFile3GP2Type,
|
||||
"video/3gp2": kAudioFile3GP2Type,
|
||||
"audio/flac": kAudioFileFLACType
|
||||
]
|
||||
|
||||
/// Method that converts mime type to AudioFileTypeID
|
||||
|
||||
@@ -8,7 +8,7 @@ Under the hood `AudioStreaming` uses `AVAudioEngine` and `CoreAudio` for playbac
|
||||
#### Supported audio
|
||||
- Online streaming (Shoutcast/ICY streams) with metadata parsing
|
||||
- AIFF, AIFC, WAVE, CAF, NeXT, ADTS, MPEG Audio Layer 3, AAC audio formats
|
||||
- M4A (_Optimized files only_)
|
||||
- M4A
|
||||
|
||||
As of 1.2.0 version, there's support for non-optimized M4A, please report any issues
|
||||
|
||||
|
||||
Reference in New Issue
Block a user