【车载HIFI播放器开发】使用golang播放WAV,MP3,FLAC,OGG等音频
Contents
起因
这是车载 HIFI 播放器开发系列的一篇:整机控制逻辑用 Go 写、跑在车载 Linux 核心板上,需要程序化地播放本地音频。Go 生态里选了 faiface/beep——它把"解码"和"输出"分层抽象得很干净,四种格式一套接口。
使用的库:https://github.com/faiface/beep(可解析播放 WAV, MP3, OGG 和 FLAC)
需求
- Go 程序直接播放 WAV / MP3 / FLAC / OGG 文件
- 后续能响应停止、切歌等控制信号(车载场景)
技术实现原理
播放音频 = 解码 + 输出,beep 的分层
音频播放要干两件事:解码(把 MP3/FLAC 这类压缩格式还原成 PCM 采样)和输出(把 PCM 按采样率节拍喂给声卡)。beep 就是按这两层设计的:
- 解码层:
mp3、wav、flac、vorbis各一个包,Decode(io.Reader)都返回统一的beep.Streamer接口——一条 float64 采样流。接口统一,换格式只是换一个 Decode 调用; - 输出层:
speaker包基于 oto 库绑定各平台的系统音频 API(macOS 是 OpenAL,Linux 是 ALSA),后台 goroutine 按 Init 时定的采样率消费 Streamer,喂给声卡。
speaker.Init 的两个参数
- 第一个参数是采样率,必须和音频文件一致:声卡播放时钟按它走,文件是 44100 却 Init 成 48000,出来的就是变调变速;
- 第二个参数是缓冲大小,
format.SampleRate.N(time.Second/10)的含义是"1/10 秒能装多少个采样",即 100ms 缓冲。这是延迟与稳定性的平衡:缓冲太小,后台取数跟不上就断音、CPU 占用高;太大,停止/切歌的响应就慢。车载这种有频繁控制的场景,这个值值得单独调。
为什么 speaker.Play 之后要 select{} 阻塞
speaker.Play(streamer) 只是把 streamer 挂进混音器就立即返回,声音在后台 goroutine 里继续放。主 goroutine 一旦跑完退出,整个进程结束——音乐还没出声就没了。所以测试代码用 select {} 把主 goroutine 永久挂起。
这也是为什么实际工程里换成 for + select 监听控制 channel:既保住主 goroutine 不退出,又能随时响应停止/切歌指令。
代码实现
参考官方的Wiki文档可以写出如下测试文件:
|
|
代码说明
Open()只负责拿到文件句柄;Play()里mp3.Decode把文件流解码成 Streamer;speaker.Init(format.SampleRate, ...)用解码得到的真实采样率初始化输出;streamer.Len()返回总采样数,除以采样率就是时长;select {}阻塞主 goroutine(原理见上)。如果要实现停止操作,则使用 for 语句再配合 select 对通信信号进行监听就好了。
环境与编译警告
我这里使用的Go语言版本为1.13.8,操作系统为macOS Catalina,所以在编译过程中会出现如下警告,但不影响使用。
警告的原理:oto 在 macOS 上的后端是 OpenAL,而 Apple 从 macOS 10.15 起把 OpenAL 标记为 deprecated(官方主推 AVAudioEngine 替代),cgo 编译这些系统调用时就带出了 -Wdeprecated-declarations 警告。它只是提示 API 将来会被移除,不影响运行;而且部署目标是车载 Linux(走 ALSA 后端),完全不经过 OpenAL:
|
|
踩坑记录与注意事项
- 采样率不匹配会变调变速:44100 的文件按 48000 播,节拍和音高都不对。多首不同采样率的歌连播,用
beep.Resample统一重采样到 speaker.Init 的采样率,而不是反复 Init; - log.Fatal 会直接 os.Exit(1):defer 都不执行,工程代码里换成返回 error 由上层决定;
- defer streamer.Close() 在 select{} 永久阻塞期间不会执行:切歌场景要在 Play 下一首之前显式 Close 上一首的 streamer,否则文件句柄慢慢泄漏;
- 缓冲大小:100ms 是稳妥起点,断音就调大、要快响应就调小;
- 交叉编译到嵌入式 Linux:oto 依赖 cgo 和 alsa-lib,要
CGO_ENABLED=1并配好目标板的交叉工具链,直接GOOS=linux go build会失败。
Author 软件开发大郭
LastMod 2022-06-01