package audio import ( "bytes" "io" "sync" "sync/atomic" "time" "github.com/ebitengine/oto/v3" "github.com/hajimehoshi/go-mp3" ) // PlayMP3Loop 循环播放 MP3(非阻塞) // 返回 player 和清理函数,调用者负责 defer cleanup() func PlayMP3Loop(r io.ReadCloser) (*oto.Player, func() error, error) { otoCtx, err := initContext() if err != nil { r.Close() return nil, func() error { return nil }, err } // Read the entire MP3 into memory for seeking support data, err := io.ReadAll(r) if err != nil { r.Close() return nil, func() error { return nil }, err } r.Close() dec, err := mp3.NewDecoder(bytes.NewReader(data)) if err != nil { return nil, func() error { return nil }, err } // 获取采样率信息 sampleRate := int(dec.SampleRate()) targetRate := UniversalSampleRate // 需要重采样 var reader io.Reader = dec if needsResample(sampleRate, targetRate) { resampleReader, err := newResamplingReader(dec, sampleRate, targetRate, 2) if err != nil { return nil, func() error { return nil }, err } reader = resampleReader } player := otoCtx.NewPlayer(reader) playing := atomic.Bool{} playing.Store(true) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() for playing.Load() { player.Play() for playing.Load() && player.IsPlaying() { time.Sleep(10 * time.Millisecond) } if playing.Load() { // 重置解码器位置 _, _ = dec.Seek(0, io.SeekStart) } } }() cleanup := func() error { playing.Store(false) wg.Wait() player.Close() return nil } return player, cleanup, nil }