Intégration de libsoxr pour le rééchantillonnage audio
This commit is contained in:
271
pmostream/core.go
Normal file
271
pmostream/core.go
Normal file
@@ -0,0 +1,271 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user