Go beep 怎样把两个音频流混合播放
来源:17golang原创
时间:2026-10-31 05:40:11 166浏览 收藏
Go beep 混合两个音频流的核心是先把音频解码成 beep.Streamer,再让它们进入同一个输出采样率,最后交给 beep.Mix 同时播放。两个文件长度不一样时,混音流会在较长的输入结束后才结束;如果播放中还要追加音频,则应改用可动态管理的 beep.Mixer。
官方地址:https://pkg.go.dev/github.com/gopxl/beep
先分清一次性混音和动态混音
beep.Mix(s1, s2) 适合“已有两个有限音频,现在一起播放”的场景。它把多个 Streamer 叠加到一个输出流里,适合背景音乐加提示音、两段旁白同时播放等任务。
beep.Mixer 更像一个长期存在的混音容器,可以在运行期间调用 Add 追加流,也可以调用 Clear 清空当前流。它本身不会因为暂时没有输入而结束,而是持续输出静音,因此不能直接拿它替代一次性播放的结束信号。

把两个 MP3 解码为 Streamer
MP3 解码器接收 io.ReadCloser,返回可定位的流、音频格式和错误。返回的流负责释放解码资源,所以成功解码后要关闭返回的 StreamSeekCloser,不要在解码后立刻关闭传入文件。
package main
import (
"fmt"
"os"
"github.com/gopxl/beep"
"github.com/gopxl/beep/mp3"
"github.com/gopxl/beep/speaker"
)
// openMP3 只负责打开并解码,调用方负责关闭返回的流。
func openMP3(path string) (beep.StreamSeekCloser, beep.Format, error) {
file, err := os.Open(path)
if err != nil {
return nil, beep.Format{}, fmt.Errorf("打开 %s: %w", path, err)
}
// mp3.Decode 会接管读取过程;释放资源时关闭返回的 StreamSeekCloser。
stream, format, err := mp3.Decode(file)
if err != nil {
// 解码失败时返回的流不可用,此时关闭原文件避免句柄泄漏。
_ = file.Close()
return nil, beep.Format{}, fmt.Errorf("解码 %s: %w", path, err)
}
return stream, format, nil
}
func main() {
first, firstFormat, err := openMP3("voice.mp3")
if err != nil {
panic(err)
}
defer first.Close()
second, secondFormat, err := openMP3("music.mp3")
if err != nil {
panic(err)
}
defer second.Close()
fmt.Println("音频格式:", firstFormat, secondFormat)
_ = speaker.Play // 下一节使用 speaker 初始化和播放
}
这里保留两个 Format 是为了后面选择输出采样率。不要只看文件扩展名判断格式,真正可用于播放的采样率和声道信息来自解码结果。
采样率不同,先用 Resample 统一
混音器接收的是采样样本,不会自动替你解决不同采样率造成的速度和音高问题。如果第一个文件是 44100 Hz、第二个文件是 48000 Hz,可以把第二个流重采样到第一个格式的采样率,再进入混音器。
targetRate := firstFormat.SampleRate
// quality=3 适合实时播放;质量越高,CPU 开销通常越大。
secondForMix := beep.Resample(
3,
secondFormat.SampleRate,
targetRate,
second,
)
// Mix 只负责把同一输出格式的两个 Streamer 叠加。
mixed := beep.Mix(first, secondForMix)
// bufferSize 以样本数表示;较大更稳定,较小延迟更低。
if err := speaker.Init(targetRate, targetRate.N(time.Second/10)); err != nil {
panic(err)
}
speaker.PlayAndWait(mixed)
上面的片段需要额外导入 time。如果两个解码结果的声道数或精度也不一致,不要把它们当成完全相同的格式直接混合;工程上应先把输入约束到 beep 支持的双声道输出,或在进入 beep 前完成转换。
用 beep.Mix 让两个有限流同时播放
下面给出一个可以直接放进命令行播放器的完整骨架。它把第一个流作为目标采样率,第二个流经过重采样后再混合;PlayAndWait 会等待混合流播放完毕,主函数退出前两个返回流也会被关闭。
package main
import (
"fmt"
"os"
"time"
"github.com/gopxl/beep"
"github.com/gopxl/beep/mp3"
"github.com/gopxl/beep/speaker"
)
func decode(path string) (beep.StreamSeekCloser, beep.Format, error) {
file, err := os.Open(path)
if err != nil {
return nil, beep.Format{}, err
}
// Decode 返回的流拥有后续读取和关闭所需的资源。
stream, format, err := mp3.Decode(file)
if err != nil {
_ = file.Close()
return nil, beep.Format{}, err
}
return stream, format, nil
}
func main() {
voice, voiceFormat, err := decode("voice.mp3")
if err != nil {
panic(fmt.Errorf("读取人声: %w", err))
}
defer voice.Close()
music, musicFormat, err := decode("music.mp3")
if err != nil {
panic(fmt.Errorf("读取背景音乐: %w", err))
}
defer music.Close()
// 统一采样率,否则不同输入可能出现速度或音高异常。
music = beep.Resample(3, musicFormat.SampleRate, voiceFormat.SampleRate, music)
mixed := beep.Mix(voice, music)
// 缓冲区取约 100 毫秒,兼顾稳定性和播放响应。
if err := speaker.Init(voiceFormat.SampleRate, voiceFormat.SampleRate.N(100*time.Millisecond)); err != nil {
panic(fmt.Errorf("初始化扬声器: %w", err))
}
// 有限流播放完成后再退出,避免主函数提前结束。
speaker.PlayAndWait(mixed)
}
播放期间还要加音频时,换成 Mixer
例如提示音需要在背景音乐播放期间随时加入,应该让 Mixer 持续存在。修改当前播放集合时要短暂锁住 speaker,否则播放线程可能同时读取正在变化的流集合。
mixer := &beep.Mixer{}
// 先把背景音乐放入动态混音器,再启动持续播放。
mixer.Add(background)
speaker.Play(mixer)
// 事件到来时只在修改集合的瞬间加锁,避免产生播放卡顿。
speaker.Lock()
mixer.Add(notification)
speaker.Unlock()
// 需要停止当前所有输入时清空 Mixer;Mixer 自身仍会输出静音。
speaker.Lock()
mixer.Clear()
speaker.Unlock()
Mixer 的特点是空集合时仍然输出静音,所以它适合常驻播放管线,不适合用“等待它自然结束”来判断一段有限音频的完成。动态场景需要自行维护退出条件,或者把有限流交给一次性的 beep.Mix。

最容易踩到的四个边界
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 混合后速度或音调异常 | 输入采样率不同 | 以输出采样率为目标调用 beep.Resample |
| 程序立刻退出 | 只调用了 speaker.Play,没有等待 | 有限流用 speaker.PlayAndWait 或自行等待 |
| 追加提示音时偶发杂音 | 并发修改 Mixer | 用 speaker.Lock 和 Unlock 包住最短修改区间 |
| 文件句柄持续增加 | 只关闭了传入文件,未关闭解码返回流 | 成功解码后对 StreamSeekCloser 执行 defer Close |
扬声器缓冲区也需要按场景取舍:更大的缓冲区通常更稳定但延迟更高,更小的缓冲区响应更快但更容易受到系统调度影响。先用约 100 毫秒作为起点,再按实际交互需求调整。
结论与使用清单
两个音频文件已经存在且只需同时播放时,使用“解码 → 统一采样率 → beep.Mix → speaker.PlayAndWait”这条短路径;播放过程中会不断加入或清除提示音时,使用“常驻 beep.Mixer → 锁定 speaker → 修改输入集合”的方式。无论选择哪条路径,都要把解码返回的流当成资源管理对象,并确认输出采样率和声道约束。
- 能否从两个解码结果中确定目标采样率?
- 第二个音频是否需要
beep.Resample? - 这是有限播放还是运行期间持续追加的场景?
- 是否在退出前等待了有限流播放完成?
- 是否在关闭前释放了每个
StreamSeekCloser?
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
495 收藏
-
179 收藏
-
463 收藏
-
310 收藏
-
240 收藏
-
380 收藏
-
326 收藏
-
278 收藏
-
452 收藏
-
307 收藏
-
173 收藏
-
302 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习