Browse Source

有问题

weicu
fdp 1 year ago
parent
commit
660a0611cf
  1. 2
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  2. 274
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
  3. 4
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
  4. 232
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/RecordFile.swift

2
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -347,7 +347,7 @@ class AgentServiceImpl: NSObject {
return false
}
azureAsrHelper?.pushAudioData(data: audioData)
azureAsrHelper?.audioStream?.saveAudioDataTo(data)
return true
}

274
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift

@ -31,7 +31,13 @@ public class AzureAsrHelper: NSObject {
private var isAutoDetectLanguage = false
private var subscriptionKey = ""
private var region = ""
// 音频处理
private let audioQueue = DispatchQueue(label: "audio.processing.queue")
private var audioEngine: AVAudioEngine?
private var audioBuffer: Data?
// 录音文件处理
private let recordFile = RecordFile.shared
// 音频源配置
public enum AudioSourceType {
/** 使用设备麦克风 */
@ -45,6 +51,11 @@ public class AzureAsrHelper: NSObject {
// 音频处理
private var externalAudioStream: ExternalAudioPullStream?
private var audioStream: AudioStream?
// 当前回调
private weak var currentContinuousCallback: ContinuousRecognizeCallback?
/**
* 初始化Azure语音服务
@ -106,28 +117,16 @@ public class AzureAsrHelper: NSObject {
}
// 创建识别器
return setupRecognizer()
return true
} catch {
os_log("初始化失败: %{public}@", log: log, type: .error, error.localizedDescription)
return false
}
}
/**
* 向音频流写入音频数据
* 仅当音频源设置为external时有效
*
* @param data 音频数据字节数组
*/
public func pushAudioData(data: Data) {
if audioSourceType != .external {
return
}
// 使用拉流模式,将数据推入队列
externalAudioStream?.pushAudio(data)
private fun isRecognizerValid(): Boolean {
return recognizer != null
}
/**
* 开始连续语音识别
*
@ -136,7 +135,7 @@ public class AzureAsrHelper: NSObject {
* @return 是否成功开始识别
*/
public func startContinuousRecognition(
callback: ContinuousRecognizeCallback,
audioSourceType: AudioSourceType = .microphone
) -> Bool {
guard speechConfig != nil else {
@ -147,22 +146,24 @@ public class AzureAsrHelper: NSObject {
if _isContinuousRecognitionActive {
return true
}
if (!isRecognizerValid()) {
Log.w(tag, "识别器已失效,正在重新创建...")
if (!setupRecognizer()) {
Log.e(tag, "重新创建识别器失败")
return false
}
}
self.audioSourceType = audioSourceType
// 重置识别器
if !setupRecognizer() {
callback.onError("重置识别器失败")
return false
}
do {
// 设置各种事件监听
setupEventListeners(callback: callback)
//setupEventListeners(callback: callback)
// 启动音频处理
startAudioProcessing()
// startAudioProcessing()
audioStream!!.startAudioRecord()
// 开始连续识别
try recognizer?.startContinuousRecognition()
_isContinuousRecognitionActive = true
@ -201,7 +202,7 @@ public class AzureAsrHelper: NSObject {
}
if audioSourceType == .external {
pushAudioData(data: Data())
//pushAudioData(data: Data())
}
// 停止连续识别
@ -266,6 +267,38 @@ public class AzureAsrHelper: NSObject {
speechConfig = nil
}
}
// MARK: - 录音功能
/// 开启录音
func enableRecord(filePath: String) {
recordFile.stopRecord(isSave: false)
recordFile.creatingFiles(atPath: filePath)
}
/// 移动文件
func moveFile(sourcePath: String, destPath: String) -> Bool {
return recordFile.moveFile(from: sourcePath, to: destPath)
}
/// 重命名文件
func renameFile(at sourcePath: String, to newName: String) -> Bool {
return recordFile.renameFile(at: sourcePath, to: newName)
}
/// 暂停录音
func pauseRecord() {
recordFile.isPause = true
}
/// 停止录音
func stopRecord(isSave: Bool) {
recordFile.stopRecord(isSave: isSave)
}
/// 输入外部音频数据
func saveAudioDataTo(_ data: Data) {
guard audioSourceType == .external else { return }
writeQueue.offer(data.copyOf())
}
// MARK: - 私有方法
@ -325,13 +358,32 @@ public class AzureAsrHelper: NSObject {
externalAudioStream = nil
}
}
/**
* 设置麦克风流 - 使用推流方式
*/
private func setupMicrophoneStream() {
do {
// 创建麦克风流对象
audioStream = AudioStream()
// 创建音频配置
audioConfig = SPXAudioConfiguration(streamInput: audioStream!.pushAudioStream)
} catch {
os_log("设置麦克风流失败: %{public}@", log: log, type: .error, error.localizedDescription)
audioStream = nil
}
}
/**
* 设置事件监听器
*/
private func setupEventListeners(callback: ContinuousRecognizeCallback) {
guard let recognizer = recognizer else { return }
// 重置识别器
if !setupRecognizer() {
callback.onError("重置识别器失败")
return false
}
// 识别中事件
recognizer.addRecognizingEventHandler { [weak self] (sender, event) in
guard let self = self else { return }
@ -432,7 +484,171 @@ public class AzureAsrHelper: NSObject {
}
}
/**
* 麦克风音频流处理
*/
private class AudioStream: NSObject {
private var audioEngine: AVAudioEngine?
private var pushAudioStream: SPXPushAudioInputStream?
private var audioFile: AVAudioFile?
private var isRecording = false
private var recordFilePath: URL?
private let writeQueue = LinkedBlockingQueue<Data>()
override init() {
super.init()
// 创建推流对象
let format = SPXAudioStreamFormat(usingPCMWithSampleRate: 16000, bitsPerSample: 16, channels: 1)!
pushAudioStream = SPXPushAudioInputStream(audioFormat: format)
}
/**
* 开始音频捕获
*/
func startAudioRecord() {
do {
// 配置音频会话
let session = AVAudioSession.sharedInstance()
try session.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth])
try session.setActive(true)
// 初始化音频引擎
audioEngine = AVAudioEngine()
guard let engine = audioEngine else { return }
// 获取输入节点
let inputNode = engine.inputNode
let inputFormat = inputNode.outputFormat(forBus: 0)
// 配置音频格式
let recordingFormat = AVAudioFormat(
commonFormat: .pcmFormatInt16,
sampleRate: 16000,
channels: 1,
interleaved: true
)!
// 安装Tap
inputNode.installTap(
onBus: 0,
bufferSize: 1024,
format: inputFormat
) { [weak self] (buffer, time) in
self?.processAudioBuffer(buffer, format: recordingFormat)
}
// 启动引擎
try engine.start()
} catch {
os_log("音频捕获启动失败: %{public}@", type: .error, error.localizedDescription)
}
}
/**
* 停止音频捕获
*/
func stopCapture() {
audioEngine?.stop()
audioEngine?.inputNode.removeTap(onBus: 0)
audioEngine = nil
stopRecord(isSave: true)
}
/**
* 处理音频缓冲区
*/
private func processAudioBuffer(_ buffer: AVAudioPCMBuffer, format: AVAudioFormat) {
guard let converter = AVAudioConverter(from: buffer.format, to: format) else { return }
// 设置音频配置
switch audioSourceType {
case .microphone:
// 使用默认麦克风输入配置
// 创建目标缓冲区
let targetFrameCapacity = AVAudioFrameCount(
(Double(buffer.frameCapacity) * format.sampleRate / buffer.format.sampleRate
)
guard let targetBuffer = AVAudioPCMBuffer(
pcmFormat: format,
frameCapacity: targetFrameCapacity
) else { return }
// 转换音频格式
var error: NSError?
let inputBlock: AVAudioConverterInputBlock = { inNumPackets, outStatus in
outStatus.pointee = .haveData
return buffer
}
converter.convert(to: targetBuffer, error: &error, withInputFrom: inputBlock)
// 获取音频数据
guard let int16Data = targetBuffer.int16ChannelData else { return }
let data = Data(
bytes: int16Data[0],
count: Int(targetBuffer.frameLength) * MemoryLayout<Int16>.size
)
case .external:
// 使用拉流方式处理外部音频
if (writeQueue.isNotEmpty()) {
let data = writeQueue.poll()
Log.d("tag", "写入数据: ${data?.size}")
bytesToWrite = data?.size ?: 0
}
}
// 推送到Azure流
pushAudioStream?.write(data)
// 保存到录音文件
// 触发音频回调
if let callback = AzureAsrHelper.shared?.currentContinuousCallback {
callback.onAudio(data)
}
}
/**
* 开启录音
*/
func enableRecord(filePath: String) {
do {
recordFilePath = URL(fileURLWithPath: filePath)
let settings: [String: Any] = [
AVFormatIDKey: kAudioFormatLinearPCM,
AVSampleRateKey: 16000.0,
AVNumberOfChannelsKey: 1,
AVEncoderBitDepthHintKey: 16,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
]
audioFile = try AVAudioFile(
forWriting: recordFilePath!,
settings: settings
)
isRecording = true
} catch {
os_log("录音文件创建失败: %{public}@", type: .error, error.localizedDescription)
}
}
/**
* 停止录音
*/
func stopRecord(isSave: Bool) {
isRecording = false
audioFile = nil
if !isSave, let path = recordFilePath {
try? FileManager.default.removeItem(at: path)
}
recordFilePath = nil
}
}
/**
* 外部音频拉流
* 实现PullAudioInputStreamCallback,将外部推送的音频数据转换为SDK可拉取的形式

4
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift

@ -183,8 +183,8 @@ import os.log
result(FlutterError(code: "INVALID_ARGUMENTS", message: "音频数据不能为空", details: nil))
return
}
azureAsrHelper.pushAudioData(data: audioBytes.data)
azureAsrHelper?.audioStream?.saveAudioDataTo(data: audioBytes.data)
result(true)
default:

232
local_plugins/azure_speech/ios/azure_speech/Sources/tools/RecordFile.swift

@ -0,0 +1,232 @@
import Foundation
class RecordFile {
static let shared = RecordFile()
private var currentAudioFile: URL?
private var fileHandle: FileHandle?
private let writeQueue = DispatchQueue(label: "audio.write.queue")
private let writeGroup = DispatchGroup()
private var isWriting = false
private var dataBuffer = Data()
private var totalBytesWritten = 0
private let sampleRate: UInt32 = 16000
private var lastSavedFile: URL?
var isPause = false
var fileName = ""
// 创建音频文件并初始化 WAV 头
func creatingFiles(atPath path: String) {
guard fileHandle == nil else { return }
let fileURL = URL(fileURLWithPath: path)
let directoryURL = fileURL.deletingLastPathComponent()
// 创建目录
do {
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true)
} catch {
print("创建目录失败: \(error.localizedDescription)")
return
}
// 创建文件
FileManager.default.createFile(atPath: path, contents: nil)
do {
fileHandle = try FileHandle(forWritingTo: fileURL)
currentAudioFile = fileURL
// 写入初始 WAV 头
let header = generateWavHeader(dataLength: 0)
try fileHandle?.write(contentsOf: header)
totalBytesWritten = 0
} catch {
print("文件初始化失败: \(error.localizedDescription)")
fileHandle = nil
currentAudioFile = nil
}
}
// 异步保存音频数据
func saveAudioDataToWav(_ data: Data) {
guard let _ = fileHandle, let _ = currentAudioFile else { return }
writeQueue.async { [weak self] in
guard let self = self else { return }
self.dataBuffer.append(data)
self.totalBytesWritten += data.count
if !self.isWriting {
self.isWriting = true
self.writeGroup.enter()
defer {
self.isWriting = false
self.writeGroup.leave()
}
while !self.dataBuffer.isEmpty {
let chunk: Data
if self.dataBuffer.count > 8192 {
chunk = self.dataBuffer.prefix(8192)
self.dataBuffer.removeFirst(8192)
} else {
chunk = self.dataBuffer
self.dataBuffer.removeAll()
}
do {
try self.fileHandle?.seekToEnd()
self.fileHandle?.write(chunk)
} catch {
print("写入数据失败: \(error.localizedDescription)")
}
}
}
}
}
// 生成 WAV 文件头
private func generateWavHeader(dataLength: Int) -> Data {
let totalLength = 36 + dataLength
let byteRate = sampleRate * 2
var header = Data()
header.append("RIFF".data(using: .ascii)!)
header.append(Data(bytes: [
UInt8(totalLength & 0xFF),
UInt8((totalLength >> 8) & 0xFF),
UInt8((totalLength >> 16) & 0xFF),
UInt8((totalLength >> 24) & 0xFF)
]))
header.append("WAVE".data(using: .ascii)!)
header.append("fmt ".data(using: .ascii)!)
header.append(Data(bytes: [16, 0, 0, 0])) // PCM 头长度
header.append(Data(bytes: [1, 0])) // PCM 格式
header.append(Data(bytes: [1, 0])) // 单声道
header.append(Data(bytes: [
UInt8(sampleRate & 0xFF),
UInt8((sampleRate >> 8) & 0xFF),
UInt8((sampleRate >> 16) & 0xFF),
UInt8((sampleRate >> 24) & 0xFF)
]))
header.append(Data(bytes: [
UInt8(byteRate & 0xFF),
UInt8((byteRate >> 8) & 0xFF),
UInt8((byteRate >> 16) & 0xFF),
UInt8((byteRate >> 24) & 0xFF)
]))
header.append(Data(bytes: [2, 0])) // 块对齐
header.append(Data(bytes: [16, 0])) // 样本位数
header.append("data".data(using: .ascii)!)
header.append(Data(bytes: [
UInt8(dataLength & 0xFF),
UInt8((dataLength >> 8) & 0xFF),
UInt8((dataLength >> 16) & 0xFF),
UInt8((dataLength >> 24) & 0xFF)
]))
return header
}
// 重命名文件
func renameFile(at sourcePath: String, to newName: String) -> Bool {
let sourceURL = URL(fileURLWithPath: sourcePath)
guard FileManager.default.fileExists(atPath: sourcePath) else {
print("重命名失败: 文件不存在 \(sourcePath)")
return false
}
if let current = currentAudioFile, current == sourceURL {
print("重命名失败: 不能重命名正在录制的文件")
return false
}
let newFileURL = sourceURL
.deletingLastPathComponent()
.appendingPathComponent(newName)
.appendingPathExtension("wav")
do {
try FileManager.default.moveItem(at: sourceURL, to: newFileURL)
if lastSavedFile == sourceURL {
lastSavedFile = newFileURL
}
return true
} catch {
print("重命名异常: \(error.localizedDescription)")
return false
}
}
// 移动文件
func moveFile(from sourcePath: String, to destPath: String) -> Bool {
let sourceURL = URL(fileURLWithPath: sourcePath)
let destURL = URL(fileURLWithPath: destPath)
guard FileManager.default.fileExists(atPath: sourcePath) else {
print("移动失败: 源文件不存在 \(sourcePath)")
return false
}
let destDir = destURL.deletingLastPathComponent()
do {
try FileManager.default.createDirectory(at: destDir, withIntermediateDirectories: true)
} catch {
print("创建目录失败: \(error.localizedDescription)")
return false
}
do {
try FileManager.default.moveItem(at: sourceURL, to: destURL)
if lastSavedFile == sourceURL {
lastSavedFile = destURL
}
return true
} catch {
print("移动文件异常: \(error.localizedDescription)")
return false
}
}
// 关闭文件
func closeFile(isSave: Bool) -> Bool {
defer {
fileHandle = nil
currentAudioFile = nil
dataBuffer.removeAll()
totalBytesWritten = 0
}
guard let fileURL = currentAudioFile, let handle = fileHandle else {
return false
}
// 等待写入完成
writeGroup.wait()
do {
try handle.close()
if !isSave || totalBytesWritten == 0 {
try FileManager.default.removeItem(at: fileURL)
return false
}
// 更新 WAV 头
let header = generateWavHeader(dataLength: totalBytesWritten)
let updateHandle = try FileHandle(forUpdating: fileURL)
try updateHandle.seek(toOffset: 0)
updateHandle.write(header)
try updateHandle.close()
lastSavedFile = fileURL
return true
} catch {
print("关闭文件失败: \(error.localizedDescription)")
return false
}
}
}
Loading…
Cancel
Save