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>
115 lines
3.9 KiB
Nim
115 lines
3.9 KiB
Nim
## 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)
|