Skip to content

Commit a729ee2

Browse files
authored
Fixes gapless playback (#130)
1 parent 633018d commit a729ee2

File tree

2 files changed

+36
-0
lines changed

2 files changed

+36
-0
lines changed

AudioStreaming/Streaming/AudioPlayer/AudioPlayer.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,6 +1082,14 @@ extension AudioPlayer: AudioStreamSourceDelegate {
10821082
return
10831083
}
10841084

1085+
// Flush any remaining packets from AudioFileStream before marking EOF
1086+
// This is critical for formats like FLAC that use sync words to determine packet boundaries.
1087+
// Without this flush, the final partial packet will not be delivered.
1088+
let flushStatus = fileStreamProcessor.flushRemainingPackets()
1089+
if flushStatus != noErr {
1090+
Logger.debug("Failed to flush remaining packets at EOF: \(flushStatus)", category: .generic)
1091+
}
1092+
10851093
readingEntry.lock.lock()
10861094
readingEntry.framesState.lastFrameQueued = readingEntry.framesState.queued
10871095
readingEntry.lock.unlock()

AudioStreaming/Streaming/AudioPlayer/Processors/AudioFileStreamProcessor.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,34 @@ final class AudioFileStreamProcessor {
125125
AudioFileStreamParseBytes(stream, UInt32(buffer.count), buffer.baseAddress, flags)
126126
}
127127
}
128+
129+
/// Flushes any remaining packets from the AudioFileStream parser.
130+
///
131+
/// This must be called at the end of a stream to ensure the final packet is delivered,
132+
/// particularly important for formats like FLAC that use sync words to determine packet boundaries.
133+
///
134+
/// As documented in AudioFileStream.h:
135+
/// "At the end of the stream, this function must be called once with null data pointer
136+
/// and zero data byte size to flush any remaining packets out of the parser."
137+
///
138+
/// - Returns: An `OSStatus` value indicating if an error occurred or not.
139+
func flushRemainingPackets() -> OSStatus {
140+
// Ogg Vorbis doesn't need flushing (handled internally)
141+
if isProcessingOggVorbis {
142+
return noErr
143+
}
144+
145+
guard let stream = audioFileStream else { return noErr }
146+
147+
// Call AudioFileStreamParseBytes with 0 bytes and nil data to flush the final packet
148+
let status = AudioFileStreamParseBytes(stream, 0, nil, [])
149+
150+
if status != noErr {
151+
Logger.debug("Failed to flush AudioFileStream packets: \(status)", category: .generic)
152+
}
153+
154+
return status
155+
}
128156

129157
func processSeek() {
130158
guard let readingEntry = playerContext.audioReadingEntry else {

0 commit comments

Comments
 (0)