mirror of
https://github.com/anotherhadi/settuings.git
synced 2026-10-05 16:18:24 +02:00
@@ -0,0 +1,87 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user