# go-mp3 **Repository Path**: chenxushui/go-mp3 ## Basic Information - **Project Name**: go-mp3 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-12 - **Last Updated**: 2026-09-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # gomp3 · Go 跨平台音频播放器 一份 Go 写成的音频播放内核,外加三套原生界面:**Windows 桌面(Win32 原生控件)**、**Android(Kotlin)**、**iOS(Swift)**。 解码、播放控制、播放列表全部在 Go 层,UI 层只负责画界面和转发操作。 内置解码器支持 **MP3 / FLAC / WAV / Ogg Vorbis**(纯 Go,手机端同样可用); 桌面端装了 ffmpeg 时,还能播 **WMA / M4A·AAC / APE / Opus** 等格式。 ``` ┌──────────────────────────────┬──────────────────────────────┬───────────────────┐ │ Windows 桌面 │ Android │ iOS │ │ cmd/gomp3-desktop │ android/ (Kotlin) │ ios/ (Swift) │ │ lxn/walk → Win32 原生控件 │ Activity + Service + 通知 │ ViewController │ └──────────────┬───────────────┴──────────────┬───────────────┴─────────┬─────────┘ │ 直接 import(同进程) │ gomobile bind → AAR │ gomobile bind → XCFramework └───────────────┬───────────────┴─────────────────────────┘ ▼ ┌─────────────────────────┐ │ mp3core(纯 Go 内核) │ │ 解码器适配层 + oto 输出 │ │ 播放列表 / 自动下一首 │ └─────────────────────────┘ ``` --- ## 一、这个项目能做什么 - **多格式解码**:MP3、FLAC、WAV(PCM 8/16/24/32bit + IEEE float)、Ogg Vorbis - 桌面端可选外挂 ffmpeg,额外支持 WMA、M4A/AAC、APE、Opus、AC3 等(装好 ffmpeg 并加进 PATH 即自动启用) - 所有解码器统一输出 16bit 小端双声道 PCM,上层只写一套重采样 / 定位逻辑 - **元数据按格式解析**:MP3 走 ID3v2.3/2.4 + ID3v1;FLAC 走 VorbisComment + PICTURE; Ogg Vorbis 走注释头 + `METADATA_BLOCK_PICTURE`;WAV 走 LIST/INFO - **内嵌封面**(JPEG/PNG)在桌面端和移动端都会显示 - **歌词显示**(桌面端):自动读取同名 `.lrc` 文件 > 内嵌歌词(ID3v2 USLT / Vorbis LYRICS)。 带 `[mm:ss]` 时间戳的 LRC 会随播放进度**逐行滚动高亮**;纯文本歌词直接展示 - **列表与设置持久化**:退出时自动把播放列表、音量、循环/随机模式、上次浏览目录 存到 `%APPDATA%\gomp3\config.json`,下次打开自动恢复;**首次运行会自动扫描 系统的「音乐」文件夹**(含子目录),不用每次手动找目录 - 采样率自动适配:44.1kHz 直通(bit-perfect、零开销),48k / 32k / 22.05k 等线性插值重采样 - 播放控制:播放 / 暂停 / 停止 / 上一首 / 下一首 / 拖动进度 / 音量 - 播放模式:列表循环、单曲循环、顺序播放、随机播放 - 播完自动下一首(由 Go 层的看门狗 goroutine 负责,UI 不用自己写定时器) - Android 上是前台服务 + 常驻通知(锁屏可控),iOS 上有 `MPNowPlayingInfoCenter` 锁屏信息与远程控制 ## 二、目录结构 ``` go-mp3/ ├── mp3core/ # ★ Go 播放内核(gomobile bind 的目标包) │ ├── audio.go # 全局音频上下文(oto 只允许有一个) │ ├── decode.go # ★ 解码器抽象(pcmStream)+ 格式识别 + 支持列表 │ ├── decode_mp3.go # MP3 → go-mp3 │ ├── decode_flac.go # FLAC → mewkiz/flac(自建帧索引做 Seek) │ ├── decode_wav.go # WAV → 自研 RIFF 解析(含 INFO 标签) │ ├── decode_vorbis.go # Ogg → jfreymuth/oggvorbis │ ├── decode_ffmpeg_desktop.go# 桌面端外挂 ffmpeg(WMA/M4A/AAC…,build tag 隔离) │ ├── spawn_windows.go # 让 ffmpeg 子进程不弹黑窗 │ ├── spawn_other.go # 非 Windows 的空实现 │ ├── source.go # 采样率重采样 + 输出/源偏移量换算 │ ├── player.go # 单曲播放器(播放/暂停/Seek/位置/时长) │ ├── playlist.go # 播放列表、播放顺序、循环/随机、自动下一首 │ ├── library.go # 目录扫描、元数据解析、时长探测 │ ├── id3.go # ID3v2 / ID3v1 标签与封面解析 │ ├── export.go # ★ 对外 API(全平台安全,不含结构体切片) │ ├── export_desktop.go # 仅桌面端可用的便捷 API(build tag 隔离) │ └── mp3core_test.go # 单元测试(含真实文件集成测试) ├── cmd/ │ ├── gomp3-desktop/ # Windows 桌面 GUI(lxn/walk + 嵌入清单) │ │ ├── main.go │ │ ├── model.go # 列表数据源 │ │ ├── gomp3.exe.manifest # comctl32 v6 / DPI 感知清单 │ │ └── rsrc_windows_amd64.syso │ └── gomp3-cli/ # 命令行播放器(全平台,验证内核用) ├── android/ # Android 工程(Kotlin) │ └── app/src/main/java/com/gomp3/player/ │ ├── MainActivity.kt # 主界面:列表 + 控制 │ ├── PlayerService.kt # 前台服务 + 通知 │ └── SongAdapter.kt ├── ios/GoMp3/ # iOS 工程源码(Swift) │ ├── AppDelegate.swift │ ├── AudioManager.swift # AVAudioSession + 锁屏控制 │ ├── PlayerViewController.swift │ └── Info.plist # 后台播放权限 └── scripts/ # 构建脚本 ├── build_desktop.sh / .bat ├── build_android.sh └── build_ios.sh ``` ## 三、Windows 桌面端:编译与使用 ```bash go test ./mp3core/ # 跑单元测试 ./scripts/build_desktop.sh # 编译 CLI + 桌面 GUI ./scripts/build_desktop.sh --selftest # 编译后自动自检(会闪一下窗口) ``` 或者直接用 Windows 批处理:`scripts\build_desktop.bat` 产物: | 文件 | 说明 | |---|---| | `bin/gomp3.exe` | 桌面 GUI(双击运行,无控制台窗口) | | `bin/gomp3-cli.exe` | 命令行播放器 | 桌面 GUI 支持 `-dir` 参数直接加载目录: ```bash bin/gomp3.exe -dir "D:\Music" ``` 界面:顶部工具栏(添加文件 / 添加文件夹 / 清空列表 / 包含子目录)、左侧歌曲列表(双击播放)、 右侧专辑封面与歌曲信息、底部进度条与播放控制、状态栏。 命令行播放器适合排查问题: ```bash bin/gomp3-cli.exe -dir "D:\Music" -list # 只扫描并列出,显示时长和采样率 bin/gomp3-cli.exe -dir "D:\Music" -play 0 -seconds 10 # 播第 1 首,10 秒后退出 bin/gomp3-cli.exe -f "D:\a.mp3" -seek 60000 -seconds 5 # 从 60 秒处开始播 bin/gomp3-cli.exe -f "D:\a.mp3" -probe # 解码诊断:标签、时长、采样率、是否重采样 bin/gomp3-cli.exe -dir "D:\Music" -repl # 交互模式(空格/n/p/s/v/r/h/q) ``` ## 四、Android 端:打包步骤 前置:Go ≥ 1.21、Android Studio(SDK 34)、NDK r25c、`gomobile`。 ```bash go install golang.org/x/mobile/cmd/gomobile@latest export ANDROID_HOME=/path/to/Android/Sdk ./scripts/build_android.sh # 生成 android/app/libs/mp3core.aar ``` 脚本里做了两件容易踩坑的事: 1. `gomobile init -ndk "$ANDROID_HOME/ndk/25.2.9519653"` 显式指定 NDK,避免多版本共存时 gomobile 猜错; 2. 用 `-javapkg com.gomp3` 指定 Java 包前缀,生成的类就是 `com.gomp3.mp3core.Mp3core`(与 Kotlin 代码里的 import 一致)。 然后 Android Studio 打开 `android/` 目录,直接 Run。 跑起来之后点「扫描音乐」:会申请 `READ_MEDIA_AUDIO`(Android 13+)或 `READ_EXTERNAL_STORAGE` (Android 12-),通过 MediaStore 查到所有 MP3 的绝对路径交给 Go 层解析并播放。 ## 五、iOS 端:打包步骤 必须在 macOS 上,Xcode 15.4+: ```bash ./scripts/build_ios.sh # 生成 ios/Mp3core.xcframework ``` 然后在 Xcode 里: 1. 新建 App 工程,把 `ios/GoMp3/*.swift` 和 `Info.plist` 加进去(`Info.plist` 里已有 `UIBackgroundModes = audio`); 2. 把 `Mp3core.xcframework` 拖进 **Frameworks, Libraries, and Embedded Content**,Embed 选 *Embed & Sign*; 3. 在 Build Phases → Link Binary With Libraries 里加上 **AVFoundation.framework** 和 **AudioToolbox.framework**(oto 在 iOS 上走 CoreAudio,缺了会链接失败); 4. 把 mp3 通过「文件」App 拷进 App 的 Documents 目录,点「扫描音乐」。 ## 六、关键实现说明 ### 6.1 音频格式:只有一种输出格式 oto 的 `Context` 在一个进程里只能创建一个,而且**同一个 Context 只能播放一种格式**,oto 自身也不做重采样。 go-mp3 的解码输出恒为「16bit 小端 + 双声道」(单声道文件也会被展开成双声道), 所以唯一需要处理的是**采样率**: - 源采样率 == 44100:直通,bit-perfect,零开销; - 源采样率 != 44100(48k / 32k / 22.05k…):线性插值重采样到 44100。 插值逻辑抽成了纯函数 `interpolate()`,配套的单测会生成 1 秒 48kHz 的 1kHz 正弦波, 重采样后校验输出帧数与过零点数——如果 ratio 用反了,音频会变调、时长也会不对,这个测试能直接抓出来。 ### 6.2 播放位置与 Seek 的单位换算 这是最容易写错的地方,约定如下: | 层 | 单位 | |---|---| | `mp3core` 对外 API | 毫秒 | | `decoderSource.Seek` / `oto.Player.Seek` | **输出 PCM 的字节偏移** | | go-mp3 `Decoder.Seek` | 解码后 PCM 字节偏移,必须是 **4 的倍数** | 换算:`字节偏移 = 毫秒 × 采样率 × 4 / 1000`(4 = 双声道 × 2 字节)。少乘这个 4 的话, 拖动进度条会跳到「约 1/4 的位置」——本项目开发过程中确实踩过这个坑,`TestMsToPCMOffset` 现在守着它。 播放位置则是反过来算的:解码器已经吐出的帧数 减去 oto 缓冲区里还没播出去的帧数 (`Player.BufferedSize()`),再换算成毫秒。 ### 6.3 自动下一首 `Playlist` 内部有一个 200ms 心跳的看门狗 goroutine:当「解码器读到 EOF」且「oto 不在播放」时判定一首歌播完, 再按循环模式决定重播 / 切下一首 / 停止。这样 Android、iOS、桌面端都不需要自己实现「播完监测」。 界面刷新则统一用轮询:Go 层每次切歌都会让 `Generation()` 自增,UI 只要比较这个值就知道「切歌了」, 不需要注册回调,也就不存在跨语言回调的生命周期问题。 ### 6.4 解码器适配层:新增格式只要实现一个接口 上层(重采样、位置换算、播放列表)只认一种东西 —— **`pcmStream`**: ```go type pcmStream interface { io.ReadSeeker // 偏移量 = 归一化 PCM(16bit 双声道)的字节偏移 SampleRate() int Length() int64 // 归一化后的总字节数,-1 表示未知 Close() error } ``` 新增一种格式只需要写一个实现 + 在 `sniffFormat` 里加一条识别规则, 重采样、Seek 换算、时长计算、播放控制全都不用改。 **接口有一条硬性契约**:`Seek(0, io.SeekCurrent)` 必须廉价且无副作用。 因为播放位置每刷新一次都要问「现在到哪了」,如果实现里真的去 seek 解码器, 会把正在解码的状态和预读缓冲全部冲掉(开发期间 FLAC 就栽在这上面: 一查位置就停摆)。各个实现现在都在 Seek 开头直接返回 `pipe.pos`。 格式识别用文件头嗅探(`fLaC` / `RIFF+WAVE` / `OggS` / `ID3` / MPEG 同步字), 认不出来时才退回扩展名 —— 所以带 APE 标签的 MP3 之类也能认出来。 ### 6.5 FLAC 的 Seek 为什么自己实现 `mewkiz/flac` 的 `Stream.Seek` 在文件**没有自带 SEEKTABLE** 时会临时全量扫描一遍 并现场造表;实测造完表后再去解帧会直接失败(`invalid sync-code`),不能依赖。 而且它的 `Stream` 内部包了 `bufio`/`bufseekio` 缓冲,绕开它直接移动文件指针会读到脏数据。 所以 FLAC 这条链路改成:**自己解析文件头(签名 + 元数据块 + STREAMINFO), 只用库里的「帧解析器」(`frame.Parse`),帧位置自己在解码过程中记成索引**: - 顺序解码时每帧记一条 `(文件偏移, 起始样本号)`,零额外成本 - Seek 时在索引里二分找「样本号 ≤ 目标」的最后一帧 → 定位过去 → 用 `skip` 丢掉多余样本 - 索引里还没有的位置,就从最近的一个已知帧往前解(最坏情况相当于从文件头解一遍) ### 6.6 外挂 ffmpeg(仅桌面端) WMA、M4A/AAC、APE、Opus 这些格式没有可用的纯 Go 解码器,只能借外部程序。 实现方式:起一个 `ffmpeg -i -f s16le -ac 2 -ar 44100 pipe:1` 子进程, 从 stdout 读 PCM —— 输出格式正好等于音频上下文格式,所以走「直通」路径,不再重采样。 Seek 靠 `-ss` 重启进程实现。 这个文件用 `//go:build !android && !ios` 挡在移动端之外:手机上没有可执行文件可起。 移动端遇到这些格式会明确报「不支持的音频格式」,而桌面的启动状态栏会提示 ffmpeg 是否可用。 ### 6.7 gomobile 的类型限制(重要) `gomobile bind` 的 Objective-C 生成器**不支持结构体切片**(源码里对 `*types.Slice` 只处理了 `[]byte`), 一旦导出签名里出现 `[]Song`,iOS 绑定会直接失败。所以对外 API 是这样设计的: | 场景 | 桌面端 | 移动端 | |---|---|---| | 取整个列表 | `GetSongs() []Song` | `GetSongsJSON() string` + 各自解析 | | 按需取单曲 | `GetSong(i)` | `GetSongCount()` + `GetSong(i)` | | 批量加入 | `AddSongs([]Song)` | `AddSongsJSON(json)` / `LoadPlaylist(...)` | | 扫描结果 | 直接返回切片 | `LoadDir()` 返回 `ScanResult` 结构体 | 桌面端专用的便捷函数放在 `mp3core/export_desktop.go`,用 `//go:build !android && !ios` 把它们挡在移动端之外。 ## 七、与原始开发文档的差异(都是实测修正) 文档里给的示例是 Go + gomobile 的正确大方向,但有一批 API 细节与实际不符,这里逐条列出,避免照抄踩坑: | 文档写法 | 实际情况 | |---|---| | 导出函数要写 `//export` 注释 | `gomobile bind` 不需要,**首字母大写即导出**;`//export` 是 cgo 的写法,写了反而要求 `import "C"` | | `github.com/hajimehoshi/oto/v3` | 已迁移为 `github.com/ebitengine/oto/v3`;v3.5+ 要求 go ≥ 1.25,本项目锁 v3.3.3 | | `oto.NewContext(op)` 返回两个值 | v3 返回三个:`(*Context, chan struct{}, error)`,第二个是「就绪」通道 | | `player.Duration()` / `player.CurrentPosition()` / `player.Seek(ms)` | oto 的 Player **没有**这些方法。`Seek` 是 `io.Seeker` 语义(字节偏移,且直接透传给数据源),位置要结合 `BufferedSize()` 自己算 | | `SetVolume` 之外还有 `Player.Volume()` | 有,但 `Duration/Position` 必须自己维护 | | 结构体不能导出,要拆成参数 | 结构体**可以**导出(会生成 `mp3core.Song` 类,字段是 `getTitle()/setTitle()`);真正的限制是**结构体切片**和**两个非 error 返回值** | | Android 调用 `Mp3core.initPlayer()` | 对(函数名首字母小写),但前提是用 `-javapkg` 指定包名,否则类在裸包 `mp3core` 下 | | iOS 调用 `Mp3coreInitPlayer()` | 不对。iOS 侧生成的是**类方法**:`GoMp3core.initPlayer()`,结构体类名是 `GoMp3coreSong` | | `SongAdapter` 只显示「歌曲 N」 | 本项目返回真实元数据,列表显示标题 / 艺术家 / 时长,并高亮当前播放项 | | 直接扫描 `getExternalFilesDir` | 改为 MediaStore 查询 + 应用私有目录兜底;Android 13+ 用 `READ_MEDIA_AUDIO` | 另外两个桌面端独有的坑: - **walk 必须带 Win32 清单**。不带清单时 comctl32 会退回 v5,`Create()` 直接报 `TTM_ADDTOOL failed`。项目里已经放好 `gomp3.exe.manifest` 和用 `rsrc` 生成的 `rsrc_windows_amd64.syso`,直接 `go build` 就会带上。 - **`ImageView.SetImage(nil)` 会 panic**(walk 内部会去读位图尺寸)。没有封面时要给一张透明占位图。 多格式改造过程中实测发现的三个深坑(都已修复,这里记录因果链供后来者参考): 1. **FLAC 跳转卡死(最严重)**。三个因素叠加: - `mewkiz/flac` 解码速度只有 ~1.5MB/s,纯 Go 解码 200 秒的音频要 20 多秒, 「从最近的已解码帧丢弃解码到目标」的朴素实现会把 UI 卡死半分钟; - 估算出的文件偏移几乎必然落在帧中间,而音频数据里充满了 `0xFF 0xF8` 假同步字—— 它们能过 14 位同步检查、却过不了 CRC-16,所以重扫必须「任何解析失败都从候选+1 继续」, 只处理 `ErrInvalidSync` 会在 CRC 错误上永久卡死; - `mewkiz/flac` 对未覆盖测试用例的采样率只 `log.Printf` 一条警告然后继续解, 日志里会出现 `sample rate 24000` 的噪音,属正常现象不是错误。 最终方案:STREAMINFO 总样本数 + 文件大小做平均码率估算,回退 2 秒保底, 同步字重扫定位真帧,再按帧头里的样本号精确丢弃(`decode_flac.go` 的 `Seek/scanForward`)。 2. **解码流的数据竞争(最隐蔽)**。oto 在自己的后台 goroutine 里调 `Read` 拉数据, UI 线程随时会调 `Seek`,而两者操作的是同一个非线程安全的解码流。MP3 时代竞争 窗口只有微秒级没暴露;FLAC 的重扫把窗口放大了几百倍后,竞争会让 Read 误报 EOF, 触发「自动切歌」看门狗把歌从头重启(表象是 seek 后位置归零)。 修复:`decoderSource` 全部方法加互斥锁(`source.go`)。 3. **尾部标签不是错误**。FLAC 音频数据结束后常跟着 ID3v1 等标签,顺序解码到这里会报 `invalid sync-code`。按真实错误处理会让「播完自动下一首」失效; 现在顺序播放中的同步失败一律按正常结束处理。 ## 八、已知限制 - **WMA / M4A / AAC / APE 需要外挂 ffmpeg**(仅桌面端)。这些格式没有可用的纯 Go 解码器, 手机端不能起子进程,因此移动端只支持内置的 MP3 / FLAC / WAV / Ogg Vorbis。 想让手机也支持,需要把解码库编成 C 库后用 gomobile 链接,那会引入 CGO 与显著体积增长。 - **FLAC 首次跳到很远的位置会有一小段停顿**(约 0.4~1 秒,用于按平均码率定位 + 丢弃解码), 同一位置第二次跳转就走帧索引、瞬间完成。后续可以加一个后台预扫描把索引一次性建好。 - 重采样用的是线性插值,对 48kHz 音源会有轻微的高频衰减;要更高质量可以换成多相 FIR。 - 移动端暂不支持从 SAF / 相册拿 `content://` URI 直接播放(Go 侧按文件路径解码)。 需要的话可以先把文件复制到应用目录,或用字节流接口扩展。 - 桌面端界面是纯 Win32 控件,没有做主题美化;列表排序只有「音轨号 + 自然序」。 ## 九、验证情况(2026-09-12 实测) | 项目 | 结果 | |---|---| | 单元测试 `go test ./mp3core/` | ✅ 全部通过(重采样数学、单位换算、ID3v1/v2 解析、列表顺序、FLAC seek/结尾集成用例) | | MP3 解码 / 时长 / Seek / 自动下一首 | ✅ 04:31 文件,seek 到 01:00 起播、连续推进 | | FLAC 解码 + VorbisComment 标签 + 封面 | ✅ 26MB / 04:13 文件,标签、时长、封面全部正确 | | FLAC seek 到 200s | ✅ seek 后 0.4 秒内恢复出声,位置 03:21→03:28 连续推进(修复前卡死 20 秒以上) | | FLAC 播到结尾 | ✅ 04:13 正常收尾并自动切歌(尾部标签按 EOF 处理) | | Ogg Vorbis 解码 + 播放 | ✅ 02:33 文件 | | WAV 解码 + 非 44.1kHz 重采样路径 | ✅ 22.05kHz 素材 | | 桌面 GUI 自检(上一版内核) | ✅ 建窗、扫描、播放、进度、封面、关闭全程无异常 | 单独验证某个格式的解码链路: ```bash ./bin/gomp3-cli-mf.exe -probe -f "D:\some\file.flac" # 看识别格式 / 时长 / 采样率 ./bin/gomp3-cli-mf.exe -f "D:\some\file.flac" -play 0 -seek 200000 -seconds 5 ``` 本机构建小贴士:如果 `go build` 报一堆 `package xxx is not in std`,那是构建缓存目录 不可写导致的假象,改用 `GOCACHE=<工作区内的非隐藏目录>` 即可(脚本里已经这么设了)。 --- ## 十、快速上手(TL;DR) ```bash git clone && cd go-mp3 go test ./mp3core/ # 1. 验证内核 ./scripts/build_desktop.sh # 2. 编桌面版 ./bin/gomp3.exe -dir "D:\你的音乐目录" # 3. 用起来 ./scripts/build_android.sh # 4. 出 Android 的 AAR(需 NDK) ./scripts/build_ios.sh # 5. 出 iOS 的 XCFramework(需 macOS) ``` ## 十一、window编译结果 ![image-20260912220953392](C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20260912220953392.png) 界面有点丑,但是体积小,没广告,超好用。 下载地址:通过网盘分享的文件:go-mp3 链接: https://pan.baidu.com/s/1c5lgmF0zR_IeKsfZ6gObkQ?pwd=btjc 提取码: btjc 源码下载:https://gitee.com/chenxushui/go-mp3