Files
settuings/internal/audio/level.go
T
Hadi 59b31e5ca3 init
Signed-off-by: Hadi <[email protected]>
2026-08-13 00:58:37 +02:00

88 lines
2.1 KiB
Go

package audio
import (
"encoding/binary"
"io"
"math"
"os/exec"
"strconv"
)
// levelSampleRate and levelFrameSamples pick a small chunk size (~100ms) so
// the UI can poll for a fresh peak reading at a responsive rate without
// reading much audio data.
const (
levelSampleRate = 16000
levelFrameFrames = levelSampleRate / 10 // 100ms
levelFrameBytes = levelFrameFrames * 4 // 4 bytes per float32 sample
)
// LevelMonitor streams raw audio from a source and reports its peak
// amplitude in short chunks, for a live input-level meter. Callers must call
// Stop when done to release the underlying pw-cat process.
type LevelMonitor struct {
cmd *exec.Cmd
stdout io.ReadCloser
}
// StartLevelMonitor begins capturing raw audio from sourceID.
func StartLevelMonitor(sourceID string) (*LevelMonitor, error) {
target, err := nodeTarget(sourceID)
if err != nil {
return nil, err
}
cmd := exec.Command("pw-cat", // #nosec G204 -- target is a node.name read back from `wpctl inspect`, never shell-interpreted
"--record",
"--target", target,
"--format", "f32",
"--rate", strconv.Itoa(levelSampleRate),
"--channels", "1",
"--raw",
"-",
)
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
if err := cmd.Start(); err != nil {
return nil, err
}
return &LevelMonitor{cmd: cmd, stdout: stdout}, nil
}
// Next blocks until one ~100ms chunk of audio has been captured and returns
// its peak amplitude as a value in [0, 1]. It returns an error once the
// monitor is stopped or the source disappears.
func (l *LevelMonitor) Next() (float64, error) {
buf := make([]byte, levelFrameBytes)
n, err := io.ReadFull(l.stdout, buf)
if n == 0 {
return 0, err
}
var peak float32
for i := 0; i+4 <= n; i += 4 {
v := math.Float32frombits(binary.LittleEndian.Uint32(buf[i : i+4]))
if v < 0 {
v = -v
}
if v > peak {
peak = v
}
}
if peak > 1 {
peak = 1
}
return float64(peak), nil
}
// Stop terminates the capture process. Safe to call more than once.
func (l *LevelMonitor) Stop() {
if l.cmd.Process != nil {
_ = l.cmd.Process.Kill()
}
_ = l.cmd.Wait()
}