From ccba51eead85fe1f8b77f63c6c2a771a7ea9fb7c Mon Sep 17 00:00:00 2001 From: kris Date: Mon, 28 Dec 2020 13:23:57 +0000 Subject: [PATCH] Add support for targeting 6502, for which the JMP (indirect) opcode takes 5 cycles instead of 6. --- encode_audio.py | 20 ++++++++++--------- opcodes.py | 52 ++++++++++++++++++++++++++++++------------------- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/encode_audio.py b/encode_audio.py index 6af6553..5a745df 100755 --- a/encode_audio.py +++ b/encode_audio.py @@ -99,7 +99,7 @@ def frame_horizon(frame_offset: int, lookahead_steps: int): def audio_bytestream(data: numpy.ndarray, step: int, lookahead_steps: int, - sample_rate: int): + sample_rate: int, is_6502: bool): """Computes optimal sequence of player opcodes to reproduce audio data.""" dlen = len(data) @@ -108,8 +108,7 @@ def audio_bytestream(data: numpy.ndarray, step: int, lookahead_steps: int, # TODO: avoid temporarily doubling memory footprint to concatenate data = numpy.ascontiguousarray(numpy.concatenate( [data, numpy.zeros(max(lookahead_steps, opcodes.cycle_length( - opcodes.Opcode.SLOWPATH)), - dtype=numpy.float32)])) + opcodes.Opcode.SLOWPATH, is_6502)), dtype=numpy.float32)])) # Starting speaker position and applied voltage. position = 0.0 @@ -121,7 +120,7 @@ def audio_bytestream(data: numpy.ndarray, step: int, lookahead_steps: int, for i in range(2048): for voltage in [-1.0, 1.0]: opcode_hash, _, voltages = opcodes.candidate_opcodes( - frame_horizon(i, lookahead_steps), lookahead_steps) + frame_horizon(i, lookahead_steps), lookahead_steps, is_6502) delta_powers, partial_positions = _partial_positions( voltage * voltages, step) @@ -135,12 +134,14 @@ def audio_bytestream(data: numpy.ndarray, step: int, lookahead_steps: int, delta_powers, partial_positions) opcode_partial_positions = {} - for op, voltages in opcodes.VOLTAGE_SCHEDULE.items(): + all_opcodes = opcodes.Opcode.__members__.values() + for op in set(all_opcodes) - {opcodes.Opcode.EXIT}: + voltages = opcodes.voltage_schedule(op, is_6502) for voltage in [-1.0, 1.0]: delta_powers, partial_positions = _partial_positions( voltage * voltages, step) assert delta_powers.shape == partial_positions.shape - assert delta_powers.shape[-1] == opcodes.cycle_length(op) + assert delta_powers.shape[-1] == opcodes.cycle_length(op, is_6502) opcode_partial_positions[op, voltage] = ( delta_powers, partial_positions, voltage * voltages[-1]) @@ -158,7 +159,8 @@ def audio_bytestream(data: numpy.ndarray, step: int, lookahead_steps: int, # Compute all possible opcode sequences for this frame offset opcode_hash, candidate_opcodes, _ = opcodes.candidate_opcodes( - frame_horizon(frame_offset, lookahead_steps), lookahead_steps) + frame_horizon(frame_offset, lookahead_steps), lookahead_steps, + is_6502) # Look up the precomputed partial values for these candidate opcode # sequences. delta_powers, partial_positions = all_partial_positions[opcode_hash, @@ -175,7 +177,7 @@ def audio_bytestream(data: numpy.ndarray, step: int, lookahead_steps: int, total_error(all_positions, data[i:i + lookahead_steps])).item() # Next opcode opcode = candidate_opcodes[opcode_idx][0] - opcode_length = opcodes.cycle_length(opcode) + opcode_length = opcodes.cycle_length(opcode, is_6502) opcode_counts[opcode] += 1 toggles += opcodes.TOGGLES[opcode] @@ -251,7 +253,7 @@ def main(): with open(args.output, "wb+") as f: for opcode in audio_bytestream( preprocess(args.input, sample_rate), args.step_size, - args.lookahead_cycles, sample_rate): + args.lookahead_cycles, sample_rate, args.cpu == '6502'): f.write(bytes([opcode.value])) diff --git a/opcodes.py b/opcodes.py index 62b90cd..59bda81 100644 --- a/opcodes.py +++ b/opcodes.py @@ -7,7 +7,7 @@ from typing import Dict, List, Tuple, Iterable def make_slowpath_voltages() -> numpy.ndarray: """Voltage sequence for slowpath TCP processing.""" - length = 4 + 14 * 10 + 6 # TODO: 6502 + length = 4 + 14 * 10 + 6 c = numpy.full(length, 1.0, dtype=numpy.float32) voltage_high = True toggles = 0 @@ -21,18 +21,27 @@ def make_slowpath_voltages() -> numpy.ndarray: Opcode = opcodes_generated.Opcode TOGGLES = opcodes_generated.TOGGLES -VOLTAGE_SCHEDULE = opcodes_generated.VOLTAGE_SCHEDULE -VOLTAGE_SCHEDULE[Opcode.SLOWPATH], TOGGLES[Opcode.SLOWPATH] = ( +_VOLTAGE_SCHEDULE = opcodes_generated.VOLTAGE_SCHEDULE +_VOLTAGE_SCHEDULE[Opcode.SLOWPATH], TOGGLES[Opcode.SLOWPATH] = ( make_slowpath_voltages()) -def cycle_length(op: Opcode) -> int: - """Returns the 65C02 cycle length of a player opcode.""" - return len(VOLTAGE_SCHEDULE[op]) +def cycle_length(op: Opcode, is_6502: bool) -> int: + """Returns the 65[C]02 cycle length of a player opcode.""" + l = len(_VOLTAGE_SCHEDULE[op]) + # JMP (indirect) is 5 cycles for 6502 instead of 6 + return l - 1 if is_6502 else l + + +def voltage_schedule(op: Opcode, is_6502: bool) -> numpy.ndarray: + """Returns the 65[C]02 applied voltage schedule of a player opcode.""" + v = _VOLTAGE_SCHEDULE[op] + # JMP (indirect) is 5 cycles for 6502 instead of 6 + return v[:-1] if is_6502 else v @functools.lru_cache(None) -def opcode_choices(frame_offset: int) -> List[Opcode]: +def opcode_choices(frame_offset: int, is_6502: bool) -> List[Opcode]: """Returns sorted list of valid opcodes for given frame offset. Sorted by decreasing cycle length, so that if two opcodes produce equally @@ -42,24 +51,28 @@ def opcode_choices(frame_offset: int) -> List[Opcode]: if frame_offset == 2047: return [Opcode.SLOWPATH] - opcodes = set(VOLTAGE_SCHEDULE.keys()) - {Opcode.SLOWPATH} - return sorted(list(opcodes), key=cycle_length, reverse=True) + def _cycle_length(op: Opcode) -> int: + return cycle_length(op, is_6502) + + opcodes = set(_VOLTAGE_SCHEDULE.keys()) - {Opcode.SLOWPATH} + return sorted(list(opcodes), key=_cycle_length, reverse=True) @functools.lru_cache(None) def opcode_lookahead( frame_offset: int, - lookahead_cycles: int) -> Tuple[Tuple[Opcode]]: + lookahead_cycles: int, is_6502: bool) -> Tuple[Tuple[Opcode]]: """Recursively enumerates all valid opcode sequences.""" - ch = opcode_choices(frame_offset) + ch = opcode_choices(frame_offset, is_6502) ops = [] for op in ch: - if cycle_length(op) >= lookahead_cycles: + if cycle_length(op, is_6502) >= lookahead_cycles: ops.append((op,)) else: - for res in opcode_lookahead((frame_offset + 1) % 2048, - lookahead_cycles - cycle_length(op)): + for res in opcode_lookahead( + (frame_offset + 1) % 2048, + lookahead_cycles - cycle_length(op, is_6502), is_6502): ops.append((op,) + res) return tuple(ops) # TODO: fix return type @@ -67,8 +80,7 @@ def opcode_lookahead( @functools.lru_cache(None) def cycle_lookahead( opcodes: Tuple[Opcode], - lookahead_cycles: int -) -> Tuple[float]: + lookahead_cycles: int, is_6502: bool) -> Tuple[float]: """Computes the applied voltage effects of a sequence of opcodes. i.e. produces the sequence of applied voltage changes that will result @@ -77,26 +89,26 @@ def cycle_lookahead( cycles = [] last_voltage = 1.0 for op in opcodes: - cycles.extend(last_voltage * VOLTAGE_SCHEDULE[op]) + cycles.extend(last_voltage * voltage_schedule(op, is_6502)) last_voltage = cycles[-1] return tuple(cycles[:lookahead_cycles]) @functools.lru_cache(None) def candidate_opcodes( - frame_offset: int, lookahead_cycles: int + frame_offset: int, lookahead_cycles: int, is_6502: bool ) -> Tuple[int, Tuple[Tuple[Opcode]], numpy.ndarray]: """Deduplicate a tuple of opcode sequences that are equivalent. For each opcode sequence whose effect is the same when truncated to lookahead_cycles, retains the first such opcode sequence. """ - opcodes = opcode_lookahead(frame_offset, lookahead_cycles) + opcodes = opcode_lookahead(frame_offset, lookahead_cycles, is_6502) seen_cycles = set() pruned_opcodes = [] pruned_cycles = [] for ops in opcodes: - cycles = cycle_lookahead(ops, lookahead_cycles) + cycles = cycle_lookahead(ops, lookahead_cycles, is_6502) if cycles in seen_cycles: continue seen_cycles.add(cycles)