diff --git a/transcoder/main.py b/transcoder/main.py index e325f88..2579ffd 100644 --- a/transcoder/main.py +++ b/transcoder/main.py @@ -3,6 +3,7 @@ import argparse import movie +import video parser = argparse.ArgumentParser( description='Transcode videos to ][Vision format.') @@ -23,6 +24,11 @@ parser.add_argument( help='Allows skipping frames of input video to lower effective output ' 'frame rate, which may give better quality for some videos.' ) +parser.add_argument( + '--video_mode', type=str, choices=video.Mode.__members__.keys(), + help='Video display mode to encode for (HGR/DHGR)' +) + def main(args): filename = args.input @@ -30,7 +36,8 @@ def main(args): filename, every_n_video_frames=args.every_n_video_frames, audio_normalization=args.audio_normalization, - max_bytes_out = 1024. * 1024 * args.max_output_mb + max_bytes_out=1024. * 1024 * args.max_output_mb, + video_mode=video.Mode[args.video_mode] ) print("Input frame rate = %f" % m.video.input_frame_rate) diff --git a/transcoder/movie.py b/transcoder/movie.py index 6270d78..253e441 100644 --- a/transcoder/movie.py +++ b/transcoder/movie.py @@ -13,15 +13,17 @@ class Movie: self, filename: str, every_n_video_frames: int = 1, audio_normalization: float = None, - max_bytes_out: int = None + max_bytes_out: int = None, + video_mode: video.Mode = video.Mode.HGR, ): self.filename = filename # type: str self.every_n_video_frames = every_n_video_frames # type: int self.max_bytes_out = max_bytes_out # type: int + self.video_mode = video_mode # type: video.Mode self.audio = audio.Audio( filename, normalization=audio_normalization) # type: audio.Audio - self.video = video.Video(filename) # type: video.Video + self.video = video.Video(filename, mode=video_mode) # type: video.Video self.stream_pos = 0 # type: int @@ -44,8 +46,10 @@ class Movie: :return: """ video_frames = self.video.frames() + main_seq = None + aux_seq = None - yield opcodes.Header(mode=video.Mode.DHGR) + yield opcodes.Header(mode=self.video_mode) for au in self.audio.audio_stream(): self.cycles += self.audio.cycles_per_tick @@ -56,9 +60,11 @@ class Movie: print("Starting frame %d" % self.video.frame_number) main_seq = self.video.encode_frame( main, self.video.memory_map, self.video.update_priority) - aux_seq = self.video.encode_frame( - aux, self.video.aux_memory_map, - self.video.aux_update_priority) + + if aux: + aux_seq = self.video.encode_frame( + aux, self.video.aux_memory_map, + self.video.aux_update_priority) # au has range -15 .. 16 (step=1) # Tick cycles are units of 2 @@ -95,10 +101,13 @@ class Movie: if socket_pos >= 2044: # 2 op_ack address bytes + 2 payload bytes from ACK must # terminate 2K stream frame + if self.video_mode == video.Mode.DHGR: + # Flip-flop between MAIN and AUX banks + self.aux_memory_bank = not self.aux_memory_bank + yield from self._emit_bytes(opcodes.Ack(self.aux_memory_bank)) assert self.stream_pos % 2048 == 0, self.stream_pos % 2048 - # Flip-flop between MAIN and AUX banks - self.aux_memory_bank = not self.aux_memory_bank + yield from self._emit_bytes(op) yield from self.done() diff --git a/transcoder/opcodes.py b/transcoder/opcodes.py index bc58091..0c9d4da 100644 --- a/transcoder/opcodes.py +++ b/transcoder/opcodes.py @@ -112,7 +112,7 @@ class Ack(Opcode): def emit_data(self) -> Iterator[int]: # Flip $C054 or $C055 soft-switches to steer subsequent writes to # MAIN/AUX screen memory - yield 0x54 if self.aux_active else 0x55 + yield 0x55 if self.aux_active else 0x54 # Dummy byte to pad out TCP frame yield 0xff diff --git a/transcoder/video.py b/transcoder/video.py index 4f8fbc5..899ec4d 100644 --- a/transcoder/video.py +++ b/transcoder/video.py @@ -32,8 +32,10 @@ class Video: def __init__( self, filename: str, + mode: Mode = Mode.HGR ): self.filename = filename # type: str + self.mode = mode # type: Mode self._reader = skvideo.io.FFmpegReader(filename) @@ -51,13 +53,14 @@ class Video: # Initialize empty screen self.memory_map = screen.MemoryMap( screen_page=1) # type: screen.MemoryMap - self.aux_memory_map = screen.MemoryMap( - screen_page=1) # type: screen.MemoryMap + if self.mode == mode.DHGR: + self.aux_memory_map = screen.MemoryMap( + screen_page=1) # type: screen.MemoryMap # Accumulates pending edit weights across frames self.update_priority = np.zeros((32, 256), dtype=np.int64) - - self.aux_update_priority = np.zeros((32, 256), dtype=np.int64) + if self.mode == mode.DHGR: + self.aux_update_priority = np.zeros((32, 256), dtype=np.int64) def tick(self, cycles: int) -> bool: if cycles > (self.cycles_per_frame * self.frame_number): @@ -117,30 +120,65 @@ class Video: q = queue.Queue(maxsize=10) + def _hgr_decode(_idx, _frame): + outfile = "%s/%08dC.BIN" % (frame_dir, _idx) + bmpfile = "%s/%08d.bmp" % (frame_dir, _idx) + + try: + os.stat(outfile) + except FileNotFoundError: + _frame = _frame.resize((280, 192), resample=Image.LANCZOS) + _frame.save(bmpfile) + + # TODO: parametrize palette + subprocess.call([ + "/usr/local/bin/bmp2dhr", bmpfile, "hgr", + "P0", # Kegs32 RGB Color palette(for //gs playback) + "D9" # Buckels dither + ]) + + os.remove(bmpfile) + + _main = np.fromfile(outfile, dtype=np.uint8) + + return _main, None + + def _dhgr_decode(_idx, _frame): + mainfile = "%s/%08d.BIN" % (frame_dir, _idx) + auxfile = "%s/%08d.AUX" % (frame_dir, _idx) + + bmpfile = "%s/%08d.bmp" % (frame_dir, _idx) + + try: + os.stat(mainfile) + os.stat(auxfile) + except FileNotFoundError: + _frame = _frame.resize((280, 192), resample=Image.LANCZOS) + _frame.save(bmpfile) + + # TODO: parametrize palette + subprocess.call([ + "/usr/local/bin/bmp2dhr", bmpfile, "dhgr", + "P0", # Kegs32 RGB Color palette (for //gs playback) + "A", # Output separate .BIN and .AUX files + "D9" # Buckels dither + ]) + + os.remove(bmpfile) + + _main = np.fromfile(mainfile, dtype=np.uint8) + _aux = np.fromfile(auxfile, dtype=np.uint8) + + return _main, _aux + def worker(): """Invoke bmp2dhr to encode input image frames and push to queue.""" for _idx, _frame in enumerate(self._frame_grabber()): - mainfile = "%s/%08d.BIN" % (frame_dir, _idx) - auxfile = "%s/%08d.AUX" % (frame_dir, _idx) - - bmpfile = "%s/%08d.bmp" % (frame_dir, _idx) - - try: - os.stat(mainfile) - os.stat(auxfile) - except FileNotFoundError: - _frame = _frame.resize((280, 192), resample=Image.LANCZOS) - _frame.save(bmpfile) - - subprocess.call( - ["/usr/local/bin/bmp2dhr", bmpfile, "dhgr", "P0", "A", - "D9"]) - - os.remove(bmpfile) - - main = np.fromfile(mainfile, dtype=np.uint8) - aux = np.fromfile(auxfile, dtype=np.uint8) - q.put((main, aux)) + if self.mode == Mode.DHGR: + res = _dhgr_decode(_idx, _frame) + else: + res = _hgr_decode(_idx, _frame) + q.put(res) q.put((None, None)) @@ -148,14 +186,19 @@ class Video: t.start() while True: + main, aux = q.get() if main is None: break - yield ( - screen.FlatMemoryMap(screen_page=1, data=main).to_memory_map(), - screen.FlatMemoryMap(screen_page=1, data=aux).to_memory_map() - ) + main_map = screen.FlatMemoryMap( + screen_page=1, data=main).to_memory_map() + if aux is None: + aux_map = None + else: + aux_map = screen.FlatMemoryMap( + screen_page=1, data=aux).to_memory_map() + yield (main_map, aux_map) q.task_done() t.join()