import Foundation // MARK: - VisFrameBuffer /// Flat, memory-efficient storage for pre-computed visualizer FFT frames. /// All frames packed into a single ContiguousArray in row-major order: /// [frame0pt0, frame0pt1, ..., frame1pt0, frame1pt1, ...] /// This eliminates the [[Float]] array-of-arrays structure and allows /// O(1) index computation with zero allocation for frame access. struct VisFrameBuffer { let data: ContiguousArray let frameCount: Int let pointsPerFrame: Int static let empty = VisFrameBuffer(data: [], frameCount: 0, pointsPerFrame: 0) var isEmpty: Bool { frameCount == 0 } /// Copy frame at `index` into a pre-allocated destination buffer. /// No intermediate allocation — copies directly from flat storage. func copyFrame(at index: Int, into dest: inout [Float]) { guard index >= 0 && index < frameCount else { return } let start = index * pointsPerFrame let end = start + pointsPerFrame guard end <= data.count else { return } // Resize destination only if count changed (rare: settings change) if dest.count != pointsPerFrame { dest = [Float](repeating: 0, count: pointsPerFrame) } data.withUnsafeBufferPointer { src in dest.withUnsafeMutableBufferPointer { dst in dst.baseAddress!.initialize( from: src.baseAddress!.advanced(by: start), count: pointsPerFrame ) } } } } /// 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) } /// Save frames in flat binary format: /// [frameCount: UInt32][pointsPerFrame: UInt32][Float data...] func saveCache(frames: [[Float]], for trackId: String) throws { let frameCount = frames.count let pointsPerFrame = frames.first?.count ?? 0 guard frameCount > 0, pointsPerFrame > 0 else { return } var data = Data(capacity: 8 + frameCount * pointsPerFrame * MemoryLayout.size) 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) } /// Load frames as a flat VisFrameBuffer using memory-mapped I/O. /// No heap allocation for frame data — the OS maps file pages on demand. func loadCache(for trackId: String) throws -> VisFrameBuffer { // .alwaysMapped: OS memory-maps the file rather than reading into heap. // Pages are faulted in on first access, never fully copied. let raw = try Data(contentsOf: fileURL(for: trackId), options: .alwaysMapped) guard raw.count >= 8 else { return .empty } let frameCount = Int(raw.withUnsafeBytes { $0.load(fromByteOffset: 0, as: UInt32.self) }) let pointsPerFrame = Int(raw.withUnsafeBytes { $0.load(fromByteOffset: 4, as: UInt32.self) }) let floatSize = MemoryLayout.size let expectedSize = 8 + frameCount * pointsPerFrame * floatSize guard raw.count >= expectedSize, frameCount > 0, pointsPerFrame > 0 else { return .empty } // Copy mapped region into a single flat ContiguousArray. // One allocation for the entire track's data — never per-frame. let totalFloats = frameCount * pointsPerFrame var flat = ContiguousArray(repeating: 0, count: totalFloats) flat.withUnsafeMutableBufferPointer { dst in raw.withUnsafeBytes { src in dst.baseAddress!.initialize( from: src.baseAddress!.advanced(by: 8).assumingMemoryBound(to: Float.self), count: totalFloats ) } } return VisFrameBuffer(data: flat, frameCount: frameCount, pointsPerFrame: pointsPerFrame) } // MARK: - Cache Management 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) } } 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)) } }