Compare commits

...

18 Commits

Author SHA1 Message Date
Dimitris C. c62aedd2ba Refactor GitHub Actions workflow for Swift package testing 2025-11-08 20:19:37 +02:00
dimitris-c 571feca637 Refactors project use pure Package.swift and swift test 2025-11-08 20:17:30 +02:00
dimitris-c 2b511561ef fix 2025-11-07 16:27:02 +02:00
dimitris-c be1b79eb84 improves isSeekable property 2025-11-07 16:06:16 +02:00
dimitris-c a2832a5c9f removes debug message 2025-11-07 15:58:21 +02:00
dimitris-c 8fb497adfc Merge branch 'dimitris/feature/ogg' of https://github.com/dimitris-c/AudioStreaming into dimitris/feature/ogg 2025-11-07 15:56:33 +02:00
dimitris-c 6b2a988891 improvements 2025-11-07 15:56:30 +02:00
Dimitris C. 0555a9a46f Update iOS requirement from 13.0 to 15.0 2025-11-07 15:22:45 +02:00
Dimitris C. 594f379afe Update README to remove M4A limitation note
Removed limitation note for non-optimized M4A files.
2025-11-07 15:20:32 +02:00
dimitris-c e5e70ad92f fix rateNode issue 2025-11-07 14:07:49 +02:00
dimitris-c 02a3d6e191 fixes 2025-11-07 14:00:07 +02:00
dimitris-c fcba80cef7 Adds support for Ogg Vorbis 2025-11-06 18:15:40 +02:00
dimitris-c 6b1dc644fe park - working 2025-11-01 22:56:26 +02:00
dimitris-c 448ad711a1 park 2025-10-27 15:14:28 +02:00
Dimitris C. 72028c261f Update swift.yml 2025-10-15 14:06:59 +03:00
dimitris-c 4b8bae96c2 bump version number 2025-10-13 18:33:32 +03:00
Maximilian Bauer bccfc20403 fix seek crash: Double value gets infinit and can't be converted to Int64 (#115)
* fix seek crash: Double value get infinit and can't be converted to Int64

* add CoreAudio import to be able to build for macOS Catalyst
2025-10-13 18:32:34 +03:00
Dimitris C. 69dc0d631c fix(MP4): MP4 restructure improvements and some other fixes (#120)
* Adds mp4 restructure improvements

* fixes data race

* fix incorrect parsing of formatList

* adds more handling on propertyListenerProc
2025-10-13 18:32:09 +03:00
40 changed files with 1604 additions and 1452 deletions
+6 -7
View File
@@ -10,15 +10,14 @@ on:
- '*'
jobs:
iOS:
name: Test iOS
test:
name: Test Swift Package
runs-on: macOS-latest
env:
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
strategy:
matrix:
destination: ["OS=latest,name=iPhone 15 Pro"]
steps:
- uses: actions/checkout@v2
- name: iOS - ${{ matrix.destination }}
run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "AudioStreaming.xcodeproj" -scheme "AudioStreaming" -destination "${{ matrix.destination }}" clean test | xcpretty
- name: Build
run: swift build
- name: Run tests
run: swift test --parallel
+3 -3
View File
@@ -41,11 +41,11 @@ playground.xcworkspace
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
*.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm
.swiftpm
.build/
@@ -58,7 +58,7 @@ playground.xcworkspace
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace
*.xcworkspace
# Carthage
#
+321
View File
@@ -0,0 +1,321 @@
#include "include/VorbisFileBridge.h"
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <vorbis/vorbisfile.h>
struct VFRemoteStream {
uint8_t *buf;
size_t cap, head, tail, size;
int eof;
long long pos; // Current read position in the stream
long long total_pushed; // Total bytes pushed into the buffer
pthread_mutex_t m;
pthread_cond_t cv;
};
// Simple ring buffer write
static size_t rb_write(struct VFRemoteStream *s, const uint8_t *src, size_t len) {
size_t written = 0;
while (written < len) {
size_t free_space = s->cap - s->size;
if (free_space == 0) break;
size_t chunk = s->cap - s->tail;
if (chunk > len - written) chunk = len - written;
if (chunk > free_space) chunk = free_space;
memcpy(s->buf + s->tail, src + written, chunk);
s->tail = (s->tail + chunk) % s->cap;
s->size += chunk;
written += chunk;
}
return written;
}
// Simple ring buffer read
static size_t rb_read(struct VFRemoteStream *s, uint8_t *dst, size_t len) {
size_t read = 0;
while (read < len && s->size > 0) {
size_t chunk = s->cap - s->head;
if (chunk > s->size) chunk = s->size;
if (chunk > len - read) chunk = len - read;
memcpy(dst + read, s->buf + s->head, chunk);
s->head = (s->head + chunk) % s->cap;
s->size -= chunk;
read += chunk;
}
return read;
}
// Create a stream buffer
VFStreamRef VFStreamCreate(size_t capacity_bytes) {
struct VFRemoteStream *s = (struct VFRemoteStream *)calloc(1, sizeof(struct VFRemoteStream));
if (!s) return NULL;
s->buf = (uint8_t *)malloc(capacity_bytes);
if (!s->buf) { free(s); return NULL; }
s->cap = capacity_bytes;
pthread_mutex_init(&s->m, NULL);
pthread_cond_init(&s->cv, NULL);
return s;
}
// Destroy a stream buffer
void VFStreamDestroy(VFStreamRef sr) {
struct VFRemoteStream *s = (struct VFRemoteStream *)sr;
if (!s) return;
pthread_mutex_destroy(&s->m);
pthread_cond_destroy(&s->cv);
free(s->buf);
free(s);
}
// Get available bytes in the buffer
size_t VFStreamAvailableBytes(VFStreamRef sr) {
struct VFRemoteStream *s = (struct VFRemoteStream *)sr;
if (!s) return 0;
pthread_mutex_lock(&s->m);
size_t sz = s->size;
pthread_mutex_unlock(&s->m);
return sz;
}
// Push data into the stream
void VFStreamPush(VFStreamRef sr, const uint8_t *data, size_t len) {
struct VFRemoteStream *s = (struct VFRemoteStream *)sr;
if (!s || !data || len == 0) return;
pthread_mutex_lock(&s->m);
size_t written_total = 0;
while (written_total < len) {
size_t w = rb_write(s, data + written_total, len - written_total);
written_total += w;
if (written_total < len) {
// Buffer full, wait for consumer to read
pthread_cond_wait(&s->cv, &s->m);
}
}
s->total_pushed += (long long)len;
pthread_cond_broadcast(&s->cv);
pthread_mutex_unlock(&s->m);
}
// Mark the stream as EOF
void VFStreamMarkEOF(VFStreamRef sr) {
struct VFRemoteStream *s = (struct VFRemoteStream *)sr;
if (!s) return;
pthread_mutex_lock(&s->m);
s->eof = 1;
pthread_cond_broadcast(&s->cv);
pthread_mutex_unlock(&s->m);
}
// libvorbisfile callbacks
// Read callback for libvorbisfile
static size_t read_cb(void *ptr, size_t size, size_t nmemb, void *datasrc) {
struct VFRemoteStream *s = (struct VFRemoteStream *)datasrc;
size_t want_bytes = size * nmemb;
size_t got = 0;
pthread_mutex_lock(&s->m);
// Read what's available NOW - don't block waiting for more data
while (got < want_bytes && s->size > 0) {
size_t chunk = rb_read(s, (uint8_t *)ptr + got, want_bytes - got);
s->pos += (long long)chunk;
got += chunk;
if (chunk == 0) break;
// Allow producer to push more
pthread_cond_broadcast(&s->cv);
}
// If nothing available and EOF, we're done
if (got == 0 && s->eof) {
// Return 0 to signal EOF to libvorbisfile
}
pthread_mutex_unlock(&s->m);
return size ? (got / size) : 0;
}
// Seek callback - seek within the ring buffer
static int seek_cb(void *datasrc, ogg_int64_t offset, int whence) {
struct VFRemoteStream *s = (struct VFRemoteStream *)datasrc;
if (!s) return -1;
pthread_mutex_lock(&s->m);
ogg_int64_t new_pos = 0;
switch (whence) {
case SEEK_SET:
new_pos = offset;
break;
case SEEK_CUR:
new_pos = s->pos + offset;
break;
case SEEK_END:
new_pos = s->total_pushed + offset;
break;
default:
pthread_mutex_unlock(&s->m);
return -1;
}
// Check if the new position is valid (within available data)
if (new_pos < 0 || new_pos > s->total_pushed) {
pthread_mutex_unlock(&s->m);
return -1; // Can't seek outside available data
}
// Calculate how much data we've already consumed from the buffer
long long already_consumed = s->pos - ((long long)s->total_pushed - (long long)s->size);
// Calculate the new head position
long long pos_delta = new_pos - s->pos;
// For forward seeks, we need to have enough data in the buffer
if (pos_delta > 0 && pos_delta > (long long)s->size) {
pthread_mutex_unlock(&s->m);
return -1; // Not enough data in buffer to seek forward
}
// For backward seeks, check if that data is still in the buffer
if (pos_delta < 0 && (-pos_delta) > already_consumed) {
pthread_mutex_unlock(&s->m);
return -1; // Data has been discarded from buffer
}
// Adjust head pointer
if (pos_delta >= 0) {
// Forward seek: advance head
s->head = (s->head + pos_delta) % s->cap;
s->size -= (size_t)pos_delta;
} else {
// Backward seek: rewind head
size_t rewind = (size_t)(-pos_delta);
if (s->head >= rewind) {
s->head -= rewind;
} else {
s->head = s->cap - (rewind - s->head);
}
s->size += rewind;
}
s->pos = new_pos;
pthread_mutex_unlock(&s->m);
return 0;
}
// Close callback - no-op
static int close_cb(void *datasrc) {
(void)datasrc;
return 0;
}
// Tell callback - return current position
static long tell_cb(void *datasrc) {
struct VFRemoteStream *s = (struct VFRemoteStream *)datasrc;
return (long)s->pos;
}
// Open a vorbis file using callbacks
int VFOpen(VFStreamRef sr, VFFileRef *out_vf) {
struct VFRemoteStream *s = (struct VFRemoteStream *)sr;
if (!s || !out_vf) return -1;
OggVorbis_File *vf = (OggVorbis_File *)malloc(sizeof(OggVorbis_File));
if (!vf) return -1;
ov_callbacks cbs;
cbs.read_func = read_cb;
cbs.seek_func = NULL; // Non-seekable streaming (seeking handled at Swift level)
cbs.close_func = close_cb;
cbs.tell_func = tell_cb;
int rc = ov_open_callbacks((void *)s, vf, NULL, 0, cbs);
if (rc < 0) { free(vf); return rc; }
*out_vf = (VFFileRef)vf;
return 0;
}
// Clear a vorbis file
void VFClear(VFFileRef fr) {
OggVorbis_File *vf = (OggVorbis_File *)fr;
if (!vf) return;
ov_clear(vf);
free(vf);
}
// Get stream info
int VFGetInfo(VFFileRef fr, VFStreamInfo *out_info) {
OggVorbis_File *vf = (OggVorbis_File *)fr;
if (!vf || !out_info) return -1;
vorbis_info const *info = ov_info(vf, -1);
if (!info) return -1;
out_info->sample_rate = info->rate;
out_info->channels = info->channels;
out_info->total_pcm_samples = ov_pcm_total(vf, -1);
out_info->duration_seconds = ov_time_total(vf, -1);
out_info->bitrate_nominal = info->bitrate_nominal;
return 0;
}
// Read deinterleaved float PCM frames
long VFReadFloat(VFFileRef fr, float ***out_pcm, int max_frames) {
OggVorbis_File *vf = (OggVorbis_File *)fr;
if (!vf || !out_pcm || max_frames <= 0) return -1;
int bitstream = 0;
long frames = ov_read_float(vf, out_pcm, max_frames, &bitstream);
// Returns: frames read (0 = EOF, <0 = error)
return frames;
}
// Read interleaved float PCM frames (legacy, less efficient)
long VFReadInterleavedFloat(VFFileRef fr, float *dst, int max_frames) {
OggVorbis_File *vf = (OggVorbis_File *)fr;
if (!vf || !dst || max_frames <= 0) return -1;
int bitstream = 0;
float **pcm = NULL;
long frames = ov_read_float(vf, &pcm, max_frames, &bitstream);
if (frames <= 0) return frames; // 0 EOF, <0 error/hole
vorbis_info const *info = ov_info(vf, -1);
int ch = info->channels;
// Interleave the PCM data
for (long f = 0; f < frames; ++f) {
for (int c = 0; c < ch; ++c) {
dst[f * ch + c] = pcm[c][f];
}
}
return frames;
}
// Seek to a specific time in seconds
int VFSeekTime(VFFileRef fr, double time_seconds) {
OggVorbis_File *vf = (OggVorbis_File *)fr;
if (!vf) return -1;
// Use ov_time_seek for time-based seeking
// Returns 0 on success, nonzero on failure
return ov_time_seek(vf, time_seconds);
}
// Check if the stream is seekable
int VFIsSeekable(VFFileRef fr) {
OggVorbis_File *vf = (OggVorbis_File *)fr;
if (!vf) return 0;
// Returns nonzero if the stream is seekable
return ov_seekable(vf);
}
+13
View File
@@ -0,0 +1,13 @@
//
// AudioCodecs.h
// AudioStreaming
//
// Created on 25/10/2025.
//
#ifndef AudioCodecs_h
#define AudioCodecs_h
#import "VorbisFileBridge.h"
#endif /* AudioCodecs_h */
+58
View File
@@ -0,0 +1,58 @@
#ifndef VORBIS_FILE_BRIDGE_H
#define VORBIS_FILE_BRIDGE_H
#include <stddef.h>
#include <stdint.h>
// Opaque refs for Swift-friendly API
typedef void * VFStreamRef;
typedef void * VFFileRef;
#ifdef __cplusplus
extern "C" {
#endif
// Stream info structure
typedef struct {
int sample_rate;
int channels;
long long total_pcm_samples; // -1 if unknown
double duration_seconds; // < 0 if unknown
long bitrate_nominal; // nominal bitrate in bits/sec, or 0 if unknown
} VFStreamInfo;
// Stream lifecycle
VFStreamRef VFStreamCreate(size_t capacity_bytes);
void VFStreamDestroy(VFStreamRef s);
size_t VFStreamAvailableBytes(VFStreamRef s);
// Feeding data
void VFStreamPush(VFStreamRef s, const uint8_t *data, size_t len);
void VFStreamMarkEOF(VFStreamRef s);
// Decoder lifecycle
// Returns 0 on success, negative on error (same codes as ov_open_callbacks)
int VFOpen(VFStreamRef s, VFFileRef *out_vf);
void VFClear(VFFileRef vf);
// Query info; returns 0 on success
int VFGetInfo(VFFileRef vf, VFStreamInfo *out_info);
// Read interleaved float32 PCM frames into dst; returns number of frames read, 0 on EOF, <0 on error
long VFReadInterleavedFloat(VFFileRef vf, float *dst, int max_frames);
// Read deinterleaved float32 PCM frames (channel-by-channel); returns number of frames read, 0 on EOF, <0 on error
// out_pcm will point to an array of channel pointers (float**)
long VFReadFloat(VFFileRef vf, float ***out_pcm, int max_frames);
// Seek to a specific time in seconds; returns 0 on success, <0 on error
int VFSeekTime(VFFileRef vf, double time_seconds);
// Check if the stream is seekable; returns 1 if seekable, 0 if not
int VFIsSeekable(VFFileRef vf);
#ifdef __cplusplus
}
#endif
#endif // VORBIS_FILE_BRIDGE_H
+4
View File
@@ -0,0 +1,4 @@
module AudioCodecs {
umbrella header "AudioCodecs.h"
export *
}
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objectVersion = 60;
objects = {
/* Begin PBXBuildFile section */
@@ -15,16 +15,16 @@
9806E8262BC5D2A900757370 /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9806E8252BC5D2A900757370 /* Sidebar.swift */; };
9806E82A2BC68F8700757370 /* AudioPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9806E8292BC68F8700757370 /* AudioPlayerView.swift */; };
9806E8312BC6927D00757370 /* AudioPlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9806E8302BC6927D00757370 /* AudioPlayerModel.swift */; };
9816A8A52BC7D8A200AD1299 /* AudioStreaming.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9816A8A42BC7D8A200AD1299 /* AudioStreaming.framework */; };
9816A8A62BC7D8A200AD1299 /* AudioStreaming.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9816A8A42BC7D8A200AD1299 /* AudioStreaming.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9816A8AA2BC7F4F000AD1299 /* AudioTrack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9816A8A92BC7F4F000AD1299 /* AudioTrack.swift */; };
9816A8AC2BC820DF00AD1299 /* AudioContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9816A8AB2BC820DF00AD1299 /* AudioContent.swift */; };
9816A8B12BC8330C00AD1299 /* bensound-jazzyfrenchy.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 9816A8AD2BC832DB00AD1299 /* bensound-jazzyfrenchy.mp3 */; };
9816A8B22BC8330C00AD1299 /* bensound-jazzyfrenchy.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 9816A8AE2BC832DB00AD1299 /* bensound-jazzyfrenchy.m4a */; };
9816A8B32BC8330C00AD1299 /* hipjazz.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9816A8AF2BC832DC00AD1299 /* hipjazz.wav */; };
9816A8BB2BC87BC200AD1299 /* AudioPlayerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */; };
981DA0762EAD61A90062223D /* AudioStreaming in Frameworks */ = {isa = PBXBuildFile; productRef = 981DA0752EAD61A90062223D /* AudioStreaming */; };
984DE9552BDAE59C004B427A /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984DE9542BDAE59C004B427A /* Notifier.swift */; };
984DE9572BDAFC7E004B427A /* AudioPlayerControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984DE9562BDAFC7E004B427A /* AudioPlayerControlsView.swift */; };
9881693E2EBCDB0100CE7EFF /* hipjazz.ogg in Resources */ = {isa = PBXBuildFile; fileRef = 9881693D2EBCDB0100CE7EFF /* hipjazz.ogg */; };
989E08E72BF7A4E300599F17 /* PrefersTabNavigationEnvironmentKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989E08E62BF7A4E300599F17 /* PrefersTabNavigationEnvironmentKey.swift */; };
98BFB41A2BC97AF800E812C0 /* DisplayLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB4192BC97AF800E812C0 /* DisplayLink.swift */; };
98BFB41D2BCD7BB800E812C0 /* EqualizerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB41C2BCD7BB800E812C0 /* EqualizerView.swift */; };
@@ -33,20 +33,6 @@
98E6119C2BC72C0E0036BC47 /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E6119B2BC72C0E0036BC47 /* DetailView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
9816A8A72BC7D8A200AD1299 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
9816A8A62BC7D8A200AD1299 /* AudioStreaming.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
42BE42F42C9322AA00C0E448 /* CustomStreamSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomStreamSource.swift; sourceTree = "<group>"; };
9806E8142BC5D12500757370 /* AudioPlayer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AudioPlayer.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -66,6 +52,7 @@
9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerService.swift; sourceTree = "<group>"; };
984DE9542BDAE59C004B427A /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
984DE9562BDAFC7E004B427A /* AudioPlayerControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerControlsView.swift; sourceTree = "<group>"; };
9881693D2EBCDB0100CE7EFF /* hipjazz.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = hipjazz.ogg; sourceTree = "<group>"; };
989E08E62BF7A4E300599F17 /* PrefersTabNavigationEnvironmentKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefersTabNavigationEnvironmentKey.swift; sourceTree = "<group>"; };
98BFB4192BC97AF800E812C0 /* DisplayLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayLink.swift; sourceTree = "<group>"; };
98BFB41B2BCAAD8A00E812C0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
@@ -80,7 +67,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9816A8A52BC7D8A200AD1299 /* AudioStreaming.framework in Frameworks */,
981DA0762EAD61A90062223D /* AudioStreaming in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -168,6 +155,7 @@
9816A8B02BC832E100AD1299 /* Resources */ = {
isa = PBXGroup;
children = (
9881693D2EBCDB0100CE7EFF /* hipjazz.ogg */,
9816A8AE2BC832DB00AD1299 /* bensound-jazzyfrenchy.m4a */,
9816A8AF2BC832DC00AD1299 /* hipjazz.wav */,
9816A8AD2BC832DB00AD1299 /* bensound-jazzyfrenchy.mp3 */,
@@ -216,7 +204,6 @@
9806E8102BC5D12500757370 /* Sources */,
9806E8112BC5D12500757370 /* Frameworks */,
9806E8122BC5D12500757370 /* Resources */,
9816A8A72BC7D8A200AD1299 /* Embed Frameworks */,
);
buildRules = (
);
@@ -251,6 +238,9 @@
Base,
);
mainGroup = 9806E80B2BC5D12500757370;
packageReferences = (
981DA0742EAD61A90062223D /* XCLocalSwiftPackageReference "../../AudioStreaming" */,
);
productRefGroup = 9806E8152BC5D12500757370 /* Products */;
projectDirPath = "";
projectRoot = "";
@@ -269,6 +259,7 @@
9806E81C2BC5D12700757370 /* Assets.xcassets in Resources */,
9816A8B12BC8330C00AD1299 /* bensound-jazzyfrenchy.mp3 in Resources */,
9816A8B22BC8330C00AD1299 /* bensound-jazzyfrenchy.m4a in Resources */,
9881693E2EBCDB0100CE7EFF /* hipjazz.ogg in Resources */,
9816A8B32BC8330C00AD1299 /* hipjazz.wav in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -444,6 +435,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.5;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioPlayer;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -477,6 +469,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.5;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioPlayer;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -510,6 +503,20 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
981DA0742EAD61A90062223D /* XCLocalSwiftPackageReference "../../AudioStreaming" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = ../../AudioStreaming;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
981DA0752EAD61A90062223D /* AudioStreaming */ = {
isa = XCSwiftPackageProductDependency;
productName = AudioStreaming;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 9806E80C2BC5D12500757370 /* Project object */;
}
@@ -0,0 +1,24 @@
{
"originHash" : "2be026a121d718059bac101ee8cabdd866a56e3b58b2908f27213c8a08755a25",
"pins" : [
{
"identity" : "ogg-binary-xcframework",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sbooth/ogg-binary-xcframework",
"state" : {
"revision" : "c0e822e18738ad913864e98d9614927ac1e9337c",
"version" : "0.1.2"
}
},
{
"identity" : "vorbis-binary-xcframework",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sbooth/vorbis-binary-xcframework",
"state" : {
"revision" : "842020eabcebe410e698c68545d6597b2d232e51",
"version" : "0.1.2"
}
}
],
"version" : 3
}
@@ -20,6 +20,8 @@ enum AudioContent {
case local
case localWave
case loopBeatFlac
case oggVorbis
case oggVorbisLocal
case custom(String)
var title: String {
@@ -45,13 +47,17 @@ enum AudioContent {
case .local:
return "Jazzy Frenchy"
case .localWave:
return "Local file"
return "Hip Jazz"
case .optimized:
return "Jazzy Frenchy"
case .nonOptimized:
return "Jazzy Frenchy"
case .loopBeatFlac:
return "Beat loop"
case .oggVorbis:
return "Jazzy Fetchy"
case .oggVorbisLocal:
return "Hip Jazz"
case .custom(let url):
return url
}
@@ -76,7 +82,7 @@ enum AudioContent {
case .piano:
return "Remote mp3"
case .remoteWave:
return "wave"
return "Local wav"
case .local:
return "Music by: bensound.com"
case .localWave:
@@ -87,6 +93,10 @@ enum AudioContent {
return "Music by: bensound.com - m4a non-optimized"
case .loopBeatFlac:
return "Remote flac"
case .oggVorbis:
return "Remote Ogg Vorbis"
case .oggVorbisLocal:
return "Local Ogg Vorbis"
case .custom:
return ""
}
@@ -124,6 +134,11 @@ enum AudioContent {
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 .oggVorbis:
return URL(string: "https://github.com/dimitris-c/sample-audio/raw/refs/heads/main/bensound-jazzyfrenchy.ogg")!
case .oggVorbisLocal:
let path = Bundle.main.path(forResource: "hipjazz", ofType: "ogg")!
return URL(fileURLWithPath: path)
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, .loopBeatFlac]
private let audioTracks: [AudioContent] = [.khruangbin, .piano, .optimized, .nonOptimized, .remoteWave, .local, .localWave, .loopBeatFlac, .oggVorbis, .oggVorbisLocal]
private let customStreams: [AudioContent] = [.custom("custom://sinwave")]
func audioTracksProvider() -> [AudioPlaylist] {
@@ -180,7 +180,7 @@ extension AudioPlayerService: AudioPlayerDelegate {
func audioPlayerDidFinishBuffering(player _: AudioPlayer, with _: AudioEntryId) {}
func audioPlayerStateChanged(player _: AudioPlayer, with newState: AudioPlayerState, previous _: AudioPlayerState) {
print("audioPlayerDidStartPlaying newState: \(newState)")
print("audioPlayerStateChanged newState: \(newState)")
Task { await statusChangedNotifier.send(newState) }
delegate?.statusChanged(status: newState)
}
Binary file not shown.
-971
View File
@@ -1,971 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 55;
objects = {
/* Begin PBXBuildFile section */
98ABF69E2BAB07A20059C441 /* Mp4Restructure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ABF69D2BAB07A20059C441 /* Mp4Restructure.swift */; };
98C82AE62B8CA8BC00AED485 /* RemoteMp4Restructure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C82AE52B8CA8BC00AED485 /* RemoteMp4Restructure.swift */; };
98CC396E28BD651E006C9FF9 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98CC396D28BD651E006C9FF9 /* Atomic.swift */; };
98DC00CC2B961F5E0068900A /* ByteBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98DC00CB2B961F5E0068900A /* ByteBuffer.swift */; };
98DC00CE2B9726380068900A /* ByteBufferTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98DC00CD2B9726380068900A /* ByteBufferTests.swift */; };
B500732024D00BAC00BB4475 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500731F24D00BAC00BB4475 /* Logger.swift */; };
B514657F248E3884005C03F7 /* DispatchTimerSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514657E248E3884005C03F7 /* DispatchTimerSource.swift */; };
B51B9F9A24DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51B9F9924DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift */; };
B51FE0C02488F67C00F2A4D2 /* Queue.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51FE0BF2488F67C00F2A4D2 /* Queue.swift */; };
B51FE0C22488F96A00F2A4D2 /* QueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51FE0C12488F96A00F2A4D2 /* QueueTests.swift */; };
B51FE0C624890CCB00F2A4D2 /* PlayerQueueEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51FE0C3248905B400F2A4D2 /* PlayerQueueEntries.swift */; };
B51FE0C824892D1600F2A4D2 /* PlayerQueueEntriesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51FE0C724892D1600F2A4D2 /* PlayerQueueEntriesTest.swift */; };
B5276B6F247D21A000D2F56A /* NetworkingClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5276B6E247D21A000D2F56A /* NetworkingClient.swift */; };
B5276B74247D4D9F00D2F56A /* NetworkSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5276B73247D4D9F00D2F56A /* NetworkSessionDelegate.swift */; };
B54C3E56255F286D00B356F2 /* Retrier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54C3E55255F286D00B356F2 /* Retrier.swift */; };
B54D876D2490E4A000C361A0 /* UnitDescriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54D876C2490E4A000C361A0 /* UnitDescriptions.swift */; };
B54D876F2490E4DD00C361A0 /* AudioRendererContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54D876E2490E4DD00C361A0 /* AudioRendererContext.swift */; };
B55A736C247FCB420050C53D /* HTTPHeaderParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55A736B247FCB420050C53D /* HTTPHeaderParser.swift */; };
B55CE96E248058B60001C498 /* MetadataParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55CE96D248058B60001C498 /* MetadataParser.swift */; };
B55CE97124810DE20001C498 /* MetadataStreamProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55CE97024810DE20001C498 /* MetadataStreamProcessor.swift */; };
B55CE97824813BCA0001C498 /* UnsafeMutablePointer+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55CE97724813BCA0001C498 /* UnsafeMutablePointer+Helpers.swift */; };
B55CEAB42485107C0001C498 /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55CEAB32485107C0001C498 /* Parser.swift */; };
B55CEAB82485172D0001C498 /* HTTPHeaderParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55CEAB72485172D0001C498 /* HTTPHeaderParserTests.swift */; };
B55CEABA248530C00001C498 /* MetadataParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55CEAB9248530C00001C498 /* MetadataParser.swift */; };
B55CEABC24853CD20001C498 /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55CEABB24853CD20001C498 /* AudioPlayer.swift */; };
B55F77CF24D82ADE0057F431 /* AudioPlayerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55F77CE24D82ADE0057F431 /* AudioPlayerDelegate.swift */; };
B55F77D124D82CD50057F431 /* AVAudioUnit+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55F77D024D82CD50057F431 /* AVAudioUnit+Convenience.swift */; };
B55F77D624DACE140057F431 /* BufferContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55F77D524DACE140057F431 /* BufferContext.swift */; };
B5667A902499018D00D93F85 /* AudioFileStreamProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5667A8F2499018D00D93F85 /* AudioFileStreamProcessor.swift */; };
B5667A922499063D00D93F85 /* AudioPlayerContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5667A912499063D00D93F85 /* AudioPlayerContext.swift */; };
B5667B3E249BC43100D93F85 /* AudioPlayerRenderProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5667B3D249BC43000D93F85 /* AudioPlayerRenderProcessor.swift */; };
B57829CF2548B32B00C78D36 /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57829CE2548B32B00C78D36 /* Lock.swift */; };
B58386382544A2C10087A712 /* EntryFrames.swift in Sources */ = {isa = PBXBuildFile; fileRef = B58386372544A2C10087A712 /* EntryFrames.swift */; };
B5838640254584A50087A712 /* ProcessedPackets.swift in Sources */ = {isa = PBXBuildFile; fileRef = B583863F254584A50087A712 /* ProcessedPackets.swift */; };
B5838644254584BE0087A712 /* AudioStreamState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5838643254584BE0087A712 /* AudioStreamState.swift */; };
B5838648254584D90087A712 /* SeekRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5838647254584D90087A712 /* SeekRequest.swift */; };
B592E1252545FF9A008866FB /* BiMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5276B71247D4D5B00D2F56A /* BiMap.swift */; };
B592E12925460146008866FB /* BiMapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B592E12825460146008866FB /* BiMapTests.swift */; };
B592E134254608B4008866FB /* DispatchTimerSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B592E133254608B4008866FB /* DispatchTimerSourceTests.swift */; };
B59CB46C25420B4D00F8CAD0 /* MetadataStreamProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59CB46B25420B4D00F8CAD0 /* MetadataStreamProcessorTests.swift */; };
B59CB4BB25421F3500F8CAD0 /* raw-stream-audio-normal-metadata in Resources */ = {isa = PBXBuildFile; fileRef = B59CB4BA25421F3500F8CAD0 /* raw-stream-audio-normal-metadata */; };
B59CB4C225421F7A00F8CAD0 /* raw-stream-audio-empty-metadata in Resources */ = {isa = PBXBuildFile; fileRef = B59CB4B225421D8200F8CAD0 /* raw-stream-audio-empty-metadata */; };
B59CB4C625421FD400F8CAD0 /* raw-stream-audio-no-metadata in Resources */ = {isa = PBXBuildFile; fileRef = B59CB4C525421FD400F8CAD0 /* raw-stream-audio-no-metadata */; };
B59CB4CE2542204D00F8CAD0 /* raw-stream-audio-normal-metadata-alt in Resources */ = {isa = PBXBuildFile; fileRef = B59CB4CD2542204D00F8CAD0 /* raw-stream-audio-normal-metadata-alt */; };
B59D0B6F255C904900D6CCE5 /* FileAudioSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59D0B6E255C904900D6CCE5 /* FileAudioSource.swift */; };
B59DF10424916FD50043C498 /* DispatchQueue+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59DF10324916FD50043C498 /* DispatchQueue+Helpers.swift */; };
B59DF1A32493E90C0043C498 /* AudioFileStream+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59DF1A22493E90C0043C498 /* AudioFileStream+Helpers.swift */; };
B5AEDBB824744153007D8101 /* AudioStreaming.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5AEDBAE24744153007D8101 /* AudioStreaming.framework */; platformFilters = (ios, tvos, ); };
B5AEDBBF24744153007D8101 /* AudioStreaming.h in Headers */ = {isa = PBXBuildFile; fileRef = B5AEDBB124744153007D8101 /* AudioStreaming.h */; settings = {ATTRIBUTES = (Public, ); }; };
B5B36E432655A32200DC96F5 /* FrameFilterProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B36E422655A32200DC96F5 /* FrameFilterProcessor.swift */; };
B5B3B7CC248647ED00656828 /* AudioPlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B3B7CB248647ED00656828 /* AudioPlayerState.swift */; };
B5D4A40925D9321400E1450C /* IcycastHeaderParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D4A40825D9321400E1450C /* IcycastHeaderParser.swift */; };
B5D4A41025D948EF00E1450C /* IcycastHeadersProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D4A40B25D9445600E1450C /* IcycastHeadersProcessor.swift */; };
B5D82E65255DD562009EDAA4 /* NetStatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D82E64255DD562009EDAA4 /* NetStatusService.swift */; };
B5DB66E2255C2EAB00B8DF53 /* AudioEntryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DB66E1255C2EAB00B8DF53 /* AudioEntryProvider.swift */; };
B5E1DE2524B70B4200955BFB /* AudioPlayerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E1DE2424B70B4200955BFB /* AudioPlayerConfiguration.swift */; };
B5EF954E247DA5AC003E8FF8 /* NetworkingClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF954D247DA5AC003E8FF8 /* NetworkingClientTests.swift */; };
B5EF9555247E9393003E8FF8 /* AudioEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF9554247E9393003E8FF8 /* AudioEntry.swift */; };
B5EF9557247E9439003E8FF8 /* AudioStreamSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF9556247E9439003E8FF8 /* AudioStreamSource.swift */; };
B5EF955B247EBCB3003E8FF8 /* AudioFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF955A247EBCB3003E8FF8 /* AudioFileType.swift */; };
B5EF955D247ECBB1003E8FF8 /* RemoteAudioSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF955C247ECBB1003E8FF8 /* RemoteAudioSource.swift */; };
B5F883BA2477CEFC00D277C1 /* AtomicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F883B82477CBF600D277C1 /* AtomicTests.swift */; };
B5F883C32477DC4400D277C1 /* NetworkDataStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F883C22477DC4400D277C1 /* NetworkDataStream.swift */; };
B5FB6C0525516507002C0A37 /* AudioConverter+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5FB6C0425516507002C0A37 /* AudioConverter+Helpers.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
B5AEDBB924744153007D8101 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = B5AEDBA524744153007D8101 /* Project object */;
proxyType = 1;
remoteGlobalIDString = B5AEDBAD24744153007D8101;
remoteInfo = AudioStreaming;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
B57A4F7D24AB4E6C00D7EA51 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
98ABF69D2BAB07A20059C441 /* Mp4Restructure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mp4Restructure.swift; sourceTree = "<group>"; };
98C82AE52B8CA8BC00AED485 /* RemoteMp4Restructure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMp4Restructure.swift; sourceTree = "<group>"; };
98CC396D28BD651E006C9FF9 /* Atomic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = "<group>"; };
98DC00CB2B961F5E0068900A /* ByteBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ByteBuffer.swift; sourceTree = "<group>"; };
98DC00CD2B9726380068900A /* ByteBufferTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ByteBufferTests.swift; sourceTree = "<group>"; };
B500731F24D00BAC00BB4475 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
B514657E248E3884005C03F7 /* DispatchTimerSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchTimerSource.swift; sourceTree = "<group>"; };
B51B9F9924DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAudioFormat+Convenience.swift"; sourceTree = "<group>"; };
B51FE0BF2488F67C00F2A4D2 /* Queue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Queue.swift; sourceTree = "<group>"; };
B51FE0C12488F96A00F2A4D2 /* QueueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueTests.swift; sourceTree = "<group>"; };
B51FE0C3248905B400F2A4D2 /* PlayerQueueEntries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueEntries.swift; sourceTree = "<group>"; };
B51FE0C724892D1600F2A4D2 /* PlayerQueueEntriesTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueEntriesTest.swift; sourceTree = "<group>"; };
B5276B6E247D21A000D2F56A /* NetworkingClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkingClient.swift; sourceTree = "<group>"; };
B5276B71247D4D5B00D2F56A /* BiMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiMap.swift; sourceTree = "<group>"; };
B5276B73247D4D9F00D2F56A /* NetworkSessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSessionDelegate.swift; sourceTree = "<group>"; };
B54C3E55255F286D00B356F2 /* Retrier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Retrier.swift; sourceTree = "<group>"; };
B54D876C2490E4A000C361A0 /* UnitDescriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitDescriptions.swift; sourceTree = "<group>"; };
B54D876E2490E4DD00C361A0 /* AudioRendererContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRendererContext.swift; sourceTree = "<group>"; };
B55A736B247FCB420050C53D /* HTTPHeaderParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeaderParser.swift; sourceTree = "<group>"; };
B55CE96D248058B60001C498 /* MetadataParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataParser.swift; sourceTree = "<group>"; };
B55CE97024810DE20001C498 /* MetadataStreamProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataStreamProcessor.swift; sourceTree = "<group>"; };
B55CE97724813BCA0001C498 /* UnsafeMutablePointer+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UnsafeMutablePointer+Helpers.swift"; sourceTree = "<group>"; };
B55CEAB32485107C0001C498 /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; };
B55CEAB72485172D0001C498 /* HTTPHeaderParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeaderParserTests.swift; sourceTree = "<group>"; };
B55CEAB9248530C00001C498 /* MetadataParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataParser.swift; sourceTree = "<group>"; };
B55CEABB24853CD20001C498 /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = "<group>"; };
B55F77CE24D82ADE0057F431 /* AudioPlayerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerDelegate.swift; sourceTree = "<group>"; };
B55F77D024D82CD50057F431 /* AVAudioUnit+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAudioUnit+Convenience.swift"; sourceTree = "<group>"; };
B55F77D524DACE140057F431 /* BufferContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BufferContext.swift; sourceTree = "<group>"; };
B5667A8F2499018D00D93F85 /* AudioFileStreamProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFileStreamProcessor.swift; sourceTree = "<group>"; };
B5667A912499063D00D93F85 /* AudioPlayerContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerContext.swift; sourceTree = "<group>"; };
B5667B3D249BC43000D93F85 /* AudioPlayerRenderProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerRenderProcessor.swift; sourceTree = "<group>"; };
B57829CE2548B32B00C78D36 /* Lock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lock.swift; sourceTree = "<group>"; };
B580CB1D25628CF4006D7DD8 /* AudioStreaming.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = AudioStreaming.podspec; sourceTree = "<group>"; };
B580CB1E25628CF4006D7DD8 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
B580CB1F25628D09006D7DD8 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
B58386372544A2C10087A712 /* EntryFrames.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryFrames.swift; sourceTree = "<group>"; };
B583863F254584A50087A712 /* ProcessedPackets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessedPackets.swift; sourceTree = "<group>"; };
B5838643254584BE0087A712 /* AudioStreamState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioStreamState.swift; sourceTree = "<group>"; };
B5838647254584D90087A712 /* SeekRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeekRequest.swift; sourceTree = "<group>"; };
B592E12825460146008866FB /* BiMapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiMapTests.swift; sourceTree = "<group>"; };
B592E133254608B4008866FB /* DispatchTimerSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchTimerSourceTests.swift; sourceTree = "<group>"; };
B59CB46B25420B4D00F8CAD0 /* MetadataStreamProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataStreamProcessorTests.swift; sourceTree = "<group>"; };
B59CB4B225421D8200F8CAD0 /* raw-stream-audio-empty-metadata */ = {isa = PBXFileReference; lastKnownFileType = file; path = "raw-stream-audio-empty-metadata"; sourceTree = "<group>"; };
B59CB4BA25421F3500F8CAD0 /* raw-stream-audio-normal-metadata */ = {isa = PBXFileReference; lastKnownFileType = file; path = "raw-stream-audio-normal-metadata"; sourceTree = "<group>"; };
B59CB4C525421FD400F8CAD0 /* raw-stream-audio-no-metadata */ = {isa = PBXFileReference; lastKnownFileType = file; path = "raw-stream-audio-no-metadata"; sourceTree = "<group>"; };
B59CB4CD2542204D00F8CAD0 /* raw-stream-audio-normal-metadata-alt */ = {isa = PBXFileReference; lastKnownFileType = file; path = "raw-stream-audio-normal-metadata-alt"; sourceTree = "<group>"; };
B59D0B6E255C904900D6CCE5 /* FileAudioSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileAudioSource.swift; sourceTree = "<group>"; };
B59DF10324916FD50043C498 /* DispatchQueue+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Helpers.swift"; sourceTree = "<group>"; };
B59DF1A22493E90C0043C498 /* AudioFileStream+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AudioFileStream+Helpers.swift"; sourceTree = "<group>"; };
B5AEDBAE24744153007D8101 /* AudioStreaming.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AudioStreaming.framework; sourceTree = BUILT_PRODUCTS_DIR; };
B5AEDBB124744153007D8101 /* AudioStreaming.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AudioStreaming.h; sourceTree = "<group>"; };
B5AEDBB224744153007D8101 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B5AEDBB724744153007D8101 /* AudioStreamingTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AudioStreamingTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
B5AEDBBE24744153007D8101 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B5B36E422655A32200DC96F5 /* FrameFilterProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameFilterProcessor.swift; sourceTree = "<group>"; };
B5B3B7CB248647ED00656828 /* AudioPlayerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerState.swift; sourceTree = "<group>"; };
B5D4A40825D9321400E1450C /* IcycastHeaderParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IcycastHeaderParser.swift; sourceTree = "<group>"; };
B5D4A40B25D9445600E1450C /* IcycastHeadersProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IcycastHeadersProcessor.swift; sourceTree = "<group>"; };
B5D82E64255DD562009EDAA4 /* NetStatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetStatusService.swift; sourceTree = "<group>"; };
B5DB66DA255C079C00B8DF53 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
B5DB66E1255C2EAB00B8DF53 /* AudioEntryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioEntryProvider.swift; sourceTree = "<group>"; };
B5E1DE2424B70B4200955BFB /* AudioPlayerConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerConfiguration.swift; sourceTree = "<group>"; };
B5EF954D247DA5AC003E8FF8 /* NetworkingClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkingClientTests.swift; sourceTree = "<group>"; };
B5EF9554247E9393003E8FF8 /* AudioEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioEntry.swift; sourceTree = "<group>"; };
B5EF9556247E9439003E8FF8 /* AudioStreamSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioStreamSource.swift; sourceTree = "<group>"; };
B5EF955A247EBCB3003E8FF8 /* AudioFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFileType.swift; sourceTree = "<group>"; };
B5EF955C247ECBB1003E8FF8 /* RemoteAudioSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteAudioSource.swift; sourceTree = "<group>"; };
B5F883B82477CBF600D277C1 /* AtomicTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicTests.swift; sourceTree = "<group>"; };
B5F883C22477DC4400D277C1 /* NetworkDataStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkDataStream.swift; sourceTree = "<group>"; };
B5FB6C0425516507002C0A37 /* AudioConverter+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AudioConverter+Helpers.swift"; sourceTree = "<group>"; };
B5FFF5FD2549FA02006BBB7C /* AudioExample.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AudioExample.xctestplan; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
B5AEDBAB24744153007D8101 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
B5AEDBB424744153007D8101 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
B5AEDBB824744153007D8101 /* AudioStreaming.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
98C82AE42B8CA8AA00AED485 /* Mp4 */ = {
isa = PBXGroup;
children = (
98C82AE52B8CA8BC00AED485 /* RemoteMp4Restructure.swift */,
98ABF69D2BAB07A20059C441 /* Mp4Restructure.swift */,
);
path = Mp4;
sourceTree = "<group>";
};
B5276B70247D4D3D00D2F56A /* Network */ = {
isa = PBXGroup;
children = (
B5D82E64255DD562009EDAA4 /* NetStatusService.swift */,
B5276B6E247D21A000D2F56A /* NetworkingClient.swift */,
B5276B73247D4D9F00D2F56A /* NetworkSessionDelegate.swift */,
B5F883C22477DC4400D277C1 /* NetworkDataStream.swift */,
);
path = Network;
sourceTree = "<group>";
};
B55A7369247FCB160050C53D /* Audio Entry */ = {
isa = PBXGroup;
children = (
B58386362544A2A60087A712 /* Models */,
B5DB66E1255C2EAB00B8DF53 /* AudioEntryProvider.swift */,
B5EF9554247E9393003E8FF8 /* AudioEntry.swift */,
);
path = "Audio Entry";
sourceTree = "<group>";
};
B55A736A247FCB310050C53D /* Parsers */ = {
isa = PBXGroup;
children = (
B55CEAB32485107C0001C498 /* Parser.swift */,
B55A736B247FCB420050C53D /* HTTPHeaderParser.swift */,
B55CE96D248058B60001C498 /* MetadataParser.swift */,
B5D4A40825D9321400E1450C /* IcycastHeaderParser.swift */,
);
path = Parsers;
sourceTree = "<group>";
};
B55CE97624813BA10001C498 /* Extensions */ = {
isa = PBXGroup;
children = (
B5FB6C0425516507002C0A37 /* AudioConverter+Helpers.swift */,
B59DF1A22493E90C0043C498 /* AudioFileStream+Helpers.swift */,
B55CE97724813BCA0001C498 /* UnsafeMutablePointer+Helpers.swift */,
B59DF10324916FD50043C498 /* DispatchQueue+Helpers.swift */,
B55F77D024D82CD50057F431 /* AVAudioUnit+Convenience.swift */,
B51B9F9924DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
B55CEAB5248517170001C498 /* Streaming */ = {
isa = PBXGroup;
children = (
B59CB4B125421D8200F8CAD0 /* Metadata Stream Processor */,
B55CEAB62485171E0001C498 /* Parsers */,
B51FE0C724892D1600F2A4D2 /* PlayerQueueEntriesTest.swift */,
);
path = Streaming;
sourceTree = "<group>";
};
B55CEAB62485171E0001C498 /* Parsers */ = {
isa = PBXGroup;
children = (
B55CEAB72485172D0001C498 /* HTTPHeaderParserTests.swift */,
B55CEAB9248530C00001C498 /* MetadataParser.swift */,
);
path = Parsers;
sourceTree = "<group>";
};
B55CEABF24855A900001C498 /* Helpers */ = {
isa = PBXGroup;
children = (
B51FE0C3248905B400F2A4D2 /* PlayerQueueEntries.swift */,
B5EF955A247EBCB3003E8FF8 /* AudioFileType.swift */,
B55F77D524DACE140057F431 /* BufferContext.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
B55CEAC024855AA20001C498 /* Processors */ = {
isa = PBXGroup;
children = (
B5B36E422655A32200DC96F5 /* FrameFilterProcessor.swift */,
B5667A8F2499018D00D93F85 /* AudioFileStreamProcessor.swift */,
B5667B3D249BC43000D93F85 /* AudioPlayerRenderProcessor.swift */,
B55CE97024810DE20001C498 /* MetadataStreamProcessor.swift */,
B5D4A40B25D9445600E1450C /* IcycastHeadersProcessor.swift */,
);
path = Processors;
sourceTree = "<group>";
};
B57A4F7A24AB4E6C00D7EA51 /* Frameworks */ = {
isa = PBXGroup;
children = (
B5DB66DA255C079C00B8DF53 /* AVFoundation.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
B580CB1C25628CE4006D7DD8 /* Deployment */ = {
isa = PBXGroup;
children = (
B580CB1D25628CF4006D7DD8 /* AudioStreaming.podspec */,
B580CB1E25628CF4006D7DD8 /* LICENSE */,
B580CB1F25628D09006D7DD8 /* Package.swift */,
);
name = Deployment;
sourceTree = "<group>";
};
B58386362544A2A60087A712 /* Models */ = {
isa = PBXGroup;
children = (
B58386372544A2C10087A712 /* EntryFrames.swift */,
B583863F254584A50087A712 /* ProcessedPackets.swift */,
B5838643254584BE0087A712 /* AudioStreamState.swift */,
B5838647254584D90087A712 /* SeekRequest.swift */,
);
path = Models;
sourceTree = "<group>";
};
B58BD7FC255DB653005B756D /* Audio Source */ = {
isa = PBXGroup;
children = (
98C82AE42B8CA8AA00AED485 /* Mp4 */,
B5EF9556247E9439003E8FF8 /* AudioStreamSource.swift */,
B5EF955C247ECBB1003E8FF8 /* RemoteAudioSource.swift */,
B59D0B6E255C904900D6CCE5 /* FileAudioSource.swift */,
);
path = "Audio Source";
sourceTree = "<group>";
};
B592E11E2545FF33008866FB /* Structures */ = {
isa = PBXGroup;
children = (
B5276B71247D4D5B00D2F56A /* BiMap.swift */,
B51FE0BF2488F67C00F2A4D2 /* Queue.swift */,
);
path = Structures;
sourceTree = "<group>";
};
B592E13025460883008866FB /* Helpers */ = {
isa = PBXGroup;
children = (
98DC00CB2B961F5E0068900A /* ByteBuffer.swift */,
98CC396D28BD651E006C9FF9 /* Atomic.swift */,
B514657E248E3884005C03F7 /* DispatchTimerSource.swift */,
B57829CE2548B32B00C78D36 /* Lock.swift */,
B500731F24D00BAC00BB4475 /* Logger.swift */,
B54C3E55255F286D00B356F2 /* Retrier.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
B59CB4B125421D8200F8CAD0 /* Metadata Stream Processor */ = {
isa = PBXGroup;
children = (
B59CB4B525421D8D00F8CAD0 /* raw-audio-streams */,
B59CB46B25420B4D00F8CAD0 /* MetadataStreamProcessorTests.swift */,
);
path = "Metadata Stream Processor";
sourceTree = "<group>";
};
B59CB4B525421D8D00F8CAD0 /* raw-audio-streams */ = {
isa = PBXGroup;
children = (
B59CB4CD2542204D00F8CAD0 /* raw-stream-audio-normal-metadata-alt */,
B59CB4C525421FD400F8CAD0 /* raw-stream-audio-no-metadata */,
B59CB4BA25421F3500F8CAD0 /* raw-stream-audio-normal-metadata */,
B59CB4B225421D8200F8CAD0 /* raw-stream-audio-empty-metadata */,
);
path = "raw-audio-streams";
sourceTree = "<group>";
};
B5AEDBA424744153007D8101 = {
isa = PBXGroup;
children = (
B580CB1C25628CE4006D7DD8 /* Deployment */,
B5AEDBB024744153007D8101 /* AudioStreaming */,
B5AEDBBB24744153007D8101 /* AudioStreamingTests */,
B5AEDBAF24744153007D8101 /* Products */,
B57A4F7A24AB4E6C00D7EA51 /* Frameworks */,
);
sourceTree = "<group>";
};
B5AEDBAF24744153007D8101 /* Products */ = {
isa = PBXGroup;
children = (
B5AEDBAE24744153007D8101 /* AudioStreaming.framework */,
B5AEDBB724744153007D8101 /* AudioStreamingTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
B5AEDBB024744153007D8101 /* AudioStreaming */ = {
isa = PBXGroup;
children = (
B5F883B42476DABE00D277C1 /* Core */,
B5EF9553247E9235003E8FF8 /* Streaming */,
B5AEDBB124744153007D8101 /* AudioStreaming.h */,
B5AEDBB224744153007D8101 /* Info.plist */,
);
path = AudioStreaming;
sourceTree = "<group>";
};
B5AEDBBB24744153007D8101 /* AudioStreamingTests */ = {
isa = PBXGroup;
children = (
B5FFF5FD2549FA02006BBB7C /* AudioExample.xctestplan */,
B5F883B72477CBC900D277C1 /* Core */,
B55CEAB5248517170001C498 /* Streaming */,
B5AEDBBE24744153007D8101 /* Info.plist */,
);
path = AudioStreamingTests;
sourceTree = "<group>";
};
B5E1DE2924B7179E00955BFB /* AudioPlayer */ = {
isa = PBXGroup;
children = (
B54D876C2490E4A000C361A0 /* UnitDescriptions.swift */,
B5B3B7CB248647ED00656828 /* AudioPlayerState.swift */,
B5E1DE2424B70B4200955BFB /* AudioPlayerConfiguration.swift */,
B55F77CE24D82ADE0057F431 /* AudioPlayerDelegate.swift */,
B55CEABB24853CD20001C498 /* AudioPlayer.swift */,
B5667A912499063D00D93F85 /* AudioPlayerContext.swift */,
B54D876E2490E4DD00C361A0 /* AudioRendererContext.swift */,
B55CEAC024855AA20001C498 /* Processors */,
);
path = AudioPlayer;
sourceTree = "<group>";
};
B5EF954A247DA450003E8FF8 /* Network */ = {
isa = PBXGroup;
children = (
B5EF954D247DA5AC003E8FF8 /* NetworkingClientTests.swift */,
);
path = Network;
sourceTree = "<group>";
};
B5EF9553247E9235003E8FF8 /* Streaming */ = {
isa = PBXGroup;
children = (
B5E1DE2924B7179E00955BFB /* AudioPlayer */,
B58BD7FC255DB653005B756D /* Audio Source */,
B55A7369247FCB160050C53D /* Audio Entry */,
B55CEABF24855A900001C498 /* Helpers */,
B55A736A247FCB310050C53D /* Parsers */,
);
path = Streaming;
sourceTree = "<group>";
};
B5F883B42476DABE00D277C1 /* Core */ = {
isa = PBXGroup;
children = (
B55CE97624813BA10001C498 /* Extensions */,
B592E11E2545FF33008866FB /* Structures */,
B5276B70247D4D3D00D2F56A /* Network */,
B592E13025460883008866FB /* Helpers */,
);
path = Core;
sourceTree = "<group>";
};
B5F883B72477CBC900D277C1 /* Core */ = {
isa = PBXGroup;
children = (
B5EF954A247DA450003E8FF8 /* Network */,
B5F883B82477CBF600D277C1 /* AtomicTests.swift */,
B51FE0C12488F96A00F2A4D2 /* QueueTests.swift */,
B592E12825460146008866FB /* BiMapTests.swift */,
B592E133254608B4008866FB /* DispatchTimerSourceTests.swift */,
98DC00CD2B9726380068900A /* ByteBufferTests.swift */,
);
path = Core;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
B5AEDBA924744153007D8101 /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
B5AEDBBF24744153007D8101 /* AudioStreaming.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
B5AEDBAD24744153007D8101 /* AudioStreaming */ = {
isa = PBXNativeTarget;
buildConfigurationList = B5AEDBC224744153007D8101 /* Build configuration list for PBXNativeTarget "AudioStreaming" */;
buildPhases = (
B5AEDBA924744153007D8101 /* Headers */,
B5AEDBAA24744153007D8101 /* Sources */,
B5AEDBAB24744153007D8101 /* Frameworks */,
B5AEDBAC24744153007D8101 /* Resources */,
B57A4F7D24AB4E6C00D7EA51 /* Embed Frameworks */,
B583864B2545858E0087A712 /* SwiftLint */,
);
buildRules = (
);
dependencies = (
);
name = AudioStreaming;
packageProductDependencies = (
);
productName = AudioStreaming;
productReference = B5AEDBAE24744153007D8101 /* AudioStreaming.framework */;
productType = "com.apple.product-type.framework";
};
B5AEDBB624744153007D8101 /* AudioStreamingTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = B5AEDBC524744153007D8101 /* Build configuration list for PBXNativeTarget "AudioStreamingTests" */;
buildPhases = (
B5AEDBB324744153007D8101 /* Sources */,
B5AEDBB424744153007D8101 /* Frameworks */,
B5AEDBB524744153007D8101 /* Resources */,
);
buildRules = (
);
dependencies = (
B5AEDBBA24744153007D8101 /* PBXTargetDependency */,
);
name = AudioStreamingTests;
productName = AudioStreamingTests;
productReference = B5AEDBB724744153007D8101 /* AudioStreamingTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
B5AEDBA524744153007D8101 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1140;
LastUpgradeCheck = 1620;
ORGANIZATIONNAME = Decimal;
TargetAttributes = {
B5AEDBAD24744153007D8101 = {
CreatedOnToolsVersion = 11.4;
LastSwiftMigration = 1200;
};
B5AEDBB624744153007D8101 = {
CreatedOnToolsVersion = 11.4;
LastSwiftMigration = 1140;
};
};
};
buildConfigurationList = B5AEDBA824744153007D8101 /* Build configuration list for PBXProject "AudioStreaming" */;
compatibilityVersion = "Xcode 11.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = B5AEDBA424744153007D8101;
packageReferences = (
);
productRefGroup = B5AEDBAF24744153007D8101 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
B5AEDBAD24744153007D8101 /* AudioStreaming */,
B5AEDBB624744153007D8101 /* AudioStreamingTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
B5AEDBAC24744153007D8101 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
B5AEDBB524744153007D8101 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B59CB4BB25421F3500F8CAD0 /* raw-stream-audio-normal-metadata in Resources */,
B59CB4C225421F7A00F8CAD0 /* raw-stream-audio-empty-metadata in Resources */,
B59CB4C625421FD400F8CAD0 /* raw-stream-audio-no-metadata in Resources */,
B59CB4CE2542204D00F8CAD0 /* raw-stream-audio-normal-metadata-alt in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
B583864B2545858E0087A712 /* SwiftLint */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = SwiftLint;
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if [[ \"$(uname -m)\" == arm64 ]]; then\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
B5AEDBAA24744153007D8101 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B58386382544A2C10087A712 /* EntryFrames.swift in Sources */,
B5EF955D247ECBB1003E8FF8 /* RemoteAudioSource.swift in Sources */,
B57829CF2548B32B00C78D36 /* Lock.swift in Sources */,
B5838640254584A50087A712 /* ProcessedPackets.swift in Sources */,
B54C3E56255F286D00B356F2 /* Retrier.swift in Sources */,
B59DF10424916FD50043C498 /* DispatchQueue+Helpers.swift in Sources */,
98CC396E28BD651E006C9FF9 /* Atomic.swift in Sources */,
B5B3B7CC248647ED00656828 /* AudioPlayerState.swift in Sources */,
B51B9F9A24DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift in Sources */,
B51FE0C624890CCB00F2A4D2 /* PlayerQueueEntries.swift in Sources */,
B5EF9557247E9439003E8FF8 /* AudioStreamSource.swift in Sources */,
B5D4A40925D9321400E1450C /* IcycastHeaderParser.swift in Sources */,
B59DF1A32493E90C0043C498 /* AudioFileStream+Helpers.swift in Sources */,
B54D876D2490E4A000C361A0 /* UnitDescriptions.swift in Sources */,
B514657F248E3884005C03F7 /* DispatchTimerSource.swift in Sources */,
B55CEABC24853CD20001C498 /* AudioPlayer.swift in Sources */,
B5667B3E249BC43100D93F85 /* AudioPlayerRenderProcessor.swift in Sources */,
B5276B6F247D21A000D2F56A /* NetworkingClient.swift in Sources */,
B5EF955B247EBCB3003E8FF8 /* AudioFileType.swift in Sources */,
B592E1252545FF9A008866FB /* BiMap.swift in Sources */,
B5DB66E2255C2EAB00B8DF53 /* AudioEntryProvider.swift in Sources */,
B5D4A41025D948EF00E1450C /* IcycastHeadersProcessor.swift in Sources */,
B5667A902499018D00D93F85 /* AudioFileStreamProcessor.swift in Sources */,
B59D0B6F255C904900D6CCE5 /* FileAudioSource.swift in Sources */,
98DC00CC2B961F5E0068900A /* ByteBuffer.swift in Sources */,
B5EF9555247E9393003E8FF8 /* AudioEntry.swift in Sources */,
B5B36E432655A32200DC96F5 /* FrameFilterProcessor.swift in Sources */,
B51FE0C02488F67C00F2A4D2 /* Queue.swift in Sources */,
B5667A922499063D00D93F85 /* AudioPlayerContext.swift in Sources */,
B55CE97124810DE20001C498 /* MetadataStreamProcessor.swift in Sources */,
B55CEAB42485107C0001C498 /* Parser.swift in Sources */,
B5FB6C0525516507002C0A37 /* AudioConverter+Helpers.swift in Sources */,
B5E1DE2524B70B4200955BFB /* AudioPlayerConfiguration.swift in Sources */,
B5F883C32477DC4400D277C1 /* NetworkDataStream.swift in Sources */,
B54D876F2490E4DD00C361A0 /* AudioRendererContext.swift in Sources */,
B55F77CF24D82ADE0057F431 /* AudioPlayerDelegate.swift in Sources */,
B55A736C247FCB420050C53D /* HTTPHeaderParser.swift in Sources */,
B55F77D124D82CD50057F431 /* AVAudioUnit+Convenience.swift in Sources */,
B55CE96E248058B60001C498 /* MetadataParser.swift in Sources */,
B5838644254584BE0087A712 /* AudioStreamState.swift in Sources */,
B500732024D00BAC00BB4475 /* Logger.swift in Sources */,
98C82AE62B8CA8BC00AED485 /* RemoteMp4Restructure.swift in Sources */,
B5276B74247D4D9F00D2F56A /* NetworkSessionDelegate.swift in Sources */,
B55F77D624DACE140057F431 /* BufferContext.swift in Sources */,
B5838648254584D90087A712 /* SeekRequest.swift in Sources */,
B5D82E65255DD562009EDAA4 /* NetStatusService.swift in Sources */,
B55CE97824813BCA0001C498 /* UnsafeMutablePointer+Helpers.swift in Sources */,
98ABF69E2BAB07A20059C441 /* Mp4Restructure.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
B5AEDBB324744153007D8101 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B5EF954E247DA5AC003E8FF8 /* NetworkingClientTests.swift in Sources */,
B59CB46C25420B4D00F8CAD0 /* MetadataStreamProcessorTests.swift in Sources */,
B51FE0C824892D1600F2A4D2 /* PlayerQueueEntriesTest.swift in Sources */,
B55CEABA248530C00001C498 /* MetadataParser.swift in Sources */,
B51FE0C22488F96A00F2A4D2 /* QueueTests.swift in Sources */,
B5F883BA2477CEFC00D277C1 /* AtomicTests.swift in Sources */,
B592E134254608B4008866FB /* DispatchTimerSourceTests.swift in Sources */,
B55CEAB82485172D0001C498 /* HTTPHeaderParserTests.swift in Sources */,
B592E12925460146008866FB /* BiMapTests.swift in Sources */,
98DC00CE2B9726380068900A /* ByteBufferTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
B5AEDBBA24744153007D8101 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
platformFilters = (
ios,
tvos,
);
target = B5AEDBAD24744153007D8101 /* AudioStreaming */;
targetProxy = B5AEDBB924744153007D8101 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
B5AEDBC024744153007D8101 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.2.7;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Debug;
};
B5AEDBC124744153007D8101 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.2.7;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Release;
};
B5AEDBC324744153007D8101 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_MODULE_VERIFIER = YES;
INFOPLIST_FILE = AudioStreaming/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.2.7;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,3";
};
name = Debug;
};
B5AEDBC424744153007D8101 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_MODULE_VERIFIER = YES;
INFOPLIST_FILE = AudioStreaming/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.2.7;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,3";
};
name = Release;
};
B5AEDBC624744153007D8101 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = AudioStreamingTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreamingTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,3";
};
name = Debug;
};
B5AEDBC724744153007D8101 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = AudioStreamingTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreamingTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,3";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
B5AEDBA824744153007D8101 /* Build configuration list for PBXProject "AudioStreaming" */ = {
isa = XCConfigurationList;
buildConfigurations = (
B5AEDBC024744153007D8101 /* Debug */,
B5AEDBC124744153007D8101 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
B5AEDBC224744153007D8101 /* Build configuration list for PBXNativeTarget "AudioStreaming" */ = {
isa = XCConfigurationList;
buildConfigurations = (
B5AEDBC324744153007D8101 /* Debug */,
B5AEDBC424744153007D8101 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
B5AEDBC524744153007D8101 /* Build configuration list for PBXNativeTarget "AudioStreamingTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
B5AEDBC624744153007D8101 /* Debug */,
B5AEDBC724744153007D8101 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = B5AEDBA524744153007D8101 /* Project object */;
}
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -1,45 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>classNames</key>
<dict>
<key>ProtectedTests</key>
<dict>
<key>testProtectedValuesAreAccessedSafely()</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>4.7921</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
<key>testThatProtectedReadAndWriteAreSafe()</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.0067462</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
</dict>
<key>QueueTests</key>
<dict>
<key>testComplexity()</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>3.7871</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
</dict>
</dict>
</dict>
</plist>
@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>runDestinationsByUUID</key>
<dict>
<key>31DA71B1-4664-472C-BD35-0711EE94837A</key>
<dict>
<key>localComputer</key>
<dict>
<key>busSpeedInMHz</key>
<integer>400</integer>
<key>cpuCount</key>
<integer>1</integer>
<key>cpuKind</key>
<string>8-Core Intel Core i9</string>
<key>cpuSpeedInMHz</key>
<integer>2300</integer>
<key>logicalCPUCoresPerPackage</key>
<integer>16</integer>
<key>modelCode</key>
<string>MacBookPro16,1</string>
<key>physicalCPUCoresPerPackage</key>
<integer>8</integer>
<key>platformIdentifier</key>
<string>com.apple.platform.macosx</string>
</dict>
<key>targetArchitecture</key>
<string>x86_64</string>
<key>targetDevice</key>
<dict>
<key>modelCode</key>
<string>iPhone12,1</string>
<key>platformIdentifier</key>
<string>com.apple.platform.iphonesimulator</string>
</dict>
</dict>
</dict>
</dict>
</plist>
@@ -1,103 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1620"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B5AEDBAD24744153007D8101"
BuildableName = "AudioStreaming.framework"
BlueprintName = "AudioStreaming"
ReferencedContainer = "container:AudioStreaming.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
enableThreadSanitizer = "YES"
codeCoverageEnabled = "YES"
onlyGenerateCoverageForSpecifiedTargets = "YES">
<CodeCoverageTargets>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B5AEDBAD24744153007D8101"
BuildableName = "AudioStreaming.framework"
BlueprintName = "AudioStreaming"
ReferencedContainer = "container:AudioStreaming.xcodeproj">
</BuildableReference>
</CodeCoverageTargets>
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B5AEDBB624744153007D8101"
BuildableName = "AudioStreamingTests.xctest"
BlueprintName = "AudioStreamingTests"
ReferencedContainer = "container:AudioStreaming.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
disableMainThreadChecker = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<AdditionalOptions>
<AdditionalOption
key = "MallocStackLogging"
value = ""
isEnabled = "YES">
</AdditionalOption>
<AdditionalOption
key = "PrefersMallocStackLoggingLite"
value = ""
isEnabled = "YES">
</AdditionalOption>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B5AEDBAD24744153007D8101"
BuildableName = "AudioStreaming.framework"
BlueprintName = "AudioStreaming"
ReferencedContainer = "container:AudioStreaming.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -1,32 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>AudioStreaming.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>B5AEDBAD24744153007D8101</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>B5AEDBB624744153007D8101</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>B5B3B7D22486993B00656828</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict>
</plist>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:AudioPlayer/AudioPlayer.xcodeproj">
</FileRef>
<FileRef
location = "container:AudioStreaming.xcodeproj">
</FileRef>
</Workspace>
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildLocationStyle</key>
<string>UseAppPreferences</string>
<key>CustomBuildLocationType</key>
<string>RelativeToDerivedData</string>
<key>DerivedDataLocationStyle</key>
<string>Default</string>
<key>IssueFilterStyle</key>
<string>ShowActiveSchemeOnly</string>
<key>LiveSourceIssuesEnabled</key>
<true/>
<key>ShowSharedSchemesAutomaticallyEnabled</key>
<true/>
</dict>
</plist>
@@ -1,74 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "FECE436A-3E7C-4680-91FF-008D425B5539"
type = "0"
version = "2.0">
<Breakpoints>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.SwiftErrorBreakpoint">
<BreakpointContent
uuid = "5041EB46-A677-4654-B257-5EE0996DB4C7"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.ExceptionBreakpoint">
<BreakpointContent
uuid = "556803C5-B0C9-4A2B-BE87-2D41721D1F54"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
breakpointStackSelectionBehavior = "1"
scope = "1"
stopOnStyle = "0">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.RuntimeIssueBreakpoint">
<BreakpointContent
uuid = "F3650228-D513-48BC-AB78-8A8A0F688628"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
breakpointStackSelectionBehavior = "1"
type = "1">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.SymbolicBreakpoint">
<BreakpointContent
uuid = "8E93EE2C-5F40-468B-9880-089C10EC6FE1"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "malloc_error_break"
moduleName = "">
<Locations>
<Location
uuid = "8E93EE2C-5F40-468B-9880-089C10EC6FE1 - 9d096611641b4e7c"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "malloc_error_break"
moduleName = "libsystem_malloc.dylib"
usesParentBreakpointCondition = "Yes"
offsetFromSymbolStart = "0">
</Location>
</Locations>
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.RuntimeIssueBreakpoint">
<BreakpointContent
uuid = "0BEDF6EA-D92A-448C-A0E4-FBDADBF90794"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
breakpointStackSelectionBehavior = "1"
type = "65535">
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket>
@@ -16,6 +16,7 @@ public enum AudioConverterError: CustomDebugStringConvertible, Sendable {
case propertyNotSupported
case requiresPacketDescriptionsError
case unspecifiedError
case cannotCreateConverter
init(osstatus: OSStatus) {
switch osstatus {
@@ -65,7 +66,9 @@ public enum AudioConverterError: CustomDebugStringConvertible, Sendable {
case .requiresPacketDescriptionsError:
return "Required packet descriptions (error)"
case .unspecifiedError:
return "Unspecified error "
return "Unspecified error"
case .cannotCreateConverter:
return "Cannot create audio converter"
}
}
}
@@ -0,0 +1,212 @@
import Foundation
import AudioCodecs
import AVFoundation
/// A simple decoder for Ogg Vorbis files using libvorbisfile
final class VorbisFileDecoder {
// Core properties
private var stream: VFStreamRef?
private var vf: VFFileRef?
// Audio format properties
private(set) var sampleRate: Int = 0
private(set) var channels: Int = 0
private(set) var durationSeconds: Double = -1
private(set) var totalPcmSamples: Int64 = -1
private(set) var nominalBitrate: Int = 0
private(set) var processingFormat: AVAudioFormat?
// Thread safety
private let decoderLock = NSLock()
// Silent frame generation
private var silentFrameBuffer: UnsafeMutablePointer<Float>?
private var silentFrameSize = 0
/// Create the stream buffer with specified capacity
/// - Parameter capacityBytes: Size of the ring buffer in bytes
func create(capacityBytes: Int) {
decoderLock.lock()
defer { decoderLock.unlock() }
stream = VFStreamCreate(capacityBytes)
}
/// Clean up resources
func destroy() {
decoderLock.lock()
defer { decoderLock.unlock() }
if let vf = vf { VFClear(vf) }
if let stream = stream { VFStreamDestroy(stream) }
vf = nil
stream = nil
if let silentFrameBuffer = silentFrameBuffer {
silentFrameBuffer.deallocate()
self.silentFrameBuffer = nil
}
}
deinit {
destroy()
}
/// Push data into the stream buffer
/// - Parameter data: The Ogg Vorbis data to decode
func push(_ data: Data) {
decoderLock.lock()
defer { decoderLock.unlock() }
data.withUnsafeBytes { rawBuf in
guard let base = rawBuf.baseAddress?.assumingMemoryBound(to: UInt8.self),
rawBuf.count > 0,
let stream = stream else { return }
VFStreamPush(stream, base, rawBuf.count)
}
}
/// Get the number of bytes currently available in the stream buffer
/// - Returns: Number of bytes available
func availableBytes() -> Int {
decoderLock.lock()
defer { decoderLock.unlock() }
guard let stream = stream else { return 0 }
return Int(VFStreamAvailableBytes(stream))
}
/// Mark the end of the stream
func markEOF() {
decoderLock.lock()
defer { decoderLock.unlock() }
if let stream = stream {
VFStreamMarkEOF(stream)
}
}
/// Try to open the Vorbis file if enough data is available
/// - Throws: Error if opening fails
func openIfNeeded() throws {
decoderLock.lock()
defer { decoderLock.unlock() }
guard vf == nil, let stream = stream else { return }
var outVF: VFFileRef?
let rc = VFOpen(stream, &outVF)
if rc < 0 {
Logger.error("Failed to open Vorbis file", category: .audioRendering)
throw NSError(domain: "VorbisFileDecoder", code: Int(rc),
userInfo: [NSLocalizedDescriptionKey: "Failed to open Vorbis file"])
}
vf = outVF
// Get stream info
var info = VFStreamInfo()
if VFGetInfo(outVF, &info) == 0 {
sampleRate = Int(info.sample_rate)
channels = Int(info.channels)
totalPcmSamples = Int64(info.total_pcm_samples)
durationSeconds = info.duration_seconds
nominalBitrate = Int(info.bitrate_nominal)
// Create audio format
let layoutTag: AudioChannelLayoutTag
switch channels {
case 1: layoutTag = kAudioChannelLayoutTag_Mono
case 2: layoutTag = kAudioChannelLayoutTag_Stereo
default: layoutTag = kAudioChannelLayoutTag_Unknown | UInt32(channels)
}
let channelLayout = AVAudioChannelLayout(layoutTag: layoutTag)!
processingFormat = AVAudioFormat(
commonFormat: .pcmFormatFloat32,
sampleRate: Double(sampleRate),
interleaved: false,
channelLayout: channelLayout
)
// Create silent frame buffer
silentFrameSize = 1024 * channels
silentFrameBuffer = UnsafeMutablePointer<Float>.allocate(capacity: silentFrameSize)
for i in 0..<silentFrameSize {
silentFrameBuffer?[i] = 0.0
}
} else {
Logger.error("Failed to get stream info", category: .audioRendering)
}
}
/// Read decoded frames into an AVAudioPCMBuffer
/// - Parameters:
/// - buffer: The buffer to fill with audio data
/// - frameCount: Maximum number of frames to read
/// - Returns: Number of frames read, 0 on EOF, negative on error
func readFrames(into buffer: AVAudioPCMBuffer, frameCount: Int) -> Int {
decoderLock.lock()
defer { decoderLock.unlock() }
guard let vf = vf, buffer.format.channelCount > 0 else {
return generateSilentFrames(into: buffer, frameCount: frameCount)
}
// Get float channel data from buffer
guard let floatChannelData = buffer.floatChannelData else {
return generateSilentFrames(into: buffer, frameCount: frameCount)
}
// Read deinterleaved frames directly
let maxFrames = min(frameCount, Int(buffer.frameCapacity))
var pcmChannels: UnsafeMutablePointer<UnsafeMutablePointer<Float>?>?
let framesRead = Int(VFReadFloat(vf, &pcmChannels, Int32(maxFrames)))
// If no frames were read, generate silent frames instead of returning 0
if framesRead <= 0 {
return generateSilentFrames(into: buffer, frameCount: frameCount)
}
// Copy deinterleaved data directly (no conversion needed!)
guard let pcm = pcmChannels else {
return generateSilentFrames(into: buffer, frameCount: frameCount)
}
let channelCount = min(Int(buffer.format.channelCount), channels)
for ch in 0..<channelCount {
guard let input = pcm[ch] else { continue }
let output = floatChannelData[ch]
memcpy(output, input, framesRead * MemoryLayout<Float>.stride)
}
return framesRead
}
/// Generate silent frames when no real audio data is available
/// This prevents EOF detection by never returning 0 frames
private func generateSilentFrames(into buffer: AVAudioPCMBuffer, frameCount: Int) -> Int {
guard let floatChannelData = buffer.floatChannelData,
channels > 0 else { return 1 }
// Use a small frame count to ensure we keep checking for real data
let framesToGenerate = min(128, frameCount)
// Fill buffer with zeros
for ch in 0..<min(Int(buffer.format.channelCount), channels) {
let dst = floatChannelData[ch]
for frame in 0..<framesToGenerate {
dst[frame] = 0.0
}
}
return framesToGenerate
}
/// Reset the decoder state
func reset() {
destroy()
}
}
@@ -108,6 +108,9 @@ class AudioEntry {
func calculatedBitrate() -> Double {
lock.lock(); defer { lock.unlock() }
if let explicitBitRate = audioStreamState.bitRate, explicitBitRate > 0 {
return explicitBitRate
}
let packets = processedPacketsState
if packetDuration > 0 {
let packetsCount = packets.count
@@ -5,6 +5,27 @@
import AVFoundation
struct OggVorbisStreamInfo {
var serialNumber: UInt32 = 0
var pageCount: UInt64 = 0
var totalSamples: UInt64 = 0
var sampleRate: UInt32 = 0
var channels: UInt8 = 0
var bitRate: UInt32 = 0
var nominalBitrate: UInt32 = 0
var minBitrate: UInt32 = 0
var maxBitrate: UInt32 = 0
var blocksize0: Int = 0
var blocksize1: Int = 0
var commentHeader: [String: String] = [:]
// For seeking
var granulePosition: Int64 = 0
var pageOffsets: [Int64] = []
var pageGranules: [Int64] = []
}
final class AudioStreamState {
var processedDataFormat: Bool = false
var dataOffset: UInt64 = 0
@@ -12,4 +33,13 @@ final class AudioStreamState {
var dataPacketOffset: UInt64?
var dataPacketCount: Double = 0
var streamFormat = AudioStreamBasicDescription()
var bitRate: Double?
// Flag to indicate when the audio format is ready for decoding
var readyForDecoding: Bool = false
// Add Ogg Vorbis-specific metadata
var oggVorbisStreamInfo: OggVorbisStreamInfo?
var hasAttemptedOggVorbisParse: Bool = false
var initialOggBytes: Data?
}
@@ -120,11 +120,15 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
if isMp4, !mp4IsAlreadyOptimized {
if !mp4Restructure.dataOptimized {
do {
if let mp4OptimizeInfo = try mp4Restructure.checkIsOptimized(data: data) {
try performMp4Restructure(inputStream: inputStream, mp4OptimizeInfo: mp4OptimizeInfo)
} else {
switch try mp4Restructure.checkIsOptimized(data: data) {
case .undetermined:
// Not enough bytes yet; wait for more data before deciding
break
case .optimized:
mp4IsAlreadyOptimized = true
delegate?.dataAvailable(source: self, data: data)
case let .needsRestructure(moovOffset):
try performMp4Restructure(inputStream: inputStream, moovOffset: moovOffset)
}
} catch {
delegate?.errorOccurred(source: self, error: error)
@@ -141,24 +145,71 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
}
}
func performMp4Restructure(inputStream: InputStream, mp4OptimizeInfo: Mp4OptimizeInfo) throws {
let offsetAccepted = inputStream.setProperty(mp4OptimizeInfo.moovOffset, forKey: .fileCurrentOffsetKey)
if offsetAccepted {
let moovDataBuffer = UnsafeMutablePointer.uint8pointer(of: mp4OptimizeInfo.moovSize)
defer { moovDataBuffer.deallocate() }
let moovRead = inputStream.read(moovDataBuffer, maxLength: mp4OptimizeInfo.moovSize)
if moovRead > 0 {
let data = Data(bytes: moovDataBuffer, count: moovRead)
let moovData = try mp4Restructure.restructureMoov(data: data)
delegate?.dataAvailable(source: self, data: moovData.initialData)
if !inputStream.setProperty(moovData.mdatOffset, forKey: .fileCurrentOffsetKey) {
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
}
} else {
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
}
} else {
func performMp4Restructure(inputStream: InputStream, moovOffset: Int) throws {
let offsetAccepted = inputStream.setProperty(moovOffset, forKey: .fileCurrentOffsetKey)
if !offsetAccepted {
delegate?.errorOccurred(source: self, error: inputStream.streamError ?? AudioSystemError.playerStartError)
return
}
// Read moov header (8 bytes)
var header = [UInt8](repeating: 0, count: 8)
let headerRead = inputStream.read(&header, maxLength: 8)
guard headerRead == 8 else {
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
return
}
// Parse size and type (big endian)
let size32 = Data(header[0 ..< 4]).withUnsafeBytes { $0.load(as: UInt32.self) }.bigEndian
let type32 = Data(header[4 ..< 8]).withUnsafeBytes { $0.load(as: UInt32.self) }.bigEndian
guard Int(type32) == Atoms.moov else {
delegate?.errorOccurred(source: self, error: Mp4RestructureError.missingMoovAtom)
return
}
var moovSize = Int(size32)
var moovData = Data(header)
// Extended size (64-bit)
if moovSize == 1 {
var ext = [UInt8](repeating: 0, count: 8)
let extRead = inputStream.read(&ext, maxLength: 8)
guard extRead == 8 else {
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
return
}
let ext64 = Data(ext).withUnsafeBytes { $0.load(as: UInt64.self) }.bigEndian
moovSize = Int(ext64)
moovData.append(contentsOf: ext)
}
let remaining = moovSize - moovData.count
if remaining < 0 {
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
return
}
if remaining > 0 {
var buffer = [UInt8](repeating: 0, count: remaining)
var total = 0
while total < remaining {
let readBytes = buffer.withUnsafeMutableBytes { ptr -> Int in
let base = ptr.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: total)
return inputStream.read(base, maxLength: remaining - total)
}
guard readBytes > 0 else {
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
return
}
total += readBytes
}
moovData.append(contentsOf: buffer)
}
let moovResult = try mp4Restructure.restructureMoov(data: moovData)
delegate?.dataAvailable(source: self, data: moovResult.initialData)
if !inputStream.setProperty(moovResult.mdatOffset, forKey: .fileCurrentOffsetKey) {
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
}
}
@@ -36,7 +36,7 @@ enum Atoms {
static var cmov: Int { fourCcToInt("cmov") }
static var stco: Int { fourCcToInt("stco") }
static var co64: Int { fourCcToInt("c064") }
static var co64: Int { fourCcToInt("co64") }
static var atomPreampleSize: Int = 8
@@ -75,6 +75,12 @@ enum Mp4RestructureError: Error {
case networkError(Error)
}
enum OptimizeCheckResult: Equatable {
case optimized
case needsRestructure(moovOffset: Int)
case undetermined
}
final class Mp4Restructure {
private var atomOffset: Int = 0
@@ -129,24 +135,36 @@ final class Mp4Restructure {
return (initialData, mdatOffset)
}
/// Returns `nil` if the data is optimized otherwise `Mp4OptimizeInfo`
func checkIsOptimized(data: Data) throws -> Mp4OptimizeInfo? {
while atomOffset < UInt64(data.count) {
var atomSize = try Int(getInteger(data: data, offset: atomOffset) as UInt32)
let atomType = try Int(getInteger(data: data, offset: atomOffset + 4) as UInt32)
/// Incrementally checks if the MP4 is optimized. Returns tri-state result.
func checkIsOptimized(data: Data) throws -> OptimizeCheckResult {
while atomOffset + 8 <= data.count {
var atomSize: Int = try Int(getInteger(data: data, offset: atomOffset) as UInt32)
let atomType: Int = try Int(getInteger(data: data, offset: atomOffset + 4) as UInt32)
var headerSize = 8
// Handle extended size (64-bit)
if atomSize == 1 {
if atomOffset + 16 > data.count { break }
let ext: UInt64 = try getInteger(data: data, offset: atomOffset + 8)
atomSize = Int(ext)
headerSize = 16
} else if atomSize == 0 {
// Size extends to EOF; with partial data we can't determine full box
break
}
// Bounds and sanity checks
if atomSize < headerSize || atomOffset + atomSize > data.count { break }
switch atomType {
case Atoms.ftyp:
let ftypData = data[Int(atomOffset) ..< atomSize]
let start = atomOffset
let end = atomOffset + atomSize
let ftypData = data[start ..< end]
let ftyp = MP4Atom(type: atomType, size: atomSize, offset: atomOffset, data: ftypData)
self.ftyp = ftyp
atoms.append(ftyp)
case Atoms.mdat:
// ref: https://developer.apple.com/documentation/quicktime-file-format/movie_data_atom
// This atom can be quite large, and may exceed 2^32 bytes, in which case the size field will be set to 1,
// and the header will contain a 64-bit extended size field.
if atomSize == 1 {
atomSize = Int(try getInteger(data: data, offset: atomOffset + 8) as UInt64)
}
let mdat = MP4Atom(type: atomType, size: atomSize, offset: atomOffset)
atoms.append(mdat)
foundMdat = true
@@ -158,19 +176,21 @@ final class Mp4Restructure {
let atom = MP4Atom(type: atomType, size: atomSize, offset: atomOffset)
atoms.append(atom)
}
if ftyp != nil {
if foundMoov && !foundMdat {
Logger.debug("🕵️ detected an optimized mp4", category: .generic)
return nil
return .optimized
} else if !foundMoov && foundMdat {
Logger.debug("🕵️ detected an non-optimized mp4", category: .generic)
let possibleMoovOffset = Int(atomOffset) + atomSize
return Mp4OptimizeInfo(moovOffset: possibleMoovOffset, moovSize: atomSize)
Logger.debug("🕵️ detected a non-optimized mp4", category: .generic)
let possibleMoovOffset = atomOffset + atomSize
return .needsRestructure(moovOffset: possibleMoovOffset)
}
}
atomOffset += atomSize
}
return nil
return .undetermined
}
/// logic taken from qt-faststart.c over at ffmpeg
@@ -236,6 +256,8 @@ final class Mp4Restructure {
// the next integer determines the `Number of entries`
// https://developer.apple.com/documentation/quicktime-file-format/chunk_offset_atom/number_of_entries
let numberOfOffsetEntries = try Int(moovAtom.getInteger() as UInt32)
// Adjust by moov size
let adjustDelta = moovAtomSize
if atomType == Atoms.stco {
Logger.debug("🏗️ patching stco atom...", category: .generic)
if moovAtom.bytesAvailable < numberOfOffsetEntries * 4 {
@@ -246,7 +268,7 @@ final class Mp4Restructure {
for _ in 0 ..< numberOfOffsetEntries {
let currentOffset = try Int(moovAtom.getInteger(moovAtom.offset) as UInt32)
// adjust the offset by adding the size of moov atom
let adjustOffset = currentOffset + moovAtomSize
let adjustOffset = currentOffset + adjustDelta
if currentOffset < 0, adjustOffset >= 0 {
throw Mp4RestructureError.unableToRestructureData
@@ -261,8 +283,8 @@ final class Mp4Restructure {
}
for _ in 0 ..< numberOfOffsetEntries {
let currentOffset: Int = try moovAtom.getInteger(moovAtom.offset)
// adjust the offset by adding the size of moov atom
moovAtom.put(currentOffset + moovAtomSize)
// adjust the offset by adding the size of moov atom (write as big-endian 64-bit)
moovAtom.put(UInt64(currentOffset + adjustDelta).bigEndian)
}
}
}
@@ -271,10 +293,10 @@ final class Mp4Restructure {
func getInteger<T: FixedWidthInteger>(data: Data, offset: Int) throws -> T {
let sizeOfInteger = MemoryLayout<T>.size
guard sizeOfInteger <= data.count else {
guard offset >= 0, offset + sizeOfInteger <= data.count else {
throw ByteBuffer.Error.eof
}
let _offset = offset + sizeOfInteger
return T(data: data[_offset - sizeOfInteger ..< _offset]).bigEndian
let end = offset + sizeOfInteger
return T(data: data[offset ..< end]).bigEndian
}
}
@@ -75,8 +75,15 @@ final class RemoteMp4Restructure {
}
self.audioData.append(data)
do {
let value = try self.mp4Restructure.checkIsOptimized(data: self.audioData)
if let value {
switch try self.mp4Restructure.checkIsOptimized(data: self.audioData) {
case .undetermined:
break // keep streaming until decision can be made
case .optimized:
self.audioData = Data()
self.task?.cancel()
self.task = nil
completion(.success(nil))
case let .needsRestructure(moovOffset):
guard response.response?.statusCode == 206 else {
Logger.error("⛔️ mp4 error: no moov before mdat and the stream is not seekable", category: .networking)
completion(.failure(Mp4RestructureError.nonOptimizedMp4AndServerCannotSeek))
@@ -86,22 +93,15 @@ final class RemoteMp4Restructure {
self.audioData = Data()
self.task?.cancel()
self.task = nil
self.fetchAndRestructureMoovAtom(offset: value.moovOffset) { result in
self.fetchAndRestructureMoovAtom(offset: moovOffset) { result in
switch result {
case let .success(value):
let data = value.data
let offset = value.offset
self.dataOptimized = true
completion(.success(RestructuredData(initialData: data, mdatOffset: offset)))
completion(.success(RestructuredData(initialData: value.data, mdatOffset: value.offset)))
case let .failure(error):
completion(.failure(Mp4RestructureError.networkError(error)))
}
}
} else {
self.audioData = Data()
self.task?.cancel()
self.task = nil
completion(.success(nil))
}
} catch {
completion(.failure(Mp4RestructureError.invalidAtomSize))
@@ -132,6 +132,8 @@ final class RemoteMp4Restructure {
}
}
// removed warmup range helper
private func urlForPartialContent(with url: URL, offset: Int) -> URLRequest {
var urlRequest = URLRequest(url: url)
urlRequest.networkServiceType = .avStreaming
@@ -31,7 +31,10 @@ open class AudioPlayer {
/// result in the audio being exhausted before it could fetch new data.
public var rate: Float {
get { rateNode.rate }
set { rateNode.rate = newValue }
set {
rateNode.rate = newValue
rateNode.bypass = (newValue == 1.0)
}
}
/// The player's current state.
@@ -90,6 +93,31 @@ open class AudioPlayer {
guard let entry = playingEntry else { return 0 }
return entry.framesPlayed
}
/// Indicates whether seeking is supported for the currently playing audio
///
/// Returns `false` if:
/// - The audio format doesn't support seeking (e.g., Ogg Vorbis streams)
/// - The stream has no valid duration (e.g., live radio streams)
///
/// Use this property to enable/disable seek controls in your UI
public var isSeekable: Bool {
guard playerContext.internalState != .pendingNext else { return true }
playerContext.entriesLock.lock()
let playingEntry = playerContext.audioPlayingEntry
playerContext.entriesLock.unlock()
guard let entry = playingEntry else { return true }
// Check if format supports seeking (Ogg Vorbis doesn't)
if entry.audioFileHint == kAudioFileOggType {
return false
}
// Check if stream has a valid duration (live streams don't)
let entryDuration = entry.duration()
return entryDuration > 0
}
public private(set) var customAttachedNodes = [AVAudioNode]()
@@ -176,6 +204,7 @@ open class AudioPlayer {
)
configPlayerContext()
configPlayerNode()
configureRateNode()
setupEngine()
}
@@ -510,6 +539,16 @@ open class AudioPlayer {
}
}
// Add this new method
private func configureRateNode() {
// Set overlap to a lower value for faster transitions (default is 8.0)
rateNode.overlap = 4.0
// Ensure pitch is not shifted
rateNode.pitch = 0
// Bypass by default since rate starts at 1.0
rateNode.bypass = true
}
/// Creates and configures an `AVAudioUnit` with an output configuration
/// and assigns it to the `player` variable.
private func configPlayerNode() {
@@ -651,18 +690,22 @@ open class AudioPlayer {
guard playerContext.internalState != .paused else { return }
let snapshot = playerContext.entriesLock.withLock {
(reading: playerContext.audioReadingEntry, playing: playerContext.audioPlayingEntry)
}
if playerContext.internalState == .pendingNext {
let entry = entriesQueue.dequeue(type: .upcoming)
playerContext.setInternalState(to: .waitingForData)
setCurrentReading(entry: entry, startPlaying: true, shouldClearQueue: true)
rendererContext.resetBuffers()
} else if let playingEntry = playerContext.audioPlayingEntry,
} else if let playingEntry = snapshot.playing,
playingEntry.seekRequest.requested,
playingEntry != playerContext.audioReadingEntry
playingEntry != snapshot.reading
{
playingEntry.audioStreamState.processedDataFormat = false
playingEntry.reset()
if let readingEntry = playerContext.audioReadingEntry {
if let readingEntry = snapshot.reading {
readingEntry.delegate = nil
readingEntry.close()
}
@@ -677,20 +720,20 @@ open class AudioPlayer {
setCurrentReading(entry: playingEntry, startPlaying: true, shouldClearQueue: false)
}
} else if playerContext.audioReadingEntry == nil {
} else if snapshot.reading == nil {
if entriesQueue.count(for: .upcoming) > 0 {
let entry = entriesQueue.dequeue(type: .upcoming)
let shouldStartPlaying = playerContext.audioPlayingEntry == nil
let shouldStartPlaying = snapshot.playing == nil
playerContext.setInternalState(to: .waitingForData)
setCurrentReading(entry: entry, startPlaying: shouldStartPlaying, shouldClearQueue: false)
} else if playerContext.audioPlayingEntry == nil {
} else if snapshot.playing == nil {
if playerContext.internalState != .stopped {
stopEngine(reason: .eof)
}
}
}
if let playingEntry = playerContext.audioPlayingEntry,
if let playingEntry = snapshot.playing,
playingEntry.audioStreamState.processedDataFormat,
playingEntry.calculatedBitrate() > 0.0
{
@@ -106,6 +106,7 @@ public enum AudioSystemError: LocalizedError, Equatable, Sendable {
case playerStartError
case fileStreamError(AudioFileStreamError)
case converterError(AudioConverterError)
case codecError
public var errorDescription: String? {
switch self {
@@ -116,6 +117,8 @@ public enum AudioSystemError: LocalizedError, Equatable, Sendable {
return "Audio file stream error'd: \(error)"
case let .converterError(error):
return "Audio converter error'd: \(error)"
case .codecError:
return "Audio codec error"
}
}
}
@@ -6,6 +6,7 @@
//
import AVFoundation
import CoreAudio
enum AudioConvertStatus: Int32 {
case done = 100
@@ -33,6 +34,13 @@ final class AudioFileStreamProcessor {
private let playerContext: AudioPlayerContext
private let rendererContext: AudioRendererContext
private let outputAudioFormat: AudioStreamBasicDescription
// Add Ogg Vorbis processor
private lazy var oggVorbisProcessor = OggVorbisStreamProcessor(
playerContext: playerContext,
rendererContext: rendererContext,
outputAudioFormat: outputAudioFormat
)
var audioFileStream: AudioFileStreamID?
var audioConverter: AudioConverterRef?
@@ -41,9 +49,12 @@ final class AudioFileStreamProcessor {
var currentFileFormat: String = ""
let fileFormatsForDelayedConverterCreation: Set = ["fa4m", "f4pm"]
// Track if we're processing Ogg Vorbis
private var isProcessingOggVorbis: Bool = false
var isFileStreamOpen: Bool {
audioFileStream != nil
audioFileStream != nil || isProcessingOggVorbis
}
init(playerContext: AudioPlayerContext,
@@ -53,6 +64,11 @@ final class AudioFileStreamProcessor {
self.playerContext = playerContext
self.rendererContext = rendererContext
self.outputAudioFormat = outputAudioFormat
// Set up Ogg Vorbis processor callback
oggVorbisProcessor.processorCallback = { [weak self] effect in
self?.fileStreamCallback?(effect)
}
}
/// Opens the `AudioFileStream`
@@ -62,12 +78,25 @@ final class AudioFileStreamProcessor {
/// - Returns: An `OSStatus` value indicating if an error occurred or not.
func openFileStream(with fileHint: AudioFileTypeID) -> OSStatus {
let data = UnsafeMutableRawPointer.from(object: self)
return AudioFileStreamOpen(data, _propertyListenerProc, _propertyPacketsProc, fileHint, &audioFileStream)
// Check if this is an Ogg Vorbis file
if fileHint == kAudioFileOggType {
isProcessingOggVorbis = true
return noErr
} else {
isProcessingOggVorbis = false
let data = UnsafeMutableRawPointer.from(object: self)
return AudioFileStreamOpen(data, _propertyListenerProc, _propertyPacketsProc, fileHint, &audioFileStream)
}
}
/// Closes the currently open `AudioFileStream` instance, if opened.
func closeFileStreamIfNeeded() {
if isProcessingOggVorbis {
isProcessingOggVorbis = false
oggVorbisProcessor.cleanup()
return
}
guard let fileStream = audioFileStream else {
Logger.debug("audio file stream not opened", category: .generic)
return
@@ -82,8 +111,14 @@ final class AudioFileStreamProcessor {
///
/// - Returns: An `OSStatus` value indicating if an error occurred or not.
func parseFileStreamBytes(data: Data) -> OSStatus {
guard let stream = audioFileStream else { return 0 }
guard !data.isEmpty else { return 0 }
// Check if we're processing Ogg Vorbis
if isProcessingOggVorbis {
return oggVorbisProcessor.parseOggVorbisData(data: data)
}
guard let stream = audioFileStream else { return 0 }
let flags: AudioFileStreamParseFlags = discontinuous ? .discontinuity : .init()
return data.withUnsafeBytes { buffer -> OSStatus in
AudioFileStreamParseBytes(stream, UInt32(buffer.count), buffer.baseAddress, flags)
@@ -91,10 +126,17 @@ final class AudioFileStreamProcessor {
}
func processSeek() {
guard let stream = audioFileStream else { return }
guard let readingEntry = playerContext.audioReadingEntry else {
return
}
// If processing Ogg Vorbis, use the Ogg Vorbis processor
if isProcessingOggVorbis {
oggVorbisProcessor.processSeek()
return
}
guard let stream = audioFileStream else { return }
guard readingEntry.calculatedBitrate() > 0.0 || (playerContext.audioPlayingEntry?.length ?? 0) > 0 else {
return
@@ -104,6 +146,8 @@ final class AudioFileStreamProcessor {
let dataLengthInBytes = Double(readingEntry.audioDataLengthBytes())
let entryDuration = readingEntry.duration()
let duration = entryDuration < readingEntry.progress && entryDuration > 0 ? readingEntry.progress : entryDuration
guard duration > 0.0 else { return }
var seekByteOffset = Int64(dataOffset + (readingEntry.seekRequest.time / duration) * dataLengthInBytes)
@@ -226,6 +270,8 @@ final class AudioFileStreamProcessor {
processDataByteCount(entry: entry, fileStream: fileStream)
case kAudioFileStreamProperty_AudioDataPacketCount:
processAudioDataPacketCount(entry: entry, fileStream: fileStream)
case kAudioFileStreamProperty_BitRate:
processBitRate(entry: entry, fileStream: fileStream)
case kAudioFileStreamProperty_ReadyToProducePackets:
// check converter for discontinuous stream
assignMagicCookieToConverterIfNeeded()
@@ -233,6 +279,8 @@ final class AudioFileStreamProcessor {
processReadyToProducePackets(entry: entry, fileStream: fileStream)
case kAudioFileStreamProperty_FormatList:
processFormatList(entry: entry, fileStream: fileStream)
case kAudioFileStreamProperty_PacketTableInfo:
processPacketTableInfo(entry: entry, fileStream: fileStream)
default:
break
}
@@ -336,28 +384,56 @@ final class AudioFileStreamProcessor {
entry.audioStreamState.dataPacketOffset = audioDataPacketCount
}
private func processFormatList(entry: AudioEntry, fileStream: AudioFileStreamID) {
private func processBitRate(entry: AudioEntry, fileStream: AudioFileStreamID) {
var bitRate: UInt32 = 0
let status = fileStreamGetProperty(value: &bitRate, fileStream: fileStream, propertyId: kAudioFileStreamProperty_BitRate)
guard status == noErr else { return }
entry.lock.lock(); defer { entry.lock.unlock() }
entry.audioStreamState.bitRate = Double(bitRate)
}
private func processPacketTableInfo(entry: AudioEntry, fileStream: AudioFileStreamID) {
var pti = AudioFilePacketTableInfo(mNumberValidFrames: 0,
mPrimingFrames: 0,
mRemainderFrames: 0)
let status = fileStreamGetProperty(value: &pti, fileStream: fileStream, propertyId: kAudioFileStreamProperty_PacketTableInfo)
guard status == noErr else { return }
// Use valid frames to refine duration if present
entry.lock.lock(); defer { entry.lock.unlock() }
if pti.mNumberValidFrames > 0 {
entry.audioStreamState.dataPacketCount = Double(pti.mNumberValidFrames) / Double(max(1, entry.audioStreamFormat.mFramesPerPacket))
}
}
private func processFormatList(entry: AudioEntry, fileStream: AudioFileStreamID) {
let info = fileStreamGetPropertyInfo(fileStream: fileStream, propertyId: kAudioFileStreamProperty_FormatList)
guard info.status == noErr else { return }
var list: [AudioFormatListItem] = Array(repeating: AudioFormatListItem(), count: Int(info.size))
var size = UInt32(info.size)
guard info.status == noErr, info.size > 0 else { return }
let itemStride = MemoryLayout<AudioFormatListItem>.stride
let itemCount = Int(info.size) / itemStride
guard itemCount > 0 else { return }
var list = [AudioFormatListItem](repeating: AudioFormatListItem(), count: itemCount)
var size = UInt32(itemCount * itemStride)
AudioFileStreamGetProperty(fileStream, kAudioFileStreamProperty_FormatList, &size, &list)
let step = MemoryLayout<AudioFormatListItem>.size
var i = 0
while i * step < size {
var chosenASBD: AudioStreamBasicDescription?
for i in 0..<itemCount {
let asbd = list[i].mASBD
let formatId = asbd.mFormatID
if formatId == kAudioFormatMPEG4AAC_HE || formatId == kAudioFormatMPEG4AAC_HE_V2 {
playerContext.audioReadingEntry?.audioStreamFormat = asbd
chosenASBD = asbd
break
}
i += step
if chosenASBD == nil {
chosenASBD = asbd
}
}
if fileFormatsForDelayedConverterCreation.contains(currentFileFormat) {
if let inputStreamFormat = playerContext.audioReadingEntry?.audioStreamFormat {
createAudioConverter(from: inputStreamFormat, to: outputAudioFormat)
if let asbd = chosenASBD {
entry.lock.withLock { entry.audioStreamFormat = asbd }
if fileFormatsForDelayedConverterCreation.contains(currentFileFormat) {
createAudioConverter(from: asbd, to: outputAudioFormat)
}
}
}
@@ -0,0 +1,489 @@
//
// OggVorbisStreamProcessor.swift
// AudioStreaming
//
// Created on 25/10/2025.
//
import Foundation
import AVFoundation
import CoreAudio
/// A processor for Ogg Vorbis audio streams using libvorbisfile
final class OggVorbisStreamProcessor {
/// The callback to notify when processing is complete or an error occurs
var processorCallback: ((FileStreamProcessorEffect) -> Void)?
// MARK: - Constants
/// Correction factor for Ogg container overhead in bitrate-based duration calculation.
/// Ogg containers add 3-4% overhead (page headers, packet headers, metadata).
/// The nominal bitrate only accounts for audio data, not container overhead.
/// By reducing the bitrate slightly, we increase the calculated duration to match reality.
private let oggContainerOverheadFactor: Double = 0.96 // 4% overhead
/// Fallback bitrate estimates when nominal bitrate is unavailable
private let fallbackBitrateStereo: Double = 160_000 // 160 kbps for stereo
private let fallbackBitrateMono: Double = 96_000 // 96 kbps for mono
// MARK: - Properties
private let playerContext: AudioPlayerContext
private let rendererContext: AudioRendererContext
private let outputAudioFormat: AudioStreamBasicDescription
private let vfDecoder = VorbisFileDecoder()
private var isInitialized = false
// Audio converter for format conversion
private var audioConverter: AVAudioConverter?
// Buffer for PCM conversion
private var pcmBuffer: AVAudioPCMBuffer?
private let frameCount = 1024
// Seeking state (currently unused - seeking not fully supported)
// Future enhancement: implement proper seeking for local files
// Debug logging
private var totalFramesProcessed = 0
private var dataChunkCount = 0
// MARK: - Initialization
/// Initialize the OggVorbisStreamProcessor
/// - Parameters:
/// - playerContext: The audio player context
/// - rendererContext: The audio renderer context
/// - outputAudioFormat: The output audio format
init(playerContext: AudioPlayerContext,
rendererContext: AudioRendererContext,
outputAudioFormat: AudioStreamBasicDescription) {
self.playerContext = playerContext
self.rendererContext = rendererContext
self.outputAudioFormat = outputAudioFormat
}
deinit {
cleanup()
}
/// Clean up all resources and reset state
func cleanup() {
cleanupBuffers()
audioConverter = nil
// Destroy and reset the decoder
vfDecoder.destroy()
isInitialized = false
totalFramesProcessed = 0
}
// MARK: - Data Processing
/// Parse Ogg Vorbis data
/// - Parameter data: The Ogg Vorbis data to parse
/// - Returns: An OSStatus indicating success or failure
func parseOggVorbisData(data: Data) -> OSStatus {
guard let entry = playerContext.audioReadingEntry else { return 0 }
dataChunkCount += 1
if !isInitialized {
vfDecoder.create(capacityBytes: 2_097_152)
isInitialized = true
totalFramesProcessed = 0
}
vfDecoder.push(data)
if !entry.audioStreamState.processedDataFormat {
let availableBytes = vfDecoder.availableBytes()
if availableBytes >= 16384 {
do {
try vfDecoder.openIfNeeded()
if vfDecoder.sampleRate > 0 && vfDecoder.channels > 0 {
setupAudioFormat()
if pcmBuffer == nil, let processingFormat = vfDecoder.processingFormat {
pcmBuffer = AVAudioPCMBuffer(pcmFormat: processingFormat, frameCapacity: UInt32(frameCount))
}
}
} catch {
return noErr
}
} else {
return noErr
}
}
guard entry.audioStreamState.processedDataFormat else {
return noErr
}
// Handle seek requests
if let playingEntry = playerContext.audioPlayingEntry,
playingEntry.seekRequest.requested, playingEntry.calculatedBitrate() > 0 {
// This is the correct usage of .processSource - only for seek requests
processorCallback?(.processSource)
if rendererContext.waiting.value {
rendererContext.packetsSemaphore.signal()
}
return noErr
}
// Decode frames continuously - matching AudioFileStreamProcessor behavior
// Wait for renderer buffer space if needed, just like regular audio processing
var consecutiveNoFrames = 0
var totalDecoded = 0
decodeLoop: while true {
// Check player state
if playerContext.internalState == .disposed
|| playerContext.internalState == .pendingNext
|| playerContext.internalState == .stopped {
break
}
// Check if there's space in the buffer
rendererContext.lock.lock()
let totalFrames = rendererContext.bufferContext.totalFrameCount
let usedFrames = rendererContext.bufferContext.frameUsedCount
rendererContext.lock.unlock()
guard usedFrames <= totalFrames else {
break decodeLoop
}
var framesLeft = totalFrames - usedFrames
if framesLeft == 0 {
while true {
rendererContext.lock.lock()
let totalFrames = rendererContext.bufferContext.totalFrameCount
let usedFrames = rendererContext.bufferContext.frameUsedCount
rendererContext.lock.unlock()
if usedFrames > totalFrames {
break decodeLoop
}
framesLeft = totalFrames - usedFrames
if framesLeft > 0 {
break
}
if playerContext.internalState == .disposed
|| playerContext.internalState == .pendingNext
|| playerContext.internalState == .stopped {
break decodeLoop
}
if let playingEntry = playerContext.audioPlayingEntry,
playingEntry.seekRequest.requested, playingEntry.calculatedBitrate() > 0 {
processorCallback?(.processSource)
if rendererContext.waiting.value {
rendererContext.packetsSemaphore.signal()
}
break decodeLoop
}
rendererContext.waiting.write { $0 = true }
rendererContext.packetsSemaphore.wait()
rendererContext.waiting.write { $0 = false }
}
}
let availableBytes = vfDecoder.availableBytes()
if availableBytes < 4096 {
consecutiveNoFrames += 1
if consecutiveNoFrames >= 3 {
break decodeLoop
}
continue
}
let status = decodeAndFillBuffer()
if status != noErr {
consecutiveNoFrames += 1
if consecutiveNoFrames >= 3 {
break decodeLoop
}
} else {
consecutiveNoFrames = 0
totalDecoded += 1
}
}
if totalDecoded > 0 && rendererContext.waiting.value {
rendererContext.packetsSemaphore.signal()
}
return noErr
}
/// Decode audio and fill the renderer buffer
/// - Returns: noErr if frames were decoded, otherwise an error/no-data status
private func decodeAndFillBuffer() -> OSStatus {
guard let pcmBuffer = pcmBuffer else {
return OSStatus(-1)
}
let framesRead = vfDecoder.readFrames(into: pcmBuffer, frameCount: frameCount)
if framesRead <= 0 {
return OSStatus(-1)
}
pcmBuffer.frameLength = UInt32(framesRead)
processDecodedAudio(pcmBuffer: pcmBuffer, framesRead: framesRead)
totalFramesProcessed += framesRead
return noErr
}
// MARK: - Audio Format Setup
// Setup audio format using the processingFormat from VorbisFileDecoder
private func setupAudioFormat() {
guard let entry = playerContext.audioReadingEntry,
let processingFormat = vfDecoder.processingFormat else { return }
entry.lock.lock()
// Use the decoder's deinterleaved format directly
var asbd = processingFormat.streamDescription.pointee
// Store the format in the entry
entry.audioStreamFormat = asbd
entry.sampleRate = Float(vfDecoder.sampleRate)
entry.packetDuration = Double(1) / Double(vfDecoder.sampleRate)
// For streaming Ogg files, totalPcmSamples may not be available (returns error code)
// In that case, use bitrate-based duration calculation with container overhead correction
if vfDecoder.totalPcmSamples > 0 {
// We have total samples - use packet offset for accurate duration
entry.audioStreamState.dataPacketOffset = UInt64(vfDecoder.totalPcmSamples)
} else {
// Streaming - use bitrate for duration estimation
if vfDecoder.nominalBitrate > 0 {
entry.audioStreamState.bitRate = Double(vfDecoder.nominalBitrate) * oggContainerOverheadFactor
} else {
// Fallback: use typical bitrates for Vorbis quality
let estimatedBitrate = vfDecoder.channels == 2 ? fallbackBitrateStereo : fallbackBitrateMono
entry.audioStreamState.bitRate = estimatedBitrate * oggContainerOverheadFactor
}
}
entry.audioStreamState.processedDataFormat = true
entry.audioStreamState.readyForDecoding = true
entry.lock.unlock()
// Create audio converter from decoder format to output format
createAudioConverter(from: processingFormat, to: outputAudioFormat)
}
/// Create audio converter from decoder format to output format
private func createAudioConverter(from sourceFormat: AVAudioFormat, to destFormat: AudioStreamBasicDescription) {
audioConverter = nil
var dest = destFormat
guard let destAVFormat = AVAudioFormat(streamDescription: &dest) else {
Logger.error("Failed to create output AVAudioFormat", category: .audioRendering)
return
}
guard let converter = AVAudioConverter(from: sourceFormat, to: destAVFormat) else {
Logger.error("Failed to create AVAudioConverter", category: .audioRendering)
return
}
audioConverter = converter
}
// MARK: - Audio Processing
/// Process decoded audio using AVAudioConverter
/// - Parameters:
/// - pcmBuffer: The PCM buffer containing decoded audio
/// - framesRead: Number of frames read
private func processDecodedAudio(pcmBuffer: AVAudioPCMBuffer, framesRead: Int) {
guard let entry = playerContext.audioReadingEntry,
let converter = audioConverter else { return }
// Set the input buffer's frame length
pcmBuffer.frameLength = UInt32(framesRead)
// Create output buffer with converter's output format
guard let outputBuffer = AVAudioPCMBuffer(
pcmFormat: converter.outputFormat,
frameCapacity: UInt32(framesRead)
) else { return }
// Process through AudioConverter
rendererContext.lock.lock()
let bufferContext = rendererContext.bufferContext
let used = bufferContext.frameUsedCount
let totalFrames = bufferContext.totalFrameCount
let end = (bufferContext.frameStartIndex + bufferContext.frameUsedCount) % bufferContext.totalFrameCount
rendererContext.lock.unlock()
guard used <= totalFrames else {
return
}
var framesLeft = totalFrames - used
// Wait for buffer space if needed
if framesLeft == 0 {
while true {
rendererContext.lock.lock()
let currentUsed = rendererContext.bufferContext.frameUsedCount
let currentTotal = rendererContext.bufferContext.totalFrameCount
rendererContext.lock.unlock()
if currentUsed > currentTotal {
return
}
framesLeft = currentTotal - currentUsed
if framesLeft > 0 {
break
}
if playerContext.internalState == .disposed
|| playerContext.internalState == .pendingNext
|| playerContext.internalState == .stopped {
return
}
if let playingEntry = playerContext.audioPlayingEntry,
playingEntry.seekRequest.requested, playingEntry.calculatedBitrate() > 0 {
processorCallback?(.processSource)
if rendererContext.waiting.value {
rendererContext.packetsSemaphore.signal()
}
return
}
rendererContext.waiting.write { $0 = true }
rendererContext.packetsSemaphore.wait()
rendererContext.waiting.write { $0 = false }
}
}
var error: NSError?
var inputConsumed = false
let status = converter.convert(to: outputBuffer, error: &error) { inNumPackets, outStatus in
if inputConsumed {
outStatus.pointee = .noDataNow
return nil
}
inputConsumed = true
outStatus.pointee = .haveData
return pcmBuffer
}
guard status != .error, outputBuffer.frameLength > 0 else {
return
}
rendererContext.lock.lock()
let start = rendererContext.bufferContext.frameStartIndex
let currentEnd = (rendererContext.bufferContext.frameStartIndex + rendererContext.bufferContext.frameUsedCount) % rendererContext.bufferContext.totalFrameCount
let totalFrameCount = rendererContext.bufferContext.totalFrameCount
let currentUsed = rendererContext.bufferContext.frameUsedCount
rendererContext.lock.unlock()
// Calculate actual space available
let actualFramesLeft = totalFrameCount - currentUsed
let framesToCopy = min(UInt32(outputBuffer.frameLength), actualFramesLeft)
guard let sourceData = outputBuffer.audioBufferList.pointee.mBuffers.mData?.assumingMemoryBound(to: UInt8.self) else { return }
let bytesPerFrame = Int(rendererContext.bufferContext.sizeInBytes)
let destData = rendererContext.audioBuffer.mData?.assumingMemoryBound(to: UInt8.self)
if currentEnd >= start {
// Ring buffer wraps
let framesToEnd = totalFrameCount - currentEnd
let firstChunkFrames = min(framesToCopy, framesToEnd)
let firstChunkBytes = Int(firstChunkFrames) * bytesPerFrame
let firstChunkOffset = Int(currentEnd) * bytesPerFrame
// Copy first chunk to end of buffer
memcpy(destData?.advanced(by: firstChunkOffset), sourceData, firstChunkBytes)
// Copy second chunk to start of buffer if needed
if firstChunkFrames < framesToCopy {
let secondChunkFrames = framesToCopy - firstChunkFrames
let secondChunkBytes = Int(secondChunkFrames) * bytesPerFrame
memcpy(destData, sourceData.advanced(by: firstChunkBytes), secondChunkBytes)
}
} else {
// No wrap
let chunkBytes = Int(framesToCopy) * bytesPerFrame
let offset = Int(currentEnd) * bytesPerFrame
memcpy(destData?.advanced(by: offset), sourceData, chunkBytes)
}
fillUsedFrames(framesCount: framesToCopy)
updateProcessedPackets(inNumberPackets: framesToCopy)
}
/// Process a seek request
///
/// Seeking is not supported for Ogg Vorbis streams.
/// For HTTP streams, seeking is extremely difficult because:
/// 1. Need to find Ogg page boundaries
/// 2. Need Vorbis headers to initialize decoder
/// 3. Headers are only at the beginning of the file
///
/// Note: Future enhancement could support seeking in local files
/// by fetching headers and using libvorbisfile's built-in seeking.
func processSeek() {
// Seeking not supported - UI should check AudioPlayer.isSeekable
}
// MARK: - Helper Methods
/// Update the processed packets information
/// - Parameter inNumberPackets: The number of packets processed
private func updateProcessedPackets(inNumberPackets: UInt32) {
guard let readingEntry = playerContext.audioReadingEntry else { return }
let processedPackCount = readingEntry.processedPacketsState.count
let maxPackets = 4096
if processedPackCount < maxPackets {
let count = min(Int(inNumberPackets), maxPackets - Int(processedPackCount))
let packetSize: UInt32 = UInt32(readingEntry.audioStreamFormat.mBytesPerFrame)
readingEntry.lock.lock()
readingEntry.processedPacketsState.sizeTotal += (packetSize * UInt32(count))
readingEntry.processedPacketsState.count += UInt32(count)
readingEntry.lock.unlock()
}
}
/// Advances the processed frames for buffer and reading entry
/// - Parameter frameCount: The number of frames to advance
@inline(__always)
private func fillUsedFrames(framesCount: UInt32) {
rendererContext.lock.lock()
rendererContext.bufferContext.frameUsedCount += framesCount
rendererContext.lock.unlock()
playerContext.audioReadingEntry?.lock.lock()
playerContext.audioReadingEntry?.framesState.queued += Int(framesCount)
playerContext.audioReadingEntry?.lock.unlock()
}
/// Clean up allocated buffers
private func cleanupBuffers() {
pcmBuffer = nil
}
}
@@ -7,6 +7,9 @@ import AudioToolbox
import Foundation
/// mapping from mime types to `AudioFileTypeID`
// Custom file type for Ogg Vorbis
let kAudioFileOggType: AudioFileTypeID = 0x6F676720 // 'ogg '
let fileTypesFromMimeType: [String: AudioFileTypeID] =
[
"audio/mp3": kAudioFileMP3Type,
@@ -33,7 +36,10 @@ let fileTypesFromMimeType: [String: AudioFileTypeID] =
"video/3gpp": kAudioFile3GPType,
"audio/3gp2": kAudioFile3GP2Type,
"video/3gp2": kAudioFile3GP2Type,
"audio/flac": kAudioFileFLACType
"audio/flac": kAudioFileFLACType,
"audio/ogg": kAudioFileOggType,
"audio/vorbis": kAudioFileOggType,
"application/ogg": kAudioFileOggType
]
/// Method that converts mime type to AudioFileTypeID
@@ -58,6 +64,8 @@ let fileTypesFromFileExtension: [String: AudioFileTypeID] =
"ac3": kAudioFileAC3Type,
"3gp": kAudioFile3GPType,
"flac": kAudioFileFLACType,
"ogg": kAudioFileOggType,
"oga": kAudioFileOggType,
]
func audioFileType(fileExtension: String) -> AudioFileTypeID {
@@ -13,8 +13,6 @@ import XCTest
class MetadataStreamProcessorTests: XCTestCase {
var metadataDelegateSpy = MetadataDelegateSpy()
let bundle = Bundle(for: MetadataStreamProcessorTests.self)
func test_Processor_SendsCorrectValues_IfItCanProcessMetadata() throws {
let parser = MetadataParser()
let processor = MetadataStreamProcessor(parser: parser.eraseToAnyParser())
@@ -36,7 +34,7 @@ class MetadataStreamProcessorTests: XCTestCase {
}
func test_Processor_Outputs_Correct_Metadata_ForStep_WithEmptyMetadata() throws {
let url = bundle.url(forResource: "raw-stream-audio-empty-metadata", withExtension: nil)!
let url = Bundle.module.url(forResource: "raw-audio-streams/raw-stream-audio-empty-metadata", withExtension: nil)!
let data = try Data(contentsOf: url)
@@ -54,7 +52,7 @@ class MetadataStreamProcessorTests: XCTestCase {
}
func test_Processor_Outputs_Correct_Metadata_ForStep_WithMetadata() throws {
let url = bundle.url(forResource: "raw-stream-audio-normal-metadata", withExtension: nil)!
let url = Bundle.module.url(forResource: "raw-audio-streams/raw-stream-audio-normal-metadata", withExtension: nil)!
let data = try Data(contentsOf: url)
@@ -72,7 +70,7 @@ class MetadataStreamProcessorTests: XCTestCase {
}
func test_Processor_Outputs_Correct_Metadata_ForStep_WithMetadata_Alt() throws {
let url = bundle.url(forResource: "raw-stream-audio-normal-metadata-alt", withExtension: nil)!
let url = Bundle.module.url(forResource: "raw-audio-streams/raw-stream-audio-normal-metadata-alt", withExtension: nil)!
let data = try Data(contentsOf: url)
@@ -94,7 +92,7 @@ class MetadataStreamProcessorTests: XCTestCase {
}
func test_Processor_Outputs_Correct_Metadata_ForStep_NoMetadata() throws {
let url = bundle.url(forResource: "raw-stream-audio-no-metadata", withExtension: nil)!
let url = Bundle.module.url(forResource: "raw-audio-streams/raw-stream-audio-no-metadata", withExtension: nil)!
let data = try Data(contentsOf: url)
+24
View File
@@ -0,0 +1,24 @@
{
"originHash" : "68edf8cb05464ad5cb63fbf7d9ab8c54c5bfd1afcf71a774b97ff4f35f7e3fd1",
"pins" : [
{
"identity" : "ogg-binary-xcframework",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sbooth/ogg-binary-xcframework",
"state" : {
"revision" : "c0e822e18738ad913864e98d9614927ac1e9337c",
"version" : "0.1.2"
}
},
{
"identity" : "vorbis-binary-xcframework",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sbooth/vorbis-binary-xcframework",
"state" : {
"revision" : "842020eabcebe410e698c68545d6597b2d232e51",
"version" : "0.1.2"
}
}
],
"version" : 3
}
+39 -9
View File
@@ -1,24 +1,55 @@
// swift-tools-version:5.9
// swift-tools-version:5.10
import PackageDescription
let package = Package(
name: "AudioStreaming",
platforms: [
.iOS(.v12),
.iOS(.v15),
.macOS(.v13),
.tvOS(.v16)
],
products: [
.library(
name: "AudioStreaming",
targets: ["AudioStreaming"]
targets: ["AudioCodecs", "AudioStreaming"]
),
],
dependencies: [
.package(url: "https://github.com/sbooth/ogg-binary-xcframework", exact: "0.1.2"),
.package(url: "https://github.com/sbooth/vorbis-binary-xcframework", exact: "0.1.2")
],
targets: [
// C target for audio codec bridges
.target(
name: "AudioCodecs",
dependencies: [
.product(name: "ogg", package: "ogg-binary-xcframework"),
.product(name: "vorbis", package: "vorbis-binary-xcframework")
],
path: "AudioCodecs",
publicHeadersPath: "include",
cSettings: [
.headerSearchPath("."),
.headerSearchPath("include")
],
linkerSettings: [
.linkedFramework("AudioToolbox"),
.linkedFramework("Foundation")
]
),
// Main Swift target
.target(
name: "AudioStreaming",
path: "AudioStreaming"
dependencies: [
"AudioCodecs",
.product(name: "ogg", package: "ogg-binary-xcframework"),
.product(name: "vorbis", package: "vorbis-binary-xcframework")
],
path: "AudioStreaming",
exclude: ["AudioStreaming.h", "Streaming/OggVorbis", "Info.plist"],
swiftSettings: []
),
.testTarget(
name: "AudioStreamingTests",
@@ -26,12 +57,11 @@ let package = Package(
"AudioStreaming"
],
path: "AudioStreamingTests",
exclude: ["Info.plist", "Streaming/output"],
resources: [
.copy("Streaming/Metadata Stream Processor/raw-audio-streams/raw-stream-audio-empty-metadata"),
.copy("Streaming/Metadata Stream Processor/raw-audio-streams/raw-stream-audio-no-metadata"),
.copy("Streaming/Metadata Stream Processor/raw-audio-streams/raw-stream-audio-normal-metadata"),
.copy("Streaming/Metadata Stream Processor/raw-audio-streams/raw-stream-audio-normal-metadata-alt")
]
// Test resources for metadata stream processor tests
.copy("Streaming/Metadata Stream Processor/raw-audio-streams")
]
)
]
)
+39 -5
View File
@@ -8,16 +8,23 @@ 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
- M4A (optimized and non-optimized) from v1.2.0
- **Ogg Vorbis** (both local and remote files) ✨
As of 1.2.0 version, there's support for non-optimized M4A, please report any issues
#### Known limitations
Known limitations:
~~- As described above non-optimised M4A files are not supported this is a limitation of [AudioFileStream Services](https://developer.apple.com/documentation/audiotoolbox/audio_file_stream_services?language=swift)~~
**Ogg Vorbis Seeking:**
- Seeking is **not supported** for Ogg Vorbis files in the current release
- This is due to technical challenges with the Ogg container format over HTTP streaming:
- Seeking requires finding precise Ogg page boundaries in the stream
- The Vorbis decoder needs the full headers (identification, comment, and setup packets) to initialize, which are only available at the beginning of the file
- HTTP range requests need to be carefully orchestrated to fetch headers and seek to the correct position
- Your UI can check `player.isSeekable` to determine if seeking is available for the currently playing file
- Future releases may add experimental support for seeking using progressive download or intelligent header caching
# Requirements
- iOS 13.0+
- iOS 15.0+
- macOS 13.0+
- tvOS 16.0+
- Swift 5.x
@@ -170,6 +177,33 @@ Under the hood the concrete class for frame filters, `FrameFilterProcessor` inst
On Xcode 11.0+ you can add a new dependency by going to **File / Swift Packages / Add Package Dependency...**
and enter package repository URL https://github.com/dimitris-c/AudioStreaming.git, then follow the instructions.
# Development
### Testing
This package uses Swift Package Manager for development and testing. To run tests:
```bash
# Run all tests
swift test
# Run tests in parallel for faster execution
swift test --parallel
# Build the package
swift build
```
### Opening in Xcode
You can open the package directly in Xcode:
```bash
open Package.swift
```
Or simply double-click the `Package.swift` file. Xcode will automatically resolve dependencies and make the package ready for development.
# Licence
AudioStreaming is available under the MIT license. See the LICENSE file for more info.