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