LBWQJEDHCNUNMEJWXILGBGYZUKQI7CDAMH2BD44HULM77SVH5UYQC package utilsimport ("os""path/filepath""testing")func TestWriteWAVFile(t *testing.T) {t.Run("writes valid samples", func(t *testing.T) {path := filepath.Join(t.TempDir(), "test.wav")samples := []float64{0.5, -0.5, 1.5, -1.5} // includes out of bounds for clampingerr := WriteWAVFile(path, samples, 8000)if err != nil {t.Fatalf("unexpected error: %v", err)}info, err := os.Stat(path)if err != nil || info.Size() == 0 {t.Error("expected file to be written with data")}})t.Run("fails on empty samples", func(t *testing.T) {path := filepath.Join(t.TempDir(), "empty.wav")err := WriteWAVFile(path, []float64{}, 8000)if err == nil {t.Error("expected error for empty samples")}})}
buf := make([]byte, dataSize)for i, sample := range samples {// Clamp to [-1, 1]if sample > 1.0 {sample = 1.0} else if sample < -1.0 {sample = -1.0}binary.LittleEndian.PutUint16(buf[i*2:], uint16(int16(sample*32767)))}
buf := Float64ToPCM16(samples)
package utilsimport ("testing")func TestExtractSegmentSamples(t *testing.T) {samples := []float64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}sampleRate := 2 // 2 samples per sec// sec 1.0 to 3.0 => indices 2 to 6 => [2, 3, 4, 5]seg := ExtractSegmentSamples(samples, sampleRate, 1.0, 3.0)if len(seg) != 4 || seg[0] != 2 || seg[len(seg)-1] != 5 {t.Errorf("unexpected segment extraction: %v", seg)}// out of boundsempty := ExtractSegmentSamples(samples, sampleRate, 5.0, 6.0)if len(empty) != 0 {t.Error("expected empty segment")}}func TestGenerateSpectrogram_Basic(t *testing.T) {samples := make([]float64, 1000) // Silent buffercfg := DefaultSpectrogramConfig(16000)res := GenerateSpectrogram(samples, cfg)if len(res) == 0 {t.Error("expected spectrogram generation to succeed")}shortSamples := []float64{0.0, 0.1}resShort := GenerateSpectrogram(shortSamples, cfg)if resShort != nil {t.Error("expected nil for samples smaller than window size")}}func TestGetCachedHannWindow(t *testing.T) {w1 := getCachedHannWindow(256)w2 := getCachedHannWindow(256)if len(w1) != 256 {t.Errorf("expected length 256, got %d", len(w1))}// Ensure memory address is the same (cached)if &w1[0] != &w2[0] {t.Error("expected cached slice to have the same memory address")}}
func TestMappingClassify(t *testing.T) {m := MappingFile{"noise": {Species: MappingNegative},"ignore": {Species: MappingIgnore},"kiwi": {Species: "Kiwi"},}c, k, ok := m.Classify("noise")if !ok || k != MappingNeg || c != "" {t.Error("failed classify negative")}c, k, ok = m.Classify("ignore")if !ok || k != MappingIgn || c != "" {t.Error("failed classify ignore")}c, k, ok = m.Classify("kiwi")if !ok || k != MappingReal || c != "Kiwi" {t.Error("failed classify real")}_, _, ok = m.Classify("missing")if ok {t.Error("expected missing to be not ok")}}func TestMappingValidateCoversSpecies(t *testing.T) {m := MappingFile{"kiwi": {Species: "Kiwi"}}missing := m.ValidateCoversSpecies(map[string]bool{"kiwi": true, "tui": true})if len(missing) != 1 || missing[0] != "tui" {t.Errorf("expected [tui], got %v", missing)}}func TestMappingClasses(t *testing.T) {m := MappingFile{"noise": {Species: MappingNegative},"kiwi": {Species: "Kiwi"},"tui": {Species: "Tui"},"duplicate": {Species: "Kiwi"},}classes := m.Classes()if len(classes) != 2 || classes[0] != "Kiwi" || classes[1] != "Tui" {t.Errorf("expected [Kiwi, Tui], got %v", classes)}}
package utilsimport ("os""path/filepath""testing")func TestFindFiles(t *testing.T) {dir := t.TempDir()// Write dummy filesos.WriteFile(filepath.Join(dir, "1.wav"), []byte("data"), 0644)os.WriteFile(filepath.Join(dir, "2.WAV"), []byte("data"), 0644)os.WriteFile(filepath.Join(dir, ".hidden.wav"), []byte("data"), 0644)os.WriteFile(filepath.Join(dir, "1.txt"), []byte("data"), 0644)subDir := filepath.Join(dir, "sub")os.Mkdir(subDir, 0755)os.WriteFile(filepath.Join(subDir, "3.wav"), []byte("data"), 0644)clipsDir := filepath.Join(dir, "Clips_1")os.Mkdir(clipsDir, 0755)os.WriteFile(filepath.Join(clipsDir, "4.wav"), []byte("data"), 0644)opts := FindFilesOptions{Extension: ".wav",Recursive: true,SkipHidden: true,SkipPrefixes: []string{"Clips_"},MinSize: 1,}files, err := FindFiles(dir, opts)if err != nil {t.Fatal(err)}if len(files) != 3 { // Should find 1.wav, 2.WAV, sub/3.wavt.Errorf("expected 3 files, got %d", len(files))}}
package utilsimport ("os""path/filepath""sort""strings")// FindFilesOptions configures directory scanningtype FindFilesOptions struct {Extension string // e.g. ".wav" or ".data"Recursive bool // whether to walk subdirectoriesSkipPrefixes []string // directory prefixes to skip (e.g. "Clips_")SkipHidden bool // skip files/folders starting with "."MinSize int64 // minimum file size in bytes}// FindFiles scans a directory for files matching the given optionsfunc FindFiles(rootPath string, opts FindFilesOptions) ([]string, error) {var results []stringextTarget := strings.ToLower(opts.Extension)if opts.Recursive {err := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error {if err != nil {return err}name := info.Name()// Skip hidden files/directoriesif opts.SkipHidden && strings.HasPrefix(name, ".") && path != rootPath {if info.IsDir() {return filepath.SkipDir}return nil}// Check directory skip prefixesif info.IsDir() && path != rootPath {for _, prefix := range opts.SkipPrefixes {if strings.HasPrefix(name, prefix) {return filepath.SkipDir}}return nil}// Check fileif !info.IsDir() {if strings.ToLower(filepath.Ext(name)) == extTarget && info.Size() >= opts.MinSize {results = append(results, path)}}return nil})if err != nil {return nil, err}} else {entries, err := os.ReadDir(rootPath)if err != nil {return nil, err}for _, entry := range entries {if entry.IsDir() {continue}name := entry.Name()if opts.SkipHidden && strings.HasPrefix(name, ".") {continue}if strings.ToLower(filepath.Ext(name)) == extTarget {path := filepath.Join(rootPath, name)if info, err := os.Stat(path); err == nil && info.Size() >= opts.MinSize {results = append(results, path)}}}}sort.Strings(results)return results, nil}
}func TestReadWAVSegmentSamples(t *testing.T) {tmpPath := filepath.Join(t.TempDir(), "segment_test.wav")// Create a simple WAV file with 4 sampleserr := WriteWAVFile(tmpPath, []float64{0.1, 0.2, 0.3, 0.4}, 8000)if err != nil {t.Fatalf("failed to create temp wav: %v", err)}// Test 1: Read specific segment (0.25s = 2 samples at 8000Hz)// Actually, 1 sample is 1/8000s. Let's just read the whole thing for the test to keep it simple and test the binary chunk reading logicsamples, rate, err := ReadWAVSegmentSamples(tmpPath, 0, 0)if err != nil {t.Fatalf("unexpected error: %v", err)}if rate != 8000 {t.Errorf("expected sample rate 8000, got %d", rate)}if len(samples) != 4 {t.Errorf("expected 4 samples, got %d", len(samples))}// Test 2: Helper ReadWAVSamples wrappersamples2, _, err := ReadWAVSamples(tmpPath)if err != nil {t.Fatalf("unexpected error: %v", err)}if len(samples2) != 4 {t.Errorf("expected 4 samples, got %d", len(samples2))}}func TestProcessSingleFile(t *testing.T) {tmpPath := filepath.Join(t.TempDir(), "20240101_120000.wav")err := WriteWAVFile(tmpPath, []float64{0.0, 0.0}, 8000)if err != nil {t.Fatalf("failed to create temp wav: %v", err)}res, err := ProcessSingleFile(tmpPath, -41.0, 174.0, "Pacific/Auckland", false)if err != nil {t.Fatalf("unexpected error: %v", err)}if res.SampleRate != 8000 {t.Errorf("expected sample rate 8000, got %d", res.SampleRate)}if res.Hash == "" {t.Error("expected non-empty hash")}
func TestParseWAVHeaderWithHash(t *testing.T) {tmpPath := filepath.Join(t.TempDir(), "hash_test.wav")err := WriteWAVFile(tmpPath, []float64{0.0, 0.0}, 8000)if err != nil {t.Fatalf("failed to create temp wav: %v", err)}meta, hash, err := ParseWAVHeaderWithHash(tmpPath)if err != nil {t.Fatalf("unexpected error: %v", err)}if meta.SampleRate != 8000 {t.Errorf("expected 8000, got %d", meta.SampleRate)}if hash == "" {t.Error("expected non-empty hash")}}
func ResolveTimestamp(wavMeta *WAVMetadata, filePath string, timezoneID string, useFileModTime bool) (*TimestampResult, error) {
func ResolveTimestamp(wavMeta *WAVMetadata, filePath string, timezoneID string, useFileModTime bool, preParsedFilenameTime *time.Time) (*TimestampResult, error) {
var files []stringentries, err := os.ReadDir(folder)if err != nil {return nil, err}for _, entry := range entries {name := entry.Name()// Skip hidden files (starting with ".")if strings.HasPrefix(name, ".") {continue}if strings.HasSuffix(name, ".data") {files = append(files, filepath.Join(folder, name))}}return files, nil
return FindFiles(folder, FindFilesOptions{Extension: ".data",Recursive: false,SkipHidden: true,})
package utilsimport ("os""path/filepath""testing")func TestLoadConfig(t *testing.T) {homeDir := t.TempDir()t.Setenv("HOME", homeDir)configDir := filepath.Join(homeDir, ".skraak")err := os.MkdirAll(configDir, 0755)if err != nil {t.Fatalf("failed to create config dir: %v", err)}jsonContent := `{"classify": {"reviewer": "Test Reviewer","color": true}}`err = os.WriteFile(filepath.Join(configDir, "config.json"), []byte(jsonContent), 0644)if err != nil {t.Fatalf("failed to write config: %v", err)}cfg, path, err := LoadConfig()if err != nil {t.Fatalf("unexpected error: %v", err)}if cfg.Classify.Reviewer != "Test Reviewer" {t.Errorf("expected Test Reviewer, got %s", cfg.Classify.Reviewer)}if !cfg.Classify.Color {t.Error("expected color to be true")}if path == "" {t.Error("expected path to be returned")}}
wavFiles, err := scanClusterFiles(input.FolderPath, input.Recursive)
wavFiles, err := FindFiles(input.FolderPath, FindFilesOptions{Extension: ".wav",Recursive: input.Recursive,SkipPrefixes: []string{"Clips_"},SkipHidden: true, // Standard to ignore hiddenMinSize: 1, // Must have size > 0})
}// scanClusterFiles recursively scans a folder for WAV files, excluding Clips_* subfoldersfunc scanClusterFiles(rootPath string, recursive bool) ([]string, error) {var wavFiles []stringif recursive {err := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error {if err != nil {return err}// Skip "Clips_*" directoriesif info.IsDir() && strings.HasPrefix(info.Name(), "Clips_") {return filepath.SkipDir}// Check for WAV filesif !info.IsDir() {ext := strings.ToLower(filepath.Ext(path))if ext == ".wav" && info.Size() > 0 {wavFiles = append(wavFiles, path)}}return nil})if err != nil {return nil, err}} else {// Non-recursive: scan only top levelentries, err := os.ReadDir(rootPath)if err != nil {return nil, err}for _, entry := range entries {if !entry.IsDir() {name := entry.Name()ext := strings.ToLower(filepath.Ext(name))if ext == ".wav" {path := filepath.Join(rootPath, name)if info, err := os.Stat(path); err == nil && info.Size() > 0 {wavFiles = append(wavFiles, path)}}}}}// Sort for consistent processing ordersort.Strings(wavFiles)return wavFiles, nil
}// Determine timestampvar timestampLocal time.Timevar isAudioMoth boolvar mothData *AudioMothData// Try AudioMoth comment firstif IsAudioMoth(info.metadata.Comment, info.metadata.Artist) {isAudioMoth = truevar parseErr errormothData, parseErr = ParseAudioMothComment(info.metadata.Comment)if parseErr == nil {timestampLocal = mothData.Timestamp} else {// AudioMoth detected but parsing failed - try filenameerrors = append(errors, FileImportError{FileName: filepath.Base(info.path),Error: fmt.Sprintf("AudioMoth comment parsing failed: %v", parseErr),Stage: "parse",})}}// If no AudioMoth timestamp, use filename timestampif timestampLocal.IsZero() {if ts, ok := filenameTimestampMap[i]; ok {timestampLocal = ts}
// If still no timestamp, use file modification time as fallbackif timestampLocal.IsZero() {if !info.metadata.FileModTime.IsZero() {// Assume FileModTime is already in location timezone// (recorder was at the location when it recorded)timestampLocal = info.metadata.FileModTime}
var preParsedTime *time.Timeif ts, ok := filenameTimestampMap[i]; ok {preParsedTime = &ts
buf := make([]byte, len(samples)*2)for i, s := range samples {// Clamp to [-1.0, 1.0]if s > 1.0 {s = 1.0} else if s < -1.0 {s = -1.0}v := int16(math.Round(s * 32767.0))binary.LittleEndian.PutUint16(buf[i*2:], uint16(v))}
buf := Float64ToPCM16(samples)
package utilsimport "testing"func TestFloat64ToPCM16(t *testing.T) {samples := []float64{0.0, 1.0, -1.0, 1.5, -1.5}bytes := Float64ToPCM16(samples)if len(bytes) != len(samples)*2 {t.Fatalf("expected length %d, got %d", len(samples)*2, len(bytes))}}
package utilsimport "encoding/binary"// Float64ToPCM16 converts float64 samples [-1.0, 1.0] to signed 16-bit PCM (LittleEndian) bytes.func Float64ToPCM16(samples []float64) []byte {buf := make([]byte, len(samples)*2)for i, sample := range samples {// Clamp to [-1.0, 1.0]if sample > 1.0 {sample = 1.0} else if sample < -1.0 {sample = -1.0}// Convert to 16-bit PCMbinary.LittleEndian.PutUint16(buf[i*2:], uint16(int16(sample*32767)))}return buf}
- Memory Inefficiency in Spectrograms: In spectrogram.go, GenerateSegmentSpectrogram loads the entireWAV file into memory using ReadWAVSamples(wavPath) before calling ExtractSegmentSamples. If a userrequests a 3-second segment from a 500MB continuous recording, the process will unnecessarilyallocate the whole file.- Path Construction: In data_file.go (FindDataFiles), paths are constructed using stringconcatenation (folder+"/"+name) instead of standard library utilities (filepath.Join), which ishandled correctly elsewhere in the codebase.- Separation of Concerns (DB vs Utils): Pure utility files are tightly coupled with the database. Forexample, validation.go mixes pure string/numeric assertions (ValidateShortID) with stateful databasequeries (e.g. ValidateLocationBelongsToDataset). Similarly, mapping.go containsValidateMappingAgainstDB. These queries belong in a db package or should rely on injected interfacesrather than hardcoding *sql.DB dependencies into utils/.- Misplaced Helpers: The SQL utility function Placeholders(n int) is randomly declared insidemapping.go instead of a dedicated database or query utility file.