diff --git a/nullsound/Makefile.in b/nullsound/Makefile.in index 555cd62..b56cc3a 100644 --- a/nullsound/Makefile.in +++ b/nullsound/Makefile.in @@ -19,7 +19,7 @@ all: nullsound.lib linkcheck.map -include ../Makefile.config INCLUDE_FILES=helpers ports ym2610 -OBJS=entrypoint bios-commands adpcm ym2610 stream timer nss-fm nss-adpcm-a nss-ssg nss-adpcm-b fx-vibrato fx-slide fx-vol-slide fx-trigger volume +OBJS=entrypoint bios-commands adpcm ym2610 stream timer nss-fm nss-ssg nss-adpcm-a nss-adpcm-b fx-vibrato fx-slide fx-vol-slide fx-trigger volume LIB=nullsound.lib VERSION=@version@ diff --git a/nullsound/nss-adpcm-a.s b/nullsound/nss-adpcm-a.s index 954e84a..fb14c89 100644 --- a/nullsound/nss-adpcm-a.s +++ b/nullsound/nss-adpcm-a.s @@ -53,6 +53,10 @@ ;;; ADPCM playback state tracker ;;; ------ + ;; This padding ensures the entire _state_ssg data sticks into + ;; a single 256 byte boundary to make 16bit arithmetic faster + .blkb 42 + _state_adpcm_start: ;;; ADPCM-A mirrored state diff --git a/nullsound/stream.s b/nullsound/stream.s index d5dd7cf..b8d298c 100644 --- a/nullsound/stream.s +++ b/nullsound/stream.s @@ -230,6 +230,7 @@ update_stream_state_tracker:: call process_streams_opcodes ld a, #0 ld (state_timer_ticks_count), a + call timer_update_ticks_for_next_row _check_update_stream_pipeline: ld a, (state_timer_tick_reached) bit TIMER_CONSUMER_STREAM_BIT, a @@ -387,9 +388,12 @@ stream_play_multi:: ld (state_ch_bits), bc call snd_configure_stream_ctx_switches - ;; hl: stream data from NSS + ;; setup speed and groove inc iy inc iy + call timer_init_ticks + + ;; hl: stream data from NSS push iy pop hl diff --git a/nullsound/timer.s b/nullsound/timer.s index cc02b96..55eeca7 100644 --- a/nullsound/timer.s +++ b/nullsound/timer.s @@ -33,15 +33,21 @@ ;;; the stream player as a reliable time source for synchronization. ;;; .area DATA +_state_timer_start: -state_timer_tick_reached:: - .db 0 +;;; ticks +state_timer_ticks_per_row:: .blkb 1 ; total number of ticks for the current row +state_timer_ticks_count:: .blkb 1 ; number of ticks reached for the current row +state_timer_tick_reached:: .blkb 1 ; has a new tick been reached -state_timer_ticks_count:: - .db 0 +;;; speed and groove +;;; This defines how many ticks to wait for each row during playback +;;; up to 16 different ticks can be configured before cycling back to start +state_timer_tick_pos:: .blkb 1 ; position in groove pattern +state_timer_nb_ticks:: .blkb 1 ; length of the the groove pattern +state_timer_ticks:: .blkb 16 ; current groove pattern -state_timer_ticks_per_row:: - .db 0 +_state_timer_end: .area CODE @@ -86,6 +92,81 @@ update_timer_state_tracker:: ret +;;; Initialize speed1 and speed2 from the stream's config +;;; ------ +;;; iy : speed1 and speed2 (if use) +timer_init_ticks:: + push bc + push hl + push de + + ;; speed steps + ld a, (iy) + ld (state_timer_nb_ticks), a + ;; copy steps + ld b, #0 + ld c, a + inc iy + push iy + pop hl + ld de, #state_timer_ticks + ldir + push hl + pop iy + + ;; initialize the first speed in a way that the first + ;; update of the stream tracker will process opcodes immediately + xor a + ld (state_timer_tick_pos), a + call timer_update_ticks_for_next_row + ld a, (state_timer_ticks_per_row) + ld (state_timer_ticks_count), a + + pop de + pop hl + pop bc + ret + + +;;; set the number of ticks for the current row from the +;;; position in the groove pattern +;;; ------ +;;; hl modified +timer_set_ticks_per_row:: + ;; hl: current tick pos (8bit aligned add) + ld hl, #state_timer_ticks + ld a, (state_timer_tick_pos) + add l + ld l, a + + ;; update ticks per row with current tick + ld a, (hl) + ld (state_timer_ticks_per_row), a + + ret + + +;;; update the position in the groove pattern and set the new +;;; numer of ticks per row +;;; ------ +;;; bc modified +timer_update_ticks_for_next_row:: + push hl + ld a, (state_timer_nb_ticks) + ld b, a + ld a, (state_timer_tick_pos) + inc a + cp b + jp c, _timer_set_pos + xor a +_timer_set_pos: + ld (state_timer_tick_pos), a + call timer_set_ticks_per_row + pop hl + ret + + + ;;; ;;; NSS opcodes ;;; @@ -124,23 +205,54 @@ timer_tempo:: ;;; ROW_SPEED ;;; number of ticks to wait before processing the next row in the streams +;;; when groove is in use, only speed2 is modified ;;; ------ ;;; [hl]: ticks row_speed:: + push de + + ;; de: tick position to store speed (+0 or +1 if groove is used) + ld de, #state_timer_ticks + ld a, (state_timer_nb_ticks) + dec a + add e + ld e, a + + ;; update ticks for speed opcode ld a, (hl) inc hl - ld (state_timer_ticks_per_row), a + ld (de), a + + ;; update current ticks per row + push hl + call timer_set_ticks_per_row + pop hl + + pop de ld a, #1 ret ;;; ROW_GROOVE ;;; number of ticks to wait before processing the next row in the streams +;;; this always modified speed1 ;;; ------ ;;; [hl]: ticks row_groove:: + push de + ;; de: tick position to store groove + ld de, #state_timer_ticks + + ;; update ticks for groove opcode ld a, (hl) inc hl - ld (state_timer_ticks_per_row), a + ld (de), a + + ;; update current ticks per row + push hl + call timer_set_ticks_per_row + pop hl + + pop de ld a, #1 ret diff --git a/tools/furtool.py b/tools/furtool.py index 81a16af..1c4b696 100755 --- a/tools/furtool.py +++ b/tools/furtool.py @@ -134,9 +134,10 @@ def ebit(data, msb, lsb): class fur_module: name: str = "" author: str = "" - speed: int = 0 + speeds: list[int] = field(default_factory=list) arpeggio: int = 0 frequency: float = 0.0 + fxcolumns: list[int] = field(default_factory=list) instruments: list[int] = field(default_factory=list) samples: list[int] = field(default_factory=list) @@ -151,8 +152,8 @@ def read_module(bs): assert bs.read(4) == b"INFO" bs.read(4) # skip size bs.u1() # skip timebase - mod.speed = bs.u1() - bs.u1() # skip speed2 + bs.u1() # skip speed 1, use info from speed patterns later + bs.u1() # skip speed 2, use info from speed patterns later mod.arpeggio = bs.u1() mod.frequency = bs.uf4() pattern_len = bs.u2() @@ -180,6 +181,39 @@ def read_module(bs): for o in range(nb_orders): mod.orders[o][i] = bs.u1() mod.fxcolumns = [bs.u1() for x in range(14)] + bs.read(14) # skip channel hide status (UI) + bs.read(14) # skip channel collapse status (UI) + for i in range(14): bs.ustr() # skip channel names + for i in range(14): bs.ustr() # skip channel short names + mod.comment = bs.ustr() + bs.uf4() # skip master volume + bs.read(28) # skip extended compatibity flags + bs.u2() # skip virtual tempo numerator + bs.u2() # skip virtual tempo denominator + # right now, subsongs are not supported + subsong_name = bs.ustr() + subsong_comment = bs.ustr() + subsongs = bs.u1() + assert subsongs == 0, "subsongs in a single Furnace file is unsupported" + bs.read(3) # skip reserved + # song's additional metadata + system_name = bs.ustr() + game_name = bs.ustr() + song_name_jp = bs.ustr() + song_author_jp = bs.ustr() + system_name_jp = bs.ustr() + game_name_jp = bs.ustr() + bs.read(12) # skip 1 "extra chip output setting" + # patchbay + bs.read(4*bs.u4()) # skip information + bs.u1() # skip auto patchbay + # more compat flags + bs.read(8) # skip compat flags + # speed pattern data + speed_length = bs.u1() + assert 1 <= speed_length <= 16 + mod.speeds = [bs.u1() for i in range(speed_length)] + # TODO: grove patterns return mod diff --git a/tools/nsstool.py b/tools/nsstool.py index a58123c..6cc95a1 100755 --- a/tools/nsstool.py +++ b/tools/nsstool.py @@ -216,7 +216,7 @@ def register_nss_ops(): # 0x08 ("nop" , ), ("speed" , ["ticks"]), - None, + ("groove", ["ticks"]), None, ("b_instr" , ["inst"]), ("b_note" , ["note"]), @@ -344,6 +344,8 @@ def convert_fm_row(row, channel): jmp_to_order = 257 elif fx == 0x0f: # Speed opcodes.append(speed(fxval)) + elif fx == 0x09: # Groove + opcodes.append(groove(fxval)) elif fx == 0x04: # vibrato # fxval == -1 means disable vibrato fxval = max(fxval, 0) @@ -440,6 +442,8 @@ def convert_s_row(row, channel): jmp_to_order = 257 elif fx == 0x0f: # Speed opcodes.append(speed(fxval)) + elif fx == 0x09: # Groove + opcodes.append(groove(fxval)) elif fx == 0x04: # vibrato # fxval == -1 means disable vibrato fxval = max(fxval, 0) @@ -513,6 +517,8 @@ def convert_a_row(row, channel): opcodes.append(a_retrigger(fxval)) elif fx == 0x0f: # Speed opcodes.append(speed(fxval)) + elif fx == 0x09: # Groove + opcodes.append(groove(fxval)) elif fx == 0xec: # cut opcodes.append(a_cut(fxval)) else: @@ -555,6 +561,8 @@ def convert_b_row(row, channel): jmp_to_order = 257 elif fx == 0x0f: # Speed opcodes.append(speed(fxval)) + elif fx == 0x09: # Groove + opcodes.append(groove(fxval)) elif fx == 0x01: # pitch slide up # fxval == -1 means disable slide fxval = max(fxval, 0) @@ -602,7 +610,7 @@ def row_to_nss(func, pat, pos): selected_b = [x for x in b_channel if x in channels] # initialize stream speed from module - tick = m.speed + tick = m.speeds[0] # -- structures # a song is composed of a sequence of orders @@ -1116,12 +1124,14 @@ def stream_name(prefix, channel): return prefix+"_%s"%stream_type[channel] -def nss_compact_header(channels, streams, name, fd): +def nss_compact_header(mod, channels, streams, name, fd): bitfield, comment = channels_bitfield(channels) if name: print("%s::" % name, file=fd) print((" .db 0x%02x"%len(streams)).ljust(40)+" ; number of streams", file=fd) print((" .dw 0x%04x"%bitfield).ljust(40)+" ; channels: %s"%comment, file=fd) + speeds=", ".join(["0x%02x"%x for x in mod.speeds]) + print((" .db 0x%02x, %s"%(len(mod.speeds), speeds)).ljust(40)+" ; speeds", file=fd) for i, c in enumerate(channels): comment = "stream %i: NSS data"%i print((" .dw %s"%(stream_name(name,c))).ljust(40)+" ; "+comment, file=fd) @@ -1163,7 +1173,6 @@ def generate_nss_stream(m, p, bs, ins, channels, stream_idx): if stream_idx <= 0: tb = round(256 - (4000000 / (1152 * m.frequency))) nss.insert(0, tempo(tb)) - nss.insert(0, speed(m.speed)) dbg("Transformation passes:") dbg(" - remove unreference NSS labels") @@ -1256,11 +1265,14 @@ def main(): streams = [generate_nss_stream(m, p, bs, ins, [c], i) for i, c in enumerate(channels)] channels, streams = remove_empty_streams(channels, streams) # NSS compact header (number of streams, channels bitfield, stream pointers) - size = 1 + 2 + (2 * len(streams)) + size = (1 + # number of streams + 2 + # channels bitfield + 1 + len(m.speeds) + # speeds + (2 * len(streams))) # stream pointers # all streams sizes size += sum([stream_size(s) for s in streams]) asm_header(streams, m, name, size, outfd) - nss_compact_header(channels, streams, name, outfd) + nss_compact_header(m, channels, streams, name, outfd) for i, ch, stream in zip(range(len(channels)), channels, streams): nss_to_asm(stream, m, stream_name(name, ch), outfd) else: @@ -1273,7 +1285,7 @@ def main(): # warn about any unknown FX during the conversion to NSS for ch in unknown_fx.keys(): - dbg("unknown FX for %s: %s" % (ch, ", ".join(sorted(unknown_fx[ch])))) + warn("unknown FX for %s: %s" % (ch, ", ".join(sorted(unknown_fx[ch]))))