105 lines
2.3 KiB
Go
105 lines
2.3 KiB
Go
package ui
|
|
|
|
import (
|
|
"math"
|
|
"strings"
|
|
)
|
|
|
|
// renderHeartbeat draws a scrolling ECG/pulse-monitor trace using Braille dots.
|
|
// Bass energy triggers sharp QRS-complex spikes; silence produces a flat line.
|
|
// The trace scrolls left each frame for the classic hospital-monitor look.
|
|
func (v *Visualizer) renderHeartbeat() string {
|
|
height := v.Rows
|
|
dotRows := height * 4
|
|
dotCols := PanelWidth * 2
|
|
|
|
samples := v.waveBuf
|
|
n := len(samples)
|
|
|
|
// Build a y-position for each dot column from raw audio.
|
|
ypos := make([]int, dotCols)
|
|
centerY := float64(dotRows) / 2.0
|
|
amplitude := float64(dotRows) * 0.45
|
|
|
|
for x := range dotCols {
|
|
var sample float64
|
|
if n > 0 {
|
|
idx := x * n / dotCols
|
|
if idx >= n {
|
|
idx = n - 1
|
|
}
|
|
sample = samples[idx]
|
|
}
|
|
|
|
// Shape the waveform like an ECG trace: sharpen peaks, flatten noise.
|
|
shaped := sample * math.Abs(sample) // square the magnitude, keep sign
|
|
y := int(centerY - shaped*amplitude)
|
|
ypos[x] = max(0, min(dotRows-1, y))
|
|
}
|
|
|
|
grid := make([]bool, dotRows*dotCols)
|
|
|
|
// Draw the ECG trace with continuous line connections.
|
|
for x := range dotCols {
|
|
y := ypos[x]
|
|
grid[y*dotCols+x] = true
|
|
if x > 0 {
|
|
lo, hi := min(y, ypos[x-1]), max(y, ypos[x-1])
|
|
for fy := lo; fy <= hi; fy++ {
|
|
grid[fy*dotCols+x] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw a faint baseline at center.
|
|
baseY := dotRows / 2
|
|
for x := range dotCols {
|
|
if !grid[baseY*dotCols+x] {
|
|
// Dashed baseline: on for 6, off for 4.
|
|
if (x/6)%2 == 0 {
|
|
grid[baseY*dotCols+x] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Render braille characters.
|
|
lines := make([]string, height)
|
|
for row := range height {
|
|
var sb, run strings.Builder
|
|
tag := -1
|
|
|
|
for ch := range PanelWidth {
|
|
var braille rune = '\u2800'
|
|
hasTrace := false
|
|
|
|
for dr := range 4 {
|
|
for dc := range 2 {
|
|
dy := row*4 + dr
|
|
dx := ch*2 + dc
|
|
if grid[dy*dotCols+dx] {
|
|
braille |= brailleBit[dr][dc]
|
|
// Check if this is part of the trace (not baseline).
|
|
if dy != baseY {
|
|
hasTrace = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
newTag := 0 // green baseline
|
|
if hasTrace {
|
|
newTag = 2 // red trace
|
|
}
|
|
if newTag != tag {
|
|
flushStyleRun(&sb, &run, tag)
|
|
tag = newTag
|
|
}
|
|
run.WriteRune(braille)
|
|
}
|
|
flushStyleRun(&sb, &run, tag)
|
|
lines[row] = sb.String()
|
|
}
|
|
|
|
return strings.Join(lines, "\n")
|
|
}
|