SDBVLSDDRPQF62XXKJKM2RQLMXOKKHOYRVUF6DIUDFRYCGL2DW3QC EW7VBNMGWFBC73ZUDLB4LIK2HWFKA74ZUTUDG4J575ZQHEFHW4UQC IFVRAERTCCDICNTYTG3TX2WASB6RXQQEJWWXQMQZJSQDQ3HLE5OQC DBOROCRFD6A5SJBMFYFEJI5S5M77X4EFEK6KDQWA5QDMQJKIHRWQC U6JEEU5O477ZOJ5UMRMOJSGPEJEU6Q7KMPKUSDF56CYVUJWL7QBQC PZHNIV62T77A3VPGPAYURINYRMUJKMNQHTHYD7L22X7WDZSSKQ7QC W3A2EECCD23SVHJZN6MXPH2PAVFHH5CNFD2XHPQRRW6M4GUTG3FAC EBCNGTNVY2YFFHKC4PHDEHNBOWJ4JDCEXTTJ3WKD5VWQZLLDZ65AC package utilsimport ("bytes""encoding/binary""math""sync""github.com/ebitengine/oto/v3")// AudioPlayer wraps oto for simple audio playback.// The oto context is created once and reused across plays.type AudioPlayer struct {ctx *oto.Contextmu sync.Mutexplayer *oto.Player}// NewAudioPlayer creates a new audio player with the given sample rate.// Only one AudioPlayer should exist per process (oto allows one context).func NewAudioPlayer(sampleRate int) (*AudioPlayer, error) {op := &oto.NewContextOptions{SampleRate: sampleRate,ChannelCount: 1,Format: oto.FormatSignedInt16LE,}ctx, readyChan, err := oto.NewContext(op)if err != nil {return nil, err}<-readyChanreturn &AudioPlayer{ctx: ctx}, nil}// Play stops any current playback and starts playing the given samples.// Samples are float64 in the range -1.0 to 1.0.// Playback is non-blocking — audio plays in the background.func (ap *AudioPlayer) Play(samples []float64, sampleRate int) {ap.mu.Lock()defer ap.mu.Unlock()// Stop previous playbackif ap.player != nil {ap.player.Pause()ap.player = nil}// Convert float64 samples to signed int16 LE bytesbuf := 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))}ap.player = ap.ctx.NewPlayer(bytes.NewReader(buf))ap.player.Play()}// IsPlaying returns true if audio is currently playing.func (ap *AudioPlayer) IsPlaying() bool {ap.mu.Lock()defer ap.mu.Unlock()return ap.player != nil && ap.player.IsPlaying()}// Stop stops any current playback.func (ap *AudioPlayer) Stop() {ap.mu.Lock()defer ap.mu.Unlock()if ap.player != nil {ap.player.Pause()ap.player = nil}}// Close stops playback and releases the oto context.func (ap *AudioPlayer) Close() {ap.Stop()}
}}// playCurrentSegment loads and plays the current segment's audio.// Returns an error message string, or empty string on success.func playCurrentSegment(state *tools.ClassifyState) string {df := state.CurrentFile()seg := state.CurrentSegment()if df == nil || seg == nil {return ""}wavPath := strings.TrimSuffix(df.FilePath, ".data")samples, sampleRate, err := utils.ReadWAVSamples(wavPath)if err != nil {return fmt.Sprintf("audio: %v", err)}// Initialize player lazily on first playif state.Player == nil {player, err := utils.NewAudioPlayer(sampleRate)if err != nil {return fmt.Sprintf("audio init: %v", err)}state.Player = player}segSamples := utils.ExtractSegmentSamples(samples, sampleRate, seg.StartTime, seg.EndTime)if len(segSamples) > 0 {state.Player.Play(segSamples, sampleRate)
b.WriteString(fmt.Sprintf("Segment: %.1fs - %.1fs (%.1fs)\n",seg.StartTime, seg.EndTime, seg.EndTime-seg.StartTime))b.WriteString("\n")
segInfo := fmt.Sprintf("Segment: %.1fs - %.1fs (%.1fs)", seg.StartTime, seg.EndTime, seg.EndTime-seg.StartTime)if m.state.Player != nil && m.state.Player.IsPlaying() {segInfo += " ▶ Playing..."}b.WriteString(segInfo)b.WriteString("\n\n")
github.com/ebitengine/oto/v3 v3.4.0 h1:br0PgASsEWaoWn38b2Goe7m1GKFYfNgnsjSd5Gg+/bQ=github.com/ebitengine/oto/v3 v3.4.0/go.mod h1:IOleLVD0m+CMak3mRVwsYY8vTctQgOM0iiL6S7Ar7eI=github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
- Added `utils/audio_player.go` — wraps ebitengine/oto v3 for PCM playback- Oto context created lazily on first play, reused across segments- Converts `[]float64` samples → signed int16 LE for oto- Playback stops automatically on navigation (`,`/`.`), binding keys, and quit- "▶ Playing..." indicator shown in segment info line- New dependency: `github.com/ebitengine/oto/v3` (requires `libasound2-dev` on Linux)