Intégration de libsoxr pour le rééchantillonnage audio
This commit is contained in:
60
pmostream/audio.go
Normal file
60
pmostream/audio.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package pmostream
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gopxl/beep"
|
||||
"github.com/gopxl/beep/flac"
|
||||
"github.com/gopxl/beep/mp3"
|
||||
"github.com/gopxl/beep/vorbis"
|
||||
"github.com/gopxl/beep/wav"
|
||||
)
|
||||
|
||||
type streamerWithCloser struct {
|
||||
beep.Streamer
|
||||
closer func() error
|
||||
}
|
||||
|
||||
func (s *streamerWithCloser) Close() error {
|
||||
if s.closer != nil {
|
||||
return s.closer()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadAudio(uri string) (beep.Streamer, beep.Format, error) {
|
||||
f, err := os.Open(uri)
|
||||
if err != nil {
|
||||
return nil, beep.Format{}, err
|
||||
}
|
||||
|
||||
var streamer beep.Streamer
|
||||
var format beep.Format
|
||||
|
||||
lowerURI := strings.ToLower(uri)
|
||||
switch {
|
||||
case strings.HasSuffix(lowerURI, ".flac"):
|
||||
streamer, format, err = flac.Decode(f)
|
||||
case strings.HasSuffix(lowerURI, ".wav"):
|
||||
streamer, format, err = wav.Decode(f)
|
||||
case strings.HasSuffix(lowerURI, ".mp3"):
|
||||
streamer, format, err = mp3.Decode(f)
|
||||
case strings.HasSuffix(lowerURI, ".ogg"):
|
||||
streamer, format, err = vorbis.Decode(f)
|
||||
default:
|
||||
f.Close()
|
||||
return nil, beep.Format{}, fmt.Errorf("unsupported format: %s", uri)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, beep.Format{}, err
|
||||
}
|
||||
|
||||
return &streamerWithCloser{
|
||||
Streamer: streamer,
|
||||
closer: f.Close,
|
||||
}, format, nil
|
||||
}
|
||||
139
pmostream/buffer.go
Normal file
139
pmostream/buffer.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package pmostream
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type AudioBuffer struct {
|
||||
mu sync.Mutex
|
||||
cond *sync.Cond
|
||||
chunks [][]byte
|
||||
size int
|
||||
available int
|
||||
format SampleFormat
|
||||
readPos int
|
||||
writePos int
|
||||
closed bool
|
||||
}
|
||||
|
||||
func NewAudioBuffer(size int, format SampleFormat) *AudioBuffer {
|
||||
log.Infof("New AudioBuffer with size %d", size)
|
||||
ab := &AudioBuffer{
|
||||
chunks: make([][]byte, size),
|
||||
size: size,
|
||||
format: format,
|
||||
}
|
||||
ab.cond = sync.NewCond(&ab.mu)
|
||||
return ab
|
||||
}
|
||||
|
||||
func (ab *AudioBuffer) Available() int {
|
||||
ab.mu.Lock()
|
||||
defer ab.mu.Unlock()
|
||||
return ab.available
|
||||
}
|
||||
|
||||
// Write ajoute un chunk dans le buffer (écrase le plus ancien si plein)
|
||||
func (ab *AudioBuffer) Write(chunk []byte) {
|
||||
ab.mu.Lock()
|
||||
defer ab.mu.Unlock()
|
||||
if ab.closed {
|
||||
return
|
||||
}
|
||||
|
||||
ab.chunks[ab.writePos] = chunk
|
||||
ab.writePos = (ab.writePos + 1) % ab.size
|
||||
|
||||
if ab.available < ab.size {
|
||||
ab.available++
|
||||
} else {
|
||||
// tampon plein, on écrase → avancer readPos
|
||||
ab.readPos = (ab.readPos + 1) % ab.size
|
||||
}
|
||||
|
||||
ab.cond.Broadcast()
|
||||
}
|
||||
|
||||
func (ab *AudioBuffer) Read() []byte {
|
||||
ab.mu.Lock()
|
||||
defer ab.mu.Unlock()
|
||||
|
||||
if ab.available == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
chunk := ab.chunks[ab.readPos]
|
||||
ab.chunks[ab.readPos] = nil
|
||||
ab.readPos = (ab.readPos + 1) % ab.size
|
||||
ab.available--
|
||||
|
||||
return chunk
|
||||
}
|
||||
|
||||
// WaitForData attend qu'au moins n chunks soient disponibles ou timeout/closed
|
||||
func (ab *AudioBuffer) WaitForData(n int, timeout time.Duration) bool {
|
||||
ab.mu.Lock()
|
||||
defer ab.mu.Unlock()
|
||||
|
||||
if n <= 0 {
|
||||
n = 1
|
||||
}
|
||||
|
||||
if timeout <= 0 {
|
||||
for ab.available < n && !ab.closed {
|
||||
ab.cond.Wait()
|
||||
}
|
||||
return ab.available >= n && !ab.closed
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(timeout)
|
||||
for ab.available < n && !ab.closed {
|
||||
remaining := time.Until(deadline)
|
||||
if remaining <= 0 {
|
||||
return false
|
||||
}
|
||||
waitCondWithTimeout(ab.cond, remaining)
|
||||
}
|
||||
|
||||
return ab.available >= n && !ab.closed
|
||||
}
|
||||
|
||||
// Wait est un raccourci pour WaitForData(1, 0)
|
||||
func (ab *AudioBuffer) Wait() {
|
||||
ab.WaitForData(1, 0)
|
||||
}
|
||||
|
||||
func (ab *AudioBuffer) Close() {
|
||||
ab.mu.Lock()
|
||||
defer ab.mu.Unlock()
|
||||
if !ab.closed {
|
||||
ab.closed = true
|
||||
ab.cond.Broadcast()
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction helper pour attendre une sync.Cond avec timeout
|
||||
func waitCondWithTimeout(c *sync.Cond, d time.Duration) bool {
|
||||
timer := time.NewTimer(d)
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
c.L.Lock()
|
||||
defer c.L.Unlock()
|
||||
c.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
return true
|
||||
case <-timer.C:
|
||||
return false
|
||||
}
|
||||
}
|
||||
88
pmostream/conversion.go
Normal file
88
pmostream/conversion.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package pmostream
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math"
|
||||
)
|
||||
|
||||
func ConvertFloat64ToFloat32(samples [][2]float64) []float32 {
|
||||
if len(samples) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]float32, len(samples)*2)
|
||||
for i, s := range samples {
|
||||
result[2*i] = float32(clamp(s[0], -1.0, 1.0))
|
||||
result[2*i+1] = float32(clamp(s[1], -1.0, 1.0))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ConvertFloat64ToPCM(samples [][2]float64) []byte {
|
||||
if len(samples) == 0 {
|
||||
return nil
|
||||
}
|
||||
buf := make([]byte, len(samples)*4)
|
||||
for i, s := range samples {
|
||||
l := int16(clamp(s[0], -1.0, 1.0) * 32767.0)
|
||||
r := int16(clamp(s[1], -1.0, 1.0) * 32767.0)
|
||||
binary.LittleEndian.PutUint16(buf[4*i:], uint16(uint16(l)))
|
||||
binary.LittleEndian.PutUint16(buf[4*i+2:], uint16(uint16(r)))
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func Float32ToPCM(samples []float32) []byte {
|
||||
if len(samples) == 0 {
|
||||
return nil
|
||||
}
|
||||
buf := make([]byte, len(samples)*2)
|
||||
for i, v := range samples {
|
||||
val := int16(clamp(float64(v), -1.0, 1.0) * 32767.0)
|
||||
binary.LittleEndian.PutUint16(buf[2*i:], uint16(val))
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func Float32ToBytes(samples []float32) []byte {
|
||||
if len(samples) == 0 {
|
||||
return nil
|
||||
}
|
||||
buf := make([]byte, len(samples)*4)
|
||||
for i, v := range samples {
|
||||
binary.LittleEndian.PutUint32(buf[i*4:], math.Float32bits(v))
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func BytesToFloat32(data []byte) []float32 {
|
||||
if len(data)%4 != 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]float32, len(data)/4)
|
||||
for i := range result {
|
||||
result[i] = math.Float32frombits(binary.LittleEndian.Uint32(data[i*4:]))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func PcmToFloat32(data []byte) []float32 {
|
||||
if len(data)%2 != 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]float32, len(data)/2)
|
||||
for i := 0; i < len(result); i++ {
|
||||
val := int16(binary.LittleEndian.Uint16(data[2*i:]))
|
||||
result[i] = float32(val) / 32768.0
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func clamp(val, min, max float64) float64 {
|
||||
if val < min {
|
||||
return min
|
||||
}
|
||||
if val > max {
|
||||
return max
|
||||
}
|
||||
return val
|
||||
}
|
||||
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
|
||||
}
|
||||
208
pmostream/fork.go
Normal file
208
pmostream/fork.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package pmostream
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gopxl/beep"
|
||||
)
|
||||
|
||||
// MasterBuffer est un buffer central qui permet de fork un flux vers plusieurs processors
|
||||
type MasterBuffer struct {
|
||||
mu sync.RWMutex
|
||||
cond *sync.Cond
|
||||
chunks [][]byte
|
||||
closed bool
|
||||
}
|
||||
|
||||
// NewMasterBuffer crée un buffer central
|
||||
func NewMasterBuffer() *MasterBuffer {
|
||||
mb := &MasterBuffer{}
|
||||
mb.cond = sync.NewCond(&mb.mu)
|
||||
return mb
|
||||
}
|
||||
|
||||
// Write ajoute un chunk au buffer central
|
||||
func (mb *MasterBuffer) Write(chunk []byte) {
|
||||
if len(chunk) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
mb.mu.Lock()
|
||||
defer mb.mu.Unlock()
|
||||
|
||||
if mb.closed {
|
||||
return
|
||||
}
|
||||
|
||||
data := make([]byte, len(chunk))
|
||||
copy(data, chunk)
|
||||
|
||||
mb.chunks = append(mb.chunks, data)
|
||||
mb.cond.Broadcast()
|
||||
}
|
||||
|
||||
// Fork crée un lecteur indépendant pour ce buffer
|
||||
func (mb *MasterBuffer) Fork() *ForkedBuffer {
|
||||
mb.mu.RLock()
|
||||
defer mb.mu.RUnlock()
|
||||
|
||||
return &ForkedBuffer{
|
||||
master: mb,
|
||||
index: len(mb.chunks),
|
||||
}
|
||||
}
|
||||
|
||||
// Close ferme le buffer et notifie tous les lecteurs
|
||||
func (mb *MasterBuffer) Close() {
|
||||
mb.mu.Lock()
|
||||
defer mb.mu.Unlock()
|
||||
mb.closed = true
|
||||
mb.cond.Broadcast()
|
||||
}
|
||||
|
||||
// ForkedBuffer permet à un processor forké de lire indépendamment
|
||||
type ForkedBuffer struct {
|
||||
master *MasterBuffer
|
||||
index int
|
||||
}
|
||||
|
||||
func (fb *ForkedBuffer) ReadAll() [][]byte {
|
||||
fb.master.mu.Lock() // Lock au lieu de RLock
|
||||
defer fb.master.mu.Unlock()
|
||||
|
||||
if fb.index >= len(fb.master.chunks) {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([][]byte, len(fb.master.chunks)-fb.index)
|
||||
for i := fb.index; i < len(fb.master.chunks); i++ {
|
||||
result[i-fb.index] = fb.master.chunks[i]
|
||||
}
|
||||
fb.index = len(fb.master.chunks)
|
||||
return result
|
||||
}
|
||||
|
||||
func (fb *ForkedBuffer) WaitForData(timeoutMs int) bool {
|
||||
deadline := time.Now().Add(time.Duration(timeoutMs) * time.Millisecond)
|
||||
fb.master.mu.Lock()
|
||||
defer fb.master.mu.Unlock()
|
||||
|
||||
for fb.index >= len(fb.master.chunks) && !fb.master.closed {
|
||||
remaining := time.Until(deadline)
|
||||
if remaining <= 0 {
|
||||
return false
|
||||
}
|
||||
fb.master.cond.Wait()
|
||||
}
|
||||
return fb.index < len(fb.master.chunks)
|
||||
}
|
||||
|
||||
// ForkStreamer crée plusieurs streamers à partir d'un streamer source en utilisant un MasterBuffer
|
||||
func ForkStreamer(streamer beep.Streamer, format beep.Format, config HiFiConfig, nForks int) ([]beep.Streamer, error) {
|
||||
if nForks < 1 {
|
||||
return nil, fmt.Errorf("nForks must be at least 1")
|
||||
}
|
||||
|
||||
// Créer un MasterBuffer
|
||||
master := NewMasterBuffer()
|
||||
|
||||
// Créer un processeur principal qui alimente le MasterBuffer
|
||||
mainProc, err := NewAudioProcessor(streamer, format, config, master)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Démarrer le traitement principal
|
||||
go mainProc.Process()
|
||||
|
||||
// Créer des streamers forké
|
||||
forks := make([]beep.Streamer, nForks)
|
||||
for i := 0; i < nForks; i++ {
|
||||
forkedBuffer := master.Fork()
|
||||
forks[i] = &forkedStreamer{
|
||||
fb: forkedBuffer,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
return forks, nil
|
||||
}
|
||||
|
||||
// forkedStreamer implémente beep.Streamer pour lire depuis un ForkedBuffer
|
||||
type forkedStreamer struct {
|
||||
fb *ForkedBuffer
|
||||
config HiFiConfig
|
||||
}
|
||||
|
||||
func (fs *forkedStreamer) Stream(samples [][2]float64) (n int, ok bool) {
|
||||
if !fs.fb.WaitForData(100) {
|
||||
return 0, true
|
||||
}
|
||||
|
||||
chunks := fs.fb.ReadAll()
|
||||
if len(chunks) == 0 {
|
||||
return 0, true
|
||||
}
|
||||
|
||||
// Concaténer tous les chunks
|
||||
var totalSize int
|
||||
for _, chunk := range chunks {
|
||||
totalSize += len(chunk)
|
||||
}
|
||||
|
||||
combined := make([]byte, 0, totalSize)
|
||||
for _, chunk := range chunks {
|
||||
combined = append(combined, chunk...)
|
||||
}
|
||||
|
||||
// Convertir en float32 selon le format
|
||||
var allData []float32
|
||||
if fs.config.Format == Float32 {
|
||||
allData = BytesToFloat32(combined)
|
||||
} else {
|
||||
allData = PcmToFloat32(combined)
|
||||
}
|
||||
|
||||
if allData == nil {
|
||||
return 0, true
|
||||
}
|
||||
|
||||
numSamples := len(allData) / 2
|
||||
if numSamples > len(samples) {
|
||||
numSamples = len(samples)
|
||||
}
|
||||
|
||||
for i := 0; i < numSamples; i++ {
|
||||
if 2*i+1 < len(allData) {
|
||||
samples[i][0] = float64(allData[2*i])
|
||||
samples[i][1] = float64(allData[2*i+1])
|
||||
}
|
||||
}
|
||||
|
||||
return numSamples, true
|
||||
}
|
||||
|
||||
func (fs *forkedStreamer) Err() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadAll retourne tous les chunks disponibles dans le buffer central
|
||||
func (mb *MasterBuffer) ReadAll() [][]byte {
|
||||
mb.mu.RLock()
|
||||
defer mb.mu.RUnlock()
|
||||
|
||||
if mb.closed || len(mb.chunks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Créer une copie de tous les chunks
|
||||
result := make([][]byte, len(mb.chunks))
|
||||
for i, chunk := range mb.chunks {
|
||||
result[i] = make([]byte, len(chunk))
|
||||
copy(result[i], chunk)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
47
pmostream/fork_eq_test.go
Normal file
47
pmostream/fork_eq_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
//go:build cgo
|
||||
// +build cgo
|
||||
|
||||
package pmostream
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gopxl/beep"
|
||||
)
|
||||
|
||||
// TestForkedEQ vérifie que chaque fork peut avoir son propre ParametricEQ appliqué
|
||||
func TestAudioProcessorWithMasterBuffer(t *testing.T) {
|
||||
stream := beep.StreamerFunc(func(samples [][2]float64) (n int, ok bool) {
|
||||
for i := range samples {
|
||||
v := 0.2 * math.Sin(2*math.Pi*440*float64(i)/44100.0)
|
||||
samples[i][0] = v
|
||||
samples[i][1] = v
|
||||
}
|
||||
return len(samples), true
|
||||
})
|
||||
|
||||
format := beep.Format{SampleRate: 44100, NumChannels: 2, Precision: 2}
|
||||
config := DefaultHiFiConfig()
|
||||
master := NewMasterBuffer()
|
||||
|
||||
proc, err := NewAudioProcessor(stream, format, config, master)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
go proc.Process()
|
||||
defer proc.Close()
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
if proc.buffer.Available() == 0 {
|
||||
t.Fatal("buffer should contain data")
|
||||
}
|
||||
|
||||
chunk := proc.buffer.Read()
|
||||
if chunk == nil || len(chunk) == 0 {
|
||||
t.Fatal("failed to read chunk from buffer")
|
||||
}
|
||||
}
|
||||
93
pmostream/parameq.go
Normal file
93
pmostream/parameq.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package pmostream
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sync"
|
||||
|
||||
"github.com/gopxl/beep"
|
||||
)
|
||||
|
||||
// ParametricEQ applique un égaliseur paramétrique stéréo (Biquad peaking) sur un Streamer.
|
||||
type ParametricEQ struct {
|
||||
input beep.Streamer
|
||||
sampleRate float64
|
||||
mu sync.Mutex
|
||||
|
||||
// Coefficients Biquad
|
||||
a0, a1, a2, b1, b2 float64
|
||||
|
||||
// États pour les deux canaux
|
||||
x1, x2, y1, y2 [2]float64
|
||||
}
|
||||
|
||||
// EQParams définit les paramètres d'un filtre peaking
|
||||
type EQParams struct {
|
||||
FreqHz float64 // fréquence centrale en Hz
|
||||
GainDB float64 // gain en dB
|
||||
Q float64 // facteur de qualité
|
||||
}
|
||||
|
||||
// NewParametricEQ construit un EQ peaking stéréo en interrogeant le streamer pour la fréquence
|
||||
func NewParametricEQ(input beep.Streamer, params EQParams, sr beep.SampleRate) *ParametricEQ {
|
||||
eq := &ParametricEQ{
|
||||
input: input,
|
||||
sampleRate: float64(sr),
|
||||
}
|
||||
|
||||
eq.setParams(params)
|
||||
return eq
|
||||
}
|
||||
|
||||
// setParams calcule les coefficients du Biquad
|
||||
func (eq *ParametricEQ) setParams(p EQParams) {
|
||||
eq.mu.Lock()
|
||||
defer eq.mu.Unlock()
|
||||
|
||||
A := math.Pow(10, p.GainDB/40) // conversion dB -> amplitude
|
||||
w0 := 2 * math.Pi * p.FreqHz / eq.sampleRate
|
||||
alpha := math.Sin(w0) / (2 * p.Q)
|
||||
|
||||
a0 := 1 + alpha/A
|
||||
eq.a0 = 1
|
||||
eq.a1 = -2 * math.Cos(w0) / a0
|
||||
eq.a2 = (1 - alpha/A) / a0
|
||||
eq.b1 = 2 * math.Cos(w0) * -1 / a0
|
||||
eq.b2 = (1 - alpha*A) / a0
|
||||
}
|
||||
|
||||
// Stream applique l'EQ sur un chunk stéréo float64 [][2]float64
|
||||
func (eq *ParametricEQ) Stream(samples [][2]float64) (n int, ok bool) {
|
||||
eq.mu.Lock()
|
||||
defer eq.mu.Unlock()
|
||||
|
||||
if len(samples) == 0 {
|
||||
return 0, true
|
||||
}
|
||||
|
||||
for i := range samples {
|
||||
for ch := 0; ch < 2; ch++ {
|
||||
x := samples[i][ch]
|
||||
y := eq.a0*x + eq.a1*eq.x1[ch] + eq.a2*eq.x2[ch] - eq.b1*eq.y1[ch] - eq.b2*eq.y2[ch]
|
||||
|
||||
eq.x2[ch] = eq.x1[ch]
|
||||
eq.x1[ch] = x
|
||||
eq.y2[ch] = eq.y1[ch]
|
||||
eq.y1[ch] = y
|
||||
|
||||
samples[i][ch] = y
|
||||
}
|
||||
}
|
||||
return len(samples), true
|
||||
}
|
||||
|
||||
// Close libère les ressources (pas nécessaire ici mais pour interface uniforme)
|
||||
func (eq *ParametricEQ) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wrap permet d'utiliser ParametricEQ comme beep.Streamer
|
||||
func (eq *ParametricEQ) Streamer() beep.Streamer {
|
||||
return beep.StreamerFunc(func(samples [][2]float64) (n int, ok bool) {
|
||||
return eq.Stream(samples)
|
||||
})
|
||||
}
|
||||
150
pmostream/streamer.go
Normal file
150
pmostream/streamer.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package pmostream
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gopxl/beep"
|
||||
)
|
||||
|
||||
// StreamManager gère plusieurs AudioProcessor et clients HTTP
|
||||
type StreamManager struct {
|
||||
processors map[string]*AudioProcessor
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewStreamManager crée un gestionnaire de flux audio
|
||||
func NewStreamManager() *StreamManager {
|
||||
return &StreamManager{
|
||||
processors: make(map[string]*AudioProcessor),
|
||||
}
|
||||
}
|
||||
|
||||
// AddProcessor ajoute un processeur audio et démarre sa boucle Process
|
||||
func (m *StreamManager) AddProcessor(id string, processor *AudioProcessor) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if _, exists := m.processors[id]; exists {
|
||||
log.Printf("Processor with id %s already exists", id)
|
||||
return
|
||||
}
|
||||
m.processors[id] = processor
|
||||
|
||||
go func() {
|
||||
if err := processor.Process(); err != nil {
|
||||
log.Printf("Processor error for %s: %v", id, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// RemoveProcessor arrête et supprime un processeur audio
|
||||
func (m *StreamManager) RemoveProcessor(id string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if processor, exists := m.processors[id]; exists {
|
||||
processor.Stop()
|
||||
processor.Close()
|
||||
delete(m.processors, id)
|
||||
}
|
||||
}
|
||||
|
||||
// GetHandler retourne un handler HTTP pour streamer un flux audio
|
||||
func (m *StreamManager) GetHandler(id string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
m.mu.RLock()
|
||||
processor, exists := m.processors[id]
|
||||
m.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
http.Error(w, "Stream not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
buffer := processor.GetBuffer()
|
||||
config := processor.config
|
||||
|
||||
w.Header().Set("Content-Type", "audio/wav")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "Streaming not supported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Écrire l'en-tête WAV pour streaming
|
||||
if err := writeWavHeader(w, config.TargetSampleRate, config.Format); err != nil {
|
||||
log.Printf("Failed to write WAV header: %v", err)
|
||||
return
|
||||
}
|
||||
flusher.Flush()
|
||||
|
||||
clientDone := r.Context().Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-clientDone:
|
||||
log.Printf("Client disconnected: %s", id)
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// Attendre les données avec timeout
|
||||
if !buffer.WaitForData(1, 100*time.Millisecond) {
|
||||
continue
|
||||
}
|
||||
|
||||
chunk := buffer.Read()
|
||||
if chunk == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Envoyer le chunk au client
|
||||
if _, err := w.Write(chunk); err != nil {
|
||||
log.Printf("Write error: %v", err)
|
||||
return
|
||||
}
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writeWavHeader écrit un en-tête WAV pour streaming
|
||||
func writeWavHeader(w http.ResponseWriter, sampleRate beep.SampleRate, format SampleFormat) error {
|
||||
var audioFormat uint16 = 1 // PCM
|
||||
var bitsPerSample uint16 = 16
|
||||
|
||||
if format == Float32 {
|
||||
audioFormat = 3 // IEEE_FLOAT
|
||||
bitsPerSample = 32
|
||||
}
|
||||
|
||||
numChannels := uint16(2)
|
||||
blockAlign := numChannels * bitsPerSample / 8
|
||||
byteRate := uint32(sampleRate) * uint32(blockAlign)
|
||||
|
||||
header := make([]byte, 44)
|
||||
copy(header[0:4], "RIFF")
|
||||
binary.LittleEndian.PutUint32(header[4:8], 0xFFFFFFFF) // Taille inconnue pour streaming
|
||||
copy(header[8:12], "WAVE")
|
||||
copy(header[12:16], "fmt ")
|
||||
binary.LittleEndian.PutUint32(header[16:20], 16)
|
||||
binary.LittleEndian.PutUint16(header[20:22], audioFormat)
|
||||
binary.LittleEndian.PutUint16(header[22:24], numChannels)
|
||||
binary.LittleEndian.PutUint32(header[24:28], uint32(sampleRate))
|
||||
binary.LittleEndian.PutUint32(header[28:32], byteRate)
|
||||
binary.LittleEndian.PutUint16(header[32:34], blockAlign)
|
||||
binary.LittleEndian.PutUint16(header[34:36], bitsPerSample)
|
||||
copy(header[36:40], "data")
|
||||
binary.LittleEndian.PutUint32(header[40:44], 0xFFFFFFFF) // Taille inconnue pour streaming
|
||||
|
||||
_, err := w.Write(header)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user