├── video_player.h ├── application.fam ├── bad_apple.h ├── video_player.c └── bad_apple.c /video_player.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define COLOR_WHITE true 4 | #define COLOR_BLACK false 5 | 6 | int vp_play_frame(BadAppleCtx* ctx); 7 | -------------------------------------------------------------------------------- /application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="badapple", 3 | name="Bad Apple", 4 | apptype=FlipperAppType.EXTERNAL, 5 | entry_point="bad_apple_main", 6 | requires=["gui"], 7 | stack_size=1 * 1024, 8 | fap_category="Misc" 9 | ) 10 | -------------------------------------------------------------------------------- /bad_apple.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | #define SCREEN_WIDTH 128 8 | #define SCREEN_HEIGHT 64 9 | #define VIDEO_WIDTH 104 10 | #define VIDEO_HEIGHT 80 11 | #define FILE_BUFFER_SIZE (1024 * 64) 12 | #define VIDEO_PATH EXT_PATH("bad_apple/video.bin") 13 | #define VIDEO_X ((SCREEN_WIDTH - VIDEO_WIDTH) / 2) 14 | #define VIDEO_Y 0 15 | 16 | typedef struct { 17 | uint8_t framebuffer[VIDEO_HEIGHT * VIDEO_WIDTH / 8]; 18 | uint8_t file_buffer[FILE_BUFFER_SIZE]; 19 | uint32_t file_buffer_offset; 20 | uint32_t frame_write_offset; // bit index 21 | Storage* storage; 22 | File* video_file; 23 | } BadAppleCtx; 24 | 25 | uint8_t bad_apple_read_byte(BadAppleCtx* inst); 26 | -------------------------------------------------------------------------------- /video_player.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include "bad_apple.h" 6 | #include "video_player.h" 7 | 8 | #define TAG "video_player" 9 | 10 | static void vp_decode_rle(BadAppleCtx* ctx); 11 | static void vp_decode_delta(BadAppleCtx* ctx); 12 | 13 | int vp_play_frame(BadAppleCtx* ctx) { 14 | // Check frame type 15 | // FURI_LOG_D(TAG, "Buffer offset: %04lx", ctx->file_buffer_offset); 16 | uint8_t b = bad_apple_read_byte(ctx); 17 | // FURI_LOG_D(TAG, "Frame byte: %02x", b); 18 | switch(b) { 19 | case 1: // PFrame (delta) 20 | vp_decode_delta(ctx); 21 | break; 22 | case 2: // IFrame (rle) 23 | vp_decode_rle(ctx); 24 | break; 25 | case 3: // DFrame (duplicate) 26 | break; 27 | case 4: // next page (not supported) 28 | case 5: // end 29 | default: 30 | return 0; 31 | } 32 | 33 | return 1; 34 | } 35 | 36 | static inline void vp_write_pixel(BadAppleCtx* ctx, bool color) { 37 | if(color) { 38 | // White 39 | ctx->framebuffer[ctx->frame_write_offset / 8] &= ~(1 << (ctx->frame_write_offset % 8)); 40 | } else { 41 | // Black 42 | ctx->framebuffer[ctx->frame_write_offset / 8] |= (1 << (ctx->frame_write_offset % 8)); 43 | } 44 | ++ctx->frame_write_offset; 45 | } 46 | 47 | static inline void vp_set_rect_fast(BadAppleCtx* ctx, int x, int y) { 48 | ctx->frame_write_offset = y * VIDEO_WIDTH + x; 49 | } 50 | 51 | static void vp_decode_rle(BadAppleCtx* ctx) { 52 | int i = 0; 53 | int repeat_byte = bad_apple_read_byte(ctx); 54 | 55 | // Set rect to video area 56 | vp_set_rect_fast(ctx, 0, 0); 57 | 58 | while(i < VIDEO_WIDTH * VIDEO_HEIGHT) { 59 | int count; 60 | unsigned pixels; 61 | int j; 62 | int b = bad_apple_read_byte(ctx); 63 | 64 | if(b == repeat_byte) { 65 | count = bad_apple_read_byte(ctx); 66 | if(count == 0) count = 256; 67 | pixels = bad_apple_read_byte(ctx); 68 | } else { 69 | count = 1; 70 | pixels = (unsigned)b; 71 | } 72 | 73 | for(j = 0; j < count; ++j) { 74 | bool out_color; 75 | int k; 76 | unsigned loop_pixels = pixels; 77 | 78 | for(k = 0; k < 8; ++k) { 79 | if(loop_pixels & 0x80) 80 | out_color = COLOR_WHITE; 81 | else 82 | out_color = COLOR_BLACK; 83 | 84 | vp_write_pixel(ctx, out_color); 85 | loop_pixels <<= 1; 86 | ++i; 87 | } 88 | } 89 | } 90 | } 91 | 92 | static void vp_decode_delta(BadAppleCtx* ctx) { 93 | int frame_header[(VIDEO_HEIGHT + 7) / 8]; 94 | uint i; 95 | int fh_byte = 0; 96 | int fh_index = 0; 97 | 98 | for(i = 0; i < sizeof(frame_header) / sizeof(frame_header[0]); ++i) { 99 | frame_header[i] = bad_apple_read_byte(ctx); 100 | } 101 | 102 | for(i = 0; i < VIDEO_HEIGHT; ++i) { 103 | if(i % 8 == 0) fh_byte = frame_header[fh_index++]; 104 | 105 | if(fh_byte & 0x80) { 106 | int j; 107 | int sl_byte = 0; 108 | 109 | for(j = 0; j < VIDEO_WIDTH;) { 110 | if(j % (8 * 8) == 0) sl_byte = bad_apple_read_byte(ctx); 111 | 112 | if(sl_byte & 0x80) { 113 | unsigned out_color; 114 | int k; 115 | unsigned pixel_group = bad_apple_read_byte(ctx); 116 | 117 | // Note: this needs to be revised for screen width not multiple of 8 118 | vp_set_rect_fast(ctx, j, i); 119 | 120 | for(k = 0; k < 8 && j < VIDEO_WIDTH; ++k, ++j) { 121 | if(pixel_group & 0x80) 122 | out_color = COLOR_WHITE; 123 | else 124 | out_color = COLOR_BLACK; 125 | 126 | vp_write_pixel(ctx, out_color); 127 | pixel_group <<= 1; 128 | } 129 | } else { 130 | j += 8; 131 | } 132 | sl_byte <<= 1; 133 | } 134 | } 135 | fh_byte <<= 1; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /bad_apple.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "bad_apple.h" 16 | #include "video_player.h" 17 | 18 | #define TAG "badapple" 19 | 20 | typedef enum { 21 | BadAppleEventTypeInput, 22 | BadAppleEventTypeTick, 23 | } BadAppleEventType; 24 | 25 | typedef struct { 26 | BadAppleEventType type; 27 | InputEvent* input; 28 | } BadAppleEvent; 29 | 30 | // Screen is 128x64 px 31 | static void app_draw_callback(Canvas* canvas, void* ctx) { 32 | BadAppleCtx* inst = ctx; 33 | 34 | canvas_clear(canvas); 35 | canvas_draw_xbm(canvas, VIDEO_X, VIDEO_Y, VIDEO_WIDTH, SCREEN_HEIGHT, inst->framebuffer); 36 | canvas_draw_box(canvas, 0, 0, VIDEO_X, SCREEN_HEIGHT); 37 | canvas_draw_box( 38 | canvas, VIDEO_X + VIDEO_WIDTH, 0, SCREEN_WIDTH - VIDEO_WIDTH - VIDEO_X, SCREEN_HEIGHT); 39 | } 40 | 41 | static void app_input_callback(InputEvent* input_event, void* ctx) { 42 | furi_assert(ctx); 43 | 44 | FuriMessageQueue* event_queue = ctx; 45 | BadAppleEvent event = {.type = BadAppleEventTypeInput, .input = input_event}; 46 | furi_message_queue_put(event_queue, &event, FuriWaitForever); 47 | } 48 | 49 | BadAppleCtx* bad_apple_ctx_alloc(void) { 50 | BadAppleCtx* inst = malloc(sizeof(BadAppleCtx)); 51 | memset(inst, 0, sizeof(BadAppleCtx)); 52 | 53 | if(inst) { 54 | inst->storage = furi_record_open(RECORD_STORAGE); 55 | inst->video_file = storage_file_alloc(inst->storage); 56 | inst->file_buffer_offset = sizeof(inst->file_buffer); 57 | } 58 | 59 | return inst; 60 | } 61 | 62 | void bad_apple_ctx_free(BadAppleCtx* inst) { 63 | if(inst) { 64 | storage_file_free(inst->video_file); 65 | furi_record_close(RECORD_STORAGE); 66 | free(inst); 67 | } 68 | } 69 | 70 | void bad_apple_load_next_video_chunk(BadAppleCtx* inst) { 71 | size_t bytes_to_read = sizeof(inst->file_buffer); 72 | uint8_t* buf_ptr = inst->file_buffer; 73 | while(bytes_to_read > 0) { 74 | uint16_t curr_bytes_to_read = bytes_to_read > (UINT16_MAX / 2 + 1) ? (UINT16_MAX / 2 + 1) : 75 | bytes_to_read; 76 | uint16_t read = storage_file_read(inst->video_file, buf_ptr, curr_bytes_to_read); 77 | bytes_to_read -= read; 78 | buf_ptr += read; 79 | if(read == 0) break; 80 | } 81 | inst->file_buffer_offset = 0; 82 | } 83 | 84 | uint8_t bad_apple_read_byte(BadAppleCtx* inst) { 85 | if(inst->file_buffer_offset >= sizeof(inst->file_buffer)) { 86 | bad_apple_load_next_video_chunk(inst); 87 | } 88 | return inst->file_buffer[inst->file_buffer_offset++]; 89 | } 90 | 91 | void bad_apple_timer_isr(void* ctx) { 92 | FuriMessageQueue* event_queue = ctx; 93 | BadAppleEvent event = {.type = BadAppleEventTypeTick}; 94 | furi_message_queue_put(event_queue, &event, 0); 95 | LL_TIM_ClearFlag_UPDATE(TIM2); 96 | } 97 | 98 | void bad_apple_timer_setup(BadAppleCtx* inst, void* ctx) { 99 | UNUSED(inst); 100 | 101 | LL_TIM_InitTypeDef tim_init = { 102 | .Prescaler = 63999, 103 | .CounterMode = LL_TIM_COUNTERMODE_UP, 104 | .Autoreload = 30, 105 | }; 106 | 107 | LL_TIM_Init(TIM2, &tim_init); 108 | LL_TIM_SetClockSource(TIM2, LL_TIM_CLOCKSOURCE_INTERNAL); 109 | LL_TIM_DisableCounter(TIM2); 110 | LL_TIM_SetCounter(TIM2, 0); 111 | furi_hal_interrupt_set_isr(FuriHalInterruptIdTIM2, bad_apple_timer_isr, ctx); 112 | LL_TIM_EnableIT_UPDATE(TIM2); 113 | } 114 | 115 | void bad_apple_timer_deinit(void) { 116 | LL_TIM_DisableCounter(TIM2); 117 | LL_TIM_DisableIT_UPDATE(TIM2); 118 | furi_hal_interrupt_set_isr(FuriHalInterruptIdTIM2, NULL, NULL); 119 | LL_TIM_DeInit(TIM2); 120 | } 121 | 122 | int32_t bad_apple_main(void* p) { 123 | UNUSED(p); 124 | BadAppleCtx* ctx = bad_apple_ctx_alloc(); 125 | FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(BadAppleEvent)); 126 | 127 | // Configure view port 128 | ViewPort* view_port = view_port_alloc(); 129 | view_port_draw_callback_set(view_port, app_draw_callback, ctx); 130 | view_port_input_callback_set(view_port, app_input_callback, event_queue); 131 | 132 | // Register view port in GUI 133 | Gui* gui = furi_record_open(RECORD_GUI); 134 | gui_add_view_port(gui, view_port, GuiLayerFullscreen); 135 | 136 | NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION); 137 | 138 | // Frame rate: 32 FPS 139 | bad_apple_timer_setup(ctx, event_queue); 140 | 141 | bool is_opened = storage_file_open(ctx->video_file, VIDEO_PATH, FSAM_READ, FSOM_OPEN_EXISTING); 142 | if(is_opened) { 143 | BadAppleEvent event; 144 | notification_message(notification, &sequence_display_backlight_enforce_on); 145 | 146 | bool running = true; 147 | LL_TIM_EnableCounter(TIM2); 148 | while(running) { 149 | if(furi_message_queue_get(event_queue, &event, 100) == FuriStatusOk) { 150 | if(event.type == BadAppleEventTypeInput) { 151 | InputEvent* input_event = event.input; 152 | if(input_event->type == InputTypeLong) { 153 | if(input_event->key == InputKeyBack) { 154 | running = false; 155 | } 156 | } 157 | } else if(event.type == BadAppleEventTypeTick) { 158 | // FURI_LOG_D(TAG, "Update frame"); 159 | if(!vp_play_frame(ctx)) { 160 | running = false; 161 | } 162 | } 163 | } 164 | view_port_update(view_port); 165 | } 166 | LL_TIM_DisableCounter(TIM2); 167 | notification_message(notification, &sequence_display_backlight_enforce_auto); 168 | storage_file_close(ctx->video_file); 169 | } 170 | 171 | furi_record_close(RECORD_NOTIFICATION); 172 | 173 | view_port_enabled_set(view_port, false); 174 | gui_remove_view_port(gui, view_port); 175 | view_port_free(view_port); 176 | furi_message_queue_free(event_queue); 177 | 178 | furi_record_close(RECORD_GUI); 179 | bad_apple_timer_deinit(); 180 | bad_apple_ctx_free(ctx); 181 | 182 | return 0; 183 | } 184 | --------------------------------------------------------------------------------