DMX caputre/playback software
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

367 lines
14 KiB

from PyQt5.QtCore import QObject, QThread, pyqtSignal
import socket
import struct
import numpy as np
import subprocess
import time
import os
class DMXRecorder(QObject):
frame_ready = pyqtSignal(np.ndarray)
def __init__(self, listen_address='0.0.0.0', port=6454):
super().__init__()
self.thread = None
self.recording_thread = None
self.listen_address = listen_address
self.port = port
def start_recording(self):
self.thread = QThread()
self.recording_thread = RecordingThread(listen_address=self.listen_address, port=self.port)
self.recording_thread.moveToThread(self.thread)
self.thread.started.connect(self.recording_thread.run)
self.recording_thread.finished.connect(self.thread.quit)
# Connect frame_ready signal
self.recording_thread.frame_ready.connect(self.frame_ready)
self.thread.finished.connect(self.thread.deleteLater)
self.recording_thread.finished.connect(self.recording_thread.deleteLater)
self.thread.start()
def stop_recording(self):
if self.recording_thread:
self.recording_thread.stop()
self.thread.quit()
self.thread.wait()
self.recording_thread = None
self.thread = None
class RecordingThread(QObject):
finished = pyqtSignal()
frame_ready = pyqtSignal(np.ndarray)
def __init__(self, listen_address='0.0.0.0', port=6454):
super().__init__()
self.running = False
self.socket = None
self.ffmpeg_process = None
self.listen_address = listen_address
self.port = port
def run(self):
self.running = True
# Set up UDP socket to listen for Art-Net DMX data
UDP_IP = self.listen_address
UDP_PORT = self.port
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()
# Adjust the path to the ffmpeg executable
ffmpeg_executable = os.path.join(os.path.dirname(__file__), 'bin', 'ffmpeg')
# For Windows, add .exe extension
if os.name == 'nt':
ffmpeg_executable += '.exe'
# Ensure the ffmpeg executable is executable
if not os.access(ffmpeg_executable, os.X_OK):
if os.name != 'nt':
os.chmod(ffmpeg_executable, 0o755)
else:
# On Windows, os.access may not check execute permissions accurately
if not os.path.isfile(ffmpeg_executable):
print(f"ffmpeg executable not found at: {ffmpeg_executable}")
self.finished.emit()
return
# Set up ffmpeg process using the local executable path
ffmpeg_cmd = [
ffmpeg_executable,
'-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'
]
try:
self.ffmpeg_process = subprocess.Popen(ffmpeg_cmd, stdin=subprocess.PIPE)
except Exception as e:
print(f"Error starting ffmpeg: {e}")
self.finished.emit()
return
universes_data = {}
try:
while self.running:
current_time = time.time()
# Receive data
try:
data, addr = self.socket.recvfrom(2048)
if data.startswith(b'Art-Net'):
op_code = struct.unpack('<H', data[8:10])[0]
# OpCode for ArtDMX is 0x5000
if op_code == 0x5000:
# Parse universe
universe = struct.unpack('<H', data[14:16])[0]
# Get length
length = struct.unpack('>H', data[16:18])[0]
dmx_data = data[18:18+length]
# Store the data for this universe
universes_data[universe] = dmx_data
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' * universe_size)
if len(dmx_data) < universe_size:
dmx_data += b'\x00' * (universe_size - 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
if self.ffmpeg_process.stdin:
try:
self.ffmpeg_process.stdin.write(frame_array.tobytes())
except BrokenPipeError:
print("FFmpeg process has terminated unexpectedly.")
self.running = False
# Emit frame for visualization
self.frame_ready.emit(frame_array)
last_frame_time = current_time
time.sleep(0.001) # Sleep briefly to avoid maxing out CPU
finally:
# Clean up
if self.ffmpeg_process and self.ffmpeg_process.stdin:
try:
self.ffmpeg_process.stdin.close()
self.ffmpeg_process.wait()
except Exception as e:
print(f"Error closing ffmpeg process: {e}")
self.socket.close()
self.finished.emit()
def stop(self):
self.running = False
class DMXPlayer(QObject):
frame_ready = pyqtSignal(np.ndarray)
playback_finished = pyqtSignal()
def __init__(self, num_universes, transmit_address='255.255.255.255', port=6454):
super().__init__()
self.thread = None
self.playback_thread = None
self.num_universes = num_universes
self.transmit_address = transmit_address
self.port = port
def start_playback(self):
self.thread = QThread()
self.playback_thread = PlaybackThread(self.num_universes, transmit_address=self.transmit_address, port=self.port)
self.playback_thread.moveToThread(self.thread)
self.thread.started.connect(self.playback_thread.run)
self.playback_thread.finished.connect(self.thread.quit)
# Connect signals
self.playback_thread.frame_ready.connect(self.frame_ready)
self.playback_thread.finished.connect(self.on_playback_thread_finished)
self.thread.finished.connect(self.on_thread_finished)
self.thread.start()
def on_playback_thread_finished(self):
self.playback_finished.emit()
# Wait for the thread to exit before cleaning up
self.thread.quit()
self.thread.wait()
self.playback_thread = None
def on_thread_finished(self):
self.thread = None
def stop_playback(self):
if self.playback_thread:
self.playback_thread.stop()
self.thread.quit()
self.thread.wait()
self.playback_thread = None
self.thread = None
class PlaybackThread(QObject):
finished = pyqtSignal()
frame_ready = pyqtSignal(np.ndarray)
def __init__(self, num_universes, transmit_address='255.255.255.255', port=6454):
super().__init__()
self.num_universes = num_universes
self.running = False
self.ffmpeg_process = None
self.socket = None
self.transmit_address = transmit_address
self.port = port
def run(self):
self.running = True
# Set up UDP socket to send Art-Net DMX data
UDP_IP = self.transmit_address
UDP_PORT = self.port
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Enable broadcasting mode if necessary
if UDP_IP == '255.255.255.255':
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
# Adjust the path to the ffmpeg executable
ffmpeg_executable = os.path.join(os.path.dirname(__file__), 'bin', 'ffmpeg')
# For Windows, add .exe extension
if os.name == 'nt':
ffmpeg_executable += '.exe'
# Ensure the ffmpeg executable is executable
if not os.access(ffmpeg_executable, os.X_OK):
if os.name != 'nt':
os.chmod(ffmpeg_executable, 0o755)
else:
# On Windows, os.access may not check execute permissions accurately
if not os.path.isfile(ffmpeg_executable):
print(f"ffmpeg executable not found at: {ffmpeg_executable}")
self.finished.emit()
return
# Define frame dimensions (must match recording)
frame_width = 64 # Must match recording settings
universe_size = 512 # DMX universe size
num_universes_in_file = 4 # Universes recorded (0 to 3)
total_pixels_in_file = num_universes_in_file * universe_size
frame_height_in_file = total_pixels_in_file // frame_width
frame_rate = 30 # frames per second
frame_interval = 1.0 / frame_rate
# Compute playback dimensions based on number of universes to playback
total_pixels_playback = self.num_universes * universe_size
frame_height_playback = total_pixels_playback // frame_width
# FFmpeg command to read frames
ffmpeg_cmd = [
ffmpeg_executable,
'-i', 'dmx_video.mkv', # Input file
'-f', 'rawvideo',
'-pix_fmt', 'gray',
'-s', f'{frame_width}x{frame_height_in_file}',
'-r', str(frame_rate),
'-an',
'-sn',
'-'
]
try:
self.ffmpeg_process = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE)
except Exception as e:
print(f"Error starting ffmpeg: {e}")
self.finished.emit()
return
# Read frames from ffmpeg stdout
frame_size = frame_width * frame_height_in_file
sequence = 0
try:
while self.running:
# Read frame data
frame_data = self.ffmpeg_process.stdout.read(frame_size)
if not frame_data or len(frame_data) < frame_size:
# End of file
break
# Extract data for the specified number of universes
total_pixels_playback_bytes = self.num_universes * universe_size
frame_data_playback = frame_data[:total_pixels_playback_bytes]
# Convert frame_data to numpy array
frame_array = np.frombuffer(frame_data_playback, dtype=np.uint8)
# Reshape to image dimensions
frame_array = frame_array.reshape((frame_height_playback, frame_width))
# Send Art-Net data
# Split frame_data into universes
expected_universes = sorted(range(self.num_universes))
for idx, universe in enumerate(expected_universes):
start_idx = idx * universe_size
end_idx = start_idx + universe_size
universe_data = frame_data_playback[start_idx:end_idx]
# Construct ArtDMX packet
packet = self.build_artdmx_packet(sequence, universe, universe_data)
# Send packet over UDP
self.socket.sendto(packet, (UDP_IP, UDP_PORT))
sequence = (sequence + 1) % 256
# Emit frame for visualization
self.frame_ready.emit(frame_array)
# Sleep to synchronize to frame rate
time.sleep(frame_interval)
finally:
if self.ffmpeg_process:
self.ffmpeg_process.stdout.close()
self.ffmpeg_process.wait()
self.socket.close()
self.finished.emit()
def stop(self):
self.running = False
def build_artdmx_packet(self, sequence, universe, data):
packet = bytearray()
packet.extend(b'Art-Net\x00') # ID
packet.extend(struct.pack('<H', 0x5000)) # OpCode ArtDMX
packet.extend(struct.pack('>H', 14)) # Protocol Version (High byte first)
packet.extend(struct.pack('B', sequence)) # Sequence
packet.extend(struct.pack('B', 0x00)) # Physical
packet.extend(struct.pack('<H', universe)) # Universe (Low byte first)
length = len(data)
packet.extend(struct.pack('>H', length)) # Length (High byte first)
packet.extend(data)
return packet