diff --git a/components/audio_mod/CMakeLists.txt b/components/audio_mod/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..c8225c633e1e910cdd6f726bb3df42fc95318f84
--- /dev/null
+++ b/components/audio_mod/CMakeLists.txt
@@ -0,0 +1,8 @@
+idf_component_register(
+    SRCS
+        audio_mod.c
+    INCLUDE_DIRS
+        .
+        ../ctx
+        ../st3m
+)
diff --git a/components/audio_mod/audio_mod.c b/components/audio_mod/audio_mod.c
new file mode 100644
index 0000000000000000000000000000000000000000..404ca18408c18b7f0bf075b782776bc63a493d66
--- /dev/null
+++ b/components/audio_mod/audio_mod.c
@@ -0,0 +1,112 @@
+#ifndef __clang__
+#pragma GCC optimize("O2")
+#endif
+
+#include <fcntl.h>
+#include <st3m_audio.h>
+#include <st3m_media.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "ctx.h"
+
+#define POCKETMOD_IMPLEMENTATION
+#include "pocketmod.h"
+
+typedef struct {
+    st3m_media control;
+    pocketmod_context pocketmod;
+    uint8_t *data;
+    size_t size;
+} mod_state;
+
+static void mod_draw(st3m_media *media, Ctx *ctx) {
+    mod_state *self = (void *)media;
+
+    ctx_rectangle(ctx, -120, -120, 240, 240);
+    ctx_gray(ctx, 0);
+    ctx_fill(ctx);
+    // ctx_arc(ctx, 0, 0, 10, 10);
+    ctx_rgb(ctx, 1.0, 1.0, 1.0);
+    ctx_font_size(ctx, 20);
+    char buf[100];
+    sprintf(buf, "p:%i/%i l:%i lc:%i", self->pocketmod.pattern,
+            self->pocketmod.num_patterns, self->pocketmod.line,
+            self->pocketmod.loop_count);
+    ctx_move_to(ctx, -90, 0);
+    ctx_text(ctx, buf);
+    ctx_fill(ctx);
+}
+
+static void mod_think(st3m_media *media, float ms_elapsed) {
+    int samples_needed = (ms_elapsed / 1000.0) * 48000;
+    if (samples_needed > 1000) samples_needed = 1000;
+
+    float rendered[samples_needed * 2];
+    mod_state *self = (void *)media;
+    int rend = pocketmod_render(&self->pocketmod, rendered, sizeof(rendered));
+    for (int i = 0; i < rend / 4; i++) {
+        self->control.audio_buffer[self->control.audio_w++] =
+            rendered[i] * 20000;
+        if (self->control.audio_w >= AUDIO_BUF_SIZE) self->control.audio_w = 0;
+    }
+}
+
+static void mod_destroy(st3m_media *media) {
+    mod_state *self = (void *)media;
+    if (self->data) free(self->data);
+    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 = ftell(file);
+
+    if (length) {
+        *length = size;
+    }
+    rewind(file);
+    buffer = malloc(size + 2);
+    if (!buffer) {
+        fclose(file);
+        return -1;
+    }
+    remaining -= fread(buffer, 1, remaining, file);
+    if (remaining) {
+        fclose(file);
+        free(buffer);
+        return -1;
+    }
+    fclose(file);
+    *contents = (unsigned char *)buffer;
+    buffer[size] = 0;
+    return 0;
+}
+
+st3m_media *st3m_media_load_mod(const char *path) {
+    mod_state *self = (mod_state *)malloc(sizeof(mod_state));
+    memset(self, 0, sizeof(mod_state));
+    self->control.draw = mod_draw;
+    self->control.think = mod_think;
+    self->control.destroy = mod_destroy;
+    file_get_contents(path, &self->data, &self->size);
+    if (!self->data ||
+        !pocketmod_init(&self->pocketmod, self->data, self->size, 48000)) {
+        printf("BOOO\n");
+        if (self->data) free(self->data);
+        free(self);
+        return NULL;
+    }
+
+    return (st3m_media *)self;
+}
diff --git a/components/audio_mod/pocketmod.h b/components/audio_mod/pocketmod.h
new file mode 100644
index 0000000000000000000000000000000000000000..f6e6524d2cb0268786c4f6b58ece4badf22384ac
--- /dev/null
+++ b/components/audio_mod/pocketmod.h
@@ -0,0 +1,969 @@
+/* See end of file for license */
+
+#ifndef POCKETMOD_H_INCLUDED
+#define POCKETMOD_H_INCLUDED
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct pocketmod_context pocketmod_context;
+int pocketmod_init(pocketmod_context *c, const void *data, int size, int rate);
+int pocketmod_render(pocketmod_context *c, void *buffer, int size);
+int pocketmod_loop_count(pocketmod_context *c);
+
+#ifndef POCKETMOD_MAX_CHANNELS
+#define POCKETMOD_MAX_CHANNELS 32
+#endif
+
+#ifndef POCKETMOD_MAX_SAMPLES
+#define POCKETMOD_MAX_SAMPLES 31
+#endif
+
+typedef struct {
+    signed char *data;   /* Sample data buffer                      */
+    unsigned int length; /* Data length (in bytes)                  */
+} _pocketmod_sample;
+
+typedef struct {
+    unsigned char dirty;       /* Pitch/volume dirty flags                */
+    unsigned char sample;      /* Sample number (0..31)                   */
+    unsigned char volume;      /* Base volume without tremolo (0..64)     */
+    unsigned char balance;     /* Stereo balance (0..255)                 */
+    unsigned short period;     /* Note period (113..856)                  */
+    unsigned short delayed;    /* Delayed note period (113..856)          */
+    unsigned short target;     /* Target period (for tone portamento)     */
+    unsigned char finetune;    /* Note finetune (0..15)                   */
+    unsigned char loop_count;  /* E6x loop counter                        */
+    unsigned char loop_line;   /* E6x target line                         */
+    unsigned char lfo_step;    /* Vibrato/tremolo LFO step counter        */
+    unsigned char lfo_type[2]; /* LFO type for vibrato/tremolo            */
+    unsigned char effect;      /* Current effect (0x0..0xf or 0xe0..0xef) */
+    unsigned char param;       /* Raw effect parameter value              */
+    unsigned char param3;      /* Parameter memory for 3xx                */
+    unsigned char param4;      /* Parameter memory for 4xy                */
+    unsigned char param7;      /* Parameter memory for 7xy                */
+    unsigned char param9;      /* Parameter memory for 9xx                */
+    unsigned char paramE1;     /* Parameter memory for E1x                */
+    unsigned char paramE2;     /* Parameter memory for E2x                */
+    unsigned char paramEA;     /* Parameter memory for EAx                */
+    unsigned char paramEB;     /* Parameter memory for EBx                */
+    unsigned char real_volume; /* Volume (with tremolo adjustment)        */
+    float position;            /* Position in sample data buffer          */
+    float increment;           /* Position increment per output sample    */
+} _pocketmod_chan;
+
+struct pocketmod_context {
+    /* Read-only song data */
+    _pocketmod_sample samples[POCKETMOD_MAX_SAMPLES];
+    unsigned char *source;      /* Pointer to source MOD data              */
+    unsigned char *order;       /* Pattern order table                     */
+    unsigned char *patterns;    /* Start of pattern data                   */
+    unsigned char length;       /* Patterns in the order (1..128)          */
+    unsigned char reset;        /* Pattern to loop back to (0..127)        */
+    unsigned char num_patterns; /* Patterns in the file (1..128)           */
+    unsigned char num_samples;  /* Sample count (15 or 31)                 */
+    unsigned char num_channels; /* Channel count (1..32)                   */
+
+    /* Timing variables */
+    int samples_per_second; /* Sample rate (set by user)               */
+    int ticks_per_line;     /* A.K.A. song speed (initially 6)         */
+    float samples_per_tick; /* Depends on sample rate and BPM          */
+
+    /* Loop detection state */
+    unsigned char visited[16]; /* Bit mask of previously visited patterns */
+    int loop_count;            /* How many times the song has looped      */
+
+    /* Render state */
+    _pocketmod_chan channels[POCKETMOD_MAX_CHANNELS];
+    unsigned char pattern_delay; /* EEx pattern delay counter               */
+    unsigned int lfo_rng;        /* RNG used for the random LFO waveform    */
+
+    /* Position in song (from least to most granular) */
+    signed char pattern; /* Current pattern in order                */
+    signed char line;    /* Current line in pattern                 */
+    short tick;          /* Current tick in line                    */
+    float sample;        /* Current sample in tick                  */
+};
+
+#ifdef POCKETMOD_IMPLEMENTATION
+
+/* Memorize a parameter unless the new value is zero */
+#define POCKETMOD_MEM(dst, src)        \
+    do {                               \
+        (dst) = (src) ? (src) : (dst); \
+    } while (0)
+
+/* Same thing, but memorize each nibble separately */
+#define POCKETMOD_MEM2(dst, src)                               \
+    do {                                                       \
+        (dst) = (((src)&0x0f) ? ((src)&0x0f) : ((dst)&0x0f)) | \
+                (((src)&0xf0) ? ((src)&0xf0) : ((dst)&0xf0));  \
+    } while (0)
+
+/* Shortcut to sample metadata (sample must be nonzero) */
+#define POCKETMOD_SAMPLE(c, sample) ((c)->source + 12 + 30 * (sample))
+
+/* Channel dirty flags */
+#define POCKETMOD_PITCH 0x01
+#define POCKETMOD_VOLUME 0x02
+
+/* The size of one sample in bytes */
+#define POCKETMOD_SAMPLE_SIZE sizeof(float[2])
+
+/* Finetune adjustment table. Three octaves for each finetune setting. */
+static const signed char _pocketmod_finetune[16][36] = {
+    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { -6, -6, -5, -5, -4, -3, -3, -3, -3, -3, -3, -3, -3, -3, -2, -3, -2, -2,
+      -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0 },
+    { -12, -12, -10, -11, -8, -8, -7, -7, -6, -6, -6, -6,
+      -6,  -6,  -5,  -5,  -4, -4, -4, -3, -3, -3, -3, -2,
+      -3,  -3,  -2,  -3,  -3, -2, -2, -2, -2, -2, -2, -1 },
+    { -18, -17, -16, -16, -13, -12, -12, -11, -10, -10, -10, -9,
+      -9,  -9,  -8,  -8,  -7,  -6,  -6,  -5,  -5,  -5,  -5,  -4,
+      -5,  -4,  -3,  -4,  -4,  -3,  -3,  -3,  -3,  -2,  -2,  -2 },
+    { -24, -23, -21, -21, -18, -17, -16, -15, -14, -13, -13, -12,
+      -12, -12, -11, -10, -9,  -8,  -8,  -7,  -7,  -7,  -7,  -6,
+      -6,  -6,  -5,  -5,  -5,  -4,  -4,  -4,  -4,  -3,  -3,  -3 },
+    { -30, -29, -26, -26, -23, -21, -20, -19, -18, -17, -17, -16,
+      -15, -14, -13, -13, -11, -11, -10, -9,  -9,  -9,  -8,  -7,
+      -8,  -7,  -6,  -6,  -6,  -5,  -5,  -5,  -5,  -4,  -4,  -4 },
+    { -36, -34, -32, -31, -27, -26, -24, -23, -22, -21, -20, -19,
+      -18, -17, -16, -15, -14, -13, -12, -11, -11, -10, -10, -9,
+      -9,  -9,  -7,  -8,  -7,  -6,  -6,  -6,  -6,  -5,  -5,  -4 },
+    { -42, -40, -37, -36, -32, -30, -29, -27, -25, -24, -23, -22,
+      -21, -20, -18, -18, -16, -15, -14, -13, -13, -12, -12, -10,
+      -10, -10, -9,  -9,  -9,  -8,  -7,  -7,  -7,  -6,  -6,  -5 },
+    { 51, 48, 46, 42, 42, 38, 36, 34, 32, 30, 24, 27, 25, 24, 23, 21, 21, 19,
+      18, 17, 16, 15, 14, 14, 12, 12, 12, 10, 10, 10, 9,  8,  8,  8,  7,  7 },
+    { 44, 42, 40, 37, 37, 35, 32, 31, 29, 27, 25, 24, 22, 21, 20, 19, 18, 17,
+      16, 15, 15, 14, 13, 12, 11, 10, 10, 9,  9,  9,  8,  7,  7,  7,  6,  6 },
+    { 38, 36, 34, 32, 31, 30, 28, 27, 25, 24, 22, 21, 19, 18, 17, 16, 16, 15,
+      14, 13, 13, 12, 11, 11, 9,  9,  9,  8,  7,  7,  7,  6,  6,  6,  5,  5 },
+    { 31, 30, 29, 26, 26, 25, 24, 22, 21, 20, 18, 17, 16, 15, 14, 13, 13, 12,
+      12, 11, 11, 10, 9,  9,  8,  7,  8,  7,  6,  6,  6,  5,  5,  5,  5,  5 },
+    { 25, 24, 23, 21, 21, 20, 19, 18, 17, 16, 14, 14, 13, 12, 11, 10, 11, 10,
+      10, 9,  9,  8,  7,  7,  6,  6,  6,  5,  5,  5,  5,  4,  4,  4,  3,  4 },
+    { 19, 18, 17, 16, 16, 15, 15, 14, 13, 12, 11, 10, 9, 9, 9, 8, 8, 18,
+      7,  7,  7,  6,  5,  6,  5,  4,  5,  4,  4,  4,  4, 3, 3, 3, 3, 3 },
+    { 12, 12, 12, 10, 11, 11, 10, 10, 9, 8, 7, 7, 6, 6, 6, 5, 6, 5,
+      5,  5,  5,  4,  4,  4,  3,  3,  3, 3, 2, 3, 3, 2, 2, 2, 2, 2 },
+    { 6, 6, 6, 5, 6, 6, 6, 5, 5, 5, 4, 4, 3, 3, 3, 3, 3, 3,
+      3, 3, 3, 2, 2, 2, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
+};
+
+/* Min/max helper functions */
+static int _pocketmod_min(int x, int y) { return x < y ? x : y; }
+static int _pocketmod_max(int x, int y) { return x > y ? x : y; }
+
+/* Clamp a volume value to the 0..64 range */
+static int _pocketmod_clamp_volume(int x) {
+    x = _pocketmod_max(x, 0x00);
+    x = _pocketmod_min(x, 0x40);
+    return x;
+}
+
+/* Zero out a block of memory */
+static void _pocketmod_zero(void *data, int size) {
+    char *byte = data, *end = byte + size;
+    while (byte != end) {
+        *byte++ = 0;
+    }
+}
+
+/* Convert a period (at finetune = 0) to a note index in 0..35 */
+static int _pocketmod_period_to_note(int period) {
+    switch (period) {
+        case 856:
+            return 0;
+        case 808:
+            return 1;
+        case 762:
+            return 2;
+        case 720:
+            return 3;
+        case 678:
+            return 4;
+        case 640:
+            return 5;
+        case 604:
+            return 6;
+        case 570:
+            return 7;
+        case 538:
+            return 8;
+        case 508:
+            return 9;
+        case 480:
+            return 10;
+        case 453:
+            return 11;
+        case 428:
+            return 12;
+        case 404:
+            return 13;
+        case 381:
+            return 14;
+        case 360:
+            return 15;
+        case 339:
+            return 16;
+        case 320:
+            return 17;
+        case 302:
+            return 18;
+        case 285:
+            return 19;
+        case 269:
+            return 20;
+        case 254:
+            return 21;
+        case 240:
+            return 22;
+        case 226:
+            return 23;
+        case 214:
+            return 24;
+        case 202:
+            return 25;
+        case 190:
+            return 26;
+        case 180:
+            return 27;
+        case 170:
+            return 28;
+        case 160:
+            return 29;
+        case 151:
+            return 30;
+        case 143:
+            return 31;
+        case 135:
+            return 32;
+        case 127:
+            return 33;
+        case 120:
+            return 34;
+        case 113:
+            return 35;
+        default:
+            return 0;
+    }
+}
+
+/* Table-based sine wave oscillator */
+static int _pocketmod_sin(int step) {
+    /* round(sin(x * pi / 32) * 255) for x in 0..15 */
+    static const unsigned char sin[16] = { 0x00, 0x19, 0x32, 0x4a, 0x62, 0x78,
+                                           0x8e, 0xa2, 0xb4, 0xc5, 0xd4, 0xe0,
+                                           0xec, 0xf4, 0xfa, 0xfe };
+    int x = sin[step & 0x0f];
+    x = (step & 0x1f) < 0x10 ? x : 0xff - x;
+    return step < 0x20 ? x : -x;
+}
+
+/* Oscillators for vibrato/tremolo effects */
+static int _pocketmod_lfo(pocketmod_context *c, _pocketmod_chan *ch, int step) {
+    switch (ch->lfo_type[ch->effect == 7] & 3) {
+        case 0:
+            return _pocketmod_sin(step & 0x3f); /* Sine   */
+        case 1:
+            return 0xff - ((step & 0x3f) << 3); /* Saw    */
+        case 2:
+            return (step & 0x3f) < 0x20 ? 0xff : -0xff; /* Square */
+        case 3:
+            return (c->lfo_rng & 0x1ff) - 0xff; /* Random */
+        default:
+            return 0; /* Hush little compiler */
+    }
+}
+
+static void _pocketmod_update_pitch(pocketmod_context *c, _pocketmod_chan *ch) {
+    /* Don't do anything if the period is zero */
+    ch->increment = 0.0f;
+    if (ch->period) {
+        float period = ch->period;
+
+        /* Apply vibrato (if active) */
+        if (ch->effect == 0x4 || ch->effect == 0x6) {
+            int step = (ch->param4 >> 4) * ch->lfo_step;
+            int rate = ch->param4 & 0x0f;
+            period += _pocketmod_lfo(c, ch, step) * rate / 128.0f;
+
+            /* Apply arpeggio (if active) */
+        } else if (ch->effect == 0x0 && ch->param) {
+            static const float arpeggio[16] = {
+                /* 2^(X/12) for X in 0..15 */
+                1.000000f, 1.059463f, 1.122462f, 1.189207f,
+                1.259921f, 1.334840f, 1.414214f, 1.498307f,
+                1.587401f, 1.681793f, 1.781797f, 1.887749f,
+                2.000000f, 2.118926f, 2.244924f, 2.378414f
+            };
+            int step = (ch->param >> ((2 - c->tick % 3) << 2)) & 0x0f;
+            period /= arpeggio[step];
+        }
+
+        /* Calculate sample buffer position increment */
+        ch->increment = 3546894.6f / (period * c->samples_per_second);
+    }
+
+    /* Clear the pitch dirty flag */
+    ch->dirty &= ~POCKETMOD_PITCH;
+}
+
+static void _pocketmod_update_volume(pocketmod_context *c,
+                                     _pocketmod_chan *ch) {
+    int volume = ch->volume;
+    if (ch->effect == 0x7) {
+        int step = ch->lfo_step * (ch->param7 >> 4);
+        volume += _pocketmod_lfo(c, ch, step) * (ch->param7 & 0x0f) >> 6;
+    }
+    ch->real_volume = _pocketmod_clamp_volume(volume);
+    ch->dirty &= ~POCKETMOD_VOLUME;
+}
+
+static void _pocketmod_pitch_slide(_pocketmod_chan *ch, int amount) {
+    int max = 856 + _pocketmod_finetune[ch->finetune][0];
+    int min = 113 + _pocketmod_finetune[ch->finetune][35];
+    ch->period += amount;
+    ch->period = _pocketmod_max(ch->period, min);
+    ch->period = _pocketmod_min(ch->period, max);
+    ch->dirty |= POCKETMOD_PITCH;
+}
+
+static void _pocketmod_volume_slide(_pocketmod_chan *ch, int param) {
+    /* Undocumented quirk: If both x and y are nonzero, then the value of x */
+    /* takes precedence. (Yes, there are songs that rely on this behavior.) */
+    int change = (param & 0xf0) ? (param >> 4) : -(param & 0x0f);
+    ch->volume = _pocketmod_clamp_volume(ch->volume + change);
+    ch->dirty |= POCKETMOD_VOLUME;
+}
+
+static void _pocketmod_next_line(pocketmod_context *c) {
+    unsigned char(*data)[4];
+    int i, pos, pattern_break = -1;
+
+    /* When entering a new pattern order index, mark it as "visited" */
+    if (c->line == 0) {
+        c->visited[c->pattern >> 3] |= 1 << (c->pattern & 7);
+    }
+
+    /* Move to the next pattern if this was the last line */
+    if (++c->line == 64) {
+        if (++c->pattern == c->length) {
+            c->pattern = c->reset;
+        }
+        c->line = 0;
+    }
+
+    /* Find the pattern data for the current line */
+    pos = (c->order[c->pattern] * 64 + c->line) * c->num_channels * 4;
+    data = (unsigned char(*)[4])(c->patterns + pos);
+    for (i = 0; i < c->num_channels; i++) {
+        /* Decode columns */
+        int sample = (data[i][0] & 0xf0) | (data[i][2] >> 4);
+        int period = ((data[i][0] & 0x0f) << 8) | data[i][1];
+        int effect = ((data[i][2] & 0x0f) << 8) | data[i][3];
+
+        /* Memorize effect parameter values */
+        _pocketmod_chan *ch = &c->channels[i];
+        ch->effect = (effect >> 8) != 0xe ? (effect >> 8) : (effect >> 4);
+        ch->param = (effect >> 8) != 0xe ? (effect & 0xff) : (effect & 0x0f);
+
+        /* Set sample */
+        if (sample) {
+            if (sample <= POCKETMOD_MAX_SAMPLES) {
+                unsigned char *sample_data = POCKETMOD_SAMPLE(c, sample);
+                ch->sample = sample;
+                ch->finetune = sample_data[2] & 0x0f;
+                ch->volume = _pocketmod_min(sample_data[3], 0x40);
+                if (ch->effect != 0xED) {
+                    ch->dirty |= POCKETMOD_VOLUME;
+                }
+            } else {
+                ch->sample = 0;
+            }
+        }
+
+        /* Set note */
+        if (period) {
+            int note = _pocketmod_period_to_note(period);
+            period += _pocketmod_finetune[ch->finetune][note];
+            if (ch->effect != 0x3) {
+                if (ch->effect != 0xED) {
+                    ch->period = period;
+                    ch->dirty |= POCKETMOD_PITCH;
+                    ch->position = 0.0f;
+                    ch->lfo_step = 0;
+                } else {
+                    ch->delayed = period;
+                }
+            }
+        }
+
+        /* Handle pattern effects */
+        switch (ch->effect) {
+            /* Memorize parameters */
+            case 0x3:
+                POCKETMOD_MEM(ch->param3, ch->param); /* Fall through */
+            case 0x5:
+                POCKETMOD_MEM(ch->target, period);
+                break;
+            case 0x4:
+                POCKETMOD_MEM2(ch->param4, ch->param);
+                break;
+            case 0x7:
+                POCKETMOD_MEM2(ch->param7, ch->param);
+                break;
+            case 0xE1:
+                POCKETMOD_MEM(ch->paramE1, ch->param);
+                break;
+            case 0xE2:
+                POCKETMOD_MEM(ch->paramE2, ch->param);
+                break;
+            case 0xEA:
+                POCKETMOD_MEM(ch->paramEA, ch->param);
+                break;
+            case 0xEB:
+                POCKETMOD_MEM(ch->paramEB, ch->param);
+                break;
+
+            /* 8xx: Set stereo balance (nonstandard) */
+            case 0x8: {
+                ch->balance = ch->param;
+            } break;
+
+            /* 9xx: Set sample offset */
+            case 0x9: {
+                if (period != 0 || sample != 0) {
+                    ch->param9 = ch->param ? ch->param : ch->param9;
+                    ch->position = ch->param9 << 8;
+                }
+            } break;
+
+            /* Bxx: Jump to pattern */
+            case 0xB: {
+                c->pattern = ch->param < c->length ? ch->param : 0;
+                c->line = -1;
+            } break;
+
+            /* Cxx: Set volume */
+            case 0xC: {
+                ch->volume = _pocketmod_clamp_volume(ch->param);
+                ch->dirty |= POCKETMOD_VOLUME;
+            } break;
+
+            /* Dxy: Pattern break */
+            case 0xD: {
+                pattern_break = (ch->param >> 4) * 10 + (ch->param & 15);
+            } break;
+
+            /* E4x: Set vibrato waveform */
+            case 0xE4: {
+                ch->lfo_type[0] = ch->param;
+            } break;
+
+            /* E5x: Set sample finetune */
+            case 0xE5: {
+                ch->finetune = ch->param;
+                ch->dirty |= POCKETMOD_PITCH;
+            } break;
+
+            /* E6x: Pattern loop */
+            case 0xE6: {
+                if (ch->param) {
+                    if (!ch->loop_count) {
+                        ch->loop_count = ch->param;
+                        c->line = ch->loop_line;
+                    } else if (--ch->loop_count) {
+                        c->line = ch->loop_line;
+                    }
+                } else {
+                    ch->loop_line = c->line - 1;
+                }
+            } break;
+
+            /* E7x: Set tremolo waveform */
+            case 0xE7: {
+                ch->lfo_type[1] = ch->param;
+            } break;
+
+            /* E8x: Set stereo balance (nonstandard) */
+            case 0xE8: {
+                ch->balance = ch->param << 4;
+            } break;
+
+            /* EEx: Pattern delay */
+            case 0xEE: {
+                c->pattern_delay = ch->param;
+            } break;
+
+            /* Fxx: Set speed */
+            case 0xF: {
+                if (ch->param != 0) {
+                    if (ch->param < 0x20) {
+                        c->ticks_per_line = ch->param;
+                    } else {
+                        float rate = c->samples_per_second;
+                        c->samples_per_tick = rate / (0.4f * ch->param);
+                    }
+                }
+            } break;
+
+            default:
+                break;
+        }
+    }
+
+    /* Pattern breaks are handled here, so that only one jump happens even  */
+    /* when multiple Dxy commands appear on the same line. (You guessed it: */
+    /* There are songs that rely on this behavior!)                         */
+    if (pattern_break != -1) {
+        c->line = (pattern_break < 64 ? pattern_break : 0) - 1;
+        if (++c->pattern == c->length) {
+            c->pattern = c->reset;
+        }
+    }
+}
+
+static void _pocketmod_next_tick(pocketmod_context *c) {
+    int i;
+
+    /* Move to the next line if this was the last tick */
+    if (++c->tick == c->ticks_per_line) {
+        if (c->pattern_delay > 0) {
+            c->pattern_delay--;
+        } else {
+            _pocketmod_next_line(c);
+        }
+        c->tick = 0;
+    }
+
+    /* Make per-tick adjustments for all channels */
+    for (i = 0; i < c->num_channels; i++) {
+        _pocketmod_chan *ch = &c->channels[i];
+        int param = ch->param;
+
+        /* Advance the LFO random number generator */
+        c->lfo_rng = 0x0019660d * c->lfo_rng + 0x3c6ef35f;
+
+        /* Handle effects that may happen on any tick of a line */
+        switch (ch->effect) {
+            /* 0xy: Arpeggio */
+            case 0x0: {
+                ch->dirty |= POCKETMOD_PITCH;
+            } break;
+
+            /* E9x: Retrigger note every x ticks */
+            case 0xE9: {
+                if (!(param && c->tick % param)) {
+                    ch->position = 0.0f;
+                    ch->lfo_step = 0;
+                }
+            } break;
+
+            /* ECx: Cut note after x ticks */
+            case 0xEC: {
+                if (c->tick == param) {
+                    ch->volume = 0;
+                    ch->dirty |= POCKETMOD_VOLUME;
+                }
+            } break;
+
+            /* EDx: Delay note for x ticks */
+            case 0xED: {
+                if (c->tick == param && ch->sample) {
+                    ch->dirty |= POCKETMOD_VOLUME | POCKETMOD_PITCH;
+                    ch->period = ch->delayed;
+                    ch->position = 0.0f;
+                    ch->lfo_step = 0;
+                }
+            } break;
+
+            default:
+                break;
+        }
+
+        /* Handle effects that only happen on the first tick of a line */
+        if (c->tick == 0) {
+            switch (ch->effect) {
+                case 0xE1:
+                    _pocketmod_pitch_slide(ch, -ch->paramE1);
+                    break;
+                case 0xE2:
+                    _pocketmod_pitch_slide(ch, +ch->paramE2);
+                    break;
+                case 0xEA:
+                    _pocketmod_volume_slide(ch, ch->paramEA << 4);
+                    break;
+                case 0xEB:
+                    _pocketmod_volume_slide(ch, ch->paramEB & 15);
+                    break;
+                default:
+                    break;
+            }
+
+            /* Handle effects that are not applied on the first tick of a line
+             */
+        } else {
+            switch (ch->effect) {
+                /* 1xx: Portamento up */
+                case 0x1: {
+                    _pocketmod_pitch_slide(ch, -param);
+                } break;
+
+                /* 2xx: Portamento down */
+                case 0x2: {
+                    _pocketmod_pitch_slide(ch, +param);
+                } break;
+
+                /* 5xy: Volume slide + tone portamento */
+                case 0x5: {
+                    _pocketmod_volume_slide(ch, param);
+                } /* Fall through */
+
+                /* 3xx: Tone portamento */
+                case 0x3: {
+                    int rate = ch->param3;
+                    int order = ch->period < ch->target;
+                    int closer = ch->period + (order ? rate : -rate);
+                    int new_order = closer < ch->target;
+                    ch->period = new_order == order ? closer : ch->target;
+                    ch->dirty |= POCKETMOD_PITCH;
+                } break;
+
+                /* 6xy: Volume slide + vibrato */
+                case 0x6: {
+                    _pocketmod_volume_slide(ch, param);
+                } /* Fall through */
+
+                /* 4xy: Vibrato */
+                case 0x4: {
+                    ch->lfo_step++;
+                    ch->dirty |= POCKETMOD_PITCH;
+                } break;
+
+                /* 7xy: Tremolo */
+                case 0x7: {
+                    ch->lfo_step++;
+                    ch->dirty |= POCKETMOD_VOLUME;
+                } break;
+
+                /* Axy: Volume slide */
+                case 0xA: {
+                    _pocketmod_volume_slide(ch, param);
+                } break;
+
+                default:
+                    break;
+            }
+        }
+
+        /* Update channel volume/pitch if either is out of date */
+        if (ch->dirty & POCKETMOD_VOLUME) {
+            _pocketmod_update_volume(c, ch);
+        }
+        if (ch->dirty & POCKETMOD_PITCH) {
+            _pocketmod_update_pitch(c, ch);
+        }
+    }
+}
+
+static void _pocketmod_render_channel(pocketmod_context *c,
+                                      _pocketmod_chan *chan, float *output,
+                                      int samples_to_write) {
+    /* Gather some loop data */
+    _pocketmod_sample *sample = &c->samples[chan->sample - 1];
+    unsigned char *data = POCKETMOD_SAMPLE(c, chan->sample);
+    const int loop_start = ((data[4] << 8) | data[5]) << 1;
+    const int loop_length = ((data[6] << 8) | data[7]) << 1;
+    const int loop_end = loop_length > 2 ? loop_start + loop_length : 0xffffff;
+    const float sample_end = 1 + _pocketmod_min(loop_end, sample->length);
+
+    /* Calculate left/right levels */
+    const float volume = chan->real_volume / (float)(128 * 64 * 4);
+    const float level_l = volume * (1.0f - chan->balance / 255.0f);
+    const float level_r = volume * (0.0f + chan->balance / 255.0f);
+
+    /* Write samples */
+    int i, num;
+    do {
+        /* Calculate how many samples we can write in one go */
+        num = (sample_end - chan->position) / chan->increment;
+        num = _pocketmod_min(num, samples_to_write);
+
+        /* Resample and write 'num' samples */
+        for (i = 0; i < num; i++) {
+            int x0 = chan->position;
+#ifdef POCKETMOD_NO_INTERPOLATION
+            float s = sample->data[x0];
+#else
+            int x1 = x0 + 1 - loop_length * (x0 + 1 >= loop_end);
+            float t = chan->position - x0;
+            float s = (1.0f - t) * sample->data[x0] + t * sample->data[x1];
+#endif
+            chan->position += chan->increment;
+            *output++ += level_l * s;
+            *output++ += level_r * s;
+        }
+
+        /* Rewind the sample when reaching the loop point */
+        if (chan->position >= loop_end) {
+            chan->position -= loop_length;
+
+            /* Cut the sample if the end is reached */
+        } else if (chan->position >= sample->length) {
+            chan->position = -1.0f;
+            break;
+        }
+
+        samples_to_write -= num;
+    } while (num > 0);
+}
+
+static int _pocketmod_ident(pocketmod_context *c, unsigned char *data,
+                            int size) {
+    int i, j;
+
+    /* 31-instrument files are at least 1084 bytes long */
+    if (size >= 1084) {
+        /* The format tag is located at offset 1080 */
+        unsigned char *tag = data + 1080;
+
+        /* List of recognized format tags (possibly incomplete) */
+        static const struct {
+            char name[5];
+            char channels;
+        } tags[] = {
+            /* TODO: FLT8 intentionally omitted because I haven't been able */
+            /* to find a specimen to test its funky pattern pairing format  */
+            { "M.K.", 4 },  { "M!K!", 4 },  { "FLT4", 4 },  { "4CHN", 4 },
+            { "OKTA", 8 },  { "OCTA", 8 },  { "CD81", 8 },  { "FA08", 8 },
+            { "1CHN", 1 },  { "2CHN", 2 },  { "3CHN", 3 },  { "4CHN", 4 },
+            { "5CHN", 5 },  { "6CHN", 6 },  { "7CHN", 7 },  { "8CHN", 8 },
+            { "9CHN", 9 },  { "10CH", 10 }, { "11CH", 11 }, { "12CH", 12 },
+            { "13CH", 13 }, { "14CH", 14 }, { "15CH", 15 }, { "16CH", 16 },
+            { "17CH", 17 }, { "18CH", 18 }, { "19CH", 19 }, { "20CH", 20 },
+            { "21CH", 21 }, { "22CH", 22 }, { "23CH", 23 }, { "24CH", 24 },
+            { "25CH", 25 }, { "26CH", 26 }, { "27CH", 27 }, { "28CH", 28 },
+            { "29CH", 29 }, { "30CH", 30 }, { "31CH", 31 }, { "32CH", 32 }
+        };
+
+        /* Check the format tag to determine if this is a 31-sample MOD */
+        for (i = 0; i < (int)(sizeof(tags) / sizeof(*tags)); i++) {
+            if (tags[i].name[0] == tag[0] && tags[i].name[1] == tag[1] &&
+                tags[i].name[2] == tag[2] && tags[i].name[3] == tag[3]) {
+                c->num_channels = tags[i].channels;
+                c->length = data[950];
+                c->reset = data[951];
+                c->order = &data[952];
+                c->patterns = &data[1084];
+                c->num_samples = 31;
+                return 1;
+            }
+        }
+    }
+
+    /* A 15-instrument MOD has to be at least 600 bytes long */
+    if (size < 600) {
+        return 0;
+    }
+
+    /* Check that the song title only contains ASCII bytes (or null) */
+    for (i = 0; i < 20; i++) {
+        if (data[i] != '\0' && (data[i] < ' ' || data[i] > '~')) {
+            return 0;
+        }
+    }
+
+    /* Check that sample names only contain ASCII bytes (or null) */
+    for (i = 0; i < 15; i++) {
+        for (j = 0; j < 22; j++) {
+            char chr = data[20 + i * 30 + j];
+            if (chr != '\0' && (chr < ' ' || chr > '~')) {
+                return 0;
+            }
+        }
+    }
+
+    /* It looks like we have an older 15-instrument MOD */
+    c->length = data[470];
+    c->reset = data[471];
+    c->order = &data[472];
+    c->patterns = &data[600];
+    c->num_samples = 15;
+    c->num_channels = 4;
+    return 1;
+}
+
+int pocketmod_init(pocketmod_context *c, const void *data, int size, int rate) {
+    int i, remaining, header_bytes, pattern_bytes;
+    unsigned char *byte = (unsigned char *)c;
+    signed char *sample_data;
+
+    /* Check that arguments look more or less sane */
+    if (!c || !data || rate <= 0 || size <= 0) {
+        return 0;
+    }
+
+    /* Zero out the whole context and identify the MOD type */
+    _pocketmod_zero(c, sizeof(pocketmod_context));
+    c->source = (unsigned char *)data;
+    if (!_pocketmod_ident(c, c->source, size)) {
+        return 0;
+    }
+
+    /* Check that we are compiled with support for enough channels */
+    if (c->num_channels > POCKETMOD_MAX_CHANNELS) {
+        return 0;
+    }
+
+    /* Check that we have enough sample slots for this file */
+    if (POCKETMOD_MAX_SAMPLES < 31) {
+        byte = (unsigned char *)data + 20;
+        for (i = 0; i < c->num_samples; i++) {
+            unsigned int length = 2 * ((byte[22] << 8) | byte[23]);
+            if (i >= POCKETMOD_MAX_SAMPLES && length > 2) {
+                return 0; /* Can't fit this sample */
+            }
+            byte += 30;
+        }
+    }
+
+    /* Check that the song length is in valid range (1..128) */
+    if (c->length == 0 || c->length > 128) {
+        return 0;
+    }
+
+    /* Make sure that the reset pattern doesn't take us out of bounds */
+    if (c->reset >= c->length) {
+        c->reset = 0;
+    }
+
+    /* Count how many patterns there are in the file */
+    c->num_patterns = 0;
+    for (i = 0; i < 128 && c->order[i] < 128; i++) {
+        c->num_patterns = _pocketmod_max(c->num_patterns, c->order[i]);
+    }
+    pattern_bytes = 256 * c->num_channels * ++c->num_patterns;
+    header_bytes = (int)((char *)c->patterns - (char *)data);
+
+    /* Check that each pattern in the order is within file bounds */
+    for (i = 0; i < c->length; i++) {
+        if (header_bytes + 256 * c->num_channels * c->order[i] > size) {
+            return 0; /* Reading this pattern would be a buffer over-read! */
+        }
+    }
+
+    /* Check that the pattern data doesn't extend past the end of the file */
+    if (header_bytes + pattern_bytes > size) {
+        return 0;
+    }
+
+    /* Load sample payload data, truncating ones that extend outside the file */
+    remaining = size - header_bytes - pattern_bytes;
+    sample_data = (signed char *)data + header_bytes + pattern_bytes;
+    for (i = 0; i < c->num_samples; i++) {
+        unsigned char *data = POCKETMOD_SAMPLE(c, i + 1);
+        unsigned int length = ((data[0] << 8) | data[1]) << 1;
+        _pocketmod_sample *sample = &c->samples[i];
+        sample->data = sample_data;
+        sample->length = _pocketmod_min(length > 2 ? length : 0, remaining);
+        sample_data += sample->length;
+        remaining -= sample->length;
+    }
+
+    /* Set up ProTracker default panning for all channels */
+    for (i = 0; i < c->num_channels; i++) {
+        c->channels[i].balance = 0x80 + ((((i + 1) >> 1) & 1) ? 0x20 : -0x20);
+    }
+
+    /* Prepare to render from the start */
+    c->ticks_per_line = 6;
+    c->samples_per_second = rate;
+    c->samples_per_tick = rate / 50.0f;
+    c->lfo_rng = 0xbadc0de;
+    c->line = -1;
+    c->tick = c->ticks_per_line - 1;
+    _pocketmod_next_tick(c);
+    return 1;
+}
+
+int pocketmod_render(pocketmod_context *c, void *buffer, int buffer_size) {
+    int i, samples_rendered = 0;
+    int samples_remaining = buffer_size / POCKETMOD_SAMPLE_SIZE;
+    if (c && buffer) {
+        float(*output)[2] = (float(*)[2])buffer;
+        while (samples_remaining > 0) {
+            /* Calculate the number of samples left in this tick */
+            int num = (int)(c->samples_per_tick - c->sample);
+            num = _pocketmod_min(num + !num, samples_remaining);
+
+            /* Render and mix 'num' samples from each channel */
+            _pocketmod_zero(output, num * POCKETMOD_SAMPLE_SIZE);
+            for (i = 0; i < c->num_channels; i++) {
+                _pocketmod_chan *chan = &c->channels[i];
+                if (chan->sample != 0 && chan->position >= 0.0f) {
+                    _pocketmod_render_channel(c, chan, *output, num);
+                }
+            }
+            samples_remaining -= num;
+            samples_rendered += num;
+            output += num;
+
+            /* Advance song position by 'num' samples */
+            if ((c->sample += num) >= c->samples_per_tick) {
+                c->sample -= c->samples_per_tick;
+                _pocketmod_next_tick(c);
+
+                /* Stop if a new pattern was reached */
+                if (c->line == 0 && c->tick == 0) {
+                    /* Increment loop counter as needed */
+                    if (c->visited[c->pattern >> 3] & (1 << (c->pattern & 7))) {
+                        _pocketmod_zero(c->visited, sizeof(c->visited));
+                        c->loop_count++;
+                    }
+                    break;
+                }
+            }
+        }
+    }
+    return samples_rendered * POCKETMOD_SAMPLE_SIZE;
+}
+
+int pocketmod_loop_count(pocketmod_context *c) { return c->loop_count; }
+
+#endif /* #ifdef POCKETMOD_IMPLEMENTATION */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* #ifndef POCKETMOD_H_INCLUDED */
+
+/*******************************************************************************
+
+MIT License
+
+Copyright (c) 2018 rombankzero
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+*******************************************************************************/
diff --git a/components/audio_mp3/CMakeLists.txt b/components/audio_mp3/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e64b76d64965c430c5e0e73fae8059e55b1c24fa
--- /dev/null
+++ b/components/audio_mp3/CMakeLists.txt
@@ -0,0 +1,8 @@
+idf_component_register(
+    SRCS
+        audio_mp3.c
+    INCLUDE_DIRS
+        .
+        ../ctx
+        ../st3m
+)
diff --git a/components/audio_mp3/LICENSE b/components/audio_mp3/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..2c4afabdb632b5d68e60c9fe9fe0b0819676a729
--- /dev/null
+++ b/components/audio_mp3/LICENSE
@@ -0,0 +1,117 @@
+CC0 1.0 Universal
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator and
+subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for the
+purpose of contributing to a commons of creative, cultural and scientific
+works ("Commons") that the public can reliably and without fear of later
+claims of infringement build upon, modify, incorporate in other works, reuse
+and redistribute as freely as possible in any form whatsoever and for any
+purposes, including without limitation commercial purposes. These owners may
+contribute to the Commons to promote the ideal of a free culture and the
+further production of creative, cultural and scientific works, or to gain
+reputation or greater distribution for their Work in part through the use and
+efforts of others.
+
+For these and/or other purposes and motivations, and without any expectation
+of additional consideration or compensation, the person associating CC0 with a
+Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
+and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
+and publicly distribute the Work under its terms, with knowledge of his or her
+Copyright and Related Rights in the Work and the meaning and intended legal
+effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not limited
+to, the following:
+
+  i. the right to reproduce, adapt, distribute, perform, display, communicate,
+  and translate a Work;
+
+  ii. moral rights retained by the original author(s) and/or performer(s);
+
+  iii. publicity and privacy rights pertaining to a person's image or likeness
+  depicted in a Work;
+
+  iv. rights protecting against unfair competition in regards to a Work,
+  subject to the limitations in paragraph 4(a), below;
+
+  v. rights protecting the extraction, dissemination, use and reuse of data in
+  a Work;
+
+  vi. database rights (such as those arising under Directive 96/9/EC of the
+  European Parliament and of the Council of 11 March 1996 on the legal
+  protection of databases, and under any national implementation thereof,
+  including any amended or successor version of such directive); and
+
+  vii. other similar, equivalent or corresponding rights throughout the world
+  based on applicable law or treaty, and any national implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention of,
+applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
+unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
+and Related Rights and associated claims and causes of action, whether now
+known or unknown (including existing as well as future claims and causes of
+action), in the Work (i) in all territories worldwide, (ii) for the maximum
+duration provided by applicable law or treaty (including future time
+extensions), (iii) in any current or future medium and for any number of
+copies, and (iv) for any purpose whatsoever, including without limitation
+commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
+the Waiver for the benefit of each member of the public at large and to the
+detriment of Affirmer's heirs and successors, fully intending that such Waiver
+shall not be subject to revocation, rescission, cancellation, termination, or
+any other legal or equitable action to disrupt the quiet enjoyment of the Work
+by the public as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason be
+judged legally invalid or ineffective under applicable law, then the Waiver
+shall be preserved to the maximum extent permitted taking into account
+Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
+is so judged Affirmer hereby grants to each affected person a royalty-free,
+non transferable, non sublicensable, non exclusive, irrevocable and
+unconditional license to exercise Affirmer's Copyright and Related Rights in
+the Work (i) in all territories worldwide, (ii) for the maximum duration
+provided by applicable law or treaty (including future time extensions), (iii)
+in any current or future medium and for any number of copies, and (iv) for any
+purpose whatsoever, including without limitation commercial, advertising or
+promotional purposes (the "License"). The License shall be deemed effective as
+of the date CC0 was applied by Affirmer to the Work. Should any part of the
+License for any reason be judged legally invalid or ineffective under
+applicable law, such partial invalidity or ineffectiveness shall not
+invalidate the remainder of the License, and in such case Affirmer hereby
+affirms that he or she will not (i) exercise any of his or her remaining
+Copyright and Related Rights in the Work or (ii) assert any associated claims
+and causes of action with respect to the Work, in either case contrary to
+Affirmer's express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+  a. No trademark or patent rights held by Affirmer are waived, abandoned,
+  surrendered, licensed or otherwise affected by this document.
+
+  b. Affirmer offers the Work as-is and makes no representations or warranties
+  of any kind concerning the Work, express, implied, statutory or otherwise,
+  including without limitation warranties of title, merchantability, fitness
+  for a particular purpose, non infringement, or the absence of latent or
+  other defects, accuracy, or the present or absence of errors, whether or not
+  discoverable, all to the greatest extent permissible under applicable law.
+
+  c. Affirmer disclaims responsibility for clearing rights of other persons
+  that may apply to the Work or any use thereof, including without limitation
+  any person's Copyright and Related Rights in the Work. Further, Affirmer
+  disclaims responsibility for obtaining any necessary consents, permissions
+  or other rights required for any use of the Work.
+
+  d. Affirmer understands and acknowledges that Creative Commons is not a
+  party to this document and has no duty or obligation with respect to this
+  CC0 or use of the Work.
+
+For more information, please see
+<http://creativecommons.org/publicdomain/zero/1.0/>
+
diff --git a/components/audio_mp3/audio_mp3.c b/components/audio_mp3/audio_mp3.c
new file mode 100644
index 0000000000000000000000000000000000000000..0d8993c7735c2276d6168b97f1826891fcf7bdd5
--- /dev/null
+++ b/components/audio_mp3/audio_mp3.c
@@ -0,0 +1,335 @@
+#ifndef __clang__
+#pragma GCC optimize("O2")
+#endif
+
+#include <fcntl.h>
+#include <st3m_audio.h>
+#include <st3m_media.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "lwip/sockets.h"
+#include "lwip/netdb.h"
+#include "lwip/ip4.h"
+#include "lwip/igmp.h"
+
+#include "ctx.h"
+
+//#define MINIMP3_ONLY_MP3
+#define MINIMP3_NONSTANDARD_BUT_LOGICAL
+#define MINIMP3_NO_SIMD
+#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 buffer_size;
+ 
+    int file_size;
+    FILE *file;
+
+    int socket;
+} mp3_state;
+
+static int has_data(mp3_state *mp3)
+{
+  fd_set rfds;
+  struct timeval tv = {0,0};
+  FD_ZERO(&rfds);
+  FD_SET(mp3->socket, &rfds);
+  if (select(mp3->socket+1, &rfds, NULL, NULL, &tv)==1)
+    return FD_ISSET(mp3->socket,&rfds);
+  return 0;
+}
+
+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;
+    }
+ 
+    if ((mp3->size - mp3->count > 0) && has_data(mp3))
+    {
+      int desire_bytes = (mp3->size-mp3->count);
+    if (desire_bytes)
+    {
+    int read_bytes;
+    if (mp3->file)
+      read_bytes = fread(mp3->data + mp3->count, 1, desire_bytes, mp3->file);
+    else
+      read_bytes = read (mp3->socket, mp3->data + mp3->count, desire_bytes);
+    mp3->count += read_bytes;
+    }
+    }
+}
+
+static void mp3_draw(st3m_media *media, Ctx *ctx) {
+    mp3_state *self = (void *)media;
+#if 1
+    ctx_rectangle(ctx, -120, -120, 240, 240);
+    ctx_gray(ctx, 0);
+    ctx_fill(ctx);
+    ctx_rgb(ctx, 1.0, 1.0, 1.0);
+    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);
+#endif
+}
+
+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;
+
+    int samples;
+    mp3dec_frame_info_t info;
+    if (samples_needed > 0 &&( (self->offset +512 < self->file_size) 
+                       || (!self->file))
+) 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;
+                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 {
+                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);
+}
+
+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);
+}
+
+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;
+    self->buffer_size = 32 * 1024;
+
+    printf ("%i\n", __LINE__);fflush(0);sleep(1);
+    if (!strncmp (path, "http://", 7))
+    {
+       int port = 80;
+       char *hostname = strdup (path + 7);
+       char *rest = NULL;
+       self->buffer_size = 64 * 1024;
+       rest = strchr(hostname, '/')+1;
+       strchr (hostname, '/')[0] = 0;
+       if (strchr (hostname, ':'))
+       {
+          port = atoi (strchr (hostname, ':')+1);
+          strchr (hostname, ':')[0] = 0;
+       }
+ 
+       printf (" {%s}  {%i} {%s}\n", hostname, port, rest);
+       fflush(0);sleep(1);
+
+       struct hostent *host;
+       struct sockaddr_in addr;
+       self->socket = socket (PF_INET, SOCK_STREAM, 0);
+       if (self->socket < 0)
+       {
+         free (hostname);
+         free (self);
+         return NULL;
+       }
+       memset (&addr, 0, sizeof (addr));
+       addr.sin_family = AF_INET;
+       addr.sin_port = htons (port);
+       host = gethostbyname (hostname);
+       if (!host) 
+       {
+         free (self);
+         free (hostname);
+         return NULL;
+       }
+       addr.sin_addr.s_addr = ((long unsigned int **) host->h_addr_list)[0][0];
+
+       if (connect (self->socket, (struct sockaddr*)&addr, sizeof(addr)) == 0)
+       {
+         char s[1024];
+         sprintf (s, "GET /%s HTTP/1.1\r\n", rest);
+         write (self->socket, s, strlen(s));
+         sprintf (s, "Range: bytes=0-\r\n");
+         write (self->socket, s, strlen(s));
+         if (hostname)
+         {
+            sprintf(s, "Host: %s\r\n", hostname);
+            write (self->socket, s, strlen(s));
+         }
+         sprintf(s, "User-Agent: flow3r\r\n");
+         write (self->socket, s, strlen(s));
+         sprintf(s, "\r\n");
+         write (self->socket, s, strlen(s));
+         fsync (self->socket);
+
+         self->data = malloc(self->buffer_size);
+         self->size = self->buffer_size;
+
+         // prebuffer a full fill - at least we get to play this without glitches
+         do {
+           int desire = self->size - self->count;
+           if (desire > 4096) desire = 4096;
+           int count = read (self->socket, self->data + self->count, desire);
+           self->count += count;
+         } while (self->count < self->size);
+
+         mp3dec_init(&self->mp3d);
+         self->control.duration = 1200;
+         free (hostname);
+         return (st3m_media*)self;
+       }
+       free (hostname);
+
+       free (self);
+       return NULL;
+    }
+
+    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(self->buffer_size);
+    self->size = self->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
new file mode 100644
index 0000000000000000000000000000000000000000..721e098eccbba66dbe850a74b75af601f0e9bdfd
--- /dev/null
+++ b/components/audio_mp3/minimp3.h
@@ -0,0 +1,2174 @@
+#ifndef MINIMP3_H
+#define MINIMP3_H
+/*
+    https://github.com/lieff/minimp3
+    To the extent possible under law, the author(s) have dedicated all copyright
+   and related and neighboring rights to this software to the public domain
+   worldwide. This software is distributed without any warranty. See
+   <http://creativecommons.org/publicdomain/zero/1.0/>.
+*/
+#include <stdint.h>
+
+#define MINIMP3_MAX_SAMPLES_PER_FRAME (1152 * 2)
+
+typedef struct {
+    int frame_bytes, frame_offset, channels, hz, layer, bitrate_kbps;
+} mp3dec_frame_info_t;
+
+typedef struct _mp3dec_scratch_t mp3dec_scratch_t;
+
+typedef struct {
+    const uint8_t *buf;
+    int pos, limit;
+} bs_t;
+
+typedef struct {
+    const uint8_t *sfbtab;
+    uint16_t part_23_length, big_values, scalefac_compress;
+    uint8_t global_gain, block_type, mixed_block_flag, n_long_sfb, n_short_sfb;
+    uint8_t table_select[3], region_count[3], subblock_gain[3];
+    uint8_t preflag, scalefac_scale, count1_table, scfsi;
+} L3_gr_info_t;
+
+typedef struct {
+    float scf[3 * 64];
+    uint8_t total_bands, stereo_bands, bitalloc[64], scfcod[64];
+} L12_scale_info;
+
+#define MAX_BITRESERVOIR_BYTES 511
+#define MAX_FREE_FORMAT_FRAME_SIZE 2304 /* more than ISO spec's */
+#define MAX_L3_FRAME_PAYLOAD_BYTES \
+    MAX_FREE_FORMAT_FRAME_SIZE /* MUST be >= 320000/8/32000*1152 = 1440 */
+
+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];
+    float grbuf[2][576], scf[45], syn[18 + 15][2 * 32];
+    uint8_t ist_pos[2][39];
+};
+
+typedef struct {
+    float mdct_overlap[2][9 * 32], qmf_state[15 * 2 * 32];
+    int reserv, free_format_bytes;
+    unsigned char header[4], reserv_buf[511];
+    mp3dec_scratch_t scratch;
+    L12_scale_info sci;
+} mp3dec_t;
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+void mp3dec_init(mp3dec_t *dec);
+#ifndef MINIMP3_FLOAT_OUTPUT
+typedef int16_t mp3d_sample_t;
+#else  /* MINIMP3_FLOAT_OUTPUT */
+typedef float mp3d_sample_t;
+void mp3dec_f32_to_s16(const float *in, int16_t *out, int num_samples);
+#endif /* MINIMP3_FLOAT_OUTPUT */
+int mp3dec_decode_frame(mp3dec_t *dec, const uint8_t *mp3, int mp3_bytes,
+                        mp3d_sample_t *pcm, mp3dec_frame_info_t *info);
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* MINIMP3_H */
+#if defined(MINIMP3_IMPLEMENTATION) && !defined(_MINIMP3_IMPLEMENTATION_GUARD)
+#define _MINIMP3_IMPLEMENTATION_GUARD
+
+#include <stdlib.h>
+#include <string.h>
+
+#ifndef MAX_FRAME_SYNC_MATCHES
+#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
+#define MODE_JOINT_STEREO 1
+#define HDR_SIZE 4
+#define HDR_IS_MONO(h) (((h[3]) & 0xC0) == 0xC0)
+#define HDR_IS_MS_STEREO(h) (((h[3]) & 0xE0) == 0x60)
+#define HDR_IS_FREE_FORMAT(h) (((h[2]) & 0xF0) == 0)
+#define HDR_IS_CRC(h) (!((h[1]) & 1))
+#define HDR_TEST_PADDING(h) ((h[2]) & 0x2)
+#define HDR_TEST_MPEG1(h) ((h[1]) & 0x8)
+#define HDR_TEST_NOT_MPEG25(h) ((h[1]) & 0x10)
+#define HDR_TEST_I_STEREO(h) ((h[3]) & 0x10)
+#define HDR_TEST_MS_STEREO(h) ((h[3]) & 0x20)
+#define HDR_GET_STEREO_MODE(h) (((h[3]) >> 6) & 3)
+#define HDR_GET_STEREO_MODE_EXT(h) (((h[3]) >> 4) & 3)
+#define HDR_GET_LAYER(h) (((h[1]) >> 1) & 3)
+#define HDR_GET_BITRATE(h) ((h[2]) >> 4)
+#define HDR_GET_SAMPLE_RATE(h) (((h[2]) >> 2) & 3)
+#define HDR_GET_MY_SAMPLE_RATE(h) \
+    (HDR_GET_SAMPLE_RATE(h) + (((h[1] >> 3) & 1) + ((h[1] >> 4) & 1)) * 3)
+#define HDR_IS_FRAME_576(h) ((h[1] & 14) == 2)
+#define HDR_IS_LAYER_1(h) ((h[1] & 6) == 6)
+
+#define BITS_DEQUANTIZER_OUT -1
+#define MAX_SCF (255 + BITS_DEQUANTIZER_OUT * 4 - 210)
+#define MAX_SCFI ((MAX_SCF + 3) & ~3)
+
+#define MINIMP3_MIN(a, b) ((a) > (b) ? (b) : (a))
+#define MINIMP3_MAX(a, b) ((a) < (b) ? (b) : (a))
+
+#if !defined(MINIMP3_NO_SIMD)
+
+#if !defined(MINIMP3_ONLY_SIMD) && (defined(_M_X64) || defined(__x86_64__) || \
+                                    defined(__aarch64__) || defined(_M_ARM64))
+/* x64 always have SSE2, arm64 always have neon, no need for generic code */
+#define MINIMP3_ONLY_SIMD
+#endif /* SIMD checks... */
+
+#if (defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64))) || \
+    ((defined(__i386__) || defined(__x86_64__)) && defined(__SSE2__))
+#if defined(_MSC_VER)
+#include <intrin.h>
+#endif /* defined(_MSC_VER) */
+#include <immintrin.h>
+#define HAVE_SSE 1
+#define HAVE_SIMD 1
+#define VSTORE _mm_storeu_ps
+#define VLD _mm_loadu_ps
+#define VSET _mm_set1_ps
+#define VADD _mm_add_ps
+#define VSUB _mm_sub_ps
+#define VMUL _mm_mul_ps
+#define VMAC(a, x, y) _mm_add_ps(a, _mm_mul_ps(x, y))
+#define VMSB(a, x, y) _mm_sub_ps(a, _mm_mul_ps(x, y))
+#define VMUL_S(x, s) _mm_mul_ps(x, _mm_set1_ps(s))
+#define VREV(x) _mm_shuffle_ps(x, x, _MM_SHUFFLE(0, 1, 2, 3))
+typedef __m128 f4;
+#if defined(_MSC_VER) || defined(MINIMP3_ONLY_SIMD)
+#define minimp3_cpuid __cpuid
+#else /* defined(_MSC_VER) || defined(MINIMP3_ONLY_SIMD) */
+static __inline__ __attribute__((always_inline)) void minimp3_cpuid(
+    int CPUInfo[], const int InfoType) {
+#if defined(__PIC__)
+    __asm__ __volatile__(
+#if defined(__x86_64__)
+        "push %%rbx\n"
+        "cpuid\n"
+        "xchgl %%ebx, %1\n"
+        "pop  %%rbx\n"
+#else  /* defined(__x86_64__) */
+        "xchgl %%ebx, %1\n"
+        "cpuid\n"
+        "xchgl %%ebx, %1\n"
+#endif /* defined(__x86_64__) */
+        : "=a"(CPUInfo[0]), "=r"(CPUInfo[1]), "=c"(CPUInfo[2]), "=d"(CPUInfo[3])
+        : "a"(InfoType));
+#else  /* defined(__PIC__) */
+    __asm__ __volatile__("cpuid"
+                         : "=a"(CPUInfo[0]), "=b"(CPUInfo[1]), "=c"(CPUInfo[2]),
+                           "=d"(CPUInfo[3])
+                         : "a"(InfoType));
+#endif /* defined(__PIC__)*/
+}
+#endif /* defined(_MSC_VER) || defined(MINIMP3_ONLY_SIMD) */
+static int have_simd(void) {
+#ifdef MINIMP3_ONLY_SIMD
+    return 1;
+#else /* MINIMP3_ONLY_SIMD */
+    static int g_have_simd;
+    int CPUInfo[4];
+#ifdef MINIMP3_TEST
+    static int g_counter;
+    if (g_counter++ > 100) return 0;
+#endif /* MINIMP3_TEST */
+    if (g_have_simd) goto end;
+    minimp3_cpuid(CPUInfo, 0);
+    g_have_simd = 1;
+    if (CPUInfo[0] > 0) {
+        minimp3_cpuid(CPUInfo, 1);
+        g_have_simd = (CPUInfo[3] & (1 << 26)) + 1; /* SSE2 */
+    }
+end:
+    return g_have_simd - 1;
+#endif /* MINIMP3_ONLY_SIMD */
+}
+#elif defined(__ARM_NEON) || defined(__aarch64__) || defined(_M_ARM64)
+#include <arm_neon.h>
+#define HAVE_SSE 0
+#define HAVE_SIMD 1
+#define VSTORE vst1q_f32
+#define VLD vld1q_f32
+#define VSET vmovq_n_f32
+#define VADD vaddq_f32
+#define VSUB vsubq_f32
+#define VMUL vmulq_f32
+#define VMAC(a, x, y) vmlaq_f32(a, x, y)
+#define VMSB(a, x, y) vmlsq_f32(a, x, y)
+#define VMUL_S(x, s) vmulq_f32(x, vmovq_n_f32(s))
+#define VREV(x) \
+    vcombine_f32(vget_high_f32(vrev64q_f32(x)), vget_low_f32(vrev64q_f32(x)))
+typedef float32x4_t f4;
+static int have_simd() { /* TODO: detect neon for !MINIMP3_ONLY_SIMD */
+    return 1;
+}
+#else /* SIMD checks... */
+#define HAVE_SSE 0
+#define HAVE_SIMD 0
+#ifdef MINIMP3_ONLY_SIMD
+#error MINIMP3_ONLY_SIMD used, but SSE/NEON not enabled
+#endif /* MINIMP3_ONLY_SIMD */
+#endif /* SIMD checks... */
+#else  /* !defined(MINIMP3_NO_SIMD) */
+#define HAVE_SIMD 0
+#endif /* !defined(MINIMP3_NO_SIMD) */
+
+#if defined(__ARM_ARCH) && (__ARM_ARCH >= 6) && !defined(__aarch64__) && \
+    !defined(_M_ARM64)
+#define HAVE_ARMV6 1
+static __inline__ __attribute__((always_inline)) int32_t minimp3_clip_int16_arm(
+    int32_t a) {
+    int32_t x = 0;
+    __asm__("ssat %0, #16, %1" : "=r"(x) : "r"(a));
+    return x;
+}
+#else
+#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;
+    bs->limit = bytes * 8;
+}
+
+static uint32_t get_bits(bs_t *bs, int n) {
+    uint32_t next, cache = 0, s = bs->pos & 7;
+    int shl = n + s;
+    const uint8_t *p = bs->buf + (bs->pos >> 3);
+    if ((bs->pos += n) > bs->limit) return 0;
+    next = *p++ & (255 >> s);
+    while ((shl -= 8) > 0) {
+        cache |= next << shl;
+        next = *p++;
+    }
+    return cache | (next >> -shl);
+}
+
+static int hdr_valid(const uint8_t *h) {
+    return h[0] == 0xff && ((h[1] & 0xF0) == 0xf0 || (h[1] & 0xFE) == 0xe2) &&
+           (HDR_GET_LAYER(h) != 0) && (HDR_GET_BITRATE(h) != 15) &&
+           (HDR_GET_SAMPLE_RATE(h) != 3);
+}
+
+static int hdr_compare(const uint8_t *h1, const uint8_t *h2) {
+    return hdr_valid(h2) && ((h1[1] ^ h2[1]) & 0xFE) == 0 &&
+           ((h1[2] ^ h2[2]) & 0x0C) == 0 &&
+           !(HDR_IS_FREE_FORMAT(h1) ^ HDR_IS_FREE_FORMAT(h2));
+}
+
+static unsigned hdr_bitrate_kbps(const uint8_t *h) {
+    static const uint8_t halfrate[2][3][15] = {
+        { { 0, 4, 8, 12, 16, 20, 24, 28, 32, 40, 48, 56, 64, 72, 80 },
+          { 0, 4, 8, 12, 16, 20, 24, 28, 32, 40, 48, 56, 64, 72, 80 },
+          { 0, 16, 24, 28, 32, 40, 48, 56, 64, 72, 80, 88, 96, 112, 128 } },
+        { { 0, 16, 20, 24, 28, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160 },
+          { 0, 16, 24, 28, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192 },
+          { 0, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208,
+            224 } },
+    };
+    return 2 * halfrate[!!HDR_TEST_MPEG1(h)][HDR_GET_LAYER(h) - 1]
+                       [HDR_GET_BITRATE(h)];
+}
+
+static unsigned hdr_sample_rate_hz(const uint8_t *h) {
+    static const unsigned g_hz[3] = { 44100, 48000, 32000 };
+    return g_hz[HDR_GET_SAMPLE_RATE(h)] >> (int)!HDR_TEST_MPEG1(h) >>
+           (int)!HDR_TEST_NOT_MPEG25(h);
+}
+
+static unsigned hdr_frame_samples(const uint8_t *h) {
+    return HDR_IS_LAYER_1(h) ? 384 : (1152 >> (int)HDR_IS_FRAME_576(h));
+}
+
+static int hdr_frame_bytes(const uint8_t *h, int free_format_size) {
+    int frame_bytes = hdr_frame_samples(h) * hdr_bitrate_kbps(h) * 125 /
+                      hdr_sample_rate_hz(h);
+    if (HDR_IS_LAYER_1(h)) {
+        frame_bytes &= ~3; /* slot align */
+    }
+    return frame_bytes ? frame_bytes : free_format_size;
+}
+
+static int hdr_padding(const uint8_t *h) {
+    return HDR_TEST_PADDING(h) ? (HDR_IS_LAYER_1(h) ? 4 : 1) : 0;
+}
+
+#ifndef MINIMP3_ONLY_MP3
+static const L12_subband_alloc_t *L12_subband_alloc_table(const uint8_t *hdr,
+                                                          L12_scale_info *sci) {
+    const L12_subband_alloc_t *alloc;
+    int mode = HDR_GET_STEREO_MODE(hdr);
+    int nbands, stereo_bands = (mode == MODE_MONO) ? 0
+                               : (mode == MODE_JOINT_STEREO)
+                                   ? (HDR_GET_STEREO_MODE_EXT(hdr) << 2) + 4
+                                   : 32;
+
+    if (HDR_IS_LAYER_1(hdr)) {
+        static const L12_subband_alloc_t g_alloc_L1[] = { { 76, 4, 32 } };
+        alloc = g_alloc_L1;
+        nbands = 32;
+    } else if (!HDR_TEST_MPEG1(hdr)) {
+        static const L12_subband_alloc_t g_alloc_L2M2[] = { { 60, 4, 4 },
+                                                            { 44, 3, 7 },
+                                                            { 44, 2, 19 } };
+        alloc = g_alloc_L2M2;
+        nbands = 30;
+    } else {
+        static const L12_subband_alloc_t g_alloc_L2M1[] = {
+            { 0, 4, 3 }, { 16, 4, 8 }, { 32, 3, 12 }, { 40, 2, 7 }
+        };
+        int sample_rate_idx = HDR_GET_SAMPLE_RATE(hdr);
+        unsigned kbps = hdr_bitrate_kbps(hdr) >> (int)(mode != MODE_MONO);
+        if (!kbps) /* free-format */
+        {
+            kbps = 192;
+        }
+
+        alloc = g_alloc_L2M1;
+        nbands = 27;
+        if (kbps < 56) {
+            static const L12_subband_alloc_t g_alloc_L2M1_lowrate[] = {
+                { 44, 4, 2 }, { 44, 3, 10 }
+            };
+            alloc = g_alloc_L2M1_lowrate;
+            nbands = sample_rate_idx == 2 ? 12 : 8;
+        } else if (kbps >= 96 && sample_rate_idx != 1) {
+            nbands = 30;
+        }
+    }
+
+    sci->total_bands = (uint8_t)nbands;
+    sci->stereo_bands = (uint8_t)MINIMP3_MIN(stereo_bands, nbands);
+
+    return alloc;
+}
+
+static void L12_read_scalefactors(bs_t *bs, uint8_t *pba, uint8_t *scfcod,
+                                  int bands, float *scf) {
+    static const float g_deq_L12[18 * 3] = {
+#define DQ(x) 9.53674316e-07f / x, 7.56931807e-07f / x, 6.00777173e-07f / x
+        DQ(3),     DQ(7),     DQ(15),    DQ(31),   DQ(63),   DQ(127),
+        DQ(255),   DQ(511),   DQ(1023),  DQ(2047), DQ(4095), DQ(8191),
+        DQ(16383), DQ(32767), DQ(65535), DQ(3),    DQ(5),    DQ(9)
+    };
+    int i, m;
+    for (i = 0; i < bands; i++) {
+        float s = 0;
+        int ba = *pba++;
+        int mask = ba ? 4 + ((19 >> scfcod[i]) & 3) : 0;
+        for (m = 4; m; m >>= 1) {
+            if (mask & m) {
+                int b = get_bits(bs, 6);
+                s = g_deq_L12[ba * 3 - 6 + b % 3] * (1 << 21 >> b / 3);
+            }
+            *scf++ = s;
+        }
+    }
+}
+
+static void L12_read_scale_info(const uint8_t *hdr, bs_t *bs,
+                                L12_scale_info *sci) {
+    static const uint8_t g_bitalloc_code_tab[] = {
+        0,  17, 3,  4, 5,  6,  7,  8,  9,  10, 11, 12, 13, 14, 15, 16,
+        0,  17, 18, 3, 19, 4,  5,  6,  7,  8,  9,  10, 11, 12, 13, 16,
+        0,  17, 18, 3, 19, 4,  5,  16, 0,  17, 18, 16, 0,  17, 18, 19,
+        4,  5,  6,  7, 8,  9,  10, 11, 12, 13, 14, 15, 0,  17, 18, 3,
+        19, 4,  5,  6, 7,  8,  9,  10, 11, 12, 13, 14, 0,  2,  3,  4,
+        5,  6,  7,  8, 9,  10, 11, 12, 13, 14, 15, 16
+    };
+    const L12_subband_alloc_t *subband_alloc =
+        L12_subband_alloc_table(hdr, sci);
+
+    int i, k = 0, ba_bits = 0;
+    const uint8_t *ba_code_tab = g_bitalloc_code_tab;
+
+    for (i = 0; i < sci->total_bands; i++) {
+        uint8_t ba;
+        if (i == k) {
+            k += subband_alloc->band_count;
+            ba_bits = subband_alloc->code_tab_width;
+            ba_code_tab = g_bitalloc_code_tab + subband_alloc->tab_offset;
+            subband_alloc++;
+        }
+        ba = ba_code_tab[get_bits(bs, ba_bits)];
+        sci->bitalloc[2 * i] = ba;
+        if (i < sci->stereo_bands) {
+            ba = ba_code_tab[get_bits(bs, ba_bits)];
+        }
+        sci->bitalloc[2 * i + 1] = sci->stereo_bands ? ba : 0;
+    }
+
+    for (i = 0; i < 2 * sci->total_bands; i++) {
+        sci->scfcod[i] =
+            sci->bitalloc[i] ? HDR_IS_LAYER_1(hdr) ? 2 : get_bits(bs, 2) : 6;
+    }
+
+    L12_read_scalefactors(bs, sci->bitalloc, sci->scfcod, sci->total_bands * 2,
+                          sci->scf);
+
+    for (i = sci->stereo_bands; i < sci->total_bands; i++) {
+        sci->bitalloc[2 * i + 1] = 0;
+    }
+}
+
+static int L12_dequantize_granule(float *grbuf, bs_t *bs, L12_scale_info *sci,
+                                  int group_size) {
+    int i, j, k, choff = 576;
+    for (j = 0; j < 4; j++) {
+        float *dst = grbuf + group_size * j;
+        for (i = 0; i < 2 * sci->total_bands; i++) {
+            int ba = sci->bitalloc[i];
+            if (ba != 0) {
+                if (ba < 17) {
+                    int half = (1 << (ba - 1)) - 1;
+                    for (k = 0; k < group_size; k++) {
+                        dst[k] = (float)((int)get_bits(bs, ba) - half);
+                    }
+                } else {
+                    unsigned mod = (2 << (ba - 17)) + 1; /* 3, 5, 9 */
+                    unsigned code =
+                        get_bits(bs, mod + 2 - (mod >> 3)); /* 5, 7, 10 */
+                    for (k = 0; k < group_size; k++, code /= mod) {
+                        dst[k] = (float)((int)(code % mod - mod / 2));
+                    }
+                }
+            }
+            dst += choff;
+            choff = 18 - choff;
+        }
+    }
+    return group_size * 4;
+}
+
+static void L12_apply_scf_384(L12_scale_info *sci, const float *scf,
+                              float *dst) {
+    int i, k;
+    memcpy(dst + 576 + sci->stereo_bands * 18, dst + sci->stereo_bands * 18,
+           (sci->total_bands - sci->stereo_bands) * 18 * sizeof(float));
+    for (i = 0; i < sci->total_bands; i++, dst += 18, scf += 6) {
+        for (k = 0; k < 12; k++) {
+            dst[k + 0] *= scf[0];
+            dst[k + 576] *= scf[3];
+        }
+    }
+}
+#endif /* MINIMP3_ONLY_MP3 */
+
+static int L3_read_side_info(bs_t *bs, L3_gr_info_t *gr, const uint8_t *hdr) {
+    static const uint8_t g_scf_long[8][23] = {
+        { 6,  6,  6,  6,  6,  6,  8,  10, 12, 14, 16, 20,
+          24, 28, 32, 38, 46, 52, 60, 68, 58, 54, 0 },
+        { 12, 12, 12, 12, 12, 12, 16, 20, 24, 28, 32, 40,
+          48, 56, 64, 76, 90, 2,  2,  2,  2,  2,  0 },
+        { 6,  6,  6,  6,  6,  6,  8,  10, 12, 14, 16, 20,
+          24, 28, 32, 38, 46, 52, 60, 68, 58, 54, 0 },
+        { 6,  6,  6,  6,  6,  6,  8,  10, 12, 14, 16, 18,
+          22, 26, 32, 38, 46, 54, 62, 70, 76, 36, 0 },
+        { 6,  6,  6,  6,  6,  6,  8,  10, 12, 14, 16, 20,
+          24, 28, 32, 38, 46, 52, 60, 68, 58, 54, 0 },
+        { 4,  4,  4,  4,  4,  4,  6,  6,  8,  8,   10, 12,
+          16, 20, 24, 28, 34, 42, 50, 54, 76, 158, 0 },
+        { 4,  4,  4,  4,  4,  4,  6,  6,  6,  8,   10, 12,
+          16, 18, 22, 28, 34, 40, 46, 54, 54, 192, 0 },
+        { 4,  4,  4,  4,  4,  4,  6,  6,  8,   10, 12, 16,
+          20, 24, 30, 38, 46, 56, 68, 84, 102, 26, 0 }
+    };
+    static const uint8_t g_scf_short[8][40] = {
+        { 4,  4,  4,  4,  4,  4,  4,  4,  4,  6,  6,  6,  8,  8,
+          8,  10, 10, 10, 12, 12, 12, 14, 14, 14, 18, 18, 18, 24,
+          24, 24, 30, 30, 30, 40, 40, 40, 18, 18, 18, 0 },
+        { 8,  8,  8,  8,  8,  8,  8,  8,  8,  12, 12, 12, 16, 16,
+          16, 20, 20, 20, 24, 24, 24, 28, 28, 28, 36, 36, 36, 2,
+          2,  2,  2,  2,  2,  2,  2,  2,  26, 26, 26, 0 },
+        { 4,  4,  4,  4,  4,  4,  4,  4,  4,  6,  6,  6,  6,  6,
+          6,  8,  8,  8,  10, 10, 10, 14, 14, 14, 18, 18, 18, 26,
+          26, 26, 32, 32, 32, 42, 42, 42, 18, 18, 18, 0 },
+        { 4,  4,  4,  4,  4,  4,  4,  4,  4,  6,  6,  6,  8,  8,
+          8,  10, 10, 10, 12, 12, 12, 14, 14, 14, 18, 18, 18, 24,
+          24, 24, 32, 32, 32, 44, 44, 44, 12, 12, 12, 0 },
+        { 4,  4,  4,  4,  4,  4,  4,  4,  4,  6,  6,  6,  8,  8,
+          8,  10, 10, 10, 12, 12, 12, 14, 14, 14, 18, 18, 18, 24,
+          24, 24, 30, 30, 30, 40, 40, 40, 18, 18, 18, 0 },
+        { 4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  6,  6,
+          6,  8,  8,  8,  10, 10, 10, 12, 12, 12, 14, 14, 14, 18,
+          18, 18, 22, 22, 22, 30, 30, 30, 56, 56, 56, 0 },
+        { 4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  6,  6,
+          6,  6,  6,  6,  10, 10, 10, 12, 12, 12, 14, 14, 14, 16,
+          16, 16, 20, 20, 20, 26, 26, 26, 66, 66, 66, 0 },
+        { 4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  6,  6,
+          6,  8,  8,  8,  12, 12, 12, 16, 16, 16, 20, 20, 20, 26,
+          26, 26, 34, 34, 34, 42, 42, 42, 12, 12, 12, 0 }
+    };
+    static const uint8_t g_scf_mixed[8][40] = {
+        { 6,  6,  6,  6,  6,  6,  6,  6,  6,  8,  8,  8,  10,
+          10, 10, 12, 12, 12, 14, 14, 14, 18, 18, 18, 24, 24,
+          24, 30, 30, 30, 40, 40, 40, 18, 18, 18, 0 },
+        { 12, 12, 12, 4,  4,  4,  8,  8,  8,  12, 12, 12, 16, 16,
+          16, 20, 20, 20, 24, 24, 24, 28, 28, 28, 36, 36, 36, 2,
+          2,  2,  2,  2,  2,  2,  2,  2,  26, 26, 26, 0 },
+        { 6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  8,
+          8,  8,  10, 10, 10, 14, 14, 14, 18, 18, 18, 26, 26,
+          26, 32, 32, 32, 42, 42, 42, 18, 18, 18, 0 },
+        { 6,  6,  6,  6,  6,  6,  6,  6,  6,  8,  8,  8,  10,
+          10, 10, 12, 12, 12, 14, 14, 14, 18, 18, 18, 24, 24,
+          24, 32, 32, 32, 44, 44, 44, 12, 12, 12, 0 },
+        { 6,  6,  6,  6,  6,  6,  6,  6,  6,  8,  8,  8,  10,
+          10, 10, 12, 12, 12, 14, 14, 14, 18, 18, 18, 24, 24,
+          24, 30, 30, 30, 40, 40, 40, 18, 18, 18, 0 },
+        { 4,  4,  4,  4,  4,  4,  6,  6,  4,  4,  4,  6,  6,
+          6,  8,  8,  8,  10, 10, 10, 12, 12, 12, 14, 14, 14,
+          18, 18, 18, 22, 22, 22, 30, 30, 30, 56, 56, 56, 0 },
+        { 4,  4,  4,  4,  4,  4,  6,  6,  4,  4,  4,  6,  6,
+          6,  6,  6,  6,  10, 10, 10, 12, 12, 12, 14, 14, 14,
+          16, 16, 16, 20, 20, 20, 26, 26, 26, 66, 66, 66, 0 },
+        { 4,  4,  4,  4,  4,  4,  6,  6,  4,  4,  4,  6,  6,
+          6,  8,  8,  8,  12, 12, 12, 16, 16, 16, 20, 20, 20,
+          26, 26, 26, 34, 34, 34, 42, 42, 42, 12, 12, 12, 0 }
+    };
+
+    unsigned tables, scfsi = 0;
+    int main_data_begin, part_23_sum = 0;
+    int sr_idx = HDR_GET_MY_SAMPLE_RATE(hdr);
+    sr_idx -= (sr_idx != 0);
+    int gr_count = HDR_IS_MONO(hdr) ? 1 : 2;
+
+    if (HDR_TEST_MPEG1(hdr)) {
+        gr_count *= 2;
+        main_data_begin = get_bits(bs, 9);
+        scfsi = get_bits(bs, 7 + gr_count);
+    } else {
+        main_data_begin = get_bits(bs, 8 + gr_count) >> gr_count;
+    }
+
+    do {
+        if (HDR_IS_MONO(hdr)) {
+            scfsi <<= 4;
+        }
+        gr->part_23_length = (uint16_t)get_bits(bs, 12);
+        part_23_sum += gr->part_23_length;
+        gr->big_values = (uint16_t)get_bits(bs, 9);
+        if (gr->big_values > 288) {
+            return -1;
+        }
+        gr->global_gain = (uint8_t)get_bits(bs, 8);
+        gr->scalefac_compress =
+            (uint16_t)get_bits(bs, HDR_TEST_MPEG1(hdr) ? 4 : 9);
+        gr->sfbtab = g_scf_long[sr_idx];
+        gr->n_long_sfb = 22;
+        gr->n_short_sfb = 0;
+        if (get_bits(bs, 1)) {
+            gr->block_type = (uint8_t)get_bits(bs, 2);
+            if (!gr->block_type) {
+                return -1;
+            }
+            gr->mixed_block_flag = (uint8_t)get_bits(bs, 1);
+            gr->region_count[0] = 7;
+            gr->region_count[1] = 255;
+            if (gr->block_type == SHORT_BLOCK_TYPE) {
+                scfsi &= 0x0F0F;
+                if (!gr->mixed_block_flag) {
+                    gr->region_count[0] = 8;
+                    gr->sfbtab = g_scf_short[sr_idx];
+                    gr->n_long_sfb = 0;
+                    gr->n_short_sfb = 39;
+                } else {
+                    gr->sfbtab = g_scf_mixed[sr_idx];
+                    gr->n_long_sfb = HDR_TEST_MPEG1(hdr) ? 8 : 6;
+                    gr->n_short_sfb = 30;
+                }
+            }
+            tables = get_bits(bs, 10);
+            tables <<= 5;
+            gr->subblock_gain[0] = (uint8_t)get_bits(bs, 3);
+            gr->subblock_gain[1] = (uint8_t)get_bits(bs, 3);
+            gr->subblock_gain[2] = (uint8_t)get_bits(bs, 3);
+        } else {
+            gr->block_type = 0;
+            gr->mixed_block_flag = 0;
+            tables = get_bits(bs, 15);
+            gr->region_count[0] = (uint8_t)get_bits(bs, 4);
+            gr->region_count[1] = (uint8_t)get_bits(bs, 3);
+            gr->region_count[2] = 255;
+        }
+        gr->table_select[0] = (uint8_t)(tables >> 10);
+        gr->table_select[1] = (uint8_t)((tables >> 5) & 31);
+        gr->table_select[2] = (uint8_t)((tables)&31);
+        gr->preflag = HDR_TEST_MPEG1(hdr) ? get_bits(bs, 1)
+                                          : (gr->scalefac_compress >= 500);
+        gr->scalefac_scale = (uint8_t)get_bits(bs, 1);
+        gr->count1_table = (uint8_t)get_bits(bs, 1);
+        gr->scfsi = (uint8_t)((scfsi >> 12) & 15);
+        scfsi <<= 4;
+        gr++;
+    } while (--gr_count);
+
+    if (part_23_sum + bs->pos > bs->limit + main_data_begin * 8) {
+        return -1;
+    }
+
+    return main_data_begin;
+}
+
+static void L3_read_scalefactors(uint8_t *scf, uint8_t *ist_pos,
+                                 const uint8_t *scf_size,
+                                 const uint8_t *scf_count, bs_t *bitbuf,
+                                 int scfsi) {
+    int i, k;
+    for (i = 0; i < 4 && scf_count[i]; i++, scfsi *= 2) {
+        int cnt = scf_count[i];
+        if (scfsi & 8) {
+            memcpy(scf, ist_pos, cnt);
+        } else {
+            int bits = scf_size[i];
+            if (!bits) {
+                memset(scf, 0, cnt);
+                memset(ist_pos, 0, cnt);
+            } else {
+                int max_scf = (scfsi < 0) ? (1 << bits) - 1 : -1;
+                for (k = 0; k < cnt; k++) {
+                    int s = get_bits(bitbuf, bits);
+                    ist_pos[k] = (s == max_scf ? -1 : s);
+                    scf[k] = s;
+                }
+            }
+        }
+        ist_pos += cnt;
+        scf += cnt;
+    }
+    scf[0] = scf[1] = scf[2] = 0;
+}
+
+static float L3_ldexp_q2(float y, int exp_q2) {
+    static const float g_expfrac[4] = { 9.31322575e-10f, 7.83145814e-10f,
+                                        6.58544508e-10f, 5.53767716e-10f };
+    int e;
+    do {
+        e = MINIMP3_MIN(30 * 4, exp_q2);
+        y *= g_expfrac[e & 3] * (1 << 30 >> (e >> 2));
+    } while ((exp_q2 -= e) > 0);
+    return y;
+}
+
+static void L3_decode_scalefactors(const uint8_t *hdr, uint8_t *ist_pos,
+                                   bs_t *bs, const L3_gr_info_t *gr, float *scf,
+                                   int ch) {
+    static const uint8_t g_scf_partitions[3][28] = {
+        { 6, 5, 5, 5, 6, 5, 5, 5, 6, 5, 7, 3, 11, 10,
+          0, 0, 7, 7, 7, 0, 6, 6, 6, 3, 8, 8, 5,  0 },
+        { 8, 9, 6, 12, 6,  9, 9, 9,  6, 9, 12, 6,  15, 18,
+          0, 0, 6, 15, 12, 0, 6, 12, 9, 6, 6,  18, 9,  0 },
+        { 9, 9, 6,  12, 9,  9, 9,  9, 9, 9, 12, 6,  18, 18,
+          0, 0, 12, 12, 12, 0, 12, 9, 9, 6, 15, 12, 9,  0 }
+    };
+    const uint8_t *scf_partition =
+        g_scf_partitions[!!gr->n_short_sfb + !gr->n_long_sfb];
+    uint8_t scf_size[4], iscf[40];
+    int i, scf_shift = gr->scalefac_scale + 1, gain_exp, scfsi = gr->scfsi;
+    float gain;
+
+    if (HDR_TEST_MPEG1(hdr)) {
+        static const uint8_t g_scfc_decode[16] = {
+            0, 1, 2, 3, 12, 5, 6, 7, 9, 10, 11, 13, 14, 15, 18, 19
+        };
+        int part = g_scfc_decode[gr->scalefac_compress];
+        scf_size[1] = scf_size[0] = (uint8_t)(part >> 2);
+        scf_size[3] = scf_size[2] = (uint8_t)(part & 3);
+    } else {
+        static const uint8_t g_mod[6 * 4] = { 5, 5, 4, 4, 5, 5, 4, 1,
+                                              4, 3, 1, 1, 5, 6, 6, 1,
+                                              4, 4, 4, 1, 4, 3, 1, 1 };
+        int k, modprod, sfc, ist = HDR_TEST_I_STEREO(hdr) && ch;
+        sfc = gr->scalefac_compress >> ist;
+        for (k = ist * 3 * 4; sfc >= 0; sfc -= modprod, k += 4) {
+            for (modprod = 1, i = 3; i >= 0; i--) {
+                scf_size[i] = (uint8_t)(sfc / modprod % g_mod[k + i]);
+                modprod *= g_mod[k + i];
+            }
+        }
+        scf_partition += k;
+        scfsi = -16;
+    }
+    L3_read_scalefactors(iscf, ist_pos, scf_size, scf_partition, bs, scfsi);
+
+    if (gr->n_short_sfb) {
+        int sh = 3 - scf_shift;
+        for (i = 0; i < gr->n_short_sfb; i += 3) {
+            iscf[gr->n_long_sfb + i + 0] += gr->subblock_gain[0] << sh;
+            iscf[gr->n_long_sfb + i + 1] += gr->subblock_gain[1] << sh;
+            iscf[gr->n_long_sfb + i + 2] += gr->subblock_gain[2] << sh;
+        }
+    } else if (gr->preflag) {
+        static const uint8_t g_preamp[10] = { 1, 1, 1, 1, 2, 2, 3, 3, 3, 2 };
+        for (i = 0; i < 10; i++) {
+            iscf[11 + i] += g_preamp[i];
+        }
+    }
+
+    gain_exp = gr->global_gain + BITS_DEQUANTIZER_OUT * 4 - 210 -
+               (HDR_IS_MS_STEREO(hdr) ? 2 : 0);
+    gain = L3_ldexp_q2(1 << (MAX_SCFI / 4), MAX_SCFI - gain_exp);
+    for (i = 0; i < (int)(gr->n_long_sfb + gr->n_short_sfb); i++) {
+        scf[i] = L3_ldexp_q2(gain, iscf[i] << scf_shift);
+    }
+}
+
+static const float g_pow43[129 + 16] = {
+    0,           -1,          -2.519842f,  -4.326749f,  -6.349604f,
+    -8.549880f,  -10.902724f, -13.390518f, -16.000000f, -18.720754f,
+    -21.544347f, -24.463781f, -27.473142f, -30.567351f, -33.741992f,
+    -36.993181f, 0,           1,           2.519842f,   4.326749f,
+    6.349604f,   8.549880f,   10.902724f,  13.390518f,  16.000000f,
+    18.720754f,  21.544347f,  24.463781f,  27.473142f,  30.567351f,
+    33.741992f,  36.993181f,  40.317474f,  43.711787f,  47.173345f,
+    50.699631f,  54.288352f,  57.937408f,  61.644865f,  65.408941f,
+    69.227979f,  73.100443f,  77.024898f,  81.000000f,  85.024491f,
+    89.097188f,  93.216975f,  97.382800f,  101.593667f, 105.848633f,
+    110.146801f, 114.487321f, 118.869381f, 123.292209f, 127.755065f,
+    132.257246f, 136.798076f, 141.376907f, 145.993119f, 150.646117f,
+    155.335327f, 160.060199f, 164.820202f, 169.614826f, 174.443577f,
+    179.305980f, 184.201575f, 189.129918f, 194.090580f, 199.083145f,
+    204.107210f, 209.162385f, 214.248292f, 219.364564f, 224.510845f,
+    229.686789f, 234.892058f, 240.126328f, 245.389280f, 250.680604f,
+    256.000000f, 261.347174f, 266.721841f, 272.123723f, 277.552547f,
+    283.008049f, 288.489971f, 293.998060f, 299.532071f, 305.091761f,
+    310.676898f, 316.287249f, 321.922592f, 327.582707f, 333.267377f,
+    338.976394f, 344.709550f, 350.466646f, 356.247482f, 362.051866f,
+    367.879608f, 373.730522f, 379.604427f, 385.501143f, 391.420496f,
+    397.362314f, 403.326427f, 409.312672f, 415.320884f, 421.350905f,
+    427.402579f, 433.475750f, 439.570269f, 445.685987f, 451.822757f,
+    457.980436f, 464.158883f, 470.357960f, 476.577530f, 482.817459f,
+    489.077615f, 495.357868f, 501.658090f, 507.978156f, 514.317941f,
+    520.677324f, 527.056184f, 533.454404f, 539.871867f, 546.308458f,
+    552.764065f, 559.238575f, 565.731879f, 572.243870f, 578.774440f,
+    585.323483f, 591.890898f, 598.476581f, 605.080431f, 611.702349f,
+    618.342238f, 625.000000f, 631.675540f, 638.368763f, 645.079578f
+};
+
+static float L3_pow_43(int x) {
+    float frac;
+    int sign, mult = 256;
+
+    if (x < 129) {
+        return g_pow43[16 + x];
+    }
+
+    if (x < 1024) {
+        mult = 16;
+        x <<= 3;
+    }
+
+    sign = 2 * x & 64;
+    frac = (float)((x & 63) - sign) / ((x & ~63) + sign);
+    return g_pow43[16 + ((x + sign) >> 6)] *
+           (1.f + frac * ((4.f / 3) + frac * (2.f / 9))) * mult;
+}
+
+static void L3_huffman(float *dst, bs_t *bs, const L3_gr_info_t *gr_info,
+                       const float *scf, int layer3gr_limit) {
+    static const int16_t tabs[] = {
+        0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
+        0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
+        0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
+        0,     0,     785,   785,   785,   785,   784,   784,   784,   784,
+        513,   513,   513,   513,   513,   513,   513,   513,   256,   256,
+        256,   256,   256,   256,   256,   256,   256,   256,   256,   256,
+        256,   256,   256,   256,   -255,  1313,  1298,  1282,  785,   785,
+        785,   785,   784,   784,   784,   784,   769,   769,   769,   769,
+        256,   256,   256,   256,   256,   256,   256,   256,   256,   256,
+        256,   256,   256,   256,   256,   256,   290,   288,   -255,  1313,
+        1298,  1282,  769,   769,   769,   769,   529,   529,   529,   529,
+        529,   529,   529,   529,   528,   528,   528,   528,   528,   528,
+        528,   528,   512,   512,   512,   512,   512,   512,   512,   512,
+        290,   288,   -253,  -318,  -351,  -367,  785,   785,   785,   785,
+        784,   784,   784,   784,   769,   769,   769,   769,   256,   256,
+        256,   256,   256,   256,   256,   256,   256,   256,   256,   256,
+        256,   256,   256,   256,   819,   818,   547,   547,   275,   275,
+        275,   275,   561,   560,   515,   546,   289,   274,   288,   258,
+        -254,  -287,  1329,  1299,  1314,  1312,  1057,  1057,  1042,  1042,
+        1026,  1026,  784,   784,   784,   784,   529,   529,   529,   529,
+        529,   529,   529,   529,   769,   769,   769,   769,   768,   768,
+        768,   768,   563,   560,   306,   306,   291,   259,   -252,  -413,
+        -477,  -542,  1298,  -575,  1041,  1041,  784,   784,   784,   784,
+        769,   769,   769,   769,   256,   256,   256,   256,   256,   256,
+        256,   256,   256,   256,   256,   256,   256,   256,   256,   256,
+        -383,  -399,  1107,  1092,  1106,  1061,  849,   849,   789,   789,
+        1104,  1091,  773,   773,   1076,  1075,  341,   340,   325,   309,
+        834,   804,   577,   577,   532,   532,   516,   516,   832,   818,
+        803,   816,   561,   561,   531,   531,   515,   546,   289,   289,
+        288,   258,   -252,  -429,  -493,  -559,  1057,  1057,  1042,  1042,
+        529,   529,   529,   529,   529,   529,   529,   529,   784,   784,
+        784,   784,   769,   769,   769,   769,   512,   512,   512,   512,
+        512,   512,   512,   512,   -382,  1077,  -415,  1106,  1061,  1104,
+        849,   849,   789,   789,   1091,  1076,  1029,  1075,  834,   834,
+        597,   581,   340,   340,   339,   324,   804,   833,   532,   532,
+        832,   772,   818,   803,   817,   787,   816,   771,   290,   290,
+        290,   290,   288,   258,   -253,  -349,  -414,  -447,  -463,  1329,
+        1299,  -479,  1314,  1312,  1057,  1057,  1042,  1042,  1026,  1026,
+        785,   785,   785,   785,   784,   784,   784,   784,   769,   769,
+        769,   769,   768,   768,   768,   768,   -319,  851,   821,   -335,
+        836,   850,   805,   849,   341,   340,   325,   336,   533,   533,
+        579,   579,   564,   564,   773,   832,   578,   548,   563,   516,
+        321,   276,   306,   291,   304,   259,   -251,  -572,  -733,  -830,
+        -863,  -879,  1041,  1041,  784,   784,   784,   784,   769,   769,
+        769,   769,   256,   256,   256,   256,   256,   256,   256,   256,
+        256,   256,   256,   256,   256,   256,   256,   256,   -511,  -527,
+        -543,  1396,  1351,  1381,  1366,  1395,  1335,  1380,  -559,  1334,
+        1138,  1138,  1063,  1063,  1350,  1392,  1031,  1031,  1062,  1062,
+        1364,  1363,  1120,  1120,  1333,  1348,  881,   881,   881,   881,
+        375,   374,   359,   373,   343,   358,   341,   325,   791,   791,
+        1123,  1122,  -703,  1105,  1045,  -719,  865,   865,   790,   790,
+        774,   774,   1104,  1029,  338,   293,   323,   308,   -799,  -815,
+        833,   788,   772,   818,   803,   816,   322,   292,   307,   320,
+        561,   531,   515,   546,   289,   274,   288,   258,   -251,  -525,
+        -605,  -685,  -765,  -831,  -846,  1298,  1057,  1057,  1312,  1282,
+        785,   785,   785,   785,   784,   784,   784,   784,   769,   769,
+        769,   769,   512,   512,   512,   512,   512,   512,   512,   512,
+        1399,  1398,  1383,  1367,  1382,  1396,  1351,  -511,  1381,  1366,
+        1139,  1139,  1079,  1079,  1124,  1124,  1364,  1349,  1363,  1333,
+        882,   882,   882,   882,   807,   807,   807,   807,   1094,  1094,
+        1136,  1136,  373,   341,   535,   535,   881,   775,   867,   822,
+        774,   -591,  324,   338,   -671,  849,   550,   550,   866,   864,
+        609,   609,   293,   336,   534,   534,   789,   835,   773,   -751,
+        834,   804,   308,   307,   833,   788,   832,   772,   562,   562,
+        547,   547,   305,   275,   560,   515,   290,   290,   -252,  -397,
+        -477,  -557,  -622,  -653,  -719,  -735,  -750,  1329,  1299,  1314,
+        1057,  1057,  1042,  1042,  1312,  1282,  1024,  1024,  785,   785,
+        785,   785,   784,   784,   784,   784,   769,   769,   769,   769,
+        -383,  1127,  1141,  1111,  1126,  1140,  1095,  1110,  869,   869,
+        883,   883,   1079,  1109,  882,   882,   375,   374,   807,   868,
+        838,   881,   791,   -463,  867,   822,   368,   263,   852,   837,
+        836,   -543,  610,   610,   550,   550,   352,   336,   534,   534,
+        865,   774,   851,   821,   850,   805,   593,   533,   579,   564,
+        773,   832,   578,   578,   548,   548,   577,   577,   307,   276,
+        306,   291,   516,   560,   259,   259,   -250,  -2107, -2507, -2764,
+        -2909, -2974, -3007, -3023, 1041,  1041,  1040,  1040,  769,   769,
+        769,   769,   256,   256,   256,   256,   256,   256,   256,   256,
+        256,   256,   256,   256,   256,   256,   256,   256,   -767,  -1052,
+        -1213, -1277, -1358, -1405, -1469, -1535, -1550, -1582, -1614, -1647,
+        -1662, -1694, -1726, -1759, -1774, -1807, -1822, -1854, -1886, 1565,
+        -1919, -1935, -1951, -1967, 1731,  1730,  1580,  1717,  -1983, 1729,
+        1564,  -1999, 1548,  -2015, -2031, 1715,  1595,  -2047, 1714,  -2063,
+        1610,  -2079, 1609,  -2095, 1323,  1323,  1457,  1457,  1307,  1307,
+        1712,  1547,  1641,  1700,  1699,  1594,  1685,  1625,  1442,  1442,
+        1322,  1322,  -780,  -973,  -910,  1279,  1278,  1277,  1262,  1276,
+        1261,  1275,  1215,  1260,  1229,  -959,  974,   974,   989,   989,
+        -943,  735,   478,   478,   495,   463,   506,   414,   -1039, 1003,
+        958,   1017,  927,   942,   987,   957,   431,   476,   1272,  1167,
+        1228,  -1183, 1256,  -1199, 895,   895,   941,   941,   1242,  1227,
+        1212,  1135,  1014,  1014,  490,   489,   503,   487,   910,   1013,
+        985,   925,   863,   894,   970,   955,   1012,  847,   -1343, 831,
+        755,   755,   984,   909,   428,   366,   754,   559,   -1391, 752,
+        486,   457,   924,   997,   698,   698,   983,   893,   740,   740,
+        908,   877,   739,   739,   667,   667,   953,   938,   497,   287,
+        271,   271,   683,   606,   590,   712,   726,   574,   302,   302,
+        738,   736,   481,   286,   526,   725,   605,   711,   636,   724,
+        696,   651,   589,   681,   666,   710,   364,   467,   573,   695,
+        466,   466,   301,   465,   379,   379,   709,   604,   665,   679,
+        316,   316,   634,   633,   436,   436,   464,   269,   424,   394,
+        452,   332,   438,   363,   347,   408,   393,   448,   331,   422,
+        362,   407,   392,   421,   346,   406,   391,   376,   375,   359,
+        1441,  1306,  -2367, 1290,  -2383, 1337,  -2399, -2415, 1426,  1321,
+        -2431, 1411,  1336,  -2447, -2463, -2479, 1169,  1169,  1049,  1049,
+        1424,  1289,  1412,  1352,  1319,  -2495, 1154,  1154,  1064,  1064,
+        1153,  1153,  416,   390,   360,   404,   403,   389,   344,   374,
+        373,   343,   358,   372,   327,   357,   342,   311,   356,   326,
+        1395,  1394,  1137,  1137,  1047,  1047,  1365,  1392,  1287,  1379,
+        1334,  1364,  1349,  1378,  1318,  1363,  792,   792,   792,   792,
+        1152,  1152,  1032,  1032,  1121,  1121,  1046,  1046,  1120,  1120,
+        1030,  1030,  -2895, 1106,  1061,  1104,  849,   849,   789,   789,
+        1091,  1076,  1029,  1090,  1060,  1075,  833,   833,   309,   324,
+        532,   532,   832,   772,   818,   803,   561,   561,   531,   560,
+        515,   546,   289,   274,   288,   258,   -250,  -1179, -1579, -1836,
+        -1996, -2124, -2253, -2333, -2413, -2477, -2542, -2574, -2607, -2622,
+        -2655, 1314,  1313,  1298,  1312,  1282,  785,   785,   785,   785,
+        1040,  1040,  1025,  1025,  768,   768,   768,   768,   -766,  -798,
+        -830,  -862,  -895,  -911,  -927,  -943,  -959,  -975,  -991,  -1007,
+        -1023, -1039, -1055, -1070, 1724,  1647,  -1103, -1119, 1631,  1767,
+        1662,  1738,  1708,  1723,  -1135, 1780,  1615,  1779,  1599,  1677,
+        1646,  1778,  1583,  -1151, 1777,  1567,  1737,  1692,  1765,  1722,
+        1707,  1630,  1751,  1661,  1764,  1614,  1736,  1676,  1763,  1750,
+        1645,  1598,  1721,  1691,  1762,  1706,  1582,  1761,  1566,  -1167,
+        1749,  1629,  767,   766,   751,   765,   494,   494,   735,   764,
+        719,   749,   734,   763,   447,   447,   748,   718,   477,   506,
+        431,   491,   446,   476,   461,   505,   415,   430,   475,   445,
+        504,   399,   460,   489,   414,   503,   383,   474,   429,   459,
+        502,   502,   746,   752,   488,   398,   501,   473,   413,   472,
+        486,   271,   480,   270,   -1439, -1455, 1357,  -1471, -1487, -1503,
+        1341,  1325,  -1519, 1489,  1463,  1403,  1309,  -1535, 1372,  1448,
+        1418,  1476,  1356,  1462,  1387,  -1551, 1475,  1340,  1447,  1402,
+        1386,  -1567, 1068,  1068,  1474,  1461,  455,   380,   468,   440,
+        395,   425,   410,   454,   364,   467,   466,   464,   453,   269,
+        409,   448,   268,   432,   1371,  1473,  1432,  1417,  1308,  1460,
+        1355,  1446,  1459,  1431,  1083,  1083,  1401,  1416,  1458,  1445,
+        1067,  1067,  1370,  1457,  1051,  1051,  1291,  1430,  1385,  1444,
+        1354,  1415,  1400,  1443,  1082,  1082,  1173,  1113,  1186,  1066,
+        1185,  1050,  -1967, 1158,  1128,  1172,  1097,  1171,  1081,  -1983,
+        1157,  1112,  416,   266,   375,   400,   1170,  1142,  1127,  1065,
+        793,   793,   1169,  1033,  1156,  1096,  1141,  1111,  1155,  1080,
+        1126,  1140,  898,   898,   808,   808,   897,   897,   792,   792,
+        1095,  1152,  1032,  1125,  1110,  1139,  1079,  1124,  882,   807,
+        838,   881,   853,   791,   -2319, 867,   368,   263,   822,   852,
+        837,   866,   806,   865,   -2399, 851,   352,   262,   534,   534,
+        821,   836,   594,   594,   549,   549,   593,   593,   533,   533,
+        848,   773,   579,   579,   564,   578,   548,   563,   276,   276,
+        577,   576,   306,   291,   516,   560,   305,   305,   275,   259,
+        -251,  -892,  -2058, -2620, -2828, -2957, -3023, -3039, 1041,  1041,
+        1040,  1040,  769,   769,   769,   769,   256,   256,   256,   256,
+        256,   256,   256,   256,   256,   256,   256,   256,   256,   256,
+        256,   256,   -511,  -527,  -543,  -559,  1530,  -575,  -591,  1528,
+        1527,  1407,  1526,  1391,  1023,  1023,  1023,  1023,  1525,  1375,
+        1268,  1268,  1103,  1103,  1087,  1087,  1039,  1039,  1523,  -604,
+        815,   815,   815,   815,   510,   495,   509,   479,   508,   463,
+        507,   447,   431,   505,   415,   399,   -734,  -782,  1262,  -815,
+        1259,  1244,  -831,  1258,  1228,  -847,  -863,  1196,  -879,  1253,
+        987,   987,   748,   -767,  493,   493,   462,   477,   414,   414,
+        686,   669,   478,   446,   461,   445,   474,   429,   487,   458,
+        412,   471,   1266,  1264,  1009,  1009,  799,   799,   -1019, -1276,
+        -1452, -1581, -1677, -1757, -1821, -1886, -1933, -1997, 1257,  1257,
+        1483,  1468,  1512,  1422,  1497,  1406,  1467,  1496,  1421,  1510,
+        1134,  1134,  1225,  1225,  1466,  1451,  1374,  1405,  1252,  1252,
+        1358,  1480,  1164,  1164,  1251,  1251,  1238,  1238,  1389,  1465,
+        -1407, 1054,  1101,  -1423, 1207,  -1439, 830,   830,   1248,  1038,
+        1237,  1117,  1223,  1148,  1236,  1208,  411,   426,   395,   410,
+        379,   269,   1193,  1222,  1132,  1235,  1221,  1116,  976,   976,
+        1192,  1162,  1177,  1220,  1131,  1191,  963,   963,   -1647, 961,
+        780,   -1663, 558,   558,   994,   993,   437,   408,   393,   407,
+        829,   978,   813,   797,   947,   -1743, 721,   721,   377,   392,
+        844,   950,   828,   890,   706,   706,   812,   859,   796,   960,
+        948,   843,   934,   874,   571,   571,   -1919, 690,   555,   689,
+        421,   346,   539,   539,   944,   779,   918,   873,   932,   842,
+        903,   888,   570,   570,   931,   917,   674,   674,   -2575, 1562,
+        -2591, 1609,  -2607, 1654,  1322,  1322,  1441,  1441,  1696,  1546,
+        1683,  1593,  1669,  1624,  1426,  1426,  1321,  1321,  1639,  1680,
+        1425,  1425,  1305,  1305,  1545,  1668,  1608,  1623,  1667,  1592,
+        1638,  1666,  1320,  1320,  1652,  1607,  1409,  1409,  1304,  1304,
+        1288,  1288,  1664,  1637,  1395,  1395,  1335,  1335,  1622,  1636,
+        1394,  1394,  1319,  1319,  1606,  1621,  1392,  1392,  1137,  1137,
+        1137,  1137,  345,   390,   360,   375,   404,   373,   1047,  -2751,
+        -2767, -2783, 1062,  1121,  1046,  -2799, 1077,  -2815, 1106,  1061,
+        789,   789,   1105,  1104,  263,   355,   310,   340,   325,   354,
+        352,   262,   339,   324,   1091,  1076,  1029,  1090,  1060,  1075,
+        833,   833,   788,   788,   1088,  1028,  818,   818,   803,   803,
+        561,   561,   531,   531,   816,   771,   546,   546,   289,   274,
+        288,   258,   -253,  -317,  -381,  -446,  -478,  -509,  1279,  1279,
+        -811,  -1179, -1451, -1756, -1900, -2028, -2189, -2253, -2333, -2414,
+        -2445, -2511, -2526, 1313,  1298,  -2559, 1041,  1041,  1040,  1040,
+        1025,  1025,  1024,  1024,  1022,  1007,  1021,  991,   1020,  975,
+        1019,  959,   687,   687,   1018,  1017,  671,   671,   655,   655,
+        1016,  1015,  639,   639,   758,   758,   623,   623,   757,   607,
+        756,   591,   755,   575,   754,   559,   543,   543,   1009,  783,
+        -575,  -621,  -685,  -749,  496,   -590,  750,   749,   734,   748,
+        974,   989,   1003,  958,   988,   973,   1002,  942,   987,   957,
+        972,   1001,  926,   986,   941,   971,   956,   1000,  910,   985,
+        925,   999,   894,   970,   -1071, -1087, -1102, 1390,  -1135, 1436,
+        1509,  1451,  1374,  -1151, 1405,  1358,  1480,  1420,  -1167, 1507,
+        1494,  1389,  1342,  1465,  1435,  1450,  1326,  1505,  1310,  1493,
+        1373,  1479,  1404,  1492,  1464,  1419,  428,   443,   472,   397,
+        736,   526,   464,   464,   486,   457,   442,   471,   484,   482,
+        1357,  1449,  1434,  1478,  1388,  1491,  1341,  1490,  1325,  1489,
+        1463,  1403,  1309,  1477,  1372,  1448,  1418,  1433,  1476,  1356,
+        1462,  1387,  -1439, 1475,  1340,  1447,  1402,  1474,  1324,  1461,
+        1371,  1473,  269,   448,   1432,  1417,  1308,  1460,  -1711, 1459,
+        -1727, 1441,  1099,  1099,  1446,  1386,  1431,  1401,  -1743, 1289,
+        1083,  1083,  1160,  1160,  1458,  1445,  1067,  1067,  1370,  1457,
+        1307,  1430,  1129,  1129,  1098,  1098,  268,   432,   267,   416,
+        266,   400,   -1887, 1144,  1187,  1082,  1173,  1113,  1186,  1066,
+        1050,  1158,  1128,  1143,  1172,  1097,  1171,  1081,  420,   391,
+        1157,  1112,  1170,  1142,  1127,  1065,  1169,  1049,  1156,  1096,
+        1141,  1111,  1155,  1080,  1126,  1154,  1064,  1153,  1140,  1095,
+        1048,  -2159, 1125,  1110,  1137,  -2175, 823,   823,   1139,  1138,
+        807,   807,   384,   264,   368,   263,   868,   838,   853,   791,
+        867,   822,   852,   837,   866,   806,   865,   790,   -2319, 851,
+        821,   836,   352,   262,   850,   805,   849,   -2399, 533,   533,
+        835,   820,   336,   261,   578,   548,   563,   577,   532,   532,
+        832,   772,   562,   562,   547,   547,   305,   275,   560,   515,
+        290,   290,   288,   258
+    };
+    static const uint8_t tab32[] = { 130, 162, 193, 209, 44,  28,  76,
+                                     140, 9,   9,   9,   9,   9,   9,
+                                     9,   9,   190, 254, 222, 238, 126,
+                                     94,  157, 157, 109, 61,  173, 205 };
+    static const uint8_t tab33[] = { 252, 236, 220, 204, 188, 172, 156, 140,
+                                     124, 108, 92,  76,  60,  44,  28,  12 };
+    static const int16_t tabindex[2 * 16] = {
+        0,    32,   64,   98,   0,    132,  180,  218,  292,  364,  426,
+        538,  648,  746,  0,    1126, 1460, 1460, 1460, 1460, 1460, 1460,
+        1460, 1460, 1842, 1842, 1842, 1842, 1842, 1842, 1842, 1842
+    };
+    static const uint8_t g_linbits[] = { 0,  0,  0, 0, 0, 0, 0, 0, 0,  0, 0,
+                                         0,  0,  0, 0, 0, 1, 2, 3, 4,  6, 8,
+                                         10, 13, 4, 5, 6, 7, 8, 9, 11, 13 };
+
+#define PEEK_BITS(n) (bs_cache >> (32 - n))
+#define FLUSH_BITS(n)     \
+    {                     \
+        bs_cache <<= (n); \
+        bs_sh += (n);     \
+    }
+#define CHECK_BITS                                     \
+    while (bs_sh >= 0) {                               \
+        bs_cache |= (uint32_t)*bs_next_ptr++ << bs_sh; \
+        bs_sh -= 8;                                    \
+    }
+#define BSPOS ((bs_next_ptr - bs->buf) * 8 - 24 + bs_sh)
+
+    float one = 0.0f;
+    int ireg = 0, big_val_cnt = gr_info->big_values;
+    const uint8_t *sfb = gr_info->sfbtab;
+    const uint8_t *bs_next_ptr = bs->buf + bs->pos / 8;
+    uint32_t bs_cache =
+        (((bs_next_ptr[0] * 256u + bs_next_ptr[1]) * 256u + bs_next_ptr[2]) *
+             256u +
+         bs_next_ptr[3])
+        << (bs->pos & 7);
+    int pairs_to_decode, np, bs_sh = (bs->pos & 7) - 8;
+    bs_next_ptr += 4;
+
+    while (big_val_cnt > 0) {
+        int tab_num = gr_info->table_select[ireg];
+        int sfb_cnt = gr_info->region_count[ireg++];
+        const int16_t *codebook = tabs + tabindex[tab_num];
+        int linbits = g_linbits[tab_num];
+        if (linbits) {
+            do {
+                np = *sfb++ / 2;
+                pairs_to_decode = MINIMP3_MIN(big_val_cnt, np);
+                one = *scf++;
+                do {
+                    int j, w = 5;
+                    int leaf = codebook[PEEK_BITS(w)];
+                    while (leaf < 0) {
+                        FLUSH_BITS(w);
+                        w = leaf & 7;
+                        leaf = codebook[PEEK_BITS(w) - (leaf >> 3)];
+                    }
+                    FLUSH_BITS(leaf >> 8);
+
+                    for (j = 0; j < 2; j++, dst++, leaf >>= 4) {
+                        int lsb = leaf & 0x0F;
+                        if (lsb == 15) {
+                            lsb += PEEK_BITS(linbits);
+                            FLUSH_BITS(linbits);
+                            CHECK_BITS;
+                            *dst = one * L3_pow_43(lsb) *
+                                   ((int32_t)bs_cache < 0 ? -1 : 1);
+                        } else {
+                            *dst =
+                                g_pow43[16 + lsb - 16 * (bs_cache >> 31)] * one;
+                        }
+                        FLUSH_BITS(lsb ? 1 : 0);
+                    }
+                    CHECK_BITS;
+                } while (--pairs_to_decode);
+            } while ((big_val_cnt -= np) > 0 && --sfb_cnt >= 0);
+        } else {
+            do {
+                np = *sfb++ / 2;
+                pairs_to_decode = MINIMP3_MIN(big_val_cnt, np);
+                one = *scf++;
+                do {
+                    int j, w = 5;
+                    int leaf = codebook[PEEK_BITS(w)];
+                    while (leaf < 0) {
+                        FLUSH_BITS(w);
+                        w = leaf & 7;
+                        leaf = codebook[PEEK_BITS(w) - (leaf >> 3)];
+                    }
+                    FLUSH_BITS(leaf >> 8);
+
+                    for (j = 0; j < 2; j++, dst++, leaf >>= 4) {
+                        int lsb = leaf & 0x0F;
+                        *dst = g_pow43[16 + lsb - 16 * (bs_cache >> 31)] * one;
+                        FLUSH_BITS(lsb ? 1 : 0);
+                    }
+                    CHECK_BITS;
+                } while (--pairs_to_decode);
+            } while ((big_val_cnt -= np) > 0 && --sfb_cnt >= 0);
+        }
+    }
+
+    for (np = 1 - big_val_cnt;; dst += 4) {
+        const uint8_t *codebook_count1 =
+            (gr_info->count1_table) ? tab33 : tab32;
+        int leaf = codebook_count1[PEEK_BITS(4)];
+        if (!(leaf & 8)) {
+            leaf = codebook_count1[(leaf >> 3) +
+                                   (bs_cache << 4 >> (32 - (leaf & 3)))];
+        }
+        FLUSH_BITS(leaf & 7);
+        if (BSPOS > layer3gr_limit) {
+            break;
+        }
+#define RELOAD_SCALEFACTOR \
+    if (!--np) {           \
+        np = *sfb++ / 2;   \
+        if (!np) break;    \
+        one = *scf++;      \
+    }
+#define DEQ_COUNT1(s)                                  \
+    if (leaf & (128 >> s)) {                           \
+        dst[s] = ((int32_t)bs_cache < 0) ? -one : one; \
+        FLUSH_BITS(1)                                  \
+    }
+        RELOAD_SCALEFACTOR;
+        DEQ_COUNT1(0);
+        DEQ_COUNT1(1);
+        RELOAD_SCALEFACTOR;
+        DEQ_COUNT1(2);
+        DEQ_COUNT1(3);
+        CHECK_BITS;
+    }
+
+    bs->pos = layer3gr_limit;
+}
+
+static void L3_midside_stereo(float *left, int n) {
+    int i = 0;
+    float *right = left + 576;
+#if HAVE_SIMD
+    if (have_simd()) {
+        for (; i < n - 3; i += 4) {
+            f4 vl = VLD(left + i);
+            f4 vr = VLD(right + i);
+            VSTORE(left + i, VADD(vl, vr));
+            VSTORE(right + i, VSUB(vl, vr));
+        }
+#ifdef __GNUC__
+        /* Workaround for spurious -Waggressive-loop-optimizations warning from
+         * gcc. For more info see: https://github.com/lieff/minimp3/issues/88
+         */
+        if (__builtin_constant_p(n % 4 == 0) && n % 4 == 0) return;
+#endif
+    }
+#endif /* HAVE_SIMD */
+    for (; i < n; i++) {
+        float a = left[i];
+        float b = right[i];
+        left[i] = a + b;
+        right[i] = a - b;
+    }
+}
+
+static void L3_intensity_stereo_band(float *left, int n, float kl, float kr) {
+    int i;
+    for (i = 0; i < n; i++) {
+        left[i + 576] = left[i] * kr;
+        left[i] = left[i] * kl;
+    }
+}
+
+static void L3_stereo_top_band(const float *right, const uint8_t *sfb,
+                               int nbands, int max_band[3]) {
+    int i, k;
+
+    max_band[0] = max_band[1] = max_band[2] = -1;
+
+    for (i = 0; i < nbands; i++) {
+        for (k = 0; k < sfb[i]; k += 2) {
+            if (right[k] != 0 || right[k + 1] != 0) {
+                max_band[i % 3] = i;
+                break;
+            }
+        }
+        right += sfb[i];
+    }
+}
+
+static void L3_stereo_process(float *left, const uint8_t *ist_pos,
+                              const uint8_t *sfb, const uint8_t *hdr,
+                              int max_band[3], int mpeg2_sh) {
+    static const float g_pan[7 * 2] = {
+        0,    1,    0.21132487f, 0.78867513f, 0.36602540f, 0.63397460f,
+        0.5f, 0.5f, 0.63397460f, 0.36602540f, 0.78867513f, 0.21132487f,
+        1,    0
+    };
+    unsigned i, max_pos = HDR_TEST_MPEG1(hdr) ? 7 : 64;
+
+    for (i = 0; sfb[i]; i++) {
+        unsigned ipos = ist_pos[i];
+        if ((int)i > max_band[i % 3] && ipos < max_pos) {
+            float kl, kr, s = HDR_TEST_MS_STEREO(hdr) ? 1.41421356f : 1;
+            if (HDR_TEST_MPEG1(hdr)) {
+                kl = g_pan[2 * ipos];
+                kr = g_pan[2 * ipos + 1];
+            } else {
+                kl = 1;
+                kr = L3_ldexp_q2(1, (ipos + 1) >> 1 << mpeg2_sh);
+                if (ipos & 1) {
+                    kl = kr;
+                    kr = 1;
+                }
+            }
+            L3_intensity_stereo_band(left, sfb[i], kl * s, kr * s);
+        } else if (HDR_TEST_MS_STEREO(hdr)) {
+            L3_midside_stereo(left, sfb[i]);
+        }
+        left += sfb[i];
+    }
+}
+
+static void L3_intensity_stereo(float *left, uint8_t *ist_pos,
+                                const L3_gr_info_t *gr, const uint8_t *hdr) {
+    int max_band[3], n_sfb = gr->n_long_sfb + gr->n_short_sfb;
+    int i, max_blocks = gr->n_short_sfb ? 3 : 1;
+
+    L3_stereo_top_band(left + 576, gr->sfbtab, n_sfb, max_band);
+    if (gr->n_long_sfb) {
+        max_band[0] = max_band[1] = max_band[2] =
+            MINIMP3_MAX(MINIMP3_MAX(max_band[0], max_band[1]), max_band[2]);
+    }
+    for (i = 0; i < max_blocks; i++) {
+        int default_pos = HDR_TEST_MPEG1(hdr) ? 3 : 0;
+        int itop = n_sfb - max_blocks + i;
+        int prev = itop - max_blocks;
+        ist_pos[itop] = max_band[i] >= prev ? default_pos : ist_pos[prev];
+    }
+    L3_stereo_process(left, ist_pos, gr->sfbtab, hdr, max_band,
+                      gr[1].scalefac_compress & 1);
+}
+
+static void L3_reorder(float *grbuf, float *scratch, const uint8_t *sfb) {
+    int i, len;
+    float *src = grbuf, *dst = scratch;
+
+    for (; 0 != (len = *sfb); sfb += 3, src += 2 * len) {
+        for (i = 0; i < len; i++, src++) {
+            *dst++ = src[0 * len];
+            *dst++ = src[1 * len];
+            *dst++ = src[2 * len];
+        }
+    }
+    memcpy(grbuf, scratch, (dst - scratch) * sizeof(float));
+}
+
+static void L3_antialias(float *grbuf, int nbands) {
+    static const float g_aa[2][8] = {
+        { 0.85749293f, 0.88174200f, 0.94962865f, 0.98331459f, 0.99551782f,
+          0.99916056f, 0.99989920f, 0.99999316f },
+        { 0.51449576f, 0.47173197f, 0.31337745f, 0.18191320f, 0.09457419f,
+          0.04096558f, 0.01419856f, 0.00369997f }
+    };
+
+    for (; nbands > 0; nbands--, grbuf += 18) {
+        int i = 0;
+#if HAVE_SIMD
+        if (have_simd())
+            for (; i < 8; i += 4) {
+                f4 vu = VLD(grbuf + 18 + i);
+                f4 vd = VLD(grbuf + 14 - i);
+                f4 vc0 = VLD(g_aa[0] + i);
+                f4 vc1 = VLD(g_aa[1] + i);
+                vd = VREV(vd);
+                VSTORE(grbuf + 18 + i, VSUB(VMUL(vu, vc0), VMUL(vd, vc1)));
+                vd = VADD(VMUL(vu, vc1), VMUL(vd, vc0));
+                VSTORE(grbuf + 14 - i, VREV(vd));
+            }
+#endif /* HAVE_SIMD */
+#ifndef MINIMP3_ONLY_SIMD
+        for (; i < 8; i++) {
+            float u = grbuf[18 + i];
+            float d = grbuf[17 - i];
+            grbuf[18 + i] = u * g_aa[0][i] - d * g_aa[1][i];
+            grbuf[17 - i] = u * g_aa[1][i] + d * g_aa[0][i];
+        }
+#endif /* MINIMP3_ONLY_SIMD */
+    }
+}
+
+static void L3_dct3_9(float *y) {
+    float s0, s1, s2, s3, s4, s5, s6, s7, s8, t0, t2, t4;
+
+    s0 = y[0];
+    s2 = y[2];
+    s4 = y[4];
+    s6 = y[6];
+    s8 = y[8];
+    t0 = s0 + s6 * 0.5f;
+    s0 -= s6;
+    t4 = (s4 + s2) * 0.93969262f;
+    t2 = (s8 + s2) * 0.76604444f;
+    s6 = (s4 - s8) * 0.17364818f;
+    s4 += s8 - s2;
+
+    s2 = s0 - s4 * 0.5f;
+    y[4] = s4 + s0;
+    s8 = t0 - t2 + s6;
+    s0 = t0 - t4 + t2;
+    s4 = t0 + t4 - s6;
+
+    s1 = y[1];
+    s3 = y[3];
+    s5 = y[5];
+    s7 = y[7];
+
+    s3 *= 0.86602540f;
+    t0 = (s5 + s1) * 0.98480775f;
+    t4 = (s5 - s7) * 0.34202014f;
+    t2 = (s1 + s7) * 0.64278761f;
+    s1 = (s1 - s5 - s7) * 0.86602540f;
+
+    s5 = t0 - s3 - t2;
+    s7 = t4 - s3 - t0;
+    s3 = t4 + s3 - t2;
+
+    y[0] = s4 - s7;
+    y[1] = s2 + s1;
+    y[2] = s0 - s3;
+    y[3] = s8 + s5;
+    y[5] = s8 - s5;
+    y[6] = s0 + s3;
+    y[7] = s2 - s1;
+    y[8] = s4 + s7;
+}
+
+static void L3_imdct36(float *grbuf, float *overlap, const float *window,
+                       int nbands) {
+    int i, j;
+    static const float g_twid9[18] = { 0.73727734f, 0.79335334f, 0.84339145f,
+                                       0.88701083f, 0.92387953f, 0.95371695f,
+                                       0.97629601f, 0.99144486f, 0.99904822f,
+                                       0.67559021f, 0.60876143f, 0.53729961f,
+                                       0.46174861f, 0.38268343f, 0.30070580f,
+                                       0.21643961f, 0.13052619f, 0.04361938f };
+
+    for (j = 0; j < nbands; j++, grbuf += 18, overlap += 9) {
+        float co[9], si[9];
+        co[0] = -grbuf[0];
+        si[0] = grbuf[17];
+        for (i = 0; i < 4; i++) {
+            si[8 - 2 * i] = grbuf[4 * i + 1] - grbuf[4 * i + 2];
+            co[1 + 2 * i] = grbuf[4 * i + 1] + grbuf[4 * i + 2];
+            si[7 - 2 * i] = grbuf[4 * i + 4] - grbuf[4 * i + 3];
+            co[2 + 2 * i] = -(grbuf[4 * i + 3] + grbuf[4 * i + 4]);
+        }
+        L3_dct3_9(co);
+        L3_dct3_9(si);
+
+        si[1] = -si[1];
+        si[3] = -si[3];
+        si[5] = -si[5];
+        si[7] = -si[7];
+
+        i = 0;
+
+#if HAVE_SIMD
+        if (have_simd())
+            for (; i < 8; i += 4) {
+                f4 vovl = VLD(overlap + i);
+                f4 vc = VLD(co + i);
+                f4 vs = VLD(si + i);
+                f4 vr0 = VLD(g_twid9 + i);
+                f4 vr1 = VLD(g_twid9 + 9 + i);
+                f4 vw0 = VLD(window + i);
+                f4 vw1 = VLD(window + 9 + i);
+                f4 vsum = VADD(VMUL(vc, vr1), VMUL(vs, vr0));
+                VSTORE(overlap + i, VSUB(VMUL(vc, vr0), VMUL(vs, vr1)));
+                VSTORE(grbuf + i, VSUB(VMUL(vovl, vw0), VMUL(vsum, vw1)));
+                vsum = VADD(VMUL(vovl, vw1), VMUL(vsum, vw0));
+                VSTORE(grbuf + 14 - i, VREV(vsum));
+            }
+#endif /* HAVE_SIMD */
+        for (; i < 9; i++) {
+            float ovl = overlap[i];
+            float sum = co[i] * g_twid9[9 + i] + si[i] * g_twid9[0 + i];
+            overlap[i] = co[i] * g_twid9[0 + i] - si[i] * g_twid9[9 + i];
+            grbuf[i] = ovl * window[0 + i] - sum * window[9 + i];
+            grbuf[17 - i] = ovl * window[9 + i] + sum * window[0 + i];
+        }
+    }
+}
+
+static void L3_idct3(float x0, float x1, float x2, float *dst) {
+    float m1 = x1 * 0.86602540f;
+    float a1 = x0 - x2 * 0.5f;
+    dst[1] = x0 + x2;
+    dst[0] = a1 + m1;
+    dst[2] = a1 - m1;
+}
+
+static void L3_imdct12(float *x, float *dst, float *overlap) {
+    static const float g_twid3[6] = { 0.79335334f, 0.92387953f, 0.99144486f,
+                                      0.60876143f, 0.38268343f, 0.13052619f };
+    float co[3], si[3];
+    int i;
+
+    L3_idct3(-x[0], x[6] + x[3], x[12] + x[9], co);
+    L3_idct3(x[15], x[12] - x[9], x[6] - x[3], si);
+    si[1] = -si[1];
+
+    for (i = 0; i < 3; i++) {
+        float ovl = overlap[i];
+        float sum = co[i] * g_twid3[3 + i] + si[i] * g_twid3[0 + i];
+        overlap[i] = co[i] * g_twid3[0 + i] - si[i] * g_twid3[3 + i];
+        dst[i] = ovl * g_twid3[2 - i] - sum * g_twid3[5 - i];
+        dst[5 - i] = ovl * g_twid3[5 - i] + sum * g_twid3[2 - i];
+    }
+}
+
+static void L3_imdct_short(float *grbuf, float *overlap, int nbands) {
+    for (; nbands > 0; nbands--, overlap += 9, grbuf += 18) {
+        float tmp[18];
+        memcpy(tmp, grbuf, sizeof(tmp));
+        memcpy(grbuf, overlap, 6 * sizeof(float));
+        L3_imdct12(tmp, grbuf + 6, overlap + 6);
+        L3_imdct12(tmp + 1, grbuf + 12, overlap + 6);
+        L3_imdct12(tmp + 2, overlap, overlap + 6);
+    }
+}
+
+static void L3_change_sign(float *grbuf) {
+    int b, i;
+    for (b = 0, grbuf += 18; b < 32; b += 2, grbuf += 36)
+        for (i = 1; i < 18; i += 2) grbuf[i] = -grbuf[i];
+}
+
+static void L3_imdct_gr(float *grbuf, float *overlap, unsigned block_type,
+                        unsigned n_long_bands) {
+    static const float g_mdct_window[2][18] = {
+        { 0.99904822f, 0.99144486f, 0.97629601f, 0.95371695f, 0.92387953f,
+          0.88701083f, 0.84339145f, 0.79335334f, 0.73727734f, 0.04361938f,
+          0.13052619f, 0.21643961f, 0.30070580f, 0.38268343f, 0.46174861f,
+          0.53729961f, 0.60876143f, 0.67559021f },
+        { 1, 1, 1, 1, 1, 1, 0.99144486f, 0.92387953f, 0.79335334f, 0, 0, 0, 0,
+          0, 0, 0.13052619f, 0.38268343f, 0.60876143f }
+    };
+    if (n_long_bands) {
+        L3_imdct36(grbuf, overlap, g_mdct_window[0], n_long_bands);
+        grbuf += 18 * n_long_bands;
+        overlap += 9 * n_long_bands;
+    }
+    if (block_type == SHORT_BLOCK_TYPE)
+        L3_imdct_short(grbuf, overlap, 32 - n_long_bands);
+    else
+        L3_imdct36(grbuf, overlap, g_mdct_window[block_type == STOP_BLOCK_TYPE],
+                   32 - n_long_bands);
+}
+
+static void L3_save_reservoir(mp3dec_t *h, mp3dec_scratch_t *s) {
+    int pos = (s->bs.pos + 7) / 8u;
+    int remains = s->bs.limit / 8u - pos;
+    if (remains > MAX_BITRESERVOIR_BYTES) {
+        pos += remains - MAX_BITRESERVOIR_BYTES;
+        remains = MAX_BITRESERVOIR_BYTES;
+    }
+    if (remains > 0) {
+        memmove(h->reserv_buf, s->maindata + pos, remains);
+    }
+    h->reserv = remains;
+}
+
+static int L3_restore_reservoir(mp3dec_t *h, bs_t *bs, mp3dec_scratch_t *s,
+                                int main_data_begin) {
+    int frame_bytes = (bs->limit - bs->pos) / 8;
+    int bytes_have = MINIMP3_MIN(h->reserv, main_data_begin);
+    memcpy(s->maindata,
+           h->reserv_buf + MINIMP3_MAX(0, h->reserv - main_data_begin),
+           MINIMP3_MIN(h->reserv, main_data_begin));
+    memcpy(s->maindata + bytes_have, bs->buf + bs->pos / 8, frame_bytes);
+    bs_init(&s->bs, s->maindata, bytes_have + frame_bytes);
+    return h->reserv >= main_data_begin;
+}
+
+static void L3_decode(mp3dec_t *h, mp3dec_scratch_t *s, L3_gr_info_t *gr_info,
+                      int nch) {
+    int ch;
+
+    for (ch = 0; ch < nch; ch++) {
+        int layer3gr_limit = s->bs.pos + gr_info[ch].part_23_length;
+        L3_decode_scalefactors(h->header, s->ist_pos[ch], &s->bs, gr_info + ch,
+                               s->scf, ch);
+        L3_huffman(s->grbuf[ch], &s->bs, gr_info + ch, s->scf, layer3gr_limit);
+    }
+
+    if (HDR_TEST_I_STEREO(h->header)) {
+        L3_intensity_stereo(s->grbuf[0], s->ist_pos[1], gr_info, h->header);
+    } else if (HDR_IS_MS_STEREO(h->header)) {
+        L3_midside_stereo(s->grbuf[0], 576);
+    }
+
+    for (ch = 0; ch < nch; ch++, gr_info++) {
+        int aa_bands = 31;
+        int n_long_bands = (gr_info->mixed_block_flag ? 2 : 0)
+                           << (int)(HDR_GET_MY_SAMPLE_RATE(h->header) == 2);
+
+        if (gr_info->n_short_sfb) {
+            aa_bands = n_long_bands - 1;
+            L3_reorder(s->grbuf[ch] + n_long_bands * 18, s->syn[0],
+                       gr_info->sfbtab + gr_info->n_long_sfb);
+        }
+
+        L3_antialias(s->grbuf[ch], aa_bands);
+        L3_imdct_gr(s->grbuf[ch], h->mdct_overlap[ch], gr_info->block_type,
+                    n_long_bands);
+        L3_change_sign(s->grbuf[ch]);
+    }
+}
+
+static void mp3d_DCT_II(float *grbuf, int n) {
+    static const float g_sec[24] = {
+        10.19000816f, 0.50060302f, 0.50241929f, 3.40760851f, 0.50547093f,
+        0.52249861f,  2.05778098f, 0.51544732f, 0.56694406f, 1.48416460f,
+        0.53104258f,  0.64682180f, 1.16943991f, 0.55310392f, 0.78815460f,
+        0.97256821f,  0.58293498f, 1.06067765f, 0.83934963f, 0.62250412f,
+        1.72244716f,  0.74453628f, 0.67480832f, 5.10114861f
+    };
+    int i, k = 0;
+#if HAVE_SIMD
+    if (have_simd())
+        for (; k < n; k += 4) {
+            f4 t[4][8], *x;
+            float *y = grbuf + k;
+
+            for (x = t[0], i = 0; i < 8; i++, x++) {
+                f4 x0 = VLD(&y[i * 18]);
+                f4 x1 = VLD(&y[(15 - i) * 18]);
+                f4 x2 = VLD(&y[(16 + i) * 18]);
+                f4 x3 = VLD(&y[(31 - i) * 18]);
+                f4 t0 = VADD(x0, x3);
+                f4 t1 = VADD(x1, x2);
+                f4 t2 = VMUL_S(VSUB(x1, x2), g_sec[3 * i + 0]);
+                f4 t3 = VMUL_S(VSUB(x0, x3), g_sec[3 * i + 1]);
+                x[0] = VADD(t0, t1);
+                x[8] = VMUL_S(VSUB(t0, t1), g_sec[3 * i + 2]);
+                x[16] = VADD(t3, t2);
+                x[24] = VMUL_S(VSUB(t3, t2), g_sec[3 * i + 2]);
+            }
+            for (x = t[0], i = 0; i < 4; i++, x += 8) {
+                f4 x0 = x[0], x1 = x[1], x2 = x[2], x3 = x[3], x4 = x[4],
+                   x5 = x[5], x6 = x[6], x7 = x[7], xt;
+                xt = VSUB(x0, x7);
+                x0 = VADD(x0, x7);
+                x7 = VSUB(x1, x6);
+                x1 = VADD(x1, x6);
+                x6 = VSUB(x2, x5);
+                x2 = VADD(x2, x5);
+                x5 = VSUB(x3, x4);
+                x3 = VADD(x3, x4);
+                x4 = VSUB(x0, x3);
+                x0 = VADD(x0, x3);
+                x3 = VSUB(x1, x2);
+                x1 = VADD(x1, x2);
+                x[0] = VADD(x0, x1);
+                x[4] = VMUL_S(VSUB(x0, x1), 0.70710677f);
+                x5 = VADD(x5, x6);
+                x6 = VMUL_S(VADD(x6, x7), 0.70710677f);
+                x7 = VADD(x7, xt);
+                x3 = VMUL_S(VADD(x3, x4), 0.70710677f);
+                x5 = VSUB(x5, VMUL_S(x7, 0.198912367f)); /* rotate by PI/8 */
+                x7 = VADD(x7, VMUL_S(x5, 0.382683432f));
+                x5 = VSUB(x5, VMUL_S(x7, 0.198912367f));
+                x0 = VSUB(xt, x6);
+                xt = VADD(xt, x6);
+                x[1] = VMUL_S(VADD(xt, x7), 0.50979561f);
+                x[2] = VMUL_S(VADD(x4, x3), 0.54119611f);
+                x[3] = VMUL_S(VSUB(x0, x5), 0.60134488f);
+                x[5] = VMUL_S(VADD(x0, x5), 0.89997619f);
+                x[6] = VMUL_S(VSUB(x4, x3), 1.30656302f);
+                x[7] = VMUL_S(VSUB(xt, x7), 2.56291556f);
+            }
+
+            if (k > n - 3) {
+#if HAVE_SSE
+#define VSAVE2(i, v) _mm_storel_pi((__m64 *)(void *)&y[i * 18], v)
+#else /* HAVE_SSE */
+#define VSAVE2(i, v) vst1_f32((float32_t *)&y[i * 18], vget_low_f32(v))
+#endif /* HAVE_SSE */
+                for (i = 0; i < 7; i++, y += 4 * 18) {
+                    f4 s = VADD(t[3][i], t[3][i + 1]);
+                    VSAVE2(0, t[0][i]);
+                    VSAVE2(1, VADD(t[2][i], s));
+                    VSAVE2(2, VADD(t[1][i], t[1][i + 1]));
+                    VSAVE2(3, VADD(t[2][1 + i], s));
+                }
+                VSAVE2(0, t[0][7]);
+                VSAVE2(1, VADD(t[2][7], t[3][7]));
+                VSAVE2(2, t[1][7]);
+                VSAVE2(3, t[3][7]);
+            } else {
+#define VSAVE4(i, v) VSTORE(&y[i * 18], v)
+                for (i = 0; i < 7; i++, y += 4 * 18) {
+                    f4 s = VADD(t[3][i], t[3][i + 1]);
+                    VSAVE4(0, t[0][i]);
+                    VSAVE4(1, VADD(t[2][i], s));
+                    VSAVE4(2, VADD(t[1][i], t[1][i + 1]));
+                    VSAVE4(3, VADD(t[2][1 + i], s));
+                }
+                VSAVE4(0, t[0][7]);
+                VSAVE4(1, VADD(t[2][7], t[3][7]));
+                VSAVE4(2, t[1][7]);
+                VSAVE4(3, t[3][7]);
+            }
+        }
+    else
+#endif /* HAVE_SIMD */
+#ifdef MINIMP3_ONLY_SIMD
+    {
+    }  /* for HAVE_SIMD=1, MINIMP3_ONLY_SIMD=1 case we do not need non-intrinsic
+          "else" branch */
+#else  /* MINIMP3_ONLY_SIMD */
+    for (; k < n; k++) {
+        float t[4][8], *x, *y = grbuf + k;
+
+        for (x = t[0], i = 0; i < 8; i++, x++) {
+            float x0 = y[i * 18];
+            float x1 = y[(15 - i) * 18];
+            float x2 = y[(16 + i) * 18];
+            float x3 = y[(31 - i) * 18];
+            float t0 = x0 + x3;
+            float t1 = x1 + x2;
+            float t2 = (x1 - x2) * g_sec[3 * i + 0];
+            float t3 = (x0 - x3) * g_sec[3 * i + 1];
+            x[0] = t0 + t1;
+            x[8] = (t0 - t1) * g_sec[3 * i + 2];
+            x[16] = t3 + t2;
+            x[24] = (t3 - t2) * g_sec[3 * i + 2];
+        }
+        for (x = t[0], i = 0; i < 4; i++, x += 8) {
+            float x0 = x[0], x1 = x[1], x2 = x[2], x3 = x[3], x4 = x[4],
+                  x5 = x[5], x6 = x[6], x7 = x[7], xt;
+            xt = x0 - x7;
+            x0 += x7;
+            x7 = x1 - x6;
+            x1 += x6;
+            x6 = x2 - x5;
+            x2 += x5;
+            x5 = x3 - x4;
+            x3 += x4;
+            x4 = x0 - x3;
+            x0 += x3;
+            x3 = x1 - x2;
+            x1 += x2;
+            x[0] = x0 + x1;
+            x[4] = (x0 - x1) * 0.70710677f;
+            x5 = x5 + x6;
+            x6 = (x6 + x7) * 0.70710677f;
+            x7 = x7 + xt;
+            x3 = (x3 + x4) * 0.70710677f;
+            x5 -= x7 * 0.198912367f; /* rotate by PI/8 */
+            x7 += x5 * 0.382683432f;
+            x5 -= x7 * 0.198912367f;
+            x0 = xt - x6;
+            xt += x6;
+            x[1] = (xt + x7) * 0.50979561f;
+            x[2] = (x4 + x3) * 0.54119611f;
+            x[3] = (x0 - x5) * 0.60134488f;
+            x[5] = (x0 + x5) * 0.89997619f;
+            x[6] = (x4 - x3) * 1.30656302f;
+            x[7] = (xt - x7) * 2.56291556f;
+        }
+        for (i = 0; i < 7; i++, y += 4 * 18) {
+            y[0 * 18] = t[0][i];
+            y[1 * 18] = t[2][i] + t[3][i] + t[3][i + 1];
+            y[2 * 18] = t[1][i] + t[1][i + 1];
+            y[3 * 18] = t[2][i + 1] + t[3][i] + t[3][i + 1];
+        }
+        y[0 * 18] = t[0][7];
+        y[1 * 18] = t[2][7] + t[3][7];
+        y[2 * 18] = t[1][7];
+        y[3 * 18] = t[3][7];
+    }
+#endif /* MINIMP3_ONLY_SIMD */
+}
+
+#ifndef MINIMP3_FLOAT_OUTPUT
+static int16_t mp3d_scale_pcm(float sample) {
+#if HAVE_ARMV6
+    int32_t s32 = (int32_t)(sample + .5f);
+    s32 -= (s32 < 0);
+    int16_t s = (int16_t)minimp3_clip_int16_arm(s32);
+#else
+    if (sample >= 32766.5) return (int16_t)32767;
+    if (sample <= -32767.5) return (int16_t)-32768;
+    int16_t s = (int16_t)(sample + .5f);
+    s -= (s < 0); /* away from zero, to be compliant */
+#endif
+    return s;
+}
+#else  /* MINIMP3_FLOAT_OUTPUT */
+static float mp3d_scale_pcm(float sample) { return sample * (1.f / 32768.f); }
+#endif /* MINIMP3_FLOAT_OUTPUT */
+
+static void mp3d_synth_pair(mp3d_sample_t *pcm, int nch, const float *z) {
+    float a;
+    a = (z[14 * 64] - z[0]) * 29;
+    a += (z[1 * 64] + z[13 * 64]) * 213;
+    a += (z[12 * 64] - z[2 * 64]) * 459;
+    a += (z[3 * 64] + z[11 * 64]) * 2037;
+    a += (z[10 * 64] - z[4 * 64]) * 5153;
+    a += (z[5 * 64] + z[9 * 64]) * 6574;
+    a += (z[8 * 64] - z[6 * 64]) * 37489;
+    a += z[7 * 64] * 75038;
+    pcm[0] = mp3d_scale_pcm(a);
+
+    z += 2;
+    a = z[14 * 64] * 104;
+    a += z[12 * 64] * 1567;
+    a += z[10 * 64] * 9727;
+    a += z[8 * 64] * 64019;
+    a += z[6 * 64] * -9975;
+    a += z[4 * 64] * -45;
+    a += z[2 * 64] * 146;
+    a += z[0 * 64] * -5;
+    pcm[16 * nch] = mp3d_scale_pcm(a);
+}
+
+static void mp3d_synth(float *xl, mp3d_sample_t *dstl, int nch, float *lins) {
+    int i;
+    float *xr = xl + 576 * (nch - 1);
+    mp3d_sample_t *dstr = dstl + (nch - 1);
+
+    static const float g_win[] = {
+        -1,     26,     -31,    208,    218,    401,    -519,   2063,   2000,
+        4788,   -5517,  7134,   5959,   35640,  -39336, 74992,  -1,     24,
+        -35,    202,    222,    347,    -581,   2080,   1952,   4425,   -5879,
+        7640,   5288,   33791,  -41176, 74856,  -1,     21,     -38,    196,
+        225,    294,    -645,   2087,   1893,   4063,   -6237,  8092,   4561,
+        31947,  -43006, 74630,  -1,     19,     -41,    190,    227,    244,
+        -711,   2085,   1822,   3705,   -6589,  8492,   3776,   30112,  -44821,
+        74313,  -1,     17,     -45,    183,    228,    197,    -779,   2075,
+        1739,   3351,   -6935,  8840,   2935,   28289,  -46617, 73908,  -1,
+        16,     -49,    176,    228,    153,    -848,   2057,   1644,   3004,
+        -7271,  9139,   2037,   26482,  -48390, 73415,  -2,     14,     -53,
+        169,    227,    111,    -919,   2032,   1535,   2663,   -7597,  9389,
+        1082,   24694,  -50137, 72835,  -2,     13,     -58,    161,    224,
+        72,     -991,   2001,   1414,   2330,   -7910,  9592,   70,     22929,
+        -51853, 72169,  -2,     11,     -63,    154,    221,    36,     -1064,
+        1962,   1280,   2006,   -8209,  9750,   -998,   21189,  -53534, 71420,
+        -2,     10,     -68,    147,    215,    2,      -1137,  1919,   1131,
+        1692,   -8491,  9863,   -2122,  19478,  -55178, 70590,  -3,     9,
+        -73,    139,    208,    -29,    -1210,  1870,   970,    1388,   -8755,
+        9935,   -3300,  17799,  -56778, 69679,  -3,     8,      -79,    132,
+        200,    -57,    -1283,  1817,   794,    1095,   -8998,  9966,   -4533,
+        16155,  -58333, 68692,  -4,     7,      -85,    125,    189,    -83,
+        -1356,  1759,   605,    814,    -9219,  9959,   -5818,  14548,  -59838,
+        67629,  -4,     7,      -91,    117,    177,    -106,   -1428,  1698,
+        402,    545,    -9416,  9916,   -7154,  12980,  -61289, 66494,  -5,
+        6,      -97,    111,    163,    -127,   -1498,  1634,   185,    288,
+        -9585,  9838,   -8540,  11455,  -62684, 65290
+    };
+    float *zlin = lins + 15 * 64;
+    const float *w = g_win;
+
+    zlin[4 * 15] = xl[18 * 16];
+    zlin[4 * 15 + 1] = xr[18 * 16];
+    zlin[4 * 15 + 2] = xl[0];
+    zlin[4 * 15 + 3] = xr[0];
+
+    zlin[4 * 31] = xl[1 + 18 * 16];
+    zlin[4 * 31 + 1] = xr[1 + 18 * 16];
+    zlin[4 * 31 + 2] = xl[1];
+    zlin[4 * 31 + 3] = xr[1];
+
+    mp3d_synth_pair(dstr, nch, lins + 4 * 15 + 1);
+    mp3d_synth_pair(dstr + 32 * nch, nch, lins + 4 * 15 + 64 + 1);
+    mp3d_synth_pair(dstl, nch, lins + 4 * 15);
+    mp3d_synth_pair(dstl + 32 * nch, nch, lins + 4 * 15 + 64);
+
+#if HAVE_SIMD
+    if (have_simd())
+        for (i = 14; i >= 0; i--) {
+#define VLOAD(k)                        \
+    f4 w0 = VSET(*w++);                 \
+    f4 w1 = VSET(*w++);                 \
+    f4 vz = VLD(&zlin[4 * i - 64 * k]); \
+    f4 vy = VLD(&zlin[4 * i - 64 * (15 - k)]);
+#define V0(k)                                          \
+    {                                                  \
+        VLOAD(k) b = VADD(VMUL(vz, w1), VMUL(vy, w0)); \
+        a = VSUB(VMUL(vz, w0), VMUL(vy, w1));          \
+    }
+#define V1(k)                                                   \
+    {                                                           \
+        VLOAD(k) b = VADD(b, VADD(VMUL(vz, w1), VMUL(vy, w0))); \
+        a = VADD(a, VSUB(VMUL(vz, w0), VMUL(vy, w1)));          \
+    }
+#define V2(k)                                                   \
+    {                                                           \
+        VLOAD(k) b = VADD(b, VADD(VMUL(vz, w1), VMUL(vy, w0))); \
+        a = VADD(a, VSUB(VMUL(vy, w1), VMUL(vz, w0)));          \
+    }
+            f4 a, b;
+            zlin[4 * i] = xl[18 * (31 - i)];
+            zlin[4 * i + 1] = xr[18 * (31 - i)];
+            zlin[4 * i + 2] = xl[1 + 18 * (31 - i)];
+            zlin[4 * i + 3] = xr[1 + 18 * (31 - i)];
+            zlin[4 * i + 64] = xl[1 + 18 * (1 + i)];
+            zlin[4 * i + 64 + 1] = xr[1 + 18 * (1 + i)];
+            zlin[4 * i - 64 + 2] = xl[18 * (1 + i)];
+            zlin[4 * i - 64 + 3] = xr[18 * (1 + i)];
+
+            V0(0)
+            V2(1)
+            V1(2)
+            V2(3) V1(4) V2(5) V1(6) V2(7)
+
+            {
+#ifndef MINIMP3_FLOAT_OUTPUT
+#if HAVE_SSE
+                static const f4 g_max = { 32767.0f, 32767.0f, 32767.0f,
+                                          32767.0f };
+                static const f4 g_min = { -32768.0f, -32768.0f, -32768.0f,
+                                          -32768.0f };
+                __m128i pcm8 = _mm_packs_epi32(
+                    _mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(a, g_max), g_min)),
+                    _mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(b, g_max), g_min)));
+                dstr[(15 - i) * nch] = _mm_extract_epi16(pcm8, 1);
+                dstr[(17 + i) * nch] = _mm_extract_epi16(pcm8, 5);
+                dstl[(15 - i) * nch] = _mm_extract_epi16(pcm8, 0);
+                dstl[(17 + i) * nch] = _mm_extract_epi16(pcm8, 4);
+                dstr[(47 - i) * nch] = _mm_extract_epi16(pcm8, 3);
+                dstr[(49 + i) * nch] = _mm_extract_epi16(pcm8, 7);
+                dstl[(47 - i) * nch] = _mm_extract_epi16(pcm8, 2);
+                dstl[(49 + i) * nch] = _mm_extract_epi16(pcm8, 6);
+#else  /* HAVE_SSE */
+                int16x4_t pcma, pcmb;
+                a = VADD(a, VSET(0.5f));
+                b = VADD(b, VSET(0.5f));
+                pcma = vqmovn_s32(
+                    vqaddq_s32(vcvtq_s32_f32(a),
+                               vreinterpretq_s32_u32(vcltq_f32(a, VSET(0)))));
+                pcmb = vqmovn_s32(
+                    vqaddq_s32(vcvtq_s32_f32(b),
+                               vreinterpretq_s32_u32(vcltq_f32(b, VSET(0)))));
+                vst1_lane_s16(dstr + (15 - i) * nch, pcma, 1);
+                vst1_lane_s16(dstr + (17 + i) * nch, pcmb, 1);
+                vst1_lane_s16(dstl + (15 - i) * nch, pcma, 0);
+                vst1_lane_s16(dstl + (17 + i) * nch, pcmb, 0);
+                vst1_lane_s16(dstr + (47 - i) * nch, pcma, 3);
+                vst1_lane_s16(dstr + (49 + i) * nch, pcmb, 3);
+                vst1_lane_s16(dstl + (47 - i) * nch, pcma, 2);
+                vst1_lane_s16(dstl + (49 + i) * nch, pcmb, 2);
+#endif /* HAVE_SSE */
+
+#else /* MINIMP3_FLOAT_OUTPUT */
+
+                static const f4 g_scale = { 1.0f / 32768.0f, 1.0f / 32768.0f,
+                                            1.0f / 32768.0f, 1.0f / 32768.0f };
+                a = VMUL(a, g_scale);
+                b = VMUL(b, g_scale);
+#if HAVE_SSE
+                _mm_store_ss(dstr + (15 - i) * nch,
+                             _mm_shuffle_ps(a, a, _MM_SHUFFLE(1, 1, 1, 1)));
+                _mm_store_ss(dstr + (17 + i) * nch,
+                             _mm_shuffle_ps(b, b, _MM_SHUFFLE(1, 1, 1, 1)));
+                _mm_store_ss(dstl + (15 - i) * nch,
+                             _mm_shuffle_ps(a, a, _MM_SHUFFLE(0, 0, 0, 0)));
+                _mm_store_ss(dstl + (17 + i) * nch,
+                             _mm_shuffle_ps(b, b, _MM_SHUFFLE(0, 0, 0, 0)));
+                _mm_store_ss(dstr + (47 - i) * nch,
+                             _mm_shuffle_ps(a, a, _MM_SHUFFLE(3, 3, 3, 3)));
+                _mm_store_ss(dstr + (49 + i) * nch,
+                             _mm_shuffle_ps(b, b, _MM_SHUFFLE(3, 3, 3, 3)));
+                _mm_store_ss(dstl + (47 - i) * nch,
+                             _mm_shuffle_ps(a, a, _MM_SHUFFLE(2, 2, 2, 2)));
+                _mm_store_ss(dstl + (49 + i) * nch,
+                             _mm_shuffle_ps(b, b, _MM_SHUFFLE(2, 2, 2, 2)));
+#else  /* HAVE_SSE */
+                vst1q_lane_f32(dstr + (15 - i) * nch, a, 1);
+                vst1q_lane_f32(dstr + (17 + i) * nch, b, 1);
+                vst1q_lane_f32(dstl + (15 - i) * nch, a, 0);
+                vst1q_lane_f32(dstl + (17 + i) * nch, b, 0);
+                vst1q_lane_f32(dstr + (47 - i) * nch, a, 3);
+                vst1q_lane_f32(dstr + (49 + i) * nch, b, 3);
+                vst1q_lane_f32(dstl + (47 - i) * nch, a, 2);
+                vst1q_lane_f32(dstl + (49 + i) * nch, b, 2);
+#endif /* HAVE_SSE */
+#endif /* MINIMP3_FLOAT_OUTPUT */
+            }
+        }
+    else
+#endif /* HAVE_SIMD */
+#ifdef MINIMP3_ONLY_SIMD
+    {
+    } /* for HAVE_SIMD=1, MINIMP3_ONLY_SIMD=1 case we do not need non-intrinsic
+         "else" branch */
+#else /* MINIMP3_ONLY_SIMD */
+    for (i = 14; i >= 0; i--) {
+#define LOAD(k)                        \
+    float w0 = *w++;                   \
+    float w1 = *w++;                   \
+    float *vz = &zlin[4 * i - k * 64]; \
+    float *vy = &zlin[4 * i - (15 - k) * 64];
+#define S0(k)                                                               \
+    {                                                                       \
+        int j;                                                              \
+        LOAD(k);                                                            \
+        for (j = 0; j < 4; j++)                                             \
+            b[j] = vz[j] * w1 + vy[j] * w0, a[j] = vz[j] * w0 - vy[j] * w1; \
+    }
+#define S1(k)                                                                 \
+    {                                                                         \
+        int j;                                                                \
+        LOAD(k);                                                              \
+        for (j = 0; j < 4; j++)                                               \
+            b[j] += vz[j] * w1 + vy[j] * w0, a[j] += vz[j] * w0 - vy[j] * w1; \
+    }
+#define S2(k)                                                                 \
+    {                                                                         \
+        int j;                                                                \
+        LOAD(k);                                                              \
+        for (j = 0; j < 4; j++)                                               \
+            b[j] += vz[j] * w1 + vy[j] * w0, a[j] += vy[j] * w1 - vz[j] * w0; \
+    }
+        float a[4], b[4];
+
+        zlin[4 * i] = xl[18 * (31 - i)];
+        zlin[4 * i + 1] = xr[18 * (31 - i)];
+        zlin[4 * i + 2] = xl[1 + 18 * (31 - i)];
+        zlin[4 * i + 3] = xr[1 + 18 * (31 - i)];
+        zlin[4 * (i + 16)] = xl[1 + 18 * (1 + i)];
+        zlin[4 * (i + 16) + 1] = xr[1 + 18 * (1 + i)];
+        zlin[4 * (i - 16) + 2] = xl[18 * (1 + i)];
+        zlin[4 * (i - 16) + 3] = xr[18 * (1 + i)];
+
+        S0(0)
+        S2(1)
+        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]);
+        dstl[(15 - i) * nch] = mp3d_scale_pcm(a[0]);
+        dstl[(17 + i) * nch] = mp3d_scale_pcm(b[0]);
+        dstr[(47 - i) * nch] = mp3d_scale_pcm(a[3]);
+        dstr[(49 + i) * nch] = mp3d_scale_pcm(b[3]);
+        dstl[(47 - i) * nch] = mp3d_scale_pcm(a[2]);
+        dstl[(49 + i) * nch] = mp3d_scale_pcm(b[2]);
+    }
+#endif /* MINIMP3_ONLY_SIMD */
+}
+
+static void mp3d_synth_granule(float *qmf_state, float *grbuf, int nbands,
+                               int nch, mp3d_sample_t *pcm, float *lins) {
+    int i;
+    for (i = 0; i < nch; i++) {
+        mp3d_DCT_II(grbuf + 576 * i, nbands);
+    }
+
+    memcpy(lins, qmf_state, sizeof(float) * 15 * 64);
+
+    for (i = 0; i < nbands; i += 2) {
+        mp3d_synth(grbuf + i, pcm + 32 * nch * i, nch, lins + i * 64);
+    }
+#ifndef MINIMP3_NONSTANDARD_BUT_LOGICAL
+    if (nch == 1) {
+        for (i = 0; i < 15 * 64; i += 2) {
+            qmf_state[i] = lins[nbands * 64 + i];
+        }
+    } else
+#endif /* MINIMP3_NONSTANDARD_BUT_LOGICAL */
+    {
+        memcpy(qmf_state, lins + nbands * 64, sizeof(float) * 15 * 64);
+    }
+}
+
+static int mp3d_match_frame(const uint8_t *hdr, int mp3_bytes,
+                            int frame_bytes) {
+    int i, nmatch;
+    for (i = 0, nmatch = 0; nmatch < MAX_FRAME_SYNC_MATCHES; nmatch++) {
+        i += hdr_frame_bytes(hdr + i, frame_bytes) + hdr_padding(hdr + i);
+        if (i + HDR_SIZE > mp3_bytes) return nmatch > 0;
+        if (!hdr_compare(hdr, hdr + i)) return 0;
+    }
+    return 1;
+}
+
+static int mp3d_find_frame(const uint8_t *mp3, int mp3_bytes,
+                           int *free_format_bytes, int *ptr_frame_bytes) {
+    int i, k;
+    for (i = 0; i < mp3_bytes - HDR_SIZE; i++, mp3++) {
+        if (hdr_valid(mp3)) {
+            int frame_bytes = hdr_frame_bytes(mp3, *free_format_bytes);
+            int frame_and_padding = frame_bytes + hdr_padding(mp3);
+
+            for (k = HDR_SIZE; !frame_bytes && k < MAX_FREE_FORMAT_FRAME_SIZE &&
+                               i + 2 * k < mp3_bytes - HDR_SIZE;
+                 k++) {
+                if (hdr_compare(mp3, mp3 + k)) {
+                    int fb = k - hdr_padding(mp3);
+                    int nextfb = fb + hdr_padding(mp3 + k);
+                    if (i + k + nextfb + HDR_SIZE > mp3_bytes ||
+                        !hdr_compare(mp3, mp3 + k + nextfb))
+                        continue;
+                    frame_and_padding = k;
+                    frame_bytes = fb;
+                    *free_format_bytes = fb;
+                }
+            }
+            if ((frame_bytes && i + frame_and_padding <= mp3_bytes &&
+                 mp3d_match_frame(mp3, mp3_bytes - i, frame_bytes)) ||
+                (!i && frame_and_padding == mp3_bytes)) {
+                *ptr_frame_bytes = frame_and_padding;
+                return i;
+            }
+            *free_format_bytes = 0;
+        }
+    }
+    *ptr_frame_bytes = 0;
+    return mp3_bytes;
+}
+
+void mp3dec_init(mp3dec_t *dec) { dec->header[0] = 0; }
+
+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;
+    const uint8_t *hdr;
+    bs_t bs_frame[1];
+    if (mp3_bytes > 4 && dec->header[0] == 0xff &&
+        hdr_compare(dec->header, mp3)) {
+        frame_size =
+            hdr_frame_bytes(mp3, dec->free_format_bytes) + hdr_padding(mp3);
+        if (frame_size != mp3_bytes && (frame_size + HDR_SIZE > mp3_bytes ||
+                                        !hdr_compare(mp3, mp3 + frame_size))) {
+            frame_size = 0;
+        }
+    }
+    if (!frame_size) {
+        memset(dec, 0, sizeof(mp3dec_t));
+        i = mp3d_find_frame(mp3, mp3_bytes, &dec->free_format_bytes,
+                            &frame_size);
+        if (!frame_size || i + frame_size > mp3_bytes) {
+            info->frame_bytes = i;
+            return 0;
+        }
+    }
+
+    hdr = mp3 + i;
+    memcpy(dec->header, hdr, HDR_SIZE);
+    info->frame_bytes = i + frame_size;
+    info->frame_offset = i;
+    info->channels = HDR_IS_MONO(hdr) ? 1 : 2;
+    info->hz = hdr_sample_rate_hz(hdr);
+    info->layer = 4 - HDR_GET_LAYER(hdr);
+    info->bitrate_kbps = hdr_bitrate_kbps(hdr);
+
+    if (!pcm) {
+        return hdr_frame_samples(hdr);
+    }
+
+    bs_init(bs_frame, hdr + HDR_SIZE, frame_size - HDR_SIZE);
+    if (HDR_IS_CRC(hdr)) {
+        get_bits(bs_frame, 16);
+    }
+
+    if (info->layer == 3) {
+        int main_data_begin =
+            L3_read_side_info(bs_frame, dec->scratch.gr_info, hdr);
+        if (main_data_begin < 0 || bs_frame->pos > bs_frame->limit) {
+            mp3dec_init(dec);
+            return 0;
+        }
+        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,
+                          info->channels);
+                mp3d_synth_granule(dec->qmf_state, dec->scratch.grbuf[0], 18,
+                                   info->channels, pcm, dec->scratch.syn[0]);
+            }
+        }
+        L3_save_reservoir(dec, &dec->scratch);
+    } else {
+#ifdef MINIMP3_ONLY_MP3
+        return 0;
+#else  /* MINIMP3_ONLY_MP3 */
+        L12_read_scale_info(hdr, bs_frame, &dec->sci);
+
+        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))) {
+                i = 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));
+                pcm += 384 * info->channels;
+            }
+            if (bs_frame->pos > bs_frame->limit) {
+                mp3dec_init(dec);
+                return 0;
+            }
+        }
+#endif /* MINIMP3_ONLY_MP3 */
+    }
+    return success * hdr_frame_samples(dec->header);
+}
+
+#ifdef MINIMP3_FLOAT_OUTPUT
+void mp3dec_f32_to_s16(const float *in, int16_t *out, int num_samples) {
+    int i = 0;
+#if HAVE_SIMD
+    int aligned_count = num_samples & ~7;
+    for (; i < aligned_count; i += 8) {
+        static const f4 g_scale = { 32768.0f, 32768.0f, 32768.0f, 32768.0f };
+        f4 a = VMUL(VLD(&in[i]), g_scale);
+        f4 b = VMUL(VLD(&in[i + 4]), g_scale);
+#if HAVE_SSE
+        static const f4 g_max = { 32767.0f, 32767.0f, 32767.0f, 32767.0f };
+        static const f4 g_min = { -32768.0f, -32768.0f, -32768.0f, -32768.0f };
+        __m128i pcm8 = _mm_packs_epi32(
+            _mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(a, g_max), g_min)),
+            _mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(b, g_max), g_min)));
+        out[i] = _mm_extract_epi16(pcm8, 0);
+        out[i + 1] = _mm_extract_epi16(pcm8, 1);
+        out[i + 2] = _mm_extract_epi16(pcm8, 2);
+        out[i + 3] = _mm_extract_epi16(pcm8, 3);
+        out[i + 4] = _mm_extract_epi16(pcm8, 4);
+        out[i + 5] = _mm_extract_epi16(pcm8, 5);
+        out[i + 6] = _mm_extract_epi16(pcm8, 6);
+        out[i + 7] = _mm_extract_epi16(pcm8, 7);
+#else  /* HAVE_SSE */
+        int16x4_t pcma, pcmb;
+        a = VADD(a, VSET(0.5f));
+        b = VADD(b, VSET(0.5f));
+        pcma = vqmovn_s32(vqaddq_s32(
+            vcvtq_s32_f32(a), vreinterpretq_s32_u32(vcltq_f32(a, VSET(0)))));
+        pcmb = vqmovn_s32(vqaddq_s32(
+            vcvtq_s32_f32(b), vreinterpretq_s32_u32(vcltq_f32(b, VSET(0)))));
+        vst1_lane_s16(out + i, pcma, 0);
+        vst1_lane_s16(out + i + 1, pcma, 1);
+        vst1_lane_s16(out + i + 2, pcma, 2);
+        vst1_lane_s16(out + i + 3, pcma, 3);
+        vst1_lane_s16(out + i + 4, pcmb, 0);
+        vst1_lane_s16(out + i + 5, pcmb, 1);
+        vst1_lane_s16(out + i + 6, pcmb, 2);
+        vst1_lane_s16(out + i + 7, pcmb, 3);
+#endif /* HAVE_SSE */
+    }
+#endif /* HAVE_SIMD */
+    for (; i < num_samples; i++) {
+        float sample = in[i] * 32768.0f;
+        if (sample >= 32766.5)
+            out[i] = (int16_t)32767;
+        else if (sample <= -32767.5)
+            out[i] = (int16_t)-32768;
+        else {
+            int16_t s = (int16_t)(sample + .5f);
+            s -= (s < 0); /* away from zero, to be compliant */
+            out[i] = s;
+        }
+    }
+}
+#endif /* MINIMP3_FLOAT_OUTPUT */
+#endif /* MINIMP3_IMPLEMENTATION && !_MINIMP3_IMPLEMENTATION_GUARD */
diff --git a/components/micropython/usermodule/micropython.cmake b/components/micropython/usermodule/micropython.cmake
index 171b8df593d9cc7897d26467fcbe594a411ca688..14551f6e26ce70078c9cce90234b942a17c9f55a 100644
--- a/components/micropython/usermodule/micropython.cmake
+++ b/components/micropython/usermodule/micropython.cmake
@@ -16,6 +16,7 @@ target_sources(usermod_badge23 INTERFACE
     ${CMAKE_CURRENT_LIST_DIR}/mp_sys_display.c
     ${CMAKE_CURRENT_LIST_DIR}/mp_sys_kernel.c
     ${CMAKE_CURRENT_LIST_DIR}/mp_uctx.c
+    ${CMAKE_CURRENT_LIST_DIR}/mp_media.c
 )
 
 target_include_directories(usermod_badge23 INTERFACE
diff --git a/components/micropython/usermodule/mp_media.c b/components/micropython/usermodule/mp_media.c
new file mode 100644
index 0000000000000000000000000000000000000000..aa813913f6683e8384b89ff202b439278d043f36
--- /dev/null
+++ b/components/micropython/usermodule/mp_media.c
@@ -0,0 +1,50 @@
+#include <st3m_media.h>
+
+#include "py/builtin.h"
+#include "py/runtime.h"
+
+typedef struct _mp_ctx_obj_t {
+    mp_obj_base_t base;
+    Ctx *ctx;
+    mp_obj_t user_data;
+} mp_ctx_obj_t;
+
+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_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);
+    st3m_media_draw(uctx->ctx);
+    return 0;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_draw_obj, mp_draw);
+
+STATIC mp_obj_t mp_think(mp_obj_t ms_in) {
+    st3m_media_think(mp_obj_get_float(ms_in));
+    return 0;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_think_obj, mp_think);
+
+STATIC mp_obj_t mp_stop(void) {
+    st3m_media_stop();
+    return 0;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_stop_obj, mp_stop);
+
+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_load), MP_ROM_PTR(&mp_load_obj) },
+};
+
+STATIC MP_DEFINE_CONST_DICT(globals, globals_table);
+
+const mp_obj_module_t mp_module_media = {
+    .base = { &mp_type_module },
+    .globals = (mp_obj_dict_t *)&globals,
+};
+
+MP_REGISTER_MODULE(MP_QSTR_media, mp_module_media);
diff --git a/components/st3m/CMakeLists.txt b/components/st3m/CMakeLists.txt
index e0beec286e02394ad7b271f7b9dc03047f701fb2..81089d86b9630b9043380ded433b6a6feedf4b78 100644
--- a/components/st3m/CMakeLists.txt
+++ b/components/st3m/CMakeLists.txt
@@ -15,6 +15,8 @@ idf_component_register(
         st3m_usb_msc.c
         st3m_usb.c
         st3m_console.c
+        st3m_media.c
+        st3m_mode.c
         st3m_mode.c
         st3m_captouch.c
         st3m_ringbuffer.c
@@ -36,6 +38,9 @@ idf_component_register(
         esp_timer
         esp_netif
         usb
+        video_mpeg
+        audio_mod
+        audio_mp3
 )
 
 idf_component_get_property(tusb_lib tinyusb COMPONENT_LIB)
diff --git a/components/st3m/st3m_media.c b/components/st3m/st3m_media.c
new file mode 100644
index 0000000000000000000000000000000000000000..021269a43e85bfe5348e1f8386470af7caa90af5
--- /dev/null
+++ b/components/st3m/st3m_media.c
@@ -0,0 +1,136 @@
+#include "st3m_media.h"
+#include "st3m_audio.h"
+
+#include <math.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "esp_log.h"
+#include "freertos/FreeRTOS.h"
+
+#ifdef CONFIG_FLOW3R_CTX_FLAVOUR_FULL
+static st3m_media *audio_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];
+
+void st3m_media_audio_out(int16_t *rx, int16_t *tx, uint16_t len) {
+    if (!audio_media) return;
+    for (int i = 0; i < len; i++) {
+        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 (!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;
+}
+
+void st3m_media_stop(void) {
+    if (audio_media && audio_media->destroy) audio_media->destroy(audio_media);
+    audio_media = 0;
+    st3m_audio_set_player_function(st3m_audio_player_function_dummy);
+}
+
+void st3m_media_pause(void) {
+    if (!audio_media) return;
+    audio_media->paused = 1;
+}
+
+void st3m_media_play(void) {
+    if (!audio_media) return;
+    audio_media->paused = 0;
+}
+
+int st3m_media_is_playing(void) {
+    if (!audio_media) return 0;
+    return !audio_media->paused;
+}
+
+float st3m_media_get_duration(void) {
+    if (!audio_media) return 0;
+    return audio_media->duration;
+}
+
+float st3m_media_get_position(void) {
+    if (!audio_media) return 0;
+    return audio_media->position;
+}
+
+float st3m_media_get_time(void) {
+    if (!audio_media) return 0;
+    return audio_media->time;
+}
+
+void st3m_media_seek(float position) {
+    if (!audio_media) return;
+    audio_media->seek = position;
+}
+
+void st3m_media_seek_relative(float time) {
+    if (!audio_media) return;
+    st3m_media_seek((audio_media->position * audio_media->duration) + time);
+}
+
+void st3m_media_draw(Ctx *ctx) {
+    if (audio_media && audio_media->draw)
+        audio_media->draw(audio_media, ctx);
+}
+
+void st3m_media_think(float ms) {
+    if (audio_media && audio_media->think) audio_media->think(audio_media, ms);
+}
+
+char *st3m_media_get_string(const char *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 (!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 (!audio_media || !audio_media->set) return;
+    return audio_media->set(audio_media, key, value);
+}
+
+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 = st3m_media_load_mpg1(path);
+    } else if (strstr(path, ".mod")) {
+        st3m_media_stop();
+        audio_media = st3m_media_load_mod(path);
+    } else if (strstr(path, "mp3") || !strncmp (path, "http://", 7)) {
+        st3m_media_stop();
+        audio_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;
+
+    return 1;
+}
+
+#endif
diff --git a/components/st3m/st3m_media.h b/components/st3m/st3m_media.h
new file mode 100644
index 0000000000000000000000000000000000000000..fbf928b8eb164fc7e4824dc15f6c6d27bb07be5e
--- /dev/null
+++ b/components/st3m/st3m_media.h
@@ -0,0 +1,103 @@
+#pragma once
+
+#include <ctx.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#define AUDIO_BUF_SIZE (8192)
+
+typedef struct _st3m_media st3m_media;
+
+struct _st3m_media {
+    // set a tunable, to a numeric value - available tunables depends on
+    // decoder
+    void (*set)(st3m_media *media, const char *key, float value);
+    // get a property/or tunable, defaulting to -1 for nonexisting keys
+    float (*get)(st3m_media *media, const char *key);
+    // get a string property/metadata, NULL if not existing or a string
+    // to be freed
+    char *(*get_string)(st3m_media *media, const char *key);
+
+    // 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);
+
+    // do decoding work corresponding to passed time
+    void (*think)(st3m_media *media, float ms);
+
+    // pointer to global pcm output buffer
+    int16_t *audio_buffer;
+    // playback head
+    int audio_r;
+    // queuing/writing head
+    int audio_w;
+
+    // Duration of media in seconds or -1 for infinite/streaming media
+    // at worst approximation of some unit, set by decoder.
+    float duration;
+
+    // currently played back position - set by decoder
+    float position;
+
+    // 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,
+    // this is toggled by st3m_media_play | st3m_media_pause
+    int paused;
+};
+
+
+// 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);
+// get current playback time in seconds
+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);
+// seek a relative amount of seconds forward or with negative values back
+void st3m_media_seek_relative(float seconds_jump);
+
+// get decoder specific string or NULL if not existing, free returned value
+//  common values:
+//     "title"  "artist"
+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
+// 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);
+
diff --git a/components/video_mpeg/CMakeLists.txt b/components/video_mpeg/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..50e483046ccb5c3c2a6b1dd6082094dcb4659e26
--- /dev/null
+++ b/components/video_mpeg/CMakeLists.txt
@@ -0,0 +1,8 @@
+idf_component_register(
+    SRCS
+        video_mpeg.c
+    INCLUDE_DIRS
+        .
+        ../ctx
+        ../st3m
+)
diff --git a/components/video_mpeg/pl_mpeg.h b/components/video_mpeg/pl_mpeg.h
new file mode 100644
index 0000000000000000000000000000000000000000..d24b3e83bb1e891cf23868664685ee6ad8c5e505
--- /dev/null
+++ b/components/video_mpeg/pl_mpeg.h
@@ -0,0 +1,4207 @@
+/*
+PL_MPEG - MPEG1 Video decoder, MP2 Audio decoder, MPEG-PS demuxer
+
+Dominic Szablewski - https://phoboslab.org
+
+
+-- LICENSE: The MIT License(MIT)
+
+Copyright(c) 2019 Dominic Szablewski
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files(the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions :
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+
+
+-- Synopsis
+
+// Define `PL_MPEG_IMPLEMENTATION` in *one* C/C++ file before including this
+// library to create the implementation.
+
+#define PL_MPEG_IMPLEMENTATION
+#include "plmpeg.h"
+
+// This function gets called for each decoded video frame
+void my_video_callback(plm_t *plm, plm_frame_t *frame, void *user) {
+        // Do something with frame->y.data, frame->cr.data, frame->cb.data
+}
+
+// This function gets called for each decoded audio frame
+void my_audio_callback(plm_t *plm, plm_samples_t *frame, void *user) {
+        // Do something with samples->interleaved
+}
+
+// Load a .mpg (MPEG Program Stream) file
+plm_t *plm = plm_create_with_filename("some-file.mpg");
+
+// Install the video & audio decode callbacks
+plm_set_video_decode_callback(plm, my_video_callback, my_data);
+plm_set_audio_decode_callback(plm, my_audio_callback, my_data);
+
+
+// Decode
+do {
+        plm_decode(plm, time_since_last_call);
+} while (!plm_has_ended(plm));
+
+// All done
+plm_destroy(plm);
+
+
+
+-- Documentation
+
+This library provides several interfaces to load, demux and decode MPEG video
+and audio data. A high-level API combines the demuxer, video & audio decoders
+in an easy to use wrapper.
+
+Lower-level APIs for accessing the demuxer, video decoder and audio decoder,
+as well as providing different data sources are also available.
+
+Interfaces are written in an object orientet style, meaning you create object
+instances via various different constructor functions (plm_*create()),
+do some work on them and later dispose them via plm_*destroy().
+
+plm_* ......... the high-level interface, combining demuxer and decoders
+plm_buffer_* .. the data source used by all interfaces
+plm_demux_* ... the MPEG-PS demuxer
+plm_video_* ... the MPEG1 Video ("mpeg1") decoder
+plm_audio_* ... the MPEG1 Audio Layer II ("mp2") decoder
+
+
+With the high-level interface you have two options to decode video & audio:
+
+ 1. Use plm_decode() and just hand over the delta time since the last call.
+    It will decode everything needed and call your callbacks (specified through
+    plm_set_{video|audio}_decode_callback()) any number of times.
+
+ 2. Use plm_decode_video() and plm_decode_audio() to decode exactly one
+    frame of video or audio data at a time. How you handle the synchronization
+    of both streams is up to you.
+
+If you only want to decode video *or* audio through these functions, you should
+disable the other stream (plm_set_{video|audio}_enabled(FALSE))
+
+Video data is decoded into a struct with all 3 planes (Y, Cr, Cb) stored in
+separate buffers. You can either convert this to RGB on the CPU (slow) via the
+plm_frame_to_rgb() function or do it on the GPU with the following matrix:
+
+mat4 bt601 = mat4(
+        1.16438,  0.00000,  1.59603, -0.87079,
+        1.16438, -0.39176, -0.81297,  0.52959,
+        1.16438,  2.01723,  0.00000, -1.08139,
+        0, 0, 0, 1
+);
+gl_FragColor = vec4(y, cb, cr, 1.0) * bt601;
+
+Audio data is decoded into a struct with either one single float array with the
+samples for the left and right channel interleaved, or if the
+PLM_AUDIO_SEPARATE_CHANNELS is defined *before* including this library, into
+two separate float arrays - one for each channel.
+
+
+Data can be supplied to the high level interface, the demuxer and the decoders
+in three different ways:
+
+ 1. Using plm_create_from_filename() or with a file handle with
+    plm_create_from_file().
+
+ 2. Using plm_create_with_memory() and supplying a pointer to memory that
+    contains the whole file.
+
+ 3. Using plm_create_with_buffer(), supplying your own plm_buffer_t instance and
+    periodically writing to this buffer.
+
+When using your own plm_buffer_t instance, you can fill this buffer using
+plm_buffer_write(). You can either monitor plm_buffer_get_remaining() and push
+data when appropriate, or install a callback on the buffer with
+plm_buffer_set_load_callback() that gets called whenever the buffer needs more
+data.
+
+A buffer created with plm_buffer_create_with_capacity() is treated as a ring
+buffer, meaning that data that has already been read, will be discarded. In
+contrast, a buffer created with plm_buffer_create_for_appending() will keep all
+data written to it in memory. This enables seeking in the already loaded data.
+
+
+There should be no need to use the lower level plm_demux_*, plm_video_* and
+plm_audio_* functions, if all you want to do is read/decode an MPEG-PS file.
+However, if you get raw mpeg1video data or raw mp2 audio data from a different
+source, these functions can be used to decode the raw data directly. Similarly,
+if you only want to analyze an MPEG-PS file or extract raw video or audio
+packets from it, you can use the plm_demux_* functions.
+
+
+This library uses malloc(), realloc() and free() to manage memory. Typically
+all allocation happens up-front when creating the interface. However, the
+default buffer size may be too small for certain inputs. In these cases plmpeg
+will realloc() the buffer with a larger size whenever needed. You can configure
+the default buffer size by defining PLM_BUFFER_DEFAULT_SIZE *before*
+including this library.
+
+
+See below for detailed the API documentation.
+
+*/
+
+#ifndef PL_MPEG_H
+#define PL_MPEG_H
+
+#include <stdint.h>
+#include <stdio.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// -----------------------------------------------------------------------------
+// Public Data Types
+
+// Object types for the various interfaces
+
+typedef struct plm_t plm_t;
+typedef struct plm_buffer_t plm_buffer_t;
+typedef struct plm_demux_t plm_demux_t;
+typedef struct plm_video_t plm_video_t;
+typedef struct plm_audio_t plm_audio_t;
+
+// Demuxed MPEG PS packet
+// The type maps directly to the various MPEG-PES start codes. PTS is the
+// presentation time stamp of the packet in seconds. Note that not all packets
+// have a PTS value, indicated by PLM_PACKET_INVALID_TS.
+
+#define PLM_PACKET_INVALID_TS -1
+
+typedef struct {
+    int type;
+    float pts;
+    size_t length;
+    uint8_t *data;
+} plm_packet_t;
+
+// Decoded Video Plane
+// The byte length of the data is width * height. Note that different planes
+// have different sizes: the Luma plane (Y) is double the size of each of
+// the two Chroma planes (Cr, Cb) - i.e. 4 times the byte length.
+// Also note that the size of the plane does *not* denote the size of the
+// displayed frame. The sizes of planes are always rounded up to the nearest
+// macroblock (16px).
+
+typedef struct {
+    unsigned int width;
+    unsigned int height;
+    uint8_t *data;
+} plm_plane_t;
+
+// Decoded Video Frame
+// width and height denote the desired display size of the frame. This may be
+// different from the internal size of the 3 planes.
+
+typedef struct {
+    float time;
+    unsigned int width;
+    unsigned int height;
+    plm_plane_t y;
+    plm_plane_t cr;
+    plm_plane_t cb;
+} plm_frame_t;
+
+// Callback function type for decoded video frames used by the high-level
+// plm_* interface
+
+typedef void (*plm_video_decode_callback)(plm_t *self, plm_frame_t *frame,
+                                          void *user);
+
+// Decoded Audio Samples
+// Samples are stored as normalized (-1, 1) float either interleaved, or if
+// PLM_AUDIO_SEPARATE_CHANNELS is defined, in two separate arrays.
+// The `count` is always PLM_AUDIO_SAMPLES_PER_FRAME and just there for
+// convenience.
+
+#define PLM_AUDIO_SAMPLES_PER_FRAME 1152
+
+typedef struct {
+    float time;
+    unsigned int count;
+#ifdef PLM_AUDIO_SEPARATE_CHANNELS
+    float left[PLM_AUDIO_SAMPLES_PER_FRAME];
+    float right[PLM_AUDIO_SAMPLES_PER_FRAME];
+#else
+    float interleaved[PLM_AUDIO_SAMPLES_PER_FRAME * 2];
+#endif
+} plm_samples_t;
+
+// Callback function type for decoded audio samples used by the high-level
+// plm_* interface
+
+typedef void (*plm_audio_decode_callback)(plm_t *self, plm_samples_t *samples,
+                                          void *user);
+
+// Callback function for plm_buffer when it needs more data
+
+typedef void (*plm_buffer_load_callback)(plm_buffer_t *self, void *user);
+
+// -----------------------------------------------------------------------------
+// plm_* public API
+// High-Level API for loading/demuxing/decoding MPEG-PS data
+
+// Create a plmpeg instance with a filename. Returns NULL if the file could not
+// be opened.
+
+plm_t *plm_create_with_filename(const char *filename);
+
+// Create a plmpeg instance with a file handle. Pass TRUE to close_when_done to
+// let plmpeg call fclose() on the handle when plm_destroy() is called.
+
+plm_t *plm_create_with_file(FILE *fh, int close_when_done);
+
+// Create a plmpeg instance with a pointer to memory as source. This assumes the
+// whole file is in memory. The memory is not copied. Pass TRUE to
+// free_when_done to let plmpeg call free() on the pointer when plm_destroy()
+// is called.
+
+plm_t *plm_create_with_memory(uint8_t *bytes, size_t length,
+                              int free_when_done);
+
+// Create a plmpeg instance with a plm_buffer as source. Pass TRUE to
+// destroy_when_done to let plmpeg call plm_buffer_destroy() on the buffer when
+// plm_destroy() is called.
+
+plm_t *plm_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done);
+
+// Destroy a plmpeg instance and free all data.
+
+void plm_destroy(plm_t *self);
+
+// Get whether we have headers on all available streams and we can accurately
+// report the number of video/audio streams, video dimensions, framerate and
+// audio samplerate.
+// This returns FALSE if the file is not an MPEG-PS file or - when not using a
+// file as source - when not enough data is available yet.
+
+int plm_has_headers(plm_t *self);
+
+// Get or set whether video decoding is enabled. Default TRUE.
+
+int plm_get_video_enabled(plm_t *self);
+void plm_set_video_enabled(plm_t *self, int enabled);
+
+// Get the number of video streams (0--1) reported in the system header.
+
+int plm_get_num_video_streams(plm_t *self);
+
+// Get the display width/height of the video stream.
+
+int plm_get_width(plm_t *self);
+int plm_get_height(plm_t *self);
+
+// Get the framerate of the video stream in frames per second.
+
+float plm_get_framerate(plm_t *self);
+
+// Get or set whether audio decoding is enabled. Default TRUE.
+
+int plm_get_audio_enabled(plm_t *self);
+void plm_set_audio_enabled(plm_t *self, int enabled);
+
+// Get the number of audio streams (0--4) reported in the system header.
+
+int plm_get_num_audio_streams(plm_t *self);
+
+// Set the desired audio stream (0--3). Default 0.
+
+void plm_set_audio_stream(plm_t *self, int stream_index);
+
+// Get the samplerate of the audio stream in samples per second.
+
+int plm_get_samplerate(plm_t *self);
+
+// Get or set the audio lead time in seconds - the time in which audio samples
+// are decoded in advance (or behind) the video decode time. Typically this
+// should be set to the duration of the buffer of the audio API that you use
+// for output. E.g. for SDL2: (SDL_AudioSpec.samples / samplerate)
+
+float plm_get_audio_lead_time(plm_t *self);
+void plm_set_audio_lead_time(plm_t *self, float lead_time);
+
+// Get the current internal time in seconds.
+
+float plm_get_time(plm_t *self);
+
+// Get the video duration of the underlying source in seconds.
+
+float plm_get_duration(plm_t *self);
+
+// Rewind all buffers back to the beginning.
+
+void plm_rewind(plm_t *self);
+
+// Get or set looping. Default FALSE.
+
+int plm_get_loop(plm_t *self);
+void plm_set_loop(plm_t *self, int loop);
+
+// Get whether the file has ended. If looping is enabled, this will always
+// return FALSE.
+
+int plm_has_ended(plm_t *self);
+
+// Set the callback for decoded video frames used with plm_decode(). If no
+// callback is set, video data will be ignored and not be decoded. The *user
+// Parameter will be passed to your callback.
+
+void plm_set_video_decode_callback(plm_t *self, plm_video_decode_callback fp,
+                                   void *user);
+
+// Set the callback for decoded audio samples used with plm_decode(). If no
+// callback is set, audio data will be ignored and not be decoded. The *user
+// Parameter will be passed to your callback.
+
+void plm_set_audio_decode_callback(plm_t *self, plm_audio_decode_callback fp,
+                                   void *user);
+
+// Advance the internal timer by seconds and decode video/audio up to this time.
+// This will call the video_decode_callback and audio_decode_callback any number
+// of times. A frame-skip is not implemented, i.e. everything up to current time
+// will be decoded.
+
+void plm_decode(plm_t *self, float seconds);
+
+// Decode and return one video frame. Returns NULL if no frame could be decoded
+// (either because the source ended or data is corrupt). If you only want to
+// decode video, you should disable audio via plm_set_audio_enabled().
+// The returned plm_frame_t is valid until the next call to plm_decode_video()
+// or until plm_destroy() is called.
+
+plm_frame_t *plm_decode_video(plm_t *self);
+
+// Decode and return one audio frame. Returns NULL if no frame could be decoded
+// (either because the source ended or data is corrupt). If you only want to
+// decode audio, you should disable video via plm_set_video_enabled().
+// The returned plm_samples_t is valid until the next call to plm_decode_audio()
+// or until plm_destroy() is called.
+
+plm_samples_t *plm_decode_audio(plm_t *self);
+
+// Seek to the specified time, clamped between 0 -- duration. This can only be
+// used when the underlying plm_buffer is seekable, i.e. for files, fixed
+// memory buffers or _for_appending buffers.
+// If seek_exact is TRUE this will seek to the exact time, otherwise it will
+// seek to the last intra frame just before the desired time. Exact seeking can
+// be slow, because all frames up to the seeked one have to be decoded on top of
+// the previous intra frame.
+// If seeking succeeds, this function will call the video_decode_callback
+// exactly once with the target frame. If audio is enabled, it will also call
+// the audio_decode_callback any number of times, until the audio_lead_time is
+// satisfied.
+// Returns TRUE if seeking succeeded or FALSE if no frame could be found.
+
+int plm_seek(plm_t *self, float time, int seek_exact);
+
+// Similar to plm_seek(), but will not call the video_decode_callback,
+// audio_decode_callback or make any attempts to sync audio.
+// Returns the found frame or NULL if no frame could be found.
+
+plm_frame_t *plm_seek_frame(plm_t *self, float time, int seek_exact);
+
+// -----------------------------------------------------------------------------
+// plm_buffer public API
+// Provides the data source for all other plm_* interfaces
+
+// The default size for buffers created from files or by the high-level API
+
+#ifndef PLM_BUFFER_DEFAULT_SIZE
+#define PLM_BUFFER_DEFAULT_SIZE (64 * 1024)
+#endif
+
+// Create a buffer instance with a filename. Returns NULL if the file could not
+// be opened.
+
+plm_buffer_t *plm_buffer_create_with_filename(const char *filename);
+
+// Create a buffer instance with a file handle. Pass TRUE to close_when_done
+// to let plmpeg call fclose() on the handle when plm_destroy() is called.
+
+plm_buffer_t *plm_buffer_create_with_file(FILE *fh, int close_when_done);
+
+// Create a buffer instance with a pointer to memory as source. This assumes
+// the whole file is in memory. The bytes are not copied. Pass 1 to
+// free_when_done to let plmpeg call free() on the pointer when plm_destroy()
+// is called.
+
+plm_buffer_t *plm_buffer_create_with_memory(uint8_t *bytes, size_t length,
+                                            int free_when_done);
+
+// Create an empty buffer with an initial capacity. The buffer will grow
+// as needed. Data that has already been read, will be discarded.
+
+plm_buffer_t *plm_buffer_create_with_capacity(size_t capacity);
+
+// Create an empty buffer with an initial capacity. The buffer will grow
+// as needed. Decoded data will *not* be discarded. This can be used when
+// loading a file over the network, without needing to throttle the download.
+// It also allows for seeking in the already loaded data.
+
+plm_buffer_t *plm_buffer_create_for_appending(size_t initial_capacity);
+
+// Destroy a buffer instance and free all data
+
+void plm_buffer_destroy(plm_buffer_t *self);
+
+// Copy data into the buffer. If the data to be written is larger than the
+// available space, the buffer will realloc() with a larger capacity.
+// Returns the number of bytes written. This will always be the same as the
+// passed in length, except when the buffer was created _with_memory() for
+// which _write() is forbidden.
+
+size_t plm_buffer_write(plm_buffer_t *self, uint8_t *bytes, size_t length);
+
+// Mark the current byte length as the end of this buffer and signal that no
+// more data is expected to be written to it. This function should be called
+// just after the last plm_buffer_write().
+// For _with_capacity buffers, this is cleared on a plm_buffer_rewind().
+
+void plm_buffer_signal_end(plm_buffer_t *self);
+
+// Set a callback that is called whenever the buffer needs more data
+
+void plm_buffer_set_load_callback(plm_buffer_t *self,
+                                  plm_buffer_load_callback fp, void *user);
+
+// Rewind the buffer back to the beginning. When loading from a file handle,
+// this also seeks to the beginning of the file.
+
+void plm_buffer_rewind(plm_buffer_t *self);
+
+// Get the total size. For files, this returns the file size. For all other
+// types it returns the number of bytes currently in the buffer.
+
+size_t plm_buffer_get_size(plm_buffer_t *self);
+
+// Get the number of remaining (yet unread) bytes in the buffer. This can be
+// useful to throttle writing.
+
+size_t plm_buffer_get_remaining(plm_buffer_t *self);
+
+// Get whether the read position of the buffer is at the end and no more data
+// is expected.
+
+int plm_buffer_has_ended(plm_buffer_t *self);
+
+// -----------------------------------------------------------------------------
+// plm_demux public API
+// Demux an MPEG Program Stream (PS) data into separate packages
+
+// Various Packet Types
+
+static const int PLM_DEMUX_PACKET_PRIVATE = 0xBD;
+static const int PLM_DEMUX_PACKET_AUDIO_1 = 0xC0;
+static const int PLM_DEMUX_PACKET_AUDIO_2 = 0xC1;
+static const int PLM_DEMUX_PACKET_AUDIO_3 = 0xC2;
+static const int PLM_DEMUX_PACKET_AUDIO_4 = 0xC2;
+static const int PLM_DEMUX_PACKET_VIDEO_1 = 0xE0;
+
+// Create a demuxer with a plm_buffer as source. This will also attempt to read
+// the pack and system headers from the buffer.
+
+plm_demux_t *plm_demux_create(plm_buffer_t *buffer, int destroy_when_done);
+
+// Destroy a demuxer and free all data.
+
+void plm_demux_destroy(plm_demux_t *self);
+
+// Returns TRUE/FALSE whether pack and system headers have been found. This will
+// attempt to read the headers if non are present yet.
+
+int plm_demux_has_headers(plm_demux_t *self);
+
+// Returns the number of video streams found in the system header. This will
+// attempt to read the system header if non is present yet.
+
+int plm_demux_get_num_video_streams(plm_demux_t *self);
+
+// Returns the number of audio streams found in the system header. This will
+// attempt to read the system header if non is present yet.
+
+int plm_demux_get_num_audio_streams(plm_demux_t *self);
+
+// Rewind the internal buffer. See plm_buffer_rewind().
+
+void plm_demux_rewind(plm_demux_t *self);
+
+// Get whether the file has ended. This will be cleared on seeking or rewind.
+
+int plm_demux_has_ended(plm_demux_t *self);
+
+// Seek to a packet of the specified type with a PTS just before specified time.
+// If force_intra is TRUE, only packets containing an intra frame will be
+// considered - this only makes sense when the type is PLM_DEMUX_PACKET_VIDEO_1.
+// Note that the specified time is considered 0-based, regardless of the first
+// PTS in the data source.
+
+plm_packet_t *plm_demux_seek(plm_demux_t *self, float time, int type,
+                             int force_intra);
+
+// Get the PTS of the first packet of this type. Returns PLM_PACKET_INVALID_TS
+// if not packet of this packet type can be found.
+
+float plm_demux_get_start_time(plm_demux_t *self, int type);
+
+// Get the duration for the specified packet type - i.e. the span between the
+// the first PTS and the last PTS in the data source. This only makes sense when
+// the underlying data source is a file or fixed memory.
+
+float plm_demux_get_duration(plm_demux_t *self, int type);
+
+// Decode and return the next packet. The returned packet_t is valid until
+// the next call to plm_demux_decode() or until the demuxer is destroyed.
+
+plm_packet_t *plm_demux_decode(plm_demux_t *self);
+
+// -----------------------------------------------------------------------------
+// plm_video public API
+// Decode MPEG1 Video ("mpeg1") data into raw YCrCb frames
+
+// Create a video decoder with a plm_buffer as source.
+
+plm_video_t *plm_video_create_with_buffer(plm_buffer_t *buffer,
+                                          int destroy_when_done);
+
+// Destroy a video decoder and free all data.
+
+void plm_video_destroy(plm_video_t *self);
+
+// Get whether a sequence header was found and we can accurately report on
+// dimensions and framerate.
+
+int plm_video_has_header(plm_video_t *self);
+
+// Get the framerate in frames per second.
+
+float plm_video_get_framerate(plm_video_t *self);
+
+// Get the display width/height.
+
+int plm_video_get_width(plm_video_t *self);
+int plm_video_get_height(plm_video_t *self);
+
+// Set "no delay" mode. When enabled, the decoder assumes that the video does
+// *not* contain any B-Frames. This is useful for reducing lag when streaming.
+// The default is FALSE.
+
+void plm_video_set_no_delay(plm_video_t *self, int no_delay);
+
+// Get the current internal time in seconds.
+
+float plm_video_get_time(plm_video_t *self);
+
+// Set the current internal time in seconds. This is only useful when you
+// manipulate the underlying video buffer and want to enforce a correct
+// timestamps.
+
+void plm_video_set_time(plm_video_t *self, float time);
+
+// Rewind the internal buffer. See plm_buffer_rewind().
+
+void plm_video_rewind(plm_video_t *self);
+
+// Get whether the file has ended. This will be cleared on rewind.
+
+int plm_video_has_ended(plm_video_t *self);
+
+// Decode and return one frame of video and advance the internal time by
+// 1/framerate seconds. The returned frame_t is valid until the next call of
+// plm_video_decode() or until the video decoder is destroyed.
+
+plm_frame_t *plm_video_decode(plm_video_t *self);
+
+// Convert the YCrCb data of a frame into interleaved R G B data. The stride
+// specifies the width in bytes of the destination buffer. I.e. the number of
+// bytes from one line to the next. The stride must be at least
+// (frame->width * bytes_per_pixel). The buffer pointed to by *dest must have a
+// size of at least (stride * frame->height).
+// Note that the alpha component of the dest buffer is always left untouched.
+
+void plm_frame_to_rgb(plm_frame_t *frame, uint8_t *dest, int stride);
+void plm_frame_to_bgr(plm_frame_t *frame, uint8_t *dest, int stride);
+void plm_frame_to_rgba(plm_frame_t *frame, uint8_t *dest, int stride);
+void plm_frame_to_bgra(plm_frame_t *frame, uint8_t *dest, int stride);
+void plm_frame_to_argb(plm_frame_t *frame, uint8_t *dest, int stride);
+void plm_frame_to_abgr(plm_frame_t *frame, uint8_t *dest, int stride);
+
+// -----------------------------------------------------------------------------
+// plm_audio public API
+// Decode MPEG-1 Audio Layer II ("mp2") data into raw samples
+
+// Create an audio decoder with a plm_buffer as source.
+
+plm_audio_t *plm_audio_create_with_buffer(plm_buffer_t *buffer,
+                                          int destroy_when_done);
+
+// Destroy an audio decoder and free all data.
+
+void plm_audio_destroy(plm_audio_t *self);
+
+// Get whether a frame header was found and we can accurately report on
+// samplerate.
+
+int plm_audio_has_header(plm_audio_t *self);
+
+// Get the samplerate in samples per second.
+
+int plm_audio_get_samplerate(plm_audio_t *self);
+
+// Get the current internal time in seconds.
+
+float plm_audio_get_time(plm_audio_t *self);
+
+// Set the current internal time in seconds. This is only useful when you
+// manipulate the underlying video buffer and want to enforce a correct
+// timestamps.
+
+void plm_audio_set_time(plm_audio_t *self, float time);
+
+// Rewind the internal buffer. See plm_buffer_rewind().
+
+void plm_audio_rewind(plm_audio_t *self);
+
+// Get whether the file has ended. This will be cleared on rewind.
+
+int plm_audio_has_ended(plm_audio_t *self);
+
+// Decode and return one "frame" of audio and advance the internal time by
+// (PLM_AUDIO_SAMPLES_PER_FRAME/samplerate) seconds. The returned samples_t
+// is valid until the next call of plm_audio_decode() or until the audio
+// decoder is destroyed.
+
+plm_samples_t *plm_audio_decode(plm_audio_t *self);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  // PL_MPEG_H
+
+// -----------------------------------------------------------------------------
+// -----------------------------------------------------------------------------
+// IMPLEMENTATION
+
+#ifdef PL_MPEG_IMPLEMENTATION
+
+#include <stdlib.h>
+#include <string.h>
+
+#ifndef TRUE
+#define TRUE 1
+#define FALSE 0
+#endif
+
+#define PLM_UNUSED(expr) (void)(expr)
+
+// -----------------------------------------------------------------------------
+// plm (high-level interface) implementation
+
+typedef struct plm_t {
+    plm_demux_t *demux;
+    float time;
+    int has_ended;
+    int loop;
+    int has_decoders;
+
+    int video_enabled;
+    int video_packet_type;
+    plm_buffer_t *video_buffer;
+    plm_video_t *video_decoder;
+
+    int audio_enabled;
+    int audio_stream_index;
+    int audio_packet_type;
+    float audio_lead_time;
+    plm_buffer_t *audio_buffer;
+    plm_audio_t *audio_decoder;
+
+    plm_video_decode_callback video_decode_callback;
+    void *video_decode_callback_user_data;
+
+    plm_audio_decode_callback audio_decode_callback;
+    void *audio_decode_callback_user_data;
+} plm_t;
+
+int plm_init_decoders(plm_t *self);
+void plm_handle_end(plm_t *self);
+void plm_read_video_packet(plm_buffer_t *buffer, void *user);
+void plm_read_audio_packet(plm_buffer_t *buffer, void *user);
+void plm_read_packets(plm_t *self, int requested_type);
+
+plm_t *plm_create_with_filename(const char *filename) {
+    plm_buffer_t *buffer = plm_buffer_create_with_filename(filename);
+    if (!buffer) {
+        return NULL;
+    }
+    return plm_create_with_buffer(buffer, TRUE);
+}
+
+plm_t *plm_create_with_file(FILE *fh, int close_when_done) {
+    plm_buffer_t *buffer = plm_buffer_create_with_file(fh, close_when_done);
+    return plm_create_with_buffer(buffer, TRUE);
+}
+
+plm_t *plm_create_with_memory(uint8_t *bytes, size_t length,
+                              int free_when_done) {
+    plm_buffer_t *buffer =
+        plm_buffer_create_with_memory(bytes, length, free_when_done);
+    return plm_create_with_buffer(buffer, TRUE);
+}
+
+plm_t *plm_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done) {
+    plm_t *self = (plm_t *)malloc(sizeof(plm_t));
+    memset(self, 0, sizeof(plm_t));
+
+    self->demux = plm_demux_create(buffer, destroy_when_done);
+    self->video_enabled = TRUE;
+    self->audio_enabled = TRUE;
+    plm_init_decoders(self);
+
+    return self;
+}
+
+int plm_init_decoders(plm_t *self) {
+    if (self->has_decoders) {
+        return TRUE;
+    }
+
+    if (!plm_demux_has_headers(self->demux)) {
+        return FALSE;
+    }
+
+    if (plm_demux_get_num_video_streams(self->demux) > 0) {
+        if (self->video_enabled) {
+            self->video_packet_type = PLM_DEMUX_PACKET_VIDEO_1;
+        }
+        self->video_buffer =
+            plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE);
+        plm_buffer_set_load_callback(self->video_buffer, plm_read_video_packet,
+                                     self);
+    }
+
+    if (plm_demux_get_num_audio_streams(self->demux) > 0) {
+        if (self->audio_enabled) {
+            self->audio_packet_type =
+                PLM_DEMUX_PACKET_AUDIO_1 + self->audio_stream_index;
+        }
+        self->audio_buffer =
+            plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE);
+        plm_buffer_set_load_callback(self->audio_buffer, plm_read_audio_packet,
+                                     self);
+    }
+
+    if (self->video_buffer) {
+        self->video_decoder =
+            plm_video_create_with_buffer(self->video_buffer, TRUE);
+    }
+
+    if (self->audio_buffer) {
+        self->audio_decoder =
+            plm_audio_create_with_buffer(self->audio_buffer, TRUE);
+    }
+
+    self->has_decoders = TRUE;
+    return TRUE;
+}
+
+void plm_destroy(plm_t *self) {
+    if (self->video_decoder) {
+        plm_video_destroy(self->video_decoder);
+    }
+    if (self->audio_decoder) {
+        plm_audio_destroy(self->audio_decoder);
+    }
+
+    plm_demux_destroy(self->demux);
+    free(self);
+}
+
+int plm_get_audio_enabled(plm_t *self) { return self->audio_enabled; }
+
+int plm_has_headers(plm_t *self) {
+    if (!plm_demux_has_headers(self->demux)) {
+        return FALSE;
+    }
+
+    if (!plm_init_decoders(self)) {
+        return FALSE;
+    }
+
+    if ((self->video_decoder && !plm_video_has_header(self->video_decoder)) ||
+        (self->audio_decoder && !plm_audio_has_header(self->audio_decoder))) {
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+void plm_set_audio_enabled(plm_t *self, int enabled) {
+    self->audio_enabled = enabled;
+
+    if (!enabled) {
+        self->audio_packet_type = 0;
+        return;
+    }
+
+    self->audio_packet_type =
+        (plm_init_decoders(self) && self->audio_decoder)
+            ? PLM_DEMUX_PACKET_AUDIO_1 + self->audio_stream_index
+            : 0;
+}
+
+void plm_set_audio_stream(plm_t *self, int stream_index) {
+    if (stream_index < 0 || stream_index > 3) {
+        return;
+    }
+    self->audio_stream_index = stream_index;
+
+    // Set the correct audio_packet_type
+    plm_set_audio_enabled(self, self->audio_enabled);
+}
+
+int plm_get_video_enabled(plm_t *self) { return self->video_enabled; }
+
+void plm_set_video_enabled(plm_t *self, int enabled) {
+    self->video_enabled = enabled;
+
+    if (!enabled) {
+        self->video_packet_type = 0;
+        return;
+    }
+
+    self->video_packet_type = (plm_init_decoders(self) && self->video_decoder)
+                                  ? PLM_DEMUX_PACKET_VIDEO_1
+                                  : 0;
+}
+
+int plm_get_num_video_streams(plm_t *self) {
+    return plm_demux_get_num_video_streams(self->demux);
+}
+
+int plm_get_width(plm_t *self) {
+    return (plm_init_decoders(self) && self->video_decoder)
+               ? plm_video_get_width(self->video_decoder)
+               : 0;
+}
+
+int plm_get_height(plm_t *self) {
+    return (plm_init_decoders(self) && self->video_decoder)
+               ? plm_video_get_height(self->video_decoder)
+               : 0;
+}
+
+float plm_get_framerate(plm_t *self) {
+    return (plm_init_decoders(self) && self->video_decoder)
+               ? plm_video_get_framerate(self->video_decoder)
+               : 0;
+}
+
+int plm_get_num_audio_streams(plm_t *self) {
+    return plm_demux_get_num_audio_streams(self->demux);
+}
+
+int plm_get_samplerate(plm_t *self) {
+    return (plm_init_decoders(self) && self->audio_decoder)
+               ? plm_audio_get_samplerate(self->audio_decoder)
+               : 0;
+}
+
+float plm_get_audio_lead_time(plm_t *self) { return self->audio_lead_time; }
+
+void plm_set_audio_lead_time(plm_t *self, float lead_time) {
+    self->audio_lead_time = lead_time;
+}
+
+float plm_get_time(plm_t *self) { return self->time; }
+
+float plm_get_duration(plm_t *self) {
+    return plm_demux_get_duration(self->demux, PLM_DEMUX_PACKET_VIDEO_1);
+}
+
+void plm_rewind(plm_t *self) {
+    if (self->video_decoder) {
+        plm_video_rewind(self->video_decoder);
+    }
+
+    if (self->audio_decoder) {
+        plm_audio_rewind(self->audio_decoder);
+    }
+
+    plm_demux_rewind(self->demux);
+    self->time = 0;
+}
+
+int plm_get_loop(plm_t *self) { return self->loop; }
+
+void plm_set_loop(plm_t *self, int loop) { self->loop = loop; }
+
+int plm_has_ended(plm_t *self) { return self->has_ended; }
+
+void plm_set_video_decode_callback(plm_t *self, plm_video_decode_callback fp,
+                                   void *user) {
+    self->video_decode_callback = fp;
+    self->video_decode_callback_user_data = user;
+}
+
+void plm_set_audio_decode_callback(plm_t *self, plm_audio_decode_callback fp,
+                                   void *user) {
+    self->audio_decode_callback = fp;
+    self->audio_decode_callback_user_data = user;
+}
+
+void plm_decode(plm_t *self, float tick) {
+    if (!plm_init_decoders(self)) {
+        return;
+    }
+
+    int decode_video = (self->video_decode_callback && self->video_packet_type);
+    int decode_audio = (self->audio_decode_callback && self->audio_packet_type);
+
+    if (!decode_video && !decode_audio) {
+        // Nothing to do here
+        return;
+    }
+
+    int did_decode = FALSE;
+    int decode_video_failed = FALSE;
+    int decode_audio_failed = FALSE;
+
+    float video_target_time = self->time + tick;
+    float audio_target_time = self->time + tick + self->audio_lead_time;
+
+    do {
+        did_decode = FALSE;
+
+        if (decode_video &&
+            plm_video_get_time(self->video_decoder) < video_target_time) {
+            plm_frame_t *frame = plm_video_decode(self->video_decoder);
+            if (frame) {
+                self->video_decode_callback(
+                    self, frame, self->video_decode_callback_user_data);
+                did_decode = TRUE;
+            } else {
+                decode_video_failed = TRUE;
+            }
+        }
+
+        if (decode_audio &&
+            plm_audio_get_time(self->audio_decoder) < audio_target_time) {
+            plm_samples_t *samples = plm_audio_decode(self->audio_decoder);
+            if (samples) {
+                self->audio_decode_callback(
+                    self, samples, self->audio_decode_callback_user_data);
+                did_decode = TRUE;
+            } else {
+                decode_audio_failed = TRUE;
+            }
+        }
+    } while (did_decode);
+
+    // Did all sources we wanted to decode fail and the demuxer is at the end?
+    if ((!decode_video || decode_video_failed) &&
+        (!decode_audio || decode_audio_failed) &&
+        plm_demux_has_ended(self->demux)) {
+        plm_handle_end(self);
+        return;
+    }
+
+    self->time += tick;
+}
+
+plm_frame_t *plm_decode_video(plm_t *self) {
+    if (!plm_init_decoders(self)) {
+        return NULL;
+    }
+
+    if (!self->video_packet_type) {
+        return NULL;
+    }
+
+    plm_frame_t *frame = plm_video_decode(self->video_decoder);
+    if (frame) {
+        self->time = frame->time;
+    } else if (plm_demux_has_ended(self->demux)) {
+        plm_handle_end(self);
+    }
+    return frame;
+}
+
+plm_samples_t *plm_decode_audio(plm_t *self) {
+    if (!plm_init_decoders(self)) {
+        return NULL;
+    }
+
+    if (!self->audio_packet_type) {
+        return NULL;
+    }
+
+    plm_samples_t *samples = plm_audio_decode(self->audio_decoder);
+    if (samples) {
+        self->time = samples->time;
+    } else if (plm_demux_has_ended(self->demux)) {
+        plm_handle_end(self);
+    }
+    return samples;
+}
+
+void plm_handle_end(plm_t *self) {
+    if (self->loop) {
+        plm_rewind(self);
+    } else {
+        self->has_ended = TRUE;
+    }
+}
+
+void plm_read_video_packet(plm_buffer_t *buffer, void *user) {
+    PLM_UNUSED(buffer);
+    plm_t *self = (plm_t *)user;
+    plm_read_packets(self, self->video_packet_type);
+}
+
+void plm_read_audio_packet(plm_buffer_t *buffer, void *user) {
+    PLM_UNUSED(buffer);
+    plm_t *self = (plm_t *)user;
+    plm_read_packets(self, self->audio_packet_type);
+}
+
+void plm_read_packets(plm_t *self, int requested_type) {
+    plm_packet_t *packet;
+    while ((packet = plm_demux_decode(self->demux))) {
+        if (packet->type == self->video_packet_type) {
+            plm_buffer_write(self->video_buffer, packet->data, packet->length);
+        } else if (packet->type == self->audio_packet_type) {
+            plm_buffer_write(self->audio_buffer, packet->data, packet->length);
+        }
+
+        if (packet->type == requested_type) {
+            return;
+        }
+    }
+
+    if (plm_demux_has_ended(self->demux)) {
+        if (self->video_buffer) {
+            plm_buffer_signal_end(self->video_buffer);
+        }
+        if (self->audio_buffer) {
+            plm_buffer_signal_end(self->audio_buffer);
+        }
+    }
+}
+
+plm_frame_t *plm_seek_frame(plm_t *self, float time, int seek_exact) {
+    if (!plm_init_decoders(self)) {
+        return NULL;
+    }
+
+    if (!self->video_packet_type) {
+        return NULL;
+    }
+
+    int type = self->video_packet_type;
+
+    float start_time = plm_demux_get_start_time(self->demux, type);
+    float duration = plm_demux_get_duration(self->demux, type);
+
+    if (time < 0) {
+        time = 0;
+    } else if (time > duration) {
+        time = duration;
+    }
+
+    plm_packet_t *packet = plm_demux_seek(self->demux, time, type, TRUE);
+    if (!packet) {
+        return NULL;
+    }
+
+    // Disable writing to the audio buffer while decoding video
+    int previous_audio_packet_type = self->audio_packet_type;
+    self->audio_packet_type = 0;
+
+    // Clear video buffer and decode the found packet
+    plm_video_rewind(self->video_decoder);
+    plm_video_set_time(self->video_decoder, packet->pts - start_time);
+    plm_buffer_write(self->video_buffer, packet->data, packet->length);
+    plm_frame_t *frame = plm_video_decode(self->video_decoder);
+
+    // If we want to seek to an exact frame, we have to decode all frames
+    // on top of the intra frame we just jumped to.
+    if (seek_exact) {
+        while (frame && frame->time < time) {
+            frame = plm_video_decode(self->video_decoder);
+        }
+    }
+
+    // Enable writing to the audio buffer again?
+    self->audio_packet_type = previous_audio_packet_type;
+
+    if (frame) {
+        self->time = frame->time;
+    }
+
+    self->has_ended = FALSE;
+    return frame;
+}
+
+int plm_seek(plm_t *self, float time, int seek_exact) {
+    plm_frame_t *frame = plm_seek_frame(self, time, seek_exact);
+
+    if (!frame) {
+        return FALSE;
+    }
+
+    if (self->video_decode_callback) {
+        self->video_decode_callback(self, frame,
+                                    self->video_decode_callback_user_data);
+    }
+
+    // If audio is not enabled we are done here.
+    if (!self->audio_packet_type) {
+        return TRUE;
+    }
+
+    // Sync up Audio. This demuxes more packets until the first audio packet
+    // with a PTS greater than the current time is found. plm_decode() is then
+    // called to decode enough audio data to satisfy the audio_lead_time.
+
+    float start_time =
+        plm_demux_get_start_time(self->demux, self->video_packet_type);
+    plm_audio_rewind(self->audio_decoder);
+
+    plm_packet_t *packet = NULL;
+    while ((packet = plm_demux_decode(self->demux))) {
+        if (packet->type == self->video_packet_type) {
+            plm_buffer_write(self->video_buffer, packet->data, packet->length);
+        } else if (packet->type == self->audio_packet_type &&
+                   packet->pts - start_time > self->time) {
+            plm_audio_set_time(self->audio_decoder, packet->pts - start_time);
+            plm_buffer_write(self->audio_buffer, packet->data, packet->length);
+            plm_decode(self, 0);
+            break;
+        }
+    }
+
+    return TRUE;
+}
+
+// -----------------------------------------------------------------------------
+// plm_buffer implementation
+
+enum plm_buffer_mode {
+    PLM_BUFFER_MODE_FILE,
+    PLM_BUFFER_MODE_FIXED_MEM,
+    PLM_BUFFER_MODE_RING,
+    PLM_BUFFER_MODE_APPEND
+};
+
+typedef struct plm_buffer_t {
+    size_t bit_index;
+    size_t capacity;
+    size_t length;
+    size_t total_size;
+    int discard_read_bytes;
+    int has_ended;
+    int free_when_done;
+    int close_when_done;
+    FILE *fh;
+    plm_buffer_load_callback load_callback;
+    void *load_callback_user_data;
+    uint8_t *bytes;
+    enum plm_buffer_mode mode;
+} plm_buffer_t;
+
+typedef struct {
+    int16_t index;
+    int16_t value;
+} plm_vlc_t;
+
+typedef struct {
+    int16_t index;
+    uint16_t value;
+} plm_vlc_uint_t;
+
+void plm_buffer_seek(plm_buffer_t *self, size_t pos);
+size_t plm_buffer_tell(plm_buffer_t *self);
+void plm_buffer_discard_read_bytes(plm_buffer_t *self);
+void plm_buffer_load_file_callback(plm_buffer_t *self, void *user);
+
+int plm_buffer_has(plm_buffer_t *self, size_t count);
+int plm_buffer_read(plm_buffer_t *self, int count);
+void plm_buffer_align(plm_buffer_t *self);
+void plm_buffer_skip(plm_buffer_t *self, size_t count);
+int plm_buffer_skip_bytes(plm_buffer_t *self, uint8_t v);
+int plm_buffer_next_start_code(plm_buffer_t *self);
+int plm_buffer_find_start_code(plm_buffer_t *self, int code);
+int plm_buffer_no_start_code(plm_buffer_t *self);
+int16_t plm_buffer_read_vlc(plm_buffer_t *self, const plm_vlc_t *table);
+uint16_t plm_buffer_read_vlc_uint(plm_buffer_t *self,
+                                  const plm_vlc_uint_t *table);
+
+plm_buffer_t *plm_buffer_create_with_filename(const char *filename) {
+    FILE *fh = fopen(filename, "rb");
+    if (!fh) {
+        return NULL;
+    }
+    return plm_buffer_create_with_file(fh, TRUE);
+}
+
+plm_buffer_t *plm_buffer_create_with_file(FILE *fh, int close_when_done) {
+    plm_buffer_t *self =
+        plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE);
+    self->fh = fh;
+    self->close_when_done = close_when_done;
+    self->mode = PLM_BUFFER_MODE_FILE;
+    self->discard_read_bytes = TRUE;
+
+    fseek(self->fh, 0, SEEK_END);
+    self->total_size = ftell(self->fh);
+    fseek(self->fh, 0, SEEK_SET);
+
+    plm_buffer_set_load_callback(self, plm_buffer_load_file_callback, NULL);
+    return self;
+}
+
+plm_buffer_t *plm_buffer_create_with_memory(uint8_t *bytes, size_t length,
+                                            int free_when_done) {
+    plm_buffer_t *self = (plm_buffer_t *)malloc(sizeof(plm_buffer_t));
+    memset(self, 0, sizeof(plm_buffer_t));
+    self->capacity = length;
+    self->length = length;
+    self->total_size = length;
+    self->free_when_done = free_when_done;
+    self->bytes = bytes;
+    self->mode = PLM_BUFFER_MODE_FIXED_MEM;
+    self->discard_read_bytes = FALSE;
+    return self;
+}
+
+plm_buffer_t *plm_buffer_create_with_capacity(size_t capacity) {
+    plm_buffer_t *self = (plm_buffer_t *)malloc(sizeof(plm_buffer_t));
+    memset(self, 0, sizeof(plm_buffer_t));
+    self->capacity = capacity;
+    self->free_when_done = TRUE;
+    self->bytes = (uint8_t *)malloc(capacity);
+    self->mode = PLM_BUFFER_MODE_RING;
+    self->discard_read_bytes = TRUE;
+    return self;
+}
+
+plm_buffer_t *plm_buffer_create_for_appending(size_t initial_capacity) {
+    plm_buffer_t *self = plm_buffer_create_with_capacity(initial_capacity);
+    self->mode = PLM_BUFFER_MODE_APPEND;
+    self->discard_read_bytes = FALSE;
+    return self;
+}
+
+void plm_buffer_destroy(plm_buffer_t *self) {
+    if (self->fh && self->close_when_done) {
+        fclose(self->fh);
+    }
+    if (self->free_when_done) {
+        free(self->bytes);
+    }
+    free(self);
+}
+
+size_t plm_buffer_get_size(plm_buffer_t *self) {
+    return (self->mode == PLM_BUFFER_MODE_FILE) ? self->total_size
+                                                : self->length;
+}
+
+size_t plm_buffer_get_remaining(plm_buffer_t *self) {
+    return self->length - (self->bit_index >> 3);
+}
+
+size_t plm_buffer_write(plm_buffer_t *self, uint8_t *bytes, size_t length) {
+    if (self->mode == PLM_BUFFER_MODE_FIXED_MEM) {
+        return 0;
+    }
+
+    if (self->discard_read_bytes) {
+        // This should be a ring buffer, but instead it just shifts all unread
+        // data to the beginning of the buffer and appends new data at the end.
+        // Seems to be good enough.
+
+        plm_buffer_discard_read_bytes(self);
+        if (self->mode == PLM_BUFFER_MODE_RING) {
+            self->total_size = 0;
+        }
+    }
+
+    // Do we have to resize to fit the new data?
+    size_t bytes_available = self->capacity - self->length;
+    if (bytes_available < length) {
+        size_t new_size = self->capacity;
+        do {
+            new_size *= 2;
+        } while (new_size - self->length < length);
+        self->bytes = (uint8_t *)realloc(self->bytes, new_size);
+        self->capacity = new_size;
+    }
+
+    memcpy(self->bytes + self->length, bytes, length);
+    self->length += length;
+    self->has_ended = FALSE;
+    return length;
+}
+
+void plm_buffer_signal_end(plm_buffer_t *self) {
+    self->total_size = self->length;
+}
+
+void plm_buffer_set_load_callback(plm_buffer_t *self,
+                                  plm_buffer_load_callback fp, void *user) {
+    self->load_callback = fp;
+    self->load_callback_user_data = user;
+}
+
+void plm_buffer_rewind(plm_buffer_t *self) { plm_buffer_seek(self, 0); }
+
+void plm_buffer_seek(plm_buffer_t *self, size_t pos) {
+    self->has_ended = FALSE;
+
+    if (self->mode == PLM_BUFFER_MODE_FILE) {
+        fseek(self->fh, pos, SEEK_SET);
+        self->bit_index = 0;
+        self->length = 0;
+    } else if (self->mode == PLM_BUFFER_MODE_RING) {
+        if (pos != 0) {
+            // Seeking to non-0 is forbidden for dynamic-mem buffers
+            return;
+        }
+        self->bit_index = 0;
+        self->length = 0;
+        self->total_size = 0;
+    } else if (pos < self->length) {
+        self->bit_index = pos << 3;
+    }
+}
+
+size_t plm_buffer_tell(plm_buffer_t *self) {
+    return self->mode == PLM_BUFFER_MODE_FILE
+               ? ftell(self->fh) + (self->bit_index >> 3) - self->length
+               : self->bit_index >> 3;
+}
+
+void plm_buffer_discard_read_bytes(plm_buffer_t *self) {
+    size_t byte_pos = self->bit_index >> 3;
+    if (byte_pos == self->length) {
+        self->bit_index = 0;
+        self->length = 0;
+    } else if (byte_pos > 0) {
+        memmove(self->bytes, self->bytes + byte_pos, self->length - byte_pos);
+        self->bit_index -= byte_pos << 3;
+        self->length -= byte_pos;
+    }
+}
+
+void plm_buffer_load_file_callback(plm_buffer_t *self, void *user) {
+    PLM_UNUSED(user);
+
+    if (self->discard_read_bytes) {
+        plm_buffer_discard_read_bytes(self);
+    }
+
+    size_t bytes_available = self->capacity - self->length;
+    size_t bytes_read =
+        fread(self->bytes + self->length, 1, bytes_available, self->fh);
+    self->length += bytes_read;
+
+    if (bytes_read == 0) {
+        self->has_ended = TRUE;
+    }
+}
+
+int plm_buffer_has_ended(plm_buffer_t *self) { return self->has_ended; }
+
+int plm_buffer_has(plm_buffer_t *self, size_t count) {
+    if (((self->length << 3) - self->bit_index) >= count) {
+        return TRUE;
+    }
+
+    if (self->load_callback) {
+        self->load_callback(self, self->load_callback_user_data);
+    }
+
+    if (((self->length << 3) - self->bit_index) >= count) {
+        return TRUE;
+    }
+
+    if (self->total_size != 0 && self->length == self->total_size) {
+        self->has_ended = TRUE;
+    }
+    return FALSE;
+}
+
+int plm_buffer_read(plm_buffer_t *self, int count) {
+    if (!plm_buffer_has(self, count)) {
+        return 0;
+    }
+
+    int value = 0;
+    while (count) {
+        int current_byte = self->bytes[self->bit_index >> 3];
+
+        int remaining = 8 - (self->bit_index & 7);  // Remaining bits in byte
+        int read = remaining < count ? remaining : count;  // Bits in self run
+        int shift = remaining - read;
+        int mask = (0xff >> (8 - read));
+
+        value = (value << read) | ((current_byte & (mask << shift)) >> shift);
+
+        self->bit_index += read;
+        count -= read;
+    }
+
+    return value;
+}
+
+void plm_buffer_align(plm_buffer_t *self) {
+    self->bit_index = ((self->bit_index + 7) >> 3) << 3;  // Align to next byte
+}
+
+void plm_buffer_skip(plm_buffer_t *self, size_t count) {
+    if (plm_buffer_has(self, count)) {
+        self->bit_index += count;
+    }
+}
+
+int plm_buffer_skip_bytes(plm_buffer_t *self, uint8_t v) {
+    plm_buffer_align(self);
+    int skipped = 0;
+    while (plm_buffer_has(self, 8) && self->bytes[self->bit_index >> 3] == v) {
+        self->bit_index += 8;
+        skipped++;
+    }
+    return skipped;
+}
+
+int plm_buffer_next_start_code(plm_buffer_t *self) {
+    plm_buffer_align(self);
+
+    while (plm_buffer_has(self, (5 << 3))) {
+        size_t byte_index = (self->bit_index) >> 3;
+        if (self->bytes[byte_index] == 0x00 &&
+            self->bytes[byte_index + 1] == 0x00 &&
+            self->bytes[byte_index + 2] == 0x01) {
+            self->bit_index = (byte_index + 4) << 3;
+            return self->bytes[byte_index + 3];
+        }
+        self->bit_index += 8;
+    }
+    return -1;
+}
+
+int plm_buffer_find_start_code(plm_buffer_t *self, int code) {
+    int current = 0;
+    while (TRUE) {
+        current = plm_buffer_next_start_code(self);
+        if (current == code || current == -1) {
+            return current;
+        }
+    }
+    return -1;
+}
+
+int plm_buffer_has_start_code(plm_buffer_t *self, int code) {
+    size_t previous_bit_index = self->bit_index;
+    int previous_discard_read_bytes = self->discard_read_bytes;
+
+    self->discard_read_bytes = FALSE;
+    int current = plm_buffer_find_start_code(self, code);
+
+    self->bit_index = previous_bit_index;
+    self->discard_read_bytes = previous_discard_read_bytes;
+    return current;
+}
+
+int plm_buffer_no_start_code(plm_buffer_t *self) {
+    if (!plm_buffer_has(self, (5 << 3))) {
+        return FALSE;
+    }
+
+    size_t byte_index = ((self->bit_index + 7) >> 3);
+    return !(self->bytes[byte_index] == 0x00 &&
+             self->bytes[byte_index + 1] == 0x00 &&
+             self->bytes[byte_index + 2] == 0x01);
+}
+
+int16_t plm_buffer_read_vlc(plm_buffer_t *self, const plm_vlc_t *table) {
+    plm_vlc_t state = { 0, 0 };
+    do {
+        state = table[state.index + plm_buffer_read(self, 1)];
+    } while (state.index > 0);
+    return state.value;
+}
+
+uint16_t plm_buffer_read_vlc_uint(plm_buffer_t *self,
+                                  const plm_vlc_uint_t *table) {
+    return (uint16_t)plm_buffer_read_vlc(self, (const plm_vlc_t *)table);
+}
+
+// ----------------------------------------------------------------------------
+// plm_demux implementation
+
+static const int PLM_START_PACK = 0xBA;
+static const int PLM_START_END = 0xB9;
+static const int PLM_START_SYSTEM = 0xBB;
+
+typedef struct plm_demux_t {
+    plm_buffer_t *buffer;
+    int destroy_buffer_when_done;
+    float system_clock_ref;
+
+    size_t last_file_size;
+    float last_decoded_pts;
+    float start_time;
+    float duration;
+
+    int start_code;
+    int has_pack_header;
+    int has_system_header;
+    int has_headers;
+
+    int num_audio_streams;
+    int num_video_streams;
+    plm_packet_t current_packet;
+    plm_packet_t next_packet;
+} plm_demux_t;
+
+void plm_demux_buffer_seek(plm_demux_t *self, size_t pos);
+float plm_demux_decode_time(plm_demux_t *self);
+plm_packet_t *plm_demux_decode_packet(plm_demux_t *self, int type);
+plm_packet_t *plm_demux_get_packet(plm_demux_t *self);
+
+plm_demux_t *plm_demux_create(plm_buffer_t *buffer, int destroy_when_done) {
+    plm_demux_t *self = (plm_demux_t *)malloc(sizeof(plm_demux_t));
+    memset(self, 0, sizeof(plm_demux_t));
+
+    self->buffer = buffer;
+    self->destroy_buffer_when_done = destroy_when_done;
+
+    self->start_time = PLM_PACKET_INVALID_TS;
+    self->duration = PLM_PACKET_INVALID_TS;
+    self->start_code = -1;
+
+    plm_demux_has_headers(self);
+    return self;
+}
+
+void plm_demux_destroy(plm_demux_t *self) {
+    if (self->destroy_buffer_when_done) {
+        plm_buffer_destroy(self->buffer);
+    }
+    free(self);
+}
+
+int plm_demux_has_headers(plm_demux_t *self) {
+    if (self->has_headers) {
+        return TRUE;
+    }
+
+    // Decode pack header
+    if (!self->has_pack_header) {
+        if (self->start_code != PLM_START_PACK &&
+            plm_buffer_find_start_code(self->buffer, PLM_START_PACK) == -1) {
+            return FALSE;
+        }
+
+        self->start_code = PLM_START_PACK;
+        if (!plm_buffer_has(self->buffer, 64)) {
+            return FALSE;
+        }
+        self->start_code = -1;
+
+        if (plm_buffer_read(self->buffer, 4) != 0x02) {
+            return FALSE;
+        }
+
+        self->system_clock_ref = plm_demux_decode_time(self);
+        plm_buffer_skip(self->buffer, 1);
+        plm_buffer_skip(self->buffer, 22);  // mux_rate * 50
+        plm_buffer_skip(self->buffer, 1);
+
+        self->has_pack_header = TRUE;
+    }
+
+    // Decode system header
+    if (!self->has_system_header) {
+        if (self->start_code != PLM_START_SYSTEM &&
+            plm_buffer_find_start_code(self->buffer, PLM_START_SYSTEM) == -1) {
+            return FALSE;
+        }
+
+        self->start_code = PLM_START_SYSTEM;
+        if (!plm_buffer_has(self->buffer, 56)) {
+            return FALSE;
+        }
+        self->start_code = -1;
+
+        plm_buffer_skip(self->buffer, 16);  // header_length
+        plm_buffer_skip(self->buffer, 24);  // rate bound
+        self->num_audio_streams = plm_buffer_read(self->buffer, 6);
+        plm_buffer_skip(self->buffer, 5);  // misc flags
+        self->num_video_streams = plm_buffer_read(self->buffer, 5);
+
+        self->has_system_header = TRUE;
+    }
+
+    self->has_headers = TRUE;
+    return TRUE;
+}
+
+int plm_demux_get_num_video_streams(plm_demux_t *self) {
+    return plm_demux_has_headers(self) ? self->num_video_streams : 0;
+}
+
+int plm_demux_get_num_audio_streams(plm_demux_t *self) {
+    return plm_demux_has_headers(self) ? self->num_audio_streams : 0;
+}
+
+void plm_demux_rewind(plm_demux_t *self) {
+    plm_buffer_rewind(self->buffer);
+    self->current_packet.length = 0;
+    self->next_packet.length = 0;
+    self->start_code = -1;
+}
+
+int plm_demux_has_ended(plm_demux_t *self) {
+    return plm_buffer_has_ended(self->buffer);
+}
+
+void plm_demux_buffer_seek(plm_demux_t *self, size_t pos) {
+    plm_buffer_seek(self->buffer, pos);
+    self->current_packet.length = 0;
+    self->next_packet.length = 0;
+    self->start_code = -1;
+}
+
+float plm_demux_get_start_time(plm_demux_t *self, int type) {
+    if (self->start_time != PLM_PACKET_INVALID_TS) {
+        return self->start_time;
+    }
+
+    int previous_pos = plm_buffer_tell(self->buffer);
+    int previous_start_code = self->start_code;
+
+    // Find first video PTS
+    plm_demux_rewind(self);
+    do {
+        plm_packet_t *packet = plm_demux_decode(self);
+        if (!packet) {
+            break;
+        }
+        if (packet->type == type) {
+            self->start_time = packet->pts;
+        }
+    } while (self->start_time == PLM_PACKET_INVALID_TS);
+
+    plm_demux_buffer_seek(self, previous_pos);
+    self->start_code = previous_start_code;
+    return self->start_time;
+}
+
+float plm_demux_get_duration(plm_demux_t *self, int type) {
+    size_t file_size = plm_buffer_get_size(self->buffer);
+
+    if (self->duration != PLM_PACKET_INVALID_TS &&
+        self->last_file_size == file_size) {
+        return self->duration;
+    }
+
+    size_t previous_pos = plm_buffer_tell(self->buffer);
+    int previous_start_code = self->start_code;
+
+    // Find last video PTS. Start searching 64kb from the end and go further
+    // back if needed.
+    long start_range = 64 * 1024;
+    long max_range = 4096 * 1024;
+    for (long range = start_range; range <= max_range; range *= 2) {
+        long seek_pos = file_size - range;
+        if (seek_pos < 0) {
+            seek_pos = 0;
+            range = max_range;  // Make sure to bail after this round
+        }
+        plm_demux_buffer_seek(self, seek_pos);
+        self->current_packet.length = 0;
+
+        float last_pts = PLM_PACKET_INVALID_TS;
+        plm_packet_t *packet = NULL;
+        while ((packet = plm_demux_decode(self))) {
+            if (packet->pts != PLM_PACKET_INVALID_TS && packet->type == type) {
+                last_pts = packet->pts;
+            }
+        }
+        if (last_pts != PLM_PACKET_INVALID_TS) {
+            self->duration = last_pts - plm_demux_get_start_time(self, type);
+            break;
+        }
+    }
+
+    plm_demux_buffer_seek(self, previous_pos);
+    self->start_code = previous_start_code;
+    self->last_file_size = file_size;
+    return self->duration;
+}
+
+plm_packet_t *plm_demux_seek(plm_demux_t *self, float seek_time, int type,
+                             int force_intra) {
+    if (!plm_demux_has_headers(self)) {
+        return NULL;
+    }
+
+    // Using the current time, current byte position and the average bytes per
+    // second for this file, try to jump to a byte position that hopefully has
+    // packets containing timestamps within one second before to the desired
+    // seek_time.
+
+    // If we hit close to the seek_time scan through all packets to find the
+    // last one (just before the seek_time) containing an intra frame.
+    // Otherwise we should at least be closer than before. Calculate the bytes
+    // per second for the jumped range and jump again.
+
+    // The number of retries here is hard-limited to a generous amount. Usually
+    // the correct range is found after 1--5 jumps, even for files with very
+    // variable bitrates. If significantly more jumps are needed, there's
+    // probably something wrong with the file and we just avoid getting into an
+    // infinite loop. 32 retries should be enough for anybody.
+
+    float duration = plm_demux_get_duration(self, type);
+    long file_size = plm_buffer_get_size(self->buffer);
+    long byterate = file_size / duration;
+
+    float cur_time = self->last_decoded_pts;
+    float scan_span = 1;
+
+    if (seek_time > duration) {
+        seek_time = duration;
+    } else if (seek_time < 0) {
+        seek_time = 0;
+    }
+    seek_time += self->start_time;
+
+    for (int retry = 0; retry < 32; retry++) {
+        int found_packet_with_pts = FALSE;
+        int found_packet_in_range = FALSE;
+        long last_valid_packet_start = -1;
+        float first_packet_time = PLM_PACKET_INVALID_TS;
+
+        long cur_pos = plm_buffer_tell(self->buffer);
+
+        // Estimate byte offset and jump to it.
+        long offset = (seek_time - cur_time - scan_span) * byterate;
+        long seek_pos = cur_pos + offset;
+        if (seek_pos < 0) {
+            seek_pos = 0;
+        } else if (seek_pos > file_size - 256) {
+            seek_pos = file_size - 256;
+        }
+
+        plm_demux_buffer_seek(self, seek_pos);
+
+        // Scan through all packets up to the seek_time to find the last packet
+        // containing an intra frame.
+        while (plm_buffer_find_start_code(self->buffer, type) != -1) {
+            long packet_start = plm_buffer_tell(self->buffer);
+            plm_packet_t *packet = plm_demux_decode_packet(self, type);
+
+            // Skip packet if it has no PTS
+            if (!packet || packet->pts == PLM_PACKET_INVALID_TS) {
+                continue;
+            }
+
+            // Bail scanning through packets if we hit one that is outside
+            // seek_time - scan_span.
+            // We also adjust the cur_time and byterate values here so the next
+            // iteration can be a bit more precise.
+            if (packet->pts > seek_time ||
+                packet->pts < seek_time - scan_span) {
+                found_packet_with_pts = TRUE;
+                byterate = (seek_pos - cur_pos) / (packet->pts - cur_time);
+                cur_time = packet->pts;
+                break;
+            }
+
+            // If we are still here, it means this packet is in close range to
+            // the seek_time. If this is the first packet for this jump position
+            // record the PTS. If we later have to back off, when there was no
+            // intra frame in this range, we can lower the seek_time to not scan
+            // this range again.
+            if (!found_packet_in_range) {
+                found_packet_in_range = TRUE;
+                first_packet_time = packet->pts;
+            }
+
+            // Check if this is an intra frame packet. If so, record the buffer
+            // position of the start of this packet. We want to jump back to it
+            // later, when we know it's the last intra frame before desired
+            // seek time.
+            if (force_intra) {
+                for (size_t i = 0; i < packet->length - 6; i++) {
+                    // Find the START_PICTURE code
+                    if (packet->data[i] == 0x00 &&
+                        packet->data[i + 1] == 0x00 &&
+                        packet->data[i + 2] == 0x01 &&
+                        packet->data[i + 3] == 0x00) {
+                        // Bits 11--13 in the picture header contain the frame
+                        // type, where 1=Intra
+                        if ((packet->data[i + 5] & 0x38) == 8) {
+                            last_valid_packet_start = packet_start;
+                        }
+                        break;
+                    }
+                }
+            }
+
+            // If we don't want intra frames, just use the last PTS found.
+            else {
+                last_valid_packet_start = packet_start;
+            }
+        }
+
+        // If there was at least one intra frame in the range scanned above,
+        // our search is over. Jump back to the packet and decode it again.
+        if (last_valid_packet_start != -1) {
+            plm_demux_buffer_seek(self, last_valid_packet_start);
+            return plm_demux_decode_packet(self, type);
+        }
+
+        // If we hit the right range, but still found no intra frame, we have
+        // to increases the scan_span. This is done exponentially to also handle
+        // video files with very few intra frames.
+        else if (found_packet_in_range) {
+            scan_span *= 2;
+            seek_time = first_packet_time;
+        }
+
+        // If we didn't find any packet with a PTS, it probably means we reached
+        // the end of the file. Estimate byterate and cur_time accordingly.
+        else if (!found_packet_with_pts) {
+            byterate = (seek_pos - cur_pos) / (duration - cur_time);
+            cur_time = duration;
+        }
+    }
+
+    return NULL;
+}
+
+plm_packet_t *plm_demux_decode(plm_demux_t *self) {
+    if (!plm_demux_has_headers(self)) {
+        return NULL;
+    }
+
+    if (self->current_packet.length) {
+        size_t bits_till_next_packet = self->current_packet.length << 3;
+        if (!plm_buffer_has(self->buffer, bits_till_next_packet)) {
+            return NULL;
+        }
+        plm_buffer_skip(self->buffer, bits_till_next_packet);
+        self->current_packet.length = 0;
+    }
+
+    // Pending packet waiting for data?
+    if (self->next_packet.length) {
+        return plm_demux_get_packet(self);
+    }
+
+    // Pending packet waiting for header?
+    if (self->start_code != -1) {
+        return plm_demux_decode_packet(self, self->start_code);
+    }
+
+    do {
+        self->start_code = plm_buffer_next_start_code(self->buffer);
+        if (self->start_code == PLM_DEMUX_PACKET_VIDEO_1 ||
+            self->start_code == PLM_DEMUX_PACKET_PRIVATE ||
+            (self->start_code >= PLM_DEMUX_PACKET_AUDIO_1 &&
+             self->start_code <= PLM_DEMUX_PACKET_AUDIO_4)) {
+            return plm_demux_decode_packet(self, self->start_code);
+        }
+    } while (self->start_code != -1);
+
+    return NULL;
+}
+
+float plm_demux_decode_time(plm_demux_t *self) {
+    int64_t clock = plm_buffer_read(self->buffer, 3) << 30;
+    plm_buffer_skip(self->buffer, 1);
+    clock |= plm_buffer_read(self->buffer, 15) << 15;
+    plm_buffer_skip(self->buffer, 1);
+    clock |= plm_buffer_read(self->buffer, 15);
+    plm_buffer_skip(self->buffer, 1);
+    return (float)clock / 90000.0f;
+}
+
+plm_packet_t *plm_demux_decode_packet(plm_demux_t *self, int type) {
+    if (!plm_buffer_has(self->buffer, 16 << 3)) {
+        return NULL;
+    }
+
+    self->start_code = -1;
+
+    self->next_packet.type = type;
+    self->next_packet.length = plm_buffer_read(self->buffer, 16);
+    self->next_packet.length -=
+        plm_buffer_skip_bytes(self->buffer, 0xff);  // stuffing
+
+    // skip P-STD
+    if (plm_buffer_read(self->buffer, 2) == 0x01) {
+        plm_buffer_skip(self->buffer, 16);
+        self->next_packet.length -= 2;
+    }
+
+    int pts_dts_marker = plm_buffer_read(self->buffer, 2);
+    if (pts_dts_marker == 0x03) {
+        self->next_packet.pts = plm_demux_decode_time(self);
+        self->last_decoded_pts = self->next_packet.pts;
+        plm_buffer_skip(self->buffer, 40);  // skip dts
+        self->next_packet.length -= 10;
+    } else if (pts_dts_marker == 0x02) {
+        self->next_packet.pts = plm_demux_decode_time(self);
+        self->last_decoded_pts = self->next_packet.pts;
+        self->next_packet.length -= 5;
+    } else if (pts_dts_marker == 0x00) {
+        self->next_packet.pts = PLM_PACKET_INVALID_TS;
+        plm_buffer_skip(self->buffer, 4);
+        self->next_packet.length -= 1;
+    } else {
+        return NULL;  // invalid
+    }
+
+    return plm_demux_get_packet(self);
+}
+
+plm_packet_t *plm_demux_get_packet(plm_demux_t *self) {
+    if (!plm_buffer_has(self->buffer, self->next_packet.length << 3)) {
+        return NULL;
+    }
+
+    self->current_packet.data =
+        self->buffer->bytes + (self->buffer->bit_index >> 3);
+    self->current_packet.length = self->next_packet.length;
+    self->current_packet.type = self->next_packet.type;
+    self->current_packet.pts = self->next_packet.pts;
+
+    self->next_packet.length = 0;
+    return &self->current_packet;
+}
+
+// -----------------------------------------------------------------------------
+// plm_video implementation
+
+// Inspired by Java MPEG-1 Video Decoder and Player by Zoltan Korandi
+// https://sourceforge.net/projects/javampeg1video/
+
+static const int PLM_VIDEO_PICTURE_TYPE_INTRA = 1;
+static const int PLM_VIDEO_PICTURE_TYPE_PREDICTIVE = 2;
+static const int PLM_VIDEO_PICTURE_TYPE_B = 3;
+
+static const int PLM_START_SEQUENCE = 0xB3;
+static const int PLM_START_SLICE_FIRST = 0x01;
+static const int PLM_START_SLICE_LAST = 0xAF;
+static const int PLM_START_PICTURE = 0x00;
+static const int PLM_START_EXTENSION = 0xB5;
+static const int PLM_START_USER_DATA = 0xB2;
+
+#define PLM_START_IS_SLICE(c) \
+    (c >= PLM_START_SLICE_FIRST && c <= PLM_START_SLICE_LAST)
+
+static const float PLM_VIDEO_PICTURE_RATE[] = {
+    0.000f,  23.976f, 24.000f, 25.000f, 29.970f, 30.000f, 50.000f, 59.940f,
+    60.000f, 0.000f,  0.000f,  0.000f,  0.000f,  0.000f,  0.000f,  0.000f
+};
+
+static const uint8_t PLM_VIDEO_ZIG_ZAG[] = {
+    0,  1,  8,  16, 9,  2,  3,  10, 17, 24, 32, 25, 18, 11, 4,  5,
+    12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6,  7,  14, 21, 28,
+    35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51,
+    58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63
+};
+
+static const uint8_t PLM_VIDEO_INTRA_QUANT_MATRIX[] = {
+    8,  16, 19, 22, 26, 27, 29, 34, 16, 16, 22, 24, 27, 29, 34, 37,
+    19, 22, 26, 27, 29, 34, 34, 38, 22, 22, 26, 27, 29, 34, 37, 40,
+    22, 26, 27, 29, 32, 35, 40, 48, 26, 27, 29, 32, 35, 40, 48, 58,
+    26, 27, 29, 34, 38, 46, 56, 69, 27, 29, 35, 38, 46, 56, 69, 83
+};
+
+static const uint8_t PLM_VIDEO_NON_INTRA_QUANT_MATRIX[] = {
+    16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+    16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+    16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+    16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16
+};
+
+static const uint8_t PLM_VIDEO_PREMULTIPLIER_MATRIX[] = {
+    32, 44, 42, 38, 32, 25, 17, 9,  44, 62, 58, 52, 44, 35, 24, 12,
+    42, 58, 55, 49, 42, 33, 23, 12, 38, 52, 49, 44, 38, 30, 20, 10,
+    32, 44, 42, 38, 32, 25, 17, 9,  25, 35, 33, 30, 25, 20, 14, 7,
+    17, 24, 23, 20, 17, 14, 9,  5,  9,  12, 12, 10, 9,  7,  5,  2
+};
+
+static const plm_vlc_t PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT[] = {
+    { 1 << 1, 0 },  { 0, 1 },        //   0: x
+    { 2 << 1, 0 },  { 3 << 1, 0 },   //   1: 0x
+    { 4 << 1, 0 },  { 5 << 1, 0 },   //   2: 00x
+    { 0, 3 },       { 0, 2 },        //   3: 01x
+    { 6 << 1, 0 },  { 7 << 1, 0 },   //   4: 000x
+    { 0, 5 },       { 0, 4 },        //   5: 001x
+    { 8 << 1, 0 },  { 9 << 1, 0 },   //   6: 0000x
+    { 0, 7 },       { 0, 6 },        //   7: 0001x
+    { 10 << 1, 0 }, { 11 << 1, 0 },  //   8: 0000 0x
+    { 12 << 1, 0 }, { 13 << 1, 0 },  //   9: 0000 1x
+    { 14 << 1, 0 }, { 15 << 1, 0 },  //  10: 0000 00x
+    { 16 << 1, 0 }, { 17 << 1, 0 },  //  11: 0000 01x
+    { 18 << 1, 0 }, { 19 << 1, 0 },  //  12: 0000 10x
+    { 0, 9 },       { 0, 8 },        //  13: 0000 11x
+    { -1, 0 },      { 20 << 1, 0 },  //  14: 0000 000x
+    { -1, 0 },      { 21 << 1, 0 },  //  15: 0000 001x
+    { 22 << 1, 0 }, { 23 << 1, 0 },  //  16: 0000 010x
+    { 0, 15 },      { 0, 14 },       //  17: 0000 011x
+    { 0, 13 },      { 0, 12 },       //  18: 0000 100x
+    { 0, 11 },      { 0, 10 },       //  19: 0000 101x
+    { 24 << 1, 0 }, { 25 << 1, 0 },  //  20: 0000 0001x
+    { 26 << 1, 0 }, { 27 << 1, 0 },  //  21: 0000 0011x
+    { 28 << 1, 0 }, { 29 << 1, 0 },  //  22: 0000 0100x
+    { 30 << 1, 0 }, { 31 << 1, 0 },  //  23: 0000 0101x
+    { 32 << 1, 0 }, { -1, 0 },       //  24: 0000 0001 0x
+    { -1, 0 },      { 33 << 1, 0 },  //  25: 0000 0001 1x
+    { 34 << 1, 0 }, { 35 << 1, 0 },  //  26: 0000 0011 0x
+    { 36 << 1, 0 }, { 37 << 1, 0 },  //  27: 0000 0011 1x
+    { 38 << 1, 0 }, { 39 << 1, 0 },  //  28: 0000 0100 0x
+    { 0, 21 },      { 0, 20 },       //  29: 0000 0100 1x
+    { 0, 19 },      { 0, 18 },       //  30: 0000 0101 0x
+    { 0, 17 },      { 0, 16 },       //  31: 0000 0101 1x
+    { 0, 35 },      { -1, 0 },       //  32: 0000 0001 00x
+    { -1, 0 },      { 0, 34 },       //  33: 0000 0001 11x
+    { 0, 33 },      { 0, 32 },       //  34: 0000 0011 00x
+    { 0, 31 },      { 0, 30 },       //  35: 0000 0011 01x
+    { 0, 29 },      { 0, 28 },       //  36: 0000 0011 10x
+    { 0, 27 },      { 0, 26 },       //  37: 0000 0011 11x
+    { 0, 25 },      { 0, 24 },       //  38: 0000 0100 00x
+    { 0, 23 },      { 0, 22 },       //  39: 0000 0100 01x
+};
+
+static const plm_vlc_t PLM_VIDEO_MACROBLOCK_TYPE_INTRA[] = {
+    { 1 << 1, 0 },
+    { 0, 0x01 },  //   0: x
+    { -1, 0 },
+    { 0, 0x11 },  //   1: 0x
+};
+
+static const plm_vlc_t PLM_VIDEO_MACROBLOCK_TYPE_PREDICTIVE[] = {
+    { 1 << 1, 0 }, { 0, 0x0a },    //   0: x
+    { 2 << 1, 0 }, { 0, 0x02 },    //   1: 0x
+    { 3 << 1, 0 }, { 0, 0x08 },    //   2: 00x
+    { 4 << 1, 0 }, { 5 << 1, 0 },  //   3: 000x
+    { 6 << 1, 0 }, { 0, 0x12 },    //   4: 0000x
+    { 0, 0x1a },   { 0, 0x01 },    //   5: 0001x
+    { -1, 0 },     { 0, 0x11 },    //   6: 0000 0x
+};
+
+static const plm_vlc_t PLM_VIDEO_MACROBLOCK_TYPE_B[] = {
+    { 1 << 1, 0 }, { 2 << 1, 0 },   //   0: x
+    { 3 << 1, 0 }, { 4 << 1, 0 },   //   1: 0x
+    { 0, 0x0c },   { 0, 0x0e },     //   2: 1x
+    { 5 << 1, 0 }, { 6 << 1, 0 },   //   3: 00x
+    { 0, 0x04 },   { 0, 0x06 },     //   4: 01x
+    { 7 << 1, 0 }, { 8 << 1, 0 },   //   5: 000x
+    { 0, 0x08 },   { 0, 0x0a },     //   6: 001x
+    { 9 << 1, 0 }, { 10 << 1, 0 },  //   7: 0000x
+    { 0, 0x1e },   { 0, 0x01 },     //   8: 0001x
+    { -1, 0 },     { 0, 0x11 },     //   9: 0000 0x
+    { 0, 0x16 },   { 0, 0x1a },     //  10: 0000 1x
+};
+
+static const plm_vlc_t *PLM_VIDEO_MACROBLOCK_TYPE[] = {
+    NULL, PLM_VIDEO_MACROBLOCK_TYPE_INTRA, PLM_VIDEO_MACROBLOCK_TYPE_PREDICTIVE,
+    PLM_VIDEO_MACROBLOCK_TYPE_B
+};
+
+static const plm_vlc_t PLM_VIDEO_CODE_BLOCK_PATTERN[] = {
+    { 1 << 1, 0 },  { 2 << 1, 0 },   //   0: x
+    { 3 << 1, 0 },  { 4 << 1, 0 },   //   1: 0x
+    { 5 << 1, 0 },  { 6 << 1, 0 },   //   2: 1x
+    { 7 << 1, 0 },  { 8 << 1, 0 },   //   3: 00x
+    { 9 << 1, 0 },  { 10 << 1, 0 },  //   4: 01x
+    { 11 << 1, 0 }, { 12 << 1, 0 },  //   5: 10x
+    { 13 << 1, 0 }, { 0, 60 },       //   6: 11x
+    { 14 << 1, 0 }, { 15 << 1, 0 },  //   7: 000x
+    { 16 << 1, 0 }, { 17 << 1, 0 },  //   8: 001x
+    { 18 << 1, 0 }, { 19 << 1, 0 },  //   9: 010x
+    { 20 << 1, 0 }, { 21 << 1, 0 },  //  10: 011x
+    { 22 << 1, 0 }, { 23 << 1, 0 },  //  11: 100x
+    { 0, 32 },      { 0, 16 },       //  12: 101x
+    { 0, 8 },       { 0, 4 },        //  13: 110x
+    { 24 << 1, 0 }, { 25 << 1, 0 },  //  14: 0000x
+    { 26 << 1, 0 }, { 27 << 1, 0 },  //  15: 0001x
+    { 28 << 1, 0 }, { 29 << 1, 0 },  //  16: 0010x
+    { 30 << 1, 0 }, { 31 << 1, 0 },  //  17: 0011x
+    { 0, 62 },      { 0, 2 },        //  18: 0100x
+    { 0, 61 },      { 0, 1 },        //  19: 0101x
+    { 0, 56 },      { 0, 52 },       //  20: 0110x
+    { 0, 44 },      { 0, 28 },       //  21: 0111x
+    { 0, 40 },      { 0, 20 },       //  22: 1000x
+    { 0, 48 },      { 0, 12 },       //  23: 1001x
+    { 32 << 1, 0 }, { 33 << 1, 0 },  //  24: 0000 0x
+    { 34 << 1, 0 }, { 35 << 1, 0 },  //  25: 0000 1x
+    { 36 << 1, 0 }, { 37 << 1, 0 },  //  26: 0001 0x
+    { 38 << 1, 0 }, { 39 << 1, 0 },  //  27: 0001 1x
+    { 40 << 1, 0 }, { 41 << 1, 0 },  //  28: 0010 0x
+    { 42 << 1, 0 }, { 43 << 1, 0 },  //  29: 0010 1x
+    { 0, 63 },      { 0, 3 },        //  30: 0011 0x
+    { 0, 36 },      { 0, 24 },       //  31: 0011 1x
+    { 44 << 1, 0 }, { 45 << 1, 0 },  //  32: 0000 00x
+    { 46 << 1, 0 }, { 47 << 1, 0 },  //  33: 0000 01x
+    { 48 << 1, 0 }, { 49 << 1, 0 },  //  34: 0000 10x
+    { 50 << 1, 0 }, { 51 << 1, 0 },  //  35: 0000 11x
+    { 52 << 1, 0 }, { 53 << 1, 0 },  //  36: 0001 00x
+    { 54 << 1, 0 }, { 55 << 1, 0 },  //  37: 0001 01x
+    { 56 << 1, 0 }, { 57 << 1, 0 },  //  38: 0001 10x
+    { 58 << 1, 0 }, { 59 << 1, 0 },  //  39: 0001 11x
+    { 0, 34 },      { 0, 18 },       //  40: 0010 00x
+    { 0, 10 },      { 0, 6 },        //  41: 0010 01x
+    { 0, 33 },      { 0, 17 },       //  42: 0010 10x
+    { 0, 9 },       { 0, 5 },        //  43: 0010 11x
+    { -1, 0 },      { 60 << 1, 0 },  //  44: 0000 000x
+    { 61 << 1, 0 }, { 62 << 1, 0 },  //  45: 0000 001x
+    { 0, 58 },      { 0, 54 },       //  46: 0000 010x
+    { 0, 46 },      { 0, 30 },       //  47: 0000 011x
+    { 0, 57 },      { 0, 53 },       //  48: 0000 100x
+    { 0, 45 },      { 0, 29 },       //  49: 0000 101x
+    { 0, 38 },      { 0, 26 },       //  50: 0000 110x
+    { 0, 37 },      { 0, 25 },       //  51: 0000 111x
+    { 0, 43 },      { 0, 23 },       //  52: 0001 000x
+    { 0, 51 },      { 0, 15 },       //  53: 0001 001x
+    { 0, 42 },      { 0, 22 },       //  54: 0001 010x
+    { 0, 50 },      { 0, 14 },       //  55: 0001 011x
+    { 0, 41 },      { 0, 21 },       //  56: 0001 100x
+    { 0, 49 },      { 0, 13 },       //  57: 0001 101x
+    { 0, 35 },      { 0, 19 },       //  58: 0001 110x
+    { 0, 11 },      { 0, 7 },        //  59: 0001 111x
+    { 0, 39 },      { 0, 27 },       //  60: 0000 0001x
+    { 0, 59 },      { 0, 55 },       //  61: 0000 0010x
+    { 0, 47 },      { 0, 31 },       //  62: 0000 0011x
+};
+
+static const plm_vlc_t PLM_VIDEO_MOTION[] = {
+    { 1 << 1, 0 },  { 0, 0 },        //   0: x
+    { 2 << 1, 0 },  { 3 << 1, 0 },   //   1: 0x
+    { 4 << 1, 0 },  { 5 << 1, 0 },   //   2: 00x
+    { 0, 1 },       { 0, -1 },       //   3: 01x
+    { 6 << 1, 0 },  { 7 << 1, 0 },   //   4: 000x
+    { 0, 2 },       { 0, -2 },       //   5: 001x
+    { 8 << 1, 0 },  { 9 << 1, 0 },   //   6: 0000x
+    { 0, 3 },       { 0, -3 },       //   7: 0001x
+    { 10 << 1, 0 }, { 11 << 1, 0 },  //   8: 0000 0x
+    { 12 << 1, 0 }, { 13 << 1, 0 },  //   9: 0000 1x
+    { -1, 0 },      { 14 << 1, 0 },  //  10: 0000 00x
+    { 15 << 1, 0 }, { 16 << 1, 0 },  //  11: 0000 01x
+    { 17 << 1, 0 }, { 18 << 1, 0 },  //  12: 0000 10x
+    { 0, 4 },       { 0, -4 },       //  13: 0000 11x
+    { -1, 0 },      { 19 << 1, 0 },  //  14: 0000 001x
+    { 20 << 1, 0 }, { 21 << 1, 0 },  //  15: 0000 010x
+    { 0, 7 },       { 0, -7 },       //  16: 0000 011x
+    { 0, 6 },       { 0, -6 },       //  17: 0000 100x
+    { 0, 5 },       { 0, -5 },       //  18: 0000 101x
+    { 22 << 1, 0 }, { 23 << 1, 0 },  //  19: 0000 0011x
+    { 24 << 1, 0 }, { 25 << 1, 0 },  //  20: 0000 0100x
+    { 26 << 1, 0 }, { 27 << 1, 0 },  //  21: 0000 0101x
+    { 28 << 1, 0 }, { 29 << 1, 0 },  //  22: 0000 0011 0x
+    { 30 << 1, 0 }, { 31 << 1, 0 },  //  23: 0000 0011 1x
+    { 32 << 1, 0 }, { 33 << 1, 0 },  //  24: 0000 0100 0x
+    { 0, 10 },      { 0, -10 },      //  25: 0000 0100 1x
+    { 0, 9 },       { 0, -9 },       //  26: 0000 0101 0x
+    { 0, 8 },       { 0, -8 },       //  27: 0000 0101 1x
+    { 0, 16 },      { 0, -16 },      //  28: 0000 0011 00x
+    { 0, 15 },      { 0, -15 },      //  29: 0000 0011 01x
+    { 0, 14 },      { 0, -14 },      //  30: 0000 0011 10x
+    { 0, 13 },      { 0, -13 },      //  31: 0000 0011 11x
+    { 0, 12 },      { 0, -12 },      //  32: 0000 0100 00x
+    { 0, 11 },      { 0, -11 },      //  33: 0000 0100 01x
+};
+
+static const plm_vlc_t PLM_VIDEO_DCT_SIZE_LUMINANCE[] = {
+    { 1 << 1, 0 }, { 2 << 1, 0 },  //   0: x
+    { 0, 1 },      { 0, 2 },       //   1: 0x
+    { 3 << 1, 0 }, { 4 << 1, 0 },  //   2: 1x
+    { 0, 0 },      { 0, 3 },       //   3: 10x
+    { 0, 4 },      { 5 << 1, 0 },  //   4: 11x
+    { 0, 5 },      { 6 << 1, 0 },  //   5: 111x
+    { 0, 6 },      { 7 << 1, 0 },  //   6: 1111x
+    { 0, 7 },      { 8 << 1, 0 },  //   7: 1111 1x
+    { 0, 8 },      { -1, 0 },      //   8: 1111 11x
+};
+
+static const plm_vlc_t PLM_VIDEO_DCT_SIZE_CHROMINANCE[] = {
+    { 1 << 1, 0 }, { 2 << 1, 0 },  //   0: x
+    { 0, 0 },      { 0, 1 },       //   1: 0x
+    { 0, 2 },      { 3 << 1, 0 },  //   2: 1x
+    { 0, 3 },      { 4 << 1, 0 },  //   3: 11x
+    { 0, 4 },      { 5 << 1, 0 },  //   4: 111x
+    { 0, 5 },      { 6 << 1, 0 },  //   5: 1111x
+    { 0, 6 },      { 7 << 1, 0 },  //   6: 1111 1x
+    { 0, 7 },      { 8 << 1, 0 },  //   7: 1111 11x
+    { 0, 8 },      { -1, 0 },      //   8: 1111 111x
+};
+
+static const plm_vlc_t *PLM_VIDEO_DCT_SIZE[] = {
+    PLM_VIDEO_DCT_SIZE_LUMINANCE, PLM_VIDEO_DCT_SIZE_CHROMINANCE,
+    PLM_VIDEO_DCT_SIZE_CHROMINANCE
+};
+
+//  dct_coeff bitmap:
+//    0xff00  run
+//    0x00ff  level
+
+//  Decoded values are unsigned. Sign bit follows in the stream.
+
+static const plm_vlc_uint_t PLM_VIDEO_DCT_COEFF[] = {
+    { 1 << 1, 0 },   { 0, 0x0001 },    //   0: x
+    { 2 << 1, 0 },   { 3 << 1, 0 },    //   1: 0x
+    { 4 << 1, 0 },   { 5 << 1, 0 },    //   2: 00x
+    { 6 << 1, 0 },   { 0, 0x0101 },    //   3: 01x
+    { 7 << 1, 0 },   { 8 << 1, 0 },    //   4: 000x
+    { 9 << 1, 0 },   { 10 << 1, 0 },   //   5: 001x
+    { 0, 0x0002 },   { 0, 0x0201 },    //   6: 010x
+    { 11 << 1, 0 },  { 12 << 1, 0 },   //   7: 0000x
+    { 13 << 1, 0 },  { 14 << 1, 0 },   //   8: 0001x
+    { 15 << 1, 0 },  { 0, 0x0003 },    //   9: 0010x
+    { 0, 0x0401 },   { 0, 0x0301 },    //  10: 0011x
+    { 16 << 1, 0 },  { 0, 0xffff },    //  11: 0000 0x
+    { 17 << 1, 0 },  { 18 << 1, 0 },   //  12: 0000 1x
+    { 0, 0x0701 },   { 0, 0x0601 },    //  13: 0001 0x
+    { 0, 0x0102 },   { 0, 0x0501 },    //  14: 0001 1x
+    { 19 << 1, 0 },  { 20 << 1, 0 },   //  15: 0010 0x
+    { 21 << 1, 0 },  { 22 << 1, 0 },   //  16: 0000 00x
+    { 0, 0x0202 },   { 0, 0x0901 },    //  17: 0000 10x
+    { 0, 0x0004 },   { 0, 0x0801 },    //  18: 0000 11x
+    { 23 << 1, 0 },  { 24 << 1, 0 },   //  19: 0010 00x
+    { 25 << 1, 0 },  { 26 << 1, 0 },   //  20: 0010 01x
+    { 27 << 1, 0 },  { 28 << 1, 0 },   //  21: 0000 000x
+    { 29 << 1, 0 },  { 30 << 1, 0 },   //  22: 0000 001x
+    { 0, 0x0d01 },   { 0, 0x0006 },    //  23: 0010 000x
+    { 0, 0x0c01 },   { 0, 0x0b01 },    //  24: 0010 001x
+    { 0, 0x0302 },   { 0, 0x0103 },    //  25: 0010 010x
+    { 0, 0x0005 },   { 0, 0x0a01 },    //  26: 0010 011x
+    { 31 << 1, 0 },  { 32 << 1, 0 },   //  27: 0000 0000x
+    { 33 << 1, 0 },  { 34 << 1, 0 },   //  28: 0000 0001x
+    { 35 << 1, 0 },  { 36 << 1, 0 },   //  29: 0000 0010x
+    { 37 << 1, 0 },  { 38 << 1, 0 },   //  30: 0000 0011x
+    { 39 << 1, 0 },  { 40 << 1, 0 },   //  31: 0000 0000 0x
+    { 41 << 1, 0 },  { 42 << 1, 0 },   //  32: 0000 0000 1x
+    { 43 << 1, 0 },  { 44 << 1, 0 },   //  33: 0000 0001 0x
+    { 45 << 1, 0 },  { 46 << 1, 0 },   //  34: 0000 0001 1x
+    { 0, 0x1001 },   { 0, 0x0502 },    //  35: 0000 0010 0x
+    { 0, 0x0007 },   { 0, 0x0203 },    //  36: 0000 0010 1x
+    { 0, 0x0104 },   { 0, 0x0f01 },    //  37: 0000 0011 0x
+    { 0, 0x0e01 },   { 0, 0x0402 },    //  38: 0000 0011 1x
+    { 47 << 1, 0 },  { 48 << 1, 0 },   //  39: 0000 0000 00x
+    { 49 << 1, 0 },  { 50 << 1, 0 },   //  40: 0000 0000 01x
+    { 51 << 1, 0 },  { 52 << 1, 0 },   //  41: 0000 0000 10x
+    { 53 << 1, 0 },  { 54 << 1, 0 },   //  42: 0000 0000 11x
+    { 55 << 1, 0 },  { 56 << 1, 0 },   //  43: 0000 0001 00x
+    { 57 << 1, 0 },  { 58 << 1, 0 },   //  44: 0000 0001 01x
+    { 59 << 1, 0 },  { 60 << 1, 0 },   //  45: 0000 0001 10x
+    { 61 << 1, 0 },  { 62 << 1, 0 },   //  46: 0000 0001 11x
+    { -1, 0 },       { 63 << 1, 0 },   //  47: 0000 0000 000x
+    { 64 << 1, 0 },  { 65 << 1, 0 },   //  48: 0000 0000 001x
+    { 66 << 1, 0 },  { 67 << 1, 0 },   //  49: 0000 0000 010x
+    { 68 << 1, 0 },  { 69 << 1, 0 },   //  50: 0000 0000 011x
+    { 70 << 1, 0 },  { 71 << 1, 0 },   //  51: 0000 0000 100x
+    { 72 << 1, 0 },  { 73 << 1, 0 },   //  52: 0000 0000 101x
+    { 74 << 1, 0 },  { 75 << 1, 0 },   //  53: 0000 0000 110x
+    { 76 << 1, 0 },  { 77 << 1, 0 },   //  54: 0000 0000 111x
+    { 0, 0x000b },   { 0, 0x0802 },    //  55: 0000 0001 000x
+    { 0, 0x0403 },   { 0, 0x000a },    //  56: 0000 0001 001x
+    { 0, 0x0204 },   { 0, 0x0702 },    //  57: 0000 0001 010x
+    { 0, 0x1501 },   { 0, 0x1401 },    //  58: 0000 0001 011x
+    { 0, 0x0009 },   { 0, 0x1301 },    //  59: 0000 0001 100x
+    { 0, 0x1201 },   { 0, 0x0105 },    //  60: 0000 0001 101x
+    { 0, 0x0303 },   { 0, 0x0008 },    //  61: 0000 0001 110x
+    { 0, 0x0602 },   { 0, 0x1101 },    //  62: 0000 0001 111x
+    { 78 << 1, 0 },  { 79 << 1, 0 },   //  63: 0000 0000 0001x
+    { 80 << 1, 0 },  { 81 << 1, 0 },   //  64: 0000 0000 0010x
+    { 82 << 1, 0 },  { 83 << 1, 0 },   //  65: 0000 0000 0011x
+    { 84 << 1, 0 },  { 85 << 1, 0 },   //  66: 0000 0000 0100x
+    { 86 << 1, 0 },  { 87 << 1, 0 },   //  67: 0000 0000 0101x
+    { 88 << 1, 0 },  { 89 << 1, 0 },   //  68: 0000 0000 0110x
+    { 90 << 1, 0 },  { 91 << 1, 0 },   //  69: 0000 0000 0111x
+    { 0, 0x0a02 },   { 0, 0x0902 },    //  70: 0000 0000 1000x
+    { 0, 0x0503 },   { 0, 0x0304 },    //  71: 0000 0000 1001x
+    { 0, 0x0205 },   { 0, 0x0107 },    //  72: 0000 0000 1010x
+    { 0, 0x0106 },   { 0, 0x000f },    //  73: 0000 0000 1011x
+    { 0, 0x000e },   { 0, 0x000d },    //  74: 0000 0000 1100x
+    { 0, 0x000c },   { 0, 0x1a01 },    //  75: 0000 0000 1101x
+    { 0, 0x1901 },   { 0, 0x1801 },    //  76: 0000 0000 1110x
+    { 0, 0x1701 },   { 0, 0x1601 },    //  77: 0000 0000 1111x
+    { 92 << 1, 0 },  { 93 << 1, 0 },   //  78: 0000 0000 0001 0x
+    { 94 << 1, 0 },  { 95 << 1, 0 },   //  79: 0000 0000 0001 1x
+    { 96 << 1, 0 },  { 97 << 1, 0 },   //  80: 0000 0000 0010 0x
+    { 98 << 1, 0 },  { 99 << 1, 0 },   //  81: 0000 0000 0010 1x
+    { 100 << 1, 0 }, { 101 << 1, 0 },  //  82: 0000 0000 0011 0x
+    { 102 << 1, 0 }, { 103 << 1, 0 },  //  83: 0000 0000 0011 1x
+    { 0, 0x001f },   { 0, 0x001e },    //  84: 0000 0000 0100 0x
+    { 0, 0x001d },   { 0, 0x001c },    //  85: 0000 0000 0100 1x
+    { 0, 0x001b },   { 0, 0x001a },    //  86: 0000 0000 0101 0x
+    { 0, 0x0019 },   { 0, 0x0018 },    //  87: 0000 0000 0101 1x
+    { 0, 0x0017 },   { 0, 0x0016 },    //  88: 0000 0000 0110 0x
+    { 0, 0x0015 },   { 0, 0x0014 },    //  89: 0000 0000 0110 1x
+    { 0, 0x0013 },   { 0, 0x0012 },    //  90: 0000 0000 0111 0x
+    { 0, 0x0011 },   { 0, 0x0010 },    //  91: 0000 0000 0111 1x
+    { 104 << 1, 0 }, { 105 << 1, 0 },  //  92: 0000 0000 0001 00x
+    { 106 << 1, 0 }, { 107 << 1, 0 },  //  93: 0000 0000 0001 01x
+    { 108 << 1, 0 }, { 109 << 1, 0 },  //  94: 0000 0000 0001 10x
+    { 110 << 1, 0 }, { 111 << 1, 0 },  //  95: 0000 0000 0001 11x
+    { 0, 0x0028 },   { 0, 0x0027 },    //  96: 0000 0000 0010 00x
+    { 0, 0x0026 },   { 0, 0x0025 },    //  97: 0000 0000 0010 01x
+    { 0, 0x0024 },   { 0, 0x0023 },    //  98: 0000 0000 0010 10x
+    { 0, 0x0022 },   { 0, 0x0021 },    //  99: 0000 0000 0010 11x
+    { 0, 0x0020 },   { 0, 0x010e },    // 100: 0000 0000 0011 00x
+    { 0, 0x010d },   { 0, 0x010c },    // 101: 0000 0000 0011 01x
+    { 0, 0x010b },   { 0, 0x010a },    // 102: 0000 0000 0011 10x
+    { 0, 0x0109 },   { 0, 0x0108 },    // 103: 0000 0000 0011 11x
+    { 0, 0x0112 },   { 0, 0x0111 },    // 104: 0000 0000 0001 000x
+    { 0, 0x0110 },   { 0, 0x010f },    // 105: 0000 0000 0001 001x
+    { 0, 0x0603 },   { 0, 0x1002 },    // 106: 0000 0000 0001 010x
+    { 0, 0x0f02 },   { 0, 0x0e02 },    // 107: 0000 0000 0001 011x
+    { 0, 0x0d02 },   { 0, 0x0c02 },    // 108: 0000 0000 0001 100x
+    { 0, 0x0b02 },   { 0, 0x1f01 },    // 109: 0000 0000 0001 101x
+    { 0, 0x1e01 },   { 0, 0x1d01 },    // 110: 0000 0000 0001 110x
+    { 0, 0x1c01 },   { 0, 0x1b01 },    // 111: 0000 0000 0001 111x
+};
+
+typedef struct {
+    int full_px;
+    int is_set;
+    int r_size;
+    int h;
+    int v;
+} plm_video_motion_t;
+
+typedef struct plm_video_t {
+    float framerate;
+    float time;
+    int frames_decoded;
+    int width;
+    int height;
+    int mb_width;
+    int mb_height;
+    int mb_size;
+
+    int luma_width;
+    int luma_height;
+
+    int chroma_width;
+    int chroma_height;
+
+    int start_code;
+    int picture_type;
+
+    plm_video_motion_t motion_forward;
+    plm_video_motion_t motion_backward;
+
+    int has_sequence_header;
+
+    int quantizer_scale;
+    int slice_begin;
+    int macroblock_address;
+
+    int mb_row;
+    int mb_col;
+
+    int macroblock_type;
+    int macroblock_intra;
+
+    int dc_predictor[3];
+
+    plm_buffer_t *buffer;
+    int destroy_buffer_when_done;
+
+    plm_frame_t frame_current;
+    plm_frame_t frame_forward;
+    plm_frame_t frame_backward;
+
+    uint8_t *frames_data;
+
+    int block_data[64];
+    uint8_t intra_quant_matrix[64];
+    uint8_t non_intra_quant_matrix[64];
+
+    int has_reference_frame;
+    int assume_no_b_frames;
+} plm_video_t;
+
+static inline uint8_t plm_clamp(int n) {
+    if (n > 255) {
+        n = 255;
+    } else if (n < 0) {
+        n = 0;
+    }
+    return n;
+}
+
+int plm_video_decode_sequence_header(plm_video_t *self);
+void plm_video_init_frame(plm_video_t *self, plm_frame_t *frame, uint8_t *base);
+void plm_video_decode_picture(plm_video_t *self);
+void plm_video_decode_slice(plm_video_t *self, int slice);
+void plm_video_decode_macroblock(plm_video_t *self);
+void plm_video_decode_motion_vectors(plm_video_t *self);
+int plm_video_decode_motion_vector(plm_video_t *self, int r_size, int motion);
+void plm_video_predict_macroblock(plm_video_t *self);
+void plm_video_copy_macroblock(plm_video_t *self, int motion_h, int motion_v,
+                               plm_frame_t *d);
+void plm_video_interpolate_macroblock(plm_video_t *self, int motion_h,
+                                      int motion_v, plm_frame_t *d);
+void plm_video_process_macroblock(plm_video_t *self, uint8_t *d, uint8_t *s,
+                                  int mh, int mb, int bs, int interp);
+void plm_video_decode_block(plm_video_t *self, int block);
+void plm_video_idct(int *block);
+
+plm_video_t *plm_video_create_with_buffer(plm_buffer_t *buffer,
+                                          int destroy_when_done) {
+    plm_video_t *self = (plm_video_t *)malloc(sizeof(plm_video_t));
+    memset(self, 0, sizeof(plm_video_t));
+
+    self->buffer = buffer;
+    self->destroy_buffer_when_done = destroy_when_done;
+
+    // Attempt to decode the sequence header
+    self->start_code =
+        plm_buffer_find_start_code(self->buffer, PLM_START_SEQUENCE);
+    if (self->start_code != -1) {
+        plm_video_decode_sequence_header(self);
+    }
+    return self;
+}
+
+void plm_video_destroy(plm_video_t *self) {
+    if (self->destroy_buffer_when_done) {
+        plm_buffer_destroy(self->buffer);
+    }
+
+    if (self->has_sequence_header) {
+        free(self->frames_data);
+    }
+
+    free(self);
+}
+
+float plm_video_get_framerate(plm_video_t *self) {
+    return plm_video_has_header(self) ? self->framerate : 0;
+}
+
+int plm_video_get_width(plm_video_t *self) {
+    return plm_video_has_header(self) ? self->width : 0;
+}
+
+int plm_video_get_height(plm_video_t *self) {
+    return plm_video_has_header(self) ? self->height : 0;
+}
+
+void plm_video_set_no_delay(plm_video_t *self, int no_delay) {
+    self->assume_no_b_frames = no_delay;
+}
+
+float plm_video_get_time(plm_video_t *self) { return self->time; }
+
+void plm_video_set_time(plm_video_t *self, float time) {
+    self->frames_decoded = self->framerate * time;
+    self->time = time;
+}
+
+void plm_video_rewind(plm_video_t *self) {
+    plm_buffer_rewind(self->buffer);
+    self->time = 0;
+    self->frames_decoded = 0;
+    self->has_reference_frame = FALSE;
+    self->start_code = -1;
+}
+
+int plm_video_has_ended(plm_video_t *self) {
+    return plm_buffer_has_ended(self->buffer);
+}
+
+plm_frame_t *plm_video_decode(plm_video_t *self) {
+    if (!plm_video_has_header(self)) {
+        return NULL;
+    }
+
+    plm_frame_t *frame = NULL;
+    do {
+        if (self->start_code != PLM_START_PICTURE) {
+            self->start_code =
+                plm_buffer_find_start_code(self->buffer, PLM_START_PICTURE);
+
+            if (self->start_code == -1) {
+                // If we reached the end of the file and the previously decoded
+                // frame was a reference frame, we still have to return it.
+                if (self->has_reference_frame && !self->assume_no_b_frames &&
+                    plm_buffer_has_ended(self->buffer) &&
+                    (self->picture_type == PLM_VIDEO_PICTURE_TYPE_INTRA ||
+                     self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE)) {
+                    self->has_reference_frame = FALSE;
+                    frame = &self->frame_backward;
+                    break;
+                }
+
+                return NULL;
+            }
+        }
+
+        // Make sure we have a full picture in the buffer before attempting to
+        // decode it. Sadly, this can only be done by seeking for the start code
+        // of the next picture. Also, if we didn't find the start code for the
+        // next picture, but the source has ended, we assume that this last
+        // picture is in the buffer.
+        if (plm_buffer_has_start_code(self->buffer, PLM_START_PICTURE) == -1 &&
+            !plm_buffer_has_ended(self->buffer)) {
+            return NULL;
+        }
+
+        plm_video_decode_picture(self);
+
+        if (self->assume_no_b_frames) {
+            frame = &self->frame_backward;
+        } else if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_B) {
+            frame = &self->frame_current;
+        } else if (self->has_reference_frame) {
+            frame = &self->frame_forward;
+        } else {
+            self->has_reference_frame = TRUE;
+        }
+    } while (!frame);
+
+    frame->time = self->time;
+    self->frames_decoded++;
+    self->time = (float)self->frames_decoded / self->framerate;
+
+    return frame;
+}
+
+int plm_video_has_header(plm_video_t *self) {
+    if (self->has_sequence_header) {
+        return TRUE;
+    }
+
+    if (self->start_code != PLM_START_SEQUENCE) {
+        self->start_code =
+            plm_buffer_find_start_code(self->buffer, PLM_START_SEQUENCE);
+    }
+    if (self->start_code == -1) {
+        return FALSE;
+    }
+
+    if (!plm_video_decode_sequence_header(self)) {
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+int plm_video_decode_sequence_header(plm_video_t *self) {
+    int max_header_size = 64 + 2 * 64 * 8;  // 64 bit header + 2x 64 byte matrix
+    if (!plm_buffer_has(self->buffer, max_header_size)) {
+        return FALSE;
+    }
+
+    self->width = plm_buffer_read(self->buffer, 12);
+    self->height = plm_buffer_read(self->buffer, 12);
+
+    if (self->width <= 0 || self->height <= 0) {
+        return FALSE;
+    }
+
+    // Skip pixel aspect ratio
+    plm_buffer_skip(self->buffer, 4);
+
+    self->framerate = PLM_VIDEO_PICTURE_RATE[plm_buffer_read(self->buffer, 4)];
+
+    // Skip bit_rate, marker, buffer_size and constrained bit
+    plm_buffer_skip(self->buffer, 18 + 1 + 10 + 1);
+
+    // Load custom intra quant matrix?
+    if (plm_buffer_read(self->buffer, 1)) {
+        for (int i = 0; i < 64; i++) {
+            int idx = PLM_VIDEO_ZIG_ZAG[i];
+            self->intra_quant_matrix[idx] = plm_buffer_read(self->buffer, 8);
+        }
+    } else {
+        memcpy(self->intra_quant_matrix, PLM_VIDEO_INTRA_QUANT_MATRIX, 64);
+    }
+
+    // Load custom non intra quant matrix?
+    if (plm_buffer_read(self->buffer, 1)) {
+        for (int i = 0; i < 64; i++) {
+            int idx = PLM_VIDEO_ZIG_ZAG[i];
+            self->non_intra_quant_matrix[idx] =
+                plm_buffer_read(self->buffer, 8);
+        }
+    } else {
+        memcpy(self->non_intra_quant_matrix, PLM_VIDEO_NON_INTRA_QUANT_MATRIX,
+               64);
+    }
+
+    self->mb_width = (self->width + 15) >> 4;
+    self->mb_height = (self->height + 15) >> 4;
+    self->mb_size = self->mb_width * self->mb_height;
+
+    self->luma_width = self->mb_width << 4;
+    self->luma_height = self->mb_height << 4;
+
+    self->chroma_width = self->mb_width << 3;
+    self->chroma_height = self->mb_height << 3;
+
+    // Allocate one big chunk of data for all 3 frames = 9 planes
+    size_t luma_plane_size = self->luma_width * self->luma_height;
+    size_t chroma_plane_size = self->chroma_width * self->chroma_height;
+    size_t frame_data_size = (luma_plane_size + 2 * chroma_plane_size);
+
+    self->frames_data = (uint8_t *)malloc(frame_data_size * 3);
+    plm_video_init_frame(self, &self->frame_current,
+                         self->frames_data + frame_data_size * 0);
+    plm_video_init_frame(self, &self->frame_forward,
+                         self->frames_data + frame_data_size * 1);
+    plm_video_init_frame(self, &self->frame_backward,
+                         self->frames_data + frame_data_size * 2);
+
+    self->has_sequence_header = TRUE;
+    return TRUE;
+}
+
+void plm_video_init_frame(plm_video_t *self, plm_frame_t *frame,
+                          uint8_t *base) {
+    size_t luma_plane_size = self->luma_width * self->luma_height;
+    size_t chroma_plane_size = self->chroma_width * self->chroma_height;
+
+    frame->width = self->width;
+    frame->height = self->height;
+    frame->y.width = self->luma_width;
+    frame->y.height = self->luma_height;
+    frame->y.data = base;
+
+    frame->cr.width = self->chroma_width;
+    frame->cr.height = self->chroma_height;
+    frame->cr.data = base + luma_plane_size;
+
+    frame->cb.width = self->chroma_width;
+    frame->cb.height = self->chroma_height;
+    frame->cb.data = base + luma_plane_size + chroma_plane_size;
+}
+
+void plm_video_decode_picture(plm_video_t *self) {
+    plm_buffer_skip(self->buffer, 10);  // skip temporalReference
+    self->picture_type = plm_buffer_read(self->buffer, 3);
+    plm_buffer_skip(self->buffer, 16);  // skip vbv_delay
+
+    // D frames or unknown coding type
+    if (self->picture_type <= 0 ||
+        self->picture_type > PLM_VIDEO_PICTURE_TYPE_B) {
+        return;
+    }
+
+    // Forward full_px, f_code
+    if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE ||
+        self->picture_type == PLM_VIDEO_PICTURE_TYPE_B) {
+        self->motion_forward.full_px = plm_buffer_read(self->buffer, 1);
+        int f_code = plm_buffer_read(self->buffer, 3);
+        if (f_code == 0) {
+            // Ignore picture with zero f_code
+            return;
+        }
+        self->motion_forward.r_size = f_code - 1;
+    }
+
+    // Backward full_px, f_code
+    if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_B) {
+        self->motion_backward.full_px = plm_buffer_read(self->buffer, 1);
+        int f_code = plm_buffer_read(self->buffer, 3);
+        if (f_code == 0) {
+            // Ignore picture with zero f_code
+            return;
+        }
+        self->motion_backward.r_size = f_code - 1;
+    }
+
+    plm_frame_t frame_temp = self->frame_forward;
+    if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_INTRA ||
+        self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE) {
+        self->frame_forward = self->frame_backward;
+    }
+
+    // Find the first slice; this skips extension and user data
+    do {
+        self->start_code = plm_buffer_next_start_code(self->buffer);
+    } while (!PLM_START_IS_SLICE(self->start_code));
+
+    // Decode all slices
+    do {
+        plm_video_decode_slice(self, self->start_code & 0x000000FF);
+        if (self->macroblock_address == self->mb_size - 1) {
+            break;
+        }
+        self->start_code = plm_buffer_next_start_code(self->buffer);
+    } while (PLM_START_IS_SLICE(self->start_code));
+
+    // If this is a reference picture rotate the prediction pointers
+    if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_INTRA ||
+        self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE) {
+        self->frame_backward = self->frame_current;
+        self->frame_current = frame_temp;
+    }
+}
+
+void plm_video_decode_slice(plm_video_t *self, int slice) {
+    self->slice_begin = TRUE;
+    self->macroblock_address = (slice - 1) * self->mb_width - 1;
+
+    // Reset motion vectors and DC predictors
+    self->motion_backward.h = self->motion_forward.h = 0;
+    self->motion_backward.v = self->motion_forward.v = 0;
+    self->dc_predictor[0] = 128;
+    self->dc_predictor[1] = 128;
+    self->dc_predictor[2] = 128;
+
+    self->quantizer_scale = plm_buffer_read(self->buffer, 5);
+
+    // Skip extra
+    while (plm_buffer_read(self->buffer, 1)) {
+        plm_buffer_skip(self->buffer, 8);
+    }
+
+    do {
+        plm_video_decode_macroblock(self);
+    } while (self->macroblock_address < self->mb_size - 1 &&
+             plm_buffer_no_start_code(self->buffer));
+}
+
+void plm_video_decode_macroblock(plm_video_t *self) {
+    // Decode self->macroblock_address_increment
+    int increment = 0;
+    int t = plm_buffer_read_vlc(self->buffer,
+                                PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT);
+
+    while (t == 34) {
+        // macroblock_stuffing
+        t = plm_buffer_read_vlc(self->buffer,
+                                PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT);
+    }
+    while (t == 35) {
+        // macroblock_escape
+        increment += 33;
+        t = plm_buffer_read_vlc(self->buffer,
+                                PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT);
+    }
+    increment += t;
+
+    // Process any skipped macroblocks
+    if (self->slice_begin) {
+        // The first self->macroblock_address_increment of each slice is
+        // relative to beginning of the preverious row, not the preverious
+        // macroblock
+        self->slice_begin = FALSE;
+        self->macroblock_address += increment;
+    } else {
+        if (self->macroblock_address + increment >= self->mb_size) {
+            return;  // invalid
+        }
+        if (increment > 1) {
+            // Skipped macroblocks reset DC predictors
+            self->dc_predictor[0] = 128;
+            self->dc_predictor[1] = 128;
+            self->dc_predictor[2] = 128;
+
+            // Skipped macroblocks in P-pictures reset motion vectors
+            if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE) {
+                self->motion_forward.h = 0;
+                self->motion_forward.v = 0;
+            }
+        }
+
+        // Predict skipped macroblocks
+        while (increment > 1) {
+            self->macroblock_address++;
+            self->mb_row = self->macroblock_address / self->mb_width;
+            self->mb_col = self->macroblock_address % self->mb_width;
+
+            plm_video_predict_macroblock(self);
+            increment--;
+        }
+        self->macroblock_address++;
+    }
+
+    self->mb_row = self->macroblock_address / self->mb_width;
+    self->mb_col = self->macroblock_address % self->mb_width;
+
+    if (self->mb_col >= self->mb_width || self->mb_row >= self->mb_height) {
+        return;  // corrupt stream;
+    }
+
+    // Process the current macroblock
+    const plm_vlc_t *table = PLM_VIDEO_MACROBLOCK_TYPE[self->picture_type];
+    self->macroblock_type = plm_buffer_read_vlc(self->buffer, table);
+
+    self->macroblock_intra = (self->macroblock_type & 0x01);
+    self->motion_forward.is_set = (self->macroblock_type & 0x08);
+    self->motion_backward.is_set = (self->macroblock_type & 0x04);
+
+    // Quantizer scale
+    if ((self->macroblock_type & 0x10) != 0) {
+        self->quantizer_scale = plm_buffer_read(self->buffer, 5);
+    }
+
+    if (self->macroblock_intra) {
+        // Intra-coded macroblocks reset motion vectors
+        self->motion_backward.h = self->motion_forward.h = 0;
+        self->motion_backward.v = self->motion_forward.v = 0;
+    } else {
+        // Non-intra macroblocks reset DC predictors
+        self->dc_predictor[0] = 128;
+        self->dc_predictor[1] = 128;
+        self->dc_predictor[2] = 128;
+
+        plm_video_decode_motion_vectors(self);
+        plm_video_predict_macroblock(self);
+    }
+
+    // Decode blocks
+    int cbp =
+        ((self->macroblock_type & 0x02) != 0)
+            ? plm_buffer_read_vlc(self->buffer, PLM_VIDEO_CODE_BLOCK_PATTERN)
+            : (self->macroblock_intra ? 0x3f : 0);
+
+    for (int block = 0, mask = 0x20; block < 6; block++) {
+        if ((cbp & mask) != 0) {
+            plm_video_decode_block(self, block);
+        }
+        mask >>= 1;
+    }
+}
+
+void plm_video_decode_motion_vectors(plm_video_t *self) {
+    // Forward
+    if (self->motion_forward.is_set) {
+        int r_size = self->motion_forward.r_size;
+        self->motion_forward.h = plm_video_decode_motion_vector(
+            self, r_size, self->motion_forward.h);
+        self->motion_forward.v = plm_video_decode_motion_vector(
+            self, r_size, self->motion_forward.v);
+    } else if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE) {
+        // No motion information in P-picture, reset vectors
+        self->motion_forward.h = 0;
+        self->motion_forward.v = 0;
+    }
+
+    if (self->motion_backward.is_set) {
+        int r_size = self->motion_backward.r_size;
+        self->motion_backward.h = plm_video_decode_motion_vector(
+            self, r_size, self->motion_backward.h);
+        self->motion_backward.v = plm_video_decode_motion_vector(
+            self, r_size, self->motion_backward.v);
+    }
+}
+
+int plm_video_decode_motion_vector(plm_video_t *self, int r_size, int motion) {
+    int fscale = 1 << r_size;
+    int m_code = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MOTION);
+    int r = 0;
+    int d;
+
+    if ((m_code != 0) && (fscale != 1)) {
+        r = plm_buffer_read(self->buffer, r_size);
+        d = ((abs(m_code) - 1) << r_size) + r + 1;
+        if (m_code < 0) {
+            d = -d;
+        }
+    } else {
+        d = m_code;
+    }
+
+    motion += d;
+    if (motion > (fscale << 4) - 1) {
+        motion -= fscale << 5;
+    } else if (motion < ((-fscale) << 4)) {
+        motion += fscale << 5;
+    }
+
+    return motion;
+}
+
+void plm_video_predict_macroblock(plm_video_t *self) {
+    int fw_h = self->motion_forward.h;
+    int fw_v = self->motion_forward.v;
+
+    if (self->motion_forward.full_px) {
+        fw_h <<= 1;
+        fw_v <<= 1;
+    }
+
+    if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_B) {
+        int bw_h = self->motion_backward.h;
+        int bw_v = self->motion_backward.v;
+
+        if (self->motion_backward.full_px) {
+            bw_h <<= 1;
+            bw_v <<= 1;
+        }
+
+        if (self->motion_forward.is_set) {
+            plm_video_copy_macroblock(self, fw_h, fw_v, &self->frame_forward);
+            if (self->motion_backward.is_set) {
+                plm_video_interpolate_macroblock(self, bw_h, bw_v,
+                                                 &self->frame_backward);
+            }
+        } else {
+            plm_video_copy_macroblock(self, bw_h, bw_v, &self->frame_backward);
+        }
+    } else {
+        plm_video_copy_macroblock(self, fw_h, fw_v, &self->frame_forward);
+    }
+}
+
+void plm_video_copy_macroblock(plm_video_t *self, int motion_h, int motion_v,
+                               plm_frame_t *d) {
+    plm_frame_t *s = &self->frame_current;
+    plm_video_process_macroblock(self, s->y.data, d->y.data, motion_h, motion_v,
+                                 16, FALSE);
+    plm_video_process_macroblock(self, s->cr.data, d->cr.data, motion_h / 2,
+                                 motion_v / 2, 8, FALSE);
+    plm_video_process_macroblock(self, s->cb.data, d->cb.data, motion_h / 2,
+                                 motion_v / 2, 8, FALSE);
+}
+
+void plm_video_interpolate_macroblock(plm_video_t *self, int motion_h,
+                                      int motion_v, plm_frame_t *d) {
+    plm_frame_t *s = &self->frame_current;
+    plm_video_process_macroblock(self, s->y.data, d->y.data, motion_h, motion_v,
+                                 16, TRUE);
+    plm_video_process_macroblock(self, s->cr.data, d->cr.data, motion_h / 2,
+                                 motion_v / 2, 8, TRUE);
+    plm_video_process_macroblock(self, s->cb.data, d->cb.data, motion_h / 2,
+                                 motion_v / 2, 8, TRUE);
+}
+
+#define PLM_BLOCK_SET(DEST, DEST_INDEX, DEST_WIDTH, SOURCE_INDEX, \
+                      SOURCE_WIDTH, BLOCK_SIZE, OP)               \
+    do {                                                          \
+        int dest_scan = DEST_WIDTH - BLOCK_SIZE;                  \
+        int source_scan = SOURCE_WIDTH - BLOCK_SIZE;              \
+        for (int y = 0; y < BLOCK_SIZE; y++) {                    \
+            for (int x = 0; x < BLOCK_SIZE; x++) {                \
+                DEST[DEST_INDEX] = OP;                            \
+                SOURCE_INDEX++;                                   \
+                DEST_INDEX++;                                     \
+            }                                                     \
+            SOURCE_INDEX += source_scan;                          \
+            DEST_INDEX += dest_scan;                              \
+        }                                                         \
+    } while (FALSE)
+
+void plm_video_process_macroblock(plm_video_t *self, uint8_t *d, uint8_t *s,
+                                  int motion_h, int motion_v, int block_size,
+                                  int interpolate) {
+    int dw = self->mb_width * block_size;
+
+    int hp = motion_h >> 1;
+    int vp = motion_v >> 1;
+    int odd_h = (motion_h & 1) == 1;
+    int odd_v = (motion_v & 1) == 1;
+
+    unsigned int si = ((self->mb_row * block_size) + vp) * dw +
+                      (self->mb_col * block_size) + hp;
+    unsigned int di = (self->mb_row * dw + self->mb_col) * block_size;
+
+    unsigned int max_address =
+        (dw * (self->mb_height * block_size - block_size + 1) - block_size);
+    if (si > max_address || di > max_address) {
+        return;  // corrupt video
+    }
+
+#define PLM_MB_CASE(INTERPOLATE, ODD_H, ODD_V, OP)        \
+    case ((INTERPOLATE << 2) | (ODD_H << 1) | (ODD_V)):   \
+        PLM_BLOCK_SET(d, di, dw, si, dw, block_size, OP); \
+        break
+
+    switch ((interpolate << 2) | (odd_h << 1) | (odd_v)) {
+        PLM_MB_CASE(0, 0, 0, (s[si]));
+        PLM_MB_CASE(0, 0, 1, (s[si] + s[si + dw] + 1) >> 1);
+        PLM_MB_CASE(0, 1, 0, (s[si] + s[si + 1] + 1) >> 1);
+        PLM_MB_CASE(0, 1, 1,
+                    (s[si] + s[si + 1] + s[si + dw] + s[si + dw + 1] + 2) >> 2);
+
+        PLM_MB_CASE(1, 0, 0, (d[di] + (s[si]) + 1) >> 1);
+        PLM_MB_CASE(1, 0, 1,
+                    (d[di] + ((s[si] + s[si + dw] + 1) >> 1) + 1) >> 1);
+        PLM_MB_CASE(1, 1, 0, (d[di] + ((s[si] + s[si + 1] + 1) >> 1) + 1) >> 1);
+        PLM_MB_CASE(
+            1, 1, 1,
+            (d[di] +
+             ((s[si] + s[si + 1] + s[si + dw] + s[si + dw + 1] + 2) >> 2) +
+             1) >>
+                1);
+    }
+
+#undef PLM_MB_CASE
+}
+
+void plm_video_decode_block(plm_video_t *self, int block) {
+    int n = 0;
+    uint8_t *quant_matrix;
+
+    // Decode DC coefficient of intra-coded blocks
+    if (self->macroblock_intra) {
+        int predictor;
+        int dct_size;
+
+        // DC prediction
+        int plane_index = block > 3 ? block - 3 : 0;
+        predictor = self->dc_predictor[plane_index];
+        dct_size =
+            plm_buffer_read_vlc(self->buffer, PLM_VIDEO_DCT_SIZE[plane_index]);
+
+        // Read DC coeff
+        if (dct_size > 0) {
+            int differential = plm_buffer_read(self->buffer, dct_size);
+            if ((differential & (1 << (dct_size - 1))) != 0) {
+                self->block_data[0] = predictor + differential;
+            } else {
+                self->block_data[0] =
+                    predictor + (-(1 << dct_size) | (differential + 1));
+            }
+        } else {
+            self->block_data[0] = predictor;
+        }
+
+        // Save predictor value
+        self->dc_predictor[plane_index] = self->block_data[0];
+
+        // Dequantize + premultiply
+        self->block_data[0] <<= (3 + 5);
+
+        quant_matrix = self->intra_quant_matrix;
+        n = 1;
+    } else {
+        quant_matrix = self->non_intra_quant_matrix;
+    }
+
+    // Decode AC coefficients (+DC for non-intra)
+    int level = 0;
+    while (TRUE) {
+        int run = 0;
+        uint16_t coeff =
+            plm_buffer_read_vlc_uint(self->buffer, PLM_VIDEO_DCT_COEFF);
+
+        if ((coeff == 0x0001) && (n > 0) &&
+            (plm_buffer_read(self->buffer, 1) == 0)) {
+            // end_of_block
+            break;
+        }
+        if (coeff == 0xffff) {
+            // escape
+            run = plm_buffer_read(self->buffer, 6);
+            level = plm_buffer_read(self->buffer, 8);
+            if (level == 0) {
+                level = plm_buffer_read(self->buffer, 8);
+            } else if (level == 128) {
+                level = plm_buffer_read(self->buffer, 8) - 256;
+            } else if (level > 128) {
+                level = level - 256;
+            }
+        } else {
+            run = coeff >> 8;
+            level = coeff & 0xff;
+            if (plm_buffer_read(self->buffer, 1)) {
+                level = -level;
+            }
+        }
+
+        n += run;
+        if (n < 0 || n >= 64) {
+            return;  // invalid
+        }
+
+        int de_zig_zagged = PLM_VIDEO_ZIG_ZAG[n];
+        n++;
+
+        // Dequantize, oddify, clip
+        level <<= 1;
+        if (!self->macroblock_intra) {
+            level += (level < 0 ? -1 : 1);
+        }
+        level =
+            (level * self->quantizer_scale * quant_matrix[de_zig_zagged]) >> 4;
+        if ((level & 1) == 0) {
+            level -= level > 0 ? 1 : -1;
+        }
+        if (level > 2047) {
+            level = 2047;
+        } else if (level < -2048) {
+            level = -2048;
+        }
+
+        // Save premultiplied coefficient
+        self->block_data[de_zig_zagged] =
+            level * PLM_VIDEO_PREMULTIPLIER_MATRIX[de_zig_zagged];
+    }
+
+    // Move block to its place
+    uint8_t *d;
+    int dw;
+    int di;
+
+    if (block < 4) {
+        d = self->frame_current.y.data;
+        dw = self->luma_width;
+        di = (self->mb_row * self->luma_width + self->mb_col) << 4;
+        if ((block & 1) != 0) {
+            di += 8;
+        }
+        if ((block & 2) != 0) {
+            di += self->luma_width << 3;
+        }
+    } else {
+        d = (block == 4) ? self->frame_current.cb.data
+                         : self->frame_current.cr.data;
+        dw = self->chroma_width;
+        di = ((self->mb_row * self->luma_width) << 2) + (self->mb_col << 3);
+    }
+
+    int *s = self->block_data;
+    int si = 0;
+    if (self->macroblock_intra) {
+        // Overwrite (no prediction)
+        if (n == 1) {
+            int clamped = plm_clamp((s[0] + 128) >> 8);
+            PLM_BLOCK_SET(d, di, dw, si, 8, 8, clamped);
+            s[0] = 0;
+        } else {
+            plm_video_idct(s);
+            PLM_BLOCK_SET(d, di, dw, si, 8, 8, plm_clamp(s[si]));
+            memset(self->block_data, 0, sizeof(self->block_data));
+        }
+    } else {
+        // Add data to the predicted macroblock
+        if (n == 1) {
+            int value = (s[0] + 128) >> 8;
+            PLM_BLOCK_SET(d, di, dw, si, 8, 8, plm_clamp(d[di] + value));
+            s[0] = 0;
+        } else {
+            plm_video_idct(s);
+            PLM_BLOCK_SET(d, di, dw, si, 8, 8, plm_clamp(d[di] + s[si]));
+            memset(self->block_data, 0, sizeof(self->block_data));
+        }
+    }
+}
+
+void plm_video_idct(int *block) {
+    int b1, b3, b4, b6, b7, tmp1, tmp2, m0, x0, x1, x2, x3, x4, y3, y4, y5, y6,
+        y7;
+
+    // Transform columns
+    for (int i = 0; i < 8; ++i) {
+        b1 = block[4 * 8 + i];
+        b3 = block[2 * 8 + i] + block[6 * 8 + i];
+        b4 = block[5 * 8 + i] - block[3 * 8 + i];
+        tmp1 = block[1 * 8 + i] + block[7 * 8 + i];
+        tmp2 = block[3 * 8 + i] + block[5 * 8 + i];
+        b6 = block[1 * 8 + i] - block[7 * 8 + i];
+        b7 = tmp1 + tmp2;
+        m0 = block[0 * 8 + i];
+        x4 = ((b6 * 473 - b4 * 196 + 128) >> 8) - b7;
+        x0 = x4 - (((tmp1 - tmp2) * 362 + 128) >> 8);
+        x1 = m0 - b1;
+        x2 = (((block[2 * 8 + i] - block[6 * 8 + i]) * 362 + 128) >> 8) - b3;
+        x3 = m0 + b1;
+        y3 = x1 + x2;
+        y4 = x3 + b3;
+        y5 = x1 - x2;
+        y6 = x3 - b3;
+        y7 = -x0 - ((b4 * 473 + b6 * 196 + 128) >> 8);
+        block[0 * 8 + i] = b7 + y4;
+        block[1 * 8 + i] = x4 + y3;
+        block[2 * 8 + i] = y5 - x0;
+        block[3 * 8 + i] = y6 - y7;
+        block[4 * 8 + i] = y6 + y7;
+        block[5 * 8 + i] = x0 + y5;
+        block[6 * 8 + i] = y3 - x4;
+        block[7 * 8 + i] = y4 - b7;
+    }
+
+    // Transform rows
+    for (int i = 0; i < 64; i += 8) {
+        b1 = block[4 + i];
+        b3 = block[2 + i] + block[6 + i];
+        b4 = block[5 + i] - block[3 + i];
+        tmp1 = block[1 + i] + block[7 + i];
+        tmp2 = block[3 + i] + block[5 + i];
+        b6 = block[1 + i] - block[7 + i];
+        b7 = tmp1 + tmp2;
+        m0 = block[0 + i];
+        x4 = ((b6 * 473 - b4 * 196 + 128) >> 8) - b7;
+        x0 = x4 - (((tmp1 - tmp2) * 362 + 128) >> 8);
+        x1 = m0 - b1;
+        x2 = (((block[2 + i] - block[6 + i]) * 362 + 128) >> 8) - b3;
+        x3 = m0 + b1;
+        y3 = x1 + x2;
+        y4 = x3 + b3;
+        y5 = x1 - x2;
+        y6 = x3 - b3;
+        y7 = -x0 - ((b4 * 473 + b6 * 196 + 128) >> 8);
+        block[0 + i] = (b7 + y4 + 128) >> 8;
+        block[1 + i] = (x4 + y3 + 128) >> 8;
+        block[2 + i] = (y5 - x0 + 128) >> 8;
+        block[3 + i] = (y6 - y7 + 128) >> 8;
+        block[4 + i] = (y6 + y7 + 128) >> 8;
+        block[5 + i] = (x0 + y5 + 128) >> 8;
+        block[6 + i] = (y3 - x4 + 128) >> 8;
+        block[7 + i] = (y4 - b7 + 128) >> 8;
+    }
+}
+
+// YCbCr conversion following the BT.601 standard:
+// https://infogalactic.com/info/YCbCr#ITU-R_BT.601_conversion
+
+#define PLM_PUT_PIXEL(RI, GI, BI, Y_OFFSET, DEST_OFFSET)          \
+    y = ((frame->y.data[y_index + Y_OFFSET] - 16) * 76309) >> 16; \
+    dest[d_index + DEST_OFFSET + RI] = plm_clamp(y + r);          \
+    dest[d_index + DEST_OFFSET + GI] = plm_clamp(y - g);          \
+    dest[d_index + DEST_OFFSET + BI] = plm_clamp(y + b);
+
+#define PLM_DEFINE_FRAME_CONVERT_FUNCTION(NAME, BYTES_PER_PIXEL, RI, GI, BI) \
+    void NAME(plm_frame_t *frame, uint8_t *dest, int stride) {               \
+        int cols = frame->width >> 1;                                        \
+        int rows = frame->height >> 1;                                       \
+        int yw = frame->y.width;                                             \
+        int cw = frame->cb.width;                                            \
+        for (int row = 0; row < rows; row++) {                               \
+            int c_index = row * cw;                                          \
+            int y_index = row * 2 * yw;                                      \
+            int d_index = row * 2 * stride;                                  \
+            for (int col = 0; col < cols; col++) {                           \
+                int y;                                                       \
+                int cr = frame->cr.data[c_index] - 128;                      \
+                int cb = frame->cb.data[c_index] - 128;                      \
+                int r = (cr * 104597) >> 16;                                 \
+                int g = (cb * 25674 + cr * 53278) >> 16;                     \
+                int b = (cb * 132201) >> 16;                                 \
+                PLM_PUT_PIXEL(RI, GI, BI, 0, 0);                             \
+                PLM_PUT_PIXEL(RI, GI, BI, 1, BYTES_PER_PIXEL);               \
+                PLM_PUT_PIXEL(RI, GI, BI, yw, stride);                       \
+                PLM_PUT_PIXEL(RI, GI, BI, yw + 1, stride + BYTES_PER_PIXEL); \
+                c_index += 1;                                                \
+                y_index += 2;                                                \
+                d_index += 2 * BYTES_PER_PIXEL;                              \
+            }                                                                \
+        }                                                                    \
+    }
+
+PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_rgb, 3, 0, 1, 2)
+PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_bgr, 3, 2, 1, 0)
+PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_rgba, 4, 0, 1, 2)
+PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_bgra, 4, 2, 1, 0)
+PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_argb, 4, 1, 2, 3)
+PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_abgr, 4, 3, 2, 1)
+
+#undef PLM_PUT_PIXEL
+#undef PLM_DEFINE_FRAME_CONVERT_FUNCTION
+
+// -----------------------------------------------------------------------------
+// plm_audio implementation
+
+// Based on kjmp2 by Martin J. Fiedler
+// http://keyj.emphy.de/kjmp2/
+
+static const int PLM_AUDIO_FRAME_SYNC = 0x7ff;
+
+static const int PLM_AUDIO_MPEG_2_5 = 0x0;
+static const int PLM_AUDIO_MPEG_2 = 0x2;
+static const int PLM_AUDIO_MPEG_1 = 0x3;
+
+static const int PLM_AUDIO_LAYER_III = 0x1;
+static const int PLM_AUDIO_LAYER_II = 0x2;
+static const int PLM_AUDIO_LAYER_I = 0x3;
+
+static const int PLM_AUDIO_MODE_STEREO = 0x0;
+static const int PLM_AUDIO_MODE_JOINT_STEREO = 0x1;
+static const int PLM_AUDIO_MODE_DUAL_CHANNEL = 0x2;
+static const int PLM_AUDIO_MODE_MONO = 0x3;
+
+static const unsigned short PLM_AUDIO_SAMPLE_RATE[] = {
+    44100, 48000, 32000, 0,  // MPEG-1
+    22050, 24000, 16000, 0   // MPEG-2
+};
+
+static const short PLM_AUDIO_BIT_RATE[] = {
+    32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384,  // MPEG-1
+    8,  16, 24, 32, 40, 48, 56,  64,  80,  96,  112, 128, 144, 160   // MPEG-2
+};
+
+static const int PLM_AUDIO_SCALEFACTOR_BASE[] = { 0x02000000, 0x01965FEA,
+                                                  0x01428A30 };
+
+static const float PLM_AUDIO_SYNTHESIS_WINDOW[] = {
+    0.0,      -0.5,     -0.5,     -0.5,     -0.5,     -0.5,     -0.5,
+    -1.0,     -1.0,     -1.0,     -1.0,     -1.5,     -1.5,     -2.0,
+    -2.0,     -2.5,     -2.5,     -3.0,     -3.5,     -3.5,     -4.0,
+    -4.5,     -5.0,     -5.5,     -6.5,     -7.0,     -8.0,     -8.5,
+    -9.5,     -10.5,    -12.0,    -13.0,    -14.5,    -15.5,    -17.5,
+    -19.0,    -20.5,    -22.5,    -24.5,    -26.5,    -29.0,    -31.5,
+    -34.0,    -36.5,    -39.5,    -42.5,    -45.5,    -48.5,    -52.0,
+    -55.5,    -58.5,    -62.5,    -66.0,    -69.5,    -73.5,    -77.0,
+    -80.5,    -84.5,    -88.0,    -91.5,    -95.0,    -98.0,    -101.0,
+    -104.0,   106.5,    109.0,    111.0,    112.5,    113.5,    114.0,
+    114.0,    113.5,    112.0,    110.5,    107.5,    104.0,    100.0,
+    94.5,     88.5,     81.5,     73.0,     63.5,     53.0,     41.5,
+    28.5,     14.5,     -1.0,     -18.0,    -36.0,    -55.5,    -76.5,
+    -98.5,    -122.0,   -147.0,   -173.5,   -200.5,   -229.5,   -259.5,
+    -290.5,   -322.5,   -355.5,   -389.5,   -424.0,   -459.5,   -495.5,
+    -532.0,   -568.5,   -605.0,   -641.5,   -678.0,   -714.0,   -749.0,
+    -783.5,   -817.0,   -849.0,   -879.5,   -908.5,   -935.0,   -959.5,
+    -981.0,   -1000.5,  -1016.0,  -1028.5,  -1037.5,  -1042.5,  -1043.5,
+    -1040.0,  -1031.5,  1018.5,   1000.0,   976.0,    946.5,    911.0,
+    869.5,    822.0,    767.5,    707.0,    640.0,    565.5,    485.0,
+    397.0,    302.5,    201.0,    92.5,     -22.5,    -144.0,   -272.5,
+    -407.0,   -547.5,   -694.0,   -846.0,   -1003.0,  -1165.0,  -1331.5,
+    -1502.0,  -1675.5,  -1852.5,  -2031.5,  -2212.5,  -2394.0,  -2576.5,
+    -2758.5,  -2939.5,  -3118.5,  -3294.5,  -3467.5,  -3635.5,  -3798.5,
+    -3955.0,  -4104.5,  -4245.5,  -4377.5,  -4499.0,  -4609.5,  -4708.0,
+    -4792.5,  -4863.5,  -4919.0,  -4958.0,  -4979.5,  -4983.0,  -4967.5,
+    -4931.5,  -4875.0,  -4796.0,  -4694.5,  -4569.5,  -4420.0,  -4246.0,
+    -4046.0,  -3820.0,  -3567.0,  3287.0,   2979.5,   2644.0,   2280.5,
+    1888.0,   1467.5,   1018.5,   541.0,    35.0,     -499.0,   -1061.0,
+    -1650.0,  -2266.5,  -2909.0,  -3577.0,  -4270.0,  -4987.5,  -5727.5,
+    -6490.0,  -7274.0,  -8077.5,  -8899.5,  -9739.0,  -10594.5, -11464.5,
+    -12347.0, -13241.0, -14144.5, -15056.0, -15973.5, -16895.5, -17820.0,
+    -18744.5, -19668.0, -20588.0, -21503.0, -22410.5, -23308.5, -24195.0,
+    -25068.5, -25926.5, -26767.0, -27589.0, -28389.0, -29166.5, -29919.0,
+    -30644.5, -31342.0, -32009.5, -32645.0, -33247.0, -33814.5, -34346.0,
+    -34839.5, -35295.0, -35710.0, -36084.5, -36417.5, -36707.5, -36954.0,
+    -37156.5, -37315.0, -37428.0, -37496.0, 37519.0,  37496.0,  37428.0,
+    37315.0,  37156.5,  36954.0,  36707.5,  36417.5,  36084.5,  35710.0,
+    35295.0,  34839.5,  34346.0,  33814.5,  33247.0,  32645.0,  32009.5,
+    31342.0,  30644.5,  29919.0,  29166.5,  28389.0,  27589.0,  26767.0,
+    25926.5,  25068.5,  24195.0,  23308.5,  22410.5,  21503.0,  20588.0,
+    19668.0,  18744.5,  17820.0,  16895.5,  15973.5,  15056.0,  14144.5,
+    13241.0,  12347.0,  11464.5,  10594.5,  9739.0,   8899.5,   8077.5,
+    7274.0,   6490.0,   5727.5,   4987.5,   4270.0,   3577.0,   2909.0,
+    2266.5,   1650.0,   1061.0,   499.0,    -35.0,    -541.0,   -1018.5,
+    -1467.5,  -1888.0,  -2280.5,  -2644.0,  -2979.5,  3287.0,   3567.0,
+    3820.0,   4046.0,   4246.0,   4420.0,   4569.5,   4694.5,   4796.0,
+    4875.0,   4931.5,   4967.5,   4983.0,   4979.5,   4958.0,   4919.0,
+    4863.5,   4792.5,   4708.0,   4609.5,   4499.0,   4377.5,   4245.5,
+    4104.5,   3955.0,   3798.5,   3635.5,   3467.5,   3294.5,   3118.5,
+    2939.5,   2758.5,   2576.5,   2394.0,   2212.5,   2031.5,   1852.5,
+    1675.5,   1502.0,   1331.5,   1165.0,   1003.0,   846.0,    694.0,
+    547.5,    407.0,    272.5,    144.0,    22.5,     -92.5,    -201.0,
+    -302.5,   -397.0,   -485.0,   -565.5,   -640.0,   -707.0,   -767.5,
+    -822.0,   -869.5,   -911.0,   -946.5,   -976.0,   -1000.0,  1018.5,
+    1031.5,   1040.0,   1043.5,   1042.5,   1037.5,   1028.5,   1016.0,
+    1000.5,   981.0,    959.5,    935.0,    908.5,    879.5,    849.0,
+    817.0,    783.5,    749.0,    714.0,    678.0,    641.5,    605.0,
+    568.5,    532.0,    495.5,    459.5,    424.0,    389.5,    355.5,
+    322.5,    290.5,    259.5,    229.5,    200.5,    173.5,    147.0,
+    122.0,    98.5,     76.5,     55.5,     36.0,     18.0,     1.0,
+    -14.5,    -28.5,    -41.5,    -53.0,    -63.5,    -73.0,    -81.5,
+    -88.5,    -94.5,    -100.0,   -104.0,   -107.5,   -110.5,   -112.0,
+    -113.5,   -114.0,   -114.0,   -113.5,   -112.5,   -111.0,   -109.0,
+    106.5,    104.0,    101.0,    98.0,     95.0,     91.5,     88.0,
+    84.5,     80.5,     77.0,     73.5,     69.5,     66.0,     62.5,
+    58.5,     55.5,     52.0,     48.5,     45.5,     42.5,     39.5,
+    36.5,     34.0,     31.5,     29.0,     26.5,     24.5,     22.5,
+    20.5,     19.0,     17.5,     15.5,     14.5,     13.0,     12.0,
+    10.5,     9.5,      8.5,      8.0,      7.0,      6.5,      5.5,
+    5.0,      4.5,      4.0,      3.5,      3.5,      3.0,      2.5,
+    2.5,      2.0,      2.0,      1.5,      1.5,      1.0,      1.0,
+    1.0,      1.0,      0.5,      0.5,      0.5,      0.5,      0.5,
+    0.5
+};
+
+// Quantizer lookup, step 1: bitrate classes
+static const uint8_t PLM_AUDIO_QUANT_LUT_STEP_1[2][16] = {
+    // 32, 48, 56, 64, 80, 96,112,128,160,192,224,256,320,384 <- bitrate
+    { 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2 },  // mono
+    // 16, 24, 28, 32, 40, 48, 56, 64, 80, 96,112,128,160,192 <- bitrate / chan
+    { 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2 }  // stereo
+};
+
+// Quantizer lookup, step 2: bitrate class, sample rate -> B2 table idx, sblimit
+#define PLM_AUDIO_QUANT_TAB_A \
+    (27 | 64)  // Table 3-B.2a: high-rate, sblimit = 27
+#define PLM_AUDIO_QUANT_TAB_B \
+    (30 | 64)                     // Table 3-B.2b: high-rate, sblimit = 30
+#define PLM_AUDIO_QUANT_TAB_C 8   // Table 3-B.2c:  low-rate, sblimit =  8
+#define PLM_AUDIO_QUANT_TAB_D 12  // Table 3-B.2d:  low-rate, sblimit = 12
+
+static const uint8_t QUANT_LUT_STEP_2[3][3] = {
+    // 44.1 kHz,              48 kHz,                32 kHz
+    { PLM_AUDIO_QUANT_TAB_C, PLM_AUDIO_QUANT_TAB_C,
+      PLM_AUDIO_QUANT_TAB_D },  // 32 - 48 kbit/sec/ch
+    { PLM_AUDIO_QUANT_TAB_A, PLM_AUDIO_QUANT_TAB_A,
+      PLM_AUDIO_QUANT_TAB_A },  // 56 - 80 kbit/sec/ch
+    { PLM_AUDIO_QUANT_TAB_B, PLM_AUDIO_QUANT_TAB_A,
+      PLM_AUDIO_QUANT_TAB_B }  // 96+	 kbit/sec/ch
+};
+
+// Quantizer lookup, step 3: B2 table, subband -> nbal, row index
+// (upper 4 bits: nbal, lower 4 bits: row index)
+static const uint8_t PLM_AUDIO_QUANT_LUT_STEP_3[3][32] = {
+    // Low-rate table (3-B.2c and 3-B.2d)
+    { 0x44, 0x44, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34 },
+    // High-rate table (3-B.2a and 3-B.2b)
+    { 0x43, 0x43, 0x43, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,
+      0x42, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31,
+      0x31, 0x31, 0x31, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 },
+    // MPEG-2 LSR table (B.2 in ISO 13818-3)
+    { 0x45, 0x45, 0x45, 0x45, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34,
+      0x34, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24,
+      0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24 }
+};
+
+// Quantizer lookup, step 4: table row, allocation[] value -> quant table index
+static const uint8_t PLM_AUDIO_QUANT_LUT_STEP4[6][16] = {
+    { 0, 1, 2, 17 },
+    { 0, 1, 2, 3, 4, 5, 6, 17 },
+    { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17 },
+    { 0, 1, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 },
+    { 0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17 },
+    { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }
+};
+
+typedef struct plm_quantizer_spec_t {
+    unsigned short levels;
+    unsigned char group;
+    unsigned char bits;
+} plm_quantizer_spec_t;
+
+static const plm_quantizer_spec_t PLM_AUDIO_QUANT_TAB[] = {
+    { 3, 1, 5 },       //  1
+    { 5, 1, 7 },       //  2
+    { 7, 0, 3 },       //  3
+    { 9, 1, 10 },      //  4
+    { 15, 0, 4 },      //  5
+    { 31, 0, 5 },      //  6
+    { 63, 0, 6 },      //  7
+    { 127, 0, 7 },     //  8
+    { 255, 0, 8 },     //  9
+    { 511, 0, 9 },     // 10
+    { 1023, 0, 10 },   // 11
+    { 2047, 0, 11 },   // 12
+    { 4095, 0, 12 },   // 13
+    { 8191, 0, 13 },   // 14
+    { 16383, 0, 14 },  // 15
+    { 32767, 0, 15 },  // 16
+    { 65535, 0, 16 }   // 17
+};
+
+typedef struct plm_audio_t {
+    float time;
+    int samples_decoded;
+    int samplerate_index;
+    int bitrate_index;
+    int version;
+    int layer;
+    int mode;
+    int bound;
+    int v_pos;
+    int next_frame_data_size;
+    int has_header;
+
+    plm_buffer_t *buffer;
+    int destroy_buffer_when_done;
+
+    const plm_quantizer_spec_t *allocation[2][32];
+    uint8_t scale_factor_info[2][32];
+    int scale_factor[2][32][3];
+    int sample[2][32][3];
+
+    plm_samples_t samples;
+    float D[1024];
+    float V[2][1024];
+    float U[32];
+} plm_audio_t;
+
+int plm_audio_find_frame_sync(plm_audio_t *self);
+int plm_audio_decode_header(plm_audio_t *self);
+void plm_audio_decode_frame(plm_audio_t *self);
+const plm_quantizer_spec_t *plm_audio_read_allocation(plm_audio_t *self, int sb,
+                                                      int tab3);
+void plm_audio_read_samples(plm_audio_t *self, int ch, int sb, int part);
+void plm_audio_matrix_transform(int s[32][3], int ss, float *d, int dp);
+
+plm_audio_t *plm_audio_create_with_buffer(plm_buffer_t *buffer,
+                                          int destroy_when_done) {
+    plm_audio_t *self = (plm_audio_t *)malloc(sizeof(plm_audio_t));
+    memset(self, 0, sizeof(plm_audio_t));
+
+    self->samples.count = PLM_AUDIO_SAMPLES_PER_FRAME;
+    self->buffer = buffer;
+    self->destroy_buffer_when_done = destroy_when_done;
+    self->samplerate_index = 3;  // Indicates 0
+
+    memcpy(self->D, PLM_AUDIO_SYNTHESIS_WINDOW, 512 * sizeof(float));
+    memcpy(self->D + 512, PLM_AUDIO_SYNTHESIS_WINDOW, 512 * sizeof(float));
+
+    // Attempt to decode first header
+    self->next_frame_data_size = plm_audio_decode_header(self);
+
+    return self;
+}
+
+void plm_audio_destroy(plm_audio_t *self) {
+    if (self->destroy_buffer_when_done) {
+        plm_buffer_destroy(self->buffer);
+    }
+    free(self);
+}
+
+int plm_audio_has_header(plm_audio_t *self) {
+    if (self->has_header) {
+        return TRUE;
+    }
+
+    self->next_frame_data_size = plm_audio_decode_header(self);
+    return self->has_header;
+}
+
+int plm_audio_get_samplerate(plm_audio_t *self) {
+    return plm_audio_has_header(self)
+               ? PLM_AUDIO_SAMPLE_RATE[self->samplerate_index]
+               : 0;
+}
+
+float plm_audio_get_time(plm_audio_t *self) { return self->time; }
+
+void plm_audio_set_time(plm_audio_t *self, float time) {
+    self->samples_decoded =
+        time * (float)PLM_AUDIO_SAMPLE_RATE[self->samplerate_index];
+    self->time = time;
+}
+
+void plm_audio_rewind(plm_audio_t *self) {
+    plm_buffer_rewind(self->buffer);
+    self->time = 0;
+    self->samples_decoded = 0;
+    self->next_frame_data_size = 0;
+}
+
+int plm_audio_has_ended(plm_audio_t *self) {
+    return plm_buffer_has_ended(self->buffer);
+}
+
+plm_samples_t *plm_audio_decode(plm_audio_t *self) {
+    // Do we have at least enough information to decode the frame header?
+    if (!self->next_frame_data_size) {
+        if (!plm_buffer_has(self->buffer, 48)) {
+            return NULL;
+        }
+        self->next_frame_data_size = plm_audio_decode_header(self);
+    }
+
+    if (self->next_frame_data_size == 0 ||
+        !plm_buffer_has(self->buffer, self->next_frame_data_size << 3)) {
+        return NULL;
+    }
+
+    plm_audio_decode_frame(self);
+    self->next_frame_data_size = 0;
+
+    self->samples.time = self->time;
+
+    self->samples_decoded += PLM_AUDIO_SAMPLES_PER_FRAME;
+    self->time = (float)self->samples_decoded /
+                 (float)PLM_AUDIO_SAMPLE_RATE[self->samplerate_index];
+
+    return &self->samples;
+}
+
+int plm_audio_find_frame_sync(plm_audio_t *self) {
+    size_t i;
+    for (i = self->buffer->bit_index >> 3; i < self->buffer->length - 1; i++) {
+        if (self->buffer->bytes[i] == 0xFF &&
+            (self->buffer->bytes[i + 1] & 0xFE) == 0xFC) {
+            self->buffer->bit_index = ((i + 1) << 3) + 3;
+            return TRUE;
+        }
+    }
+    self->buffer->bit_index = (i + 1) << 3;
+    return FALSE;
+}
+
+int plm_audio_decode_header(plm_audio_t *self) {
+    if (!plm_buffer_has(self->buffer, 48)) {
+        return 0;
+    }
+
+    plm_buffer_skip_bytes(self->buffer, 0x00);
+    int sync = plm_buffer_read(self->buffer, 11);
+
+    // Attempt to resync if no syncword was found. This sucks balls. The MP2
+    // stream contains a syncword just before every frame (11 bits set to 1).
+    // However, this syncword is not guaranteed to not occur elswhere in the
+    // stream. So, if we have to resync, we also have to check if the header
+    // (samplerate, bitrate) differs from the one we had before. This all
+    // may still lead to garbage data being decoded :/
+
+    if (sync != PLM_AUDIO_FRAME_SYNC && !plm_audio_find_frame_sync(self)) {
+        return 0;
+    }
+
+    self->version = plm_buffer_read(self->buffer, 2);
+    self->layer = plm_buffer_read(self->buffer, 2);
+    int hasCRC = !plm_buffer_read(self->buffer, 1);
+
+    if (self->version != PLM_AUDIO_MPEG_1 ||
+        self->layer != PLM_AUDIO_LAYER_II) {
+        return 0;
+    }
+
+    int bitrate_index = plm_buffer_read(self->buffer, 4) - 1;
+    if (bitrate_index > 13) {
+        return 0;
+    }
+
+    int samplerate_index = plm_buffer_read(self->buffer, 2);
+    if (samplerate_index == 3) {
+        return 0;
+    }
+
+    int padding = plm_buffer_read(self->buffer, 1);
+    plm_buffer_skip(self->buffer, 1);  // f_private
+    int mode = plm_buffer_read(self->buffer, 2);
+
+    // If we already have a header, make sure the samplerate, bitrate and mode
+    // are still the same, otherwise we might have missed sync.
+    if (self->has_header &&
+        (self->bitrate_index != bitrate_index ||
+         self->samplerate_index != samplerate_index || self->mode != mode)) {
+        return 0;
+    }
+
+    self->bitrate_index = bitrate_index;
+    self->samplerate_index = samplerate_index;
+    self->mode = mode;
+    self->has_header = TRUE;
+
+    // Parse the mode_extension, set up the stereo bound
+    if (mode == PLM_AUDIO_MODE_JOINT_STEREO) {
+        self->bound = (plm_buffer_read(self->buffer, 2) + 1) << 2;
+    } else {
+        plm_buffer_skip(self->buffer, 2);
+        self->bound = (mode == PLM_AUDIO_MODE_MONO) ? 0 : 32;
+    }
+
+    // Discard the last 4 bits of the header and the CRC value, if present
+    plm_buffer_skip(self->buffer, 4);
+    if (hasCRC) {
+        plm_buffer_skip(self->buffer, 16);
+    }
+
+    // Compute frame size, check if we have enough data to decode the whole
+    // frame.
+    int bitrate = PLM_AUDIO_BIT_RATE[self->bitrate_index];
+    int samplerate = PLM_AUDIO_SAMPLE_RATE[self->samplerate_index];
+    int frame_size = (144000 * bitrate / samplerate) + padding;
+    return frame_size - (hasCRC ? 6 : 4);
+}
+
+void plm_audio_decode_frame(plm_audio_t *self) {
+    // Prepare the quantizer table lookups
+    int tab3 = 0;
+    int sblimit = 0;
+
+    int tab1 = (self->mode == PLM_AUDIO_MODE_MONO) ? 0 : 1;
+    int tab2 = PLM_AUDIO_QUANT_LUT_STEP_1[tab1][self->bitrate_index];
+    tab3 = QUANT_LUT_STEP_2[tab2][self->samplerate_index];
+    sblimit = tab3 & 63;
+    tab3 >>= 6;
+
+    if (self->bound > sblimit) {
+        self->bound = sblimit;
+    }
+
+    // Read the allocation information
+    for (int sb = 0; sb < self->bound; sb++) {
+        self->allocation[0][sb] = plm_audio_read_allocation(self, sb, tab3);
+        self->allocation[1][sb] = plm_audio_read_allocation(self, sb, tab3);
+    }
+
+    for (int sb = self->bound; sb < sblimit; sb++) {
+        self->allocation[0][sb] = self->allocation[1][sb] =
+            plm_audio_read_allocation(self, sb, tab3);
+    }
+
+    // Read scale factor selector information
+    int channels = (self->mode == PLM_AUDIO_MODE_MONO) ? 1 : 2;
+    for (int sb = 0; sb < sblimit; sb++) {
+        for (int ch = 0; ch < channels; ch++) {
+            if (self->allocation[ch][sb]) {
+                self->scale_factor_info[ch][sb] =
+                    plm_buffer_read(self->buffer, 2);
+            }
+        }
+        if (self->mode == PLM_AUDIO_MODE_MONO) {
+            self->scale_factor_info[1][sb] = self->scale_factor_info[0][sb];
+        }
+    }
+
+    // Read scale factors
+    for (int sb = 0; sb < sblimit; sb++) {
+        for (int ch = 0; ch < channels; ch++) {
+            if (self->allocation[ch][sb]) {
+                int *sf = self->scale_factor[ch][sb];
+                switch (self->scale_factor_info[ch][sb]) {
+                    case 0:
+                        sf[0] = plm_buffer_read(self->buffer, 6);
+                        sf[1] = plm_buffer_read(self->buffer, 6);
+                        sf[2] = plm_buffer_read(self->buffer, 6);
+                        break;
+                    case 1:
+                        sf[0] = sf[1] = plm_buffer_read(self->buffer, 6);
+                        sf[2] = plm_buffer_read(self->buffer, 6);
+                        break;
+                    case 2:
+                        sf[0] = sf[1] = sf[2] =
+                            plm_buffer_read(self->buffer, 6);
+                        break;
+                    case 3:
+                        sf[0] = plm_buffer_read(self->buffer, 6);
+                        sf[1] = sf[2] = plm_buffer_read(self->buffer, 6);
+                        break;
+                }
+            }
+        }
+        if (self->mode == PLM_AUDIO_MODE_MONO) {
+            self->scale_factor[1][sb][0] = self->scale_factor[0][sb][0];
+            self->scale_factor[1][sb][1] = self->scale_factor[0][sb][1];
+            self->scale_factor[1][sb][2] = self->scale_factor[0][sb][2];
+        }
+    }
+
+    // Coefficient input and reconstruction
+    int out_pos = 0;
+    for (int part = 0; part < 3; part++) {
+        for (int granule = 0; granule < 4; granule++) {
+            // Read the samples
+            for (int sb = 0; sb < self->bound; sb++) {
+                plm_audio_read_samples(self, 0, sb, part);
+                plm_audio_read_samples(self, 1, sb, part);
+            }
+            for (int sb = self->bound; sb < sblimit; sb++) {
+                plm_audio_read_samples(self, 0, sb, part);
+                self->sample[1][sb][0] = self->sample[0][sb][0];
+                self->sample[1][sb][1] = self->sample[0][sb][1];
+                self->sample[1][sb][2] = self->sample[0][sb][2];
+            }
+            for (int sb = sblimit; sb < 32; sb++) {
+                self->sample[0][sb][0] = 0;
+                self->sample[0][sb][1] = 0;
+                self->sample[0][sb][2] = 0;
+                self->sample[1][sb][0] = 0;
+                self->sample[1][sb][1] = 0;
+                self->sample[1][sb][2] = 0;
+            }
+
+            // Synthesis loop
+            for (int p = 0; p < 3; p++) {
+                // Shifting step
+                self->v_pos = (self->v_pos - 64) & 1023;
+
+                for (int ch = 0; ch < 2; ch++) {
+                    plm_audio_matrix_transform(self->sample[ch], p, self->V[ch],
+                                               self->v_pos);
+
+                    // Build U, windowing, calculate output
+                    memset(self->U, 0, sizeof(self->U));
+
+                    int d_index = 512 - (self->v_pos >> 1);
+                    int v_index = (self->v_pos % 128) >> 1;
+                    while (v_index < 1024) {
+                        for (int i = 0; i < 32; ++i) {
+                            self->U[i] +=
+                                self->D[d_index++] * self->V[ch][v_index++];
+                        }
+
+                        v_index += 128 - 32;
+                        d_index += 64 - 32;
+                    }
+
+                    d_index -= (512 - 32);
+                    v_index = (128 - 32 + 1024) - v_index;
+                    while (v_index < 1024) {
+                        for (int i = 0; i < 32; ++i) {
+                            self->U[i] +=
+                                self->D[d_index++] * self->V[ch][v_index++];
+                        }
+
+                        v_index += 128 - 32;
+                        d_index += 64 - 32;
+                    }
+
+// Output samples
+#ifdef PLM_AUDIO_SEPARATE_CHANNELS
+                    float *out_channel =
+                        ch == 0 ? self->samples.left : self->samples.right;
+                    for (int j = 0; j < 32; j++) {
+                        out_channel[out_pos + j] = self->U[j] / 2147418112.0f;
+                    }
+#else
+                    for (int j = 0; j < 32; j++) {
+                        self->samples.interleaved[((out_pos + j) << 1) + ch] =
+                            self->U[j] / 2147418112.0f;
+                    }
+#endif
+                }  // End of synthesis channel loop
+                out_pos += 32;
+            }  // End of synthesis sub-block loop
+
+        }  // Decoding of the granule finished
+    }
+
+    plm_buffer_align(self->buffer);
+}
+
+const plm_quantizer_spec_t *plm_audio_read_allocation(plm_audio_t *self, int sb,
+                                                      int tab3) {
+    int tab4 = PLM_AUDIO_QUANT_LUT_STEP_3[tab3][sb];
+    int qtab = PLM_AUDIO_QUANT_LUT_STEP4[tab4 & 15][plm_buffer_read(
+        self->buffer, tab4 >> 4)];
+    return qtab ? (&PLM_AUDIO_QUANT_TAB[qtab - 1]) : 0;
+}
+
+void plm_audio_read_samples(plm_audio_t *self, int ch, int sb, int part) {
+    const plm_quantizer_spec_t *q = self->allocation[ch][sb];
+    int sf = self->scale_factor[ch][sb][part];
+    int *sample = self->sample[ch][sb];
+    int val = 0;
+
+    if (!q) {
+        // No bits allocated for this subband
+        sample[0] = sample[1] = sample[2] = 0;
+        return;
+    }
+
+    // Resolve scalefactor
+    if (sf == 63) {
+        sf = 0;
+    } else {
+        int shift = (sf / 3) | 0;
+        sf =
+            (PLM_AUDIO_SCALEFACTOR_BASE[sf % 3] + ((1 << shift) >> 1)) >> shift;
+    }
+
+    // Decode samples
+    int adj = q->levels;
+    if (q->group) {
+        // Decode grouped samples
+        val = plm_buffer_read(self->buffer, q->bits);
+        sample[0] = val % adj;
+        val /= adj;
+        sample[1] = val % adj;
+        sample[2] = val / adj;
+    } else {
+        // Decode direct samples
+        sample[0] = plm_buffer_read(self->buffer, q->bits);
+        sample[1] = plm_buffer_read(self->buffer, q->bits);
+        sample[2] = plm_buffer_read(self->buffer, q->bits);
+    }
+
+    // Postmultiply samples
+    int scale = 65536 / (adj + 1);
+    adj = ((adj + 1) >> 1) - 1;
+
+    val = (adj - sample[0]) * scale;
+    sample[0] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12;
+
+    val = (adj - sample[1]) * scale;
+    sample[1] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12;
+
+    val = (adj - sample[2]) * scale;
+    sample[2] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12;
+}
+
+void plm_audio_matrix_transform(int s[32][3], int ss, float *d, int dp) {
+    float t01, t02, t03, t04, t05, t06, t07, t08, t09, t10, t11, t12, t13, t14,
+        t15, t16, t17, t18, t19, t20, t21, t22, t23, t24, t25, t26, t27, t28,
+        t29, t30, t31, t32, t33;
+
+    t01 = (float)(s[0][ss] + s[31][ss]);
+    t02 = (float)(s[0][ss] - s[31][ss]) * 0.500602998235f;
+    t03 = (float)(s[1][ss] + s[30][ss]);
+    t04 = (float)(s[1][ss] - s[30][ss]) * 0.505470959898f;
+    t05 = (float)(s[2][ss] + s[29][ss]);
+    t06 = (float)(s[2][ss] - s[29][ss]) * 0.515447309923f;
+    t07 = (float)(s[3][ss] + s[28][ss]);
+    t08 = (float)(s[3][ss] - s[28][ss]) * 0.53104259109f;
+    t09 = (float)(s[4][ss] + s[27][ss]);
+    t10 = (float)(s[4][ss] - s[27][ss]) * 0.553103896034f;
+    t11 = (float)(s[5][ss] + s[26][ss]);
+    t12 = (float)(s[5][ss] - s[26][ss]) * 0.582934968206f;
+    t13 = (float)(s[6][ss] + s[25][ss]);
+    t14 = (float)(s[6][ss] - s[25][ss]) * 0.622504123036f;
+    t15 = (float)(s[7][ss] + s[24][ss]);
+    t16 = (float)(s[7][ss] - s[24][ss]) * 0.674808341455f;
+    t17 = (float)(s[8][ss] + s[23][ss]);
+    t18 = (float)(s[8][ss] - s[23][ss]) * 0.744536271002f;
+    t19 = (float)(s[9][ss] + s[22][ss]);
+    t20 = (float)(s[9][ss] - s[22][ss]) * 0.839349645416f;
+    t21 = (float)(s[10][ss] + s[21][ss]);
+    t22 = (float)(s[10][ss] - s[21][ss]) * 0.972568237862f;
+    t23 = (float)(s[11][ss] + s[20][ss]);
+    t24 = (float)(s[11][ss] - s[20][ss]) * 1.16943993343f;
+    t25 = (float)(s[12][ss] + s[19][ss]);
+    t26 = (float)(s[12][ss] - s[19][ss]) * 1.48416461631f;
+    t27 = (float)(s[13][ss] + s[18][ss]);
+    t28 = (float)(s[13][ss] - s[18][ss]) * 2.05778100995f;
+    t29 = (float)(s[14][ss] + s[17][ss]);
+    t30 = (float)(s[14][ss] - s[17][ss]) * 3.40760841847f;
+    t31 = (float)(s[15][ss] + s[16][ss]);
+    t32 = (float)(s[15][ss] - s[16][ss]) * 10.1900081235f;
+
+    t33 = t01 + t31;
+    t31 = (t01 - t31) * 0.502419286188f;
+    t01 = t03 + t29;
+    t29 = (t03 - t29) * 0.52249861494f;
+    t03 = t05 + t27;
+    t27 = (t05 - t27) * 0.566944034816f;
+    t05 = t07 + t25;
+    t25 = (t07 - t25) * 0.64682178336f;
+    t07 = t09 + t23;
+    t23 = (t09 - t23) * 0.788154623451f;
+    t09 = t11 + t21;
+    t21 = (t11 - t21) * 1.06067768599f;
+    t11 = t13 + t19;
+    t19 = (t13 - t19) * 1.72244709824f;
+    t13 = t15 + t17;
+    t17 = (t15 - t17) * 5.10114861869f;
+    t15 = t33 + t13;
+    t13 = (t33 - t13) * 0.509795579104f;
+    t33 = t01 + t11;
+    t01 = (t01 - t11) * 0.601344886935f;
+    t11 = t03 + t09;
+    t09 = (t03 - t09) * 0.899976223136f;
+    t03 = t05 + t07;
+    t07 = (t05 - t07) * 2.56291544774f;
+    t05 = t15 + t03;
+    t15 = (t15 - t03) * 0.541196100146f;
+    t03 = t33 + t11;
+    t11 = (t33 - t11) * 1.30656296488f;
+    t33 = t05 + t03;
+    t05 = (t05 - t03) * 0.707106781187f;
+    t03 = t15 + t11;
+    t15 = (t15 - t11) * 0.707106781187f;
+    t03 += t15;
+    t11 = t13 + t07;
+    t13 = (t13 - t07) * 0.541196100146f;
+    t07 = t01 + t09;
+    t09 = (t01 - t09) * 1.30656296488f;
+    t01 = t11 + t07;
+    t07 = (t11 - t07) * 0.707106781187f;
+    t11 = t13 + t09;
+    t13 = (t13 - t09) * 0.707106781187f;
+    t11 += t13;
+    t01 += t11;
+    t11 += t07;
+    t07 += t13;
+    t09 = t31 + t17;
+    t31 = (t31 - t17) * 0.509795579104f;
+    t17 = t29 + t19;
+    t29 = (t29 - t19) * 0.601344886935f;
+    t19 = t27 + t21;
+    t21 = (t27 - t21) * 0.899976223136f;
+    t27 = t25 + t23;
+    t23 = (t25 - t23) * 2.56291544774f;
+    t25 = t09 + t27;
+    t09 = (t09 - t27) * 0.541196100146f;
+    t27 = t17 + t19;
+    t19 = (t17 - t19) * 1.30656296488f;
+    t17 = t25 + t27;
+    t27 = (t25 - t27) * 0.707106781187f;
+    t25 = t09 + t19;
+    t19 = (t09 - t19) * 0.707106781187f;
+    t25 += t19;
+    t09 = t31 + t23;
+    t31 = (t31 - t23) * 0.541196100146f;
+    t23 = t29 + t21;
+    t21 = (t29 - t21) * 1.30656296488f;
+    t29 = t09 + t23;
+    t23 = (t09 - t23) * 0.707106781187f;
+    t09 = t31 + t21;
+    t31 = (t31 - t21) * 0.707106781187f;
+    t09 += t31;
+    t29 += t09;
+    t09 += t23;
+    t23 += t31;
+    t17 += t29;
+    t29 += t25;
+    t25 += t09;
+    t09 += t27;
+    t27 += t23;
+    t23 += t19;
+    t19 += t31;
+    t21 = t02 + t32;
+    t02 = (t02 - t32) * 0.502419286188f;
+    t32 = t04 + t30;
+    t04 = (t04 - t30) * 0.52249861494f;
+    t30 = t06 + t28;
+    t28 = (t06 - t28) * 0.566944034816f;
+    t06 = t08 + t26;
+    t08 = (t08 - t26) * 0.64682178336f;
+    t26 = t10 + t24;
+    t10 = (t10 - t24) * 0.788154623451f;
+    t24 = t12 + t22;
+    t22 = (t12 - t22) * 1.06067768599f;
+    t12 = t14 + t20;
+    t20 = (t14 - t20) * 1.72244709824f;
+    t14 = t16 + t18;
+    t16 = (t16 - t18) * 5.10114861869f;
+    t18 = t21 + t14;
+    t14 = (t21 - t14) * 0.509795579104f;
+    t21 = t32 + t12;
+    t32 = (t32 - t12) * 0.601344886935f;
+    t12 = t30 + t24;
+    t24 = (t30 - t24) * 0.899976223136f;
+    t30 = t06 + t26;
+    t26 = (t06 - t26) * 2.56291544774f;
+    t06 = t18 + t30;
+    t18 = (t18 - t30) * 0.541196100146f;
+    t30 = t21 + t12;
+    t12 = (t21 - t12) * 1.30656296488f;
+    t21 = t06 + t30;
+    t30 = (t06 - t30) * 0.707106781187f;
+    t06 = t18 + t12;
+    t12 = (t18 - t12) * 0.707106781187f;
+    t06 += t12;
+    t18 = t14 + t26;
+    t26 = (t14 - t26) * 0.541196100146f;
+    t14 = t32 + t24;
+    t24 = (t32 - t24) * 1.30656296488f;
+    t32 = t18 + t14;
+    t14 = (t18 - t14) * 0.707106781187f;
+    t18 = t26 + t24;
+    t24 = (t26 - t24) * 0.707106781187f;
+    t18 += t24;
+    t32 += t18;
+    t18 += t14;
+    t26 = t14 + t24;
+    t14 = t02 + t16;
+    t02 = (t02 - t16) * 0.509795579104f;
+    t16 = t04 + t20;
+    t04 = (t04 - t20) * 0.601344886935f;
+    t20 = t28 + t22;
+    t22 = (t28 - t22) * 0.899976223136f;
+    t28 = t08 + t10;
+    t10 = (t08 - t10) * 2.56291544774f;
+    t08 = t14 + t28;
+    t14 = (t14 - t28) * 0.541196100146f;
+    t28 = t16 + t20;
+    t20 = (t16 - t20) * 1.30656296488f;
+    t16 = t08 + t28;
+    t28 = (t08 - t28) * 0.707106781187f;
+    t08 = t14 + t20;
+    t20 = (t14 - t20) * 0.707106781187f;
+    t08 += t20;
+    t14 = t02 + t10;
+    t02 = (t02 - t10) * 0.541196100146f;
+    t10 = t04 + t22;
+    t22 = (t04 - t22) * 1.30656296488f;
+    t04 = t14 + t10;
+    t10 = (t14 - t10) * 0.707106781187f;
+    t14 = t02 + t22;
+    t02 = (t02 - t22) * 0.707106781187f;
+    t14 += t02;
+    t04 += t14;
+    t14 += t10;
+    t10 += t02;
+    t16 += t04;
+    t04 += t08;
+    t08 += t14;
+    t14 += t28;
+    t28 += t10;
+    t10 += t20;
+    t20 += t02;
+    t21 += t16;
+    t16 += t32;
+    t32 += t04;
+    t04 += t06;
+    t06 += t08;
+    t08 += t18;
+    t18 += t14;
+    t14 += t30;
+    t30 += t28;
+    t28 += t26;
+    t26 += t10;
+    t10 += t12;
+    t12 += t20;
+    t20 += t24;
+    t24 += t02;
+
+    d[dp + 48] = -t33;
+    d[dp + 49] = d[dp + 47] = -t21;
+    d[dp + 50] = d[dp + 46] = -t17;
+    d[dp + 51] = d[dp + 45] = -t16;
+    d[dp + 52] = d[dp + 44] = -t01;
+    d[dp + 53] = d[dp + 43] = -t32;
+    d[dp + 54] = d[dp + 42] = -t29;
+    d[dp + 55] = d[dp + 41] = -t04;
+    d[dp + 56] = d[dp + 40] = -t03;
+    d[dp + 57] = d[dp + 39] = -t06;
+    d[dp + 58] = d[dp + 38] = -t25;
+    d[dp + 59] = d[dp + 37] = -t08;
+    d[dp + 60] = d[dp + 36] = -t11;
+    d[dp + 61] = d[dp + 35] = -t18;
+    d[dp + 62] = d[dp + 34] = -t09;
+    d[dp + 63] = d[dp + 33] = -t14;
+    d[dp + 32] = -t05;
+    d[dp + 0] = t05;
+    d[dp + 31] = -t30;
+    d[dp + 1] = t30;
+    d[dp + 30] = -t27;
+    d[dp + 2] = t27;
+    d[dp + 29] = -t28;
+    d[dp + 3] = t28;
+    d[dp + 28] = -t07;
+    d[dp + 4] = t07;
+    d[dp + 27] = -t26;
+    d[dp + 5] = t26;
+    d[dp + 26] = -t23;
+    d[dp + 6] = t23;
+    d[dp + 25] = -t10;
+    d[dp + 7] = t10;
+    d[dp + 24] = -t15;
+    d[dp + 8] = t15;
+    d[dp + 23] = -t12;
+    d[dp + 9] = t12;
+    d[dp + 22] = -t19;
+    d[dp + 10] = t19;
+    d[dp + 21] = -t20;
+    d[dp + 11] = t20;
+    d[dp + 20] = -t13;
+    d[dp + 12] = t13;
+    d[dp + 19] = -t24;
+    d[dp + 13] = t24;
+    d[dp + 18] = -t31;
+    d[dp + 14] = t31;
+    d[dp + 17] = -t02;
+    d[dp + 15] = t02;
+    d[dp + 16] = 0.0;
+}
+
+#endif  // PL_MPEG_IMPLEMENTATION
diff --git a/components/video_mpeg/video_mpeg.c b/components/video_mpeg/video_mpeg.c
new file mode 100644
index 0000000000000000000000000000000000000000..f78259d4b6c53fc7c7c160f29286be7d457ece36
--- /dev/null
+++ b/components/video_mpeg/video_mpeg.c
@@ -0,0 +1,236 @@
+#ifndef __clang__
+#pragma GCC optimize("O3")
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <st3m_audio.h>
+#include <st3m_media.h>
+
+#include "ctx.h"
+
+#define PL_MPEG_IMPLEMENTATION
+#include "pl_mpeg.h"
+
+typedef struct {
+    st3m_media control;
+
+    plm_t *plm;
+    uint8_t *frame_data;
+    int width;
+    int height;
+    int frame_drop;
+    int sample_rate;
+    int frame_no;
+    int prev_frame_no;
+    int prev_prev_frame_no;
+    // last decoded frame contained chroma samples
+    // this allows us to take a grayscale fast-path
+    unsigned last_frame_chroma : 1;
+    unsigned color : 1;
+    // whether we smooth the video when scaling it up
+    unsigned smoothing : 1;
+    unsigned video : 1;
+    unsigned audio : 1;
+    unsigned loop : 1;
+    float scale;
+} mpg1_state;
+
+static void mpg1_on_video(plm_t *player, plm_frame_t *frame, void *user);
+static void mpg1_on_audio(plm_t *player, plm_samples_t *samples, void *user);
+
+static void mpg1_think(mpg1_state *self, float ms_elapsed) {
+    float elapsed_time = ms_elapsed / 1000.0;
+    double seek_to = -1;
+
+    if (self->control.seek >= 0.0) {
+        seek_to = self->control.seek * self->control.duration;
+        self->control.seek = -1;
+    }
+
+    if (elapsed_time > 1.0 / 25.0) {
+        elapsed_time = 1.0 / 25.0;
+    }
+
+    if (self->control.paused) elapsed_time = 0;
+
+    // Seek or advance decode
+    if (seek_to != -1) {
+        // XXX : clear queued audio
+        plm_seek(self->plm, seek_to, FALSE);
+    } else {
+        plm_decode(self->plm, elapsed_time);
+    }
+
+    if (plm_has_ended(self->plm)) {
+    }
+}
+
+static inline int memcpy_chroma(uint8_t *restrict target, uint8_t *restrict src,
+                                int count) {
+    int ret = 0;
+    for (int i = 0; i < count; i++) {
+        uint8_t val = src[i];
+        target[i] = val;
+        ret = (ret | (val != 128));
+    }
+    return ret;
+}
+
+static void mpg1_on_video(plm_t *mpeg, plm_frame_t *frame, void *user) {
+    mpg1_state *self = (mpg1_state *)user;
+
+    self->frame_no++;
+
+    self->width = frame->y.width;
+    self->height = frame->y.height;
+    memcpy(self->frame_data, frame->y.data, frame->y.width * frame->y.height);
+
+    if (self->color) {
+        /* copy u and v components */
+        self->last_frame_chroma = memcpy_chroma(
+            self->frame_data + frame->y.width * frame->y.height, frame->cb.data,
+            (frame->y.width / 2) * (frame->y.height / 2));
+        self->last_frame_chroma = memcpy_chroma(
+            self->frame_data + frame->y.width * frame->y.height +
+                (frame->y.width / 2) * (frame->y.height / 2),
+            frame->cr.data, (frame->y.width / 2) * (frame->y.height / 2));
+    }
+}
+
+static void mpg1_on_audio(plm_t *mpeg, plm_samples_t *samples, void *user) {
+    mpg1_state *mpg1 = user;
+    // if (self->control.paused) return;
+    if (mpg1->sample_rate == 44100) {
+        int phase = 0;
+        for (int i = 0; i < samples->count; i++) {
+        again:
+            mpg1->control.audio_buffer[mpg1->control.audio_w++] =
+                samples->interleaved[i * 2] * 20000;
+            if (mpg1->control.audio_w >= AUDIO_BUF_SIZE)
+                mpg1->control.audio_w = 0;
+            mpg1->control.audio_buffer[mpg1->control.audio_w++] =
+                samples->interleaved[i * 2 + 1] * 20000;
+            if (mpg1->control.audio_w >= AUDIO_BUF_SIZE)
+                mpg1->control.audio_w = 0;
+            phase += ((48000 / 44100.0) - 1.0) * 65536;
+            if (phase > 65536) {
+                phase -= 65536;
+                phase -= ((48000 / 44100.0) - 1.0) * 65536;
+                goto again;
+            }
+        }
+    } else
+        for (int i = 0; i < samples->count; i++) {
+            mpg1->control.audio_buffer[mpg1->control.audio_w++] =
+                samples->interleaved[i * 2] * 20000;
+            if (mpg1->control.audio_w >= AUDIO_BUF_SIZE)
+                mpg1->control.audio_w = 0;
+            mpg1->control.audio_buffer[mpg1->control.audio_w++] =
+                samples->interleaved[i * 2 + 1] * 20000;
+            if (mpg1->control.audio_w >= AUDIO_BUF_SIZE)
+                mpg1->control.audio_w = 0;
+        }
+}
+
+static void mpg1_draw(st3m_media *media, Ctx *ctx) {
+    mpg1_state *mpg1 = (mpg1_state *)media;
+
+    {
+        float dim = 240 * mpg1->scale;
+
+        if (mpg1->video) {
+            float scale = dim / mpg1->width;
+            float scaleh = dim / mpg1->height;
+            if (scaleh < scale) scale = scaleh;
+            char eid[16];
+            sprintf(eid, "%i", mpg1->frame_no);
+            if (mpg1->frame_no != mpg1->prev_frame_no) {
+                if (mpg1->frame_no <
+                    10) {  // ensure we've filled at least some complete frames
+                    ctx_rectangle(ctx, -120, -120, 240, 240);
+                    ctx_gray(ctx, 0.0);
+                    ctx_fill(ctx);
+                }
+                ctx_translate(ctx, -dim / 2, -dim / 2);
+                ctx_translate(ctx, (dim - mpg1->width * scale) / 2.0,
+                              (dim - mpg1->height * scale) / 2.0);
+                ctx_scale(ctx, scale, scale);
+                ctx_rectangle(ctx, 0, 0, dim, dim);
+                ctx_define_texture(ctx, eid, mpg1->width, mpg1->height,
+                                   mpg1->width,
+                                   mpg1->last_frame_chroma ? CTX_FORMAT_YUV420
+                                                           : CTX_FORMAT_GRAY8,
+                                   mpg1->frame_data, NULL);
+                ctx_image_smoothing(ctx, mpg1->smoothing);
+                ctx_compositing_mode(ctx, CTX_COMPOSITE_COPY);
+                ctx_fill(ctx);
+                char eid[16];
+                sprintf(eid, "%i", mpg1->prev_prev_frame_no);
+                ctx_drop_eid(ctx, eid);
+                mpg1->prev_prev_frame_no = mpg1->prev_frame_no;
+                mpg1->prev_frame_no = mpg1->frame_no;
+            } else {
+                // do nothing, keep display contents
+            }
+        } else {
+            ctx_rgb(ctx, 0.2, 0.3, 0.4);
+            ctx_fill(ctx);
+        }
+    }
+}
+
+static void mpg1_destroy(st3m_media *media) {
+    mpg1_state *self = (void *)media;
+    plm_destroy(self->plm);
+    free(self->frame_data);
+    free(self);
+}
+
+st3m_media *st3m_media_load_mpg1(const char *path) {
+    mpg1_state *self = (mpg1_state *)malloc(sizeof(mpg1_state));
+    memset(self, 0, sizeof(mpg1_state));
+    self->control.draw = mpg1_draw;
+    self->control.think = mpg1_think;
+    self->control.destroy = mpg1_destroy;
+
+    self->plm = plm_create_with_filename(path);
+    self->color = 1;
+    self->last_frame_chroma = 0;
+    self->prev_frame_no = 255;  // anything but 0
+    self->scale = 0.75;
+    self->audio = 1;
+    self->video = 1;
+    self->loop = 0;
+    self->smoothing = 0;
+    self->frame_drop = 1;
+    if (!self->plm) {
+        printf("Couldn't open %s", path);
+        free(self);
+        return NULL;
+    }
+
+    self->sample_rate = plm_get_samplerate(self->plm);
+    self->control.duration = plm_get_duration(self->plm);
+
+    plm_set_video_decode_callback(self->plm, mpg1_on_video, self);
+    plm_set_audio_decode_callback(self->plm, mpg1_on_audio, self);
+    plm_set_video_enabled(self->plm, self->video);
+
+    plm_set_loop(self->plm, self->loop);
+    plm_set_audio_enabled(self->plm, self->audio);
+    plm_set_audio_stream(self->plm, 0);
+
+    if (plm_get_num_audio_streams(self->plm) > 0) {
+        plm_set_audio_lead_time(self->plm, 0.05);
+    }
+
+    self->frame_data =
+        (uint8_t *)malloc(plm_get_width(self->plm) * plm_get_height(self->plm) *
+                          2);  // XXX : this is not quite riught
+
+    mpg1_think(self, 0);  // the frame is constructed in think
+    return (st3m_media *)self;
+}
diff --git a/python_payload/apps/mp3/__init__.py b/python_payload/apps/mp3/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..73c9e3c856b8497011fe2d5cea27b73affc0eb0c
--- /dev/null
+++ b/python_payload/apps/mp3/__init__.py
@@ -0,0 +1,23 @@
+from st3m.application import Application, ApplicationContext
+from st3m.input import InputState
+import st3m.run
+import media
+from ctx import Context
+
+class MediaMp3(Application):
+    def __init__(self, app_ctx: ApplicationContext) -> None:
+        super().__init__(app_ctx)
+        self._filename = "/sd/toms_diner.mp3"
+
+    def draw(self, ctx: Context) -> None:
+        media.draw(ctx)
+
+    def on_enter(self, vm: Optional[ViewManager]) -> None:
+        super().on_enter(vm)
+        media.load(self._filename)
+
+    def on_exit(self) -> None:
+        media.stop()
+
+if __name__ == '__main__':
+    st3m.run.run_view(MediaMp3(ApplicationContext()))
diff --git a/python_payload/apps/mp3/flow3r.toml b/python_payload/apps/mp3/flow3r.toml
new file mode 100644
index 0000000000000000000000000000000000000000..b6226206322a09de3455321221120f7c2f667889
--- /dev/null
+++ b/python_payload/apps/mp3/flow3r.toml
@@ -0,0 +1,11 @@
+[app]
+name = "MediaMP3"
+menu = "Apps"
+
+[entry]
+class = "MediaMp3"
+
+[metadata]
+author = "Flow3r Badge Authors"
+license = "LGPL-3.0-only"
+url = "https://git.flow3r.garden/flow3r/flow3r-firmware"
diff --git a/python_payload/apps/mpg1/__init__.py b/python_payload/apps/mpg1/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2466e9c862e22640b875fa8fb1a60425edaa32f
--- /dev/null
+++ b/python_payload/apps/mpg1/__init__.py
@@ -0,0 +1,29 @@
+from st3m.application import Application, ApplicationContext
+from st3m.input import InputState
+import st3m.run
+import media
+from ctx import Context
+
+class MediaMpgVideo(Application):
+    def __init__(self, app_ctx: ApplicationContext) -> None:
+        super().__init__(app_ctx)
+        # self._filename = "/sd/toms_diner.mp3"
+        # self._filename = "/sd/fanitullen.mp3"
+        self._filename = "/sd/alien.mpg"
+        # self._filename = "/sd/bapple.mpg"
+        # self._filename = "/sd/video.mpg"
+        # self._filename = "/sd/bunny.mpg"
+        # self._filename = "/sd/bjork-160x120.mpg"
+
+    def draw(self, ctx: Context) -> None:
+        media.draw(ctx)
+
+    def on_enter(self, vm: Optional[ViewManager]) -> None:
+        super().on_enter(vm)
+        media.load(self._filename)
+
+    def on_exit(self) -> None:
+        media.stop()
+
+if __name__ == '__main__':
+    st3m.run.run_view(MediaMpgVideo(ApplicationContext()))
diff --git a/python_payload/apps/mpg1/flow3r.toml b/python_payload/apps/mpg1/flow3r.toml
new file mode 100644
index 0000000000000000000000000000000000000000..7f180ae9cdcc9c38ce598928a90ca6bd0a24ba9c
--- /dev/null
+++ b/python_payload/apps/mpg1/flow3r.toml
@@ -0,0 +1,11 @@
+[app]
+name = "MediaMPEG1"
+menu = "Apps"
+
+[entry]
+class = "MediaMpgVideo"
+
+[metadata]
+author = "Flow3r Badge Authors"
+license = "LGPL-3.0-only"
+url = "https://git.flow3r.garden/flow3r/flow3r-firmware"
diff --git a/python_payload/main.py b/python_payload/main.py
index 51fb0c396be75622570185dcf80584405a06f866..1fbaf1b7306db561a8915497bbe05c0577193e3e 100644
--- a/python_payload/main.py
+++ b/python_payload/main.py
@@ -1,3 +1,8 @@
 from st3m.run import run_main
 
+import network
+station = network.WLAN(network.STA_IF)
+station.active(True)
+station.connect("Camp2023-open")
+
 run_main()
diff --git a/python_payload/mypystubs/ctx.pyi b/python_payload/mypystubs/ctx.pyi
index bbd830b3767a2d469112a65c174b4e9521c99cd5..7eff84ceb5e96da6d8bc749f4372f46b8ce899fa 100644
--- a/python_payload/mypystubs/ctx.pyi
+++ b/python_payload/mypystubs/ctx.pyi
@@ -75,22 +75,6 @@ class Context(Protocol):
         should be balanced.
         """
         pass
-    def start_frame(self) -> "Context":
-        """
-        Prepare for rendering a new frame, clears internal drawlist and
-        initializes the state.
-
-        TODO(q3k): we probably shouldn't expose this
-        """
-        pass
-    def end_frame(self) -> "Context":
-        """
-        We're done rendering a frame, this does nothing on a context created for
-        a framebuffer, where drawing commands are immediate.
-
-        TODO(q3k): we probably shouldn't expose this
-        """
-        pass
     def start_group(self) -> "Context":
         """
         Start a compositing group.
@@ -194,24 +178,30 @@ class Context(Protocol):
         pass
     def rel_line_to(self, x: float, y: float) -> "Context":
         """
-        TOD(q3k): document
+        Adds a line segment from the current point to current_x + x,
+        current_y + y.
         """
         pass
     def rel_move_to(self, x: float, y: float) -> "Context":
         """
-        TOD(q3k): document
+        Moves the virtual pen a relative amount without adding a segment to
+        the current path.
         """
         pass
     def rel_curve_to(
         self, cx0: float, cy0: float, cx1: float, cy1: float, x: float, y: float
     ) -> "Context":
         """
-        TOD(q3k): document
+        Adds a cubic bezier segment using relative coordinates, behaves
+        like curve_to but all coordinate arguments are relative to the
+        coordinates of the virtual pen when called.
         """
         pass
     def rel_quad_to(self, cx: float, cy: float, x: float, y: float) -> "Context":
         """
-        TOD(q3k): document
+        Adds a quadratic bezier segment using relative coordinates, behaves
+        like quad_to but all coordinate arguments are relative to the
+        coordinates of the virtual pen when called.
         """
         pass
     def rel_arc_to(
@@ -262,17 +252,14 @@ class Context(Protocol):
         pass
     def fill(self) -> "Context":
         """
-        TOD(q3k): document
+        Fill the current path with the current source (color, gradient or
+        texture).
         """
         pass
     def stroke(self) -> "Context":
         """
-        TOD(q3k): document
-        """
-        pass
-    def paint(self) -> "Context":
-        """
-        TOD(q3k): document
+        Stroke the current path with the current source (color, gradient or
+        texture), with current line_width
         """
         pass
     def linear_gradient(self, x0: float, y0: float, x1: float, y1: float) -> "Context":
@@ -305,12 +292,13 @@ class Context(Protocol):
         pass
     def logo(self, x: float, y: float, dim: float) -> "Context":
         """
-        TOD(q3k): document
+        Draws the ctx logo, centered at x,y with a footprint of roughly dim
+        units.
         """
         pass
     def text(self, text: str) -> "Context":
         """
-        TOD(q3k): document
+        Add a text fragment using the current fill source, font and font_size.
         """
         pass
     def scope(self) -> "Context":
diff --git a/python_payload/mypystubs/mpeg1video.pyi b/python_payload/mypystubs/mpeg1video.pyi
new file mode 100644
index 0000000000000000000000000000000000000000..375504243384c6066df633fe553f1c060be920f2
--- /dev/null
+++ b/python_payload/mypystubs/mpeg1video.pyi
@@ -0,0 +1,20 @@
+from ctx import Context
+
+def load(path: str) -> int:
+    """
+    Load path
+    """
+    ...
+
+def iterate(ctx: Context) -> int:
+    """
+    Iterates video decoding, draws frame to ctx.
+    Returns 0 if video is still playing
+    """
+    ...
+
+def cleanup() -> int:
+    """
+    Clean up resources used by loaded video.
+    """
+    ...
diff --git a/python_payload/st3m/reactor.py b/python_payload/st3m/reactor.py
index 56f52e8c51e6c75ebbcdff9e6e26a1e4f32b7b63..cecc327d492813f07cea34d460a4f0af59b869ac 100644
--- a/python_payload/st3m/reactor.py
+++ b/python_payload/st3m/reactor.py
@@ -2,6 +2,7 @@ from st3m.goose import ABCBase, abstractmethod, List, Optional
 from st3m.input import InputState
 from ctx import Context
 
+import media
 import time
 import sys_display
 import sys_kernel
@@ -149,6 +150,7 @@ class Reactor:
 
         # Think!
         self._top.think(hr, delta)
+        media.think(delta)
 
         # Draw!
         if self._ctx is None:
diff --git a/recovery/bootloader_components/main/bootloader_start.c b/recovery/bootloader_components/main/bootloader_start.c
index a24de7f3dd465ece00a70ac87a89b8d383fbeaee..37d0fe7337b1986a3ebb6fcbf45feb18779d5cf0 100644
--- a/recovery/bootloader_components/main/bootloader_start.c
+++ b/recovery/bootloader_components/main/bootloader_start.c
@@ -86,4 +86,6 @@ static int select_partition_number(bootloader_state_t *bs, bool want_recovery) {
 }
 
 // Return global reent struct if any newlib functions are linked to bootloader
-struct _reent *__getreent(void) { return _GLOBAL_REENT; }
+struct _reent *__getreent(void) {
+    return _GLOBAL_REENT;
+}
diff --git a/tools/encode-mpg.sh b/tools/encode-mpg.sh
new file mode 100755
index 0000000000000000000000000000000000000000..c84381af8fc8099573acdc43a2959eca05638bec
--- /dev/null
+++ b/tools/encode-mpg.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+set -e -x
+if (( $# != 2 )); then
+    >&2 echo "Usage: mpg1-encode <video-file> <output-file>"
+    exit
+fi
+
+ffmpeg -i $1 -vf scale=128:96 -c:v mpeg1video -b:v 96k -ac 1 -c:a mp2 -format mpeg -b:a 64k  -ar 48000  -r 24 $2 
+
+