sherpa-onnx: Build similar project, get "ld: Undefined symbols"

I made a project similar to sherpa-onnx to perform inference on the onnx model. I build project structure same as sherpa-onnx and only for ios. I created an xcframework wakeword-onnx so I can link to the project in Xcode. Afrer linking xcframework and adding the bridging header file, I notices I was suggested to use C functions in the bridging header file. However, when I build the project. I get the error

ld: Undefined symbols:
  _initEnv, referenced from:
      wakewordUI.ViewController.initRecognizer() -> () in ViewController.o
  _onnxProcess, referenced from:
      closure #1 (__C.AVAudioPCMBuffer, __C.AVAudioTime) -> () in wakewordUI.ViewController.initRecorder() -> () in ViewController.o
clang: error: linker command failed with exit code 1 (use -v to see invocation)

I have found many solutions for this problem but it doesn’t seem to be clear why it causes the error to fix in my case. Appreciate for any help.

The content of ViewController and Bridging Header File is below. ViewController.swift

import AVFoundation
import UIKit

extension AudioBuffer {
    func array() -> [Float] {
        return Array(UnsafeBufferPointer(self))
    }
}

extension AVAudioPCMBuffer {
    func array() -> [Float] {
        return self.audioBufferList.pointee.mBuffers.array()
    }
}

class ViewController: UIViewController {
    @IBOutlet weak var resultLabel: UILabel!
    @IBOutlet weak var recordBtn: UIButton!

    var audioEngine: AVAudioEngine? = nil

    /// It saves the decoded results so far
    var sentences: [String] = [] {
        didSet {
            updateLabel()
        }
    }
    var lastSentence: String = ""
    var results: String {
        if sentences.isEmpty && lastSentence.isEmpty {
            return ""
        }
        if sentences.isEmpty {
            return "0: \(lastSentence.lowercased())"
        } else {
            return ""
        }
    }

    func updateLabel(mode: String = "KW") {
        DispatchQueue.main.async {
            if mode == "KW"{
                self.resultLabel.text = "Keyword"
            }
            else {
                self.resultLabel.text = "Non-keyword"
            }
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        resultLabel.text = "Keyword Spotting \n\nPress the Start button to run!"
        recordBtn.setTitle("Start", for: .normal)
        initRecognizer()
        initRecorder()
    }

    @IBAction func onRecordBtnClick(_ sender: UIButton) {
        if recordBtn.currentTitle == "Start" {
            startRecorder()
            recordBtn.setTitle("Stop", for: .normal)
        } else {
            stopRecorder()
            recordBtn.setTitle("Start", for: .normal)
        }
    }
    
    func getResource(_ forResource: String, _ ofType: String) -> String {
      let path = Bundle.main.path(forResource: forResource, ofType: ofType)
      precondition(
        path != nil,
        "\(forResource).\(ofType) does not exist!\n" + "Remember to change \n"
          + "  Build Phases -> Copy Bundle Resources\n" + "to add it!"
      )
      return path!
    }
    
    
    func initRecognizer() {
        initEnv(toCPointer(getResource("chaoVN_model", "onnx")))
    }

    func initRecorder() {
        print("init recorder")
        audioEngine = AVAudioEngine()
        let inputNode = self.audioEngine?.inputNode
        let bus = 0
        let inputFormat = inputNode?.outputFormat(forBus: bus)
        let sampleRate = inputFormat?.sampleRate
        
        let outputFormat = AVAudioFormat(
            commonFormat: .pcmFormatFloat32,
            sampleRate: 16000, channels: 1,
            interleaved: false)!

        let converter = AVAudioConverter(from: inputFormat!, to: outputFormat)!

        inputNode!.installTap(
            onBus: bus,
            bufferSize: 1024,
            format: inputFormat
        ) {
            (buffer: AVAudioPCMBuffer, when: AVAudioTime) in
            var newBufferAvailable = true

            let inputCallback: AVAudioConverterInputBlock = {
                inNumPackets, outStatus in
                if newBufferAvailable {
                    outStatus.pointee = .haveData
                    newBufferAvailable = false

                    return buffer
                } else {
                    outStatus.pointee = .noDataNow
                    return nil
                }
            }

            let convertedBuffer = AVAudioPCMBuffer(
                pcmFormat: outputFormat,
                frameCapacity:
                    AVAudioFrameCount(outputFormat.sampleRate)
                * buffer.frameLength
                / AVAudioFrameCount(buffer.format.sampleRate))!

            var error: NSError?
            let _ = converter.convert(
                to: convertedBuffer,
                error: &error, withInputFrom: inputCallback)

            // TODO(fangjun): Handle status != haveData

            let array = convertedBuffer.array()
            if !array.isEmpty {
                let probs = onnxProcess(array, Int32(array.count))

                if probs![2] >= 13 {
                    self.updateLabel(mode: "KW")
                } else {
                    self.updateLabel(mode: "nonKW")
                }
            }
        }

    }

    func startRecorder() {
        lastSentence = ""
        sentences = []

        do {
            try self.audioEngine?.start()
        } catch let error as NSError {
            print("Got an error starting audioEngine: \(error.domain), \(error)")
        }
        print("started")
    }

    func stopRecorder() {
        audioEngine?.stop()
        print("stopped")
    }
}

wakewordOnnx_Bridging_Header.h

#define wakewordUI_Bridging_Header_h

#import "wakeword-onnx/c-api/c-api.h" 

#endif /* wakewordUI_Bridging_Header_h */

c-api.h

#ifndef WAKEWORD_ONNX_C_API_C_API_H_
#define WAKEWORD_ONNX_C_API_C_API_H_

#include <stdint.h>

#ifdef __cplusplus
extern "C" 
{
#endif

#if defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wattributes"
#endif

#if defined(_WIN32)
#if defined(WAKEWORD_ONNX_BUILD_SHARED_LIBS)
#define WAKEWORD_ONNX_EXPORT __declspec(dllexport)
#define WAKEWORD_ONNX_IMPORT __declspec(dllimport)
#else
#define WAKEWORD_ONNX_EXPORT
#define WAKEWORD_ONNX_IMPORT
#endif
#else  // WIN32
#define WAKEWORD_ONNX_EXPORT __attribute__((visibility("default")))

#define WAKEWORD_ONNX_IMPORT WAKEWORD_ONNX_EXPORT
#endif  // WIN32

#if defined(WAKEWORD_ONNX_BUILD_MAIN_LIB)
#define WAKEWORD_ONNX_API WAKEWORD_ONNX_EXPORT
#else
#define WAKEWORD_ONNX_API WAKEWORD_ONNX_IMPORT
#endif

/// Initilize the env, session options, session and fbank object 
///
/// @param inModelPath onnx model filepath
WAKEWORD_ONNX_API void initEnv(const char* inModelPath);

/// Foward waveform array to get the probalities of wakeword's presence
///
/// @param waveform float array waveform 
/// @param wavLength number of elements in waveform array
/// @return Return probs that is 3's length array or NULL
WAKEWORD_ONNX_API float* onnxProcess(const float* waveform, int wavLength);

/// Release the env, session options and session.
///
WAKEWORD_ONNX_API void releaseEnv();

// #if defined(__GNUC__)
// #pragma GCC diagnostic pop
// #endif

#ifdef __cplusplus
}  /* extern "C" */
#endif

#endif  // WAKEWORD_ONNX_C_API_C_API_H_

About this issue

  • Original URL
  • State: closed
  • Created 7 months ago
  • Comments: 21

Most upvoted comments

Maybe it was compiled as C++ and not C, e.g. needed “extern “C”” inside the .cc file also, not just the header? (not sure if C++ compiler will output “C” code in this circumstance, or you need to invoke a C compiler, e.g. change the filename to .c)