Skip to content

Commit

Permalink
feat: enhance audio file validation and processing in FileAnalysis
Browse files Browse the repository at this point in the history
- Refactored the FileAnalysis function to improve audio file validation, including checks for file existence, size, and format.
- Introduced a new validateAudioFile function to encapsulate validation logic, ensuring only valid audio files are processed.
- Added detailed error messages with ANSI color codes for better visibility in case of validation failures.
- Updated processAudioFile to handle audio chunk processing more efficiently.
- Enhanced FLAC and WAV file reading functions with additional validations for bit depth and channel count, improving robustness.
  • Loading branch information
tphakala committed Dec 19, 2024
1 parent 2d4df7e commit d0c45d1
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 29 deletions.
99 changes: 72 additions & 27 deletions internal/analysis/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,72 @@ import (
"github.com/tphakala/birdnet-go/internal/observation"
)

var bn *birdnet.BirdNET

// executeFileAnalysis conducts an analysis of an audio file and outputs the results.
// FileAnalysis conducts an analysis of an audio file and outputs the results.
// It reads an audio file, analyzes it for bird sounds, and prints the results based on the provided configuration.
func FileAnalysis(settings *conf.Settings) error {
// Initialize the BirdNET interpreter only if not already initialized
if bn == nil {
var err error
bn, err = birdnet.NewBirdNET(settings)
if err != nil {
return fmt.Errorf("failed to initialize BirdNET: %w", err)
}
// Initialize BirdNET interpreter
if err := initializeBirdNET(settings); err != nil {
return err
}

if err := validateAudioFile(settings.Input.Path); err != nil {
return err
}

fileInfo, err := os.Stat(settings.Input.Path)
// Get audio file information
audioInfo, err := myaudio.GetAudioInfo(settings.Input.Path)
if err != nil {
return fmt.Errorf("error accessing the path: %w", err)
return fmt.Errorf("error getting audio info: %w", err)
}

notes, err := processAudioFile(settings, &audioInfo)
if err != nil {
return err
}

return writeResults(settings, notes)
}

// validateAudioFile checks if the provided file path is a valid audio file.
func validateAudioFile(filePath string) error {
fileInfo, err := os.Stat(filePath)
if err != nil {
return fmt.Errorf("\033[31m❌ Error accessing file %s: %w\033[0m", filepath.Base(filePath), err)
}

// Check if it's a file (not a directory)
if fileInfo.IsDir() {
return fmt.Errorf("the path is a directory, not a file")
return fmt.Errorf("\033[31m❌ The path %s is a directory, not a file\033[0m", filepath.Base(filePath))
}

// Get audio file information
audioInfo, err := myaudio.GetAudioInfo(settings.Input.Path)
// Check if file size is 0
if fileInfo.Size() == 0 {
return fmt.Errorf("\033[31m❌ File %s is empty (0 bytes)\033[0m", filepath.Base(filePath))
}

// Open the file to check if it's a valid FLAC file
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("error getting audio info: %w", err)
return fmt.Errorf("\033[31m❌ Error opening file %s: %w\033[0m", filepath.Base(filePath), err)
}
defer file.Close()

// Try to get audio info to validate the file format
audioInfo, err := myaudio.GetAudioInfo(filePath)
if err != nil {
return fmt.Errorf("\033[31m❌ Invalid audio file %s: %w\033[0m", filepath.Base(filePath), err)
}

// Check if the audio duration is valid (greater than 0)
if audioInfo.TotalSamples == 0 {
return fmt.Errorf("\033[31m❌ File %s contains no samples or is still being written\033[0m", filepath.Base(filePath))
}

return nil
}

// processAudioFile processes the audio file and returns the notes.
func processAudioFile(settings *conf.Settings, audioInfo *myaudio.AudioInfo) ([]datastore.Note, error) {
// Calculate total chunks
totalChunks := myaudio.GetTotalChunks(
audioInfo.SampleRate,
Expand All @@ -58,16 +94,13 @@ func FileAnalysis(settings *conf.Settings) error {
chunkCount := 0

// Get filename and truncate if necessary (showing max 30 chars)
filename := filepath.Base(settings.Input.Path)
if len(filename) > 30 {
filename = filename[:27] + "..."
}
filename := truncateFilename(settings.Input.Path)

// Lets set predStart to 0 time
// Set predStart to 0 time
predStart := time.Time{}

// Process audio chunks as they're read
err = myaudio.ReadAudioFileBuffered(settings, func(chunk []float32) error {
err := myaudio.ReadAudioFileBuffered(settings, func(chunk []float32) error {
chunkCount++
fmt.Printf("\r\033[K\033[37m📄 %s [%s]\033[0m | \033[33m🔍 Analyzing chunk %d/%d\033[0m %s",
filename,
Expand All @@ -76,7 +109,6 @@ func FileAnalysis(settings *conf.Settings) error {
totalChunks,
birdnet.EstimateTimeRemaining(startTime, chunkCount, totalChunks))

//notes, err := bn.ProcessChunk(chunk, float64(chunkCount-1)*(3-settings.BirdNET.Overlap))
notes, err := bn.ProcessChunk(chunk, predStart)
if err != nil {
return err
Expand All @@ -85,12 +117,11 @@ func FileAnalysis(settings *conf.Settings) error {

// advance predStart by 3 seconds - overlap
predStart = predStart.Add(time.Duration((3.0 - bn.Settings.BirdNET.Overlap) * float64(time.Second)))

return nil
})

if err != nil {
return fmt.Errorf("error processing audio: %w", err)
return nil, fmt.Errorf("error processing audio: %w", err)
}

// Show total time taken for analysis, including audio length
Expand All @@ -99,6 +130,20 @@ func FileAnalysis(settings *conf.Settings) error {
duration.Round(time.Second),
birdnet.FormatDuration(time.Since(startTime)))

return allNotes, nil
}

// truncateFilename truncates the filename to 30 characters if it's longer.
func truncateFilename(path string) string {
filename := filepath.Base(path)
if len(filename) > 30 {
return filename[:27] + "..."
}
return filename
}

// writeResults writes the notes to the output file based on the configuration.
func writeResults(settings *conf.Settings, notes []datastore.Note) error {
// Prepare the output file path if OutputDir is specified in the configuration.
var outputFile string
if settings.Output.File.Path != "" {
Expand All @@ -109,13 +154,13 @@ func FileAnalysis(settings *conf.Settings) error {
// Output the notes based on the desired output type in the configuration.
// If OutputType is not specified or if it's set to "table", output as a table format.
if settings.Output.File.Type == "" || settings.Output.File.Type == "table" {
if err := observation.WriteNotesTable(settings, allNotes, outputFile); err != nil {
if err := observation.WriteNotesTable(settings, notes, outputFile); err != nil {
return fmt.Errorf("failed to write notes table: %w", err)
}
}
// If OutputType is set to "csv", output as CSV format.
if settings.Output.File.Type == "csv" {
if err := observation.WriteNotesCsv(settings, allNotes, outputFile); err != nil {
if err := observation.WriteNotesCsv(settings, notes, outputFile); err != nil {
return fmt.Errorf("failed to write notes CSV: %w", err)
}
}
Expand Down
11 changes: 10 additions & 1 deletion internal/myaudio/readfile_flac.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@ import (
func readFLACInfo(file *os.File) (AudioInfo, error) {
decoder, err := flac.NewDecoder(file)
if err != nil {
return AudioInfo{}, err
return AudioInfo{}, fmt.Errorf("invalid FLAC file: %w", err)
}

// Additional FLAC-specific validations
if decoder.BitsPerSample != 16 && decoder.BitsPerSample != 24 && decoder.BitsPerSample != 32 {
return AudioInfo{}, fmt.Errorf("unsupported bit depth: %d", decoder.BitsPerSample)
}

if decoder.NChannels != 1 && decoder.NChannels != 2 {
return AudioInfo{}, fmt.Errorf("unsupported number of channels: %d", decoder.NChannels)
}

return AudioInfo{
Expand Down
12 changes: 11 additions & 1 deletion internal/myaudio/readfile_wav.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,18 @@ import (
func readWAVInfo(file *os.File) (AudioInfo, error) {
decoder := wav.NewDecoder(file)
decoder.ReadInfo()

if !decoder.IsValidFile() {
return AudioInfo{}, errors.New("input is not a valid WAV audio file")
return AudioInfo{}, errors.New("invalid WAV file format")
}

// Additional WAV-specific validations
if decoder.BitDepth != 16 && decoder.BitDepth != 24 && decoder.BitDepth != 32 {
return AudioInfo{}, fmt.Errorf("unsupported bit depth: %d", decoder.BitDepth)
}

if decoder.NumChans != 1 && decoder.NumChans != 2 {
return AudioInfo{}, fmt.Errorf("unsupported number of channels: %d", decoder.NumChans)
}

// Get file size in bytes
Expand Down

0 comments on commit d0c45d1

Please sign in to comment.