Listen and zero in on that annoying cricket in your house.
import Foundation
/// A 3D LUT. `data` holds RGB triplets with red varying fastest, then green, then blue (the .cube order).
public struct LUT3D: Equatable, Sendable {
public var size: Int
public var data: [Float]
public var domainMin = Vec3(0, 0, 0)
public var domainMax = Vec3(1, 1, 1)
public var title: String = ""
public init(size: Int, data: [Float]) {
precondition(data.count == size * size * size * 3)
self.size = size; self.data = data
}
public static func identity(size: Int) -> LUT3D { bake(size: size) { $0 } }
/// Evaluate `f` at every grid point.
public static func bake(size n: Int, _ f: (Vec3) -> Vec3) -> LUT3D {
var d = [Float](repeating: 0, count: n * n * n * 3)
let s = 1.0 / Double(n - 1)
var i = 0
for b in 0..<n { for g in 0..<n { for r in 0..<n {
let o = f(Vec3(Double(r) * s, Double(g) * s, Double(b) * s))
d[i] = Float(o.x); d[i + 1] = Float(o.y); d[i + 2] = Float(o.z); i += 3
} } }
return LUT3D(size: n, data: d)
}
@inline(__always) private func at(_ r: Int, _ g: Int, _ b: Int) -> Vec3 {
let i = ((b * size + g) * size + r) * 3
return Vec3(Double(data[i]), Double(data[i + 1]), Double(data[i + 2]))
}
/// Tetrahedral interpolation (the method Resolve uses).
public func sample(_ v: Vec3) -> Vec3 {
let m = Double(size - 1)
func coord(_ x: Double, _ lo: Double, _ hi: Double) -> (Int, Double) {
let t = min(max((x - lo) / (hi - lo), 0), 1) * m
let i = min(Int(t), size - 2)
return (i, t - Double(i))
}
let (r0, fr) = coord(v.x, domainMin.x, domainMax.x)
let (g0, fg) = coord(v.y, domainMin.y, domainMax.y)
let (b0, fb) = coord(v.z, domainMin.z, domainMax.z)
let c000 = at(r0, g0, b0), c111 = at(r0 + 1, g0 + 1, b0 + 1)
if fr > fg {
if fg > fb {
let c100 = at(r0 + 1, g0, b0), c110 = at(r0 + 1, g0 + 1, b0)
return c000 * (1 - fr) + c100 * (fr - fg) + c110 * (fg - fb) + c111 * fb
Mac · iPhone · Colour
QuickLUT
Build camera LUTs for Apple Log footage, preview the grade live, and export .cube files and graded clips.
import Foundation
import CoreMediaIO
import CoreMedia
import CoreVideo
/// Pushes frames into the Camera Extension's sink stream (the extension republishes them as the "DSLR Connect" camera).
final class SinkSender {
static let deviceUID = "D51C0A11-7B3E-4F52-9C1A-0E5D2B5D3001"
static let sinkName = "DSLR Connect.Sink"
private let lock = NSLock()
private var deviceID: CMIODeviceID = 0
private var streamID: CMIOStreamID = 0
private var queue: CMSimpleQueue?
private var lastAttempt = Date.distantPast
private var failedEnqueues = 0
private var loggedMissing = false
private var loggedFirstSend = false
/// Lets CoreMediaIO deliver device add/remove notifications to this process, so a camera extension that is
/// installed or approved while the app is running shows up without restarting the app.
static func enableDeviceNotifications() {
var a = address(0x726E6C70) // kCMIOHardwarePropertyRunLoop ('rnlp')
var runLoop = CFRunLoopGetMain()
let st = CMIOObjectSetPropertyData(CMIOObjectID(kCMIOObjectSystemObject), &a, 0, nil,
UInt32(MemoryLayout<CFRunLoop?>.size), &runLoop)
if st != noErr { logLine("sink: could not set CoreMediaIO run loop (\(st))") }
}
private var loggedViewerFormat = false
/// How many apps are currently reading the camera, or nil if that can't be determined.
func viewerCount() -> Int? {
lock.lock(); defer { lock.unlock() }
guard queue != nil else { return nil }
var a = Self.address(0x76777273) // 'vwrs'
guard CMIOObjectHasProperty(deviceID, &a) else { return nil }
var size: UInt32 = 0
guard CMIOObjectGetPropertyDataSize(deviceID, &a, 0, nil, &size) == noErr, size >= 4 else { return nil }
var buf = [UInt8](repeating: 0, count: Int(size))
var used: UInt32 = 0
guard CMIOObjectGetPropertyData(deviceID, &a, 0, nil, size, &used, &buf) == noErr else { return nil }
Mac app · Camera extension · Website
DSLR Connect
Turns a DSLR into a webcam for Zoom, FaceTime and OBS, with a native menu-bar app and a virtual camera.
The craft behind it.
Logos, book covers, maps, product shoots and motion — and the tools I make it all with.