// Fenrir console screen reader. // Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later // // This program is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation, either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for // more details. // // You should have received a copy of the GNU General Public License along with // this program. If not, see . package sound import ( "context" "fmt" "os" "time" "github.com/jfreymuth/pulse" "github.com/jfreymuth/pulse/proto" ) func open_pulse(s *Service, _ string) error { options := []pulse.ClientOption{pulse.ClientApplicationName("Fenrir sounds"), pulse.ClientTimeout(500 * time.Millisecond)} // Try the invoking user's session, then the bundled PipeWire helper's // socket and the system-wide service, unless PULSE_SERVER selects a server. options = append(options, pulse.ClientServerString(Server(os.Getenv("PULSE_SERVER"), os.Getenv("SUDO_UID")))) client, err := pulse.NewClient(options...) if err != nil { return fmt.Errorf("connect sound server: %w", err) } drainRequests := make(chan *notificationPlayback, 1) read := func(samples []float32) (int, error) { return s.read_pulse_samples(samples, drainRequests) } stream, err := client.NewPlayback(pulse.Float32Reader(read), pulse.PlaybackStereo, pulse.PlaybackSampleRate(sampleRate), pulse.PlaybackLatency(0.02), pulse.PlaybackMediaName("Fenrir sound cues")) if err != nil { client.Close() return err } s.closeBackend = func() { stream.Close(); client.Close() } // Drain even after the final reader callback reports EndOfData. The // library's Drain helper skips idle streams, whose buffered tail may // still be audible on the server. s.drainBackend = func(_ context.Context) error { return client.RawRequest(&proto.DrainPlaybackStream{StreamIndex: stream.StreamIndex()}, nil) } stream.Start() s.workers.Go(func() { tick := time.NewTicker(time.Second) defer tick.Stop() for { select { case <-s.done: return case request := <-drainRequests: if err := s.drain_notification(request, s.drainBackend); err != nil { s.errors <- fmt.Errorf("notification sound drain: %w", err) return } case <-tick.C: if stream.Closed() { s.errors <- fmt.Errorf("sound server connection closed") return } } } }) return nil }