129 lines
5.8 KiB
Python
Executable file
129 lines
5.8 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
|
# See LICENSE.md for license information.
|
|
# SPDX-License-Identifier: MIT
|
|
#
|
|
# ntpIrqWindow.py SRC DST - bound NinjaTrackerPlus's interrupts-off stretches.
|
|
#
|
|
# NinjaTrackerPlus moves sample data into the DOC with interrupts masked,
|
|
# ~4 us a byte (measured under MAME), and the Z8530 SCC's receive FIFO only
|
|
# lasts ~3.1 ms at 9600 baud (~2.3 ms under MAME). Two stretches outlast it:
|
|
# - NTPstreamsound fills a stream's 512-byte DOC page with two unrolled
|
|
# 256-byte copies under its own SEI: ~2.4 ms. stream_window goes between
|
|
# the halves: pending interrupts run there, then the DOC's mode and
|
|
# address are put back for the second half. The stream's oscillators are
|
|
# already halted, and its job is cleared first, so a refill interrupt left
|
|
# pending by the slot's previous sound cannot stream into the page.
|
|
# - The DOC interrupt handler refills every streaming oscillator that is
|
|
# due before it returns, 256 bytes each: ~1 ms apiece, ~2.5 ms for two.
|
|
# It returns after one instead; the DOC keeps its IRQ up for the rest, so
|
|
# the handler is re-entered at once, after anything the ROM's interrupt
|
|
# manager puts first (the SCC is on its fast path).
|
|
# Both only while the word at offset 27 of the replayer (irq_window, right
|
|
# after the documented jump table) is nonzero. The word after it, at offset
|
|
# 29 (playing_addr), holds the address of the replayer's playing flag, so
|
|
# audio_full.c can see a song that does not loop come to its end. audio_full.c sets it from
|
|
# hal.c's tight interrupt-latency bound before every NTPstreamsound, so with
|
|
# the bound loose the replayer does exactly what it always did.
|
|
#
|
|
# The source is the installer's pinned ntpsources.zip (checksummed), so
|
|
# every anchor below is exact; a miss means NTP changed and this script
|
|
# must be revisited, so it fails the build instead of guessing.
|
|
import sys
|
|
|
|
TABLE_END = """ jmp streamsound
|
|
"""
|
|
TABLE_END_NEW = """ jmp streamsound
|
|
|
|
; irq_window (JoeyLib, offset 27): nonzero bounds the interrupts-off
|
|
; stretches -- see stream_window and interrupt_handler_exit.
|
|
irq_window dw 0
|
|
|
|
; playing_addr (JoeyLib, offset 29): where the replayer keeps its playing
|
|
; flag (0 once a song played once has ended), for jlAudioIsPlayingMod.
|
|
playing_addr dw playing
|
|
"""
|
|
FILL = """ ldy streamsound_osc2
|
|
jsr stream_copy ;fill buffer with 512 bytes for first play
|
|
jsr stream_copy_no_init
|
|
"""
|
|
FILL_NEW = """ ldy streamsound_osc2
|
|
jsr stream_copy ;fill buffer with 512 bytes for first play
|
|
jsr stream_window ;JoeyLib: interrupts in between the halves, if asked
|
|
jsr stream_copy_no_init
|
|
"""
|
|
COPY_HELPERS = """ mx %10
|
|
streamsound_copy = * ;copy a byte from stream structure to multiple doc registers
|
|
"""
|
|
COPY_HELPERS_NEW = """; stream_window (JoeyLib): between the two halves of streamsound's first
|
|
; DOC fill, when irq_window is nonzero, let pending interrupts run, then
|
|
; point the DOC back at the second half (a handler may have used it).
|
|
; In and out: mx %00, Y = the stream's track * 2.
|
|
mx %00
|
|
stream_window = *
|
|
lda irq_window
|
|
beq stream_window_1
|
|
sep #$20
|
|
mx %10
|
|
ldx streamsound_osc ;no refill job left pending from the slot's last sound
|
|
stz oscillator_job_tbl,x
|
|
cli
|
|
nop ;a pending interrupt is taken here
|
|
sei
|
|
jsr access_doc_ram
|
|
rep #$20
|
|
mx %00
|
|
lda stream_doc_page,y ;the page's second half
|
|
inc
|
|
xba
|
|
sta sound_address
|
|
stream_window_1 = *
|
|
rts
|
|
|
|
mx %10
|
|
streamsound_copy = * ;copy a byte from stream structure to multiple doc registers
|
|
"""
|
|
HANDLER_JOB = """ ldx interrupt_job
|
|
jsr (interrupt_jump_tbl,x)
|
|
|
|
]loop lda sound_control
|
|
"""
|
|
HANDLER_JOB_NEW = """ ldx interrupt_job
|
|
jsr (interrupt_jump_tbl,x)
|
|
|
|
lda irq_window ;JoeyLib: one oscillator an interrupt when asked;
|
|
bne interrupt_handler_exit ;the DOC keeps its IRQ up for any others
|
|
]loop lda sound_control
|
|
"""
|
|
HANDLER_END = """ bpl interrupt_handler_loop
|
|
|
|
sep #$30
|
|
"""
|
|
HANDLER_END_NEW = """ bpl interrupt_handler_loop
|
|
|
|
interrupt_handler_exit = *
|
|
sep #$30
|
|
"""
|
|
|
|
|
|
def patchOnce(text, old, new):
|
|
if text.count(old) != 1:
|
|
sys.exit("ntpIrqWindow: anchor not found exactly once -- NinjaTrackerPlus changed:\n" + old)
|
|
return text.replace(old, new)
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) != 3:
|
|
sys.exit("usage: ntpIrqWindow.py SRC DST")
|
|
with open(sys.argv[1], "r", encoding="ascii") as f:
|
|
text = f.read()
|
|
text = patchOnce(text, TABLE_END, TABLE_END_NEW)
|
|
text = patchOnce(text, FILL, FILL_NEW)
|
|
text = patchOnce(text, COPY_HELPERS, COPY_HELPERS_NEW)
|
|
text = patchOnce(text, HANDLER_JOB, HANDLER_JOB_NEW)
|
|
text = patchOnce(text, HANDLER_END, HANDLER_END_NEW)
|
|
with open(sys.argv[2], "w", encoding="ascii") as f:
|
|
f.write(text)
|
|
|
|
|
|
main()
|