当前版本新增了Sound Blaster 16相关的声卡驱动,同时新增了play程式,在ramdisk中放入了两个测试用的wav文件,通过 play test.wav 或 play test2.wav 命令就可以播放这两个音频文件...

    页面导航:
项目下载地址:

    v3.0.2版本的项目地址:

    github.com地址:https://github.com/zenglong/zenglOX (只包含git提交上来的源代码)

    Dropbox地址:点此进入Dropbox网盘 

    Google Drive地址:点此进入Google Drive云端硬盘

    sourceforge地址:https://sourceforge.net/projects/zenglox/files 

    在上面的三个网盘中(不包括github),v3.1.0版本的相关文件,都位于zenglOX_v3.1.0的文件夹中,在该文件夹里,zenglOX_v3.1.0.zip文件为当前版本的源代码的压缩包,zenglOX.iso文件为当前版本的iso镜像,SoundBlaster_RemovePassword.pdf文件为Sound Blaster系列的声卡的编程手册,readme.txt文件为当前版本的说明文档。

VirtualBox及VMware下的运行测试:

    当前版本新增了Sound Blaster 16相关的声卡驱动,同时新增了play程式,在ramdisk中放入了两个测试用的wav文件,通过 play test.wav 或 play test2.wav 命令就可以播放这两个音频文件。

    不过当前版本的开发,遇到了很多阻力,一方面有来自模拟器的问题,另一方面还有来自Linux系统本身的问题,等等。

    可以先将网盘中的zenglOX.iso光盘镜像,挂载到VirtualBox或VMware下进行简单的测试(作者是在windows环境下安装的VirtualBox和VMware)。

    在测试之前,还需要先对VirtualBox及VMware的声卡进行配置,让zenglOX系统所在的虚拟机,使用sound blaster 16的声卡。

    对于VirtualBox,可以在虚拟机的设置界面的左侧,选择声音。然后在右边的声音界面中,从控制芯片里,选择"SoundBlaster 16",最后点确定即可,如下所示:


图1

    对于VMware,则需要修改vmx的配置文件,例如:作者在VMware中安装zenglOX时,所设置的配置文件名为zenglOX.vmx,那么就打开该配置文件,并在其中加入一条
sound.virtualDev = "sb16" 的配置信息即可:
 

图2

    如果想在VMware下设置详细点的话,可以在配置文件中设置如下信息:

sound.autodetect = "FALSE"
sound.virtualDev = "sb16"
sound.baseAddr = "0x220"
sound.dma16 = "5"
sound.irq = "5"


    上面配置信息里的sound.autodetect的原始值为TRUE,可以将其设置为FALSE,关闭自动检测,要求VMware使用sound.virtualDev中所指定的sb16(也就是sound blaster 16声卡)。

    sound.baseAddr = "0x220" 用于设置sound blaster 16中的DSP芯片的I/O端口的基地址。这个声卡是ISA插槽上的声卡,通过设置主板上的jumper(跳线),可以将DSP的I/O基地址设置为0x220、0x240等等,默认是0x220。主要是在zenglOX的sb16的声卡驱动程式中,已经将这个基地址通过宏定义,设置为了0x220。因此,如果你设置为0x240的话,就无法对DSP的I/O端口进行读写操作了。zenglOX的sb16的声卡驱动,是从Minix3中移植过来的。在Minix3的源码中,也将其固定为了0x220。各个模拟器的缺省值也都是0x220 。

    sound.dma16 = "5" 用于将DMA16的通道设置为5,在ISA的DMA中,5、6、7三个通道是用于16位传输的,各模拟器中的缺省值为5,在zenglOX的驱动程式里,也是通过通道5来进行DMA的16位传输的。有关ISA DMA的相关信息,可以参考 http://wiki.osdev.org/ISA_DMA 该链接对应的文章。

    sound.irq = "5" 用于将sb16声卡的中断号设置为5,这也是和zenglOX的驱动程式相匹配的。在某些版本的VMware里,这个中断号的缺省值可能是7。因此,可以将其设置为5,从而让zenglOX的声卡驱动能够正常运作。Minix3的源码里,中断号默认为7。作者之所以使用5,是因为在Qemu模拟器下,通过分析Qemu的源代码可知,它在初始化时,就将sb16的中断号,固定为5了。而且,无法通过sb16的mixer芯片的0x80寄存器来修改这个中断号,因此,为了让zenglOX的声卡驱动能够在VirtualBox,VMware及Qemu下都正常运行,就将中断号统一设置为5了 。

    当然,一开始你可以只添加一条sound.virtualDev = "sb16"的配置信息。如果不能正常工作的话,再将其他的配置信息加进去。

    Qemu模拟器的配置,放在稍候再进行介绍。因为qemu的编译配置,会涉及到比较多的内容。

    在VirtualBox及VMware下配置好声卡后。挂载zenglOX v3.1.0版本的iso镜像文件,在启动进入桌面前,应该就可以看到Sound Blaster 16声卡的DSP芯片的版本号信息了。qemu及VirtualBox中的DSP版本号为4.5,VMware中的DSP版本号则为4.13 。如下所示:


图3

    进入zenglOX的桌面后,启动term终端。在终端里,通过 play test.wav 和 play test2.wav 命令,就可以播放test.wav与test2.wav这两个音频文件了:
 

图4

    上面,当处理器每收到一个声卡的IRQ中断时,就会向play程式发送一个消息。在该消息的处理过程中,就会将百分数信息给显示出来,表示当前大概播放到了什么位置。
在play程式的播放过程中,可以按p键暂停,按c键继续,按ESC键退出。VMware下,如果使用暂停和继续功能的话,会导致一些播放上的问题。这有可能是VMware模拟器的BUG,也有可能是当前驱动的问题。

    在VirtualBox及VMware下,还可以通过mixer程式来调节音量:
 

图5

    mixer -mv命令在调节音量时,其有效值在0到31之间,上面的mixer -mv 31 31命令中,输入的两个31,是分别用来设置左右两声道的。将两个声道设为不同的值,只能对test.wav这种stereo(双通道立体声)文件起作用,对test2.wav这种mono(单通道)文件不起作用。

    另外,在作者的笔记本的XP系统中,双声道调节可以起到作用(也就是可以给两个声道分别设置不同的音量,比如将一边设为31,另一边设为0,则可以让一边有声音,另一边没声音)。但是在作者的台式机的win7系统中,双声道调节不起作用(也就是虽然可以对整体的音量大小进行调节,但是不能将两个声道分别设为两个不同的音量,两边始终会是相同的音量效果)。

    可以通过mixer -h命令来查看mixer的帮助信息。虽然,mixer有很多的参数,但是只有-mv参数可以在VMware及VirtualBox下起作用,因为模拟器,并没有完全按照硬件来进行模拟,很多环节都被忽略掉了。尤其是在Qemu模拟器下,Mixer芯片对音量控制完全没作用。因此,在Qemu模拟器下使用mixer程式并没有多大的含义,顶多用mixer -l命令来查看当前Mixer芯片里各音量控制相关的寄存器的值而已。

    Mixer芯片相关的内容,可以参考pdf手册的第4章。在pdf手册的第71页(顶部分页输入框中的页数),有一幅音量控制相关的电路原理图。真实的硬件,应该是按照这个图来调节音量的。从图中可以看到每个Mixer寄存器在音量控制中的作用。

    上面提到的play程式,mixer程式,test.wav及test2.wav音频文件,都存储在ramdisk中。因此,都可以直接进行测试。

Qemu编译配置相关的内容:

    对于zenglOX v3.1.0及之后的版本,由于引入了sound blaster 16之类的声卡的驱动。并且,声音输出在qemu下执行时,它的I/O线程会很繁忙。以至于在Ubuntu系统下,如果不开启kvm硬件加速的话,qemu的I/O线程会严重阻塞VCPU线程,从而导致qemu的CPU指令无法正常执行。也就会让qemu里的音乐播放、图形界面渲染、鼠标操作等一系列功能都无法正常运作(主要是在播放音乐的过程中)。

    因此,对于zenglOX v3.1.0及之后的版本,就必须开启KVM硬件加速(至少在ubuntu的系统下是这样的)。

    要开启KVM(也就是处理器的虚拟化技术),读者可以参考 http://www.linuxfromscratch.org/blfs/view/svn/postlfs/qemu.html 该链接里的内容。该链接中,提示读者可以先用如下命令,来查看处理器是否支持虚拟化技术:

egrep '^flags.*(vmx|svm)' /proc/cpuinfo

    如果什么输出信息都没有,那么很遗憾,你的处理器不支持虚拟化技术。或者是,你的Linux是安装在VirtualBox之类的虚拟机中的。VirtualBox不会将处理器的虚拟化功能,传递给运行在其中的系统。

    如果该命令产生了任何输出信息的话,就说明你的处理器支持虚拟化技术(或者叫虚拟化功能)。如下所示:

[email protected]:~$ egrep '^flags.*(vmx|svm)' /proc/cpuinfo
flags		: fpu vme de pse tsc msr pae mce cx8 apic sep 
mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 
ss ht tm pbe nx rdtscp lm constant_tsc arch_perfmon pebs bts 
xtopology nonstop_tsc aperfmperf pni dtes64 monitor ds_cpl 
vmx est tm2 ssse3 cx16 xtpr pdcm pcid sse4_1 sse4_2 popcnt 
lahf_lm arat dtherm tpr_shadow vnmi flexpriority ept vpid
........................................................
[email protected]:~$ cat /proc/cpuinfo 
processor	: 0
vendor_id	: GenuineIntel
cpu family	: 6
model		: 37
model name	: Intel(R) Core(TM) i3 CPU       M 370  @ 2.40GHz
........................................................
[email protected]:~$ 


    从cpuinfo的flags标志字段中,我们可以看到CPU所支持的功能,当flags中出现vmxsvm时,就说明当前处理器支持虚拟化功能。其中,vmx对应Intel处理器的虚拟化功能,svm则对应AMD处理器的虚拟化功能。从cpuinfo开头的vendor_id和model name部分,就可以看到作者的笔记本是Intel Core i3处理器,支持vmx虚拟化功能。

    接着就可以进入BIOS中查看该虚拟化功能是否开启了,如果没开启就手动开启。

    作者的台式机用的是技嘉的主板(型号GA-F2A85XM-DS2)。在该主板的BIOS里的M.I.T.页面下,有一个Advanced Frequency Settings项目,进入该项目后,再进入Advanced CPU Core Features项目。该项目里,有一个SVM Mode选项(AMD处理器的虚拟化功能),该选项默认是Enabled开启状态。因此,作者的技嘉主板默认就开启了CPU的虚拟化功能。作者的华硕笔记本默认也是开启了处理器的虚拟化功能的。至于其他主板,则需要查看主板的说明书,或者网上搜索。

    如果CPU不支持虚拟化技术的话,就只能将生成的iso镜像放置到VirtualBox或VMware下进行测试了。

    在开启了CPU的虚拟化功能后,还需要查看Linux内核是否将KVM编译为了内建模式,或者将KVM编译为了模块模式。在上面那个KVM相关的链接里已经写的比较详细了,这里就不多说了。在ubuntu系统下(作者所使用的是ubuntu 12.04的系统),KVM默认就被编译为了模块模式。读者可以使用 modinfo kvm-intel 和 modinfo kvm-amd 命令来查看这两个模块的相关信息。对于Intel处理器,需要使用kvm-intel模块,对于AMD处理器,则需要使用kvm-amd模块。

    要加载kvm-amd模块,可以使用sudo modprobe kvm-amd命令。要加载kvm-intel模块,则可以使用sudo modprobe kvm-intel命令:

[email protected]:~$ sudo modprobe kvm-amd
[sudo] password for zengl: 
FATAL: Error inserting kvm_amd (/lib/modules/3.2.0-52-lowlatency-pae/kernel/arch/x86/kvm/kvm-amd.ko): 
Operation not supported
[email protected]:~$ sudo modprobe kvm-intel
[email protected]:~$ lsmod | grep kvm
kvm_intel             127518  0 
kvm                   363583  1 kvm_intel
[email protected]:~$ sudo modprobe kvm-amd
[email protected]:~$ 


    从上面的输出中可以看到,如果你的处理器不支持某个模块,那么modprobe命令执行时,就会抛出Operation not supported的信息。当成功加载了kvm-intel或kvm-amd模块后,可以通过如上所示的 lsmod | grep kvm 命令来查看,当显示出kvm相关的信息后(比如kvm_intel之类的信息),就说明对应的模块被加载成功了。从上面的输出中,还可以看到,当成功加载了kvm-intel模块后,再去加载kvm-amd模块时,就不会再抛出Operation not supported的信息了。只有第一次加载KVM模块时,当该模块与处理器的虚拟化功能不匹配时,才会抛出相关错误信息。

    如果想让kvm-intel或kvm-amd模块在启动时,自动被加载的话,在ubuntu系统下,可以在/etc/modules文件中加入对应的模块名,如下所示:

[email protected]:~$ sudo vi /etc/modules
# /etc/modules: kernel modules to load at boot time.
#
# This file contains the names of kernel modules that should be loaded
# at boot time, one per line. Lines beginning with "#" are ignored.

lp
kvm-intel
[email protected]:~$ 


    上面在/etc/modules文件里加入了kvm-intel的模块名,那么该模块就会在boot time(启动时)被自动加载了。

    在kvm模块被成功加载后,qemu不需要什么额外的配置(qemu本身就支持kvm),只需在qemu的启动命令行参数中,加入-enable-kvm参数即可。

    从zenglOX v3.1.0版本开始,作者默认就在startQemu的脚本文件中,加入了-enable-kvm的参数:

#!/bin/bash
export QEMU_AUDIO_DRV=alsa
qemu-system-i386 -enable-kvm -hda hd_qemu.img -cdrom zenglOX.iso -boot d -m 26 -gdb tcp::1234 -S -net nic -net user -soundhw sb16 -display sdl


    如果你的Linux系统环境下,不需要开启kvm硬件加速,qemu也能正常播放音乐的话,可以将该参数去掉。不过如果能开启kvm硬件加速的话,最好还是开启。因为它可以有效的提高qemu的执行速度。

    此外,从上面的startQemu脚本文件中可以看到:在该文件的开头,加入了一条export QEMU_AUDIO_DRV=alsa命令,这条命令用于强制要求qemu使用ALSA音频驱动来播放声音。

    要让qemu播放出声音,首先需要确保qemu所在的Linux系统能发出声音。如果Linux系统都不能发出声音的话,那么运行在Linux系统中的qemu模拟器就更不可能发出声音了。

    作者的台式机安装的是ubuntu 12.04的Linux发行版,一开始该系统是没有声音的。alsamixer命令执行后,显示出"This sound device does not have any controls"的信息,根本没有音量可供调节。因此,应该是alsa音频驱动的问题。

    作者就参考 http://blog.csdn.net/rainysia/article/details/12907443 这篇文章,将ALSA音频驱动重新安装了一下(作者和这篇文章中涉及到的声卡一样,也是realtek alc887的声卡)。此外,作者还参考 http://blog.csdn.net/wangzhilife/article/details/7881722http://blog.163.com/seven_7_one/blog/static/162606412201185114049121 这两个链接里的文章,将ALSA相关的alsa-lib及alsa-utils都安装了一遍(安装时,要注意alsa-driver,alsa-lib及alsa-utils的版本号要一致)。重启后,alsamixer就可以正常调节音量了, ubuntu也就可以正常播放声音了。

    作者的笔记本上,装的也是ubuntu 12.04的系统。但是,默认就可以播放声音。因为,现在的Linux内核中,默认就集成了alsa的音频驱动。作者笔记本上的声卡比较旧,内核集成的alsa驱动可以识别出来。不像作者台式机上的声卡,内核集成的alsa驱动无法识别。就只有到alsa的官方网站(http://www.alsa-project.org/),下载新版的驱动来安装了。当然,你也可以将ubuntu 12.04升级到14.04或者15.04的新版本,让新版本的内核中集成的alsa驱动来识别。不过,作者不喜欢升级,一方面要等满久,另一方面,新版本是否稳定也不清楚。

    对于alsa-driver,1.0.25及之前的版本可以从官方的old drivers (alsa-driver)的ftp站点(当前地址为 ftp://ftp.alsa-project.org/pub/driver/)下载到。1.0.25之后的版本,alsa-driver已经被彻底整合进Linux的内核源码中了,alsa官方已经不再提供单独的alsa-driver的下载地址了。

    在Linux系统加入了ALSA音频驱动以及相关的alsa-lib,alsa-utils的情况下。如果系统能正常播放声音,那么接下来,就只需在qemu的编译配置中加入alsa的支持即可,对于zenglOX v3.1.0之前的版本,我们使用的是如下编译配置:

[email protected]:~/Downloads/qemu-build$ ../qemu-2.2.0/configure --target-list=i386-softmmu --enable-debug --disable-pie

    对于zenglOX v3.1.0的版本,就需要使用如下所示的配置:

[email protected]:~/Downloads/qemu-build$ ../qemu-2.2.0/configure --target-list=i386-softmmu --enable-debug --disable-pie --enable-sdl --audio-drv-list='oss alsa sdl'

    增加了--enable-sdl--audio-drv-list='oss alsa sdl'两个配置参数。

    --enable-sdl表示允许使用SDL来进行渲染。有的linux系统中默认会使用gtk来渲染,但是gtk渲染下,播放声音时,会阻塞gtk的渲染工作。因此,在startQemu的启动脚本中,还加入了一条-display sdl的启动参数,表示强制使用SDL引擎来进行渲染。

    --audio-drv-list='oss alsa sdl'表示qemu将支持oss,alsa及sdl的音频驱动。这样在startQemu脚本文件里加入export QEMU_AUDIO_DRV=alsa命令后,qemu就会通过alsa音频驱动来播放声音了。如果没有--audio-drv-list='oss alsa sdl'这条配置信息的话,那么qemu就将只支持原始的oss音频驱动,当export QEMU_AUDIO_DRV=alsa命令执行后,再启动qemu时,会提示audio: Unknown audio driver `alsa'的信息,这样qemu就无法通过alsa音频驱动来播放声音了。因此,必须加入--audio-drv-list='oss alsa sdl'这条配置信息。

    在使用configure对qemu重新配置完后,还需要通过make和make install命令来重新编译安装qemu:


[email protected]:~/Downloads/qemu-build$ ../qemu-2.2.0/configure --target-list=i386-softmmu --enable-debug --disable-pie --enable-sdl --audio-drv-list='oss alsa sdl'
[email protected]:~/Downloads/qemu-build$ make
[email protected]:~/Downloads/qemu-build$ sudo make install
[email protected]:~/Downloads/qemu-build$ qemu-system-i386 --version
QEMU emulator version 2.2.0, Copyright (c) 2003-2008 Fabrice Bellard
[email protected]:~/Downloads/qemu-build$

    在重新配置,和编译安装qemu后,编译运行zenglOX的方式与之前的版本是一样的。

    因此,对于qemu来说,当前版本,主要就是需要开启kvm虚拟化技术,还有就是加入alsa音频驱动的支持(对于大部分Linux发行版而言,只要能播放出声音即可,因为较新的Linux内核中都集成了ALSA音频驱动)。在startQemu的启动脚本中,还加入了一条-soundhw sb16的启动参数,表示在qemu中将使用Sound blaster 16的声卡。

    当前版本及以后的版本都将不再提供Bochs相关的启动脚本,因为Bochs已经不再适合后续版本的开发了。

    可以看到,声卡驱动的开发,会遇到不少问题。如果读者想在自己的hobby OS中写入声卡驱动的话,也需要对这些问题一一进行处理。

源码解析:

    与Sound blaster 16相关的驱动程式位于zenglOX源码根目录的zlox_sb16.c及zlox_sb16.h的文件中。

    与play程式相关的源码位于build_initrd_img目录中的play.c文件里,

    与mixer程式相关的源码位于build_initrd_img目录中的mixer.c文件里。

    此外,还新增了zlox_audio.c及zlox_audio.h两个音频驱动文件,有点类似于Linux中的alsa,属于中间驱动层,他会将用户指令传递到底层的具体的声卡驱动中。

    和Sound blaster 16声卡相关的信息可以参考网盘中的pdf手册,以及参考 http://wiki.osdev.org/Sound_Blaster_16 该链接对应的文章,不过这篇文章的内容过于简单了,还是pdf手册讲的详细点。网盘中的pdf手册名为SoundBlaster_RemovePassword.pdf ,该名称里的RemovePassword表示该PDF手册是去除了密码保护的。可以对PDF里的内容进行复制操作。比如对某些英文句子进行复制后,就可以放到Google翻译中,进行翻译。原版的PDF受到保护无法复制,不方便查看和翻译。

    在PDF手册的第22页,有一幅Sound blaster 16声卡的结构图:


图6

    在zenglOX的sb16驱动程式中,主要是通过DSP芯片来完成音频的播放,暂停和继续等功能的。上面的MIXER芯片则用于控制音频输出的音量大小。sb16声卡的CSP芯片主要用于高级数据处理,比如解压缩之类的操作,不过在zenglOX的驱动程式里,目前并没有用到CSP芯片。sb16在播放音频数据时,会通过ISA总线的DMA通道从内存中读取数据到sb16中,经过D/A数模转换后,最后将模拟音频信号传递到MIXER芯片,由MIXER控制音量大小并输出到扬声器。

    因此,驱动程式中的核心组件就是DSP(Digital Sound Processor 数字声音处理器),在PDF手册的第2章就对DSP的编程操作进行了详细的介绍。

    DSP有一个I/O基地址,通过设置主板上的跳线,这个基地址可以是0x220、0x240等(读者可以在PDF手册的第123页查看到sb16可用的I/O基地址)。驱动程式中,已经将这个基地址固定为0x220了(这也是各模拟器的缺省值)。因此,下表中的2x6h,2xAh等,在驱动程式里就对应为0x226,0x22A等。下面表格位于PDF手册的第24页(其中定义了DSP相关的I/O端口):

端口名 端口地址 端口作用
Reset
重置端口
2x6h(只能进行写入操作) Used to reset the DSP to its default state.
用于将DSP芯片重置到默认状态。
Read Data
读数据端口
2xAh(只读端口) Used to access in-bound DSP data.
用于从DSP中读取数据,例如一些反馈数据等。
Write Command/Data
写命令或写数据端口
2xCh
(当对2xC进行写入操作时,
可以写入相关的命令或数据)
Used to send commands or data to the DSP.
用于向DSP芯片写入命令,或写入数据。
Write-Buffer Status
写缓冲状态端口
2xCh
(当对2xC进行读操作时,
它会将写缓冲状态信息读取
出来)
indicates whether the DSP is ready to accept commands or data.
通过读取的状态信息,可以判断DSP当前是否
可以写入命令,或者写入数据。因此,在向
DSP写入命令之前,最好先读取该状态信息来进行判断。
Read-Buffer Status
读缓冲状态端口
2xEh(只读端口) Indicates whether there is any in-bound data available for reading.
用于判断DSP的Read Data端口里是否有数据可供读取了。

    在zenglOX源码根目录的zlox_sb16.h的头文件中,就对这些I/O端口及0x220的基地址,进行了相关的宏定义:

/*zlox_sb16.h the sb16 header*/

....................................................
#define ZLOX_SB_BASE_ADDR 0x220

....................................................

/* IO ports for sound blaster */
#define ZLOX_SB_DSP_RESET 0x6 + ZLOX_SB_BASE_ADDR
#define ZLOX_SB_DSP_READ 0xA + ZLOX_SB_BASE_ADDR
#define ZLOX_SB_DSP_COMMAND 0xC + ZLOX_SB_BASE_ADDR
#define ZLOX_SB_DSP_STATUS 0xC + ZLOX_SB_BASE_ADDR
#define ZLOX_SB_DSP_DATA_AVL 0xE + ZLOX_SB_BASE_ADDR
#define ZLOX_SB_DSP_DATA16_AVL 0xF + ZLOX_SB_BASE_ADDR
#define ZLOX_SB_MIXER_REG 0x4 + ZLOX_SB_BASE_ADDR
#define ZLOX_SB_MIXER_DATA 0x5 + ZLOX_SB_BASE_ADDR

....................................................


    上面的ZLOX_SB_BASE_ADDR的值为0x220,也就是DSP的I/O基地址。DSP其他的I/O端口地址,都是在这个基地址的基础上得来的。例如,ZLOX_SB_DSP_RESET即重置端口的I/O地址为0x6 + ZLOX_SB_BASE_ADDR,也就是0x226 。

    通过DSP的Reset(重置)端口,我们可以用来检测当前主机中,是否存在Sound Blaster系列的声卡。在PDF手册的第24页的Resetting DSP节中,可以看到重置DSP芯片的基本步骤:
  1. Write a "1" to the Reset port (2x6h) and wait for 3 microseconds.
    首先,将"1"写入到Reset端口,并等待大约3微秒。
  2. Write a "0" to the Reset port.
    接着,将"0"写入到Reset端口。
  3. Poll for a ready byte 0AAh from the Read Data port. You must check the
    Read-Buffer Status port to ensure there is data before reading the Read
    Data port.
    最后,从DSP的Read Data(读数据)端口中,读取出字节值,并判断该字节值是否等于0xAA,
    如果等于0xAA则说明当前主机中存在Sound Blaster系列的声卡。如果不等于0xAA,则说明主机中不存在该系列的声卡。
    在从Read Data端口中读取数据之前,你必须先检测Read-Buffer Status即读缓冲状态端口里的值,
    当读缓冲状态端口里的值的位7为1时,则说明Read Data端口中有数据可供读取。
    根据上述步骤,就有了zlox_sb16.c文件中的zlox_sb16_dsp_reset函数:

ZLOX_SINT32 zlox_sb16_dsp_reset()
{
	ZLOX_SINT32 i;
	// 先向Reset端口写入1
	zlox_outb(ZLOX_SB_DSP_RESET, 1);
	// 等待一小段时间
	for(i = 0; i < 1000; i++)
		; /* wait a while */
	// 接着向Reset端口写入0
	zlox_outb(ZLOX_SB_DSP_RESET, 0);

	// 循环检测读缓冲状态端口的值
	// 当位7为1时,就说明Read Data(读数据)端口中有数据了
	for(i = 0; i < 1000 && 
		!(zlox_inb(ZLOX_SB_DSP_DATA_AVL) & 0x80); 
		i++)
		;

	// 从Read Data端口中读取出字节值
	// 当该值不为0xAA时,返回-1,表示重置失败
	// 当该值为0xAA时,返回0,表示重置成功
	if(zlox_inb(ZLOX_SB_DSP_READ) != 0xAA) 
		return -1;
	return 0;
}


    上面棕色的注释是此处额外添加的,在源文件里并没有。此外,在zlox_audio.c文件中,还有一个zlox_audio_reset()函数,该函数最终会通过上面的zlox_sb16_dsp_reset函数来重置DSP芯片,并通过重置的结果来判断声卡是否存在:

ZLOX_SINT32 zlox_audio_reset()
{
	if(zlox_sb16_dsp_reset() != 0)
	{
		audio_exist = ZLOX_FALSE;
		return -1;
	}
	audio_exist = ZLOX_TRUE;
	return 0;
}


    在内核的主入口函数里,就会先通过上面这个zlox_audio_reset函数来检测声卡是否存在,当声卡存在时,才会去执行相关的初始化工作,否则,就跳过声卡的初始化过程:

/*zlox_kernel.c Defines the C-code kernel entry point, calls initialisation routines.*/

....................................................

//zenglOX kernel main entry
ZLOX_SINT32 zlox_kernel_main(ZLOX_MULTIBOOT * mboot_ptr, ZLOX_UINT32 initial_stack)
{
	....................................................

	ZLOX_SINT32 audio_reset = zlox_audio_reset();
	if(audio_reset == 0)
		zlox_audio_alloc_res_before_init();

	....................................................

	if(audio_reset == 0)
		zlox_audio_init();
	
	....................................................
}


    从上面的代码中可以看到,当zlox_audio_reset函数返回0时,也就是声卡存在时,会先通过zlox_audio_alloc_res_before_init函数来分配声卡所需的资源,比如ISA DMA操作所需要的物理内存等。在分配到相关的资源后,再通过zlox_audio_init函数来初始化声卡。这两个函数都定义在zlox_audio.c文件里:

// zlox_audio.c -- something relate to audio

....................................................

ZLOX_SINT32 zlox_audio_init()
{
	if(zlox_sb16_init() == -1)
	{
		return -1;
	}
	return 0;
}

....................................................

// alloc resource before init
ZLOX_SINT32 zlox_audio_alloc_res_before_init()
{
	ZLOX_UINT32 DmaPhys;
	zlox_kmalloc_128k_align(ZLOX_SB_DMA_SIZE + 4096, &DmaPhys);
	if(DmaPhys == 0)
	{
		zlox_monitor_write("audio: DmaPhys alloc failed!\n");
		return -1;
	}
	return zlox_sb16_set_dma_phys(DmaPhys, ZLOX_TRUE);
}

....................................................


    在分配资源的函数里,有一个zlox_kmalloc_128k_align函数,该函数定义在zlox_kheap.c文件中:

/* zlox_kheap.c Kernel heap functions, also provides
	a placement malloc() for use before the heap is 
	initialised. */

....................................................

ZLOX_UINT32 zlox_kmalloc_128k_align(ZLOX_UINT32 sz, ZLOX_UINT32 *phys)
{
	ZLOX_UINT32 tmp;
	if(kheap == 0)
	{
		if((placement_address & 0x0001FFFF))
		{
			// Align the placement address;
			placement_address &= 0xFFFE0000;
			placement_address += 0x20000;
		}
		if (phys)
		{
			*phys = placement_address;
		}
		tmp = placement_address;
		placement_address += sz;
		return tmp;
	}
	return 0;
}

....................................................


    上面这个函数必须在初始化内核堆之前进行调用(也就是kheap为空指针时进行调用)。该函数会根据sz参数分配一段内存空间,该内存空间的起始物理内存地址会按照128K对齐。之所以要按照128K对齐,是ISA DMA传输所规定的硬性标准。

    sb16声卡在使用ISA总线时,如果进行的是8位DMA数据传输,那么数据所在的内存的起始物理地址必须按照64K对齐,当进行16位DMA传输时,起始物理地址就必须按照128K对齐。为了能让8位和16位传输都正常执行,在zenglOX的驱动程式中,就只使用了128K对齐,因为128K对齐的物理内存地址,同时也能满足64K对齐的要求。

    有关ISA DMA的相关内容,可以参考 http://wiki.osdev.org/ISA_DMA 该链接对应的文章。

    在分配到DMA所需的物理内存资源后,就可以通过zlox_audio_init函数来初始化声卡了,从前面zlox_audio_init函数的源代码中可以看到,该函数最终会调用zlox_sb16_init函数来完成sb16声卡的初始化操作,该函数定义在zlox_sb16.c文件里:

ZLOX_SINT32 DspVersion[2];

....................................................

ZLOX_SINT32 zlox_sb16_init()
{
	ZLOX_SINT32 i;
	//ZLOX_SINT32 DspVersion[2];
	if(zlox_sb16_dsp_reset() != 0) { 
		zlox_monitor_write("No SoundBlaster!\n"); /* No SoundBlaster */
		return -1;
	}
	zlox_monitor_write("SoundBlaster is detected! ");
	DspVersion[0] = DspVersion[1] = 0;
	zlox_sb16_dsp_command(ZLOX_SB_DSP_GET_VERSION);

	for(i = 1000; i; i--) {
		if(zlox_inb(ZLOX_SB_DSP_DATA_AVL) & 0x80) {		
			if(DspVersion[0] == 0) {
				DspVersion[0] = zlox_inb(ZLOX_SB_DSP_READ);
			} else {
				DspVersion[1] = zlox_inb(ZLOX_SB_DSP_READ);
				break;
			}
		}
	}

	if(DspVersion[0] < 4) {
		zlox_monitor_write("sb16: No SoundBlaster 16 compatible card detected\n");
		return -1;
	} 

	zlox_monitor_write("sb16: SoundBlaster DSP version ");
	zlox_monitor_write_dec(DspVersion[0]);
	zlox_monitor_write(".");
	zlox_monitor_write_dec(DspVersion[1]);
	zlox_monitor_write(" detected!\n");

	/* set SB to use our IRQ and DMA channels */
	zlox_sb16_mixer_set(ZLOX_SB_MIXER_SET_IRQ, (1 << (ZLOX_SB_IRQ / 2 - 1)));
	zlox_sb16_mixer_set(ZLOX_SB_MIXER_SET_DMA, (1 << ZLOX_SB_DMA_8 | 1 << ZLOX_SB_DMA_16));

	if(interrupt_callbacks[ZLOX_IRQ0 + ZLOX_SB_IRQ] != 0)
	{
		zlox_monitor_write("sb16: irq has been occupied!\n");
		return -1;
	}

	zlox_register_interrupt_callback(ZLOX_IRQ0 + ZLOX_SB_IRQ, 
				&zlox_sb16_callback);
	return 0;
}


    在上面的初始化函数里,最开始会对DSP芯片进行重置操作。接着,会通过DSP的相关命令来获取DSP芯片的版本号信息,因为通过DSP芯片的版本号,就可以判断当前声卡是否是Sound Blaster 16声卡。Sound Blaster系列的声卡包含了好几种声卡,但是我们的驱动目前只能对Sound Blaster 16的声卡进行相关操作。

    在PDF手册的第12页的Determining User's Sound Blaster Card节里,就介绍了Sound Blaster系列声卡的型号与DSP版本号之间的关系:

Cards
(SB系列的声卡)
Version Number
(DSP版本号)
SB1.5, SBMCV 1.xx to 2.00
SB2.0 2.01+
SBPRO, SBPRO MCV 3.xx
SB16, Sound Blaster 16 with Advanced Signal Processing 4.xx

    当DSP的版本号为4.xx时(比如Qemu和VirtualBox中为4.5,VMware中为4.13),就说明当前声卡是sb16的声卡。上面的zlox_sb16_init初始化函数里,会通过ZLOX_SB_DSP_GET_VERSION命令(这是一个值为0xE1的宏),来获取DSP的版本号。当向DSP写入该命令后,第一次从DSP的Read Data端口中读取出来的数据会是主版本号信息,第二次从Read Data端口读出来的数据则是次版本号。在PDF手册的第114页有这个0xE1即获取DSP版本号命令的描述信息。

    在获取到版本号信息后,sb16的初始化函数就会将这个版本号信息给显示出来。

    显示出版本号信息后,就可以去配置DMA通道的中断请求线,以及配置8位和16位传输所需使用的DMA通道了:

ZLOX_SINT32 zlox_sb16_init()
{
	................................................
	/* set SB to use our IRQ and DMA channels */
	zlox_sb16_mixer_set(ZLOX_SB_MIXER_SET_IRQ, (1 << (ZLOX_SB_IRQ / 2 - 1)));
	zlox_sb16_mixer_set(ZLOX_SB_MIXER_SET_DMA, (1 << ZLOX_SB_DMA_8 | 1 << ZLOX_SB_DMA_16));
	................................................
}


    要理解上面这两行代码的含义,可以参考PDF手册的第28页的Configuring DMA and Interrupt Settings节里的内容。通过Mixer芯片的0x80和0x81寄存器就可以设置IRQ与DMA通道了(上面的ZLOX_SB_MIXER_SET_IRQ宏的值为0x80,ZLOX_SB_MIXER_SET_DMA宏的值则为0x81)。

    最后只需将IRQ绑定到对应的中断处理例程中即可:

ZLOX_SINT32 zlox_sb16_init()
{
	................................................

	zlox_register_interrupt_callback(ZLOX_IRQ0 + ZLOX_SB_IRQ, 
				&zlox_sb16_callback);
	return 0;
}


    上面ZLOX_SB_IRQ的值为5,也就是将IRQ5作为中断请求线。当sb16声卡播放完一段数据后,就会通过IRQ5向处理器发送中断请求,处理器最后就会调用zlox_sb16_callback函数来处理该中断请求。

    在sb16播放音频数据时,驱动程式会先将开头的64K音频数据写入到DMA的物理内存中,并将DSP的传输字节设置为32K,这样当播放完32K的音频数据后,sb16就会发送IRQ5中断请求,在zlox_sb16_callback的处理例程里,就会继续将后面的32K音频数据覆盖到已播放完的32K物理内存处,也就是Double Buffer双缓冲方式,通过这种双缓冲方式就可以将所有音频数据流畅的播放完。DMA传输时,会使用auto-initialize(自动初始化)方式,这种方式下,当64K音频数据被传输完后,它会将传输地址自动初始化到DMA物理内存的起始位置处,然后将新写入的32K音频数据播放出来。大概的过程,可以参考下面的图7

    和sb16播放音频数据相关的代码,定义在zlox_sb16_dsp_start函数中(该函数也定义在zlox_sb16.c文件里,要理解这段函数的代码,请参考PDF手册的第57页的8-bit or 16-bit Auto-initialize Transfer节里的内容):

ZLOX_SINT32 zlox_sb16_dsp_start(ZLOX_UINT32 DmaPhys, ZLOX_SINT32 DmaMode)
{
	if(dsp_start)
		return -2;
	if(DmaPhys == 0)
		DmaPhys = SB16_DmaPhys;
	zlox_sb16_dsp_reset();
	// 将DMA的物理内存起始地址设置到ISA的DMA芯片中
	// 请参考前面提到的ISA DMA相关的链接
	zlox_sb16_dsp_dma_setup(DmaPhys, ZLOX_SB_DMA_SIZE, DmaMode);
	// 设置DSP的采样速率
	zlox_sb16_dsp_set_speed(DspSpeed, ZLOX_TRUE);
	/* Put the speaker on */
	if(DmaMode == ZLOX_SB_WRITE_DMA)
	{
		// ZLOX_SB_DSP_CMD_SPKON的值为0xD1
		// 有关0xD1命令的详情,请参考PDF手册的第110页
		zlox_sb16_dsp_command(ZLOX_SB_DSP_CMD_SPKON); /* put speaker on */
		// 下面的ZLOX_SB_DSP_CMD_8BITAUTO_OUT等都是
		// 定义在zlox_sb16.h头文件中的宏,
		// 这些宏所对应的DSP命令以及这些命令所需的参数
		// 请参考PDF手册的第57页的内容
		/* Program DSP with dma mode */
		zlox_sb16_dsp_command((DspBits == 8 ? ZLOX_SB_DSP_CMD_8BITAUTO_OUT : 
						ZLOX_SB_DSP_CMD_16BITAUTO_OUT)); 
	} else {
		zlox_sb16_dsp_command(ZLOX_SB_DSP_CMD_SPKOFF); /* put speaker off */
		/* Program DSP with dma mode */
		zlox_sb16_dsp_command((DspBits == 8 ? ZLOX_SB_DSP_CMD_8BITAUTO_IN : 
						ZLOX_SB_DSP_CMD_16BITAUTO_IN));
	}
	/* Program DSP with transfer mode */
	if (!DspSign) 
	{
		zlox_sb16_dsp_command((DspStereo == 1 ? 
				ZLOX_SB_DSP_MODE_STEREO_US : ZLOX_SB_DSP_MODE_MONO_US));
	}
	else 
	{
		zlox_sb16_dsp_command((DspStereo == 1 ? 
				ZLOX_SB_DSP_MODE_STEREO_S : ZLOX_SB_DSP_MODE_MONO_S));
	}
	if (DspBits == 8) 
	{	/* 8 bit transfer */
		/* #bytes - 1 */
		zlox_sb16_dsp_command((ZLOX_SB_DSP_FRAG_SIZE - 1) >> 0); 
		zlox_sb16_dsp_command((ZLOX_SB_DSP_FRAG_SIZE - 1) >> 8);
	}
	else {
		/* 16 bit transfer */
		/* #words - 1 */
		zlox_sb16_dsp_command((ZLOX_SB_DSP_FRAG_SIZE - 2) >> 1);
		zlox_sb16_dsp_command((ZLOX_SB_DSP_FRAG_SIZE - 2) >> 9);
	}

	dsp_start = ZLOX_TRUE;
	running = ZLOX_TRUE;
	return 0;
}


    zlox_sb16_callback中断处理例程的C代码也定义在zlox_sb16.c文件中:

static ZLOX_VOID zlox_sb16_callback()
{
	//zlox_monitor_write("sb16: in callback\n");
	if(running) 
	{
		ZLOX_TASK_MSG msg = {0};
		// ZLOX_SB_MIXER_IRQ_STATUS的宏值为0x82,
		// 通过Mixer芯片的0x82寄存器,可以获取到中断的状态信息,
		// 0x82中断状态寄存器的详情,可以参考PDF手册的第27页,
		// 从手册上可以看到,当0x82寄存器的值的位0和位1被置位时,
		// 就说明当前的中断是8位或16位DMA传输所产生的中断
		ZLOX_SINT32 status = zlox_sb16_mixer_get(ZLOX_SB_MIXER_IRQ_STATUS);
		/*zlox_monitor_write("[sb16: irq: \n");
		zlox_monitor_write_hex(status);
		zlox_monitor_write("]");*/
		// 当前中断例程只处理8位或16位的DMA中断
		if(!(status & 0x03)) // must be 8 bit or 16 bit irq interrupt!
			return;
		// 通过zlox_audio_get_data函数(定义在zlox_audio.c文件里),
		// 从用户数据中读取新的音频数据,并写入到DMA物理内存中,
		// 从而将已播放完的音频数据给覆盖掉
		if(zlox_audio_get_data() == ZLOX_TRUE)
		{
			// 向用户程式发送MT_AUDIO_INT消息,
			// 通知用户当前已播放完一小段数据
			if(SB16_Task != ZLOX_NULL)
			{
				msg.type = ZLOX_MT_AUDIO_INT;
				zlox_send_tskmsg(SB16_Task,&msg);
			}
			// 下面是VMware里的BUG处理,
			// 在VMware中如果使用了暂停功能的话,
			// 每次中断时都必须使用DMA8CONT或DMA16CONT
			// 的继续执行命令,否则播放就会一直处于暂停状态,
			// 这个也许是VMware中的BUG,
			// 也可能是DSP 4.13需要进行一些额外的处理,
			// 原因还不清楚,因为VMware是闭源的商业产品,
			// 我们无法像Qemu那样去调试内部的源代码
			if(has_vmware_cont)
			{
				zlox_sb16_dsp_command((DspBits == 8 ? ZLOX_SB_DSP_CMD_DMA8CONT : 
						ZLOX_SB_DSP_CMD_DMA16CONT));
			}
			// 下面函数会通过读取DSP的8位或16位的
			// Read-Buffer Status寄存器,来对
			// 当前中断信号进行反馈,并释放掉当前的中断信号,
			// 如果不进行反馈的话,当前中断信号就无法被释放掉,
			// 下一次8位或16位传输结束时,就无法触发新的中断了
			zlox_sb16_dsp_reenable_int();
			return;
		}
		else
		{
			// 当所有的用户音频数据都播放完后,
			// 可以通过DMA8EXIT或DMA16EXIT命令
			// 来退出播放,在Qemu与VirtualBox下,
			// 这些命令执行后,需要播放完当前的一小段才会退出。
			// VMware下则会立即退出播放。
			zlox_sb16_dsp_command((DspBits == 8 ? ZLOX_SB_DSP_CMD_DMA8EXIT : 
							ZLOX_SB_DSP_CMD_DMA16EXIT));
			dsp_start = ZLOX_FALSE;
			running = ZLOX_FALSE;
			// 通过zlox_audio_end函数,
			// 清理掉音频播放时所分配的一些资源,
			// 比如,播放音频数据时,会为
			// 用户数据分配一段内核堆空间,
			// 并将用户数据拷贝到这段内核堆空间中,
			// 每次中断时,就会从堆里读取出新的音频数据,
			// 因此,播放结束时,需要将这段堆空间给释放掉。
			zlox_audio_end();
			// 向用户程式发送MT_AUDIO_END消息,
			// 从而告诉用户程式当前的音频播放已结束。
			if(SB16_Task != ZLOX_NULL)
			{
				msg.type = ZLOX_MT_AUDIO_END;
				zlox_send_tskmsg(SB16_Task,&msg);
				SB16_Task = ZLOX_NULL;
			}
			if(has_vmware_cont)
				has_vmware_cont = ZLOX_FALSE;
			zlox_sb16_dsp_reenable_int();
			return;
		}
	}
}


    当DSP开启了音频播放的操作后,ISA DMA的传输与IRQ中断例程的执行过程,可以参考下面这幅图:


图7

    DSP开启音频播放的操作后,还可以在播放的过程中,执行暂停、继续及退出的操作。与暂停、继续及退出相关的C代码如下(这些C代码也都定义在zlox_sb16.c文件里):

ZLOX_SINT32 zlox_sb16_dsp_pause()
{
	if(running)
	{
		// 通过DSP的DMA8PAUSE或DMA16PAUSE命令,
		// 来暂停8位或16位的DMA传输及播放操作,
		// 这两个暂停命令对应的值分别为0xD0和0xD5,
		// 这两个命令的详情可以参考PDF手册的
		// 第109页与第112页的内容。
		zlox_sb16_dsp_command((DspBits == 8 ? ZLOX_SB_DSP_CMD_DMA8PAUSE : 
				ZLOX_SB_DSP_CMD_DMA16PAUSE));
		running = ZLOX_FALSE;
		zlox_sb16_dsp_reenable_int();
		return 0;
	}
	return -1;
}

ZLOX_SINT32 zlox_sb16_dsp_continue()
{
	if(dsp_start && !running)
	{
		// 通过DSP的DMA8CONT或DMA16CONT命令,
		// 来继续执行8位或16位的DMA传输及播放操作,
		// 这两个继续命令对应的值分别为0xD4和0xD6,
		// 这两个命令的详情可以参考PDF手册的
		// 第111页与第112页的内容。
		zlox_sb16_dsp_command((DspBits == 8 ? ZLOX_SB_DSP_CMD_DMA8CONT : 
				ZLOX_SB_DSP_CMD_DMA16CONT));
		running = ZLOX_TRUE;
		if(DspVersion[1] == 13)
			has_vmware_cont = ZLOX_TRUE;
		return 0;
	}
	return -1;
}

ZLOX_SINT32 zlox_sb16_dsp_exit()
{
	if(!dsp_start)
		return -1;
	// 通过DSP的DMA8EXIT或DMA16EXIT命令,
	// 来结束8位或16位的DMA传输及播放操作,
	// 这两个结束命令对应的值分别为0xDA和0xD9,
	// 这两个命令的详情可以参考PDF手册的
	// 第114页与第113页的内容。
	zlox_sb16_dsp_command((DspBits == 8 ? ZLOX_SB_DSP_CMD_DMA8EXIT : 
					ZLOX_SB_DSP_CMD_DMA16EXIT));
	dsp_start = ZLOX_FALSE;
	running = ZLOX_FALSE;
	zlox_audio_end();
	if(SB16_Task != ZLOX_NULL)
		SB16_Task = ZLOX_NULL;
	if(has_vmware_cont)
		has_vmware_cont = ZLOX_FALSE;
	zlox_sb16_dsp_reenable_int();
	return 0;
}


    以上是和DSP相关的驱动代码。在zlox_sb16.c文件的底部,定义了和Mixer芯片相关的代码,Mixer芯片的编程原理,可以参考PDF手册的第4章,也就是从第59页开始的内容。

    从PDF手册的第60页可以看到,Mixer芯片会使用两个I/O端口来访问Mixer内部的寄存器。一个是2x4h端口,另一个是2x5h端口(x是由DSP的I/O基地址来确定的)。在驱动程式中的实际端口地址分别为0x224与0x225。其中,0x224为Mixer寄存器的索引端口,0x225为Mixer寄存器的数据端口。要对Mixer里的寄存器进行读写操作的话,需要先将Mixer寄存器的索引值写入到0x224的端口中,再对0x225的数据端口进行读写操作即可。在zlox_sb16.c文件里的zlox_sb16_mixer_set与zlox_sb16_mixer_get函数就是根据这两个端口来对Mixer寄存器进行读写操作的:

ZLOX_SINT32 zlox_sb16_mixer_set(ZLOX_SINT32 reg, ZLOX_SINT32 data)
{
	ZLOX_SINT32 i;

	// 先向ZLOX_SB_MIXER_REG即0x224端口写入
	// Mixer寄存器的索引值
	zlox_outb(ZLOX_SB_MIXER_REG, reg);
	for(i = 0; i < 100; i++)
		;
	// 再将数据写入到ZLOX_SB_MIXER_DATA即0x225的数据端口中
	zlox_outb(ZLOX_SB_MIXER_DATA, data);

	return 0;
}

ZLOX_SINT32 zlox_sb16_mixer_get(ZLOX_SINT32 reg)
{
	ZLOX_SINT32 i;

	// 先向ZLOX_SB_MIXER_REG即0x224端口写入
	// Mixer寄存器的索引值
	zlox_outb(ZLOX_SB_MIXER_REG, reg);
	for(i = 0; i < 100; i++)
		;
	// 再从0x225的数据端口中将寄存器的值读取出来
	return zlox_inb(ZLOX_SB_MIXER_DATA) & 0xff;
}


    在PDF手册的第70页,有一个表格,在该表格中定义了Mixer芯片里各寄存器的索引值,以及这些寄存器的名称。在第71页,有一副Mixer音频输出的逻辑示意图,在该图中就显示出了各寄存器在Mixer音量控制中的作用。

    zlox_sb16.c文件里的zlox_sb16_mixer_get_volume函数,就可以将Mixer中各音量控制相关的寄存器的值给读取出来:

ZLOX_SINT32 zlox_sb16_mixer_get_volume(ZLOX_AUD_CTRL_TYPE ctrl_type, 
					ZLOX_AUD_EXTRA_DATA * extra_data)
{
	// 将寄存器的值读取出来后,还需要进行位移才能得到真实的值
	// 例如PDF手册的第70页的表格中的Master volume.L寄存器的
	// 低3位属于保留位,因此,将该寄存器的值读取出来后,需要
	// 右移3位,才能得到实际的左声道的Master音量值。
	ZLOX_SINT32 shift = 3;
	if(ctrl_type == ZLOX_ACT_MIXER_GET_VOLUME)
	{
		extra_data->mixer.master_left = zlox_sb16_mixer_get(ZLOX_SB_MIXER_MASTER_LEFT);
		extra_data->mixer.master_right = zlox_sb16_mixer_get(ZLOX_SB_MIXER_MASTER_RIGHT);
		extra_data->mixer.master_left >>= shift;
		extra_data->mixer.master_right >>= shift;
		extra_data->mixer.voice_left = zlox_sb16_mixer_get(ZLOX_SB_MIXER_DAC_LEFT);
		extra_data->mixer.voice_right = zlox_sb16_mixer_get(ZLOX_SB_MIXER_DAC_RIGHT);
		extra_data->mixer.voice_left >>= shift;
		extra_data->mixer.voice_right >>= shift;
		extra_data->mixer.midi_left = zlox_sb16_mixer_get(ZLOX_SB_MIXER_FM_LEFT);
		extra_data->mixer.midi_right = zlox_sb16_mixer_get(ZLOX_SB_MIXER_FM_RIGHT);
		extra_data->mixer.midi_left >>= shift;
		extra_data->mixer.midi_right >>= shift;
		extra_data->mixer.cd_left = zlox_sb16_mixer_get(ZLOX_SB_MIXER_CD_LEFT);
		extra_data->mixer.cd_right = zlox_sb16_mixer_get(ZLOX_SB_MIXER_CD_RIGHT);
		extra_data->mixer.cd_left >>= shift;
		extra_data->mixer.cd_right >>= shift;
		extra_data->mixer.line_left = zlox_sb16_mixer_get(ZLOX_SB_MIXER_LINE_LEFT);
		extra_data->mixer.line_right = zlox_sb16_mixer_get(ZLOX_SB_MIXER_LINE_RIGHT);
		extra_data->mixer.line_left >>= shift;
		extra_data->mixer.line_right >>= shift;
		extra_data->mixer.mic_volume = zlox_sb16_mixer_get(ZLOX_SB_MIXER_MIC_LEVEL);
		extra_data->mixer.mic_volume >>= shift;
		extra_data->mixer.pc_volume = zlox_sb16_mixer_get(ZLOX_SB_MIXER_PC_LEVEL);
		extra_data->mixer.pc_volume >>= 6;
		extra_data->mixer.treble_left = zlox_sb16_mixer_get(ZLOX_SB_MIXER_TREBLE_LEFT);
		extra_data->mixer.treble_right = zlox_sb16_mixer_get(ZLOX_SB_MIXER_TREBLE_RIGHT);
		extra_data->mixer.treble_left >>= 4;
		extra_data->mixer.treble_right >>= 4;
		extra_data->mixer.bass_left = zlox_sb16_mixer_get(ZLOX_SB_MIXER_BASS_LEFT);
		extra_data->mixer.bass_right = zlox_sb16_mixer_get(ZLOX_SB_MIXER_BASS_RIGHT);
		extra_data->mixer.bass_left >>= 4;
		extra_data->mixer.bass_right >>= 4;
		extra_data->mixer.output_gain_left = zlox_sb16_mixer_get(ZLOX_SB_MIXER_GAIN_OUT_LEFT);
		extra_data->mixer.output_gain_right = zlox_sb16_mixer_get(ZLOX_SB_MIXER_GAIN_OUT_RIGHT);
		extra_data->mixer.output_gain_left >>= 6;
		extra_data->mixer.output_gain_right >>= 6;
		extra_data->mixer.output_ctrl = zlox_sb16_mixer_get(ZLOX_SB_MIXER_OUTPUT_CTRL);
		return 0;
	}
	else if(ctrl_type == ZLOX_ACT_MIXER_GET_MASTER)
	{
		extra_data->mixer.master_left = zlox_sb16_mixer_get(ZLOX_SB_MIXER_MASTER_LEFT);
		extra_data->mixer.master_right = zlox_sb16_mixer_get(ZLOX_SB_MIXER_MASTER_RIGHT);
		extra_data->mixer.master_left >>= shift;
		extra_data->mixer.master_right >>= shift;
		return 0;
	}
	else if(ctrl_type == ZLOX_ACT_MIXER_GET_VOICE)
	{
		extra_data->mixer.voice_left = zlox_sb16_mixer_get(ZLOX_SB_MIXER_DAC_LEFT);
		extra_data->mixer.voice_right = zlox_sb16_mixer_get(ZLOX_SB_MIXER_DAC_RIGHT);
		extra_data->mixer.voice_left >>= shift;
		extra_data->mixer.voice_right >>= shift;
		return 0;
	}
	return -1;
}


    此外,zlox_sb16.c文件里,还有一个zlox_sb16_mixer_set_volume函数,通过该函数可以对Mixer各寄存器里的值进行设置:

ZLOX_SINT32 zlox_sb16_mixer_set_volume(ZLOX_AUD_CTRL_TYPE ctrl_type, 
					ZLOX_AUD_EXTRA_DATA * extra_data)
{
	ZLOX_SINT32 shift = 3;
	ZLOX_SINT32 max_level = 0x1F;
	ZLOX_SINT32 reg_left, reg_right;
	ZLOX_SINT32 level_left, level_right;
	switch(ctrl_type)
	{
	case ZLOX_ACT_MIXER_SET_MASTER:
		reg_left = ZLOX_SB_MIXER_MASTER_LEFT;
		reg_right = ZLOX_SB_MIXER_MASTER_RIGHT;
		level_left = extra_data->mixer.master_left;
		level_right = extra_data->mixer.master_right;
		break;
	case ZLOX_ACT_MIXER_SET_VOICE:
		reg_left = ZLOX_SB_MIXER_DAC_LEFT;
		reg_right = ZLOX_SB_MIXER_DAC_RIGHT;
		level_left = extra_data->mixer.voice_left;
		level_right = extra_data->mixer.voice_right;
		break;
	.................................................
	}

	if(level_left < 0) 
		level_left = 0;
	else if(level_left > max_level) 
		level_left = max_level;

	if(level_right < 0) 
		level_right = 0;
	else if(level_right > max_level) 
		level_right = max_level;

	// 设置到寄存器中的值,需要将保留位也考虑到里面,
	// 因此就需要进行相关的位移操作。
	zlox_sb16_mixer_set(reg_left, level_left << shift);
	if(reg_left != reg_right)
		zlox_sb16_mixer_set(reg_right, level_right << shift);
	return 0;
}


    通过对Mixer芯片中这些寄存器的值进行设置,就可以调节音量的大小。从PDF手册的第73页开始,可以看到这些寄存器的音量级别的有效值。例如,Master volume与Voice volume都是0到31的音量级别。在VirtualBox和VMware中调节音量时,也只有这两种寄存器可以起到音量调节的作用。在Qemu中则音量调节根本不起作用。

    从build_initrd_img目录中的syscall.c文件里,可以看到,当前版本新增了如下所示的4个系统调用:

// syscall.c Defines the implementation of a system call system.

#include "syscall.h"

....................................................

DEFN_SYSCALL2(audio_set_databuf, SYSCALL_AUDIO_SET_DATABUF, UINT8 *, SINT32);
DEFN_SYSCALL5(audio_set_args, SYSCALL_AUDIO_SET_ARGS, UINT32, UINT32, UINT32, UINT32, void *);
DEFN_SYSCALL0(audio_play, SYSCALL_AUDIO_PLAY);
DEFN_SYSCALL2(audio_ctrl, SYSCALL_AUDIO_CTRL, UINT32, void *);


    上面这几个系统调用,最终会通过zlox_audio.c文件里的同名函数(不包括zlox_的前缀)来完成具体的操作,例如,audio_set_databuf系统调用最终会通过zlox_audio_set_databuf函数来完成操作等:

// 该函数会根据用户的音频数据创建一个内核堆空间,
// 并将用户的音频数据拷贝到该堆空间中,
// 当DSP产生中断时,会直接从内核堆里将音频数据拷贝到
// DMA的物理内存中。之所以要创建一个内核堆空间,
// 是因为用户进程所创建的堆空间在发生任务切换时,
// 由于页目录等也会跟着切换,
// 因此,不能保证始终能被内核代码访问到。
ZLOX_SINT32 zlox_audio_set_databuf(ZLOX_UINT8 * userdata, ZLOX_SINT32 size)
{
	if(dsp_start)
		return -2;
	if(userdata == ZLOX_NULL)
		return -1;
	if(size <= 0)
		return -1;
	if(audio_buf != ZLOX_NULL)
	{
		zlox_kfree(audio_buf);
		audio_buf = ZLOX_NULL;
		audio_buf_size = 0;
	}
	audio_buf_size = size;
	// 在内核堆中为用户的音频数据创建堆空间。
	audio_buf = (ZLOX_UINT8 *)zlox_kmalloc(audio_buf_size);
	if(audio_buf == ZLOX_NULL)
	{
		audio_buf_size = 0;
		return -1;
	}
	// 将用户的音频数据拷贝到内核堆中。
	zlox_memcpy(audio_buf, userdata, audio_buf_size);
	return 0;
}

// 下面这个zlox_audio_set_args函数是用于设置音频数据的采样格式的。
// 有关音频数据的采样格式,
// 可以参考PDF手册的第31页的Digitized Sound Data Format节里的内容。
// bits参数:设置音频播放时,每个采样的位数,
//           有8位和16位两种采样位数。
// sign参数:设置每个采样的符号位,也就是每个采样是有符号数,还是无符号数。
//           当sign为1时,说明是有符号数,当为0时,则说明是无符号数。
// stereo参数:设置每个采样的通道数,
//           当stereo为0时,会设置为单通道, 
//           当stereo为1时,会设置为双通道。
// speed参数:设置采样的速率,sb16声卡的采样速率必须位于5000到44100之间。
ZLOX_SINT32 zlox_audio_set_args(ZLOX_UINT32 bits, ZLOX_UINT32 sign, ZLOX_UINT32 stereo, ZLOX_UINT32 speed, 
			ZLOX_TASK * task)
{
	if(dsp_start)
		return -2;
	if(zlox_sb16_dsp_set_bits(bits) != 0)
		return -1;
	zlox_sb16_dsp_set_sign(sign);
	zlox_sb16_dsp_set_stereo(stereo);
	if(zlox_sb16_set_task(task) != 0)
		return -1;
	if(zlox_sb16_dsp_set_speed(speed, ZLOX_FALSE) != 0)
		return -1;
	return 0;
}

// 先将一部分音频数据,比如开头的64K的音频数据拷贝到DMA物理内存中,
// 接着通过zlox_sb16_dsp_start函数来启动音频的播放操作。
// 音频播放时,就会将DMA物理内存里的音频数据给播放出来了,
// 并在收到DSP的中断时,将新的音频数据拷贝到DMA物理内存中,
// 覆盖掉已播放完的音频数据。
ZLOX_SINT32 zlox_audio_play()
{
	if(dsp_start)
		return -2;
	if(audio_buf == ZLOX_NULL || audio_buf_size == 0)
		return -1;
	ZLOX_UINT32 DmaPhys = zlox_sb16_get_dma_phys();
	audio_buf_cpy_cnt = 0;
	audio_flip_flop = 0;
	audio_end = ZLOX_FALSE;
	if(audio_buf_size < ZLOX_SB_DMA_SIZE)
	{
		zlox_memcpy((ZLOX_UINT8 *)DmaPhys, audio_buf, audio_buf_size);
		zlox_memset(((ZLOX_UINT8 *)DmaPhys + audio_buf_size), SB16_DspFillData, 
				(ZLOX_SB_DMA_SIZE - audio_buf_size));
		audio_buf_cpy_cnt += audio_buf_size;
	}
	else
	{
		zlox_memcpy((ZLOX_UINT8 *)DmaPhys, audio_buf, ZLOX_SB_DMA_SIZE);
		audio_buf_cpy_cnt += ZLOX_SB_DMA_SIZE;
	}
	zlox_sb16_dsp_start(DmaPhys, ZLOX_SB_WRITE_DMA);
	return 0;
}

....................................................

// 下面这个函数可以对音频的播放和音量大小等进行控制和调节。
// 例如当ctrl_type为ZLOX_ACT_PAUSE时,就通过
// zlox_audio_pause()函数来暂停音频的播放操作等。
ZLOX_SINT32 zlox_audio_ctrl(ZLOX_AUD_CTRL_TYPE ctrl_type, 
				ZLOX_AUD_EXTRA_DATA * extra_data)
{
	ZLOX_UNUSED(extra_data);
	switch(ctrl_type)
	{
	case ZLOX_ACT_PAUSE:
		return zlox_audio_pause();
	case ZLOX_ACT_CONTINUE:
		return zlox_audio_continue();
	case ZLOX_ACT_EXIT:
		return zlox_audio_exit();
	case ZLOX_ACT_GET_PLAY_SIZE:
		return audio_buf_cpy_cnt;
	case ZLOX_ACT_DETECT_AUD_EXIST:
		return zlox_audio_detect_aud_exist(extra_data);
	case ZLOX_ACT_SET_FILL_DATA:
		SB16_DspFillData = extra_data->audio.FillData;
		return 0;
	case ZLOX_ACT_MIXER_INIT:
		return zlox_audio_mixer_init();
	case ZLOX_ACT_MIXER_GET_VOLUME:
	case ZLOX_ACT_MIXER_GET_MASTER:
	case ZLOX_ACT_MIXER_GET_VOICE:
	case ZLOX_ACT_MIXER_SET_MASTER:
	case ZLOX_ACT_MIXER_SET_VOICE:
	case ZLOX_ACT_MIXER_SET_MIDI:
	case ZLOX_ACT_MIXER_SET_CD:
	case ZLOX_ACT_MIXER_SET_LINE:
	case ZLOX_ACT_MIXER_SET_MIC:
	case ZLOX_ACT_MIXER_SET_PC:
	case ZLOX_ACT_MIXER_SET_TREBLE:
	case ZLOX_ACT_MIXER_SET_BASS:
	case ZLOX_ACT_MIXER_SET_OUT_GAIN:
	case ZLOX_ACT_MIXER_SET_OUT_CTRL:
		return zlox_audio_mixer_get_set_volume(ctrl_type, extra_data);
	default:
		break;
	}
	return -1;
}


    在play和mixer两个用户程式中,就会通过上面这4个系统调用,来完成音频数据的播放和音量调节等各种操作。

    play程式目前只能解析和播放wav格式的音频文件。

    wav音频文件的格式,可以参考 https://web.archive.org/web/20141213140451/https://ccrma.stanford.edu/courses/422/projects/WaveFormat/ 该链接,这个链接地址有点古怪,URL的中间还有一个https://,这个地址需要通过代理访问。必须将上面这条古怪的地址完全输入到浏览器的地址栏中,才能访问。并且浏览器还需要通过GoAgent之类的代理才能访问到。

    在上面这个链接中,有一副如下所示的wav文件的结构图:


图8

    play程式就是根据该结构图来解析wav文件的(下面这些C代码都定义在build_initrd_img目录里的play.c文件中):

// play.c -- 播放指定的wav音频文件

#include "common.h"
#include "syscall.h"
#include "task.h"
#include "fs.h"
#include "audio.h"

// wav文件的头部结构
struct _WAV_FILE_HEADER
{
	char ChunkID[4];
	UINT32 ChunkSize;
	char Format[4];
	char Subchunk1ID[4];
	UINT32 Subchunk1Size;
	UINT16 AudioFormat;
	UINT16 NumChannels;
	UINT32 SampleRate;
	UINT32 ByteRate;
	UINT16 BlockAlign;
	UINT16 BitsPerSample;
	char Subchunk2ID[4];
	UINT32 Subchunk2Size;
}__attribute__((packed));

typedef struct _WAV_FILE_HEADER WAV_FILE_HEADER;

// 通过wav_file_detect函数来检测buf文件缓冲的头部结构,
// 是否和wav文件格式相匹配,如果相匹配的话,就从头部
// 结构中将采样速率,采样位数等提取出来。
int wav_file_detect(UINT8 * buf, UINT32 * bits, UINT32 * sign, 
				UINT32 * stereo, UINT32 * speed, UINT32 * datasize, UINT8 ** data)
{
	WAV_FILE_HEADER * wav = (WAV_FILE_HEADER *)buf;
	// 对ChunkID进行检测
	if(strcmpn(wav->ChunkID, "RIFF", 4) != 0)
	{
		syscall_cmd_window_write("invalid wav file: ChunkID is not \"RIFF\"");
		return -1;
	}
	if(strcmpn(wav->Format, "WAVE", 4) != 0)
	{
		syscall_cmd_window_write("invalid wav file: Format is not \"WAVE\"");
		return -1;
	}
	if(strcmpn(wav->Subchunk1ID, "fmt ", 4) != 0)
	{
		syscall_cmd_window_write("invalid wav file: Subchunk1ID is not \"fmt \"");
		return -1;
	}
	if(wav->Subchunk1Size != 16)
	{
		syscall_cmd_window_write("Unsupported wav file: Subchunk1Size is not 16");
		return -1;
	}
	if(wav->AudioFormat != 1)
	{
		syscall_cmd_window_write("Unsupported wav file: AudioFormat is not 1, I only support PCM"
					", I don't support compression");
		return -1;
	}
	// 通过wav文件头部结构里的NumChannels字段来设置采样的通道数
	if((wav->NumChannels != 1) && (wav->NumChannels != 2))
	{
		syscall_cmd_window_write("Unsupported wav file: NumChannels is not 1 or 2, I only support Mono "
					"or Stereo wav file");
		return -1;
	}
	else
	{
		(*stereo) = (wav->NumChannels == 1 ? 0 : 1); 
	}
	// 通过头部结构里的SampleRate字段来设置采样速率,
	// 从PDF手册的第38页的Digitized Sound Output Capability节中,
	// 可以看到,sb16声卡要求采样速率必须位于5000到44100之间。
	if((wav->SampleRate < 5000) || (wav->SampleRate > 44100))
	{
		syscall_cmd_window_write("Unsupported wav file: SampleRate must be in 5000 to 44100");
		return -1;
	}
	(*speed) = wav->SampleRate;
	// 通过BitsPerSample字段设置每个采样的位数。
	if((wav->BitsPerSample != 8) && (wav->BitsPerSample != 16))
	{
		syscall_cmd_window_write("Unsupported wav file: BitsPerSample must be 8 bit or 16 bit");
		return -1;
	}
	// wav文件的头部结构中,
	// 并没有单独的字段来设置采样数据的符号位。
	// 当为8位采样时,采样数据默认就是无符号的,
	// 因此,8位采样数据的有效值在0到255之间。
	// 当为16位采样时,采样数据默认就是有符号的,
	// 因此,16位采样数据的有效值在-32768到32767之间。
	(*bits) = (wav->BitsPerSample == 8 ? 8 : 16);
	(*sign) = (wav->BitsPerSample == 8 ? 0 : 1);
	if(strcmpn(wav->Subchunk2ID, "data", 4) != 0)
	{
		syscall_cmd_window_write("invalid wav file: Subchunk2ID is not \"data\"");
		return -1;
	}
	// 通过Subchunk2Size字段来设置需要播放的音频数据的尺寸,
	// wav文件的实际的音频数据紧跟在WAV_FILE_HEADER头部
	// 结构的后面。
	(*datasize) = wav->Subchunk2Size;
	(*data) = buf + sizeof(WAV_FILE_HEADER);
	return 0;
}

.....................................................


    在通过上面的wav_file_detect函数对文件进行检测后,就可以通过syscall_audio_play之类的系统调用来播放音频文件了:

// play.c -- 播放指定的wav音频文件

....................................................

int main(VOID * task, int argc, char * argv[])
{
	....................................................
	else if ((fsnode->flags & 0x7) == FS_FILE)
	{
		CHAR * buf = (CHAR *)syscall_umalloc(fsnode->length + 1);
		UINT32 bits, sign, stereo, speed, datasize;
		UINT8 * data;
		syscall_read_fs(fsnode,0,fsnode->length,buf);
		buf[fsnode->length] = '\0';
		// 通过wav_file_detect函数对buf文件缓冲进行检测
		if(wav_file_detect((UINT8 *)buf, &bits, &sign, &stereo, 
					&speed, &datasize, &data) == -1)
		{
			syscall_ufree(buf);
			goto end;
		}
		// 如果syscall_audio_set_databuf系统调用返回-2,
		// 则说明当前有另一个播放程式在播放音频文件,
		// 则直接返回,因为目前只能同时执行一个音频播放操作。
		if(syscall_audio_set_databuf(data, (int)datasize) == -2)
		{
			syscall_cmd_window_write("At the same time, there is another audio player is working, "
						"you can't play now!");
			syscall_ufree(buf);
			goto end;
		}
		syscall_ufree(buf);
		// 将wav文件中提取出来的采样位数,采样速率等设置到sb16声卡
		// 的驱动程式的相关变量中,下面在启动音频播放操作时,
		// 就会使用这些采样参数来播放音频数据。
		syscall_audio_set_args(bits, sign, stereo, speed, task);
		if(bits == 8)
		{
			extra_data.audio.FillData = 0x80;
		}
		else
			extra_data.audio.FillData = 0x0;
		// 设置填充数据,当音频数据不能将DMA物理内存全部填满时,
		// 剩余的空间将使用填充数据来进行填充。
		syscall_audio_ctrl(ACT_SET_FILL_DATA, &extra_data);
		// 通过syscall_audio_play系统调用来启动音频的播放操作。
		syscall_audio_play();
		syscall_cmd_window_write("play ");
		syscall_cmd_window_write(argv[1]);
		syscall_cmd_window_write("...");
	}
	................................................

	syscall_set_input_focus(task);
	while(TRUE)
	{
		ret = syscall_get_tskmsg(task,&msg,TRUE);
		if(ret != 1)
		{
			syscall_idle_cpu();
			continue;
		}

		// 当收到MT_AUDIO_INT消息时,就说明当前
		// 已播放了一小段音频数据了,目前是每播放完32K
		// 音频数据时,就会产生一个MT_AUDIO_INT消息,
		// 在处理该消息时,会通过向syscall_audio_ctrl系统调用
		// 传递ACT_GET_PLAY_SIZE参数,来获取到所有已拷贝到DMA
		// 物理内存中的音频数据的总字节数,然后使用该字节数
		// 来大致的推算出当前播放的百分比。
		if(msg.type == MT_AUDIO_INT)
		{
			//syscall_cmd_window_write("audio int...");
			int play_size = syscall_audio_ctrl(ACT_GET_PLAY_SIZE, NULL);
			syscall_cmd_window_write("[");
			syscall_cmd_window_write_dec((int)(((double)play_size / 
							(double)fsnode->length) * 100));
			syscall_cmd_window_write("%]...");
			continue;
		}
		// 当收到MT_AUDIO_END消息时,就说明播放结束了,
		// 然后就会显示出audio end!信息,最后break跳出消息循环,
		// 并退出play程式。
		else if(msg.type == MT_AUDIO_END)
		{
			syscall_cmd_window_write("audio end!");
			break;
		}
		else if(msg.type == MT_KEYBOARD && msg.keyboard.type == MKT_ASCII)
		{
			switch(msg.keyboard.ascii)
			{
			// 可以通过'p'键来暂停播放(大小写都可以)
			case 'p':
			case 'P':
				syscall_cmd_window_write("pause...");
				syscall_audio_ctrl(ACT_PAUSE, NULL);
				break;
			// 通过'c'键来继续播放(大小写都可以)
			case 'c':
			case 'C':
				syscall_cmd_window_write("continue...");
				syscall_audio_ctrl(ACT_CONTINUE, NULL);
				break;
			// 还可以通过ESC键来退出当前的播放。
			case 0x1B: //ESC
				syscall_cmd_window_write("exit");
				syscall_audio_ctrl(ACT_EXIT, NULL);
				exit = TRUE;
				break;
			}
			if(exit)
				break;
		}
	}

end:
	if(fsnode != NULL)
		syscall_ufree(fsnode);
	return 0;
}


    以上就是当前版本新增的主要代码,至于其他没讲解到的C代码,请读者自行通过gdb调试器来进行分析。

文章中的相关链接:

    本篇文章里面,涉及到的一些资料的链接地址如下:

    http://wiki.osdev.org/ISA_DMA 该链接的文章中,详细的介绍了和ISA DMA相关的内容。

    http://www.linuxfromscratch.org/blfs/view/svn/postlfs/qemu.html 该链接对应的文章中,介绍了和KVM(处理器的虚拟化技术)相关的内容。

    http://blog.csdn.net/rainysia/article/details/12907443 介绍了如何通过重新安装ALSA音频驱动,来解决Debian下没有声音的问题。

    http://blog.csdn.net/wangzhilife/article/details/7881722http://blog.163.com/seven_7_one/blog/static/162606412201185114049121 两个链接中,都介绍了alsa-driver,alsa-lib及alsa-utils的安装方法。

    ftp://ftp.alsa-project.org/pub/driver/  在该ftp站点中,存储了1.0.25及之前版本的alsa-driver的安装包。

    http://wiki.osdev.org/Sound_Blaster_16 该链接中简单的介绍了Sound Blaster 16声卡的相关内容。

    https://web.archive.org/web/20141213140451/https://ccrma.stanford.edu/courses/422/projects/WaveFormat/ 该链接中,对WAV文件格式进行了详细的介绍。必须将这条古怪的地址完全输入到浏览器的地址栏中,才能访问。并且浏览器还需要通过GoAgent之类的代理才能访问到。

    如果文章中,有链接地址无法直接访问的话,请使用代理访问。

    OK,就到这里,休息,休息一下 o(∩_∩)o~~

    我有我的音乐,有了音乐我就觉得挺幸福。

—— 和你在一起
 
上下篇

下一篇: zenglOX v3.2.0 USB v1.1, UHCI, USB KeyBoard, USB Mouse

上一篇: zenglOX v3.0.2 PNG(Portable Network Graphics) and BUG Fix

相关文章

zenglOX v1.1.0 通过ATAPI读取光盘里的数据

zenglOX v1.4.0与v1.4.1 通过ATA驱动读写硬盘里的数据, BUG修复, VirtualBox与VMware的调试功能

zenglOX v0.0.10 Keyboard(获取键盘的输入)

zenglOX v2.2.0 ee(easy editor)文本编辑器 C标准库函数 uheap(单独的用户堆空间) atapi驱动BUG zenglfs文件系统BUG 分页BUG 堆算法BUG等修复

zenglOX v0.0.8 Multitask(多任务)

zenglOX v1.6.0 保护模式下, VGA图形模式驱动程式