mirror of
https://github.com/cjfranko/NTP-Timeturner.git
synced 2025-11-08 10:22:02 +00:00
current build before revised methodology to decode live
This commit is contained in:
parent
026373dd77
commit
57b6fc6eeb
2 changed files with 133 additions and 41 deletions
39
decode_ltc.py
Normal file
39
decode_ltc.py
Normal 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.")
|
||||
135
timeturner.py
135
timeturner.py
|
|
@ -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"):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue