current build before revised methodology to decode live

This commit is contained in:
Chris Frankland-Wright 2025-07-02 18:33:40 +01:00
parent 026373dd77
commit 57b6fc6eeb
2 changed files with 133 additions and 41 deletions

39
decode_ltc.py Normal file
View file

@ -0,0 +1,39 @@
import subprocess
import time
import shutil
# Check tools
if not shutil.which("ltcdump") or not shutil.which("ffmpeg"):
print("❌ Required tools not found. Please ensure ffmpeg and ltcdump are installed.")
exit(1)
print("🕰️ Starting LTC timecode reader (refreshes every second)...\n")
try:
while True:
# Capture 1 second of audio and pipe into ltcdump
ffmpeg = subprocess.Popen(
["ffmpeg", "-f", "alsa", "-i", "hw:1", "-t", "1", "-f", "s16le", "-ac", "1", "-ar", "48000", "-"],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL
)
ltcdump = subprocess.Popen(
["ltcdump", "-f", "-"],
stdin=ffmpeg.stdout,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL
)
ffmpeg.stdout.close()
output, _ = ltcdump.communicate()
# Extract and print LTC timecode
lines = output.decode().splitlines()
if lines:
print(f"\r⏱️ LTC: {lines[-1]}", end="")
else:
print("\r⚠️ No LTC decoded...", end="")
time.sleep(1)
except KeyboardInterrupt:
print("\n🛑 Stopped by user.")

View file

@ -2,62 +2,115 @@
import curses
import subprocess
import shutil
import time
import shutil
import fcntl
import os
import errno
from datetime import datetime
def start_ltc_stream():
# Launch ffmpeg piped into ltcdump
ffmpeg = subprocess.Popen(
["ffmpeg", "-f", "alsa", "-i", "default", "-ac", "1", "-ar", "48000", "-f", "s16le", "-"],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL
)
ltcdump = subprocess.Popen(
["ltcdump", "-f", "-"],
stdin=ffmpeg.stdout,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True
)
ffmpeg.stdout.close() # Let ltcdump consume the pipe
return ffmpeg, ltcdump
MAX_LOG_LINES = 5
def set_nonblocking(fileobj):
fd = fileobj.fileno()
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
def nonblocking_readline(output):
try:
return output.readline()
except IOError as e:
if e.errno == errno.EAGAIN:
return ""
else:
raise
def frame_to_int(line):
try:
return int(line.strip().split(":")[-1])
except Exception:
return None
def start_ltc_stream(log_lines):
try:
ffmpeg = subprocess.Popen(
["ffmpeg", "-f", "alsa", "-i", "default", "-ac", "1", "-ar", "48000", "-f", "s16le", "-"],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL
)
ltcdump = subprocess.Popen(
["ltcdump", "-f", "-"],
stdin=ffmpeg.stdout,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
ffmpeg.stdout.close()
set_nonblocking(ltcdump.stdout)
set_nonblocking(ltcdump.stderr)
log_lines.append(f"[{datetime.now().strftime('%H:%M:%S')}] ✅ Subprocesses started successfully")
return ffmpeg, ltcdump
except Exception as e:
log_lines.append(f"[{datetime.now().strftime('%H:%M:%S')}] ❌ Failed to start subprocesses: {e}")
return None, None
def main(stdscr):
curses.curs_set(0)
stdscr.nodelay(True)
stdscr.addstr(1, 2, "🌀 NTP Timeturner Status")
stdscr.addstr(3, 4, "Streaming LTC from default input...")
log_lines = []
ffmpeg_proc, ltcdump_proc = start_ltc_stream(log_lines)
ffmpeg_proc, ltcdump_proc = start_ltc_stream()
last_tc = "⌛ Waiting for LTC..."
last_frame = None
fail_count = 0
latest_tc = "⌛ Waiting for LTC..."
last_refresh = time.time()
while True:
# Check for subprocess failure
if ffmpeg_proc and ffmpeg_proc.poll() is not None:
log_lines.append(f"[{datetime.now().strftime('%H:%M:%S')}] ❌ ffmpeg exited (code {ffmpeg_proc.returncode})")
ffmpeg_proc = None
if ltcdump_proc and ltcdump_proc.poll() is not None:
err_output = ltcdump_proc.stderr.read().strip()
log_lines.append(f"[{datetime.now().strftime('%H:%M:%S')}] ❌ ltcdump exited (code {ltcdump_proc.returncode})")
if err_output:
log_lines.append(err_output.splitlines()[-1])
ltcdump_proc = None
try:
while True:
stdscr.clear()
stdscr.addstr(1, 2, "🌀 NTP Timeturner Status")
stdscr.addstr(3, 4, "Streaming LTC from default input...")
stdscr.addstr(5, 6, f"🕰️ LTC Timecode: {latest_tc}")
stdscr.refresh()
# Check if new LTC line available
if ltcdump_proc.stdout.readable():
line = ltcdump_proc.stdout.readline().strip()
# Read LTC line if possible
if ltcdump_proc:
try:
line = nonblocking_readline(ltcdump_proc.stdout).strip()
if line and line[0].isdigit():
latest_tc = line
current_frame = frame_to_int(line)
if last_frame is not None and current_frame is not None:
delta = abs(current_frame - last_frame)
if delta > 3:
log_lines.append(f"[{datetime.now().strftime('%H:%M:%S')}] ⚠️ Timecode jump: Δ{delta} frames")
last_tc = line
last_frame = current_frame
elif line:
fail_count += 1
except Exception as e:
log_lines.append(f"[{datetime.now().strftime('%H:%M:%S')}] ⚠️ Read error: {e}")
fail_count += 1
# Limit screen redraw to ~10fps
time.sleep(0.1)
log_lines = log_lines[-MAX_LOG_LINES:]
# Draw UI
stdscr.clear()
stdscr.addstr(1, 2, "🌀 NTP Timeturner Status")
stdscr.addstr(3, 4, "Streaming LTC from default input..." if ltcdump_proc else "⚠️ LTC decoder inactive")
stdscr.addstr(5, 6, f"🕰️ LTC Timecode: {last_tc}")
stdscr.addstr(6, 6, f"❌ Decode Failures: {fail_count}")
stdscr.addstr(8, 4, "📜 Logs:")
for i, log in enumerate(log_lines):
stdscr.addstr(9 + i, 6, log[:curses.COLS - 8])
except KeyboardInterrupt:
stdscr.addstr(8, 6, "🔚 Shutting down...")
stdscr.refresh()
time.sleep(1)
finally:
ffmpeg_proc.terminate()
ltcdump_proc.terminate()
if __name__ == "__main__":
if not shutil.which("ltcdump") or not shutil.which("ffmpeg"):