Skip to content

Commit 5658dd0

Browse files
committed
Clean up speech recognition case study. (#812)
* Clean up speech recognition case study. * fix tests * clean up;
1 parent c95f74a commit 5658dd0

File tree

5 files changed

+81
-103
lines changed

5 files changed

+81
-103
lines changed

Examples/SpeechRecognition/SpeechRecognition/SpeechClient/Client.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import ComposableArchitecture
33
import Speech
44

55
struct SpeechClient {
6-
var finishTask: (AnyHashable) -> Effect<Never, Never>
7-
var recognitionTask: (AnyHashable, SFSpeechAudioBufferRecognitionRequest) -> Effect<Action, Error>
6+
var finishTask: () -> Effect<Never, Never>
7+
var recognitionTask: (SFSpeechAudioBufferRecognitionRequest) -> Effect<Action, Error>
88
var requestAuthorization: () -> Effect<SFSpeechRecognizerAuthorizationStatus, Never>
99

1010
enum Action: Equatable {

Examples/SpeechRecognition/SpeechRecognition/SpeechClient/Failing.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import Speech
55
#if DEBUG
66
extension SpeechClient {
77
static let failing = Self(
8-
finishTask: { _ in .failing("SpeechClient.finishTask") },
9-
recognitionTask: { _, _ in .failing("SpeechClient.recognitionTask") },
8+
finishTask: { .failing("SpeechClient.finishTask") },
9+
recognitionTask: { _ in .failing("SpeechClient.recognitionTask") },
1010
requestAuthorization: { .failing("SpeechClient.requestAuthorization") }
1111
)
1212
}

Examples/SpeechRecognition/SpeechRecognition/SpeechClient/Live.swift

Lines changed: 69 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -3,113 +3,91 @@ import ReactiveSwift
33
import Speech
44

55
extension SpeechClient {
6-
static let live = SpeechClient(
7-
finishTask: { id in
8-
.fireAndForget {
9-
dependencies[id]?.finish()
10-
dependencies[id]?.subscriber.sendCompleted()
11-
dependencies[id] = nil
12-
}
13-
},
14-
recognitionTask: { id, request in
15-
Effect { subscriber, lifetime in
16-
lifetime += AnyDisposable {
17-
dependencies[id]?.cancel()
18-
dependencies[id] = nil
6+
static var live: Self {
7+
var audioEngine: AVAudioEngine?
8+
var inputNode: AVAudioInputNode?
9+
var recognitionTask: SFSpeechRecognitionTask?
10+
11+
return Self(
12+
finishTask: {
13+
.fireAndForget {
14+
audioEngine?.stop()
15+
inputNode?.removeTap(onBus: 0)
16+
recognitionTask?.finish()
1917
}
18+
},
19+
recognitionTask: { request in
20+
Effect { subscriber, lifetime in
21+
let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "en-US"))!
22+
let speechRecognizerDelegate = SpeechRecognizerDelegate(
23+
availabilityDidChange: { available in
24+
subscriber.send(value: .availabilityDidChange(isAvailable: available))
25+
}
26+
)
27+
speechRecognizer.delegate = speechRecognizerDelegate
2028

21-
let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "en-US"))!
22-
let speechRecognizerDelegate = SpeechRecognizerDelegate(
23-
availabilityDidChange: { available in
24-
subscriber.send(value: .availabilityDidChange(isAvailable: available))
29+
let cancellable = AnyDisposable {
30+
audioEngine?.stop()
31+
inputNode?.removeTap(onBus: 0)
32+
recognitionTask?.cancel()
33+
_ = speechRecognizer
34+
_ = speechRecognizerDelegate
2535
}
26-
)
27-
speechRecognizer.delegate = speechRecognizerDelegate
2836

29-
let audioEngine = AVAudioEngine()
30-
let audioSession = AVAudioSession.sharedInstance()
31-
do {
32-
try audioSession.setCategory(.record, mode: .measurement, options: .duckOthers)
33-
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
34-
} catch {
35-
subscriber.send(error: .couldntConfigureAudioSession)
36-
return
37-
}
38-
let inputNode = audioEngine.inputNode
37+
lifetime += cancellable
3938

40-
let recognitionTask = speechRecognizer.recognitionTask(with: request) { result, error in
41-
switch (result, error) {
42-
case let (.some(result), _):
43-
subscriber.send(value: .taskResult(SpeechRecognitionResult(result)))
44-
case let (_, .some(error)):
45-
subscriber.send(error: .taskError)
46-
case (.none, .none):
47-
fatalError("It should not be possible to have both a nil result and nil error.")
39+
audioEngine = AVAudioEngine()
40+
let audioSession = AVAudioSession.sharedInstance()
41+
do {
42+
try audioSession.setCategory(.record, mode: .measurement, options: .duckOthers)
43+
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
44+
} catch {
45+
subscriber.send(error: .couldntConfigureAudioSession)
46+
return
4847
}
49-
}
48+
inputNode = audioEngine!.inputNode
5049

51-
dependencies[id] = SpeechDependencies(
52-
audioEngine: audioEngine,
53-
inputNode: inputNode,
54-
recognitionTask: recognitionTask,
55-
speechRecognizer: speechRecognizer,
56-
speechRecognizerDelegate: speechRecognizerDelegate,
57-
subscriber: subscriber
58-
)
50+
recognitionTask = speechRecognizer.recognitionTask(with: request) { result, error in
51+
switch (result, error) {
52+
case let (.some(result), _):
53+
subscriber.send(value: .taskResult(SpeechRecognitionResult(result)))
54+
case (_, .some):
55+
subscriber.send(error: .taskError)
56+
case (.none, .none):
57+
fatalError("It should not be possible to have both a nil result and nil error.")
58+
}
59+
}
5960

60-
inputNode.installTap(
61-
onBus: 0,
62-
bufferSize: 1024,
63-
format: inputNode.outputFormat(forBus: 0)
64-
) { buffer, when in
65-
request.append(buffer)
66-
}
61+
inputNode!.installTap(
62+
onBus: 0,
63+
bufferSize: 1024,
64+
format: inputNode!.outputFormat(forBus: 0)
65+
) { buffer, when in
66+
request.append(buffer)
67+
}
68+
69+
audioEngine!.prepare()
70+
do {
71+
try audioEngine!.start()
72+
} catch {
73+
subscriber.send(error: .couldntStartAudioEngine)
74+
return
75+
}
6776

68-
audioEngine.prepare()
69-
do {
70-
try audioEngine.start()
71-
} catch {
72-
subscriber.send(error: .couldntStartAudioEngine)
7377
return
7478
}
75-
76-
return
77-
}
78-
.cancellable(id: id)
79-
},
80-
requestAuthorization: {
81-
.future { callback in
82-
SFSpeechRecognizer.requestAuthorization { status in
83-
callback(.success(status))
79+
},
80+
requestAuthorization: {
81+
.future { callback in
82+
SFSpeechRecognizer.requestAuthorization { status in
83+
callback(.success(status))
84+
}
8485
}
8586
}
86-
}
87-
)
88-
}
89-
90-
private struct SpeechDependencies {
91-
let audioEngine: AVAudioEngine
92-
let inputNode: AVAudioInputNode
93-
let recognitionTask: SFSpeechRecognitionTask
94-
let speechRecognizer: SFSpeechRecognizer
95-
let speechRecognizerDelegate: SpeechRecognizerDelegate
96-
let subscriber: Signal<SpeechClient.Action, SpeechClient.Error>.Observer
97-
98-
func finish() {
99-
self.audioEngine.stop()
100-
self.inputNode.removeTap(onBus: 0)
101-
self.recognitionTask.finish()
102-
}
103-
104-
func cancel() {
105-
self.audioEngine.stop()
106-
self.inputNode.removeTap(onBus: 0)
107-
self.recognitionTask.cancel()
87+
)
10888
}
10989
}
11090

111-
private var dependencies: [AnyHashable: SpeechDependencies] = [:]
112-
11391
private class SpeechRecognizerDelegate: NSObject, SFSpeechRecognizerDelegate {
11492
var availabilityDidChange: (Bool) -> Void
11593

Examples/SpeechRecognition/SpeechRecognition/SpeechRecognition.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, e
4848
.observe(on: environment.mainQueue)
4949
.map(AppAction.speechRecognizerAuthorizationStatusResponse)
5050
} else {
51-
return environment.speechClient.finishTask(SpeechRecognitionId())
51+
return environment.speechClient.finishTask()
5252
.fireAndForget()
5353
}
5454

@@ -58,15 +58,15 @@ let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, e
5858
case let .speech(.success(.taskResult(result))):
5959
state.transcribedText = result.bestTranscription.formattedString
6060
if result.isFinal {
61-
return environment.speechClient.finishTask(SpeechRecognitionId())
61+
return environment.speechClient.finishTask()
6262
.fireAndForget()
6363
} else {
6464
return .none
6565
}
6666

6767
case let .speech(.failure(error)):
6868
state.alert = .init(title: .init("An error occured while transcribing. Please try again."))
69-
return environment.speechClient.finishTask(SpeechRecognitionId())
69+
return environment.speechClient.finishTask()
7070
.fireAndForget()
7171

7272
case let .speechRecognizerAuthorizationStatusResponse(status):
@@ -96,7 +96,7 @@ let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, e
9696
let request = SFSpeechAudioBufferRecognitionRequest()
9797
request.shouldReportPartialResults = true
9898
request.requiresOnDeviceRecognition = false
99-
return environment.speechClient.recognitionTask(SpeechRecognitionId(), request)
99+
return environment.speechClient.recognitionTask(request)
100100
.catchToEffect(AppAction.speech)
101101

102102
@unknown default:

Examples/SpeechRecognition/SpeechRecognitionTests/SpeechRecognitionTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,10 @@ class SpeechRecognitionTests: XCTestCase {
6161

6262
func testAllowAndRecord() {
6363
var speechClient = SpeechClient.failing
64-
speechClient.finishTask = { _ in
64+
speechClient.finishTask = {
6565
.fireAndForget { self.recognitionTaskSubject.input.sendCompleted() }
6666
}
67-
speechClient.recognitionTask = { _, _ in self.recognitionTaskSubject.output.producer }
67+
speechClient.recognitionTask = { _ in self.recognitionTaskSubject.output.producer }
6868
speechClient.requestAuthorization = { Effect(value: .authorized) }
6969

7070
let store = TestStore(
@@ -111,7 +111,7 @@ class SpeechRecognitionTests: XCTestCase {
111111

112112
func testAudioSessionFailure() {
113113
var speechClient = SpeechClient.failing
114-
speechClient.recognitionTask = { _, _ in self.recognitionTaskSubject.output.producer }
114+
speechClient.recognitionTask = { _ in self.recognitionTaskSubject.output.producer }
115115
speechClient.requestAuthorization = { Effect(value: .authorized) }
116116

117117
let store = TestStore(
@@ -141,7 +141,7 @@ class SpeechRecognitionTests: XCTestCase {
141141

142142
func testAudioEngineFailure() {
143143
var speechClient = SpeechClient.failing
144-
speechClient.recognitionTask = { _, _ in self.recognitionTaskSubject.output.producer }
144+
speechClient.recognitionTask = { _ in self.recognitionTaskSubject.output.producer }
145145
speechClient.requestAuthorization = { Effect(value: .authorized) }
146146

147147
let store = TestStore(

0 commit comments

Comments
 (0)