178 lines
4.3 KiB
Go
178 lines
4.3 KiB
Go
package spectrogram
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"hash/crc32"
|
|
"image"
|
|
"image/color"
|
|
"image/png"
|
|
"testing"
|
|
)
|
|
|
|
func TestImageToWAVAndRenderSpectrogram(t *testing.T) {
|
|
img := testPNG(t)
|
|
wav, summary, err := ImageToWAV(img, EncodeOptions{
|
|
DurationSec: 0.25,
|
|
SampleRate: 8000,
|
|
MinHz: 300,
|
|
MaxHz: 3000,
|
|
Bins: 32,
|
|
Columns: 32,
|
|
Invert: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ImageToWAV failed: %v", err)
|
|
}
|
|
if !isWAV(wav) {
|
|
t.Fatalf("expected WAV header")
|
|
}
|
|
if summary["wavBytes"].(int) != len(wav) {
|
|
t.Fatalf("summary wavBytes mismatch")
|
|
}
|
|
|
|
rendered, viewSummary, err := AudioSpectrogramPNG(wav, "test.wav", ViewOptions{
|
|
FFTSize: 512,
|
|
Height: 96,
|
|
Aspect: 2,
|
|
MaxWidth: 128,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("AudioSpectrogramPNG failed: %v", err)
|
|
}
|
|
if _, err := png.Decode(bytes.NewReader(rendered)); err != nil {
|
|
t.Fatalf("rendered spectrogram is not a PNG: %v", err)
|
|
}
|
|
if viewSummary["sampleRate"].(int) != 8000 {
|
|
t.Fatalf("unexpected sample rate: %v", viewSummary["sampleRate"])
|
|
}
|
|
if viewSummary["height"].(int) != 192 {
|
|
t.Fatalf("expected aspect-adjusted height 192, got %v", viewSummary["height"])
|
|
}
|
|
}
|
|
|
|
func TestPreserveAspectRatioAdjustsFrequencyRange(t *testing.T) {
|
|
img := testPNGSize(t, 32, 16)
|
|
_, summary, err := ImageToWAV(img, EncodeOptions{
|
|
DurationSec: 0.1,
|
|
SampleRate: 8000,
|
|
MinHz: 200,
|
|
MaxHz: 3000,
|
|
Bins: 16,
|
|
Columns: 16,
|
|
PreserveAR: true,
|
|
Invert: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ImageToWAV failed: %v", err)
|
|
}
|
|
if summary["preserveAR"] != true {
|
|
t.Fatalf("expected preserveAR summary")
|
|
}
|
|
if got := summary["maxHz"].(float64); got != 1600 {
|
|
t.Fatalf("expected landscape image to halve frequency range to 1600Hz, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestImageToWAVRejectsOversizedImageDimensionsBeforeDecode(t *testing.T) {
|
|
_, _, err := ImageToWAV(hugePNGHeader(t, 100000, 100000), EncodeOptions{})
|
|
if err == nil {
|
|
t.Fatal("expected oversized image dimensions to be rejected")
|
|
}
|
|
if !bytes.Contains([]byte(err.Error()), []byte("image dimensions are too large")) {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestInsertImageIntoAudioExtendsCarrier(t *testing.T) {
|
|
img := testPNG(t)
|
|
carrier, _, err := ImageToWAV(img, EncodeOptions{
|
|
DurationSec: 0.1,
|
|
SampleRate: 8000,
|
|
MinHz: 300,
|
|
MaxHz: 2500,
|
|
Bins: 16,
|
|
Columns: 16,
|
|
Invert: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("carrier generation failed: %v", err)
|
|
}
|
|
out, summary, err := InsertImageIntoAudio(img, carrier, "carrier.wav", InsertOptions{
|
|
EncodeOptions: EncodeOptions{
|
|
DurationSec: 0.2,
|
|
MinHz: 300,
|
|
MaxHz: 2500,
|
|
Bins: 16,
|
|
Columns: 16,
|
|
Invert: true,
|
|
},
|
|
StartSec: 0.05,
|
|
MixLevel: 0.25,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("InsertImageIntoAudio failed: %v", err)
|
|
}
|
|
if !isWAV(out) {
|
|
t.Fatalf("expected WAV output")
|
|
}
|
|
if summary["mode"] != "insert-into-audio" {
|
|
t.Fatalf("unexpected mode: %v", summary["mode"])
|
|
}
|
|
if len(out) <= len(carrier) {
|
|
t.Fatalf("expected carrier to be extended")
|
|
}
|
|
}
|
|
|
|
func testPNG(t *testing.T) []byte {
|
|
t.Helper()
|
|
return testPNGSize(t, 32, 32)
|
|
}
|
|
|
|
func testPNGSize(t *testing.T, width, height int) []byte {
|
|
t.Helper()
|
|
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
|
for y := 0; y < height; y++ {
|
|
for x := 0; x < width; x++ {
|
|
img.Set(x, y, color.RGBA{R: 255, G: 255, B: 255, A: 255})
|
|
}
|
|
}
|
|
limit := width
|
|
if height < limit {
|
|
limit = height
|
|
}
|
|
for i := 0; i < limit; i++ {
|
|
img.Set(i, i, color.RGBA{A: 255})
|
|
img.Set(width-1-i, i, color.RGBA{A: 255})
|
|
}
|
|
var buf bytes.Buffer
|
|
if err := png.Encode(&buf, img); err != nil {
|
|
t.Fatalf("encode png: %v", err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func hugePNGHeader(t *testing.T, width, height uint32) []byte {
|
|
t.Helper()
|
|
var buf bytes.Buffer
|
|
buf.Write([]byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'})
|
|
ihdr := make([]byte, 13)
|
|
binary.BigEndian.PutUint32(ihdr[0:4], width)
|
|
binary.BigEndian.PutUint32(ihdr[4:8], height)
|
|
ihdr[8] = 8
|
|
ihdr[9] = 2
|
|
writePNGChunk(&buf, "IHDR", ihdr)
|
|
writePNGChunk(&buf, "IEND", nil)
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func writePNGChunk(buf *bytes.Buffer, kind string, data []byte) {
|
|
_ = binary.Write(buf, binary.BigEndian, uint32(len(data)))
|
|
buf.WriteString(kind)
|
|
buf.Write(data)
|
|
crc := crc32.NewIEEE()
|
|
crc.Write([]byte(kind))
|
|
crc.Write(data)
|
|
_ = binary.Write(buf, binary.BigEndian, crc.Sum32())
|
|
}
|