import Foundation import CoreGraphics /// CD+G packet decoder. Packet parity is ignored; malformed coordinates are rejected. struct CDGDecoder: Sendable { private(set) var pixels = [UInt8](repeating: 0, count: 300 * 216) private(set) var palette = [UInt32](repeating: 0x000000FF, count: 16) private(set) var packetIndex = 0 private(set) var horizontalOffset = 0 private(set) var verticalOffset = 0 private var transparent: UInt8? mutating func render(_ data: Data, at seconds: Double, cancelled: () -> Bool = { false }) { let target = min(data.count / 24, max(0, Int(max(0, seconds) * 300))) if target < packetIndex { self = CDGDecoder() } while packetIndex < target { if packetIndex % 300 == 0 && cancelled() { return } apply(Array(data[(packetIndex * 24)..<(packetIndex * 24 + 24)])) packetIndex += 1 } } mutating func apply(_ packet: [UInt8]) { guard packet.count == 24, packet[0] & 63 == 9 else { return } let d = packet[4..<20].map { Int($0 & 63) } switch packet[1] & 63 { case 1: if d[1] == 0 { pixels = .init(repeating: UInt8(d[0] & 15), count: pixels.count) } case 2: for y in 0..<216 { for x in 0..<300 where x < 6 || x >= 294 || y < 12 || y >= 204 { pixels[y * 300 + x] = UInt8(d[0] & 15) } } case 6, 38: let row = d[2] & 31, col = d[3] & 63 guard row < 18, col < 50 else { return } for y in 0..<12 { for x in 0..<6 { let color = UInt8(d[(y + 4)] & (1 << (5 - x)) == 0 ? d[0] & 15 : d[1] & 15) let i = (row * 12 + y) * 300 + col * 6 + x if packet[1] & 63 == 38 { pixels[i] ^= color } else { pixels[i] = color } } } case 20, 24: horizontalOffset = min(d[1] & 7, 5); verticalOffset = min(d[2] & 15, 11) let hx = (d[1] >> 4) & 3, vy = (d[2] >> 4) & 3 let dx = hx == 1 ? 6 : hx == 2 ? -6 : 0 let dy = vy == 1 ? 12 : vy == 2 ? -12 : 0 let old = pixels for y in 0..<216 { for x in 0..<300 { let sx = x - dx, sy = y - dy if packet[1] & 63 == 24 || (sx >= 0 && sx < 300 && sy >= 0 && sy < 216) { pixels[y * 300 + x] = old[((sy + 216) % 216) * 300 + (sx + 300) % 300] } else { pixels[y * 300 + x] = UInt8(d[0] & 15) } } } case 28: transparent = UInt8(d[0] & 15) case 30, 31: let start = packet[1] & 63 == 30 ? 0 : 8 for i in 0..<8 { let v = (d[i * 2] << 6) | d[i * 2 + 1] palette[start + i] = UInt32(((v >> 8) & 15) * 17) << 24 | UInt32(((v >> 4) & 15) * 17) << 16 | UInt32((v & 15) * 17) << 8 | 255 } default: break } } func image() -> CGImage? { var rgba = [UInt8](); rgba.reserveCapacity(288 * 192 * 4) for y in 12..<204 { for x in 6..<294 { let index = pixels[(y + verticalOffset) * 300 + x + horizontalOffset] let c = palette[Int(index)] rgba += [UInt8((c >> 24) & 255), UInt8((c >> 16) & 255), UInt8((c >> 8) & 255), index == transparent ? 0 : 255] } } guard let provider = CGDataProvider(data: Data(rgba) as CFData) else { return nil } return CGImage(width: 288, height: 192, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: 288 * 4, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.last.rawValue), provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent) } }