From 0b43e70f4adb7b924e7f7f5ebe51bfa7d52d3667 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Tue, 29 Oct 2024 09:36:30 -0500 Subject: [PATCH] first commit --- .gitignore | 3 + dmx.py | 141 +++++++++++++++++++++++++++++++++++++++++++++++ main.py | 85 ++++++++++++++++++++++++++++ requirements.txt | 2 + run.bat | 9 +++ send.bat | 9 +++ sender.py | 55 ++++++++++++++++++ win-install.bat | 14 +++++ 8 files changed, 318 insertions(+) create mode 100644 .gitignore create mode 100644 dmx.py create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 run.bat create mode 100644 send.bat create mode 100644 sender.py create mode 100644 win-install.bat diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b50736 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +venv +__pycache__ +*.mkv \ No newline at end of file diff --git a/dmx.py b/dmx.py new file mode 100644 index 0000000..2556216 --- /dev/null +++ b/dmx.py @@ -0,0 +1,141 @@ +# dmx.py + +from PyQt5.QtCore import QObject, QThread, pyqtSignal +import socket +import struct +import numpy as np +import subprocess +import time + +class DMXRecorder(QObject): + def __init__(self): + super().__init__() + self.thread = None + self.recording_thread = None + + def start_recording(self): + self.thread = QThread() + self.recording_thread = RecordingThread() + self.recording_thread.moveToThread(self.thread) + + self.thread.started.connect(self.recording_thread.run) + self.recording_thread.finished.connect(self.thread.quit) + self.recording_thread.finished.connect(self.recording_thread.deleteLater) + self.thread.finished.connect(self.thread.deleteLater) + + self.thread.start() + + def stop_recording(self): + if self.recording_thread: + self.recording_thread.stop() + if self.thread: + self.thread.quit() + self.thread.wait() + +class RecordingThread(QObject): + finished = pyqtSignal() + + def __init__(self): + super().__init__() + self.running = False + self.socket = None + self.ffmpeg_process = None + + def run(self): + self.running = True + + # Set up UDP socket to listen for Art-Net DMX data + UDP_IP = "0.0.0.0" + UDP_PORT = 6454 + + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.bind((UDP_IP, UDP_PORT)) + + # Set socket to non-blocking + self.socket.setblocking(False) + + # Prepare for recording + expected_universes = set(range(4)) # Universes 0 to 3 (adjust as needed) + num_universes = len(expected_universes) + universe_size = 512 + + # Define frame dimensions + frame_width = 64 # Adjust as needed (must divide evenly into total data size) + total_pixels = num_universes * universe_size + frame_height = total_pixels // frame_width + + # Frame rate and timing + frame_rate = 30 # frames per second + frame_interval = 1.0 / frame_rate + last_frame_time = time.time() + + # Set up ffmpeg process + ffmpeg_cmd = [ + 'ffmpeg', + '-y', + '-f', 'rawvideo', + '-pix_fmt', 'gray', + '-s', f'{frame_width}x{frame_height}', + '-r', str(frame_rate), + '-i', '-', # Input from stdin + '-c:v', 'ffv1', + 'dmx_video.mkv' + ] + + self.ffmpeg_process = subprocess.Popen(ffmpeg_cmd, stdin=subprocess.PIPE) + + universes_data = {} + last_received = {} + + try: + while self.running: + current_time = time.time() + + # Receive data + try: + data, addr = self.socket.recvfrom(1024) + if data.startswith(b'Art-Net'): + op_code = struct.unpack('H', data[16:18])[0] + dmx_data = data[18:18+length] + # Store the data for this universe + universes_data[universe] = dmx_data + last_received[universe] = current_time + except BlockingIOError: + # No data received + pass + except Exception as e: + print(f"Error receiving data: {e}") + + # Check if it's time to write a frame + if current_time - last_frame_time >= frame_interval: + # Assemble frame data + frame_data = b'' + for universe in sorted(expected_universes): + dmx_data = universes_data.get(universe, b'\x00'*512) + if len(dmx_data) < 512: + dmx_data += b'\x00' * (512 - len(dmx_data)) + frame_data += dmx_data + # Convert frame_data to numpy array + frame_array = np.frombuffer(frame_data, dtype=np.uint8) + # Reshape to image dimensions + frame_array = frame_array.reshape((frame_height, frame_width)) + # Write frame to ffmpeg stdin + self.ffmpeg_process.stdin.write(frame_array.tobytes()) + last_frame_time = current_time + + time.sleep(0.001) # Sleep briefly to avoid maxing out CPU + finally: + # Clean up + self.ffmpeg_process.stdin.close() + self.ffmpeg_process.wait() + self.socket.close() + self.finished.emit() + + def stop(self): + self.running = False \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..d62f24b --- /dev/null +++ b/main.py @@ -0,0 +1,85 @@ +# main.py + +import sys +from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QWidget, QVBoxLayout +from PyQt5.QtCore import Qt +from dmx import DMXRecorder + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.initUI() + self.isRecording = False + self.recorder = None + + def initUI(self): + self.setWindowTitle("DMX Recorder") + self.resize(300, 200) + + # Use dark theme + self.setStyleSheet(''' + QWidget { + background-color: #222222; + color: #FFFFFF; + } + QSlider::groove:horizontal { + border: 1px solid #444444; + height: 8px; + background: #555555; + } + QSlider::handle:horizontal { + background: #888888; + border: 1px solid #444444; + width: 18px; + margin: -8px 0; + } + QLineEdit { + background-color: #333333; + border: 1px solid #555555; + color: #FFFFFF; + padding: 4px; + } + QPushButton { + background-color: #444444; + border: 1px solid #666666; + padding: 6px; + } + QPushButton:hover { + background-color: #555555; + } + QLabel { + color: #FFFFFF; + } + ''') + + self.central_widget = QWidget() + self.setCentralWidget(self.central_widget) + + self.layout = QVBoxLayout() + self.central_widget.setLayout(self.layout) + + self.record_button = QPushButton("Record") + self.record_button.clicked.connect(self.toggle_recording) + + self.layout.addWidget(self.record_button) + + def toggle_recording(self): + if not self.isRecording: + # Start recording + self.recorder = DMXRecorder() + self.recorder.start_recording() + self.record_button.setText("Stop") + self.isRecording = True + else: + # Stop recording + if self.recorder: + self.recorder.stop_recording() + self.recorder = None + self.record_button.setText("Record") + self.isRecording = False + +if __name__ == '__main__': + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ae0ef2a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +PyQt5>=5.15.0 +numpy>=1.19.0 \ No newline at end of file diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..8c4e6f9 --- /dev/null +++ b/run.bat @@ -0,0 +1,9 @@ +@echo off +echo Activating the virtual environment... +call venv\Scripts\activate + +echo Running main.py... +python main.py + +echo Deactivating the virtual environment... +call venv\Scripts\deactivate \ No newline at end of file diff --git a/send.bat b/send.bat new file mode 100644 index 0000000..f745b82 --- /dev/null +++ b/send.bat @@ -0,0 +1,9 @@ +@echo off +echo Activating the virtual environment... +call venv\Scripts\activate + +echo Running sender.py... +python sender.py + +echo Deactivating the virtual environment... +call venv\Scripts\deactivate \ No newline at end of file diff --git a/sender.py b/sender.py new file mode 100644 index 0000000..6df1638 --- /dev/null +++ b/sender.py @@ -0,0 +1,55 @@ +import socket +import struct +import time + +def send_dmx(universe=0, num_channels=512, fade_time=5): + UDP_IP = '255.255.255.255' # Broadcast address + UDP_PORT = 6454 + + # Create UDP socket + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + sequence = 0 + physical = 0 + + # Fading parameters + fade_steps = 100 + fade_interval = fade_time / fade_steps + + try: + while True: + # Fade up + for value in range(0, 256): + dmx_data = [value] * num_channels + dmx_packet = build_artdmx_packet(sequence, physical, universe, dmx_data) + sock.sendto(dmx_packet, (UDP_IP, UDP_PORT)) + time.sleep(fade_interval / 256) + + # Fade down + for value in range(255, -1, -1): + dmx_data = [value] * num_channels + dmx_packet = build_artdmx_packet(sequence, physical, universe, dmx_data) + sock.sendto(dmx_packet, (UDP_IP, UDP_PORT)) + time.sleep(fade_interval / 256) + + except KeyboardInterrupt: + print("DMX transmission stopped.") + finally: + sock.close() + +def build_artdmx_packet(sequence, physical, universe, dmx_data): + header = b'Art-Net\x00' # Protocol identifier + opcode = struct.pack('H', 14) # Protocol version (14 for Art-Net) + sequence = struct.pack('B', sequence) # Sequence + physical = struct.pack('B', physical) # Physical port + universe = struct.pack('H', len(dmx_data)) # Data length, big endian + data = bytes(dmx_data) # DMX data + + packet = header + opcode + protocol_version + sequence + physical + universe + length + data + return packet + +if __name__ == '__main__': + send_dmx() \ No newline at end of file diff --git a/win-install.bat b/win-install.bat new file mode 100644 index 0000000..6850f32 --- /dev/null +++ b/win-install.bat @@ -0,0 +1,14 @@ +@echo off +echo Creating virtual environment named venv... +python -m venv venv + +echo Activating virtual environment... +call venv\Scripts\activate + +echo Installing dependencies from requirements.txt... +pip install -r requirements.txt + +echo Installation complete. Virtual environment is ready to use. + +echo Deactivating the virtual environment... +call venv\Scripts\deactivate \ No newline at end of file