package audio import ( "fmt" "io" "os" "sync" "time" "github.com/gopxl/beep/v2" "github.com/gopxl/beep/v2/flac" "github.com/gopxl/beep/v2/mp3" "github.com/gopxl/beep/v2/speaker" "github.com/gopxl/beep/v2/vorbis" "github.com/gopxl/beep/v2/wav" ) // Player handles audio playback type Player struct { mu sync.Mutex filePath string file *os.File streamer beep.StreamSeekCloser format beep.Format ctrl *beep.Ctrl resampler *beep.Resampler playing bool initialized bool } // NewPlayer creates a new audio player func NewPlayer() *Player { return &Player{} } // Load opens an audio file for playback func (p *Player) Load(path string) error { p.mu.Lock() defer p.mu.Unlock() // Close any existing file if p.file != nil { p.streamer.Close() p.file.Close() } format := DetectFormat(path) if format == FormatUnknown { return fmt.Errorf("unsupported audio format") } file, err := os.Open(path) if err != nil { return fmt.Errorf("failed to open audio file: %w", err) } var streamer beep.StreamSeekCloser var audioFormat beep.Format switch format { case FormatMP3: streamer, audioFormat, err = mp3.Decode(file) case FormatWAV: streamer, audioFormat, err = wav.Decode(file) case FormatFLAC: streamer, audioFormat, err = flac.Decode(file) case FormatOGG: streamer, audioFormat, err = vorbis.Decode(file) } if err != nil { file.Close() return fmt.Errorf("failed to decode audio: %w", err) } p.filePath = path p.file = file p.streamer = streamer p.format = audioFormat p.ctrl = &beep.Ctrl{Streamer: streamer, Paused: true} // Initialize speaker if not already done if !p.initialized { sampleRate := beep.SampleRate(44100) if err := speaker.Init(sampleRate, sampleRate.N(time.Second/10)); err != nil { return fmt.Errorf("failed to initialize speaker: %w", err) } p.initialized = true } // Resample if needed targetRate := beep.SampleRate(44100) if audioFormat.SampleRate != targetRate { p.resampler = beep.Resample(4, audioFormat.SampleRate, targetRate, p.ctrl) speaker.Play(p.resampler) } else { p.resampler = nil speaker.Play(p.ctrl) } return nil } // Play starts or resumes playback func (p *Player) Play() { p.mu.Lock() defer p.mu.Unlock() if p.ctrl == nil { return } speaker.Lock() p.ctrl.Paused = false speaker.Unlock() p.playing = true } // Pause pauses playback func (p *Player) Pause() { p.mu.Lock() defer p.mu.Unlock() if p.ctrl == nil { return } speaker.Lock() p.ctrl.Paused = true speaker.Unlock() p.playing = false } // Toggle toggles between play and pause func (p *Player) Toggle() { p.mu.Lock() if p.ctrl == nil { p.mu.Unlock() return } playing := p.playing p.mu.Unlock() if playing { p.Pause() } else { p.Play() } } // IsPlaying returns true if audio is currently playing func (p *Player) IsPlaying() bool { p.mu.Lock() defer p.mu.Unlock() return p.playing } // Position returns the current playback position func (p *Player) Position() time.Duration { p.mu.Lock() defer p.mu.Unlock() if p.streamer == nil { return 0 } speaker.Lock() pos := p.format.SampleRate.D(p.streamer.Position()) speaker.Unlock() return pos } // Duration returns the total duration func (p *Player) Duration() time.Duration { p.mu.Lock() defer p.mu.Unlock() if p.streamer == nil { return 0 } return p.format.SampleRate.D(p.streamer.Len()) } // Seek moves to the specified position func (p *Player) Seek(pos time.Duration) error { p.mu.Lock() defer p.mu.Unlock() if p.streamer == nil { return nil } sample := p.format.SampleRate.N(pos) if sample < 0 { sample = 0 } if sample > p.streamer.Len() { sample = p.streamer.Len() } speaker.Lock() err := p.streamer.Seek(sample) speaker.Unlock() return err } // SeekRelative seeks relative to current position func (p *Player) SeekRelative(delta time.Duration) error { pos := p.Position() return p.Seek(pos + delta) } // Close releases resources func (p *Player) Close() { p.mu.Lock() defer p.mu.Unlock() if p.streamer != nil { p.streamer.Close() } if p.file != nil { p.file.Close() } } // Format returns the audio format info func (p *Player) Format() beep.Format { p.mu.Lock() defer p.mu.Unlock() return p.format } // Streamer returns the underlying streamer (for waveform extraction) func (p *Player) Streamer() beep.StreamSeekCloser { p.mu.Lock() defer p.mu.Unlock() return p.streamer } // GetSamples extracts sample data for waveform visualization func (p *Player) GetSamples(numSamples int) ([]float64, error) { p.mu.Lock() defer p.mu.Unlock() if p.streamer == nil { return nil, fmt.Errorf("no audio loaded") } totalSamples := p.streamer.Len() if totalSamples == 0 { return nil, fmt.Errorf("empty audio") } samples := make([]float64, numSamples) samplesPerBucket := totalSamples / numSamples if samplesPerBucket < 1 { samplesPerBucket = 1 } // Save current position speaker.Lock() currentPos := p.streamer.Position() speaker.Unlock() buf := make([][2]float64, samplesPerBucket) for i := 0; i < numSamples; i++ { targetPos := i * samplesPerBucket if targetPos >= totalSamples { break } speaker.Lock() p.streamer.Seek(targetPos) speaker.Unlock() speaker.Lock() n, ok := p.streamer.Stream(buf) speaker.Unlock() if !ok || n == 0 { if err, isErr := p.streamer.(interface{ Err() error }); isErr && err.Err() != nil { if err.Err() != io.EOF { continue } } continue } // Calculate average absolute amplitude var sum float64 for j := 0; j < n; j++ { val := (buf[j][0] + buf[j][1]) / 2 if val < 0 { val = -val } sum += val } samples[i] = sum / float64(n) } // Restore position speaker.Lock() p.streamer.Seek(currentPos) speaker.Unlock() return samples, nil }