Last active
August 9, 2020 00:51
-
-
Save iluvcapra/df42035c93005b80b0e0c3361d7add70 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extension Data { | |
func getInteger<Value: FixedWidthInteger>(at: Int, count: Int, as: Value.Type, | |
bigEndian : Bool = false) -> Value { | |
var rawValue = Value.zero | |
for i in 0..<count { | |
let byte = withUnsafeBytes { $0.load(fromByteOffset: at + i, as: UInt8.self) } | |
rawValue <<= 8 | |
rawValue ^= Value(byte) | |
} | |
return bigEndian ? rawValue : Value(bigEndian: rawValue) | |
} | |
} | |
extension FileHandle { | |
func readInteger<Value: FixedWidthInteger>(count: Int, as: Value.Type, | |
bigEndian : Bool = false) -> Value { | |
let data = self.readData(ofLength: count) | |
return data.getInteger(at: 0, count: count, as: Value.self, bigEndian: bigEndian) | |
} | |
} | |
struct Bitfield<Value: FixedWidthInteger> { | |
let value : Value | |
func split(widths : [Int]) -> [Value] { | |
var ranges = [Range<Int>]() | |
var at = 0 | |
for width in widths { | |
let thisRange = Range(uncheckedBounds: (lower: at, upper: at + width)) | |
ranges.append(thisRange) | |
at += width | |
} | |
return ranges.map { self[$0] } | |
} | |
subscript(bounds : Range<Int>) -> Value { | |
let mask = Value( (1 << bounds.count) - 1 ) | |
return (value >> bounds.startIndex) & mask | |
} | |
subscript(index : Int) -> Value { | |
return (value >> index) & 0x1 | |
} | |
} | |
/// | |
struct FlacFile { | |
let url : URL | |
struct StreamMarkerNotFoundError : Error { let position : UInt64 } | |
struct MetadataIterator: Sequence, IteratorProtocol { | |
private let file : FileHandle | |
private var state : Bool = true | |
struct Metadata { | |
let type: UInt8 | |
let isLast: Bool | |
let data: Data | |
} | |
func expectStreamMarker() throws { | |
let offset = file.offsetInFile | |
guard let data = try file.read(upToCount: 4), | |
data == "fLaC".data(using: .ascii) else { | |
throw StreamMarkerNotFoundError(position: offset) | |
} | |
} | |
init(url : URL) throws { | |
self.file = try FileHandle(forReadingFrom: url) | |
try expectStreamMarker() | |
} | |
mutating func next() -> Metadata? { | |
guard state else { return nil } | |
let magic = file.readInteger(count: 1, as: UInt8.self) | |
let isLast = magic & 0x80 > 0 | |
let blockType = magic & 0x7f | |
let dataSize = file.readInteger(count: 3, as: UInt32.self, bigEndian: true) | |
defer { | |
if isLast { | |
file.closeFile() | |
self.state = false | |
} | |
} | |
let data = file.readData(ofLength: Int(dataSize)) | |
return Metadata(type: blockType, isLast: isLast, data: data) | |
} | |
} | |
func metadata() throws -> MetadataIterator { | |
return try MetadataIterator(url: url) | |
} | |
} | |
let f = FlacFile(url: Bundle.main.url(forResource: "file1", withExtension: "flac")!) | |
struct StreamInfoBlock { | |
let minimumBlockSize : Int | |
let maximumBlockSize : Int | |
let minimumFrameSize : Int | |
let maximumFrameSize : Int | |
let sampleRate : Int | |
let channelCount : Int | |
let bitsPerSample : Int | |
let totalSampleFrames : Int | |
let md5Signature : Data | |
static func from(data: Data) -> StreamInfoBlock { | |
let minBlockSize = data.getInteger(at: 0, count: 2, as: UInt16.self, bigEndian: true) | |
let maxBlockSize = data.getInteger(at: 2, count: 2, as: UInt16.self, bigEndian: true) | |
let minFrameSize = data.getInteger(at: 4, count: 3, as: UInt32.self, bigEndian: true) | |
let maxFrameSize = data.getInteger(at: 7, count: 3, as: UInt32.self, bigEndian: true) | |
let packed = data.getInteger(at: 10, count: 8, as: UInt64.self, bigEndian: true) | |
let bits = Bitfield(value: packed) | |
let sampleRate = bits[44..<64] | |
let channelCount = bits[41..<44] + 1 | |
let bitsPerSample = bits[36..<41] + 1 | |
let totalSampleFrames = bits[0..<36] | |
return StreamInfoBlock(minimumBlockSize: Int(minBlockSize), | |
maximumBlockSize: Int(maxBlockSize), | |
minimumFrameSize: Int(minFrameSize), | |
maximumFrameSize: Int(maxFrameSize), | |
sampleRate: Int(sampleRate), | |
channelCount: Int(channelCount), | |
bitsPerSample: Int(bitsPerSample), | |
totalSampleFrames: Int(totalSampleFrames), | |
md5Signature: Data(data[18..<34])) | |
} | |
} | |
struct VorbisCommentBlock { | |
let vendor : String | |
let comments : [String] | |
var collatedCommments : [String : [String]] { | |
var retval = [String : [String]]() | |
for comment in self.comments { | |
let (key, value) = VorbisCommentBlock.parse(comment: comment) | |
if retval.keys.contains(key) { | |
retval[key] = retval[key]! + [value] | |
} else { | |
retval[key] = [value] | |
} | |
} | |
return retval | |
} | |
static func parse(comment: String) -> (key: String, value: String) { | |
let p = Scanner(string: comment) | |
let k = p.scanUpToString("=") ?? "" | |
p.scanString("=") | |
let v = p.scanUpToString("\0") ?? "" | |
return (key: k.uppercased(), value: v) | |
} | |
static func from(data: Data) -> VorbisCommentBlock { | |
var offset = 0 | |
let vendorLength = Int(data.getInteger(at: offset, count: 4, as: UInt32.self)) | |
offset += 4 | |
let vendorString = String(data: data[offset..<vendorLength+offset], encoding: .utf8)! | |
offset += vendorLength | |
let vectorCount = data.getInteger(at: offset, count: 4, as: UInt32.self) | |
offset += 4 | |
var comments = [String]() | |
for _ in 0..<vectorCount { | |
let thisLength = Int(data.getInteger(at: offset, count: 4, as: UInt32.self)) | |
offset += 4 | |
let thisComment = String(data: data[offset..<offset+thisLength], encoding: .utf8)! | |
offset += thisLength | |
comments.append(thisComment) | |
} | |
return VorbisCommentBlock(vendor: vendorString, comments: comments) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment