OpenWakeWord.AXERA / cpp /openwakeword_ax.cpp
HY-2012's picture
First commit
759d0dd verified
Raw
History Blame Contribute Delete
14.1 kB
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <limits>
#include <stdexcept>
#include <string>
#include <vector>
#include "ax_engine_api.h"
#include "ax_sys_api.h"
#include "src/engine_wrapper.hpp"
#include "src/wav_reader.hpp"
#ifndef AXERA_TARGET_NAME
#define AXERA_TARGET_NAME "AXERA"
#endif
namespace {
constexpr int kSampleRate = 16000;
constexpr int kChunkSamples = 1280;
constexpr int kHistorySamples = 480;
constexpr int kFftSize = 512;
constexpr int kSpectrumBins = 257;
constexpr int kMelBins = 32;
constexpr int kMelFrames = 8;
constexpr int kEmbeddingFrames = 76;
constexpr int kEmbeddingSize = 96;
constexpr int kFeatureFrames = 34;
using Clock = std::chrono::steady_clock;
double ElapsedSeconds(Clock::time_point begin, Clock::time_point end) {
return std::chrono::duration<double>(end - begin).count();
}
struct Args {
std::string models_dir = "models";
std::string weights = "config/openwakeword_mel_weights.bin";
std::string audio = "audio/openwakeword/alexa_test.wav";
float threshold = 0.5f;
};
void Usage(const char *program) {
std::printf(
"Usage: %s [--models-dir DIR] [--mel-weights FILE] [--audio WAV] "
"[--threshold VALUE]\n",
program);
}
Args ParseArgs(int argc, char **argv) {
Args args;
for (int i = 1; i < argc; ++i) {
const std::string key = argv[i];
auto value = [&]() -> std::string {
if (++i >= argc) throw std::runtime_error("Missing value for " + key);
return argv[i];
};
if (key == "--models-dir") {
args.models_dir = value();
} else if (key == "--mel-weights") {
args.weights = value();
} else if (key == "--audio") {
args.audio = value();
} else if (key == "--threshold") {
args.threshold = std::stof(value());
} else if (key == "-h" || key == "--help") {
Usage(argv[0]);
std::exit(0);
} else {
throw std::runtime_error("Unknown argument: " + key);
}
}
return args;
}
std::string Join(const std::string &left, const std::string &right) {
return left.empty() || left.back() == '/' ? left + right
: left + "/" + right;
}
class AxRuntime {
public:
AxRuntime() {
if (AX_SYS_Init() != 0) throw std::runtime_error("AX_SYS_Init failed");
sys_initialized_ = true;
AX_ENGINE_NPU_ATTR_T attr{};
if (AX_ENGINE_Init(&attr) != 0) {
AX_SYS_Deinit();
sys_initialized_ = false;
throw std::runtime_error("AX_ENGINE_Init failed");
}
engine_initialized_ = true;
}
~AxRuntime() {
if (engine_initialized_) AX_ENGINE_Deinit();
if (sys_initialized_) AX_SYS_Deinit();
}
private:
bool sys_initialized_ = false;
bool engine_initialized_ = false;
};
template <typename T>
T ReadScalar(std::istream &input) {
T value{};
input.read(reinterpret_cast<char *>(&value), sizeof(value));
if (!input) throw std::runtime_error("Truncated mel weight file");
return value;
}
struct MelWeights {
std::vector<float> real;
std::vector<float> imag;
std::vector<float> mel;
float floor = 0.0f;
static MelWeights Load(const std::string &path) {
std::ifstream input(path, std::ios::binary);
if (!input) throw std::runtime_error("Cannot open mel weights: " + path);
char magic[8]{};
input.read(magic, 8);
if (std::memcmp(magic, "OWWMEL1", 7) != 0 ||
ReadScalar<uint32_t>(input) != 1) {
throw std::runtime_error("Invalid openWakeWord mel weight file");
}
const uint32_t real_rows = ReadScalar<uint32_t>(input);
const uint32_t real_cols = ReadScalar<uint32_t>(input);
const uint32_t imag_rows = ReadScalar<uint32_t>(input);
const uint32_t imag_cols = ReadScalar<uint32_t>(input);
const uint32_t mel_rows = ReadScalar<uint32_t>(input);
const uint32_t mel_cols = ReadScalar<uint32_t>(input);
MelWeights result;
result.floor = ReadScalar<float>(input);
if (real_rows != kSpectrumBins || real_cols != kFftSize ||
imag_rows != kSpectrumBins || imag_cols != kFftSize ||
mel_rows != kSpectrumBins || mel_cols != kMelBins) {
throw std::runtime_error("Unexpected openWakeWord mel weight shapes");
}
result.real.resize(static_cast<std::size_t>(real_rows) * real_cols);
result.imag.resize(static_cast<std::size_t>(imag_rows) * imag_cols);
result.mel.resize(static_cast<std::size_t>(mel_rows) * mel_cols);
input.read(reinterpret_cast<char *>(result.real.data()),
result.real.size() * sizeof(float));
input.read(reinterpret_cast<char *>(result.imag.data()),
result.imag.size() * sizeof(float));
input.read(reinterpret_cast<char *>(result.mel.data()),
result.mel.size() * sizeof(float));
if (!input) throw std::runtime_error("Truncated openWakeWord mel weights");
return result;
}
};
std::array<float, kMelFrames * kMelBins> ComputeMel(
const std::array<float, kHistorySamples + kChunkSamples> &samples,
const MelWeights &weights) {
std::array<float, kMelFrames * kMelBins> result{};
std::array<float, kSpectrumBins> power{};
float max_db = -std::numeric_limits<float>::infinity();
for (int frame = 0; frame < kMelFrames; ++frame) {
const float *frame_samples = samples.data() + frame * 160;
for (int frequency = 0; frequency < kSpectrumBins; ++frequency) {
const float *real = weights.real.data() + frequency * kFftSize;
const float *imag = weights.imag.data() + frequency * kFftSize;
float real_sum = 0.0f;
float imag_sum = 0.0f;
for (int n = 0; n < kFftSize; ++n) {
real_sum += frame_samples[n] * real[n];
imag_sum += frame_samples[n] * imag[n];
}
power[frequency] = real_sum * real_sum + imag_sum * imag_sum;
}
for (int bin = 0; bin < kMelBins; ++bin) {
float value = 0.0f;
for (int frequency = 0; frequency < kSpectrumBins; ++frequency) {
value += power[frequency] *
weights.mel[frequency * kMelBins + bin];
}
value = std::max(value, weights.floor);
const float db = std::log(value) * 10.0f / 2.3025851249694824f;
result[frame * kMelBins + bin] = db;
max_db = std::max(max_db, db);
}
}
const float minimum = max_db - 80.0f;
for (float &value : result) {
value = std::max(value, minimum) / 10.0f + 2.0f;
}
return result;
}
struct Classifier {
std::string name;
int frames;
EngineWrapper engine;
std::vector<float> maximum;
};
void Run(const Args &args) {
const PcmWav wav = ReadPcmWav(args.audio);
if (wav.sample_rate != kSampleRate) {
throw std::runtime_error("Input WAV must use 16 kHz sample rate");
}
const double audio_seconds =
static_cast<double>(wav.samples.size()) / kSampleRate;
if (audio_seconds <= 0.0) {
throw std::runtime_error("Input WAV contains no samples");
}
const MelWeights weights = MelWeights::Load(args.weights);
const auto model_load_begin = Clock::now();
AxRuntime runtime;
EngineWrapper embedding;
if (embedding.Init(Join(args.models_dir,
"openwakeword__embedding_model.axmodel")) != 0) {
throw std::runtime_error("Failed to load embedding model");
}
const std::array<std::pair<const char *, int>, 6> definitions{{
{"alexa_v0.1", 16},
{"hey_jarvis_v0.1", 16},
{"hey_mycroft_v0.1", 16},
{"hey_rhasspy_v0.1", 16},
{"timer_v0.1", 34},
{"weather_v0.1", 22},
}};
std::array<Classifier, 6> classifiers;
for (std::size_t classifier_index = 0;
classifier_index < definitions.size(); ++classifier_index) {
const auto &definition = definitions[classifier_index];
Classifier &classifier = classifiers[classifier_index];
classifier.name = definition.first;
classifier.frames = definition.second;
const std::string model =
Join(args.models_dir, "openwakeword__" + classifier.name + ".axmodel");
if (classifier.engine.Init(model) != 0) {
throw std::runtime_error("Failed to load classifier: " + classifier.name);
}
const int output_bytes = classifier.engine.GetOutputSizeByName(
classifier.engine.OutputName(0));
if (output_bytes <= 0 || output_bytes % sizeof(float) != 0) {
throw std::runtime_error("Unexpected classifier output: " +
classifier.name);
}
classifier.maximum.assign(output_bytes / sizeof(float),
-std::numeric_limits<float>::infinity());
}
const double model_load_seconds =
ElapsedSeconds(model_load_begin, Clock::now());
std::vector<int16_t> padded = wav.samples;
const std::size_t remainder = padded.size() % kChunkSamples;
if (remainder != 0) padded.resize(padded.size() + kChunkSamples - remainder);
std::array<int16_t, kHistorySamples> history{};
std::array<float, kEmbeddingFrames * kMelBins> mel_buffer{};
mel_buffer.fill(1.0f);
std::array<float, kFeatureFrames * kEmbeddingSize> feature_buffer{};
int chunks = 0;
double feature_seconds = 0.0;
double npu_seconds = 0.0;
const auto inference_begin = Clock::now();
for (std::size_t start = 0; start < padded.size(); start += kChunkSamples) {
std::array<float, kHistorySamples + kChunkSamples> mel_input{};
for (int i = 0; i < kHistorySamples; ++i) mel_input[i] = history[i];
for (int i = 0; i < kChunkSamples; ++i) {
mel_input[kHistorySamples + i] = padded[start + i];
}
for (int i = 0; i < kHistorySamples; ++i) {
history[i] = padded[start + kChunkSamples - kHistorySamples + i];
}
const auto feature_begin = Clock::now();
const auto mel = ComputeMel(mel_input, weights);
feature_seconds += ElapsedSeconds(feature_begin, Clock::now());
std::memmove(mel_buffer.data(), mel_buffer.data() + kMelFrames * kMelBins,
(kEmbeddingFrames - kMelFrames) * kMelBins * sizeof(float));
std::memcpy(mel_buffer.data() +
(kEmbeddingFrames - kMelFrames) * kMelBins,
mel.data(), mel.size() * sizeof(float));
const std::string &embedding_input = embedding.InputName(0);
if (embedding.SetInputByName(embedding_input, mel_buffer.data(),
mel_buffer.size() * sizeof(float)) != 0) {
throw std::runtime_error("Failed to set embedding input");
}
const auto embedding_begin = Clock::now();
const int embedding_ret = embedding.RunSync();
npu_seconds += ElapsedSeconds(embedding_begin, Clock::now());
if (embedding_ret != 0) {
throw std::runtime_error("Embedding inference failed");
}
std::array<float, kEmbeddingSize> feature{};
if (embedding.GetOutputByName(embedding.OutputName(0), feature.data(),
feature.size() * sizeof(float)) != 0) {
throw std::runtime_error("Failed to read embedding output");
}
std::memmove(feature_buffer.data(), feature_buffer.data() + kEmbeddingSize,
(kFeatureFrames - 1) * kEmbeddingSize * sizeof(float));
std::memcpy(feature_buffer.data() +
(kFeatureFrames - 1) * kEmbeddingSize,
feature.data(), feature.size() * sizeof(float));
for (Classifier &classifier : classifiers) {
const float *input =
feature_buffer.data() +
(kFeatureFrames - classifier.frames) * kEmbeddingSize;
const std::string &input_name = classifier.engine.InputName(0);
if (classifier.engine.SetInputByName(
input_name, input,
classifier.frames * kEmbeddingSize * sizeof(float)) != 0) {
throw std::runtime_error("Failed to set classifier input: " +
classifier.name);
}
const auto classifier_begin = Clock::now();
const int classifier_ret = classifier.engine.RunSync();
npu_seconds += ElapsedSeconds(classifier_begin, Clock::now());
if (classifier_ret != 0) {
throw std::runtime_error("Classifier inference failed: " +
classifier.name);
}
std::vector<float> output(classifier.maximum.size());
if (classifier.engine.GetOutputByName(
classifier.engine.OutputName(0), output.data(),
output.size() * sizeof(float)) != 0) {
throw std::runtime_error("Failed to read classifier output");
}
for (std::size_t i = 0; i < output.size(); ++i) {
classifier.maximum[i] = std::max(classifier.maximum[i], output[i]);
}
}
++chunks;
}
const double inference_seconds =
ElapsedSeconds(inference_begin, Clock::now());
const double rtf = inference_seconds / audio_seconds;
std::printf("\nopenWakeWord C++ inference complete\n");
std::printf("target: %s\naudio: %s\nchunks: %d\nthreshold: %.3f\n",
AXERA_TARGET_NAME, args.audio.c_str(), chunks, args.threshold);
bool detected = false;
for (const Classifier &classifier : classifiers) {
std::printf("%-22s", classifier.name.c_str());
float score = -std::numeric_limits<float>::infinity();
for (std::size_t i = 0; i < classifier.maximum.size(); ++i) {
const float value = classifier.maximum[i];
std::printf(" %.6f", value);
if (classifier.name != "timer_v0.1" || i != 0) {
score = std::max(score, value);
}
}
if (score >= args.threshold) {
std::printf(" WAKEUP");
detected = true;
}
std::printf("\n");
}
std::printf("detected: %s\n", detected ? "true" : "false");
std::printf("audio_seconds: %.6f\n", audio_seconds);
std::printf("feature_seconds: %.6f\n", feature_seconds);
std::printf("npu_seconds: %.6f\n", npu_seconds);
std::printf("model_load_seconds: %.6f\n", model_load_seconds);
std::printf("inference_seconds: %.6f\n", inference_seconds);
std::printf("rtf: %.6f\n", rtf);
}
} // namespace
int main(int argc, char **argv) {
try {
const Args args = ParseArgs(argc, argv);
Run(args);
return 0;
} catch (const std::exception &error) {
std::fprintf(stderr, "ERROR: %s\n", error.what());
return 1;
}
}