## 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..