#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