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) }