166 lines
3.0 KiB
Go
166 lines
3.0 KiB
Go
package pipewire
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Output implements the sendspin output.Output interface via pw-cat --playback.
|
|
type Output struct {
|
|
target string
|
|
nodeName string
|
|
|
|
mu sync.Mutex
|
|
cmd *exec.Cmd
|
|
stdin io.WriteCloser
|
|
sampleRate int
|
|
channels int
|
|
bitShift uint
|
|
buf []byte
|
|
|
|
volume float64
|
|
muted bool
|
|
}
|
|
|
|
func NewOutput(target, nodeName string) *Output {
|
|
return &Output{
|
|
target: target,
|
|
nodeName: nodeName,
|
|
volume: 1.0,
|
|
}
|
|
}
|
|
|
|
func (o *Output) Open(sampleRate, channels, bitDepth int) error {
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
o.sampleRate = sampleRate
|
|
o.channels = channels
|
|
// Samples from sendspin are right-justified in int32; shift to fill
|
|
// the full 32-bit range so pw-cat receives proper amplitude.
|
|
if bitDepth > 0 && bitDepth < 32 {
|
|
o.bitShift = uint(32 - bitDepth)
|
|
} else {
|
|
o.bitShift = 0
|
|
}
|
|
o.killUnlocked()
|
|
return o.startUnlocked()
|
|
}
|
|
|
|
func (o *Output) startUnlocked() error {
|
|
nameJSON, _ := json.Marshal(o.nodeName)
|
|
props := fmt.Sprintf(`{"node.name":%s}`, nameJSON)
|
|
|
|
args := []string{
|
|
"--playback",
|
|
"--raw",
|
|
"--format", "s32",
|
|
"--rate", strconv.Itoa(o.sampleRate),
|
|
"--channels", strconv.Itoa(o.channels),
|
|
"--properties", props,
|
|
}
|
|
if o.target != "" {
|
|
args = append(args, "--target", o.target)
|
|
}
|
|
args = append(args, "-")
|
|
|
|
cmd := exec.Command("pw-cat", args...)
|
|
|
|
stderr, _ := cmd.StderrPipe()
|
|
stdin, err := cmd.StdinPipe()
|
|
if err != nil {
|
|
return fmt.Errorf("pw-cat stdin: %w", err)
|
|
}
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("pw-cat start: %w", err)
|
|
}
|
|
|
|
go func() {
|
|
b, _ := io.ReadAll(stderr)
|
|
if s := strings.TrimSpace(string(b)); s != "" {
|
|
log.Printf("pw-cat stderr: %s", s)
|
|
}
|
|
}()
|
|
|
|
o.cmd = cmd
|
|
o.stdin = stdin
|
|
if o.buf == nil {
|
|
o.buf = make([]byte, 0, 4096)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (o *Output) killUnlocked() {
|
|
if o.stdin != nil {
|
|
o.stdin.Close()
|
|
o.stdin = nil
|
|
}
|
|
if o.cmd != nil && o.cmd.Process != nil {
|
|
o.cmd.Process.Kill()
|
|
o.cmd.Wait()
|
|
o.cmd = nil
|
|
}
|
|
}
|
|
|
|
func (o *Output) Write(samples []int32) error {
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
if o.stdin == nil {
|
|
return fmt.Errorf("output not opened")
|
|
}
|
|
|
|
need := len(samples) * 4
|
|
if cap(o.buf) < need {
|
|
o.buf = make([]byte, need)
|
|
}
|
|
o.buf = o.buf[:need]
|
|
|
|
for i, s := range samples {
|
|
if o.muted {
|
|
s = 0
|
|
} else if o.volume != 1.0 {
|
|
s = int32(float64(s) * o.volume)
|
|
}
|
|
s <<= o.bitShift
|
|
binary.LittleEndian.PutUint32(o.buf[i*4:], uint32(s))
|
|
}
|
|
|
|
_, err := o.stdin.Write(o.buf)
|
|
if err != nil {
|
|
log.Printf("pw-cat pipe broken (%v), restarting", err)
|
|
o.killUnlocked()
|
|
if rerr := o.startUnlocked(); rerr != nil {
|
|
return fmt.Errorf("pw-cat restart: %w", rerr)
|
|
}
|
|
_, err = o.stdin.Write(o.buf)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (o *Output) SetVolume(volume int) {
|
|
o.mu.Lock()
|
|
o.volume = float64(volume) / 100.0
|
|
o.mu.Unlock()
|
|
}
|
|
|
|
func (o *Output) SetMuted(muted bool) {
|
|
o.mu.Lock()
|
|
o.muted = muted
|
|
o.mu.Unlock()
|
|
}
|
|
|
|
func (o *Output) Close() error {
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
o.killUnlocked()
|
|
return nil
|
|
}
|