212 lines
3.5 KiB
Go
212 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"math"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
sampleRate = 48000
|
|
channels = 2
|
|
bytesPerSample = 2 // Signed 16-bit PCM
|
|
windowDuration = 100 * time.Millisecond
|
|
minimumDBFS = -96.0
|
|
meterWidth = 50
|
|
)
|
|
|
|
func main() {
|
|
for _, program := range []string{"pactl", "parec"} {
|
|
if _, err := exec.LookPath(program); err != nil {
|
|
log.Fatalf("%s was not found in PATH: %v", program, err)
|
|
}
|
|
}
|
|
|
|
ctx, stop := signal.NotifyContext(
|
|
context.Background(),
|
|
os.Interrupt,
|
|
syscall.SIGTERM,
|
|
)
|
|
defer stop()
|
|
|
|
defaultSink, err := commandOutput(
|
|
ctx,
|
|
"pactl",
|
|
"get-default-sink",
|
|
)
|
|
if err != nil {
|
|
log.Fatalf("get default output sink: %v", err)
|
|
}
|
|
|
|
monitorSource := defaultSink + ".monitor"
|
|
|
|
fmt.Printf("Default output: %s\n", defaultSink)
|
|
fmt.Printf("Capturing from: %s\n", monitorSource)
|
|
fmt.Println("Press Ctrl+C to stop.")
|
|
|
|
cmd := exec.CommandContext(
|
|
ctx,
|
|
"parec",
|
|
"--device="+monitorSource,
|
|
"--format=s16le",
|
|
fmt.Sprintf("--rate=%d", sampleRate),
|
|
fmt.Sprintf("--channels=%d", channels),
|
|
"--raw",
|
|
"--latency-msec=50",
|
|
)
|
|
|
|
cmd.Stderr = os.Stderr
|
|
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
log.Fatalf("open parec output: %v", err)
|
|
}
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
log.Fatalf("start parec: %v", err)
|
|
}
|
|
|
|
framesPerWindow := int(
|
|
float64(sampleRate) * windowDuration.Seconds(),
|
|
)
|
|
|
|
buffer := make(
|
|
[]byte,
|
|
framesPerWindow*channels*bytesPerSample,
|
|
)
|
|
|
|
for {
|
|
n, readErr := io.ReadFull(stdout, buffer)
|
|
|
|
if n > 0 {
|
|
rmsDBFS, peakDBFS := measurePCM16LE(buffer[:n])
|
|
|
|
fmt.Printf(
|
|
"\rRMS %6.1f dBFS Peak %6.1f dBFS |%s|",
|
|
rmsDBFS,
|
|
peakDBFS,
|
|
meter(rmsDBFS, -60, 0, meterWidth),
|
|
)
|
|
}
|
|
|
|
if readErr != nil {
|
|
if ctx.Err() != nil ||
|
|
errors.Is(readErr, io.EOF) ||
|
|
errors.Is(readErr, io.ErrUnexpectedEOF) {
|
|
break
|
|
}
|
|
|
|
log.Fatalf("read PCM audio: %v", readErr)
|
|
}
|
|
}
|
|
|
|
fmt.Println()
|
|
|
|
if err := cmd.Wait(); err != nil && ctx.Err() == nil {
|
|
log.Fatalf("parec stopped unexpectedly: %v", err)
|
|
}
|
|
}
|
|
|
|
func commandOutput(
|
|
ctx context.Context,
|
|
name string,
|
|
args ...string,
|
|
) (string, error) {
|
|
output, err := exec.CommandContext(
|
|
ctx,
|
|
name,
|
|
args...,
|
|
).Output()
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return strings.TrimSpace(string(output)), nil
|
|
}
|
|
|
|
func measurePCM16LE(pcm []byte) (
|
|
rmsDBFS float64,
|
|
peakDBFS float64,
|
|
) {
|
|
sampleCount := len(pcm) / bytesPerSample
|
|
|
|
if sampleCount == 0 {
|
|
return minimumDBFS, minimumDBFS
|
|
}
|
|
|
|
var sumSquares float64
|
|
var peak float64
|
|
|
|
for offset := 0; offset+1 < len(pcm); offset += bytesPerSample {
|
|
sample := int16(
|
|
binary.LittleEndian.Uint16(
|
|
pcm[offset : offset+2],
|
|
),
|
|
)
|
|
|
|
value := float64(sample) / 32768.0
|
|
absolute := math.Abs(value)
|
|
|
|
sumSquares += value * value
|
|
|
|
if absolute > peak {
|
|
peak = absolute
|
|
}
|
|
}
|
|
|
|
rms := math.Sqrt(
|
|
sumSquares / float64(sampleCount),
|
|
)
|
|
|
|
return amplitudeToDBFS(rms), amplitudeToDBFS(peak)
|
|
}
|
|
|
|
func amplitudeToDBFS(amplitude float64) float64 {
|
|
if amplitude <= 0 {
|
|
return minimumDBFS
|
|
}
|
|
|
|
db := 20 * math.Log10(amplitude)
|
|
|
|
if db < minimumDBFS {
|
|
return minimumDBFS
|
|
}
|
|
|
|
return db
|
|
}
|
|
|
|
func meter(
|
|
db float64,
|
|
minimum float64,
|
|
maximum float64,
|
|
width int,
|
|
) string {
|
|
ratio := (db - minimum) / (maximum - minimum)
|
|
|
|
if ratio < 0 {
|
|
ratio = 0
|
|
}
|
|
|
|
if ratio > 1 {
|
|
ratio = 1
|
|
}
|
|
|
|
filled := int(
|
|
math.Round(ratio * float64(width)),
|
|
)
|
|
|
|
return strings.Repeat("#", filled) +
|
|
strings.Repeat(" ", width-filled)
|
|
}
|