Files
Maven/internal/audio/audio_test.go
T
claude 936c6d71db audio and tts sweep: walk WAV chunks, name the stub sample rate (V-581)
The WAV parser now walks chunk headers to find the data chunk instead of
scanning for the four bytes "data". A LIST chunk between fmt and data is
common, arecord and ffmpeg both write one, and its payload is free text that
can spell the word. A byte scan took that text for a chunk header and read the
comment as samples.

WAVHeader named WAVFromPCM in its error, so a caller of WAVHeader read the
wrong function. internal/capture calls it twice.

PCMFromWAV returns PCM that aliases the buffer it was given. That is the right
trade for a long recording and it was undocumented, so the doc comment now says
so and names the two ways a caller gets it wrong.

The TTS stub wrote 16000 three times. It reads the rate and the sample width
off audio.PCM16kMono now, so the tone stays in tune with the shape the seam
declares, and the sample write goes through binary.LittleEndian.

Identify computed the clip length twice to report it once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 03:28:45 +04:00

137 lines
3.9 KiB
Go

package audio
import (
"bytes"
"encoding/binary"
"testing"
)
func TestAudioDuration(t *testing.T) {
t.Parallel()
// 16k mono int16 ⇒ 2 bytes/sample ⇒ 32000 bytes/second.
a := Audio{Format: PCM16kMono, Bytes: make([]byte, 32000)}
if got, want := a.Duration(), 1.0; got != want {
t.Fatalf("Duration: got %v, want %v", got, want)
}
// 4 seconds
a.Bytes = make([]byte, 128000)
if got, want := a.Duration(), 4.0; got != want {
t.Fatalf("Duration: got %v, want %v", got, want)
}
}
func TestAudioDurationEmptyAndBad(t *testing.T) {
t.Parallel()
if (Audio{}).Duration() != 0 {
t.Fatalf("empty audio: Duration should be 0")
}
a := Audio{Format: Format{}, Bytes: make([]byte, 32000)}
if a.Duration() != 0 {
t.Fatalf("zero format: Duration should be 0")
}
a = Audio{Format: Format{SampleRate: 16000, Channels: 1, SampleBits: 16, Encoding: "opus"}, Bytes: make([]byte, 32000)}
if a.Duration() != 0 {
t.Fatalf("unsupported encoding: Duration should be 0")
}
}
func TestFormatIsValid(t *testing.T) {
t.Parallel()
if !PCM16kMono.IsValid() {
t.Fatalf("PCM16kMono should be valid")
}
if (Format{SampleRate: 8000, Channels: 1, SampleBits: 16, Encoding: "pcm_s16le"}).IsValid() {
t.Fatalf("8k should be rejected")
}
if (Format{SampleRate: 16000, Channels: 2, SampleBits: 16, Encoding: "pcm_s16le"}).IsValid() {
t.Fatalf("stereo should be rejected")
}
}
func TestWAVRoundTrip(t *testing.T) {
t.Parallel()
// 0.5s of square wave (alternating samples) — sndfile/aplay can play it.
const nsamp = 8000
pcm := make([]byte, nsamp*2)
for i := 0; i < nsamp; i++ {
var v int16 = -16384
if i%2 == 0 {
v = 16384
}
binary.LittleEndian.PutUint16(pcm[i*2:], uint16(v))
}
wav, err := WAVFromPCM(PCM16kMono, pcm)
if err != nil {
t.Fatalf("WAVFromPCM: %v", err)
}
if len(wav) != 44+len(pcm) {
t.Fatalf("wav length: got %d, want %d", len(wav), 44+len(pcm))
}
if string(wav[0:4]) != "RIFF" || string(wav[8:12]) != "WAVE" {
t.Fatalf("missing RIFF/WAVE marker: %q", wav[0:12])
}
f, pcm2, err := PCMFromWAV(wav)
if err != nil {
t.Fatalf("PCMFromWAV: %v", err)
}
if !f.IsValid() {
t.Fatalf("parsed format invalid: %+v", f)
}
if !bytes.Equal(pcm, pcm2) {
t.Fatalf("PCM mismatch after round-trip: in=%d bytes, out=%d bytes", len(pcm), len(pcm2))
}
}
// A LIST chunk sitting between fmt and data is common (arecord and ffmpeg both
// write one), and its payload is free text that can spell "data". The parser
// walks chunk headers, so the text is skipped and the real samples are read.
func TestPCMFromWAVSkipsLISTChunk(t *testing.T) {
t.Parallel()
pcm := []byte{1, 0, 2, 0, 3, 0, 4, 0}
list := []byte("LIST")
payload := []byte("INFOICMTdata is not here")
list = binary.LittleEndian.AppendUint32(list, uint32(len(payload)))
list = append(list, payload...)
plain, err := WAVFromPCM(PCM16kMono, pcm)
if err != nil {
t.Fatalf("WAVFromPCM: %v", err)
}
wav := append([]byte{}, plain[:36]...)
wav = append(wav, list...)
wav = append(wav, plain[36:]...)
binary.LittleEndian.PutUint32(wav[4:8], uint32(len(wav)-8))
f, got, err := PCMFromWAV(wav)
if err != nil {
t.Fatalf("PCMFromWAV: %v", err)
}
if !f.IsValid() {
t.Fatalf("parsed format invalid: %+v", f)
}
if !bytes.Equal(got, pcm) {
t.Fatalf("PCM mismatch: got %v, want %v", got, pcm)
}
}
func TestPCMFromWAVRejectsNonCanonical(t *testing.T) {
t.Parallel()
// too short
if _, _, err := PCMFromWAV([]byte("RIFF")); err == nil {
t.Fatalf("short input should error")
}
// bad RIFF marker
bad := make([]byte, 44)
copy(bad[0:4], []byte("RIFF"))
copy(bad[8:12], []byte("XXXX"))
if _, _, err := PCMFromWAV(bad); err == nil {
t.Fatalf("non-WAVE marker should error")
}
// format code 3 (float), canonical otherwise
wav, _ := WAVFromPCM(PCM16kMono, []byte{0, 0})
binary.LittleEndian.PutUint16(wav[20:22], 3) // IEEE float, not PCM
if _, _, err := PCMFromWAV(wav); err == nil {
t.Fatalf("non-PCM format should error")
}
}