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