mirror of
https://github.com/KrisKennaway/ii-sound.git
synced 2024-06-09 18:29:32 +00:00
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:
parent
ccba51eead
commit
daee564255
|
@ -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]))
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
12
opcodes.py
12
opcodes.py
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue
Block a user