272 lines
5.8 KiB
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
|
|
}
|