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.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.. 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)) } }