diff --git a/decode_ltc.py b/decode_ltc.py new file mode 100644 index 0000000..9e2b6bd --- /dev/null +++ b/decode_ltc.py @@ -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.") diff --git a/timeturner.py b/timeturner.py index dce1be1..03aceca 100644 --- a/timeturner.py +++ b/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"):