Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. See LICENSE.md for license information. SPDX-License-Identifier: MIT ntpIrqWindow.patch - bound NinjaTrackerPlus's interrupts-off stretches. make/iigs.mk applies it to the replayer's source with patch --fuzz=0 before Merlin32 assembles it. 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 hunk applies exactly; a hunk that does not means NTP changed and this patch must be revisited, so the build fails instead of guessing. --- ninjatrackerplus.s +++ ninjatrackerplus.s @@ -138,6 +138,14 @@ ; 1 byte channel (0-7 placed in the upper 4 bits, example: $00 for left, $10 for right) 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 + *------------------------------- ; to stay out of the way, do not use the direct page at all @@ -1301,6 +1309,7 @@ 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 mx %00 ;we're in 16bit mode on return @@ -1383,6 +1392,31 @@ clc rtl +; 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 ldy streamsound_osc_count @@ -1454,6 +1488,8 @@ 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 bmi ]loop @@ -1465,6 +1501,7 @@ lda sound_data bpl interrupt_handler_loop +interrupt_handler_exit = * sep #$30 pld plb