登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

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 清空当前流。它本身不会因为暂时没有输入而结束,而是持续输出静音,因此不能直接拿它替代一次性播放的结束信号。

Go beep 两个音频流经过解码和重采样后进入 beep.Mix 与扬声器的静态结构图
图1:操作示意图,展示两个音频源、格式统一、beep.Mix 和扬声器之间的静态关系,不是运行截图。

把两个 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。

Go beep 动态 Mixer 连接背景音乐、提示音与 speaker 的静态结构图
图2:结果示意图,展示常驻 Mixer、动态输入和 speaker 的静态边界,不是运行结果或界面截图。

最容易踩到的四个边界

现象原因处理方式
混合后速度或音调异常输入采样率不同以输出采样率为目标调用 beep.Resample
程序立刻退出只调用了 speaker.Play,没有等待有限流用 speaker.PlayAndWait 或自行等待
追加提示音时偶发杂音并发修改 Mixer用 speaker.Lock 和 Unlock 包住最短修改区间
文件句柄持续增加只关闭了传入文件,未关闭解码返回流成功解码后对 StreamSeekCloser 执行 defer Close

扬声器缓冲区也需要按场景取舍:更大的缓冲区通常更稳定但延迟更高,更小的缓冲区响应更快但更容易受到系统调度影响。先用约 100 毫秒作为起点,再按实际交互需求调整。

结论与使用清单

两个音频文件已经存在且只需同时播放时,使用“解码 → 统一采样率 → beep.Mix → speaker.PlayAndWait”这条短路径;播放过程中会不断加入或清除提示音时,使用“常驻 beep.Mixer → 锁定 speaker → 修改输入集合”的方式。无论选择哪条路径,都要把解码返回的流当成资源管理对象,并确认输出采样率和声道约束。

  • 能否从两个解码结果中确定目标采样率?
  • 第二个音频是否需要 beep.Resample?
  • 这是有限播放还是运行期间持续追加的场景?
  • 是否在退出前等待了有限流播放完成?
  • 是否在关闭前释放了每个 StreamSeekCloser?
声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>