From b1132e97f1d5a53794fe08799f95f9d68976ae33 Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 17 Jan 2023 21:09:18 +0000 Subject: [PATCH 01/19] Remove pregenerated data files, they are using up all my LFS quota --- transcoder/data/.gitattributes | 1 - transcoder/data/DHGR_palette_0_edit_distance.pickle.bz2 | 3 --- transcoder/data/DHGR_palette_5_edit_distance.pickle.bz2 | 3 --- transcoder/data/HGR_palette_0_edit_distance.pickle.bz2 | 3 --- transcoder/data/HGR_palette_5_edit_distance.pickle.bz2 | 3 --- 5 files changed, 13 deletions(-) delete mode 100644 transcoder/data/.gitattributes delete mode 100644 transcoder/data/DHGR_palette_0_edit_distance.pickle.bz2 delete mode 100644 transcoder/data/DHGR_palette_5_edit_distance.pickle.bz2 delete mode 100644 transcoder/data/HGR_palette_0_edit_distance.pickle.bz2 delete mode 100644 transcoder/data/HGR_palette_5_edit_distance.pickle.bz2 diff --git a/transcoder/data/.gitattributes b/transcoder/data/.gitattributes deleted file mode 100644 index 7e1ef73..0000000 --- a/transcoder/data/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.bz2 filter=lfs diff=lfs merge=lfs -text diff --git a/transcoder/data/DHGR_palette_0_edit_distance.pickle.bz2 b/transcoder/data/DHGR_palette_0_edit_distance.pickle.bz2 deleted file mode 100644 index e669108..0000000 --- a/transcoder/data/DHGR_palette_0_edit_distance.pickle.bz2 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b47eadfdf8c8e16c6539f9a16ed0b5a393b17e0cbd03831aacda7f659e9522d6 -size 120830327 diff --git a/transcoder/data/DHGR_palette_5_edit_distance.pickle.bz2 b/transcoder/data/DHGR_palette_5_edit_distance.pickle.bz2 deleted file mode 100644 index b06628c..0000000 --- a/transcoder/data/DHGR_palette_5_edit_distance.pickle.bz2 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8c245981f91ffa89b47abdd1c9d646c2e79499a0c82c38c91234be0a59e52f1f -size 118832545 diff --git a/transcoder/data/HGR_palette_0_edit_distance.pickle.bz2 b/transcoder/data/HGR_palette_0_edit_distance.pickle.bz2 deleted file mode 100644 index ed26b42..0000000 --- a/transcoder/data/HGR_palette_0_edit_distance.pickle.bz2 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3fd52feb08eb6f99b267a1050c68905f25d0d106ad7c2c63473cc0a0f6aa1b25 -size 224334626 diff --git a/transcoder/data/HGR_palette_5_edit_distance.pickle.bz2 b/transcoder/data/HGR_palette_5_edit_distance.pickle.bz2 deleted file mode 100644 index 9ca1922..0000000 --- a/transcoder/data/HGR_palette_5_edit_distance.pickle.bz2 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dbf83e3d0b6c7867ccf7ae1d55a6ed4e906409b08043dec514e1104cec95f0fc -size 220565577 From efe821d215536ddef1ddbff5db2641c8ce410976 Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 17 Jan 2023 21:09:34 +0000 Subject: [PATCH 02/19] Fix warning --- transcoder/audio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/transcoder/audio.py b/transcoder/audio.py index 278e555..40fb4e5 100644 --- a/transcoder/audio.py +++ b/transcoder/audio.py @@ -55,8 +55,8 @@ class Audio: 'float32').reshape((f.channels, -1), order='F') a = librosa.core.to_mono(data) - a = librosa.resample(a, f.samplerate, - self.sample_rate, + a = librosa.resample(a, orig_sr=f.samplerate, + target_sr=self.sample_rate, res_type='scipy', scale=True).flatten() return a From 5c728e7ff1b903dfdcba79f6adc39e40c3a9c2f8 Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 17 Jan 2023 21:10:49 +0000 Subject: [PATCH 03/19] Allow edit transposes again, there doesn't seem to be a good reason to prevent it and I'm not sure it can even happen in practice --- transcoder/make_data_tables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transcoder/make_data_tables.py b/transcoder/make_data_tables.py index bb438b2..a539f5c 100644 --- a/transcoder/make_data_tables.py +++ b/transcoder/make_data_tables.py @@ -39,7 +39,7 @@ class EditDistanceParams: # Smallest substitution value is ~20 from palette.diff_matrices, i.e. # we always prefer to transpose 2 pixels rather than substituting colours. # TODO: is quality really better allowing transposes? - transpose_costs = np.ones((128, 128), dtype=np.float64) * 100000 # 10 + transpose_costs = np.ones((128, 128), dtype=np.float64) # These will be filled in later substitute_costs = np.zeros((128, 128), dtype=np.float64) From 0a3c81c8c6f92963b22081254b79517e036bde3e Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 17 Jan 2023 21:39:05 +0000 Subject: [PATCH 04/19] - have Bitmap.apply() update the memory representation instead of requiring callers to keep track of it - stop trying to cache content_deltas, I think it results in losing deltas. Instead just recompute the deltas for each page as we need it. This is fast enough in practice. - track the average fill rate for the additional offsets we emit. This should be close to 3 if we're succeeding in finding enough collateral work - overhaul how we pass in the target memory maps. The previous way didn't make sense: we weren't actually encoding for the target video frame, but were using an inconsistent mix of old and new frames. I think this was causing image artifacting because we were aiming for the wrong thing. - Add some debugging assertions that were used to track this down. --- transcoder/movie.py | 43 ++++++++++---- transcoder/screen.py | 58 ++++++++++++++++++- transcoder/video.py | 134 +++++++++++++++++++++++-------------------- 3 files changed, 161 insertions(+), 74 deletions(-) diff --git a/transcoder/movie.py b/transcoder/movie.py index 73dedff..d32be20 100644 --- a/transcoder/movie.py +++ b/transcoder/movie.py @@ -6,6 +6,7 @@ import audio import frame_grabber import machine import opcodes +import screen import video from palette import Palette from video_mode import VideoMode @@ -58,34 +59,54 @@ class Movie: :return: """ video_frames = self.frame_grabber.frames() - main_seq = None - aux_seq = None + op_seq = None yield opcodes.Header(mode=self.video_mode) + last_memory_bank = self.aux_memory_bank for au in self.audio.audio_stream(): self.ticks += 1 - if self.video.tick(self.ticks): + new_video_frame = self.video.tick(self.ticks) + if new_video_frame: try: main, aux = next(video_frames) except StopIteration: break - if ((self.video.frame_number - 1) % self.every_n_video_frames - == 0): - print("Starting frame %d" % self.video.frame_number) - main_seq = self.video.encode_frame(main, is_aux=False) + should_encode_frame = ( + (self.video.frame_number - 1) % + self.every_n_video_frames == 0 + ) + if should_encode_frame: + if self.video_mode == VideoMode.DHGR: + target_pixelmap = screen.DHGRBitmap( + main_memory=main, + aux_memory=aux, + palette=self.palette + ) + else: + target_pixelmap = screen.HGRBitmap( + main_memory=main, + palette=self.palette + ) - if aux: - aux_seq = self.video.encode_frame(aux, is_aux=True) + print("Starting frame %d" % self.video.frame_number) + op_seq = self.video.encode_frame( + target_pixelmap, is_aux=self.aux_memory_bank) + self.video.out_of_work = {True: False, False: False} + + if self.aux_memory_bank != last_memory_bank: + # We've flipped memory banks, start new opcode sequence + last_memory_bank = self.aux_memory_bank + op_seq = self.video.encode_frame( + target_pixelmap, is_aux=self.aux_memory_bank) # au has range -15 .. 16 (step=1) # Tick cycles are units of 2 tick = au * 2 # -30 .. 32 (step=2) tick += 34 # 4 .. 66 (step=2) - (page, content, offsets) = next( - aux_seq if self.aux_memory_bank else main_seq) + (page, content, offsets) = next(op_seq) yield opcodes.TICK_OPCODES[(tick, page)](content, offsets) diff --git a/transcoder/screen.py b/transcoder/screen.py index 73cc7f2..4ac9400 100644 --- a/transcoder/screen.py +++ b/transcoder/screen.py @@ -268,6 +268,11 @@ class Bitmap: byte_offset, self.packed[page, packed_offset], value) self._fix_scalar_neighbours(page, packed_offset, byte_offset) + if is_aux: + self.aux_memory.write(page, offset, value) + else: + self.main_memory.write(page, offset, value) + def _fix_scalar_neighbours( self, page: int, @@ -445,6 +450,51 @@ class Bitmap: return diff + # TODO: combine with _diff_weights + # TODO: unit test + def _diff_weights_page( + self, + source_packed: np.ndarray, + target_packed: np.ndarray, + is_aux: bool, + content: np.uint8 = None + ) -> np.ndarray: + """Computes edit distance matrix from source_packed to self.packed + + If content is set, the distance will be computed as if this value + was stored into each offset position of source_packed, i.e. to + allow evaluating which offsets (if any) should be chosen for storing + this content byte. + """ + + diff = np.ndarray((256,), dtype=np.int32) + + offsets = self._byte_offsets(is_aux) + + dists = [] + for o in offsets: + if content is not None: + compare_packed = self.masked_update(o, source_packed, content) + self._fix_array_neighbours(compare_packed, o) + else: + compare_packed = source_packed + + # Pixels influenced by byte offset o + source_pixels = self.mask_and_shift_data(compare_packed, o) + target_pixels = self.mask_and_shift_data(target_packed, o) + + # Concatenate N-bit source and target into 2N-bit values + pair = (source_pixels << self.MASKED_BITS) + target_pixels + dist = self.edit_distances(self.palette)[o][pair].reshape( + pair.shape) + dists.append(dist) + + # Interleave even/odd columns + diff[0::2] = dists[0] + diff[1::2] = dists[1] + + return diff + def _check_consistency(self): """Sanity check that headers and footers are consistent.""" @@ -474,8 +524,9 @@ class Bitmap: assert ok # TODO: unit tests - def compute_delta( + def compute_delta_page( self, + page: int, content: int, diff_weights: np.ndarray, is_aux: bool @@ -490,7 +541,10 @@ class Bitmap: """ # TODO: use error edit distance? - new_diff = self._diff_weights(self.packed, is_aux, content) + packed_page = self.packed[page, :].reshape(1, -1) + + new_diff = self._diff_weights_page( + packed_page, packed_page, is_aux, content) # TODO: try different weightings return (new_diff * 5) - diff_weights diff --git a/transcoder/video.py b/transcoder/video.py index 81e14b2..7806710 100644 --- a/transcoder/video.py +++ b/transcoder/video.py @@ -27,13 +27,16 @@ class Video: ): self.mode = mode # type: VideoMode self.frame_grabber = frame_grabber # type: FrameGrabber - self.ticks_per_second = ticks_per_second # type: float + self.ticks_per_second = float(ticks_per_second) # type: float self.ticks_per_frame = ( self.ticks_per_second / frame_grabber.input_frame_rate ) # type: float self.frame_number = 0 # type: int self.palette = palette # type: Palette + self._opcodes = 0 + self._offsets = 0 + # Initialize empty screen self.memory_map = screen.MemoryMap( screen_page=1) # type: screen.MemoryMap @@ -57,6 +60,10 @@ class Video: if self.mode == mode.DHGR: self.aux_update_priority = np.zeros((32, 256), dtype=np.int32) + # Indicates whether we have run out of work for the main/aux banks. + # Key is True for aux bank and False for main bank + self.out_of_work = {True: False, False: False} + def tick(self, ticks: int) -> bool: """Keep track of when it is time for a new image frame.""" @@ -67,7 +74,7 @@ class Video: def encode_frame( self, - target: screen.MemoryMap, + target: screen.Bitmap, is_aux: bool, ) -> Iterator[opcodes.Opcode]: """Converge towards target frame in priority order of edit distance.""" @@ -84,6 +91,8 @@ class Video: memory_map.page_offset[screen.SCREEN_HOLES]) == 0 print("Similarity %f" % (update_priority.mean())) + if self._opcodes: + print("Opcode fill rate %f" % (self._offsets / self._opcodes)) yield from self._index_changes( memory_map, target, update_priority, is_aux) @@ -91,30 +100,16 @@ class Video: def _index_changes( self, source: screen.MemoryMap, - target: screen.MemoryMap, + target_pixelmap: screen.Bitmap, update_priority: np.array, - is_aux: True + is_aux: bool ) -> Iterator[Tuple[int, int, List[int]]]: """Transform encoded screen to sequence of change tuples.""" - if self.mode == VideoMode.DHGR: - if is_aux: - target_pixelmap = screen.DHGRBitmap( - main_memory=self.memory_map, - aux_memory=target, - palette=self.palette - ) - else: - target_pixelmap = screen.DHGRBitmap( - main_memory=target, - aux_memory=self.aux_memory_map, - palette=self.palette - ) + if self.mode == VideoMode.DHGR and is_aux: + target = target_pixelmap.aux_memory else: - target_pixelmap = screen.HGRBitmap( - main_memory=target, - palette=self.palette - ) + target = target_pixelmap.main_memory diff_weights = target_pixelmap.diff_weights(self.pixelmap, is_aux) # Don't bother storing into screen holes @@ -124,11 +119,10 @@ class Video: # with new frame update_priority[diff_weights == 0] = 0 update_priority += diff_weights + assert np.all(update_priority >= 0) priorities = self._heapify_priorities(update_priority) - content_deltas = {} - while priorities: pri, _, page, offset = heapq.heappop(priorities) @@ -152,23 +146,14 @@ class Video: diff_weights[page, offset] = 0 # Update memory maps - source.page_offset[page, offset] = content self.pixelmap.apply(page, offset, is_aux, content) - # Make sure we don't emit this offset as a side-effect of some - # other offset later. - for cd in content_deltas.values(): - cd[page, offset] = 0 - # TODO: what if we add another content_deltas entry later? - # We might clobber it again - # Need to find 3 more offsets to fill this opcode for err, o in self._compute_error( page, content, target_pixelmap, diff_weights, - content_deltas, is_aux ): assert o != offset @@ -180,13 +165,6 @@ class Video: # Someone already resolved this diff. continue - # Make sure we don't end up considering this (page, offset) - # again until the next image frame. Even if a better match - # comes along, it's probably better to fix up some other byte. - # TODO: or should we recompute it with new error? - for cd in content_deltas.values(): - cd[page, o] = 0 - byte_offset = target_pixelmap.byte_offset(o, is_aux) old_packed = target_pixelmap.packed[page, o // 2] @@ -196,13 +174,11 @@ class Video: # Update priority for the offset we're emitting update_priority[page, o] = p - source.page_offset[page, o] = content self.pixelmap.apply(page, o, is_aux, content) - if p: # This content byte introduced an error, so put back on the # heap in case we can get back to fixing it exactly - # during this frame. Otherwise we'll get to it later. + # during this frame. Otherwise, we'll get to it later. heapq.heappush( priorities, (-p, random.getrandbits(8), page, o)) @@ -210,13 +186,34 @@ class Video: if len(offsets) == 3: break + # Record how many additional offsets we were able to fill + self._opcodes += 1 + self._offsets += len(offsets) # Pad to 4 if we didn't find enough for _ in range(len(offsets), 4): offsets.append(offsets[0]) - yield (page + 32, content, offsets) + yield page + 32, content, offsets - # # TODO: there is still a bug causing residual diffs when we have - # # apparently run out of work to do + self.out_of_work[is_aux] = True + + # These debugging assertions validate that when we are out of work, + # our source and target representations should be identical. + # + # They only work correctly for palettes that do not have identical + # colours (e.g. IIGS but not NTSC which has two identical greys). + # + # The problem is that if we have substituted one grey for the other + # there may be no diff if they are part of an extended run of greys. + # + # The only difference is at the end of the run where these produce + # different artifact colours, but this may only be visible in the + # other bank. + # + # It may take several iterations of main/aux before we will notice and + # correct all of these differences. That means we don't have a + # deterministic point in time when we can assert that all diffs should + # have been resolved. + # TODO: add flag to enable debug assertions if not np.array_equal(source.page_offset, target.page_offset): diffs = np.nonzero(source.page_offset != target.page_offset) for i in range(len(diffs[0])): @@ -238,12 +235,28 @@ class Video: diff_p, diff_o, source.page_offset[diff_p, diff_o], target.page_offset[diff_p, diff_o] )) - # assert False + assert False + + # If we've finished both main and aux pages, there should be no residual + # diffs in packed representation + all_done = self.out_of_work[True] and self.out_of_work[False] + if all_done and not np.array_equal(self.pixelmap.packed, + target_pixelmap.packed): + diffs = np.nonzero( + self.pixelmap.packed != target_pixelmap.packed) + print("is_aux: %s" % is_aux) + for i in range(len(diffs[0])): + diff_p = diffs[0][i] + diff_o = diffs[1][i] + print("(%d, %d): got %d want %d" % ( + diff_p, diff_o, self.pixelmap.packed[diff_p, diff_o], + target_pixelmap.packed[diff_p, diff_o])) + assert False # If we run out of things to do, pad forever content = target.page_offset[0, 0] while True: - yield (32, content, [0, 0, 0, 0]) + yield 32, content, [0, 0, 0, 0] @staticmethod def _heapify_priorities(update_priority: np.array) -> List: @@ -254,7 +267,9 @@ class Video: pages, offsets = update_priority.nonzero() priorities = [tuple(data) for data in np.stack(( -update_priority[pages, offsets], - # Don't use deterministic order for page, offset + # Don't use deterministic order for page, offset. Otherwise, + # we get the "venetian blind" effect when filling large blocks of + # colour. np.random.randint(0, 2 ** 8, size=pages.shape[0]), pages, offsets) @@ -265,24 +280,21 @@ class Video: _OFFSETS = np.arange(256) - def _compute_error(self, page, content, target_pixelmap, diff_weights, - content_deltas, is_aux): + def _compute_error( + self, page, content, target_pixelmap, diff_weights, is_aux): """Build priority queue of other offsets at which to store content. Ordered by offsets which are closest to the target content value. """ - # TODO: move this up into parent - delta_screen = content_deltas.get(content) - if delta_screen is None: - delta_screen = target_pixelmap.compute_delta( - content, diff_weights, is_aux) - content_deltas[content] = delta_screen - - delta_page = delta_screen[page] + delta_page = target_pixelmap.compute_delta_page( + page, content, diff_weights[page, :], is_aux) cond = delta_page < 0 candidate_offsets = self._OFFSETS[cond] priorities = delta_page[cond] + # Don't use deterministic order for page, offset. Otherwise, + # we get the "venetian blind" effect when filling large blocks of + # colour. deltas = [ (priorities[i], random.getrandbits(8), candidate_offsets[i]) for i in range(len(candidate_offsets)) @@ -290,8 +302,8 @@ class Video: heapq.heapify(deltas) while deltas: - pri, _, o = heapq.heappop(deltas) + pri, _, offset = heapq.heappop(deltas) assert pri < 0 - assert o <= 255 + assert 0 <= offset <= 255 - yield -pri, o + yield -pri, offset From f7f59506373769c11e624f3aab0b9f9fe9d879e5 Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 17 Jan 2023 21:42:28 +0000 Subject: [PATCH 05/19] - Stop using the 5x weighting for diff values, it was preventing from consistently finding enough additional offsets (~2.5x avg instead of >2.9) - Remove instrumentation for fill rate now that it's served its purpose --- transcoder/screen.py | 3 +-- transcoder/video.py | 8 -------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/transcoder/screen.py b/transcoder/screen.py index 4ac9400..486fe3e 100644 --- a/transcoder/screen.py +++ b/transcoder/screen.py @@ -546,8 +546,7 @@ class Bitmap: new_diff = self._diff_weights_page( packed_page, packed_page, is_aux, content) - # TODO: try different weightings - return (new_diff * 5) - diff_weights + return new_diff - diff_weights class HGRBitmap(Bitmap): diff --git a/transcoder/video.py b/transcoder/video.py index 7806710..fef6e83 100644 --- a/transcoder/video.py +++ b/transcoder/video.py @@ -34,9 +34,6 @@ class Video: self.frame_number = 0 # type: int self.palette = palette # type: Palette - self._opcodes = 0 - self._offsets = 0 - # Initialize empty screen self.memory_map = screen.MemoryMap( screen_page=1) # type: screen.MemoryMap @@ -91,8 +88,6 @@ class Video: memory_map.page_offset[screen.SCREEN_HOLES]) == 0 print("Similarity %f" % (update_priority.mean())) - if self._opcodes: - print("Opcode fill rate %f" % (self._offsets / self._opcodes)) yield from self._index_changes( memory_map, target, update_priority, is_aux) @@ -186,9 +181,6 @@ class Video: if len(offsets) == 3: break - # Record how many additional offsets we were able to fill - self._opcodes += 1 - self._offsets += len(offsets) # Pad to 4 if we didn't find enough for _ in range(len(offsets), 4): offsets.append(offsets[0]) From 7384878ecb2e98add4cc451ba86b2ea9c2971dda Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 17 Jan 2023 21:44:43 +0000 Subject: [PATCH 06/19] Disable debugging assertions now they've served their purpose --- transcoder/video.py | 76 ++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/transcoder/video.py b/transcoder/video.py index fef6e83..fe90147 100644 --- a/transcoder/video.py +++ b/transcoder/video.py @@ -206,44 +206,44 @@ class Video: # deterministic point in time when we can assert that all diffs should # have been resolved. # TODO: add flag to enable debug assertions - if not np.array_equal(source.page_offset, target.page_offset): - diffs = np.nonzero(source.page_offset != target.page_offset) - for i in range(len(diffs[0])): - diff_p = diffs[0][i] - diff_o = diffs[1][i] - - # For HGR, 0x00 or 0x7f may be visually equivalent to the same - # bytes with high bit set (depending on neighbours), so skip - # them - if (source.page_offset[diff_p, diff_o] & 0x7f) == 0 and \ - (target.page_offset[diff_p, diff_o] & 0x7f) == 0: - continue - - if (source.page_offset[diff_p, diff_o] & 0x7f) == 0x7f and \ - (target.page_offset[diff_p, diff_o] & 0x7f) == 0x7f: - continue - - print("Diff at (%d, %d): %d != %d" % ( - diff_p, diff_o, source.page_offset[diff_p, diff_o], - target.page_offset[diff_p, diff_o] - )) - assert False - - # If we've finished both main and aux pages, there should be no residual - # diffs in packed representation - all_done = self.out_of_work[True] and self.out_of_work[False] - if all_done and not np.array_equal(self.pixelmap.packed, - target_pixelmap.packed): - diffs = np.nonzero( - self.pixelmap.packed != target_pixelmap.packed) - print("is_aux: %s" % is_aux) - for i in range(len(diffs[0])): - diff_p = diffs[0][i] - diff_o = diffs[1][i] - print("(%d, %d): got %d want %d" % ( - diff_p, diff_o, self.pixelmap.packed[diff_p, diff_o], - target_pixelmap.packed[diff_p, diff_o])) - assert False + # if not np.array_equal(source.page_offset, target.page_offset): + # diffs = np.nonzero(source.page_offset != target.page_offset) + # for i in range(len(diffs[0])): + # diff_p = diffs[0][i] + # diff_o = diffs[1][i] + # + # # For HGR, 0x00 or 0x7f may be visually equivalent to the same + # # bytes with high bit set (depending on neighbours), so skip + # # them + # if (source.page_offset[diff_p, diff_o] & 0x7f) == 0 and \ + # (target.page_offset[diff_p, diff_o] & 0x7f) == 0: + # continue + # + # if (source.page_offset[diff_p, diff_o] & 0x7f) == 0x7f and \ + # (target.page_offset[diff_p, diff_o] & 0x7f) == 0x7f: + # continue + # + # print("Diff at (%d, %d): %d != %d" % ( + # diff_p, diff_o, source.page_offset[diff_p, diff_o], + # target.page_offset[diff_p, diff_o] + # )) + # assert False + # + # # If we've finished both main and aux pages, there should be no residual + # # diffs in packed representation + # all_done = self.out_of_work[True] and self.out_of_work[False] + # if all_done and not np.array_equal(self.pixelmap.packed, + # target_pixelmap.packed): + # diffs = np.nonzero( + # self.pixelmap.packed != target_pixelmap.packed) + # print("is_aux: %s" % is_aux) + # for i in range(len(diffs[0])): + # diff_p = diffs[0][i] + # diff_o = diffs[1][i] + # print("(%d, %d): got %d want %d" % ( + # diff_p, diff_o, self.pixelmap.packed[diff_p, diff_o], + # target_pixelmap.packed[diff_p, diff_o])) + # assert False # If we run out of things to do, pad forever content = target.page_offset[0, 0] From 89633aa8453d1a626b38549765130478651c3315 Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 17 Jan 2023 21:47:26 +0000 Subject: [PATCH 07/19] Add release notes for v0.3 --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d2cab67..947a2b5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ]\[-Vision v0.2 +# ]\[-Vision v0.3 Streaming video and audio for the Apple II. @@ -71,6 +71,12 @@ TODO: download instructions ## Release Notes +### v0.3 (17 Jan 2023) + +- Fixed an image quality bug in the transcoder +- Quality of life improvements to installation process +- Stop using LFS to store the generated data files in git, they're using up my quota + ### v0.2 (19 July 2019) #### Transcoder From 1d5bcfd74ebc6dcbbe51b414efd01949696cb5a4 Mon Sep 17 00:00:00 2001 From: kris Date: Wed, 18 Jan 2023 00:01:34 +0000 Subject: [PATCH 08/19] Optimize make_data_tables and use numpy.save instead of pickling. The file sizes are a bit larger but it unblocks updating to python 3.8. --- README.md | 13 ++++---- transcoder/make_data_tables.py | 54 +++++++++++++++++----------------- transcoder/screen.py | 15 +++++----- 3 files changed, 39 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 947a2b5..88cce23 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,7 @@ TODO: link video once it is available. ## Installation -This currently requires python3.7 because some dependencies (e.g. weighted-levenshtein) don't compile with 3.9+, and 3.8 -has a [bug](https://bugs.python.org/issue44439) in object pickling. +This currently requires python3.7 because some dependencies (e.g. weighted-levenshtein) don't compile with 3.9+. ``` python3.7 -m venv venv @@ -59,23 +58,21 @@ source venv/bin/activate pip install -r requirements.txt ``` -To generate the data files required by the transcoder: +Before you can run the transcoder you need to generate the data files it requires: ``` % python transcoder/make_data_tables.py ``` -This takes about 3 hours on my machine. - -TODO: download instructions +This is a one-time setup. It takes about 90 minutes on my machine. ## Release Notes ### v0.3 (17 Jan 2023) - Fixed an image quality bug in the transcoder -- Quality of life improvements to installation process -- Stop using LFS to store the generated data files in git, they're using up my quota +- Documentation/quality of life improvements to installation process +- Stop using LFS to store the generated data files in git, they're using up all my quota ### v0.2 (19 July 2019) diff --git a/transcoder/make_data_tables.py b/transcoder/make_data_tables.py index a539f5c..baf464b 100644 --- a/transcoder/make_data_tables.py +++ b/transcoder/make_data_tables.py @@ -113,7 +113,7 @@ def compute_edit_distance( edp: EditDistanceParams, bitmap_cls: Type[screen.Bitmap], nominal_colours: Type[colours.NominalColours] -): +) -> np.ndarray: """Computes edit distance matrix between all pairs of pixel strings. Enumerates all possible values of the masked bit representation from @@ -131,44 +131,45 @@ def compute_edit_distance( bitrange = np.uint64(2 ** bits) - edit = [] - for _ in range(len(bitmap_cls.BYTE_MASKS)): - edit.append( - np.zeros(shape=np.uint64(bitrange * bitrange), dtype=np.uint16)) + edit = np.zeros( + shape=(len(bitmap_cls.BYTE_MASKS), np.uint64(bitrange * bitrange)), + dtype=np.uint16) - # Matrix is symmetrical with zero diagonal so only need to compute upper - # triangle - bar = ProgressBar((bitrange * (bitrange - 1)) / 2, max_width=80) + bar = ProgressBar( + bitrange * (bitrange - 1) / 2 * len(bitmap_cls.PHASES), max_width=80) num_dots = bitmap_cls.MASKED_DOTS cnt = 0 for i in range(np.uint64(bitrange)): - for j in range(i): - cnt += 1 + pair_base = np.uint64(i) << bits + for o, ph in enumerate(bitmap_cls.PHASES): + # Compute this in the outer loop since it's invariant under j + first_dots = bitmap_cls.to_dots(i, byte_offset=o) + first_pixels = pixel_string( + colours.dots_to_nominal_colour_pixel_values( + num_dots, first_dots, nominal_colours, + init_phase=ph) + ) - if cnt % 10000 == 0: - bar.numerator = cnt - print(bar, end='\r') - sys.stdout.flush() + # Matrix is symmetrical with zero diagonal so only need to compute + # upper triangle + for j in range(i): + cnt += 1 + if cnt % 100000 == 0: + bar.numerator = cnt + print(bar, end='\r') + sys.stdout.flush() - pair = (np.uint64(i) << bits) + np.uint64(j) + pair = pair_base + np.uint64(j) - for o, ph in enumerate(bitmap_cls.PHASES): - first_dots = bitmap_cls.to_dots(i, byte_offset=o) second_dots = bitmap_cls.to_dots(j, byte_offset=o) - - first_pixels = pixel_string( - colours.dots_to_nominal_colour_pixel_values( - num_dots, first_dots, nominal_colours, - init_phase=ph) - ) second_pixels = pixel_string( colours.dots_to_nominal_colour_pixel_values( num_dots, second_dots, nominal_colours, init_phase=ph) ) - edit[o][pair] = edit_distance( + edit[o, pair] = edit_distance( edp, first_pixels, second_pixels, error=False) return edit @@ -183,10 +184,9 @@ def make_edit_distance( """Write file containing (D)HGR edit distance matrix for a palette.""" dist = compute_edit_distance(edp, bitmap_cls, nominal_colours) - data = "transcoder/data/%s_palette_%d_edit_distance.pickle.bz2" % ( + data = "transcoder/data/%s_palette_%d_edit_distance.npz" % ( bitmap_cls.NAME, pal.ID.value) - with bz2.open(data, "wb", compresslevel=9) as out: - pickle.dump(dist, out, protocol=pickle.HIGHEST_PROTOCOL) + np.savez_compressed(data, edit_distance=dist) def main(): diff --git a/transcoder/screen.py b/transcoder/screen.py index 486fe3e..780f45c 100644 --- a/transcoder/screen.py +++ b/transcoder/screen.py @@ -342,15 +342,13 @@ class Bitmap: @classmethod @functools.lru_cache(None) - def edit_distances(cls, palette_id: pal.Palette) -> List[np.ndarray]: + def edit_distances(cls, palette_id: pal.Palette) -> np.ndarray: """Load edit distance matrices for masked, shifted byte values.""" - data = "transcoder/data/%s_palette_%d_edit_distance.pickle.bz2" % ( - cls.NAME, - palette_id.value + data = "transcoder/data/%s_palette_%d_edit_distance.npz" % ( + cls.NAME, palette_id.value ) - with bz2.open(data, "rb") as ed: - dist = pickle.load(ed) # type: List[np.ndarray] + dist = np.load(data)['edit_distance'] # dist is an upper-triangular matrix of edit_distance(a, b) # encoded as dist[(a << N) + b] = edit_distance(a, b) @@ -363,8 +361,8 @@ class Bitmap: (identity & np.uint64(2 ** cls.MASKED_BITS - 1)) << cls.MASKED_BITS) - for i in range(len(dist)): - dist[i][transpose] += dist[i][identity] + for i in range(dist.shape[0]): + dist[i, transpose] += dist[i, identity] return dist @@ -741,6 +739,7 @@ class HGRBitmap(Bitmap): return double @classmethod + @functools.lru_cache(None) def to_dots(cls, masked_val: int, byte_offset: int) -> int: """Convert masked representation to bit sequence of display dots. From 9cea6f7d187cb2a9374c556ef19ae26bacd209e2 Mon Sep 17 00:00:00 2001 From: kris Date: Wed, 18 Jan 2023 00:02:44 +0000 Subject: [PATCH 09/19] Update to python 3.8 --- README.md | 4 ++-- requirements.txt | 26 ++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 88cce23..7815f75 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,10 @@ TODO: link video once it is available. ## Installation -This currently requires python3.7 because some dependencies (e.g. weighted-levenshtein) don't compile with 3.9+. +This currently requires python3.8 because some dependencies (e.g. weighted-levenshtein) don't compile with 3.9+. ``` -python3.7 -m venv venv +python3.8 -m venv venv source venv/bin/activate pip install -r requirements.txt ``` diff --git a/requirements.txt b/requirements.txt index 3a29dfe..e303a70 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,30 @@ +appdirs==1.4.4 audioread==3.0.0 +certifi==2022.12.7 +cffi==1.15.1 +charset-normalizer==3.0.1 colormath==3.0.0 +decorator==5.1.1 etaprogress==1.1.1 +idna==3.4 +importlib-metadata==6.0.0 +joblib==1.2.0 librosa==0.9.2 -networkx==2.6.3 -numpy==1.21.6 +llvmlite==0.39.1 +networkx==3.0 +numba==0.56.4 +numpy==1.23.5 +packaging==23.0 Pillow==9.4.0 -scikit-learn==1.0.2 +pooch==1.6.0 +pycparser==2.21 +requests==2.28.2 +resampy==0.4.2 +scikit-learn==1.2.0 scikit-video==1.1.11 -scipy==1.7.3 +scipy==1.10.0 soundfile==0.11.0 +threadpoolctl==3.1.0 +urllib3==1.26.14 weighted-levenshtein==0.2.1 +zipp==3.11.0 \ No newline at end of file From 157d7596d7000d52e25a7a9427cf42455f0b5c2f Mon Sep 17 00:00:00 2001 From: kris Date: Wed, 18 Jan 2023 21:25:19 +0000 Subject: [PATCH 10/19] Downgrade numpy until colormath supports 1.23+ --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index e303a70..f8c99d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ librosa==0.9.2 llvmlite==0.39.1 networkx==3.0 numba==0.56.4 -numpy==1.23.5 +numpy==1.22.4 # Until colormath supports 1.23+ packaging==23.0 Pillow==9.4.0 pooch==1.6.0 @@ -27,4 +27,4 @@ soundfile==0.11.0 threadpoolctl==3.1.0 urllib3==1.26.14 weighted-levenshtein==0.2.1 -zipp==3.11.0 \ No newline at end of file +zipp==3.11.0 From 6b612ffb0ab0d516210aa449870c3226f76f146c Mon Sep 17 00:00:00 2001 From: kris Date: Wed, 18 Jan 2023 21:25:52 +0000 Subject: [PATCH 11/19] Normalize audio at 0.5/99.5%iles to clip less --- transcoder/audio.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/transcoder/audio.py b/transcoder/audio.py index 40fb4e5..f537be2 100644 --- a/transcoder/audio.py +++ b/transcoder/audio.py @@ -64,8 +64,8 @@ class Audio: def _normalization(self, read_bytes=1024 * 1024 * 10): """Read first read_bytes of audio stream and compute normalization. - We compute the 2.5th and 97.5th percentiles i.e. only 2.5% of samples - will clip. + We normalize based on the 0.5th and 99.5th percentiles, i.e. only <1% of + samples will clip. :param read_bytes: :return: @@ -77,7 +77,7 @@ class Audio: if len(raw) > read_bytes: break a = self._decode(f, raw) - norm = np.max(np.abs(np.percentile(a, [2.5, 97.5]))) + norm = np.max(np.abs(np.percentile(a, [0.5, 99.5]))) return 16384. / norm From 990e1c9d745553f025d9b806aeaf0a342e75935a Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 17 Jan 2023 21:39:05 +0000 Subject: [PATCH 12/19] - have Bitmap.apply() update the memory representation instead of requiring callers to keep track of it - stop trying to cache content_deltas, I think it results in losing deltas. Instead just recompute the deltas for each page as we need it. This is fast enough in practice. - track the average fill rate for the additional offsets we emit. This should be close to 3 if we're succeeding in finding enough collateral work - overhaul how we pass in the target memory maps. The previous way didn't make sense: we weren't actually encoding for the target video frame, but were using an inconsistent mix of old and new frames. I think this was causing image artifacting because we were aiming for the wrong thing. - Add some debugging assertions that were used to track this down. --- transcoder/video.py | 84 +++++++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/transcoder/video.py b/transcoder/video.py index fe90147..7806710 100644 --- a/transcoder/video.py +++ b/transcoder/video.py @@ -34,6 +34,9 @@ class Video: self.frame_number = 0 # type: int self.palette = palette # type: Palette + self._opcodes = 0 + self._offsets = 0 + # Initialize empty screen self.memory_map = screen.MemoryMap( screen_page=1) # type: screen.MemoryMap @@ -88,6 +91,8 @@ class Video: memory_map.page_offset[screen.SCREEN_HOLES]) == 0 print("Similarity %f" % (update_priority.mean())) + if self._opcodes: + print("Opcode fill rate %f" % (self._offsets / self._opcodes)) yield from self._index_changes( memory_map, target, update_priority, is_aux) @@ -181,6 +186,9 @@ class Video: if len(offsets) == 3: break + # Record how many additional offsets we were able to fill + self._opcodes += 1 + self._offsets += len(offsets) # Pad to 4 if we didn't find enough for _ in range(len(offsets), 4): offsets.append(offsets[0]) @@ -206,44 +214,44 @@ class Video: # deterministic point in time when we can assert that all diffs should # have been resolved. # TODO: add flag to enable debug assertions - # if not np.array_equal(source.page_offset, target.page_offset): - # diffs = np.nonzero(source.page_offset != target.page_offset) - # for i in range(len(diffs[0])): - # diff_p = diffs[0][i] - # diff_o = diffs[1][i] - # - # # For HGR, 0x00 or 0x7f may be visually equivalent to the same - # # bytes with high bit set (depending on neighbours), so skip - # # them - # if (source.page_offset[diff_p, diff_o] & 0x7f) == 0 and \ - # (target.page_offset[diff_p, diff_o] & 0x7f) == 0: - # continue - # - # if (source.page_offset[diff_p, diff_o] & 0x7f) == 0x7f and \ - # (target.page_offset[diff_p, diff_o] & 0x7f) == 0x7f: - # continue - # - # print("Diff at (%d, %d): %d != %d" % ( - # diff_p, diff_o, source.page_offset[diff_p, diff_o], - # target.page_offset[diff_p, diff_o] - # )) - # assert False - # - # # If we've finished both main and aux pages, there should be no residual - # # diffs in packed representation - # all_done = self.out_of_work[True] and self.out_of_work[False] - # if all_done and not np.array_equal(self.pixelmap.packed, - # target_pixelmap.packed): - # diffs = np.nonzero( - # self.pixelmap.packed != target_pixelmap.packed) - # print("is_aux: %s" % is_aux) - # for i in range(len(diffs[0])): - # diff_p = diffs[0][i] - # diff_o = diffs[1][i] - # print("(%d, %d): got %d want %d" % ( - # diff_p, diff_o, self.pixelmap.packed[diff_p, diff_o], - # target_pixelmap.packed[diff_p, diff_o])) - # assert False + if not np.array_equal(source.page_offset, target.page_offset): + diffs = np.nonzero(source.page_offset != target.page_offset) + for i in range(len(diffs[0])): + diff_p = diffs[0][i] + diff_o = diffs[1][i] + + # For HGR, 0x00 or 0x7f may be visually equivalent to the same + # bytes with high bit set (depending on neighbours), so skip + # them + if (source.page_offset[diff_p, diff_o] & 0x7f) == 0 and \ + (target.page_offset[diff_p, diff_o] & 0x7f) == 0: + continue + + if (source.page_offset[diff_p, diff_o] & 0x7f) == 0x7f and \ + (target.page_offset[diff_p, diff_o] & 0x7f) == 0x7f: + continue + + print("Diff at (%d, %d): %d != %d" % ( + diff_p, diff_o, source.page_offset[diff_p, diff_o], + target.page_offset[diff_p, diff_o] + )) + assert False + + # If we've finished both main and aux pages, there should be no residual + # diffs in packed representation + all_done = self.out_of_work[True] and self.out_of_work[False] + if all_done and not np.array_equal(self.pixelmap.packed, + target_pixelmap.packed): + diffs = np.nonzero( + self.pixelmap.packed != target_pixelmap.packed) + print("is_aux: %s" % is_aux) + for i in range(len(diffs[0])): + diff_p = diffs[0][i] + diff_o = diffs[1][i] + print("(%d, %d): got %d want %d" % ( + diff_p, diff_o, self.pixelmap.packed[diff_p, diff_o], + target_pixelmap.packed[diff_p, diff_o])) + assert False # If we run out of things to do, pad forever content = target.page_offset[0, 0] From 6a8d49bd9743f88dac802067deddf393245717cc Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 17 Jan 2023 21:42:28 +0000 Subject: [PATCH 13/19] - Stop using the 5x weighting for diff values, it was preventing from consistently finding enough additional offsets (~2.5x avg instead of >2.9) - Remove instrumentation for fill rate now that it's served its purpose --- transcoder/video.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/transcoder/video.py b/transcoder/video.py index 7806710..fef6e83 100644 --- a/transcoder/video.py +++ b/transcoder/video.py @@ -34,9 +34,6 @@ class Video: self.frame_number = 0 # type: int self.palette = palette # type: Palette - self._opcodes = 0 - self._offsets = 0 - # Initialize empty screen self.memory_map = screen.MemoryMap( screen_page=1) # type: screen.MemoryMap @@ -91,8 +88,6 @@ class Video: memory_map.page_offset[screen.SCREEN_HOLES]) == 0 print("Similarity %f" % (update_priority.mean())) - if self._opcodes: - print("Opcode fill rate %f" % (self._offsets / self._opcodes)) yield from self._index_changes( memory_map, target, update_priority, is_aux) @@ -186,9 +181,6 @@ class Video: if len(offsets) == 3: break - # Record how many additional offsets we were able to fill - self._opcodes += 1 - self._offsets += len(offsets) # Pad to 4 if we didn't find enough for _ in range(len(offsets), 4): offsets.append(offsets[0]) From 53dac6a47c196ac4505d85f80b32337bcfc40f76 Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 17 Jan 2023 21:44:43 +0000 Subject: [PATCH 14/19] Disable debugging assertions now they've served their purpose --- transcoder/video.py | 76 ++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/transcoder/video.py b/transcoder/video.py index fef6e83..fe90147 100644 --- a/transcoder/video.py +++ b/transcoder/video.py @@ -206,44 +206,44 @@ class Video: # deterministic point in time when we can assert that all diffs should # have been resolved. # TODO: add flag to enable debug assertions - if not np.array_equal(source.page_offset, target.page_offset): - diffs = np.nonzero(source.page_offset != target.page_offset) - for i in range(len(diffs[0])): - diff_p = diffs[0][i] - diff_o = diffs[1][i] - - # For HGR, 0x00 or 0x7f may be visually equivalent to the same - # bytes with high bit set (depending on neighbours), so skip - # them - if (source.page_offset[diff_p, diff_o] & 0x7f) == 0 and \ - (target.page_offset[diff_p, diff_o] & 0x7f) == 0: - continue - - if (source.page_offset[diff_p, diff_o] & 0x7f) == 0x7f and \ - (target.page_offset[diff_p, diff_o] & 0x7f) == 0x7f: - continue - - print("Diff at (%d, %d): %d != %d" % ( - diff_p, diff_o, source.page_offset[diff_p, diff_o], - target.page_offset[diff_p, diff_o] - )) - assert False - - # If we've finished both main and aux pages, there should be no residual - # diffs in packed representation - all_done = self.out_of_work[True] and self.out_of_work[False] - if all_done and not np.array_equal(self.pixelmap.packed, - target_pixelmap.packed): - diffs = np.nonzero( - self.pixelmap.packed != target_pixelmap.packed) - print("is_aux: %s" % is_aux) - for i in range(len(diffs[0])): - diff_p = diffs[0][i] - diff_o = diffs[1][i] - print("(%d, %d): got %d want %d" % ( - diff_p, diff_o, self.pixelmap.packed[diff_p, diff_o], - target_pixelmap.packed[diff_p, diff_o])) - assert False + # if not np.array_equal(source.page_offset, target.page_offset): + # diffs = np.nonzero(source.page_offset != target.page_offset) + # for i in range(len(diffs[0])): + # diff_p = diffs[0][i] + # diff_o = diffs[1][i] + # + # # For HGR, 0x00 or 0x7f may be visually equivalent to the same + # # bytes with high bit set (depending on neighbours), so skip + # # them + # if (source.page_offset[diff_p, diff_o] & 0x7f) == 0 and \ + # (target.page_offset[diff_p, diff_o] & 0x7f) == 0: + # continue + # + # if (source.page_offset[diff_p, diff_o] & 0x7f) == 0x7f and \ + # (target.page_offset[diff_p, diff_o] & 0x7f) == 0x7f: + # continue + # + # print("Diff at (%d, %d): %d != %d" % ( + # diff_p, diff_o, source.page_offset[diff_p, diff_o], + # target.page_offset[diff_p, diff_o] + # )) + # assert False + # + # # If we've finished both main and aux pages, there should be no residual + # # diffs in packed representation + # all_done = self.out_of_work[True] and self.out_of_work[False] + # if all_done and not np.array_equal(self.pixelmap.packed, + # target_pixelmap.packed): + # diffs = np.nonzero( + # self.pixelmap.packed != target_pixelmap.packed) + # print("is_aux: %s" % is_aux) + # for i in range(len(diffs[0])): + # diff_p = diffs[0][i] + # diff_o = diffs[1][i] + # print("(%d, %d): got %d want %d" % ( + # diff_p, diff_o, self.pixelmap.packed[diff_p, diff_o], + # target_pixelmap.packed[diff_p, diff_o])) + # assert False # If we run out of things to do, pad forever content = target.page_offset[0, 0] From bbb4d14db8b44a009c41b2011f77291b7f2c23db Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 24 Jan 2023 22:31:28 +0000 Subject: [PATCH 15/19] Link to sample videos --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 7815f75..d577fe4 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,11 @@ Before you can run the transcoder you need to generate the data files it require This is a one-time setup. It takes about 90 minutes on my machine. +## Sample videos + +Some sample videos are available [here](https://www.dropbox.com/sh/kq2ej63smrzruwk/AADZSaqbNuTwAfnPWT6r9TJra?dl=0) for +streaming (see `server/server.py`) + ## Release Notes ### v0.3 (17 Jan 2023) From f055920dc84fee9d184598a31654ce635f8946f7 Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 24 Jan 2023 22:34:28 +0000 Subject: [PATCH 16/19] Point to b2d fork instead of the original bmp2dhr source, it's somewhat newer and seems to have made some improvements. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d577fe4..e005b1a 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ This ends up streaming data at about 100KB/sec of which 56KB/sec are updates to The video frames are actually encoded at the original frame rate (or optionally by skipping frames), prioritizing differences in the screen content, so the effective frame rate is higher than this if only a fraction of the screen is changing between frames (which is the typical case). -I'm using the excellent (though under-documented ;) [BMP2DHR](http://www.appleoldies.ca/bmp2dhr/) to encode the input video stream into a sequence of memory maps, then post-processing the frame deltas to prioritize the screen bytes to stream in order to approximate these deltas as closely as possible within the timing budget. +I'm using the excellent (though under-documented ;) [BMP2DHR](https://github.com/digarok/b2d) to encode the input video stream into a sequence of memory maps, then post-processing the frame deltas to prioritize the screen bytes to stream in order to approximate these deltas as closely as possible within the timing budget. ### KansasFest 2019 presentation From 4529bc3c7470a15da8e6187a251ecfbc84a38000 Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 24 Jan 2023 22:38:56 +0000 Subject: [PATCH 17/19] Add a TODO to upload more youtube videos showing quality improvements --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e005b1a..570f7ec 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ Dedicated to the memory of [Bob Bishop](https://www.kansasfest.org/2014/11/remem Sample videos (recording of playback on Apple //gs with RGB monitor, or HDMI via VidHD) +TODO: These are from older versions, for which quality was not as good. + Double Hi-Res: - [Try getting this song out of your head](https://youtu.be/S7aNcyojoZI) - [Babylon 5 title credits](https://youtu.be/PadKk8n1xY8) @@ -28,8 +30,6 @@ Older Hi-Res videos: - [Paranoimia ft Max Headroom](https://youtu.be/wfdbEyP6v4o) - [How many of us still feel about our Apple II's](https://youtu.be/-e5LRcnQF-A) -(These are from older versions, for which quality was not as good) - There may be more on this [YouTube playlist](https://www.youtube.com/playlist?list=PLoAt3SC_duBiIjqK8FBoDG_31nUPB8KBM) ## Details From 97403ab431aa70cee8350ca20d1a3c70e86ea345 Mon Sep 17 00:00:00 2001 From: kris Date: Wed, 25 Jan 2023 22:57:18 +0000 Subject: [PATCH 18/19] Create data dir before writing to it --- transcoder/make_data_tables.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/transcoder/make_data_tables.py b/transcoder/make_data_tables.py index baf464b..66260a1 100644 --- a/transcoder/make_data_tables.py +++ b/transcoder/make_data_tables.py @@ -1,6 +1,5 @@ -import bz2 import functools -import pickle +import os import sys from typing import Iterable, Type @@ -17,7 +16,7 @@ import screen PIXEL_CHARS = "0123456789ABCDEF" - +DATA_DIR = "transcoder/data" def pixel_char(i: int) -> str: return PIXEL_CHARS[i] @@ -184,12 +183,17 @@ def make_edit_distance( """Write file containing (D)HGR edit distance matrix for a palette.""" dist = compute_edit_distance(edp, bitmap_cls, nominal_colours) - data = "transcoder/data/%s_palette_%d_edit_distance.npz" % ( - bitmap_cls.NAME, pal.ID.value) + data = "%s/%s_palette_%d_edit_distance.npz" % ( + DATA_DIR, bitmap_cls.NAME, pal.ID.value) np.savez_compressed(data, edit_distance=dist) def main(): + try: + os.mkdir(DATA_DIR, mode=0o755) + except FileExistsError: + pass + for p in palette.PALETTES.values(): print("Processing palette %s" % p) edp = compute_substitute_costs(p) From a925a897a7e9c073ed0c19a2fc4d660f042ff8d5 Mon Sep 17 00:00:00 2001 From: kris Date: Sat, 28 Jan 2023 21:37:41 +0000 Subject: [PATCH 19/19] Update README to note emulation options --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 570f7ec..28baf6c 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ Streaming video and audio for the Apple II. Apple II hardware. Requires: -- 64K 6502 Apple II machine (only tested on //gs so far, but should work on older systems) +- 64K 6502 Apple II machine (tested on //gs and //e but should also work on ]\[/]\[+) - [Uthernet II](http://a2retrosystems.com/products.htm) ethernet card - - AFAIK no emulators support this hardware so you'll need to run it on a real machine to see it in action + - AppleWin ([Windows](https://github.com/AppleWin/AppleWin) and [Linux](https://github.com/audetto/AppleWin)) and [Ample](https://github.com/ksherlock/ample) (Mac) emulate the Uthernet II. ]\[-Vision has been confirmed to work with Ample. Dedicated to the memory of [Bob Bishop](https://www.kansasfest.org/2014/11/remembering-bob-bishop/), early pioneer of Apple II [video](https://www.youtube.com/watch?v=RiWE-aO-cyU) and [audio](http://www.faddensoftware.com/appletalker.png).