欢迎光临散文网 会员登陆 & 注册

11-对part9-sound的翻译和搬运

2023-08-16 22:14 作者:Xdz-2333  | 我要投稿

非黑色字体均为我自己添加

原文在文章末尾

从音频插口中播放声音

        我们游戏中丢失的一样东西就是声音!一些哔哔和吱吱声的加入会使得游戏更加吸引人.让我们动手吧!

        我写这些代码的时候参考了 Peter Lemon的工作 (https://github.com/PeterLemon/RaspberryPi/tree/master/Sound/PWM/8BIT/44100Hz/Stereo/CPU),因此我很感激.它需要做一些很重要的改动来在树莓派硬件上运行.

设计目标

        可能最终要的是,我们必须要在背景中播放音乐.如果我们的录音播放绑定在CPU上,播放音频时游戏就会暂停.我认为任何玩家都立即会对这种粗鲁的侵犯反感.

        一种解决方案就是实现多任务,通过利用四个CPU核(目前我们只用了一个).这不是一项小举动,对于几声哔哔声和吱吱声来讲是很大的投入.

        幸运的是,树莓派的硬件允许我们避免这种痛苦(对于现在),使用一个叫DMA的东西.这允许特定的硬件子系统来完全独立于CPU访问主存.

为自己记的笔记

        这些事我这趟旅途上所学到的一些东西,它们帮了很重要的忙:

  • 当GPIO40和41映射到选择函数0时,树莓派使用PWM1作为音频插口的输出

  • 树莓派时钟震荡频率是54M赫兹

  • PWM1的DMA被映射到通道1

  • PWM1被映射到DREQ1

  • 我们必须使用遗留的主地址(从0x7E开始而不是0xFE)来为DMA转移数据到外设上

  • DMA控制块的结构必须32位对齐

        并且总是要开启浏览器ARM外设文档(https://datasheets.raspberrypi.com/bcm2711/bcm2711-peripherals.pdf)的标签.它对于寄存器地址和位图有非常详细的参考.

音频采样格式

        audio.bin 是我们将要播放的音频文件.从技术上讲,它是8-bit的44.1K赫兹的无符号PCM数据.在现代它并不是一个常见格式,但是可以用ffmpeg(https://ffmpeg.org/)等工具进行转换.

        为了将我们的 .bin 文件转换为可以被电脑播放的 .wav 文件,做这个:

ffmpeg -f u8 -ar 44.1k -ac 2 -i audio.bin audio.wav

        将其转换为我们的二进制文件,做这个:

ffmpeg -i audio.wav -f u8 -ar 44.1k -ac 2 audio.bin

        这将帮助你对你自己的音频采样文件进行编码!这是我的歌声的一段(啊!),伴随着我妻子的木管乐器.另一旁,我十分推荐原始记录(https://www.youtube.com/watch?v=k1UoUNC3Wj0)

使用CPU和PWM模块测试录音

        我知道DMA传输可能比较棘手,所以我从证明我能从树莓派的接口输出音频开始.

        看到main(),你可以看到第一个函数调用 audio_init().这个函数确保PWM1被正确映射.PWM是用于使用数字信号控制模拟信号的技术.由于数字信号只有开(1 - 全功率)和关(0 - 没有功率),模拟信号可能是0到1之间无限位数字的一个值.PWM通过快速脉冲/脉冲调节电压来伪造模拟信号.产生的平均电压最终看起来像一个模拟信号,尽管它不是.明白了?

        关于PWM的一点补充:

        由于数字设备只有0和1,它是离散的,因此只能周期性的发出高低两种电平,这在示波器上看起来就是方波,PWM可以调节的参数有三个,频率,占空比和电压,它主要是通过不同的占空比来达到不同的平均输出功率,从而驱动电机,音频等设备.

        这些脉冲/爆发确实需要很高的精确度来使其工作,所以我们需要可靠的时钟源.就像我们的厨房的时钟每秒滴答一下,树莓派上的振荡器有一个常规的"滴答" -- 在这个例子里 , 54,000,000 次每秒(54M赫兹).我们的音频采样在44.1k赫兹,所以我们需要使其"慢下来".我们做到这一点,首先通过停止时钟,然后设置一个分频器,设置PWM的范围,然后再次开启时钟.在我的代码里,我使用2作为时钟分频(所以我们降到了27M赫兹),然后把范围设置到612(0x264).实际上,这意味着我们的PWM模块会在 27,000,000/612 次每秒移动到下一个采样上 -- 大约等于 44.1k赫兹,它恰好是被包括的音频采样文件 audio.bin 的采样频率(我已经包括了 audio.wav 所以你也可以在你的电脑上正常的听它!)

        关于时钟的一点补充:

        电路里面的时钟和平常说的时钟概念不太相同,它本质上是固定频率的电流脉冲.不过万事万物只要是做周期性运动的都能拿来计时,和尺子一样,如果一个尺子最小刻度是1mm,那么它就测量不了1mm以下的物体的长度,同样的,如果电路中的时钟每秒有1千个脉冲,那么它就不能用来表示1ms以下的时间,因此时钟频率越高,对于时间的计量就越精确.另外,时钟在数字电路中经常被用来做同步信号,因此时钟频率越高,数字电路的状态转换速度越快.不过频率过高也是有缺点的,因为电路的功耗和晶体管电压翻转频率的四次方成正比,所以过高的频率会带来严重的积热.

        那么它是怎么来的呢,一般有两种方法,第一是振荡电路产生,不过这种方法产生的时钟频率不高,质量也达不到要求.另一种就是晶振,利用不同频率的电流通过石英晶体的时候,特定的频率会与晶体共振,而其他的频率被衰减这一特点得到高质量高频率的脉冲电流,然后经过PLL电路进行倍频或者分频的到更高频率的脉冲,再传输给各个部件使用.因此数字设备上会有一个"时钟树",这个搞过FPGA开发的同学就知道是怎回事了.

        当我们启动PWM模块的时候,告诉他等待它的FIFO输入的数据,然后我们继续.直到我们开始填充缓冲区,没有音频播放.

        希望你可以注意到我们同样设置了通道0和通道1.这是因为我们使用了立体声采样.

开始音频播放

        在 playaudio_cpu()中我们使用CPU来驱动我们的采样数据到FIFO缓冲区中.这些采样数据被编译到内核中,于我们在part7-bluetooth中对蓝牙固件所作的那样进行引用(我们还是没有完成SD卡的文件访问.抱歉!).

        这个代码相当的自我说明化.在我们向左通道和右通道发送字节之间我们基本上只检查了FIFO缓冲是否被填满(记住,立体声!).如果我们看到了错误,我们清除它并继续.

        这就是了...PWM会接收缓冲区中的数据并且发送它,PWM-风格(冒充一个模拟信号),在一个正确的速度上(感谢我们的时钟分频器和PWM范围调控器),发送给音频接口.

使用DMA完成

        在palyaudio_dma() 中我们第一个挑战是DMA运输和FIFO寄存器是4个字节宽,所以我们的单字节采样需要零填充.因为数据是 unsigned char * 并且 safe 是(技术上的) unsigned int *,我们可以使用一个简单的循环来完成它. 你会注意到safe是一个RAM内的内存地址,我们知道它超越了我们的代码(io.h 里面定义了 SAFE_ADDRESS ).

        我们设置DMA控制模块.这是一个内存结构,它将告诉DMA引擎执行传输所需要知道的一切.它有非常多的文档,但是值得指出的是DMA_DEST_DREQ 和 DMA_PERMAP_1.这些设置确保我们使用之前设置好的硬件时钟来"同步"运输.如果我们不这么做,DMA引擎将要尽快完成它(我们的音频听起来不会很好!).SRC_INC 只是告诉DMA引擎在运输过程中递增源地址.因为我们将要保持目的地址为常量,因为我们希望它总是指向PWM模块的FIFO输入.同样注意PWM_LEGACY_BASE的使用而不是PWM_BASE来寻址外设内存.这是树莓派4硬件的另一个怪事!

        注意我们最后设置 .nextconbk 为 0x00.这是告诉DMA引擎这个任务完成后没有什么要做的了.如果我们需要无限循环这段音频(没有人那么需要我的歌声!),我们可以再次简单的设置地址到同一个块控制结构体.

        随着我们启动DMA引擎,音频开始播放.然而,值得注意的是,我们立即回到mian(),CPU可以开始其他的事情,因此达到了我们的目的.

结论

        我们现在准备将声音整合到我们的Breakout游戏中,但与其这样做,不如让我们进入兔子洞,看看我们是否不能在单独的核心上运行CPU播放!我说过这很难,但我喜欢挑战.



原文如下

Playing sound from the audio jack

One thing our game is missing is the excitement of sound! Some beeps and squeaks would be a wonderful addition to make the gameplay more compelling. Let's work to do just that!


I wrote this code as I referenced [Peter Lemon's work](https://github.com/PeterLemon/RaspberryPi/tree/master/Sound/PWM/8BIT/44100Hz/Stereo/CPU), for which I am very grateful. It did need some significant modification to work on the Raspberry Pi 4 hardware.


Design goal

Perhaps most importantly, we must be able to play sounds in the background. If our audio playback ties up the CPU, then gameplay will stop whilst the sound is playing. I think any player would be immediately put off by the rude intrusion into their adventure!


One solution for this is to implement multi-tasking, thereby making use of the four CPU cores (so far we've only used one). This is no small feat, and a big commitment for a few beeps and squeaks.


Fortunately, the Raspberry Pi 4's hardware allows us to avoid this pain (for now), using something called DMA. This allows specific hardware subsystems to access main system memory completely independently of the CPU.


Notes to self

Here are a few things I learned on this journey, which helped me along significantly:


 * The Raspberry Pi 4 uses PWM1 for output on the audio jack when GPIO 40 and 41 are mapped to Alternate Function 0

 * The Raspberry Pi 4's clock oscillator frequency is 54 MHz

 * PWM1 DMA is mapped to DMA channel 1

 * PWM1 is mapped to DREQ 1

 * We must use Legacy Master (starting `0x7E`, not `0xFE`) addresses for DMA transfers to peripherals

 * The DMA Control Block structures must be 32-bit aligned


And always, always have a browser tab open on the [BCM2711 ARM Peripherals document](https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2711/rpi_DATA_2711_1p0.pdf). It's a very handy reference for register addresses and bitmaps etc.


Audio sample format

_audio.bin_ is the audio file we'll be playing. Technically speaking, it's 8-bit, unsigned PCM data at 44.1 KHz. This is an unusual format in this modern day and age, but it's easily converted using a tool like [ffmpeg](https://ffmpeg.org/).


To convert from our _.bin_ file to a _.wav_ file that any laptop can play natively, do this:


`ffmpeg -f u8 -ar 44.1k -ac 2 -i audio.bin audio.wav`


To convert back to our binary format, do this:


`ffmpeg -i audio.wav -f u8 -ar 44.1k -ac 2 audio.bin`


This should help you try the code with your own audio samples! This one is a short excerpt of [me](https://isometim.es) singing (argh!), with my wife playing woodwind. As an aside, I highly recommend checking out [the original track](https://www.youtube.com/watch?v=k1UoUNC3Wj0).


Testing playback using the CPU and PWM module

I knew DMA transfers might be a tricky beast, so I began by just proving I could play audio to the jack output of the Raspberry Pi 4.


Looking at `main()`, you'll see that we first call `audio_init()`. This function ensures that PWM1 is correctly mapped. PWM is a technique used to control analogue devices using digital signals. Whilst digital signals are either on (1 - full power) or off (0 - no power), analogue signals may be an infinite number of values between 1 and 0. PWM fakes an analogue signal by applying power in quick pulses/bursts of regulated voltage. The resultant average voltage will end up looking roughly like an analogue signal, despite not being one. Clever, eh?


These pulses/bursts do need to be highly accurate for this trick to work, and so we need a reliable clock source. Just like your kitchen clock ticks every second, so the oscillator on the Raspberry Pi 4 has a regular 'tick' - in this case, 54,000,000 times per second (54 MHz)! Our audio sample is at 44.1 KHz though, so we need to 'slow it down'. We do this by first stopping the clock, then setting a clock divisor, setting the PWM range, and enabling the clock again. In my code, I use 2 as the clock divisor (so we're down to 27 MHz) and set the range to 612 (0x264). Essentially, this means that our PWM module will move to a new sample every 27,000,000/612 times per second - roughly equivalent to 44.1 KHz, which just happens to be the sample rate of the included audio sample _audio.bin_ (I've included _audio.wav_ so you can listen normally on your laptop too!).


We then enable the PWM module, telling it to wait for sample data on its FIFO input, and we're good to go. Until we start filling the buffer, no audio will play.


Hopefully you'll notice that we set both channel 0 and channel 1 up similarly. This is because we're working with a stereo sample.


Starting the playback

In `playaudio_cpu()` we use the CPU to drive our sample data into the FIFO buffer. The sample data is built into the kernel and referenced exactly as we did with the Bluetooth firmware file back in part7-bluetooth (we still haven't done SD card file access, sorry!).


The code is fairly self-documenting. We essentially check the FIFO buffer isn't full before we send the left channel byte and the right channel byte (stereo, remember!). If we see errors, we clear them as we go.


And that's it... The PWM will pick up the digital data in the buffer and send it, PWM-style (faking an analogue signal), at the right speed (thanks to our clock divisor/PWM range mastery), to the audio jack.


Doing it with DMA

In `playaudio_dma()` our first challenge is that DMA transfers and FIFO registers are 4 bytes wide, so our single byte samples need some zero-padding. Because `data` is an `unsigned char *` and `safe` is (technically) an `unsigned int *`, we can do this copy with a simple _for loop_. You'll notice that `safe` is a memory location in RAM which we know to be beyond our program code (`SAFE_ADDRESS` is defined in _io.h_).


We then set up the DMA Control Block. This is an in-memory structure that will tell the DMA engine everything it needs to know to perform the transfer. This is very well-documented already, but worth pointing out is `DMA_DEST_DREQ` and `DMA_PERMAP_1`. These settings ensure that we use our previously-set hardware clock to 'pace' the transfer. If we didn't do this, the DMA engine would just get it done as fast as it could (and our audio wouldn't sound great!). `SRC_INC` simply tells the DMA engine to increment the source address throughout the transfer. We'll be keeping the destination address constant though, since we want it to always point to the PWM module's FIFO input. Note also the use of `PWM_LEGACY_BASE` rather than `PWM_BASE` to address this peripheral memory. This is another quirk of the Raspberry Pi 4 hardware!


Note finally how we set `.nextconbk` to 0x00. This tells the DMA engine that there is no more work to do after this job is complete. If we wanted to loop the audio sample infinitely (nobody needs that much of me singing!), we could simply set this to address the same control block structure again.


As we enable the DMA engine, playback begins. Notably, however, we're returned to `main()` immediately and the CPU can get on with other things, thereby meeting our design goal.


Conclusion

We're now ready to integrate sound into our Breakout game. But rather than do that, let's go down a rabbit hole and see if we can't get CPU playback running on a separate core! I said it was hard, but I love a challenge.



11-对part9-sound的翻译和搬运的评论 (共 条)

分享到微博请遵守国家法律