写一个自己的C++控制台字符画版BadApple!!
字符画的原理十分简单,就是先将图片转换成文本,然后在控制台输出实现播放。
我写的badapple程序:https://files.cnblogs.com/files/CodeMIRACLE/badapple.zip
首先下载 badapple!影绘.mp4
利用ffmpeg将这个视频按照每秒8帧截取成24位位图序列
ffmpeg -i badapple.mp4 -r 8 -vcodec bmp \p\%04d.bmp
在p文件夹下生成了1754个bmp文件,我们利用下面的程序把它们转化为字符画,并整合到一个txt文件中。
1 #include <cstdio>
2 #include <cstring>
3 #include <stdint.h>
4 #include <windows.h>
5 int32_t width,height;
6 RGBQUAD *pixels;
7 bool OpenBitmap(char const *filename)
8 {
9 FILE *file = fopen(filename, "rb");
10 if (file)
11 {
12 width=0;
13 height=0;
14 BITMAPFILEHEADER bf;
15 BITMAPINFOHEADER bi;
16 fread(&bf, sizeof(bf), 1, file);
17 fread(&bi, sizeof(bi), 1, file);
18 if(bi.biBitCount!=24)
19 return false;
20 if(bi.biCompression!=BI_RGB)
21 return false;
22 width=bi.biWidth;
23 height=bi.biHeight;
24 pixels=new RGBQUAD[width*height];
25 uint32_t rowSize = (bi.biBitCount * width + 31) / 32 * 4;
26 uint8_t *line = new uint8_t[rowSize];
27 for (int y = 0; y < height; y++)
28 {
29 fread(line, rowSize, 1, file);
30 for (int x = 0; x < width; x++)
31 {
32 uint8_t *color = line + x * 3;
33 RGBQUAD *pixel = &pixels[(height-y-1) * width+x];
34 pixel->rgbBlue = color[0];
35 pixel->rgbGreen = color[1];
36 pixel->rgbRed = color[2];
37 }
38 }
39 delete[] line;
40 fclose(file);
41 return true;
42 }
43 return false;
44 }
45 RGBQUAD GetColor(int x, int y, int w, int h)
46 {
47 int r = 0, g = 0, b = 0;
48 for (int i = 0; i < w; i++)
49 {
50 if (i + x >= width) continue;
51 for (int j = 0; j < h; j++)
52 {
53 if (j + y >= height) continue;
54 RGBQUAD const& color = pixels[(y + j) * width + (x + i)];
55 r += color.rgbRed;
56 g += color.rgbGreen;
57 b += color.rgbBlue;
58 }
59 }
60 return RGBQUAD{r / (w * h), g / (w * h),b / (w * h)};
61 }
62 char ColorToCharacter(RGBQUAD const& color)
63 {
64 int brightness = (color.rgbRed + color.rgbGreen + color.rgbBlue) / 3;
65 static char const *characters = "Qdogc*;:-. ";
66 int len = strlen(characters);
67 int span = 0xFF / len;
68 int cidx = brightness / span;
69 if (cidx == len)
70 cidx--;
71 return characters[cidx];
72 }
73 void OutputAscii(const char* filename, int w, int h)
74 {
75 FILE *file=fopen(filename,"a+");
76 int x = width / w;
77 int y = height / h;
78 for (int i = 0; i < height; i += y)
79 {
80 for (int j = 0; j < width; j += x)
81 {
82 RGBQUAD color = GetColor(j, i, x, y);
83 fprintf(file, "%c", ColorToCharacter(color));
84 //printf("%c", ColorToCharacter(color));
85 }
86 fprintf(file, "\n");
87 //printf("\n");
88 }
89 delete [] pixels;
90 fclose(file);
91 }
92 int main()
93 {
94 char filename[1024];
95 for(int i=1;i<=1754;i++)
96 {
97 sprintf(filename,"p/%04d.bmp",i);
98 if(OpenBitmap(filename));
99 OutputAscii("badapple.txt",width/6,height/12);
100 }
101 }
接下来要做的就是播放了,读取文件然后输出在控制台中。
为了保证和原来的视频有一样的播放速度,我们要限制其播放帧数。
1 #include <cstdio>
2 #include <windows.h>
3 struct fps_limit {
4
5 int previous_time;
6 int tpf_limit;
7 int tpf;
8 fps_limit(int fps = 60) : previous_time(GetTickCount()), tpf(0) {
9 limit_fps(fps);
10 }
11 void reset() {
12 previous_time = GetTickCount(),
13 tpf = 0;
14 tpf_limit = 60;
15 }
16 void limit_fps(int fps) {
17 tpf_limit = (int)(1000.0f / (float)fps);
18 }
19 void delay() {
20 tpf = GetTickCount() - previous_time;
21
22 if(tpf < tpf_limit)
23 Sleep(tpf_limit - tpf - 1);
24
25 previous_time = GetTickCount();
26 }
27 };
28 int main()
29 {
30 FILE* fp=fopen("badapple.txt","r");
31 char buf[1024];
32 fps_limit fps(8);
33 while(!feof(fp))
34 {
35 HANDLE hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);
36 COORD pos;
37 pos.X = 0;
38 pos.Y = 0;
39 SetConsoleCursorPosition(hConsoleOutput, pos);
40 for(int i=0;i<32;i++)
41 {
42 fgets(buf,1024,fp);
43 printf("%s",buf);
44 }
45 fps.delay();
46 }
47 return 0;
48 }
到这位置badapple就写完了
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
后来看见网上很多大神写的badapple程序,他们的exe文件都特别小,只有单个exe文件。我开始考虑改进一下自己的程序。
先是压缩数据,写了个huffman压缩,原来4.7M的badapple.txt文件,压缩了成1.3M。感觉压缩效率太低了。
肯定是我姿势不对......
1 #include<cstdio>
2 #include<vector>
3 #include<map>
4 #include<algorithm>
5 #include<queue>
6 #include<windows.h>
7 #include<string>
8 using namespace std;
9 int cnum[256]= {0};
10 map<char,string> huffmap;
11 static const unsigned char mask[8] =
12 {
13 0x80, /* 10000000 */
14 0x40, /* 01000000 */
15 0x20, /* 00100000 */
16 0x10, /* 00010000 */
17 0x08, /* 00001000 */
18 0x04, /* 00000100 */
19 0x02, /* 00000010 */
20 0x01 /* 00000001 */
21 };
22
23 typedef struct HTNode
24 {
25 char c;
26 unsigned int freq;
27 HTNode *lchild, *rchild;
28 HTNode(char key='\0', unsigned int fr=0, HTNode *l=NULL, HTNode *r=NULL):
29 c(key),freq(fr),lchild(l),rchild(r) {};
30 } HTNode,*pNode;
31
32 struct cmp
33 {
34 bool operator()(pNode node1, pNode node2)
35 {
36 return node1->freq > node2->freq;
37 }
38 };
39
40 priority_queue<pNode, vector<pNode>, cmp> pq;
41 void HuffmanCode(int n)
42 {
43 pNode l, r;
44 while (pq.size() > 1)
45 {
46 pNode z = new HTNode;
47 l = pq.top();
48 pq.pop();
49 r = pq.top();
50 pq.pop();
51 z->lchild = l;
52 z->rchild = r;
53 z->freq = l->freq + r->freq;
54 pq.push(z);
55 }
56 }
57
58 void GetCode(pNode t, string str)
59 {
60 if (t == NULL)
61 return;
62 if (t->lchild)
63 {
64 str += '0';
65 GetCode(t->lchild, str);
66 }
67 if (t->lchild == NULL && t->rchild == NULL)
68 {
69 //printf("%c's code: %s\n",t->c,str.c_str());
70 huffmap[t->c]=str;
71 }
72 str.erase(str.end()-1);
73 if (t->rchild)
74 {
75 str += '1';
76 GetCode(t->rchild, str);
77 }
78 }
79
80 void WriteCode(FILE *fp,string& code)
81 {
82 char bitBuf = 0x00;
83 int bitPos = 0;
84 for(int i = 0; i < code.size(); ++i)
85 {
86 if(code[i] == '1')
87 {
88 bitBuf |= mask[bitPos++];
89 }
90 else
91 {
92 bitPos++;
93 }
94 if(bitPos == 8)
95 {
96 fwrite(&bitBuf, 1, sizeof(bitBuf), fp);
97 bitBuf = 0x00;
98 bitPos = 0;
99 }
100 }
101 if(bitPos != 0)
102 {
103 fwrite(&bitBuf, 1, sizeof(bitBuf), fp);
104 }
105 }
106
107 int main()
108 {
109 FILE* fp=fopen("badapple.txt","r");
110 while(!feof(fp))
111 {
112 int c=fgetc(fp);
113 cnum[c]++;
114 }
115 int n=0;
116 for(int i=0; i<256; i++)
117 if(cnum[i])
118 {
119 n++;
120 pNode p = new HTNode;
121 p->c = i;
122 p->freq = cnum[i];
123 pq.push(p);
124 }
125 string str;
126 HuffmanCode(n);
127 GetCode(pq.top(), str);
128 FILE *fout=fopen("badapple.dat","wb+");
129 fseek(fp,0,SEEK_SET);
130 str="";
131 while(!feof(fp))
132 {
133 int c=fgetc(fp);
134 str+=huffmap[c];
135 }
136 fclose(fp);
137 WriteCode(fout,str);
138 fclose(fout);
139 }
压缩率好低啊, (╯‵□′)╯︵┴─┴
后来试了几种其他方法,不想再试了,直接用别人的库,后来用的zlib进行压缩。
下面的程序把4.7M的文件,压缩到了470多KB。
1 #include <stdlib.h>
2 #include <string.h>
3 #include <cstdio>
4 #include "zlib.h"
5
6 int main()
7 {
8 FILE* fp=fopen("badapple.txt","r");
9 char* buf = NULL;
10 char* src = NULL;
11 fseek(fp,0,SEEK_END);
12 unsigned int flen=ftell(fp);
13 fseek(fp,0,SEEK_SET);
14 if((src = (char*)malloc(sizeof(char) * flen)) == NULL)
15 {
16 printf("no enough memory!\n");
17 return -1;
18 }
19 fread(src,flen,sizeof(char),fp);
20 unsigned int blen=compressBound(flen);
21 if((buf = (char*)malloc(sizeof(char) * blen)) == NULL)
22 {
23 printf("no enough memory!\n");
24 return -1;
25 }
26 if(compress(buf, &blen, src, flen) != Z_OK)
27 {
28 printf("compress failed!\n");
29 return -1;
30 }
31 fclose(fp);
32 free(src);
33 fp=fopen("badapple.dat","wb");
34 fwrite(&flen,1,sizeof(int),fp);
35 fwrite(buf,blen,sizeof(char),fp);
36 fclose(fp);
37 free(buf);
38 return 0;
39 }
然后我写了一个解压程序测试一下。
1 #include <cstdio>
2 #include <iostream>
3 #include <cstdlib>
4 #include "zlib.h"
5 using namespace std;
6 int main()
7 {
8 FILE* fp=fopen("badapple.dat","rb");
9 unsigned int slen;
10 fread(&slen,1,sizeof(int),fp);
11 char* buf = NULL;
12 char* dst = NULL;
13 fseek(fp,0,SEEK_END);
14 unsigned int flen=ftell(fp);
15 fseek(fp,4,SEEK_SET);
16 flen-=4;
17 unsigned int blen=compressBound(slen);
18 if((dst = (char*)malloc(sizeof(char) * slen)) == NULL)
19 {
20 printf("no enough memory!\n");
21 return -1;
22 }
23 if((buf = (char*)malloc(sizeof(char) * blen)) == NULL)
24 {
25 printf("no enough memory!\n");
26 return -1;
27 }
28 fread(buf,flen,sizeof(char),fp);
29 if(uncompress(dst, &slen, buf, blen) != Z_OK)
30 {
31 printf("uncompress failed!\n");
32 return -1;
33 }
34 free(buf);
35 fclose(fp);
36 for(int i=0;i<slen;i++)
37 printf("%c",dst[i]);
38 }
解压和压缩没问题了,就该把数据文件打包生成exe文件了,有了图像之后顺便也把音乐加上吧。
先编写rc文件
0 TYPEDATA "badapple.dat"
1 TYPEWAV "BadApple.wav"
调用windres生成res文件,然后和下面的程序链接生成exe文件
1 #include <cstdio>
2 #include <windows.h>
3 #include "zlib.h"
4 char* dst = NULL;
5 struct fps_limit {
6
7 int previous_time;
8 int tpf_limit;
9 int tpf;
10 fps_limit(int fps = 60) : previous_time(GetTickCount()), tpf(0) {
11 limit_fps(fps);
12 }
13 void reset() {
14 previous_time = GetTickCount(),
15 tpf = 0;
16 tpf_limit = 60;
17 }
18 void limit_fps(int fps) {
19 tpf_limit = (int)(1000.0f / (float)fps);
20 }
21 void delay() {
22 tpf = GetTickCount() - previous_time;
23
24 if(tpf < tpf_limit)
25 Sleep(tpf_limit - tpf - 1);
26
27 previous_time = GetTickCount();
28 }
29 };
30 void Uncompressdata()
31 {
32 HRSRC hrscdat =FindResource(NULL,MAKEINTRESOURCE(0),"TYPEDATA");
33 char *a=(char *)LockResource(LoadResource(NULL,hrscdat));
34 DWORD flen = SizeofResource(NULL, hrscdat);
35 DWORD slen;
36 memcpy(&slen,a,sizeof(int));
37 char* buf = NULL;
38 flen-=4;
39 unsigned int blen=compressBound(slen);
40 if((dst = (char*)malloc(sizeof(char) * (slen+1))) == NULL)
41 {
42 printf("no enough memory!\n");
43 exit(1);
44 }
45 if((buf = (char*)malloc(sizeof(char) * blen)) == NULL)
46 {
47 printf("no enough memory!\n");
48 exit(1);
49 }
50 memcpy(buf,a+4,flen*sizeof(char));
51 if(uncompress(dst, &slen, buf, blen) != Z_OK)
52 {
53 printf("uncompress failed! %d\n");
54 exit(1);
55 }
56 dst[slen]='\0';
57 free(buf);
58 }
59 int main()
60 {
61 printf("Loading......");
62 HRSRC hrscwav=FindResource(NULL,MAKEINTRESOURCE(1),"TYPEWAV");
63 Uncompressdata();
64 printf("OK\n把控制台的高度和宽度都调大点哦\n");
65 system("PAUSE");
66 system("CLS");
67 PlaySound((LPCSTR)LockResource(LoadResource(NULL,hrscwav)),NULL,SND_MEMORY|SND_ASYNC);
68 fps_limit fps(8);
69 int i=0,j=0;
70 char buf[3000];
71 Sleep(2000);
72 while(1)
73 {
74 HANDLE hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);
75 COORD pos;
76 pos.X = 0;
77 pos.Y = 0;
78 SetConsoleCursorPosition(hConsoleOutput, pos);
79 while(j<32*87)
80 {
81 if(dst[i]=='\0')
82 return 0;
83 buf[j++]=dst[i++];
84 }
85 j=0;
86 printf("%s",buf);
87 fps.delay();
88 }
89 }
好了,大功告成,写个这种程序还是蛮有意思的。