• 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 0
    _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+2]
            xchg word ptr es:[di+2], bx
            mov word ptr [si+2], 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的数据信息

      文档说明如下

      

      那么代码如下

    .286
    _CODE segment
        assume cs:_CODE
        setDMA:
            push ax
            push bx
            push cx
            push dx
            push si
    
            ;重新设置,禁用通道
            mov dx, REG_DMA_MASK
            mov al, 4 + SB16_HDMA MOD 4
            out dx, al
    
            ;清零操作
            mov dx, REG_DMA_CLEAR_FF
            out dx, al
    
            ;重新设置模式
            mov dx, REG_DMA_MODE
            mov al, 58h + SB16_HDMA MOD 4
            out dx, al
    
            ;设置DMA地址
            ;ES = buffer segment
            ;SI = buffer offset
            ;DI = block size
            mov bx, es
            shr bx, 13
            mov cx, es
            shl cx, 3
            shr si, 1
            add cx, si
            adc bx, 0
    
            ;输出地址
            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, 1
            mov dx, REG_DMA_COUNT
            out dx, al
            mov al, ah
            out dx, al
    
            ;启用频道
            mov dx, REG_DMA_MASK
            mov al, SB16_HDMA mod 4
            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', 0
        filehandle dw 0
        samplerate dw 0
    _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, 12
            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 1024
        BUFFER_SIZE EQU 1024
    assume ds:_DT, es:_DT
    _DT segment para public 'DATA'
    
        buffer db BUFFER_SIZE DUP(0)
        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 5
    
     ;IRQ Number
     SB16_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 + 6
     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 - 4) * 4
     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 - 5
        REG_DMA_PAGE       EQU 8bh      
     ELSE
        IF SB16_HDMA - 6
           REG_DMA_PAGE       EQU 89h
        ELSE
           REG_DMA_PAGE       EQU 8ah
        ENDIF
     ENDIF
    
     ;ISR vector
     ISR_VECTOR            EQU ((SB16_IRQ SHR 3) * (70h - 08h) + (SB16_IRQ AND 7) + 08h) * 4
    
     PIC_DATA        EQU (SB16_IRQ AND 8) + 21h
     PIC_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 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 * 2
        call setDMA
        call resetDSP
        mov ax, word ptr [samplerate]
        mov bx, FORMAT_MONO or FORMAT_SIGNED
        mov cx, BLOCK_SIZE
        call startplay
        mov ah, 0
        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 大概就是这样了,如果有什么问题,欢迎指出

  • 相关阅读:
    C# MVC模式设置404&500
    jQuery实现滚动条滚动到子元素位置(方便定位)
    Oracle数据库的导出导入
    Oracle之clob字段不能union的问题
    div中内容垂直居中的方法小结
    C#实现Oracle数据库插入clob字段类型数据
    C_结构体_笔记
    Practice_17_01_ABC
    Practice_17_01_GID
    【转】 vim显示行号、语法高亮、自动缩进的设置
  • 原文地址:https://www.cnblogs.com/Saurus/p/7124064.html
Copyright © 2020-2023  润新知