开更

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

原理是基于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即可。

  代码如下

  1. _DT segment para public 'DATA'
  2. sbISR dw offset sb16ISR
  3. dw _CODE
  4. blockmask dw
  5. _DT ends
  6.  
  7. _CODE segment
  8. assume cs:_CODE, ds:_DT, es:_DT
  9.  
  10. swappointers:
  11. ; swap si and di pointer
  12. push bx
  13. mov bx, word ptr [si]
  14. xchg word ptr es:[di], bx
  15. mov word ptr [si], bx
  16.  
  17. mov bx, word ptr [si+]
  18. xchg word ptr es:[di+], bx
  19. mov word ptr [si+], bx
  20.  
  21. pop bx
  22. ret
  23.  
  24. installISR:
  25. push es
  26. push si
  27. push di
  28. push dx
  29. push ax
  30.  
  31. cli ; clear int
  32.  
  33. mov si, offset sbISR
  34. sub di, di
  35. mov es, di
  36. mov di, ISR_VECTOR
  37. call swappointers
  38.  
  39. sti ; set int
  40.  
  41. ; set mask
  42. mov dx, PIC_DATA
  43. in al, dx
  44. xor al, PIC_MASK
  45. out dx, al
  46.  
  47. pop ax
  48. pop dx
  49. pop di
  50. pop si
  51. pop es
  52. ret
  53.  
  54. sb16ISR:
  55. push ax
  56. push dx
  57. push ds
  58. push es
  59.  
  60. ;Acknowledge the interrupt with the SB by reading from port 2xF for 16-bit sound.
  61. mov dx, REG_DSP_ACK_16
  62. in al, dx
  63.  
  64. ;Acknowledge the end of interrupt with the PIC by writing 20h to port 20h.
  65. mov al, 20h
  66. out 20h, al
  67.  
  68. ;maintain buffer
  69. mov ax, _DT
  70. mov ds, ax
  71. mov bx, word ptr [BlockMask]
  72. call maintainbuffer
  73.  
  74. not bx
  75. mov word ptr [BlockMask], bx
  76.  
  77. pop es
  78. pop ds
  79. pop dx
  80. pop ax
  81. iret
  82. _CODE ends

四、编写DMA,载入音频流

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

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

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

  文档说明如下

  

  那么代码如下

  1. .
  2. _CODE segment
  3. assume cs:_CODE
  4. setDMA:
  5. push ax
  6. push bx
  7. push cx
  8. push dx
  9. push si
  10.  
  11. ;重新设置,禁用通道
  12. mov dx, REG_DMA_MASK
  13. mov al, + SB16_HDMA MOD
  14. out dx, al
  15.  
  16. ;清零操作
  17. mov dx, REG_DMA_CLEAR_FF
  18. out dx, al
  19.  
  20. ;重新设置模式
  21. mov dx, REG_DMA_MODE
  22. mov al, 58h + SB16_HDMA MOD
  23. out dx, al
  24.  
  25. ;设置DMA地址
  26. ;ES = buffer segment
  27. ;SI = buffer offset
  28. ;DI = block size
  29. mov bx, es
  30. shr bx,
  31. mov cx, es
  32. shl cx,
  33. shr si,
  34. add cx, si
  35. adc bx,
  36.  
  37. ;输出地址
  38. mov dx, REG_DMA_ADDRESS
  39. mov al, cl
  40. out dx, al
  41. mov al, ch
  42. out dx, al
  43.  
  44. mov dx, REG_DMA_PAGE
  45. mov al, bl
  46. out dx, al
  47.  
  48. ;设置size
  49. mov ax, di
  50. shr ax,
  51. mov dx, REG_DMA_COUNT
  52. out dx, al
  53. mov al, ah
  54. out dx, al
  55.  
  56. ;启用频道
  57. mov dx, REG_DMA_MASK
  58. mov al, SB16_HDMA mod
  59. out dx, al
  60.  
  61. pop si
  62. pop dx
  63. pop cx
  64. pop bx
  65. pop ax
  66. ret
  67. _CODE ends

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

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

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

  1. _DT segment para public 'DATA'
  2. myfile db 'mymusic.wav',
  3. filehandle dw
  4. samplerate dw
  5. _DT ends
  6.  
  7. _CODE segment
  8. assume cs:_CODE, ds:_DT, es:_DT
  9. maintainbuffer:
  10. push es
  11. push di
  12. push bx
  13. push ax
  14. push si
  15. push cx
  16. push dx
  17.  
  18. mov di, word ptr [buffersegment]
  19. mov es, di
  20. mov di, BLOCK_SIZE
  21. and di, bx
  22. add di, word ptr [bufferoffset]
  23.  
  24. push ds
  25. ;从文件中读入一段音频流
  26. mov ax, es
  27. mov ds, ax
  28. mov dx, di
  29. mov ah, 3fh
  30. mov bx, word ptr [filehandle]
  31. mov cx, BLOCK_SIZE
  32. int 21h
  33.  
  34. pop ds
  35.  
  36. cmp ax, BLOCK_SIZE
  37. je mydateret
  38.  
  39. ;循环播放
  40. mov ax, 4200h
  41. mov bx, word ptr [filehandle]
  42. sub cx, cx
  43. sub dx, dx
  44. int 21h
  45.  
  46. mydateret:
  47. pop dx
  48. pop cx
  49. pop si
  50. pop ax
  51. pop bx
  52. pop di
  53. pop es
  54. ret
  55.  
  56. initbuffer:
  57. push ax
  58. push bx
  59. push cx
  60. push dx
  61. ;打开文件
  62. mov ax, 3d00h
  63. mov dx, offset myfile
  64. int 21h
  65.  
  66. mov word ptr [filehandle], ax
  67.  
  68. mov bx, ax
  69. mov ax, 4200h
  70. sub cx, cx
  71. sub dx, dx
  72. int 21h
  73.  
  74. mov ah, 3fh
  75. mov bx, WORD PTR [fileHandle]
  76. mov cx,
  77. mov dx, OFFSET sampleRate
  78. int 21h
  79.  
  80. mov ax, 4200h
  81. mov bx, WORD PTR [fileHandle]
  82. xor cx, cx
  83. sub dx, dx
  84. int 21h
  85.  
  86. pop dx
  87. pop cx
  88. pop bx
  89. pop ax
  90. ret
  91.  
  92. _CODE ends

七、编写DSP

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

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

  

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

  代码如下

  1. FORMAT_MONO EQU 00h
  2. FORMAT_STEREO EQU 20h
  3. FORMAT_SIGNED EQU 10h
  4. FORMAT_UNSIGNED EQU 00h
  5.  
  6. _CODE segment
  7. assume cs:_CODE
  8. resetDSP:
  9. push ax
  10. push dx
  11.  
  12. ;设置DSP
  13. mov dx, REG_DSP_RESET
  14. mov al, 01h
  15. out dx, al
  16. sub al, al
  17. out dx, al
  18.  
  19. mov dx, REG_DSP_READ_BS
  20. ;等待sb16响应
  21. DSPwait1:
  22. in al, dx
  23. test al, 80h
  24. jz DSPwait1
  25.  
  26. mov dx, REG_DSP_READ
  27. DSPwait2:
  28. in al, dx
  29. cmp al, 0aah
  30. jne DSPwait2
  31.  
  32. pop dx
  33. pop ax
  34. ret
  35.  
  36. writeDSP:
  37. push dx
  38. push ax
  39.  
  40. mov dx, REG_DSP_WRITE_BS
  41. DSPwait3:
  42. in al, dx
  43. test al, 80h
  44. jz DSPwait3
  45. pop ax
  46.  
  47. mov dx, REG_DSP_WRITE_DATA
  48. out dx, al
  49.  
  50. pop dx
  51. ret
  52.  
  53. readDSP:
  54. push dx
  55. mov dx, REG_DSP_READ_BS
  56. dspwait4:
  57. in al, dx
  58. test al, 80h
  59. jz dspwait4
  60. pop ax
  61. mov dx, REG_DSP_READ
  62. in al, dx
  63. pop dx
  64. ret
  65.  
  66. setsample:
  67. push dx
  68. xchg al, ah
  69. push ax
  70. mov al, DSP_SET_SAMPLING_OUTPUT
  71. call writeDSP
  72. pop ax
  73. call writeDSP
  74. mov al, ah
  75. call writeDSP
  76. pop dx
  77. ret
  78.  
  79. ;AX = Sampling
  80. ;BL = Mode
  81. ;CX = Size
  82. startplay:
  83. call setsample
  84.  
  85. mov al, 00b6h
  86. call writeDSP
  87. mov al, bl
  88. call writeDSP
  89. mov al, cl
  90. call writeDSP
  91. mov al, ch
  92. call writeDSP
  93.  
  94. ret
  95.  
  96. pauseplay:
  97. push ax
  98. mov al, 00d5H
  99. call WriteDSP
  100. pop ax
  101. ret
  102.  
  103. continueplay:
  104. push ax
  105. mov al, 00d6H
  106. call WriteDSP
  107. pop ax
  108. ret
  109.  
  110. _CODE ends

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

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

  1. BLOCK_SIZE EQU
  2. BUFFER_SIZE EQU
  3. assume ds:_DT, es:_DT
  4. _DT segment para public 'DATA'
  5.  
  6. buffer db BUFFER_SIZE DUP()
  7. bufferoffset db offset buffer
  8. buffersegment dw _DT
  9. _DT ends
  1. ;These are the only configurable constants
  2.  
  3. ;IO Base
  4. SB16_BASE EQU 220h
  5.  
  6. ;16-bit DMA channel (must be between 5-7)
  7. SB16_HDMA EQU
  8.  
  9. ;IRQ Number
  10. SB16_IRQ EQU
  11.  
  12. ;These a computed values, don't touch them if you don't know what
  13. ;you are doing
  14.  
  15. ;REGISTER NAMES
  16.  
  17. REG_DSP_RESET EQU SB16_BASE +
  18. REG_DSP_READ EQU SB16_BASE + 0ah
  19. REG_DSP_WRITE_BS EQU SB16_BASE + 0ch
  20. REG_DSP_WRITE_CMD EQU SB16_BASE + 0ch
  21. REG_DSP_WRITE_DATA EQU SB16_BASE + 0ch
  22. REG_DSP_READ_BS EQU SB16_BASE + 0eh
  23. REG_DSP_ACK EQU SB16_BASE + 0eh
  24. REG_DSP_ACK_16 EQU SB16_BASE + 0fh
  25.  
  26. ;DSP COMMANDS
  27.  
  28. DSP_SET_SAMPLING_OUTPUT EQU 41h
  29. DSP_DMA_16_OUTPUT_AUTO EQU 0b6h
  30. DSP_STOP_DMA_16 EQU 0d5h
  31.  
  32. ;DMA REGISTERS
  33.  
  34. REG_DMA_ADDRESS EQU 0c0h + (SB16_HDMA - ) *
  35. REG_DMA_COUNT EQU REG_DMA_ADDRESS + 02h
  36.  
  37. REG_DMA_MASK EQU 0d4h
  38. REG_DMA_MODE EQU 0d6h
  39. REG_DMA_CLEAR_FF EQU 0d8h
  40.  
  41. IF SB16_HDMA -
  42. REG_DMA_PAGE EQU 8bh
  43. ELSE
  44. IF SB16_HDMA -
  45. REG_DMA_PAGE EQU 89h
  46. ELSE
  47. REG_DMA_PAGE EQU 8ah
  48. ENDIF
  49. ENDIF
  50.  
  51. ;ISR vector
  52. ISR_VECTOR EQU ((SB16_IRQ SHR ) * (70h - 08h) + (SB16_IRQ AND ) + 08h) *
  53.  
  54. PIC_DATA EQU (SB16_IRQ AND ) + 21h
  55. PIC_MASK EQU SHL (SB16_IRQ AND )

九、主程序编写

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

  按照步骤顺序来就可以

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

  1. INCLUDE cfg.asm
  2. INCLUDE mybuffer.asm
  3. INCLUDE mydata.asm
  4. INCLUDE myisr.asm
  5. INCLUDE mydsp.asm
  6. INCLUDE mydma.asm
  7.  
  8. _CODE segment
  9. assume cs:_CODE
  10. start:
  11. mov ax, _DT
  12. mov ds, ax
  13. call installISR
  14. call initbuffer
  15. mov si, word ptr [buffersegment]
  16. mov es, si
  17. mov si, word ptr [bufferoffset]
  18. mov di, BLOCK_SIZE *
  19. call setDMA
  20. call resetDSP
  21. mov ax, word ptr [samplerate]
  22. mov bx, FORMAT_MONO or FORMAT_SIGNED
  23. mov cx, BLOCK_SIZE
  24. call startplay
  25. mov ah,
  26. int 16h
  27. call pauseplay
  28. call installISR
  29. mov ah, 4ch
  30. int 21h
  31. _CODE ends
  32. 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. 五、RegExp(正则表达式)篇

    正则表达式,只用记住: 0./pattern/igm   i--不区分大小写 g--找到所有相匹配的 m--多行匹配  可以只写其中一个  ps:/pattern/i (无视大小写) 1." ...

  2. python查询mysql数据

    >>>cur.execute("select * from 表名") >>>lines=cur.fetchall() >>>f ...

  3. Flask初见

    Flask是一个使用 Python 编写的轻量级 Web 应用框架.其 WSIG工具箱采用 Werkzeug ,模板引擎则使用 Jinja2 .Flask使用 BSD 授权. Flask也被称为 “m ...

  4. docker和docker compose常用操作命令

    首先区分一下docker中几个概念 Image:镜像,相当于一个root文件系统,不包含任何动态数据 Container:容器,镜像运行时的实体,实质是进程,容器进程运行于属于自己的独立的命名空间 d ...

  5. ESP32 LyraT音频开发板试玩(二):播放音乐

    我是卓波,很高兴你来看我的博客. 系列文章: ESP32 LyraT音频开发板试玩(一):搭建开发环境 ESP32 LyraT音频开发板试玩(二):播放音乐 本文延续上一篇博客 将D:\msys32\ ...

  6. Kubernetes-DNS

    Kubernetes提供的虚拟DNS服务名为skydns,由四个组件组成: etcd:DNS存储 kube2sky:将Kubernetes Master中的Service(服务)注册到etcd sky ...

  7. (数据科学学习手札32)Python中re模块的详细介绍

    一.简介 关于正则表达式,我在前一篇(数据科学学习手札31)中已经做了详细介绍,本篇将对Python中自带模块re的常用功能进行总结: re作为Python中专为正则表达式相关功能做出支持的模块,提供 ...

  8. XML文件中关键字自动提示和不全配置

    一.获得mybatis-3-config.dtd.mybatis-3-mapper.dtd 这两个文件. 建立一个Maven的项目 在Pom.xml文件中的Mybatis jar包的下载设置(也可以从 ...

  9. DDR分析与布线要求

    基本知识 Double Data Rate Synchronous Dynamic Random Access Memory 简称 DDR SDRAM 双倍数据率同步动态随机存取内存 DDR SDRA ...

  10. 第四模块:网络编程进阶&数据库开发 考核实战

     1.什么是进程?什么是线程? 什么是协程? 进程:正在进行的一个过程或者说一个任务.而负责执行任务则是cpu. 线程:在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程 协程是一种用 ...