1328 lines
28 KiB
Plaintext
1328 lines
28 KiB
Plaintext
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. L’OBJECTIF 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 ===============
|