initial commit

This commit is contained in:
Peder Bergebakken Sundt 2023-07-08 01:49:20 +02:00
commit 0b2189683b
7 changed files with 381 additions and 0 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use nix

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.direnv/
*.mp4
*.bin
__pycache__
result
result-*

5
Makefile Normal file
View File

@ -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

171
decode.cpp Normal file
View File

@ -0,0 +1,171 @@
#include <Wire.h>
#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() {
}

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
av

11
shell.nix Normal file
View File

@ -0,0 +1,11 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShellNoCC {
packages = with pkgs; [
gnumake
yt-dlp
(python3.withPackages (ps: with ps; [
av
]))
];
}

186
test.py Executable file
View File

@ -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()