4L2IWSLSDFB5BZWQYBHQCHGCMOKLB2QVONUYJ4OEJJG5N6XVYUPQC V2HX6HEB2OBNI4IMWD5XJN3RKAZYHAFJAJAPFP3BFYFZVZVEYN6AC 3DVPQOKB6BX63XSBIYYCPWBL2RBG3LXZS3XPQBANJP2FWVRAOVZQC PXQDGTR53ST5T4EV6XFRCAOC7N5RQX23GWVKMJGS2J35VUQLZL4AC N57PNZPFM6QU5FK4SHC3473IV6IN3HRVKSPZSFIJJ5LLCAXICNIAC XU7FTYK3YAM5TADBGEJZ44UJ6LNJ7YVABSFEXKOBGUHXBFHVLR2AC 3ETJ6KPIYI23DLXSKISNJSY3DUGHOACE6CPCPF6V7KJK4EXIADQAC package callsimport ("skraak/datafile")// SetComment sets the comment on the current segment's filter label.// Returns the previous comment (for undo) or empty string if none.func (s *ClassifyState) SetComment(comment string) string {seg := s.CurrentSegment()if seg == nil {return ""}df := s.CurrentFile()if df == nil {return ""}// Set reviewerdf.Meta.Reviewer = s.Config.Reviewer// Get labels matching filterfilterLabels := seg.GetFilterLabels(s.Config.Filter)var oldComment stringif len(filterLabels) == 0 {// No matching labels, add new one with commentlabel := &datafile.Label{Species: "Don't Know",Certainty: 0,Filter: s.Config.Filter,Comment: comment,}seg.Labels = append(seg.Labels, label)} else {// Set comment on first matching labeloldComment = filterLabels[0].CommentfilterLabels[0].Comment = comment}s.Dirty = truereturn oldComment}// GetCurrentComment returns the comment on the current segment's filter label.func (s *ClassifyState) GetCurrentComment() string {seg := s.CurrentSegment()if seg == nil {return ""}filterLabels := seg.GetFilterLabels(s.Config.Filter)if len(filterLabels) == 0 {return ""}return filterLabels[0].Comment}// ConfirmLabel upgrades the current segment's existing filter label certainty// to 100. Returns true if a write is needed (label existed and was below 100).// Returns false for Don't Know (certainty=0) — confirming a Don't Know is a no-op;// the caller should just advance to the next segment.func (s *ClassifyState) ConfirmLabel() bool {seg := s.CurrentSegment()if seg == nil {return false}filterLabels := seg.GetFilterLabels(s.Config.Filter)if len(filterLabels) == 0 {return false}if filterLabels[0].Certainty == 0 {return false}if filterLabels[0].Certainty == 100 {return false}df := s.CurrentFile()if df == nil {return false}df.Meta.Reviewer = s.Config.ReviewerfilterLabels[0].Certainty = 100s.Dirty = truereturn true}
package callsimport ("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 configuredif 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 defaultif 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 playif 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 = speeds.Player.PlayAtSpeed(segSamples, playSampleRate, speed)}return ""}
package callsimport ("fmt""strings""skraak/datafile")// FormatLabels formats labels for display in TUI.// If filter is non-empty, only labels matching that filter are shown.func FormatLabels(labels []*datafile.Label, filter string) string {var parts []stringfor _, l := range labels {if filter != "" && l.Filter != filter {continue}part := l.Speciesif l.CallType != "" {part += "/" + l.CallType}part += fmt.Sprintf(" (%d%%)", l.Certainty)if l.Filter != "" {part += " [" + l.Filter + "]"}if l.Comment != "" {part += fmt.Sprintf(" \"%s\"", l.Comment)}parts = append(parts, part)}return strings.Join(parts, ", ")}
package callsimport ("skraak/datafile")// getFilterLabel returns the label matching the current filter, or first label if no filter.func (s *ClassifyState) getFilterLabel(seg *datafile.Segment) *datafile.Label {if s.Config.Filter == "" {if len(seg.Labels) > 0 {return seg.Labels[0]}return nil}for _, label := range seg.Labels {if label.Filter == s.Config.Filter {return label}}return nil}// getOrCreateFilterLabel gets existing label or creates new one for the current filter.func (s *ClassifyState) getOrCreateFilterLabel(seg *datafile.Segment) *datafile.Label {label := s.getFilterLabel(seg)if label != nil {return label}// Create new labellabel = &datafile.Label{Species: "Don't Know",Certainty: 0,Filter: s.Config.Filter,}seg.Labels = append(seg.Labels, label)s.Dirty = truereturn label}// HasBookmark returns true if current segment has a bookmark on the filter label.func (s *ClassifyState) HasBookmark() bool {seg := s.CurrentSegment()if seg == nil {return false}label := s.getFilterLabel(seg)return label != nil && label.Bookmark}// ToggleBookmark toggles the bookmark on the current segment's filter label.func (s *ClassifyState) ToggleBookmark() {seg := s.CurrentSegment()if seg == nil {return}df := s.CurrentFile()if df == nil {return}// Set reviewerdf.Meta.Reviewer = s.Config.Reviewerlabel := s.getOrCreateFilterLabel(seg)label.Bookmark = !label.Bookmarks.Dirty = true}// NextBookmark navigates to the next bookmark, wrapping around if needed.// Returns false if no bookmarks found (back at start position).func (s *ClassifyState) NextBookmark() bool {startFile := s.FileIdxstartSeg := s.SegmentIdxfirst := truefor {// Advance to next segmentif !s.NextSegment() {// Wrap to start of folders.FileIdx = 0s.SegmentIdx = 0}// Check if we've looped back to startif !first && s.FileIdx == startFile && s.SegmentIdx == startSeg {return false // full circle, no bookmark found}first = false// Check if current segment has bookmarkif s.hasFilterBookmark() {return true}}}// PrevBookmark navigates to the previous bookmark, wrapping around if needed.// Returns false if no bookmarks found (back at start position).func (s *ClassifyState) PrevBookmark() bool {startFile := s.FileIdxstartSeg := s.SegmentIdxfirst := truefor {// Move to previous segmentif !s.PrevSegment() {// Wrap to end of folders.FileIdx = len(s.DataFiles) - 1segs := s.filteredSegs[s.FileIdx]s.SegmentIdx = max(len(segs)-1, 0)}// Check if we've looped back to startif !first && s.FileIdx == startFile && s.SegmentIdx == startSeg {return false // full circle, no bookmark found}first = false// Check if current segment has bookmarkif s.hasFilterBookmark() {return true}}}// hasFilterBookmark checks if current segment has bookmark on filter-matching label.func (s *ClassifyState) hasFilterBookmark() bool {seg := s.CurrentSegment()if seg == nil {return false}label := s.getFilterLabel(seg)return label != nil && label.Bookmark}
package callsimport ("slices""sort""skraak/datafile")// KeyBinding maps a key to a species/calltype for TUI classification.type KeyBinding struct {Key string // single char: "k", "n", "p"Species string // "Kiwi", "Don't Know", "Morepork"CallType string // "Duet", "Female", "Male" (optional)}// BindingResult represents parsed key result for TUI classification.type BindingResult struct {Species stringCallType string // empty string = remove calltype}// ParseKeyBuffer parses a single key into binding result.// Returns nil if no matching binding is found.func (s *ClassifyState) ParseKeyBuffer(key string) *BindingResult {for _, b := range s.Config.Bindings {if b.Key == key {return &BindingResult{Species: b.Species,CallType: b.CallType,}}}return nil}// ApplyBinding applies a binding result to the current segment.// This is a TUI operation that modifies the segment's labels.func (s *ClassifyState) ApplyBinding(result *BindingResult) {seg := s.CurrentSegment()if seg == nil {return}df := s.CurrentFile()if df == nil {return}// Set reviewerdf.Meta.Reviewer = s.Config.Reviewer// Get labels matching filterfilterLabels := seg.GetFilterLabels(s.Config.Filter)// Determine certainty: 0 for Don't Know, 100 for otherscertainty := 100if result.Species == "Don't Know" {certainty = 0}if len(filterLabels) == 0 {// No matching labels, add new oneseg.Labels = append(seg.Labels, &datafile.Label{Species: result.Species,Certainty: certainty,Filter: s.Config.Filter,CallType: result.CallType,})} else {// Edit first matching label, remove restfilterLabels[0].Species = result.SpeciesfilterLabels[0].Certainty = certaintyfilterLabels[0].CallType = result.CallType // always set (empty = remove)// Remove extra matching labelsif len(filterLabels) > 1 {var newLabels []*datafile.Labelfor _, l := range seg.Labels {keep := !slices.Contains(filterLabels[1:], l)if keep {newLabels = append(newLabels, l)}}seg.Labels = newLabels}}// Re-sort labelssort.Slice(seg.Labels, func(i, j int) bool {return seg.Labels[i].Species < seg.Labels[j].Species})s.Dirty = true}// ApplyCallTypeOnly sets the CallType on the current segment's first// filter-matching label. Used after a Shift+primary keypress labeled the// species and we now receive the secondary key for the calltype.// No-op if there is no matching label to update.func (s *ClassifyState) ApplyCallTypeOnly(callType string) {seg := s.CurrentSegment()if seg == nil {return}df := s.CurrentFile()if df == nil {return}filterLabels := seg.GetFilterLabels(s.Config.Filter)if len(filterLabels) == 0 {return}df.Meta.Reviewer = s.Config.ReviewerfilterLabels[0].CallType = callTypes.Dirty = true}// HasSecondary reports whether the given primary key has any secondary// (calltype) bindings configured.func (s *ClassifyState) HasSecondary(primaryKey string) bool {return len(s.Config.SecondaryBindings[primaryKey]) > 0}
// KeyBinding maps a key to a species/calltypetype KeyBinding struct {Key string // single char: "k", "n", "p"Species string // "Kiwi", "Don't Know", "Morepork"CallType string // "Duet", "Female", "Male" (optional)}
// KeyBinding maps a key to a species/calltype for TUI classification.// See classify_bindings.go for KeyBinding type and related methods.
// ClassifyConfig holds the configuration for classification
// ClassifyConfig holds the configuration for classification.//// TUI-only fields (used only by the classify TUI, not CLI tools):// - Color: enable color output for spectrograms// - Sixel: use Sixel graphics protocol// - ITerm: use iTerm2 inline image protocol// - ImageSize: spectrogram display size in pixels// - Bindings: keybindings for species/calltype assignment// - SecondaryBindings: secondary keybindings for calltype after species// - BandpassLow/BandpassHigh: bandpass filter for audio playback// - Night/Day: time-of-day filters for file selection// - Lat/Lng/Timezone: location data for astronomical calculations//// These fields are ignored by CLI tools that use ClassifyConfig purely// for file/segment filtering (Filter, Species, CallType, Certainty, Sample, Goto).
}// ParseKeyBuffer parses a single key into binding resultfunc (s *ClassifyState) ParseKeyBuffer(key string) *BindingResult {for _, b := range s.Config.Bindings {if b.Key == key {return &BindingResult{Species: b.Species,CallType: b.CallType,}}}return nil}// SetComment sets the comment on the current segment's filter label.// Returns the previous comment (for undo) or empty string if none.func (s *ClassifyState) SetComment(comment string) string {seg := s.CurrentSegment()if seg == nil {return ""}df := s.CurrentFile()if df == nil {return ""}// Set reviewerdf.Meta.Reviewer = s.Config.Reviewer// Get labels matching filterfilterLabels := seg.GetFilterLabels(s.Config.Filter)var oldComment stringif len(filterLabels) == 0 {// No matching labels, add new one with commentlabel := &datafile.Label{Species: "Don't Know",Certainty: 0,Filter: s.Config.Filter,Comment: comment,}seg.Labels = append(seg.Labels, label)} else {// Set comment on first matching labeloldComment = filterLabels[0].CommentfilterLabels[0].Comment = comment}s.Dirty = truereturn oldComment}// GetCurrentComment returns the comment on the current segment's filter label.func (s *ClassifyState) GetCurrentComment() string {seg := s.CurrentSegment()if seg == nil {return ""}filterLabels := seg.GetFilterLabels(s.Config.Filter)if len(filterLabels) == 0 {return ""}return filterLabels[0].Comment
// ApplyBinding applies a binding result to the current segmentfunc (s *ClassifyState) ApplyBinding(result *BindingResult) {seg := s.CurrentSegment()if seg == nil {return}df := s.CurrentFile()if df == nil {return}// Set reviewerdf.Meta.Reviewer = s.Config.Reviewer// Get labels matching filterfilterLabels := seg.GetFilterLabels(s.Config.Filter)// Determine certainty: 0 for Don't Know, 100 for otherscertainty := 100if result.Species == "Don't Know" {certainty = 0}if len(filterLabels) == 0 {// No matching labels, add new oneseg.Labels = append(seg.Labels, &datafile.Label{Species: result.Species,Certainty: certainty,Filter: s.Config.Filter,CallType: result.CallType,})} else {// Edit first matching label, remove restfilterLabels[0].Species = result.SpeciesfilterLabels[0].Certainty = certaintyfilterLabels[0].CallType = result.CallType // always set (empty = remove)// Remove extra matching labelsif len(filterLabels) > 1 {var newLabels []*datafile.Labelfor _, l := range seg.Labels {keep := !slices.Contains(filterLabels[1:], l)if keep {newLabels = append(newLabels, l)}}seg.Labels = newLabels}}// Re-sort labelssort.Slice(seg.Labels, func(i, j int) bool {return seg.Labels[i].Species < seg.Labels[j].Species})s.Dirty = true}// ApplyCallTypeOnly sets the CallType on the current segment's first// filter-matching label. Used after a Shift+primary keypress labeled the// species and we now receive the secondary key for the calltype.// No-op if there is no matching label to update.func (s *ClassifyState) ApplyCallTypeOnly(callType string) {seg := s.CurrentSegment()if seg == nil {return}df := s.CurrentFile()if df == nil {return}filterLabels := seg.GetFilterLabels(s.Config.Filter)if len(filterLabels) == 0 {return}df.Meta.Reviewer = s.Config.ReviewerfilterLabels[0].CallType = callTypes.Dirty = true}// HasSecondary reports whether the given primary key has any secondary// (calltype) bindings configured.func (s *ClassifyState) HasSecondary(primaryKey string) bool {return len(s.Config.SecondaryBindings[primaryKey]) > 0}// ConfirmLabel upgrades the current segment's existing filter label certainty// to 100. Returns true if a write is needed (label existed and was below 100).// Returns false for Don't Know (certainty=0) — confirming a Don't Know is a no-op;// the caller should just advance to the next segment.func (s *ClassifyState) ConfirmLabel() bool {seg := s.CurrentSegment()if seg == nil {return false}filterLabels := seg.GetFilterLabels(s.Config.Filter)if len(filterLabels) == 0 {return false}if filterLabels[0].Certainty == 0 {return false}if filterLabels[0].Certainty == 100 {return false}df := s.CurrentFile()if df == nil {return false}df.Meta.Reviewer = s.Config.ReviewerfilterLabels[0].Certainty = 100s.Dirty = truereturn true}
return nil}// getFilterLabel returns the label matching the current filter, or first label if no filter.func (s *ClassifyState) getFilterLabel(seg *datafile.Segment) *datafile.Label {if s.Config.Filter == "" {if len(seg.Labels) > 0 {return seg.Labels[0]}return nil}for _, label := range seg.Labels {if label.Filter == s.Config.Filter {return label}}
// getOrCreateFilterLabel gets existing label or creates new one for the current filter.func (s *ClassifyState) getOrCreateFilterLabel(seg *datafile.Segment) *datafile.Label {label := s.getFilterLabel(seg)if label != nil {return label}// Create new labellabel = &datafile.Label{Species: "Don't Know",Certainty: 0,Filter: s.Config.Filter,}seg.Labels = append(seg.Labels, label)s.Dirty = truereturn label}// HasBookmark returns true if current segment has a bookmark on the filter label.func (s *ClassifyState) HasBookmark() bool {seg := s.CurrentSegment()if seg == nil {return false}label := s.getFilterLabel(seg)return label != nil && label.Bookmark}// ToggleBookmark toggles the bookmark on the current segment's filter label.func (s *ClassifyState) ToggleBookmark() {seg := s.CurrentSegment()if seg == nil {return}df := s.CurrentFile()if df == nil {return}// Set reviewerdf.Meta.Reviewer = s.Config.Reviewerlabel := s.getOrCreateFilterLabel(seg)label.Bookmark = !label.Bookmarks.Dirty = true}// NextBookmark navigates to the next bookmark, wrapping around if needed.// Returns false if no bookmarks found (back at start position).func (s *ClassifyState) NextBookmark() bool {startFile := s.FileIdxstartSeg := s.SegmentIdxfirst := truefor {// Advance to next segmentif !s.NextSegment() {// Wrap to start of folders.FileIdx = 0s.SegmentIdx = 0}// Check if we've looped back to startif !first && s.FileIdx == startFile && s.SegmentIdx == startSeg {return false // full circle, no bookmark found}first = false// Check if current segment has bookmarkif s.hasFilterBookmark() {return true}}}// PrevBookmark navigates to the previous bookmark, wrapping around if needed.// Returns false if no bookmarks found (back at start position).func (s *ClassifyState) PrevBookmark() bool {startFile := s.FileIdxstartSeg := s.SegmentIdxfirst := truefor {// Move to previous segmentif !s.PrevSegment() {// Wrap to end of folders.FileIdx = len(s.DataFiles) - 1segs := s.filteredSegs[s.FileIdx]s.SegmentIdx = max(len(segs)-1, 0)}// Check if we've looped back to startif !first && s.FileIdx == startFile && s.SegmentIdx == startSeg {return false // full circle, no bookmark found}first = false// Check if current segment has bookmarkif s.hasFilterBookmark() {return true}}}// hasFilterBookmark checks if current segment has bookmark on filter-matching label.func (s *ClassifyState) hasFilterBookmark() bool {seg := s.CurrentSegment()if seg == nil {return false}label := s.getFilterLabel(seg)return label != nil && label.Bookmark}// FormatLabels formats labels for displayfunc FormatLabels(labels []*datafile.Label, filter string) string {var parts []stringfor _, l := range labels {if filter != "" && l.Filter != filter {continue}part := l.Speciesif l.CallType != "" {part += "/" + l.CallType}part += fmt.Sprintf(" (%d%%)", l.Certainty)if l.Filter != "" {part += " [" + l.Filter + "]"}if l.Comment != "" {part += fmt.Sprintf(" \"%s\"", l.Comment)}parts = append(parts, part)}return strings.Join(parts, ", ")}// LoadFilteredSegment reads, bandpass-shifts, and downsamples a segment's audio.// Returns samples and effective sample rate after any filtering/downsampling.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 configuredif 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 defaultif 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.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.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 playif 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 = speeds.Player.PlayAtSpeed(segSamples, playSampleRate, speed)}return ""}