Files
pmomusic/pmostream/core.go

272 lines
5.8 KiB
Go

package pmostream
import (
"fmt"
"math"
"sync"
"sync/atomic"
"time"
"gargoton.petite-maison-orange.fr/eric/pmomusic/pmosoxr"
"github.com/gopxl/beep"
"github.com/gopxl/beep/effects"
)
type SampleFormat int
const (
Float32 SampleFormat = iota
PCM16
)
type HiFiConfig struct {
TargetSampleRate beep.SampleRate
BufferSeconds int
ChunkSize int
ResampleQuality pmosoxr.Quality
Volume float64
Format SampleFormat
}
func DefaultHiFiConfig() HiFiConfig {
return HiFiConfig{
TargetSampleRate: 48000,
BufferSeconds: 5,
ChunkSize: 1024,
ResampleQuality: pmosoxr.HQ,
Volume: 0.0,
Format: PCM16,
}
}
type AudioProcessor struct {
config HiFiConfig
streamer beep.Streamer
format beep.Format
resampler *pmosoxr.Resampler
volume *effects.Volume
volumeValue float64
buffer *AudioBuffer
masterBuffer *MasterBuffer
processMutex sync.Mutex
running atomic.Bool
wg sync.WaitGroup
resampleBuf []float32
volumeMu sync.Mutex
streamDone atomic.Bool
}
func NewAudioProcessor(streamer beep.Streamer, format beep.Format, config HiFiConfig, master *MasterBuffer) (*AudioProcessor, error) {
if streamer == nil && master == nil {
return nil, fmt.Errorf("streamer cannot be nil if no master buffer is provided")
}
if format.NumChannels != 2 {
return nil, fmt.Errorf("only stereo format is supported")
}
var vol *effects.Volume
if streamer != nil {
vol = &effects.Volume{
Streamer: streamer,
Base: 2,
Volume: config.Volume,
Silent: false,
}
}
var resampler *pmosoxr.Resampler
var err error
if streamer != nil && format.SampleRate != config.TargetSampleRate {
resampler, err = pmosoxr.New(float64(format.SampleRate), float64(config.TargetSampleRate), 2, config.ResampleQuality)
if err != nil {
return nil, fmt.Errorf("failed to create resampler: %w", err)
}
}
sampleSize := 4
if config.Format == PCM16 {
sampleSize = 2
}
bytesPerSecond := int(config.TargetSampleRate) * sampleSize * 2 // stéréo
bufferSize := (bytesPerSecond * config.BufferSeconds) / config.ChunkSize
if bufferSize < 1 {
bufferSize = 1
}
ap := &AudioProcessor{
config: config,
streamer: streamer,
format: format,
resampler: resampler,
volume: vol,
volumeValue: config.Volume,
buffer: NewAudioBuffer(bufferSize, config.Format),
masterBuffer: master,
}
ap.running.Store(true)
return ap, nil
}
func (p *AudioProcessor) GetBuffer() *AudioBuffer {
return p.buffer
}
func (p *AudioProcessor) SetVolume(volume float64) {
p.volumeMu.Lock()
defer p.volumeMu.Unlock()
p.volumeValue = volume
if p.volume != nil {
p.volume.Volume = volume
}
}
func (p *AudioProcessor) Stop() {
p.running.Store(false)
}
func (p *AudioProcessor) Close() error {
p.Stop()
p.wg.Wait()
p.processMutex.Lock()
defer p.processMutex.Unlock()
if p.resampler != nil {
p.resampler.Delete()
}
if closer, ok := p.streamer.(interface{ Close() error }); ok {
return closer.Close()
}
return nil
}
// Process lit depuis le streamer ou le master buffer, applique resampling + volume et écrit dans le buffer
func (p *AudioProcessor) Process() error {
if !p.running.Load() {
return nil
}
p.wg.Add(1)
defer p.wg.Done()
chunkSize := p.config.ChunkSize
if chunkSize <= 0 {
chunkSize = 1024
}
for p.running.Load() {
var samples [][2]float64
// 1) Lire depuis le streamer
if p.streamer != nil {
samples = make([][2]float64, chunkSize)
n, ok := p.streamer.Stream(samples)
if !ok {
p.streamDone.Store(true)
break
}
samples = samples[:n]
if n == 0 {
time.Sleep(5 * time.Millisecond)
continue
}
} else if p.masterBuffer != nil {
// Lecture via ForkedBuffer
chunks := p.masterBuffer.ReadAll()
if len(chunks) == 0 {
time.Sleep(5 * time.Millisecond)
continue
}
samples = make([][2]float64, 0)
for _, c := range chunks {
var fs []float32
if p.config.Format == Float32 {
fs = BytesToFloat32(c)
} else {
fs = PcmToFloat32(c)
}
if len(fs)%2 != 0 {
continue
}
for i := 0; i < len(fs); i += 2 {
samples = append(samples, [2]float64{float64(fs[i]), float64(fs[i+1])})
}
}
if len(samples) == 0 {
time.Sleep(5 * time.Millisecond)
continue
}
} else {
time.Sleep(5 * time.Millisecond)
continue
}
// 2) Resampler si nécessaire
var processed []float32
if p.resampler != nil {
if len(p.resampleBuf) < len(samples)*2 {
p.resampleBuf = make([]float32, len(samples)*2)
}
inBuf := ConvertFloat64ToFloat32(samples)
_, np, err := p.resampler.Process(inBuf, p.resampleBuf)
if err != nil {
return err
}
processed = p.resampleBuf[:np]
} else {
processed = ConvertFloat64ToFloat32(samples)
}
// 3) Appliquer volume
p.volumeMu.Lock()
volumeFactor := float32(math.Pow(2, p.volumeValue))
p.volumeMu.Unlock()
for i := range processed {
processed[i] *= volumeFactor
}
// 4) Convertir et écrire
var chunk []byte
if p.config.Format == Float32 {
chunk = Float32ToBytes(processed)
} else {
chunk = Float32ToPCM(processed)
}
p.buffer.Write(chunk)
}
// Flush resampler à la fin uniquement
if p.resampler != nil {
flushBuf := make([]float32, 4096)
for {
_, np, err := p.resampler.Process(nil, flushBuf)
if err != nil {
return err
}
if np == 0 {
break
}
processed := flushBuf[:np]
p.volumeMu.Lock()
volumeFactor := float32(math.Pow(2, p.volumeValue))
p.volumeMu.Unlock()
for i := range processed {
processed[i] *= volumeFactor
}
var chunk []byte
if p.config.Format == Float32 {
chunk = Float32ToBytes(processed)
} else {
chunk = Float32ToPCM(processed)
}
p.buffer.Write(chunk)
}
}
p.buffer.Close() // fermer uniquement à la fin
return nil
}