「杂文」基于 MASM 的音乐盒程序
写在前面
计算机原理与汇编实验。
因为比较好玩于是先在这里随性地写一份然后再改成杀软实验报告。
问题描述
实现一个音乐盒程序,要求:
- 主界面显示点歌单,至少要有 3 首乐曲;
- 按相应的按键(1、2、3 等)选择对应的乐曲;
- 按 Q 键退出;
- 在乐曲演奏过程中按下相应按键可以播放另一首乐曲或退出。
实验环境
- IDE:Visual Studio Code 1.88.1 with Extension MASM/TASM v1.1.1
- 汇编工具:MASM-v6.11
- DOS环境模拟器:jsdos
实验原理
主界面
使用 INT 21H
调用 DOS 的 9 号功能完成字符串输出,输出主界面。
输出主界面后再调用 DOS 的 1 号功能进行键盘读入单个字符,据此判断接下来要执行的操作。
代码节选如下:
DATA SEGMENT
MENU DB 0DH, 0AH, 'Choose the song you want and press the key:'
DB 0DH, 0AH, '1: Senbon Zakura'
DB 0DH, 0AH, '2: Merry Christmas Mr. Lawrence'
DB 0DH, 0AH, '3: Bad Apple!'
DB 0DH, 0AH, 'q: EXIT' ;退出
DB 0AH, 0AH, '$'
DATA ENDS
INPUT: ;控制音乐播放的主程序
LEA DX, MENU ;主界面字符串
MOV AH, 9 ;调用9号中断,将菜单显示在屏幕上
INT 21H
MOV AH, 1
INT 21H ;调用1号中断,输入播放哪首音乐或者退出播放
播放过程中切歌/退出
要求实现在播放过程中随时可以切换歌曲或退出,一个类似多线程输入的功能。
考虑在循环枚举音符并进行发声的同时,使用 INT 16H
的 1 号功能。该功能用于查询键盘缓冲区,对键盘扫描但是不等待(也就是不会中断程序),不会删除缓冲区中对应字符,并设置标志寄存器中的 ZF
。若如果有键盘输入(即键盘缓冲区不空),则令 ZF=0
,令 AL
存放当前输入的 ASCII
码,AH
存放输入字符的扩展码;若无键盘操作,则标志位 ZF=1
。
于是可通过如下代码检查是否读入了字符,若读入则中断当前过程并跳转到主界面,再在主界面中通过 DOS 的 9 号功能,从缓冲区读入字符并判断接下来的操作:
MOV AH, 1
INT 16H
JNZ INPUT
乐理
乐谱中每个音符具有音高(频率)和音长(持续时长)两种属性。将音符转换为相应频率的脉冲方波,通到扬声器上即可发出该音符的声音。此时控制输出波形的频率即可控制音高,通过维持频率波形的时间即可控制音长。
转换过程参考如下音符与频率对照表:
控制扬声器
考虑使用计算机内部的 8253 芯片进行计数与定时并产生音符对应的脉冲方波信号,8255 芯片向扬声器进行数据传输。
以下为 8253 芯片结构图:
由图可知, 8255 芯片 PB 端口的第 0 位 PB0
同时控制 8253 芯片的 GATE2
以驱动扬声器;8253 芯片通过 OUT2
连向扬声器,且扬声器同时由 8255 芯片的 PB 端口的第 1 位 PB1
控制。
对于 8253 芯片:
- 其控制端口地址为
43H
,定时器 2 端口地址为42H
。 - 初始化,将芯片设置为模式 3;此时输出线中 0 和 1 各占计数时间的一半,从而产生一系列间隔均匀的方波信号,从而通过输出波形的频率控制音调。
- 对定时器 2 编程,使其寄存器接收控制声音频率的计数值。石英震荡器每秒震荡
1193180 = 1234DCH
次,也即8253
芯片的主频为1.193180Mhz
。因此对于频率为 \(f\) 的音符,其对应方波频率的计数值 \(F\)(即每秒内产生方波的数量)可由下式计算:
- 计算出方波频率的计数值 \(F\) 后,将其送入定时器 2 的端口地址,产生方波作为 8255 芯片的控制信号。
对于 8255 芯片:
- 其 PB 端口地址为
61H
。 - 当端口 PB 的第 0 位
PB0
为 1 时,控制 8253 定时器来驱动扬声器,发声频率由 8253 芯片定时器 2 输出OUT2
决定;当第 1 位PB1
为 1 时,扬声器的门电路接通开始发声,并一直保持到位 1 变为 0 时关闭停止发声,即控制电路能以位触发和定时器控制两种不同的方式驱动扬声器发声。 - 通过循环控制
PB0, PB1
均为 1 的时间间隔,即可通过改变扬声器的发声时间以控制音长。
控制扬声器,输出一个音符代码节选如下:
;调用 sound 前,将音高传入寄存器 DI,音长传入寄存器 BX
sound proc near
PUSH AX
PUSH BX
PUSH CX
PUSH DX
PUSH DI
MOV AL, 0B6H ;8253 初始化
OUT 43H, AL ;43H 是 8253 芯片控制口的端口地址
MOV DX, 12H ;高 16 位
MOV AX, 3280H ;低 16 位
DIV DI ;计算分频值, 赋给 AX, DI 中存放声音的频率值。
OUT 42H, AL ;先送低 8 位到计数器,42H 是 8253 定时器 2 的端口地址
MOV AL, AH
OUT 42H, AL ;后送高 8 位计数器
;设置 8255 芯片, 控制扬声器的开/关
IN AL, 61H ;读取 8255 B 端口原值
MOV AH, AL ;保存原值
OR AL, 3 ;使低两位置变为 1,打开开关
OUT 61H, AL ;开扬声器, 发声
WAIT1:
MOV CX, 28000 ;设置一拍的长度
DELAY1:
NOP
LOOP DELAY1
DEC BX ;循环 BX 拍
JNZ WAIT1
MOV AL, AH ;恢复扬声器端口原值
OUT 61H, AL
POP DI
POP DX
POP CX
POP BX
POP AX
RET
sound ENDP
代码结构
简谱数据
每张简谱由音高和音长两张表,其中每个元素形容了一个音符的音高或音长构成。
函数列表
INPUT
函数:- 用于显示主界面选歌菜单,接受来自键盘的读入并判断接下来播放的歌曲,或退出程序。
- 形式参数:无。
sound
函数:- 接受单个音符的音高与音长,调用 8253 与 8255 芯片,播放单个音符的声音。
- 形式参数:寄存器
DI
存储音符的音高,寄存器BX
存储音符的音长。
play_music
函数:- 接受简谱的音高与音长,循环枚举简谱中所有音符并调用
sound
函数发声,控制音乐播放。 - 同时检查播放过程中是否有字符读入,若有则跳转到主界面,实现播放过程中切歌或退出。
- 形式参数:寄存器
SI
存储简谱中音高的偏移地址,使用寄存器BP
存储简谱中音长的偏移地址。
- 接受简谱的音高与音长,循环枚举简谱中所有音符并调用
函数调用关系图
程序流程图
运行结果测试
程序中的三首音乐为:
- 千本桜 - 黒うさP/初音ミク
- Merry Christmas Mr. Lawrence - 坂本龍一
- Bad Apple!! - のみこ/Alstroemeria Records
Bilibili:https://www.bilibili.com/video/BV1wx4y1r7nu/。
完整代码
见 Github 项目:https://github.com/Luckyblock233/Musicbox-based-on-MASM。
吐槽
\(\text{533H} \times 896 = \text{123280H} \not= \text{1234DCH}\)
每个音符的频率值 \(f\) 经过转换后送入定时器的
42H
端口,以产生相应频率 \(F\) 的脉冲。当定时器的计数值为533H
时能产生896 Hz
的声音,则转换的公式为:\[F = \frac{\text{533H} \times 896}{f}=\frac{\text{1234DCH}}{f} \]
中文互联网上有关该实验的报告中,在计算音符对应方波频率的计数值时无一例外地提到了上述结论。然而我用计算器算了八十万遍也只能得到 \(\text{533H} \times 896 = \text{123280H} \not= \text{1234DCH}\),到底怎么转换成右侧形式的?为什么非要转换成右侧形式?
于是去查询了 1234DCH
这个常量的信息,发现该常量即石英震荡器每秒的震荡频率的近似值 1193180HZ
,即芯片工作的主频,也即计时器的时钟输入。于是恍然大悟:\(\frac{\text{1234DCH}}{f}\) 即计算通过计数器的时钟输入每秒可以产生多少频率为 \(f\) 的方波信号,也即定时器的计数值。
既然等式右侧的转换是有实际意义的,那么问题来了,为什么大部分实验报告不直接指出 1234DCH
为时钟输入,而是选择引用定时器的计数值为 533H
时能产生 896 Hz
的声音这个莫名其妙的用结果反推过程近似求得时钟周期的结论?以及基于这个结论的莫名其妙的式子 \(\text{533H} \times 896 = \text{1234DCH}\) 又是从何而来?为什么大家都要提一嘴这东西?
于是跑去 Google 了一下,发现该式比较早的出处为 2004 年机械工业出版社出版的《微机接口技术 500 问 - 李恩林 陈斌生》中例 4-74 举例说明用 8253 如何产生乐曲的代码注释中,节选如下:
mov al, 0b6h ;置 8253/ 8254 方式寄存器的值
out 43h, al ;43h 为 8253/ 8254 方式寄存器地址
mov dx, 12h
mov ax, 533h * 896 ;当定时器的计数值为 533h 时, 能产生 896 Hz 的声音 533h * 896 = 1234dch
div di
据我个人猜测,此书大概在曾经计算机专业的授课中占有一定地位,于是该样例被选入了各大高校的实验中计算机原理实验,这个莫名奇妙的结论于是被原封不动地抄进了实验指导书。曾经学习计算机原理并被分配到做这个实验的先辈们也没有在意这个莫名奇妙的等式是否正确、从何而来,这个莫名奇妙的结论于是又被原封不动地抄进了实验报告里。
终于在 20 年后的今天——一个尝试搞清楚原理的大冤种用计算器验算了一下——发现这他妈有问题啊啊啊啊!!!
令人感慨!
有关音乐
妈的为什么做汇编实验需要乐理啊幸好我学过点。
已弃坑的:「学习笔记」乐理从入门到出门。高考完暑假想学编曲来着、、、后来发现大概没那么有热情于是弃坑了哈哈,菜。
为什么选了这三首音乐?
- 第一想法是找一首人尽皆知的二次元歌,第一反应就是千本樱,经典!
- 感觉 8bit 风不好找适合的音乐,受群友启发找了 Bad Apple。
- 提起 8bit 想到了这个:【电机】圣诞快乐,劳伦斯先生 - 坂本龙一 - 哔哩哔哩,很有感觉,于是选择了这首。
- 中途还尝试了 BA 终章的 Re Aoharu,然而没有钢琴直接没灵魂了,放弃!
参考
原理:
- 主要参考:基于汇编语言的音乐盒设计_音乐盒程序汇编-CSDN博客
- 微机原理课程设计乐曲演奏程序设计与实现 - 百度文库
- 汇编课设报告 字符分类统计和音乐盒.doc
- 基于汇编语言的音乐盒设计与实现_音乐盒汇编语言-CSDN博客
- 汇编语言中OUT和IN的用法_汇编语言out-CSDN博客
- x86汇编利用int 16h中断实现伪多线程输入 - scyq - 博客园
- 键盘I_O中断调用(INT 16H)-CSDN博客
- 键盘I_O中断调用(INT 16H)和常见的int 17H、int 1A H_int13中断的17h功能用法-CSDN博客
- 微机原理 __ 8253 芯片 (详细讲解 + 经典例题)-CSDN博客
- 8253练习题(8253端口地址怎么求?怎么求初值?怎么看出工作方式)-CSDN博客
- 微机接口芯片(1)—— 可编程并行接口芯片8255_8255芯片-CSDN博客
- 关于8253 芯片计数器初值的问题_8253用小时频率输出初值是多少-CSDN博客
- EE314 - labs - labmanual - EXPERIMENT FIFTEEN INTERRUPT HOOKS
- Dos系统功能调用表9号功能_dos9号-CSDN博客
- INT 21H 指令说明及使用方法(汇编语言学习)_int 21h指令-CSDN博客
- 汇编语言(第3版,王爽著):第 17 章 使用 BIOS 进行键盘输入的学习总结 - 夏夜星空晚风 - 博客园
简谱: