+ New

utensil

Public
← utensil / examples / speech_commands_loader.h
#ifndef SPEECH_COMMANDS_LOADER_H
#define SPEECH_COMMANDS_LOADER_H

// Loader + feature extraction for the Google Speech Commands dataset
// (v0.02, http://download.tensorflow.org/data/speech_commands_v0.02.tar.gz).
// Each utterance is a ~1s, 16kHz, mono, 16-bit PCM WAV file living in a
// directory named after the word spoken. This header turns that directory
// tree into fixed-length [T, NMEL] log-mel spectrograms plus integer labels,
// the same role mnist_loader.h/cifar10_loader.h play for their datasets.
//
// Classification task: the 10 core command words, plus "_unknown_" (a
// subsample of the other ~25 recorded words) and "_silence_" (crops of the
// bundled background noise clips) -- the standard 12-way Speech Commands
// benchmark. Include this after utensil.h and dirent.h is available (POSIX;
// this loader is example-only code, not part of the library itself).

#include <dirent.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define SC_SR 16000
#define SC_FRAME 400  // 25ms analysis window
#define SC_HOP 160    // 10ms hop
#define SC_NFFT 512   // next power of 2 >= SC_FRAME
#define SC_NMEL 40
#define SC_CLIP_LEN SC_SR                       // every clip is padded/truncated to 1s
#define SC_T ((SC_CLIP_LEN - SC_FRAME) / SC_HOP + 1)  // frames per clip
#define SC_NCLASS 12
#define SC_PI 3.14159265358979323846f

static const char* SC_CLASSES[SC_NCLASS] = {"yes",  "no",   "up",         "down",
                                             "left", "right", "on",        "off",
                                             "stop", "go",   "_unknown_", "_silence_"};

// caps keep feature extraction and the resulting in-memory dataset bounded
// regardless of how many raw files a class actually has
#define SC_MAX_TRAIN_PER_CLASS 2000
#define SC_MAX_VAL_PER_CLASS 300
#define SC_MAX_TEST_PER_CLASS 400

typedef struct {
  int n;
  float* feats;     // [n, SC_T, SC_NMEL]
  uint8_t* labels;  // [n], index into SC_CLASSES
} sc_dataset_t;

// =========================================================
// WAV reading
// =========================================================

// Reads a 16-bit PCM mono WAV file into [-1,1] floats. Returns NULL (and
// frees any partial buffer) if the file is missing or not in that format --
// every clip in the dataset is, so callers just skip a NULL return.
static float* sc_wav_read(const char* path, int* n) {
  FILE* f = fopen(path, "rb");
  if (!f) return NULL;
  char tag[4];
  uint32_t sz;
  if (fread(tag, 1, 4, f) != 4 || memcmp(tag, "RIFF", 4) != 0) { fclose(f); return NULL; }
  fread(&sz, 4, 1, f);
  if (fread(tag, 1, 4, f) != 4 || memcmp(tag, "WAVE", 4) != 0) { fclose(f); return NULL; }

  int channels = 0, bits = 0;
  uint32_t rate = 0;
  int16_t* pcm = NULL;
  uint32_t nsamp = 0;

  while (fread(tag, 1, 4, f) == 4 && fread(&sz, 4, 1, f) == 1) {
    if (memcmp(tag, "fmt ", 4) == 0) {
      uint16_t fmt_tag, ch, block_align, bps;
      uint32_t sr, byte_rate;
      fread(&fmt_tag, 2, 1, f);
      fread(&ch, 2, 1, f);
      fread(&sr, 4, 1, f);
      fread(&byte_rate, 4, 1, f);
      fread(&block_align, 2, 1, f);
      fread(&bps, 2, 1, f);
      channels = ch;
      rate = sr;
      bits = bps;
      if (sz > 16) fseek(f, (long)(sz - 16), SEEK_CUR);
      (void)byte_rate;
      (void)block_align;
    } else if (memcmp(tag, "data", 4) == 0) {
      nsamp = sz / 2;
      pcm = malloc(sz);
      if (fread(pcm, 1, sz, f) != sz) { free(pcm); pcm = NULL; }
    } else {
      fseek(f, (long)sz, SEEK_CUR);
    }
    if (sz & 1) fseek(f, 1, SEEK_CUR);  // chunks are word-aligned
  }
  fclose(f);

  if (!pcm || channels != 1 || bits != 16 || rate != SC_SR) {
    free(pcm);
    return NULL;
  }
  float* out = malloc((size_t)nsamp * sizeof(float));
  for (uint32_t i = 0; i < nsamp; i++) out[i] = (float)pcm[i] / 32768.f;
  free(pcm);
  *n = (int)nsamp;
  return out;
}

// =========================================================
// Log-mel feature extraction
// =========================================================

// In-place iterative radix-2 Cooley-Tukey FFT; n must be a power of 2.
static void sc_fft(float* re, float* im, int n) {
  for (int i = 1, j = 0; i < n; i++) {
    int bit = n >> 1;
    for (; j & bit; bit >>= 1) j ^= bit;
    j ^= bit;
    if (i < j) {
      float t = re[i];
      re[i] = re[j];
      re[j] = t;
      t = im[i];
      im[i] = im[j];
      im[j] = t;
    }
  }
  for (int len = 2; len <= n; len <<= 1) {
    float ang = -2.f * SC_PI / (float)len;
    float wr = cosf(ang), wi = sinf(ang);
    for (int i = 0; i < n; i += len) {
      float cwr = 1.f, cwi = 0.f;
      for (int k = 0; k < len / 2; k++) {
        float ur = re[i + k], ui = im[i + k];
        float vr = re[i + k + len / 2] * cwr - im[i + k + len / 2] * cwi;
        float vi = re[i + k + len / 2] * cwi + im[i + k + len / 2] * cwr;
        re[i + k] = ur + vr;
        im[i + k] = ui + vi;
        re[i + k + len / 2] = ur - vr;
        im[i + k + len / 2] = ui - vi;
        float nwr = cwr * wr - cwi * wi;
        float nwi = cwr * wi + cwi * wr;
        cwr = nwr;
        cwi = nwi;
      }
    }
  }
}

// Triangular mel filterbank, HTK mel scale, 0..Nyquist. fb[m][k] weights FFT
// bin k (0..SC_NFFT/2) into mel band m.
static void sc_mel_filterbank(float fb[SC_NMEL][SC_NFFT / 2 + 1]) {
  float low_mel = 0.f;
  float high_mel = 2595.f * log10f(1.f + (SC_SR / 2.f) / 700.f);
  int bin[SC_NMEL + 2];
  for (int i = 0; i < SC_NMEL + 2; i++) {
    float mel = low_mel + (high_mel - low_mel) * (float)i / (float)(SC_NMEL + 1);
    float hz = 700.f * (powf(10.f, mel / 2595.f) - 1.f);
    bin[i] = (int)floorf((SC_NFFT + 1) * hz / SC_SR);
  }
  memset(fb, 0, sizeof(float) * SC_NMEL * (SC_NFFT / 2 + 1));
  for (int m = 1; m <= SC_NMEL; m++) {
    int f_prev = bin[m - 1], f_curr = bin[m], f_next = bin[m + 1];
    for (int k = f_prev; k < f_curr; k++)
      if (k >= 0 && k <= SC_NFFT / 2 && f_curr > f_prev)
        fb[m - 1][k] = (float)(k - f_prev) / (float)(f_curr - f_prev);
    for (int k = f_curr; k < f_next; k++)
      if (k >= 0 && k <= SC_NFFT / 2 && f_next > f_curr)
        fb[m - 1][k] = (float)(f_next - k) / (float)(f_next - f_curr);
  }
}

// samples/n: raw waveform (any length -- shorter than 1s is zero-padded,
// longer is truncated, matching the dataset's mostly-1s clips). Writes
// SC_T*SC_NMEL log-mel values into out, row-major [SC_T, SC_NMEL].
static void sc_logmel(const float* samples, int n, const float fb[SC_NMEL][SC_NFFT / 2 + 1],
                      float* out) {
  float frame[SC_NFFT], re[SC_NFFT], im[SC_NFFT], power[SC_NFFT / 2 + 1];
  for (int t = 0; t < SC_T; t++) {
    int start = t * SC_HOP;
    for (int k = 0; k < SC_NFFT; k++) {
      if (k >= SC_FRAME) {
        frame[k] = 0.f;
        continue;
      }
      float s = (start + k < n) ? samples[start + k] : 0.f;
      float w = 0.5f - 0.5f * cosf(2.f * SC_PI * (float)k / (float)(SC_FRAME - 1));
      frame[k] = s * w;
    }
    memcpy(re, frame, sizeof(frame));
    memset(im, 0, sizeof(im));
    sc_fft(re, im, SC_NFFT);
    for (int k = 0; k <= SC_NFFT / 2; k++) power[k] = (re[k] * re[k] + im[k] * im[k]) / SC_NFFT;
    for (int m = 0; m < SC_NMEL; m++) {
      float e = 0.f;
      for (int k = 0; k <= SC_NFFT / 2; k++) e += power[k] * fb[m][k];
      out[t * SC_NMEL + m] = logf(e + 1e-6f);
    }
  }
}

// =========================================================
// Dataset assembly
// =========================================================

typedef struct {
  char** paths;  // "word/file.wav", relative to the dataset root
  int n, cap;
} sc_pathlist_t;

static char* sc_strdup(const char* s) {
  size_t len = strlen(s) + 1;
  char* out = malloc(len);
  memcpy(out, s, len);
  return out;
}

static void sc_pathlist_add(sc_pathlist_t* l, const char* rel) {
  if (l->n == l->cap) {
    l->cap = l->cap ? l->cap * 2 : 1024;
    l->paths = realloc(l->paths, (size_t)l->cap * sizeof(char*));
  }
  l->paths[l->n++] = sc_strdup(rel);
}

static void sc_pathlist_free(sc_pathlist_t* l) {
  for (int i = 0; i < l->n; i++) free(l->paths[i]);
  free(l->paths);
}

// loads testing_list.txt or validation_list.txt ("word/file.wav" per line)
static void sc_load_split_list(const char* root, const char* name, sc_pathlist_t* out) {
  char path[1024];
  snprintf(path, sizeof(path), "%s/%s", root, name);
  FILE* f = fopen(path, "r");
  if (!f) return;
  char line[512];
  while (fgets(line, sizeof(line), f)) {
    size_t l = strlen(line);
    while (l > 0 && (line[l - 1] == '\n' || line[l - 1] == '\r')) line[--l] = 0;
    if (l > 0) sc_pathlist_add(out, line);
  }
  fclose(f);
}

static int sc_in_pathlist(sc_pathlist_t* l, const char* rel) {
  for (int i = 0; i < l->n; i++)
    if (strcmp(l->paths[i], rel) == 0) return 1;
  return 0;
}

static void sc_shuffle_str(char** a, int n) {
  for (int i = n - 1; i > 0; i--) {
    int j = rand() % (i + 1);
    char* t = a[i];
    a[i] = a[j];
    a[j] = t;
  }
}

typedef enum { SC_TRAIN, SC_VAL, SC_TEST } sc_split_t;

// Scans `root` (the extracted speech_commands_v0.02 directory) and builds
// one split. `root` must contain testing_list.txt/validation_list.txt (the
// dataset's own split) plus a _background_noise_ directory (for
// "_silence_"). Every word directory not in SC_CLASSES[0..9] is a candidate
// "_unknown_" source; feature extraction runs once per clip here, so the
// caller pays this cost once per split, not once per epoch.
static sc_dataset_t sc_load_split(const char* root, sc_split_t split) {
  sc_pathlist_t test_list = {0}, val_list = {0};
  sc_load_split_list(root, "testing_list.txt", &test_list);
  sc_load_split_list(root, "validation_list.txt", &val_list);

  int max_per_class = split == SC_TRAIN   ? SC_MAX_TRAIN_PER_CLASS
                      : split == SC_VAL   ? SC_MAX_VAL_PER_CLASS
                                          : SC_MAX_TEST_PER_CLASS;

  static float fb[SC_NMEL][SC_NFFT / 2 + 1];
  static int fb_ready = 0;
  if (!fb_ready) {
    sc_mel_filterbank(fb);
    fb_ready = 1;
  }

  sc_dataset_t d = {0};
  int cap = SC_NCLASS * max_per_class;
  d.feats = malloc((size_t)cap * SC_T * SC_NMEL * sizeof(float));
  d.labels = malloc((size_t)cap);

  DIR* rd = opendir(root);
  if (!rd) return d;
  struct dirent* ent;

  // core keywords + "_unknown_" source words share one directory scan
  sc_pathlist_t unknown_pool = {0};
  while ((ent = readdir(rd))) {
    if (ent->d_name[0] == '.' || strcmp(ent->d_name, "_background_noise_") == 0) continue;
    char wordpath[768];
    snprintf(wordpath, sizeof(wordpath), "%s/%s", root, ent->d_name);
    DIR* wd = opendir(wordpath);
    if (!wd) continue;
    int label = -1;
    for (int c = 0; c < 10; c++)
      if (strcmp(ent->d_name, SC_CLASSES[c]) == 0) label = c;

    sc_pathlist_t files = {0};
    struct dirent* we;
    while ((we = readdir(wd))) {
      if (we->d_name[0] == '.') continue;
      char rel[512];
      snprintf(rel, sizeof(rel), "%s/%s", ent->d_name, we->d_name);
      int is_test = sc_in_pathlist(&test_list, rel);
      int is_val = sc_in_pathlist(&val_list, rel);
      int want = (split == SC_TEST && is_test) || (split == SC_VAL && is_val) ||
                 (split == SC_TRAIN && !is_test && !is_val);
      if (want) sc_pathlist_add(&files, rel);
    }
    closedir(wd);

    if (label >= 0) {
      sc_shuffle_str(files.paths, files.n);
      int take = files.n < max_per_class ? files.n : max_per_class;
      for (int i = 0; i < take && d.n < cap; i++) {
        char full[1024];
        snprintf(full, sizeof(full), "%s/%s", root, files.paths[i]);
        int n;
        float* samples = sc_wav_read(full, &n);
        if (!samples) continue;
        sc_logmel(samples, n, fb, d.feats + (size_t)d.n * SC_T * SC_NMEL);
        d.labels[d.n] = (uint8_t)label;
        d.n++;
        free(samples);
      }
    } else {
      for (int i = 0; i < files.n; i++) sc_pathlist_add(&unknown_pool, files.paths[i]);
    }
    sc_pathlist_free(&files);
  }
  closedir(rd);

  // "_unknown_"
  sc_shuffle_str(unknown_pool.paths, unknown_pool.n);
  int take = unknown_pool.n < max_per_class ? unknown_pool.n : max_per_class;
  for (int i = 0; i < take && d.n < cap; i++) {
    char full[1024];
    snprintf(full, sizeof(full), "%s/%s", root, unknown_pool.paths[i]);
    int n;
    float* samples = sc_wav_read(full, &n);
    if (!samples) continue;
    sc_logmel(samples, n, fb, d.feats + (size_t)d.n * SC_T * SC_NMEL);
    d.labels[d.n] = 10;  // "_unknown_"
    d.n++;
    free(samples);
  }
  sc_pathlist_free(&unknown_pool);

  // "_silence_": random 1s crops of the background noise recordings. Splits
  // draw from disjoint offset ranges so the same crop can't leak across
  // train/val/test.
  char bgpath[768];
  snprintf(bgpath, sizeof(bgpath), "%s/_background_noise_", root);
  DIR* bd = opendir(bgpath);
  if (bd) {
    sc_pathlist_t bg_files = {0};
    struct dirent* be;
    while ((be = readdir(bd))) {
      if (be->d_name[0] == '.' || strstr(be->d_name, ".wav") == NULL) continue;
      sc_pathlist_add(&bg_files, be->d_name);
    }
    closedir(bd);
    for (int i = 0; i < max_per_class && d.n < cap && bg_files.n > 0; i++) {
      const char* fname = bg_files.paths[rand() % bg_files.n];
      char full[1024];
      snprintf(full, sizeof(full), "%s/%s", bgpath, fname);
      int n;
      float* samples = sc_wav_read(full, &n);
      if (!samples || n <= SC_CLIP_LEN) {
        free(samples);
        continue;
      }
      // carve the file into thirds, one per split, then pick a random crop within it
      int third = (n - SC_CLIP_LEN) / 3;
      int lo = split * third, hi = lo + third;
      int start = lo + (hi > lo ? rand() % (hi - lo) : 0);
      sc_logmel(samples + start, SC_CLIP_LEN, fb, d.feats + (size_t)d.n * SC_T * SC_NMEL);
      d.labels[d.n] = 11;  // "_silence_"
      d.n++;
      free(samples);
    }
    sc_pathlist_free(&bg_files);
  }

  sc_pathlist_free(&test_list);
  sc_pathlist_free(&val_list);
  return d;
}

static void sc_dataset_free(sc_dataset_t* d) {
  free(d->feats);
  free(d->labels);
}

static void sc_shuffle(int* a, int n) {
  for (int i = n - 1; i > 0; i--) {
    int j = rand() % (i + 1);
    int t = a[i];
    a[i] = a[j];
    a[j] = t;
  }
}

static int sc_argmax(ut_tensor* t, int row) {
  int C = t->shape.shape[1], best = 0;
  float* r = t->data + row * C;
  for (int j = 1; j < C; j++)
    if (r[j] > r[best]) best = j;
  return best;
}

#endif