diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 5598138..70bfbfa 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -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 17"] 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 diff --git a/.gitignore b/.gitignore index be35341..7232f78 100644 --- a/.gitignore +++ b/.gitignore @@ -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 # diff --git a/AudioCodecs/VorbisFileBridge.c b/AudioCodecs/VorbisFileBridge.c new file mode 100644 index 0000000..3645eb5 --- /dev/null +++ b/AudioCodecs/VorbisFileBridge.c @@ -0,0 +1,321 @@ +#include "include/VorbisFileBridge.h" + +#include +#include +#include +#include + +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); +} diff --git a/AudioCodecs/include/AudioCodecs.h b/AudioCodecs/include/AudioCodecs.h new file mode 100644 index 0000000..ee189d9 --- /dev/null +++ b/AudioCodecs/include/AudioCodecs.h @@ -0,0 +1,13 @@ +// +// AudioCodecs.h +// AudioStreaming +// +// Created on 25/10/2025. +// + +#ifndef AudioCodecs_h +#define AudioCodecs_h + +#import "VorbisFileBridge.h" + +#endif /* AudioCodecs_h */ diff --git a/AudioCodecs/include/VorbisFileBridge.h b/AudioCodecs/include/VorbisFileBridge.h new file mode 100644 index 0000000..b8f967a --- /dev/null +++ b/AudioCodecs/include/VorbisFileBridge.h @@ -0,0 +1,58 @@ +#ifndef VORBIS_FILE_BRIDGE_H +#define VORBIS_FILE_BRIDGE_H + +#include +#include + +// 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 \ No newline at end of file diff --git a/AudioCodecs/include/module.modulemap b/AudioCodecs/include/module.modulemap new file mode 100644 index 0000000..92d6ff4 --- /dev/null +++ b/AudioCodecs/include/module.modulemap @@ -0,0 +1,4 @@ +module AudioCodecs { + umbrella header "AudioCodecs.h" + export * +} diff --git a/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj b/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj index b98978a..5c72d10 100644 --- a/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj +++ b/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj @@ -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 = ""; }; 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 = ""; }; 984DE9542BDAE59C004B427A /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = ""; }; 984DE9562BDAFC7E004B427A /* AudioPlayerControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerControlsView.swift; sourceTree = ""; }; + 9881693D2EBCDB0100CE7EFF /* hipjazz.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = hipjazz.ogg; sourceTree = ""; }; 989E08E62BF7A4E300599F17 /* PrefersTabNavigationEnvironmentKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefersTabNavigationEnvironmentKey.swift; sourceTree = ""; }; 98BFB4192BC97AF800E812C0 /* DisplayLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayLink.swift; sourceTree = ""; }; 98BFB41B2BCAAD8A00E812C0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; @@ -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 */; } diff --git a/AudioStreaming.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/AudioPlayer/AudioPlayer.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from AudioStreaming.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to AudioPlayer/AudioPlayer.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/AudioPlayer/AudioPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AudioPlayer/AudioPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..4c0e7e8 --- /dev/null +++ b/AudioPlayer/AudioPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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 +} diff --git a/AudioPlayer/AudioPlayer/Common/AudioContent.swift b/AudioPlayer/AudioPlayer/Common/AudioContent.swift index be02110..35e1b0e 100644 --- a/AudioPlayer/AudioPlayer/Common/AudioContent.swift +++ b/AudioPlayer/AudioPlayer/Common/AudioContent.swift @@ -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)! } diff --git a/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerModel.swift b/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerModel.swift index 3637b08..9f5a442 100644 --- a/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerModel.swift +++ b/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerModel.swift @@ -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] { diff --git a/AudioPlayer/AudioPlayer/Dependencies/AudioPlayerService.swift b/AudioPlayer/AudioPlayer/Dependencies/AudioPlayerService.swift index b61d383..3c56949 100644 --- a/AudioPlayer/AudioPlayer/Dependencies/AudioPlayerService.swift +++ b/AudioPlayer/AudioPlayer/Dependencies/AudioPlayerService.swift @@ -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) } diff --git a/AudioPlayer/AudioPlayer/Resources/hipjazz.ogg b/AudioPlayer/AudioPlayer/Resources/hipjazz.ogg new file mode 100644 index 0000000..e59f7bd Binary files /dev/null and b/AudioPlayer/AudioPlayer/Resources/hipjazz.ogg differ diff --git a/AudioStreaming.xcodeproj/project.pbxproj b/AudioStreaming.xcodeproj/project.pbxproj deleted file mode 100644 index b2a0d50..0000000 --- a/AudioStreaming.xcodeproj/project.pbxproj +++ /dev/null @@ -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 = ""; }; - 98C82AE52B8CA8BC00AED485 /* RemoteMp4Restructure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMp4Restructure.swift; sourceTree = ""; }; - 98CC396D28BD651E006C9FF9 /* Atomic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; - 98DC00CB2B961F5E0068900A /* ByteBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ByteBuffer.swift; sourceTree = ""; }; - 98DC00CD2B9726380068900A /* ByteBufferTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ByteBufferTests.swift; sourceTree = ""; }; - B500731F24D00BAC00BB4475 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; - B514657E248E3884005C03F7 /* DispatchTimerSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchTimerSource.swift; sourceTree = ""; }; - B51B9F9924DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAudioFormat+Convenience.swift"; sourceTree = ""; }; - B51FE0BF2488F67C00F2A4D2 /* Queue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Queue.swift; sourceTree = ""; }; - B51FE0C12488F96A00F2A4D2 /* QueueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueTests.swift; sourceTree = ""; }; - B51FE0C3248905B400F2A4D2 /* PlayerQueueEntries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueEntries.swift; sourceTree = ""; }; - B51FE0C724892D1600F2A4D2 /* PlayerQueueEntriesTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueEntriesTest.swift; sourceTree = ""; }; - B5276B6E247D21A000D2F56A /* NetworkingClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkingClient.swift; sourceTree = ""; }; - B5276B71247D4D5B00D2F56A /* BiMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiMap.swift; sourceTree = ""; }; - B5276B73247D4D9F00D2F56A /* NetworkSessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSessionDelegate.swift; sourceTree = ""; }; - B54C3E55255F286D00B356F2 /* Retrier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Retrier.swift; sourceTree = ""; }; - B54D876C2490E4A000C361A0 /* UnitDescriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitDescriptions.swift; sourceTree = ""; }; - B54D876E2490E4DD00C361A0 /* AudioRendererContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRendererContext.swift; sourceTree = ""; }; - B55A736B247FCB420050C53D /* HTTPHeaderParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeaderParser.swift; sourceTree = ""; }; - B55CE96D248058B60001C498 /* MetadataParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataParser.swift; sourceTree = ""; }; - B55CE97024810DE20001C498 /* MetadataStreamProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataStreamProcessor.swift; sourceTree = ""; }; - B55CE97724813BCA0001C498 /* UnsafeMutablePointer+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UnsafeMutablePointer+Helpers.swift"; sourceTree = ""; }; - B55CEAB32485107C0001C498 /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = ""; }; - B55CEAB72485172D0001C498 /* HTTPHeaderParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeaderParserTests.swift; sourceTree = ""; }; - B55CEAB9248530C00001C498 /* MetadataParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataParser.swift; sourceTree = ""; }; - B55CEABB24853CD20001C498 /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = ""; }; - B55F77CE24D82ADE0057F431 /* AudioPlayerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerDelegate.swift; sourceTree = ""; }; - B55F77D024D82CD50057F431 /* AVAudioUnit+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAudioUnit+Convenience.swift"; sourceTree = ""; }; - B55F77D524DACE140057F431 /* BufferContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BufferContext.swift; sourceTree = ""; }; - B5667A8F2499018D00D93F85 /* AudioFileStreamProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFileStreamProcessor.swift; sourceTree = ""; }; - B5667A912499063D00D93F85 /* AudioPlayerContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerContext.swift; sourceTree = ""; }; - B5667B3D249BC43000D93F85 /* AudioPlayerRenderProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerRenderProcessor.swift; sourceTree = ""; }; - B57829CE2548B32B00C78D36 /* Lock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lock.swift; sourceTree = ""; }; - B580CB1D25628CF4006D7DD8 /* AudioStreaming.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = AudioStreaming.podspec; sourceTree = ""; }; - B580CB1E25628CF4006D7DD8 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; - B580CB1F25628D09006D7DD8 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; - B58386372544A2C10087A712 /* EntryFrames.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryFrames.swift; sourceTree = ""; }; - B583863F254584A50087A712 /* ProcessedPackets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessedPackets.swift; sourceTree = ""; }; - B5838643254584BE0087A712 /* AudioStreamState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioStreamState.swift; sourceTree = ""; }; - B5838647254584D90087A712 /* SeekRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeekRequest.swift; sourceTree = ""; }; - B592E12825460146008866FB /* BiMapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiMapTests.swift; sourceTree = ""; }; - B592E133254608B4008866FB /* DispatchTimerSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchTimerSourceTests.swift; sourceTree = ""; }; - B59CB46B25420B4D00F8CAD0 /* MetadataStreamProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataStreamProcessorTests.swift; sourceTree = ""; }; - B59CB4B225421D8200F8CAD0 /* raw-stream-audio-empty-metadata */ = {isa = PBXFileReference; lastKnownFileType = file; path = "raw-stream-audio-empty-metadata"; sourceTree = ""; }; - B59CB4BA25421F3500F8CAD0 /* raw-stream-audio-normal-metadata */ = {isa = PBXFileReference; lastKnownFileType = file; path = "raw-stream-audio-normal-metadata"; sourceTree = ""; }; - B59CB4C525421FD400F8CAD0 /* raw-stream-audio-no-metadata */ = {isa = PBXFileReference; lastKnownFileType = file; path = "raw-stream-audio-no-metadata"; sourceTree = ""; }; - B59CB4CD2542204D00F8CAD0 /* raw-stream-audio-normal-metadata-alt */ = {isa = PBXFileReference; lastKnownFileType = file; path = "raw-stream-audio-normal-metadata-alt"; sourceTree = ""; }; - B59D0B6E255C904900D6CCE5 /* FileAudioSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileAudioSource.swift; sourceTree = ""; }; - B59DF10324916FD50043C498 /* DispatchQueue+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Helpers.swift"; sourceTree = ""; }; - B59DF1A22493E90C0043C498 /* AudioFileStream+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AudioFileStream+Helpers.swift"; sourceTree = ""; }; - 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 = ""; }; - B5AEDBB224744153007D8101 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 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 = ""; }; - B5B36E422655A32200DC96F5 /* FrameFilterProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameFilterProcessor.swift; sourceTree = ""; }; - B5B3B7CB248647ED00656828 /* AudioPlayerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerState.swift; sourceTree = ""; }; - B5D4A40825D9321400E1450C /* IcycastHeaderParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IcycastHeaderParser.swift; sourceTree = ""; }; - B5D4A40B25D9445600E1450C /* IcycastHeadersProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IcycastHeadersProcessor.swift; sourceTree = ""; }; - B5D82E64255DD562009EDAA4 /* NetStatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetStatusService.swift; sourceTree = ""; }; - 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 = ""; }; - B5E1DE2424B70B4200955BFB /* AudioPlayerConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerConfiguration.swift; sourceTree = ""; }; - B5EF954D247DA5AC003E8FF8 /* NetworkingClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkingClientTests.swift; sourceTree = ""; }; - B5EF9554247E9393003E8FF8 /* AudioEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioEntry.swift; sourceTree = ""; }; - B5EF9556247E9439003E8FF8 /* AudioStreamSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioStreamSource.swift; sourceTree = ""; }; - B5EF955A247EBCB3003E8FF8 /* AudioFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFileType.swift; sourceTree = ""; }; - B5EF955C247ECBB1003E8FF8 /* RemoteAudioSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteAudioSource.swift; sourceTree = ""; }; - B5F883B82477CBF600D277C1 /* AtomicTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicTests.swift; sourceTree = ""; }; - B5F883C22477DC4400D277C1 /* NetworkDataStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkDataStream.swift; sourceTree = ""; }; - B5FB6C0425516507002C0A37 /* AudioConverter+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AudioConverter+Helpers.swift"; sourceTree = ""; }; - B5FFF5FD2549FA02006BBB7C /* AudioExample.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AudioExample.xctestplan; sourceTree = ""; }; -/* 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 = ""; - }; - B5276B70247D4D3D00D2F56A /* Network */ = { - isa = PBXGroup; - children = ( - B5D82E64255DD562009EDAA4 /* NetStatusService.swift */, - B5276B6E247D21A000D2F56A /* NetworkingClient.swift */, - B5276B73247D4D9F00D2F56A /* NetworkSessionDelegate.swift */, - B5F883C22477DC4400D277C1 /* NetworkDataStream.swift */, - ); - path = Network; - sourceTree = ""; - }; - B55A7369247FCB160050C53D /* Audio Entry */ = { - isa = PBXGroup; - children = ( - B58386362544A2A60087A712 /* Models */, - B5DB66E1255C2EAB00B8DF53 /* AudioEntryProvider.swift */, - B5EF9554247E9393003E8FF8 /* AudioEntry.swift */, - ); - path = "Audio Entry"; - sourceTree = ""; - }; - B55A736A247FCB310050C53D /* Parsers */ = { - isa = PBXGroup; - children = ( - B55CEAB32485107C0001C498 /* Parser.swift */, - B55A736B247FCB420050C53D /* HTTPHeaderParser.swift */, - B55CE96D248058B60001C498 /* MetadataParser.swift */, - B5D4A40825D9321400E1450C /* IcycastHeaderParser.swift */, - ); - path = Parsers; - sourceTree = ""; - }; - 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 = ""; - }; - B55CEAB5248517170001C498 /* Streaming */ = { - isa = PBXGroup; - children = ( - B59CB4B125421D8200F8CAD0 /* Metadata Stream Processor */, - B55CEAB62485171E0001C498 /* Parsers */, - B51FE0C724892D1600F2A4D2 /* PlayerQueueEntriesTest.swift */, - ); - path = Streaming; - sourceTree = ""; - }; - B55CEAB62485171E0001C498 /* Parsers */ = { - isa = PBXGroup; - children = ( - B55CEAB72485172D0001C498 /* HTTPHeaderParserTests.swift */, - B55CEAB9248530C00001C498 /* MetadataParser.swift */, - ); - path = Parsers; - sourceTree = ""; - }; - B55CEABF24855A900001C498 /* Helpers */ = { - isa = PBXGroup; - children = ( - B51FE0C3248905B400F2A4D2 /* PlayerQueueEntries.swift */, - B5EF955A247EBCB3003E8FF8 /* AudioFileType.swift */, - B55F77D524DACE140057F431 /* BufferContext.swift */, - ); - path = Helpers; - sourceTree = ""; - }; - B55CEAC024855AA20001C498 /* Processors */ = { - isa = PBXGroup; - children = ( - B5B36E422655A32200DC96F5 /* FrameFilterProcessor.swift */, - B5667A8F2499018D00D93F85 /* AudioFileStreamProcessor.swift */, - B5667B3D249BC43000D93F85 /* AudioPlayerRenderProcessor.swift */, - B55CE97024810DE20001C498 /* MetadataStreamProcessor.swift */, - B5D4A40B25D9445600E1450C /* IcycastHeadersProcessor.swift */, - ); - path = Processors; - sourceTree = ""; - }; - B57A4F7A24AB4E6C00D7EA51 /* Frameworks */ = { - isa = PBXGroup; - children = ( - B5DB66DA255C079C00B8DF53 /* AVFoundation.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - B580CB1C25628CE4006D7DD8 /* Deployment */ = { - isa = PBXGroup; - children = ( - B580CB1D25628CF4006D7DD8 /* AudioStreaming.podspec */, - B580CB1E25628CF4006D7DD8 /* LICENSE */, - B580CB1F25628D09006D7DD8 /* Package.swift */, - ); - name = Deployment; - sourceTree = ""; - }; - B58386362544A2A60087A712 /* Models */ = { - isa = PBXGroup; - children = ( - B58386372544A2C10087A712 /* EntryFrames.swift */, - B583863F254584A50087A712 /* ProcessedPackets.swift */, - B5838643254584BE0087A712 /* AudioStreamState.swift */, - B5838647254584D90087A712 /* SeekRequest.swift */, - ); - path = Models; - sourceTree = ""; - }; - B58BD7FC255DB653005B756D /* Audio Source */ = { - isa = PBXGroup; - children = ( - 98C82AE42B8CA8AA00AED485 /* Mp4 */, - B5EF9556247E9439003E8FF8 /* AudioStreamSource.swift */, - B5EF955C247ECBB1003E8FF8 /* RemoteAudioSource.swift */, - B59D0B6E255C904900D6CCE5 /* FileAudioSource.swift */, - ); - path = "Audio Source"; - sourceTree = ""; - }; - B592E11E2545FF33008866FB /* Structures */ = { - isa = PBXGroup; - children = ( - B5276B71247D4D5B00D2F56A /* BiMap.swift */, - B51FE0BF2488F67C00F2A4D2 /* Queue.swift */, - ); - path = Structures; - sourceTree = ""; - }; - 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 = ""; - }; - B59CB4B125421D8200F8CAD0 /* Metadata Stream Processor */ = { - isa = PBXGroup; - children = ( - B59CB4B525421D8D00F8CAD0 /* raw-audio-streams */, - B59CB46B25420B4D00F8CAD0 /* MetadataStreamProcessorTests.swift */, - ); - path = "Metadata Stream Processor"; - sourceTree = ""; - }; - 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 = ""; - }; - B5AEDBA424744153007D8101 = { - isa = PBXGroup; - children = ( - B580CB1C25628CE4006D7DD8 /* Deployment */, - B5AEDBB024744153007D8101 /* AudioStreaming */, - B5AEDBBB24744153007D8101 /* AudioStreamingTests */, - B5AEDBAF24744153007D8101 /* Products */, - B57A4F7A24AB4E6C00D7EA51 /* Frameworks */, - ); - sourceTree = ""; - }; - B5AEDBAF24744153007D8101 /* Products */ = { - isa = PBXGroup; - children = ( - B5AEDBAE24744153007D8101 /* AudioStreaming.framework */, - B5AEDBB724744153007D8101 /* AudioStreamingTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - B5AEDBB024744153007D8101 /* AudioStreaming */ = { - isa = PBXGroup; - children = ( - B5F883B42476DABE00D277C1 /* Core */, - B5EF9553247E9235003E8FF8 /* Streaming */, - B5AEDBB124744153007D8101 /* AudioStreaming.h */, - B5AEDBB224744153007D8101 /* Info.plist */, - ); - path = AudioStreaming; - sourceTree = ""; - }; - B5AEDBBB24744153007D8101 /* AudioStreamingTests */ = { - isa = PBXGroup; - children = ( - B5FFF5FD2549FA02006BBB7C /* AudioExample.xctestplan */, - B5F883B72477CBC900D277C1 /* Core */, - B55CEAB5248517170001C498 /* Streaming */, - B5AEDBBE24744153007D8101 /* Info.plist */, - ); - path = AudioStreamingTests; - sourceTree = ""; - }; - 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 = ""; - }; - B5EF954A247DA450003E8FF8 /* Network */ = { - isa = PBXGroup; - children = ( - B5EF954D247DA5AC003E8FF8 /* NetworkingClientTests.swift */, - ); - path = Network; - sourceTree = ""; - }; - B5EF9553247E9235003E8FF8 /* Streaming */ = { - isa = PBXGroup; - children = ( - B5E1DE2924B7179E00955BFB /* AudioPlayer */, - B58BD7FC255DB653005B756D /* Audio Source */, - B55A7369247FCB160050C53D /* Audio Entry */, - B55CEABF24855A900001C498 /* Helpers */, - B55A736A247FCB310050C53D /* Parsers */, - ); - path = Streaming; - sourceTree = ""; - }; - B5F883B42476DABE00D277C1 /* Core */ = { - isa = PBXGroup; - children = ( - B55CE97624813BA10001C498 /* Extensions */, - B592E11E2545FF33008866FB /* Structures */, - B5276B70247D4D3D00D2F56A /* Network */, - B592E13025460883008866FB /* Helpers */, - ); - path = Core; - sourceTree = ""; - }; - 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 = ""; - }; -/* 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.8; - 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.8; - 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.8; - 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.8; - 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 */; -} diff --git a/AudioStreaming.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/AudioStreaming.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/AudioStreaming.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/AudioStreaming.xcodeproj/xcshareddata/xcbaselines/B5AEDBB624744153007D8101.xcbaseline/31DA71B1-4664-472C-BD35-0711EE94837A.plist b/AudioStreaming.xcodeproj/xcshareddata/xcbaselines/B5AEDBB624744153007D8101.xcbaseline/31DA71B1-4664-472C-BD35-0711EE94837A.plist deleted file mode 100644 index 3d02f16..0000000 --- a/AudioStreaming.xcodeproj/xcshareddata/xcbaselines/B5AEDBB624744153007D8101.xcbaseline/31DA71B1-4664-472C-BD35-0711EE94837A.plist +++ /dev/null @@ -1,45 +0,0 @@ - - - - - classNames - - ProtectedTests - - testProtectedValuesAreAccessedSafely() - - com.apple.XCTPerformanceMetric_WallClockTime - - baselineAverage - 4.7921 - baselineIntegrationDisplayName - Local Baseline - - - testThatProtectedReadAndWriteAreSafe() - - com.apple.XCTPerformanceMetric_WallClockTime - - baselineAverage - 0.0067462 - baselineIntegrationDisplayName - Local Baseline - - - - QueueTests - - testComplexity() - - com.apple.XCTPerformanceMetric_WallClockTime - - baselineAverage - 3.7871 - baselineIntegrationDisplayName - Local Baseline - - - - - - diff --git a/AudioStreaming.xcodeproj/xcshareddata/xcbaselines/B5AEDBB624744153007D8101.xcbaseline/Info.plist b/AudioStreaming.xcodeproj/xcshareddata/xcbaselines/B5AEDBB624744153007D8101.xcbaseline/Info.plist deleted file mode 100644 index bc6f935..0000000 --- a/AudioStreaming.xcodeproj/xcshareddata/xcbaselines/B5AEDBB624744153007D8101.xcbaseline/Info.plist +++ /dev/null @@ -1,40 +0,0 @@ - - - - - runDestinationsByUUID - - 31DA71B1-4664-472C-BD35-0711EE94837A - - localComputer - - busSpeedInMHz - 400 - cpuCount - 1 - cpuKind - 8-Core Intel Core i9 - cpuSpeedInMHz - 2300 - logicalCPUCoresPerPackage - 16 - modelCode - MacBookPro16,1 - physicalCPUCoresPerPackage - 8 - platformIdentifier - com.apple.platform.macosx - - targetArchitecture - x86_64 - targetDevice - - modelCode - iPhone12,1 - platformIdentifier - com.apple.platform.iphonesimulator - - - - - diff --git a/AudioStreaming.xcodeproj/xcshareddata/xcschemes/AudioStreaming.xcscheme b/AudioStreaming.xcodeproj/xcshareddata/xcschemes/AudioStreaming.xcscheme deleted file mode 100644 index 33ffb01..0000000 --- a/AudioStreaming.xcodeproj/xcshareddata/xcschemes/AudioStreaming.xcscheme +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/AudioStreaming.xcodeproj/xcuserdata/dimitrisc.xcuserdatad/xcschemes/xcschememanagement.plist b/AudioStreaming.xcodeproj/xcuserdata/dimitrisc.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index 00d095d..0000000 --- a/AudioStreaming.xcodeproj/xcuserdata/dimitrisc.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - SchemeUserState - - AudioStreaming.xcscheme_^#shared#^_ - - orderHint - 0 - - - SuppressBuildableAutocreation - - B5AEDBAD24744153007D8101 - - primary - - - B5AEDBB624744153007D8101 - - primary - - - B5B3B7D22486993B00656828 - - primary - - - - - diff --git a/AudioStreaming.xcworkspace/contents.xcworkspacedata b/AudioStreaming.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index ed8f13d..0000000 --- a/AudioStreaming.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/AudioStreaming.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/AudioStreaming.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/AudioStreaming.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/AudioStreaming.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/AudioStreaming.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c..0000000 --- a/AudioStreaming.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/AudioStreaming.xcworkspace/xcuserdata/dimitrisc.xcuserdatad/WorkspaceSettings.xcsettings b/AudioStreaming.xcworkspace/xcuserdata/dimitrisc.xcuserdatad/WorkspaceSettings.xcsettings deleted file mode 100644 index 379adbe..0000000 --- a/AudioStreaming.xcworkspace/xcuserdata/dimitrisc.xcuserdatad/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,18 +0,0 @@ - - - - - BuildLocationStyle - UseAppPreferences - CustomBuildLocationType - RelativeToDerivedData - DerivedDataLocationStyle - Default - IssueFilterStyle - ShowActiveSchemeOnly - LiveSourceIssuesEnabled - - ShowSharedSchemesAutomaticallyEnabled - - - diff --git a/AudioStreaming.xcworkspace/xcuserdata/dimitrisc.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/AudioStreaming.xcworkspace/xcuserdata/dimitrisc.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist deleted file mode 100644 index 9c8db6d..0000000 --- a/AudioStreaming.xcworkspace/xcuserdata/dimitrisc.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/AudioStreaming/Core/Extensions/AudioConverter+Helpers.swift b/AudioStreaming/Core/Extensions/AudioConverter+Helpers.swift index d5c5ed4..a7d0e22 100644 --- a/AudioStreaming/Core/Extensions/AudioConverter+Helpers.swift +++ b/AudioStreaming/Core/Extensions/AudioConverter+Helpers.swift @@ -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" } } } diff --git a/AudioStreaming/OggVorbis/VorbisFileDecoder.swift b/AudioStreaming/OggVorbis/VorbisFileDecoder.swift new file mode 100644 index 0000000..90ff3d8 --- /dev/null +++ b/AudioStreaming/OggVorbis/VorbisFileDecoder.swift @@ -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? + 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.allocate(capacity: silentFrameSize) + for i in 0.. 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?>? + 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...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.. 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() { diff --git a/AudioStreaming/Streaming/AudioPlayer/AudioPlayerState.swift b/AudioStreaming/Streaming/AudioPlayer/AudioPlayerState.swift index 768ab80..fb0d86b 100644 --- a/AudioStreaming/Streaming/AudioPlayer/AudioPlayerState.swift +++ b/AudioStreaming/Streaming/AudioPlayer/AudioPlayerState.swift @@ -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" } } } diff --git a/AudioStreaming/Streaming/AudioPlayer/Processors/AudioFileStreamProcessor.swift b/AudioStreaming/Streaming/AudioPlayer/Processors/AudioFileStreamProcessor.swift index 3f07f6f..82b6cff 100644 --- a/AudioStreaming/Streaming/AudioPlayer/Processors/AudioFileStreamProcessor.swift +++ b/AudioStreaming/Streaming/AudioPlayer/Processors/AudioFileStreamProcessor.swift @@ -34,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? @@ -42,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, @@ -54,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` @@ -63,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 @@ -83,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) @@ -92,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 diff --git a/AudioStreaming/Streaming/AudioPlayer/Processors/OggVorbisStreamProcessor.swift b/AudioStreaming/Streaming/AudioPlayer/Processors/OggVorbisStreamProcessor.swift new file mode 100644 index 0000000..cb01857 --- /dev/null +++ b/AudioStreaming/Streaming/AudioPlayer/Processors/OggVorbisStreamProcessor.swift @@ -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 + } +} diff --git a/AudioStreaming/Streaming/Helpers/AudioFileType.swift b/AudioStreaming/Streaming/Helpers/AudioFileType.swift index e7c9d0a..742bb6f 100644 --- a/AudioStreaming/Streaming/Helpers/AudioFileType.swift +++ b/AudioStreaming/Streaming/Helpers/AudioFileType.swift @@ -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 { diff --git a/AudioStreamingTests/Streaming/Metadata Stream Processor/MetadataStreamProcessorTests.swift b/AudioStreamingTests/Streaming/Metadata Stream Processor/MetadataStreamProcessorTests.swift index 40bbac4..f40acad 100644 --- a/AudioStreamingTests/Streaming/Metadata Stream Processor/MetadataStreamProcessorTests.swift +++ b/AudioStreamingTests/Streaming/Metadata Stream Processor/MetadataStreamProcessorTests.swift @@ -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) diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..546296c --- /dev/null +++ b/Package.resolved @@ -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 +} diff --git a/Package.swift b/Package.swift index a658aee..5249157 100644 --- a/Package.swift +++ b/Package.swift @@ -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") + ] ) ] ) diff --git a/README.md b/README.md index ea83b00..8423257 100644 --- a/README.md +++ b/README.md @@ -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.