After commit c018f5fd29 Fenrir is being switched over to go. If you do not want to be part of the testing process, please switch to master.
This commit is contained in:
1 parent
c018f5fd29
commit
0e50de4c76
1047 files changed
+58364
-63747
No files matched your search
@@ -0,0 +1,137 @@
|
||||
// Fenrir console screen reader.
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package sound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/jfreymuth/pulse"
|
||||
)
|
||||
|
||||
const audioPeriod = 5 * time.Millisecond
|
||||
const pcmTimeout = 500 * time.Millisecond
|
||||
|
||||
type pcmOutput interface {
|
||||
write([]float32) (int, error)
|
||||
prepare() error
|
||||
drain() error
|
||||
close()
|
||||
}
|
||||
|
||||
func (s *Service) start_pcm(pcm pcmOutput) {
|
||||
drained := make(chan error, 1)
|
||||
s.closeBackend = pcm.close
|
||||
s.drainBackend = func(ctx context.Context) error {
|
||||
select {
|
||||
case err := <-drained:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-s.done:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
s.workers.Go(func() {
|
||||
err := s.run_pcm(pcm)
|
||||
drained <- err
|
||||
if err != nil && !s.closed.Load() {
|
||||
select {
|
||||
case s.errors <- fmt.Errorf("ALSA playback: %w", err):
|
||||
default:
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) run_pcm(pcm pcmOutput) error {
|
||||
// Pace even a virtual PCM such as ALSA's null sink. No busy silence loop,
|
||||
// and the same cancellation and cue priority apply to both audio drivers.
|
||||
tick := time.NewTicker(audioPeriod)
|
||||
defer tick.Stop()
|
||||
samples := make([]float32, int(int64(sampleRate*2)*int64(audioPeriod)/int64(time.Second)))
|
||||
for {
|
||||
select {
|
||||
case <-s.done:
|
||||
return nil
|
||||
case <-tick.C:
|
||||
}
|
||||
count, err := s.read_samples(samples)
|
||||
if err == pulse.EndOfData {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), pcmTimeout)
|
||||
defer cancel()
|
||||
return s.drain_pcm(ctx, pcm)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.write_pcm(pcm, samples[:count]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) write_pcm(pcm pcmOutput, samples []float32) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), pcmTimeout)
|
||||
defer cancel()
|
||||
for len(samples) > 0 {
|
||||
select {
|
||||
case <-s.done:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("PCM write stalled: %w", ctx.Err())
|
||||
default:
|
||||
}
|
||||
count, err := pcm.write(samples)
|
||||
if count < 0 || count > len(samples) || count%2 != 0 {
|
||||
return fmt.Errorf("invalid PCM sample count %d", count)
|
||||
}
|
||||
samples = samples[count:]
|
||||
switch {
|
||||
case errors.Is(err, syscall.EPIPE), errors.Is(err, syscall.ESTRPIPE):
|
||||
// prepare drops the broken buffer without snd_pcm_resume/recover's
|
||||
// potentially blocking suspend retry loop. Bound repeated failures.
|
||||
if err := pcm.prepare(); err != nil {
|
||||
return err
|
||||
}
|
||||
case err != nil && !errors.Is(err, syscall.EAGAIN) && !errors.Is(err, syscall.EINTR):
|
||||
return err
|
||||
case err == nil && count > 0:
|
||||
continue
|
||||
}
|
||||
if err := s.wait_pcm(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) drain_pcm(ctx context.Context, pcm pcmOutput) error {
|
||||
for {
|
||||
err := pcm.drain()
|
||||
if !errors.Is(err, syscall.EAGAIN) && !errors.Is(err, syscall.EINTR) {
|
||||
return err
|
||||
}
|
||||
if err := s.wait_pcm(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) wait_pcm(ctx context.Context) error {
|
||||
timer := time.NewTimer(audioPeriod)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-s.done:
|
||||
return context.Canceled
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user