Intégration de libsoxr pour le rééchantillonnage audio

This commit is contained in:
2025-09-09 21:15:53 +02:00
parent 8770725cfa
commit be2792b27d
152 changed files with 17834 additions and 346 deletions

60
pmostream/audio.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}