Skip to main content

Audio Storage (LittleFS)

No physical wiring is needed for audio storage. Alarm audio files are stored in a dedicated LittleFS partition in the ESP32's flash memory — no SD card module, no extra wires, no separate connector. This page explains the partition layout, how to write an audio file to flash during development, and how the OTA audio update mechanism works.


LittleFS vs. SD Card

LittleFS (this project)SD card
Physical connectionNone — uses built-in flashSPI wiring, CS pin, module board
Power-fail safetyYes — copy-on-write filesystemNo — FAT32 can corrupt on power loss
Update without reflashYes — HTTP OTA uploadYes — remove card, copy file, reinsert
Swap audio without reflashYes — HTTP uploadYes — copy file to card
Cost$0 (uses existing flash)$3–5 (module + card)

Partition Layout

The partition table lives in partitions.csv in the firmware project. A 4 MB LittleFS partition alongside a 3 MB app partition is comfortable in the 32 MB flash:

# Name, Type, SubType, Offset, Size
nvs, data, nvs, 0x9000, 0x5000
phy_init, data, phy, 0xe000, 0x1000
factory, app, factory, 0x10000, 3M
lfs, data, spiffs, , 4M

The spiffs subtype is how ESP-IDF's partition tool identifies a data partition that will be used with either SPIFFS or LittleFS — the filesystem choice is made in the driver, not the partition table.

The lfs label must match the partition_label field in the firmware driver configuration (see the LittleFS driver doc).


Writing Audio Files During Development

Step 1 — Prepare the audio directory

Create a local directory to mirror the LittleFS filesystem layout:

audio/
└── alarms/
└── alarm.wav

Place your 16-bit PCM WAV file (mono or stereo, 22–44 kHz) at audio/alarms/alarm.wav.

Step 2 — Build the LittleFS image

Use mklittlefs to package the directory into a binary image:

mklittlefs -c audio/ -b 4096 -p 256 -s 0x400000 lfs.bin
FlagValueMeaning
-c audio/Source directory to package
-b 40964096 bytesBlock size (match your flash page size)
-p 256256 bytesPage size
-s 0x4000004 MBImage size (match the lfs partition size)

Install mklittlefs:

pip install mklittlefs # or download from GitHub

Step 3 — Flash the image

Look up the lfs partition's offset in partitions.csv (or run idf.py partition-table to confirm). Then flash:

esptool.py --port /dev/ttyUSB0 write_flash 0x310000 lfs.bin

Replace 0x310000 with the actual offset of the lfs partition in your partition table.

note

idf.py flash flashes the firmware binary only, not the LittleFS image. You must flash the LittleFS image separately with esptool.py whenever you update the audio file during development.


Mounting LittleFS in Firmware

#include "esp_littlefs.h"

esp_vfs_littlefs_conf_t conf = {
.base_path = "/lfs",
.partition_label = "lfs",
.format_if_mount_failed = false, // use true during initial development only
.dont_mount = false,
};

esp_err_t err = esp_vfs_littlefs_register(&conf);
if (err != ESP_OK) {
ESP_LOGE("lfs", "Failed to mount LittleFS: %s", esp_err_to_name(err));
return;
}
ESP_LOGI("lfs", "LittleFS mounted at /lfs");

After mounting, files are accessible via standard POSIX calls:

FILE *f = fopen("/lfs/alarms/alarm.wav", "rb");
if (!f) {
ESP_LOGE("lfs", "alarm.wav not found");
return;
}
// ... read and play ...
fclose(f);

OTA Audio Update

When the clock is connected to WiFi, you can update the alarm audio file over HTTP without reflashing the firmware. The firmware receives the new WAV file via an HTTP POST to /upload, writes it to a temporary path, then atomically renames it to the live path:

esp_err_t lfs_update_alarm_wav(const uint8_t *data, size_t len) {
// Write to temp file first — if power is lost mid-write, the original is intact
FILE *tmp = fopen("/lfs/alarms/alarm_tmp.wav", "wb");
if (!tmp) return ESP_FAIL;

size_t written = fwrite(data, 1, len, tmp);
fclose(tmp);

if (written != len) {
unlink("/lfs/alarms/alarm_tmp.wav");
return ESP_FAIL;
}

// Atomic rename — LittleFS guarantees this is power-fail safe
if (rename("/lfs/alarms/alarm_tmp.wav", "/lfs/alarms/alarm.wav") != 0) {
return ESP_FAIL;
}

ESP_LOGI("lfs", "alarm.wav updated successfully");
return ESP_OK;
}

Why temp-then-rename? If power is lost while writing the new file directly to /lfs/alarms/alarm.wav, the file would be partially written and the alarm would fail on next boot. By writing to a temp file and renaming only on success, the original file remains intact. LittleFS's copy-on-write design makes rename() atomic — either the whole rename completes or neither file is modified.


Verification

Add this check to your bring-up sequence to confirm the partition mounted and the audio file is present:

FILE *f = fopen("/lfs/alarms/alarm.wav", "rb");
if (f) {
fseek(f, 0, SEEK_END);
long size = ftell(f);
fclose(f);
ESP_LOGI("lfs", "alarm.wav found, size %ld bytes", size);
} else {
ESP_LOGE("lfs", "alarm.wav not found — check partition flash and path");
}

Expected serial output:

I (xxx) lfs: LittleFS mounted at /lfs
I (xxx) lfs: alarm.wav found, size 1234567 bytes

Gotchas

format_if_mount_failed = false in production — if set to true, a mount failure silently reformats the partition and loses all data. Use false in production; the error log will tell you if the partition image is missing or corrupted.

Flash offset must match partition table — if esptool.py writes to the wrong offset, the partition label will not match and esp_vfs_littlefs_register will return ESP_FAIL. Run idf.py partition-table to confirm the offset before flashing.

WAV format — the audio driver expects 16-bit PCM WAV. Verify your WAV file is not compressed (MP3 headers inside a WAV container will not play). On macOS, afinfo alarm.wav confirms the format.