97 lines
3.6 KiB
Swift
97 lines
3.6 KiB
Swift
|
|
import Foundation
|
||
|
|
|
||
|
|
/// Caches pre-computed visualizer FFT frames to disk for instant playback
|
||
|
|
actor VisualizerStorageManager {
|
||
|
|
static let shared = VisualizerStorageManager()
|
||
|
|
|
||
|
|
private let fileManager = FileManager.default
|
||
|
|
private var cacheDir: URL {
|
||
|
|
let dir = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||
|
|
.appendingPathComponent("VisualizerCache", isDirectory: true)
|
||
|
|
if !fileManager.fileExists(atPath: dir.path) {
|
||
|
|
try? fileManager.createDirectory(at: dir, withIntermediateDirectories: true)
|
||
|
|
}
|
||
|
|
return dir
|
||
|
|
}
|
||
|
|
|
||
|
|
private func fileURL(for trackId: String) -> URL {
|
||
|
|
cacheDir.appendingPathComponent("mitsuha_\(trackId).bin")
|
||
|
|
}
|
||
|
|
|
||
|
|
func hasCache(for trackId: String) -> Bool {
|
||
|
|
fileManager.fileExists(atPath: fileURL(for: trackId).path)
|
||
|
|
}
|
||
|
|
|
||
|
|
func saveCache(frames: [[Float]], for trackId: String) throws {
|
||
|
|
// Binary format: [frameCount: UInt32][pointsPerFrame: UInt32][float data...]
|
||
|
|
let frameCount = frames.count
|
||
|
|
let pointsPerFrame = frames.first?.count ?? 0
|
||
|
|
guard frameCount > 0, pointsPerFrame > 0 else { return }
|
||
|
|
|
||
|
|
var data = Data()
|
||
|
|
var fc = UInt32(frameCount)
|
||
|
|
var ppf = UInt32(pointsPerFrame)
|
||
|
|
data.append(Data(bytes: &fc, count: 4))
|
||
|
|
data.append(Data(bytes: &ppf, count: 4))
|
||
|
|
|
||
|
|
for frame in frames {
|
||
|
|
frame.withUnsafeBytes { data.append(contentsOf: $0) }
|
||
|
|
}
|
||
|
|
|
||
|
|
try data.write(to: fileURL(for: trackId), options: .atomic)
|
||
|
|
}
|
||
|
|
|
||
|
|
func loadCache(for trackId: String) throws -> [[Float]] {
|
||
|
|
let data = try Data(contentsOf: fileURL(for: trackId))
|
||
|
|
guard data.count >= 8 else { return [] }
|
||
|
|
|
||
|
|
let frameCount = data.withUnsafeBytes { $0.load(fromByteOffset: 0, as: UInt32.self) }
|
||
|
|
let pointsPerFrame = data.withUnsafeBytes { $0.load(fromByteOffset: 4, as: UInt32.self) }
|
||
|
|
|
||
|
|
let floatSize = MemoryLayout<Float>.size
|
||
|
|
let expectedSize = 8 + Int(frameCount) * Int(pointsPerFrame) * floatSize
|
||
|
|
guard data.count >= expectedSize else { return [] }
|
||
|
|
|
||
|
|
var frames: [[Float]] = []
|
||
|
|
frames.reserveCapacity(Int(frameCount))
|
||
|
|
|
||
|
|
var offset = 8
|
||
|
|
for _ in 0..<frameCount {
|
||
|
|
let frame: [Float] = data.withUnsafeBytes { raw in
|
||
|
|
let ptr = raw.baseAddress!.advanced(by: offset).assumingMemoryBound(to: Float.self)
|
||
|
|
return Array(UnsafeBufferPointer(start: ptr, count: Int(pointsPerFrame)))
|
||
|
|
}
|
||
|
|
frames.append(frame)
|
||
|
|
offset += Int(pointsPerFrame) * floatSize
|
||
|
|
}
|
||
|
|
|
||
|
|
return frames
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Total cache size on disk
|
||
|
|
func cacheSize() -> Int64 {
|
||
|
|
guard let files = try? fileManager.contentsOfDirectory(at: cacheDir, includingPropertiesForKeys: [.fileSizeKey]) else {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
return files.reduce(0) { sum, url in
|
||
|
|
let size = (try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0
|
||
|
|
return sum + Int64(size)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Number of cached tracks
|
||
|
|
func cachedTrackCount() -> Int {
|
||
|
|
(try? fileManager.contentsOfDirectory(at: cacheDir, includingPropertiesForKeys: nil).count) ?? 0
|
||
|
|
}
|
||
|
|
|
||
|
|
func clearCache() {
|
||
|
|
if let files = try? fileManager.contentsOfDirectory(at: cacheDir, includingPropertiesForKeys: nil) {
|
||
|
|
for file in files { try? fileManager.removeItem(at: file) }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func removeCache(for trackId: String) {
|
||
|
|
try? fileManager.removeItem(at: fileURL(for: trackId))
|
||
|
|
}
|
||
|
|
}
|