音频数据是原始PCM时直接写入文件播放无声,因缺少WAV格式头(RIFF头和fmt子块),需用github.com/hajimehoshi/ebiten/v2/audio.NewWAVEncoder自动构造合法WAV文件,或调用ffmpeg将PCM转MP3。

音频数据是原始 PCM,直接写入文件但播放无声?
Go 本身不内置音频编码器,os.WriteFile 或 io.Copy 写入的裸 PCM 数据(如 int16 样本)无法被系统媒体播放器识别——它们期待的是带格式头的文件(如 WAV、MP3)。直接写二进制流进去,文件后缀改 .wav 也没用,因为缺 RIFF 头和 fmt 子块。
常见错误现象:file.wav 在双击播放时提示“无法解析”或静音;用 ffprobe file.wav 报错 Invalid data found when processing input。
- 确认采集源输出的数据格式:采样率、位深(bit depth)、声道数(mono/stereo)、字节序(通常是小端)
- WAV 文件必须以标准 RIFF 头开头(44 字节),否则不是合法 WAV
- 不要手动拼接 WAV 头——容易出错;优先用成熟库生成头信息
用 github.com/hajimehoshi/ebiten/v2/audio 写 WAV 最省事
这个库虽为游戏音频设计,但它的 audio.NewWAVEncoder 能正确构造 WAV 头并封装 PCM 数据,且不依赖 CGO。适合快速验证采集链路。
假设你已从麦克风或网络拿到 []int16 样本(44.1kHz、stereo、16-bit):
立即学习“go语言免费学习笔记(深入)”;
import (
"os"
"github.com/hajimehoshi/ebiten/v2/audio"
)
func writePCMToWAV(samples []int16, sampleRate int, channels int, filename string) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
enc := audio.NewWAVEncoder(f, sampleRate, channels, 16)
defer enc.Close()
_, err = enc.Write(samples)
return err
}
-
audio.NewWAVEncoder自动写入 RIFF 头,你只需传原始样本切片 - 它内部按 little-endian 序写入,与主流硬件采集一致;若你拿到的是 big-endian,需先翻转字节
- 该 encoder 不支持 float32 PCM;若采集库输出
[]float32(如gordonklaus/portaudio),得先缩放转成[]int16
需要 MP3 或更小体积?绕不开 lame 或 ffmpeg
Go 原生无 MP3 编码能力。硬编只能调外部命令,软编得靠 CGO 绑定 C 库(如 github.com/mjibson/go-dsp 不含编码器,github.com/tcolgate/mp3 只解码不编码)。
最稳妥的实操路径是调用系统 ffmpeg:
cmd := exec.Command("ffmpeg", "-f", "s16le", "-ar", "44100", "-ac", "2", "-i", "-", "-c:a", "libmp3lame", "-y", "out.mp3")
stdin, _ := cmd.StdinPipe()
go func() {
defer stdin.Close()
binary.Write(stdin, binary.LittleEndian, samples) // samples 是 []int16
}()
cmd.Run()
-
-f s16le告诉 ffmpeg 输入是小端 16 位 PCM;-ar和-ac必须与实际采集参数严格一致,否则音调失真 - 确保目标机器已安装 ffmpeg 且在 PATH 中;Docker 部署时需在镜像里预装
- 不要用
os/exec启动后不等结束就返回——音频可能只写一半
实时写入大文件要注意缓冲和中断恢复
采集时间长时,别用 os.WriteFile 一次性写(内存爆炸),也别每帧都 f.Write(I/O 过多)。用带缓冲的 bufio.Writer,并监听信号做安全截断。
- 打开文件用
os.O_CREATE | os.O_WRONLY | os.O_APPEND,便于分段追加 - 写入前先写好 WAV 头(44 字节),后续只追加 data 子块;中断时用
os.Truncate修正末尾 chunk size 字段(需重新读写头) - 若采集过程可能被 kill(如 Ctrl+C),用
signal.Notify捕获os.Interrupt,关闭 encoder 并 flush 缓冲区
WAV 头里的 data chunk size 字段在文件开头,写完所有样本后才知其值——所以要么全缓存再写头,要么先占位、最后 seek 回去覆写。后者更省内存,但得小心文件系统是否支持随机写。


















