diff --git a/encode_audio.py b/encode_audio.py index 9414813..2f3bb88 100755 --- a/encode_audio.py +++ b/encode_audio.py @@ -101,7 +101,8 @@ def frame_horizon(frame_offset: int, lookahead_steps: int): return frame_offset -def audio_bytestream(data: numpy.ndarray, step: int, lookahead_steps: int): +def audio_bytestream(data: numpy.ndarray, step: int, lookahead_steps: int, + sample_rate: int): """Computes optimal sequence of player opcodes to reproduce audio data.""" dlen = len(data) @@ -117,6 +118,7 @@ def audio_bytestream(data: numpy.ndarray, step: int, lookahead_steps: int): position = 0.0 voltage = -1.0 + toggles = 0 all_partial_positions = {} # Precompute partial_positions so we don't skew ETA during encoding. for i in range(2048): @@ -178,6 +180,7 @@ def audio_bytestream(data: numpy.ndarray, step: int, lookahead_steps: int): opcode = candidate_opcodes[opcode_idx][0] opcode_length = opcodes.cycle_length(opcode) opcode_counts[opcode] += 1 + toggles += opcodes.TOGGLES[opcode] # Apply this opcode to evolve the speaker position delta_powers, partial_positions, last_voltage = \ @@ -200,6 +203,8 @@ def audio_bytestream(data: numpy.ndarray, step: int, lookahead_steps: int): yield opcodes.Opcode.EXIT eta.done() print("Total error %f" % total_err) + toggles_per_sec = toggles / dlen * sample_rate + print("%d speaker toggles/sec" % toggles_per_sec) print("Opcodes used:") for v, k in sorted(list(opcode_counts.items()), key=lambda kv: kv[1], @@ -240,7 +245,8 @@ def main(argv): with open(out, "wb+") as f: for opcode in audio_bytestream( - preprocess(serve_file, sample_rate), step, lookahead_steps): + preprocess(serve_file, sample_rate), step, lookahead_steps, + sample_rate): f.write(bytes([opcode.value])) diff --git a/generate_player.py b/generate_player.py index 70f32c5..f6f0d98 100644 --- a/generate_player.py +++ b/generate_player.py @@ -12,19 +12,20 @@ class Opcode(enum.Enum): INC = 5 INCX = 6 JMP_INDIRECT = 7 + NOPNOP = 8 # Byte length of opcodes OPCODE_LEN = { Opcode.NOP: 1, Opcode.NOP3: 2, Opcode.STA: 3, Opcode.STAX: 3, - Opcode.INC: 3, Opcode.INCX: 3, Opcode.JMP_INDIRECT: 3 + Opcode.INC: 3, Opcode.INCX: 3, Opcode.JMP_INDIRECT: 3, Opcode.NOPNOP: 2 } ASM = { Opcode.NOP: "NOP", Opcode.NOP3: "STA zpdummy", Opcode.STA: "STA $C030", Opcode.STAX: "STA $C030,X", Opcode.INC: "INC $C030", Opcode.INCX: "INC $C030,X", - Opcode.JMP_INDIRECT: "JMP (WDATA)" + Opcode.JMP_INDIRECT: "JMP (WDATA)", Opcode.NOPNOP: "NOP NOP" } # Applied speaker voltages resulting from executing opcode @@ -37,19 +38,24 @@ VOLTAGES = { Opcode.NOP3: [1, 1, 1], # TODO: support 6502 cycle counts as well Opcode.JMP_INDIRECT: [1, 1, 1, 1, 1, 1], + Opcode.NOPNOP: [1, 1, 1, 1], } def voltage_sequence( - opcodes: Iterable[Opcode], starting_voltage=1.0) -> numpy.ndarray: + opcodes: Iterable[Opcode], starting_voltage=1.0 +) -> Tuple[numpy.float32, int]: """Voltage sequence for sequence of opcodes.""" out = [] v = starting_voltage + toggles = 0 for op in opcodes: for nv in v * numpy.array(VOLTAGES[op]): + if v != nv: + toggles += 1 v = nv out.append(v) - return numpy.array(out, dtype=numpy.float64) + return tuple(numpy.array(out, dtype=numpy.float32)), toggles def all_opcodes( @@ -72,27 +78,28 @@ def all_opcodes( def generate_player(player_ops: List[Tuple[Opcode]], opcode_filename: str, - player_filename: str, truncate_to_page=True): + player_filename: str): num_bytes = 0 seqs = {} num_op = 0 offset = 0 unique_opcodes = {} + toggles = {} + done = False with open(player_filename, "w+") as f: for i, k in enumerate(player_ops): - is_unique = False + new_unique = [] player_op = [] player_op_len = 0 new_offset = offset for j, o in enumerate(k): - seq = tuple(voltage_sequence(k[j:], 1.0)) + seq, tog = voltage_sequence(k[j:], 1.0) dup = seqs.setdefault(seq, i) if dup == i: player_op.append("tick_%02x: ; voltages %s" % ( new_offset, seq)) - is_unique = True - unique_opcodes[new_offset] = seq + new_unique.append((new_offset, seq, tog)) player_op.append(" %s" % ASM[o]) player_op_len += OPCODE_LEN[o] new_offset += OPCODE_LEN[o] @@ -100,13 +107,17 @@ def generate_player(player_ops: List[Tuple[Opcode]], opcode_filename: str, # If at least one of the partial opcode sequences was not # a dup, then add it to the player - if is_unique: - if (num_bytes + player_op_len) >= 252: + if new_unique: + # Reserve 9 bytes for EXIT and SLOWPATH + if (num_bytes + player_op_len) > (256 - 9): print("Out of space, truncating.") break num_op += 1 f.write("\n".join(player_op)) num_bytes += player_op_len + for op_offset, seq, tog in new_unique: + unique_opcodes[op_offset] = seq + toggles[op_offset] = tog offset = new_offset f.write("; %d bytes\n" % num_bytes) @@ -126,6 +137,13 @@ def generate_player(player_ops: List[Tuple[Opcode]], opcode_filename: str, "\n" % (o, v)) f.write("}\n") + f.write("\n\nTOGGLES = {\n") + for o, v in toggles.items(): + f.write( + " Opcode.TICK_%02x: %d,\n" % (o, v) + ) + f.write("}\n") + def all_opcode_combinations( max_cycles: int, opcodes: List[Opcode], start_opcodes: List[Opcode] @@ -151,6 +169,7 @@ if __name__ == "__main__": opcodes=[ Opcode.NOP, Opcode.NOP3, + # Opcode.NOPNOP, Opcode.STA, # Opcode.INC, # Opcode.INCX