Files
pmomusic/all.txt

1328 lines
28 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Et bien maintenant sans modifier aucune ligne du code que je te fournis ci dessous, développe la collection la plus complète possible de tests unitaire pour ces deux packages
IGNORE TOUTES LES INFÉRENCES. NE TRAVAILLE QUE SUR LE CODE FOURNI. LOBJECTIF EST QUE LES TEST COMPILENT ET PASSENT.
============== Debut des sources des packages ===============
------- pmosoxr/concurency_test.go ------
//go:build cgo
// +build cgo
package pmosoxr
import (
"math"
"sync"
"testing"
"time"
)
// genStereoSine génère nFrames frames interlacées float32 (L,R identiques) à amplitude 0.2.
func genStereoSine(nFrames int, freq float64, sr float64) []float32 {
out := make([]float32, nFrames*2)
for i := 0; i < nFrames; i++ {
v := float32(0.2 * math.Sin(2*math.Pi*freq*float64(i)/sr))
out[2*i] = v
out[2*i+1] = v
}
return out
}
func TestResamplerConcurrentProcess(t *testing.T) {
inRate := 44100.0
outRate := 48000.0
channels := 2
quality := MQ
r, err := New(inRate, outRate, channels, quality)
if err != nil {
t.Fatalf("failed to create resampler: %v", err)
}
defer r.Delete()
workers := 8
iterations := 200
framesPerIter := 256
var wg sync.WaitGroup
errCh := make(chan error, workers)
totalProduced := int64(0)
var prodMu sync.Mutex
for w := 0; w < workers; w++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for i := 0; i < iterations; i++ {
in := genStereoSine(framesPerIter, 440.0+float64(id), inRate)
ratio := outRate / inRate
outFrames := int(float64(framesPerIter)*ratio) + 64
out := make([]float32, outFrames*2)
consumed, produced, perr := r.Process(in, out)
if perr != nil {
errCh <- perr
return
}
if consumed < 0 || produced < 0 {
errCh <- &testError{"negative sample count"}
return
}
if produced > len(out) {
errCh <- &testError{"produced > out buffer"}
return
}
prodMu.Lock()
totalProduced += int64(produced)
prodMu.Unlock()
time.Sleep(1 * time.Millisecond)
}
}(w)
}
wg.Wait()
close(errCh)
for e := range errCh {
t.Fatalf("resampler error: %v", e)
}
if totalProduced == 0 {
t.Fatal("no samples produced")
}
}
type testError struct {
s string
}
func (e *testError) Error() string { return e.s }
-----------------
------- pmosoxr/quality.go ------
package pmosoxr
/*
#cgo CFLAGS: -I${SRCDIR}/../C/include
#include <soxr.h>
#include <stdlib.h>
static soxr_quality_spec_t q_spec(int q) {
switch(q) {
case 0: return soxr_quality_spec(SOXR_QQ, 0);
case 1: return soxr_quality_spec(SOXR_LQ, 0);
case 2: return soxr_quality_spec(SOXR_MQ, 0);
case 3: return soxr_quality_spec(SOXR_HQ, 0);
case 4: return soxr_quality_spec(SOXR_VHQ, 0);
default: return soxr_quality_spec(SOXR_MQ, 0);
}
}
*/
import "C"
type Quality int
const (
QQ Quality = iota
LQ
MQ
HQ
VHQ
)
func (q Quality) toC() C.soxr_quality_spec_t {
return C.q_spec(C.int(q))
}
-----------------
------- pmosoxr/soxr.go ------
//go:build cgo
// +build cgo
package pmosoxr
/*
#cgo CFLAGS: -I${SRCDIR}/../C/include
#cgo LDFLAGS: ${SRCDIR}/../C/lib/libsoxr.a -lomp
#include <stdlib.h>
#include <soxr.h>
*/
import "C"
import (
"errors"
"sync"
"unsafe"
)
type Resampler struct {
handle C.soxr_t
channels int
mu sync.Mutex
deleted bool
}
func New(inRate, outRate float64, channels int, q Quality) (*Resampler, error) {
if channels != 2 {
return nil, errors.New("only stereo supported")
}
var err C.soxr_error_t
qspec := q.toC()
handle := C.soxr_create(
C.double(inRate),
C.double(outRate),
C.uint(channels),
&err,
nil,
&qspec,
nil,
)
if handle == nil {
if err != nil {
return nil, errors.New(C.GoString(err))
}
return nil, errors.New("soxr_create failed without error message")
}
return &Resampler{handle: handle, channels: channels}, nil
}
func (r *Resampler) Process(in []float32, out []float32) (consumedSamples int, producedSamples int, err error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.deleted {
return 0, 0, errors.New("resampler deleted")
}
if r.handle == nil {
return 0, 0, errors.New("resampler not initialized")
}
if len(in)%r.channels != 0 || len(out)%r.channels != 0 {
return 0, 0, errors.New("buffer size not divisible by channel count")
}
var idone, odone C.size_t
var inPtr C.soxr_in_t
var outPtr C.soxr_out_t
if len(in) > 0 {
inPtr = C.soxr_in_t(unsafe.Pointer(&in[0]))
}
if len(out) > 0 {
outPtr = C.soxr_out_t(unsafe.Pointer(&out[0]))
}
st := C.soxr_process(
r.handle,
inPtr, C.size_t(len(in)/r.channels),
&idone,
outPtr, C.size_t(len(out)/r.channels),
&odone,
)
if st != nil {
return 0, 0, errors.New(C.GoString(st))
}
return int(idone) * r.channels, int(odone) * r.channels, nil
}
func (r *Resampler) Flush(out []float32) (int, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.deleted {
return 0, errors.New("resampler deleted")
}
if r.handle == nil {
return 0, errors.New("resampler not initialized")
}
var odone C.size_t
if len(out) == 0 {
return 0, nil
}
st := C.soxr_process(r.handle, nil, 0, nil,
C.soxr_out_t(unsafe.Pointer(&out[0])), C.size_t(len(out)/r.channels), &odone)
if st != nil {
return 0, errors.New(C.GoString(st))
}
return int(odone) * r.channels, nil
}
func (r *Resampler) Delete() {
r.mu.Lock()
defer r.mu.Unlock()
if !r.deleted && r.handle != nil {
C.soxr_delete(r.handle)
r.handle = nil
r.deleted = true
}
}
-----------------
------- pmostream/audio.go ------
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
}
-----------------
------- pmostream/buffer.go ------
package pmostream
import (
"sync"
"time"
)
// AudioBuffer est un buffer circulaire thread-safe pour l'audio
type AudioBuffer struct {
mu sync.Mutex
cond *sync.Cond
chunks [][]byte
size int
format SampleFormat
readPos int
writePos int
closed bool
}
// NewAudioBuffer crée un nouveau buffer circulaire
func NewAudioBuffer(size int, format SampleFormat) *AudioBuffer {
ab := &AudioBuffer{
chunks: make([][]byte, size),
size: size,
format: format,
}
ab.cond = sync.NewCond(&ab.mu)
return ab
}
// Available retourne le nombre de chunks disponibles pour lecture
func (ab *AudioBuffer) Available() int {
ab.mu.Lock()
defer ab.mu.Unlock()
return ab.availableLocked()
}
// 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.writePos == ab.readPos {
// Écrase le plus ancien
ab.readPos = (ab.readPos + 1) % ab.size
}
ab.cond.Broadcast()
}
// Read lit un chunk du buffer, ou nil si vide ou fermé
func (ab *AudioBuffer) Read() []byte {
ab.mu.Lock()
defer ab.mu.Unlock()
if ab.availableLocked() == 0 {
return nil
}
chunk := ab.chunks[ab.readPos]
ab.chunks[ab.readPos] = nil
ab.readPos = (ab.readPos + 1) % ab.size
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
}
deadline := time.Now().Add(timeout)
for ab.availableLocked() < n && !ab.closed {
if timeout <= 0 {
ab.cond.Wait()
} else {
remaining := time.Until(deadline)
if remaining <= 0 {
return false
}
// Wait avec timeout en utilisant Wait + Broadcast classique
timer := time.NewTimer(remaining)
done := make(chan struct{})
go func() {
ab.cond.Wait()
close(done)
}()
ab.mu.Unlock()
select {
case <-done:
// réveillé par Broadcast
if !timer.Stop() {
<-timer.C
}
case <-timer.C:
ab.mu.Lock()
return false
}
ab.mu.Lock()
}
}
return ab.availableLocked() >= n && !ab.closed
}
// Close ferme le buffer et réveille tous les Waiters
func (ab *AudioBuffer) Close() {
ab.mu.Lock()
defer ab.mu.Unlock()
if !ab.closed {
ab.closed = true
ab.cond.Broadcast()
}
}
// availableLocked retourne le nombre de chunks disponibles, doit être appelé avec ab.mu locké
func (ab *AudioBuffer) availableLocked() int {
if ab.writePos >= ab.readPos {
return ab.writePos - ab.readPos
}
return ab.size - ab.readPos + ab.writePos
}
-----------------
------- pmostream/conversion.go ------
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
}
-----------------
------- pmostream/core.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
}
-----------------
------- pmostream/fork_eq_test.go ------
//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")
}
}
-----------------
------- pmostream/fork.go ------
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
}
-----------------
------- pmostream/parameq.go ------
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)
})
}
-----------------
------- pmostream/streamer.go ------
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
}
-----------------
============== Fin des sources des packages ===============
============== Analyse des bugs par chatgpt ===============