Files
go-ts/pkg/audio/level.go
2026-01-17 16:41:17 +01:00

138 lines
2.4 KiB
Go

package audio
import (
"math"
)
// CalculateRMSLevel calculates the RMS level of PCM samples and returns 0-100 (Logarithmic/dB)
func CalculateRMSLevel(samples []int16) int {
if len(samples) == 0 {
return 0
}
var sum float64
for _, s := range samples {
sum += float64(s) * float64(s)
}
rms := math.Sqrt(sum / float64(len(samples)))
// Normalize to 0.0 - 1.0
val := rms / 32768.0
if val < 0.000001 { // Avoid log(0)
return 0
}
// Convert to dB
db := 20 * math.Log10(val)
// Map -50dB (silence floor) to 0dB (max) to 0-100
const minDB = -50.0
if db < minDB {
return 0
}
// Scale
level := int((db - minDB) * (100.0 / (0 - minDB)))
if level > 100 {
level = 100
}
return level
}
// CalculatePeakLevel returns the peak level of PCM samples as 0-100 (Logarithmic/dB)
func CalculatePeakLevel(samples []int16) int {
if len(samples) == 0 {
return 0
}
var peak int16
for _, s := range samples {
if s < 0 {
s = -s
}
if s > peak {
peak = s
}
}
// Normalize
val := float64(peak) / 32768.0
if val < 0.000001 {
return 0
}
db := 20 * math.Log10(val)
const minDB = -50.0
if db < minDB {
// Linear falloff for very low signals to avoid clutter
return 0
}
level := int((db - minDB) * (100.0 / (0 - minDB)))
if level > 100 {
level = 100
}
return level
}
// LevelToBar converts a 0-100 level to a visual bar string
func LevelToBar(level, width int) string {
if level < 0 {
level = 0
}
if level > 100 {
level = 100
}
filled := level * width / 100
empty := width - filled
bar := ""
for i := 0; i < filled; i++ {
bar += "█"
}
for i := 0; i < empty; i++ {
bar += "░"
}
return bar
}
// LevelToMeter converts a 0-100 level to a visual VU meter with varying heights
func LevelToMeter(level, width int) string {
if level < 0 {
level = 0
}
if level > 100 {
level = 100
}
// Use block characters of varying heights
blocks := []rune{'░', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
meter := ""
for i := 0; i < width; i++ {
// Each position represents a portion of the level
threshold := (i + 1) * 100 / width
if level >= threshold {
meter += string(blocks[8]) // Full
} else if level >= threshold-10 {
// Partial - calculate which block to use
partial := (level - (threshold - 10)) * 8 / 10
if partial < 0 {
partial = 0
}
meter += string(blocks[partial])
} else {
meter += string(blocks[0]) // Empty
}
}
return meter
}