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 #include 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 #include */ 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 ===============