8086汇编语言 调用声卡播放wav文件(sound blaster)
开更
大概最后做了一个能播放无损音乐(无压缩、不需解码)的播放器
原理是基于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)的更多相关文章
- C#播放wav文件
C#使用HWQPlayer类播放wav文件 类的代码: using System.IO; using System.Runtime.InteropServices; namespace HoverTr ...
- python 播放 wav 文件
未使用其他库, 只是使用 pywin32 调用系统底层 API 播放 wav 文件. # Our raison d'etre - playing sounds import pywintypes im ...
- 如何播放 WAV 文件?
from http://www.vckbase.com/index.php/wv/434 平时,你在多媒体软件的设计中是怎样处理声音文件的呢?使用Windows 提供的API函数 sndPlaySou ...
- WinAPI: sndPlaySound - 播放 wav 文件
WinAPI: sndPlaySound - 播放 wav 文件 //声明: sndPlaySound( lpszSoundName: PChar; {声音文件} uFlags: UINT{播 ...
- C#调用mciSendString播放音频文件
mciSendString函数是一个WinAPI,主要用来向MCI(Media Control Interface)设备发送字符串命令. 一.函数的声明如下: private static exter ...
- Linux音频编程--使用ALSA库播放wav文件
在UBUNTU系统上使用alsa库完成了对外播放的wav文件的案例. 案例代码: /** *test.c * *注意:这个例子在Ubuntu 12.04.1环境下编译运行成功. * */ #inclu ...
- c++(qt)播放wav文件的四种方式
//方法一(要符合RIFF规范) 1 QSound::play("E:/Projects/报警声1-1.wav"); //方法二(要符合RIFF规范) 1 QSoundEffect ...
- windows下使用waveout函数族播放wav文件
要使用waveout函数组,族,首先要知道几个数据结构,首先是这个 typedef struct tWAVEFORMATEX { WORD wFormatTag; /* 格式的类型 */ WORD n ...
- java播放wav文件
import java.io.File; import java.io.IOException; import javax.sound.sampled.AudioFormat; import java ...
随机推荐
- 五、RegExp(正则表达式)篇
正则表达式,只用记住: 0./pattern/igm i--不区分大小写 g--找到所有相匹配的 m--多行匹配 可以只写其中一个 ps:/pattern/i (无视大小写) 1." ...
- python查询mysql数据
>>>cur.execute("select * from 表名") >>>lines=cur.fetchall() >>>f ...
- Flask初见
Flask是一个使用 Python 编写的轻量级 Web 应用框架.其 WSIG工具箱采用 Werkzeug ,模板引擎则使用 Jinja2 .Flask使用 BSD 授权. Flask也被称为 “m ...
- docker和docker compose常用操作命令
首先区分一下docker中几个概念 Image:镜像,相当于一个root文件系统,不包含任何动态数据 Container:容器,镜像运行时的实体,实质是进程,容器进程运行于属于自己的独立的命名空间 d ...
- ESP32 LyraT音频开发板试玩(二):播放音乐
我是卓波,很高兴你来看我的博客. 系列文章: ESP32 LyraT音频开发板试玩(一):搭建开发环境 ESP32 LyraT音频开发板试玩(二):播放音乐 本文延续上一篇博客 将D:\msys32\ ...
- Kubernetes-DNS
Kubernetes提供的虚拟DNS服务名为skydns,由四个组件组成: etcd:DNS存储 kube2sky:将Kubernetes Master中的Service(服务)注册到etcd sky ...
- (数据科学学习手札32)Python中re模块的详细介绍
一.简介 关于正则表达式,我在前一篇(数据科学学习手札31)中已经做了详细介绍,本篇将对Python中自带模块re的常用功能进行总结: re作为Python中专为正则表达式相关功能做出支持的模块,提供 ...
- XML文件中关键字自动提示和不全配置
一.获得mybatis-3-config.dtd.mybatis-3-mapper.dtd 这两个文件. 建立一个Maven的项目 在Pom.xml文件中的Mybatis jar包的下载设置(也可以从 ...
- DDR分析与布线要求
基本知识 Double Data Rate Synchronous Dynamic Random Access Memory 简称 DDR SDRAM 双倍数据率同步动态随机存取内存 DDR SDRA ...
- 第四模块:网络编程进阶&数据库开发 考核实战
1.什么是进程?什么是线程? 什么是协程? 进程:正在进行的一个过程或者说一个任务.而负责执行任务则是cpu. 线程:在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程 协程是一种用 ...