🎉 live server seems to be working now
This commit is contained in:
165
internal/pipewire/output.go
Normal file
165
internal/pipewire/output.go
Normal file
@@ -0,0 +1,165 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user