开更

大概最后做了一个能播放无损音乐(无压缩、不需解码)的播放器

原理是基于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 sb16ISR
dw _CODE
blockmask dw
_DT ends _CODE segment
assume cs:_CODE, ds:_DT, es:_DT swappointers:
; swap si and di pointer
push bx
mov bx, word ptr [si]
xchg word ptr es:[di], bx
mov word ptr [si], bx mov bx, word ptr [si+]
xchg word ptr es:[di+], bx
mov word ptr [si+], bx pop bx
ret installISR:
push es
push si
push di
push dx
push ax cli ; clear int mov si, offset sbISR
sub di, di
mov es, di
mov di, ISR_VECTOR
call swappointers sti ; set int ; set mask
mov dx, PIC_DATA
in al, dx
xor al, PIC_MASK
out dx, al pop ax
pop dx
pop di
pop si
pop es
ret sb16ISR:
push ax
push dx
push ds
push es ;Acknowledge the interrupt with the SB by reading from port 2xF for 16-bit sound.
mov dx, REG_DSP_ACK_16
in al, dx ;Acknowledge the end of interrupt with the PIC by writing 20h to port 20h.
mov al, 20h
out 20h, al ;maintain buffer
mov ax, _DT
mov ds, ax
mov bx, word ptr [BlockMask]
call maintainbuffer not bx
mov word ptr [BlockMask], bx pop es
pop ds
pop dx
pop ax
iret
_CODE ends

四、编写DMA,载入音频流

  DMA是什么呢?引用百科里的解释,DMA(Direct Memory Access,直接内存存取) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于CPU 的大量中断负载。

  为什么要编写DMA,实际上是因为sound blaster 是ISA(外部硬件),CPU是不能直接向其端口输入值的,需要DMA进行中介,也就是说

  CPU向DMA输送音频流,sound blaster从DMA获取音频流来播放,设置DMA,一是设置它的模式,二是设置它的端口对应,通知sound blaster这一段内存地址存放DMA的数据信息

  文档说明如下

  

  那么代码如下

.
_CODE segment
assume cs:_CODE
setDMA:
push ax
push bx
push cx
push dx
push si ;重新设置,禁用通道
mov dx, REG_DMA_MASK
mov al, + SB16_HDMA MOD
out dx, al ;清零操作
mov dx, REG_DMA_CLEAR_FF
out dx, al ;重新设置模式
mov dx, REG_DMA_MODE
mov al, 58h + SB16_HDMA MOD
out dx, al ;设置DMA地址
;ES = buffer segment
;SI = buffer offset
;DI = block size
mov bx, es
shr bx,
mov cx, es
shl cx,
shr si,
add cx, si
adc bx, ;输出地址
mov dx, REG_DMA_ADDRESS
mov al, cl
out dx, al
mov al, ch
out dx, al mov dx, REG_DMA_PAGE
mov al, bl
out dx, al ;设置size
mov ax, di
shr ax,
mov dx, REG_DMA_COUNT
out dx, al
mov al, ah
out dx, al ;启用频道
mov dx, REG_DMA_MASK
mov al, SB16_HDMA mod
out dx, al pop si
pop dx
pop cx
pop bx
pop ax
ret
_CODE ends

六、编写data,用于音频读入

  这个文档里没有提到,但也是必须要写的。大体工作就是从文件中读入信息,放入到buffer里,然后更新DMA

  没有什么额外的地方,代码如下

_DT segment para public 'DATA'
myfile db 'mymusic.wav',
filehandle dw
samplerate dw
_DT ends _CODE segment
assume cs:_CODE, ds:_DT, es:_DT
maintainbuffer:
push es
push di
push bx
push ax
push si
push cx
push dx mov di, word ptr [buffersegment]
mov es, di
mov di, BLOCK_SIZE
and di, bx
add di, word ptr [bufferoffset] push ds
;从文件中读入一段音频流
mov ax, es
mov ds, ax
mov dx, di
mov ah, 3fh
mov bx, word ptr [filehandle]
mov cx, BLOCK_SIZE
int 21h pop ds cmp ax, BLOCK_SIZE
je mydateret ;循环播放
mov ax, 4200h
mov bx, word ptr [filehandle]
sub cx, cx
sub dx, dx
int 21h mydateret:
pop dx
pop cx
pop si
pop ax
pop bx
pop di
pop es
ret initbuffer:
push ax
push bx
push cx
push dx
;打开文件
mov ax, 3d00h
mov dx, offset myfile
int 21h mov word ptr [filehandle], ax mov bx, ax
mov ax, 4200h
sub cx, cx
sub dx, dx
int 21h mov ah, 3fh
mov bx, WORD PTR [fileHandle]
mov cx,
mov dx, OFFSET sampleRate
int 21h mov ax, 4200h
mov bx, WORD PTR [fileHandle]
xor cx, cx
sub dx, dx
int 21h pop dx
pop cx
pop bx
pop ax
ret _CODE ends

七、编写DSP

  DSP就是数字信号处理器,用于数字信号处理,cpu向它发出信号,就可以借助它向声卡做一些简单的指令操作。

  DSP的相关端口信息文档里也有说,对DSP的读写操作如下所述

  

  还有一些对DSP的指令来控制声卡模式,这些文档里都有,我就不再粘贴了

  代码如下

    FORMAT_MONO     EQU 00h
FORMAT_STEREO EQU 20h
FORMAT_SIGNED EQU 10h
FORMAT_UNSIGNED EQU 00h _CODE segment
assume cs:_CODE
resetDSP:
push ax
push dx ;设置DSP
mov dx, REG_DSP_RESET
mov al, 01h
out dx, al
sub al, al
out dx, al mov dx, REG_DSP_READ_BS
;等待sb16响应
DSPwait1:
in al, dx
test al, 80h
jz DSPwait1 mov dx, REG_DSP_READ
DSPwait2:
in al, dx
cmp al, 0aah
jne DSPwait2 pop dx
pop ax
ret writeDSP:
push dx
push ax mov dx, REG_DSP_WRITE_BS
DSPwait3:
in al, dx
test al, 80h
jz DSPwait3
pop ax mov dx, REG_DSP_WRITE_DATA
out dx, al pop dx
ret readDSP:
push dx
mov dx, REG_DSP_READ_BS
dspwait4:
in al, dx
test al, 80h
jz dspwait4
pop ax
mov dx, REG_DSP_READ
in al, dx
pop dx
ret setsample:
push dx
xchg al, ah
push ax
mov al, DSP_SET_SAMPLING_OUTPUT
call writeDSP
pop ax
call writeDSP
mov al, ah
call writeDSP
pop dx
ret ;AX = Sampling
;BL = Mode
;CX = Size
startplay:
call setsample mov al, 00b6h
call writeDSP
mov al, bl
call writeDSP
mov al, cl
call writeDSP
mov al, ch
call writeDSP ret pauseplay:
push ax
mov al, 00d5H
call WriteDSP
pop ax
ret continueplay:
push ax
mov al, 00d6H
call WriteDSP
pop ax
ret _CODE ends

八、流的设置,常见端口的配置

  这些都是一些常量配置,就不在多叙述了,具体端口位置文档里也有提到

    BLOCK_SIZE EQU
BUFFER_SIZE EQU
assume ds:_DT, es:_DT
_DT segment para public 'DATA' buffer db BUFFER_SIZE DUP()
bufferoffset db offset buffer
buffersegment 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 ;IRQ Number
SB16_IRQ EQU ;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 +
REG_DSP_READ EQU SB16_BASE + 0ah
REG_DSP_WRITE_BS EQU SB16_BASE + 0ch
REG_DSP_WRITE_CMD EQU SB16_BASE + 0ch
REG_DSP_WRITE_DATA EQU SB16_BASE + 0ch
REG_DSP_READ_BS EQU SB16_BASE + 0eh
REG_DSP_ACK EQU SB16_BASE + 0eh
REG_DSP_ACK_16 EQU SB16_BASE + 0fh ;DSP COMMANDS DSP_SET_SAMPLING_OUTPUT EQU 41h
DSP_DMA_16_OUTPUT_AUTO EQU 0b6h
DSP_STOP_DMA_16 EQU 0d5h ;DMA REGISTERS REG_DMA_ADDRESS EQU 0c0h + (SB16_HDMA - ) *
REG_DMA_COUNT EQU REG_DMA_ADDRESS + 02h REG_DMA_MASK EQU 0d4h
REG_DMA_MODE EQU 0d6h
REG_DMA_CLEAR_FF EQU 0d8h IF SB16_HDMA -
REG_DMA_PAGE EQU 8bh
ELSE
IF SB16_HDMA -
REG_DMA_PAGE EQU 89h
ELSE
REG_DMA_PAGE EQU 8ah
ENDIF
ENDIF ;ISR vector
ISR_VECTOR EQU ((SB16_IRQ SHR ) * (70h - 08h) + (SB16_IRQ AND ) + 08h) * PIC_DATA EQU (SB16_IRQ AND ) + 21h
PIC_MASK EQU SHL (SB16_IRQ AND )

九、主程序编写

  在各个部分都完成以后,主程序就比较好写了

  按照步骤顺序来就可以

  不要忘了最后把ISR再交换回来,让dos系统能正常运行

INCLUDE cfg.asm
INCLUDE mybuffer.asm
INCLUDE mydata.asm
INCLUDE myisr.asm
INCLUDE mydsp.asm
INCLUDE mydma.asm _CODE segment
assume cs:_CODE
start:
mov ax, _DT
mov ds, ax
call installISR
call initbuffer
mov si, word ptr [buffersegment]
mov es, si
mov si, word ptr [bufferoffset]
mov di, BLOCK_SIZE *
call setDMA
call resetDSP
mov ax, word ptr [samplerate]
mov bx, FORMAT_MONO or FORMAT_SIGNED
mov cx, BLOCK_SIZE
call startplay
mov ah,
int 16h
call pauseplay
call installISR
mov ah, 4ch
int 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 大概就是这样了,如果有什么问题,欢迎指出

8086汇编语言 调用声卡播放wav文件(sound blaster)的更多相关文章

  1. C#播放wav文件

    C#使用HWQPlayer类播放wav文件 类的代码: using System.IO; using System.Runtime.InteropServices; namespace HoverTr ...

  2. python 播放 wav 文件

    未使用其他库, 只是使用 pywin32 调用系统底层 API 播放 wav 文件. # Our raison d'etre - playing sounds import pywintypes im ...

  3. 如何播放 WAV 文件?

    from http://www.vckbase.com/index.php/wv/434 平时,你在多媒体软件的设计中是怎样处理声音文件的呢?使用Windows 提供的API函数 sndPlaySou ...

  4. WinAPI: sndPlaySound - 播放 wav 文件

    WinAPI: sndPlaySound - 播放 wav 文件 //声明: sndPlaySound(   lpszSoundName: PChar; {声音文件}   uFlags: UINT{播 ...

  5. C#调用mciSendString播放音频文件

    mciSendString函数是一个WinAPI,主要用来向MCI(Media Control Interface)设备发送字符串命令. 一.函数的声明如下: private static exter ...

  6. Linux音频编程--使用ALSA库播放wav文件

    在UBUNTU系统上使用alsa库完成了对外播放的wav文件的案例. 案例代码: /** *test.c * *注意:这个例子在Ubuntu 12.04.1环境下编译运行成功. * */ #inclu ...

  7. c++(qt)播放wav文件的四种方式

    //方法一(要符合RIFF规范) 1 QSound::play("E:/Projects/报警声1-1.wav"); //方法二(要符合RIFF规范) 1 QSoundEffect ...

  8. windows下使用waveout函数族播放wav文件

    要使用waveout函数组,族,首先要知道几个数据结构,首先是这个 typedef struct tWAVEFORMATEX { WORD wFormatTag; /* 格式的类型 */ WORD n ...

  9. java播放wav文件

    import java.io.File; import java.io.IOException; import javax.sound.sampled.AudioFormat; import java ...

随机推荐

  1. linux总结及常用命令

    一.操作系统的作用: 1.是现代计算机系统中最基本和最重要的系统软件  2.承上启下的作用  3.向下对硬件操作进行封装  4.向上对用户和应用程序提供方便访问硬件的接口 二.不同领域的操作系统: 1 ...

  2. I/O流、文件操作

    1)操作文件 Path和Files是在JavaSE7中新添加进来的类,它们封装了在用户机器上处理文件系统所需的所有功能.Path表示的一个目录名序列,其后还可以跟着一个文件名.路径中的第一个参数可以是 ...

  3. 洛谷 U45568 赌神:决斗

    题目描述 \mathcal{tomoo}tomoo决定与\mathcal{CYJian}CYJian进行决斗! 已知\mathcal{tomoo}tomoo有\mathcal{N}N张扑克牌,每张扑克 ...

  4. Delphi方法

    unit Unit1; interface uses   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, For ...

  5. python3 练习题100例 (二十六)回文数判断

    题目内容: 给一个5位数,判断它是不是回文数,是则输出yes,不是则输出no. 例如12321是回文数,它的个位与万位相同,十位与千位相同. 输入格式: 共一行,为一个5位数. 输出格式: 共一行,y ...

  6. 2019js面试题前端必问点小视频

    其实市面上的面试题有很多,但是大部分都是总结的blog居多,有时候说明一个事物也许口述几分钟就可以搞定,但是看帖子可能要分析半天 所以我就出一部分前端js必考的小视频,不管我们什么时候面试基本都绕不过 ...

  7. Delphi中客户端获取数据库更新信息(更新条数)

    1.SQL语句 from tb where xxx='XXX') //不存在,则插入数据 begin insert into tb(xxx) values('XXX') //这里自己定义,插入或更新都 ...

  8. Android开发——告诉你Adapter应该写在Activity里面还是外面

    0. 前言 本文转载自AItsuki的博客. 首先说明一下为什么要写这么一篇博客:最近看了一些其他人的项目,发现很多项目的做法是建立一个专门存放Adapter类的Package包,也有的项目干脆直接都 ...

  9. bootstrap重新设计checkbox样式

    文章采集于: https://www.cnblogs.com/GumpYan/p/7845445.html#undefined 在原文基础上修改了勾勾的内容,直接采用bootstrap字体库.修改了横 ...

  10. LeetCode:17. Letter Combinations of a Phone Number(Medium)

    1. 原题链接 https://leetcode.com/problems/letter-combinations-of-a-phone-number/description/ 2. 题目要求 给定一 ...