replacement in utils/wav_metadata_test.go at line 288
[4.16117]→[4.10040:10126](∅→∅),
[4.10040]→[4.10040:10126](∅→∅) − func TestParseWAVHeader(t *testing.T) {
− // Create temporary directory for test files
+ func TestParseWAVHeader_BasicMetadata(t *testing.T) {
replacement in utils/wav_metadata_test.go at line 291
[4.10150]→[4.10150:10561](∅→∅) − t.Run("should parse basic WAV metadata", func(t *testing.T) {
− path := createTestWAVFile(t, tmpDir, "test_basic.wav", struct {
− duration float64
− sampleRate int
− channels int
− bitsPerSample int
− comment string
− artist string
− }{
− duration: 60.0,
− sampleRate: 44100,
− channels: 2,
− bitsPerSample: 16,
− comment: "",
− artist: "",
− })
+ path := createTestWAVFile(t, tmpDir, "test_basic.wav", struct {
+ duration float64
+ sampleRate int
+ channels int
+ bitsPerSample int
+ comment string
+ artist string
+ }{
+ duration: 60.0,
+ sampleRate: 44100,
+ channels: 2,
+ bitsPerSample: 16,
+ comment: "",
+ artist: "",
+ })
replacement in utils/wav_metadata_test.go at line 307
[4.10562]→[4.10562:10675](∅→∅) − metadata, err := ParseWAVHeader(path)
− if err != nil {
− t.Fatalf("Failed to parse WAV header: %v", err)
− }
+ metadata, err := ParseWAVHeader(path)
+ if err != nil {
+ t.Fatalf("Failed to parse WAV header: %v", err)
+ }
replacement in utils/wav_metadata_test.go at line 312
[4.10676]→[4.10676:10897](∅→∅) − if metadata.SampleRate != 44100 {
− t.Errorf("SampleRate incorrect: got %d, want 44100", metadata.SampleRate)
− }
−
− if metadata.Channels != 2 {
− t.Errorf("Channels incorrect: got %d, want 2", metadata.Channels)
− }
+ if metadata.SampleRate != 44100 {
+ t.Errorf("SampleRate incorrect: got %d, want 44100", metadata.SampleRate)
+ }
+ if metadata.Channels != 2 {
+ t.Errorf("Channels incorrect: got %d, want 2", metadata.Channels)
+ }
+ if metadata.BitsPerSample != 16 {
+ t.Errorf("BitsPerSample incorrect: got %d, want 16", metadata.BitsPerSample)
+ }
+ if metadata.Duration < 59.9 || metadata.Duration > 60.1 {
+ t.Errorf("Duration incorrect: got %f, want ~60.0", metadata.Duration)
+ }
+ }
replacement in utils/wav_metadata_test.go at line 326
[4.10898]→[4.10898:11018](∅→∅) − if metadata.BitsPerSample != 16 {
− t.Errorf("BitsPerSample incorrect: got %d, want 16", metadata.BitsPerSample)
− }
+ func TestParseWAVHeader_CommentMetadata(t *testing.T) {
+ tmpDir := t.TempDir()
replacement in utils/wav_metadata_test.go at line 329
[4.11019]→[4.11019:11234](∅→∅) − // Duration should be approximately 60 seconds (allow small rounding error)
− if metadata.Duration < 59.9 || metadata.Duration > 60.1 {
− t.Errorf("Duration incorrect: got %f, want ~60.0", metadata.Duration)
− }
+ expectedComment := "Recorded at 21:00:00 24/02/2025 (UTC+13) by AudioMoth 248AB50153AB0549"
+ path := createTestWAVFile(t, tmpDir, "test_comment.wav", struct {
+ duration float64
+ sampleRate int
+ channels int
+ bitsPerSample int
+ comment string
+ artist string
+ }{
+ duration: 10.0,
+ sampleRate: 48000,
+ channels: 1,
+ bitsPerSample: 16,
+ comment: expectedComment,
+ artist: "",
replacement in utils/wav_metadata_test.go at line 346
[4.11239]→[4.11239:11759](∅→∅) − t.Run("should extract comment metadata", func(t *testing.T) {
− expectedComment := "Recorded at 21:00:00 24/02/2025 (UTC+13) by AudioMoth 248AB50153AB0549"
− path := createTestWAVFile(t, tmpDir, "test_comment.wav", struct {
− duration float64
− sampleRate int
− channels int
− bitsPerSample int
− comment string
− artist string
− }{
− duration: 10.0,
− sampleRate: 48000,
− channels: 1,
− bitsPerSample: 16,
− comment: expectedComment,
− artist: "",
− })
+ metadata, err := ParseWAVHeader(path)
+ if err != nil {
+ t.Fatalf("Failed to parse WAV header: %v", err)
+ }
replacement in utils/wav_metadata_test.go at line 351
[4.11760]→[4.11760:11873](∅→∅) − metadata, err := ParseWAVHeader(path)
− if err != nil {
− t.Fatalf("Failed to parse WAV header: %v", err)
− }
+ if metadata.Comment != expectedComment {
+ t.Errorf("Comment incorrect: got %q, want %q", metadata.Comment, expectedComment)
+ }
+ }
replacement in utils/wav_metadata_test.go at line 356
[4.11874]→[4.11874:12010](∅→∅) − if metadata.Comment != expectedComment {
− t.Errorf("Comment incorrect: got %q, want %q", metadata.Comment, expectedComment)
− }
− })
+ func TestParseWAVHeader_ArtistMetadata(t *testing.T) {
+ tmpDir := t.TempDir()
replacement in utils/wav_metadata_test.go at line 359
[4.12011]→[4.12011:12707](∅→∅) − t.Run("should extract artist metadata", func(t *testing.T) {
− expectedArtist := "AudioMoth"
− path := createTestWAVFile(t, tmpDir, "test_artist.wav", struct {
− duration float64
− sampleRate int
− channels int
− bitsPerSample int
− comment string
− artist string
− }{
− duration: 5.0,
− sampleRate: 48000,
− channels: 1,
− bitsPerSample: 16,
− comment: "",
− artist: expectedArtist,
− })
−
− metadata, err := ParseWAVHeader(path)
− if err != nil {
− t.Fatalf("Failed to parse WAV header: %v", err)
− }
−
− if metadata.Artist != expectedArtist {
− t.Errorf("Artist incorrect: got %q, want %q", metadata.Artist, expectedArtist)
− }
+ expectedArtist := "AudioMoth"
+ path := createTestWAVFile(t, tmpDir, "test_artist.wav", struct {
+ duration float64
+ sampleRate int
+ channels int
+ bitsPerSample int
+ comment string
+ artist string
+ }{
+ duration: 5.0,
+ sampleRate: 48000,
+ channels: 1,
+ bitsPerSample: 16,
+ comment: "",
+ artist: expectedArtist,
replacement in utils/wav_metadata_test.go at line 376
[4.12712]→[4.12712:13234](∅→∅) − t.Run("should extract both comment and artist", func(t *testing.T) {
− expectedComment := "Test recording comment"
− expectedArtist := "Test Artist"
− path := createTestWAVFile(t, tmpDir, "test_both.wav", struct {
− duration float64
− sampleRate int
− channels int
− bitsPerSample int
− comment string
− artist string
− }{
− duration: 15.0,
− sampleRate: 44100,
− channels: 2,
− bitsPerSample: 16,
− comment: expectedComment,
− artist: expectedArtist,
− })
+ metadata, err := ParseWAVHeader(path)
+ if err != nil {
+ t.Fatalf("Failed to parse WAV header: %v", err)
+ }
replacement in utils/wav_metadata_test.go at line 381
[4.13235]→[4.13235:13348](∅→∅) − metadata, err := ParseWAVHeader(path)
− if err != nil {
− t.Fatalf("Failed to parse WAV header: %v", err)
− }
+ if metadata.Artist != expectedArtist {
+ t.Errorf("Artist incorrect: got %q, want %q", metadata.Artist, expectedArtist)
+ }
+ }
replacement in utils/wav_metadata_test.go at line 386
[4.13349]→[4.13349:13481](∅→∅) − if metadata.Comment != expectedComment {
− t.Errorf("Comment incorrect: got %q, want %q", metadata.Comment, expectedComment)
− }
+ func TestParseWAVHeader_CommentAndArtist(t *testing.T) {
+ tmpDir := t.TempDir()
replacement in utils/wav_metadata_test.go at line 389
[4.13482]→[4.13482:13609](∅→∅) − if metadata.Artist != expectedArtist {
− t.Errorf("Artist incorrect: got %q, want %q", metadata.Artist, expectedArtist)
− }
+ expectedComment := "Test recording comment"
+ expectedArtist := "Test Artist"
+ path := createTestWAVFile(t, tmpDir, "test_both.wav", struct {
+ duration float64
+ sampleRate int
+ channels int
+ bitsPerSample int
+ comment string
+ artist string
+ }{
+ duration: 15.0,
+ sampleRate: 44100,
+ channels: 2,
+ bitsPerSample: 16,
+ comment: expectedComment,
+ artist: expectedArtist,
replacement in utils/wav_metadata_test.go at line 407
[4.13614]→[4.13614:13806](∅→∅) − t.Run("should handle different sample rates", func(t *testing.T) {
− testCases := []struct {
− sampleRate int
− }{
− {8000},
− {16000},
− {22050},
− {44100},
− {48000},
− {96000},
− }
+ metadata, err := ParseWAVHeader(path)
+ if err != nil {
+ t.Fatalf("Failed to parse WAV header: %v", err)
+ }
replacement in utils/wav_metadata_test.go at line 412
[4.13807]→[4.13807:14256](∅→∅) − for _, tc := range testCases {
− t.Run("", func(t *testing.T) {
− path := createTestWAVFile(t, tmpDir, "test_sr.wav", struct {
− duration float64
− sampleRate int
− channels int
− bitsPerSample int
− comment string
− artist string
− }{
− duration: 1.0,
− sampleRate: tc.sampleRate,
− channels: 1,
− bitsPerSample: 16,
− comment: "",
− artist: "",
− })
+ if metadata.Comment != expectedComment {
+ t.Errorf("Comment incorrect: got %q, want %q", metadata.Comment, expectedComment)
+ }
+ if metadata.Artist != expectedArtist {
+ t.Errorf("Artist incorrect: got %q, want %q", metadata.Artist, expectedArtist)
+ }
+ }
replacement in utils/wav_metadata_test.go at line 420
[4.14257]→[4.14257:14378](∅→∅) − metadata, err := ParseWAVHeader(path)
− if err != nil {
− t.Fatalf("Failed to parse WAV header: %v", err)
− }
+ func TestParseWAVHeader_SampleRates(t *testing.T) {
+ tmpDir := t.TempDir()
replacement in utils/wav_metadata_test.go at line 423
[4.14379]→[4.14379:14522](∅→∅) − if metadata.SampleRate != tc.sampleRate {
− t.Errorf("SampleRate incorrect: got %d, want %d", metadata.SampleRate, tc.sampleRate)
− }
+ testCases := []int{8000, 16000, 22050, 44100, 48000, 96000}
+ for _, sr := range testCases {
+ t.Run(fmt.Sprintf("%dHz", sr), func(t *testing.T) {
+ path := createTestWAVFile(t, tmpDir, fmt.Sprintf("test_sr_%d.wav", sr), struct {
+ duration float64
+ sampleRate int
+ channels int
+ bitsPerSample int
+ comment string
+ artist string
+ }{
+ duration: 1.0,
+ sampleRate: sr,
+ channels: 1,
+ bitsPerSample: 16,
+ comment: "",
+ artist: "",
edit in utils/wav_metadata_test.go at line 441
[4.14528]→[4.14528:14536](∅→∅) replacement in utils/wav_metadata_test.go at line 442
[4.14537]→[4.14537:14692](∅→∅) − t.Run("should handle different channel counts", func(t *testing.T) {
− testCases := []struct {
− channels int
− }{
− {1}, // Mono
− {2}, // Stereo
− }
+ metadata, err := ParseWAVHeader(path)
+ if err != nil {
+ t.Fatalf("Failed to parse WAV header: %v", err)
+ }
replacement in utils/wav_metadata_test.go at line 447
[4.14693]→[4.14693:15144](∅→∅) − for _, tc := range testCases {
− t.Run("", func(t *testing.T) {
− path := createTestWAVFile(t, tmpDir, "test_ch.wav", struct {
− duration float64
− sampleRate int
− channels int
− bitsPerSample int
− comment string
− artist string
− }{
− duration: 1.0,
− sampleRate: 44100,
− channels: tc.channels,
− bitsPerSample: 16,
− comment: "",
− artist: "",
− })
+ if metadata.SampleRate != sr {
+ t.Errorf("SampleRate incorrect: got %d, want %d", metadata.SampleRate, sr)
+ }
+ })
+ }
+ }
replacement in utils/wav_metadata_test.go at line 454
[4.15145]→[4.15145:15266](∅→∅) − metadata, err := ParseWAVHeader(path)
− if err != nil {
− t.Fatalf("Failed to parse WAV header: %v", err)
− }
+ func TestParseWAVHeader_ChannelCounts(t *testing.T) {
+ tmpDir := t.TempDir()
replacement in utils/wav_metadata_test.go at line 457
[4.15267]→[4.15267:15400](∅→∅) − if metadata.Channels != tc.channels {
− t.Errorf("Channels incorrect: got %d, want %d", metadata.Channels, tc.channels)
− }
+ testCases := []int{1, 2}
+ for _, ch := range testCases {
+ t.Run(fmt.Sprintf("%dch", ch), func(t *testing.T) {
+ path := createTestWAVFile(t, tmpDir, fmt.Sprintf("test_ch_%d.wav", ch), struct {
+ duration float64
+ sampleRate int
+ channels int
+ bitsPerSample int
+ comment string
+ artist string
+ }{
+ duration: 1.0,
+ sampleRate: 44100,
+ channels: ch,
+ bitsPerSample: 16,
+ comment: "",
+ artist: "",
edit in utils/wav_metadata_test.go at line 475
[4.15406]→[4.15406:15414](∅→∅) replacement in utils/wav_metadata_test.go at line 476
[4.15415]→[4.15415:15572](∅→∅) − t.Run("should handle different bit depths", func(t *testing.T) {
− testCases := []struct {
− bitsPerSample int
− }{
− {8},
− {16},
− {24},
− {32},
− }
+ metadata, err := ParseWAVHeader(path)
+ if err != nil {
+ t.Fatalf("Failed to parse WAV header: %v", err)
+ }
replacement in utils/wav_metadata_test.go at line 481
[4.15573]→[4.15573:16030](∅→∅) − for _, tc := range testCases {
− t.Run("", func(t *testing.T) {
− path := createTestWAVFile(t, tmpDir, "test_bits.wav", struct {
− duration float64
− sampleRate int
− channels int
− bitsPerSample int
− comment string
− artist string
− }{
− duration: 1.0,
− sampleRate: 44100,
− channels: 1,
− bitsPerSample: tc.bitsPerSample,
− comment: "",
− artist: "",
− })
+ if metadata.Channels != ch {
+ t.Errorf("Channels incorrect: got %d, want %d", metadata.Channels, ch)
+ }
+ })
+ }
+ }
replacement in utils/wav_metadata_test.go at line 488
[4.16031]→[4.16031:16152](∅→∅) − metadata, err := ParseWAVHeader(path)
− if err != nil {
− t.Fatalf("Failed to parse WAV header: %v", err)
− }
+ func TestParseWAVHeader_BitDepths(t *testing.T) {
+ tmpDir := t.TempDir()
replacement in utils/wav_metadata_test.go at line 491
[4.16153]→[4.16153:16311](∅→∅) − if metadata.BitsPerSample != tc.bitsPerSample {
− t.Errorf("BitsPerSample incorrect: got %d, want %d", metadata.BitsPerSample, tc.bitsPerSample)
− }
+ testCases := []int{8, 16, 24, 32}
+ for _, bits := range testCases {
+ t.Run(fmt.Sprintf("%dbit", bits), func(t *testing.T) {
+ path := createTestWAVFile(t, tmpDir, fmt.Sprintf("test_bits_%d.wav", bits), struct {
+ duration float64
+ sampleRate int
+ channels int
+ bitsPerSample int
+ comment string
+ artist string
+ }{
+ duration: 1.0,
+ sampleRate: 44100,
+ channels: 1,
+ bitsPerSample: bits,
+ comment: "",
+ artist: "",
edit in utils/wav_metadata_test.go at line 509
[4.16317]→[4.16317:16325](∅→∅) replacement in utils/wav_metadata_test.go at line 510
[4.16326]→[4.16326:16743](∅→∅) − t.Run("should handle very short durations", func(t *testing.T) {
− path := createTestWAVFile(t, tmpDir, "test_short.wav", struct {
− duration float64
− sampleRate int
− channels int
− bitsPerSample int
− comment string
− artist string
− }{
− duration: 0.1, // 100ms
− sampleRate: 44100,
− channels: 1,
− bitsPerSample: 16,
− comment: "",
− artist: "",
+ metadata, err := ParseWAVHeader(path)
+ if err != nil {
+ t.Fatalf("Failed to parse WAV header: %v", err)
+ }
+
+ if metadata.BitsPerSample != bits {
+ t.Errorf("BitsPerSample incorrect: got %d, want %d", metadata.BitsPerSample, bits)
+ }
edit in utils/wav_metadata_test.go at line 519
replacement in utils/wav_metadata_test.go at line 522
[4.16749]→[4.16749:16862](∅→∅) − metadata, err := ParseWAVHeader(path)
− if err != nil {
− t.Fatalf("Failed to parse WAV header: %v", err)
− }
+ func TestParseWAVHeader_ShortDuration(t *testing.T) {
+ tmpDir := t.TempDir()
replacement in utils/wav_metadata_test.go at line 525
[4.16863]→[4.16863:16999](∅→∅) − if metadata.Duration < 0.09 || metadata.Duration > 0.11 {
− t.Errorf("Duration incorrect: got %f, want ~0.1", metadata.Duration)
− }
+ path := createTestWAVFile(t, tmpDir, "test_short.wav", struct {
+ duration float64
+ sampleRate int
+ channels int
+ bitsPerSample int
+ comment string
+ artist string
+ }{
+ duration: 0.1, // 100ms
+ sampleRate: 44100,
+ channels: 1,
+ bitsPerSample: 16,
+ comment: "",
+ artist: "",
replacement in utils/wav_metadata_test.go at line 541
[4.17004]→[4.17004:17426](∅→∅) − t.Run("should handle long durations", func(t *testing.T) {
− path := createTestWAVFile(t, tmpDir, "test_long.wav", struct {
− duration float64
− sampleRate int
− channels int
− bitsPerSample int
− comment string
− artist string
− }{
− duration: 600.0, // 10 minutes
− sampleRate: 44100,
− channels: 1,
− bitsPerSample: 16,
− comment: "",
− artist: "",
− })
+ metadata, err := ParseWAVHeader(path)
+ if err != nil {
+ t.Fatalf("Failed to parse WAV header: %v", err)
+ }
replacement in utils/wav_metadata_test.go at line 546
[4.17427]→[4.17427:17540](∅→∅) − metadata, err := ParseWAVHeader(path)
− if err != nil {
− t.Fatalf("Failed to parse WAV header: %v", err)
− }
+ if metadata.Duration < 0.09 || metadata.Duration > 0.11 {
+ t.Errorf("Duration incorrect: got %f, want ~0.1", metadata.Duration)
+ }
+ }
replacement in utils/wav_metadata_test.go at line 551
[4.17541]→[4.17541:17685](∅→∅) − if metadata.Duration < 599.0 || metadata.Duration > 601.0 {
− t.Errorf("Duration incorrect: got %f, want ~600.0", metadata.Duration)
− }
− })
+ func TestParseWAVHeader_LongDuration(t *testing.T) {
+ tmpDir := t.TempDir()
replacement in utils/wav_metadata_test.go at line 554
[4.17686]→[4.17686:17884](∅→∅) − t.Run("should return error for non-existent file", func(t *testing.T) {
− _, err := ParseWAVHeader("/nonexistent/file.wav")
− if err == nil {
− t.Error("Expected error for non-existent file")
− }
+ path := createTestWAVFile(t, tmpDir, "test_long.wav", struct {
+ duration float64
+ sampleRate int
+ channels int
+ bitsPerSample int
+ comment string
+ artist string
+ }{
+ duration: 600.0, // 10 minutes
+ sampleRate: 44100,
+ channels: 1,
+ bitsPerSample: 16,
+ comment: "",
+ artist: "",
replacement in utils/wav_metadata_test.go at line 570
[4.17889]→[4.17889:18173](∅→∅) − t.Run("should return error for non-WAV file", func(t *testing.T) {
− // Create a non-WAV file
− path := filepath.Join(tmpDir, "not_a_wav.txt")
− if err := os.WriteFile(path, []byte("This is not a WAV file"), 0644); err != nil {
− t.Fatalf("Failed to create test file: %v", err)
− }
+ metadata, err := ParseWAVHeader(path)
+ if err != nil {
+ t.Fatalf("Failed to parse WAV header: %v", err)
+ }
replacement in utils/wav_metadata_test.go at line 575
[4.18174]→[4.18174:18279](∅→∅) − _, err := ParseWAVHeader(path)
− if err == nil {
− t.Error("Expected error for non-WAV file")
− }
− })
+ if metadata.Duration < 599.0 || metadata.Duration > 601.0 {
+ t.Errorf("Duration incorrect: got %f, want ~600.0", metadata.Duration)
+ }
+ }
replacement in utils/wav_metadata_test.go at line 580
[4.18280]→[4.18280:18573](∅→∅) − t.Run("should return error for truncated file", func(t *testing.T) {
− // Create a file that's too small to be valid WAV
− path := filepath.Join(tmpDir, "truncated.wav")
− if err := os.WriteFile(path, []byte("RIFF"), 0644); err != nil {
− t.Fatalf("Failed to create test file: %v", err)
− }
+ func TestParseWAVHeader_NonExistentFile(t *testing.T) {
+ _, err := ParseWAVHeader("/nonexistent/file.wav")
+ if err == nil {
+ t.Error("Expected error for non-existent file")
+ }
+ }
replacement in utils/wav_metadata_test.go at line 587
[4.18574]→[4.18574:18681](∅→∅) − _, err := ParseWAVHeader(path)
− if err == nil {
− t.Error("Expected error for truncated file")
− }
− })
+ func TestParseWAVHeader_NonWAVFile(t *testing.T) {
+ tmpDir := t.TempDir()
replacement in utils/wav_metadata_test.go at line 590
[4.18682]→[4.18682:19098](∅→∅) − t.Run("should handle empty metadata strings", func(t *testing.T) {
− path := createTestWAVFile(t, tmpDir, "test_empty.wav", struct {
− duration float64
− sampleRate int
− channels int
− bitsPerSample int
− comment string
− artist string
− }{
− duration: 10.0,
− sampleRate: 44100,
− channels: 1,
− bitsPerSample: 16,
− comment: "",
− artist: "",
− })
+ path := filepath.Join(tmpDir, "not_a_wav.txt")
+ if err := os.WriteFile(path, []byte("This is not a WAV file"), 0644); err != nil {
+ t.Fatalf("Failed to create test file: %v", err)
+ }
+
+ _, err := ParseWAVHeader(path)
+ if err == nil {
+ t.Error("Expected error for non-WAV file")
+ }
+ }
+
+ func TestParseWAVHeader_TruncatedFile(t *testing.T) {
+ tmpDir := t.TempDir()
+
+ path := filepath.Join(tmpDir, "truncated.wav")
+ if err := os.WriteFile(path, []byte("RIFF"), 0644); err != nil {
+ t.Fatalf("Failed to create test file: %v", err)
+ }
replacement in utils/wav_metadata_test.go at line 609
[4.19099]→[4.19099:19212](∅→∅) − metadata, err := ParseWAVHeader(path)
− if err != nil {
− t.Fatalf("Failed to parse WAV header: %v", err)
− }
+ _, err := ParseWAVHeader(path)
+ if err == nil {
+ t.Error("Expected error for truncated file")
+ }
+ }
replacement in utils/wav_metadata_test.go at line 615
[4.19213]→[4.19213:19312](∅→∅) − if metadata.Comment != "" {
− t.Errorf("Comment should be empty, got %q", metadata.Comment)
− }
+ func TestParseWAVHeader_EmptyMetadataStrings(t *testing.T) {
+ tmpDir := t.TempDir()
replacement in utils/wav_metadata_test.go at line 618
[4.19313]→[4.19313:19409](∅→∅) − if metadata.Artist != "" {
− t.Errorf("Artist should be empty, got %q", metadata.Artist)
− }
+ path := createTestWAVFile(t, tmpDir, "test_empty.wav", struct {
+ duration float64
+ sampleRate int
+ channels int
+ bitsPerSample int
+ comment string
+ artist string
+ }{
+ duration: 10.0,
+ sampleRate: 44100,
+ channels: 1,
+ bitsPerSample: 16,
+ comment: "",
+ artist: "",
replacement in utils/wav_metadata_test.go at line 634
[4.19414]→[4.19414:19720](∅→∅) − t.Run("should handle long comment strings", func(t *testing.T) {
− longComment := "Recorded at 21:00:00 24/02/2025 (UTC+13) by AudioMoth 248AB50153AB0549 at medium gain while battery was 4.3V and temperature was 15.8C. This is a very long comment with additional information about the recording session."
+ metadata, err := ParseWAVHeader(path)
+ if err != nil {
+ t.Fatalf("Failed to parse WAV header: %v", err)
+ }
replacement in utils/wav_metadata_test.go at line 639
[4.19721]→[4.19721:20085](∅→∅) − path := createTestWAVFile(t, tmpDir, "test_long_comment.wav", struct {
− duration float64
− sampleRate int
− channels int
− bitsPerSample int
− comment string
− artist string
− }{
− duration: 10.0,
− sampleRate: 44100,
− channels: 1,
− bitsPerSample: 16,
− comment: longComment,
− artist: "",
− })
+ if metadata.Comment != "" {
+ t.Errorf("Comment should be empty, got %q", metadata.Comment)
+ }
+ if metadata.Artist != "" {
+ t.Errorf("Artist should be empty, got %q", metadata.Artist)
+ }
+ }
+
+ func TestParseWAVHeader_LongCommentString(t *testing.T) {
+ tmpDir := t.TempDir()
replacement in utils/wav_metadata_test.go at line 650
[4.20086]→[4.20086:20199](∅→∅) − metadata, err := ParseWAVHeader(path)
− if err != nil {
− t.Fatalf("Failed to parse WAV header: %v", err)
− }
+ longComment := "Recorded at 21:00:00 24/02/2025 (UTC+13) by AudioMoth 248AB50153AB0549 at medium gain while battery was 4.3V and temperature was 15.8C. This is a very long comment with additional information about the recording session."
replacement in utils/wav_metadata_test.go at line 652
[4.20200]→[4.20200:20324](∅→∅) − if metadata.Comment != longComment {
− t.Errorf("Comment incorrect: got %q, want %q", metadata.Comment, longComment)
− }
+ path := createTestWAVFile(t, tmpDir, "test_long_comment.wav", struct {
+ duration float64
+ sampleRate int
+ channels int
+ bitsPerSample int
+ comment string
+ artist string
+ }{
+ duration: 10.0,
+ sampleRate: 44100,
+ channels: 1,
+ bitsPerSample: 16,
+ comment: longComment,
+ artist: "",
replacement in utils/wav_metadata_test.go at line 668
[4.20329]→[4.20329:20747](∅→∅) − t.Run("should extract file modification time", func(t *testing.T) {
− path := createTestWAVFile(t, tmpDir, "test_modtime.wav", struct {
− duration float64
− sampleRate int
− channels int
− bitsPerSample int
− comment string
− artist string
− }{
− duration: 5.0,
− sampleRate: 44100,
− channels: 1,
− bitsPerSample: 16,
− comment: "",
− artist: "",
− })
+ metadata, err := ParseWAVHeader(path)
+ if err != nil {
+ t.Fatalf("Failed to parse WAV header: %v", err)
+ }
+
+ if metadata.Comment != longComment {
+ t.Errorf("Comment incorrect: got %q, want %q", metadata.Comment, longComment)
+ }
+ }
+
+ func TestParseWAVHeader_FileModTime(t *testing.T) {
+ tmpDir := t.TempDir()
replacement in utils/wav_metadata_test.go at line 681
[4.20748]→[4.20748:20906](∅→∅) − // Get expected mod time
− info, err := os.Stat(path)
− if err != nil {
− t.Fatalf("Failed to stat file: %v", err)
− }
− expectedModTime := info.ModTime()
+ path := createTestWAVFile(t, tmpDir, "test_modtime.wav", struct {
+ duration float64
+ sampleRate int
+ channels int
+ bitsPerSample int
+ comment string
+ artist string
+ }{
+ duration: 5.0,
+ sampleRate: 44100,
+ channels: 1,
+ bitsPerSample: 16,
+ comment: "",
+ artist: "",
+ })
replacement in utils/wav_metadata_test.go at line 697
[4.20907]→[4.20907:21020](∅→∅) − metadata, err := ParseWAVHeader(path)
− if err != nil {
− t.Fatalf("Failed to parse WAV header: %v", err)
− }
+ info, err := os.Stat(path)
+ if err != nil {
+ t.Fatalf("Failed to stat file: %v", err)
+ }
+ expectedModTime := info.ModTime()
replacement in utils/wav_metadata_test.go at line 703
[4.21021]→[4.21021:21301](∅→∅) − // Allow 1 second tolerance for filesystem granularity
− diff := metadata.FileModTime.Sub(expectedModTime)
− if diff < -1*time.Second || diff > 1*time.Second {
− t.Errorf("FileModTime incorrect: got %v, want %v (diff: %v)",
− metadata.FileModTime, expectedModTime, diff)
− }
+ metadata, err := ParseWAVHeader(path)
+ if err != nil {
+ t.Fatalf("Failed to parse WAV header: %v", err)
+ }
replacement in utils/wav_metadata_test.go at line 708
[4.21302]→[4.21302:21428](∅→∅) − // Ensure FileModTime is not zero
− if metadata.FileModTime.IsZero() {
− t.Error("FileModTime should not be zero")
− }
− })
+ // Allow 1 second tolerance for filesystem granularity
+ diff := metadata.FileModTime.Sub(expectedModTime)
+ if diff < -1*time.Second || diff > 1*time.Second {
+ t.Errorf("FileModTime incorrect: got %v, want %v (diff: %v)",
+ metadata.FileModTime, expectedModTime, diff)
+ }
+ if metadata.FileModTime.IsZero() {
+ t.Error("FileModTime should not be zero")
+ }
edit in tui/classify.go at line 206
[4.232167]→[4.232167:232216](∅→∅) − // If in comment mode, route to comment handler
edit in tui/classify.go at line 209
[4.232272]→[4.232272:232316](∅→∅) −
− // If in clip mode, route to clip handler
edit in tui/classify.go at line 215
[4.232380]→[4.232380:232526](∅→∅) − key := msg.Key()
−
− // Secondary-wait mode: next keypress is interpreted as a calltype key
− // for the species we just labeled via Shift+primary.
replacement in tui/classify.go at line 216
[4.232561]→[4.232561:232803](∅→∅) − primary := m.awaitingSecondaryFor
− m.awaitingSecondaryFor = ""
−
− // Esc cancels wait mode; species stays labeled without calltype,
− // segment does not advance.
− if key.Code == tea.KeyEscape || key.Code == tea.KeyEsc {
− return m, nil
+ if handled, model, cmd := m.handleSecondaryWait(msg); handled {
+ return model, cmd
edit in tui/classify.go at line 219
replacement in tui/classify.go at line 221
[4.232808]→[4.232808:233218](∅→∅) − s := msg.String()
− if len(s) == 1 {
− if callType, ok := m.state.Config.SecondaryBindings[primary][s]; ok {
− if m.state.Player != nil {
− m.state.Player.Stop()
− }
− m.state.ApplyCallTypeOnly(callType)
− if err := m.state.Save(); err != nil {
− m.err = err.Error()
− }
− if !m.state.NextSegment() {
− m.quitting = true
− return m, tea.Quit
− }
− return m, m.segmentChangeCmd()
+ if handled, model, cmd := m.handleSpecialKey(msg); handled {
+ return model, cmd
+ }
+
+ return m.handleSwitchKey(msg)
+ }
+
+ // handleSecondaryWait handles keypresses while awaiting a secondary calltype key.
+ // Returns (true, model, cmd) if the key was consumed; (false, model, cmd) to fall through.
+ func (m Model) handleSecondaryWait(msg tea.KeyPressMsg) (bool, tea.Model, tea.Cmd) {
+ primary := m.awaitingSecondaryFor
+ m.awaitingSecondaryFor = ""
+
+ if msg.Key().Code == tea.KeyEscape || msg.Key().Code == tea.KeyEsc {
+ return true, m, nil
+ }
+
+ s := msg.String()
+ if len(s) == 1 {
+ if callType, ok := m.state.Config.SecondaryBindings[primary][s]; ok {
+ m.stopPlayer()
+ m.state.ApplyCallTypeOnly(callType)
+ if err := m.state.Save(); err != nil {
+ m.err = err.Error()
edit in tui/classify.go at line 246
+ model, cmd := m.advanceOrQuit()
+ return true, model, cmd
edit in tui/classify.go at line 249
[4.233227]→[4.233227:233298](∅→∅) − // Unknown key — fall through to normal handling of this keypress.
edit in tui/classify.go at line 250
+ return false, m, nil
+ }
replacement in tui/classify.go at line 253
[4.233302]→[4.233302:233371](∅→∅) − // Handle Enter key (main or numpad, check code to catch modifiers)
+ // handleSpecialKey handles single-key-code bindings (Enter, Esc, Space, Ctrl+S).
+ // Returns (true, model, cmd) if the key was consumed.
+ func (m Model) handleSpecialKey(msg tea.KeyPressMsg) (bool, tea.Model, tea.Cmd) {
+ key := msg.Key()
+
replacement in tui/classify.go at line 266
[4.233594]→[4.233594:233621](∅→∅) − return m, playbackTick()
+ return true, m, playbackTick()
edit in tui/classify.go at line 269
[4.233625]→[4.233625:233659](∅→∅) − // Check for Escape key for quit
replacement in tui/classify.go at line 270
[4.233717]→[4.233717:233775](∅→∅) − if m.state.Player != nil {
− m.state.Player.Stop()
− }
replacement in tui/classify.go at line 272
[4.233795]→[4.233795:233816](∅→∅) + return true, m, tea.Quit
edit in tui/classify.go at line 275
[4.233820]→[4.233820:233866](∅→∅) − // Check for Space key (open comment dialog)
replacement in tui/classify.go at line 277
[4.233943]→[4.233943:233998](∅→∅) − m.commentCursor = len(m.commentText) // start at end
+ m.commentCursor = len(m.commentText)
replacement in tui/classify.go at line 279
[4.234021]→[4.234021:234037](∅→∅) edit in tui/classify.go at line 282
[4.234041]→[4.234041:234081](∅→∅) − // Check for Ctrl+S (save clip dialog)
replacement in tui/classify.go at line 285
[4.234151]→[4.234151:234167](∅→∅) edit in tui/classify.go at line 288
+ return false, m, nil
+ }
+
+ // handleSwitchKey handles string-based key bindings (ctrl+c, arrows, digits, bindings).
+ func (m Model) handleSwitchKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
replacement in tui/classify.go at line 295
[4.234210]→[4.234210:234268](∅→∅) − if m.state.Player != nil {
− m.state.Player.Stop()
− }
replacement in tui/classify.go at line 300
[4.234329]→[4.234329:234409](∅→∅) − // Previous segment
− if m.state.Player != nil {
− m.state.Player.Stop()
− }
replacement in tui/classify.go at line 305
[4.234487]→[4.234487:234573](∅→∅) − // Next segment (no edit)
− if m.state.Player != nil {
− m.state.Player.Stop()
− }
edit in tui/classify.go at line 313
[4.234700]→[4.234700:234721](∅→∅) replacement in tui/classify.go at line 320
[4.234849]→[4.234849:234930](∅→∅) − // Previous bookmark
− if m.state.Player != nil {
− m.state.Player.Stop()
− }
replacement in tui/classify.go at line 328
[4.235062]→[4.235062:235139](∅→∅) − // Next bookmark
− if m.state.Player != nil {
− m.state.Player.Stop()
− }
replacement in tui/classify.go at line 336
[4.235266]→[4.235266:235379](∅→∅) − // Confirm existing label (upgrade certainty to 100)
− if m.state.Player != nil {
− m.state.Player.Stop()
− }
replacement in tui/classify.go at line 343
[4.235502]→[4.235502:235612](∅→∅) − if !m.state.NextSegment() {
− m.quitting = true
− return m, tea.Quit
− }
− return m, m.segmentChangeCmd()
+ return m.advanceOrQuit()
replacement in tui/classify.go at line 346
[4.235623]→[4.235623:235695](∅→∅) − // Check for binding
− s := msg.String()
− if len(s) == 1 {
− k := s
+ return m.handleBindingKey(msg)
+ }
+ }
replacement in tui/classify.go at line 350
[4.235696]→[4.235696:236390](∅→∅) − // Shift+letter: if the lowercase primary has secondary bindings,
− // label species-only and enter wait mode. Otherwise map to the
− // lowercase equivalent and dispatch as a normal primary keypress.
− if key.Mod&tea.ModShift != 0 {
− lower := strings.ToLower(s)
− if lower != s {
− if m.state.HasSecondary(lower) {
− if result := m.state.ParseKeyBuffer(lower); result != nil {
− if m.state.Player != nil {
− m.state.Player.Stop()
− }
− m.state.ApplyBinding(&tools.BindingResult{Species: result.Species})
− if err := m.state.Save(); err != nil {
− m.err = err.Error()
− }
− m.awaitingSecondaryFor = lower
− return m, nil
− }
+ // handleBindingKey handles single-character key bindings (species/calltype shortcuts).
+ func (m Model) handleBindingKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
+ s := msg.String()
+ if len(s) != 1 {
+ return m, nil
+ }
+
+ k := s
+ key := msg.Key()
+
+ // Shift+letter: if the lowercase primary has secondary bindings,
+ // label species-only and enter wait mode. Otherwise map to the
+ // lowercase equivalent and dispatch as a normal primary keypress.
+ if key.Mod&tea.ModShift != 0 {
+ lower := strings.ToLower(s)
+ if lower != s {
+ if m.state.HasSecondary(lower) {
+ if result := m.state.ParseKeyBuffer(lower); result != nil {
+ m.stopPlayer()
+ m.state.ApplyBinding(&tools.BindingResult{Species: result.Species})
+ if err := m.state.Save(); err != nil {
+ m.err = err.Error()
replacement in tui/classify.go at line 373
[4.236397]→[4.236397:236412](∅→∅) + m.awaitingSecondaryFor = lower
+ return m, nil
edit in tui/classify.go at line 377
replacement in tui/classify.go at line 381
[4.236424]→[4.236424:236779](∅→∅) − if result := m.state.ParseKeyBuffer(k); result != nil {
− if m.state.Player != nil {
− m.state.Player.Stop()
− }
− m.state.ApplyBinding(result)
− if err := m.state.Save(); err != nil {
− m.err = err.Error()
− }
− if !m.state.NextSegment() {
− m.quitting = true
− return m, tea.Quit
− }
− return m, m.segmentChangeCmd()
− }
+ if result := m.state.ParseKeyBuffer(k); result != nil {
+ m.stopPlayer()
+ m.state.ApplyBinding(result)
+ if err := m.state.Save(); err != nil {
+ m.err = err.Error()
replacement in tui/classify.go at line 387
[4.236783]→[4.236783:236799](∅→∅) + return m.advanceOrQuit()
+ }
+ return m, nil
+ }
+
+ // stopPlayer stops the audio player if it exists.
+ func (m Model) stopPlayer() {
+ if m.state.Player != nil {
+ m.state.Player.Stop()
+ }
+ }
+
+ // advanceOrQuit advances to the next segment, or quits if none remain.
+ func (m Model) advanceOrQuit() (tea.Model, tea.Cmd) {
+ if !m.state.NextSegment() {
+ m.quitting = true
+ return m, tea.Quit
edit in tui/classify.go at line 405
+ return m, m.segmentChangeCmd()
edit in lint_test.go at line 8
[4.776214]→[4.776214:776423](∅→∅) − func TestGolangciLint(t *testing.T) {
− cmd := exec.Command("golangci-lint", "run", "./...")
− cmd.Dir = "."
− out, err := cmd.CombinedOutput()
− if err != nil {
− t.Errorf("golangci-lint failed:\n%s", out)
− }
− }
−
replacement in lint_test.go at line 20
[4.776737]→[4.776737:776813](∅→∅) − func TestDeadcode(t *testing.T) {
− cmd := exec.Command("deadcode", "./...")
+ func TestFix(t *testing.T) {
+ cmd := exec.Command("go", "fix", "./...")
replacement in lint_test.go at line 25
[4.776879]→[4.776879:776919](∅→∅) − t.Errorf("deadcode failed:\n%s", out)
+ t.Errorf("go fix failed:\n%s", out)
replacement in lint_test.go at line 29
[3.7662]→[3.7662:7734](∅→∅) − func TestFix(t *testing.T) {
− cmd := exec.Command("go", "fix", "./...")
+ func TestGolangciLint(t *testing.T) {
+ cmd := exec.Command("golangci-lint", "run", "./...")
+ cmd.Dir = "."
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Errorf("golangci-lint failed:\n%s", out)
+ }
+ }
+
+ func TestDeadcode(t *testing.T) {
+ cmd := exec.Command("deadcode", "./...")
replacement in lint_test.go at line 43
[3.7800]→[3.7800:7838](∅→∅) − t.Errorf("go fix failed:\n%s", out)
+ t.Errorf("deadcode failed:\n%s", out)
replacement in db/tx_logger.go at line 332
[4.850798]→[4.850798:850864](∅→∅) − // marshalParam converts a parameter to a JSON-serializable value
+ // marshalParam converts a parameter to a JSON-serializable value.
+ // Pointer types (including all *T) are handled via reflection: nil → null,
+ // non-nil → dereference and recurse.
replacement in db/tx_logger.go at line 340
[4.850935]→[4.850935:851050](∅→∅) − switch v := param.(type) {
− case time.Time:
− return v.Format(time.RFC3339Nano)
− case *time.Time:
− if v == nil {
+ // Handle pointer types via reflection: nil → null, else dereference and recurse.
+ // This covers all *T cases (including *time.Time) without explicit type switches.
+ rv := reflect.ValueOf(param)
+ if rv.Kind() == reflect.Pointer {
+ if rv.IsNil() {
edit in db/tx_logger.go at line 347
+ return marshalParam(rv.Elem().Interface())
+ }
+
+ // Value types
+ switch v := param.(type) {
+ case time.Time:
edit in db/tx_logger.go at line 355
[4.851118]→[4.851118:852131](∅→∅) − return v
− case *string:
− if v == nil {
− return nil
− }
− return *v
− case int:
− return v
− case *int:
− if v == nil {
− return nil
− }
− return *v
− case int8:
− return v
− case *int8:
− if v == nil {
− return nil
− }
− return *v
− case int16:
− return v
− case *int16:
− if v == nil {
− return nil
− }
− return *v
− case int32:
− return v
− case *int32:
− if v == nil {
− return nil
− }
− return *v
− case int64:
− return v
− case *int64:
− if v == nil {
− return nil
− }
− return *v
− case uint:
− return v
− case *uint:
− if v == nil {
− return nil
− }
− return *v
− case uint8:
− return v
− case *uint8:
− if v == nil {
− return nil
− }
− return *v
− case uint16:
− return v
− case *uint16:
− if v == nil {
− return nil
− }
− return *v
− case uint32:
− return v
− case *uint32:
− if v == nil {
− return nil
− }
− return *v
− case uint64:
− return v
− case *uint64:
− if v == nil {
− return nil
− }
− return *v
− case float32:
− return v
− case *float32:
− if v == nil {
− return nil
− }
− return *v
− case float64:
replacement in db/tx_logger.go at line 356
[4.852142]→[4.852142:852216](∅→∅) − case *float64:
− if v == nil {
− return nil
− }
− return *v
− case bool:
+ case int, int8, int16, int32, int64,
+ uint, uint8, uint16, uint32, uint64,
+ float32, float64, bool:
edit in db/tx_logger.go at line 360
[4.852227]→[4.852227:852286](∅→∅) − case *bool:
− if v == nil {
− return nil
− }
− return *v
edit in db/tx_logger.go at line 363
[4.852321]→[4.852321:852676](∅→∅) − // Handle pointer types via reflection (e.g., *GainLevel, *CustomType)
− rv := reflect.ValueOf(param)
− if rv.Kind() == reflect.Pointer {
− if rv.IsNil() {
− return nil
− }
− // Dereference and recursively marshal the underlying value
− return marshalParam(rv.Elem().Interface())
− }
− // For other types, try to convert to string via fmt.Sprintf
edit in cmd/calls_clip.go at line 12
+
+ // clipArgParser holds state for parsing CLI arguments incrementally.
+ type clipArgParser struct {
+ args []string
+ i int
+ }
+
+ // nextValue returns the next argument after the current flag, or exits with an error.
+ func (p *clipArgParser) nextValue(flag string) string {
+ if p.i+1 >= len(p.args) {
+ fmt.Fprintf(os.Stderr, "Error: %s requires a value\n", flag)
+ os.Exit(1)
+ }
+ v := p.args[p.i+1]
+ p.i += 2
+ return v
+ }
+
+ // nextInt parses the next argument as an integer, or exits with an error.
+ func (p *clipArgParser) nextInt(flag string) int {
+ s := p.nextValue(flag)
+ v, err := strconv.Atoi(s)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error: %s must be an integer\n", flag)
+ os.Exit(1)
+ }
+ return v
+ }
edit in cmd/calls_clip.go at line 41
+ // nextFloat parses the next argument as a float64, or exits with an error.
+ func (p *clipArgParser) nextFloat(flag string) float64 {
+ s := p.nextValue(flag)
+ v, err := strconv.ParseFloat(s, 64)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error: %s must be a number\n", flag)
+ os.Exit(1)
+ }
+ return v
+ }
+
replacement in cmd/calls_clip.go at line 111
[4.1137119]→[4.1137119:1137165](∅→∅) − i := 0
− for i < len(args) {
− arg := args[i]
+ p := &clipArgParser{args: args}
+ for p.i < len(args) {
+ arg := args[p.i]
replacement in cmd/calls_clip.go at line 117
[4.1137198]→[4.1137198:1137337](∅→∅) − if i+1 >= len(args) {
− fmt.Fprintf(os.Stderr, "Error: --file requires a value\n")
− os.Exit(1)
− }
− file = args[i+1]
− i += 2
−
+ file = p.nextValue(arg)
replacement in cmd/calls_clip.go at line 119
[4.1137356]→[4.1137356:1137499](∅→∅) − if i+1 >= len(args) {
− fmt.Fprintf(os.Stderr, "Error: --folder requires a value\n")
− os.Exit(1)
− }
− folder = args[i+1]
− i += 2
−
+ folder = p.nextValue(arg)
replacement in cmd/calls_clip.go at line 121
[4.1137518]→[4.1137518:1137661](∅→∅) − if i+1 >= len(args) {
− fmt.Fprintf(os.Stderr, "Error: --output requires a value\n")
− os.Exit(1)
− }
− output = args[i+1]
− i += 2
−
+ output = p.nextValue(arg)
replacement in cmd/calls_clip.go at line 123
[4.1137680]→[4.1137680:1137823](∅→∅) − if i+1 >= len(args) {
− fmt.Fprintf(os.Stderr, "Error: --prefix requires a value\n")
− os.Exit(1)
− }
− prefix = args[i+1]
− i += 2
−
+ prefix = p.nextValue(arg)
edit in cmd/calls_clip.go at line 125
[4.1137842]→[4.1137842:1137952](∅→∅) − if i+1 >= len(args) {
− fmt.Fprintf(os.Stderr, "Error: --filter requires a value\n")
− os.Exit(1)
− }
replacement in cmd/calls_clip.go at line 129
[4.1138068]→[4.1138068:1138101](∅→∅) − filter = args[i+1]
− i += 2
−
+ filter = p.nextValue(arg)
edit in cmd/calls_clip.go at line 131
[4.1138121]→[4.1138121:1138232](∅→∅) − if i+1 >= len(args) {
− fmt.Fprintf(os.Stderr, "Error: --species requires a value\n")
− os.Exit(1)
− }
replacement in cmd/calls_clip.go at line 135
[4.1138350]→[4.1138350:1138384](∅→∅) − species = args[i+1]
− i += 2
−
+ species = p.nextValue(arg)
replacement in cmd/calls_clip.go at line 137
[4.1138406]→[4.1138406:1138665](∅→∅) − if i+1 >= len(args) {
− fmt.Fprintf(os.Stderr, "Error: --certainty requires a value\n")
− os.Exit(1)
− }
− v, err := strconv.Atoi(args[i+1])
− if err != nil {
− fmt.Fprintf(os.Stderr, "Error: --certainty must be an integer\n")
− os.Exit(1)
− }
edit in cmd/calls_clip.go at line 143
[4.1138804]→[4.1138804:1138815](∅→∅) replacement in cmd/calls_clip.go at line 144
[4.1138832]→[4.1138832:1139104](∅→∅) − if i+1 >= len(args) {
− fmt.Fprintf(os.Stderr, "Error: --size requires a value\n")
− os.Exit(1)
− }
− v, err := strconv.Atoi(args[i+1])
− if err != nil {
− fmt.Fprintf(os.Stderr, "Error: --size must be an integer\n")
− os.Exit(1)
− }
− size = v
− i += 2
−
replacement in cmd/calls_clip.go at line 147
[4.1139138]→[4.1139138:1139146](∅→∅) replacement in cmd/calls_clip.go at line 150
[4.1139185]→[4.1139185:1139193](∅→∅) replacement in cmd/calls_clip.go at line 153
[4.1139227]→[4.1139227:1139235](∅→∅) replacement in cmd/calls_clip.go at line 156
[4.1139265]→[4.1139265:1139273](∅→∅) replacement in cmd/calls_clip.go at line 158
[4.1139289]→[4.1139289:1139555](∅→∅) − if i+1 >= len(args) {
− fmt.Fprintf(os.Stderr, "Error: --lat requires a value\n")
− os.Exit(1)
− }
− v, err := strconv.ParseFloat(args[i+1], 64)
− if err != nil {
− fmt.Fprintf(os.Stderr, "Error: --lat must be a number\n")
− os.Exit(1)
− }
− lat = v
edit in cmd/calls_clip.go at line 160
[4.1139572]→[4.1139572:1139583](∅→∅) replacement in cmd/calls_clip.go at line 161
[4.1139599]→[4.1139599:1139865](∅→∅) − if i+1 >= len(args) {
− fmt.Fprintf(os.Stderr, "Error: --lng requires a value\n")
− os.Exit(1)
− }
− v, err := strconv.ParseFloat(args[i+1], 64)
− if err != nil {
− fmt.Fprintf(os.Stderr, "Error: --lng must be a number\n")
− os.Exit(1)
− }
− lng = v
edit in cmd/calls_clip.go at line 163
[4.1139882]→[4.1139882:1139893](∅→∅) replacement in cmd/calls_clip.go at line 164
[4.1139914]→[4.1139914:1140061](∅→∅) − if i+1 >= len(args) {
− fmt.Fprintf(os.Stderr, "Error: --timezone requires a value\n")
− os.Exit(1)
− }
− timezone = args[i+1]
− i += 2
−
+ timezone = p.nextValue(arg)
edit in cmd/calls_clip.go at line 168
[4.1140118]→[4.1140118:1140119](∅→∅) edit in cmd/calls_clip.go at line 169
[4.1140130]→[4.1140130:1140160](∅→∅) − // Check for unknown flags
replacement in cmd/calls_clip.go at line 174
[4.1140301]→[4.1140301:1140308](∅→∅) edit in CHANGELOG.md at line 4
+
+ ## [2026-05-04] Reduce cyclomatic complexity of marshalParam, RunCallsClip, TestParseWAVHeader
+
+ Three functions exceeded gocyclo threshold of 40:
+
+ 1. **`marshalParam` (db/tx_logger.go) — 50 → ~8**: Replaced 25 repetitive pointer-type
+ cases (`*int`, `*string`, `*float64`, etc.) with the existing reflection-based pointer
+ handling that was already in the `default` branch. All value types kept as a single
+ multi-type switch case.
+
+ 2. **`RunCallsClip` (cmd/calls_clip.go) — 50 → 35**: Extracted `clipArgParser` struct
+ with `nextValue()`, `nextInt()`, `nextFloat()` helpers that encapsulate the
+ "check bounds + advance + error" pattern repeated 13 times in the arg loop.
+
+ 3. **`TestParseWAVHeader` (utils/wav_metadata_test.go) — 44 → ~5 each**: Split the
+ monolithic test with 14 `t.Run` sub-tests into 14 top-level `TestParseWAVHeader_*`
+ functions. Each now has minimal complexity.
+
+ ## [2026-05-04] Reduce cyclomatic complexity of handleKey (51 → 6)
+
+ Refactored `Model.handleKey` in `tui/classify.go` from a single 51-complexity
+ monolith into 6 focused methods plus 2 utility helpers:
+
+ - `handleKey` (6) — dispatcher routing to mode-specific handlers
+ - `handleSecondaryWait` (6) — awaiting-secondary-calltype logic
+ - `handleSpecialKey` (9) — Enter/Esc/Space/Ctrl+S
+ - `handleSwitchKey` (14) — switch-based key dispatch
+ - `handleBindingKey` (9) — single-char species/calltype bindings
+ - `stopPlayer` — extracted repeated nil-check + Stop pattern
+ - `advanceOrQuit` — extracted repeated next-segment-or-quit pattern
+
+ No behavioral changes.