Files
terminal-oscilloscope/src/osc/audio.nim
rolandnsharp 666af905f3 Fix AVPacket struct layout — was missing pts/dts fields
The packet layout is buf(8), pts(8), dts(8), data(8), size(4),
stream_index(4). We had buf, data, size, stream_index — reading
the pts timestamp as the data pointer, so every packet was garbage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 19:56:02 +10:00

115 lines
3.9 KiB
Nim
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Audio capture via libavdevice/libavformat (dlopen at runtime).
import osproc, strutils
import scope
# ── libav C helper bindings ──────────────────────────────────────────
{.compile: "avhelper.c".}
{.passL: "-ldl".}
type
AVFormatContext = object
AVPacket = object
proc av_helper_init(): cint {.importc, cdecl.}
proc av_helper_open_pulse(ctx: ptr ptr AVFormatContext,
device: cstring): cint {.importc, cdecl.}
proc av_helper_find_stream_info(ctx: ptr AVFormatContext): cint
{.importc, cdecl.}
proc av_helper_find_audio_stream(ctx: ptr AVFormatContext): cint
{.importc, cdecl.}
proc av_helper_read_frame(ctx: ptr AVFormatContext,
pkt: ptr AVPacket): cint {.importc, cdecl.}
proc av_helper_packet_stream(pkt: ptr AVPacket): cint {.importc, cdecl.}
proc av_helper_packet_data(pkt: ptr AVPacket): ptr UncheckedArray[uint8]
{.importc, cdecl.}
proc av_helper_packet_size(pkt: ptr AVPacket): cint {.importc, cdecl.}
proc av_helper_packet_alloc(): ptr AVPacket {.importc, cdecl.}
proc av_helper_packet_unref(pkt: ptr AVPacket) {.importc, cdecl.}
proc av_helper_packet_free(pkt: ptr ptr AVPacket) {.importc, cdecl.}
proc av_helper_close(ctx: ptr ptr AVFormatContext) {.importc, cdecl.}
# ── Monitor source detection ─────────────────────────────────────────
proc findMonitorSource(): string =
try:
let inspect = execProcess("wpctl",
args = ["inspect", "@DEFAULT_AUDIO_SINK@"],
options = {poUsePath, poStdErrToStdOut})
for line in inspect.splitLines():
if "node.name" in line:
let eq = line.find("=")
if eq >= 0:
return line[eq+1..^1].strip().strip(chars = {'"', ' '}) & ".monitor"
except: discard
""
# ── Audio capture ────────────────────────────────────────────────────
type
AudioCapture* = object
fmtCtx: ptr AVFormatContext
packet: ptr AVPacket
streamIdx: cint
live*: bool
proc startAudio*(): AudioCapture =
let monitor = findMonitorSource()
if monitor.len == 0: return
if av_helper_init() < 0: return
var ctx: ptr AVFormatContext = nil
if av_helper_open_pulse(addr ctx, monitor.cstring) < 0: return
if av_helper_find_stream_info(ctx) < 0:
av_helper_close(addr ctx)
return
let idx = av_helper_find_audio_stream(ctx)
let pkt = av_helper_packet_alloc()
if pkt == nil:
av_helper_close(addr ctx)
return
AudioCapture(fmtCtx: ctx, packet: pkt, streamIdx: idx.cint, live: true)
proc stop*(cap: var AudioCapture) =
if cap.live:
if cap.packet != nil: av_helper_packet_free(addr cap.packet)
if cap.fmtCtx != nil: av_helper_close(addr cap.fmtCtx)
proc sourceLabel*(cap: AudioCapture): string =
if cap.live: "LIVE" else: "NO SIGNAL"
proc readSamples*(cap: var AudioCapture, scope: var Scope) =
if not cap.live: return
const frameSize = 4 # 2ch × 16-bit
# Read one packet — av_read_frame blocks until data arrives,
# which naturally rate-limits the render loop to the audio rate
let ret = av_helper_read_frame(cap.fmtCtx, cap.packet)
if ret < 0:
scope.sampleCount = 0
return
if av_helper_packet_stream(cap.packet) != cap.streamIdx:
av_helper_packet_unref(cap.packet)
scope.sampleCount = 0
return
let data = av_helper_packet_data(cap.packet)
let size = av_helper_packet_size(cap.packet)
let frames = min(size div frameSize, scope.samplesL.len)
for i in 0..<frames:
let off = i * frameSize
let left = cast[int16]((data[off + 1].uint16 shl 8) or data[off].uint16)
let right = cast[int16]((data[off + 3].uint16 shl 8) or data[off + 2].uint16)
scope.samplesL[i] = left.float / 32768.0
scope.samplesR[i] = right.float / 32768.0
scope.sampleCount = frames
av_helper_packet_unref(cap.packet)