diff --git a/config.json b/config.json new file mode 100644 index 0000000..83fe386 --- /dev/null +++ b/config.json @@ -0,0 +1,10 @@ +{ + "ltc_device": "/dev/audio", + "offset": { + "hours": 0, + "minutes": 0, + "seconds": 0, + "milliseconds": 0 + }, + "frame_rate": 25 +} diff --git a/hermione.py b/hermione.py deleted file mode 100644 index 5d3a007..0000000 --- a/hermione.py +++ /dev/null @@ -1,109 +0,0 @@ -ο»Ώimport json -import subprocess -import time -import threading -import os -from datetime import datetime - -CONFIG_FILE = "hermione_config.json" - -# Load config or create default -def load_config(): - if not os.path.exists(CONFIG_FILE): - default_config = { - "framerate": 25, - "start_mode": "system", - "manual_time": "12:00:00", - "duration_seconds": 3600, - "ltc_gen_path": "ltc-gen.exe", - "autostart_timeout": 5 - } - with open(CONFIG_FILE, "w") as f: - json.dump(default_config, f, indent=4) - return default_config - else: - with open(CONFIG_FILE, "r") as f: - return json.load(f) - -# Save updated config -def save_config(config): - with open(CONFIG_FILE, "w") as f: - json.dump(config, f, indent=4) - -# Prompt with timeout -def prompt_with_timeout(prompt, timeout): - print(prompt, end='', flush=True) - input_data = [] - - def get_input(): - try: - input_data.append(input()) - except EOFError: - pass - - thread = threading.Thread(target=get_input) - thread.daemon = True - thread.start() - thread.join(timeout) - return input_data[0] if input_data else "" - -# Get timecode based on config -def get_start_time(config): - if config["start_mode"] == "system": - now = datetime.now() - return now.strftime("%H:%M:%S") - else: - return config["manual_time"] - -# Run ltc-gen -def run_ltc_gen(config): - start_time = get_start_time(config) - framerate = str(config["framerate"]) - duration = str(config["duration_seconds"]) - ltc_gen_path = config["ltc_gen_path"] - - cmd = [ - ltc_gen_path, - "-f", framerate, - "-l", duration, - "-t", start_time - ] - - print(f"\n🎬 Running Hermione with:") - print(f" Start Time: {start_time}") - print(f" Framerate: {framerate} fps") - print(f" Duration: {duration} seconds") - print(f" Executable: {ltc_gen_path}\n") - - try: - subprocess.run(cmd) - except FileNotFoundError: - print(f"❌ Error: {ltc_gen_path} not found!") - except Exception as e: - print(f"❌ Failed to run Hermione: {e}") - -# Main logic -def main(): - config = load_config() - user_input = prompt_with_timeout( - "\nPress [Enter] to run with config or type 'm' to modify (auto-starts in 5s): ", - config.get("autostart_timeout", 5) - ) - - if user_input.lower() == 'm': - try: - config["framerate"] = int(input("Enter framerate (e.g. 25): ")) - config["start_mode"] = input("Start from system time or manual? (system/manual): ").strip().lower() - if config["start_mode"] == "manual": - config["manual_time"] = input("Enter manual start time (HH:MM:SS): ") - config["duration_seconds"] = int(input("Enter duration in seconds: ")) - config["ltc_gen_path"] = input("Enter path to ltc-gen.exe (or leave blank for default): ") or config["ltc_gen_path"] - save_config(config) - except Exception as e: - print(f"⚠️ Error updating config: {e}") - return - - run_ltc_gen(config) - -if __name__ == "__main__": - main() diff --git a/hermione_config.json b/hermione_config.json deleted file mode 100644 index cd64a0a..0000000 --- a/hermione_config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "framerate": 25, - "start_mode": "system", - "manual_time": "12:00:00", - "duration_seconds": 3600, - "ltc_gen_path": "ltc-gen.exe", - "autostart_timeout": 5 -} diff --git a/ltc_probe.py b/ltc_probe.py new file mode 100644 index 0000000..9dded3c --- /dev/null +++ b/ltc_probe.py @@ -0,0 +1,91 @@ +ο»Ώ#!/usr/bin/env python3 + +""" +ltc_probe.py +Advanced LTC-like signal probe using pulse duration clustering +for reliable short/long classification β€” works even with imbalanced timecodes. +""" + +import numpy as np +import sounddevice as sd + +DURATION = 1.0 # seconds +SAMPLERATE = 48000 +CHANNELS = 1 +MIN_EDGES = 1000 + +def detect_rising_edges(signal): + above_zero = signal > 0 + edges = np.where(np.logical_and(~above_zero[:-1], above_zero[1:]))[0] + return edges + +def cluster_durations(durations): + if len(durations) < 2: + return None, None + + # Use 2-means clustering (basic method) + durations = np.array(durations) + mean1, mean2 = np.min(durations), np.max(durations) + + for _ in range(10): # converge in a few iterations + group1 = durations[np.abs(durations - mean1) < np.abs(durations - mean2)] + group2 = durations[np.abs(durations - mean1) >= np.abs(durations - mean2)] + if len(group1) == 0 or len(group2) == 0: + break + mean1 = np.mean(group1) + mean2 = np.mean(group2) + + short = group1 if mean1 < mean2 else group2 + long = group2 if mean1 < mean2 else group1 + return short, long + +def analyze_pulse_durations(edges, samplerate): + durations = np.diff(edges) / samplerate + if len(durations) == 0: + return None + + short, long = cluster_durations(durations) + if short is None or long is None: + return None + + total = len(durations) + return { + "count": total, + "avg_width_ms": np.mean(durations) * 1000, + "short_pulses": len(short), + "long_pulses": len(long), + "short_pct": (len(short) / total) * 100, + "long_pct": (len(long) / total) * 100 + } + +def verdict(pulse_data): + if pulse_data is None or pulse_data["count"] < MIN_EDGES: + return "❌ No signal or not enough pulses" + elif 10 <= pulse_data["short_pct"] <= 90: + return f"βœ… LTC-like bi-phase signal detected ({pulse_data['count']} pulses)" + else: + return f"⚠️ Pulse imbalance suggests non-LTC or noisy signal" + +def main(): + print("πŸ” Capturing 1 second of audio for LTC probing...") + audio = sd.rec(int(DURATION * SAMPLERATE), samplerate=SAMPLERATE, channels=CHANNELS, dtype='float32') + sd.wait() + + signal = audio.flatten() + edges = detect_rising_edges(signal) + pulse_data = analyze_pulse_durations(edges, SAMPLERATE) + + print(f"\nπŸ“Š Pulse Analysis:") + if pulse_data: + print(f" Total pulses: {pulse_data['count']}") + print(f" Avg pulse width: {pulse_data['avg_width_ms']:.2f} ms") + print(f" Short pulses: {pulse_data['short_pulses']} ({pulse_data['short_pct']:.1f}%)") + print(f" Long pulses: {pulse_data['long_pulses']} ({pulse_data['long_pct']:.1f}%)") + else: + print(" Not enough data to analyze.") + + print("\n🧭 Verdict:") + print(" ", verdict(pulse_data)) + +if __name__ == "__main__": + main() diff --git a/setup.sh b/setup.sh index 7ec0a9b..55dbb73 100644 --- a/setup.sh +++ b/setup.sh @@ -1,117 +1,43 @@ -ο»Ώ#!/bin/bash -set -e +#!/bin/bash -echo "" -echo "─────────────────────────────────────────────" -echo " Welcome to the NTP Timeturner Installer" -echo "─────────────────────────────────────────────" -echo "" -echo "\"It's a very complicated piece of magic...\" – Hermione Granger" -echo "Initialising temporal calibration sequence..." -echo "Requesting clearance from the Ministry of Time Standards..." -echo "" +# NTP Timeturner Setup Script +# Tested on Debian Bookworm - Raspberry Pi 3 +# Author: cjfranko -# --------------------------------------------------------- -# Step 1: Update and upgrade packages -# --------------------------------------------------------- -echo "" -echo "Step 1: Updating package lists..." -sudo apt update +echo "Step 1: Updating system packages..." +sudo apt-get update && sudo apt-get upgrade -y -echo "Upgrading installed packages..." -sudo apt upgrade -y +echo "Step 2: Installing required system packages..." +sudo apt-get install -y git cmake build-essential libjack-jackd2-dev \ + libsndfile1-dev libtool autoconf automake \ + pkg-config libasound2-dev libfftw3-dev \ + python3-full python3-venv python3-pip \ + libltc-dev python3-numpy python3-matplotlib python3-sounddevice -# --------------------------------------------------------- -# Step 2: Install essential development tools -# --------------------------------------------------------- -echo "" -echo "Step 2: Installing development tools..." -sudo apt install -y git curl python3 python3-pip build-essential autoconf automake libtool cmake +echo "Step 3: Cloning libltc and ltc-tools..." +cd /home/hermione +git clone https://github.com/x42/libltc.git +git clone https://github.com/x42/ltc-tools.git -# --------------------------------------------------------- -# Step 3: Install audio and media dependencies -# --------------------------------------------------------- -echo "" -echo "Step 3: Installing audio libraries and tools..." -sudo apt install -y alsa-utils ffmpeg \ - portaudio19-dev python3-pyaudio \ - libasound2-dev libjack-jackd2-dev \ - libsndfile-dev \ - || echo "Warning: Some audio dependencies may have failed to install β€” continuing anyway." - -# --------------------------------------------------------- -# Step 4: Install Python packages -# --------------------------------------------------------- -echo "" -echo "Step 4: Installing Python packages..." -pip3 install numpy --break-system-packages - -# --------------------------------------------------------- -# Step 5: Build and install libltc (needed by ltc-tools) -# --------------------------------------------------------- -echo "" -echo "Step 5: Building libltc (the heart of our time-magic)..." -cd ~ -if [ ! -d "libltc" ]; then - echo "Cloning libltc from GitHub..." - git clone https://github.com/x42/libltc.git -fi +echo "Step 4: Building libltc (the heart of our time-magic)..." cd libltc - -echo "Preparing libltc build..." -./autogen.sh -./configure - -echo "Compiling libltc..." -make - -echo "Installing libltc..." +mkdir -p build && cd build +cmake .. +make -j$(nproc) sudo make install sudo ldconfig -# --------------------------------------------------------- -# Step 6: Build and install ltc-tools -# --------------------------------------------------------- -echo "" -echo "Step 6: Building ltc-tools (with a gentle nudge)..." -cd ~ -if [ ! -d "ltc-tools" ]; then - echo "Cloning ltc-tools from GitHub..." - git clone https://github.com/x42/ltc-tools.git -fi -cd ltc-tools - -echo "Compiling ltc-tools (bypassing package check)..." -make HAVE_LIBLTC=true - -echo "Installing ltc-tools..." +echo "Step 5: Building ltc-tools..." +cd /home/hermione/ltc-tools +make -j$(nproc) sudo make install -sudo ldconfig -# --------------------------------------------------------- -# Step 7: Apply Custom Splash Screen -# --------------------------------------------------------- -echo "" -echo "Step 7: Applying splash screen..." +echo "Step 6: Setting splash screen..." +sudo cp /home/hermione/splash.png /usr/share/plymouth/themes/pix/splash.png -sudo curl -L -o /usr/share/plymouth/themes/pix/splash.png https://raw.githubusercontent.com/cjfranko/NTP-Timeturner/master/splash.png -sudo chmod 644 /usr/share/plymouth/themes/pix/splash.png +echo "Step 7: Making timeturner scripts executable..." +chmod +x /home/hermione/*.py -# --------------------------------------------------------- -# Final Message & Reboot Option -# --------------------------------------------------------- -echo "" -echo "─────────────────────────────────────────────" -echo " Setup Complete" -echo "─────────────────────────────────────────────" -echo "" -echo "The TimeTurner is ready. But remember:" -echo "\"You must not be seen.\" – Hermione Granger" -echo "Visual enhancements are in place. Terminal timeline is stable." -echo "" -echo "The system will reboot in 30 seconds to complete setup..." -echo "Press [Enter] to reboot immediately, or Ctrl+C to cancel." - -read -t 30 -p "" || true -sleep 1 -sudo reboot +echo "Step 8: Setup complete. System will reboot in 30 seconds unless you press Enter..." +echo "Press Ctrl+C or Enter now to cancel automatic reboot." +read -t 30 -p ">> " input && sudo reboot || sudo reboot diff --git a/test_audioinput.py b/test_audioinput.py new file mode 100644 index 0000000..213e1a4 --- /dev/null +++ b/test_audioinput.py @@ -0,0 +1,36 @@ +ο»Ώ#!/usr/bin/env python3 + +""" +test_audioinput.py +Records 2 seconds of audio from the default input device +and saves the waveform as 'waveform.png' β€” works headless. +""" + +import numpy as np +import matplotlib +matplotlib.use('Agg') # Headless backend +import matplotlib.pyplot as plt +import sounddevice as sd + +DURATION = 2 # seconds +SAMPLERATE = 48000 +CHANNELS = 1 + +print("πŸ”Š Recording 2 seconds from default input device...") +recording = sd.rec(int(DURATION * SAMPLERATE), samplerate=SAMPLERATE, channels=CHANNELS, dtype='float32') +sd.wait() + +# Generate time axis +time_axis = np.linspace(0, DURATION, len(recording)) + +# Plot and save +plt.figure(figsize=(10, 4)) +plt.plot(time_axis, recording, linewidth=0.5) +plt.title("Audio Input Waveform") +plt.xlabel("Time [s]") +plt.ylabel("Amplitude") +plt.grid(True) +plt.tight_layout() +plt.savefig("waveform.png") + +print("βœ… Waveform saved as 'waveform.png'") diff --git a/timeturner.py b/timeturner.py new file mode 100644 index 0000000..943b32e --- /dev/null +++ b/timeturner.py @@ -0,0 +1,108 @@ +ο»Ώ#!/usr/bin/env python3 + +""" +timeturner.py +NTP Timeturner Core UI +Displays LTC signal probe info using curses, updated in real-time. +""" + +import curses +import threading +import time +import numpy as np +import sounddevice as sd + +# --- CONFIGURATION --- +SAMPLERATE = 48000 +CHANNELS = 1 +PROBE_INTERVAL = 1.0 # seconds +MIN_EDGES = 1000 + +status = { + "count": 0, + "avg_width_ms": 0.0, + "short_pct": 0.0, + "long_pct": 0.0, + "verdict": "Waiting for signal...", +} + + +# --- LTC PROBE THREAD --- +def detect_rising_edges(signal): + above_zero = signal > 0 + edges = np.where(np.logical_and(~above_zero[:-1], above_zero[1:]))[0] + return edges + +def cluster_durations(durations): + if len(durations) < 2: + return None, None + + durations = np.array(durations) + mean1, mean2 = np.min(durations), np.max(durations) + + for _ in range(10): + group1 = durations[np.abs(durations - mean1) < np.abs(durations - mean2)] + group2 = durations[np.abs(durations - mean1) >= np.abs(durations - mean2)] + if len(group1) == 0 or len(group2) == 0: + break + mean1 = np.mean(group1) + mean2 = np.mean(group2) + + short = group1 if mean1 < mean2 else group2 + long = group2 if mean1 < mean2 else group1 + return short, long + +def analyze_signal(): + global status + while True: + try: + audio = sd.rec(int(PROBE_INTERVAL * SAMPLERATE), samplerate=SAMPLERATE, + channels=CHANNELS, dtype='float32') + sd.wait() + signal = audio.flatten() + edges = detect_rising_edges(signal) + durations = np.diff(edges) / SAMPLERATE + short, long = cluster_durations(durations) + + if short is None or long is None or len(durations) < MIN_EDGES: + status["verdict"] = "❌ No signal or not enough pulses" + continue + + status.update({ + "count": len(durations), + "avg_width_ms": np.mean(durations) * 1000, + "short_pct": (len(short) / len(durations)) * 100, + "long_pct": (len(long) / len(durations)) * 100, + "verdict": "βœ… LTC-like signal detected" if 10 <= (len(short) / len(durations)) * 100 <= 90 + else "⚠️ Pulse imbalance β€” possible noise or non-LTC" + }) + except Exception as e: + status["verdict"] = f"⚠️ Audio error: {e}" + +# --- CURSES UI --- +def draw_ui(stdscr): + curses.curs_set(0) + stdscr.nodelay(True) + stdscr.timeout(500) + + while True: + stdscr.clear() + stdscr.addstr(0, 2, "πŸ•°οΈ NTP Timeturner - Live LTC Monitor", curses.A_BOLD) + stdscr.addstr(2, 4, f"Pulses captured: {status['count']}") + stdscr.addstr(3, 4, f"Avg pulse width: {status['avg_width_ms']:.2f} ms") + stdscr.addstr(4, 4, f"Short pulse ratio: {status['short_pct']:.1f}%") + stdscr.addstr(5, 4, f"Long pulse ratio: {status['long_pct']:.1f}%") + stdscr.addstr(7, 4, f"Status: {status['verdict']}") + stdscr.addstr(9, 4, "Press Ctrl+C to exit.") + stdscr.refresh() + + try: + time.sleep(1) + except KeyboardInterrupt: + break + +# --- ENTRY POINT --- +if __name__ == "__main__": + probe_thread = threading.Thread(target=analyze_signal, daemon=True) + probe_thread.start() + curses.wrapper(draw_ui)