package transcode import ( "bytes" "errors" "fmt" "io" "os" "strings" "github.com/gopxl/beep" "github.com/gopxl/beep/vorbis" "github.com/pion/opus" "github.com/pion/opus/pkg/oggreader" ) func decodeOggFile(path string) (beep.Streamer, beep.Format, io.Closer, error) { switch sniffOggCodec(path) { case "opus": return decodeOggOpus(path) case "vorbis": return decodeOggVorbis(path) default: streamer, format, closer, err := decodeOggVorbis(path) if err == nil { return streamer, format, closer, nil } if isVorbisInvalidHeader(err) { return decodeOggOpus(path) } return nil, beep.Format{}, nil, err } } func sniffOggCodec(path string) string { f, err := os.Open(path) if err != nil { return "" } defer f.Close() buf := make([]byte, 8192) n, _ := io.ReadFull(f, buf) buf = buf[:n] if len(buf) < 4 || !bytes.HasPrefix(buf, []byte("OggS")) { return "" } if bytes.Contains(buf, []byte("OpusHead")) { return "opus" } // Vorbis ID packet: 0x01 + "vorbis" if bytes.Contains(buf, []byte{0x01, 'v', 'o', 'r', 'b', 'i', 's'}) { return "vorbis" } return "" } func isVorbisInvalidHeader(err error) bool { if err == nil { return false } msg := strings.ToLower(err.Error()) return strings.Contains(msg, "invalid header") || strings.Contains(msg, "vorbis:") } func decodeOggVorbis(path string) (beep.Streamer, beep.Format, io.Closer, error) { f, err := os.Open(path) if err != nil { return nil, beep.Format{}, nil, err } streamer, format, decErr := vorbis.Decode(f) if decErr != nil { f.Close() return nil, beep.Format{}, nil, fmt.Errorf("ogg/vorbis: %w", decErr) } return streamer, format, f, nil } func decodeOggOpus(path string) (beep.Streamer, beep.Format, io.Closer, error) { f, err := os.Open(path) if err != nil { return nil, beep.Format{}, nil, err } ogg, header, err := oggreader.NewWith(f) if err != nil { f.Close() return nil, beep.Format{}, nil, fmt.Errorf("ogg/opus: %w", err) } sr := int(header.SampleRate) if sr <= 0 { sr = 48000 } ch := int(header.Channels) if ch <= 0 { ch = 1 } dec, err := opus.NewDecoderWithOutput(sr, ch) if err != nil { f.Close() return nil, beep.Format{}, nil, fmt.Errorf("ogg/opus decoder: %w", err) } const maxFrameSamples = 5760 pcmBuf := make([]float32, maxFrameSamples*ch) var samples []float64 for { pkt, _, err := ogg.ParseNextPacket() if errors.Is(err, io.EOF) { break } if err != nil { f.Close() return nil, beep.Format{}, nil, fmt.Errorf("ogg/opus read: %w", err) } if len(pkt) == 0 || bytes.HasPrefix(pkt, []byte("OpusHead")) || bytes.HasPrefix(pkt, []byte("OpusTags")) { continue } n, err := dec.DecodeToFloat32(pkt, pcmBuf) if err != nil { f.Close() return nil, beep.Format{}, nil, fmt.Errorf("ogg/opus decode: %w", err) } if n <= 0 { continue } total := n * ch if total > len(pcmBuf) { total = len(pcmBuf) } for i := 0; i < total; i++ { samples = append(samples, float64(pcmBuf[i])) } } if len(samples) == 0 { f.Close() return nil, beep.Format{}, nil, fmt.Errorf("ogg/opus: no audio samples") } outCh := ch if outCh > 1 { samples = interleavedToMono(samples, outCh) outCh = 1 } return newSamplesStreamer(samples, sr), beep.Format{ SampleRate: beep.SampleRate(sr), NumChannels: outCh, Precision: 2, }, f, nil }