package audio import ( "bytes" "fmt" "io" mpg "github.com/hajimehoshi/go-mp3" ) // decodeMP3 dekodiert MP3-Daten — das Lieferformat der Cloud-Provider ab // Task 03 (ElevenLabs, OpenAI TTS, Edge-TTS) — in rohe PCM-Samples für // oto. // // Entscheidung mp3-Dekodierung (Task 03): eingebauter, reiner Go-Dekoder // github.com/hajimehoshi/go-mp3 v0.3.4 statt WAV/PCM-Anforderung bei den // APIs. Begründung: (1) Alle drei Cloud-Provider liefern mp3 als ihr // natives Format; nur OpenAI kennt response_format "wav"/"pcm", ElevenLabs // und Edge-TTS aber nicht — eine einheitliche mp3-Pipeline hält die Kette // symmetrisch. (2) Der Dekoder ist nachweislich frei von CGO (kein // import "C" im Modul), damit bleibt die Cross-Compile-Verpflichtung // (CGO_ENABLED=0 für alle 6 Targets) unangetastet — verifiziert per // GOOS/GOARCH-Matrix-Build, siehe Task-03-Report. // // Der Dekoder liefert immer 16-Bit-PCM little-endian mit 2 Kanälen — auch // für Mono-Quellen (etwa Edge-TTS: 24 kHz mono) wird auf Stereo // gespiegelt; die Sample-Rate stammt aus dem ersten 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: dekodieren: %w", err) } if len(pcm) == 0 { return pcmFormat{}, nil, fmt.Errorf("mp3: Dekodierung lieferte keine Samples") } return pcmFormat{ SampleRate: dec.SampleRate(), Channels: 2, // go-mp3 dekodiert grundsätzlich 2 Kanäle (s. doc) BitsPerCoded: 16, }, pcm, nil } // looksLikeMP3 meldet, ob data nach MP3 aussieht: ID3v2-Tag (führt mit // "ID3") oder ein roher Frame-Sync (erstes Byte 0xFF, oberste 3 Bits des // zweiten Bytes gesetzt). 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 ist die Format-Weiche der Sinks: RIFF/WAVE (PCM) nach // decodeWAV, MP3 (ID3-Tag oder Frame-Sync) nach decodeMP3. Alles andere // fällt durch zu decodeWAV, das den präzisen „kein RIFF"-Fehler liefert. 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) }