commit:e3ca95698f59547e7ada7612a7293fdcb2c1028b
author:Chip
committer:Chip
date:Sat Aug 6 01:59:17 2022 -0500
parents:02a84002fb86e66b05722a42062781162c716406
FLAC streaming playback works
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
line changes: +3/-0
index af0e6a4..4514ef7
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -11,5 +11,8 @@ idf_component_register(
 	     "battery.c"
 	     "play.c"
 	     "file.c"
+	     "sd.c"
+		 "browse.c"
+		 "flac.c"
 	INCLUDE_DIRS "."
 )

diff --git a/main/audio.c b/main/audio.c
line changes: +1/-1
index 0742c10..9c24d81
--- a/main/audio.c
+++ b/main/audio.c
@@ -11,10 +11,10 @@
 
 QueueHandle_t audio_queue;
 SemaphoreHandle_t audio_access_sem;
-audio_block_t audio_block;
 
 void audio_task(void *pvParameters) {
 	const char TAG[] = "audio_task";
+	audio_block_t audio_block;
 
 	while (1) {
 		BaseType_t success = xQueueReceive(audio_queue, &audio_block, portMAX_DELAY);

diff --git a/main/audio.h b/main/audio.h
line changes: +0/-1
index 9486a4d..89ca260
--- a/main/audio.h
+++ b/main/audio.h
@@ -11,7 +11,6 @@
 #define I2S_DMA_COUNT 2
 #define I2S_DMA_LEN   256
 
-#define AUDIO_BLOCK_SIZE 256 // 16-bit samples
 #define AUDIO_QUEUE_SIZE 2   // blocks
 
 typedef struct audio_block {

diff --git a/main/browse.c b/main/browse.c
line changes: +195/-0
index 0000000..7f6b63f
--- /dev/null
+++ b/main/browse.c
@@ -0,0 +1,195 @@
+#include <dirent.h>
+#include <string.h>
+
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "cwalk.h"
+#include "esp_vfs_fat.h"
+
+#include "gfx.h"
+#include "keyboard.h"
+#include "sd.h"
+#include "browse.h"
+
+typedef struct browse_file_entry {
+	char* name;
+	unsigned char type;
+} browse_file_entry_t;
+
+typedef struct browse_file_list {
+	browse_file_entry_t** files;
+	size_t count;
+	size_t capacity;
+} browse_file_list_t;
+
+void bfl_init(browse_file_list_t* bfl) {
+	bfl->count = 0;
+	bfl->capacity = 10;
+	bfl->files = (browse_file_entry_t**) malloc(bfl->capacity * sizeof(browse_file_entry_t*));
+}
+
+void bfl_free_entry(browse_file_list_t* bfl, size_t index) {
+	browse_file_entry_t* be = bfl->files[index];
+	free(be->name);
+	free(be);
+}
+
+void bfl_free(browse_file_list_t* bfl) {
+	for (int i = 0; i < bfl->count; i++) {
+		bfl_free_entry(bfl, i);
+	}
+	free(bfl->files);
+}
+
+void bfl_resize(browse_file_list_t* bfl, size_t new_capacity) {
+	if (new_capacity < bfl->count) {
+		for (int i = new_capacity; i < bfl->count; i++) {
+			bfl_free_entry(bfl, i);
+		}
+		bfl->count = new_capacity;
+	}
+
+	bfl->capacity = new_capacity;
+	bfl->files = (browse_file_entry_t**) realloc(bfl->files, bfl->capacity * sizeof(browse_file_entry_t*));
+}
+
+void bfl_clear(browse_file_list_t* bfl) {
+	bfl_free(bfl);
+	bfl_init(bfl);
+}
+
+void bfl_add_entry(browse_file_list_t* bfl, const char* name, unsigned char type) {
+	browse_file_entry_t* be = (browse_file_entry_t*) malloc(sizeof(browse_file_entry_t));
+	size_t l = strlen(name);
+	be->name = (char*) malloc(l + 1);
+	strncpy(be->name, name, l);
+	be->name[l] = 0;
+	be->type = type;
+	if (bfl->count == bfl->capacity) {
+		bfl_resize(bfl, bfl->capacity * 2);
+	}
+	bfl->files[bfl->count] = be;
+	bfl->count++;
+}
+
+void bfl_fetch(browse_file_list_t* bfl, const char* path) {
+	struct dirent *e;
+	DIR* d = opendir(path);
+
+	bfl_clear(bfl);
+	while ((e = readdir(d)) != NULL) {
+		bfl_add_entry(bfl, e->d_name, e->d_type);
+	}
+	closedir(d);
+}
+
+void browse() {
+	const char TAG[] = "browse_files";
+	browse_file_list_t bfl;
+	char* path = (char*) malloc(FILENAME_MAX);
+	strcpy(path, "/sd");
+
+	FATFS* fs = sd_get();
+	if (fs == NULL) {
+		printf("No SD card\n");
+		return;
+	}
+
+	bfl_init(&bfl);
+	bfl_fetch(&bfl, path);
+
+	int cursor = 0;
+	key_event_t k;
+
+	while (1) {
+		gfx_data_t *gd = gfx_acquire_data(SCENE_BROWSE);
+		gd->browse.state = BROWSE_NONE;
+		if (cursor == 0) {
+			gd->browse.state = BROWSE_AT_TOP;
+		}
+		if (cursor >= bfl.count - 4) {
+			gd->browse.state = BROWSE_AT_BOTTOM;
+		}
+		for (int i = 0; i < 4; i++) {
+			if (cursor + i < bfl.count) {
+				gd->browse.names[i] = bfl.files[cursor + i]->name;
+				gd->browse.types[i] = bfl.files[cursor + i]->type;
+			} else {
+				gd->browse.names[i] = NULL;
+			}
+		}
+
+		gfx_release_data();
+
+		int selected = -1;
+		k = key_get_event(portMAX_DELAY);
+		if (k.type == KEY_PRESSED) {
+			switch (k.code) {
+			case KEY_BAND:
+				if (strncmp(path, "/sd", FILENAME_MAX) == 0) {
+					goto browse_end;
+				}
+				size_t l;
+				cwk_path_get_dirname(path, &l);
+				path[l - 1] = 0;
+				ESP_LOGI(TAG, "new path = %s", path);
+				bfl_clear(&bfl);
+				bfl_fetch(&bfl, path);
+				cursor = 0;
+				break;
+			case KEY_TUNE_PLUS:
+				if (cursor >= 4) {
+					cursor -= 4;
+				}
+				break;
+			case KEY_TUNE_MINUS:
+				if (cursor < bfl.count - 4) {
+					cursor += 4;
+				}
+				break;
+			case KEY_1:
+				selected = 0;
+				break;
+			case KEY_2:
+				selected = 1;
+				break;
+			case KEY_3:
+				selected = 2;
+				break;
+			case KEY_4:
+				selected = 3;
+				break;
+			default:
+				break;
+			}
+		}
+		if (selected != -1 && cursor + selected < bfl.count) {
+			char* name = bfl.files[cursor + selected]->name;
+			unsigned char t = bfl.files[cursor + selected]->type;
+
+			if (strlen(path) + strlen(name) + 2 > FILENAME_MAX) {
+				ESP_LOGE(TAG, "File path would be too long; cannot navigate to %s", name);
+				continue;
+			}
+			char* fullpath = (char*) malloc(FILENAME_MAX);
+			cwk_path_join(path, name, fullpath, FILENAME_MAX);
+
+			if (t == DT_REG) {
+				play_file(fullpath);
+				free(fullpath);
+				break;
+			} else if (t == DT_DIR) {
+				free(path);
+				path = fullpath;
+				ESP_LOGI(TAG, "new path = %s", path);
+				bfl_clear(&bfl);
+				bfl_fetch(&bfl, path);
+				cursor = 0;
+			}
+		}
+	}
+
+browse_end:
+	bfl_free(&bfl);
+	free(path);
+} 
\ No newline at end of file

diff --git a/main/browse.h b/main/browse.h
line changes: +8/-0
index 0000000..e934814
--- /dev/null
+++ b/main/browse.h
@@ -0,0 +1,8 @@
+#pragma once
+
+typedef enum file_type {
+    FILE_TYPE_FLAC,
+    FILE_TYPE_MP3,
+} file_type_t;
+
+void browse(); 
\ No newline at end of file

diff --git a/main/file.c b/main/file.c
line changes: +13/-4
index a1d70ce..1b516b9
--- a/main/file.c
+++ b/main/file.c
@@ -42,8 +42,9 @@ SemaphoreHandle_t seek_sync_sem = NULL;
 void file_reader_task(void* pvParameters) {
 	BaseType_t err;
 	bool reading = false;
-	size_t bytes_read = 0, bytes_sent = 0;
+	size_t bytes_read = 0, bytes_sent = 0, total_bytes = 0;
 	FILE* f = NULL;
+	int64_t start = 0;
 
 	uint8_t *buf = (uint8_t *) malloc(BUFSIZE);
 	if (buf == NULL) {
@@ -72,6 +73,7 @@ void file_reader_task(void* pvParameters) {
 				file_reader_streambuffer = a.subscribe_sb;
 				break;
 			case FILE_READER_START:
+				start = esp_timer_get_time();
 				reading = true;
 				break;
 			case FILE_READER_STOP:
@@ -102,7 +104,7 @@ void file_reader_task(void* pvParameters) {
 		}
 
 		if (reading) {
-			ESP_LOGI(TAG, "%d sent %d read", bytes_sent, bytes_read);
+			//ESP_LOGI(TAG, "%d sent %d read", bytes_sent, bytes_read);
 			if (bytes_sent == bytes_read) {
 				if (f == NULL) {
 					ESP_LOGE(TAG, "FILE pointer null while reading");
@@ -110,11 +112,18 @@ void file_reader_task(void* pvParameters) {
 				} else if (feof(f)) {
 					ESP_LOGI(TAG, "end of file");
 					fclose(f);
-					f = NULL;
+					break;
 				} else {
 					bytes_sent = 0;
 					bytes_read = fread(buf, 1, BUFSIZE, f);
-					ESP_LOGI(TAG, "read %d bytes", bytes_read);
+					total_bytes += bytes_read;
+					if (total_bytes >= 1048576) {
+						int64_t now = esp_timer_get_time();
+						ESP_LOGI(TAG, "%0.1f KB/sec", 1000.0 * (double)total_bytes / (double)(now - start));
+						start = now;
+						total_bytes = 0;
+					}
+					//ESP_LOGI(TAG, "read %d bytes", bytes_read);
 				}
 			}
 			bytes_sent += xStreamBufferSend(

diff --git a/main/flac.c b/main/flac.c
line changes: +112/-0
index 0000000..d15ff43
--- /dev/null
+++ b/main/flac.c
@@ -0,0 +1,112 @@
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "freertos/stream_buffer.h"
+#include "esp_log.h"
+#include "FLAC/stream_decoder.h"
+
+#include "audio.h"
+#include "file.h"
+#include "play.h"
+#include "flac.h"
+
+FLAC__StreamDecoderReadStatus flac_read_callback(const FLAC__StreamDecoder *sd, FLAC__byte buffer[], size_t *bytes, void *client_data) {
+    StreamBufferHandle_t sb = (StreamBufferHandle_t) client_data;
+	size_t n = xStreamBufferReceive(sb, buffer, *bytes, portMAX_DELAY);
+	*bytes = n;
+
+	// Check the abort semaphore
+	uint32_t abort = ulTaskNotifyTake(pdFALSE, 0);
+
+	if (abort) {
+		return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM;
+	}
+	return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE;
+}
+
+FLAC__StreamDecoderWriteStatus flac_write_callback(const FLAC__StreamDecoder* sd, const FLAC__Frame* frame, const FLAC__int32* const buffer[], void* client_data) {
+    // FLAC header block size is the number of 16-bit stereo samples (4 bytes per sample)
+	uint16_t* outbuf = malloc(256 * sizeof(uint16_t));
+    if (outbuf == NULL) {
+        ESP_LOGE("flac_write_callback", "Failed to allocate output buffer");
+        heap_caps_print_heap_info(MALLOC_CAP_8BIT);
+        return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
+    }
+
+    int c = 0;
+	for (int i = 0; i < frame->header.blocksize; i++) {
+		outbuf[c  ] = (uint16_t) buffer[0][i];
+		outbuf[c+1] = (uint16_t) buffer[1][i];
+        c += 2;
+        if (c == 256) {
+            audio_send_ptr(outbuf, 256);
+            outbuf = malloc(256 * sizeof(uint16_t));
+            if (outbuf == NULL) {
+                ESP_LOGE("flac_write_callback", "Failed to reallocate output buffer");
+                return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
+            }
+            c = 0;
+        }
+	}
+
+	audio_send_ptr(outbuf, c);
+
+	return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
+}
+
+void flac_error_callback(const FLAC__StreamDecoder *sd, FLAC__StreamDecoderErrorStatus status, void *client_data) {
+	ESP_LOGE("flac_error_callback", "%d", status);
+}
+
+void play_flac(void *pvParameters) {
+	const char TAG[] = "play_flac";
+    play_parameters_t* pp = (play_parameters_t*) pvParameters;
+    StreamBufferHandle_t sb = xStreamBufferCreate(4096, 2048);
+
+    file_reader_open(pp->filename);
+    file_reader_subscribe(sb);
+    file_reader_start();
+
+    if (!audio_open()) {
+        ESP_LOGE(TAG, "audio not available");
+    }
+
+    free(pp);
+
+	FLAC__StreamDecoder *sd = FLAC__stream_decoder_new();
+	if (sd == NULL) {
+		ESP_LOGE(TAG, "Could not allocate FLAC decoder");
+		return;
+	}
+
+	FLAC__StreamDecoderInitStatus s = FLAC__stream_decoder_init_stream(
+		sd,
+		flac_read_callback,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		flac_write_callback,
+		NULL,
+		flac_error_callback,
+		sb
+	);
+	if (s != FLAC__STREAM_DECODER_INIT_STATUS_OK) {
+		ESP_LOGE(TAG, "Could not initialize FLAC decoder: %d", s);
+		return;
+	}
+
+	while (FLAC__stream_decoder_process_single(sd)) {
+		if (ulTaskNotifyTake(pdTRUE, 0)) {
+			break;
+		}
+	}
+
+	FLAC__stream_decoder_delete(sd);
+
+    audio_close();
+
+    file_reader_stop();
+    file_reader_close();
+
+	vTaskDelete(NULL);
+}

diff --git a/main/flac.h b/main/flac.h
line changes: +3/-0
index 0000000..a437ec3
--- /dev/null
+++ b/main/flac.h
@@ -0,0 +1,3 @@
+#pragma once
+
+void play_flac(void* pvParameters); 
\ No newline at end of file

diff --git a/main/fonts.c b/main/fonts.c
line changes: +32/-26
index f6a6d59..07c5b2d
--- a/main/fonts.c
+++ b/main/fonts.c
@@ -27,31 +27,37 @@ const uint8_t hyperspace_7n[130] U8G2_FONT_SECTION("hyperspace_7n") =
 /*
   Fontname: unknown
   Copyright: unknown
-  Glyphs: 103/103
+  Glyphs: 127/127
   BBX Build Mode: 0
 */
-const uint8_t maximum_overdrift[753] U8G2_FONT_SECTION("maximum_overdrift") = 
-  "g\0\3\3\3\3\3\4\4\5\6\0\0\6\6\6\1\1&\2,\2\324\1\12-\323%\311\62\311$"
-  "\1\2\12-\323\61\215\210R$\11\3\7+\223\23\71\24\4\10+\223\23\271\244\0\5\10+\223\23\241"
-  "\244\14\6\7+\223\223\213\1\7\10,\263\61\231\134\10 \5\0}\1!\6)S\61\11\42\7\23\231"
-  "\21\221\4#\12-\323\223RI\251\244\0$\13-\323\63I#E\42\23\0%\7-\323\21\313\7&"
-  "\12-\323\61\12\232\42\24\0'\5\21Y!(\7*s#I\12)\11*s\21\212D$\0*\7"
-  "\33\225\21\311\1+\10\33\225\23\231D\0,\5\21Q!-\5\13\227\61.\5\11S\21/\10-\323"
-  "\31\313\21\0\60\11-\323a\232DF\6\61\5\251\323Q\62\7-\323Q<\26\63\6-\323Qt\64"
-  "\10-\323\21\223\31\23\65\7-\323a,\32\66\7-\323a\264\31\67\7-\323Q\314\1\70\7-\323"
-  "a\273\31\71\7-\323a\63\32:\6\31U\21\11;\6!S\21\21<\7+\223\25IK=\6\33"
-  "\225\61\33>\10+\223\21KI\2\77\12-\323Q\14\315\1!\0@\10-\323a\223\20\13A\10-"
-  "\323a\273\311\2B\12-\323A\211Ub\25\0C\7-\323aL,D\11-\323A\211\251U\0E"
-  "\10-\323a\244\4\13F\11-\323a\244\4\203\0G\10-\323a\14\315\14H\11-\323\21\223\335d"
-  "\1I\10-\323Q\12&\25J\7-\323\231\321\0K\13-\323\21\23EF)\261\0L\7-\323\21"
-  "\314XM\12-\323!\231Ddj\1N\13-\323\21\33I\42\242Y\0O\7-\323aS\63P\10"
-  "-\323a;\6\1Q\11-\323a\223DD\7R\12-\323A\211Ub\262\0S\7-\323a,\32"
-  "T\10-\323Q\12f\2U\7-\323\21\323fV\12-\323\21S\213\244\205\0W\11-\323\21\323\22"
-  "\231\10X\12-\323\21\213\244\245\244\5Y\12-\323\21\213\244\5\223\0Z\7-\323Q\313V[\7*"
-  "s\61I\21\134\7-\323\21\315\1]\7*s!I\31^\6\23\231\223\1_\5\15\323Q`\6\22"
-  "y\21\12a\5\0\335\1b\5\0\335\1c\5\0\335\1d\5\0\335\1e\5\0\335\1f\5\0\335"
-  "\1g\5\0\335\1h\5\0\335\1i\5\0\335\1j\5\0\335\1k\5\0\335\1l\5\0\335\1m"
-  "\5\0\335\1n\5\0\335\1o\5\0\335\1p\5\0\335\1q\5\0\335\1r\5\0\335\1s\5\0"
-  "\335\1t\5\0\335\1u\5\0\335\1v\5\0\335\1w\5\0\335\1x\5\0\335\1y\5\0\335\1"
-  "z\5\0\335\1{\10+\223#I\13\11|\5)SQ}\12+\223!\212E\42\22\0~\10\24\271"
-  "\23\221D\0\177\5\0\335\1\0\0\0\4\377\377\0";
+const uint8_t maximum_overdrift[945] U8G2_FONT_SECTION("maximum_overdrift") = 
+  "\177\0\3\3\3\3\1\4\4\5\6\0\0\6\0\6\1\1\251\2\253\3\224\1\12\355tI\262L\62I"
+  "\0\2\12\355tL#\242\24I\2\3\7\353\344D\16\5\4\10\353\344D.)\0\5\10\353\344D("
+  ")\3\6\6\353\344\344b\7\10\354lL&\27\2\10\10\354lL\16\7\1\11\11\354lL\42\24\211"
+  "\1\12\11\354lL\42#Q\1\13\10\354lLJ\7\1\14\11\354lL\42#\211\1\15\11\354lL"
+  "\42\243I\1\16\10\354lLJ\242\2\17\10\354lL\42K\5\20\10\345t\310\16\207\0\21\4@w"
+  "\22\4@w\23\4@w\24\4@w\25\4@w\26\4@w\27\4@w\30\4@w\31\4@w"
+  "\32\4@w\33\4@w\34\4@w\35\4@w\36\4@w\37\4@w \4@_!\6\351T"
+  "L\2\42\7SfD$\1#\12\355\364\244TR*)\0$\13\355\364L\322H\221\310\4\0%\7"
+  "\355t\304\362\1&\12\355t\214\202\246\10\5\0'\5QV\10(\7\352\334H\222\2)\11\352\134\204"
+  "\42\21\11\0*\6[eDr+\10[\345D&\21\0,\5QT\10-\5\313e\14.\5\311T"
+  "\4/\7\355t\306r\4\60\11\355t\230&\221\221\1\61\5\351T\24\62\7\355t\24\217\5\63\6\355"
+  "t\24\35\64\10\355t\304d\306\4\65\7\355t\30\213\6\66\7\355t\30m\6\67\6\355t\24s\70"
+  "\7\355t\330n\6\71\7\355t\330\214\6:\6YUD\2;\6\341TD\4<\7\353dE\322\22"
+  "=\6[e\314\6>\10\353d\304R\222\0\77\12\355t\24Cs@\10\0@\10\355t\330$\304\2"
+  "A\10\355t\330n\262\0B\12\355tPb\225X\5\0C\7\355t\30\23\13D\11\355tPbj"
+  "\25\0E\10\355t\30)\301\2F\11\355t\30)\301 \0G\10\355t\30C\63\3H\11\355t\304"
+  "d\67Y\0I\10\355t\224\202I\5J\6\355tf\64K\13\355t\304D\221QJ,\0L\7\355"
+  "t\4\63\26M\12\355tH&\21\231Z\0N\12\355t\304F\222\210h\26O\7\355t\330\324\14P"
+  "\10\355t\330\216A\0Q\11\355t\330$\21\321\1R\12\355tPb\225\230,\0S\7\355t\30\213"
+  "\6T\10\355t\224\202\231\0U\7\355t\304\264\31V\12\355t\304\324\42i!\0W\11\355t\304\264"
+  "D&\2X\12\355t\304\42i)i\1Y\12\355t\304\42i\301$\0Z\7\355t\324\262\25[\7"
+  "\352\134LR\4\134\6\355tDs]\7\352\134HR\6^\5S\346d_\5\315t\24`\6R^"
+  "\204\2a\6\334\354\220\12b\10\354l\304b\245\2c\6\334l\324\10d\7\354\354\305L\5e\6\334"
+  "l\34\12f\12\353\354H\42\223P\4\0g\7dl\34*\3h\11\354l\304b%Q\0i\6\351"
+  "TD\6j\10r\334\304\42\221\1k\10\353d\204R&\11l\5\351T\24m\10\335tX\42\222\4"
+  "n\7\334l\224D\1o\6\334l\224\12p\7dl\224l\0q\7dl\224j\1r\7\333\344\214"
+  "B\0s\7\334\354\34&\0t\10\343\344D&!\1u\7\334l\204D\5v\11\334l\204D\21\11"
+  "\0w\11\335tD\42\222\210\1x\11\334l\204\42\222P\0y\7dl\204j\5z\7\334lP$"
+  "\4{\10\353\344H\322B\2|\5\351T\24}\12\353d\210b\221\210\4\0~\10T\356D$\21\0"
+  "\177\11\355tMR&\331\6\0\0\0\4\377\377\0";

diff --git a/main/gfx.c b/main/gfx.c
line changes: +34/-0
index 7230b89..af2eaa0
--- a/main/gfx.c
+++ b/main/gfx.c
@@ -1,3 +1,4 @@
+#include <dirent.h>
 #include <string.h>
 #include <time.h>
 
@@ -177,6 +178,36 @@ void draw_bt(bt_info_t* info) {
 	}
 }
 
+void draw_browse(browse_scene_t* bs) {
+	u8g2_ClearBuffer(&u8g2);
+	u8g2_SetFont(&u8g2, maximum_overdrift);
+
+	for (int i = 0; i < 4; i++) {
+		if (bs->names[i] == 0) {
+			break;
+		}
+		u8g2_DrawXBM(&u8g2, 2, i * 8, btn_1_width, btn_1_height, btn_list[i]);
+		u8g2_DrawStr(&u8g2, 9, i * 8 + 7, bs->types[i] == DT_DIR ? "\x10" : "\x8");
+		u8g2_DrawStr(&u8g2, 16, i * 8 + 7, bs->names[i]);
+	}
+
+	u8g2_SetDrawColor(&u8g2, 0);
+	u8g2_DrawBox(&u8g2, 122, 0, 6, 32);
+	if (bs->state == BROWSE_NONE || bs->state == BROWSE_AT_BOTTOM) {
+		u8g2_SetDrawColor(&u8g2, 1);
+		u8g2_DrawBox(&u8g2, 123, 0, 5, 15);
+		u8g2_SetDrawColor(&u8g2, 0);
+		u8g2_DrawStr(&u8g2, 124, 11, "+");
+	}
+	if (bs->state == BROWSE_NONE || bs->state == BROWSE_AT_TOP) {
+		u8g2_SetDrawColor(&u8g2, 1);
+		u8g2_DrawBox(&u8g2, 123, 17, 5, 15);
+		u8g2_SetDrawColor(&u8g2, 0);
+		u8g2_DrawStr(&u8g2, 124, 28, "-");
+	}
+	u8g2_SetDrawColor(&u8g2, 1);
+}
+
 void gfx_thread(void *pvParameters) {
 	while (1) {
 		// wait for data update
@@ -205,6 +236,9 @@ void gfx_thread(void *pvParameters) {
 		case SCENE_BT:
 			draw_bt(&gfx_data.bt);
 			break;
+		case SCENE_BROWSE:
+			draw_browse(&gfx_data.browse);
+			break;
 		case SCENE_NO_CHANGE:
 			ESP_LOGE(TAG, "SCENE_NO_CHANGE somehow set in gfx_data");
 			esp_system_abort("invalid state");

diff --git a/main/gfx.h b/main/gfx.h
line changes: +14/-5
index 2d2362b..b74f337
--- a/main/gfx.h
+++ b/main/gfx.h
@@ -1,5 +1,4 @@
-#ifndef __GFX_H
-#define __GFX_H
+#pragma once
 
 #include <stdbool.h>
 #include "u8g2_esp32_hal.h"
@@ -16,6 +15,7 @@ typedef enum {
 	SCENE_MENU,
 	SCENE_PLAY,
 	SCENE_BT,
+	SCENE_BROWSE,
 	SCENE_NO_CHANGE,
 } scene_t;
 
@@ -42,9 +42,19 @@ typedef struct {
 } menu_scene_t;
 
 typedef struct {
-	play_state play_state;
+	play_state_t play_state;
 } play_scene_t;
 
+#define BROWSE_NONE      0
+#define BROWSE_AT_TOP    1
+#define BROWSE_AT_BOTTOM 2
+
+typedef struct {
+	char* names[4];
+	unsigned char types[4];
+	uint8_t state;
+} browse_scene_t;
+
 typedef struct {
 	bt_mode mode;
 	uint32_t confirm;
@@ -59,6 +69,7 @@ typedef struct {
 		clock_scene_t clock;
 		menu_scene_t menu;
 		play_scene_t play;
+		browse_scene_t browse;
 	};
 } gfx_data_t;
 
@@ -70,5 +81,3 @@ void gfx_release_data();
 void gfx_set_power(int on);
 void gfx_message(const char *str);
 void gfx_set_enabled(bool v);
-
-#endif //__GFX_H

diff --git a/main/keyboard.h b/main/keyboard.h
line changes: +1/-4
index d341330..b3da2f7
--- a/main/keyboard.h
+++ b/main/keyboard.h
@@ -1,5 +1,4 @@
-#ifndef __KEYBOARD_H
-#define __KEYBOARD_H
+#pragma once
 
 #include "freertos/FreeRTOS.h"
 
@@ -51,5 +50,3 @@ int key_init();
 key_event_t key_get_event(const TickType_t ticksToWait);
 keycode_t key_get_pressed();
 void key_test();
-
-#endif //__KEYBOARD_H

diff --git a/main/main.c b/main/main.c
line changes: +31/-256
index 6ef7e8a..3cdfc41
--- a/main/main.c
+++ b/main/main.c
@@ -10,25 +10,21 @@
 #include "freertos/FreeRTOS.h"
 #include "freertos/task.h"
 #include "freertos/stream_buffer.h"
+#include "cwalk.h"
 #include "driver/i2s.h"
 #include "driver/gpio.h"
 #include "soc/rtc_cntl_reg.h"
-#include "diskio_impl.h"
-#include "diskio_sdmmc.h"
 #include "esp_system.h"
 #include "esp_log.h"
-#include "esp_vfs_fat.h"
-#include "sdmmc_cmd.h"
 #include "nvs_flash.h"
 #include "esp_sleep.h"
 #include "mp3dec.h"
-#include "FLAC/stream_decoder.h"
-#include "xmp.h"
 #include "u8g2.h"
 
 #include "sin_table.h"
 #include "audio.h"
 #include "battery.h"
+#include "browse.h"
 #include "bt.h"
 #include "file.h"
 #include "fonts.h"
@@ -37,108 +33,7 @@
 #include "keyboard.h"
 #include "gfx.h"
 #include "pm.h"
-
-#define BUFSIZE 16384
-
-sdmmc_host_t host = SDSPI_HOST_DEFAULT();
-const char mount_point[] = "/sd";
-
-int sdspi_init() {
-	const char TAG[] = "sdspi_init";
-	esp_err_t ret;
-
-	ESP_LOGI(TAG, "Initializing SPI bus");
-
-	spi_bus_config_t bus_cfg = {
-		.mosi_io_num = 23,
-		.miso_io_num = 19,
-		.sclk_io_num = 18,
-		.quadwp_io_num = -1,
-		.quadhd_io_num = -1,
-		.max_transfer_sz = 4000,
-	};
-	ret = spi_bus_initialize(host.slot, &bus_cfg, SPI_DMA_CH_AUTO);
-	if (ret != ESP_OK) {
-		ESP_LOGE(TAG, "Failed to initialize bus: %s", esp_err_to_name(ret));
-		return 0;
-	}
-
-	sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
-	slot_config.gpio_cs = 5;
-	slot_config.host_id = host.slot;
-
-	ESP_LOGI(TAG, "Initializing SDSPI device");
-	sdspi_dev_handle_t sdspi;
-	ret = sdspi_host_init_device(&slot_config, &sdspi);
-	if (ret != ESP_OK) {
-		ESP_LOGE(TAG, "Failed to init SDSPI device: %d", ret);
-		return 0;
-	}
-
-	ret = sdspi_host_set_card_clk(sdspi, 1600);
-	if (ret != ESP_OK) {
-		ESP_LOGE(TAG, "Failed to set SPI card speed: %d", ret);
-		return 0;
-	}
-
-	return 1;
-}
-
-FATFS * sd_card_mount() {
-	esp_err_t ret;
-	const char TAG[] = "init_sd";
-
-	ESP_LOGI(TAG, "Initializing SD card");
-
-	sdmmc_card_t *card = (sdmmc_card_t *) malloc(sizeof(sdmmc_card_t));
-	if (card == NULL) {
-		ESP_LOGE(TAG, "Could not allocate card struct");
-	}
-
-	host.max_freq_khz = 1600;
-	ret = sdmmc_card_init(&host, card);
-	if (ret != ESP_OK) {
-		ESP_LOGE(TAG, "Failed to init SD card: %s", esp_err_to_name(ret));
-		return NULL;
-	}
-
-	sdmmc_card_print_info(stdout, card);
-
-	ESP_LOGI(TAG, "Mounting filesystem");
-	BYTE pdrv = 0;
-	ff_diskio_register_sdmmc(pdrv, card);
-	FATFS *fs = NULL;
-	ret = esp_vfs_fat_register(mount_point, "0:", 5, &fs);
-	if (ret != ESP_OK) {
-		ESP_LOGE(TAG, "Failed to register VFS: %d", ret);
-		return NULL;
-	}
-
-	FRESULT fret = f_mount(fs, "0:", 1);
-	if (fret != FR_OK) {
-		ESP_LOGE(TAG, "Failed to mount FATFS: %d", fret);
-	}
-
-	ESP_LOGI(TAG, "Filesystem mounted on %s", mount_point);
-
-	return fs;
-}
-
-void sd_card_unmount() {
-	const char TAG[] = "deinit_sd";
-	esp_err_t ret;
-
-	f_unmount("0:");
-	ret = esp_vfs_fat_unregister_path(mount_point);
-	if (ret != ESP_OK) {
-		ESP_LOGE(TAG, "can't unmount: %s not mounted", mount_point);
-		return;
-	}
-	ESP_LOGI(TAG, "%s unmounted", mount_point);
-
-	ff_diskio_register(0, NULL);
-	ESP_LOGI(TAG, "disk driver freed");
-}
+#include "sd.h"
 
 size_t fill_from_streambuffer(StreamBufferHandle_t sb, uint8_t *buf, int size) {
 	size_t n = 0;
@@ -290,94 +185,6 @@ void play_mp3() {
 	}
 }
 
-FLAC__StreamDecoderReadStatus flac_read_callback(const FLAC__StreamDecoder *sd, FLAC__byte buffer[], size_t *bytes, void *client_data) {
-	read_task_parameter_t *params = (read_task_parameter_t *) client_data;
-
-	size_t n = xStreamBufferReceive(params->sb, buffer, *bytes, portMAX_DELAY);
-	*bytes = n;
-
-	// Check the abort semaphore
-	uint32_t abort = ulTaskNotifyTake(pdFALSE, 0);
-
-	if (params->eof || abort) {
-		return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM;
-	}
-	return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE;
-}
-
-FLAC__StreamDecoderWriteStatus flac_write_callback(const FLAC__StreamDecoder *sd, const FLAC__Frame *frame, const FLAC__int32 *const buffer[], void *client_data) {
-	uint16_t *outbuf = malloc(frame->header.blocksize * 4);
-
-	for (int i = 0; i < frame->header.blocksize; i++) {
-		outbuf[i*2  ] = (uint16_t) buffer[0][i];
-		outbuf[i*2+1] = (uint16_t) buffer[1][i];
-	}
-
-	audio_send_ptr(outbuf, frame->header.blocksize * 2);
-
-	return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
-}
-
-void flac_error_callback(const FLAC__StreamDecoder *sd, FLAC__StreamDecoderErrorStatus status, void *client_data) {
-	ESP_LOGE("flac_error_callback", "%d", status);
-}
-
-void play_flac(void *pvParameters) {
-	const char TAG[] = "play_flac";
-	BaseType_t xerr;
-	TaskHandle_t rt;
-	StreamBufferHandle_t sb = xStreamBufferCreate(BUFSIZE, 256);
-	read_task_parameter_t params = {
-		.filename = "/sd/cooler.flac",
-		.sb = sb,
-		.eof = 0,
-	};
-	xerr = xTaskCreate(read_task, "read_task", 4096, &params, 2, &rt);
-	if (xerr != pdPASS) {
-		ESP_LOGE(TAG, "Could not launch read task: %d", xerr);
-	}
-
-	FLAC__StreamDecoder *sd = FLAC__stream_decoder_new();
-	if (sd == NULL) {
-		ESP_LOGE(TAG, "Could not allocate FLAC decoder");
-		return;
-	}
-
-	FLAC__StreamDecoderInitStatus s = FLAC__stream_decoder_init_stream(
-		sd,
-		flac_read_callback,
-		NULL,
-		NULL,
-		NULL,
-		NULL,
-		flac_write_callback,
-		NULL,
-		flac_error_callback,
-		&params
-	);
-	if (s != FLAC__STREAM_DECODER_INIT_STATUS_OK) {
-		ESP_LOGE(TAG, "Could not initialize FLAC decoder: %d", s);
-		return;
-	}
-
-	while (FLAC__stream_decoder_process_single(sd)) {
-		if (ulTaskNotifyTake(pdTRUE, 0)) {
-			break;
-		}
-	}
-
-	ESP_LOGI(TAG, "Shutting down I/O thread");
-
-	vTaskDelete(rt);
-
-	FLAC__stream_decoder_delete(sd);
-
-	SemaphoreHandle_t waiter = *((SemaphoreHandle_t*) pvParameters);
-	xSemaphoreGive(waiter);
-
-	vTaskDelete(NULL);
-}
-
 void play_mod() {
 	const char TAG[] = "play_mod";
 	xmp_context c;
@@ -560,58 +367,6 @@ void test_audio() {
 	gfx_set_enabled(true);
 }
 
-void test_sd() {
-	gfx_set_enabled(false);
-
-	const char TAG[] = "test_sd";
-	char buf[40];
-	key_event_t ke;
-	int cs_state = 1;
-	FATFS *fs = NULL;
-
-	while (ke = key_get_event(0), !(ke.type == KEY_PRESSED && ke.code == KEY_BAND)) {
-		int new_cs_state = gpio_get_level(GPIO_NUM_13);
-		if (!new_cs_state && cs_state) {
-			// card inserted
-			fs = sd_card_mount();
-			if (fs == NULL) {
-				ESP_LOGE(TAG, "Could not initialize SD");
-				cs_state = new_cs_state;
-				continue;
-			}
-
-			DIR *d = opendir("/sd");
-			struct dirent *e;
-			while ((e = readdir(d)) != NULL) {
-				printf("%02x %s\n", e->d_type, e->d_name);
-			}
-			closedir(d);
-		} else if (new_cs_state && !cs_state) {
-			sd_card_unmount();
-			fs = NULL;
-			// card removed
-		}
-		cs_state = new_cs_state;
-
-		u8g2_ClearBuffer(&u8g2);
-
-		sprintf(buf, "CS: %c", cs_state ? 'H' : 'L');
-		u8g2_DrawStr(&u8g2, 0, 6, buf);
-		sprintf(buf, "FS: %p", fs);
-		for (int i = 0; i < strlen(buf); i++) {
-			buf[i] = toupper(buf[i]);
-		}
-		u8g2_DrawStr(&u8g2, 0, 12, buf);
-
-		u8g2_SendBuffer(&u8g2);
-
-		vTaskDelay(100 / portTICK_PERIOD_MS);
-	}
-
-	gfx_set_enabled(true);
-}
-
-
 void do_menu(const char *title, int n_items, menu_def_t *items) {
 	assert(n_items > 0 && n_items <= 5);
 	gfx_data_t *gd = gfx_acquire_data(SCENE_MENU);
@@ -708,13 +463,16 @@ void erase_nvs() {
 	esp_restart();
 }
 
+void do_nothing() { }
+
 void main_menu() {
 	menu_def_t menu[] = {
 		{"PLAY", play_controller},
+		{"FILE", browse},
 		{"TEST", test_mode},
 		{"BT", bt_config_mode},
 	};
-	do_menu("MAIN", 3, menu);
+	do_menu("MAIN", 4, menu);
 }
 
 void clock_mode() {
@@ -844,23 +602,39 @@ void print_hexdump_line(uint32_t addr, uint8_t* buf) {
 	printf("\n");
 }
 
+/*
 void stream_test() {
 	const char TAG[] = "stream_test";
 	uint8_t* buf = malloc(256);
 	StreamBufferHandle_t h = xStreamBufferCreate(256, 1);
 	uint32_t addr = 0;
 
-	sd_card_mount();
+	sd_get();
 
 	ESP_LOGI(TAG, "-------- start --------");
 
-	file_reader_open("/sd/rock.gcode");
+	FILE* f = fopen("/sd/cooler.flac", "r");
+	int64_t t0 = esp_timer_get_time();
+	for (int i = 0; i < 8 * 1048576; i += 256) {
+		size_t len = fread(buf, 1, 256, f);
+		addr += len;
+	}
+	int64_t t1 = esp_timer_get_time();
+	ESP_LOGI(TAG, "Read %d bytes in %lld us: %0.3f KB/sec", addr, t1 - t0, 1000.0 * (double)addr / (double)(t1 - t0));
+	fclose(f);
+
+	file_reader_open("/sd/cooler.flac");
 	file_reader_subscribe(h);
 	file_reader_start();
-	for (int i = 0; i < 256 / 16; i++) {
-		addr += xStreamBufferReceive(h, buf, 16, portMAX_DELAY);
-		print_hexdump_line(addr, buf);
+
+	int64_t t0 = esp_timer_get_time();
+	for (int i = 0; i < 8 * 1048576; i += 256) {
+		size_t len = xStreamBufferReceive(h, buf, 256, portMAX_DELAY);
+		//print_hexdump_line(addr, buf);
+		addr += len;
 	}
+	int64_t t1 = esp_timer_get_time();
+	ESP_LOGI(TAG, "Read %d bytes in %lld us: %0.3f KB/sec", addr, t1 - t0, 1000.0 * (double)addr / (double)(t1 - t0));
 	file_reader_seek(0, SEEK_SET);
 	ESP_LOGI(TAG, "--------- seek --------");
 	for (int i = 0; i < 256 / 16; i++) {
@@ -871,6 +645,7 @@ void stream_test() {
 
 	ESP_LOGI(TAG, "--------- end ---------");
 }
+*/
 
 void app_main(void) {
 	nvs_init();
@@ -878,14 +653,14 @@ void app_main(void) {
 	io_init();
 	audio_init();
 	gfx_init();
-	sdspi_init();
+	sd_init();
 	file_init();
 	battery_init();
 	bt_init();
 
 	//xTaskCreate(print_task_list, "task list", 4096, NULL, 1, NULL);
 
-	stream_test();
+	//stream_test();
 
 	clock_mode();
 }

diff --git a/main/play.c b/main/play.c
line changes: +38/-51
index c2cd7fd..568c8e4
--- a/main/play.c
+++ b/main/play.c
@@ -1,29 +1,35 @@
+#include <string.h>
 #include <sys/time.h>
+
 #include "freertos/FreeRTOS.h"
 #include "esp_log.h"
+#include "cwalk.h"
 
+#include "browse.h"
 #include "bt.h"
+#include "flac.h"
 #include "gfx.h"
 #include "keyboard.h"
 #include "play.h"
 
-play_media_type media_type = PLAY_MEDIA_NONE;
+play_media_type_t media_type = PLAY_MEDIA_NONE;
+TaskHandle_t play_task;
 
-void play_set_media_type(play_media_type t) {
+void play_set_media_type(play_media_type_t t) {
 	media_type = t;
 }
 
-play_state get_play_state() {
+play_state_t get_play_state() {
 	switch (media_type) {
 	case PLAY_MEDIA_BLUETOOTH:
-		return (play_state) bt_get_playback_state()->state;
+		return (play_state_t) bt_get_playback_state()->state;
 	default:
 		ESP_LOGE("PLAY", "Unfinished media type: %d", media_type);
 		return PLAY_STATE_ERROR;
 	}
 }
 
-void play_send_command(play_command c) {
+void play_send_command(play_command_t c) {
 	switch (media_type) {
 		case PLAY_MEDIA_BLUETOOTH: {
 			esp_avrc_pt_cmd_t button;
@@ -66,7 +72,7 @@ void play_controller() {
 	key_event_t ke;
 
 	while (ke = key_get_event(0), !(ke.type == KEY_PRESSED && ke.code == KEY_BAND)) {
-		play_state ps = get_play_state();
+		play_state_t ps = get_play_state();
 		if (ke.type == KEY_PRESSED) {
 			switch (ke.code) {
 			case KEY_SNOOZE:
@@ -108,53 +114,34 @@ void play_controller() {
 	}
 }
 
-/*
-void play_mode() {
-	TaskHandle_t decoder_task;
-	key_event_t ke;
-	struct timeval tv;
-	float now, t;
-
-	FATFS *fs = sd_card_mount();
-	if (fs == NULL) {
-		u8g2_ClearBuffer(&u8g2);
-		u8g2_DrawStr(&u8g2, 51, 19, "NO SD");
-		u8g2_SendBuffer(&u8g2);
-		vTaskDelay(1000 / portTICK_PERIOD_MS);
-		return;
+void play_start_file_type(const char* filename, file_type_t file_type) {
+	play_parameters_t* pp = (play_parameters_t*) malloc(sizeof(play_parameters_t));
+	strncpy(pp->filename, filename, FILENAME_MAX);
+
+	switch (file_type) {
+	case FILE_TYPE_FLAC:
+		xTaskCreate(play_flac, "play_flac", 4096, pp, 1, &play_task);
+		break;
+	case FILE_TYPE_MP3:
+		printf("MP3 Unimplemented");
+		break;
 	}
+}
 
-	int ret = audio_open();
-	if (!ret) {
-		gfx_message("NO AUDIO");
+void play_file(const char* filename) {
+	file_type_t file_type;
+	const char* extension;
+	size_t extension_len;
+	cwk_path_get_extension(filename, &extension, &extension_len);
+	if (strncmp(extension, ".flac", 6) == 0) {
+		file_type = FILE_TYPE_FLAC;
+	} else if (strncmp(extension, ".mp3", 5) == 0) {
+		file_type = FILE_TYPE_MP3;
+	} else {
+		ESP_LOGE("play_file", "Could not play %s", filename);
 		return;
 	}
 
-	SemaphoreHandle_t waiter = xSemaphoreCreateBinary();
-	xTaskCreate(play_flac, "flac", 4096, &waiter, 5, &decoder_task);
-
-	gettimeofday(&tv, NULL);
-	now = (float)tv.tv_sec + (float)tv.tv_usec / 1000000.0;
-
-	while (ke = key_get_event(0), !(ke.type == KEY_PRESSED && ke.code == KEY_BAND)) {
-		if (ke.code == KEY_SNOOZE && ke.type != KEY_NO_EVENT) {
-			if (state == ESP_AVRC_PLAYBACK_PLAYING) {
-				bt_send_key(ESP_AVRC_PT_CMD_PAUSE, ke.type - 1);
-			} else {
-				bt_send_key(ESP_AVRC_PT_CMD_PLAY, ke.type - 1);
-			}
-		}
-
-		vTaskDelay(100 / portTICK_PERIOD_MS);
-	}
-
-	// Kill the decoder
-	xTaskNotifyGive(decoder_task);
-
-	xSemaphoreTake(waiter, portMAX_DELAY);
-
-	sd_card_unmount();
-
-	audio_close();
-}
-*/
+	printf("Playing %s\n", filename);
+	play_start_file_type(filename, file_type);
+} 
\ No newline at end of file

diff --git a/main/play.h b/main/play.h
line changes: +13/-8
index 98413a7..6bfa68a
--- a/main/play.h
+++ b/main/play.h
@@ -1,22 +1,22 @@
 #pragma once
 
-typedef enum {
+typedef enum play_media_type {
 	PLAY_MEDIA_NONE,
 	PLAY_MEDIA_BLUETOOTH,
 	PLAY_MEDIA_FLAC,
 	PLAY_MEDIA_MP3,
-} play_media_type;
+} play_media_type_t;
 
-typedef enum {
+typedef enum play_state {
         PLAY_STATE_STOPPED,
         PLAY_STATE_PLAYING,
         PLAY_STATE_PAUSED,
         PLAY_STATE_FFWD,
         PLAY_STATE_RRWD,
         PLAY_STATE_ERROR,
-} play_state;
+} play_state_t;
 
-typedef enum {
+typedef enum play_command {
 	PLAY_COMMAND_STOP,
 	PLAY_COMMAND_PLAY,
 	PLAY_COMMAND_PAUSE,
@@ -24,8 +24,13 @@ typedef enum {
 	PLAY_COMMAND_RREV,
 	PLAY_COMMAND_FTRK,
 	PLAY_COMMAND_RTRK,
-} play_command;
+} play_command_t;
 
-void play_set_media_type(play_media_type t);
-void play_send_command(play_command c);
+typedef struct play_parameters {
+	char filename[FILENAME_MAX];
+} play_parameters_t;
+
+void play_set_media_type(play_media_type_t t);
+void play_send_command(play_command_t c);
 void play_controller();
+void play_file(const char* filename);

diff --git a/main/sd.c b/main/sd.c
line changes: +185/-0
index 0000000..076d18a
--- /dev/null
+++ b/main/sd.c
@@ -0,0 +1,185 @@
+#include <ctype.h>
+#include <dirent.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "diskio_impl.h"
+#include "diskio_sdmmc.h"
+#include "esp_vfs_fat.h"
+#include "sdmmc_cmd.h"
+
+#include "gfx.h"
+#include "keyboard.h"
+#include "sd.h"
+
+#define TAG "sd"
+
+sdmmc_host_t host = SDSPI_HOST_DEFAULT();
+const char mount_point[] = "/sd";
+FATFS* fs;
+
+bool sd_card_detect() {
+	return !(bool)gpio_get_level(GPIO_NUM_13);
+}
+
+int sd_init() {
+	esp_err_t ret;
+
+	ESP_LOGI(TAG, "Initializing SPI bus");
+
+	spi_bus_config_t bus_cfg = {
+		.mosi_io_num = 23,
+		.miso_io_num = 19,
+		.sclk_io_num = 18,
+		.quadwp_io_num = -1,
+		.quadhd_io_num = -1,
+		.max_transfer_sz = 4000,
+	};
+	ret = spi_bus_initialize(host.slot, &bus_cfg, SPI_DMA_CH_AUTO);
+	if (ret != ESP_OK) {
+		ESP_LOGE(TAG, "Failed to initialize bus: %s", esp_err_to_name(ret));
+		return 0;
+	}
+
+	sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
+	slot_config.gpio_cs = 5;
+	slot_config.host_id = host.slot;
+
+	ESP_LOGI(TAG, "Initializing SDSPI device");
+	sdspi_dev_handle_t sdspi;
+	ret = sdspi_host_init_device(&slot_config, &sdspi);
+	if (ret != ESP_OK) {
+		ESP_LOGE(TAG, "Failed to init SDSPI device: %d", ret);
+		return 0;
+	}
+
+	ret = sdspi_host_set_card_clk(sdspi, 20000);
+	if (ret != ESP_OK) {
+		ESP_LOGE(TAG, "Failed to set SPI card speed: %d", ret);
+		return 0;
+	}
+
+	return 1;
+}
+
+FATFS * sd_card_mount() {
+	esp_err_t ret;
+
+	ESP_LOGI(TAG, "Initializing SD card");
+
+	sdmmc_card_t *card = (sdmmc_card_t *) malloc(sizeof(sdmmc_card_t));
+	if (card == NULL) {
+		ESP_LOGE(TAG, "Could not allocate card struct");
+	}
+
+	host.max_freq_khz = 1600;
+	ret = sdmmc_card_init(&host, card);
+	if (ret != ESP_OK) {
+		ESP_LOGE(TAG, "Failed to init SD card: %s", esp_err_to_name(ret));
+		return NULL;
+	}
+
+	sdmmc_card_print_info(stdout, card);
+
+	ESP_LOGI(TAG, "Mounting filesystem");
+	BYTE pdrv = 0;
+	ff_diskio_register_sdmmc(pdrv, card);
+	FATFS *fs = NULL;
+	ret = esp_vfs_fat_register(mount_point, "0:", 5, &fs);
+	if (ret != ESP_OK) {
+		ESP_LOGE(TAG, "Failed to register VFS: %d", ret);
+		return NULL;
+	}
+
+	FRESULT fret = f_mount(fs, "0:", 1);
+	if (fret != FR_OK) {
+		ESP_LOGE(TAG, "Failed to mount FATFS: %d", fret);
+	}
+
+	ESP_LOGI(TAG, "Filesystem mounted on %s", mount_point);
+
+	return fs;
+}
+
+void sd_card_unmount() {
+	esp_err_t ret;
+
+	f_unmount("0:");
+	ret = esp_vfs_fat_unregister_path(mount_point);
+	if (ret != ESP_OK) {
+		ESP_LOGE(TAG, "can't unmount: %s not mounted", mount_point);
+		return;
+	}
+	ESP_LOGI(TAG, "%s unmounted", mount_point);
+
+	ff_diskio_register(0, NULL);
+	ESP_LOGI(TAG, "disk driver freed");
+}
+
+FATFS* sd_get() {
+	if (!sd_card_detect()) {
+		sd_card_unmount();
+		fs = NULL;
+		return NULL;
+	}
+
+	if (fs == NULL) {
+		fs = sd_card_mount();
+	}
+	return fs;
+}
+
+bool sd_ready() {
+	return sd_card_detect() && fs != NULL;
+}
+
+void test_sd() {
+	gfx_set_enabled(false);
+
+	char buf[40];
+	key_event_t ke;
+	int cs_state = 1;
+	FATFS *fs = NULL;
+
+	while (ke = key_get_event(0), !(ke.type == KEY_PRESSED && ke.code == KEY_BAND)) {
+		int new_cs_state = gpio_get_level(GPIO_NUM_13);
+		if (!new_cs_state && cs_state) {
+			// card inserted
+			fs = sd_card_mount();
+			if (fs == NULL) {
+				ESP_LOGE(TAG, "Could not initialize SD");
+				cs_state = new_cs_state;
+				continue;
+			}
+
+			DIR *d = opendir("/sd");
+			struct dirent *e;
+			while ((e = readdir(d)) != NULL) {
+				printf("%02x %s\n", e->d_type, e->d_name);
+			}
+			closedir(d);
+		} else if (new_cs_state && !cs_state) {
+			sd_card_unmount();
+			fs = NULL;
+			// card removed
+		}
+		cs_state = new_cs_state;
+
+		u8g2_ClearBuffer(&u8g2);
+
+		sprintf(buf, "CS: %c", cs_state ? 'H' : 'L');
+		u8g2_DrawStr(&u8g2, 0, 6, buf);
+		sprintf(buf, "FS: %p", fs);
+		for (int i = 0; i < strlen(buf); i++) {
+			buf[i] = toupper(buf[i]);
+		}
+		u8g2_DrawStr(&u8g2, 0, 12, buf);
+
+		u8g2_SendBuffer(&u8g2);
+
+		vTaskDelay(100 / portTICK_PERIOD_MS);
+	}
+
+	gfx_set_enabled(true);
+}

diff --git a/main/sd.h b/main/sd.h
line changes: +8/-0
index 0000000..a727bd4
--- /dev/null
+++ b/main/sd.h
@@ -0,0 +1,8 @@
+#pragma once
+
+#include "esp_vfs_fat.h"
+
+int sd_init();
+FATFS* sd_get();
+bool sd_ready();
+void test_sd();