Skip to content

audio hiccup when hovering mouse above youtube video thumbnail in Chrome on MacOS #884

@Wang-Yue

Description

@Wang-Yue

Please Confirm

  • I have read the FAQ and Wiki where most common issues can be resolved
  • I have searched Discussions to see if the same question has already been asked
  • This is a bug and not a question about audio routing or configuration, which should be posted in Discussions

macOS Version

macOS 26 Tahoe

BlackHole Build(s) Affected

  • 2 channel
  • 16 channel
  • 64 channel
  • other/custom build

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.

  1. install blackhole, Chrome, and camilladsp. The version I use is 3.0 and 4.0 (via next31 branch).
  2. run with the following config
---
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

  1. Play any music via blackhole
  2. Open Chrome browser, navigate to youtube.com, hover mouse over any video clip thumbnail

Observed behavior

You will hear hiccup.

Log

I found nothing wrong in the log when running into this issue:

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.

Additional comment

  1. This bug only occurs with Chrome. Safari seems fine.
  2. Chrome works fine when audio is not routed through camilladsp and blackhole. => hovering mouse over video thumbnail will not cause audio hiccups if I don't run camilladsp and blackhole
  3. Other websites (such as instagram / twitter) have same issue with mouse hovering over video clip thumbnail.

Expected Behavior

audio should not exibit hiccups when playing

Screenshots

Camilladsp can be replaced by the following raw CoreAudio swift program

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()

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions