Skip to content

PhraseSpot

This Swift application runs a phrase spotter and shows the results in a text window.

Instructions

Open sample/ios/PhraseSpot/PhraseSpot.xcodeproj in Xcode, then choose Run from the Product menu.

To run on a real device, select your Team in General > Signing and change Identity > Bundle Identifier to match your development domain.

The app starts listening for "hello blue genie" upon startup. Say this trigger phrase to see the recognizer response.

Code

This application uses the native C API of the TrulyNatural SDK by using a Swift bridging header.

Available in this TrulyNatural SDK installation at ~/Sensory/TrulyNaturalSDK/7.6.1/sample/ios/PhraseSpot/PhraseSpot/PhraseSpot.swift

PhraseSpot.swift

//
//  PhraseSpot.swift
//  PhraseSpot
//
//  Copyright © 2018-2025 Sensory, Inc. https://sensory.com/
//  All rights reserved.
//

import Foundation
import AVFoundation

protocol PhraseSpotDelegate: AnyObject {
    func recogniserWillStart()
    func recognizerDidStop(code: SnsrRC, message: String)
    func recognizerDidSpot(text: String, beginMs: Double, endMs: Double)
}

enum PhraseSpotError: Error {
    case api(code: SnsrRC, message: String)
}

class PhraseSpot {
    //MARK: Nested classes
    enum State {
        case stopped, started, paused
    }

    //MARK: Private properties
    private var session: SnsrSession?
    private var audio: SnsrStream?
    private var rC: SnsrRC = SNSR_RC_OK


    //MARK: Properties
    var libraryInfo: String?
    weak var delegate: PhraseSpotDelegate?
    var state: State = .stopped {
        didSet {
            if state == .started {
                startRecog()
            }
        }
    }

    //MARK: Initialization
    init(modelName: String) throws {
        try load(modelName: modelName)
    }

    deinit {
        if state == .started {
            stop()
        }
        release(&session)
        release(&audio)
    }

    //MARK: Private methods
    // C library wrappers, for convenience
    private func release(_ ptr: inout Optional<OpaquePointer>) {
        snsrRelease(UnsafeRawPointer(ptr))
        ptr = nil
    }

    private func retain(_ ptr: Optional<OpaquePointer>) {
        snsrRetain(UnsafeRawPointer(ptr))
    }

    // Find a model in the applications main bundle.
    private func modelPath(_ modelName: String) -> String {
        guard let path = Bundle.main.path(forResource: modelName, ofType: "snsr", inDirectory: "models") else {
            return modelName
        }
        return path
    }

    // Create and throw a PhraseSpotError.api
    private func throwIfError(_ session: SnsrSession?) throws {
        let rc = snsrRC(session)
        if (rc != SNSR_RC_OK) {
            let msg = String(cString: snsrErrorDetail(session))
            print(msg)
            throw PhraseSpotError.api(code: rc, message: msg)
        }
    }

    private func load(modelName: String) throws {
        snsrNewIncludeOSS(&session, SNSR_VERSION)
        try throwIfError(session)
        var libInfo: UnsafePointer<CChar>?
        snsrGetString(session, SNSR_LIBRARY_INFO, &libInfo)
        try throwIfError(session)
        libraryInfo = String(cString: libInfo!)
        snsrLoad(session, snsrStreamFromFileName(modelPath(modelName), "r"))
        snsrRequire(session, SNSR_TASK_TYPE, SNSR_PHRASESPOT)
        snsrRequire(session, SNSR_TASK_VERSION, "~0.5.0 || 1.0.0")

        // Convert self into a pointer to pass to the C library
        let selfPtr = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())

        // Report recognition results with a delegate.recognizerDidSpot()
        snsrSetHandler(session, SNSR_RESULT_EVENT,
                       snsrCallback({ (session, key, selfPtr) -> SnsrRC in
                        let my = Unmanaged<PhraseSpot>.fromOpaque(selfPtr!).takeUnretainedValue()
                        var text: UnsafePointer<CChar>?
                        var beginMs: Double = 0
                        var endMs: Double = 0
                        snsrGetString(session, SNSR_RES_TEXT, &text)
                        snsrGetDouble(session, SNSR_RES_BEGIN_MS, &beginMs)
                        snsrGetDouble(session, SNSR_RES_END_MS, &endMs)
                        let spot = String(cString: text!)
                        DispatchQueue.main.sync {
                            my.delegate?.recognizerDidSpot(text: spot, beginMs: beginMs, endMs: endMs)
                        }
                        return SNSR_RC_OK
                       }, nil, selfPtr))

        // Stop background recognition if the state changes from .started
        snsrSetHandler(session, SNSR_SAMPLES_EVENT,
                       snsrCallback({ (session, key, selfPtr) -> SnsrRC in
                        let my = Unmanaged<PhraseSpot>.fromOpaque(selfPtr!).takeUnretainedValue()
                        return my.state == .started ? SNSR_RC_OK : SNSR_RC_STOP
                       }, nil, selfPtr))

        // Allow Bluetooth headsets
        try AVAudioSession.sharedInstance()
            .setCategory(.playAndRecord, options: AVAudioSession.CategoryOptions.allowBluetooth)

        // Live audio
        audio = snsrStreamFromDefaultAudioDevice()
        retain(audio)
        snsrSetStream(session, SNSR_SOURCE_AUDIO_PCM, audio)
        try throwIfError(session)
    }

    // Run phrase spotter on a background thread
    private func startRecog() {
        snsrClearRC(session)
        self.delegate?.recogniserWillStart()
        DispatchQueue.global(qos: .background).async {
            let code = snsrRun(self.session)
            // Stop recording when we are not spotting
            snsrStreamClose(self.audio)
            let msg = String(cString: snsrErrorDetail(self.session))
            DispatchQueue.main.sync {
                self.delegate?.recognizerDidStop(code: code, message: msg)
            }
        }
    }


    //MARK: Public methods
    // Change scalar SnsrSession settings
    func set(_ key: String, _ value: Double) throws {
        let code = snsrSetInt(session, key, Int32(value))
        if code == SNSR_RC_INCORRECT_SETTING_TYPE {
            snsrClearRC(session)
            snsrSetDouble(session, key, value)
        }
        try throwIfError(session)
    }

    // Start the phrase spotter
    func start() {
        if state != .started {
            state = .started
        }
    }

    // Stop the recognizer
    func stop() {
        if state != .stopped {
            state = .stopped
        }
    }

    // Pause a running spotter
    func pause() {
        if state == .started {
            state = .paused
        }
    }

    // Resume a paused spotter
    func resume() {
        if state == .paused {
            state = .started
        }
    }
}