init commit
This commit is contained in:
60
internal/audio/formats.go
Normal file
60
internal/audio/formats.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AudioFormat represents a supported audio format
|
||||
type AudioFormat int
|
||||
|
||||
const (
|
||||
FormatUnknown AudioFormat = iota
|
||||
FormatMP3
|
||||
FormatWAV
|
||||
FormatFLAC
|
||||
FormatOGG
|
||||
)
|
||||
|
||||
// DetectFormat returns the audio format based on file extension
|
||||
func DetectFormat(path string) AudioFormat {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
switch ext {
|
||||
case ".mp3":
|
||||
return FormatMP3
|
||||
case ".wav":
|
||||
return FormatWAV
|
||||
case ".flac":
|
||||
return FormatFLAC
|
||||
case ".ogg":
|
||||
return FormatOGG
|
||||
default:
|
||||
return FormatUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the format name
|
||||
func (f AudioFormat) String() string {
|
||||
switch f {
|
||||
case FormatMP3:
|
||||
return "MP3"
|
||||
case FormatWAV:
|
||||
return "WAV"
|
||||
case FormatFLAC:
|
||||
return "FLAC"
|
||||
case FormatOGG:
|
||||
return "OGG"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateFormat checks if the file format is supported
|
||||
func ValidateFormat(path string) error {
|
||||
format := DetectFormat(path)
|
||||
if format == FormatUnknown {
|
||||
return fmt.Errorf("unsupported audio format: %s", filepath.Ext(path))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
313
internal/audio/player.go
Normal file
313
internal/audio/player.go
Normal file
@@ -0,0 +1,313 @@
|
||||
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
|
||||
}
|
||||
36
internal/audio/waveform.go
Normal file
36
internal/audio/waveform.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package audio
|
||||
|
||||
// WaveformData holds pre-computed waveform samples
|
||||
type WaveformData struct {
|
||||
Samples []float64
|
||||
MaxValue float64
|
||||
}
|
||||
|
||||
// NewWaveformData creates waveform data from raw samples
|
||||
func NewWaveformData(samples []float64) *WaveformData {
|
||||
wd := &WaveformData{
|
||||
Samples: samples,
|
||||
}
|
||||
|
||||
// Find max value for normalization
|
||||
for _, s := range samples {
|
||||
if s > wd.MaxValue {
|
||||
wd.MaxValue = s
|
||||
}
|
||||
}
|
||||
|
||||
if wd.MaxValue == 0 {
|
||||
wd.MaxValue = 1 // Avoid division by zero
|
||||
}
|
||||
|
||||
return wd
|
||||
}
|
||||
|
||||
// Normalized returns samples normalized to 0-1 range
|
||||
func (w *WaveformData) Normalized() []float64 {
|
||||
normalized := make([]float64, len(w.Samples))
|
||||
for i, s := range w.Samples {
|
||||
normalized[i] = s / w.MaxValue
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
Reference in New Issue
Block a user