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
@@ -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