3 MML播放器(harib27c)

我们来想想看,用现存的API,还能做出什么有趣的应用程序呢?想起来了,做个演奏音乐的程序吧,这样一来我们就可以边听音乐边看电子书了,好不惬意。不过说是音乐,其实只是用音质很烂的蜂鸣器来演奏啦。

先来看看实际运行的画面。输入“mmlplay daigo.mml”后如下图。

3 MML播放器(harib27c) - 图1

正在演奏daigo.mml1

1 乐曲名称为《c小调第五交响曲“命运”op.67》。——译者注

■■■■■

在命令行中指定的文件名,是一个用MML(music macro language)编写的文本文件。这个文件中包含的内容是乐谱数据,如“哆来咪发唆啦西哆”分别对应“CDEFGABC”(在C大调中)。在音符的后面加上“+”或者“#”表示升高半音,加上“-”表示降低半音。再后面还可以加上数字,“4”表示四分音符,“8”表示八分音符,“1”表示全音符,如果是“2.”的话则表示符点二分音符。

“O”命令用来设定八度音区,例如可以设为“O4”。“L”命令用来设定音长,表示音符或者休止符(R)在不指定音长时的默认音长。“T”命令用来设定乐曲速度,“>”用来升高一个八度,“<”用来降低一个八度,“&”表示连音线。“Q”用来指定音符演奏是短促还是连贯,如“Q4”标出断奏,“Q8”表示一直使用连音奏法。

“$”开头的是扩展命令,是笔者自己对MML语法扩展出来的。“$K”是卡拉OK命令,后面指定的字符串会显示在卡拉OK栏中。“$E”表示卡拉OK歌词数据的字符编码,不过这个信息在mmlplay.hrb中是被忽略的。

MML数据中是不区分大小写的,演奏到最后会自动回到开头重新演奏,按下“Q”可以退出。

在演奏过程中有一条蓝色的竖线在不停地动,它是根据音阶来移动的,笔者觉得这个看上去挺好玩的。

乐曲数据我们先准备了4个2,选曲按照笔者的喜好(笑)。乐曲数据文件貌似都比较大,因此全部用tek5进行了压缩。

2 这里的几首歌曲给出的是日文版的歌词,如果大家没有对日文显示支持的部分进行改造,而是沿用作者编写的日文显示程序和日文字库,那么在这里只有用日文的歌词才能正常显示。如果大家对日文显示的部分进行了改造,并使用了中文字库,那么就需要将歌词替换为中文。为了方便大家使用,我们在原文下面给出了相应的中文歌词。——译者注

■■■■■

kirakira.mml

  1. /* 《小星星》3 法国民歌 */
  2.  
  3. $E"SJIS"; T120L4O4
  4.  
  5. $K"一闪一闪亮晶晶,满天都是小星星"; CCGGAAG2 FFEEDDC2
  6. $K"挂在天上放光明,好像许多小眼睛"; GGFFEED2 GGFFEED2
  7. $K"一闪一闪亮晶晶,满天都是小星星"; CCGGAAG2 FFEEDDC2
  8. $K""; R1
  9.  
  10. $K"一闪一闪亮晶晶,满天都是小星星"; CCGGAAG2 FFEEDDC2
  11. $K"挂在天上放光明,好像许多小眼睛"; GGFFEED2 GGFFEED2
  12. $K"一闪一闪亮晶晶,满天都是小星星"; CCGGAAG2 FFEEDDC2
  13. $K""; R1 R1

3 这首歌中文版歌词只有一段,第二段重复第一段歌词即可。——译者注

fujisan.mml

  1. /* 《富士山》4日本文部省民歌*/
  2.  
  3. $E"SJIS"; T120L4O4
  4.  
  5. $K"冲破云霄"; G.G8AGEC8D8E2 D.G8GF8E8D2.R
  6. $K"傲视群山"; G.G8ECA.B8>C<A G.A8G8F8E8D8C2.R
  7. $K"问鼎惊雷"; D.D8DDC8D8E8F8G2 A.B8>C<AG2.R
  8. $K"富士,日本第一山"; >C2<AGE.E8AG FED.C8C2.R
  9.  
  10. $K"高耸蓝天"; G.G8AGEC8D8E2 D.G8GF8E8D2.R
  11. $K"银装素裹"; G.G8ECA.B8>C<A G.A8G8F8E8D8C2.R
  12. $K"身披彩霞"; D.D8DDC8D8E8F8G2 A.B8>C<AG2.R
  13. $K"富士,日本第一山"; >C2<AGE.E8AG FED.C8C2.R
  14.  
  15. $K""; R1R1

4 这首歌是日本的歌谣,没有相应的中文版歌词,这里给出翻译的歌词大意。——译者注

daigo.mml

  1. /* "第五交响曲 c小调 "命运" op. 67"选段 路德维希•范•贝多芬*/
  2.  
  3. $E"SJIS"; $K"第五交响曲 c小调 "命运" op.67";
  4.  
  5. T155Q7L8O4
  6. RGGGE-2.RFFFD4&D1
  7. RGGGQ8E-Q7A-A-A-Q8GQ7>E-E-E-C8&C2<GGG
  8. Q8DQ7A-A-A-Q8GQ7>FFFD8&D2GGF
  9. Q8E-Q7<E-E-FQ8G>Q7GGFQ8E-Q7<E-E-F
  10. Q8GQ7>GGFL4E-RCRG2.L8R<A-A-A-F4&F1
  11. RA-A-A-Q8FQ7DDDQ8<BQ7A-A-A-Q8GQ7<GGG
  12. >>E-A-A-A-Q8FQ7DDDQ8<BQ7A-A-A-Q8GQ7<GGG
  13. >>E-G>CCQ8C2Q7<BBB>DQ8D2Q7CCCE-Q8E-Q7DQ4DF
  14. Q8FQ7EQ4EGQ7GQ7FQ4FA-Q8A-Q7GQ4GB-Q8B-Q7A-Q4A->C
  15. Q8CQ7<BQ4B>DQ7CE-E-E-C<GGGE-C<GGE-CCC<B>>>FDD
  16.  
  17. <BGFFD<BGFD<B>CCC>>E-E-E-
  18. C<AAAG-E-E-E-C<AAAA4R2R4B-4R4
  19. RB-B-B-E-2F2Q8<B-2>L4B->E-DE-FQ7CQ8CQ7<B-
  20. Q8B->E-DE-FQ7CQ8CQ7<B-Q8>B->E-DE-FQ7CQ8C<B-
  21. Q8<B->CD-Q7CQ8<B->C<B-Q7A-Q8>D-E-FQ7E-
  22. Q8D-E-D-Q7CQ8E-FG-FE-Q7FQ8G-FE-Q7FQ8G-F
  23. E-Q7FQ8G-FE-FG-FG-Q7AL8B-&B-2Q4>C<B-A-
  24. Q8A-Q7GQ4FE-Q8E-Q7DQ4CDQ8FQ7E-Q4<B-G
  25. Q8>DQ7CQ4<A-FQ8>CQ7<B-Q4GE-Q7<B-Q8>>AB-A
  26. B-AB-Q7AQ4B->C<B-A-Q8A-Q7GQ4FE-Q8E-Q7DQ4CD
  27. Q8FQ7E-Q4<B-GQ8>DQ7CQ4<A-FQ8>CQ7<B-Q4GE-
  28. Q7<B->B->B-B-E-GGGE-<B-B-B-GE-E-E-
  29. Q8<B-Q7>DDDQ8E-Q7>GGGE-<B-B-B-GE-E-E-
  30. Q8<B-Q7>B-B-B-B-4R4.B-B-B-B-4R4.>DDDE-4
  31. R1R4

daiku.mml

  1. /* "第九交响曲 d小调 "合唱" op.125"选段 路德维希•范•贝多芬5 */
  2.  
  3. $E"SJIS"; T110L4
  4.  
  5. O4
  6. $K"欢乐女神,圣洁美丽,灿烂光芒照大地"; F+F+GA AGF+E DDEF+ F+.E8E2
  7. $K"我们心中充满热情,来到你的圣殿里"; F+F+GA AGF+E DDEF+ E.D8D2
  8. $K"你的力量能使人们消除一切分歧"; EEF+D EF+8G8F+D EF+8G8F+E DE<A>
  9. $K"在你光辉照耀下面,人们团结成兄弟"; F+& F+F+GA AGF+E DDEF+ E.D8D2
  10.  
  11. O5
  12. $K"在这美丽大地上,普世众生共欢乐"; F+F+GA AGF+E DDEF+ F+.E8E2
  13. $K"一切人们不论善恶,都蒙自然赐恩泽"; F+F+GA AGF+E DDEF+ E.D8D2
  14. $K"它给我们爱情美酒,同生共死好朋友"; EEF+D EF+8G8F+D EF+8G8F+E DE<A>
  15. $K"它让众生共享欢乐,天使也高声同唱歌"; F+& F+F+GA AGF+E DDEF+ E.D8D2
  16.  
  17. $K""; R1

5 《欢乐颂》中文歌词共有4段,这里选了有代表性的第1段和第3段。——译者注

■■■■■

然后我们来看程序。

mmlplay.c

  1. #include "apilib.h"
  2. #include <string.h> /* strlen */
  3. int strtol(char *s, char **endp, int base); /*标准函数(stdlib.h) */
  4. void waittimer(int timer, int time);
  5. void end(char *s);
  6. void HariMain(void)
  7. {
  8. char winbuf[256 * 112], txtbuf[100 * 1024];
  9. char s[32], *p, *r;
  10. int win, timer, i, j, t = 120, l = 192 / 4, o = 4, q = 7, note_old = 0;
  11. /*音号与频率(mHz)的对照表*/
  12. /*例如,04A为440Hz,即440000 */
  13. /*第16八度的A为1802240Hz,即1802240000 */
  14. /*以下为第16八度的列表(C~B) */
  15. static int tonetable[12] = {
  16. 1071618315, 1135340056, 1202850889, 1274376125, 1350154473, 1430438836,
  17. 1515497155, 1605613306, 1701088041, 1802240000, 1909406767, 2022946002
  18. };
  19. static int notetable[7] = { +9, +11, +0 /* C */, +2, +4, +5, +7 };
  20. /*命令行解析*/
  21. api_cmdline(s, 30);
  22. for (p = s; *p > ' '; p++) { } /*一直读到空格为止*/
  23. for (; *p == ' '; p++) { } /*跳过空格*/
  24. i = strlen(p);
  25. if (i > 12) {
  26. file_error:
  27. end("file open error.\n");
  28. }
  29. if (i == 0) {
  30. end(0);
  31. }
  32. /*准备窗口*/
  33. win = api_openwin(winbuf, 256, 112, -1, "mmlplay");
  34. api_putstrwin(win, 128, 32, 0, i, p);
  35. api_boxfilwin(win, 8, 60, 247, 76, 7);
  36. api_boxfilwin(win, 6, 86, 249, 105, 7);
  37. /*载入文件*/
  38. i = api_fopen(p);
  39. if (i == 0) {
  40. goto file_error;
  41. }
  42. j = api_fsize(i, 0);
  43. if (j >= 100 * 1024) {
  44. j = 100 * 1024 - 1;
  45. }
  46. api_fread(txtbuf, j, i);
  47. api_fclose(i);
  48. txtbuf[j] = 0;
  49. r = txtbuf;
  50. i = 0; /*通常模式*/
  51. for (p = txtbuf; *p != 0; p++) { /*为了方便处理,将注释和空白删去*/
  52. if (i == 0 && *p > ' ') { /*不是空格或换行符*/
  53. if (*p == '/') {
  54. if (p[1] == '*') {
  55. i = 1;
  56. } else if (p[1] == '/') {
  57. i = 2;
  58. } else {
  59. *r = *p;
  60. if ('a' <= *p && *p <= 'z') {
  61. *r += 'A' - 'a'; /*将小写字母转换为大写字母*/
  62. }
  63. r++;
  64. }
  65. } else if (*p == 0x22) {
  66. *r = *p;
  67. r++;
  68. i = 3;
  69. } else {
  70. *r = *p;
  71. r++;
  72. }
  73. } else if (i == 1 && *p == '*' && p[1] == '/') { /*段注释*/
  74. p++;
  75. i = 0;
  76. } else if (i == 2 && *p == 0x0a) { /*行注释*/
  77. i = 0;
  78. } else if (i == 3) { /*字符串*/
  79. *r = *p;
  80. r++;
  81. if (*p == 0x22) {
  82. i = 0;
  83. } else if (*p == '%') {
  84. p++;
  85. *r = *p;
  86. r++;
  87. }
  88. }
  89. }
  90. *r = 0;
  91. /*定时器准备*/
  92. timer = api_alloctimer();
  93. api_inittimer(timer, 128);
  94. /*主体*/
  95. p = txtbuf;
  96. for (;;) {
  97. if (('A' <= *p && *p <= 'G') || *p == 'R') { /*音符、休止符*/
  98. /*计算频率*/
  99. if (*p == 'R') {
  100. i = 0;
  101. s[0] = 0;
  102. } else {
  103. i = o * 12 + notetable[*p - 'A'] + 12;
  104. s[0] = 'O';
  105. s[1] = '0' + o;
  106. s[2] = *p;
  107. s[3] = ' ';
  108. s[4] = 0;
  109. }
  110. p++;
  111. if (*p == '+' || *p == '-' || *p == '#') {
  112. s[3] = *p;
  113. if (*p == '-') {
  114. i--;
  115. } else {
  116. i++;
  117. }
  118. p++;
  119. }
  120. if (i != note_old) {
  121. api_boxfilwin(win + 1, 32, 36, 63, 51, 8);
  122. if (s[0] != 0) {
  123. api_putstrwin(win + 1, 32, 36, 10, 4, s);
  124. }
  125. api_refreshwin(win, 32, 36, 64, 52);
  126. if (28 <= note_old && note_old <= 107) {
  127. api_boxfilwin(win, (note_old - 28) * 3 + 8, 60, (note_old - 28) * 3 + 10, 76, 7);
  128. }
  129. if (28 <= i && i <= 107) {
  130. api_boxfilwin(win, (i - 28) * 3 + 8, 60, (i - 28) * 3 + 10, 76, 4);
  131. }
  132. if (s[0] != 0) {
  133. api_beep(tonetable[i % 12] >> (17 - i / 12));
  134. } else {
  135. api_beep(0);
  136. }
  137. note_old = i;
  138. }
  139. /*音长计算*/
  140. if ('0' <= *p && *p <= '9') {
  141. i = 192 / strtol(p, &p, 10);
  142. } else {
  143. i = l;
  144. }
  145. for (; *p == '.'; ) {
  146. p++;
  147. i += i / 2;
  148. }
  149. i *= (60 * 100 / 48);
  150. i /= t;
  151. if (s[0] != 0 && q < 8 && *p != '&') {
  152. j = i * q / 8;
  153. waittimer(timer, j);
  154. api_boxfilwin(win, 32, 36, 63, 51, 8);
  155. if (28 <= note_old && note_old <= 107) {
  156. api_boxfilwin(win, (note_old - 28) * 3 + 8, 60, (note_old - 28) * 3 + 10, 76, 7);
  157. }
  158. note_old = 0;
  159. api_beep(0);
  160. } else {
  161. j = 0;
  162. if (*p == '&') {
  163. p++;
  164. }
  165. }
  166. waittimer(timer, i - j);
  167. } else if (*p == '<') { /*八度-- */
  168. p++;
  169. o--;
  170. } else if (*p == '>') { /*八度++ */
  171. p++;
  172. o++;
  173. } else if (*p == 'O') { /*八度指定*/
  174. o = strtol(p + 1, &p, 10);
  175. } else if (*p == 'Q') { /* Q参数指定*/
  176. q = strtol(p + 1, &p, 10);
  177. } else if (*p == 'L') { /*默认音长指定*/
  178. l = strtol(p + 1, &p, 10);
  179. if (l == 0) {
  180. goto syntax_error;
  181. }
  182. l = 192 / l;
  183. for (; *p == '.'; ) {
  184. p++;
  185. l += l / 2;
  186. }
  187. } else if (*p == 'T') { /*速度指定*/
  188. t = strtol(p + 1, &p, 10);
  189. } else if (*p == '$') { /*扩展命令*/
  190. if (p[1] == 'K') { /*卡拉OK命令*/
  191. p += 2;
  192. for (; *p != 0x22; p++) {
  193. if (*p == 0) {
  194. goto syntax_error;
  195. }
  196. }
  197. p++;
  198. for (i = 0; i < 32; i++) {
  199. if (*p == 0) {
  200. goto syntax_error;
  201. }
  202. if (*p == 0x22) {
  203. break;
  204. }
  205. if (*p == '%') {
  206. s[i] = p[1];
  207. p += 2;
  208. } else {
  209. s[i] = *p;
  210. p++;
  211. }
  212. }
  213. if (i > 30) {
  214. end("karaoke too long.\n");
  215. }
  216. api_boxfilwin(win + 1, 8, 88, 247, 103, 7);
  217. s[i] = 0;
  218. if (i != 0) {
  219. api_putstrwin(win + 1, 128 - i * 4, 88, 0, i, s);
  220. }
  221. api_refreshwin(win, 8, 88, 248, 104);
  222. }
  223. for (; *p != ';'; p++) {
  224. if (*p == 0) {
  225. goto syntax_error;
  226. }
  227. }
  228. p++;
  229. } else if (*p == 0) {
  230. p = txtbuf;
  231. } else {
  232. syntax_error:
  233. end("mml syntax error.\n");
  234. }
  235. }
  236. }
  237. void waittimer(int timer, int time)
  238. {
  239. int i;
  240. api_settimer(timer, time);
  241. for (;;) {
  242. i = api_getkey(1);
  243. if (i == 'Q' || i == 'q') {
  244. api_beep(0);
  245. api_end();
  246. }
  247. if (i == 128) {
  248. return;
  249. }
  250. }
  251. }
  252. void end(char *s)
  253. {
  254. if (s != 0) {
  255. api_putstr0(s);
  256. }
  257. api_beep(0);
  258. api_end();
  259. }

只要读过之前的程序,这段程序应该不会难懂吧。由音符数据计算频率,以及由音符的长度计算定时器应该设定为几秒这些地方可能不太容易理解,其实只要有相应的音乐知识(例如一个音升高半音后,频率是原来的约1.059463倍)就很容易看懂了。

总之(老生常谈了),本书的目的是编写操作系统,而不是编写MML播放器,关于相关音乐理论的讲解就省略了,不好意思。当然,笔者也是想给大家仔细讲讲的,只不过要讲清楚的话,又得增加1章的篇幅了。

将这个程序make一下,得到的mmlplay.hrb大小为1975字节,还是非常小的,真不错!