Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bee679d3e1 | ||
|
|
0660130653 | ||
| ff639015c2 | |||
| 5d969888fc |
34
README.md
34
README.md
@@ -57,22 +57,30 @@ sudo ln -s $(pwd)/osc /usr/local/bin/osc
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Edit the constants at the top of `src/osc.nim` or `src/osc_braille.nim`:
|
Runtime options can be passed via CLI flags:
|
||||||
|
|
||||||
```nim
|
```bash
|
||||||
const
|
./osc --palette:amber --decay:0.92 --beam:0.6 --bloom:0.12
|
||||||
# Phosphor physics
|
./osc_braille --palette:green --hot:0.8 --warm:0.45 --cool:0.12
|
||||||
Decay = 0.85 # persistence per frame (0.0–1.0)
|
```
|
||||||
Beam = 0.4 # intensity at beam impact
|
|
||||||
Bloom = 0.08 # horizontal glow spread
|
|
||||||
|
|
||||||
# Phosphor glow thresholds
|
Available options:
|
||||||
HotGlow = 0.7 # white-hot beam core
|
|
||||||
WarmGlow = 0.4 # bright phosphor
|
|
||||||
CoolGlow = 0.15 # dim persistence trail
|
|
||||||
|
|
||||||
# Palette: green, amber, cyan, blue, white, red
|
| Option | Description | Default |
|
||||||
Palette = "green"
|
|--------|-------------|---------|
|
||||||
|
| `-p`, `--palette:NAME` | Palette name | `green` |
|
||||||
|
| `-d`, `--decay:FLOAT` | Phosphor persistence per frame | `0.85` |
|
||||||
|
| `--beam:FLOAT` | Beam impact intensity | `0.4` |
|
||||||
|
| `--bloom:FLOAT` | Horizontal glow spread | `0.08` |
|
||||||
|
| `--hot:FLOAT` | White-hot beam core threshold | `0.7` |
|
||||||
|
| `--warm:FLOAT` | Bright phosphor threshold | `0.4` |
|
||||||
|
| `--cool:FLOAT` | Dim persistence trail threshold | `0.15` |
|
||||||
|
|
||||||
|
Show help:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./osc --help
|
||||||
|
./osc_braille --help
|
||||||
```
|
```
|
||||||
|
|
||||||
### Palettes
|
### Palettes
|
||||||
|
|||||||
228
src/osc.nim
228
src/osc.nim
@@ -1,28 +1,83 @@
|
|||||||
## Terminal oscilloscope with CRT phosphor physics.
|
## Terminal oscilloscope with CRT phosphor physics.
|
||||||
## Zero dependencies beyond Nim stdlib + libav (dlopen at runtime).
|
## Zero dependencies beyond Nim stdlib + libav (dlopen at runtime).
|
||||||
|
|
||||||
import os, strutils
|
import os, strutils, parseopt
|
||||||
import posix/termios as ptermios
|
import posix/termios as ptermios
|
||||||
from posix import read
|
from posix import read
|
||||||
import osc/canvas/[term, effects]
|
import osc/canvas/[term, effects]
|
||||||
import osc/[scope, audio]
|
import osc/[scope, audio]
|
||||||
|
|
||||||
# ── Configuration ────────────────────────────────────────────────────
|
# ── Configuration ────────────────────────────────────────────────────
|
||||||
# Edit these to tune the look.
|
|
||||||
|
|
||||||
const
|
type Config = object
|
||||||
# Phosphor physics
|
decay: float
|
||||||
Decay = 0.85 # persistence per frame (0.0–1.0)
|
beam: float
|
||||||
Beam = 0.4 # intensity at beam impact
|
bloom: float
|
||||||
Bloom = 0.08 # horizontal glow spread
|
hotGlow: float
|
||||||
|
warmGlow: float
|
||||||
|
coolGlow: float
|
||||||
|
palette: string
|
||||||
|
|
||||||
# Phosphor glow thresholds
|
proc defaultConfig(): Config =
|
||||||
HotGlow = 0.7 # white-hot beam core
|
Config(
|
||||||
WarmGlow = 0.4 # bright phosphor
|
decay: 0.85,
|
||||||
CoolGlow = 0.15 # dim persistence trail
|
beam: 0.4,
|
||||||
|
bloom: 0.08,
|
||||||
|
hotGlow: 0.7,
|
||||||
|
warmGlow: 0.4,
|
||||||
|
coolGlow: 0.15,
|
||||||
|
palette: "green"
|
||||||
|
)
|
||||||
|
|
||||||
# Palette: green, amber, cyan, blue, white, red
|
proc usage() =
|
||||||
Palette = "green"
|
echo """
|
||||||
|
Terminal oscilloscope
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-p, --palette:NAME Palette name, default: green
|
||||||
|
-d, --decay:FLOAT Phosphor decay, default: 0.85
|
||||||
|
--beam:FLOAT Beam intensity, default: 0.4
|
||||||
|
--bloom:FLOAT Bloom intensity, default: 0.08
|
||||||
|
--hot:FLOAT Hot glow, default: 0.7
|
||||||
|
--warm:FLOAT Warm glow, default: 0.4
|
||||||
|
--cool:FLOAT Cool glow, default: 0.15
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
Palettes:
|
||||||
|
green, amber, cyan, blue, white, red
|
||||||
|
|
||||||
|
Example:
|
||||||
|
./osc --palette:amber --decay:0.92 --beam:0.6 --bloom:0.12
|
||||||
|
"""
|
||||||
|
|
||||||
|
proc parseConfig(): Config =
|
||||||
|
result = defaultConfig()
|
||||||
|
|
||||||
|
for kind, key, val in getopt():
|
||||||
|
case kind
|
||||||
|
of cmdLongOption, cmdShortOption:
|
||||||
|
case key
|
||||||
|
of "h", "help":
|
||||||
|
usage()
|
||||||
|
quit 0
|
||||||
|
of "palette", "p":
|
||||||
|
result.palette = val
|
||||||
|
of "decay", "d":
|
||||||
|
result.decay = parseFloat(val)
|
||||||
|
of "beam":
|
||||||
|
result.beam = parseFloat(val)
|
||||||
|
of "bloom":
|
||||||
|
result.bloom = parseFloat(val)
|
||||||
|
of "hot":
|
||||||
|
result.hotGlow = parseFloat(val)
|
||||||
|
of "warm":
|
||||||
|
result.warmGlow = parseFloat(val)
|
||||||
|
of "cool":
|
||||||
|
result.coolGlow = parseFloat(val)
|
||||||
|
else:
|
||||||
|
quit "Unknown option: " & key
|
||||||
|
else:
|
||||||
|
discard
|
||||||
|
|
||||||
# ── Audio thread via Channel ─────────────────────────────────────────
|
# ── Audio thread via Channel ─────────────────────────────────────────
|
||||||
|
|
||||||
@@ -66,94 +121,171 @@ proc readKey(): char =
|
|||||||
|
|
||||||
# ── Phosphor ─────────────────────────────────────────────────────────
|
# ── Phosphor ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
proc plotDot(c: var Canvas, fx, fy: float) =
|
proc plotDot(c: var Canvas, fx, fy: float, cfg: Config) =
|
||||||
let x = int(fx); let y = int(fy)
|
let x = int(fx)
|
||||||
c.addPixel(x, y, Beam)
|
let y = int(fy)
|
||||||
c.addPixel(x - 1, y, Bloom)
|
|
||||||
c.addPixel(x + 1, y, Bloom)
|
|
||||||
|
|
||||||
proc plotLine(c: var Canvas, x0, y0, x1, y1: float) =
|
c.addPixel(x, y, cfg.beam)
|
||||||
|
c.addPixel(x - 1, y, cfg.bloom)
|
||||||
|
c.addPixel(x + 1, y, cfg.bloom)
|
||||||
|
|
||||||
|
proc plotLine(
|
||||||
|
c: var Canvas,
|
||||||
|
x0, y0, x1, y1: float,
|
||||||
|
cfg: Config
|
||||||
|
) =
|
||||||
let steps = max(int(max(abs(x1 - x0), abs(y1 - y0))), 1)
|
let steps = max(int(max(abs(x1 - x0), abs(y1 - y0))), 1)
|
||||||
|
|
||||||
for i in 0..steps:
|
for i in 0..steps:
|
||||||
let t = i.float / steps.float
|
let t = i.float / steps.float
|
||||||
c.plotDot(x0 + (x1 - x0) * t, y0 + (y1 - y0) * t)
|
|
||||||
|
|
||||||
proc renderTrace(c: var Canvas, scope: Scope) =
|
c.plotDot(
|
||||||
if scope.sampleCount < 2: return
|
x0 + (x1 - x0) * t,
|
||||||
let w = c.pixW; let h = c.pixH
|
y0 + (y1 - y0) * t,
|
||||||
let cy = h.float / 2.0; let gain = scope.gain
|
cfg
|
||||||
|
)
|
||||||
|
|
||||||
|
proc renderTrace(c: var Canvas, scope: Scope, cfg: Config) =
|
||||||
|
if scope.sampleCount < 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
let w = c.pixW
|
||||||
|
let h = c.pixH
|
||||||
|
let cy = h.float / 2.0
|
||||||
|
let gain = scope.gain
|
||||||
|
|
||||||
case scope.mode
|
case scope.mode
|
||||||
of ModeYT:
|
of ModeYT:
|
||||||
let visible = max(int(scope.sampleCount.float / scope.timeDiv), 2)
|
let visible = max(int(scope.sampleCount.float / scope.timeDiv), 2)
|
||||||
var px, py: float; var first = true
|
|
||||||
|
var px, py: float
|
||||||
|
var first = true
|
||||||
|
|
||||||
for col in 0..<w:
|
for col in 0..<w:
|
||||||
let s = min((col * visible) div w, scope.sampleCount - 1)
|
let s = min((col * visible) div w, scope.sampleCount - 1)
|
||||||
let x = col.float; let y = cy - scope.samplesL[s] * gain * cy * 0.5
|
let x = col.float
|
||||||
if first: c.plotDot(x, y) else: c.plotLine(px, py, x, y)
|
let y = cy - scope.samplesL[s] * gain * cy * 0.5
|
||||||
first = false; px = x; py = y
|
|
||||||
|
if first:
|
||||||
|
c.plotDot(x, y, cfg)
|
||||||
|
else:
|
||||||
|
c.plotLine(px, py, x, y, cfg)
|
||||||
|
|
||||||
|
first = false
|
||||||
|
px = x
|
||||||
|
py = y
|
||||||
|
|
||||||
of ModeXY:
|
of ModeXY:
|
||||||
var px, py: float; var first = true
|
var px, py: float
|
||||||
|
var first = true
|
||||||
|
|
||||||
let step = max(scope.sampleCount div 1024, 1)
|
let step = max(scope.sampleCount div 1024, 1)
|
||||||
|
|
||||||
for i in countup(0, scope.sampleCount - 1, step):
|
for i in countup(0, scope.sampleCount - 1, step):
|
||||||
let x = (1.0 + scope.samplesL[i] * gain * 0.5) * w.float / 2.0
|
let x = (1.0 + scope.samplesL[i] * gain * 0.5) * w.float / 2.0
|
||||||
let y = (1.0 - scope.samplesR[i] * gain * 0.5) * h.float / 2.0
|
let y = (1.0 - scope.samplesR[i] * gain * 0.5) * h.float / 2.0
|
||||||
if first: c.plotDot(x, y) else: c.plotLine(px, py, x, y)
|
|
||||||
first = false; px = x; py = y
|
if first:
|
||||||
|
c.plotDot(x, y, cfg)
|
||||||
|
else:
|
||||||
|
c.plotLine(px, py, x, y, cfg)
|
||||||
|
|
||||||
|
first = false
|
||||||
|
px = x
|
||||||
|
py = y
|
||||||
|
|
||||||
# ── Main ─────────────────────────────────────────────────────────────
|
# ── Main ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
proc main() =
|
proc main() =
|
||||||
|
let cfg = parseConfig()
|
||||||
|
|
||||||
initTerm()
|
initTerm()
|
||||||
setRawMode()
|
setRawMode()
|
||||||
|
|
||||||
var w = termWidth(); var h = termHeight()
|
var w = termWidth()
|
||||||
var c = newCanvas(w, h, Palette, [HotGlow, WarmGlow, CoolGlow])
|
var h = termHeight()
|
||||||
|
|
||||||
|
var c = newCanvas(
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
cfg.palette,
|
||||||
|
[cfg.hotGlow, cfg.warmGlow, cfg.coolGlow]
|
||||||
|
)
|
||||||
|
|
||||||
crtTurnOn(c)
|
crtTurnOn(c)
|
||||||
|
|
||||||
var scope = initScope(w, h)
|
var scope = initScope(w, h)
|
||||||
var aud = startAudio()
|
var aud = startAudio()
|
||||||
var running = true
|
var running = true
|
||||||
|
|
||||||
audioChan.open()
|
audioChan.open()
|
||||||
audioRunning = true
|
audioRunning = true
|
||||||
|
|
||||||
var aThread: Thread[ptr AudioCapture]
|
var aThread: Thread[ptr AudioCapture]
|
||||||
createThread(aThread, audioThread, addr aud)
|
createThread(aThread, audioThread, addr aud)
|
||||||
|
|
||||||
while running:
|
while running:
|
||||||
let nw = termWidth(); let nh = termHeight()
|
let nw = termWidth()
|
||||||
|
let nh = termHeight()
|
||||||
|
|
||||||
if nw != w or nh != h:
|
if nw != w or nh != h:
|
||||||
w = nw; h = nh; c.resize(w, h); scope.resize(w, h)
|
w = nw
|
||||||
|
h = nh
|
||||||
|
c.resize(w, h)
|
||||||
|
scope.resize(w, h)
|
||||||
|
|
||||||
let got = audioChan.tryRecv()
|
let got = audioChan.tryRecv()
|
||||||
|
|
||||||
if got.dataAvailable:
|
if got.dataAvailable:
|
||||||
scope.sampleCount = got.msg.count
|
scope.sampleCount = got.msg.count
|
||||||
|
|
||||||
for i in 0..<got.msg.count:
|
for i in 0..<got.msg.count:
|
||||||
scope.samplesL[i] = got.msg.samples[i][0]
|
scope.samplesL[i] = got.msg.samples[i][0]
|
||||||
scope.samplesR[i] = got.msg.samples[i][1]
|
scope.samplesR[i] = got.msg.samples[i][1]
|
||||||
|
|
||||||
c.decayPixels(Decay)
|
c.decayPixels(cfg.decay)
|
||||||
c.renderTrace(scope)
|
c.renderTrace(scope, cfg)
|
||||||
|
|
||||||
|
let hud =
|
||||||
|
" " &
|
||||||
|
(if scope.mode == ModeYT: "Y-T" else: "X-Y") &
|
||||||
|
" G:" &
|
||||||
|
scope.gain.formatFloat(ffDecimal, 1) &
|
||||||
|
" "
|
||||||
|
|
||||||
let hud = " " & (if scope.mode == ModeYT: "Y-T" else: "X-Y") &
|
|
||||||
" G:" & scope.gain.formatFloat(ffDecimal, 1) & " "
|
|
||||||
let help = " m:mode +/-:gain [/]:time q:quit "
|
let help = " m:mode +/-:gain [/]:time q:quit "
|
||||||
c.flush([(1, 0, tNormal, hud),
|
|
||||||
(w - help.len - 1, h - 1, tDim, help)])
|
c.flush([
|
||||||
|
(1, 0, tNormal, hud),
|
||||||
|
(w - help.len - 1, h - 1, tDim, help)
|
||||||
|
])
|
||||||
|
|
||||||
sleep(16)
|
sleep(16)
|
||||||
|
|
||||||
case readKey()
|
case readKey()
|
||||||
of 'q', '\x1b': running = false
|
of 'q', '\x1b':
|
||||||
of 'm': scope.mode = if scope.mode == ModeYT: ModeXY else: ModeYT
|
running = false
|
||||||
of '+', '=': scope.gain = min(scope.gain * 1.3, 20.0)
|
of 'm':
|
||||||
of '-': scope.gain = max(scope.gain / 1.3, 0.5)
|
scope.mode =
|
||||||
of ']': scope.timeDiv = min(scope.timeDiv * 1.5, 16.0)
|
if scope.mode == ModeYT:
|
||||||
of '[': scope.timeDiv = max(scope.timeDiv / 1.5, 0.25)
|
ModeXY
|
||||||
else: discard
|
else:
|
||||||
|
ModeYT
|
||||||
|
of '+', '=':
|
||||||
|
scope.gain = min(scope.gain * 1.3, 20.0)
|
||||||
|
of '-':
|
||||||
|
scope.gain = max(scope.gain / 1.3, 0.5)
|
||||||
|
of ']':
|
||||||
|
scope.timeDiv = min(scope.timeDiv * 1.5, 16.0)
|
||||||
|
of '[':
|
||||||
|
scope.timeDiv = max(scope.timeDiv / 1.5, 0.25)
|
||||||
|
else:
|
||||||
|
discard
|
||||||
|
|
||||||
audioRunning = false
|
audioRunning = false
|
||||||
joinThread(aThread)
|
joinThread(aThread)
|
||||||
audioChan.close()
|
audioChan.close()
|
||||||
|
|
||||||
aud.stop()
|
aud.stop()
|
||||||
crtTurnOff(c)
|
crtTurnOff(c)
|
||||||
restoreMode()
|
restoreMode()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
## Terminal oscilloscope — braille dot rendering (4× resolution).
|
## Terminal oscilloscope — braille dot rendering (4× resolution).
|
||||||
|
|
||||||
import os, strutils
|
import os, strutils, parseopt
|
||||||
import posix/termios as ptermios
|
import posix/termios as ptermios
|
||||||
from posix import read
|
from posix import read
|
||||||
import osc/canvas/[braille, term, effects]
|
import osc/canvas/[braille, term, effects]
|
||||||
@@ -8,14 +8,72 @@ import osc/[scope, audio]
|
|||||||
|
|
||||||
# ── Configuration ────────────────────────────────────────────────────
|
# ── Configuration ────────────────────────────────────────────────────
|
||||||
|
|
||||||
const
|
type Config = object
|
||||||
Decay = 0.85
|
decay: float
|
||||||
Beam = 0.4
|
beam: float
|
||||||
Bloom = 0.08
|
bloom: float
|
||||||
HotGlow = 0.7
|
hotGlow: float
|
||||||
WarmGlow = 0.4
|
warmGlow: float
|
||||||
CoolGlow = 0.15
|
coolGlow: float
|
||||||
Palette = "green"
|
palette: string
|
||||||
|
|
||||||
|
proc defaultConfig(): Config =
|
||||||
|
Config(
|
||||||
|
decay: 0.85,
|
||||||
|
beam: 0.4,
|
||||||
|
bloom: 0.08,
|
||||||
|
hotGlow: 0.7,
|
||||||
|
warmGlow: 0.4,
|
||||||
|
coolGlow: 0.15,
|
||||||
|
palette: "green"
|
||||||
|
)
|
||||||
|
|
||||||
|
proc usage() =
|
||||||
|
echo """
|
||||||
|
Terminal oscilloscope
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-p, --palette:NAME Palette name, default: green
|
||||||
|
-d, --decay:FLOAT Phosphor decay, default: 0.85
|
||||||
|
--beam:FLOAT Beam intensity, default: 0.4
|
||||||
|
--bloom:FLOAT Bloom intensity, default: 0.08
|
||||||
|
--hot:FLOAT Hot glow, default: 0.7
|
||||||
|
--warm:FLOAT Warm glow, default: 0.4
|
||||||
|
--cool:FLOAT Cool glow, default: 0.15
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
Example:
|
||||||
|
./osc_braille --palette:amber --decay:0.92 --beam:0.6 --bloom:0.12
|
||||||
|
"""
|
||||||
|
|
||||||
|
proc parseConfig(): Config =
|
||||||
|
result = defaultConfig()
|
||||||
|
|
||||||
|
for kind, key, val in getopt():
|
||||||
|
case kind
|
||||||
|
of cmdLongOption, cmdShortOption:
|
||||||
|
case key
|
||||||
|
of "h", "help":
|
||||||
|
usage()
|
||||||
|
quit 0
|
||||||
|
of "palette", "p":
|
||||||
|
result.palette = val
|
||||||
|
of "decay", "d":
|
||||||
|
result.decay = parseFloat(val)
|
||||||
|
of "beam":
|
||||||
|
result.beam = parseFloat(val)
|
||||||
|
of "bloom":
|
||||||
|
result.bloom = parseFloat(val)
|
||||||
|
of "hot":
|
||||||
|
result.hotGlow = parseFloat(val)
|
||||||
|
of "warm":
|
||||||
|
result.warmGlow = parseFloat(val)
|
||||||
|
of "cool":
|
||||||
|
result.coolGlow = parseFloat(val)
|
||||||
|
else:
|
||||||
|
quit "Unknown option: " & key
|
||||||
|
else:
|
||||||
|
discard
|
||||||
|
|
||||||
# ── Audio thread ─────────────────────────────────────────────────────
|
# ── Audio thread ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -59,95 +117,173 @@ proc readKey(): char =
|
|||||||
|
|
||||||
# ── Phosphor ─────────────────────────────────────────────────────────
|
# ── Phosphor ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
proc plotDot(c: var BrailleCanvas, fx, fy: float) =
|
proc plotDot(c: var BrailleCanvas, fx, fy: float, cfg: Config) =
|
||||||
let x = int(fx); let y = int(fy)
|
let x = int(fx)
|
||||||
c.addPixel(x, y, Beam)
|
let y = int(fy)
|
||||||
c.addPixel(x - 1, y, Bloom)
|
c.addPixel(x, y, cfg.beam)
|
||||||
c.addPixel(x + 1, y, Bloom)
|
c.addPixel(x - 1, y, cfg.bloom)
|
||||||
|
c.addPixel(x + 1, y, cfg.bloom)
|
||||||
|
|
||||||
proc plotLine(c: var BrailleCanvas, x0, y0, x1, y1: float) =
|
proc plotLine(
|
||||||
|
c: var BrailleCanvas,
|
||||||
|
x0, y0, x1, y1: float,
|
||||||
|
cfg: Config
|
||||||
|
) =
|
||||||
let steps = max(int(max(abs(x1 - x0), abs(y1 - y0))), 1)
|
let steps = max(int(max(abs(x1 - x0), abs(y1 - y0))), 1)
|
||||||
for i in 0..steps:
|
for i in 0..steps:
|
||||||
let t = i.float / steps.float
|
let t = i.float / steps.float
|
||||||
c.plotDot(x0 + (x1 - x0) * t, y0 + (y1 - y0) * t)
|
c.plotDot(
|
||||||
|
x0 + (x1 - x0) * t,
|
||||||
|
y0 + (y1 - y0) * t,
|
||||||
|
cfg
|
||||||
|
)
|
||||||
|
|
||||||
proc renderTrace(c: var BrailleCanvas, scope: Scope) =
|
proc renderTrace(c: var BrailleCanvas, scope: Scope, cfg: Config) =
|
||||||
if scope.sampleCount < 2: return
|
if scope.sampleCount < 2:
|
||||||
let w = c.pixW; let h = c.pixH
|
return
|
||||||
let cy = h.float / 2.0; let gain = scope.gain
|
|
||||||
|
let w = c.pixW
|
||||||
|
let h = c.pixH
|
||||||
|
let cy = h.float / 2.0
|
||||||
|
let gain = scope.gain
|
||||||
|
|
||||||
case scope.mode
|
case scope.mode
|
||||||
of ModeYT:
|
of ModeYT:
|
||||||
let visible = max(int(scope.sampleCount.float / scope.timeDiv), 2)
|
let visible = max(int(scope.sampleCount.float / scope.timeDiv), 2)
|
||||||
var px, py: float; var first = true
|
var px, py: float
|
||||||
|
var first = true
|
||||||
|
|
||||||
for col in 0..<w:
|
for col in 0..<w:
|
||||||
let s = min((col * visible) div w, scope.sampleCount - 1)
|
let s = min((col * visible) div w, scope.sampleCount - 1)
|
||||||
let x = col.float; let y = cy - scope.samplesL[s] * gain * cy * 0.5
|
let x = col.float
|
||||||
if first: c.plotDot(x, y) else: c.plotLine(px, py, x, y)
|
let y = cy - scope.samplesL[s] * gain * cy * 0.5
|
||||||
first = false; px = x; py = y
|
|
||||||
|
if first:
|
||||||
|
c.plotDot(x, y, cfg)
|
||||||
|
else:
|
||||||
|
c.plotLine(px, py, x, y, cfg)
|
||||||
|
|
||||||
|
first = false
|
||||||
|
px = x
|
||||||
|
py = y
|
||||||
|
|
||||||
of ModeXY:
|
of ModeXY:
|
||||||
var px, py: float; var first = true
|
var px, py: float
|
||||||
|
var first = true
|
||||||
let step = max(scope.sampleCount div 1024, 1)
|
let step = max(scope.sampleCount div 1024, 1)
|
||||||
|
|
||||||
for i in countup(0, scope.sampleCount - 1, step):
|
for i in countup(0, scope.sampleCount - 1, step):
|
||||||
let x = (1.0 + scope.samplesL[i] * gain * 0.5) * w.float / 2.0
|
let x = (1.0 + scope.samplesL[i] * gain * 0.5) * w.float / 2.0
|
||||||
let y = (1.0 - scope.samplesR[i] * gain * 0.5) * h.float / 2.0
|
let y = (1.0 - scope.samplesR[i] * gain * 0.5) * h.float / 2.0
|
||||||
if first: c.plotDot(x, y) else: c.plotLine(px, py, x, y)
|
|
||||||
first = false; px = x; py = y
|
if first:
|
||||||
|
c.plotDot(x, y, cfg)
|
||||||
|
else:
|
||||||
|
c.plotLine(px, py, x, y, cfg)
|
||||||
|
|
||||||
|
first = false
|
||||||
|
px = x
|
||||||
|
py = y
|
||||||
|
|
||||||
# ── Main ─────────────────────────────────────────────────────────────
|
# ── Main ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
proc main() =
|
proc main() =
|
||||||
|
let cfg = parseConfig()
|
||||||
|
|
||||||
initTerm()
|
initTerm()
|
||||||
setRawMode()
|
setRawMode()
|
||||||
|
|
||||||
var w = termWidth(); var h = termHeight()
|
var w = termWidth()
|
||||||
var hb = newCanvas(w, h, Palette, [HotGlow, WarmGlow, CoolGlow])
|
var h = termHeight()
|
||||||
|
|
||||||
|
var hb = newCanvas(
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
cfg.palette,
|
||||||
|
[cfg.hotGlow, cfg.warmGlow, cfg.coolGlow]
|
||||||
|
)
|
||||||
|
|
||||||
crtTurnOn(hb)
|
crtTurnOn(hb)
|
||||||
var c = newBrailleCanvas(w, h, Palette, [HotGlow, WarmGlow, CoolGlow])
|
|
||||||
|
var c = newBrailleCanvas(
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
cfg.palette,
|
||||||
|
[cfg.hotGlow, cfg.warmGlow, cfg.coolGlow]
|
||||||
|
)
|
||||||
|
|
||||||
var scope = initScope(w, h)
|
var scope = initScope(w, h)
|
||||||
var aud = startAudio()
|
var aud = startAudio()
|
||||||
var running = true
|
var running = true
|
||||||
|
|
||||||
audioChan.open()
|
audioChan.open()
|
||||||
audioRunning = true
|
audioRunning = true
|
||||||
|
|
||||||
var aThread: Thread[ptr AudioCapture]
|
var aThread: Thread[ptr AudioCapture]
|
||||||
createThread(aThread, audioThread, addr aud)
|
createThread(aThread, audioThread, addr aud)
|
||||||
|
|
||||||
while running:
|
while running:
|
||||||
let nw = termWidth(); let nh = termHeight()
|
let nw = termWidth()
|
||||||
|
let nh = termHeight()
|
||||||
|
|
||||||
if nw != w or nh != h:
|
if nw != w or nh != h:
|
||||||
w = nw; h = nh; c.resize(w, h); scope.resize(w, h)
|
w = nw
|
||||||
|
h = nh
|
||||||
|
c.resize(w, h)
|
||||||
|
scope.resize(w, h)
|
||||||
|
|
||||||
let got = audioChan.tryRecv()
|
let got = audioChan.tryRecv()
|
||||||
|
|
||||||
if got.dataAvailable:
|
if got.dataAvailable:
|
||||||
scope.sampleCount = got.msg.count
|
scope.sampleCount = got.msg.count
|
||||||
|
|
||||||
for i in 0..<got.msg.count:
|
for i in 0..<got.msg.count:
|
||||||
scope.samplesL[i] = got.msg.samples[i][0]
|
scope.samplesL[i] = got.msg.samples[i][0]
|
||||||
scope.samplesR[i] = got.msg.samples[i][1]
|
scope.samplesR[i] = got.msg.samples[i][1]
|
||||||
|
|
||||||
c.decayPixels(Decay)
|
c.decayPixels(cfg.decay)
|
||||||
c.renderTrace(scope)
|
c.renderTrace(scope, cfg)
|
||||||
|
|
||||||
|
let hud =
|
||||||
|
" " &
|
||||||
|
(if scope.mode == ModeYT: "Y-T" else: "X-Y") &
|
||||||
|
" G:" &
|
||||||
|
scope.gain.formatFloat(ffDecimal, 1) &
|
||||||
|
" "
|
||||||
|
|
||||||
let hud = " " & (if scope.mode == ModeYT: "Y-T" else: "X-Y") &
|
|
||||||
" G:" & scope.gain.formatFloat(ffDecimal, 1) & " "
|
|
||||||
let help = " m:mode +/-:gain [/]:time q:quit "
|
let help = " m:mode +/-:gain [/]:time q:quit "
|
||||||
c.flush([(1, 0, tNormal, hud),
|
|
||||||
(w - help.len - 1, h - 1, tDim, help)])
|
c.flush([
|
||||||
|
(1, 0, tNormal, hud),
|
||||||
|
(w - help.len - 1, h - 1, tDim, help)
|
||||||
|
])
|
||||||
|
|
||||||
sleep(16)
|
sleep(16)
|
||||||
|
|
||||||
case readKey()
|
case readKey()
|
||||||
of 'q', '\x1b': running = false
|
of 'q', '\x1b':
|
||||||
of 'm': scope.mode = if scope.mode == ModeYT: ModeXY else: ModeYT
|
running = false
|
||||||
of '+', '=': scope.gain = min(scope.gain * 1.3, 20.0)
|
of 'm':
|
||||||
of '-': scope.gain = max(scope.gain / 1.3, 0.5)
|
scope.mode =
|
||||||
of ']': scope.timeDiv = min(scope.timeDiv * 1.5, 16.0)
|
if scope.mode == ModeYT:
|
||||||
of '[': scope.timeDiv = max(scope.timeDiv / 1.5, 0.25)
|
ModeXY
|
||||||
else: discard
|
else:
|
||||||
|
ModeYT
|
||||||
|
of '+', '=':
|
||||||
|
scope.gain = min(scope.gain * 1.3, 20.0)
|
||||||
|
of '-':
|
||||||
|
scope.gain = max(scope.gain / 1.3, 0.5)
|
||||||
|
of ']':
|
||||||
|
scope.timeDiv = min(scope.timeDiv * 1.5, 16.0)
|
||||||
|
of '[':
|
||||||
|
scope.timeDiv = max(scope.timeDiv / 1.5, 0.25)
|
||||||
|
else:
|
||||||
|
discard
|
||||||
|
|
||||||
audioRunning = false
|
audioRunning = false
|
||||||
joinThread(aThread)
|
joinThread(aThread)
|
||||||
audioChan.close()
|
audioChan.close()
|
||||||
|
|
||||||
aud.stop()
|
aud.stop()
|
||||||
crtTurnOff(hb)
|
crtTurnOff(hb)
|
||||||
restoreMode()
|
restoreMode()
|
||||||
|
|||||||
Reference in New Issue
Block a user