feat(audio): Audio-Sinks oto und null mit eigener WAV-Dekodierung
Sink-Vertrag Play/Name; oto-Sink (github.com/ebitengine/oto/v3, auf v3.5.0 gepinnt — aktuelle stabile Major-Version; purego statt CGO, damit cross-compilierbar) spielt RIFF/WAVE-PCM blockierend, ein Context pro PCM-Format gecacht, da oto nur einen Kontext pro Prozess erlaubt. null-Sink verwirft (CI/Server/Tests) und loggt optional. WAV-Parser (RIFF-Chunks, PCM 8/16 Bit, Float32) mit klarer Fehler für alles andere. Echte Wiedergabe nur hinter TALK_TEST_REAL_AUDIO=1.
This commit is contained in:
1 parent
5f450ea015
commit
523e6f4bf2
7 files changed
+439
-4
No files matched your search
@@ -2,4 +2,13 @@ module code.m3ta.dev/m3tam3re/talk
|
|||||||
|
|
||||||
go 1.26.8
|
go 1.26.8
|
||||||
|
|
||||||
require github.com/BurntSushi/toml v1.6.0
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.6.0
|
||||||
|
github.com/ebitengine/oto/v3 v3.5.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/ebitengine/purego v0.11.0 // indirect
|
||||||
|
github.com/jfreymuth/pulse v0.1.3 // indirect
|
||||||
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
|
)
|
||||||
@@ -1,2 +1,10 @@
|
|||||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
|
github.com/ebitengine/oto/v3 v3.5.0 h1:8ZDhKP3Hhslyb9NL3g4HWvhRQqwI+hKOHAXAqD6GPXs=
|
||||||
|
github.com/ebitengine/oto/v3 v3.5.0/go.mod h1:Elkm7yzTRns3w2efvibzVOoQ65YOwmec9a76dCiK10o=
|
||||||
|
github.com/ebitengine/purego v0.11.0 h1:jhp/D+Nyv7UUW8HAcmcjt2N2rYrYi9m3SL21k0Ua/NI=
|
||||||
|
github.com/ebitengine/purego v0.11.0/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
|
||||||
|
github.com/jfreymuth/pulse v0.1.3 h1:bc5TdxiB8E+2INnFjFWWgyfgXtz2IyNNNCX+Wt/ZD14=
|
||||||
|
github.com/jfreymuth/pulse v0.1.3/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no=
|
||||||
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package audio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sink-Namen (Teil des öffentlichen Vertrags; Spec: audio.sink = "oto"|"null").
|
||||||
|
const (
|
||||||
|
NameOto = "oto"
|
||||||
|
NameNull = "null"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sink gibt Audio-Bytes wieder. Die Bytes stammen aus Provider.Synthesize;
|
||||||
|
// oto versteht RIFF/WAVE (PCM) — das Lieferformat von osbuiltin.
|
||||||
|
type Sink interface {
|
||||||
|
// Play spielt die Daten blockierend ab (bis zum Ende oder bis ctx
|
||||||
|
// abgebrochen wird).
|
||||||
|
Play(ctx context.Context, data []byte) error
|
||||||
|
|
||||||
|
// Name liefert den Sink-Namen (audio.sink-Konfiguration).
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSink liefert den Sink zum konfigurierten Namen ("oto" oder "null").
|
||||||
|
// Unbekannte Namen sind ein klarer Fehler.
|
||||||
|
func NewSink(name string) (Sink, error) {
|
||||||
|
switch name {
|
||||||
|
case NameOto:
|
||||||
|
return OtoSink{}, nil
|
||||||
|
case NameNull:
|
||||||
|
return NullSink{}, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unbekannter audio sink %q (erlaubt: %q, %q)", name, NameOto, NameNull)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NullSink verwirft jede Ansage ohne Ton — für CI, Server und Tests. Wurde
|
||||||
|
// ein W gesetzt, loggt Play eine Zeile darüber (der CLI-Pfad hängt stderr
|
||||||
|
// an, damit „läuft ohne Ton durch" auch sichtbar bleibt).
|
||||||
|
type NullSink struct {
|
||||||
|
W io.Writer // optionaler Log-Output; nil = still verwerfen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name implementiert Sink.
|
||||||
|
func (NullSink) Name() string { return NameNull }
|
||||||
|
|
||||||
|
// Play implementiert Sink: verwirft die Daten.
|
||||||
|
func (n NullSink) Play(ctx context.Context, data []byte) error {
|
||||||
|
if n.W != nil {
|
||||||
|
fmt.Fprintf(n.W, "audio: %d Bytes verworfen (null sink)\n", len(data))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package audio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// buildWAV baut nach RIFF-Spezifikation eine WAV-Datei (unabhängig vom
|
||||||
|
// Parser implementiert, damit Parser und Test-Fixture nicht denselben
|
||||||
|
// Fehler teilen).
|
||||||
|
func buildWAV(audioFormat uint16, channels, rate, bits int, samples []byte) []byte {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString("RIFF")
|
||||||
|
payload := 28 + len(samples) // fmt (8+16) + data (8+n) + 4 bytes WAVE
|
||||||
|
binary.Write(&buf, binary.LittleEndian, uint32(payload))
|
||||||
|
buf.WriteString("WAVE")
|
||||||
|
|
||||||
|
buf.WriteString("fmt ")
|
||||||
|
binary.Write(&buf, binary.LittleEndian, uint32(16))
|
||||||
|
binary.Write(&buf, binary.LittleEndian, audioFormat)
|
||||||
|
binary.Write(&buf, binary.LittleEndian, uint16(channels))
|
||||||
|
binary.Write(&buf, binary.LittleEndian, uint32(rate))
|
||||||
|
blockAlign := channels * bits / 8
|
||||||
|
binary.Write(&buf, binary.LittleEndian, uint32(rate*blockAlign))
|
||||||
|
binary.Write(&buf, binary.LittleEndian, uint16(blockAlign))
|
||||||
|
binary.Write(&buf, binary.LittleEndian, uint16(bits))
|
||||||
|
|
||||||
|
buf.WriteString("data")
|
||||||
|
binary.Write(&buf, binary.LittleEndian, uint32(len(samples)))
|
||||||
|
buf.Write(samples)
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeWAV(t *testing.T) {
|
||||||
|
t.Run("16-bit mono", func(t *testing.T) {
|
||||||
|
wav := buildWAV(1, 1, 22050, 16, []byte{1, 2, 3, 4})
|
||||||
|
f, pcm, err := decodeWAV(wav)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decodeWAV: %v", err)
|
||||||
|
}
|
||||||
|
if f.SampleRate != 22050 || f.Channels != 1 || f.BitsPerCoded != 16 || f.Float {
|
||||||
|
t.Errorf("Format = %+v", f)
|
||||||
|
}
|
||||||
|
if string(pcm) != "\x01\x02\x03\x04" {
|
||||||
|
t.Errorf("PCM = %x", pcm)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("8-bit stereo mit Extra-Chunk", func(t *testing.T) {
|
||||||
|
wav := buildWAV(1, 2, 8000, 8, []byte{7, 8, 9})
|
||||||
|
// LIST-Chunk (word-aligned, ungerade Länge) zwischen fmt und data einfügen
|
||||||
|
withList := append(wav[:36], append([]byte("LIST\x03\x00\x00\x00abc\x00"), wav[36:]...)...)
|
||||||
|
f, pcm, err := decodeWAV(withList)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decodeWAV: %v", err)
|
||||||
|
}
|
||||||
|
if f.BitsPerCoded != 8 || f.Channels != 2 {
|
||||||
|
t.Errorf("Format = %+v", f)
|
||||||
|
}
|
||||||
|
if string(pcm) != "\x07\x08\x09" {
|
||||||
|
t.Errorf("PCM = %x", pcm)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("float32", func(t *testing.T) {
|
||||||
|
wav := buildWAV(3, 1, 44100, 32, []byte{0, 0, 0, 0})
|
||||||
|
f, _, err := decodeWAV(wav)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decodeWAV: %v", err)
|
||||||
|
}
|
||||||
|
if !f.Float || f.BitsPerCoded != 32 {
|
||||||
|
t.Errorf("Format = %+v, want Float32", f)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("leerer data-Chunk ist erlaubt", func(t *testing.T) {
|
||||||
|
wav := buildWAV(1, 1, 22050, 16, nil)
|
||||||
|
_, pcm, err := decodeWAV(wav)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decodeWAV: %v", err)
|
||||||
|
}
|
||||||
|
if len(pcm) != 0 {
|
||||||
|
t.Errorf("PCM = %x, want leer", pcm)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeWAVFehler(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
data []byte
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"zu kurz", []byte("RIFF"), "zu kurz"},
|
||||||
|
{"kein riff", []byte("RIFX" + "\x00\x00\x00\x00" + "WAVE"), "RIFF"},
|
||||||
|
{"24-bit pcm", buildWAV(1, 1, 44100, 24, []byte{1, 2, 3}), "nicht unterstütztes Format"},
|
||||||
|
{"3 kanäle", buildWAV(1, 3, 44100, 16, []byte{1, 2, 3, 4, 5, 6}), "Kanäle"},
|
||||||
|
{"ohne data", func() []byte {
|
||||||
|
wav := buildWAV(1, 1, 22050, 16, []byte{1, 2})
|
||||||
|
return wav[:36] // nur fmt-Chunk behalten
|
||||||
|
}(), "data"},
|
||||||
|
{"kaputter chunk", append(buildWAV(1, 1, 22050, 16, nil), "data\xff\xff\xff\xff"...), "überragt"},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
_, _, err := decodeWAV(tc.data)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||||
|
t.Errorf("decodeWAV-Fehler = %v, will Meldung mit %q", err, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSink(t *testing.T) {
|
||||||
|
for _, name := range []string{NameOto, NameNull} {
|
||||||
|
s, err := NewSink(name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewSink(%q): %v", name, err)
|
||||||
|
}
|
||||||
|
if s.Name() != name {
|
||||||
|
t.Errorf("NewSink(%q).Name() = %q", name, s.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := NewSink("mpv"); err == nil {
|
||||||
|
t.Error("NewSink(\"mpv\") soll fehlschlagen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNullSinkVerwirftUndLoggt(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
sink := NullSink{W: &buf}
|
||||||
|
if err := sink.Play(context.Background(), []byte("audio")); err != nil {
|
||||||
|
t.Fatalf("NullSink.Play: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(buf.String(), "verworfen") {
|
||||||
|
t.Errorf("NullSink-Log = %q, will Verwurf-Zeile", buf.String())
|
||||||
|
}
|
||||||
|
if sink.Name() != NameNull {
|
||||||
|
t.Errorf("Name() = %q", sink.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOtoSinkUngueltigeDaten: OtoSink.Play muss an der WAV-Dekodierung
|
||||||
|
// scheitern, bevor irgend ein Audio-Gerät angefasst wird — das ist der
|
||||||
|
// CI-/Server-sichere Fehlerpfad des oto-Sinks.
|
||||||
|
func TestOtoSinkUngueltigeDaten(t *testing.T) {
|
||||||
|
err := (OtoSink{}).Play(context.Background(), []byte("kein audio"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("OtoSink.Play mit Müll: kein Fehler")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "wav:") {
|
||||||
|
t.Errorf("Fehler soll aus dem WAV-Parser kommen: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOtoSinkEchteWiedergabe läuft nur mit TALK_TEST_REAL_AUDIO=1 (erzeugt
|
||||||
|
// echten Ton); Standard-Testläufe bleiben stumm.
|
||||||
|
func TestOtoSinkEchteWiedergabe(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Übersprungen (-short)")
|
||||||
|
}
|
||||||
|
if os.Getenv("TALK_TEST_REAL_AUDIO") != "1" {
|
||||||
|
t.Skip("Übersprungen (hörbar) — mit TALK_TEST_REAL_AUDIO=1 ausführen")
|
||||||
|
}
|
||||||
|
// 0,1 s 440-Hz-Sinus, 16 Bit mono 22050 Hz.
|
||||||
|
const rate = 22050
|
||||||
|
samples := make([]byte, rate/10*2)
|
||||||
|
for i := 0; i < rate/10; i++ {
|
||||||
|
v := int16(8000 * math.Sin(float64(i)/float64(rate)*2*math.Pi*440))
|
||||||
|
binary.LittleEndian.PutUint16(samples[i*2:], uint16(v))
|
||||||
|
}
|
||||||
|
if err := (OtoSink{}).Play(context.Background(), buildWAV(1, 1, rate, 16, samples)); err != nil {
|
||||||
|
t.Fatalf("OtoSink.Play: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// Package audio implementiert die Audio-Ausgabe von talk über das
|
// Package audio implementiert die Audio-Ausgabe von talk über das
|
||||||
// Sink-Interface: Playback embedded via oto (PCM/WAV) und ein null-Sink,
|
// Sink-Interface: Playback embedded via oto (RIFF/WAVE-PCM) und ein
|
||||||
// das verwirft (CI, Server, Tests). Auswahl per Config audio.sink.
|
// null-Sink, das verwirft (CI, Server, Tests). Auswahl per Config
|
||||||
// Implementierung folgt in Task 02.
|
// audio.sink = "oto" | "null" (Default oto).
|
||||||
package audio
|
package audio
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package audio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ebitengine/oto/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OtoSink spielt Audio embedded über oto (github.com/ebitengine/oto/v3,
|
||||||
|
// gepinnt auf v3.5.0) ab — kein externer Player nötig. oto lädt die
|
||||||
|
// Audiotreiber zur Laufzeit ohne CGO (purego), dadurch bleibt talk
|
||||||
|
// cross-compilierbar.
|
||||||
|
type OtoSink struct{}
|
||||||
|
|
||||||
|
// Name implementiert Sink.
|
||||||
|
func (OtoSink) Name() string { return NameOto }
|
||||||
|
|
||||||
|
// contextCache hält je PCM-Format einen oto-Kontext vor: oto unterstützt
|
||||||
|
// nur EINEN Kontext pro Prozess ("Creating multiple contexts is NOT
|
||||||
|
// supported"), und ein Kontext ist an genau ein Format gebunden. Die CLI
|
||||||
|
// spielt pro Prozess einmal (Task 02); der Daemon (Task 04) profitiert vom
|
||||||
|
// Cache.
|
||||||
|
var contextCache = struct {
|
||||||
|
sync.Mutex
|
||||||
|
byFormat map[pcmFormat]*otoContext
|
||||||
|
}{byFormat: map[pcmFormat]*otoContext{}}
|
||||||
|
|
||||||
|
type otoContext struct {
|
||||||
|
ctx *oto.Context
|
||||||
|
ready chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// otoFormat mappt ein PCM-Format auf das oto-Format.
|
||||||
|
func (f pcmFormat) otoFormat() oto.Format {
|
||||||
|
switch {
|
||||||
|
case f.Float:
|
||||||
|
return oto.FormatFloat32LE
|
||||||
|
case f.BitsPerCoded == 8:
|
||||||
|
return oto.FormatUnsignedInt8
|
||||||
|
default:
|
||||||
|
return oto.FormatSignedInt16LE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// contextFor liefert den (gecachten) oto-Kontext für ein Format.
|
||||||
|
func contextFor(f pcmFormat) (*otoContext, error) {
|
||||||
|
contextCache.Lock()
|
||||||
|
defer contextCache.Unlock()
|
||||||
|
if c, ok := contextCache.byFormat[f]; ok {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
op := &oto.NewContextOptions{
|
||||||
|
SampleRate: f.SampleRate,
|
||||||
|
ChannelCount: f.Channels,
|
||||||
|
Format: f.otoFormat(),
|
||||||
|
}
|
||||||
|
ctx, ready, err := oto.NewContext(op)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("oto: Kontext (Audio-Gerät?) fehlgeschlagen: %w", err)
|
||||||
|
}
|
||||||
|
c := &otoContext{ctx: ctx, ready: ready}
|
||||||
|
contextCache.byFormat[f] = c
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play implementiert Sink: dekodiert WAV (RIFF/PCM) und spielt die Samples
|
||||||
|
// blockierend ab. Fehler des Audio-Geräts oder des Formats werden klar
|
||||||
|
// gemeldet.
|
||||||
|
func (OtoSink) Play(ctx context.Context, data []byte) error {
|
||||||
|
f, pcm, err := decodeWAV(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c, err := contextFor(f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-c.ready:
|
||||||
|
if err := c.ctx.Err(); err != nil {
|
||||||
|
return fmt.Errorf("oto: Initialisierung fehlgeschlagen: %w", err)
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
player := c.ctx.NewPlayer(bytes.NewReader(pcm))
|
||||||
|
defer player.Close()
|
||||||
|
player.Play()
|
||||||
|
for player.IsPlaying() {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
player.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := player.Err(); err != nil {
|
||||||
|
return fmt.Errorf("oto: Wiedergabe fehlgeschlagen: %w", err)
|
||||||
|
}
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
}
|
||||||
|
return player.Err()
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package audio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pcmFormat beschreibt rohe PCM-Samples, wie sie oto zur Wiedergabe
|
||||||
|
// braucht.
|
||||||
|
type pcmFormat struct {
|
||||||
|
SampleRate int
|
||||||
|
Channels int
|
||||||
|
BitsPerCoded int // 8 (unsigned), 16 (signed LE), 32 (nur IEEE float)
|
||||||
|
Float bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeWAV zerlegt RIFF/WAVE-Bytes in Format und PCM-Daten.
|
||||||
|
//
|
||||||
|
// Unterstützt: PCM (AudioFormat 1) mit 8/16 Bit sowie IEEE-Float
|
||||||
|
// (AudioFormat 3) mit 32 Bit; Mono und Stereo. Alles andere — erst recht
|
||||||
|
// komprimierte Formate — ist ein klarer Fehler; die OS-TTS-Wege von
|
||||||
|
// osbuiltin liefern ausschließlich 16-Bit-PCM-WAV.
|
||||||
|
func decodeWAV(data []byte) (pcmFormat, []byte, error) {
|
||||||
|
var f pcmFormat
|
||||||
|
if len(data) < 12 {
|
||||||
|
return f, nil, fmt.Errorf("wav: zu kurz für RIFF-Header (%d Bytes)", len(data))
|
||||||
|
}
|
||||||
|
if string(data[0:4]) != "RIFF" || string(data[8:12]) != "WAVE" {
|
||||||
|
return f, nil, fmt.Errorf("wav: kein RIFF/WAVE-Header")
|
||||||
|
}
|
||||||
|
|
||||||
|
var haveFmt bool
|
||||||
|
var payload []byte
|
||||||
|
pos := 12
|
||||||
|
for pos+8 <= len(data) {
|
||||||
|
id := string(data[pos : pos+4])
|
||||||
|
size := int(binary.LittleEndian.Uint32(data[pos+4 : pos+8]))
|
||||||
|
body := pos + 8
|
||||||
|
if body+size > len(data) {
|
||||||
|
return f, nil, fmt.Errorf("wav: Chunk %q überragt die Datei (Size %d)", id, size)
|
||||||
|
}
|
||||||
|
switch id {
|
||||||
|
case "fmt ":
|
||||||
|
if size < 16 {
|
||||||
|
return f, nil, fmt.Errorf("wav: fmt-Chunk zu kurz (%d)", size)
|
||||||
|
}
|
||||||
|
audioFormat := binary.LittleEndian.Uint16(data[body : body+2])
|
||||||
|
f.Channels = int(binary.LittleEndian.Uint16(data[body+2 : body+4]))
|
||||||
|
f.SampleRate = int(binary.LittleEndian.Uint32(data[body+4 : body+8]))
|
||||||
|
f.BitsPerCoded = int(binary.LittleEndian.Uint16(data[body+14 : body+16]))
|
||||||
|
switch {
|
||||||
|
case audioFormat == 1 && (f.BitsPerCoded == 8 || f.BitsPerCoded == 16):
|
||||||
|
case audioFormat == 3 && f.BitsPerCoded == 32:
|
||||||
|
f.Float = true
|
||||||
|
default:
|
||||||
|
return f, nil, fmt.Errorf("wav: nicht unterstütztes Format (AudioFormat %d, %d Bit) — erlaubt: PCM 8/16 Bit, Float 32 Bit",
|
||||||
|
audioFormat, f.BitsPerCoded)
|
||||||
|
}
|
||||||
|
if f.Channels < 1 || f.Channels > 2 {
|
||||||
|
return f, nil, fmt.Errorf("wav: %d Kanäle (erlaubt: 1, 2)", f.Channels)
|
||||||
|
}
|
||||||
|
if f.SampleRate <= 0 {
|
||||||
|
return f, nil, fmt.Errorf("wav: SampleRate %d", f.SampleRate)
|
||||||
|
}
|
||||||
|
haveFmt = true
|
||||||
|
case "data":
|
||||||
|
payload = data[body : body+size]
|
||||||
|
}
|
||||||
|
pos = body + size
|
||||||
|
if size%2 == 1 { // Chunks sind word-aligned
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !haveFmt {
|
||||||
|
return f, nil, fmt.Errorf("wav: kein fmt-Chunk")
|
||||||
|
}
|
||||||
|
if payload == nil {
|
||||||
|
return f, nil, fmt.Errorf("wav: kein data-Chunk")
|
||||||
|
}
|
||||||
|
return f, payload, nil
|
||||||
|
}
|
||||||
Reference in new issue
Block a user