8 changed files with 1024 additions and 607 deletions
@ -0,0 +1,534 @@ |
|||||
|
import Foundation |
||||
|
import AVFoundation |
||||
|
import MicrosoftCognitiveServicesSpeech |
||||
|
import os.log |
||||
|
|
||||
|
/** |
||||
|
* 简单音频接收器类,用于处理音频录制和流传输 |
||||
|
* 对应Android的SimpleAudioReceiver功能 |
||||
|
*/ |
||||
|
public class SimpleAudioReceiver: NSObject { |
||||
|
|
||||
|
private let tag = "SimpleAudioReceiver" |
||||
|
private let log = OSLog(subsystem: "com.azure.speech", category: "SimpleAudioReceiver") |
||||
|
|
||||
|
/** |
||||
|
* 音频来源类型 |
||||
|
*/ |
||||
|
public enum AudioSourceType { |
||||
|
/** 使用设备麦克风 */ |
||||
|
case microphone |
||||
|
/** 使用外部提供的音频数据 */ |
||||
|
case external |
||||
|
} |
||||
|
|
||||
|
// MARK: - 音频流类 |
||||
|
|
||||
|
public private(set) var pushAudioStream: SPXPushAudioInputStream? |
||||
|
private let writeQueue = LinkedBlockingQueue<Data>() |
||||
|
private var audioEngine: AVAudioEngine? |
||||
|
private var audioFormat: AVAudioFormat? |
||||
|
private let audioSession = AVAudioSession.sharedInstance() |
||||
|
private var audioSourceType = AudioSourceType.microphone |
||||
|
private var audioDataCallback: AudioDataCallback? |
||||
|
private var isRunning = false |
||||
|
public var _isWriting = false |
||||
|
private let bufferSize: Int = 4096 |
||||
|
private var writeThread: DispatchQueue? |
||||
|
private var currentRoute: AudioOutputRoute? |
||||
|
//public var onAudioData: ((Data) -> Void)? |
||||
|
public var recordfile: RecordFile? |
||||
|
// 添加对外部类的弱引用 |
||||
|
private weak var parentHelper: AzureAsrHelper? |
||||
|
|
||||
|
// 添加初始化方法,接收外部类引用 |
||||
|
init(parentHelper: AzureAsrHelper) { |
||||
|
self.parentHelper = parentHelper |
||||
|
super.init() |
||||
|
} |
||||
|
/** |
||||
|
* 初始化 |
||||
|
*/ |
||||
|
/** |
||||
|
* 初始化音频录制组件 |
||||
|
* 包括音频格式、推流、音频引擎等核心组件的初始化 |
||||
|
*/ |
||||
|
public func initAudioRecord() { |
||||
|
print("初始化了") |
||||
|
audioFormat = getOptimalAudioFormat() |
||||
|
pushAudioStream = SPXPushAudioInputStream() |
||||
|
|
||||
|
// 初始化音频引擎 |
||||
|
audioEngine = AVAudioEngine() |
||||
|
|
||||
|
isRunning = true |
||||
|
// 创建新的写线程 |
||||
|
writeThread = DispatchQueue(label: "audio.stream.writer") |
||||
|
startWriteThread() |
||||
|
} |
||||
|
|
||||
|
/// 获取最佳音频格式 (iOS 通常支持标准采样率) |
||||
|
public func getOptimalAudioFormat() -> AVAudioFormat? { |
||||
|
let sampleRate: Double = 16000 // iOS 通常支持 16kHz |
||||
|
return AVAudioFormat( |
||||
|
commonFormat: .pcmFormatInt16, |
||||
|
sampleRate: sampleRate, |
||||
|
channels: 1, |
||||
|
interleaved: true |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置音频配置 |
||||
|
* @param sampleRate 采样率,默认16000 |
||||
|
* @param channels 声道数,默认1 |
||||
|
*/ |
||||
|
/** |
||||
|
* 设置音频配置参数 |
||||
|
* @param sampleRate 采样率 |
||||
|
* @param channels 声道数 |
||||
|
*/ |
||||
|
public func setAudioConfig(sampleRate: Int, channels: Int) { |
||||
|
print( |
||||
|
"AudioStream设置音频配置: sampleRate=\(sampleRate), channels=\(channels)" |
||||
|
) |
||||
|
|
||||
|
// 如果已经有推流,重新创建 |
||||
|
if pushAudioStream != nil { |
||||
|
// 修复:安全解包 SPXAudioStreamFormat |
||||
|
guard let audioStreamFormat = SPXAudioStreamFormat.init( |
||||
|
usingPCMWithSampleRate: UInt(sampleRate), |
||||
|
bitsPerSample: 16, |
||||
|
channels: UInt(channels) |
||||
|
) else { |
||||
|
print("创建音频流格式失败") |
||||
|
return |
||||
|
} |
||||
|
pushAudioStream = SPXPushAudioInputStream(audioFormat: audioStreamFormat) |
||||
|
} |
||||
|
|
||||
|
print("AudioStream音频配置设置完成") |
||||
|
} |
||||
|
/** |
||||
|
* 开始音频输入 |
||||
|
*/ |
||||
|
public func startAudioRecord(audioSourceType: AudioSourceType = .microphone, audioDataCallback: AudioDataCallback?) { |
||||
|
_isWriting=true |
||||
|
self.audioSourceType = audioSourceType; |
||||
|
self.audioDataCallback = audioDataCallback |
||||
|
print("startAudioRecord=audioSourceType\(audioSourceType)") |
||||
|
switch audioSourceType { |
||||
|
case .microphone: |
||||
|
runMicrophoneCapture() |
||||
|
case .external: |
||||
|
runExternalCapture() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 开启音频写入线程 |
||||
|
* 修复:使用userInitiated QoS避免优先级反转 |
||||
|
*/ |
||||
|
public func startWriteThread() { |
||||
|
// 使用userInitiated QoS匹配音频录制线程的优先级 |
||||
|
writeThread = DispatchQueue(label: "audio.stream.writer", qos: .userInitiated) |
||||
|
|
||||
|
writeThread?.async { [weak self] in |
||||
|
guard let self = self else { return } |
||||
|
|
||||
|
while self.isRunning { |
||||
|
if !self._isWriting { |
||||
|
print("startWriteThread 111") |
||||
|
usleep(10_000) |
||||
|
continue |
||||
|
} |
||||
|
print("startWriteThread 222") |
||||
|
guard let dataToWrite = self.writeQueue.take() else { continue } |
||||
|
|
||||
|
do { |
||||
|
if let callback = self.audioDataCallback { |
||||
|
callback.onAudio(dataToWrite) |
||||
|
} |
||||
|
try self.pushAudioStream?.write(dataToWrite) |
||||
|
if self.recordfile != nil { |
||||
|
print("写入recordfile数据长度: \(dataToWrite.count)") |
||||
|
recordfile?.saveAudioDataToWav(dataToWrite) |
||||
|
} |
||||
|
print("写入数据长度: \(dataToWrite.count)") |
||||
|
} catch { |
||||
|
print("推送音频数据失败: \(error.localizedDescription)") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 向音频流写入音频数据 |
||||
|
* 仅当音频源设置为external时有效 |
||||
|
* |
||||
|
* @param data 音频数据字节数组 |
||||
|
*/ |
||||
|
public func saveAudioDataTo(data: Data) { |
||||
|
// 通过父类引用调用方法 |
||||
|
if audioSourceType != .external || !(parentHelper?.isContinuousRecognitionActive() ?? false) { |
||||
|
return |
||||
|
} |
||||
|
// print("外部data=\(data)") |
||||
|
// 放入队列,由写线程写入 |
||||
|
writeQueue.put(data) |
||||
|
} |
||||
|
|
||||
|
// 私有方法:启动麦克风捕获 |
||||
|
/** |
||||
|
* 启动麦克风捕获 |
||||
|
* 优化:复用已初始化的音频引擎,减少启动延迟 |
||||
|
*/ |
||||
|
private func runMicrophoneCapture() { |
||||
|
do { |
||||
|
// 【新增】首先配置音频会话 |
||||
|
try audioSession.setCategory( |
||||
|
.playAndRecord, |
||||
|
mode: .measurement, // 使用 measurement 模式获得最佳录音质量 |
||||
|
options: [.allowBluetooth, .defaultToSpeaker] |
||||
|
) |
||||
|
|
||||
|
// 【新增】请求麦克风权限(如果尚未授权) |
||||
|
if audioSession.recordPermission != .granted { |
||||
|
audioSession.requestRecordPermission { granted in |
||||
|
if !granted { |
||||
|
print("麦克风权限被拒绝") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 【新增】激活音频会话(在配置音频引擎之前) |
||||
|
try audioSession.setActive(true, options: [.notifyOthersOnDeactivation]) |
||||
|
|
||||
|
// 检查音频引擎是否已初始化,避免重复创建 |
||||
|
if audioEngine == nil { |
||||
|
audioEngine = AVAudioEngine() |
||||
|
} |
||||
|
|
||||
|
// 如果音频引擎正在运行,先停止 |
||||
|
if audioEngine?.isRunning == true { |
||||
|
audioEngine?.stop() |
||||
|
} |
||||
|
|
||||
|
// 获取音频输入节点(麦克风) |
||||
|
guard let inputNode = audioEngine?.inputNode else { |
||||
|
throw NSError(domain: "AudioSetup", code: 1, userInfo: [NSLocalizedDescriptionKey: "无法获取音频输入节点"]) |
||||
|
} |
||||
|
|
||||
|
// 移除之前的音频处理块,避免重复添加 |
||||
|
inputNode.removeTap(onBus: 0) |
||||
|
|
||||
|
// 获取硬件支持的原始音频格式 |
||||
|
let hardwareFormat = inputNode.inputFormat(forBus: 0) |
||||
|
|
||||
|
// iOS 13+ 启用语音处理 |
||||
|
if #available(iOS 13.0, *) { |
||||
|
try inputNode.setVoiceProcessingEnabled(true) |
||||
|
} |
||||
|
|
||||
|
// 检查目标音频格式和转换器是否可用 |
||||
|
guard let targetFormat = audioFormat, // 外部定义的期望音频格式 |
||||
|
let converter = AVAudioConverter(from: hardwareFormat, to: targetFormat) else { |
||||
|
throw NSError(domain: "AudioSetup", code: 2) |
||||
|
} |
||||
|
|
||||
|
// 在输入节点上安装录音回调 |
||||
|
inputNode.installTap(onBus: 0, |
||||
|
bufferSize: UInt32(bufferSize), // 每次回调的缓冲区大小 |
||||
|
format: hardwareFormat) { // 使用原始硬件格式 |
||||
|
[weak self] buffer, time in // 弱引用避免循环引用 |
||||
|
|
||||
|
// 确保实例存在且正在写入状态 |
||||
|
guard let self = self, self._isWriting else { return } |
||||
|
|
||||
|
// 创建目标格式的音频缓冲区 |
||||
|
let convertedBuffer = AVAudioPCMBuffer( |
||||
|
pcmFormat: targetFormat, |
||||
|
// 计算转换后的帧容量(考虑采样率差异) |
||||
|
frameCapacity: AVAudioFrameCount( |
||||
|
targetFormat.sampleRate * Double(buffer.frameLength) / buffer.format.sampleRate |
||||
|
) |
||||
|
)! |
||||
|
|
||||
|
var error: NSError? |
||||
|
// 执行音频格式转换 |
||||
|
let status = converter.convert( |
||||
|
to: convertedBuffer, |
||||
|
error: &error, |
||||
|
withInputFrom: { inNumPackets, outStatus in |
||||
|
outStatus.pointee = .haveData // 标记有数据可用 |
||||
|
return buffer // 返回原始音频数据 |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
// 转换成功且无错误 |
||||
|
if status == .haveData, error == nil { |
||||
|
// 将音频缓冲区转换为二进制数据 |
||||
|
let data = self.audioBufferToData(convertedBuffer) |
||||
|
// 将数据放入写入队列(后续处理) |
||||
|
self.writeQueue.put(data) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 激活音频会话(允许录音) |
||||
|
try audioSession.setActive(true, options: [.notifyOthersOnDeactivation]) |
||||
|
|
||||
|
try audioEngine?.start() |
||||
|
} catch { |
||||
|
print("麦克风启动失败: \(error)") |
||||
|
// 【新增】添加详细错误处理 |
||||
|
if let nsError = error as NSError? { |
||||
|
print("错误域: \(nsError.domain), 错误代码: \(nsError.code)") |
||||
|
print("错误描述: \(nsError.localizedDescription)") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
private func audioBufferToData(_ buffer: AVAudioPCMBuffer) -> Data { |
||||
|
let frameLength = Int(buffer.frameLength) |
||||
|
let channelCount = 1 |
||||
|
let dataLength = frameLength * channelCount * MemoryLayout<Int16>.size |
||||
|
|
||||
|
// Handle 16-bit integer format |
||||
|
if let int16Data = buffer.int16ChannelData { |
||||
|
return Data( |
||||
|
bytes: int16Data.pointee, |
||||
|
count: dataLength |
||||
|
) |
||||
|
} |
||||
|
// Handle float format |
||||
|
else if let floatData = buffer.floatChannelData { |
||||
|
var int16Array = [Int16](repeating: 0, count: frameLength) |
||||
|
let floatBuffer = floatData.pointee |
||||
|
|
||||
|
for i in 0..<frameLength { |
||||
|
let sample = floatBuffer[i] |
||||
|
let clamped = max(-1.0, min(sample, 1.0)) |
||||
|
let scaled = clamped * Float(Int16.max) |
||||
|
int16Array[i] = Int16(scaled) |
||||
|
} |
||||
|
|
||||
|
return Data( |
||||
|
bytes: int16Array, |
||||
|
count: dataLength |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
return Data() // Fallback for unsupported formats |
||||
|
} |
||||
|
private func runExternalCapture() { |
||||
|
if audioSourceType == .external { |
||||
|
//pushAudioData(data: Data()) |
||||
|
|
||||
|
audioEngine?.pause() |
||||
|
//audioEngine?.inputNode.removeTap(onBus: 0) |
||||
|
} |
||||
|
} |
||||
|
/** |
||||
|
* 继续麦克风捕获 |
||||
|
*/ |
||||
|
/** |
||||
|
* 继续麦克风捕获 |
||||
|
*/ |
||||
|
public func resumeRecord() { |
||||
|
guard _isWriting==false else { return } |
||||
|
_isWriting = true |
||||
|
// 启动录音引擎 |
||||
|
do { |
||||
|
print("resumeRecord") |
||||
|
try audioEngine?.start() |
||||
|
} catch { |
||||
|
os_log("Failed to start audio engine: %@", log: log, type: .error, error.localizedDescription) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
public func stopMicrophoneCapture() { |
||||
|
guard _isWriting==true else { return } |
||||
|
|
||||
|
_isWriting=false |
||||
|
audioEngine?.pause() |
||||
|
print("stopMicrophoneCapture") |
||||
|
|
||||
|
} |
||||
|
public func releaseAudioResources() { |
||||
|
print("释放了") |
||||
|
stopMicrophoneCapture() |
||||
|
audioEngine?.stop() |
||||
|
writeThread?.async { |
||||
|
self.writeQueue.close() |
||||
|
} |
||||
|
writeThread = nil |
||||
|
audioEngine?.inputNode.removeTap(onBus: 0) |
||||
|
isRunning=false |
||||
|
audioEngine = nil |
||||
|
} |
||||
|
|
||||
|
// 音频路由管理 |
||||
|
public enum AudioOutputRoute { |
||||
|
case speaker |
||||
|
case receiver |
||||
|
case bluetooth |
||||
|
} |
||||
|
public func setAudioOutputRoute(_ route: AudioOutputRoute) { |
||||
|
|
||||
|
do { |
||||
|
// |
||||
|
if self._isWriting { |
||||
|
print("当前正在识别") |
||||
|
try audioEngine?.pause() |
||||
|
// 先停用以避免冲突 |
||||
|
try audioSession.setActive(false) |
||||
|
|
||||
|
} |
||||
|
print("setAudioOutputRoutecurrent,route=\(route)") |
||||
|
switch route { |
||||
|
case .speaker: |
||||
|
print("setAudioOutputRoutecurrent,speaker") |
||||
|
// 使用扬声器时必须用videoChat模式 |
||||
|
try audioSession.setCategory( |
||||
|
.playAndRecord, |
||||
|
mode: .videoChat, |
||||
|
options: [.defaultToSpeaker] |
||||
|
) |
||||
|
try audioSession.overrideOutputAudioPort(.speaker) |
||||
|
|
||||
|
case .receiver: |
||||
|
// 听筒模式使用voiceChat节省资源 |
||||
|
try audioSession.setCategory( |
||||
|
.playAndRecord, |
||||
|
mode: .voiceChat, |
||||
|
options: [.allowBluetooth] |
||||
|
) |
||||
|
try audioSession.overrideOutputAudioPort(.none) |
||||
|
|
||||
|
case .bluetooth: |
||||
|
// 完整蓝牙设备支持 |
||||
|
try audioSession.setCategory( |
||||
|
.playAndRecord, |
||||
|
mode: .voiceChat, |
||||
|
options: [.allowBluetooth, .allowBluetoothA2DP] |
||||
|
) |
||||
|
try audioSession.overrideOutputAudioPort(.none) |
||||
|
// 不需要override,系统自动路由 |
||||
|
} |
||||
|
self.currentRoute = route |
||||
|
if self._isWriting { |
||||
|
|
||||
|
// 重新激活 |
||||
|
try audioSession.setActive(true, options: [.notifyOthersOnDeactivation]) |
||||
|
|
||||
|
try audioEngine?.start() |
||||
|
// writeThread?.async { |
||||
|
// self.writeQueue.close() |
||||
|
// } |
||||
|
} |
||||
|
} catch { |
||||
|
print("路由切换失败: \(error)") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 音频数据回调接口 |
||||
|
*/ |
||||
|
public protocol AudioDataCallback { |
||||
|
/** |
||||
|
* 音频数据回调 |
||||
|
* @param data 音频数据 |
||||
|
*/ |
||||
|
func onAudio(_ data: Data) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 检查是否正在写入 |
||||
|
* @return 是否正在写入音频数据 |
||||
|
*/ |
||||
|
public func isWriting() -> Bool { |
||||
|
return _isWriting |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
// MARK: - iOS版LinkedBlockingQueue实现 |
||||
|
private class LinkedBlockingQueue<T> { |
||||
|
private var queue: [T] = [] |
||||
|
private var isClosed = false |
||||
|
|
||||
|
// 使用DispatchSemaphore实现阻塞行为 |
||||
|
private let availableItems: DispatchSemaphore |
||||
|
private let queueLock = NSLock() |
||||
|
|
||||
|
init() { |
||||
|
self.availableItems = DispatchSemaphore(value: 0) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 阻塞式获取元素(等价于Android的take()) |
||||
|
* @return 队列中的元素,如果队列已关闭则返回nil |
||||
|
*/ |
||||
|
func take() -> T? { |
||||
|
// 等待可用元素(阻塞直到有元素或队列关闭) |
||||
|
availableItems.wait() |
||||
|
|
||||
|
queueLock.lock() |
||||
|
defer { queueLock.unlock() } |
||||
|
|
||||
|
// 检查队列是否已关闭 |
||||
|
if isClosed && queue.isEmpty { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// 获取第一个元素 |
||||
|
guard !queue.isEmpty else { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
return queue.removeFirst() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加元素(等价于Android的put()) |
||||
|
* @param item 要添加的元素 |
||||
|
*/ |
||||
|
func put(_ item: T) { |
||||
|
queueLock.lock() |
||||
|
defer { queueLock.unlock() } |
||||
|
|
||||
|
// 检查队列是否已关闭 |
||||
|
if isClosed { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 添加元素(无容量限制,与Android一致) |
||||
|
queue.append(item) |
||||
|
|
||||
|
// 通知有新元素可用 |
||||
|
availableItems.signal() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 关闭队列 |
||||
|
* 关闭后不能再添加新元素,但可以继续取出已有元素 |
||||
|
*/ |
||||
|
func close() { |
||||
|
queueLock.lock() |
||||
|
defer { queueLock.unlock() } |
||||
|
|
||||
|
isClosed = true |
||||
|
|
||||
|
// 唤醒所有等待的take()操作 |
||||
|
for _ in 0..<100 { // 假设最多100个等待者 |
||||
|
availableItems.signal() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - 协议定义 |
||||
|
|
||||
Loading…
Reference in new issue