def __init__(self, args, file_class=File, chunk_class=Chunk, segment_class=Segment, peer_class=Peer): if hasattr(self, "_initialized") and self._initialized: return self.file_class = file_class self.chunk_class = chunk_class self.segment_class = segment_class self.peer_class = peer_class self.args = args self.sync = args.sync self.width = args.width self.height = args.height self.margin = args.margin self.show_fps = args.show_fps self.export = args.export self.capture_message_log = args.capture_message_log self.play_message_log = args.play_message_log self.waveform_gain = args.waveform_gain self._standalone = args.standalone self._target_aspect_ratio = self._get_aspect_ratio_from_args() self.logger = logging.getLogger("visualizer") self.reset() self._frame_count = 0 self.exiting = False self.time_increment = 0 self.stopwatch = Stopwatch() self._synth_instance = None self._synth_port = None self._synced = False self._layers = [] self._warned_about_missing_pan_segment = False self.gl_display_mode = GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH self._accum_enabled = False self._3d_enabled = False self.fovy = 45 self.near = 0.1 self.far = 100.0 self._fullscreen = False self._text_renderer_class = getattr(text_renderer_module, TEXT_RENDERERS[args.text_renderer]) if args.camera_script: self._camera_script = CameraScriptInterpreter(args.camera_script) else: self._camera_script = None if self.show_fps: self.fps_history = collections.deque(maxlen=10) self.previous_shown_fps_time = None if not args.standalone: if args.port: port = args.port else: port = self._get_orchestra_port() self.orchestra_host = args.host self.orchestra_port = port self.setup_osc() self.orchestra.register(self.server.port) self._screen_dumper = Exporter(".", self.margin, self.margin, self.width, self.height) if self.export: self.export_fps = args.export_fps import shutil if args.export_dir: export_dir = args.export_dir elif hasattr(args, "sessiondir"): export_dir = "%s/rendered_%s" % (args.sessiondir, self.__class__.__name__) else: export_dir = "export" if os.path.exists(export_dir): shutil.rmtree(export_dir) os.mkdir(export_dir) self.exporter = Exporter(export_dir, self.margin, self.margin, self.width, self.height) if self.play_message_log: self._message_log_reader = MessageLogReader(self.play_message_log) if self.capture_message_log: self._message_log_writer = MessageLogWriter(self.capture_message_log) self._audio_capture_start_time = None self._initialized = True
class Visualizer: def __init__(self, args, file_class=File, chunk_class=Chunk, segment_class=Segment, peer_class=Peer): if hasattr(self, "_initialized") and self._initialized: return self.file_class = file_class self.chunk_class = chunk_class self.segment_class = segment_class self.peer_class = peer_class self.args = args self.sync = args.sync self.width = args.width self.height = args.height self.margin = args.margin self.show_fps = args.show_fps self.export = args.export self.capture_message_log = args.capture_message_log self.play_message_log = args.play_message_log self.waveform_gain = args.waveform_gain self._standalone = args.standalone self._target_aspect_ratio = self._get_aspect_ratio_from_args() self.logger = logging.getLogger("visualizer") self.reset() self._frame_count = 0 self.exiting = False self.time_increment = 0 self.stopwatch = Stopwatch() self._synth_instance = None self._synth_port = None self._synced = False self._layers = [] self._warned_about_missing_pan_segment = False self.gl_display_mode = GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH self._accum_enabled = False self._3d_enabled = False self.fovy = 45 self.near = 0.1 self.far = 100.0 self._fullscreen = False self._text_renderer_class = getattr(text_renderer_module, TEXT_RENDERERS[args.text_renderer]) if args.camera_script: self._camera_script = CameraScriptInterpreter(args.camera_script) else: self._camera_script = None if self.show_fps: self.fps_history = collections.deque(maxlen=10) self.previous_shown_fps_time = None if not args.standalone: if args.port: port = args.port else: port = self._get_orchestra_port() self.orchestra_host = args.host self.orchestra_port = port self.setup_osc() self.orchestra.register(self.server.port) self._screen_dumper = Exporter(".", self.margin, self.margin, self.width, self.height) if self.export: self.export_fps = args.export_fps import shutil if args.export_dir: export_dir = args.export_dir elif hasattr(args, "sessiondir"): export_dir = "%s/rendered_%s" % (args.sessiondir, self.__class__.__name__) else: export_dir = "export" if os.path.exists(export_dir): shutil.rmtree(export_dir) os.mkdir(export_dir) self.exporter = Exporter(export_dir, self.margin, self.margin, self.width, self.height) if self.play_message_log: self._message_log_reader = MessageLogReader(self.play_message_log) if self.capture_message_log: self._message_log_writer = MessageLogWriter(self.capture_message_log) self._audio_capture_start_time = None self._initialized = True def _get_aspect_ratio_from_args(self): w, h = map(float, self.args.aspect.split(":")) return w / h def _get_orchestra_port(self): if self.args.host == "localhost": return self._read_port_from_disk() else: return self._read_port_from_network_share() def _read_port_from_disk(self): self._read_port_from_file("server_port.txt") def _read_port_from_file(self, filename): f = open(filename, "r") line = f.read() port = int(line) f.close() return port def _read_port_from_network_share(self): if platform.system() == "Linux": return self._read_port_with_unix_smbclient() elif platform.system() == "Windows": return self._read_port_via_windows_samba_access() else: raise Exception("don't know how to handle your OS (%s)" % platform.system()) def _read_port_with_unix_smbclient(self): subprocess.call( 'smbclient -N \\\\\\\\%s\\\\TorrentialForms -c "get server_port.txt server_remote_port.txt"' % self.args.host, shell=True) return self._read_port_from_file("server_remote_port.txt") def _read_port_via_windows_samba_access(self): return self._read_port_from_file( '\\\\%s\\TorrentialForms\\server_port.txt' % self.args.host) def reset(self): self.files = {} self.peers = {} self.peers_by_addr = {} self._segments_by_id = {} self.torrent_length = 0 self.torrent_title = "" self.torrent_download_completion_time = None self.num_segments = 0 self.num_received_segments = 0 self._notified_finished = False def enable_3d(self): self._3d_enabled = True def run(self): self.window_width = self.width + self.margin*2 self.window_height = self.height + self.margin*2 glutInit(sys.argv) if self.args.left is None: self._left = (glutGet(GLUT_SCREEN_WIDTH) - self.window_width) / 2 else: self._left = self.args.left if self.args.top is None: self._top = (glutGet(GLUT_SCREEN_HEIGHT) - self.window_height) / 2 else: self._top = self.args.top glutInitDisplayMode(self.gl_display_mode) glutInitWindowSize(self.window_width, self.window_height) self._non_fullscreen_window = glutCreateWindow("") glutDisplayFunc(self.DrawGLScene) glutIdleFunc(self.DrawGLScene) glutReshapeFunc(self.ReSizeGLScene) glutKeyboardFunc(self.keyPressed) self.InitGL() glutPositionWindow(self._left, self._top) if self.args.fullscreen: self._open_fullscreen_window() self._fullscreen = True self.ReSizeGLScene(self.window_width, self.window_height) glutMainLoop() def _open_fullscreen_window(self): glutGameModeString("%dx%d:32@75" % (self.window_width, self.window_height)) glutEnterGameMode() glutSetCursor(GLUT_CURSOR_NONE) glutDisplayFunc(self.DrawGLScene) glutIdleFunc(self.DrawGLScene) glutReshapeFunc(self.ReSizeGLScene) glutKeyboardFunc(self.keyPressed) self.InitGL() glutPositionWindow(self._left, self._top) def handle_torrent_message(self, num_files, download_duration, total_size, num_chunks, num_segments, encoded_torrent_title): self.num_files = num_files self.download_duration = download_duration self.total_size = total_size self.num_segments = num_segments self.torrent_title = encoded_torrent_title.decode("unicode_escape") def handle_file_message(self, filenum, offset, length): f = self.files[filenum] = self.file_class(self, filenum, offset, length) self.logger.debug("added file %s" % f) self.torrent_length += length self.added_file(f) if len(self.files) == self.num_files: self.logger.debug("added all files") self.added_all_files() def handle_chunk_message(self, chunk_id, torrent_position, byte_size, filenum, peer_id, t): if filenum in self.files: f = self.files[filenum] peer = self.peers[peer_id] begin = torrent_position - f.offset end = begin + byte_size chunk = self.chunk_class( chunk_id, begin, end, byte_size, filenum, f, peer, t, self.current_time(), self) self.files[filenum].add_chunk(chunk) else: print "ignoring chunk from undeclared file %s" % filenum def handle_segment_message(self, segment_id, torrent_position, byte_size, filenum, peer_id, t, duration): if filenum in self.files: f = self.files[filenum] peer = self.peers[peer_id] begin = torrent_position - f.offset end = begin + byte_size segment = self.segment_class( segment_id, begin, end, byte_size, filenum, f, peer, t, duration, self.current_time(), self) self._segments_by_id[segment_id] = segment self.add_segment(segment) else: print "ignoring segment from undeclared file %s" % filenum def handle_peer_message(self, peer_id, addr, bearing, pan, location): peer = self.peer_class(self, addr, bearing, pan, location) self.peers[peer_id] = peer self.peers_by_addr[addr] = peer def add_segment(self, segment): f = self.files[segment.filenum] segment.f = f segment.pan = 0.5 f.add_segment(segment) self.pan_segment(segment) segment.peer.add_segment(segment) self.num_received_segments += 1 def added_file(self, f): pass def added_all_files(self): pass def pan_segment(self, segment): if not self._warned_about_missing_pan_segment: print "WARNING: pan_segment undefined in visualizer. Orchestra and synth now control panning." self._warned_about_missing_pan_segment = True def handle_shutdown(self): self.exiting = True def handle_reset(self): self.reset() def handle_amp_message(self, segment_id, amp): try: segment = self._segments_by_id[segment_id] except KeyError: print "WARNING: amp message for unknown segment ID %s" % segment_id return self.handle_segment_amplitude(segment, amp) def handle_segment_amplitude(self, segment, amp): pass def handle_waveform_message(self, segment_id, value): try: segment = self._segments_by_id[segment_id] except KeyError: print "WARNING: waveform message for unknown segment ID %s" % segment_id return self.handle_segment_waveform_value(segment, value * self.waveform_gain) def handle_segment_waveform_value(self, segment, value): pass def handle_synth_address(self, port): self._synth_instance = None self._synth_port = port self.synth_address_received() def handle_audio_captured_started(self, start_time): self._audio_capture_start_time = float(start_time) def synth_address_received(self): pass def setup_osc(self): self.orchestra = OrchestraController(self.orchestra_host, self.orchestra_port) self.server = simple_osc_receiver.OscReceiver( listen=self.args.listen, name="Visualizer") self.server.add_method("/torrent", "ifiiis", self._handle_osc_message, "handle_torrent_message") self.server.add_method("/file", "iii", self._handle_osc_message, "handle_file_message") self.server.add_method("/chunk", "iiiiif", self._handle_osc_message, "handle_chunk_message") self.server.add_method("/segment", "iiiiiff", self._handle_osc_message, "handle_segment_message") self.server.add_method("/peer", "isffs", self._handle_osc_message, "handle_peer_message") self.server.add_method("/reset", "", self._handle_osc_message, "handle_reset") self.server.add_method("/shutdown", "", self._handle_osc_message, "handle_shutdown") self.server.add_method("/synth_address", "i", self._handle_osc_message, "handle_synth_address") self.server.add_method("/audio_captured_started", "s", self._handle_osc_message, "handle_audio_captured_started") self.server.start() self.waveform_server = None def setup_waveform_server(self): if not self._standalone: import osc_receiver self.waveform_server = osc_receiver.OscReceiver(proto=osc.UDP) self.waveform_server.add_method("/amp", "if", self._handle_osc_message, "handle_amp_message") self.waveform_server.add_method("/waveform", "if", self._handle_osc_message, "handle_waveform_message") self.waveform_server.start() def _handle_osc_message(self, path, args, types, src, handler_name): if self.capture_message_log: received_time = time.time() self._call_handler(handler_name, args) if self.capture_message_log: if self._audio_capture_start_time is None: capture_time = 0.0 print "WARNING: received OSC before audio capture started: %s" % path else: capture_time = received_time - self._audio_capture_start_time self._message_log_writer.write( capture_time, handler_name, args) def _call_handler(self, handler_name, args): handler = getattr(self, handler_name) handler(*args) def InitGL(self): glClearColor(1.0, 1.0, 1.0, 0.0) glClearAccum(0.0, 0.0, 0.0, 0.0) glClearDepth(1.0) glShadeModel(GL_SMOOTH) glutMouseFunc(self._mouse_clicked) glutMotionFunc(self._mouse_moved) glutSpecialFunc(self._special_key_pressed) def ReSizeGLScene(self, window_width, window_height): self.window_width = window_width self.window_height = window_height if window_height == 0: window_height = 1 glViewport(0, 0, window_width, window_height) self.width = window_width - 2*self.margin self.height = window_height - 2*self.margin self._aspect_ratio = float(window_width) / window_height self.min_dimension = min(self.width, self.height) self._refresh_layers() if not self._3d_enabled: self.configure_2d_projection() self.resized_window() def resized_window(self): pass def configure_2d_projection(self): glMatrixMode(GL_PROJECTION) glLoadIdentity() glOrtho(0.0, self.window_width, self.window_height, 0.0, -1.0, 1.0) glMatrixMode(GL_MODELVIEW) def _refresh_layers(self): for layer in self._layers: layer.refresh() def DrawGLScene(self): if self.exiting: self.logger.debug("total number of rendered frames: %s" % self._frame_count) if self.stopwatch.get_elapsed_time() > 0: self.logger.debug("total FPS: %s" % (float(self._frame_count) / self.stopwatch.get_elapsed_time())) if self.args.profile: import yappi yappi.print_stats(sys.stdout, yappi.SORTTYPE_TTOT) glutDestroyWindow(glutGetWindow()) return try: self._draw_gl_scene_error_handled() except Exception as error: traceback_printer.print_traceback() self.exiting = True raise error def _draw_gl_scene_error_handled(self): if self._camera_script: self._move_camera_by_script() glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) glLoadIdentity() if self.export: self.current_export_time = float(self._frame_count) / self.export_fps self.now = self.current_time() is_waiting_for_synth = (self.sync and not self._synth() and not self._synced) is_waiting_for_audio_capture_to_start = ( self.capture_message_log and self._audio_capture_start_time is None) if self._frame_count == 0 and \ not is_waiting_for_synth and \ not is_waiting_for_audio_capture_to_start: self.stopwatch.start() if self.sync: self._synced = True else: if self._frame_count == 0: self.time_increment = 0 else: self.time_increment = self.now - self.previous_frame_time self.handle_incoming_messages() self.update() if not self.capture_message_log: glTranslatef(self.margin, self.margin, 0) if self.args.border: self.draw_border() if self._3d_enabled and not self._accum_enabled: self.set_perspective( 0, 0, -self._camera_position.x, -self._camera_position.y, self._camera_position.z) self.render() if self.show_fps and self._frame_count > 0: self.update_fps_history() self.show_fps_if_timely() if self.export: self.exporter.export_frame() glutSwapBuffers() self.previous_frame_time = self.now finished = self.finished() if (self.export or self.args.exit_when_finished) and finished: self.exiting = True if not self._standalone: if finished and not self._notified_finished: self.orchestra.notify_finished() self._notified_finished = True if not is_waiting_for_synth: self._frame_count += 1 def finished(self): return False def handle_incoming_messages(self): if self.args.standalone: if self.play_message_log: self._process_message_log_until(self.now) else: self.server.serve() if self.waveform_server: self.waveform_server.serve() def _process_message_log_until(self, t): messages = self._message_log_reader.read_until(t) for _t, handler_name, args in messages: self._call_handler(handler_name, args) def update_fps_history(self): fps = 1.0 / self.time_increment self.fps_history.append(fps) def show_fps_if_timely(self): if self.previous_shown_fps_time: if (self.now - self.previous_shown_fps_time) > 1.0: self.calculate_and_show_fps() else: self.calculate_and_show_fps() def calculate_and_show_fps(self): print sum(self.fps_history) / len(self.fps_history) self.previous_shown_fps_time = self.now def draw_border(self): x1 = y1 = -1 x2 = self.width y2 = self.height glDisable(GL_LINE_SMOOTH) glLineWidth(1) glColor3f(BORDER_OPACITY, BORDER_OPACITY, BORDER_OPACITY) glBegin(GL_LINE_LOOP) glVertex2i(x1, y2) glVertex2i(x2, y2) glVertex2i(x2, y1) glVertex2i(x1, y1) glEnd() def keyPressed(self, key, x, y): if key == ESCAPE: # stop_all disabled as it also deletes ~reverb # self._synth().stop_all() self.exiting = True elif key == 's': self._dump_screen() elif key == 'f': if self._fullscreen: glutSetCursor(GLUT_CURSOR_INHERIT) glutLeaveGameMode() glutSetWindow(self._non_fullscreen_window) self._fullscreen = False else: self._open_fullscreen_window() self._fullscreen = True def _dump_screen(self): self._screen_dumper.export_frame() def playing_segment(self, segment): if not self._standalone: self.orchestra.visualizing_segment(segment.id) segment.playing = True def current_time(self): if self.export: return self.current_export_time else: return self.stopwatch.get_elapsed_time() def set_color(self, color_vector, alpha=1.0): glColor4f(color_vector[0], color_vector[1], color_vector[2], alpha) def set_listener_position(self, x, y): self.orchestra.set_listener_position(x, y) def set_listener_orientation(self, orientation): self.orchestra.set_listener_orientation(-orientation) def place_segment(self, segment_id, x, y, duration): self.orchestra.place_segment(segment_id, -x, y, duration) def _mouse_clicked(self, button, state, x, y): if self._3d_enabled: if button == GLUT_LEFT_BUTTON: self._dragging_orientation = (state == GLUT_DOWN) else: self._dragging_orientation = False if button == GLUT_RIGHT_BUTTON: self._dragging_y_position = (state == GLUT_DOWN) if state == GLUT_DOWN: self._drag_x_previous = x self._drag_y_previous = y def _mouse_moved(self, x, y): if self._3d_enabled: if self._dragging_orientation: self._disable_camera_script() self._set_camera_orientation( self._camera_y_orientation + x - self._drag_x_previous, self._camera_x_orientation - y + self._drag_y_previous) self._print_camera_settings() elif self._dragging_y_position: self._disable_camera_script() self._camera_position.y += CAMERA_Y_SPEED * (y - self._drag_y_previous) self._print_camera_settings() self._drag_x_previous = x self._drag_y_previous = y def _disable_camera_script(self): self._camera_script = None def _special_key_pressed(self, key, x, y): if self._3d_enabled: r = math.radians(self._camera_y_orientation) new_position = self._camera_position if key == GLUT_KEY_LEFT: new_position.x += CAMERA_KEY_SPEED * math.cos(r) new_position.z += CAMERA_KEY_SPEED * math.sin(r) elif key == GLUT_KEY_RIGHT: new_position.x -= CAMERA_KEY_SPEED * math.cos(r) new_position.z -= CAMERA_KEY_SPEED * math.sin(r) elif key == GLUT_KEY_UP: new_position.x += CAMERA_KEY_SPEED * math.cos(r + math.pi/2) new_position.z += CAMERA_KEY_SPEED * math.sin(r + math.pi/2) elif key == GLUT_KEY_DOWN: new_position.x -= CAMERA_KEY_SPEED * math.cos(r + math.pi/2) new_position.z -= CAMERA_KEY_SPEED * math.sin(r + math.pi/2) self._set_camera_position(new_position) self._print_camera_settings() def _print_camera_settings(self): print print "%s, %s, %s" % ( self._camera_position.v, self._camera_y_orientation, self._camera_x_orientation) def _set_camera_position(self, position): self._camera_position = position if not self._standalone: self.set_listener_position(position.z, position.x) def _set_camera_orientation(self, y_orientation, x_orientation): self._camera_y_orientation = y_orientation self._camera_x_orientation = x_orientation if not self._standalone: self.set_listener_orientation(y_orientation) def set_perspective(self, pixdx, pixdy, eyedx, eyedy, eyedz): assert self._3d_enabled fov2 = ((self.fovy*math.pi) / 180.0) / 2.0 top = self.near * math.tan(fov2) bottom = -top right = top * self._aspect_ratio left = -right xwsize = right - left ywsize = top - bottom # dx = -(pixdx*xwsize/self.width + eyedx*self.near/focus) # dy = -(pixdy*ywsize/self.height + eyedy*self.near/focus) # I don't understand why this modification solved the problem (focus was 1.0) dx = -(pixdx*xwsize/self.width) dy = -(pixdy*ywsize/self.height) glMatrixMode(GL_PROJECTION) glLoadIdentity() glFrustum (left + dx, right + dx, bottom + dy, top + dy, self.near, self.far) glMatrixMode(GL_MODELVIEW) glLoadIdentity() glRotatef(self._camera_x_orientation, 1.0, 0.0, 0.0) glRotatef(self._camera_y_orientation, 0.0, 1.0, 0.0) glTranslatef(self._camera_position.x, self._camera_position.y, self._camera_position.z) def enable_accum(self): self.gl_display_mode |= GLUT_ACCUM self._accum_enabled = True def accum(self, render_method): glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ACCUM_BUFFER_BIT) for jitter in range(NUM_ACCUM_SAMPLES): glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) self.set_perspective(ACCUM_JITTER[jitter][0], ACCUM_JITTER[jitter][1], -self._camera_position.x, -self._camera_position.y, self._camera_position.z) render_method() glAccum(GL_ACCUM, 1.0/NUM_ACCUM_SAMPLES) glAccum(GL_RETURN, 1.0) def subscribe_to_amp(self): if not self.waveform_server: self.setup_waveform_server() self._synth().subscribe_to_amp(self.waveform_server.port) def subscribe_to_waveform(self): if not self._standalone: if not self.waveform_server: self.setup_waveform_server() self._synth().subscribe_to_waveform(self.waveform_server.port) def _move_camera_by_script(self): position, orientation = self._camera_script.position_and_orientation( self.current_time()) self._set_camera_position(position) self._set_camera_orientation(orientation.y, orientation.x) def new_layer(self, rendering_function): layer = Layer(rendering_function, self.new_display_list_id()) self._layers.append(layer) return layer def new_display_list_id(self): return glGenLists(1) def _synth(self): if self._synth_instance is None and self._synth_port: from synth_controller import SynthController self._synth_instance = SynthController(self.logger) self._synth_instance.connect(self._synth_port) return self._synth_instance def draw_text(self, text, size, x, y, font=None, spacing=None, v_align="left", h_align="top"): if font is None: font = self.args.font self.text_renderer(text, size, font).render(x, y, v_align, h_align) def text_renderer(self, text, size, font=None): if font is None: font = self.args.font return self._text_renderer_class(self, text, size, font, aspect_ratio=self._target_aspect_ratio) def download_completed(self): if self.torrent_download_completion_time: return True else: if self.num_segments > 0 and self.num_received_segments == self.num_segments and not self.active(): self.torrent_download_completion_time = self.current_time() return True def active(self): return False def update(self): pass @staticmethod def add_parser_arguments(parser): parser.add_argument("-host", type=str, default="localhost") parser.add_argument('-port', type=int) parser.add_argument("-listen", type=str) parser.add_argument('-sync', action='store_true') try: parser.add_argument('-width', dest='width', type=int, default=1024) parser.add_argument('-height', dest='height', type=int, default=768) except argparse.ArgumentError: pass parser.add_argument("-left", type=int) parser.add_argument("-top", type=int) parser.add_argument('-margin', dest='margin', type=int, default=0) parser.add_argument('-show-fps', dest='show_fps', action='store_true') parser.add_argument('-capture-message-log', dest='capture_message_log') parser.add_argument('-play-message-log', dest='play_message_log') parser.add_argument('-export', dest='export', action='store_true') parser.add_argument('-export-fps', dest='export_fps', default=25.0, type=float) parser.add_argument('-export-dir') parser.add_argument("-waveform", dest="waveform", action='store_true') parser.add_argument("-waveform-gain", dest="waveform_gain", default=1, type=float) parser.add_argument("-camera-script", dest="camera_script", type=str) parser.add_argument("-border", action="store_true") parser.add_argument("-fullscreen", action="store_true") parser.add_argument("-standalone", action="store_true") parser.add_argument("-profile", action="store_true") parser.add_argument("-check-opengl-errors", action="store_true") parser.add_argument("-exit-when-finished", action="store_true") parser.add_argument("--text-renderer", choices=TEXT_RENDERERS.keys(), default="glut") parser.add_argument("--font", type=str) parser.add_argument("-aspect", type=str, default="1:1", help="Target aspect ratio (e.g. 16:9)") @staticmethod def add_margin_argument(parser, name): parser.add_argument(name, type=str, default="0,0,0,0", help="top,right,bottom,left in relative units") def parse_margin_argument(self, argument_string): return MarginAttributes.from_argument(argument_string, self)