fix: MTAudioProcessingTapCallbacks参数名initCallback→init

修复编译错误:Swift bridging 期望的参数名是 init 而非 initCallback
This commit is contained in:
yumoqing 2026-06-24 23:16:08 +08:00
parent 5ae4cc4e70
commit d9f12ced06

View File

@ -1,50 +1,43 @@
import AVFoundation import AVFoundation
import CoreMedia import CoreMedia
import MediaToolbox
#if os(macOS) #if os(macOS)
import AppKit import AppKit
// MARK: - tapStorage C // MARK: -
class AudioCaptureContext { class AudioCaptureContext {
var writerInput: AVAssetWriterInput? var writerInput: AVAssetWriterInput?
nonisolated(unsafe) var isRunning: Bool = true nonisolated(unsafe) var isRunning: Bool = true
nonisolated(unsafe) var formatDescription: CMAudioFormatDescription?
nonisolated(unsafe) var appendCount: Int = 0 nonisolated(unsafe) var appendCount: Int = 0
nonisolated(unsafe) var formatDescription: CMAudioFormatDescription?
nonisolated(unsafe) var firstAppendLogged: Bool = false
} }
// MARK: - MTAudioProcessingTap C /// C 访线 isRunning
private nonisolated(unsafe) var gAudioContext: AudioCaptureContext?
private func tapInit(_ tap: MTAudioProcessingTap,
_ clientInfo: UnsafeMutableRawPointer?,
_ tapStorageOut: UnsafeMutablePointer<UnsafeMutableRawPointer?>) {
// clientInfo tapStorage
tapStorageOut.pointee = clientInfo
}
private func tapFinalize(_ tap: MTAudioProcessingTap) {
// context Unmanaged
guard let storage = Optional(MTAudioProcessingTapGetStorage(tap)),
let ctxPtr = storage.assumingMemoryBound(to: UnsafeMutableRawPointer?.self).pointee else { return }
Unmanaged<AudioCaptureContext>.fromOpaque(ctxPtr).release()
}
// MARK: - MTAudioProcessingTap C
private func tapPrepare(_ tap: MTAudioProcessingTap, private func tapPrepare(_ tap: MTAudioProcessingTap,
_ maxFrames: Int, _ maxFramesPerSlice: CMItemCount,
_ processingFormat: UnsafePointer<AudioStreamBasicDescription>) { _ processingFormat: UnsafePointer<AudioStreamBasicDescription>) {
guard let storage = Optional(MTAudioProcessingTapGetStorage(tap)), print("[Recorder] tapPrepare called, maxFrames=\(maxFramesPerSlice)")
let ctxPtr = storage.assumingMemoryBound(to: UnsafeMutableRawPointer?.self).pointee else { return } guard let ctx = gAudioContext else {
let ctx = Unmanaged<AudioCaptureContext>.fromOpaque(ctxPtr).takeUnretainedValue() print("[Recorder] tapPrepare: gAudioContext is nil!")
return
}
var asbd = processingFormat.pointee var asbd = processingFormat.pointee
var fmtDesc: CMAudioFormatDescription? print("[Recorder] tapPrepare: sampleRate=\(asbd.mSampleRate), channels=\(asbd.mChannelsPerFrame)")
CMAudioFormatDescriptionCreate(allocator: kCFAllocatorDefault, CMAudioFormatDescriptionCreate(allocator: kCFAllocatorDefault,
asbd: &asbd, asbd: &asbd,
layoutSize: 0, layout: nil, layoutSize: 0, layout: nil,
magicCookieSize: 0, magicCookie: nil, magicCookieSize: 0, magicCookie: nil,
extensions: nil, extensions: nil,
formatDescriptionOut: &fmtDesc) formatDescriptionOut: &ctx.formatDescription)
ctx.formatDescription = fmtDesc }
print("[Recorder] Audio tap prepared: \(asbd.mSampleRate)Hz, \(asbd.mChannelsPerFrame)ch, format=\(asbd.mFormatID)")
private func tapInit(_ tap: MTAudioProcessingTap,
_ clientInfo: UnsafeMutableRawPointer?,
_ tapStorageOut: UnsafeMutablePointer<UnsafeMutableRawPointer?>) {
// tap
} }
private func tapUnprepare(_ tap: MTAudioProcessingTap) { private func tapUnprepare(_ tap: MTAudioProcessingTap) {
@ -52,7 +45,7 @@ private func tapUnprepare(_ tap: MTAudioProcessingTap) {
} }
private func tapProcess(_ tap: MTAudioProcessingTap, private func tapProcess(_ tap: MTAudioProcessingTap,
_ numberOfFrames: CMItemCount, _ numberFrames: CMItemCount,
_ flags: MTAudioProcessingTapFlags, _ flags: MTAudioProcessingTapFlags,
_ bufferListInOut: UnsafeMutablePointer<AudioBufferList>, _ bufferListInOut: UnsafeMutablePointer<AudioBufferList>,
_ numberFramesOut: UnsafeMutablePointer<CMItemCount>, _ numberFramesOut: UnsafeMutablePointer<CMItemCount>,
@ -62,18 +55,14 @@ private func tapProcess(_ tap: MTAudioProcessingTap,
guard numberFrames > 0 else { return } guard numberFrames > 0 else { return }
// + //
var timeRange = CMTimeRange() var timeRange = CMTimeRange()
var srcFlags: UInt32 = 0 var srcFlags: MTAudioProcessingTapFlags = 0
var actualFrames: Int = 0 var actualFrames: CMItemCount = 0
let status = MTAudioProcessingTapGetSourceAudio( let status = MTAudioProcessingTapGetSourceAudio(
tap, tap, numberFrames, bufferListInOut,
numberFrames, &srcFlags, &timeRange, &actualFrames
bufferListInOut,
&srcFlags,
&timeRange,
&actualFrames
) )
numberFramesOut.pointee = actualFrames numberFramesOut.pointee = actualFrames
@ -81,71 +70,71 @@ private func tapProcess(_ tap: MTAudioProcessingTap,
guard status == noErr, actualFrames > 0 else { return } guard status == noErr, actualFrames > 0 else { return }
// context // context
guard let storage = Optional(MTAudioProcessingTapGetStorage(tap)), guard let ctx = gAudioContext, ctx.isRunning,
let ctxPtr = storage.assumingMemoryBound(to: UnsafeMutableRawPointer?.self).pointee else { return } let writerInput = ctx.writerInput,
let ctx = Unmanaged<AudioCaptureContext>.fromOpaque(ctxPtr).takeUnretainedValue()
guard ctx.isRunning, let writerInput = ctx.writerInput,
writerInput.isReadyForMoreMediaData else { return } writerInput.isReadyForMoreMediaData else { return }
let bufferList = bufferListInOut.pointee let bufferList = bufferListInOut.pointee
guard bufferList.mNumberBuffers > 0 else { return } guard bufferList.mNumberBuffers > 0 else { return }
// let buf = bufferList.mBuffers
var totalBytes: UInt32 = 0 let dataSize = Int(buf.mDataByteSize)
for i in 0..<Int(bufferList.mNumberBuffers) { guard dataSize > 0, let srcData = buf.mData else { return }
let buf = bufferList.mBuffers // Note: only single buffer for interleaved
totalBytes = buf.mDataByteSize
break
}
guard totalBytes > 0, let srcData = bufferList.mBuffers.mData else { return }
// CMBlockBuffer // CMBlockBuffer
var blockBuffer: CMBlockBuffer? var blockBuffer: CMBlockBuffer?
guard CMBlockBufferCreateWithMemoryBlock( let blockStatus = CMBlockBufferCreateWithMemoryBlock(
allocator: kCFAllocatorDefault, allocator: kCFAllocatorDefault, memoryBlock: nil,
memoryBlock: nil, blockLength: dataSize, blockAllocator: kCFAllocatorDefault,
blockLength: Int(totalBytes), customBlockSource: nil, offsetToData: 0,
blockAllocator: kCFAllocatorDefault, dataLength: dataSize,
customBlockSource: nil,
offsetToData: 0,
dataLength: Int(totalBytes),
flags: kCMBlockBufferAssureMemoryNowFlag, flags: kCMBlockBufferAssureMemoryNowFlag,
blockBufferOut: &blockBuffer blockBufferOut: &blockBuffer
) == kCMBlockBufferNoErr, let bb = blockBuffer else { return } )
guard blockStatus == kCMBlockBufferNoErr, let bb = blockBuffer else { return }
// let replaceStatus = CMBlockBufferReplaceDataBytes(
guard CMBlockBufferReplaceDataBytes( with: srcData, blockBuffer: bb,
with: srcData, offsetIntoDestination: 0, dataLength: dataSize
blockBuffer: bb, )
offsetIntoDestination: 0, guard replaceStatus == kCMBlockBufferNoErr else { return }
dataLength: Int(totalBytes)
) == kCMBlockBufferNoErr else { return }
// // format description
guard let fmtDesc = ctx.formatDescription else { return } guard let fd = ctx.formatDescription else { return }
// PTS
let sampleRate = Double(timeRange.duration.timescale) > 0
? Double(timeRange.duration.timescale) : 44100.0
let ptsValue = Double(ctx.appendCount) / sampleRate
let pts = CMTime(seconds: ptsValue, preferredTimescale: Int32(sampleRate))
// CMSampleBuffer使 timeRange start PTS
var sampleBuffer: CMSampleBuffer? var sampleBuffer: CMSampleBuffer?
let createStatus = CMAudioSampleBufferCreateReadyWithPacketDescriptions( let createStatus = CMAudioSampleBufferCreateReadyWithPacketDescriptions(
allocator: kCFAllocatorDefault, allocator: kCFAllocatorDefault,
dataBuffer: bb, dataBuffer: bb,
formatDescription: fmtDesc, formatDescription: fd,
sampleCount: actualFrames, sampleCount: actualFrames,
presentationTimeStamp: timeRange.start, presentationTimeStamp: pts,
packetDescriptions: nil, packetDescriptions: nil,
sampleBufferOut: &sampleBuffer sampleBufferOut: &sampleBuffer
) )
guard createStatus == noErr, let sb = sampleBuffer else { return } guard createStatus == noErr, let sb = sampleBuffer else { return }
if writerInput.append(sb) { if writerInput.append(sb) {
ctx.appendCount += actualFrames ctx.appendCount += actualFrames
if !ctx.firstAppendLogged {
ctx.firstAppendLogged = true
print("[Recorder] ✓ First audio frame appended! pts=\(pts.seconds)s, frames=\(actualFrames)")
}
} }
} }
/// AVPlayerItemVideoOutput + MTAudioProcessingTap // init/finalize
private func tapInit(_ tap: MTAudioProcessingTap) {}
private func tapFinalize(_ tap: MTAudioProcessingTap) {}
/// AVPlayerItemVideoOutput + MTAudioProcessingTap mp4
@MainActor @MainActor
final class PlayerRecorder: NSObject { final class PlayerRecorder: NSObject {
@ -157,12 +146,13 @@ final class PlayerRecorder: NSObject {
private var weakOutput: AVPlayerItemVideoOutput? private var weakOutput: AVPlayerItemVideoOutput?
nonisolated(unsafe) private var isRunning = false nonisolated(unsafe) private var isRunning = false
// //
private var lastPixelBuffer: CVPixelBuffer? private var lastPixelBuffer: CVPixelBuffer?
private var captureFrameCount: Int = 0 private var captureFrameCount: Int = 0
// (MTAudioProcessingTap) // (MTAudioProcessingTap)
private var audioTap: MTAudioProcessingTap? private var audioTap: MTAudioProcessingTap?
private var audioContext: AudioCaptureContext?
private weak var currentPlayerItem: AVPlayerItem? private weak var currentPlayerItem: AVPlayerItem?
nonisolated(unsafe) private var recordStartTime: CMTime = .zero nonisolated(unsafe) private var recordStartTime: CMTime = .zero
@ -236,14 +226,15 @@ final class PlayerRecorder: NSObject {
audioInput = aInput audioInput = aInput
} }
guard writer.startWriting() else { let success = writer.startWriting()
if !success {
print("[Recorder] startWriting failed: \(writer.error?.localizedDescription ?? "unknown")") print("[Recorder] startWriting failed: \(writer.error?.localizedDescription ?? "unknown")")
onError?("Cannot start writer: \(writer.error?.localizedDescription ?? "unknown")") onError?("Cannot start writer: \(writer.error?.localizedDescription ?? "unknown")")
return return
} }
writer.startSession(atSourceTime: .zero) writer.startSession(atSourceTime: .zero)
print("[Recorder] Writer started") print("[Recorder] Writer started, session at .zero")
// //
isRecording = true isRecording = true
@ -251,7 +242,7 @@ final class PlayerRecorder: NSObject {
startTimer() startTimer()
startCaptureLoop() startCaptureLoop()
// // MTAudioProcessingTap
setupAudioTap(playerItem: playerItem, audioWriterInput: aInput) setupAudioTap(playerItem: playerItem, audioWriterInput: aInput)
} }
@ -265,9 +256,16 @@ final class PlayerRecorder: NSObject {
stopTimer() stopTimer()
// tap // tap
audioContext?.isRunning = false
// audioMix tap
currentPlayerItem?.audioMix = nil currentPlayerItem?.audioMix = nil
currentPlayerItem = nil currentPlayerItem = nil
// tap context
audioTap = nil audioTap = nil
audioContext = nil
gAudioContext = nil
videoInput?.markAsFinished() videoInput?.markAsFinished()
audioInput?.markAsFinished() audioInput?.markAsFinished()
@ -275,8 +273,8 @@ final class PlayerRecorder: NSObject {
writer?.finishWriting { [weak self] in writer?.finishWriting { [weak self] in
Task { @MainActor in Task { @MainActor in
guard let self = self, let w = self.writer else { return } guard let self = self, let w = self.writer else { return }
let vFrames = self.captureFrameCount let count = self.captureFrameCount
print("[Recorder] finishWriting status: \(w.status.rawValue), error: \(w.error?.localizedDescription ?? "none"), video frames: \(vFrames)") print("[Recorder] finishWriting status: \(w.status.rawValue), error: \(w.error?.localizedDescription ?? "none"), video frames: \(count)")
if w.status == .completed, let url = self.tempURL { if w.status == .completed, let url = self.tempURL {
self.showSaveDialog(tempURL: url) self.showSaveDialog(tempURL: url)
} else { } else {
@ -299,22 +297,29 @@ final class PlayerRecorder: NSObject {
do { do {
let audioTracks = try await asset.loadTracks(withMediaType: .audio) let audioTracks = try await asset.loadTracks(withMediaType: .audio)
guard let audioTrack = audioTracks.first else { guard let audioTrack = audioTracks.first else {
print("[Recorder] No audio track for tap") print("[Recorder] ❌ No audio track for tap")
return
}
guard self.isRunning else {
print("[Recorder] ❌ isRunning=false, aborting audio tap setup")
return return
} }
guard self.isRunning else { return }
// context Unmanaged C print("[Recorder] Audio track found: \(audioTrack.trackID), setting up tap...")
//
let context = AudioCaptureContext() let context = AudioCaptureContext()
context.writerInput = audioWriterInput context.writerInput = audioWriterInput
context.isRunning = true context.isRunning = true
self.audioContext = context
let contextPtr = Unmanaged.passRetained(context).toOpaque() // C 访
gAudioContext = context
// // MTAudioProcessingTap
var callbacks = MTAudioProcessingTapCallbacks( var callbacks = MTAudioProcessingTapCallbacks(
version: kMTAudioProcessingTapCallbacksVersion_0, version: kMTAudioProcessingTapCallbacksVersion_0,
clientInfo: contextPtr, clientInfo: nil,
init: tapInit, init: tapInit,
finalize: tapFinalize, finalize: tapFinalize,
prepare: tapPrepare, prepare: tapPrepare,
@ -323,33 +328,31 @@ final class PlayerRecorder: NSObject {
) )
var tap: MTAudioProcessingTap? var tap: MTAudioProcessingTap?
let status = withUnsafePointer(to: &callbacks) { cbPtr in let status = MTAudioProcessingTapCreate(
MTAudioProcessingTapCreate( kCFAllocatorDefault,
kCFAllocatorDefault, &callbacks,
cbPtr, kMTAudioProcessingTapCreationFlag_PostEffects,
kMTAudioProcessingTapCreationFlag_PostEffects, &tap
&tap )
)
}
guard status == noErr, let audioTap = tap else { guard status == noErr, let audioTap = tap else {
print("[Recorder] MTAudioProcessingTapCreate failed: \(status)") print("[Recorder] MTAudioProcessingTapCreate failed: \(status)")
Unmanaged<AudioCaptureContext>.fromOpaque(contextPtr).release() gAudioContext = nil
return return
} }
self.audioTap = audioTap self.audioTap = audioTap
// audioMix // audioMix tap
let params = AVMutableAudioMixInputParameters(track: audioTrack) let params = AVMutableAudioMixInputParameters(track: audioTrack)
params.audioTapProcessor = audioTap params.audioTapProcessor = audioTap
let audioMix = AVMutableAudioMix() let audioMix = AVMutableAudioMix()
audioMix.inputParameters = [params] audioMix.inputParameters = [params]
playerItem.audioMix = audioMix playerItem.audioMix = audioMix
print("[Recorder] Audio tap installed") print("[Recorder] Audio tap installed on player item")
} catch { } catch {
print("[Recorder] Audio tap setup failed: \(error)") print("[Recorder] Audio tap setup failed: \(error)")
} }
} }
} }
@ -372,12 +375,14 @@ final class PlayerRecorder: NSObject {
let hostTime = CACurrentMediaTime() let hostTime = CACurrentMediaTime()
let itemTime = output.itemTime(forHostTime: hostTime) let itemTime = output.itemTime(forHostTime: hostTime)
//
if let pb = output.copyPixelBuffer(forItemTime: itemTime, itemTimeForDisplay: nil) { if let pb = output.copyPixelBuffer(forItemTime: itemTime, itemTimeForDisplay: nil) {
lastPixelBuffer = pb lastPixelBuffer = pb
} }
guard let pb = lastPixelBuffer else { return } guard let pb = lastPixelBuffer else { return }
//
let relativeTime = CMTimeSubtract(itemTime, recordStartTime) let relativeTime = CMTimeSubtract(itemTime, recordStartTime)
guard relativeTime.seconds >= 0 else { return } guard relativeTime.seconds >= 0 else { return }
@ -405,7 +410,7 @@ final class PlayerRecorder: NSObject {
return sampleBuffer return sampleBuffer
} }
// MARK: - Timer // MARK: - Timer ()
private func startTimer() { private func startTimer() {
startDate = Date() startDate = Date()
durationText = "00:00" durationText = "00:00"