开更
大概最后做了一个能播放无损音乐(无压缩、不需解码)的播放器
原理是基于dosbox的模拟声卡,通过硬件之间的相互通讯做到的
关于详细内容接下来再讲。
一、从dosbox入手
我们知道cpu可以直接输出到蜂鸣器的端口,然后让蜂鸣器发声。但是蜂鸣器的局限性很大,大多数蜂鸣器只支持两种电压,也就只能发出非常单一的声音。所以,从播放音乐角度来讲,调用蜂鸣器是比较简单但局限性很大的。所以这里不会采用调用蜂鸣器的做法。
要用8086发出复杂的声音,最简单的想法就是调用声卡,但在dos环境下,想调用windows的声卡是不可能的,一是windows的声卡驱动不兼容,第二是也没有提供可用的输出方式(驱动封装性好)。于是我就查阅了dosbox的sound方面的资料,发现了dosbox是支持模拟声卡的,最简单的就是PC speaker(蜂鸣器),还有disney声卡、midi声卡等等,不过在96年最普及的一款声卡是sound blaster 16。它同样也可以被dosbox模拟,查阅dosbox的document,我们会找到dosbox的模拟端口位置
有了这个,我们就只需要查阅sound blaster的document,就可以知道如何使用sound blaster了
二、Sound Blaster 简要说明
sound blaster的document网址:http://homepages.cae.wisc.edu/~brodskye/sb16doc/sb16doc.html
非常推荐先读一遍这篇document
这篇文章介绍了sound blaster每个端口的作用和位置,以及如何配置sound blaster。
1、安装sound blaster中断
2、编写DMA,用于音频流载入
3、设定一个采样的速率
4、编写DSP的读写I/O操作
5、向DSP写入转换模式操作(转换到sound blaster模式)
6、向DSP写入音频流的大小和播放设定
那么整体的一个流程其实是这样
向DSP写入转换模式操作(转换到soundblaster)->利用DSP向声卡发出播放命令->声卡发现DMA中没有数据,引发中断->中断更新DMA中的数据->声卡获取数据开始播放
在这个过程中,一旦声卡没有数据了,就会引发中断获取新的数据,实现播放音乐的功能。
下面分步说明
三、替换ISR(就是安装新的中断)
这里说法可能有些跳跃,ISR实际上是PIC的一部分
关于PIC的一些知识,可以看链接 http://wiki.osdev.org/8259_PIC
实际上它有15条IRQ lines,对应的就是15个中断,我们需要替换其中的一个中断,作为声卡的中断
那么这样的话,自己要编写新的中断,内容要包括(文档里有写)
我的实现里没有double-buffering操作,所以就不需要复制了(代价就是块的size大的时候,音乐会有明显的跳跃间断)
我们想要播放16位单声道音乐,所以要向2xF口读写信息
这里我配置里是放到IRQ0~7里的,所以向20h写20h即可。
代码如下
_DT segment para public 'DATA'sbISR dw offset sb16ISRdw _CODEblockmask dw 0 _DT ends_CODE segmentassume cs:_CODE, ds:_DT, es:_DTswappointers:; swap si and di pointerpush bxmov bx, word ptr [si]xchg word ptr es:[di], bxmov word ptr [si], bxmov bx, word ptr [si+2]xchg word ptr es:[di+2], bxmov word ptr [si+2], bxpop bxretinstallISR:push espush sipush dipush dxpush axcli ; clear intmov si, offset sbISRsub di, dimov es, dimov di, ISR_VECTORcall swappointerssti ; set int; set maskmov dx, PIC_DATAin al, dxxor al, PIC_MASKout dx, alpop axpop dxpop dipop sipop esretsb16ISR:push axpush dxpush dspush es;Acknowledge the interrupt with the SB by reading from port 2xF for 16-bit sound.mov dx, REG_DSP_ACK_16in al, dx;Acknowledge the end of interrupt with the PIC by writing 20h to port 20h.mov al, 20hout 20h, al;maintain buffermov ax, _DTmov ds, axmov bx, word ptr [BlockMask]call maintainbuffernot bxmov word ptr [BlockMask], bxpop espop dspop dxpop axiret _CODE ends
四、编写DMA,载入音频流
DMA是什么呢?引用百科里的解释,DMA(Direct Memory Access,直接内存存取) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于CPU 的大量中断负载。
为什么要编写DMA,实际上是因为sound blaster 是ISA(外部硬件),CPU是不能直接向其端口输入值的,需要DMA进行中介,也就是说
CPU向DMA输送音频流,sound blaster从DMA获取音频流来播放,设置DMA,一是设置它的模式,二是设置它的端口对应,通知sound blaster这一段内存地址存放DMA的数据信息
文档说明如下
那么代码如下
.286 _CODE segmentassume cs:_CODEsetDMA:push axpush bxpush cxpush dxpush si;重新设置,禁用通道mov dx, REG_DMA_MASKmov al, 4 + SB16_HDMA MOD 4out dx, al;清零操作mov dx, REG_DMA_CLEAR_FFout dx, al;重新设置模式mov dx, REG_DMA_MODEmov al, 58h + SB16_HDMA MOD 4out dx, al;设置DMA地址;ES = buffer segment;SI = buffer offset;DI = block sizemov bx, esshr bx, 13mov cx, esshl cx, 3shr si, 1add cx, siadc bx, 0;输出地址mov dx, REG_DMA_ADDRESSmov al, clout dx, almov al, chout dx, almov dx, REG_DMA_PAGEmov al, blout dx, al;设置sizemov ax, dishr ax, 1mov dx, REG_DMA_COUNTout dx, almov al, ahout dx, al;启用频道mov dx, REG_DMA_MASKmov al, SB16_HDMA mod 4out dx, alpop sipop dxpop cxpop bxpop axret _CODE ends
六、编写data,用于音频读入
这个文档里没有提到,但也是必须要写的。大体工作就是从文件中读入信息,放入到buffer里,然后更新DMA
没有什么额外的地方,代码如下
_DT segment para public 'DATA'myfile db 'mymusic.wav', 0filehandle dw 0samplerate dw 0 _DT ends_CODE segmentassume cs:_CODE, ds:_DT, es:_DTmaintainbuffer:push espush dipush bxpush axpush sipush cxpush dxmov di, word ptr [buffersegment]mov es, dimov di, BLOCK_SIZEand di, bxadd di, word ptr [bufferoffset]push ds;从文件中读入一段音频流mov ax, esmov ds, axmov dx, dimov ah, 3fhmov bx, word ptr [filehandle]mov cx, BLOCK_SIZEint 21hpop dscmp ax, BLOCK_SIZEje mydateret;循环播放mov ax, 4200hmov bx, word ptr [filehandle]sub cx, cxsub dx, dxint 21hmydateret:pop dxpop cxpop sipop axpop bxpop dipop esretinitbuffer:push axpush bxpush cxpush dx;打开文件mov ax, 3d00hmov dx, offset myfileint 21hmov word ptr [filehandle], axmov bx, axmov ax, 4200hsub cx, cxsub dx, dxint 21hmov ah, 3fhmov bx, WORD PTR [fileHandle]mov cx, 12mov dx, OFFSET sampleRateint 21hmov ax, 4200hmov bx, WORD PTR [fileHandle]xor cx, cxsub dx, dxint 21hpop dxpop cxpop bxpop axret_CODE ends
七、编写DSP
DSP就是数字信号处理器,用于数字信号处理,cpu向它发出信号,就可以借助它向声卡做一些简单的指令操作。
DSP的相关端口信息文档里也有说,对DSP的读写操作如下所述
还有一些对DSP的指令来控制声卡模式,这些文档里都有,我就不再粘贴了
代码如下
FORMAT_MONO EQU 00hFORMAT_STEREO EQU 20hFORMAT_SIGNED EQU 10hFORMAT_UNSIGNED EQU 00h_CODE segmentassume cs:_CODEresetDSP:push axpush dx;设置DSPmov dx, REG_DSP_RESETmov al, 01hout dx, alsub al, alout dx, almov dx, REG_DSP_READ_BS;等待sb16响应DSPwait1:in al, dxtest al, 80hjz DSPwait1mov dx, REG_DSP_READDSPwait2:in al, dxcmp al, 0aahjne DSPwait2pop dxpop axretwriteDSP:push dxpush axmov dx, REG_DSP_WRITE_BSDSPwait3:in al, dxtest al, 80hjz DSPwait3pop axmov dx, REG_DSP_WRITE_DATAout dx, alpop dxretreadDSP:push dxmov dx, REG_DSP_READ_BSdspwait4:in al, dxtest al, 80hjz dspwait4pop axmov dx, REG_DSP_READin al, dxpop dxretsetsample:push dxxchg al, ahpush axmov al, DSP_SET_SAMPLING_OUTPUTcall writeDSPpop axcall writeDSPmov al, ahcall writeDSPpop dxret;AX = Sampling;BL = Mode;CX = Sizestartplay:call setsamplemov al, 00b6hcall writeDSPmov al, blcall writeDSPmov al, clcall writeDSPmov al, chcall writeDSPretpauseplay:push axmov al, 00d5Hcall WriteDSPpop axretcontinueplay:push axmov al, 00d6Hcall WriteDSPpop axret_CODE ends
八、流的设置,常见端口的配置
这些都是一些常量配置,就不在多叙述了,具体端口位置文档里也有提到
BLOCK_SIZE EQU 1024BUFFER_SIZE EQU 1024 assume ds:_DT, es:_DT _DT segment para public 'DATA'buffer db BUFFER_SIZE DUP(0)bufferoffset db offset bufferbuffersegment dw _DT _DT ends
;These are the only configurable constants;IO Base SB16_BASE EQU 220h;16-bit DMA channel (must be between 5-7)SB16_HDMA EQU 5;IRQ NumberSB16_IRQ EQU 7;These a computed values, don't touch them if you don't know what;you are doing;REGISTER NAMES REG_DSP_RESET EQU SB16_BASE + 6REG_DSP_READ EQU SB16_BASE + 0ahREG_DSP_WRITE_BS EQU SB16_BASE + 0chREG_DSP_WRITE_CMD EQU SB16_BASE + 0chREG_DSP_WRITE_DATA EQU SB16_BASE + 0chREG_DSP_READ_BS EQU SB16_BASE + 0ehREG_DSP_ACK EQU SB16_BASE + 0ehREG_DSP_ACK_16 EQU SB16_BASE + 0fh;DSP COMMANDS DSP_SET_SAMPLING_OUTPUT EQU 41hDSP_DMA_16_OUTPUT_AUTO EQU 0b6hDSP_STOP_DMA_16 EQU 0d5h;DMA REGISTERS REG_DMA_ADDRESS EQU 0c0h + (SB16_HDMA - 4) * 4REG_DMA_COUNT EQU REG_DMA_ADDRESS + 02hREG_DMA_MASK EQU 0d4hREG_DMA_MODE EQU 0d6hREG_DMA_CLEAR_FF EQU 0d8hIF SB16_HDMA - 5REG_DMA_PAGE EQU 8bh ELSEIF SB16_HDMA - 6REG_DMA_PAGE EQU 89hELSEREG_DMA_PAGE EQU 8ahENDIFENDIF;ISR vectorISR_VECTOR EQU ((SB16_IRQ SHR 3) * (70h - 08h) + (SB16_IRQ AND 7) + 08h) * 4PIC_DATA EQU (SB16_IRQ AND 8) + 21hPIC_MASK EQU 1 SHL (SB16_IRQ AND 7)
九、主程序编写
在各个部分都完成以后,主程序就比较好写了
按照步骤顺序来就可以
不要忘了最后把ISR再交换回来,让dos系统能正常运行
INCLUDE cfg.asm INCLUDE mybuffer.asm INCLUDE mydata.asm INCLUDE myisr.asm INCLUDE mydsp.asm INCLUDE mydma.asm_CODE segmentassume cs:_CODE start:mov ax, _DTmov ds, axcall installISRcall initbuffermov si, word ptr [buffersegment]mov es, simov si, word ptr [bufferoffset]mov di, BLOCK_SIZE * 2call setDMAcall resetDSPmov ax, word ptr [samplerate]mov bx, FORMAT_MONO or FORMAT_SIGNEDmov cx, BLOCK_SIZEcall startplaymov ah, 0int 16hcall pauseplaycall installISRmov ah, 4chint 21h _CODE ends end start
十、后记
最后总算是完成了,幸运的是期间的debug比较顺利,感觉这个过程学到了很多。
在查阅资料时,主要看了stackoverflow的有关问题,慢慢有所启发,去查阅dosbox的模拟声卡,最后发现了sound blaster这款声卡,查阅了很多文档
也参考了github上的开源项目 https://github.com/margaretbloom/sb16-wav
非常感谢这个项目对我的启发
不过这个项目还是存在问题的,它并没有实现double-buffering(可能只是我不会调用吧)
关于音频格式的问题,以及为什么它可以播放wav格式,这里就简单说明一下
wav是一种无损的格式,它包括一个头段和数据段,头段包含了对wav的说明
而最关键的数据段,实际上是没有经过任何压缩的,也就是完整记录了每个声道的频率
所以你把这些信息直接传递给声卡就是可以播放的
你也可以把头段信息删除(这样普通播放器就无法播放了)。直接给这个程序,也是可以播放的
然后还有一点是,其实它是16位单声道的播放模式,所以说如果你给它一个32位的wav,或者是双声道的wav,它就会变成1/2速度播放
这个道理也很显然,就是它把32位当成2个16位,当然就会变成1/2速度了
更重要的一点是,它的播放是有频率的,由于我们是直接往DMA里面输入,所以播放频率就是输入的速率,大概是10000HZ左右,所以只要转换音乐到这个频率附近,就可以正常播放了,如果太小或太大,则播放速率会有明显的加快或变慢的特点。
关于编译环境,在dosbox环境下,我这边tasm和masm都可以编译和链接成功和正常运行
orz 大概就是这样了,如果有什么问题,欢迎指出