Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ final class AudioFileStreamProcessor {
// Track if we're processing Ogg Vorbis
private var isProcessingOggVorbis: Bool = false

/// Set by `processSeek()` for FLAC streams; the first packet received afterwards is used to
/// replace the estimated seek time with the position actually reached.
private var correctSeekTimeFromNextFlacFrame = false

var isFileStreamOpen: Bool {
audioFileStream != nil || isProcessingOggVorbis
}
Expand Down Expand Up @@ -210,6 +214,7 @@ final class AudioFileStreamProcessor {
}

readingEntry.reset()
correctSeekTimeFromNextFlacFrame = readingEntry.audioStreamFormat.mFormatID == kAudioFormatFLAC
readingEntry.seek(at: Int(seekByteOffset))
rendererContext.waitingForDataAfterSeekFrameCount.write { $0 = 0 }
playerContext.setInternalState(to: .waitingForDataAfterSeek)
Expand Down Expand Up @@ -496,6 +501,15 @@ final class AudioFileStreamProcessor {
// reset discontinuity
discontinuous = false

if correctSeekTimeFromNextFlacFrame {
correctSeekTimeFromFlacFrame(
entry: entry,
inInputData: inInputData,
inNumberBytes: inNumberBytes,
inPacketDescriptions: inPacketDescriptions
)
}

var convertInfo = AudioConvertInfo(
done: false,
numberOfPackets: inNumberPackets,
Expand Down Expand Up @@ -658,6 +672,30 @@ final class AudioFileStreamProcessor {
}
}

/// Replaces the seek time with the start time of the first frame received after a FLAC seek.
///
/// `processSeek()` jumps to a byte offset proportional to the requested time, but FLAC is variable
/// bitrate: the frame found there can be seconds away from that time, and the error would carry
/// on into `progress` until the end of the track.
private func correctSeekTimeFromFlacFrame(
entry: AudioEntry,
inInputData: UnsafeRawPointer,
inNumberBytes: UInt32,
inPacketDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>?
) {
correctSeekTimeFromNextFlacFrame = false
let packetStart = Int(inPacketDescriptions?.pointee.mStartOffset ?? 0)
guard packetStart < Int(inNumberBytes) else { return }
let packet = UnsafeRawBufferPointer(start: inInputData + packetStart, count: Int(inNumberBytes) - packetStart)

entry.lock.lock(); defer { entry.lock.unlock() }
let format = entry.audioStreamFormat
guard format.mSampleRate > 0,
let sampleNumber = FlacFrameHeader.firstSampleNumber(in: packet, nominalBlockSize: format.mFramesPerPacket)
else { return }
entry.seekTime = Double(sampleNumber) / format.mSampleRate
}

/// Fills the `AudioBuffer` with data as required
///
/// - parameter list: An `UnsafeMutableAudioBufferListPointer` object representing the buffer list be filled with data
Expand Down
110 changes: 110 additions & 0 deletions AudioStreaming/Streaming/Helpers/FlacFrameHeader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//
// FlacFrameHeader.swift
// AudioStreaming
//

import Foundation

/// Reads the position of a FLAC frame from its header (RFC 9639, section 9.1).
///
/// Seeking a FLAC stream lands on a byte offset estimated from the bitrate. FLAC is variable
/// bitrate, so that offset rarely corresponds to the requested time. Every frame header records
/// where the frame starts, which lets the player report the position it actually reached.
enum FlacFrameHeader {
/// Returns the index of the first sample in the frame starting at `bytes`, or `nil` if `bytes`
/// does not start with a valid frame header.
///
/// - Parameters:
/// - bytes: Data starting at the first byte of a frame.
/// - nominalBlockSize: Samples per frame of a fixed-blocksize stream (0 if unknown). Fixed-blocksize
/// frames store a frame number instead of a sample number, and only the last frame may be
/// shorter than the others, so the larger of this value and the frame's own block size is used.
static func firstSampleNumber(in bytes: UnsafeRawBufferPointer, nominalBlockSize: UInt32) -> UInt64? {
// sync code (14 bits) + reserved bit (must be 0) + blocking strategy bit
guard bytes.count >= 6, bytes[0] == 0xFF, bytes[1] & 0xFE == 0xF8 else { return nil }
let isVariableBlockSize = bytes[1] & 0x01 == 1

let blockSizeCode = bytes[2] >> 4
let sampleRateCode = bytes[2] & 0x0F
// block size 0 is reserved and sample rate 0b1111 is forbidden; rejecting them helps
// discard false sync codes found inside audio data
guard blockSizeCode != 0, sampleRateCode != 0x0F else { return nil }
// channel assignments 0b1011...0b1111 and the trailing reserved bit are invalid
guard bytes[3] >> 4 <= 0x0A, bytes[3] & 0x01 == 0 else { return nil }

var index = 4
guard let codedNumber = readCodedNumber(bytes, at: &index, maxLength: isVariableBlockSize ? 7 : 6) else {
return nil
}

let blockSize: UInt32
switch blockSizeCode {
case 1:
blockSize = 192
case 2 ... 5:
blockSize = 576 << (UInt32(blockSizeCode) - 2)
case 6:
guard index < bytes.count else { return nil }
blockSize = UInt32(bytes[index]) + 1
index += 1
case 7:
guard index + 1 < bytes.count else { return nil }
blockSize = (UInt32(bytes[index]) << 8 | UInt32(bytes[index + 1])) + 1
index += 2
default: // 8...15
blockSize = 256 << (UInt32(blockSizeCode) - 8)
}

switch sampleRateCode {
case 12: index += 1
case 13, 14: index += 2
default: break
}

guard index < bytes.count, crc8(bytes, count: index) == bytes[index] else { return nil }

if isVariableBlockSize {
return codedNumber
}
return codedNumber * UInt64(max(blockSize, nominalBlockSize))
}

/// Decodes the UTF-8-like coded frame or sample number.
private static func readCodedNumber(_ bytes: UnsafeRawBufferPointer, at index: inout Int, maxLength: Int) -> UInt64? {
guard index < bytes.count else { return nil }
let first = bytes[index]
let length: Int
switch first {
case 0x00 ... 0x7F: length = 1
case 0xC0 ... 0xDF: length = 2
case 0xE0 ... 0xEF: length = 3
case 0xF0 ... 0xF7: length = 4
case 0xF8 ... 0xFB: length = 5
case 0xFC ... 0xFD: length = 6
case 0xFE: length = 7
default: return nil
}
guard length <= maxLength, index + length <= bytes.count else { return nil }

var value = length == 1 ? UInt64(first) : UInt64(first & (0x7F >> length))
for offset in 1 ..< length {
let byte = bytes[index + offset]
guard byte & 0xC0 == 0x80 else { return nil }
value = value << 6 | UInt64(byte & 0x3F)
}
index += length
return value
}

/// CRC-8 with polynomial x^8 + x^2 + x + 1, initialised to 0.
private static func crc8(_ bytes: UnsafeRawBufferPointer, count: Int) -> UInt8 {
var crc: UInt8 = 0
for i in 0 ..< count {
crc ^= bytes[i]
for _ in 0 ..< 8 {
crc = crc & 0x80 != 0 ? (crc << 1) ^ 0x07 : crc << 1
}
}
return crc
}
}
57 changes: 57 additions & 0 deletions AudioStreamingTests/Streaming/Helpers/FlacFrameHeaderTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// FlacFrameHeaderTests.swift
// AudioStreamingTests
//

import XCTest
@testable import AudioStreaming

final class FlacFrameHeaderTests: XCTestCase {
// Frame headers taken from a 44.1 kHz FLAC file encoded with a fixed block size of 4608 samples
private let firstFrame: [UInt8] = [0xFF, 0xF8, 0x59, 0x88, 0x00, 0x8A]
private let frame1000: [UInt8] = [0xFF, 0xF8, 0x59, 0x88, 0xCF, 0xA8, 0xC0]
// last frame (number 2296) is 4032 samples long, stored with an explicit 16-bit block size
private let lastFrame: [UInt8] = [0xFF, 0xF8, 0x79, 0x88, 0xE0, 0xA3, 0xB8, 0x0F, 0xBF, 0xBE]

func test_FixedBlockSize_Frame_Number_Is_Converted_To_Sample_Number() {
XCTAssertEqual(sampleNumber(firstFrame + [0x00, 0x2C, 0x41], nominalBlockSize: 4608), 0)
XCTAssertEqual(sampleNumber(frame1000, nominalBlockSize: 4608), 4_608_000)
}

func test_FixedBlockSize_Without_Nominal_Uses_Frame_Block_Size() {
XCTAssertEqual(sampleNumber(frame1000, nominalBlockSize: 0), 4_608_000)
}

func test_Shorter_Last_Frame_Is_Positioned_With_Nominal_Block_Size() {
XCTAssertEqual(sampleNumber(lastFrame, nominalBlockSize: 4608), 2296 * 4608)
}

func test_VariableBlockSize_Returns_Sample_Number_Directly() {
// 36-bit sample number (7-byte coded number), 16-bit block size, 16-bit sample rate in Hz
let header: [UInt8] = [0xFF, 0xF9, 0x7D, 0x18, 0xFE, 0xA6, 0x87, 0x99, 0x94, 0x8C, 0xA1, 0x0F, 0x9F, 0xAC, 0x44, 0x33]
XCTAssertEqual(sampleNumber(header, nominalBlockSize: 4608), 40_926_266_145)

let shortHeader: [UInt8] = [0xFF, 0xF9, 0xC9, 0x18, 0xF4, 0xAD, 0x9A, 0x87, 0xF9]
XCTAssertEqual(sampleNumber(shortHeader, nominalBlockSize: 0), 1_234_567)
}

func test_Header_With_Wrong_CRC_Is_Rejected() {
var header = frame1000
header[header.count - 1] ^= 0x01
XCTAssertNil(sampleNumber(header, nominalBlockSize: 4608))

var corruptedNumber = frame1000
corruptedNumber[5] ^= 0x01
XCTAssertNil(sampleNumber(corruptedNumber, nominalBlockSize: 4608))
}

func test_Data_Not_Starting_With_A_Frame_Header_Is_Rejected() {
XCTAssertNil(sampleNumber(Array(firstFrame.dropFirst()), nominalBlockSize: 4608))
XCTAssertNil(sampleNumber([0xFF, 0xFA] + firstFrame.dropFirst(2), nominalBlockSize: 4608))
XCTAssertNil(sampleNumber(Array(lastFrame.dropLast()), nominalBlockSize: 4608))
}

private func sampleNumber(_ bytes: [UInt8], nominalBlockSize: UInt32) -> UInt64? {
bytes.withUnsafeBytes { FlacFrameHeader.firstSampleNumber(in: $0, nominalBlockSize: nominalBlockSize) }
}
}