package audio import ( "encoding/base64" "strings" "testing" ) // Zwei einmalig erzeugte, echte MP3-Fixtures (ffmpeg am Dev-Host, in den // Test eingebettet — Tests bleiben zur Laufzeit frei von Werkzeug-Abhängig- // keiten). Beide entsprechen dem Lieferformat der Cloud-Provider aus // Task 03: // // fixtureMP3Mono: 0,15 s Stille, 24 kHz mono 48 kbit/s — exakt das // Edge-TTS-Ausgabeformat (audio-24khz-48kbitrate-mono- // mp3); übt den Mono→Stereo-Upsample-Pfad des Dekoders. // fixtureMP3Stereo: 0,15 s 440-Hz-Sinus, 44,1 kHz stereo 64 kbit/s — das // typische ElevenLabs/OpenAI-Format. const fixtureMP3MonoB64 = "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjYwLjE2LjEwMQAAAAAAAAAAAAAA//OEwAAAAAAAAAAAAEluZm8AAAAPAAAACQAABdAAOTk5OTk5OTk5OTlSUlJSUlJSUlJSUmtra2tra2tra2trhISEhISEhISEhIScnJycnJycnJycnLW1tbW1tbW1tbW1zs7Ozs7Ozs7Ozs7n5+fn5+fn5+fn5///////////////AAAAAExhdmM2MC4zMQAAAAAAAAAAAAAAACQD8AAAAAAAAAXQx638BQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//NkxAAAAANIAAAAAExBTUVVVVVMQU1FNC4wVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//NkxHwAAANIAAAAAFVVVVVVVVVMQU1FNC4wVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//NkxHwAAANIAAAAAFVVVVVVVVVMQU1FNC4wVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//NkxHwAAANIAAAAAFVVVVVVVVVMQU1FNC4wVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//NkxHwAAANIAAAAAFVVVVVVVVVMQU1FNC4wVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//NkxHwAAANIAAAAAFVVVVVVVVVMQU1FNC4wVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//NkxHwAAANIAAAAAFVVVVVVVVVMQU1FNC4wVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//NkxHwAAANIAAAAAFVVVVVVVVVMQU1FNC4wVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//NkxHwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV" const fixtureMP3StereoB64 = "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" func fixture(t *testing.T, b64 string) []byte { t.Helper() data, err := base64.StdEncoding.DecodeString(b64) if err != nil { t.Fatalf("Fixture dekodieren: %v", err) } return data } // TestDecodeMP3Mono: das Edge-Format (24 kHz mono) dekodiert zu 16-Bit-PCM // stereo — der Dekoder spiegelt Mono auf zwei Kanäle, Samples sind 4 Bytes // (2 Kanäle × 2 Bytes) aligned. func TestDecodeMP3Mono(t *testing.T) { f, pcm, err := decodeMP3(fixture(t, fixtureMP3MonoB64)) if err != nil { t.Fatalf("decodeMP3 (mono): %v", err) } if f.SampleRate != 24000 { t.Errorf("SampleRate = %d, want 24000", f.SampleRate) } if f.Channels != 2 || f.BitsPerCoded != 16 || f.Float { t.Errorf("Format = %+v, want 2 Kanäle/16 Bit/int", f) } if len(pcm) == 0 || len(pcm)%4 != 0 { t.Errorf("PCM-Länge = %d, want > 0 und durch 4 teilbar (16-Bit-Samples stereo)", len(pcm)) } } // TestDecodeMP3Stereo: ElevenLabs/OpenAI-typisches Format (44,1 kHz stereo). func TestDecodeMP3Stereo(t *testing.T) { f, pcm, err := decodeMP3(fixture(t, fixtureMP3StereoB64)) if err != nil { t.Fatalf("decodeMP3 (stereo): %v", err) } if f.SampleRate != 44100 || f.Channels != 2 || f.BitsPerCoded != 16 { t.Errorf("Format = %+v, want 44100 Hz stereo 16 Bit", f) } if len(pcm) == 0 || len(pcm)%4 != 0 { t.Errorf("PCM-Länge = %d, want > 0 und durch 4 teilbar", len(pcm)) } } // TestDecodeMP3Fehler: Daten, die nach MP3 aussehen, aber keine Frames // enthalten, sind ein klarer mp3-Fehler (nicht der WAV-Fehlerpfad). func TestDecodeMP3Fehler(t *testing.T) { cases := []struct{ name, want string }{ {"Müll mit Frame-Sync", "mp3:"}, {"ID3-Tag ohne Frames", "mp3:"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { var data []byte switch tc.name { case "Müll mit Frame-Sync": data = []byte{0xFF, 0xFB, 'k', 'e', 'i', 'n', ' ', 'm', 'p', '3'} case "ID3-Tag ohne Frames": data = []byte("ID3\x04\x00\x00\x00\x00\x00\x00") } _, _, err := decodeMP3(data) if err == nil || !strings.Contains(err.Error(), tc.want) { t.Errorf("decodeMP3-Fehler = %v, will Meldung mit %q", err, tc.want) } }) } } // TestLooksLikeMP3 pinnt die Format-Erkennung. func TestLooksLikeMP3(t *testing.T) { yes := [][]byte{ fixture(t, fixtureMP3StereoB64), // ID3-Tag {0xFF, 0xFB, 0x00, 0x00}, // roher Frame-Sync } no := [][]byte{ []byte("RIFF....WAVE"), []byte("kein audio"), {0xFF}, {}, } for _, d := range yes { if !looksLikeMP3(d) { t.Errorf("looksLikeMP3(%x) = false, want true", d[:min(4, len(d))]) } } for _, d := range no { if looksLikeMP3(d) { t.Errorf("looksLikeMP3(%q) = true, want false", d) } } } // TestDecodeAudioWeiche: RIFF geht an den WAV-Parser, MP3 an den MP3- // Dekoder, alles andere fällt mit dem WAV-Fehler durch. func TestDecodeAudioWeiche(t *testing.T) { f, _, err := decodeAudio(buildWAV(1, 1, 22050, 16, []byte{1, 2, 3, 4})) if err != nil || f.SampleRate != 22050 { t.Errorf("decodeAudio(WAV) = %+v, %v — soll den WAV-Pfad nehmen", f, err) } f, _, err = decodeAudio(fixture(t, fixtureMP3MonoB64)) if err != nil || f.SampleRate != 24000 { t.Errorf("decodeAudio(MP3) = %+v, %v — soll den MP3-Pfad nehmen", f, err) } if _, _, err = decodeAudio([]byte("kein audio")); err == nil || !strings.Contains(err.Error(), "wav:") { t.Errorf("decodeAudio(Müll) = %v, will WAV-Fehler", err) } }