Skip to content

Commit

Permalink
nullsound: groove FX
Browse files Browse the repository at this point in the history
Updated parsing of Furnace module to read speed pattern (more than one
speed). Added groove FX support.
This commit doesn't implement groove pattern, the groove FX only works
for 2-speeds modules.
  • Loading branch information
dciabrin committed Dec 13, 2024
1 parent 259e50e commit fda751e
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 20 deletions.
2 changes: 1 addition & 1 deletion nullsound/Makefile.in
Original file line number Diff line number Diff line change
Expand Up @@ -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@
Expand Down
4 changes: 4 additions & 0 deletions nullsound/nss-adpcm-a.s
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion nullsound/stream.s
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
128 changes: 120 additions & 8 deletions nullsound/timer.s
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
;;;
Expand Down Expand Up @@ -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
40 changes: 37 additions & 3 deletions tools/furtool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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()
Expand Down Expand Up @@ -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


Expand Down
26 changes: 19 additions & 7 deletions tools/nsstool.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ def register_nss_ops():
# 0x08
("nop" , ),
("speed" , ["ticks"]),
None,
("groove", ["ticks"]),
None,
("b_instr" , ["inst"]),
("b_note" , ["note"]),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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:
Expand All @@ -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]))))



Expand Down

0 comments on commit fda751e

Please sign in to comment.