mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-14 06:36:15 +00:00
Speakers need a moment to start buffering after receiving a ContentItem; the first ~2 s of audio is often missed. Repeating the ding 3 times with 0.4 s gaps between each ensures at least one repetition is audible. - Add Repeat (default 3) and RepeatGapDuration (default 0.40 s) to Options - Render() appends silence + base audio for each extra repetition - WithDefaults() fills zero values for the new fields - Handler exposes ?repeat= (1–10) and ?repeat-gap-ms= query knobs - Update TestRender_DefaultSizeApproximately52KB → ~229 KB (2.6 s) - Add TestRender_RepeatProducesLongerAudio Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
164 lines
4.7 KiB
Go
164 lines
4.7 KiB
Go
package ding
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"testing"
|
|
)
|
|
|
|
func TestRender_ProducesWAVHeader(t *testing.T) {
|
|
data := Render(DefaultOptions())
|
|
if len(data) < 44 {
|
|
t.Fatalf("expected at least 44 bytes (WAV header), got %d", len(data))
|
|
}
|
|
|
|
if !bytes.HasPrefix(data, []byte("RIFF")) {
|
|
t.Errorf("expected RIFF prefix")
|
|
}
|
|
|
|
if !bytes.Equal(data[8:12], []byte("WAVE")) {
|
|
t.Errorf("expected WAVE format marker")
|
|
}
|
|
|
|
if !bytes.Equal(data[12:16], []byte("fmt ")) {
|
|
t.Errorf("expected fmt chunk")
|
|
}
|
|
|
|
// PCM format
|
|
if pcm := binary.LittleEndian.Uint16(data[20:22]); pcm != 1 {
|
|
t.Errorf("expected PCM (1), got %d", pcm)
|
|
}
|
|
|
|
// Channels
|
|
if ch := binary.LittleEndian.Uint16(data[22:24]); ch != 2 {
|
|
t.Errorf("expected stereo (2), got %d", ch)
|
|
}
|
|
|
|
// Sample rate
|
|
if sr := binary.LittleEndian.Uint32(data[24:28]); sr != 22050 {
|
|
t.Errorf("expected default sample rate 22050, got %d", sr)
|
|
}
|
|
|
|
// Bits per sample
|
|
if bps := binary.LittleEndian.Uint16(data[34:36]); bps != 16 {
|
|
t.Errorf("expected 16 bits/sample, got %d", bps)
|
|
}
|
|
}
|
|
|
|
func TestRender_DefaultSizeApproximately229KB(t *testing.T) {
|
|
data := Render(DefaultOptions())
|
|
|
|
// Default: 3 repetitions of 0.6 s + 2 gaps of 0.4 s = 2.6 s total.
|
|
// 22050 Hz * 2 ch * 2 bytes * 2.6 s ≈ 229320 data bytes + 44 byte header.
|
|
const wantData = 22050 * 2 * 2 * 260 / 100 // 2.6 seconds, integer math
|
|
if got := len(data); got < wantData || got > wantData+500 {
|
|
t.Errorf("expected ~%d bytes, got %d", wantData, got)
|
|
}
|
|
}
|
|
|
|
func TestRender_OverridePitchAffectsContent(t *testing.T) {
|
|
a := Render(DefaultOptions())
|
|
b := Render(Options{PitchHigh: 1200}.WithDefaults())
|
|
|
|
if bytes.Equal(a, b) {
|
|
t.Errorf("expected different content for different PitchHigh values")
|
|
}
|
|
|
|
// Same length (envelope doesn't change).
|
|
if len(a) != len(b) {
|
|
t.Errorf("expected same byte length: %d vs %d", len(a), len(b))
|
|
}
|
|
}
|
|
|
|
func TestRender_OverrideSampleRateChangesByteRate(t *testing.T) {
|
|
data := Render(Options{SampleRate: 44100}.WithDefaults())
|
|
if sr := binary.LittleEndian.Uint32(data[24:28]); sr != 44100 {
|
|
t.Errorf("expected 44100, got %d", sr)
|
|
}
|
|
}
|
|
|
|
func TestSafeSampleRate_ClampsOutOfRange(t *testing.T) {
|
|
cases := []struct {
|
|
in int
|
|
want uint32
|
|
}{
|
|
{22050, 22050},
|
|
{44100, 44100},
|
|
{192000, 192000},
|
|
{0, 22050}, // zero → default
|
|
{-1, 22050}, // negative → default
|
|
{200000, 22050}, // above max → default
|
|
{1 << 31, 22050}, // way beyond uint32 → default (the original CodeQL concern)
|
|
{1 << 33, 22050}, // wraps to a different value on int→uint32; default protects us
|
|
{int(maxSampleRate) + 1, 22050},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
if got := safeSampleRate(c.in); got != c.want {
|
|
t.Errorf("safeSampleRate(%d) = %d, want %d", c.in, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRender_HugeSampleRateDoesNotTruncateOrPanic(t *testing.T) {
|
|
// Regression for the int→uint32 truncation CodeQL flagged.
|
|
// A caller (test, future SDK user) bypassing the handler's
|
|
// sampleRateParam guard with an int well above uint32 used
|
|
// to silently wrap. The defensive clamp now substitutes the
|
|
// default sample rate instead.
|
|
data := Render(Options{SampleRate: 1 << 33}.WithDefaults())
|
|
if sr := binary.LittleEndian.Uint32(data[24:28]); sr != uint32(DefaultOptions().SampleRate) {
|
|
t.Errorf("expected clamp to default sample rate, got %d", sr)
|
|
}
|
|
}
|
|
|
|
func TestRender_RepeatProducesLongerAudio(t *testing.T) {
|
|
once := Render(Options{Repeat: 1}.WithDefaults())
|
|
thrice := Render(Options{Repeat: 3}.WithDefaults())
|
|
|
|
if len(thrice) <= len(once) {
|
|
t.Errorf("expected Repeat:3 to produce more bytes than Repeat:1: %d vs %d", len(thrice), len(once))
|
|
}
|
|
}
|
|
|
|
func TestWithDefaults_FillsZeroFields(t *testing.T) {
|
|
got := Options{PitchHigh: 1000}.WithDefaults()
|
|
if got.PitchHigh != 1000 {
|
|
t.Errorf("override should be preserved, got %f", got.PitchHigh)
|
|
}
|
|
|
|
if got.SampleRate != 22050 {
|
|
t.Errorf("expected default SampleRate, got %d", got.SampleRate)
|
|
}
|
|
|
|
if got.PitchMid <= 0 {
|
|
t.Errorf("expected PitchMid filled from default, got %f", got.PitchMid)
|
|
}
|
|
}
|
|
|
|
func TestWithDefaults_ClampsInvalidPeak(t *testing.T) {
|
|
got := Options{Peak: 2.0}.WithDefaults()
|
|
if got.Peak != DefaultOptions().Peak {
|
|
t.Errorf("expected default Peak for invalid input, got %f", got.Peak)
|
|
}
|
|
|
|
got = Options{Peak: -0.5}.WithDefaults()
|
|
if got.Peak != DefaultOptions().Peak {
|
|
t.Errorf("expected default Peak for negative input, got %f", got.Peak)
|
|
}
|
|
}
|
|
|
|
func TestRender_SamplesDontClip(t *testing.T) {
|
|
data := Render(DefaultOptions())
|
|
|
|
// Walk every sample, ensure no value is at the 16-bit extreme
|
|
// (which would indicate clipping). Header is 44 bytes.
|
|
for i := 44; i+1 < len(data); i += 2 {
|
|
s := int16(binary.LittleEndian.Uint16(data[i : i+2]))
|
|
if s == 32767 || s == -32768 {
|
|
t.Errorf("sample at offset %d hit clipping (%d)", i, s)
|
|
return
|
|
}
|
|
}
|
|
}
|