Skip to content

Commit

Permalink
nullsound: generic trigger FX
Browse files Browse the repository at this point in the history
The trigger FX currently implements Furnace's 'delay FX', i.e it allows
a note to be registered after a configured number of ticks.
The same FX will be used to implement the 'cut FX', i.e stop the
playback of a note after a configured number of ticks.
  • Loading branch information
dciabrin committed Nov 28, 2024
1 parent ee4d0a8 commit d40c5d4
Show file tree
Hide file tree
Showing 7 changed files with 303 additions and 31 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 nss-ssg fx-vibrato fx-slide fx-vol-slide volume
OBJS=entrypoint bios-commands adpcm ym2610 stream timer nss-fm nss-adpcm nss-ssg fx-vibrato fx-slide fx-vol-slide fx-trigger volume
LIB=nullsound.lib

VERSION=@version@
Expand Down
117 changes: 117 additions & 0 deletions nullsound/fx-trigger.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
;;;
;;; nullsound - modular sound driver
;;; Copyright (c) 2024 Damien Ciabrini
;;; This file is part of ngdevkit
;;;
;;; ngdevkit is free software: you can redistribute it and/or modify
;;; it under the terms of the GNU Lesser General Public License as
;;; published by the Free Software Foundation, either version 3 of the
;;; License, or (at your option) any later version.
;;;
;;; ngdevkit is distributed in the hope that it will be useful,
;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;; GNU Lesser General Public License for more details.
;;;
;;; You should have received a copy of the GNU Lesser General Public License
;;; along with ngdevkit. If not, see <http://www.gnu.org/licenses/>.

;;; Trigger effect (delay, cut...), common functions for FM and SSG
;;;

.module nullsound

.include "ym2610.inc"
.include "struct-fx.inc"


.area CODE


;;; Enable delayed trigger for the next note and volume
;;; (note and volume and played after a number of ticks)
;;; ------
;;; ix : state for channel
;;; [ hl ]: delay
;;; [ hl modified ]
trigger_delay_init::
;; a: delay
ld a, (hl)
inc a
inc hl

;; configure trigger FX for delay
ld TRIGGER_DELAY(ix), a
xor a
set BIT_TRIGGER_ACTION_DELAY, a
ld TRIGGER_ACTION(ix), a
set BIT_FX_TRIGGER, FX(ix)

ret


;;; Call an function from the action lookup table
;;; ------
;;; hl: function lookup table
;;; bc: offset in bytes in the function lookup table
;;; a: input (note, vol...)
;;; [bc, de modified]
trigger_action_function::
push hl

;; bc: function to call
add hl, bc
ld c, (hl)
inc hl
ld b, (hl)

;; call
ld de, #_trigger_post_action
push de
push bc
ret
_trigger_post_action:
pop hl
ret


;;; Update the trigger configuration for the current channel
;;; ------
;;; ix: mirrored state of the current channel
;;; hl: function lookup table for the current channel
;;; [hl, bc, de modified]
eval_trigger_step::
;; is the trigger a delay?
bit BIT_TRIGGER_ACTION_DELAY, TRIGGER_ACTION(ix)
jr z, _trigger_post_delay
;; check whether delay is reached
dec TRIGGER_DELAY(ix)
jr nz, _trigger_end
jr _trigger_load_and_clear
_trigger_post_delay:
_trigger_end:
ret

_trigger_load_and_clear:
;; load new note?
bit BIT_TRIGGER_LOAD_NOTE, TRIGGER_ACTION(ix)
jr z, _trigger_post_load_note
ld a, TRIGGER_NOTE(ix)
ld bc, #TRIGGER_LOAD_NOTE_FUNC
call trigger_action_function
_trigger_post_load_note:

;; load new vol?
bit BIT_TRIGGER_LOAD_VOL, TRIGGER_ACTION(ix)
jr z, _trigger_post_load_vol
ld a, TRIGGER_VOL(ix)
ld bc, #TRIGGER_LOAD_VOL_FUNC
call trigger_action_function
_trigger_post_load_vol:

;; trigger is finished
xor a
ld TRIGGER_ACTION(ix), a
res BIT_FX_TRIGGER, FX(ix)

ret
89 changes: 76 additions & 13 deletions nullsound/nss-fm.s
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ state_fm1:
state_fm_pipeline: .blkb 1 ; actions to run at every tick (load note, vol, other regs)
state_fm_fx: .blkb 1 ; enabled FX for this channel
;;; FX state trackers
state_fm_trigger: .blkb TRIGGER_SIZE
state_fm_fx_vol_slide: .blkb VOL_SLIDE_SIZE
state_fm_fx_slide: .blkb SLIDE_SIZE
state_fm_fx_vibrato: .blkb VIBRATO_SIZE
Expand Down Expand Up @@ -156,6 +157,12 @@ _state_fm_end:
.area CODE


;;; context: channel action functions for FM
state_fm_action_funcs:
.dw fm_configure_note_on
.dw fm_configure_vol


;;; Reset FM playback state.
;;; Called before playing a stream
;;; ------
Expand Down Expand Up @@ -597,6 +604,11 @@ _fm_update_loop:

;; Pipeline action: evaluate one FX step for each enabled FX

bit BIT_FX_TRIGGER, FX(ix)
jr z, _fm_post_fx_trigger
ld hl, #state_fm_action_funcs
call eval_trigger_step
_fm_post_fx_trigger:
bit BIT_FX_VIBRATO, FX(ix)
jr z, _fm_post_fx_vibrato
call eval_fm_vibrato_step
Expand Down Expand Up @@ -705,10 +717,18 @@ fm_vol::
sub (hl)
inc hl

;; register pending volume configuration for channel
ld VOL(ix), a
set BIT_LOAD_VOL, PIPELINE(ix)
;; delay load via the trigger FX?
bit BIT_TRIGGER_ACTION_DELAY, TRIGGER_ACTION(ix)
jr z, _fm_vol_immediate
ld TRIGGER_VOL(ix), a
set BIT_TRIGGER_LOAD_VOL, TRIGGER_ACTION(ix)
jr _fm_vol_end

_fm_vol_immediate:
;; else load vol immediately
call fm_configure_vol

_fm_vol_end:
ld a, #1
ret

Expand Down Expand Up @@ -996,32 +1016,63 @@ _end_fm_slide_load_fnum2:
ret


;;; FM_NOTE_ON
;;; Emit a specific note (frequency) on an FM channel
;;; Configure state for new note and trigger a load in the pipeline
;;; ------
;;; [ hl ]: note (0xAB: A=octave B=semitone)
fm_note_on::
fm_configure_note_on:
push bc

ld NOTE(ix), a

;; stop current FM channel (disable all OPs)
;; CHECK: do it in the pipeline instead?

ld a, (state_fm_ym2610_channel)
ld c, a
ld b, #REG_FM_KEY_ON_OFF_OPS
call ym2610_write_port_a

;; record note, block and freq to FM state
;; b: note (0xAB: A=octave B=semitone)
ld b, (hl)
inc hl
ld NOTE_SEMITONE(ix), b

ld a, PIPELINE(ix)
or #(STATE_PLAYING|STATE_EVAL_MACRO|STATE_LOAD_NOTE)
ld PIPELINE(ix), a

pop bc

ret


;;; Configure state for new volume and trigger a load in the pipeline
;;; ------
fm_configure_vol:
ld VOL(ix), a

;; reload configured vol at the next pipeline run
set BIT_LOAD_VOL, PIPELINE(ix)

ret


;;; FM_NOTE_ON
;;; Emit a specific note (frequency) on an FM channel
;;; ------
;;; [ hl ]: note (0xAB: A=octave B=semitone)
fm_note_on::
;; a: note (0xAB: A=octave B=semitone)
ld a, (hl)
inc hl

;; delay load via the trigger FX?
bit BIT_TRIGGER_ACTION_DELAY, TRIGGER_ACTION(ix)
jr z, _fm_note_on_immediate
ld TRIGGER_NOTE(ix), a
set BIT_TRIGGER_LOAD_NOTE, TRIGGER_ACTION(ix)
jr _fm_note_on_end

_fm_note_on_immediate:
;; else load note immediately
ld NOTE_SEMITONE(ix), a
call fm_configure_note_on

_fm_note_on_end:
;; fm context will now target the next channel
ld a, (state_fm_channel)
inc a
Expand Down Expand Up @@ -1231,3 +1282,15 @@ fm_vol_slide_down::

ld a, #1
ret


;;; FM_DELAY
;;; Enable delayed trigger for the next note and volume
;;; (note and volume and played after a number of ticks)
;;; ------
;;; [ hl ]: delay
fm_delay::
call trigger_delay_init

ld a, #1
ret
Loading

0 comments on commit d40c5d4

Please sign in to comment.