diff --git a/components/audio_mp3/audio_mp3.c b/components/audio_mp3/audio_mp3.c
index 33b962eafea43cf79dc43df2cff3270c45492374..73758ad054bc7ba2b3dc9d03f6e3fce327d4b87e 100644
--- a/components/audio_mp3/audio_mp3.c
+++ b/components/audio_mp3/audio_mp3.c
@@ -17,18 +17,40 @@
 #define MINIMP3_IMPLEMENTATION
 #include "minimp3.h"
 
+#define BUFFER_SIZE (32 * 1024)
+
 typedef struct {
     st3m_media control;
     mp3dec_t mp3d;
     char *path;
+    char *artist;
+    char *title;
+    int year;
     int started;
     int samplerate;
     int channels;
     uint8_t *data;
     size_t size;
+    size_t count;
     int pos;
+    int offset;
+
+    int file_size;
+    FILE *file;
 } mp3_state;
 
+static void mp3_fetch_data(mp3_state *mp3) {
+    if (mp3->pos + (16 * 1024) >= mp3->count) {
+        memmove(mp3->data, &mp3->data[mp3->pos], mp3->count - mp3->pos);
+        mp3->offset += mp3->pos;
+        mp3->count -= mp3->pos;
+        mp3->pos = 0;
+    }
+    int read =
+        fread(mp3->data + mp3->count, 1, mp3->size - mp3->count, mp3->file);
+    mp3->count += read;
+}
+
 static void mp3_draw(st3m_media *media, Ctx *ctx) {
     mp3_state *self = (void *)media;
 
@@ -36,163 +58,161 @@ static void mp3_draw(st3m_media *media, Ctx *ctx) {
     ctx_gray(ctx, 0);
     ctx_fill(ctx);
     ctx_rgb(ctx, 1.0, 1.0, 1.0);
-    ctx_font_size(ctx, 120);
-    ctx_rectangle (ctx,-120 + self->pos * 240.0 / self->size, 0, 10, 10);
+    ctx_rectangle(ctx, -120, 0, 240, 1);
+    ctx_rectangle(ctx, -120 + self->offset * 240.0 / self->file_size, -32, 2,
+                  64);
     ctx_fill(ctx);
+    ctx_font_size(ctx, 24);
+    ctx_text_align(ctx, CTX_TEXT_ALIGN_CENTER);
+    ctx_move_to(ctx, 0, -40);
+    ctx_text(ctx, self->artist);
+    ctx_move_to(ctx, 0, 64);
+    ctx_text(ctx, self->title);
 }
 
-static int over_render = 0;
-
 static void mp3_think(st3m_media *media, float ms_elapsed) {
     mp3_state *self = (void *)media;
+    mp3_fetch_data(self);
     if (!self->started) {
         self->started = 1;
         mp3_think(media, 100);
     }
-    int samples_needed = ((AUDIO_BUF_SIZE - st3m_media_samples_queued ()) / 2) - 2400;
-    //if (samples_needed > 7000) samples_needed = 7000;
+    int samples_needed =
+        ((AUDIO_BUF_SIZE - st3m_media_samples_queued()) / 2) - 2400;
 
     int samples;
     mp3dec_frame_info_t info;
-    if (samples_needed>0) do {
-        int16_t rendered[MINIMP3_MAX_SAMPLES_PER_FRAME];
-        samples = mp3dec_decode_frame(&self->mp3d, self->data + self->pos,
-                                      self->size - self->pos, rendered, &info);
-        self->samplerate = info.hz;
-        self->channels = info.channels;
-        self->pos += info.frame_bytes;
-        //printf (".. %i\n", samples);
-#if 1
-        if (self->samplerate == 44100) {
-            int phase = 0;
-            if (info.channels == 1)
-                for (int i = 0; i < samples; i++) {
-                again1:
-                    self->control.audio_buffer[self->control.audio_w++] =
-                        rendered[i];
-                    if (self->control.audio_w >= AUDIO_BUF_SIZE)
-                        self->control.audio_w = 0;
-                    phase += ((48000 / 44100.0) - 1.0) * 65536;
-                    if (phase > 65536) {
-                        phase -= 65536;
-                        phase -= ((48000 / 44100.0) - 1.0) * 65536;
-                        goto again1;
-                    }
-                }
-            else if (info.channels == 2) {
+    if (samples_needed > 0 && self->offset + 8192 < self->file_size) do {
+            int16_t rendered[MINIMP3_MAX_SAMPLES_PER_FRAME];
+            samples =
+                mp3dec_decode_frame(&self->mp3d, self->data + self->pos,
+                                    self->count - self->pos, rendered, &info);
+            self->samplerate = info.hz;
+            self->channels = info.channels;
+            self->pos += info.frame_bytes;
+
+            if (self->samplerate != 48000) {
                 int phase = 0;
-                for (int i = 0; i < samples ; i++) {
-                again2:
-                    self->control.audio_buffer[self->control.audio_w++] =
-                        rendered[i*2];
-                    if (self->control.audio_w >= AUDIO_BUF_SIZE)
-                        self->control.audio_w = 0;
-                    self->control.audio_buffer[self->control.audio_w++] =
-                        rendered[i*2+1];
-                    if (self->control.audio_w >= AUDIO_BUF_SIZE)
-                        self->control.audio_w = 0;
-
-                    phase += ((48000 / 44100.0) - 1.0) * 65536;
-                    if (phase > 65536) {
-                        phase -= 65536;
-                        phase -= ((48000 / 44100.0) - 1.0) * 65536;
-                        goto again2;
+                int fraction = ((48000.0 / self->samplerate) - 1.0) * 65536;
+                if (info.channels == 1)
+                    for (int i = 0; i < samples; i++) {
+                    again1:
+                        self->control.audio_buffer[self->control.audio_w++] =
+                            rendered[i];
+                        if (self->control.audio_w >= AUDIO_BUF_SIZE)
+                            self->control.audio_w = 0;
+                        phase += fraction;
+                        if (phase > 65536) {
+                            phase -= 65536;
+                            phase -= fraction;
+                            goto again1;
+                        }
+                    }
+                else if (info.channels == 2) {
+                    int phase = 0;
+                    for (int i = 0; i < samples; i++) {
+                    again2:
+                        self->control.audio_buffer[self->control.audio_w++] =
+                            rendered[i * 2];
+                        if (self->control.audio_w >= AUDIO_BUF_SIZE)
+                            self->control.audio_w = 0;
+                        self->control.audio_buffer[self->control.audio_w++] =
+                            rendered[i * 2 + 1];
+                        if (self->control.audio_w >= AUDIO_BUF_SIZE)
+                            self->control.audio_w = 0;
+
+                        phase += fraction;
+                        if (phase > 65536) {
+                            phase -= 65536;
+                            phase -= fraction;
+                            goto again2;
+                        }
                     }
                 }
-            }
-        } else
-#endif
-        {
-            if (info.channels == 1)
-                for (int i = 0; i < samples; i++) {
-                    self->control.audio_buffer[self->control.audio_w++] =
-                        rendered[i];
-                    if (self->control.audio_w >= AUDIO_BUF_SIZE)
-                        self->control.audio_w = 0;
-                    self->control.audio_buffer[self->control.audio_w++] =
-                        rendered[i];
-                    if (self->control.audio_w >= AUDIO_BUF_SIZE)
-                        self->control.audio_w = 0;
-                }
-            else if (info.channels == 2) {
-                for (int i = 0; i < samples; i++) {
-                    self->control.audio_buffer[self->control.audio_w++] =
-                        rendered[i*2];
-                    if (self->control.audio_w >= AUDIO_BUF_SIZE)
-                        self->control.audio_w = 0;
-                    self->control.audio_buffer[self->control.audio_w++] =
-                        rendered[i*2+1];
-                    if (self->control.audio_w >= AUDIO_BUF_SIZE)
-                        self->control.audio_w = 0;
+            } else {
+                if (info.channels == 1)
+                    for (int i = 0; i < samples; i++) {
+                        self->control.audio_buffer[self->control.audio_w++] =
+                            rendered[i];
+                        if (self->control.audio_w >= AUDIO_BUF_SIZE)
+                            self->control.audio_w = 0;
+                        self->control.audio_buffer[self->control.audio_w++] =
+                            rendered[i];
+                        if (self->control.audio_w >= AUDIO_BUF_SIZE)
+                            self->control.audio_w = 0;
+                    }
+                else if (info.channels == 2) {
+                    for (int i = 0; i < samples; i++) {
+                        self->control.audio_buffer[self->control.audio_w++] =
+                            rendered[i * 2];
+                        if (self->control.audio_w >= AUDIO_BUF_SIZE)
+                            self->control.audio_w = 0;
+                        self->control.audio_buffer[self->control.audio_w++] =
+                            rendered[i * 2 + 1];
+                        if (self->control.audio_w >= AUDIO_BUF_SIZE)
+                            self->control.audio_w = 0;
+                    }
                 }
             }
-        }
 
-        samples_needed -= (samples);
-    } while (samples_needed > 0);
+            samples_needed -= (samples);
+        } while (samples_needed > 0);
 }
 
 static void mp3_destroy(st3m_media *media) {
     mp3_state *self = (void *)media;
     if (self->data) free(self->data);
+    if (self->file) fclose(self->file);
+    if (self->path) free(self->path);
+    if (self->title) free(self->title);
+    if (self->artist) free(self->artist);
     free(self);
 }
 
-static int file_get_contents(const char *path, uint8_t **contents,
-                             size_t *length) {
-    FILE *file;
-    long size;
-    long remaining;
-    uint8_t *buffer;
-    file = fopen(path, "rb");
-    if (!file) {
-        return -1;
-    }
-    // fseek(file, 0, SEEK_END);
-    size = remaining = 1024 * 1024;  // ftell(file);
-
-    if (length) {
-        *length = size;
-    }
-    // rewind(file);
-    buffer = malloc(size + 2);
-    if (!buffer) {
-        fclose(file);
-        return -1;
-    }
-    int pos = 0;
-    do {
-        int to_read = 1024;
-        if (remaining < to_read) to_read = remaining;
-        int read = fread(buffer + pos, 1, to_read, file);
-        remaining -= read;
-        pos += read;
-    } while (remaining);
-    if (remaining) {
-        fclose(file);
-        free(buffer);
-        return -1;
-    }
-    fclose(file);
-    *contents = (unsigned char *)buffer;
-    buffer[size] = 0;
-    return 0;
-}
+typedef struct {
+    char tag[3];
+    char artist[30];
+    char title[30];
+    char year[4];
+    char pad[128];
+} id3tag_t;
 
 st3m_media *st3m_media_load_mp3(const char *path) {
     mp3_state *self = (mp3_state *)malloc(sizeof(mp3_state));
+    id3tag_t id3;
     memset(self, 0, sizeof(mp3_state));
     self->control.draw = mp3_draw;
     self->control.think = mp3_think;
     self->control.destroy = mp3_destroy;
     self->samplerate = 44100;
-    file_get_contents(path, &self->data, &self->size);
-    if (!self->data) {
+    self->file = fopen(path, "r");
+    fseek(self->file, 0, SEEK_END);
+    self->file_size = ftell(self->file);
+    fseek(self->file, self->file_size - 128, SEEK_SET);
+    fread(&id3, 128, 1, self->file);
+    if (id3.tag[0] == 'T' && id3.tag[1] == 'A' && id3.tag[2] == 'G') {
+        self->title = strndup(id3.title, 30);
+        while (self->title[strlen(self->title) - 1] == ' ')
+            self->title[strlen(self->title) - 1] = 0;
+        self->artist = strndup(id3.artist, 30);
+        while (self->artist[strlen(self->artist) - 1] == ' ')
+            self->artist[strlen(self->artist) - 1] = 0;
+        self->year = atoi(id3.year);
+    } else {
+        self->artist = "Anonymous";
+        self->title = strdup(strrchr(path, '/') + 1);
+    }
+    self->path = strdup(path);
+    rewind(self->file);
+
+    self->data = malloc(BUFFER_SIZE);
+    self->size = BUFFER_SIZE;
+    if (!self->file) {
         printf("!!!!bo\n");
         free(self);
         return NULL;
     }
     mp3dec_init(&self->mp3d);
-
+    self->control.duration = 1200.0;
     return (st3m_media *)self;
 }
diff --git a/components/audio_mp3/minimp3.h b/components/audio_mp3/minimp3.h
index 93ca4c6e8018b40f059a7f705726d3b2b9c15de8..721e098eccbba66dbe850a74b75af601f0e9bdfd 100644
--- a/components/audio_mp3/minimp3.h
+++ b/components/audio_mp3/minimp3.h
@@ -40,7 +40,7 @@ typedef struct {
 #define MAX_L3_FRAME_PAYLOAD_BYTES \
     MAX_FREE_FORMAT_FRAME_SIZE /* MUST be >= 320000/8/32000*1152 = 1440 */
 
-struct _mp3dec_scratch_t{
+struct _mp3dec_scratch_t {
     bs_t bs;
     uint8_t maindata[MAX_BITRESERVOIR_BYTES + MAX_L3_FRAME_PAYLOAD_BYTES];
     L3_gr_info_t gr_info[4];
@@ -53,7 +53,7 @@ typedef struct {
     int reserv, free_format_bytes;
     unsigned char header[4], reserv_buf[511];
     mp3dec_scratch_t scratch;
-    L12_scale_info sci; 
+    L12_scale_info sci;
 } mp3dec_t;
 
 #ifdef __cplusplus
@@ -85,7 +85,6 @@ int mp3dec_decode_frame(mp3dec_t *dec, const uint8_t *mp3, int mp3_bytes,
 #define MAX_FRAME_SYNC_MATCHES 10
 #endif /* MAX_FRAME_SYNC_MATCHES */
 
-
 #define SHORT_BLOCK_TYPE 2
 #define STOP_BLOCK_TYPE 3
 #define MODE_MONO 3
@@ -235,14 +234,10 @@ static __inline__ __attribute__((always_inline)) int32_t minimp3_clip_int16_arm(
 #define HAVE_ARMV6 0
 #endif
 
-
-
 typedef struct {
     uint8_t tab_offset, code_tab_width, band_count;
 } L12_subband_alloc_t;
 
-
-
 static void bs_init(bs_t *bs, const uint8_t *data, int bytes) {
     bs->buf = data;
     bs->pos = 0;
@@ -1817,7 +1812,8 @@ static void mp3d_synth(float *xl, mp3d_sample_t *dstl, int nch, float *lins) {
 
             V0(0)
             V2(1)
-            V1(2) V2(3) V1(4) V2(5) V1(6) V2(7)
+            V1(2)
+            V2(3) V1(4) V2(5) V1(6) V2(7)
 
             {
 #ifndef MINIMP3_FLOAT_OUTPUT
@@ -1940,7 +1936,8 @@ static void mp3d_synth(float *xl, mp3d_sample_t *dstl, int nch, float *lins) {
 
         S0(0)
         S2(1)
-        S1(2) S2(3) S1(4) S2(5) S1(6) S2(7)
+        S1(2)
+        S2(3) S1(4) S2(5) S1(6) S2(7)
 
             dstr[(15 - i) * nch] = mp3d_scale_pcm(a[1]);
         dstr[(17 + i) * nch] = mp3d_scale_pcm(b[1]);
@@ -2026,8 +2023,6 @@ static int mp3d_find_frame(const uint8_t *mp3, int mp3_bytes,
 
 void mp3dec_init(mp3dec_t *dec) { dec->header[0] = 0; }
 
-static L12_scale_info *sci = NULL;
-
 int mp3dec_decode_frame(mp3dec_t *dec, const uint8_t *mp3, int mp3_bytes,
                         mp3d_sample_t *pcm, mp3dec_frame_info_t *info) {
     int i = 0, igr, frame_size = 0, success = 1;
@@ -2077,12 +2072,14 @@ int mp3dec_decode_frame(mp3dec_t *dec, const uint8_t *mp3, int mp3_bytes,
             mp3dec_init(dec);
             return 0;
         }
-        success = L3_restore_reservoir(dec, bs_frame, &dec->scratch, main_data_begin);
+        success =
+            L3_restore_reservoir(dec, bs_frame, &dec->scratch, main_data_begin);
         if (success) {
             for (igr = 0; igr < (HDR_TEST_MPEG1(hdr) ? 2 : 1);
                  igr++, pcm += 576 * info->channels) {
                 memset(dec->scratch.grbuf[0], 0, 576 * 2 * sizeof(float));
-                L3_decode(dec, &dec->scratch, dec->scratch.gr_info + igr * info->channels,
+                L3_decode(dec, &dec->scratch,
+                          dec->scratch.gr_info + igr * info->channels,
                           info->channels);
                 mp3d_synth_granule(dec->qmf_state, dec->scratch.grbuf[0], 18,
                                    info->channels, pcm, dec->scratch.syn[0]);
@@ -2097,11 +2094,12 @@ int mp3dec_decode_frame(mp3dec_t *dec, const uint8_t *mp3, int mp3_bytes,
 
         memset(dec->scratch.grbuf[0], 0, 576 * 2 * sizeof(float));
         for (i = 0, igr = 0; igr < 3; igr++) {
-            if (12 ==
-                (i += L12_dequantize_granule(dec->scratch.grbuf[0] + i, bs_frame,
-                                             &dec->sci, info->layer | 1))) {
+            if (12 == (i += L12_dequantize_granule(dec->scratch.grbuf[0] + i,
+                                                   bs_frame, &dec->sci,
+                                                   info->layer | 1))) {
                 i = 0;
-                L12_apply_scf_384(&dec->sci, dec->sci.scf + igr, dec->scratch.grbuf[0]);
+                L12_apply_scf_384(&dec->sci, dec->sci.scf + igr,
+                                  dec->scratch.grbuf[0]);
                 mp3d_synth_granule(dec->qmf_state, dec->scratch.grbuf[0], 12,
                                    info->channels, pcm, dec->scratch.syn[0]);
                 memset(dec->scratch.grbuf[0], 0, 576 * 2 * sizeof(float));
diff --git a/components/micropython/usermodule/mp_media.c b/components/micropython/usermodule/mp_media.c
index f9f30e7f66dc66606882e8c2e3375cf600f88715..aa813913f6683e8384b89ff202b439278d043f36 100644
--- a/components/micropython/usermodule/mp_media.c
+++ b/components/micropython/usermodule/mp_media.c
@@ -9,10 +9,10 @@ typedef struct _mp_ctx_obj_t {
     mp_obj_t user_data;
 } mp_ctx_obj_t;
 
-STATIC mp_obj_t mp_set_path(mp_obj_t path) {
-    return mp_obj_new_int(st3m_media_set_path(mp_obj_str_get_str(path)));
+STATIC mp_obj_t mp_load(mp_obj_t path) {
+    return mp_obj_new_int(st3m_media_load(mp_obj_str_get_str(path)));
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_set_path_obj, mp_set_path);
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_load_obj, mp_load);
 
 STATIC mp_obj_t mp_draw(mp_obj_t uctx_mp) {
     mp_ctx_obj_t *uctx = MP_OBJ_TO_PTR(uctx_mp);
@@ -37,7 +37,7 @@ STATIC const mp_rom_map_elem_t globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR_draw), MP_ROM_PTR(&mp_draw_obj) },
     { MP_ROM_QSTR(MP_QSTR_think), MP_ROM_PTR(&mp_think_obj) },
     { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&mp_stop_obj) },
-    { MP_ROM_QSTR(MP_QSTR_set_path), MP_ROM_PTR(&mp_set_path_obj) },
+    { MP_ROM_QSTR(MP_QSTR_load), MP_ROM_PTR(&mp_load_obj) },
 };
 
 STATIC MP_DEFINE_CONST_DICT(globals, globals_table);
diff --git a/components/st3m/st3m_media.c b/components/st3m/st3m_media.c
index 074facb1324ac81b116399bd1c614b905d3eea18..7f3301ff0639da93d983d31ca7cbb69a5ebe8d68 100644
--- a/components/st3m/st3m_media.c
+++ b/components/st3m/st3m_media.c
@@ -8,125 +8,417 @@
 #include "esp_log.h"
 #include "freertos/FreeRTOS.h"
 
-//static const char *TAG = "st3m-media";
+// static const char *TAG = "st3m-media";
 
 #ifdef CONFIG_FLOW3R_CTX_FLAVOUR_FULL
-static st3m_media *media = NULL;
+static st3m_media *audio_media = NULL;
+static st3m_media *visual_media = NULL;
 
+// XXX should be refactored to either be temporary SPIRAM allocation
+// or a static shared global pcm queuing API
+//
 static int16_t audio_buffer[AUDIO_BUF_SIZE];
 
+static st3m_media_playmode playmode = st3m_media_playmode_queue;
+static float current_audio_in = 0.0;
+static float current_audio_out = -1.0;
+static float current_visual_in = 0.0;
+static float current_visual_out = -1.0;
+
+static int file_is_visual(const char *path) {
+    if (!path) return 0;
+    if (strstr(path, ".png") || strstr(path, ".PNG") || strstr(path, ".jpg") ||
+        strstr(path, ".JPG") || strstr(path, ".gif") || strstr(path, ".GIF") ||
+        strstr(path, ".txt") || strstr(path, ".TXT")) {
+        return 1;
+    }
+    return 0;
+}
+
+///////////////////////////////
+
 void st3m_media_audio_out(int16_t *rx, int16_t *tx, uint16_t len) {
-    if (!media) return;
+    if (!audio_media) return;
     for (int i = 0; i < len; i++) {
-        if ((media->audio_r + 1 != media->audio_w) &&
-            (media->audio_r + 1 - AUDIO_BUF_SIZE != media->audio_w)) {
-            tx[i] = media->audio_buffer[media->audio_r++];
-            if (media->audio_r >= AUDIO_BUF_SIZE) media->audio_r = 0;
+        if ((audio_media->audio_r + 1 != audio_media->audio_w) &&
+            (audio_media->audio_r + 1 - AUDIO_BUF_SIZE !=
+             audio_media->audio_w)) {
+            tx[i] = audio_media->audio_buffer[audio_media->audio_r++];
+            if (audio_media->audio_r >= AUDIO_BUF_SIZE)
+                audio_media->audio_r = 0;
         } else
             tx[i] = 0;
     }
 }
-int st3m_media_samples_queued (void)
-{
-  if (!media) return 0;
-  if (media->audio_r > media->audio_w)
-    return (AUDIO_BUF_SIZE-media->audio_r) + media->audio_w;
-  return media->audio_w - media->audio_r;
+int st3m_media_samples_queued(void) {
+    if (!audio_media) return 0;
+    if (audio_media->audio_r > audio_media->audio_w)
+        return (AUDIO_BUF_SIZE - audio_media->audio_r) + audio_media->audio_w;
+    return audio_media->audio_w - audio_media->audio_r;
 }
 
-st3m_media *st3m_media_load_mpg1(const char *path);
-st3m_media *st3m_media_load_mod(const char *path);
-st3m_media *st3m_media_load_mp3(const char *path);
-
 void st3m_media_stop(void) {
-    if (media && media->destroy) media->destroy(media);
-    media = NULL;
+    if (audio_media && audio_media->destroy) audio_media->destroy(audio_media);
+    if (visual_media == audio_media) visual_media = NULL;
+    audio_media = NULL;
     st3m_audio_set_player_function(st3m_audio_player_function_dummy);
-}
-
-int st3m_media_set_path(const char *path) {
-    st3m_media_stop();
-    if (strstr(path, ".mpg"))
-        media = st3m_media_load_mpg1(path);
-    else if (strstr(path, ".mod"))
-        media = st3m_media_load_mod(path);
-    else if (strstr(path, ".mp3"))
-        media = st3m_media_load_mp3(path);
-
-    if (!media) return 0;
-
-    st3m_audio_set_player_function(st3m_media_audio_out);
-    media->audio_buffer = audio_buffer;
-    media->audio_r = 0;
-    media->audio_w = 1;
-
-    return 1;
+    if (visual_media) {
+        visual_media->destroy(visual_media);
+        visual_media = NULL;
+    }
 }
 
 void st3m_media_pause(void) {
-    if (!media) return;
-    media->paused = 1;
+    if (!audio_media) return;
+    audio_media->paused = 1;
 }
 
 void st3m_media_play(void) {
-    if (!media) return;
-    media->paused = 0;
+    if (!audio_media) return;
+    audio_media->paused = 0;
 }
 
 int st3m_media_is_playing(void) {
-    if (!media) return 0;
-    return !media->paused;
+    if (!audio_media) return 0;
+    return !audio_media->paused;
 }
 
 float st3m_media_get_duration(void) {
-    if (!media) return 0;
-    return media->duration;
+    if (!audio_media) return 0;
+    return audio_media->duration;
+}
+
+float st3m_media_get_visual_duration(void) {
+    if (!visual_media) return 0;
+    return visual_media->duration;
 }
 
 float st3m_media_get_position(void) {
-    if (!media) return 0;
-    return media->position;
+    if (!audio_media) return 0;
+    return audio_media->position;
 }
 
 float st3m_media_get_time(void) {
-    if (!media) return 0;
-    return media->position * media->duration;
+    if (!audio_media) return 0;
+    return audio_media->time;
+}
+
+float st3m_media_get_visual_time(void) {
+    if (!visual_media) return 0;
+    return visual_media->time;
 }
 
 void st3m_media_seek(float position) {
-    if (!media) return;
-    media->seek = position;
+    if (!audio_media) return;
+    audio_media->seek = position;
 }
 
 void st3m_media_seek_relative(float time) {
-    if (!media) return;
-    st3m_media_seek((media->position * media->duration) + time);
+    if (!audio_media) return;
+    st3m_media_seek((audio_media->position * audio_media->duration) + time);
 }
 
 void st3m_media_draw(Ctx *ctx) {
-    if (!media) return;
-    if (media->draw) media->draw(media, ctx);
+    if (!audio_media) return;
+    if (visual_media && visual_media->draw)
+        visual_media->draw(visual_media, ctx);
+    else if (audio_media->draw)
+        audio_media->draw(audio_media, ctx);
+}
+
+typedef struct st3m_playlist_entry {
+    char *path_or_uri;
+    float in;
+    float out;
+} st3m_playlist_entry;
+
+static int st3m_playlist_cursor = 0;
+static int st3m_playlist_visual_cursor = 0;
+static st3m_playlist_entry *st3m_playlist = NULL;
+static size_t st3m_playlist_count = 0;
+static size_t st3m_playlist_capacity = 0;
+
+static int st3m_media_current_visual_is_done(void) {
+    if (visual_media == audio_media) return 0;
+    if ((current_visual_out <= 0.0f)) return visual_media->position >= 1.0;
+    float out =
+        current_visual_out <= 0.0 ? st3m_media_get_visual_duration() : 0;
+    return st3m_media_get_visual_time() >= out;
+}
+
+static int st3m_media_current_is_done(void) {
+    if (!audio_media) return 1;
+    if ((current_audio_out <= 0.0f)) return st3m_media_get_position() >= 1.0;
+    float out = current_audio_out <= 0.0 ? st3m_media_get_duration() : 0;
+    return st3m_media_get_time() >= out;
 }
 
 void st3m_media_think(float ms) {
-    if (!media) return;
-    if (media->think) media->think(media, ms);
+    if (st3m_media_current_is_done()) {
+        // find next item to play if any - depending on play mode
+        switch (playmode) {
+            case st3m_media_playmode_queue_consume:
+                st3m_media_load(
+                    st3m_playlist[st3m_playlist_cursor].path_or_uri);
+                current_audio_in = st3m_playlist[st3m_playlist_cursor].in;
+                current_audio_out = st3m_playlist[st3m_playlist_cursor].out;
+                st3m_playlist_visual_cursor = st3m_playlist_cursor;
+                if (file_is_visual(
+                        st3m_playlist_get(st3m_playlist_visual_cursor + 1))) {
+                    st3m_playlist_visual_cursor++;
+                    st3m_media_load(
+                        st3m_playlist[st3m_playlist_visual_cursor].path_or_uri);
+                }
+                memmove(&st3m_playlist[st3m_playlist_cursor],
+                        &st3m_playlist[st3m_playlist_cursor + 1],
+                        sizeof(st3m_playlist_entry) *
+                            (--st3m_playlist_count - st3m_playlist_cursor));
+                break;
+
+            case st3m_media_playmode_queue:
+            case st3m_media_playmode_shuffle:
+                if (st3m_playlist) {
+                    st3m_media_load(
+                        st3m_playlist[st3m_playlist_cursor].path_or_uri);
+                    current_audio_in = st3m_playlist[st3m_playlist_cursor].in;
+                    current_audio_out = st3m_playlist[st3m_playlist_cursor].out;
+                    st3m_playlist_visual_cursor = st3m_playlist_cursor;
+                    if (file_is_visual(st3m_playlist_get(
+                            st3m_playlist_visual_cursor + 1))) {
+                        st3m_playlist_visual_cursor++;
+                        st3m_media_load(
+                            st3m_playlist[st3m_playlist_visual_cursor]
+                                .path_or_uri);
+                    }
+
+                    if (playmode == st3m_media_playmode_shuffle)
+                        st3m_playlist_cursor = random() % st3m_playlist_count;
+                    else
+                        st3m_playlist_cursor =
+                            (st3m_playlist_cursor + 1) % st3m_playlist_count;
+                }
+                break;
+
+            case st3m_media_playmode_single:
+                st3m_media_pause();
+                break;
+            case st3m_media_playmode_loop:
+                st3m_media_seek(0);
+                st3m_playlist_visual_cursor = st3m_playlist_cursor;
+                if (file_is_visual(
+                        st3m_playlist_get(st3m_playlist_visual_cursor + 1))) {
+                    st3m_playlist_visual_cursor++;
+                    st3m_media_load(
+                        st3m_playlist[st3m_playlist_visual_cursor].path_or_uri);
+                }
+                break;
+        }
+
+        // fast-forward until in-point
+        if (audio_media && current_audio_in > 0.0) switch (playmode) {
+                case st3m_media_playmode_queue_consume:
+                case st3m_media_playmode_queue:
+                case st3m_media_playmode_shuffle:
+                case st3m_media_playmode_loop:
+                    audio_media->think(audio_media, current_audio_in * 1000);
+                    break;
+
+                case st3m_media_playmode_single:
+                    audio_media->think(audio_media, current_audio_in * 1000);
+                    return;
+            }
+    }
+
+    if (!audio_media) return;
+
+    if (audio_media->think) audio_media->think(audio_media, ms);
+
+    if (visual_media != audio_media) {
+        if (st3m_media_current_visual_is_done()) {
+            // find next item to play if any - depending on play mode
+            switch (playmode) {
+                case st3m_media_playmode_queue_consume:
+                    if (file_is_visual(st3m_playlist_get(
+                            st3m_playlist_visual_cursor + 1))) {
+                        st3m_playlist_visual_cursor++;
+                        st3m_media_load(
+                            st3m_playlist[st3m_playlist_visual_cursor]
+                                .path_or_uri);
+                        current_visual_in =
+                            st3m_playlist[st3m_playlist_visual_cursor].in;
+                        current_visual_out =
+                            st3m_playlist[st3m_playlist_visual_cursor].out;
+                        memmove(&st3m_playlist[st3m_playlist_visual_cursor],
+                                &st3m_playlist[st3m_playlist_cursor + 1],
+                                sizeof(st3m_playlist_entry) *
+                                    (--st3m_playlist_count -
+                                     st3m_playlist_visual_cursor));
+                    }
+                    break;
+
+                case st3m_media_playmode_queue:
+                case st3m_media_playmode_shuffle:
+                    if (st3m_playlist) {
+                        if (file_is_visual(st3m_playlist_get(
+                                st3m_playlist_visual_cursor + 1))) {
+                            st3m_playlist_visual_cursor++;
+                            st3m_media_load(
+                                st3m_playlist[st3m_playlist_visual_cursor]
+                                    .path_or_uri);
+                        } else {
+                            st3m_playlist_visual_cursor =
+                                st3m_playlist_cursor + 1;
+                            if (st3m_playlist_visual_cursor >=
+                                st3m_playlist_count)
+                                st3m_playlist_visual_cursor =
+                                    st3m_playlist_cursor;
+                            st3m_media_load(
+                                st3m_playlist[st3m_playlist_visual_cursor]
+                                    .path_or_uri);
+                        }
+                    }
+                    break;
+
+                case st3m_media_playmode_single:
+                    break;
+                case st3m_media_playmode_loop:
+                    st3m_playlist_visual_cursor = st3m_playlist_cursor + 1;
+                    if (st3m_playlist_visual_cursor >= st3m_playlist_count)
+                        st3m_playlist_visual_cursor = st3m_playlist_cursor;
+                    st3m_media_load(
+                        st3m_playlist[st3m_playlist_visual_cursor].path_or_uri);
+                    break;
+            }
+
+            // fast-forward until in-point
+            if (visual_media != audio_media && current_visual_in > 0.0)
+                switch (playmode) {
+                    case st3m_media_playmode_queue_consume:
+                    case st3m_media_playmode_queue:
+                        visual_media->think(visual_media,
+                                            current_visual_in * 1000);
+                    case st3m_media_playmode_shuffle:
+                        visual_media->think(visual_media,
+                                            current_visual_in * 1000);
+                    case st3m_media_playmode_loop:
+                        break;
+
+                    case st3m_media_playmode_single:
+                        visual_media->think(visual_media,
+                                            current_visual_in * 1000);
+                        return;
+                }
+        }
+    }
 }
 
 char *st3m_media_get_string(const char *key) {
-    if (!media) return NULL;
-    if (!media->get_string) return NULL;
-    return media->get_string(media, key);
+    if (!audio_media) return NULL;
+    if (!audio_media->get_string) return NULL;
+    return audio_media->get_string(audio_media, key);
 }
 
 float st3m_media_get(const char *key) {
-    if (!media || !media->get_string) return -1.0f;
-    return media->get(media, key);
+    if (!audio_media || !audio_media->get_string) return -1.0f;
+    return audio_media->get(audio_media, key);
 }
 
 void st3m_media_set(const char *key, float value) {
-    if (!media || !media->set) return;
-    return media->set(media, key, value);
+    if (!audio_media || !audio_media->set) return;
+    return audio_media->set(audio_media, key, value);
+}
+
+st3m_media_playmode st3m_media_get_playmode(void) { return playmode; }
+
+void st3m_media_set_playmode(st3m_media_playmode new_mode) {
+    playmode = new_mode;
+}
+
+void st3m_playlist_add_with_in_out(int index, const char *path_or_uri, float in,
+                                   float out) {
+    if (index > st3m_playlist_count) return;
+    if (index < 0) index = st3m_playlist_count;
+    if (index >= st3m_playlist_capacity) {
+        st3m_playlist =
+            realloc(st3m_playlist,
+                    sizeof(st3m_playlist_entry) * (st3m_playlist_capacity + 8));
+        st3m_playlist_capacity += 8;
+    }
+}
+
+void st3m_playlist_add(int index, const char *path_or_uri) {
+    return st3m_playlist_add_with_in_out(index, path_or_uri, 0.0f, -1.0f);
+}
+
+// remove the media item at queued position
+// -1 to clear queue
+void st3m_playlist_remove(int index) {
+    if (index < 0) {
+        for (int i = 0; i < st3m_playlist_count; i++)
+            free(st3m_playlist[i].path_or_uri);
+        st3m_playlist_count = 0;
+    } else if (index < st3m_playlist_count) {
+        free(st3m_playlist[index].path_or_uri);
+        memmove(&st3m_playlist[index], &st3m_playlist[index + 1],
+                (st3m_playlist_count - (index + 1)));
+        st3m_playlist_count--;
+    }
+}
+
+// get media at given position
+const char *st3m_playlist_get(int index) {
+    if (!(index >= 0 && index < st3m_playlist_count)) return NULL;
+    return st3m_playlist[index].path_or_uri;
+}
+
+// set in and out value for a clip in queue
+void st3m_media_set_in_out(int index, float in, float out) {
+    if (!(index >= 0 && index < st3m_playlist_count)) return;
+    st3m_playlist[index].in = in;
+    st3m_playlist[index].out = out;
+}
+
+// get in and out values for a clip in queue
+void st3m_media_get_in_out(int index, float *in, float *out) {
+    if (!(index >= 0 && index < st3m_playlist_count)) {
+        if (in) *in = 0.0;
+        if (out) *out = -1.0;
+        return;
+    }
+    if (in) *in = st3m_playlist[index].in;
+    if (out) *out = st3m_playlist[index].out;
+}
+
+///////////////////////////////////////////////////////////////////
+// loading dispatch - entirely based on substring matching.
+///////////////////////////////////////////////////////////////////
+
+st3m_media *st3m_media_load_mpg1(const char *path);
+st3m_media *st3m_media_load_mod(const char *path);
+st3m_media *st3m_media_load_mp3(const char *path);
+
+int st3m_media_load(const char *path) {
+    if (strstr(path, ".mpg")) {
+        st3m_media_stop();
+        audio_media = visual_media = st3m_media_load_mpg1(path);
+    } else if (strstr(path, ".mod")) {
+        st3m_media_stop();
+        audio_media = visual_media = st3m_media_load_mod(path);
+    } else if (strstr(path, ".mp3")) {
+        st3m_media_stop();
+        audio_media = visual_media = st3m_media_load_mp3(path);
+    }
+
+    if (!audio_media) return 0;
+
+    st3m_audio_set_player_function(st3m_media_audio_out);
+    audio_media->audio_buffer = audio_buffer;
+    audio_media->audio_r = 0;
+    audio_media->audio_w = 1;
+
+    current_audio_in = 0.0;
+    current_audio_out = -1.0;
+    return 1;
 }
 
 #endif
diff --git a/components/st3m/st3m_media.h b/components/st3m/st3m_media.h
index 66ea9d9bd89a1f45d1ed4e8b32b2cef6495db73c..681dd66b8a27cc9adaf6886588479e9773f9bc7f 100644
--- a/components/st3m/st3m_media.h
+++ b/components/st3m/st3m_media.h
@@ -6,6 +6,14 @@
 
 #define AUDIO_BUF_SIZE (8192)
 
+typedef enum {
+    st3m_media_playmode_queue,
+    st3m_media_playmode_queue_consume,
+    st3m_media_playmode_shuffle,
+    st3m_media_playmode_single,
+    st3m_media_playmode_loop
+} st3m_media_playmode;
+
 typedef struct _st3m_media st3m_media;
 
 struct _st3m_media {
@@ -20,7 +28,7 @@ struct _st3m_media {
 
     // free resources used by this media instance
     void (*destroy)(st3m_media *media);
- 
+
     // draw the current frame / visualization / metadata screen
     void (*draw)(st3m_media *media, Ctx *ctx);
 
@@ -36,12 +44,17 @@ struct _st3m_media {
 
     // Duration of media in seconds or -1 for infinite/streaming media
     // at worst approximation of some unit, set by decoder.
-    float duration; 
+    float duration;
 
     // currently played back position - set by decoder
     float position;
 
-    // decoder should seek to this position if not -1, and set it to -1
+    // currently played back position, in seconds - set by decoder
+    float time;  // time might be precise even when duration is not
+                 // until first play through of some media, when time
+                 // duration should also be set exact.
+
+    // decoder should seek to this relative if not -1, and set it to -1
     float seek;
 
     // if set to 1 playback is momentarily stopped but can be resumed,
@@ -49,12 +62,26 @@ struct _st3m_media {
     int paused;
 };
 
-int   st3m_media_samples_queued (void);
-int   st3m_media_set_path(const char *path);
-void  st3m_media_pause(void);
-void  st3m_media_play(void);
-void  st3m_media_stop(void);
-int   st3m_media_is_playing(void);
+st3m_media_playmode st3m_media_get_playmode(void);
+void st3m_media_set_playmode(st3m_media_playmode);
+
+// stops the currently playing media item
+void st3m_media_stop(void);
+// set a new media item
+int st3m_media_load(const char *path_or_uri);
+
+// decode current media item ms ahead (unless paused)
+void st3m_media_think(float ms);
+
+// draw the codecs own view of itself / its meta data - progress
+// for video or animations formats this should draw the media-content
+// other codecs can find mixes of debug visualizations.
+void st3m_media_draw(Ctx *ctx);
+
+// controls whether we are playing
+void st3m_media_pause(void);
+void st3m_media_play(void);
+int st3m_media_is_playing(void);
 
 // get duration in seconds
 float st3m_media_get_duration(void);
@@ -63,13 +90,9 @@ float st3m_media_get_time(void);
 // get current playback position relative to overall duration
 float st3m_media_get_position(void);
 // seek to a position relative to overall duration
-void  st3m_media_seek(float position);
+void st3m_media_seek(float position);
 // seek a relative amount of seconds forward or with negative values back
-void  st3m_media_seek_relative(float seconds_jump);
-// draw the codecs own view of itself / its meta data - progress
-void  st3m_media_draw(Ctx *ctx);
-// iterate time forwards this many milliseconds
-void  st3m_media_think(float ms);
+void st3m_media_seek_relative(float seconds_jump);
 
 // get decoder specific string or NULL if not existing, free returned value
 //  common values:
@@ -78,8 +101,37 @@ char *st3m_media_get_string(const char *key);
 // get a decoder specific numeric value, defaulting to -1 for nonexisting values
 float st3m_media_get(const char *key);
 // set a decoder specific floating point value
-// common values:
-//    scale 0.0 - 1.0  - how large part of the screen to take up
-//    grayscale 0 or 1
-void  st3m_media_set(const char *key, float value);
+// example posible/or already used values:
+//    "scale"       0.0 - 1.0  - how large part of the screen to take up
+//    "grayscale"   0 or 1     - drop color bits for performance
+//    "smoothing"   0 or 1     - enable smooth texture scaling
+void st3m_media_set(const char *key, float value);
+
+// API for use in implementations
+// query how manu audio samples have been queued in the pcm output buffer
+int st3m_media_samples_queued(void);
+
+// add a path or uri to the media queue
+// 0 to insert at front of queue
+// 1 to queue after upcoming song
+// -1 to queue at end
+// -2 to replace whole queue
+void st3m_playlist_add(int index, const char *path_or_uri);
+
+// adds a media item with in and out point, in point is the time of a timed
+// media to start playing at defaults to 0.0 out is the time to stop playing
+// defaulting to -1 which means play to end.
+void st3m_playlist_add_with_in_out(int index, const char *path_or_uri, float in,
+                                   float out);
+
+// remove the media item at queued position
+// -1 to clear queue
+void st3m_playlist_remove(int index);
+
+// get media at given position
+const char *st3m_playlist_get(int index);
 
+// set in and out value for a clip in queue
+void st3m_playlist_set_in_out(int index, float in, float out);
+// get in and out values for a clip in queue
+void st3m_playlist_get_in_out(int index, float *in, float *out);
diff --git a/python_payload/apps/mod/__init__.py b/python_payload/apps/mod/__init__.py
deleted file mode 100644
index 940003c9a48d7560bdcd322943f9a4a0f37aebd7..0000000000000000000000000000000000000000
--- a/python_payload/apps/mod/__init__.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from st3m.application import Application, ApplicationContext
-from ctx import Context
-
-import media
-
-
-class ModApp(Application):
-    def __init__(self, app_ctx: ApplicationContext) -> None:
-        super().__init__(app_ctx)
-        self._filename = "/sd/elysium.mod"
-
-    def draw(self, ctx: Context) -> None:
-        media.draw(ctx)
-
-    def think(self, ins: InputState, delta_ms: int) -> None:
-        super().think(ins, delta_ms)
-        media.think(delta_ms)
-
-    def on_enter(self, vm: Optional[ViewManager]) -> None:
-        super().on_enter(vm)
-        media.set_path(self._filename)
-
-    def on_exit(self) -> None:
-        media.stop()
-        self._started = False
diff --git a/python_payload/apps/mod/flow3r.toml b/python_payload/apps/mod/flow3r.toml
deleted file mode 100644
index 7af1e9ec00f4d08f9f9a4232885e910c3e3289ff..0000000000000000000000000000000000000000
--- a/python_payload/apps/mod/flow3r.toml
+++ /dev/null
@@ -1,11 +0,0 @@
-[app]
-name = "MOD"
-menu = "Apps"
-
-[entry]
-class = "ModApp"
-
-[metadata]
-author = "Flow3r Badge Authors"
-license = "LGPL-3.0-only"
-url = "https://git.flow3r.garden/flow3r/flow3r-firmware"