Sound waves visualization for voice recognition ios SwiftUI (Speech framework)

Viewed 341

I am using SwiftSpeech framework (convenience framework for ios Speech) to listen to user's voice input and parse speech. I would like to add wave style visualization for what the user is speaking. Not just abstract gif indicating the app is listening, but an actual real time representation of sound waves made by user. In my current implementation, if I turn on the visualization, recognition works with HUGE lags - I guess recognition and visualization both work on same AVAudioSession (shared one for microphone) and they mess with each other. Is there a way to make speech recognition work fast while also visualizing user's input?

Following is all the relevant code. Note that this is just a test project, and in an actual app it works much much worse. But still you can see the difference if you comment out the visualization part.

import SwiftUI
import Speech
import SwiftSpeech
import Combine

struct ContentView: View {

    @State private var searchQuery = ""
    @State private var speechSession: SwiftSpeech.Session?
    @State private var cancelBag = Set<AnyCancellable>()

    var body: some View {
        VStack {
            Text(searchQuery)
            Spacer()
            MicrophoneVisualization()
            Button("Listen") {
                if SFSpeechRecognizer.authorizationStatus() == .authorized {
                    startListening()
                } else {
                    SFSpeechRecognizer.requestAuthorization { _ in
                        startListening()
                    }
                }
            }
        }
        .padding()
    }

    func startListening() {
        speechSession = SwiftSpeech.Session(configuration: SwiftSpeech.Session.Configuration(locale: Locale(identifier: "en-US"), audioSessionConfiguration: .playAndRecord))

        speechSession?.stringPublisher?.sink(receiveCompletion: { _ in }, receiveValue: { value in
            self.searchQuery = value
        })
            .store(in: &cancelBag)

        speechSession?.startRecording()
    }
}

Here is my visualization code:

fileprivate let numberOfSamples: Int = 25

struct MicrophoneVisualization: View {

    @ObservedObject private var mic = MicrophoneMonitor(numberOfSamples: numberOfSamples)

    private func normalizeSoundLevel(level: Float, maxHeight: CGFloat) -> CGFloat {
        let level = max(0.2, CGFloat(level) + 50) / 2 // between 0.1 and 25
        return CGFloat(level * (maxHeight / 25))
    }

    var body: some View {
        GeometryReader { g in
            main(spacing: g.size.width / (2 * CGFloat(numberOfSamples)),
                 height: g.size.height)
        }
    }

    func main(spacing: CGFloat, height: CGFloat) -> some View {
        VStack {
            HStack(spacing: spacing) {
                ForEach(mic.soundSamples.indices, id: \.self) { i in
                    BarView(value: self.normalizeSoundLevel(level: mic.soundSamples[i], maxHeight: height),
                            width: spacing)
                }
            }
        }
        .frame(maxHeight: .infinity)
    }
}

struct BarView: View {
    
    var value: CGFloat
    var width: CGFloat

    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: width / 2)
                .foregroundColor(.red)
                .frame(width: width, height: max(width, value))
        }
    }
}

class MicrophoneMonitor: ObservableObject {

    private var audioRecorder: AVAudioRecorder
    private var timer: Timer?

    private var currentSample: Int
    private let numberOfSamples: Int

    @Published public var soundSamples: [Float]

    init(numberOfSamples: Int) {
        self.numberOfSamples = numberOfSamples
        self.soundSamples = [Float](repeating: .zero, count: numberOfSamples)
        self.currentSample = 0

        let audioSession = AVAudioSession.sharedInstance()
        if audioSession.recordPermission != .granted {
            audioSession.requestRecordPermission { (isGranted) in
                if !isGranted {
                    fatalError("You must allow audio recording for this demo to work")
                }
            }
        }

        let url = URL(fileURLWithPath: "/dev/null", isDirectory: true)
        let recorderSettings: [String:Any] = [
            AVFormatIDKey: NSNumber(value: kAudioFormatAppleLossless),
            AVSampleRateKey: 44100.0,
            AVNumberOfChannelsKey: 1,
            AVEncoderAudioQualityKey: AVAudioQuality.min.rawValue
        ]

        do {
            audioRecorder = try AVAudioRecorder(url: url, settings: recorderSettings)
            try audioSession.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker])

            startMonitoring()
        } catch {
            fatalError(error.localizedDescription)
        }
    }

    private func startMonitoring() {
        audioRecorder.isMeteringEnabled = true
        audioRecorder.record()
        self.timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true, block: { (timer) in
            self.audioRecorder.updateMeters()
            DispatchQueue.main.async {
                self.soundSamples[self.currentSample] = self.audioRecorder.averagePower(forChannel: 0)
                self.currentSample = (self.currentSample + 1) % self.numberOfSamples
            }
        })
    }

    deinit {
        timer?.invalidate()
        audioRecorder.stop()
    }
}
0 Answers
Related