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
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
|
|
}
|
|
}
|