Rename slowpath opcode to end_of_frame to better describe what it does.

Add command-line flags for audio normalization params.
This commit is contained in:
kris 2020-12-28 22:42:34 +00:00
parent ccba51eead
commit daee564255
4 changed files with 40 additions and 33 deletions

View File

@ -6,6 +6,7 @@
# reproduce a target audio waveform, we upscale it to 1MHz sample rate,
# and compute the sequence of player opcodes to best reproduce this waveform.
#
# XXX
# Since the player opcodes are chosen to allow ticking the speaker during any
# given clock cycle (though with some limits on the minimum time
# between ticks), this means that we are able to control the Apple II speaker
@ -17,11 +18,11 @@
# e.g. this allows us to anticipate large amplitude changes by pre-moving
# the speaker to better approximate them.
#
# This also needs to take into account scheduling the "slow path" opcode every
# 2048 output bytes, where the Apple II will manage the TCP socket buffer while
# ticking the speaker at a regular cadence of 13 cycles to keep it in a
# net-neutral position. When looking ahead we can also (partially)
# compensate for this "dead" period by pre-positioning.
# This also needs to take into account scheduling the "end of frame" opcode
# every 2048 output bytes, where the Apple II will manage the TCP socket buffer
# while ticking the speaker at a regular cadence to keep it in a net-neutral
# position. When looking ahead we can also (partially) compensate for this
# "dead" period by pre-positioning.
import argparse
import collections
@ -86,10 +87,10 @@ def total_error(positions: numpy.ndarray, data: numpy.ndarray) -> numpy.ndarray:
@functools.lru_cache(None)
def frame_horizon(frame_offset: int, lookahead_steps: int):
"""Optimize frame_offset when we're not within lookahead_steps of slowpath.
"""Optimize frame_offset when more than lookahead_steps from end of frame.
When computing candidate opcodes, all frame offsets are the same until the
end-of-frame slowpath comes within our lookahead horizon.
Candidate opcodes for all values of frame_offset are equal, until the
end-of-frame opcode comes within our lookahead horizon.
"""
# TODO: This could be made tighter because a step is always at least 5
# cycles towards lookahead_steps.
@ -104,11 +105,11 @@ def audio_bytestream(data: numpy.ndarray, step: int, lookahead_steps: int,
dlen = len(data)
# Leave enough padding at the end to look ahead from the last data value,
# and in case we schedule a slowpath opcode towards the end.
# and in case we schedule an end-of-frame opcode towards the end.
# 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, is_6502)), dtype=numpy.float32)]))
opcodes.Opcode.END_OF_FRAME, is_6502)), dtype=numpy.float32)]))
# Starting speaker position and applied voltage.
position = 0.0
@ -212,8 +213,8 @@ def audio_bytestream(data: numpy.ndarray, step: int, lookahead_steps: int,
def preprocess(
filename: str, target_sample_rate: int, normalize: float = 1.0,
normalization_percentile: int = 100) -> numpy.ndarray:
filename: str, target_sample_rate: int, normalize: float,
normalization_percentile: int) -> numpy.ndarray:
"""Upscale input audio to target sample rate and normalize signal."""
data, _ = librosa.load(filename, sr=target_sample_rate, mono=True)
@ -236,12 +237,19 @@ def main():
help="Target machine CPU type")
parser.add_argument("--step_size", type=int,
help="Delta encoding step size")
# TODO: if we're not looking ahead beyond the longest (non-slowpath) opcode
# TODO: if we're not looking ahead beyond the longest (non-end-of-frame)
# opcode
# then this will reduce quality, e.g. two opcodes may truncate to the
# same prefix, but have different results when we apply them fully.
parser.add_argument("--lookahead_cycles", type=int,
help="Number of clock cycles to look ahead in audio "
"stream.")
parser.add_argument("--normalization", default=1.0, type=float,
help="Overall multiplier to rescale input audio "
"values.")
parser.add_argument("--norm_percentile", default=99,
help="Normalize to specified percentile value of input "
"audio")
parser.add_argument("input", type=str, help="input audio file to convert")
parser.add_argument("output", type=str, help="output audio file")
args = parser.parse_args()
@ -252,7 +260,8 @@ def main():
with open(args.output, "wb+") as f:
for opcode in audio_bytestream(
preprocess(args.input, sample_rate), args.step_size,
preprocess(args.input, sample_rate, args.normalization,
args.norm_percentile), args.step_size,
args.lookahead_cycles, sample_rate, args.cpu == '6502'):
f.write(bytes([opcode.value]))

View File

@ -60,7 +60,7 @@ def voltage_sequence(
def all_opcodes(
max_len: int, opcodes: Iterable[Opcode], start_opcodes: Iterable[Opcode]
max_len: int, opcodes: Iterable[Opcode], start_opcodes: Iterable[int]
) -> Iterable[Tuple[Opcode]]:
"""Enumerate all combinations of opcodes up to max_len cycles"""
num_opcodes = 0
@ -109,7 +109,7 @@ 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 new_unique:
# Reserve 9 bytes for EXIT and SLOWPATH
# Reserve 9 bytes for END_OF_FRAME and EXIT
if (num_bytes + player_op_len) > (256 - 9):
print("Out of space, truncating.")
break
@ -129,7 +129,7 @@ def generate_player(player_ops: List[Tuple[Opcode]], opcode_filename: str,
for o in unique_opcodes.keys():
f.write(" TICK_%02x = 0x%02x\n" % (o, o))
f.write(" EXIT = 0x%02x\n" % num_bytes)
f.write(" SLOWPATH = 0x%02x\n" % (num_bytes + 3))
f.write(" END_OF_FRAME = 0x%02x\n" % (num_bytes + 3))
f.write("\n\nVOLTAGE_SCHEDULE = {\n")
for o, v in unique_opcodes.items():
@ -147,7 +147,7 @@ def generate_player(player_ops: List[Tuple[Opcode]], opcode_filename: str,
def all_opcode_combinations(
max_cycles: int, opcodes: List[Opcode], start_opcodes: List[Opcode]
max_cycles: int, opcodes: Iterable[Opcode], start_opcodes: List[int]
) -> List[Tuple[Opcode]]:
return sorted(
list(all_opcodes(max_cycles, opcodes, start_opcodes)),
@ -155,7 +155,7 @@ def all_opcode_combinations(
def sort_by_opcode_count(
player_opcodes: List[Tuple[Opcode]], count_opcodes: List[Opcode]
player_opcodes: List[Tuple[Opcode]], count_opcodes: List[int]
) -> List[Tuple[Opcode]]:
return sorted(
player_opcodes, key=lambda ops: sum(o in count_opcodes for o in ops),
@ -164,15 +164,16 @@ def sort_by_opcode_count(
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--max_cycles", default=25,
parser.add_argument("--max_cycles", type=int, required=True,
help="Maximum cycle length of player opcodes")
parser.add_argument("opcodes", default=["NOP3", "STA"], nargs="+",
parser.add_argument("opcodes", nargs="+",
choices=Opcode.__members__.keys(),
help="6502 opcodes to use when generating player "
"opcodes")
args = parser.parse_args()
opcodes = set(Opcode.__members__[op] for op in args.opcodes)
# TODO: use Opcode instead of int values
non_nops = [Opcode.STA, Opcode.INC, Opcode.INCX, Opcode.STAX,
Opcode.JMP_INDIRECT]

View File

@ -5,8 +5,8 @@ import numpy
from typing import Dict, List, Tuple, Iterable
def make_slowpath_voltages() -> numpy.ndarray:
"""Voltage sequence for slowpath TCP processing."""
def _make_end_of_frame_voltages() -> numpy.ndarray:
"""Voltage sequence for end-of-frame TCP processing."""
length = 4 + 14 * 10 + 6
c = numpy.full(length, 1.0, dtype=numpy.float32)
voltage_high = True
@ -22,8 +22,8 @@ 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] = (
make_slowpath_voltages())
_VOLTAGE_SCHEDULE[Opcode.END_OF_FRAME], TOGGLES[Opcode.END_OF_FRAME] = (
_make_end_of_frame_voltages())
def cycle_length(op: Opcode, is_6502: bool) -> int:
@ -49,12 +49,12 @@ def opcode_choices(frame_offset: int, is_6502: bool) -> List[Opcode]:
stream bitrate.
"""
if frame_offset == 2047:
return [Opcode.SLOWPATH]
return [Opcode.END_OF_FRAME]
def _cycle_length(op: Opcode) -> int:
return cycle_length(op, is_6502)
opcodes = set(_VOLTAGE_SCHEDULE.keys()) - {Opcode.SLOWPATH}
opcodes = set(_VOLTAGE_SCHEDULE.keys()) - {Opcode.END_OF_FRAME}
return sorted(list(opcodes), key=_cycle_length, reverse=True)

View File

@ -286,14 +286,11 @@ exit_parmtable:
.BYTE 0 ; Byte reserved for future use
.WORD 0000 ; Pointer reserved for future use
; real_slowpath:
; The actual player code, which will be copied to $3xx for execution
;
; opcode cycle counts are for 65c02, for 6502 they are 1 less because JMP (indirect) is 5 cycles instead of 6.
begin_copy_page1:
; generated audio playback code
.include "player_generated.s"
@ -318,12 +315,12 @@ exit:
; If we do stall waiting for data then there is no need to worry about maintaining an even cadence, because audio
; will already be disrupted (since the encoder won't have predicted it, so will be tracking wrong). The speaker will
; resynchronize within a few hundred microseconds though.
slowpath:
end_of_frame:
STA TICK ; 4
JMP _slowpath ; 3 rest of slowpath doesn't fit in page 3
JMP _end_of_frame ; 3 rest of end_of_frame doesn't fit in page 3
end_copy_page1:
_slowpath:
_end_of_frame:
STA zpdummy ; 3
; Save the W5100 address pointer so we can come back here later
; We know the low-order byte is 0 because Socket RX memory is page-aligned and so is 2K frame.