Browse Source

10/31/2024

master
Joe DiPrima 12 months ago
parent
commit
894cbd504b
  1. 4
      .gitignore
  2. 212
      dmx.py
  3. 116
      main.py

4
.gitignore

@ -1,4 +1,6 @@
venv
__pycache__
*.mkv
*.json
*.json
push.bat
pull.bat

212
dmx.py

@ -8,50 +8,55 @@ import os
class DMXRecorder(QObject):
frame_ready = pyqtSignal(np.ndarray)
def __init__(self):
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()
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)
self.recording_thread.finished.connect(self.recording_thread.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
# 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()
if self.thread:
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):
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 = "0.0.0.0"
UDP_PORT = 6454
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))
@ -82,7 +87,6 @@ class RecordingThread(QObject):
ffmpeg_executable += '.exe'
# Ensure the ffmpeg executable is executable
# For Unix-like systems, set the execute permission if needed
if not os.access(ffmpeg_executable, os.X_OK):
if os.name != 'nt':
os.chmod(ffmpeg_executable, 0o755)
@ -114,7 +118,6 @@ class RecordingThread(QObject):
return
universes_data = {}
last_received = {}
try:
while self.running:
@ -134,7 +137,6 @@ class RecordingThread(QObject):
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
@ -178,4 +180,188 @@ class RecordingThread(QObject):
self.finished.emit()
def stop(self):
self.running = False
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

116
main.py

@ -1,19 +1,26 @@
# main.py
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QWidget, QVBoxLayout, QLabel
import json
import os
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QWidget, QVBoxLayout, QLabel, QSpinBox, QLineEdit, QGridLayout
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QImage, QPixmap, qRgb
from dmx import DMXRecorder
from dmx import DMXRecorder, DMXPlayer
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.settings = self.load_settings()
self.initUI()
self.isRecording = False
self.isPlaying = False
self.recorder = None
self.player = None
def initUI(self):
self.setWindowTitle("DMX Recorder")
self.resize(300, 400)
self.setWindowTitle("DMX Recorder and Player")
self.resize(300, 500)
# Use dark theme
self.setStyleSheet('''
@ -60,15 +67,57 @@ class MainWindow(QMainWindow):
self.record_button = QPushButton("Record")
self.record_button.clicked.connect(self.toggle_recording)
self.play_button = QPushButton("Play")
self.play_button.clicked.connect(self.toggle_playback)
# Input fields for settings
self.universes_label = QLabel("Universes to Playback:")
self.universes_spinbox = QSpinBox()
self.universes_spinbox.setMinimum(1)
self.universes_spinbox.setMaximum(4) # Maximum universes recorded
self.universes_spinbox.setValue(self.settings.get('universes', 4))
# Port number
self.port_label = QLabel("Port Number:")
self.port_input = QLineEdit(str(self.settings.get('port', 6454)))
# Transmit Address (Playback)
self.transmit_label = QLabel("Transmit Address:")
self.transmit_input = QLineEdit(self.settings.get('transmit_address', '255.255.255.255'))
# Listen Address (Recording)
self.listen_label = QLabel("Listen Address:")
self.listen_input = QLineEdit(self.settings.get('listen_address', '0.0.0.0'))
self.video_label = QLabel()
self.video_label.setAlignment(Qt.AlignCenter)
self.layout.addWidget(self.video_label)
# Add settings input fields to the layout
settings_layout = QGridLayout()
settings_layout.addWidget(self.universes_label, 0, 0)
settings_layout.addWidget(self.universes_spinbox, 0, 1)
settings_layout.addWidget(self.port_label, 1, 0)
settings_layout.addWidget(self.port_input, 1, 1)
settings_layout.addWidget(self.transmit_label, 2, 0)
settings_layout.addWidget(self.transmit_input, 2, 1)
settings_layout.addWidget(self.listen_label, 3, 0)
settings_layout.addWidget(self.listen_input, 3, 1)
self.layout.addLayout(settings_layout)
self.layout.addWidget(self.record_button)
self.layout.addWidget(self.play_button)
def toggle_recording(self):
if not self.isRecording:
if self.isPlaying:
# Can't record while playing
return
# Get settings
port = int(self.port_input.text())
listen_address = self.listen_input.text()
# Start recording
self.recorder = DMXRecorder()
self.recorder = DMXRecorder(listen_address=listen_address, port=port)
self.recorder.frame_ready.connect(self.update_frame)
self.recorder.start_recording()
self.record_button.setText("Stop")
@ -82,6 +131,38 @@ class MainWindow(QMainWindow):
self.record_button.setText("Record")
self.isRecording = False
def toggle_playback(self):
if not self.isPlaying:
if self.isRecording:
# Can't play while recording
return
# Get settings
num_universes_to_playback = self.universes_spinbox.value()
port = int(self.port_input.text())
transmit_address = self.transmit_input.text()
# Start playback
self.player = DMXPlayer(num_universes=num_universes_to_playback, transmit_address=transmit_address, port=port)
self.player.frame_ready.connect(self.update_frame)
self.player.playback_finished.connect(self.on_playback_finished)
self.player.start_playback()
self.play_button.setText("Stop")
self.isPlaying = True
else:
# Stop playback
if self.player:
self.player.stop_playback()
self.player = None
self.play_button.setText("Play")
self.isPlaying = False
def on_playback_finished(self):
if self.isPlaying:
self.play_button.setText("Play")
self.isPlaying = False
if self.player:
self.player.frame_ready.disconnect(self.update_frame)
self.player = None
def update_frame(self, frame_array):
# Convert the numpy array to QImage and display
height, width = frame_array.shape
@ -96,6 +177,31 @@ class MainWindow(QMainWindow):
# Set the pixmap to the label
self.video_label.setPixmap(pixmap)
def closeEvent(self, event):
# Save settings to JSON file
self.save_settings()
super().closeEvent(event)
def load_settings(self):
# Load settings from settings.json if it exists
settings_file = 'settings.json'
if os.path.exists(settings_file):
with open(settings_file, 'r') as f:
return json.load(f)
else:
# Return default settings
return {}
def save_settings(self):
settings = {
'universes': self.universes_spinbox.value(),
'port': int(self.port_input.text()),
'transmit_address': self.transmit_input.text(),
'listen_address': self.listen_input.text(),
}
with open('settings.json', 'w') as f:
json.dump(settings, f)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()

Loading…
Cancel
Save