feat(audio): mp3 dekodieren für Cloud-Provider (reiner Go-Dekoder)
oto-Sink bekommt eine Format-Weiche: RIFF/WAVE-PCM (osbuiltin) und MP3 (ElevenLabs/OpenAI/Edge ab Task 03). Dekoder ist github.com/hajimehoshi/ go-mp3 v0.3.4 — reines Go ohne CGO, verifiziert per GOOS/GOARCH-Matrix mit CGO_ENABLED=0 (alle 6 Targets bauen). Der Dekoder liefert immer 16-Bit-LE stereo (Mono-Quellen wie Edge 24 kHz werden gespiegelt); die Sample-Rate kommt aus dem ersten Frame. Fixtures sind echte, einmalig erzeugte MP3s (mono 24 kHz 48 kbit/s = Edge-Format, stereo 44,1 kHz = Cloud-typisch), als Base64 eingebettet — Tests bleiben werkzeugfrei.
This commit is contained in:
1 parent
ae9f3b680e
commit
7a843ae441
7 files changed
+211
-6
No files matched your search
@@ -0,0 +1,68 @@
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user