classify_io.go
package calls
import (
"fmt"
"skraak/audio"
"skraak/datafile"
"skraak/spectrogram"
"skraak/wav"
)
// LoadFilteredSegment reads, bandpass-shifts, and downsamples a segment's audio.
// Returns samples and effective sample rate after any filtering/downsampling.
// This is a TUI operation for audio playback.
func (s *ClassifyState) LoadFilteredSegment(df *datafile.DataFile, seg *datafile.Segment) ([]float64, int, error) {
wavPath := df.FilePath[:len(df.FilePath)-5] // strip ".data"
segSamples, sampleRate, err := wav.ReadWAVSegmentSamples(wavPath, seg.StartTime, seg.EndTime)
if err != nil {
return nil, 0, fmt.Errorf("failed to read WAV: %w", err)
}
if len(segSamples) == 0 {
return nil, 0, fmt.Errorf("no samples in segment")
}
// Apply bandpass+shift+downsample if configured
if s.Config.BandpassLow > 0 || s.Config.BandpassHigh > 0 {
segSamples, sampleRate = audio.BandpassShiftFilter(segSamples, sampleRate, s.Config.BandpassLow, s.Config.BandpassHigh)
return segSamples, sampleRate, nil
}
// No bandpass: downsample if sample rate exceeds default
if sampleRate > audio.DefaultMaxSampleRate {
segSamples = audio.ResampleRate(segSamples, sampleRate, audio.DefaultMaxSampleRate)
sampleRate = audio.DefaultMaxSampleRate
}
return segSamples, sampleRate, nil
}
// SaveClip saves a spectrogram PNG and WAV of the current segment to outputDir.
// The prefix is prepended to the filename.
// This is a TUI operation for exporting clips.
func (s *ClassifyState) SaveClip(outputDir, prefix string) ([]string, error) {
df := s.CurrentFile()
seg := s.CurrentSegment()
if df == nil || seg == nil {
return nil, fmt.Errorf("no segment selected")
}
basename := spectrogram.WAVBasename(df.FilePath)
pngPath, wavPath, err := spectrogram.ClipPaths(outputDir, prefix, basename, seg.StartTime, seg.EndTime)
if err != nil {
return nil, err
}
segSamples, sampleRate, err := s.LoadFilteredSegment(df, seg)
if err != nil {
return nil, err
}
// Generate spectrogram image (always color, 224px for clips)
img := spectrogram.SpectrogramImageFromSamples(segSamples, sampleRate, true, 224)
if img == nil {
return nil, fmt.Errorf("failed to generate spectrogram")
}
if err := spectrogram.WritePNGFile(pngPath, img); err != nil {
return nil, err
}
if err := wav.WriteWAVFile(wavPath, segSamples, sampleRate); err != nil {
return nil, fmt.Errorf("failed to write WAV: %w", err)
}
return []string{pngPath, wavPath}, nil
}
// PlaySegmentAtSpeed loads and plays the current segment's audio at the given speed.
// speed=1.0 is normal, speed=0.5 is half speed.
// Returns an error message string, or empty string on success.
// This is a TUI operation for audio playback.
func (s *ClassifyState) PlaySegmentAtSpeed(speed float64) string {
df := s.CurrentFile()
seg := s.CurrentSegment()
if df == nil || seg == nil {
return ""
}
segSamples, playSampleRate, err := s.LoadFilteredSegment(df, seg)
if err != nil {
return fmt.Sprintf("audio: %v", err)
}
// Initialize player lazily on first play
if s.Player == nil {
player, err := audio.NewAudioPlayer(playSampleRate)
if err != nil {
return fmt.Sprintf("audio init: %v", err)
}
s.Player = player
}
if len(segSamples) > 0 {
s.PlaybackSpeed = speed
s.Player.PlayAtSpeed(segSamples, playSampleRate, speed)
}
return ""
}