Fork channel

Create a new channel as a copy of main.

Rename channel

Rename main to:

Delete channel

Delete main? This cannot be undone.

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 ""
}