Files
talk/internal/audio/mp3.go
T

66 lines
2.2 KiB
Go
Raw Normal View History

package audio
import (
"bytes"
"fmt"
"io"
mpg "github.com/hajimehoshi/go-mp3"
)
// decodeMP3 decodes MP3 data, the delivery format of the cloud providers
// (ElevenLabs, OpenAI TTS, Edge TTS), into raw PCM samples for oto.
//
// Decision: a built-in pure Go decoder (github.com/hajimehoshi/go-mp3
// v0.3.4) instead of requesting WAV/PCM from the APIs. Reasons: (1) all
// three cloud providers deliver mp3 as their native format; only OpenAI
// knows response_format "wav"/"pcm", ElevenLabs and Edge TTS do not, so a
// single mp3 pipeline keeps the chain symmetric. (2) The decoder is free of
// CGO (no import "C" in the module), which keeps the cross-compile
// commitment (CGO_ENABLED=0 for all 6 targets) intact, verified by a
// GOOS/GOARCH matrix build.
//
// The decoder always yields 16-bit little-endian PCM with 2 channels; mono
// sources (such as Edge TTS: 24 kHz mono) are mirrored to stereo. The
// sample rate comes from the first MP3 frame.
func decodeMP3(data []byte) (pcmFormat, []byte, error) {
dec, err := mpg.NewDecoder(bytes.NewReader(data))
if err != nil {
return pcmFormat{}, nil, fmt.Errorf("mp3: %w", err)
}
pcm, err := io.ReadAll(dec)
if err != nil {
return pcmFormat{}, nil, fmt.Errorf("mp3: decoding: %w", err)
}
if len(pcm) == 0 {
return pcmFormat{}, nil, fmt.Errorf("mp3: decoding produced no samples")
}
return pcmFormat{
SampleRate: dec.SampleRate(),
Channels: 2, // go-mp3 always decodes to 2 channels (see its docs)
BitsPerCoded: 16,
}, pcm, nil
}
// looksLikeMP3 reports whether data looks like MP3: an ID3v2 tag (leading
// "ID3") or a raw frame sync (first byte 0xFF, top 3 bits of the second
// byte set).
func looksLikeMP3(data []byte) bool {
if len(data) >= 3 && string(data[0:3]) == "ID3" {
return true
}
return len(data) >= 2 && data[0] == 0xFF && data[1]&0xE0 == 0xE0
}
// decodeAudio is the format switch of the sinks: RIFF/WAVE (PCM) goes to
// decodeWAV, MP3 (ID3 tag or frame sync) to decodeMP3. Everything else
// falls through to decodeWAV, which yields the precise "no RIFF" error.
func decodeAudio(data []byte) (pcmFormat, []byte, error) {
if len(data) >= 4 && string(data[0:4]) == "RIFF" {
return decodeWAV(data)
}
if looksLikeMP3(data) {
return decodeMP3(data)
}
return decodeWAV(data)
}