雷精灵
2009-06-06 17:47:52
[广告]
X病专治!
药到病除!
一夜解决你的难言之隐!
[/广告]
——囧rz,你改行当江湖郎中了么?
如今金融危机,多学一门手艺就能多一条活路。
——囧rz,学当江湖郎中?还是卖假药的骗子郎中!
X,你又不是受害者,你管个P闲事啊!
现在这个社会就是这样,如果没有威胁到你的利益,请不要多管闲事。模块化的宗旨就是尽可能减少模块之间的关联。因此尽可能减少使用全局变量,使用消息机制来代替传递信息的全局变量;尽可能使用私有成员,尽量避免使用友元;尽可能将函数包装,哪怕是内联也好,尽量避免代码展开。想必你也不希望看到更新了XXOO模块,于是导致OOXX模块崩溃吧?
所以,你管我是卖药的还是卖淫的,反正不是卖你的。
——囧rz。
为了启动ARM7的音频操作,必须使用CPU通信,让ARM9向ARM7发送消息。当ARM7接收到消息之后启动相应操作。
[基础知识]
DS的CPU通信用到了一种数据结构:FIFO队列。DS的内存中有一块区域被当成FIFO队列。这块内存区域非常安全,除了FIFO队列之外,其他一切设备都无法访问。当任意一个CPU向另一块CPU发送消息的时候,该CPU会将要发送的数据写入该FIFO队列。写完之后该CPU继续去忙它的了。如果这个时候另一块CPU访问了FIFO队列,于是就会从队列中将刚才的数据提取过去。于是消息就传递给另一块CPU了。
我这么比喻一下你就明白了:
如今邪道可不好做。不能正大光明地行事不说,还会遭到正道人士的攻击。我们杀手工会就是在这种艰难的环境中成长起来的。
杀手之间也是需要情报交流的,我们最忌讳两个杀手同时去杀一个人。浪费时间精力不说,还可能会造成冲突……还有,某些情况下我们发现自己能力有限,无法完成某些任务,而凑巧又知道有个同行是专门对付这种任务的……对于这种情形我们想出了一种解决方案——设立一个极其安全的情报管道。
雇主:把雷叔给我干掉!很早以前我就看他不顺眼了。
六面叔:介个……我不擅长杀人。我擅长给孩子换尿布……
雇主:我管你擅长个鸟!这是任务!不管你用什么方法,今天晚上我要看到雷叔那个婊子的尸体!
六面叔:好吧。我去联络我的同行。
于是六面叔就往杀手工会的情报管道中留了一封密信。
没过多久那封密信被牛叔取走了。
然后今天晚上六面叔的雇主很高兴地看到了仆人端着盛放雷叔脑袋的银盘。那个死人脑袋嘴边还残留着一丝邪恶的笑容,可能干事正干到幸福的时候就被牛叔一刀拆了……
[/基础知识]
基础知识就这么多,很简单也很直观。我们先来看一下用到的API:
[code=c]
#include "nds.h"
#include "fifocommon.h"
bool fifoSendValue32(int channel, u32 value32);
bool fifoSendAddress(int channel, void *address);
u32 fifoGetValue32(int channel);
void * fifoGetAddress(int channel);
[/code]
这是我们将要用到的几个CPU通信的API。DS的FIFO通道不止只有一个,而是有十几个。其中有几个是被系统保留的,有几个专做它用,最后给我们剩下来8个用户自定义通道。在通信的时候,千万要记住通道要写正确,否则你不会收到正确的消息。
那么,我们就先把上次的DEMO移植过来。
[code=c]
ARM9:
#include
#include "fifocommon.h"
...
int void main(){
...
void* sndBuf=你的载入资源的函数("/Piano_8Bit_C3.snd");
int sndSize=你的获取资源大小的函数("/Piano_8Bit_C3.snd");
...
while(1){
...
scanKeys();
if(keysDown() & KEY_A){
fifoSendValue32(FIFO_USER_01,0xF0F0F0F0);
fifoSendValue32(FIFO_USER_01,(u32)sndBuf);
fifoSendValue32(FIFO_USER_01,(u32)sndSize);
}
...
swiWaitForVBlank();
...
}
}
ARM7:
#include
#include "audio.h"
#include "fifocommon.h"
...
int void main(){
// 初始化
irqInit();
fifoInit();
// 读用户信息
readUserSettings();
// 初始化真实时钟IRQ
initClockIRQ();
// 设置LYC
SetYtrigger(80);
// 安装FIFO队列
installSoundFIFO();
installSystemFIFO();
irqSet(IRQ_VCOUNT,VcountHandler);
irqSet(IRQ_VBLANK,VblankHandler);
irqEnable(IRQ_VBLANK|IRQ_VCOUNT|IRQ_NETWORK);
// 开启声音模块
powerOn(POWER_SOUND);
writePowerManagement(PM_CONTROL_REG,(readPowerManagement(PM_CONTROL_REG) & ~PM_SOUND_MUTE)|PM_SOUND_AMP);
REG_SOUNDCNT=SOUND_ENABLE;
REG_MASTER_VOLUME=127;
...
void* sndBuf=NULL;
int sndSize=0;
while(1){
if(fifoGetValue32(FIFO_USER_01)==0xF0F0F0F0){
sndBuf=(void*)fifoGetValue32(FIFO_USER_01);
sndSize=fifoGetValue32(FIFO_USER_01);
SCHANNEL_SOURCE(0)=(u32)sndBuf;
SCHANNEL_REPEAT_POINT(0)=0;
SCHANNEL_LENGTH(0)=sndSize>>2;
SCHANNEL_TIMER(0)=SOUND_FREQ(22050);
SCHANNEL_CR(0)=
SCHANNEL_ENABLE|
SOUND_VOL(127)|
SOUND_PAN(64)|
SOUND_FORMAT_8BIT|
SOUND_ONE_SHOT;
}
swiWaitForVBlank();
}
...
}
[/code]
来吧,看看按下A键之后,是不是如同上次一样正确发出C3的声音了?
——面白い……惯例,解释代码吧。
既然是两个CPU,那么就不存在谁先谁后的问题,主机一启动,两个CPU就开始各干各的。我们先看ARM9这边。
当按下A键之后,首先发送常数0xF0F0F0F0,然后发送采样的首地址和采样大小。为什么要先发送一个常数呢?由于两个CPU各干各的,因此ARM7不晓得什么时候会来消息,也不知道什么数据是什么消息。为了解决这种情况我们首先发送一个常数,如果ARM7收到这个消息之后则认为“啊!这是先前规定的暗号!有效的消息来了!”。根据规定,“暗号”的后面第一个消息是采样首地址,第二个消息是采样大小,然后不管有没有消息都不再认为是有效的消息了。
ARM7这边首先开始进行初始化。由于使用了fifo队列因此必须初始化fifo系统(注意,ARM9无需初始化fifo队列。相反,如果初始化的话反而会导致异常……)。音频由ARM7负责因此需要初始化音频设备。当一切初始化完毕之后,程序进入死循环,不断查询fifo队列。如果队列中传来值为0xF0F0F0F0的消息,则根据规定,接收后两个消息作为采样首地址和大小,然后设置相应的音频寄存器将其播放出来。
由此可见,由于DS CPU通信属于异步通信的缘故,这个“联络信号”就显得尤为重要。
同样,假如我们打算让ARM7收到消息之后再给ARM9一个反馈信号,又或者是打算输出ARM7的一些运行状态,则可以使用另一路FIFO通道,从ARM7向ARM9发送消息。
——原来如此。不过我发现一件事情。对于ARM7来说,每一帧检测一次fifo队列,如果有“暗号”则接收消息。假如ARM9端在一帧之内发送N次消息,那么ARM7岂不是必须得N帧才能响应所有消息?
正解。所以“查询”是一种很没效率的方法。最好的方法还是“中断”。FIFO系统提供带回调的接收函数,用回调就可以模拟出中断的效果了。因为很简单,所以我也懒得讲了,有兴趣自己去研究。
——啊!我学会了。那么现在是不是可以研究MIDI的解码了?
不。关于采样,还有一些必须了解的东西。我们现在就来看看邪恶的ADSR属性。
——ADSR?
这是Attack、Decay、Sustain、Release的缩写。我们先来看看一些实例——
[基础知识]
我希望你曾经弹过钢琴。当你按下任何一个琴键的时候,该键的声音即会发出。但是如果你按住这个键不放,琴声并不会像我们这个DEMO一样播放到采样结束就停止,而是音量逐渐衰减直至停止。对于这种现象,我们应该怎么去模拟呢?
——这个啊……我们录制一个带有衰减效果的采样吧!
嗯,是个好主意!不过真正的钢琴在当你按下一个键之后立刻放开,琴声是迅速衰减,不会像我们的DEMO一样非得把整个采样全部播放完才停止。这又该怎么做呢?
——这个就更好办了……检测到“松开琴键”事件,那就更改这个channel的音量,让它迅速降低。
好!非常好!看来我难不住你了。我们换一种乐器。不知道你有没有吹过……呃,长号。对,你有没有吹过长号?
——我知道你本来打算说什么。你什么德行我还不一清二楚?你就别硬把那句话咽到肚子里去了。
不,真的是长号……(擦汗)真的是长号,不是那啥……
——好吧。就算是长号吧。吹过。如何?那东西可不容易吹,肺活量小的话甚至根本吹不响。
说得好!我们可以把它理解成,如果力度不够,则该乐器不会发声。也就是说,从你开始吹,到长号发出声音,需要一定的“启动时间”。
——呃……不理解。
囧rz……我换个说法吧。加特林六筒机关枪。用过吧?
——囧rz……谁用过这种东西……不过游戏中确实用过。电动机一开,面前的丧尸就随着“呜呜”声跳舞。
嗯。那么你一定有注意到,加特林机关枪有个“启动时间”。在这块时间之内,电动机速度由0开始加速直至稳定射出弹药。
——没错。啊,我知道你想说什么……嗯,如果遇到这种情况,当检测到“按下琴键”事件后,延迟一定的时间再开始操纵那个channel发声,或者在录制采样的时候将前面的“预热”一起录进去。
嗯。这也是个解决方法。还有最后一种情况,电子琴。当你按住电子琴键的时候,声音会一直发出永不衰减,直至你松开琴键。
——哎?这个……比较难办……总不能弄一个足够长的采样吧?
终于把你难倒了。其实这是很容易办到的。不要忘了我们的音频播放通道支持loop。
——你是打算让这个采样在指定地方开始循环播放?
正解。我们把循环开始点,也就是loopStart的值设在当这个采样开始稳定发出“余音”的时候。很明显当播放这个采样的时候,声音就会一直延续永不停止。
——那么松开琴键,声音就要停止该怎么做?
那还不简单?检测到“松开琴键”事件的时候,让channel的音量迅速衰减到0,然后停止该channel。
——嗯……说得对……
其实这就是采样的4种属性——ADSR。
A=Attack。这个值用来指定该采样的启动时间。这个值越大,采样的“启动时间”越短。该值=0时采样立刻开始播放。
D=Decay。这个值用来指定该采样从启动到loopStart这之间的时间长度。这个值越大,则该采样越快进入稳定状态。该值=0时整个采样都是循环体。
S=Sustain。这个值用于指定在你“按住琴键”的时候采样音量衰减的程度。这个值越大,衰减得越快。该值=0时完全不会衰减,只要你按住琴键,声音则一直播放。
R=Release。这个值用于指定在你“松开琴键”的时候采样音量衰减的程度。这个值越大,衰减得越快。该值不能为0,否则声音无法停止。
——等等,R=0有什么关系?我只要让S>0就行了。
放屁!你见过那种松开琴键不会停止,按住琴键反而会停止的乐器?
——当我没说……
这4个参数中,后两个尤为重要。在我们这个囧rz的播放器中,我甚至直接忽略了前两个参数,只针对后两个进行处理。
——啊?这样不会造成问题吗?比如在PC上播放和在DS上播放效果不同……
当然会。
——那你……
事实上我告诉你,上面4个参数的定义都是我自己定义的,和标准定义完全不同。
——哎???
其实,现在说来,这个囧rz的MIDI播放引擎,其实已经算不上是“MIDI播放”引擎了。它只是在播放MIDI文件罢了。播放出来的效果虽然类似,但却是毫无疑问和PC不同的。说白了,这只不过是一个“按照我自己的规则来解释MIDI”的播放器。
——我到现在才知道为什么这个播放引擎要叫做“囧rz的MIDI播放引擎”……真够囧rz的……
[/基础知识]
那么,我们来把ADSR四大属性——呃,实际上只有SR两大属性——来模拟一下。
——好。开始写代码吧。
等等,我又累了。我们去小黑屋沟通沟通,补充一下体力怎么样?
——唉……当这家伙的学生真可怜,不晓得是出来学习的还是出来卖的~~~~
つづく
1989lzhh
2009-06-07 12:02:14
期待中。。。。
whm3d
2009-06-08 16:09:06
:dizzy:
whm3d
2009-06-08 16:12:10
:kiss:
niubo_
2009-06-08 16:18:17
哦?我居然变成把雷叔做掉的杀手了么?活活活……
hewenxie
2009-06-18 19:29:38
快更新。。。。。