70 lines
3.6 KiB
Swift
70 lines
3.6 KiB
Swift
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)
|
|
}
|
|
}
|