使用Speech Framework实现语音转文字

这篇日志记录自己在学习使用苹果原生框架语音识别库Speech Framework时的总结和示例代码。看了一遍官方文档,把该框架中的相关类和方法了解了一遍,然后总结了一张XMind结构图。

前提:需要Xcode 8 以上和一个运行iOS10以上系统的iOS设备.

Speech Framework中的类和方法概念

Paste_Image.png

Note: 因为涉及到权限问题,需要在info.plist文件中添加两个key。分别是Privacy - Microphone Usage Description(麦克风权限)和 Privacy - Speech Recognition Usage Description(语音识别权限)

Swift代码

import UIKit
import Speech

class ViewController: UIViewController {

  @IBOutlet weak var textView: UITextView!
  @IBOutlet weak var microphoneButton: UIButton!

  /// 语音识别操作类对象
  private let speechRecognizer = SFSpeechRecognizer()

  /// 处理语音识别请求,给语音识别提供语音输入
  private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?

  /// 告诉用户语音识别对象的结果。拥有这个对象很方便因为你可以 用它删除或中断任务
  private var recognitionTask: SFSpeechRecognitionTask?

  /// 语音引擎。负责提供语音输入
  private let audioEngine = AVAudioEngine()

  override func viewDidLoad() {
      super.viewDidLoad()
      // Do any additional setup after loading the view, typically from a nib.
      microphoneButton.isEnabled = false
    
      speechRecognizer?.delegate = self

      /// 申请用户语音识别权限
      SFSpeechRecognizer.requestAuthorization { (authStatus) in

          var isButtonEnabled = false

          switch authStatus {
          case .authorized: // 用户授权语音识别
              isButtonEnabled = true

          case .denied: // 用户拒绝授权语音识别
              isButtonEnabled = false
              print("User denied access to speech recognition")

          case .restricted: // 设备不支持语音识别功能
              isButtonEnabled = false
              print("Speech recognition restricted on this device")

          case .notDetermined: // 结果未知 用户尚未进行选择
              isButtonEnabled = false
              print("Speech recognition not yet authorized")
          }

          OperationQueue.main.addOperation {
              self.microphoneButton.isEnabled = isButtonEnabled
          }
      }
  }

  @IBAction func microphoneButtonClick(_ sender: UIButton) {
      if audioEngine.isRunning {
          audioEngine.stop()
          recognitionRequest?.endAudio()
          microphoneButton.isEnabled = false
          microphoneButton.setTitle("Start Recording", for: .normal)
      } else {
          startRecording()
          microphoneButton.setTitle("Stop Recording", for: .normal)
      }
  }

  func startRecording() {
      if recognitionTask != nil { /// 检查recognitionTask是否在运行,如果在就取消任务和识别
          recognitionTask?.cancel()
          recognitionTask = nil
      }

      let audioSession = AVAudioSession.sharedInstance() /// 记录语音做准备
      do {
          try audioSession.setCategory(AVAudioSessionCategoryRecord)
          try audioSession.setMode(AVAudioSessionModeMeasurement)
          try audioSession.setActive(true, with: .notifyOthersOnDeactivation)
      } catch {
          print("audioSession properties weren't set because of an error.")
      }

      /// 实例化recognitionRequest 利用它把语音数据传到苹果后台
      recognitionRequest = SFSpeechAudioBufferRecognitionRequest()

      /// 检查audioEngine(你的设备)是否有做录音功能作为语音输入
      guard let inputNode = audioEngine.inputNode else {
          fatalError("Audio engine has no input node")
      }

      /// 检查recognitionRequest对象是否被实例化或不是nil
      guard let recognitionRequest = recognitionRequest else {
          fatalError("Unable to create an SFSpeechAudioBufferRecongitionRequest object")
    }

      /// 当用户说话的时候让recognitionRequest报告语音识别的部分结果
      recognitionRequest.shouldReportPartialResults = true

      /// 开启语音识别, 回调每次都会在识别引擎收到输入的时候,完善了当前识别的信息时候,或者被删除或者停止的时候被调用,最后会返回一个最终的文本
      recognitionTask = speechRecognizer?.recognitionTask(with: recognitionRequest, resultHandler: { (result, error) in
          var isFinal = false // 定义一个布尔值决定识别是否已经结束

          /// 如果结果result不是nil,把textView.text的值设置为我们的最优文本。如果结果是最终结果,设置isFinal为true
          if result != nil {
              self.textView.text = result?.bestTranscription.formattedString
              isFinal = (result?.isFinal)!
          }

          /// 如果没有错误或者结果是最终结果,停止audioEngine(语音输入)并且停止recognitionRequest和recognitionTask
          if error != nil || isFinal {
              self.audioEngine.stop()
              inputNode.removeTap(onBus: 0)

              self.recognitionRequest = nil
              self.recognitionTask = nil

              self.microphoneButton.isEnabled = true
          }
      })

      /// 向recognitionRequest增加一个语音输入。注意在开始了recognitionTask之后增加语音输入是OK的。SpeechFramework会在语音输入被加入的同时就开始进行解析识别
      let recordingFormat = inputNode.outputFormat(forBus: 0)
      inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer, when) in
          self.recognitionRequest?.append(buffer)
      }

      /// 准备并且开始audioEngine
      audioEngine.prepare()

      do {
          try audioEngine.start()
      } catch {
          print("audioEngine couldn't start because of an error")
     }

      textView.text = "Say something, I'm listening!"
  }
}

extension ViewController: SFSpeechRecognizerDelegate {
  /// 可用性状态改变时被调用
  func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) {
      if available {
          microphoneButton.isEnabled = true
      } else {
          microphoneButton.isEnabled = false
      }
  }
}  

Objective-C 代码

#import <Speech/Speech.h>

@interface ViewController ()<SFSpeechRecognizerDelegate>

@property (nonatomic, strong) SFSpeechRecognizer *speechRecognizer;

@property (nonatomic, strong) SFSpeechRecognitionTask *recognitionTask;

@property (nonatomic, strong) SFSpeechAudioBufferRecognitionRequest *recognitionRequest;

/// 音频引擎
@property (nonatomic, strong) AVAudioEngine *audioEngine;

@property (weak, nonatomic) IBOutlet UITextView *textView;
@property (weak, nonatomic) IBOutlet UIButton *microphoneBtn;

@end

@implementation ViewController

- (void)dealloc {
    [self.recognitionTask cancel];
    self.recognitionTask = nil;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.view.backgroundColor = [UIColor whiteColor];

    NSLog(@"supportedLocales: %@", [SFSpeechRecognizer supportedLocales]);

    self.microphoneBtn.enabled = NO;

    /// 创建语音识别器对象并设置代理
    self.speechRecognizer = [[SFSpeechRecognizer alloc] init];

    self.speechRecognizer.delegate = self;

    /// 请求用户授权
    [SFSpeechRecognizer requestAuthorization:^(SFSpeechRecognizerAuthorizationStatus status) {

    BOOL isButtonEnabled = NO;

    switch (status) {
        case SFSpeechRecognizerAuthorizationStatusNotDetermined:
            isButtonEnabled = NO;
            NSLog(@"SFSpeechRecognizerAuthorizationStatusNotDetermined");
            break;
        case SFSpeechRecognizerAuthorizationStatusDenied:
            isButtonEnabled = NO;
            NSLog(@"SFSpeechRecognizerAuthorizationStatusDenied");
            break;
        case SFSpeechRecognizerAuthorizationStatusRestricted:
            isButtonEnabled = NO;
            NSLog(@"SFSpeechRecognizerAuthorizationStatusRestricted");
            break;
        case SFSpeechRecognizerAuthorizationStatusAuthorized:
            NSLog(@"SFSpeechRecognizerAuthorizationStatusAuthorized");
            isButtonEnabled = YES;
            break;
        default:
            break;
    }

    dispatch_async(dispatch_get_main_queue(), ^{
        self.microphoneBtn.enabled = isButtonEnabled;
    });
}];


    /// 创建音频引擎对象
    self.audioEngine = [[AVAudioEngine alloc] init];
}

- (IBAction)microphoneBtnClick:(UIButton *)sender {
    if (self.audioEngine.isRunning) {
        [self.audioEngine stop];
        [self.recognitionRequest endAudio];
        self.microphoneBtn.enabled = NO;
        [self.microphoneBtn setTitle:@"Start Recording" forState:UIControlStateNormal];
    } else {
        [self startRecording];
        [self.microphoneBtn setTitle:@"Stop Recording" forState:UIControlStateNormal];
    }
}

#pragma mark - private method
- (void)startRecording {
    if (self.recognitionTask != nil) {
        [self.recognitionTask cancel]; // 取消当前语音识别任务
        self.recognitionTask = nil;
    }

    AVAudioSession *audioSession = [AVAudioSession sharedInstance];
    NSError *categoryError = nil;
    if (![audioSession setCategory:AVAudioSessionCategoryRecord error:&categoryError]) {
        NSLog(@"categoryError: %@", categoryError.localizedDescription);
    }

    NSError *modeError = nil;
    if (![audioSession setMode:AVAudioSessionModeMeasurement error:&modeError]) {
        NSLog(@"modeError: %@", modeError.localizedDescription);
    }

    NSError *activeError = nil;
    if (![audioSession setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&activeError]) {
        NSLog(@"activeError: %@", activeError.localizedDescription);
    }

    /// 实例化 通过设备麦克风识别现场语音的请求 对象
    self.recognitionRequest = [[SFSpeechAudioBufferRecognitionRequest alloc] init];

    if (!self.audioEngine.inputNode) {// 系统输入节点
        NSLog(@"Audio engine has no input node");
        return;
    }

    if (!self.recognitionRequest) {
        NSLog(@"Unable to create an SFSpeechAudioBufferRecongitionRequest object");
        return;
    }

    /// 报告每个发音的部分非精确结果
    self.recognitionRequest.shouldReportPartialResults = YES;

    /// 执行语音识别任务 完成回调
    self.recognitionTask = [self.speechRecognizer recognitionTaskWithRequest:self.recognitionRequest resultHandler:^(SFSpeechRecognitionResult * _Nullable result, NSError * _Nullable error) {

    BOOL isFinal = NO;

    if (result) {
        self.textView.text = result.bestTranscription.formattedString;
        isFinal = result.isFinal;
    }

    if (error || isFinal) {
        [self.audioEngine stop];
        [self.audioEngine.inputNode removeTapOnBus:0];

        self.recognitionRequest = nil;
        self.recognitionTask = nil;

        self.microphoneBtn.enabled = YES;
    }
}];

    AVAudioFormat *recordingFormat = [self.audioEngine.inputNode outputFormatForBus:0];

    [self.audioEngine.inputNode installTapOnBus:0 bufferSize:1024 format:recordingFormat block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when) {
        /// 将PCM格式的音频追加到识别请求的结尾
        [self.recognitionRequest appendAudioPCMBuffer:buffer];
    }];

    [self.audioEngine prepare];

    NSError *startError = nil;
    if(![self.audioEngine startAndReturnError:&startError]) {
        NSLog(@"startError: %@", startError.localizedDescription);
    }

    self.textView.text = @"Say something, I'm listening";
}

#pragma mark - SFSpeechRecognizerDelegate
- (void)speechRecognizer:(SFSpeechRecognizer *)speechRecognizer availabilityDidChange:(BOOL)available {
    if (available) {
        self.microphoneBtn.enabled = YES;
    } else {
        self.microphoneBtn.enabled = NO;
    }
}

参考链接:
Building a Speech-to-Text App Using Speech Framework in iOS 10
SpeakToMe: Using Speech Recognition with AVAudioEngine

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容

  • 因为要结局swift3.0中引用snapKit的问题,看到一篇介绍Xcode8,swift3变化的文章,觉得很详细...
    uniapp阅读 4,388评论 0 12
  • iOS 10新特性以及适配点 SiriKit 所有第三方应用都可以用Siri,支持音频、视频、消息发送接收、搜索照...
    越过三阅读 6,166评论 11 67
  • APP开发避免不开系统权限的问题,今天做定位时需要在不允许定位的时候做一些操作,所以,今天就大概的了解了一些。 权...
    SunshineBrother阅读 16,326评论 4 62
  • 已经好久没有回去 故乡却更清晰了起来 父亲的声音也不再有力 母亲走路都成了问题 河里的水好像很久没有流动了 门口还...
    西兰河阅读 147评论 0 0
  • 17岁,我与他相遇,一起学习 18岁,我们相熟,相约打篮球 高三,文理分班,他成为她的 大一,他们异地分手 而我,...
    李易峰女友阅读 305评论 5 6