movie compression, add another movie
Stefan Schuermans

Stefan Schuermans commited on 2019-06-22 21:35:43
Showing 3 changed files, with 236 additions and 27 deletions.


add movie compression: run length encoding (if shorter than plain frame) and
refer to previous frames if frames repeat,
add breathe movie
... ...
@@ -579,36 +579,173 @@ ANIM_BW_WORM_LOOP:
579 579
         ret
580 580
 
581 581
 
582
-
583
-; play animation
582
+; movie data format
583
+;   movie = frame ... frame end
584
+;   frame = 0x0_ ... -> plain frame
585
+;           0x1_ ... -> rle (run length encoded) frame
586
+;           0x2_ ... -> back reference
587
+;   plain frame = duration_high (0x00..0x0F), duration_low (0x00..0xFF),
588
+;                 21 * ( pixel (0x00..0x0F)) << 4 | pixel (0x00..0x0F) )
589
+;   rle frame = 0x10 | duration_high (0x00..0x0F), duration_low (0x00..0xFF),
590
+;               rle entry, ..., rle_entry (until 42 pixels are encoded)
591
+;   rle_entry = repeat (0x00..0x0F) << 4 | value (0x00..0x0F)
592
+;               meaning: repeat + 1 pixels of value
593
+;   back reference = 0x20 | back_high (0x00..0x0F), back_low (0x00..0xFF)
594
+;                    meaning: read frame from earlier position,
595
+;                             frame data start = pos after back ref - back
596
+;   end = 0xF_
597
+
598
+
599
+
600
+; play animation - process code and following section
584 601
 ; input: Z = pointer to movie data
585
-; output: -
586
-; changes: X, Z, FRAME, CNT, DATA, TMP, TMP2
587
-ANIM_MOVIE:
588
-; get duration in 6ms steps, zero means end of movie
589
-        lpm     TMP,Z+
590
-        cpi     TMP,0
591
-        breq    ANIM_MOVIE_END
602
+;        TMP2 = 0 -> initial call, 1 -> nested call
603
+; output: Z = pointer to behind processed movie data (only for initial call)
604
+;         TMP = 0 -> go on, 1 -> end
605
+; changes: X, FRAME, TMP, DATA, CNT
606
+;          Z (only for nested call)
607
+ANIM_MOVIE_CODE:
608
+; get 4 bit code and 12 bit value
609
+        lpm     DATA,Z+
610
+        lpm     CNT,Z+
611
+        mov     TMP,DATA
612
+        andi    TMP,0xF0                ; 4 bit code (shifted left 4) is in TMP
613
+        andi    DATA,0x0F               ; 12 bit value is in DATA:CNT
614
+; interpret code
615
+        cpi     TMP,0x20
616
+        brsh    ANIM_MOVIE_CODE_2_TO_F
617
+        cpi     TMP,0x00
618
+        breq    ANIM_MOVIE_CODE_0
619
+        rjmp    ANIM_MOVIE_CODE_1
620
+ANIM_MOVIE_CODE_2_TO_F:
621
+        cpi     TMP,0x20
622
+        breq    ANIM_MOVIE_CODE_2
623
+; unknown code -> end
624
+        ldi     TMP,1                   ; end
625
+        ret
626
+
627
+; plain frame
628
+ANIM_MOVIE_CODE_0:
629
+; save 12 bit value
630
+        push    DATA
631
+        push    CNT
592 632
 ; extract frame to frame buffer
593 633
         ldi     XL,low(FRAME)           ; ptr to pixel data
594 634
                                         ;   XH not there on ATtiny2313
595
-ANIM_MOVIE_FRAME_LOOP:
635
+ANIM_MOVIE_CODE_FRAME_PLAIN_LOOP:
596 636
         lpm     DATA,Z+                 ; get two pixels
597
-        mov     TMP2,DATA               ; write first pixel
598
-        swap    TMP2
599
-        andi    TMP2,0x0F
600
-        st      X+,TMP2
637
+        mov     TMP,DATA                ; write first pixel
638
+        swap    TMP
639
+        andi    TMP,0x0F
640
+        st      X+,TMP
601 641
         andi    DATA,0x0F               ; write second pixel
602 642
         st      X+,DATA
603 643
         cpi     XL,low(FRAME)+42        ; bottom of loop
604 644
                                         ;   XH not there on ATtiny2313
605
-        brlo    ANIM_MOVIE_FRAME_LOOP
645
+        brlo    ANIM_MOVIE_CODE_FRAME_PLAIN_LOOP
646
+; restore 12 bit value
647
+        pop     CNT
648
+        pop     DATA
649
+; show frame
650
+        rjmp    ANIM_MOVIE_CODE_SHOW
651
+
652
+; rle frame
653
+ANIM_MOVIE_CODE_1:
654
+; save 12 bit value
655
+        push    DATA
656
+        push    CNT
657
+; extract frame to frame buffer
658
+        ldi     XL,low(FRAME)           ; ptr to pixel data
659
+                                        ;   XH not there on ATtiny2313
660
+        ldi     CNT,0                   ; no pixel data yet
661
+ANIM_MOVIE_CODE_FRAME_RLE_LOOP:
662
+; load new pixel data if none available
663
+        cpi     CNT,0
664
+        brne    ANIM_MOVIE_CODE_FRAME_RLE_DATA_OK
665
+        lpm     DATA,Z+                 ; get repeat count and pixel value
666
+        mov     CNT,DATA
667
+        andi    DATA,0x0F               ; pixel value in DATA
668
+        swap    CNT
669
+        andi    CNT,0x0F                ; repeat count in CNT
670
+        inc     CNT                     ; use for repeat count + 1 pixels
671
+ANIM_MOVIE_CODE_FRAME_RLE_DATA_OK:
672
+; write pixel data to frame
673
+        st      X+,DATA
674
+; count down available pixel data
675
+        dec     CNT
676
+; bottom of loop
677
+        cpi     XL,low(FRAME)+42        ;   XH not there on ATtiny2313
678
+        brlo    ANIM_MOVIE_CODE_FRAME_RLE_LOOP
679
+; restore 12 bit value
680
+        pop     CNT
681
+        pop     DATA
606 682
 ; show frame
607
-        rcall   OUT_FRAME_TIME          ; frame time is already in TMP
608
-; next frame
609
-        rjmp    ANIM_MOVIE
610
-; end of movie
611
-ANIM_MOVIE_END:
683
+        rjmp    ANIM_MOVIE_CODE_SHOW
684
+
685
+; back reference
686
+ANIM_MOVIE_CODE_2:
687
+; check if in nested call
688
+        cpi     TMP2,0
689
+        brne    ANIM_MOVIE_CODE_2_NESTED
690
+; save pointer to frame data
691
+        push    ZL
692
+        push    ZH
693
+; go back by 12 bit value in DATA:CNT
694
+        sub     ZL,CNT
695
+        sbc     ZH,DATA
696
+; recursive call
697
+        ldi     TMP2,1                  ; nested call
698
+        rcall   ANIM_MOVIE_CODE
699
+; restore pointer to frame data
700
+        pop     ZH
701
+        pop     ZL
702
+; done
703
+        ldi     TMP,0                   ; continue
704
+        ret
705
+
706
+; back reference - nested call
707
+ANIM_MOVIE_CODE_2_NESTED:
708
+; go back by 12 bit value in DATA:CNT
709
+        sub     ZL,CNT
710
+        sbc     ZH,DATA
711
+; recursive tail call (no need to save Z on nested call)
712
+        ldi     TMP2,1                  ; nested call
713
+        rjmp    ANIM_MOVIE_CODE
714
+
715
+; show frame
716
+ANIM_MOVIE_CODE_SHOW:
717
+; high part of frame time
718
+        cpi     DATA,0
719
+        breq    ANIM_MOVIE_CODE_SHOW_HIGH_DONE
720
+        ldi     TMP,0                   ; means 256 frames times
721
+        rcall   OUT_FRAME_TIME
722
+        dec     DATA
723
+        rjmp    ANIM_MOVIE_CODE_SHOW
724
+ANIM_MOVIE_CODE_SHOW_HIGH_DONE:
725
+; low part of frame time
726
+        cpi     CNT,0
727
+        breq    ANIM_MOVIE_CODE_SHOW_LOW_DONE
728
+        mov     TMP,CNT
729
+        rcall   OUT_FRAME_TIME
730
+ANIM_MOVIE_CODE_SHOW_LOW_DONE:
731
+; done
732
+        ldi     TMP,0                   ; continue
733
+        ret
734
+
735
+
736
+
737
+; play animation
738
+; input: Z = pointer to movie data
739
+; output: -
740
+; changes: X, Z, FRAME, CNT, DATA, TMP, TMP2
741
+ANIM_MOVIE:
742
+; process code block
743
+        ldi     TMP2,0                  ; initial call
744
+        rcall   ANIM_MOVIE_CODE
745
+; continue if not yet end
746
+        cpi     TMP,0
747
+        breq    ANIM_MOVIE
748
+; done
612 749
         ret
613 750
 
614 751
 
... ...
@@ -8,6 +8,8 @@ import sys
8 8
 class Movie(object):
9 9
     class Frame(object):
10 10
 
11
+        # offsets (y * width(66) + x) of the pixels whose green value
12
+        # is taken from the *.bbm frames for the LEDs
11 13
         LED_COORDS = [
12 14
             408, 414, 420, 468, 492, 677, 684, 863, 945, 951, 957, 963, 971,
13 15
             975, 1034, 1209, 1215, 1221, 1227, 1259, 1299, 1304, 1373, 1469,
... ...
@@ -15,6 +17,9 @@ class Movie(object):
15 17
             2106, 2163, 2225, 2365, 2559, 2620, 2956
16 18
         ]
17 19
 
20
+        # offsets (y * width(66) + x) of the pixels whose color is set to
21
+        # the LED brightness in the *.bbm frames on output
22
+        # (multiple (-> list) per LED)
18 23
         LED_COORDS_OUT = [
19 24
             [408, 407, 409], [414, 413, 415], [420, 419, 421], [468, 533, 403],
20 25
             [492, 425, 559], [677, 610, 744], [684, 617, 751], [863, 797, 929],
... ...
@@ -36,12 +41,15 @@ class Movie(object):
36 41
             self.leds = len(self.LED_COORDS) * [0]
37 42
 
38 43
         def from_frame_data(self, duration, data):
44
+            """parse frame from duration and *.bbm pixel data"""
39 45
             self.duration = duration
40 46
             for i in range(len(self.LED_COORDS)):
41 47
                 ledno = self.LED_COORDS[i]
42 48
                 self.leds[i] = data[ledno * 3 + 1]
43 49
 
44 50
         def to_frame_data(self):
51
+            """convert frame to *.bbm frame data,
52
+               return duation and pixel data"""
45 53
             pixels = 51 * 66 * [0, 0, 255]
46 54
             for i in range(len(self.LED_COORDS_OUT)):
47 55
                 for ledno in self.LED_COORDS_OUT[i]:
... ...
@@ -52,21 +60,55 @@ class Movie(object):
52 60
             return self.duration, data
53 61
 
54 62
         def to_firmware_data(self):
63
+            """convert a frame to firmware data"""
64
+            # duration: in 6ms steps, 12 bits
55 65
             duration = (self.duration + 3) // 6
56 66
             if duration < 1:
57 67
                 duration = 1
58
-            if duration > 255:
59
-                duration = 255
60
-            fw_data = [duration]
68
+            if duration > 0x3FF:
69
+                duration = 0x3FF
70
+            # use shorter encoding
71
+            plain = self._fw_pix_data_plain()
72
+            rle = self._fw_pix_data_rle()
73
+            if len(rle) < len(plain):
74
+                code = 0x10 # rle compressed
75
+                data = rle
76
+            else:
77
+                code = 0x00 # plain
78
+                data = plain
79
+            # encode code and duration at begin of data
80
+            dur_h = (duration >> 8) & 0x0F
81
+            dur_l = duration & 0xFF
82
+            return [code | dur_h, dur_l] + data
83
+
84
+        def _fw_pix_data_plain(self):
85
+            """return pixel data, plain, no compression"""
86
+            data = []
61 87
             half = None
62 88
             for led in self.leds:
63 89
                 val = (led >> 4) & 0x0F
64 90
                 if half is None:
65 91
                     half = val
66 92
                 else:
67
-                    fw_data.append(half << 4 | val)
93
+                    data.append(half << 4 | val)
68 94
                     half = None
69
-            return fw_data
95
+            return data
96
+
97
+        def _fw_pix_data_rle(self):
98
+            """return pixel data, compressed using run length encoding"""
99
+            data = []
100
+            val = (self.leds[0] >> 4) & 0x0F
101
+            cnt = 0
102
+            for led in self.leds:
103
+                ledval = (led >> 4) & 0x0F
104
+                if val == ledval and cnt < 0x10:  # same value -> count
105
+                    cnt += 1
106
+                else:
107
+                    data.append((cnt - 1) << 4 | val)  # append RLE item
108
+                    val = ledval
109
+                    cnt = 1
110
+            data.append((cnt - 1) << 4 | val)  # last RLE item
111
+            return data
70 112
 
71 113
     def __init__(self):
72 114
         self.frames = []
... ...
@@ -77,6 +119,7 @@ class Movie(object):
77 119
         self.frame_hdr = struct.Struct("!H")
78 120
 
79 121
     def read_bbm(self, filename):
122
+        """read movie from *.bbm file"""
80 123
         try:
81 124
             with open(filename, "rb") as f:
82 125
                 # main header
... ...
@@ -119,6 +162,7 @@ class Movie(object):
119 162
             return False
120 163
 
121 164
     def write_bbm(self, filename):
165
+        """write movie as *.bbm file"""
122 166
         with open(filename, "wb") as f:
123 167
             # main header
124 168
             f.write(self.main_hdr.pack(0x23542666, 51, 66, 3, 255))
... ...
@@ -136,13 +180,41 @@ class Movie(object):
136 180
                 f.write(data)
137 181
 
138 182
     def write_firmware(self, filename):
139
-        with open(filename, "w") as f:
183
+        """write movie as firmware (assembly include file)"""
184
+        # convert all frames to firware data
185
+        fw_frames = []
186
+        fw_len = 0
140 187
         for frame in self.frames:
141 188
             fw_data = frame.to_firmware_data()
189
+            # search for identical frame before
190
+            id_len = 0
191
+            for id_frame in fw_frames:
192
+                if id_frame == fw_data and id_frame[0] & 0xE0 == 0x00:
193
+                    # identical frame found (and code is 0x00 or 0x10)
194
+                    # -> replace data with back reference
195
+                    back = fw_len - id_len + 2
196
+                    if back <= 0x3FF:
197
+                        back_h = (back >> 8) & 0x0F
198
+                        back_l = back & 0xFF
199
+                        fw_data = [0x20 | back_h, back_l]
200
+                        print("DEBUG back", fw_data)
201
+                        break
202
+                id_len += len(id_frame)
203
+            # append frame to list
204
+            fw_frames.append(fw_data)
205
+            fw_len += len(fw_data)
206
+        # build firmware data
207
+        fw_data = []
208
+        for fw_frame in fw_frames:
209
+            fw_data += fw_frame
210
+        fw_data.append(0xF0) # end marker
211
+        if len(fw_data) & 1 != 0:
212
+            fw_data.append(0) # ensure even length
213
+        # write firmware data as assembly
214
+        with open(filename, "w") as f:
142 215
             for i in range(0, len(fw_data), 8):
143 216
                 vals = ["0x{:02X}".format(v) for v in fw_data[i:i + 8]]
144 217
                 print("        .db     {:s}".format(",".join(vals)), file=f)
145
-            print("        .db     0,0", file=f)
146 218
 
147 219
 
148 220
 def parse_arguments():