简介这是一份面向嵌入式Linux初学者的Qt应用开发实例基于S3C6410开发板实现“欢乐颂”音乐播放与跑马灯控制。资源定位清晰涵盖Qt GUI编程、GPIO操作、蜂鸣器驱动及按键事件监听等关键知识点适合正在学习Linux开发板应用开发或需要课程设计参考的开发者。压缩包共10个文件以3个C源文件、2个头文件为核心代码另含Qt工程文件pro、资源文件qrc、界面预览图片及工程配置整体仅35KB结构精简便于快速阅读目前已有307人学习。借助这套工程代码读者可以直观地看到如何在Linux环境下使用Qt Creator编写嵌入式交互程序理解音乐播放模块的音频处理思路以及通过sysfs接口操作GPIO控制LED跑马灯的实现方法。整体代码量不大但逻辑完整是入门嵌入式Qt开发的良好范例也可作为实际项目中音频与LED控制的参考实现。1. 在Linux开发板上用Qt实现《欢乐颂》入门到放弃只有三步很多刚接触嵌入式Linux的人看到“qt实现Linux开发板欢乐颂”第一反应是不就是放个MP3吗真把工程建起来、交叉编译推到板子上之后才发现这条路上一共有三道坎第一道是Qt的交叉编译环境第二道是音频设备节点的访问权限第三道是底层音频栈和Qt时间线的对齐。这三道坎任何一个过不去屏幕上那个钢琴界面都只能是摆设。这个项目最适合两类人一类是刚拿到一块Linux开发板、想验证Qt部署流程的人另一类是已经跑过Qt界面、想往工程里加音频和后台线程的人。做完之后你手里会有一套带音频合成、自绘界面和板级适配的完整Demo而不是一个空壳窗口。2. 音频方案怎么选QMediaPlayer、QSoundEffect、还是自己合成PCM开始动手之前先解决一个问题《欢乐颂》在开发板上用什么方式“响”出来。要响不外乎三种路子播放音频文件、播放系统音效、直接生成PCM音频流。这一章先把三条路的边界讲清楚再给你一个能直接落地的选择。2.1 QMediaPlayer播放音频文件最省事但最容易被开发板坑在PC上写Qt要放音乐第一反应是QMediaPlayerQMediaPlayer *player new QMediaPlayer(this); player-setMedia(QUrl::fromLocalFile(/home/user/joy.wav)); player-setVolume(80); player-play();这段代码在PC上一点问题都没有但放到Linux开发板上问题接踵而至。QMediaPlayer在Qt 5里走的是多媒体后端框架常见后端是GStreamer而在很多裁剪过的嵌入式系统里GStreamer的插件和音频输出插件比如alsa sinks根本没打进rootfs。你部署完程序播放时控制台会刷出一堆“No such element”或“decodebin error”的报错界面还在声音就是出不来。就算后端插件都在Flash类的小板子解码一个MP3要占30%以上的CPU界面掉帧是常有的事情播放时点按钮要等一两秒才响应。结论除非你的rootfs里有完整的GStreamer插件集和硬件解码驱动否则不要用QMediaPlayer做这个Demo。它把解码器、音频管道、音量通路都包成黑匣子出了错你连从哪查起都不知道。2.2 QSoundEffect播放WAV能响但做不了《欢乐颂》的多音符合成有人会说那我用QSoundEffect不就行了它底层用音频引擎直接播放WAV不依赖解码框架延迟也低。这个方向是对的但QSoundEffect的设计场景是“短音效”不是“一首曲子”。有个明显的坑QSoundEffect播放WAV文件时同一个QSoundEffect实例如果要换音高必须重新加载文件。做《欢乐颂》需要对每个音符切换频率要么给每个音符准备一个WAV文件要么动态打印WAV头然后交给QSoundEffect播放。给61个半音准备61个WAV文件这不现实动态生成WAV再让QSoundEffect加载虽然可行但每次切换音符的文件加载和内存拷贝会产生几十毫秒的间隙曲子听起来一顿一顿的。所以QSoundEffect适合做按键反馈不适合做旋律合成。如果你只在界面上摆七个按钮按一下响一个音阶QSoundEffect是个好选择如果你想让《欢乐颂》自动连起来跑放弃这条路。2.3 我推荐的方案合成正弦波 QAudioOutput 推流必要时兜底ALSA真正的可行做法是自己生成PCM数据用QAudioOutput以push模式连续喂音频设备。核心逻辑分三块音符表转频率、按节拍合成波形、把样本推给输出设备。先给一个音符频率生成片段后面建工程的时候会用到double noteFrequency(int midiNote) { // A4 440HzMIDI编号6912平均律每差1个半音频率乘2^(1/12) return 440.0 * std::pow(2.0, (midiNote - 69) / 12.0); }用MIDI编号的好处是《欢乐颂》的乐谱可以直接转成数字序列不用每次查频率表。比如中央C是MIDI 60D是62E是64F是65G是67A是69。下面第4章写乐谱就基于这一条公式。为什么选QAudioOutput而不是直接用ALSA因为QAudioOutput能挂到Qt的事件循环上配合QTimer的节奏控制更容易而且Qt的音频插件QLinuxAudioPlugin在嵌入式板上就是封装ALSA的本质一样但接口更安全。如果QAudioOutput在你的板子上持续卡顿后面避坑章会讲一个常见原因再直接降到ALSA调snd_pcm_writei做最后一层兜底。3. 搭建Qt交叉编译环境SDK、sysroot和设备插件一次配明白方案定下来之后下一步不是写代码是让Qt跑到Linux开发板上。很多人的Demo死在第一步不是代码写得不对而是编译出来的二进制跟板子的运行库不匹配。3.1 先做三个工具链验证再进Qt配置拿到一块Linux开发板先别急着装Qt先把三件事确认掉从我的习惯来说这三件事不做后面排查要花十倍时间第一确认交叉编译器版本arm-linux-gnueabihf-gcc --version如果输出里没有明显的“arm”和“hf”硬浮点标识或者板子是armv8但编译器是armv7的编出来的程序很可能跑到半路就Illegal instruction。很多开发板的SDK里带了专门的工具链前缀比如arm-ox0-linux-gnueabihf-这种直接用SDK自带的别用系统里装的通用交叉编译器。问一下拿到板子的sdk目录里是否有编译器文件夹有就用它的。第二确认板子的浮点运行库是否完整最简单方法是有条件地把编译器的sysroot与板子的/lib、/usr/lib做一次对比ls -l /lib/ld-linux-armhf.so.3如果根文件系统里没有ld-linux-armhf.so.3那板子可能是纯软浮点或aarch64的类型这时候交叉编译器也得对应换。建议用上述命令在板子上跑一遍再继续。第三确认音频设备节点存在cat /proc/asound/cards如果没有输出说明板子的声卡驱动没加载后面Qt怎么写都没声音。至少应该看到一块“xxx codec”之类的声卡。3.2 编译一套面向设备的Qt参数这样给拿到确认过的工具链后下载对应版本的Qt源码我一般用Qt 5.15 LTS范围最适合嵌入式模块全、插件多底层alsa插件是默认编译进去的然后一次性配置编译./configure -prefix /opt/qt-arm \ -xplatform linux-arm-gnueabi-g \ -release -optimized-qmake \ -no-opengl \ -no-feature-eglfs \ -linuxfb \ -no-xcb \ -no-dbus \ -no-gstreamer \ -qt-zlib -qt-libpng -qt-libjpeg \ -nomake examples -nomake tests make -j4 make install这里把GStreamer关掉是故意的前面说过嵌入式板子上后端插件不齐与其让QMediaPlayer在后端缺库不如一开始就不编它。加了-linuxfb就把Qt的帧缓冲插件打开这样不带X11/Weston的板子也能显示界面。-no-opengl适合没有GPU的Cortex-A7/Cortex-A9系列有GPU的板子要开eglfs养起硬件加速。内存小的板子建议把QtWebEngine这类重型模块关掉反正这个Demo用不上。3.3 最小工程跑通先让一个窗口亮起来交叉编译环境好了别直接写《欢乐颂》先写一个最简的QWidget工程验证环境链路。widget.pro如下QT core gui widgets TARGET helloJoy TEMPLATE app SOURCES main.cppmain.cpp#include QApplication #include QLabel int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label(Hello JoySun); label.show(); return app.exec(); }编译/opt/qt-arm/bin/qmake make编译完helloJoy用adb或scp或U盘拷到板子上执行方式chmod x helloJoy export QT_QPA_PLATFORMlinuxfb export QT_QPA_FB_DRM1 # 如果板子内核支持DRM ./helloJoy看到屏幕上出现“Hello JoySun”说明Qt的显示通路是好的下一步再往里填音频。有两个export缺一不可第一个指定Qt使用linuxfb平台插件Qt在嵌入式Linux上默认没有X11走不指定平台插件会直接报“could not find a Qt platform plugin”退出第二个是告诉linuxfb后端用DRM提交画面避免用传统mmap方式在某些新内核上出现花屏。4. 把《欢乐颂》编进程序音符序列、波形合成与音频输出显示通了接下来是整个项目最核心的部分让曲子响起来。这一章分三步做先用文本序列定义乐谱然后写合成器生成PCM样本最后把样本通过QAudioOutput输出。4.1 乐谱转成MIDI音高序列节拍从数组里读《欢乐颂》的主旋律是4/4拍主题句是大家熟悉的“3 3 4 5 5 4 3 2 1 1 2 3 3 2 2”。用简谱表示就是mi mi fa sol sol fa mi re do do re mi mi re re。转成MIDI编号就是E4(64) E4(64) F4(65) G4(67) G4(67) F4(65) E4(64) D4(62) C4(60) C4(60) D4(62) E4(64) E4(64) D4(62) D4(62)。节奏上第一拍、第四拍的音符时值长第二三拍短我会用一个结构体来保存每个音符的MIDI号和拍长struct Note { int midi; // 0 表示休止 float beats; }; Note melody[] { {64, 1.0f}, {64, 1.0f}, {65, 1.0f}, {67, 1.0f}, {67, 1.0f}, {65, 1.0f}, {64, 1.0f}, {62, 1.0f}, {60, 1.0f}, {60, 1.0f}, {62, 1.0f}, {64, 1.0f}, {64, 1.5f}, {62, 0.5f}, {62, 2.0f}, {0, 0.5f}, // 休止半拍卡准换气口 };注意这里把“多瑞米多”那段的“发”处理成一拍半加半拍的休止这样可以避免相邻音符堆叠导致的爆音。为什么用0表示休止而不是跳过因为如果不写休止符合成器会立刻接下一个音符的波形两个波形相位如果没对好会出现“啪”的杂音。休止符本质上是给包络一个release时间消除掉毛刺。后面会处理。4.2 合成器正弦波加包络防爆音的关键把音符变成音频样本用最简单的正弦波合成。但直接每帧用std::sin计算会浪费CPU优化方式是累积相位每次只做一次sinclass ToneGenerator { public: ToneGenerator(int sampleRate 22050) : m_sr(sampleRate), m_phase(0.0), m_phaseInc(0.0) {} void setFrequency(double freq) { // 每秒要走多少个采样点一个正弦周期占freq个采样 // 所以每个采样点增量是 2*pi*freq/sr m_phaseInc 2.0 * M_PI * freq / m_sr; } void generate(std::vectorint16_t buffer, int count, double attack, double release) { buffer.resize(count); for (int i 0; i count; i) { double t static_castdouble(i) / count; double env 1.0; if (t attack) env t / attack; if (t 1.0 - release) env (1.0 - t) / release; double sample std::sin(m_phase) * env; buffer[i] static_castint16_t(sample * 32767 * 0.8); m_phase m_phaseInc; if (m_phase 2.0 * M_PI) m_phase - 2.0 * M_PI; } } private: int m_sr; double m_phase; double m_phaseInc; };这里有几个参数不是随便给的。采样率选22050Hz而不是44100因为Cortex-A7这类板子的音频编解码芯片大多标称支持48kHz但实际上内置的I2S控制器在22050Hz下PCM缓冲区吞吐量减半CPU中断压力小很多。音量系数0.8是给ALSA混音器留了余量避免整曲叠加后削波。包络的attack和release我实际取0.01和0.05attack太音乐会“冲”耳机release太短会“切”波形。为什么不能只是连续sin而不做包络因为音符之间如果没有下降沿相位突变会在耳机里形成宽带脉冲噪声就是俗称的爆音。包络的引入让每个音符的起落都经过一个短斜坡实测爆音立刻消失。4.3 push模式输出QAudioOutput的缓冲与线程说明有了样本数据下一步交给QAudioOutput。常见做法是用一个QIODevice子类配合open(QIODevice::WriteOnly)模式或者直接start()后不断write我一般用后者更直观QAudioFormat fmt; fmt.setSampleRate(22050); fmt.setChannelCount(1); fmt.setSampleSize(16); fmt.setCodec(audio/pcm); fmt.setByteOrder(QAudioFormat::LittleEndian); fmt.setSampleType(QAudioFormat::SignedInt); QAudioOutput *audio new QAudioOutput(fmt, this); QIODevice *dev audio-start(); audio-setVolume(0.7);然后在一个定时器里不断喂数据每次先判断剩余缓冲量再写QTimer *timer new QTimer(this); connect(timer, QTimer::timeout, this, []() { const int chunkSamples 1024; while (audio-bytesFree() chunkSamples * 2) { std::vectorint16_t buf; m_gen.generate(buf, chunkSamples, 0.01, 0.05); dev-write(reinterpret_castconst char *(buf.data()), chunkSamples * 2); m_written chunkSamples; // 已经写完当前音符所需要的样本数切换到下一个音符 if (m_written currentNoteDuration * 22050) { if (m_idx noteCount) { timer-stop(); return; } setNextNote(); } } });这里有一个关键设计QAudioOutput的缓冲是内部线程从ALSA来回报数据的假如定时器刷得太快磁盘IO、UI绘制和音频写入挤在同一个线程板子上会明显出现“破音”。所以我把写音频放在一个普通间隔20毫秒的QTimer里每次查bytesFree再写不盲目写满。bytesFree返回的是音频输出缓冲的空闲字节数单位是字节1024个采样乘2字节正好是每块的大小。块大小别设太大256乘2测试过有卡顿1024乘2在大多数板子上都顺滑如果你的板子爆音把块大小调成512再试。这一整节的逻辑串起来就是Timer触发生成PCM → bytesFree判定可以写 → 写入设备 → 写满当前音符就切换下一个音符。计时依赖QTimer不是最精确但对一首不需要卡秒的曲子完全够用。5. 开发板音频实现避坑排查5个必踩的坑和解决办法这部分我把压箱底的经验都放在这。每一个都是几乎必现的坑不是玄学是底层机制造成的。按“现象→原因→解决”写你照着排查即可。5.1 程序跑起来没声音ALSA报错现象Qt界面正常亮起来控制台刷出类似“ALSA function snd_pcm_open failed: No such file or directory”或“cannot open control”的报错音量调到最大也没声音。原因开发板的声卡设备节点/dev/snd/pcmC0D0c和pcmC0D0p不存在或者当前用户没有访问/dev/snd/*的权限。很多Linux开发板默认rootfs不带udev的音频设备rule导致声卡节点没创建或者用户不是audio组而是root才可访问。解决先确认声卡寄存器是否识别到了codec。执行cat /proc/asound/cards如果看到类似“0 [xxx] : xxx - xxx”的内容说明声卡在只是节点权限问题chmod 777 /dev/snd/* 是临时手段正规做法是把运行程序的用户加入audio组usermod -aG audio root如果是你自己启的进程先手动挂载ls /proc/asound/cards aplay -l如果aplay -l也报错说明内核里声卡驱动模块没加载比如snd-soc-xxx.ko没insmod。这种时候别怀疑Qt程序先用测试ALSA本身是否正常的命令排查一遍比盯Qt日志有效得多。5.2 声音断断续续一卡一卡像断轴现象《欢乐颂》旋律播放时每隔几百毫秒顿一下听起来像旧电脑放在线音频。CPU占用不高的时候也这样。原因最常见原因是Qt的QAudioOutput默认使用系统alsa混音设备而alsa dmix重采样线程在高负载下会造成period中断不稳定。另一个常见因素是我在上面设定的write块大小和目标采样率不匹配块太大导致ALSA buffer underrun。解决把采样率降到22050已经是推荐值把QAudioOutput的bufferSize手动设一下具体做法是start之前调用audioOutput-setBufferSize(8192)或者减少定时器的触发间隔到10ms。另一个更见效的做法是把ALSA设备从默认的“default”改成“plughw:0,0”让ALSA绕开软件混音直接硬件PCM输出export ALSA_CARD0 export ALSA_PCM_CARD0运行前在板子上试这句aplay -D plughw:0,0 test.wav如果这样播放没有断续而aplay默认设备断续就在程序里把QAudioDeviceInfo::defaultOutputDevice换成一个手动的设备或者干脆用QProcess调aplay兜底。5.3 播放音频时Qt界面卡死按钮点了没反应现象曲子播放到一半UI完全无响应按键点击无反馈过几秒后整个进程被系统杀掉或重启。原因音频定时器里做的工作太重。我见过有人直接在QTimer回调里做std::vector重新分配、做动态波形可视化把音频合成、FIFO读写和Widget重绘全部压在GUI线程里。当板子CPU吃紧时GUI线程来不及处理窗口系统消息Qt的事件循环就被阻塞。更别提到音频设备的write是阻塞调用缓冲满时它会一直等直接卡住调用线程。解决把音频I/O和合成器移到单独的QThread里GUI线程只接收进度信号。我在第4章已经用了QTimer但不代表推荐那只是演示最小化如果你要做完整项目请改成音频线程。class AudioThread : public QThread { Q_OBJECT protected: void run() override { m_running true; while (m_running) { if (audio-bytesFree() chunkBytes) { writeChunk(); } else { QThread::usleep(2000); } } } };这样音频线程独立睡眠等待GUI线程的刷新完全不受影响。Cortex-A7单核板子实测卡死率从100%降到0。需要警惕的是QAudioOutput是Qt主线程创建的跨线程调用要先移动QObject到音频线程不然还是会在内部触发队列同步性能损耗极大解决办法是音频线程内部new出QAudioOutput。5.4 音符开头有明显的“啪”爆音现象播放《欢乐颂》时每个长音的开头都有一声低沉的“啪”像是用指甲弹桌面。PC上播放同一段PCM没有板子上就有。原因alsa的PCM输出在块与块之间如果样本不连续DAC会产生一个宽谱脉冲。我在4.2已经加了包络但如果块大小是1024而音符恰好落在第1023个样本时被切掉下一个音符直接从第0个样本开始充满幅值包络还没起就爆了。另外一个原因是板子的I2S与codec的左右声道时钟没有完全对齐间隙里填充的样本不是静音而是上一个样本的符号翻转。解决一是给每个音符的结尾生成一个“短停顿”静音样本比如在release之后补32个静音采样二是把包络的release时间从0.01改成0.05让切点永远不会落在一个高斜率处。如果仍然爆音检查一下I2S的时钟极性配置这个通常在设备树里节点名为xxx_i2s和xxx_codec的status和clock-names。用示波器看波形其实很少人做可以先建议用以下方法验证把合成的PCM以文件形式保存一份放到PC的Audacity里放大检查每个音符边界是不是从零点附近过渡的。5.5 开机自启后音频没声音但手动运行正常现象把程序加入rc.local或systemd服务后开机自启界面能起来但音频设备拿不到日志里有EACCES或无权限错误。手动在终端里sudo ./app却正常。原因systemd的沙箱限制和音频设备节点的访问权限导致。rc.local启动时/dev/snd节点可能尚未被udev初始化完成systemd服务则默认没有继承用户的supplementary groups即使root也可能丢掉audio组权限另外某些开发板的音频驱动要开机后等若干秒才注册完成。解决先给服务写一个明确的音频设备依赖[Unit] DescriptionJoySun Player Aftersound.target Wantssound.target [Service] Typesimple ExecStart/home/root/apps/JoySun Restarton-failure Groupaudio在systemd文件中加上Groupaudio再在启动前加一秒钟延迟我甚至在ExecStart前面加过/bin/sleep 2虽然很土但实测管用。最稳妥的做法还是加上Aftersound.target。如果板子用的是sysvinit在脚本里先写sleep 1; aplay -l 确认声卡节点再启动程序。6. 让《欢乐颂》看得见加一个轻量频谱可视化验证音频链路闭环很多开发板Demo做到能响就停了但你也知道光能响无法长经验。这里建议给《欢乐颂》加一个轻量频谱可视化它有一个额外好处它能实时验证你的QAudioOutput缓冲区是否真的挤满数据。如果频谱条不动说明缓冲没工作是物理层的bug不是逻辑层的问题。可视化技术上别走重的路线。嵌入式板子用QPainter逐帧绘制是可行的但要注意绘制区域。不要全窗口重绘而是用一个上下限差距为20毫秒的局部更新仅更新的几个条带矩形。代码上非常短// 在widget的paintEvent里只绘制缓存好的条带矩阵 void JoyWidget::paintEvent(QPaintEvent *event) { QPainter p(this); p.fillRect(rect(), Qt::black); for (int i 0; i m_barCount; i) { int h m_barHeights[i]; p.fillRect(i * barWidth, height() - h - 1, barWidth - 3, h, Qt::green); } }数据的来源不用自己去采集模拟信号直接从PCM缓冲里读。因为我们已经把样本写在内存里用一个峰值统计函数同时做可视化数据源void computeBars(const int16_t *data, int len, int *bars, int nBars) { // 只做简单的分桶RMS避免嵌入式上做FFT耗时太长 int perBucket len / nBars; for (int i 0; i nBars; i) { int64_t sum 0; for (int j 0; j perBucket; j) sum (int64_t)data[i * perBucket j] * data[i * perBucket j]; bars[i] int(std::sqrt(sum / perBucket)) / 300; } }RMS分桶能体现音量随旋律起伏的强弱对《欢乐颂》这类强节奏旋律够用。如果是平直波形的Rect输出会显得很呆加一个“峰值保持”功能每个栏保存一个最高值每帧下降2像素实现类似均衡器的下落效果。这个技巧可以用两个int数组存当前值和下降值实现。然后让这个Widget在主窗体的一个固定区域里以30ms间隔刷新即可。记得在timer时间片里只对update调用不要调用repaintrepaint是同步重绘会卡住音频线程的写操作。30ms的刷新率人眼足够平滑Cortex-A7单核完全扛得住。做完这一步你的“qt实现Linux开发板欢乐颂”就不只是一个能响的播放器了。它成了一块试验田改合成器能得到不同音色改分桶数量能得到不同粒度的频谱节奏把条形图换成波形滑动窗口又能变成示波器风格的Demo。回想我自己头一回在开发板上做类似功能调试爆音花了整整两晚最后发现是一句笔误把sample size设成32位导致alsa和qt之间的位深不匹配。那次之后我就养成一个习惯任何音频问题先怀疑位深和采样率而不是怀疑线程和驱动因为这类问题最容易隐藏又最致命。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?