100% reproducible.
---
devices:
samplerate: 44100
chunksize: 1024
enable_rate_adjust: true
capture:
type: CoreAudio
device: "BlackHole 2ch"
channels: 2
format: F32
playback:
type: CoreAudio
channels: 2
device: "MacBook Pro Speakers"
format: F32
exclusive: true
You will hear hiccup.
2026-03-03 01:16:57.965739 DEBUG [camillalib::coreaudiodevice] <src/coreaudiodevice.rs:1021> Rate watcher, measured sample rate is 44104.6 Hz.
2026-03-03 01:16:58.987274 DEBUG [camillalib::coreaudiodevice] <src/coreaudiodevice.rs:998> Measured sample rate is 44100.1 Hz.
2026-03-03 01:16:58.987342 DEBUG [camillalib::coreaudiodevice] <src/coreaudiodevice.rs:1021> Rate watcher, measured sample rate is 44103.3 Hz.
2026-03-03 01:17:00.009002 DEBUG [camillalib::coreaudiodevice] <src/coreaudiodevice.rs:998> Measured sample rate is 44098.1 Hz.
import Foundation
import CoreAudio
// --- Configuration ---
let inputDeviceName = "BlackHole 2ch"
let outputDeviceName = "MacBook Pro Speakers"
let bufferSize: UInt32 = 1024 * 128 // 128KB buffer
class AudioCircle {
var data = UnsafeMutableRawPointer.allocate(byteCount: Int(bufferSize), alignment: MemoryLayout<Float>.alignment)
var writeOffset: Int = 0
var readOffset: Int = 0
}
let sharedBuffer = AudioCircle()
// --- Helper: Find Device ID ---
func findDeviceID(named name: String) -> AudioDeviceID? {
var propertyAddress = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDevices,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
var dataSize: UInt32 = 0
AudioObjectGetPropertyDataSize(AudioObjectID(kAudioObjectSystemObject), &propertyAddress, 0, nil, &dataSize)
let deviceCount = Int(dataSize) / MemoryLayout<AudioDeviceID>.size
var deviceIDs = [AudioDeviceID](repeating: 0, count: deviceCount)
AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &propertyAddress, 0, nil, &dataSize, &deviceIDs)
for id in deviceIDs {
var nameSize = UInt32(MemoryLayout<CFString?>.size)
var deviceName: CFString?
var nameAddress = AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyDeviceNameCFString,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
AudioObjectGetPropertyData(id, &nameAddress, 0, nil, &nameSize, &deviceName)
if let nameStr = deviceName as String?, nameStr == name { return id }
}
return nil
}
// 1. Get Device IDs
guard let inputID = findDeviceID(named: inputDeviceName),
let outputID = findDeviceID(named: outputDeviceName) else {
print("Devices not found."); exit(1)
}
// 2. Input Callback (Device A -> Buffer)
let inputProc: AudioDeviceIOProc = { (inDevice, inNow, inInputData, inInputTime, outOutputData, inOutputTime, inClientData) -> OSStatus in
let ring = Unmanaged<AudioCircle>.fromOpaque(inClientData!).takeUnretainedValue()
// Access the first buffer in the list
let inputBuf = inInputData.pointee.mBuffers
if let src = inputBuf.mData {
let size = Int(inputBuf.mDataByteSize)
// Simple wrap-around logic
if ring.writeOffset + size <= Int(bufferSize) {
memcpy(ring.data + ring.writeOffset, src, size)
}
ring.writeOffset = (ring.writeOffset + size) % Int(bufferSize)
}
return noErr
}
// 3. Output Callback (Buffer -> Device B)
let outputProc: AudioDeviceIOProc = { (inDevice, inNow, inInputData, inInputTime, outOutputData, inOutputTime, inClientData) -> OSStatus in
let ring = Unmanaged<AudioCircle>.fromOpaque(inClientData!).takeUnretainedValue()
// Corrected access: Get the pointer to the AudioBuffer within the AudioBufferList
let outBufferList = outOutputData.pointee
let size = Int(outBufferList.mBuffers.mDataByteSize)
if let dest = outBufferList.mBuffers.mData {
if ring.readOffset + size <= Int(bufferSize) {
memcpy(dest, ring.data + ring.readOffset, size)
}
ring.readOffset = (ring.readOffset + size) % Int(bufferSize)
}
return noErr
}
// 4. Register and Start
var inputProcID: AudioDeviceIOProcID?
var outputProcID: AudioDeviceIOProcID?
let context = Unmanaged.passUnretained(sharedBuffer).toOpaque()
// Create IO Procs
AudioDeviceCreateIOProcID(inputID, inputProc, context, &inputProcID)
AudioDeviceCreateIOProcID(outputID, outputProc, context, &outputProcID)
// Start Hardware
AudioDeviceStart(inputID, inputProcID)
AudioDeviceStart(outputID, outputProcID)
print("🚀 CoreAudio Router Active: \(inputDeviceName) -> \(outputDeviceName)")
print("Check Audio MIDI Setup to ensure both are at the same Sample Rate.")
CFRunLoopRun()
Please Confirm
macOS Version
macOS 26 Tahoe
BlackHole Build(s) Affected
Describe the bug
similar to #273
First reported to camilladsp HEnquist/camilladsp#436 but quickly realized it's not a camilladsp issue.
Reproduction Steps
Step to repro
100% reproducible.
Observed behavior
You will hear hiccup.
Log
I found nothing wrong in the log when running into this issue:
Additional comment
Expected Behavior
audio should not exibit hiccups when playing
Screenshots
Camilladsp can be replaced by the following raw CoreAudio swift program