nimphea/nimphea_filetable

Search:
Group by:
Source   Edit  

FileTable

File index and metadata management for FAT filesystems on Daisy hardware.

This module provides a utility for creating an indexed table of files from a directory, with support for filtering, sorting, and metadata access. Commonly used for managing audio sample libraries (.wav files) on SD cards.

What is FileTable?

FileTable scans a directory on an SD card or QSPI flash, builds an in-memory index of files, and provides fast access to file names and sizes.

Typical use cases:

  • Loading .wav sample libraries
  • Managing preset/patch files
  • Building a file browser UI
  • Batch file operations

Key features:

  • Alphabetical sorting (case-insensitive)
  • File extension filtering (e.g., only ".wav" files)
  • Fixed-size table (compile-time capacity)
  • Metadata access (file name, size)
  • Log file generation for debugging

Basic Usage

Step 1: Define table capacity

# Create table for up to 64 files
type SampleTable = FileTable[64]

var samples: SampleTable

Step 2: Scan directory

# Load all .wav files from /samples/ directory
if samples.fill("/samples", ".wav"):
  echo "Found ", samples.getNumFiles(), " .wav files"
else:
  echo "Error scanning directory"

Step 3: Access file info

for i in 0..<samples.getNumFiles():
  let name = samples.getFileName(i)
  let size = samples.getFileSize(i)
  echo i, ": ", name, " (", size, " bytes)"

Complete Example

Sample player with SD card library:

import nimphea_filetable
import nimphea/per/sdmmc

type SampleLibrary = FileTable[128]

var samples: SampleLibrary
var sd: SdmmcHandler

proc loadSampleLibrary() =
  # Initialize SD card
  sd.init()
  
  if not samples.fill("/samples", ".wav"):
    echo "Error: Could not scan samples directory"
    return
  
  echo "Sample Library:"
  echo "---------------"
  
  for i in 0..<samples.getNumFiles():
    if samples.isFileInSlot(i):
      let name = samples.getFileName(i)
      let size = samples.getFileSize(i)
      echo i + 1, ". ", name, " (", size, " bytes)"
  
  # Optional: Write log file to SD card
  if samples.writeLog("/samples_index.txt"):
    echo "Index written to samples_index.txt"

Filtering Files

Example 1: Load only .wav files

samples.fill("/audio", ".wav")

Example 2: Load only .txt files

presets.fill("/presets", ".txt")

Example 3: Load all files (no filter)

allFiles.fill("/data", nil)  # nil = no extension filter

File Access Patterns

Pattern 1: Iterate all files

for i in 0..<samples.getNumFiles():
  let filename = samples.getFileName(i)
  processSample(filename)

Pattern 2: Check if slot has file

if samples.isFileInSlot(5):
  let name = samples.getFileName(5)
  echo "Slot 5: ", name
else:
  echo "Slot 5 is empty"

Pattern 3: Find file by name

proc findFile(table: var SampleTable, searchName: cstring): int =
  for i in 0..<table.getNumFiles():
    if table.getFileName(i) == searchName:
      return i
  return -1

let idx = findFile(samples, "kick.wav")
if idx >= 0:
  echo "Found at index ", idx

Load/Save State Management

FileTable includes optional flags for coordinating file I/O operations:

Save workflow:

# User presses "Save" button
samples.setSavePending(3)  # Mark slot 3 for saving

# In main loop
if samples.isSavePending():
  let slot = samples.getSlotForSaveLoad()
  let filename = samples.getFileName(slot)
  saveDataToFile(filename)
  samples.clearSavePending()

Load workflow:

# User selects file 7
samples.setLoadPending(7)

# In main loop
if samples.isLoadPending():
  let slot = samples.getSlotForSaveLoad()
  let filename = samples.getFileName(slot)
  loadDataFromFile(filename)
  samples.clearLoadPending()

Log File Generation

Generate a text file listing all indexed files (useful for debugging):

samples.fill("/samples", ".wav")
samples.writeLog("/samples_log.txt")

Example log file output:

1:  kick.wav    45632 bytes
2:  snare.wav   38912 bytes
3:  hihat.wav   12048 bytes

Capacity and Limitations

Compile-time capacity:

The table size is fixed at compile time via the template parameter:

type
  SmallTable = FileTable[16]   # Max 16 files
  MediumTable = FileTable[64]  # Max 64 files
  LargeTable = FileTable[256]  # Max 256 files

Memory usage:

Each slot uses ~260 bytes (255 chars for filename + size field).

FileTable[16]:  ~4KB RAM
FileTable[64]:  ~16KB RAM
FileTable[256]: ~64KB RAM

Filename length:

Maximum filename length is 255 characters (FAT long filename support).

What happens when table is full?

fill() stops after loading max_slots files. Remaining files are ignored. Use a larger table size if needed.

Sorting Behavior

Files are automatically sorted alphabetically (case-insensitive) after loading:

Directory contents:    Sorted in table:
- snare.wav            1. bass.wav
- kick.wav             2. hihat.wav
- Bass.wav             3. kick.wav
- HiHat.wav            4. snare.wav

Case-insensitive: "Bass.wav" and "bass.wav" are treated equally.

Error Handling

Check return values:

if not samples.fill("/samples", ".wav"):
  echo "ERROR: Could not scan directory"
  echo "Possible causes:"
  echo "- SD card not mounted"
  echo "- Directory does not exist"
  echo "- Filesystem error"
  return

if samples.getNumFiles() == 0:
  echo "WARNING: No .wav files found in /samples/"

Performance Notes

  • Scanning is relatively slow (depends on number of files)
  • Call fill() once during initialization, not in main loop
  • Accessing file metadata (name, size) is instant (in-memory lookup)
  • Sorting adds minimal overhead (insertion sort, ~O(n²) but n is small)

See Also

  • per/sdmmc - SD card access
  • per/qspi - QSPI flash access
  • FatFs documentation - Underlying filesystem library
Compile-time examples (not executable without hardware) All examples use discard to avoid compile errors in test mode

Types

FileTable[max_slots] {.importcpp: "daisy::FileTable<\'0>".} = object

File index table for FAT filesystem directories.

Template parameter:

  • max_slots - Maximum number of files to index (compile-time constant)

Example:

type SampleTable = FileTable[64]
var samples: SampleTable

Internal state:

  • Array of file metadata (name + size)
  • Number of files found
  • Load/save pending flags (optional state management)
Source   Edit  

Procs

proc clear[N: static int](this: var FileTable[N]) {.importcpp: "#.Clear()",
    ...raises: [], tags: [], forbids: [].}

Reset table to empty state.

Clears all file entries and resets file count to zero.

Example:

samples.fill("/samples", ".wav")
# ... use table ...
samples.clear()  # Erase all entries
samples.fill("/drums", ".wav")  # Reload with different directory

Source   Edit  
proc clearLoadPending[N: static int](this: var FileTable[N]) {.
    importcpp: "#.ClearLoadPending()", ...raises: [], tags: [], forbids: [].}
Clear the load pending flag. Source   Edit  
proc clearSavePending[N: static int](this: var FileTable[N]) {.
    importcpp: "#.ClearSavePending()", ...raises: [], tags: [], forbids: [].}
Clear the save pending flag. Source   Edit  
proc fill[N: static int](this: var FileTable[N]; path: cstring;
                         endswith: cstring = nil): bool {.
    importcpp: "#.Fill(@)", ...raises: [], tags: [], forbids: [].}

Scan directory and fill table with matching files.

Parameters:

  • path - Directory path (e.g., "/samples")
  • endswith - File extension filter (e.g., ".wav"), or nil for all files

Returns: True if scan succeeded, false on error

Behavior:

  • Scans directory recursively (one level)
  • Filters by extension if provided
  • Skips hidden files, directories, and system files
  • Sorts results alphabetically (case-insensitive)
  • Stops after max_slots files

Example:

# Load only .wav files
if samples.fill("/audio", ".wav"):
  echo "Loaded ", samples.getNumFiles(), " samples"

# Load all files
if allFiles.fill("/data", nil):
  echo "Loaded ", allFiles.getNumFiles(), " files"

Source   Edit  
proc findByName[N: static int](table: FileTable[N]; searchName: cstring): int

Find a file by name (exact match, case-sensitive).

Parameters:

  • searchName - Filename to search for

Returns: File index, or -1 if not found

Example:

let idx = samples.findByName("kick.wav")
if idx >= 0:
  echo "Found kick.wav at index ", idx
else:
  echo "kick.wav not found"

Source   Edit  
proc getFileName[N: static int](this: FileTable[N]; idx: csize_t): cstring {.
    importcpp: "#.GetFileName(@)", ...raises: [], tags: [], forbids: [].}

Get filename for the file at the given index.

Parameters:

  • idx - File index (0 to numFiles-1)

Returns: Filename as C-string (pointer to internal buffer)

Warning: Do not modify the returned string. It points to internal storage.

Example:

for i in 0..<samples.getNumFiles():
  echo i, ": ", samples.getFileName(i)

Source   Edit  
proc getFileSize[N: static int](this: FileTable[N]; idx: csize_t): csize_t {.
    importcpp: "#.GetFileSize(@)", ...raises: [], tags: [], forbids: [].}

Get file size in bytes for the file at the given index.

Parameters:

  • idx - File index (0 to numFiles-1)

Returns: File size in bytes, or 0 if slot is empty

Example:

let size = samples.getFileSize(0)
echo "First file is ", size, " bytes"

Source   Edit  
proc getNumFiles[N: static int](this: FileTable[N]): csize_t {.
    importcpp: "#.GetNumFiles()", ...raises: [], tags: [], forbids: [].}

Get the number of files found in the table.

Returns: Number of files (0 to max_slots)

Example:

samples.fill("/samples", ".wav")
echo "Found ", samples.getNumFiles(), " files"

Source   Edit  
proc getSlotForSaveLoad[N: static int](this: FileTable[N]): cint {.
    importcpp: "#.GetSlotForSaveLoad()", ...raises: [], tags: [], forbids: [].}

Get the slot index marked for save/load.

Returns: Slot index, or -1 if none pending

Example:

if samples.isLoadPending():
  let slot = samples.getSlotForSaveLoad()
  let filename = samples.getFileName(csize_t(slot))
  loadFile(filename)

Source   Edit  
proc isFileInSlot[N: static int](this: FileTable[N]; idx: csize_t): bool {.
    importcpp: "#.IsFileInSlot(@)", ...raises: [], tags: [], forbids: [].}

Check if a file exists at the given index.

Parameters:

  • idx - Slot index (0 to max_slots-1)

Returns: True if slot contains a file, false if empty

Example:

if samples.isFileInSlot(3):
  echo "Slot 3: ", samples.getFileName(3)
else:
  echo "Slot 3 is empty"

Source   Edit  
proc isLoadPending[N: static int](this: FileTable[N]): bool {.
    importcpp: "#.IsLoadPending()", ...raises: [], tags: [], forbids: [].}

Check if a load operation is pending.

Returns: True if load pending, false otherwise

Example:

if samples.isLoadPending():
  let slot = samples.getSlotForSaveLoad()
  loadFile(samples.getFileName(slot))
  samples.clearLoadPending()

Source   Edit  
proc isSavePending[N: static int](this: FileTable[N]): bool {.
    importcpp: "#.IsSavePending()", ...raises: [], tags: [], forbids: [].}

Check if a save operation is pending.

Returns: True if save pending, false otherwise

Source   Edit  
proc printTable[N: static int](table: FileTable[N])

Print all files in the table to stdout.

Example:

samples.fill("/samples", ".wav")
samples.printTable()
# Output:
# 0: kick.wav (45632 bytes)
# 1: snare.wav (38912 bytes)

Source   Edit  
proc setLoadPending[N: static int](this: var FileTable[N]; slot: cint) {.
    importcpp: "#.SetLoadPending(@)", ...raises: [], tags: [], forbids: [].}

Mark a slot for loading.

Parameters:

  • slot - Slot index to load

Example:

# User selects file 5
samples.setLoadPending(5)

Source   Edit  
proc setSavePending[N: static int](this: var FileTable[N]; slot: cint) {.
    importcpp: "#.SetSavePending(@)", ...raises: [], tags: [], forbids: [].}

Mark a slot for saving.

Parameters:

  • slot - Slot index to save
Source   Edit  
proc writeLog[N: static int](this: var FileTable[N]; log_file_name: cstring): bool {.
    importcpp: "#.WriteLog(@)", ...raises: [], tags: [], forbids: [].}

Write a log file listing all indexed files.

Parameters:

  • log_file_name - Path to log file (e.g., "/index.txt")

Returns: True if log written successfully, false on error

Log format:

1:  filename1.wav  12345 bytes
2:  filename2.wav  67890 bytes

Example:

samples.fill("/samples", ".wav")
if samples.writeLog("/samples_index.txt"):
  echo "Index written successfully"

Source   Edit  

Iterators

iterator items[N: static int](table: FileTable[N]): tuple[idx: int,
    name: cstring, size: int]

Iterate over all files in the table.

Yields: Tuple of (index, filename, size)

Example:

for (idx, name, size) in samples.items():
  echo idx, ": ", name, " (", size, " bytes)"

Source   Edit