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:
Storm Dragon committed 2026-10-27 20:30:55 -04:00
1 parent c018f5fd29
commit 0e50de4c76
1047 files changed
+58364 -63747

No files matched your search

+137
View File
@@ -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
}
}