ViewController.swift 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. /*
  2. * Copyright 2020, gRPC Authors All rights reserved.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import AVFoundation
  17. import GRPC
  18. import SnapKit
  19. import UIKit
  20. final class ViewController: UIViewController {
  21. private lazy var recordButton: UIButton = {
  22. var button = UIButton()
  23. button.setTitle("Record", for: .normal)
  24. button.setImage(UIImage(systemName: "mic"), for: .normal)
  25. button.backgroundColor = .darkGray
  26. button.layer.cornerRadius = 15
  27. button.clipsToBounds = true
  28. button.addTarget(self, action: #selector(self.recordTapped), for: .touchUpInside)
  29. return button
  30. }()
  31. private lazy var textView: UITextView = {
  32. var textView = UITextView()
  33. textView.isEditable = false
  34. textView.isSelectable = false
  35. textView.textColor = .white
  36. textView.textAlignment = .left
  37. textView.font = UIFont.systemFont(ofSize: 30)
  38. return textView
  39. }()
  40. private var isRecording: Bool = false {
  41. didSet {
  42. if self.isRecording {
  43. self.startRecording()
  44. } else {
  45. self.stopRecording()
  46. }
  47. }
  48. }
  49. private var audioData = Data()
  50. private let speechService: SpeechService
  51. private let audioStreamManager: AudioStreamManager
  52. init(
  53. speechService: SpeechService,
  54. audioStreamManager: AudioStreamManager
  55. ) {
  56. self.speechService = speechService
  57. self.audioStreamManager = audioStreamManager
  58. super.init(nibName: nil, bundle: nil)
  59. }
  60. convenience init() {
  61. self.init(
  62. speechService: SpeechService(),
  63. audioStreamManager: AudioStreamManager.shared
  64. )
  65. }
  66. required init?(coder: NSCoder) {
  67. return nil
  68. }
  69. override func viewDidLoad() {
  70. super.viewDidLoad()
  71. view.backgroundColor = .black
  72. title = "gRPC Speech To Text"
  73. self.audioStreamManager.delegate = self
  74. let recordingSession = AVAudioSession.sharedInstance()
  75. recordingSession.requestRecordPermission { [weak self] allowed in
  76. DispatchQueue.main.async {
  77. if allowed {
  78. do {
  79. try self?.audioStreamManager.configure()
  80. self?.setupRecordingLayout()
  81. } catch {
  82. self?.setupConfigurationFailedLayout()
  83. }
  84. } else {
  85. self?.setupErrorLayout()
  86. }
  87. }
  88. }
  89. }
  90. func setupRecordingLayout() {
  91. view.addSubview(self.textView)
  92. view.addSubview(self.recordButton)
  93. self.textView.snp.makeConstraints { make in
  94. make.top.equalTo(view.safeAreaLayoutGuide.snp.topMargin)
  95. make.left.right.equalToSuperview()
  96. make.bottom.equalTo(self.recordButton.snp.top)
  97. }
  98. self.recordButton.snp.makeConstraints { make in
  99. make.height.equalTo(50)
  100. make.left.equalTo(40)
  101. make.right.equalTo(-40)
  102. make.bottom.equalToSuperview().inset(100)
  103. make.centerX.equalToSuperview()
  104. }
  105. }
  106. func setupErrorLayout() {
  107. self.textView.text = "Microphone Permissions are required in order to use this App."
  108. self.recordButton.isEnabled = false
  109. }
  110. func setupConfigurationFailedLayout() {
  111. self.textView.text = "An error occured while configuring your device for audio streaming."
  112. self.recordButton.isEnabled = false
  113. }
  114. @objc
  115. func recordTapped() {
  116. self.isRecording.toggle()
  117. }
  118. func startRecording() {
  119. self.audioData = Data()
  120. self.audioStreamManager.start()
  121. UIView.animate(withDuration: 0.02) { [weak self] in
  122. self?.recordButton.backgroundColor = .red
  123. }
  124. }
  125. func stopRecording() {
  126. self.audioStreamManager.stop()
  127. self.speechService.stopStreaming()
  128. UIView.animate(withDuration: 0.02) { [weak self] in
  129. self?.recordButton.backgroundColor = .darkGray
  130. }
  131. }
  132. }
  133. extension ViewController: StreamDelegate {
  134. func processAudio(_ data: Data) {
  135. self.audioData.append(data)
  136. // 100 ms chunk size
  137. let chunkSize = Int(0.1 * Constants.sampleRate * 2)
  138. // When the audio data gets big enough
  139. if self.audioData.count > chunkSize {
  140. // Send to server
  141. self.speechService.stream(self.audioData) { [weak self] response in
  142. guard let self = self else { return }
  143. DispatchQueue.main.async {
  144. UIView.transition(
  145. with: self.textView,
  146. duration: 0.25,
  147. options: .transitionCrossDissolve,
  148. animations: {
  149. guard
  150. let results = response.results.first,
  151. let text = results.alternatives.first?.transcript else { return }
  152. if self.textView.text != text {
  153. self.textView.text = text
  154. }
  155. },
  156. completion: nil
  157. )
  158. }
  159. }
  160. }
  161. }
  162. }