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) }