You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

377 lines
12 KiB

import AVFoundation
import os.log
import Accelerate
/**
* 实时音频管理器
*
* 负责音频的录制和播放,对应Android示例中的AudioRecord和AudioTrack功能
*/
class RealtimeAudioManager {
private let log = OSLog(subsystem: "com.yunqiinnovation.realtime", category: "RealtimeAudioManager")
// 音频配置
private var audioConfig: AudioConfig?
// 录音相关
private var audioEngine: AVAudioEngine?
private var inputNode: AVAudioInputNode?
private var recordingFormat: AVAudioFormat?
private var isRecording = false
// 播放相关
private var audioPlayer: AVAudioPlayerNode?
private var playbackFormat: AVAudioFormat?
private var playbackQueue: DispatchQueue
private var audioBufferQueue: [AVAudioPCMBuffer] = []
private var isPlaying = false
// 回调
var onAudioData: ((Data) -> Void)?
var onStatusChanged: ((VoiceStatus) -> Void)?
var onRmsChanged: ((Float) -> Void)?
// 录音缓冲区
private var recordingBuffer: AVAudioPCMBuffer?
private var frameSize: Int = 320 // 20ms @ 16kHz
init() {
playbackQueue = DispatchQueue(label: "com.realtime.playback", qos: .userInitiated)
}
/// 激活会话,抢占音频焦点(暂停外部和内部其他音频)
private func activateAudioSession() {
do {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.playAndRecord,
mode: .voiceChat,
options: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP])
try session.setActive(true, options: .notifyOthersOnDeactivation)
os_log("音频会话已激活(抢占焦点)", log: log, type: .info)
} catch {
os_log("音频会话激活失败: %@", log: log, type: .error, error.localizedDescription)
}
}
/// 释放会话,归还音频焦点(让外部和内部其他音频恢复)
private func deactivateAudioSession() {
do {
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
os_log("音频会话已释放(归还焦点)", log: log, type: .info)
} catch {
os_log("音频会话释放失败: %@", log: log, type: .error, error.localizedDescription)
}
}
/// 初始化音频管理器
func initialize(config: AudioConfig) -> Bool {
self.audioConfig = config
self.frameSize = config.frameSize
guard setupAudioEngine(config: config) else {
os_log("音频引擎初始化失败", log: log, type: .error)
return false
}
guard setupAudioPlayer(config: config) else {
os_log("音频播放器初始化失败", log: log, type: .error)
return false
}
os_log("音频管理器初始化成功", log: log, type: .info)
return true
}
/// 设置音频引擎(录音)
private func setupAudioEngine(config: AudioConfig) -> Bool {
audioEngine = AVAudioEngine()
guard let audioEngine = audioEngine else { return false }
inputNode = audioEngine.inputNode
// 创建录音格式:16kHz, 16-bit, 单声道
recordingFormat = AVAudioFormat(commonFormat: .pcmFormatInt16,
sampleRate: Double(config.sampleRate),
channels: AVAudioChannelCount(config.channels),
interleaved: true)
guard let recordingFormat = recordingFormat else {
os_log("无法创建录音格式", log: log, type: .error)
return false
}
// 创建录音缓冲区
let frameCount = AVAudioFrameCount(config.frameSize / (config.bitsPerSample / 8))
recordingBuffer = AVAudioPCMBuffer(pcmFormat: recordingFormat, frameCapacity: frameCount)
return true
}
/// 设置音频播放器
private func setupAudioPlayer(config: AudioConfig) -> Bool {
guard let audioEngine = audioEngine else { return false }
audioPlayer = AVAudioPlayerNode()
// 创建播放格式:16kHz, 16-bit, 单声道
playbackFormat = AVAudioFormat(commonFormat: .pcmFormatInt16,
sampleRate: Double(config.sampleRate),
channels: AVAudioChannelCount(config.channels),
interleaved: true)
guard let audioPlayer = audioPlayer,
let playbackFormat = playbackFormat else {
os_log("无法创建播放格式", log: log, type: .error)
return false
}
// 连接播放器到音频引擎
audioEngine.attach(audioPlayer)
audioEngine.connect(audioPlayer, to: audioEngine.outputNode, format: playbackFormat)
return true
}
/// 开始录音
func startRecording() -> Bool {
guard let audioEngine = audioEngine,
let inputNode = inputNode,
let recordingFormat = recordingFormat,
!isRecording else {
return false
}
// 抢占音频焦点(暂停外部和内部其他音乐)
activateAudioSession()
// 安装录音tap
inputNode.installTap(onBus: 0, bufferSize: AVAudioFrameCount(frameSize / 2), format: recordingFormat) { [weak self] buffer, time in
self?.processRecordedAudio(buffer: buffer)
}
do {
try audioEngine.start()
isRecording = true
onStatusChanged?(.recording)
os_log("开始录音", log: log, type: .info)
return true
} catch {
os_log("开始录音失败: %@", log: log, type: .error, error.localizedDescription)
return false
}
}
/// 处理录制的音频数据
private func processRecordedAudio(buffer: AVAudioPCMBuffer) {
guard let channelData = buffer.int16ChannelData?[0],
buffer.frameLength > 0 else {
return
}
// 确保数据长度匹配frameSize
let dataLength = min(Int(buffer.frameLength) * 2, frameSize) // 2 bytes per sample
// 转换为Data
let data = Data(bytes: channelData, count: dataLength)
// 计算RMS值
let rms = calculateRMS(buffer: buffer)
onRmsChanged?(rms)
// 回调音频数据
onAudioData?(data)
}
/// 停止录音
func stopRecording() -> Bool {
guard let audioEngine = audioEngine,
let inputNode = inputNode,
isRecording else {
return false
}
inputNode.removeTap(onBus: 0)
audioEngine.stop()
isRecording = false
onStatusChanged?(.idle)
os_log("停止录音", log: log, type: .info)
// 录音和播放都停止时归还焦点,让外部音乐恢复
if !isPlaying {
deactivateAudioSession()
}
return true
}
/// 播放音频数据
func playAudioData(_ data: Data) {
guard let playbackFormat = playbackFormat,
let audioPlayer = audioPlayer else {
return
}
playbackQueue.async { [weak self] in
self?.enqueueAudioData(data, format: playbackFormat, player: audioPlayer)
}
}
/// 将音频数据加入播放队列
private func enqueueAudioData(_ data: Data, format: AVAudioFormat, player: AVAudioPlayerNode) {
// 创建PCM缓冲区
let frameCount = AVAudioFrameCount(data.count / 2) // 16-bit = 2 bytes per sample
guard let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: frameCount) else {
os_log("无法创建播放缓冲区", log: log, type: .error)
return
}
buffer.frameLength = frameCount
// 复制数据到缓冲区
guard let channelData = buffer.int16ChannelData?[0] else {
return
}
data.withUnsafeBytes { bytes in
let int16Pointer = bytes.bindMemory(to: Int16.self)
channelData.update(from: int16Pointer.baseAddress!, count: Int(frameCount))
}
// 计算播放数据的RMS值
let rms = calculateRMS(buffer: buffer)
DispatchQueue.main.async { [weak self] in
self?.onRmsChanged?(rms)
}
// 加入播放队列
audioBufferQueue.append(buffer)
// 如果没在播放,开始播放
if !isPlaying {
startPlayback()
}
}
/// 开始播放
private func startPlayback() {
guard let audioEngine = audioEngine,
let audioPlayer = audioPlayer,
!audioBufferQueue.isEmpty else {
return
}
if !audioEngine.isRunning {
do {
try audioEngine.start()
} catch {
os_log("启动音频引擎失败: %@", log: log, type: .error, error.localizedDescription)
return
}
}
if !audioPlayer.isPlaying {
audioPlayer.play()
}
isPlaying = true
onStatusChanged?(.playing)
// 播放队列中的音频
playNextBuffer()
}
/// 播放下一个缓冲区
private func playNextBuffer() {
guard let audioPlayer = audioPlayer,
!audioBufferQueue.isEmpty else {
isPlaying = false
onStatusChanged?(.idle)
return
}
let buffer = audioBufferQueue.removeFirst()
audioPlayer.scheduleBuffer(buffer) { [weak self] in
DispatchQueue.main.async {
self?.playNextBuffer()
}
}
}
/// 停止播放
func stopPlaying() -> Bool {
guard let audioPlayer = audioPlayer else {
return false
}
audioPlayer.stop()
audioBufferQueue.removeAll()
isPlaying = false
onStatusChanged?(.idle)
os_log("停止播放", log: log, type: .info)
// 录音和播放都停止时归还焦点,让外部音乐恢复
if !isRecording {
deactivateAudioSession()
}
return true
}
/// 更新配置
func updateConfig(config: AudioConfig) -> Bool {
// 如果正在录音或播放,先停止
if isRecording {
_ = stopRecording()
}
if isPlaying {
_ = stopPlaying()
}
return initialize(config: config)
}
/// 释放资源
func dispose() {
_ = stopRecording()
_ = stopPlaying()
audioEngine?.stop()
audioEngine = nil
audioPlayer = nil
audioBufferQueue.removeAll()
os_log("音频管理器已释放", log: log, type: .info)
}
/// 计算音频缓冲区的RMS值
private func calculateRMS(buffer: AVAudioPCMBuffer) -> Float {
guard let channelData = buffer.int16ChannelData?[0],
buffer.frameLength > 0 else {
return 0.0
}
let frameCount = Int(buffer.frameLength)
let channelDataPointer = UnsafeMutablePointer(mutating: channelData)
// 创建浮点缓冲区用于转换
var floatData = [Float](repeating: 0.0, count: frameCount)
// 1. 将16位整型转换为浮点型
vDSP_vflt16(channelDataPointer, 1, &floatData, 1, vDSP_Length(frameCount))
// 2. 归一化到[-1.0, 1.0]范围
var normalizedData = [Float](repeating: 0.0, count: frameCount)
var divisor: Float = 32768.0
vDSP_vsdiv(floatData, 1, &divisor, &normalizedData, 1, vDSP_Length(frameCount))
// 3. 计算均方值
var meanSquare: Float = 0.0
vDSP_measqv(normalizedData, 1, &meanSquare, vDSP_Length(frameCount))
// 4. 计算RMS值
let rms = sqrt(meanSquare)
return rms
}
}