updated setup

This commit is contained in:
Chris Frankland-Wright 2025-07-02 22:55:15 +01:00
parent 57b6fc6eeb
commit 057d394567
2 changed files with 80 additions and 122 deletions

View file

@ -33,12 +33,7 @@ sudo apt install -y git curl python3 python3-pip build-essential autoconf automa
# --------------------------------------------------------- # ---------------------------------------------------------
echo "" echo ""
echo "Step 3: Installing audio libraries and tools..." echo "Step 3: Installing audio libraries and tools..."
sudo apt install -y alsa-utils ffmpeg \ sudo apt install -y alsa-utils ffmpeg portaudio19-dev python3-pyaudio libasound2-dev libjack-jackd2-dev libsndfile-dev python3-numpy python3-matplotlib || echo "Warning: Some audio dependencies may have failed to install — continuing anyway."
portaudio19-dev python3-pyaudio \
libasound2-dev libjack-jackd2-dev \
libsndfile-dev \
python3-numpy python3-matplotlib \
|| echo "Warning: Some audio dependencies may have failed to install — continuing anyway."
echo "" echo ""
echo "Installing 'sounddevice' with pip3 (system-wide)..." echo "Installing 'sounddevice' with pip3 (system-wide)..."
@ -57,10 +52,10 @@ wget -O test_audioinput.py https://raw.githubusercontent.com/cjfranko/NTP-Timetu
wget -O splash.png https://raw.githubusercontent.com/cjfranko/NTP-Timeturner/master/splash.png wget -O splash.png https://raw.githubusercontent.com/cjfranko/NTP-Timeturner/master/splash.png
# --------------------------------------------------------- # ---------------------------------------------------------
# Step 5: Build and install libltc (needed by ltc-tools) # Step 5: Build and install libltc (the heart of our time-magic)
# --------------------------------------------------------- # ---------------------------------------------------------
echo "" echo ""
echo "Step 5: Building libltc (the heart of our time-magic)..." echo "Step 5: Building libltc..."
cd ~ cd ~
if [ ! -d "libltc" ]; then if [ ! -d "libltc" ]; then
echo "Cloning libltc from GitHub..." echo "Cloning libltc from GitHub..."
@ -84,19 +79,19 @@ sudo make install
sudo ldconfig sudo ldconfig
# --------------------------------------------------------- # ---------------------------------------------------------
# Step 6: Build and install ltc-tools # Step 6: Clone and build custom ltc-tools (with ltcstream)
# --------------------------------------------------------- # ---------------------------------------------------------
echo "" echo ""
echo "Step 6: Building ltc-tools (with a gentle nudge)..." echo "Step 6: Building custom ltc-tools (ltc-tools-timeturner)..."
cd ~ cd ~
if [ ! -d "ltc-tools" ]; then if [ ! -d "ltc-tools-timeturner" ]; then
echo "Cloning ltc-tools from GitHub..." echo "Cloning your custom ltc-tools fork..."
git clone https://github.com/x42/ltc-tools.git git clone https://github.com/cjfranko/ltc-tools-timeturner.git
fi fi
cd ltc-tools cd ltc-tools-timeturner
echo "Compiling ltc-tools (bypassing package check)..." echo "Compiling ltc-tools and ltcstream..."
make HAVE_LIBLTC=true make HAVE_LIBLTC=true LOADLIBES="-lasound"
echo "Installing ltc-tools..." echo "Installing ltc-tools..."
sudo make install sudo make install
@ -119,7 +114,7 @@ fi
# Step 8: Make Python scripts executable # Step 8: Make Python scripts executable
# --------------------------------------------------------- # ---------------------------------------------------------
echo "" echo ""
echo "Step 8: Making *.py scripts executable (if any)..." echo "Step 8: Making *.py scripts executable..."
shopt -s nullglob shopt -s nullglob
PYFILES=(/home/hermione/*.py) PYFILES=(/home/hermione/*.py)
if [ ${#PYFILES[@]} -gt 0 ]; then if [ ${#PYFILES[@]} -gt 0 ]; then

View file

@ -1,120 +1,83 @@
#!/usr/bin/env python3 import numpy as np
import sounddevice as sd
import curses import scipy.signal as signal
import subprocess import matplotlib.pyplot as plt
import time import time
import shutil
import fcntl
import os
import errno
from datetime import datetime
MAX_LOG_LINES = 5 # --- Configuration ---
SAMPLE_RATE = 48000
CUTOFF_FREQ = 1000.0
BLOCK_SIZE = 2048
SYNC_WORD = "0011111111111101"
def set_nonblocking(fileobj): # --- Filtering ---
fd = fileobj.fileno() def highpass_filter(data, cutoff=CUTOFF_FREQ, fs=SAMPLE_RATE, order=5):
flags = fcntl.fcntl(fd, fcntl.F_GETFL) nyq = 0.5 * fs
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) normal_cutoff = cutoff / nyq
b, a = signal.butter(order, normal_cutoff, btype='high', analog=False)
return signal.lfilter(b, a, data)
def nonblocking_readline(output): # --- Edge Detection ---
try: def detect_edges(data):
return output.readline() return np.where(np.diff(np.signbit(data)))[0]
except IOError as e:
if e.errno == errno.EAGAIN:
return ""
else:
raise
def frame_to_int(line): # --- Pulse Width to Bitstream ---
try: def classify_bits(edges, fs):
return int(line.strip().split(":")[-1]) durations = np.diff(edges) / fs
except Exception: threshold = np.median(durations) * 1.2
return None bits = ['1' if dur < threshold else '0' for dur in durations]
return bits, durations, threshold
def start_ltc_stream(log_lines): # --- LTC Sync & Decode ---
try: def extract_ltc_frame(bitstream):
ffmpeg = subprocess.Popen( bits = ''.join(bitstream)
["ffmpeg", "-f", "alsa", "-i", "default", "-ac", "1", "-ar", "48000", "-f", "s16le", "-"], idx = bits.find(SYNC_WORD)
stdout=subprocess.PIPE, if idx != -1 and idx + 80 <= len(bits):
stderr=subprocess.DEVNULL return bits[idx:idx+80], idx
)
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 return None, None
def main(stdscr): def decode_timecode(bits):
curses.curs_set(0) def bcd(b): return int(b[0:4], 2) + 10 * int(b[4:8], 2)
stdscr.nodelay(True) frames = bcd(bits[0:8])
seconds = bcd(bits[16:24])
minutes = bcd(bits[32:40])
hours = bcd(bits[48:56])
return f"{hours:02}:{minutes:02}:{seconds:02}:{frames:02}"
log_lines = [] # --- Stream Callback ---
ffmpeg_proc, ltcdump_proc = start_ltc_stream(log_lines) def process(indata, frames, time_info, status):
audio = indata[:, 0]
filtered = highpass_filter(audio)
last_tc = "⌛ Waiting for LTC..." peak_db = 20 * np.log10(np.max(np.abs(filtered)) + 1e-6)
last_frame = None print(f"🎚️ Input Level: {peak_db:.2f} dB")
fail_count = 0
edges = detect_edges(filtered)
print(f"📎 Found {len(edges)} edges.")
if len(edges) < 10:
return
bits, durations, threshold = classify_bits(edges, SAMPLE_RATE)
print(f"📊 Avg pulse width: {np.mean(durations):.5f} sec")
print(f"📊 Min: {np.min(durations):.5f}, Max: {np.max(durations):.5f}")
print(f"🔧 Adaptive threshold: {threshold:.5f} sec")
print(f"🧮 Extracted {len(bits)} bits.")
print(f"🧾 Bitstream (first 80 bits):\n{''.join(bits[:80])}")
frame, idx = extract_ltc_frame(bits)
if frame:
print(f"🔓 Sync word found at bit {idx}")
print(f"✅ LTC Timecode: {decode_timecode(frame)}")
else:
print(f"⚠️ No valid LTC frame detected.")
# --- Main ---
print("🔊 Starting real-time audio stream...")
try:
with sd.InputStream(callback=process, channels=1, samplerate=SAMPLE_RATE, blocksize=BLOCK_SIZE):
while True: while True:
# Check for subprocess failure time.sleep(0.5)
if ffmpeg_proc and ffmpeg_proc.poll() is not None: except KeyboardInterrupt:
log_lines.append(f"[{datetime.now().strftime('%H:%M:%S')}] ❌ ffmpeg exited (code {ffmpeg_proc.returncode})") print("🛑 Stopped by user.")
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
# Read LTC line if possible
if ltcdump_proc:
try:
line = nonblocking_readline(ltcdump_proc.stdout).strip()
if line and line[0].isdigit():
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
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])
stdscr.refresh()
time.sleep(1)
if __name__ == "__main__":
if not shutil.which("ltcdump") or not shutil.which("ffmpeg"):
print("❌ Required tools not found (ltcdump or ffmpeg). Install and retry.")
exit(1)
curses.wrapper(main)