commit 0b2189683b47df7c15f275d119320581c69892f0 Author: Peder Bergebakken Sundt Date: Sat Jul 8 01:49:20 2023 +0200 initial commit diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..1d953f4 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2220927 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.direnv/ +*.mp4 +*.bin +__pycache__ +result +result-* diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e569eb9 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +bad_apple.bin: bad_apple.mp4 + python test.py + +bad_apple.mp4: + yt-dlp "https://www.youtube.com/watch?v=FtutLA63Cp8" -o bad_apple.mp4 -f mp4 diff --git a/decode.cpp b/decode.cpp new file mode 100644 index 0000000..4d6987a --- /dev/null +++ b/decode.cpp @@ -0,0 +1,171 @@ +#include + +#define SSD1306_MEMORYMODE 0x20 +#define SSD1306_COLUMNADDR 0x21 +#define SSD1306_PAGEADDR 0x22 +#define SSD1306_SETCONTRAST 0x81 +#define SSD1306_CHARGEPUMP 0x8D +#define SSD1306_SEGREMAP 0xA0 +#define SSD1306_DISPLAYALLON_RESUME 0xA4 +#define SSD1306_DISPLAYALLON 0xA5 +#define SSD1306_NORMALDISPLAY 0xA6 +#define SSD1306_INVERTDISPLAY 0xA7 +#define SSD1306_SETMULTIPLEX 0xA8 +#define SSD1306_DISPLAYOFF 0xAE +#define SSD1306_DISPLAYON 0xAF +#define SSD1306_COMSCANINC 0xC0 +#define SSD1306_COMSCANDEC 0xC8 +#define SSD1306_SETDISPLAYOFFSET 0xD3 +#define SSD1306_SETDISPLAYCLOCKDIV 0xD5 +#define SSD1306_SETPRECHARGE 0xD9 +#define SSD1306_SETCOMPINS 0xDA +#define SSD1306_SETVCOMDETECT 0xDB + +#define SSD1306_SETLOWCOLUMN 0x00 +#define SSD1306_SETHIGHCOLUMN 0x10 +#define SSD1306_SETSTARTLINE 0x40 + +// TODO: remove +#define SSD1306_EXTERNALVCC 0x01 // External display voltage source +#define SSD1306_SWITCHCAPVCC 0x02 // Gen. display voltage from 3.3V + +#define SSD1306_RIGHT_HORIZONTAL_SCROLL 0x26 // Init rt scroll +#define SSD1306_LEFT_HORIZONTAL_SCROLL 0x27 // Init left scroll +#define SSD1306_VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL 0x29 // Init diag scroll +#define SSD1306_VERTICAL_AND_LEFT_HORIZONTAL_SCROLL 0x2A // Init diag scroll +#define SSD1306_DEACTIVATE_SCROLL 0x2E // Stop scroll +#define SSD1306_ACTIVATE_SCROLL 0x2F // Start scroll +#define SSD1306_SET_VERTICAL_SCROLL_AREA 0xA3 // Set scroll range + +#define WIDTH 85 +#define HEIGHT 64 +#define BUFFERSIZE (WIDTH * HEIGHT / 8) +#define I2C_ADDR 0x3c +#define OLED_RESET 4 + +uint8_t framebuffer[BUFFERSIZE] = {}; + +void pixel_set(int x, int y, bool value) { + uint8_t bitmask = (1 << (y & 0x7)); + uint8_t* offset = &buffer[x + (y / 8) * WIDTH]; + if (value) *offset |= bitmask; + else *offset &= ~bitmask; +} + +void pixel_flip(int x, int y) { + uint8_t bitmask = (1 << (y & 0x7)); + uint8_t* offset = &buffer[x + (y / 8) * WIDTH]; + *offset ^= bitmask; +} + +uint8_t* decode_frame(uint8_t* data) { + int pixel_x = 0; + int pixel_y = 0; + while (pixel_x != WIDHT-1 && pixel_y != HEIGHT-1) { + uint8_t byte = *data++; + + int do_flip = byte & 0x80; + int n_affected = byte & 0x3f; + if (byte & 0x40) { + n_affected = (n_affected << 8) | *(data++); + } + + // idea: traverse along y instead, to optimize this logic + while (n_affected--) { + if (do_flip) pixel_flip(pixel_x, pixel_y); + if (pixel_x == WIDTH-1) { + pixel_x = 0; + pixel_y++; + } else { + pixel_x++; + } + } + } + return data; // returns the next frame offset +} + +void ssd1306_command(uint8_t c) { + Wire->beginTransmission(I2C_ADDR); + Wire.write(0x00); // Co = 0, D/C = 0 + Wire.write(c); + Wire->endTransmission(); +} + +void init_display(int reset_pin, bool vcc_is_5v_external) { + Wire.begin(); + pinMode(OLED_RESET, OUTPUT); + + // reset + digitalWrite(OLED_RESET, HIGH); + delay(1); + digitalWrite(OLED_RESET, LOW); + delay(10); + digitalWrite(OLED_RESET, HIGH); + + // init sequence + ssd1306_command(SSD1306_DISPLAYOFF); + ssd1306_command(SSD1306_SETDISPLAYCLOCKDIV); + ssd1306_command(0x80); // the suggested ratio + ssd1306_command(SSD1306_SETMULTIPLEX); + ssd1306_command(HEIGHT - 1); + + ssd1306_command(SSD1306_SETDISPLAYOFFSET); + ssd1306_command(0x00); // no offset + ssd1306_command(SSD1306_SETSTARTLINE | 0x0); // (line #0) + ssd1306_command(SSD1306_CHARGEPUMP); + + ssd1306_command((vcc_is_5v_external) ? 0x10 : 0x14); + + ssd1306_command(SSD1306_MEMORYMODE); // 0x20 + ssd1306_command(0x00); // 0x0 act like ks0108 + ssd1306_command(SSD1306_SEGREMAP | 0x1); + ssd1306_command(SSD1306_COMSCANDEC); + + uint8_t comPins, contrast; + if ((WIDTH == 128) && (HEIGHT == 64)) { comPins = 0x12; contrast = (vcc_is_5v_external) ? 0x9F : 0xCF; } + //else if ((WIDTH == 128) && (HEIGHT == 32)) { comPins = 0x02; contrast = 0x8F; } + //else if ((WIDTH == 96) && (HEIGHT == 16)) { comPins = 0x02; contrast = (vcc_is_5v_external) ? 0x10 : 0xAF; } + //else { /* Other screen varieties -- TBD */ } + + ssd1306_command(SSD1306_SETCOMPINS); + ssd1306_command(comPins); + ssd1306_command(SSD1306_SETCONTRAST); + ssd1306_command(contrast); + + ssd1306_command(SSD1306_SETPRECHARGE); // 0xd9 + ssd1306_command((vcc_is_5v_external) ? 0x22 : 0xF1); + + ssd1306_command(SSD1306_SETVCOMDETECT); + ssd1306_command(0x40); + ssd1306_command(SSD1306_DISPLAYALLON_RESUME); + ssd1306_command(SSD1306_NORMALDISPLAY); + ssd1306_command(SSD1306_DEACTIVATE_SCROLL); + ssd1306_command(SSD1306_DISPLAYON); // Main screen turn on + + ssd1306_command(); +} + +void send_framebuffer() { + int buffersize = 32; + Wire.beginTransmission(I2C_ADDR); + Wire.write(0x40); + for (int i = 0; i < BUFFERSIZE; i++) { + if (!--buffersize) { + Wire.endTransmission(); + Wire.beginTransmission(I2C_ADDR); + buffersize = 32; + } + Wire.write(framebuffer[i]); + } + Wire.endTransmission(); +} + + +void Setup() { + init_display(); + send_framebuffer(); +} + +Void Loop() { + +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..dc1ce6e --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +av diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..760f64f --- /dev/null +++ b/shell.nix @@ -0,0 +1,11 @@ +{ pkgs ? import {} }: + +pkgs.mkShellNoCC { + packages = with pkgs; [ + gnumake + yt-dlp + (python3.withPackages (ps: with ps; [ + av + ])) + ]; +} diff --git a/test.py b/test.py new file mode 100755 index 0000000..5963398 --- /dev/null +++ b/test.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +from glm import ivec2 +from PIL import Image +from typing import Iterable +import av +import numpy as np + +SIZE = ivec2(4, 3) * 64 / 4 +SIZE = ivec2(4, 3) * 64 / 3 +LENGTH = SIZE.x * SIZE.y +RENDER_SCALE = 13 + + +def read_mp4() -> Iterable[np.array]: + container = av.open("bad_apple.mp4") + for frame_handler in container.decode(video=0): + # convert frame to bool array + image = frame_handler.to_image() + image = image.resize(SIZE) # downscale + frame = np.array(image)[:,:,0].T > 128 + yield frame + +def encode_frames(frames) -> Iterable[bytes]: + no_compression_cumulative, compression_cumulative = 0, 0 + + frame_prev = np.zeros((SIZE), dtype=bool) # assume black frame at start + for n, frame in enumerate(frames): + data = encode_frame(frame, frame_prev) + yield data + frame_prev = frame + + no_compression_cumulative += LENGTH // 8 + compression_cumulative += len(data) + print("frame", + f"{n:>5}| len:", + f"{len(data):>5} =", + f"{compression_cumulative:>8} /", + f"{no_compression_cumulative} =>", + f"{compression_cumulative / no_compression_cumulative * 100:>10.3}% ratio", + end="\r") + print() + +def encode_frame(frame, frame_prev) -> bytes: + def group(frame_data): + mask = frame_data[:-1] != frame_data[1:] + mask = np.concatenate((mask, [True])) + sizes = np.arange(LENGTH) + sizes = sizes[mask] + sizes[1:] -= sizes[:-1] + sizes[0] += 1 + return zip(sizes, frame_data[mask]) + def group_(frame_data): # slow, but readable + n = 1 + for a, b in zip(frame_data[:-1], frame_data[1:]): + if a != b: + yield n, a + n = 1 + else: + n += 1 + yield n, b + + out = [] # bytearray() ? + write_byte = out.append + + prev_skipped = True # don't skip the first one + n_pixels_left = LENGTH + + #for n_values, set_value in group((frame != frame_prev).reshape(-1).T): + for n_values, set_value in group((frame != frame_prev).reshape(-1)): + assert n_values <= 0x3fff + + ## skip n_values == 1, they can be inferred + n_pixels_left -= n_values # avoid skipping the last one + if n_values == 1 and not prev_skipped and n_pixels_left: + prev_skipped = True # avoid skippin multiple times in a row + continue + prev_skipped = False + + if n_values > 0x3f: + write_byte( (set_value << 7) | (1 << 6) | (n_values >> 8) ) + write_byte( n_values & 0xff ) + else: + write_byte( (set_value << 7) | (n_values & 0x3f) ) + + #print(bytes(out)) + return bytes(out) + +def decode_frame(target_buffer, data, data_offset=0) -> int: + def read_byte(): + nonlocal data_offset + try: + out = data[data_offset] + except: + print(f"\n\n{data_offset=} {len(data)=}") + raise + data_offset += 1 + return out + + buffer = np.zeros(LENGTH, dtype=bool) + buffer_offset = 0 + + prev_value = None + while buffer_offset != LENGTH: + + byte = read_byte() + value = byte >> 7 + if byte & 0x40: + n_values = ((byte & 0x3f) << 8) | read_byte() + else: + n_values = (byte & 0x3f) + + if prev_value == value: # there was an omitted n_values=1 in between + buffer[buffer_offset] = not value + buffer_offset += 1 + + buffer[buffer_offset:buffer_offset + n_values] = value + buffer_offset += n_values + prev_value = value + + #target_buffer ^= buffer.reshape((SIZE.y, SIZE.x)).T + target_buffer ^= buffer.reshape(SIZE) + + return data_offset + +def render_frames(frames, fps = None): + import pygame + pygame.init() + display = pygame.display.set_mode(SIZE * RENDER_SCALE) + clock = pygame.time.Clock() + + ignored_rows = 0 + for frame in frames: + for event in pygame.event.get(): + if event.type == pygame.KEYDOWN and event.key == pygame.K_q: + pygame.quit(); return + if event.type == pygame.QUIT: + pygame.quit(); return + + # render frame + draw_buffer = np.zeros((*SIZE, 3), dtype=np.uint8) + draw_buffer[frame,:] = 0xff + + if 0: # updated line vizualisation + for y in range(SIZE.y): + if not (frame[:, y] != frame_prev[:, y]).any(): + draw_buffer[:, y, 1] = 0 + draw_buffer[:, y, 2] = 0xff + ignored_rows += 1 + + surf = pygame.surfarray.make_surface(draw_buffer) + display.blit(pygame.transform.scale(surf, SIZE*RENDER_SCALE), (0, 0)) + + pygame.display.update() + if fps: clock.tick(fps) + + pygame.quit() + + +def main(): + def encode_then_decode(frames): + decode_buffer = np.zeros(SIZE, dtype=bool) + for data in encode_frames(frames): + offset = decode_frame(decode_buffer, data) + assert len(data) == offset + yield decode_buffer + + def decoder(encoded_data, reuse=True): + data_offset = 0 + decode_buffer = np.zeros(SIZE, dtype=bool) + while data_offset < len(encoded_data): + data_offset = decode_frame(decode_buffer, encoded_data, data_offset) + yield decode_buffer if reuse else decode_buffer.copy() + + print(f"{SIZE = }") + print(f"{LENGTH = }") + print(f"{RENDER_SCALE = }") + + #render_frames(read_mp4()) + #render_frames(encode_then_decode(read_mp4())) + with open("bad_apple.bin", "wb") as f: f.write(b''.join(encode_frames(read_mp4()))) + + #with open("bad_apple.bak.bin", "rb") as f: render_frames(decoder(f.read()) )#, fps = 30) + #with open("bad_apple.bak.bin", "rb") as f: list(encode_frames(decoder(f.read(), reuse=0))) + #with open("bad_apple.bak.bin", "rb") as f: list(encode_then_decode(decoder(f.read(), reuse=0))) + +main()