160 lines
6.5 KiB
Swift
160 lines
6.5 KiB
Swift
import Foundation
|
|
import Speech
|
|
import AVFoundation
|
|
import AudioToolbox
|
|
import RxSwift
|
|
import os.log
|
|
import ExceptionCatcher
|
|
|
|
class Recorder {
|
|
|
|
let engine = AVAudioEngine()
|
|
var fileRef: ExtAudioFileRef? = nil
|
|
let recognizer = SFSpeechRecognizer(locale: Locale(identifier: "ru_RU"))
|
|
let request = SFSpeechAudioBufferRecognitionRequest()
|
|
var recognitionTask: SFSpeechRecognitionTask?
|
|
var endRecognitionTimer: Timer?
|
|
var result: String = ""
|
|
|
|
let recordingSettings: [String:Any] = [
|
|
AVFormatIDKey:kAudioFormatMPEG4AAC_HE,
|
|
AVSampleRateKey:44100.0,
|
|
AVNumberOfChannelsKey:2,
|
|
//AVEncoderBitRateKey:320*1024,
|
|
//AVLinearPCMBitDepthKey:16,
|
|
AVEncoderAudioQualityKey:AVAudioQuality.max.rawValue
|
|
]
|
|
|
|
init() {
|
|
self.request.contextualStrings = ["61", "161", "761", "123", "750", "777", "799"]
|
|
}
|
|
|
|
func microphoneAvailable() -> Bool {
|
|
// FIXME:
|
|
return true
|
|
}
|
|
|
|
func requestPermissions() -> Single<Void> {
|
|
return Single<Void>.create { observer in
|
|
AVAudioSession.sharedInstance().requestRecordPermission { allowed in
|
|
if allowed {
|
|
SFSpeechRecognizer.requestAuthorization { status in
|
|
switch status {
|
|
case .authorized:
|
|
observer(.success(()))
|
|
break
|
|
case .denied:
|
|
let error = CocoaError.error("Access error", reason: "Access to speech recognition is denied", suggestion: "Please give permission to use speech recognition in system settings")
|
|
observer(.failure(error))
|
|
break
|
|
case .restricted:
|
|
let error = CocoaError.error("Access error", reason: "Speech recognition is restricted on this device")
|
|
observer(.failure(error))
|
|
break
|
|
case .notDetermined:
|
|
let error = CocoaError.error("Access error", reason: "Speech recognition status is not yet determined")
|
|
observer(.failure(error))
|
|
break
|
|
@unknown default:
|
|
let error = CocoaError.error("Access error", reason: "Unknown error accessing speech recognizer")
|
|
observer(.failure(error))
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
let error = CocoaError.error("Access error", reason: "Access to microphone is denied", suggestion: "Please give permission to use microphone in system settings")
|
|
observer(.failure(error))
|
|
}
|
|
}
|
|
|
|
return Disposables.create()
|
|
}
|
|
}
|
|
|
|
func startRecording(to file: URL) -> Single<String> {
|
|
guard self.microphoneAvailable() else {
|
|
return Single.error(CocoaError.error("Recording error", reason: "Microphone not found"))
|
|
}
|
|
|
|
return Single<String>.create { observer in
|
|
guard let aac = AVAudioFormat(settings: self.recordingSettings) else {
|
|
observer(.failure(CocoaError.error("Recording error", reason: "Format not supported")))
|
|
return Disposables.create()
|
|
}
|
|
|
|
ExtAudioFileCreateWithURL(file as CFURL, kAudioFileM4AType, aac.streamDescription, nil, AudioFileFlags.eraseFile.rawValue, &self.fileRef)
|
|
guard let fileRef = self.fileRef else {
|
|
observer(.failure(CocoaError.error(CocoaError.Code.fileWriteUnknown)))
|
|
return Disposables.create()
|
|
}
|
|
|
|
do {
|
|
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default, options: [])
|
|
try AVAudioSession.sharedInstance().setActive(true)
|
|
|
|
let inFormat = self.engine.inputNode.outputFormat(forBus: 0)
|
|
ExtAudioFileSetProperty(fileRef, kExtAudioFileProperty_ClientDataFormat, UInt32(MemoryLayout<AudioStreamBasicDescription>.size), inFormat.streamDescription)
|
|
|
|
try ExceptionCatcher.catch {
|
|
self.engine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: inFormat) { buffer, time in
|
|
self.request.append(buffer)
|
|
ExtAudioFileWrite(fileRef, buffer.frameLength, buffer.audioBufferList)
|
|
}
|
|
}
|
|
|
|
self.recognitionTask = self.recognizer!.recognitionTask(with: self.request) { result, error in
|
|
if let transcription = result?.bestTranscription {
|
|
self.result = transcription.formattedString
|
|
self.endRecognitionTimer?.invalidate()
|
|
self.endRecognitionTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { timer in
|
|
self.finishRecording()
|
|
observer(.success(self.result))
|
|
}
|
|
}
|
|
}
|
|
|
|
self.endRecognitionTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { timer in
|
|
self.finishRecording()
|
|
observer(.success(self.result))
|
|
}
|
|
|
|
self.engine.prepare()
|
|
try self.engine.start()
|
|
} catch {
|
|
observer(.failure(error))
|
|
}
|
|
|
|
return Disposables.create {
|
|
self.cancelRecording()
|
|
}
|
|
}
|
|
}
|
|
|
|
func cancelRecording() {
|
|
self.finishRecording()
|
|
self.endRecognitionTimer?.invalidate()
|
|
self.endRecognitionTimer = nil
|
|
}
|
|
|
|
func finishRecording() {
|
|
guard self.engine.isRunning else { return }
|
|
|
|
self.engine.stop()
|
|
self.engine.inputNode.removeTap(onBus: 0)
|
|
self.request.endAudio()
|
|
self.recognitionTask?.cancel()
|
|
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
|
try? AVAudioSession.sharedInstance().setCategory(.soloAmbient)
|
|
|
|
if let fileRef = self.fileRef {
|
|
ExtAudioFileDispose(fileRef)
|
|
}
|
|
}
|
|
|
|
func stopRecording() {
|
|
self.finishRecording()
|
|
self.endRecognitionTimer?.fire()
|
|
self.endRecognitionTimer = nil
|
|
}
|
|
}
|