From 6a21bd11ab00b644910c7deae7b763a91d0a288c Mon Sep 17 00:00:00 2001 From: Damien Ciabrini Date: Thu, 21 Nov 2024 22:13:51 +0100 Subject: [PATCH] nullsound: big refactor and new pipeline processing This commit holds a massive refactoring of the FM and SSG processing that is hapenning at every step. Now every channel has a fixed sound pipeline where every NSS opcode records actions to perform for a row, and the processing of those actions and FX takes place at every step. SSG channel has its macro processing reworked. Now every register can be updated at every step of the macro definition, and loop support has been added. This is a breaking commit that requires recompiling the NSS instruments and streams. The user API stays the same. --- nullsound/Makefile.in | 2 +- nullsound/buffers.s | 35 +- nullsound/fx-slide.s | 178 +++--- nullsound/fx-vibrato.s | 178 +++--- nullsound/fx-vol-slide.s | 88 +++ nullsound/nss-fm.s | 1154 +++++++++++++++---------------------- nullsound/nss-ssg.s | 1168 ++++++++++++++------------------------ nullsound/stream.s | 117 ++-- nullsound/struct-fx.inc | 55 +- nullsound/volume.s | 102 ++-- tools/furtool.py | 190 +++++-- tools/nsstool.py | 65 ++- 12 files changed, 1528 insertions(+), 1804 deletions(-) create mode 100644 nullsound/fx-vol-slide.s diff --git a/nullsound/Makefile.in b/nullsound/Makefile.in index ac173f9..bf6fdab 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 nss-ssg fx-vibrato fx-slide volume +OBJS=entrypoint bios-commands adpcm ym2610 stream timer nss-fm nss-adpcm nss-ssg fx-vibrato fx-slide fx-vol-slide volume LIB=nullsound.lib VERSION=@version@ diff --git a/nullsound/buffers.s b/nullsound/buffers.s index d7e91b0..1927f96 100644 --- a/nullsound/buffers.s +++ b/nullsound/buffers.s @@ -61,12 +61,33 @@ ssg_semitone_distance:: .db 0x07, 0x07, 0x06, 0x06, 0x06, 0x05, 0x05, 0x05, 0x04, 0x04, 0x04, 0x04, 0x04, 0, 0, 0 .db 0x04, 0x03, 0x03, 0x03, 0x03, 0x03, 0x02, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0, 0, 0 -;;; Sine precalc +;;; Convert note flat representation to representation ;;; ------ -;;; a 64 bytes 2*Pi sign precalc encoded as 3-bit magnitude + 1-bit sign -;;; This is used by the vibrato effect +;;; Precalc for 8 octaves + .bndry 128 +note_to_octave_semitone:: + .db 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b + .db 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b + .db 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b + .db 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b + .db 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b + .db 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b + .db 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b + .db 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b + +;;; fixed-point sine precalc +;;; ------ +;;; a 64 entries (128bytes) signed fixed point precalc of sin(x) for x in [0..2*pi], +;;; encoded as [-1.0..1.0] as [s..iffffffff....]. This serves as a base increment +;;; for vibrato displacement from 1..16, which yields a 9bits fixed point for +;;; displacement of the current NSS note + .bndry 128 sine:: - .db 0, 0, 9, 10, 11, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 15 - .db 15, 15, 15, 15, 15, 15, 14, 14, 13, 13, 12, 11, 11, 10, 9, 0 - .db 0, 0, 1, 2, 3, 3, 4, 5, 5, 6, 6, 7, 7, 7, 7, 7 - .db 7, 7, 7, 7, 7, 7, 6, 6, 5, 5, 4, 3, 3, 2, 1, 0 + .dw 0x0000, 0x0190, 0x0310, 0x04a0, 0x0610, 0x0780, 0x08e0, 0x0a20 + .dw 0x0b50, 0x0c50, 0x0d40, 0x0e10, 0x0ec0, 0x0f40, 0x0fb0, 0x0fe0 + .dw 0x1000, 0x0fe0, 0x0fb0, 0x0f40, 0x0ec0, 0x0e10, 0x0d40, 0x0c50 + .dw 0x0b50, 0x0a20, 0x08e0, 0x0780, 0x0610, 0x04a0, 0x0310, 0x0190 + .dw 0x8000, 0x8190, 0x8310, 0x84a0, 0x8610, 0x8780, 0x88e0, 0x8a20 + .dw 0x8b50, 0x8c50, 0x8d40, 0x8e10, 0x8ec0, 0x8f40, 0x8fb0, 0x8fe0 + .dw 0x9000, 0x8fe0, 0x8fb0, 0x8f40, 0x8ec0, 0x8e10, 0x8d40, 0x8c50 + .dw 0x8b50, 0x8a20, 0x88e0, 0x8780, 0x8610, 0x84a0, 0x8310, 0x8190 diff --git a/nullsound/fx-slide.s b/nullsound/fx-slide.s index 4be8ffa..7e6d2dd 100644 --- a/nullsound/fx-slide.s +++ b/nullsound/fx-slide.s @@ -27,19 +27,20 @@ .area CODE -;;; Enable slide effect for the current SSG channel +;;; Enable slide effect for the current channel ;;; ------ -;;; ix : FM state for channel +;;; ix : state for channel ;;; a : slide direction: 0 == up, 1 == down ;;; [ hl ]: speed (4bits) and depth (4bits) slide_init:: + push bc + push de + ;; b: slide direction (from a) ld b, a - ;; slide fx on - ld a, FX(ix) - set 1, a - ld FX(ix), a + ;; enable slide FX + set BIT_FX_SLIDE, FX(ix) ;; a: speed ld a, (hl) @@ -48,6 +49,7 @@ slide_init:: rra rra and #0xf + ;; de: inc16 = speed / 8 ld d, a ld e, #0 @@ -85,67 +87,86 @@ _post_depth_negate: inc hl + ;; init semitone position, fixed point representation + ld a, #0 + ld SLIDE_POS16(ix), a + ld SLIDE_POS16+1(ix), a + + ;; save target depth + ld a, SLIDE_DEPTH(ix) + ld SLIDE_END(ix), a + + pop de + pop bc + ret -;;; Setup the end semitone for the currently configured slide +;;; Enable slide effect for the current channel ;;; ------ -;;; ix : state for channel -;;; e : semitone to configure increments from -slide_setup_increments:: +;;; ix : state for channel +;;; a : slide direction: 0 == up, 1 == down +;;; [ hl ]: speed (4bits) and depth (4bits) +slide_pitch_init:: push bc push de - ;; init semitone position, fixed point representation - ld SLIDE_POS16+1(ix), e - ld a, #0 - ld SLIDE_POS16(ix), a - - ;; d: depth, negative if slide goes down - ld d, SLIDE_DEPTH(ix) + ;; b: slide direction (from a) + ld b, a - ;; c: note adjust. slide up: 4, slide down: -4 - ld c, #4 - bit 7, d - jr z, _post_inc_adjust - ld c, #-4 -_post_inc_adjust: + ;; enable slide FX + set BIT_FX_SLIDE, FX(ix) - ;; b: current octave - ld a, SLIDE_POS16+1(ix) - and #0xf0 - ld b, a + ;; a: speed + ld a, (hl) - ;; e: target octave - ld a, SLIDE_POS16+1(ix) - add d - and #0xf0 + ;; de: inc16 = speed / 32 + ld d, a + ld e, #0 + srl d + rr e + srl d + rr e + srl d + rr e + srl d + rr e + srl d + rr e + ;; down: negate inc16 + bit 0, b + jr z, _post_inc16_negate2 + ld a, #0 + sub e ld e, a - - ;; if current and target octave differ, skip missing steps in the depth - ld a, b - cp e - jr z, _post_depth_adjust - ld a, d - add c ; slide up: 4, slide down: -4 + ld a, #0 + sbc d ld d, a -_post_depth_adjust: +_post_inc16_negate2: + ld SLIDE_INC16(ix), e + ld SLIDE_INC16+1(ix), d - ;; a: current octave/note - ld a, SLIDE_POS16+1(ix) + ;; depth + ld a, #127 + ;; down: negate depth + ;; we also need to go one seminote below, to account for the + ;; fractional parts of the slide. + bit 0, b + jr z, _post_depth_negate2 + neg + dec a +_post_depth_negate2: + ld SLIDE_DEPTH(ix), a - ;; d: target octave/note - add d - ld d, a + inc hl - ;; when target note is a missing step, adjust to the next note - and #0xf - cp #12 - ld a, d - jr c, _post_target_note - add c ; slide up: 4, slide down: -4 -_post_target_note: - ;; save target octave/note + ;; init semitone position, fixed point representation + ld a, #0 + ld SLIDE_POS16(ix), a + ld SLIDE_POS16+1(ix), a + + ;; save target depth + ld a, SLIDE_DEPTH(ix) ld SLIDE_END(ix), a pop de @@ -211,20 +232,17 @@ _post_sign_chk: ret -;;; Increment current fixed point position in the semitone table and -;;; stop effects when the target position is reached +;;; Increment current fixed point displacement and +;;; stop effects when the target displacement is reached ;;; ------ ;;; IN: ;;; ix : state for channel ;;; c : slide direction: 0 == up, 1 == down ;;; OUT: ;;; a : whether effect is finished (0: finished, 1: still running) -;;; d : when effect is finished, target semitone +;;; d : when effect is finished, target displacement +;;; de modified eval_slide_step: - ;; ix: state for the current channel - push hl - pop ix - ;; c: 0 slide up, 1 slide down ld a, SLIDE_INC16+1(ix) rlc a @@ -234,7 +252,7 @@ eval_slide_step: ;; INC16 increment is 1/8 semitone (0x0020) * depth ;; negative for slide down - ;; add/sub increment to the current semitone POS16 + ;; add/sub increment to the current semitone displacement POS16 ;; e: fractional part ld a, SLIDE_INC16(ix) add SLIDE_POS16(ix) @@ -243,23 +261,8 @@ eval_slide_step: ;; d: integer part ld a, SLIDE_INC16+1(ix) adc SLIDE_POS16+1(ix) + ld SLIDE_POS16+1(ix), a ld d, a - ;; do we need to skip missing steps in the note table - and #0xf - cp #0xc - jr c, _post_skip - ld a, d - ;; slide direction - bit 0, c - jr z, _slide_dist_up - add #-4 - ld d, a - jr _post_skip -_slide_dist_up: - add #4 - ld d, a -_post_skip: - ld SLIDE_POS16+1(ix), d ;; have we reached the end of the slide? ;; slide up: continue if cur < end @@ -276,31 +279,18 @@ _slide_cp: jr c, _slide_intermediate ;; slide is finished, stop effect - ld (ix), #0 + res BIT_FX_SLIDE, FX(ix) - ;; d: clamp the last slide pos to the target semitone + ;; d: clamp the last slide pos to the target displacement ld d, SLIDE_END(ix) ;; for slide down, we finish one note below the real target to play - ;; all ticks with fractional parts. Adjust the end note back if needed + ;; all ticks with fractional parts. Adjust the end displacement back if needed bit 0, c jr z, _post_adjust - ld a, d - and #0xf - cp #11 - jr c, _neg_inc_adjust - ;; adjust to next note (after the missing steps in the note table) - ld a, d - add #5 - ld d, a - jr _post_adjust -_neg_inc_adjust: - ;; adjust to next note - ld a, d - inc a - ld d, a + inc d _post_adjust: - ;; effect is finished, new semitone in d + ;; effect is finished, new displacement in d ld a, #0 ret diff --git a/nullsound/fx-vibrato.s b/nullsound/fx-vibrato.s index 20fae5f..7a872eb 100644 --- a/nullsound/fx-vibrato.s +++ b/nullsound/fx-vibrato.s @@ -26,36 +26,41 @@ .area CODE + .equ VIBRATO_PRECALC_SIZE, 64 -;;; Setup prev and next increments for vibrato + + +;;; Enable vibrato effect for the current channel ;;; ------ -;;; IN: -;;; ix : fm state for channel -;;; the note semitone must be already configured -;;; [ hl ]: prev semitone distance -;;; [hl+1]: next semitone distance -;;; OUT: -;;; de : prev increment (fixed-point) -;;; hl : prev increment (fixed-point) -;;; bc, de, hl modified -vibrato_setup_increments:: - ;; bc: prev distance from current note - push hl ; +(prev distance) - ld b, (hl) - ;; de: output prev increment, scaled by depth (a) - ld a, VIBRATO_DEPTH(ix) - call vibrato_scale_increment - ld e, l - ld d, h +;;; ix : state for channel +;;; [ hl ]: speed (4bits) and depth (4bits) +vibrato_init:: + + ;; if vibrato was in use, keep the current vibrato pos + bit BIT_FX_VIBRATO, FX(ix) + jp nz, _post_vibrato_pos + ;; reset vibrato sine pos + ld VIBRATO_POS(ix), #0 +_post_vibrato_pos: + ;; enable vibrato FX + set BIT_FX_VIBRATO, FX(ix) + + ;; speed + ld a, (hl) + rra + rra + rra + rra + and #0xf + ld VIBRATO_SPEED(ix), a + + ;; depth, clamped to [1..16] + ld a, (hl) + and #0xf + inc a + ld VIBRATO_DEPTH(ix), a - ;; bc: next distance from current note - pop hl ; (prev distance) inc hl - ld b, (hl) - ;; hl: output next increment, scaled by depth (a) - ld a, VIBRATO_DEPTH(ix) - call vibrato_scale_increment - ret @@ -109,79 +114,100 @@ _post_bit1: ;;; Update the vibrato for the current channel -;;; Vibrato oscillates the current note's frequency between the previous -;;; and the next semitones of the current note, and follows a sine wave. +;;; Vibrato oscillates the current fixed-point note's between the previous +;;; and the next semitones [-1.0..+1.0], and follows a sine wave. ;;; This function update the frequency by one step among the 64 steps ;;; defined in the sine wave. ;;; ------ ;;; IN: ;;; ix: mirrored state of the current fm channel -;;; OUT: -;;; hl: new note for step (FM: f-num, SSG: period) ;;; bc, de, hl modified vibrato_eval_step:: ;; e: next vibrato pos ld a, VIBRATO_POS(ix) add a, VIBRATO_SPEED(ix) - and #63 - ld e, a + and #(VIBRATO_PRECALC_SIZE-1) ld VIBRATO_POS(ix), a - + ;; e: offset for sine precalc + sla a + ld e, a ;; hl: pos in sine precalc ld hl, #sine ld a, l add e ld l, a - ;; a: sine precalc (a2a1a0) - ld a, (hl) + ;; bc: displacement from sine precalc + ld c, (hl) + inc hl + ld b, (hl) - ;; bc: increment for next or previous semitone based on - ;; precalc's sign (a3) - bit 3, a - jr z, _prev_semitone - ld c, VIBRATO_NEXT(ix) - ld b, VIBRATO_NEXT+1(ix) - jr _post_increment -_prev_semitone: - ld c, VIBRATO_PREV(ix) - ld b, VIBRATO_PREV+1(ix) -_post_increment: - - ;; scale increment (with scale precalc) - - ;; multiply increment by the precalc factor (0..7) - ld h, b - ld l, c + ;; hl: displacement = precalc * depth scaling +_v_mul: + ld a, #0 + ld l, a ; precalc's 4 LSB + ld h, a ; precalc's 4 MSB + ld e, a ; precalc's 9th bit + ld d, a ; precalc's sign + + ;; d: precalc sign + sla b + rl d + srl b + + ;; a effect depth clamped to [1..16] (5 bits) + ld a, VIBRATO_DEPTH(ix) + ;; TODO move shifts in the vibrato setup, and store bounds + ;; as [0..15], to avoid shifts at every tick (FM and SSG) + sla a + sla a + sla a + sla a + jr nc, _v_post_bit4 + add hl, bc +_v_post_bit4: add hl, hl - ld d, h - ld e, l + add a, a + jr nc, _v_post_bit3 + add hl, bc +_v_post_bit3: add hl, hl - bit 2, a - jr nz, _post_mul_a2 - ld h, #0 - ld l, h -_post_mul_a2: - bit 1, a - jr z, _post_mul_a1 - add hl, de -_post_mul_a1: - bit 0, a - jr z, _post_mul_a0 + add a, a + jr nc, _v_post_bit2 add hl, bc -_post_mul_a0: - ;; hl is now bc * magnitude(a), keep the integral part only - ;; and extend sign to 16bits - ld a, h +_v_post_bit2: + add hl, hl + add a, a + jr nc, _v_post_bit1 + add hl, bc +_v_post_bit1: + ;; this 16bit shift might overflow now if the precalc is one full + ;; note displacement (0x1000) and depth is full (0x10). Recall + ;; the potential overflow bit in e + add hl, hl + rl e + add a, a + jr nc, _v_post_bit0 + add hl, bc +_v_post_bit0: + + ;; after scaling, h holds the floating part of the displacement + ;; and e holds the 9th bit of the displacement + ld l, h + ld h, e + + ;; negate the position based on the precalc's sign + bit 0, d + jr z, _v_post_sign + xor a + sub l ld l, a - add a - sbc a + sbc a, a + sub h ld h, a +_v_post_sign: - ;; de: current note freq - ld e, NOTE_OFFSET(ix) - ld d, NOTE_OFFSET+1(ix) - ;; hl: new note - add hl, de + ld VIBRATO_POS16(ix), l + ld VIBRATO_POS16+1(ix), h ret diff --git a/nullsound/fx-vol-slide.s b/nullsound/fx-vol-slide.s new file mode 100644 index 0000000..640d7fc --- /dev/null +++ b/nullsound/fx-vol-slide.s @@ -0,0 +1,88 @@ +;;; +;;; 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 . + +;;; Volume slide effect, common functions for FM and SSG +;;; + + .module nullsound + + .include "ym2610.inc" + .include "struct-fx.inc" + + .area CODE + + +;;; Enable volume slide effect for the current channel +;;; TODO: handle slide up +;;; ------ +;;; ix : state for channel +;;; a : slide direction: 0 == up, 1 == down +;;; bc : volume increment +;;; d : max volume +;;; [ hl ]: speed (4bits) +;;; [ hl modified ] +vol_slide_init:: + ;; a: speed + ld a, (hl) + inc hl + + ;; 0 speed means 'disable FX' + cp #0 + jr nz, _setup_vol_slide + res BIT_FX_VOL_SLIDE, FX(ix) + ret +_setup_vol_slide: + ;; setup FX + ld a, #0x40 + ld VOL_SLIDE_INC16(ix), c + ld VOL_SLIDE_INC16+1(ix), b + ld a, #0 + ld VOL_SLIDE_POS16(ix), a + ld VOL_SLIDE_POS16+1(ix), a + ld a, #15 + ld VOL_SLIDE_END(ix), d + + ;; enable FX + set BIT_FX_VOL_SLIDE, FX(ix) + + ret + + +;;; Update the volume slide for the current channel by one increment +;;; ------ +;;; IN: +;;; ix: state for the current channel +eval_vol_slide_step:: + push hl + push bc + ld l, VOL_SLIDE_POS16(ix) + ld h, VOL_SLIDE_POS16+1(ix) + ld c, VOL_SLIDE_INC16(ix) + ld b, VOL_SLIDE_INC16+1(ix) + add hl, bc + ld a, h + cp VOL_SLIDE_END(ix) + jr c, _post_vol_slide_clamp + ;; the slide FX reached past its end, clamp it + ld h, VOL_SLIDE_END(ix) +_post_vol_slide_clamp: + ld VOL_SLIDE_POS16(ix), l + ld VOL_SLIDE_POS16+1(ix), h + pop bc + pop hl + ret diff --git a/nullsound/nss-fm.s b/nullsound/nss-fm.s index 66148cd..c2850ff 100644 --- a/nullsound/nss-fm.s +++ b/nullsound/nss-fm.s @@ -25,32 +25,53 @@ .include "struct-fx.inc" - .equ NSS_FM_INSTRUMENT_PROPS, 28 - .equ NSS_FM_NEXT_REGISTER, 4 - .equ NSS_FM_NEXT_REGISTER_GAP, 16 - .equ NSS_FM_END_OF_REGISTERS, 0xb3 - .equ INSTR_TL_OFFSET, 30 - .equ INSTR_FB_ALGO_OFFSET, 28 - .equ INSTR_ALGO_MASK, 7 - .equ L_R_MASK, 0xc0 - .equ AMS_PMS_MASK, 0x37 - - .equ FM_STATE_SIZE,(state_fm_end-state_fm) - .equ FM_FX,(state_fm_fx-state_fm) - - .equ NOTE_SEMITONE,(state_fm_note_semitone-state_fm) - .equ NOTE_FNUM,(state_fm_note_fnum-state_fm) - .equ NOTE_BLOCK,(state_fm_note_block-state_fm) - - .equ OP1_BIT, 1 - .equ OP2_BIT, 4 - .equ OP3_BIT, 2 - .equ OP4_BIT, 8 - - ;; this is to use IY as two IYH and IYL 8bits registers - .macro dec_iyl - .db 0xfd, 0x2d - .endm + .lclequ FM_STATE_SIZE,(state_fm_end-state_fm) + + ;; FM constants + .lclequ NSS_FM_INSTRUMENT_PROPS, 28 + .lclequ NSS_FM_NEXT_REGISTER, 4 + .lclequ NSS_FM_NEXT_REGISTER_GAP, 16 + .lclequ NSS_FM_END_OF_REGISTERS, 0xb3 + .lclequ INSTR_TL_OFFSET, 30 + .lclequ INSTR_FB_ALGO_OFFSET, 28 + .lclequ INSTR_ALGO_MASK, 7 + .lclequ L_R_MASK, 0xc0 + .lclequ AMS_PMS_MASK, 0x37 + .lclequ OP1_BIT, 1 + .lclequ OP2_BIT, 4 + .lclequ OP3_BIT, 2 + .lclequ OP4_BIT, 8 + + ;; getters for FM state + .lclequ NOTE,(state_fm_note_semitone-state_fm) + .lclequ NOTE_SEMITONE,(state_fm_note_semitone-state_fm) + .lclequ NOTE_POS16,(state_fm_note_pos16-state_fm) + .lclequ NOTE_FNUM,(state_fm_note_fnum-state_fm) + .lclequ NOTE_BLOCK,(state_fm_note_block-state_fm) + .lclequ INSTRUMENT, (state_fm_instrument-state_fm) + .lclequ OP1, (state_fm_op1_vol-state_fm) + .lclequ OP2, (state_fm_op2_vol-state_fm) + .lclequ OP3, (state_fm_op3_vol-state_fm) + .lclequ OP4, (state_fm_op4_vol-state_fm) + .lclequ OUT_OPS, (state_fm_out_ops-state_fm) + .lclequ OUT_OP1, (state_fm_out_op1-state_fm) + .lclequ VOL, (state_fm_vol-state_fm) + + ;; pipeline state for FM channel + .lclequ STATE_PLAYING, 0x01 + .lclequ STATE_EVAL_MACRO, 0x02 + .lclequ STATE_LOAD_NOTE, 0x04 + .lclequ STATE_LOAD_VOL, 0x08 + .lclequ STATE_LOAD_REGS, 0x10 + .lclequ STATE_LOAD_ALL, 0x1e + .lclequ STATE_CONFIG_VOL, 0x20 + .lclequ BIT_PLAYING, 0 + .lclequ BIT_EVAL_MACRO, 1 + .lclequ BIT_LOAD_NOTE, 2 + .lclequ BIT_LOAD_VOL, 3 + .lclequ BIT_LOAD_REGS, 4 + .lclequ BIT_CONFIG_VOL, 5 + .area DATA @@ -71,64 +92,55 @@ state_fm_ym2610_channel:: ;;; FM mirrored state state_fm: ;;; FM1 -state_fm_fx: .db 0 ; must be the first field on the FM state -;;; FX: slide -state_fm_slide: -state_fm_slide_speed: .db 0 ; number of increments per tick -state_fm_slide_depth: .db 0 ; distance in semitones -state_fm_slide_inc16: .dw 0 ; 1/8 semitone increment * speed -state_fm_slide_pos16: .dw 0 ; slide pos -state_fm_slide_end: .db 0 ; end note (octave/semitone) -;;; FX: vibrato -state_fm_vibrato: -state_fm_vibrato_speed: .db 0 ; vibrato_speed -state_fm_vibrato_depth: .db 0 ; vibrato_depth -state_fm_vibrato_pos: .db 0 ; vibrato_pos -state_fm_vibrato_prev: .dw 0 ; vibrato_prev -state_fm_vibrato_next: .dw 0 ; vibrato_next +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_fx_vol_slide: .blkb VOL_SLIDE_SIZE +state_fm_fx_slide: .blkb SLIDE_SIZE +state_fm_fx_vibrato: .blkb VIBRATO_SIZE +;;; FM-specific state ;;; Note state_fm_note: -state_fm_note_semitone: .db 0 ; note (octave+semitone) -state_fm_note_fnum: .dw 0 ; note base f-num -state_fm_note_block: .db 0 ; note block (multiplier) +state_fm_instrument: .blkb 1 ; instrument +state_fm_note_semitone: .blkb 1 ; NSS note (octave+semitone) to be played on the FM channel +state_fm_note_pos16: .blkb 2 ; channel's fixed-point note after the FX pipeline +state_fm_note_fnum: .blkb 2 ; channel's f-num after the FX pipeline +state_fm_note_block: .blkb 1 ; channel's FM block (multiplier) after the FX pipeline +;; volume +state_fm_vol: .blkb 1 ; configured note volume (attenuation) +state_fm_op1_vol: .blkb 1 ; configured volume for OP1 +state_fm_op2_vol: .blkb 1 ; configured volume for OP2 +state_fm_op3_vol: .blkb 1 ; configured volume for OP3 +state_fm_op4_vol: .blkb 1 ; configured volume for OP4 +state_fm_out_ops: .blkb 1 ; bitmask of output OPs based on the configured FM algorithm +state_fm_out_op1: .blkb 1 ; ym2610 volume for OP1 after the FX pipeline +state_fm_out_op2: .blkb 1 ; ym2610 volume for OP2 after the FX pipeline +state_fm_out_op3: .blkb 1 ; ym2610 volume for OP3 after the FX pipeline +state_fm_out_op4: .blkb 1 ; ym2610 volume for OP4 after the FX pipeline +;;; state_fm_end: ;;; FM2 +state_fm2: .blkb FM_STATE_SIZE ;;; FM3 +state_fm3: .blkb FM_STATE_SIZE ;;; FM4 +state_fm4: .blkb FM_STATE_SIZE ;;; detune per FM channel +;;; TODO move to the state struct state_fm_detune:: .db 0 .db 0 .db 0 .db 0 -;;; current note volume per FM channel -state_fm_vol:: - .db 0 - .db 0 - .db 0 - .db 0 - -;;; bitfields for the output OPs based on the channel's configured algorithm -state_fm_out_ops:: - .db 0 - .db 0 - .db 0 - .db 0 - -;;; current instrument per FM channel -state_fm_instr:: - .db 0 - .db 0 - .db 0 - .db 0 - ;;; current pan (and instrument's AMS PMS) per FM channel +;;; TODO move to the state struct state_pan_ams_pms:: .db 0 .db 0 @@ -155,18 +167,26 @@ init_nss_fm_state_tracker:: inc de ;; zero state up to instr, which has a different init state ld (hl), #0 - ld bc, #state_fm_instr-_state_fm_start - ldir - ;; init instr to a non-existing instr (0xff) - ld (hl), #0xff - ld bc, #4 + ld bc, #state_pan_ams_pms-_state_fm_start ldir ;; pan has L and R enabled by default (0xc0) ld (hl), #0xc0 ld bc, #3 ldir + ;; init instr to a non-existing instr (0xff) + ld a, #0xff + ld hl, #(state_fm+INSTRUMENT) + ld bc, #FM_STATE_SIZE + add hl, bc + ld (hl), a + add hl, bc + ld (hl), a + add hl, bc + ld (hl), a + add hl, bc + ld (hl), a ;; global FM volume is initialized in the volume state tracker - ;; init ym2610 function pointer + ;; init YM2610 function pointer ld a, #0xc3 ; jp 0x.... ld (ym2610_write_func), a call fm_ctx_reset @@ -189,6 +209,22 @@ fm_ctx_reset:: fm_ctx_set_current:: ;; set FM context ld (state_fm_channel), a + + ;; set FM struct pointer for context + ld ix, #state_fm + push bc + bit 0, a + jr z, _fm_ctx_post_bit0 + ld bc, #FM_STATE_SIZE + add ix, bc +_fm_ctx_post_bit0: + bit 1, a + jr z, _fm_ctx_post_bit1 + ld bc, #FM_STATE_SIZE*2 + add ix, bc +_fm_ctx_post_bit1: + pop bc + ;; set YM2610 channel value for context cp #2 jp c, _ctx_no_2 @@ -287,19 +323,6 @@ fm_ctx_4:: ret -;;; FM_INSTRUMENT_EXT -;;; Configure the operators of an FM channel based on an instrument's data -;;; ------ -;;; [ hl ]: FM channel -;;; [hl+1]: instrument number -fm_instrument_ext:: - ;; set new current FM channel - ld a, (hl) - call fm_ctx_set_current - inc hl - jp fm_instrument - - ;;; output OPs based on the layout of each FM algorithm of the YM2610 ;;; 7 6 5 4 3 2 1 0 ;;; ___ ___ ___ ___ OP4 OP2 OP3 OP1 @@ -314,232 +337,353 @@ fm_out_ops_table: .db 0xf ; algo 7: [1111] OP4, OP2, OP3, OP1 -;;; fm_set_out_ops_bitfield ;;; Configure the output OPs on an instrument's data ;;; ------ ;;; hl: instrument address +;;; modified: hl, bc fm_set_out_ops_bitfield:: + push iy push hl - - ;; de: OPs out address for channel - ld hl, #state_fm_out_ops - ld a, (state_fm_channel) - ld c, a - ld b, #0 - add hl, bc - ld d, h - ld e, l - - ;; hl: address of instrument data's algo - pop hl - ld bc, #INSTR_FB_ALGO_OFFSET - add hl, bc + ;; iy: address of instrument data + pop iy ;; a: algo - ld a, (hl) + ld a, INSTR_FB_ALGO_OFFSET(iy) and #INSTR_ALGO_MASK - ;; hl: bitfield address for algo + ;; hl: bitmask address for algo ld hl, #fm_out_ops_table ld c, a ld b, #0 add hl, bc - ;; set OPs out info for current channel + ;; set out OPs bitmask for current channel ld a, (hl) - ld (de), a + ld OUT_OPS(ix), a + pop iy ret -;;; fm_set_ops_level -;;; Configure the operator levels for the current FM channel -;;; based on an instrument's data +;;; Update the current state's output level for all OPs based on an instrument ;;; ------ -;;; hl: instrument address +;;; ix: state for the current channel +;;; hl: address of the instrument's data +;;; [hl, iy modified] fm_set_ops_level:: push hl + pop iy - ;; bc: current channel - ld a, (state_fm_channel) + ;; set base OP levels from instruments + ld a, INSTR_TL_OFFSET(iy) + ld OP1(ix), a + ld a, INSTR_TL_OFFSET+1(iy) + ld OP2(ix), a + ld a, INSTR_TL_OFFSET+2(iy) + ld OP3(ix), a + ld a, INSTR_TL_OFFSET+3(iy) + ld OP4(ix), a + + ld a, INSTR_FB_ALGO_OFFSET(iy) + and #INSTR_ALGO_MASK + + ;; hl: bitmask address for algo + ld hl, #fm_out_ops_table ld c, a ld b, #0 + add hl, bc + + ;; set out OPs bitmask for current channel + ld a, (hl) + ld OUT_OPS(ix), a + + ret + + +;;; Compute fixed-point note position after FX-pipeline +;;; ------ +;;; ix: state for the current channel +compute_fm_fixed_point_note:: + ;; hl: note from currently configured note (fixed point) + ld a, #0 + ld l, a + ld h, NOTE_SEMITONE(ix) - ;; clear pending volume change flag for current channel - ld hl, #state_fm_vol + ;; bc: slide offset if the slide FX is enabled + bit BIT_FX_SLIDE, FX(ix) + jr z, _fm_post_add_slide + ld c, SLIDE_POS16(ix) + ld b, SLIDE_POS16+1(ix) add hl, bc - res 7, (hl) - ;; d: note volume for current channel - ld d, (hl) +_fm_post_add_slide:: - ;; e: bitfields for the output OPs - ld hl, #state_fm_out_ops + ;; bc vibrato offset if the vibrato FX is enabled + bit BIT_FX_VIBRATO, FX(ix) + jr z, _fm_post_add_vibrato + ld c, VIBRATO_POS16(ix) + ld b, VIBRATO_POS16+1(ix) add hl, bc - ld e, (hl) +_fm_post_add_vibrato:: - ;; b: OP1 start register in YM2610 for current channel - res 1, a - add a, #REG_FM1_OP1_TOTAL_LEVEL + ;; update computed fixed-point note position + ld NOTE_POS16(ix), l + ld NOTE_POS16+1(ix), h + ret + + +;;; Compute the YM2610's volume registers value from the OP's volumes +;;; ------ +;;; modified: bc, de, hl +compute_ym2610_fm_vol:: + ;; a: note vol (attenuation) for current channel + ld a, VOL(ix) + bit BIT_FX_VOL_SLIDE, FX(ix) + jr z, _vol_post_clamp_up + ;; add slide down vol and clamp + add VOL_SLIDE_POS16+1(ix) + bit 7, a + jr z, _vol_post_clamp_up + ld a, #127 +_vol_post_clamp_up: + ;; b: intermediate attenuation (note vol + vol slide) ld b, a - ;; hl: ops total level (8bit add) - pop hl ; instrument address - ld a, #INSTR_TL_OFFSET - add a, l - ld l, a - adc a, h - sub l - ld h, a + ;; c: bitmask for the output OPs + sentinel bits for looping + ld a, OUT_OPS(ix) + or #0xF0 ; 4 bits => 4 total loads (1 load per OP). + ld c, a + + ;; hl: address of instrument's ops volumes + push ix + pop hl + ld de, #OP1 + add hl, de + + ;; de: address of computed volumes for ops register (8bit aligned add) + push ix + pop de + ld a, #OUT_OP1 + add e + ld e, a -_ops_loop: - ;; check whether current OP is an output - bit 0, e - jr z, _ops_no_out -_ops_out: - ;; configure OP from instrument's TL faded with current volume - ;; current OP's total level per instrument +_c_ops_loop: + ;; a: OP level ld a, (hl) - ;; mix with note volume and clamp - add d + inc hl + + ;; check whether OP is an output + bit 0, c + jr z, _c_ops_result + + ;; if so, subtract intermediate volume (attenuation) + add b bit 7, a - jr z, _ops_post_clamp + jr z, _c_ops_post_clamp ld a, #127 -_ops_post_clamp: - ;; CHECK aren't we always below 128 here? - and #0x7f - ;; last attenuation to match the configured FM output level +_c_ops_post_clamp: + + ;; substract global volume attenuation ;; NOTE: YM2610's FM output level ramp follows an exponential curve, ;; so we implement this output level attenuation via a basic ;; addition, clamped to 127 (max attenuation). - ld c, a + ld b, a ld a, (state_fm_volume_attenuation) - add c + add b bit 7, a - jr z, _ops_post_level_clamp + jr z, _c_ops_post_global_clamp ld a, #127 -_ops_post_level_clamp: - ld c, a - jr _ops_set_and_next -_ops_no_out: - ;; OP not an output, configure it from instrument TL only - ld c, (hl) -_ops_set_and_next: - call ym2610_write_func - ;; next OP in instrument data - inc hl - ;; next OP in YM2610 - ld a, b - add a, #NSS_FM_NEXT_REGISTER - ld b, a - ;; shirt right to get next OP in bitfield (keep e6 bit clean) - sra e - res 6, e - ld a, e - and #0xf - jr nz, _ops_loop -_ops_end_loop: +_c_ops_post_global_clamp: + +_c_ops_result: + ;; saved configured OP value + ld (de), a + inc de +_c_ops_next: + srl c + bit 4, c + jr nz, _c_ops_loop + ret -;;; fm_check_set_ops_level -;;; Check whether we need to reconfigure operators levels for the current FM channel +;;; Compute the YM2610's note registers value from state's fixed-point note ;;; ------ ;;; modified: bc, de, hl -fm_check_set_ops_level: - ;; hl: volume setting for current channel - ld hl, #state_fm_vol - ld a, (state_fm_channel) - ld c, a - ld b, #0 - add hl, bc +compute_ym2610_fm_note:: + ;; b: current note (integer part) + ld b, NOTE_POS16+1(ix) - ;; check pending volume change flag and update if necessary - bit 7, (hl) - jr nz, fm_set_ops_level_for_instr - ret -fm_set_ops_level_for_instr: - ;; hl: instrument for channel (8bit add) - ld hl, #state_fm_instr - ld a, (state_fm_channel) - add a, l + ;; d: octave and semitone from note + ld hl, #note_to_octave_semitone + ld a, l + add b ld l, a - ld a, (hl) + ld d, (hl) - ;; hl: instrument address in ROM + ;; c: block + ld a, d + and #0xf0 + sra a + ld NOTE_BLOCK(ix), a + + ;; a: current semitone + ld a, d + and #0xf + ;; hl: base f-num for current semitone (8bit-add) + ld hl, #fm_note_f_num sla a - ld c, a - ;; ld a, b - ld b, #0 - ld hl, (state_stream_instruments) - add hl, bc - ;; ld b, a - ld e, (hl) - inc hl - ld d, (hl) + add a, l + ld l, a + adc a, h + sub l + ld h, a + ld b, (hl) inc hl - push de - pop hl + ld c, (hl) + ld h, b + ld l, c + push hl ; + f-num + + ;; b: next semitone distance from current note + ;; TODO 8bit add or aligned add + ld a, d + and #0xf + ld hl, #fm_semitone_distance + add l + inc a + ld l, a + ld b, (hl) + ;; c: FM: intermediate frequency is positive + ld c, #0 + ;; e: intermediate note position (fractional part) + ld e, NOTE_POS16(ix) + ;; de: current intermediate frequency f_dist + call slide_intermediate_freq - jp fm_set_ops_level + pop hl ; - f-num + add hl, de + ld NOTE_FNUM(ix), l + ld NOTE_FNUM+1(ix), h + + ret -;;; update_fm_effects +;;; run_fm_pipeline ;;; ------ -;;; For all FM channels: -;;; - update the state of all enabled effects +;;; Run the entire FM pipeline once. for each FM channels: +;;; - update the state of all enabled FX +;;; - load specific parts of the state (note, vol...) into YM2610 registers ;;; Meant to run once per tick -update_fm_effects:: +run_fm_pipeline:: push de - ;; TODO should we consider IX and IY scratch registers? push iy push ix - ;; effects expect the right FM channel context, - ;; so save the current channel context and loop - ;; it artificially before calling the macro + ;; we loop though every channel during the execution, + ;; so save the current channel context ld a, (state_fm_channel) push af - ;; update mirrored state of all FM channels - - ld de, #state_fm ; FM1 mirrored state + ;; update state of all FM channels, starting from FM1 xor a - call fm_ctx_set_current ; fm ctx: fm1 - ld iy, #4 -_2_update_loop: - push de ; +state_mirrored - ;; hl: mirrored state - push de ; state_mirrored - pop hl ; state_mirrored - - ;; configure - ld a, (hl) -_fm_chk_fx_vibrato: - bit 0, a - jr z, _fm_chk_fx_slide +_fm_update_loop: + call fm_ctx_set_current + + ;; bail out if the current channel is not in use + ld a, PIPELINE(ix) + or a, FX(ix) + cp #0 + jp z, _end_fm_channel_pipeline + + ;; Pipeline action: evaluate one FX step for each enabled FX + + bit BIT_FX_VIBRATO, FX(ix) + jr z, _fm_post_fx_vibrato call eval_fm_vibrato_step - jr _fm_post_effects -_fm_chk_fx_slide: - bit 1, a - jr z, _fm_post_effects + set BIT_LOAD_NOTE, PIPELINE(ix) +_fm_post_fx_vibrato: + bit BIT_FX_SLIDE, FX(ix) + jr z, _fm_post_fx_slide call eval_fm_slide_step -_fm_post_effects: - ;; prepare to update the next channel - ;; de: next state_mirrored - pop hl ; -state_mirrored - ld bc, #FM_STATE_SIZE + set BIT_LOAD_NOTE, PIPELINE(ix) +_fm_post_fx_slide: + bit BIT_FX_VOL_SLIDE, FX(ix) + jr z, _fm_post_fx_vol_slide + call eval_vol_slide_step + set BIT_LOAD_VOL, PIPELINE(ix) +_fm_post_fx_vol_slide: + + ;; Pipeline action: make sure no load note takes place when not playing + bit BIT_PLAYING, PIPELINE(ix) + jr nz, _fm_post_check_playing + res BIT_LOAD_NOTE, PIPELINE(ix) +_fm_post_check_playing: + + ;; Pipeline action: load volume registers when the volume state is modified + bit BIT_LOAD_VOL, PIPELINE(ix) + jr z, _post_load_fm_vol + res BIT_LOAD_VOL, PIPELINE(ix) + + call compute_ym2610_fm_vol + + ;; hl: OPs volume data + push ix + pop hl + ld bc, #OUT_OP1 add hl, bc - ld d, h - ld e, l - ;; next FM context + + ;; b: OP1 start register in YM2610 for current channel ld a, (state_fm_channel) - inc a - call fm_ctx_set_current + res 1, a + add #REG_FM1_OP1_TOTAL_LEVEL + ld b, a + + ;; load all OPs volumes + ld d, #4 +fm_vol_load_loop: + ld c, (hl) + call ym2610_write_func + dec d + jr z, _post_load_fm_vol + inc hl + ld a, b + add a, #NSS_FM_NEXT_REGISTER + ld b, a + jr fm_vol_load_loop +_post_load_fm_vol: + + ;; Pipeline action: load note register when the note state is modified + bit BIT_LOAD_NOTE, PIPELINE(ix) + jr z, _post_load_fm_note + res BIT_LOAD_NOTE, PIPELINE(ix) + + call compute_fm_fixed_point_note + call compute_ym2610_fm_note + + ;; reload computed note + ld l, NOTE_FNUM(ix) + ld h, NOTE_FNUM+1(ix) + ld c, NOTE_BLOCK(ix) + call fm_set_fnum_registers - dec_iyl - jp nz, _2_update_loop + ;; start current FM channel (enable all OPs) + ld a, (state_fm_ym2610_channel) + or #0xf0 + ld c, a + ld b, #REG_FM_KEY_ON_OFF_OPS + call ym2610_write_port_a +_post_load_fm_note: + +_end_fm_channel_pipeline: + ;; next context + ld a, (state_fm_channel) + inc a + cp #4 + jr nc, _fm_end_pipeline + jp _fm_update_loop - ;; restore the real fm channel context +_fm_end_pipeline: + ;; restore the real channel context pop af call fm_ctx_set_current @@ -556,24 +700,15 @@ _fm_post_effects: ;;; ------ ;;; [ hl ]: volume [0-127] fm_vol:: - push de - - ;; de: volume for channel (8bit add) - ld de, #state_fm_vol - ld a, (state_fm_channel) - add a, e - ld e, a - ;; a: volume (difference from max volume) ld a, #127 sub (hl) inc hl ;; register pending volume configuration for channel - set 7, a - ld (de), a + ld VOL(ix), a + set BIT_LOAD_VOL, PIPELINE(ix) - pop de ld a, #1 ret @@ -591,23 +726,16 @@ fm_instrument:: push hl push de - ;; hl: instrument for channel (8bit add) - push af - ld hl, #state_fm_instr - ld a, (state_fm_channel) - add a, l - ld l, a - pop af - ;; if the current instrument for channel is not updated, bail out - ld b, (hl) + ld b, INSTRUMENT(ix) cp b jp z, _fm_instr_end ;; else recall new instrument for channel - ld (hl), a + ld INSTRUMENT(ix), a ;; stop current FM channel (disable all OPs) + ;; TODO move that to the pipeline? push af ld a, (state_fm_ym2610_channel) @@ -635,10 +763,7 @@ fm_instrument:: inc hl push de pop hl - - ;; save instrument address for helper funcs - push hl - push hl + push hl ; +instrument address ;; d: all FM properties ld d, #NSS_FM_INSTRUMENT_PROPS @@ -671,14 +796,12 @@ _fm_port_loop: _fm_end: ;; set the pan, AMS and PMS settings for this instrument call fm_set_pan_ams_pms - ;; set the output OPs for this instrument - pop hl - call fm_set_out_ops_bitfield - ;; adjust real volume for channel based on instrument's - ;; config and current note volume - pop hl + ;; set the state's output OPs from this instrument + pop hl ; -instrument address call fm_set_ops_level + set BIT_LOAD_VOL, PIPELINE(ix) + _fm_instr_end: pop de pop hl @@ -686,6 +809,7 @@ _fm_instr_end: ld a, #1 ret + ;;; fm_set_pan_ams_pms ;;; set the AMS and PMS settings for this instrument, ;;; augmented with the current pan config for the channel @@ -822,54 +946,16 @@ _detune_positive: ret -;;; Configure the FM channel based on a macro's data -;;; ------ -;;; IN: -;;; de: start offset in FM state data -;;; OUT -;;; de: start offset for the current channel -;;; de, c modified -fm_state_for_channel: - ;; c: current channel - ld a, (state_fm_channel) - ld c, a - ;; a: offset in bytes for current mirrored state - xor a - bit 1, c - jp z, _fm_post_double - ld a, #FM_STATE_SIZE - add a -_fm_post_double: - bit 0, c - jp z, _fm_post_plus - add #FM_STATE_SIZE -_fm_post_plus: - ;; de + a (8bit add) - add a, e - ld e, a - ret - - -;;; Update the vibrato for the current FM channel and update the YM2610 +;;; Update the vibrato for the current FM channel ;;; ------ ;;; hl: mirrored state of the current fm channel eval_fm_vibrato_step:: push hl push de push bc - push ix - - ;; ix: state fx for current channel - push hl - pop ix call vibrato_eval_step - ;; ;; configure FM channel with new frequency - ld c, NOTE_BLOCK(ix) - call fm_set_fnum_registers - - pop ix pop bc pop de pop hl @@ -877,154 +963,32 @@ eval_fm_vibrato_step:: ret -;;; Setup FM vibrato: position and increments -;;; ------ -;;; ix : ssg state for channel -;;; the note semitone must be already configured -fm_vibrato_setup_increments:: - push bc - push hl - push de - - ld hl, #fm_semitone_distance - ld a, NOTE_SEMITONE(ix) - and #0xf - add l - ld l, a - call vibrato_setup_increments - - ;; de: vibrato prev increment, fixed point (negate) - xor a - sub e - ld e, a - sbc a, a - sub d - ld d, a - ld VIBRATO_PREV(ix), e - ld VIBRATO_PREV+1(ix), d - ;; hl: vibrato next increment, fixed point - ld VIBRATO_NEXT(ix), l - ld VIBRATO_NEXT+1(ix), h - - pop de - pop hl - pop bc - ret - - -;;; Setup slide effect for the current FM channel -;;; ------ -;;; [ hl ]: speed (4bits) and depth (4bits) -;;; a : slide direction: 0 == up, 1 == down -fm_slide_common:: - push bc - push de - - ;; de: FX for channel - ld b, a - ld de, #state_fm_fx - call fm_state_for_channel - ld a, b - - ;; ix: FM state for channel - push de - pop ix - - call slide_init - ld e, NOTE_SEMITONE(ix) - call slide_setup_increments - - pop de - pop bc - - ret - - ;;; Update the slide for the current channel ;;; Slide moves up or down by 1/8 of semitone increments * slide depth. ;;; ------ -;;; hl: state for the current channel +;;; IN: +;;; hl: state for the current channel +;;; OUT: +;;; bc: eval_fm_slide_step:: push hl push de push bc - push ix + ;; push ix ;; update internal state for the next slide step call eval_slide_step ;; effect still in progress? cp a, #0 - jp nz, _fm_slide_add_intermediate - ;; otherwise reset note state and load into YM2610 - ld NOTE_SEMITONE(ix), d - ;; a: semitone - ld a, d - and #0xf - ;; hl: base f-num for current semitone (8bit-add) - ld hl, #fm_note_f_num - sla a - add a, l - ld l, a - adc a, h - sub l - ld h, a - ;; restore detune at the end of the effect if there was any - call fm_get_f_num - ld NOTE_FNUM(ix), l - ld NOTE_FNUM+1(ix), h - ld a, d - jr _fm_slide_load_fnum - -_fm_slide_add_intermediate: - ;; a: current semitone - ld a, SLIDE_POS16+1(ix) - and #0xf - ;; b: next semitone distance from current note - ld hl, #fm_semitone_distance - add l - inc a - ld l, a - ld b, (hl) - ;; c: FM: intermediate frequency is positive - ld c, #0 - ;; e: intermediate semitone position (fractional part) - ld e, SLIDE_POS16(ix) - ;; de: current intermediate frequency f_dist - call slide_intermediate_freq - - ;; a: semitone - ld a, SLIDE_POS16+1(ix) - and #0xf - ;; hl: base f-num for current semitone (8bit-add) - ld hl, #fm_note_f_num - sla a - add a, l - ld l, a - adc a, h - sub l - ld h, a - ld b, (hl) - inc hl - ld c, (hl) - ld h, b - ld l, c - - ;; load new frequency into the YM2610 - ;; hl: semitone frequency + f_dist - add hl, de - - ;; a: block - ld a, SLIDE_POS16+1(ix) - -_fm_slide_load_fnum: - and #0xf0 - sra a - ld NOTE_BLOCK(ix), a - ld c, a - call fm_set_fnum_registers + jp nz, _end_fm_slide_load_fnum2 + ;; otherwise set the end note as the new base note + ld a, NOTE(ix) + add d + ld NOTE(ix), a +_end_fm_slide_load_fnum2: - pop ix + ;; pop ix pop bc pop de pop hl @@ -1032,34 +996,15 @@ _fm_slide_load_fnum: ret -;;; FM_NOTE_ON_EXT -;;; Emit a specific note (frequency) on an FM channel -;;; ------ -;;; [ hl ]: FM channel -;;; [hl+1]: note (0xAB: A=octave B=semitone) -fm_note_on_ext:: - ;; set new current FM channel - ld a, (hl) - call fm_ctx_set_current - inc hl - jp fm_note_on - - ;;; FM_NOTE_ON ;;; Emit a specific note (frequency) on an FM channel ;;; ------ ;;; [ hl ]: note (0xAB: A=octave B=semitone) fm_note_on:: - push de push bc - ;; iy: note for channel - ld de, #state_fm - call fm_state_for_channel - push de - pop ix - ;; 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 @@ -1069,62 +1014,13 @@ fm_note_on:: ;; b: note (0xAB: A=octave B=semitone) ld b, (hl) inc hl - push hl ld NOTE_SEMITONE(ix), b - ;; check active effects - ld a, (ix) -_fm_on_check_vibrato: - bit 0, a - jr z, _fm_on_check_slide - ;; reconfigure increments for current semitone - call fm_vibrato_setup_increments -_fm_on_check_slide: - bit 1, a - jr z, _fm_on_post_fx - ;; reconfigure increments for current semitone - ld e, NOTE_SEMITONE(ix) - call slide_setup_increments -_fm_on_post_fx: - - ;; d: block (octave) - ld a, b - and #0xf0 - sra a - ld d, a - ld NOTE_BLOCK(ix), d - - ;; a: semitone - ld a, b - and #0xf - ;; hl: semitone -> f_num address - ld hl, #fm_note_f_num - sla a - ld b, #0 - ld c, a - add hl, bc - ;; hl: fnum address -> (de)tuned F-num - call fm_get_f_num - ld NOTE_FNUM(ix), l - ld NOTE_FNUM+1(ix), h - ;; c: block - ld c, d - call fm_set_fnum_registers - - ;; update volume if a change was requested prior - ;; to playing this new note. - call fm_check_set_ops_level - - ;; start current FM channel (enable all OPs) - ld a, (state_fm_ym2610_channel) - or #0xf0 - ld c, a - ld b, #REG_FM_KEY_ON_OFF_OPS - call ym2610_write_port_a + ld a, PIPELINE(ix) + or #(STATE_PLAYING|STATE_EVAL_MACRO|STATE_LOAD_NOTE) + ld PIPELINE(ix), a - pop hl pop bc - pop de ;; fm context will now target the next channel ld a, (state_fm_channel) @@ -1135,34 +1031,23 @@ _fm_on_post_fx: ret -;;; FM_NOTE_OFF_EXT -;;; Release the note on an FM channel. The sound will decay according -;;; to the current configuration of the FM channel's operators. -;;; ------ -;;; [ hl ]: FM channel -fm_note_off_ext:: - ;; set new current FM channel - ld a, (hl) - call fm_ctx_set_current - inc hl - jp fm_note_off - - ;;; FM_NOTE_OFF ;;; Release the note on an FM channel. The sound will decay according ;;; to the current configuration of the FM channel's operators. ;;; ------ fm_note_off:: push bc - - ;; stop all OP of FM channel + ;; stop all OPs of FM channel ld a, (state_fm_ym2610_channel) ld c, a ld b, #REG_FM_KEY_ON_OFF_OPS call ym2610_write_port_a - pop bc + ;; disable playback in the pipeline, any note lod_note bit + ;; will get cleaned during the next pipeline run + res BIT_PLAYING, PIPELINE(ix) + ;; FM context will now target the next channel ld a, (state_fm_channel) inc a @@ -1172,80 +1057,15 @@ fm_note_off:: ret -;;; OPX_SET_COMMON -;;; Set an operator's property for the current FM channel -;;; ------ -;;; [ b ]: register of the OP's property -;;; [ c ]: value - -opx_set_common:: - push bc - push de - - ;; e: fm channel - ld a, (state_fm_channel) - ld e, a - - ;; adjust register based on channel - bit 0, e - jp z, _no_adj - inc b -_no_adj: - call ym2610_write_func - - pop de - pop bc - ret - - -;;; Scale the volume of an operator based on the global FM -;;; attenuation, if this OP is configured as an output OP -;;; for the current channel -;;; ------ -;;; b: OP bit -;;; c: volume -;;; [a, bc, de modified] -opx_level_scale:: - ;; a: output OPs for the current FM channel - ld de, #state_fm_out_ops - ld a, (state_fm_channel) - add e - ld e, a - ld a, (de) - - ;; if the volume updates an output OP, scale it to - ;; match the currently configured FM output level - and b - jr z, _post_opx_level_scale - ;; NOTE: YM2610's FM output level ramp follows an exponential curve, - ;; so we implement this output level attenuation via a basic - ;; addition, clamped to 127 (max attenuation). - ld a, (state_fm_volume_attenuation) - add c - bit 7, a - jr z, _opx_post_level_clamp - ld a, #127 -_opx_post_level_clamp: - ld c, a -_post_opx_level_scale: - ret - - ;;; OP1_LVL ;;; Set the volume of OP1 for the current FM channel ;;; ------ ;;; [ hl ]: volume level op1_lvl:: - push bc - push de - ld c, (hl) + ld a, (hl) inc hl - ld b, #OP1_BIT - call opx_level_scale - ld b, #REG_FM1_OP1_TOTAL_LEVEL - call opx_set_common - pop de - pop bc + ld OP1(ix), a + set BIT_LOAD_VOL, PIPELINE(ix) ld a, #1 ret @@ -1255,16 +1075,10 @@ op1_lvl:: ;;; ------ ;;; [ hl ]: volume level op2_lvl:: - push bc - push de - ld c, (hl) + ld a, (hl) inc hl - ld b, #OP2_BIT - call opx_level_scale - ld b, #REG_FM1_OP2_TOTAL_LEVEL - call opx_set_common - pop de - pop bc + ld OP2(ix), a + set BIT_LOAD_VOL, PIPELINE(ix) ld a, #1 ret @@ -1274,16 +1088,10 @@ op2_lvl:: ;;; ------ ;;; [ hl ]: volume level op3_lvl:: - push bc - push de - ld c, (hl) + ld a, (hl) inc hl - ld b, #OP3_BIT - call opx_level_scale - ld b, #REG_FM1_OP3_TOTAL_LEVEL - call opx_set_common - pop de - pop bc + ld OP3(ix), a + set BIT_LOAD_VOL, PIPELINE(ix) ld a, #1 ret @@ -1293,16 +1101,10 @@ op3_lvl:: ;;; ------ ;;; [ hl ]: volume level op4_lvl:: - push bc - push de - ld c, (hl) + ld a, (hl) inc hl - ld b, #OP4_BIT - call opx_level_scale - ld b, #REG_FM1_OP4_TOTAL_LEVEL - call opx_set_common - pop de - pop bc + ld OP4(ix), a + set BIT_LOAD_VOL, PIPELINE(ix) ld a, #1 ret @@ -1312,92 +1114,60 @@ op4_lvl:: ;;; ------ ;;; [ hl ]: speed (4bits) and depth (4bits) fm_vibrato:: - push bc - push de - - ;; de: fx for channel - ld de, #state_fm_fx - call fm_state_for_channel - push de - pop ix + ;; TODO: move this part to common vibrato_init ;; hl == 0 means disable vibrato ld a, (hl) cp #0 jr nz, _setup_fm_vibrato - push hl ; NSS stream pos ;; disable vibrato fx - ld a, FM_FX(ix) - res 0, a - ld FM_FX(ix), a - ;; reconfigure the original note into the YM2610 - ld l, NOTE_FNUM(ix) - ld h, NOTE_FNUM+1(ix) - ld c, NOTE_BLOCK(ix) - call fm_set_fnum_registers + res BIT_FX_VIBRATO, FX(ix) - pop hl ; NSS stream pos + ;; reload configured note at the next pipeline run + set BIT_LOAD_NOTE, PIPELINE(ix) + + inc hl jr _post_fm_vibrato_setup _setup_fm_vibrato: - ;; vibrato fx on - ld a, FM_FX(ix) - ;; if vibrato was in use, keep the current vibrato pos - bit 0, a - jp nz, _post_fm_vibrato_pos - ;; reset vibrato sine pos - ld VIBRATO_POS(ix), #0 -_post_fm_vibrato_pos: - set 0, a - ld FM_FX(ix), a - - ;; speed - ld a, (hl) - rra - rra - rra - rra - and #0xf - ld VIBRATO_SPEED(ix), a - - ;; depth, clamped to [1..16] - ld a, (hl) - and #0xf - inc a - ld VIBRATO_DEPTH(ix), a - - ;; increments for last configured note - call fm_vibrato_setup_increments + call vibrato_init _post_fm_vibrato_setup: - inc hl - - pop de - pop bc ld a, #1 ret -;;; FM_SLIDE_UP +;;; FM_NOTE_SLIDE_UP ;;; Enable slide up effect for the current FM channel ;;; ------ ;;; [ hl ]: speed (4bits) and depth (4bits) -fm_slide_up:: +fm_note_slide_up:: ld a, #0 - call fm_slide_common + call slide_init ld a, #1 ret -;;; FM_SLIDE_DOWN +;;; FM_NOTE_SLIDE_DOWN ;;; Enable slide down effect for the current FM channel ;;; ------ ;;; [ hl ]: speed (4bits) and depth (4bits) -fm_slide_down:: +fm_note_slide_down:: + ld a, #1 + call slide_init + ld a, #1 + ret + + +;;; FM_PITCH_SLIDE_DOWN +;;; Enable slide down effect for the current FM channel +;;; ------ +;;; [ hl ]: speed (8bits) +fm_pitch_slide_down:: ld a, #1 - call fm_slide_common + call slide_pitch_init ld a, #1 ret @@ -1441,3 +1211,23 @@ fm_pan:: pop de ld a, #1 ret + + +;;; FM_VOL_SLIDE_DOWN +;;; Enable volume slide down effect for the current FM channel +;;; ------ +;;; [ hl ]: speed (4bits) +fm_vol_slide_down:: + push bc + push de + + ld bc, #0x40 + ld d, #127 + ld a, #1 + call vol_slide_init + + pop de + pop bc + + ld a, #1 + ret diff --git a/nullsound/nss-ssg.s b/nullsound/nss-ssg.s index e80ffe5..ef094be 100644 --- a/nullsound/nss-ssg.s +++ b/nullsound/nss-ssg.s @@ -24,19 +24,41 @@ .include "ym2610.inc" .include "struct-fx.inc" - - .equ NOTE_OFFSET,(state_mirrored_ssg_note-state_mirrored_ssg) - .equ NOTE_SEMITONE_OFFSET,(state_mirrored_ssg_note_semitone-state_mirrored_ssg) - .equ PROPS_OFFSET,(state_mirrored_ssg_props-state_mirrored_ssg) - .equ ENVELOPE_OFFSET,(state_mirrored_ssg_envelope-state_mirrored_ssg) - .equ WAVEFORM_OFFSET,(state_mirrored_ssg_waveform-state_mirrored_ssg) - .equ SSG_STATE_SIZE,(state_mirrored_ssg_end-state_mirrored_ssg) - .equ SSG_FX,(state_fx-state_mirrored_ssg) - - ;; this is to use IY as two IYH and IYL 8bits registers - .macro dec_iyl - .db 0xfd, 0x2d - .endm + + .lclequ SSG_STATE_SIZE,(state_mirrored_ssg_end-state_mirrored_ssg) + ;; .lclequ PIPELINE,(state_ssg_pipeline-state_mirrored_ssg) + + ;; getters for SSG state + .lclequ NOTE,(state_ssg_note-state_mirrored_ssg) + .lclequ NOTE_POS16,(state_ssg_note_pos16-state_mirrored_ssg) + .lclequ NOTE_FINE_COARSE,(state_ssg_note_fine_coarse-state_mirrored_ssg) + .lclequ PROPS_OFFSET,(state_mirrored_ssg_props-state_mirrored_ssg) + .lclequ ENVELOPE_OFFSET,(state_mirrored_ssg_envelope-state_mirrored_ssg) + .lclequ WAVEFORM_OFFSET,(state_mirrored_ssg_waveform-state_mirrored_ssg) + .lclequ MACRO_DATA,(state_ssg_macro_data-state_mirrored_ssg) + .lclequ MACRO_POS,(state_ssg_macro_pos-state_mirrored_ssg) + .lclequ MACRO_LOAD,(state_ssg_macro_load-state_mirrored_ssg) + .lclequ REG_VOL, (state_ssg_reg_vol-state_mirrored_ssg) + .lclequ VOL, (state_ssg_vol-state_mirrored_ssg) + .lclequ OUT_VOL, (state_ssg_out_vol-state_mirrored_ssg) + + ;; pipeline state for SSG channel + .lclequ STATE_PLAYING, 0x01 + .lclequ STATE_EVAL_MACRO, 0x02 + .lclequ STATE_LOAD_NOTE, 0x04 + .lclequ STATE_LOAD_WAVEFORM, 0x08 + .lclequ STATE_LOAD_VOL, 0x10 + .lclequ STATE_LOAD_REGS, 0x20 + .lclequ STATE_STOP_NOTE, 0x40 + .lclequ BIT_PLAYING, 0 + .lclequ BIT_EVAL_MACRO, 1 + .lclequ BIT_LOAD_NOTE, 2 + .lclequ BIT_LOAD_WAVEFORM, 3 + .lclequ BIT_LOAD_VOL, 4 + .lclequ BIT_LOAD_REGS, 5 + .lclequ BIT_STOP_NOTE, 6 + + .area DATA @@ -44,7 +66,7 @@ ;;; ------ ;; This padding ensures the entire _state_ssg data sticks into ;; a single 256 byte boundary to make 16bit arithmetic faster - ;; .blkb 90 + .blkb 117 _state_ssg_start: @@ -52,16 +74,6 @@ _state_ssg_start: state_ssg_channel:: .db 0 -;;; address of the current instrument macro for all SSG channels -state_macro: - .blkw 3 - -state_macro_pos: - .blkw 3 - -state_macro_load_func: - .blkw 3 - ;;; YM2610 mirrored state ;;; ------ ;;; used to compute final register values to be loaded into the YM2610 @@ -69,44 +81,33 @@ state_macro_load_func: ;;; merged waveforms of all SSG channels for REG_SSG_ENABLE state_mirrored_enabled: .db 0 - + ;;; ssg mirrored state state_mirrored_ssg: ;;; SSG A state_mirrored_ssg_a: -state_fx: - .db 0 ; must be the first field on the ssg state -;;; FX: slide -state_slide: -state_slide_speed: .db 0 ; number of increments per tick -state_slide_depth: .db 0 ; distance in semitones -state_slide_inc16: .dw 0 ; 1/8 semitone increment * speed -state_slide_pos16: .dw 0 ; slide pos -state_slide_end: .db 0 ; end note (octave/semitone) -;;; FX: vibrato -state_vibrato: -state_vibrato_speed: - .db 0 ; vibrato_speed -state_vibrato_depth: - .db 0 ; vibrato_depth -state_vibrato_pos: - .db 0 ; vibrato_pos -state_vibrato_prev: - .dw 0 ; vibrato_prev -state_vibrato_next: - .dw 0 ; vibrato_next -state_mirrored_ssg_note_semitone: - .db 0 ; note (octave+semitone) -state_mirrored_ssg_note: - .dw 0 ; note (fine+coarse) +state_ssg_pipeline: .blkb 1 ; actions to run at every tick (eval macro, load note, vol, other regs) +state_ssg_fx: .blkb 1 ; enabled FX for this channel +;;; FX state trackers +state_ssg_fx_vol_slide: .blkb VOL_SLIDE_SIZE +state_ssg_fx_slide: .blkb SLIDE_SIZE +state_ssg_fx_vibrato: .blkb VIBRATO_SIZE +;;; SSG-specific state +;;; Note +state_ssg_note_pos16: .blkb 2 ; fixed-point note after the FX pipeline +state_ssg_note: .blkb 1 ; NSS note to be played on the FM channel +state_ssg_note_fine_coarse: .blkb 2 ; YM2610 note factors (fine+coarse) state_mirrored_ssg_props: -state_mirrored_ssg_envelope: - .db 0 ; envelope shape - .db 0 ; vol envelope fine - .db 0 ; vol envelope coarse - .db 0 ; mode+volume -state_mirrored_ssg_waveform: - .db 0 ; noise+tone (shifted per channel) +state_mirrored_ssg_envelope: .blkb 1 ; envelope shape + .blkb 1 ; vol envelope fine + .blkb 1 ; vol envelope coarse +state_ssg_reg_vol: .blkb 1 ; mode+volume +state_mirrored_ssg_waveform: .blkb 1 ; noise+tone (shifted per channel) +state_ssg_macro_data: .blkb 2 ; adress of the start of the macro program +state_ssg_macro_pos: .blkb 2 ; address of the current position in the macro program +state_ssg_macro_load: .blkb 2 ; function to load the SSG registers modified by the macro program +state_ssg_vol: .blkb 1 ; note volume (attenuation) +state_ssg_out_vol: .blkb 1 ; ym2610 volume for SSG channel after the FX pipeline state_mirrored_ssg_end: ;;; SSG B state_mirrored_ssg_b: @@ -115,10 +116,6 @@ state_mirrored_ssg_b: state_mirrored_ssg_c: .blkb SSG_STATE_SIZE -;;; note volume, to be substracted from instrument/macro volume -state_note_vol: - .blkb 3 - ;;; Global volume attenuation for all SSG channels state_ssg_volume_attenuation:: .blkb 1 @@ -126,7 +123,7 @@ _state_ssg_end: .area CODE - + ;;; Reset SSG playback state. ;;; Called before playing a stream ;;; ------ @@ -142,14 +139,10 @@ init_nss_ssg_state_tracker:: ;; global SSG volume is initialized in the volume state tracker ld a, #0xff ld (state_mirrored_enabled), a - ld bc, #macro_noop_load - ld (state_macro_load_func), bc - ld (state_macro_load_func+2), bc - ld (state_macro_load_func+4), bc ret -;;; +;;; ;;; Macro instrument - internal functions ;;; @@ -157,25 +150,19 @@ init_nss_ssg_state_tracker:: ;;; update the mirror state for a SSG channel based on ;;; the macro program configured for this channel ;;; ------ -;;; IN: -;;; de: mirrored state of the current ssg channel -;;; hl: pointer to macro location for the current ssg channel -;;; OUT: -;;; de: address of the next macro step -;;; a: 1: step updated the mirrored state -;;; 0: end of macro (no update) ;;; bc, de, hl modified eval_macro_step:: - push hl ; macro location ptr - ;; hl: (hl) - ld a, (hl) - ld c, a - inc hl - ld a, (hl) - ld h, a - ld l, c - or c - jp z, _end_macro + ;; de: state_mirrored_ssg_props (8bit add) + push ix + pop de + ld a, e + add #PROPS_OFFSET + ld e, a + + ;; hl: macro location ptr + ld l, MACRO_POS(ix) + ld h, MACRO_POS+1(ix) + ;; update mirrored state with macro values ld a, (hl) inc hl @@ -191,153 +178,216 @@ _upd_macro: inc hl jp _upd_macro _end_upd_macro: - ;; return the end address of the step - ld d, h - ld e, l + ;; update load flags for this macro step + ld a, PIPELINE(ix) + or (hl) + inc hl + ld PIPELINE(ix), a + ;; did we reached the end of macro ld a, (hl) cp a, #0xff - jp nz, _end_macro - ;; end of macro, clear current macro (hl) - pop hl ; macro location ptr - xor a - ld (hl), a + jp nz, _finish_macro_step + ;; end of macro, set loop/no-loop information + ;; the load bits have been set in the previous step inc hl - ld (hl), a - ;; a: macro cleared, but still load this step - ld a, #1 + ld a, (hl) + ld MACRO_POS(ix), a + inc hl + ld a, (hl) + ld MACRO_POS+1(ix), a ret -_end_macro: - push hl +_finish_macro_step: + ;; keep track of the current location for the next call + ld MACRO_POS(ix), l + ld MACRO_POS+1(ix), h + ret + + +;;; Set the current SSG channel and SSG state context +;;; ------ +;;; a : SSG channel +ssg_ctx_set_current:: + ld (state_ssg_channel), a + ld ix, #state_mirrored_ssg + push bc + bit 0, a + jr z, _ssg_ctx_post_bit0 + ld bc, #SSG_STATE_SIZE + add ix, bc +_ssg_ctx_post_bit0: + bit 1, a + jr z, _ssg_ctx_post_bit1 + ld bc, #SSG_STATE_SIZE*2 + add ix, bc +_ssg_ctx_post_bit1: pop bc - pop hl ; macro location ptr - ;; (hl): bc - ld a, c - ld (hl), a - inc hl - ld a, b - ld (hl), a - ;; a: macro == 0, will drive the next load - or c - ret + ret - -;;; update_ssg_macros_and_effects + +;;; run_ssg_pipeline ;;; ------ -;;; For all ssg channels: +;;; Run the entire SSG pipeline once. for each SSG channels: ;;; - run a single round of macro steps configured -;;; - update the state of all enabled effects +;;; - update the state of all enabled FX +;;; - load specific parts of the state (note, vol...) into YM2610 registers ;;; Meant to run once per tick -update_ssg_macros_and_effects:: +run_ssg_pipeline:: push de ;; TODO should we consider IX and IY scratch registers? push iy push ix - ;; macros expect the right ssg channel context, - ;; so save the current channel context and loop - ;; it artificially before calling the macro + ;; we loop though every channel during the execution, + ;; so save the current channel context ld a, (state_ssg_channel) push af - ;; update mirrored state of all SSG channels - - ;; state: - ld de, #state_mirrored_ssg_props ; ssg_a mirror state - ld hl, #state_macro_pos ; ssg_a macro pos - ld ix, #state_macro_load_func ; ssg_a load function + ;; update mirrored state of all SSG channels, starting from SSGA xor a - ld (state_ssg_channel), a ; ssg ctx: ssg_a - - ld iy, #3 + _update_loop: - push hl ; macro_pos - push de ; state_mirrored - push de ; state_mirrored - call eval_macro_step - pop hl ; state_mirrored + call ssg_ctx_set_current - ;; skip loading for this channel if macro is finished + ;; bail out if the current channel is not in use + ld a, PIPELINE(ix) + or a, FX(ix) cp #0 - jr nz, _prepare_ld_call - inc ix - inc ix - jr _post_call_load_func -_prepare_ld_call: - ;; bc: load_func for this SSG channel - ld a, (ix) - ld c, a - inc ix - ld a, (ix) - ld b, a - inc ix + jp z, _end_ssg_channel_pipeline + + ;; Pipeline action: evaluate one macro step to update current state + bit BIT_EVAL_MACRO, PIPELINE(ix) + jr z, _ssg_pipeline_post_macro + res BIT_EVAL_MACRO, PIPELINE(ix) + + ;; the macro evaluation decides whether or not to load + ;; registers later in the pipeline, and if we must continue + ;; to evaluation the macro during the next pipeline run + call eval_macro_step +_ssg_pipeline_post_macro:: + - ;; a: bitfield representation of current channel + ;; Pipeline action: evaluate one FX step for each enabled FX + + bit BIT_FX_VIBRATO, FX(ix) + jr z, _ssg_post_fx_vibrato + call eval_ssg_vibrato_step + set BIT_LOAD_NOTE, PIPELINE(ix) +_ssg_post_fx_vibrato: + bit BIT_FX_SLIDE, FX(ix) + jr z, _ssg_post_fx_side + call eval_ssg_slide_step + set BIT_LOAD_NOTE, PIPELINE(ix) +_ssg_post_fx_side: + bit BIT_FX_VOL_SLIDE, FX(ix) + jr z, _ssg_post_fx_vol_slide + call eval_vol_slide_step + set #BIT_LOAD_VOL, PIPELINE(ix) +_ssg_post_fx_vol_slide: + + ;; Pipeline action: make sure no load note takes place when not playing + bit BIT_PLAYING, PIPELINE(ix) + jr nz, _ssg_post_check_playing + res BIT_LOAD_NOTE, PIPELINE(ix) +_ssg_post_check_playing: + + ;; Pipeline action: load note register when the note state is modified + bit BIT_LOAD_NOTE, PIPELINE(ix) + jr z, _post_load_ssg_note + res BIT_LOAD_NOTE, PIPELINE(ix) + + call compute_ssg_fixed_point_note + call compute_ym2610_ssg_note + + ;; YM2610: load note ld a, (state_ssg_channel) sla a - jp nz, _ld_call - inc a -_ld_call: + add #REG_SSG_A_FINE_TUNE + ld b, a + ld c, NOTE_FINE_COARSE(ix) + call ym2610_write_port_a + inc b + ld c, NOTE_FINE_COARSE+1(ix) + call ym2610_write_port_a +_post_load_ssg_note: - ;; check whether the current channel is playing a note - ld d, a - ld a, (state_mirrored_enabled) - xor #0xff - and d - jp z, _post_effects - ;; call the load_func (address: bc, args: hl) - ld de, #_post_call_load_func + ;; Pipeline action: load registers modified by macros + ;; (do not load if macro is finished) + bit BIT_LOAD_REGS, PIPELINE(ix) + jr z, _post_ssg_macro_load + res BIT_LOAD_REGS, PIPELINE(ix) +_prepare_ld_call: + + ;; de: return address + ld de, #_post_ssg_macro_load push de + + ;; bc: load_func for this SSG channel + ld c, MACRO_LOAD(ix) + ld b, MACRO_LOAD+1(ix) push bc - ret -_post_call_load_func: - ;; TODO: check whether effect should run before or after - ;; macros. Also, the load function should be generic to - ;; load note and volume even if only one of the macro or - ;; the effect was in use. - ;; hl: start of mirrored_ssg - pop hl ; state_mirrored - push hl ; state_mirrored - - ;; start of mirrored_ssg + + ;; call args: hl: state_mirrored_ssg_props (8bit aligned add) + push ix + pop hl ld a, l - sub #PROPS_OFFSET + add #PROPS_OFFSET ld l, a - ;; configure - ld a, (hl) -_ssg_chk_fx_vibrato: - bit 0, a - jr z, _ssg_chk_fx_slide - call eval_ssg_vibrato_step - jr _post_effects -_ssg_chk_fx_slide: - bit 1, a - jr z, _post_effects - call eval_ssg_slide_step -_post_effects: - ;; prepare to update the next channel - ;; de: next state_mirrored - pop hl ; state_mirrored - ld bc, #SSG_STATE_SIZE - add hl, bc - ld d, h - ld e, l - ;; hl: next macro_pos - pop hl ; macro_pos - inc hl - inc hl - ;; ix: next load function is already set + ;; indirect call + ret + +_post_ssg_macro_load: + + ;; Pipeline action: load volume registers when the volume state is modified + ;; Note: this is after macro load as currently, this step sets the VOL LOAD + ;; bit if the macro updated the volume register + bit BIT_LOAD_VOL, PIPELINE(ix) + jr z, _post_load_ssg_vol + res BIT_LOAD_VOL, PIPELINE(ix) + + call compute_ym2610_ssg_vol + + ;; load into ym2610 + ld c, OUT_VOL(ix) + ld a, (state_ssg_channel) + add #REG_SSG_A_VOLUME + ld b, a + call ym2610_write_port_a +_post_load_ssg_vol: + + + ;; Pipeline action: configure waveform and start note playback + + bit BIT_LOAD_WAVEFORM, PIPELINE(ix) + jr z, _post_load_waveform + res BIT_LOAD_WAVEFORM, PIPELINE(ix) + + ;; c: waveform (shifted for channel) + ld c, WAVEFORM_OFFSET(ix) + call waveform_for_channel + + ;; start note + ld a, (state_mirrored_enabled) + and c + ld (state_mirrored_enabled), a + ld b, #REG_SSG_ENABLE + ld c, a + call ym2610_write_port_a +_post_load_waveform: + +_end_ssg_channel_pipeline: ;; next ssg context ld a, (state_ssg_channel) inc a - ld (state_ssg_channel), a - - dec_iyl - jp nz, _update_loop + cp #3 + jr nc, _ssg_end_macro + call ssg_ctx_set_current + jp _update_loop +_ssg_end_macro: ;; restore the real ssg channel context pop af - ld (state_ssg_channel), a + call ssg_ctx_set_current pop ix pop iy @@ -345,84 +395,112 @@ _post_effects: ret - -;;; macro_noop_load -;;; no-op function when no macro is configured for a SSG channel -;;; TODO is it still in use? +;;; Update the current fixed-point position ;;; ------ -macro_noop_load: +;;; current note (integer) + all the note effects (fixed point) +compute_ssg_fixed_point_note:: + ;; hl: from currently configured note (fixed point) + ld a, #0 + ld l, a + ld h, NOTE(ix) + + ld a, FX(ix) + + ;; bc: add vibrato offset if the vibrato FX is enabled + bit 0, a + jr z, _ssg_post_add_vibrato + ld c, VIBRATO_POS16(ix) + ld b, VIBRATO_POS16+1(ix) + add hl, bc +_ssg_post_add_vibrato:: + ;; bc: add slide offset if the slide FX is enabled + bit 1, a + jr z, _ssg_post_add_slide + ld c, SLIDE_POS16(ix) + ld b, SLIDE_POS16+1(ix) + add hl, bc +_ssg_post_add_slide:: + + ;; update computed fixed-point note position + ld NOTE_POS16(ix), l + ld NOTE_POS16+1(ix), h ret +compute_ym2610_ssg_note:: + ;; b: current note (integer part) + ld b, NOTE_POS16+1(ix) - -;;; Mix requested volume with current note volume -;;; ------ -;;; b : channel -;;; c : requested volume -;;; [a, bc, hl modified] -ssg_mix_volume:: - push hl - ld hl, #state_note_vol - ;; hl + channel (8bit add) + ;; b: octave and semitone from note + ld hl, #note_to_octave_semitone + ld a, l + add b + ld l, a + ld b, (hl) + + ;; de: ym2610 base tune for note + ld hl, #ssg_tune ld a, b - add a, l + sla a ld l, a + ld e, (hl) + inc hl + ld d, (hl) + push de ; +base tune + + ;; b: next semitone distance from current note + ld hl, #ssg_semitone_distance + ld l, b + ld b, (hl) - ;; l: current note volume for channel - ld l, (hl) + ;; c: SSG: intermediate frequency is negative + ld c, #1 - ;; attenuate instrument volume with note volume, clamp to 0 - ld a, c - sub l - jr nc, _mix_set - ld a, #0 -_mix_set: - ;; last attenuation to match the configured SSG output level + ;; e: current position (fractional part) + ld e, NOTE_POS16(ix) + + ;; de: current intermediate frequency f_dist + call slide_intermediate_freq + + ;; hl: final ym2610 tune + pop hl ; -base tune + add hl, de + ld NOTE_FINE_COARSE(ix), l + ld NOTE_FINE_COARSE+1(ix), h + ret + + +;;; Blend all volumes together to yield the volume for the ym2610 register +;;; ------ +;;; [b modified] +compute_ym2610_ssg_vol:: + ;; a: current note volume for channel + ld a, REG_VOL(ix) + and #0xf + + ;; substract slide down FX volume if used (attenuation) + bit BIT_FX_VOL_SLIDE, FX(ix) + jr z, _post_ssg_sub_vol_slide + sub VOL_SLIDE_POS16+1(ix) +_post_ssg_sub_vol_slide: + + ;; substract configured volume (attenuation) + sub VOL(ix) + + ;; substract global volume attenuation ;; NOTE: YM2610's SSG output level ramp follows an exponential curve, - ;; so we implement this output level attenuation via a basic - ;; substraction, clamped to 0. - ld c, a + ;; so we implement this output level attenuation via a basic substraction + ld b, a ld a, (state_ssg_volume_attenuation) neg - add c + add b + + ;; clamp result volume bit 7, a - jr z, _ssg_post_level_clamp + jr z, _post_ssg_vol_clamp ld a, #0 -_ssg_post_level_clamp: - ld c, a - ld a, b - add #REG_SSG_A_VOLUME - ld b, a - call ym2610_write_port_a - pop hl - ret - +_post_ssg_vol_clamp: -;;; Configure the SSG channel based on a macro's data -;;; ------ -;;; IN: -;;; de: start offset in mirrored state data -;;; OUT -;;; de: start offset for the current channel -;;; de, c modified -mirrored_ssg_for_channel: - ;; c: current channel - ld a, (state_ssg_channel) - ld c, a - ;; a: offset in bytes for current mirrored state - xor a - bit 1, c - jp z, _m_post_double - ld a, #SSG_STATE_SIZE - add a -_m_post_double: - bit 0, c - jp z, _m_post_plus - add #SSG_STATE_SIZE -_m_post_plus: - ;; de + a (8bit add) - add a, e - ld e, a + ld OUT_VOL(ix), a ret @@ -432,7 +510,7 @@ _m_post_plus: ;;; c: waveform ;;; OUT ;;; c: shifted waveform for the current channel -;;; c modified +;;; [c modified] waveform_for_channel: ld a, (state_ssg_channel) bit 0, a @@ -453,7 +531,7 @@ _w_post_s1: ;;; [a modified - other registers saved] ssg_ctx_reset:: ld a, #0 - ld (state_ssg_channel), a + call ssg_ctx_set_current ret @@ -468,7 +546,7 @@ ssg_ctx_reset:: ssg_ctx_1:: ;; set new current SSG channel ld a, #0 - ld (state_ssg_channel), a + call ssg_ctx_set_current ld a, #1 ret @@ -479,7 +557,7 @@ ssg_ctx_1:: ssg_ctx_2:: ;; set new current SSG channel ld a, #1 - ld (state_ssg_channel), a + call ssg_ctx_set_current ld a, #1 ret @@ -490,7 +568,7 @@ ssg_ctx_2:: ssg_ctx_3:: ;; set new current SSG channel ld a, #2 - ld (state_ssg_channel), a + call ssg_ctx_set_current ld a, #1 ret @@ -501,14 +579,14 @@ ssg_ctx_3:: ;;; [ hl ]: macro number ssg_macro:: push de - + ;; a: macro ld a, (hl) inc hl push hl - ;; hl: macro address in ROM (hl:base + a:offset) + ;; hl: macro address from instruments ld hl, (state_stream_instruments) sla a ;; hl + a (8bit add) @@ -524,71 +602,23 @@ ssg_macro:: ld d, (hl) ld h, d ld l, e - - ;; de: push function in ROM - ;; TODO should be replaced by list of memory offsets to - ;; load into ym2610 registers. - ;; NOTE: the destination registers would be offset: - ;; - 0: for a SSG register shared across SSG channels - ;; - n: for targeting the (base+n'th) register (CHECK) - ld e, (hl) - inc hl - ld d, (hl) - inc hl - ;; hl (at this point): macro data - - ;; bc: address of current macro's data for current channel - ld bc, #state_macro - ld a, (state_ssg_channel) - sla a - ;; bc + a (8bit add) - add a, c - ld c, a - push bc ; save address of current macro's data - ld a, l - ld (bc), a - inc bc - ld a, h - ld (bc), a - - ;; configure push function for this channel - ld hl, #state_macro_load_func - ld a, (state_ssg_channel) - sla a - ;; hl + a (8bit add) - add a, l - ld l, a - ;; set push function - ld (hl), e + ;; initialize the state of the new macro + ld a, (hl) + ld MACRO_LOAD(ix), a inc hl - ld (hl), d - - ;; bc: mirrored state's properties for current channel - ld de, #state_mirrored_ssg_props - call mirrored_ssg_for_channel - ld b, d - ld c, e - - ;; mirrored: update mirrored state with macro's properties - pop hl ; (hl) = address of current macro's data - push bc ; save mirrored_state's properties - call eval_macro_step - ;; after this call, de points to the next macro step, - ;; which is the part meant to be played for notes - - ;; load the envelope shape into ym2610, if it's present - pop hl ; mirrored_state's properties - ;; ld bc, #ENVELOPE_OFFSET - ;; add hl, bc - ;; a: mirrored envelope shape ld a, (hl) - bit 7, a - jr nz, _on_post_load - ld b, #REG_SSG_ENV_SHAPE - ld c, a - call ym2610_write_port_a -_on_post_load: + ld MACRO_LOAD+1(ix), a + inc hl + ld MACRO_DATA(ix), l + ld MACRO_DATA+1(ix), h + ld MACRO_POS(ix), l + ld MACRO_POS+1(ix), h + + ;; reconfigure pipeline to start evaluating macro + ld a, PIPELINE(ix) + or #STATE_EVAL_MACRO + ld PIPELINE(ix), a pop hl pop de @@ -597,34 +627,16 @@ _on_post_load: ret -;;; Update the vibrato for the current FM channel and update the YM2610 +;;; Update the vibrato for the current SSG channel ;;; ------ -;;; hl: mirrored state of the current fm channel +;;; ix: mirrored state of the current fm channel eval_ssg_vibrato_step:: push hl push de push bc - push ix - - ;; ix: state fx for current channel - push hl - pop ix call vibrato_eval_step - ;; ;; configure FM channel with new frequency - ;; YM2610: load note - ld a, (state_ssg_channel) - sla a - add #REG_SSG_A_FINE_TUNE - ld b, a - ld c, l - call ym2610_write_port_a - inc b - ld c, h - call ym2610_write_port_a - - pop ix pop bc pop de pop hl @@ -632,176 +644,46 @@ eval_ssg_vibrato_step:: ret -;;; Setup SSG vibrato: position and increments -;;; ------ -;;; ix : ssg state for channel -;;; the note semitone must be already configured -ssg_vibrato_setup_increments:: - push bc - push hl - push de - - ld hl, #ssg_semitone_distance - ld l, NOTE_SEMITONE_OFFSET(ix) - call vibrato_setup_increments - - ;; de: vibrato prev increment, fixed point - ld VIBRATO_PREV(ix), e - ld VIBRATO_PREV+1(ix), d - ;; hl: vibrato next increment, fixed point (negate) - xor a - sub l - ld l, a - sbc a, a - sub h - ld h, a - ld VIBRATO_NEXT(ix), l - ld VIBRATO_NEXT+1(ix), h - - pop de - pop hl - pop bc - ret - - -;;; Setup slide effect for the current FM channel -;;; ------ -;;; [ hl ]: speed (4bits) and depth (4bits) -;;; a : slide direction: 0 == up, 1 == down -ssg_slide_common:: - push bc - push de - - ;; de: FX for channel - ld b, a - ld de, #state_fx - call mirrored_ssg_for_channel - ld a, b - - ;; ix: SSG state for channel - push de - pop ix - - call slide_init - ld e, NOTE_SEMITONE_OFFSET(ix) - call slide_setup_increments - - pop de - pop bc - - ret - - - ;;; Update the slide for the current channel ;;; Slide moves up or down by 1/8 of semitone increments * slide depth. ;;; ------ -;;; hl: state for the current channel +;;; IN: +;;; hl: state for the current channel +;;; OUT: +;;; bc: eval_ssg_slide_step:: - push hl push de - push bc - push ix ;; update internal state for the next slide step call eval_slide_step ;; effect still in progress? cp a, #0 - jp nz, _ssg_slide_add_intermediate - ;; otherwise reset note state and load into YM2610 - ;; ld NOTE_SEMITONE_OFFSET(ix), d - ;; hl: base note period for current semitone - ld hl, #ssg_tune - ld a, d - sla a - ld l, a - ld c, (hl) - inc hl - ld b, (hl) - ld h, b - ld l, c - ;; save new current note frequency - ld NOTE_OFFSET(ix), l - ld NOTE_OFFSET+1(ix), h - jr _ssg_slide_load_note - -_ssg_slide_add_intermediate: - ;; a: current semitone - ld a, SLIDE_POS16+1(ix) - ;; b: next semitone distance from current note - ld hl, #ssg_semitone_distance - ld l, a - ld b, (hl) - ;; c: SSG: intermediate frequency is negative - ld c, #1 - ;; e: intermediate semitone position (fractional part) - ld e, SLIDE_POS16(ix) - ;; de: current intermediate frequency f_dist - call slide_intermediate_freq + jp nz, _end_ssg_slide_step + ;; otherwise set the end note as the new base note + ld a, NOTE(ix) + add d + ld NOTE(ix), a +_end_ssg_slide_step: - ;; hl: base note period for current semitone - ld hl, #ssg_tune - ld a, SLIDE_POS16+1(ix) - sla a - ld l, a - ld c, (hl) - inc hl - ld b, (hl) - ld h, b - ld l, c - - ;; load new frequency into the YM2610 - ;; hl: semitone frequency + f_dist - add hl, de - -_ssg_slide_load_note: - ;; configure SSG channel with new note - ld a, (state_ssg_channel) - sla a - ld b, a - ld c, l - call ym2610_write_port_a - inc b - ld c, h - call ym2610_write_port_a - - pop ix - pop bc pop de - pop hl ret - + ;;; SSG_NOTE_OFF ;;; Release (stop) the note on the current SSG channel. ;;; ------ ssg_note_off:: - push de push bc - push hl - - ;; de: mirrored state for current channel - ld de, #state_mirrored_ssg - call mirrored_ssg_for_channel - - ;; stop effects - ld a, #0 - ld (de), a - - ;; de: mirrored waveform (8bit add) - ld a, #WAVEFORM_OFFSET - add a, e - ld e, a ;; c: disable mask (shifted for channel) - ld a, (de) + ld a, WAVEFORM_OFFSET(ix) ld b, #0xff xor b ld c, a call waveform_for_channel - + ;; stop channel ld a, (state_mirrored_enabled) or c @@ -817,27 +699,16 @@ ssg_note_off:: ld c, #0 call ym2610_write_port_a - ;; de: macro ptr for current channel (8bit add) - ld de, #state_macro_pos - ld a, (state_ssg_channel) - sla a - add a, e - ld e, a + pop bc - ;; remove current macro program - xor a - ld (de), a - inc de - ld (de), a - - pop hl - pop bc - pop de + ;; disable playback in the pipeline, any note lod_note bit + ;; will get cleaned during the next pipeline run + res BIT_PLAYING, PIPELINE(ix) - ;; ssg context will now target the next channel + ;; SSG context will now target the next channel ld a, (state_ssg_channel) inc a - ld (state_ssg_channel), a + call fm_ctx_set_current ld a, #1 ret @@ -848,194 +719,48 @@ ssg_note_off:: ;;; ------ ;;; [ hl ]: volume level ssg_vol:: - push de - - ;; de: note volume for current channel (8bit add) - ld de, #state_note_vol - ld a, (state_ssg_channel) - add a, e - ld e, a - ;; a: volume ld a, (hl) inc hl - ld b, a ;; (de): substracted mix volume (15-a) sub a, #15 neg - ld (de), a + ld VOL(ix), a - pop de + ;; reload configured vol at the next pipeline run + set BIT_LOAD_VOL, PIPELINE(ix) ld a, #1 ret - - + + ;;; SSG_NOTE_ON ;;; Emit a specific note (frequency) on a SSG channel ;;; ------ ;;; [ hl ]: note (0xAB: A=octave B=semitone) ssg_note_on:: - push de - push bc - ;; b: note (0xAB: A=octave B=semitone) ld a, (hl) + ld NOTE(ix), a ld b, a inc hl - push hl - - ;; init current macro program - - ;; hl: macro for current channel (8bit add) - ld hl, #state_macro - ld a, (state_ssg_channel) - sla a - add a, l - ld l, a - - ;; de: macro ptr for current channel (8bit add) - ;; +save ssg_macro - ld de, #state_macro_pos - ld a, (state_ssg_channel) - sla a - add a, e - ld e, a - push de - - ;; (de): start of macro program, from (hl) - ld a, b - ld bc, #2 - ldir - ld b, a - - ;; load ssg mirrored state - - ;; l: note - ld l, b - - ;; ;; de: mirrored note for current channel - ld de, #state_mirrored_ssg - call mirrored_ssg_for_channel - - ;; bc: mirrored_note_semitone, from mirrored_ssg (de) - ld b, d - ld c, e - ld a, #NOTE_SEMITONE_OFFSET - add c - ld c, a - ;; store current octave/semitone - ld a, l - ld (bc), a - - ;; bc: mirrored_note (expected: from semitone) - inc c - - push de - pop ix - - ;; check active effects - ld a, (de) -_on_check_vibrato: - bit 0, a - jr z, _on_check_slide - ;; reconfigure increments for current semitone - call ssg_vibrato_setup_increments -_on_check_slide: - bit 1, a - jr z, _on_post_fx - ;; reconfigure increments for current semitone - ld e, NOTE_SEMITONE_OFFSET(ix) - call slide_setup_increments -_on_post_fx: - - ;; de: ssg_note - ld d, b - ld e, c - - ;; mirrored: note frequency - ld a, l - ld hl, #ssg_tune - sla a - ld l, a - ldi - inc bc - ldi - inc bc - - ;; mirrored: update mirrored state with macro's properties - ;; TODO: check the offset of eval macro and w.r.t generated macro - pop hl ; ssg_macro - push bc ; mirrored_note - call eval_macro_step - - ;; load mirrored state into the YM2610 - - ;; YM2610: load note - pop hl ; mirrored_note - ld a, (state_ssg_channel) - sla a - add #REG_SSG_A_FINE_TUNE - ld b, a - ld c, (hl) - call ym2610_write_port_a - inc hl - inc b - ld c, (hl) - call ym2610_write_port_a - - ;; hl: go to ssg_props (expected: from ssg_note) - inc hl - - ;; YM2610: load properties (except waveform) - push hl ; mirrored_props - ;; de: pointer to push macro for current channel (8bit add) - ld de, #state_macro_load_func - ld a, (state_ssg_channel) - sla a - add a, e - ld e, a - ;; call macro - ld bc, #_on_ret - push bc - ld a, (de) - ld c, a - inc de - ld a, (de) - ld b, a - push bc - ret -_on_ret: + ;; init macro position + ld a, MACRO_DATA(ix) + ld MACRO_POS(ix), a + ld a, MACRO_DATA+1(ix) + ld MACRO_POS+1(ix), a - ;; YM2610: load waveform - pop hl ; mirrored_props - ;; hl: mirrored_waveform (8bit add) - ld a, #(WAVEFORM_OFFSET-PROPS_OFFSET) - add a, l - ld l, a - - ;; c: waveform (shifted for channel) - ld c, (hl) - call waveform_for_channel - - ;; start note - ld a, (state_mirrored_enabled) - and c - ld (state_mirrored_enabled), a - ld b, #REG_SSG_ENABLE - ld c, a - call ym2610_write_port_a - - pop hl - pop bc - pop de + ;; reload all registers at the next pipeline run + ld a, PIPELINE(ix) + or #(STATE_PLAYING|STATE_EVAL_MACRO|STATE_LOAD_NOTE) + ld PIPELINE(ix), a ;; ssg context will now target the next channel ld a, (state_ssg_channel) inc a - ld (state_ssg_channel), a + call fm_ctx_set_current ld a, #1 ret @@ -1072,85 +797,26 @@ ssg_env_period:: ;;; ------ ;;; [ hl ]: speed (4bits) and depth (4bits) ssg_vibrato:: - push bc - push de - - ;; de: fx for channel (expect: from mirrored_ssg) - ld de, #state_fx - call mirrored_ssg_for_channel + ;; TODO: move this part to common vibrato_init ;; hl == 0 means disable vibrato ld a, (hl) cp #0 - jr nz, _setup_vibrato - push hl ; save NSS stream pos - ;; disable vibrato fx - ld a, (de) - res 0, a - ld (de), a - ;; hl: address of original note frequency (8bit add) - ld h, d - ld a, #NOTE_OFFSET - add e - ld l, a - ;; reconfigure the note into the YM2610 - ld a, (state_ssg_channel) - sla a - add #REG_SSG_A_FINE_TUNE - ld b, a - ld c, (hl) - call ym2610_write_port_a - inc hl - inc b - ld c, (hl) - call ym2610_write_port_a - pop hl ; NSS stream pos - jr _post_setup + jr nz, _setup_ssg_vibrato -_setup_vibrato: - ;; ix: ssg state for channel - push de - pop ix - - ;; vibrato fx on - ld a, SSG_FX(ix) - ;; if vibrato was in use, keep the current vibrato pos - bit 0, a - jp nz, _post_ssg_vibrato_pos - ;; reset vibrato sine pos - ld VIBRATO_POS(ix), #0 -_post_ssg_vibrato_pos: - set 0, a - ld SSG_FX(ix), a - - ;; speed - ld a, (hl) - rra - rra - rra - rra - and #0xf - ld VIBRATO_SPEED(ix), a - - ;; depth, clamped to [1..16] - ld a, (hl) - and #0xf - inc a - ld VIBRATO_DEPTH(ix), a + ;; disable vibrato FX + res BIT_FX_VIBRATO, FX(ix) - ;; increments for last configured note - call ssg_vibrato_setup_increments + ;; reload configured note at the next pipeline run + set BIT_LOAD_NOTE, PIPELINE(ix) -_post_setup: inc hl + jr _post_ssg_setup - pop de - pop bc - - ;; de: fx for channel (expect: from mirrored_ssg) - ld de, #state_fx - call mirrored_ssg_for_channel +_setup_ssg_vibrato: + call vibrato_init +_post_ssg_setup: ld a, #1 ret @@ -1162,7 +828,7 @@ _post_setup: ;;; [ hl ]: speed (4bits) and depth (4bits) ssg_slide_up:: ld a, #0 - call ssg_slide_common + call slide_init ld a, #1 ret @@ -1173,6 +839,26 @@ ssg_slide_up:: ;;; [ hl ]: speed (4bits) and depth (4bits) ssg_slide_down:: ld a, #1 - call ssg_slide_common + call slide_init + ld a, #1 + ret + + +;;; SSG_VOL_SLIDE_DOWN +;;; Enable volume slide down effect for the current SSG channel +;;; ------ +;;; [ hl ]: speed (4bits) +ssg_vol_slide_down:: + push bc + push de + + ld bc, #0x40 + ld d, #15 + ld a, #1 + call vol_slide_init + + pop de + pop bc + ld a, #1 ret diff --git a/nullsound/stream.s b/nullsound/stream.s index cf16a73..1b09784 100644 --- a/nullsound/stream.s +++ b/nullsound/stream.s @@ -171,17 +171,17 @@ _loop_chs: ;; process the stream's next opcodes ;; hl: current stream's position - ld ix, (state_current_ch_stream) - ld l, CH_STREAM_POS(ix) - ld h, CH_STREAM_POS+1(ix) + ld iy, (state_current_ch_stream) + ld l, CH_STREAM_POS(iy) + ld h, CH_STREAM_POS+1(iy) _loop_opcode: call process_nss_opcode or a jp nz, _loop_opcode ;; no more opcodes can be processed, save stream's new pos - ld ix, (state_current_ch_stream) - ld CH_STREAM_POS(ix), l - ld CH_STREAM_POS+1(ix), h + ld iy, (state_current_ch_stream) + ld CH_STREAM_POS(iy), l + ld CH_STREAM_POS+1(iy), h _post_ch_process: ld a, (state_streams) ld b, a @@ -223,23 +223,21 @@ update_stream_state_tracker:: ld b, a ld a, (state_timer_ticks_count) cp b - ;; if we can't, check whether we have macros or effects to process - jp c, _check_update_macros_and_effects + jp c, _check_update_stream_pipeline + ;; process the NSS opcodes (this will update the pipeline) + ;; the next row processing will take place once a new row is reached. call update_streams_wait_rows call process_streams_opcodes - ;; reset row and tick reached counters and exit ld a, #0 ld (state_timer_ticks_count), a - jp _reset_tick_reached -_check_update_macros_and_effects: +_check_update_stream_pipeline: ld a, (state_timer_tick_reached) bit TIMER_CONSUMER_STREAM_BIT, a jp z, _end_update_stream - call update_fm_effects - call update_ssg_macros_and_effects -_reset_tick_reached: - ;; reset the 'tick reached' marker bit for this tracker, next - ;; macro/effect processing will take place once a new tick is reached + ;; process the current stream pipeline + ;; the next processing will take place once a new tick is reached + call run_fm_pipeline + call run_ssg_pipeline res TIMER_CONSUMER_STREAM_BIT, a ld (state_timer_tick_reached), a _end_update_stream: @@ -369,46 +367,46 @@ stream_all_ctx_switch:: stream_play_multi:: call stream_stop push de - pop ix + pop iy ;; setup current instruments ld (state_stream_instruments), bc ;; a: number of streams - ld a, (ix) + ld a, (iy) ld (state_streams), a ;; setup enabled channels bitfield for this music and ;; configure every stream with the right channel ctx opcode - inc ix - ld c, (ix) - ld b, 1(ix) + inc iy + ld c, (iy) + ld b, 1(iy) ld (state_ch_bits), bc call snd_configure_stream_ctx_switches ;; hl: stream data from NSS - inc ix - inc ix - push ix + inc iy + inc iy + push iy pop hl ;; init streams state - ld ix, #state_ch_stream + ld iy, #state_ch_stream ld de, #CH_STREAM_SIZE ld a, (state_streams) ld c, a _stream_play_init_loop: ;; a: stream data LSB ld a, (hl) - ld CH_STREAM_START(ix), a - ld CH_STREAM_POS(ix), a + ld CH_STREAM_START(iy), a + ld CH_STREAM_POS(iy), a inc hl ;; a: stream data MSB ld a, (hl) - ld CH_STREAM_START+1(ix), a - ld CH_STREAM_POS+1(ix), a + ld CH_STREAM_START+1(iy), a + ld CH_STREAM_POS+1(iy), a inc hl - add ix, de + add iy, de dec c jr nz, _stream_play_init_loop @@ -499,29 +497,33 @@ nss_opcodes: .nss_op ssg_slide_up .nss_op ssg_slide_down .nss_op fm_vibrato - .nss_op fm_slide_up - .nss_op fm_slide_down + .nss_op fm_note_slide_up + .nss_op fm_note_slide_down .nss_op adpcm_b_vol .nss_op adpcm_a_vol .nss_op fm_pan + .nss_op fm_vol_slide_down + .nss_op ssg_vol_slide_down + .nss_op fm_pitch_slide_down + ;;; Process a single NSS opcode ;;; ------ ;;; bc: address in the stream pointing to the opcode and its args -;;; [a, bc, ix modified - other registers saved] +;;; [a, bc, iy modified - other registers saved] process_nss_opcode:: ;; op ld a, (hl) inc hl ;; get function for opcode and tail call into it - ld ix, #nss_opcodes + ld iy, #nss_opcodes sla a ld b, #0 ld c, a - add ix, bc - ld b, 1(ix) - ld c, (ix) + add iy, bc + ld b, 1(iy) + ld c, (iy) push bc ret @@ -571,14 +573,14 @@ nss_jmp:: ld c, (hl) inc hl ld b, (hl) - ld ix, (state_current_ch_stream) + ld iy, (state_current_ch_stream) ;; hl: start of stream - ld l, CH_STREAM_START(ix) - ld h, CH_STREAM_START+1(ix) + ld l, CH_STREAM_START(iy) + ld h, CH_STREAM_START+1(iy) ;; hl: new pos (call offset) add hl, bc - ld CH_STREAM_POS(ix), l - ld CH_STREAM_POS+1(ix), h + ld CH_STREAM_POS(iy), l + ld CH_STREAM_POS+1(iy), h pop bc ld a, #1 ret @@ -588,7 +590,14 @@ nss_jmp:: ;;; signal the end of the NSS stream to the player ;;; ------ nss_end:: + ;; signal the end of playback for the entire music call stream_stop + ;; ensure that the current channel's pipeline is stopped + ;; TODO use FX and PIPELINE macros rather than hardcodes + xor a + ld (ix), a + ld 1(ix), a + ld a, #0 ret @@ -628,22 +637,22 @@ _post_wait_rows: nss_call:: push bc - ld ix, (state_current_ch_stream) + ld iy, (state_current_ch_stream) ;; bc: offset ld c, (hl) inc hl ld b, (hl) inc hl ;; save current stream pos - ld CH_STREAM_SAVED(ix), l - ld CH_STREAM_SAVED+1(ix), h + ld CH_STREAM_SAVED(iy), l + ld CH_STREAM_SAVED+1(iy), h ;; hl: start of stream - ld l, CH_STREAM_START(ix) - ld h, CH_STREAM_START+1(ix) + ld l, CH_STREAM_START(iy) + ld h, CH_STREAM_START+1(iy) ;; hl: new pos (call offset) add hl, bc - ld CH_STREAM_POS(ix), l - ld CH_STREAM_POS+1(ix), h + ld CH_STREAM_POS(iy), l + ld CH_STREAM_POS+1(iy), h pop bc ld a, #1 @@ -654,13 +663,13 @@ nss_call:: ;;; Continue playback past the previous NSS_CALL statement ;;; ------ nss_ret:: - ld ix, (state_current_ch_stream) + ld iy, (state_current_ch_stream) ;; hl: saved current stream pos - ld l, CH_STREAM_SAVED(ix) - ld h, CH_STREAM_SAVED+1(ix) + ld l, CH_STREAM_SAVED(iy) + ld h, CH_STREAM_SAVED+1(iy) ;; hl: restore new stream pos - ld CH_STREAM_POS(ix), l - ld CH_STREAM_POS+1(ix), h + ld CH_STREAM_POS(iy), l + ld CH_STREAM_POS+1(iy), h ld a, #1 ret diff --git a/nullsound/struct-fx.inc b/nullsound/struct-fx.inc index 6bdb8ff..ef77fb4 100644 --- a/nullsound/struct-fx.inc +++ b/nullsound/struct-fx.inc @@ -26,12 +26,21 @@ .area struct - .local fx_fx, fx_slide, fx_vibrato + .local pipeline, fx_fx, fx_vol_slide, fx_slide, fx_vibrato + .local _vol_slide_inc16, _vol_slide_pos16, _vol_slide_end, _vol_slide_size .local _slide_speed, _slide_depth, _slide_inc16, _slide_pos16, _slide_end, _slide_size - .local _vibrato_speed, _vibrato_depth, _vibrato_pos, _vibrato_prev, _vibrato_next, _vibrato_size + .local _vibrato_speed, _vibrato_depth, _vibrato_pos, _vibrato_pos16, _vibrato_size -;;; enabled FX. This must be the first field of a channel's state +;;; actions to run at every tick. This must be the first field of a channel's state +pipeline: .blkb 1 +;;; enabled FX. This must be the second field of a channel's state fx_fx: .blkb 1 +;;; FX: volume slide +fx_vol_slide: +_vol_slide_inc16: .blkw 1 ; volume slide speed +_vol_slide_pos16: .blkw 1 ; volume slide position (attenuation) +_vol_slide_end: .blkb 1 ; volume slide end (0: no attenuation) +_vol_slide_size: ;;; FX: slide fx_slide: _slide_speed: .blkb 1 ; number of increments per tick @@ -45,25 +54,33 @@ fx_vibrato: _vibrato_speed: .blkb 1 ; vibrato_speed _vibrato_depth: .blkb 1 ; vibrato_depth _vibrato_pos: .blkb 1 ; vibrato_pos -_vibrato_prev: .blkw 1 ; vibrato_prev -_vibrato_next: .blkw 1 ; vibrato_next +_vibrato_pos16: .blkw 1 ; vibrato_pos16 _vibrato_size: ;; FX getter for a channel's state ;; The offset assumes that that the FX structure are located ;; sequentially from the start of the channel's state - .lclequ FX, (fx_fx - fx_fx) - .lclequ SLIDE, (fx_slide - fx_fx) - .lclequ SLIDE_SPEED, (_slide_speed - fx_fx) - .lclequ SLIDE_DEPTH, (_slide_depth - fx_fx) - .lclequ SLIDE_INC16, (_slide_inc16 - fx_fx) - .lclequ SLIDE_POS16, (_slide_pos16 - fx_fx) - .lclequ SLIDE_END, (_slide_end - fx_fx) - .lclequ SLIDE_SIZE, (_vibrato_size - fx_slide) - .lclequ VIBRATO, (fx_vibrato - fx_fx) - .lclequ VIBRATO_SPEED, (_vibrato_speed - fx_fx) - .lclequ VIBRATO_DEPTH, (_vibrato_depth - fx_fx) - .lclequ VIBRATO_POS, (_vibrato_pos - fx_fx) - .lclequ VIBRATO_PREV, (_vibrato_prev - fx_fx) - .lclequ VIBRATO_NEXT, (_vibrato_next - fx_fx) + .lclequ PIPELINE, (pipeline - pipeline) + .lclequ FX, (fx_fx - pipeline) + .lclequ VOL_SLIDE, (fx_vol_slide - pipeline) + .lclequ VOL_SLIDE_INC16, (_vol_slide_inc16 - pipeline) + .lclequ VOL_SLIDE_POS16, (_vol_slide_pos16 - pipeline) + .lclequ VOL_SLIDE_END, (_vol_slide_end - pipeline) + .lclequ VOL_SLIDE_SIZE, (_vol_slide_size - fx_vol_slide) + .lclequ SLIDE, (fx_slide - pipeline) + .lclequ SLIDE_SPEED, (_slide_speed - pipeline) + .lclequ SLIDE_DEPTH, (_slide_depth - pipeline) + .lclequ SLIDE_INC16, (_slide_inc16 - pipeline) + .lclequ SLIDE_POS16, (_slide_pos16 - pipeline) + .lclequ SLIDE_END, (_slide_end - pipeline) + .lclequ SLIDE_SIZE, (_slide_size - fx_slide) + .lclequ VIBRATO, (fx_vibrato - pipeline) + .lclequ VIBRATO_SPEED, (_vibrato_speed - pipeline) + .lclequ VIBRATO_DEPTH, (_vibrato_depth - pipeline) + .lclequ VIBRATO_POS, (_vibrato_pos - pipeline) + .lclequ VIBRATO_POS16, (_vibrato_pos16 - pipeline) .lclequ VIBRATO_SIZE, (_vibrato_size - fx_vibrato) + + .lclequ BIT_FX_VIBRATO, 0 + .lclequ BIT_FX_SLIDE, 1 + .lclequ BIT_FX_VOL_SLIDE, 2 diff --git a/nullsound/volume.s b/nullsound/volume.s index b7b35ea..9dade07 100644 --- a/nullsound/volume.s +++ b/nullsound/volume.s @@ -24,9 +24,11 @@ .include "ym2610.inc" .include "ports.inc" .include "timer.inc" + .include "struct-fx.inc" - ;; TODO replace hardcoded offset with struct include - .equ PROPS_VOL_OFFSET, 21 + ;; TODO remove bitmask hardcodes + .lclequ FM_BIT_LOAD_VOL, 3 + .lclequ SSG_BIT_LOAD_VOL, 4 ;;; @@ -135,7 +137,7 @@ volume_reset_music_levels:: ld (state_volume_adpcm_a_master), a ;; reset channels level based on current music level call volume_update_channels_levels - call volume_update_ym2610 + call volume_update_stream_state pop hl pop de pop bc @@ -234,59 +236,52 @@ _fade_post_bit1: ;;; Update currently playing notes in the YM2610 to reflect the how ;;; the channels' output levels are currently configured in nullsound ;;; ------ -;;; [a, de, bc, hl modified] -volume_update_ym2610: +;;; [a, de, bc, hl, iy modified] +volume_update_stream_state: + push iy ;; d: FM + SSG channels in use ld a, (state_ch_bits) ld d, a - ;; save the current FM channel context - ld a, (state_fm_channel) - push af - ld a, #0 - ld (state_fm_channel), a - ;; Loop over all the FM channels that need to be updated - ;; e: total number of FM channels to process - ld e, #4 -_vol_fm_loop: - ;; channel used in the music? - bit 0, d - jr z, _vol_fm_next - push de - call fm_ctx_set_current - call fm_set_ops_level_for_instr - pop de - ld a, (state_fm_channel) -_vol_fm_next: - inc a - sra d - dec e - jr nz, _vol_fm_loop - ;; restore the current FM channel context - pop af - call fm_ctx_set_current - ;; update SSG channels, no loop here, it's smaller that way + ;; Notify the FM pipeline bit 0, d - jr z, _vol_ssg_b - ld a, (state_mirrored_ssg_a+PROPS_VOL_OFFSET) - ld c, a - ld b, #0 - call ssg_mix_volume -_vol_ssg_b: + jr z, _vol_upd_post_fm1 + ld iy, #state_fm1 + set FM_BIT_LOAD_VOL, PIPELINE(iy) +_vol_upd_post_fm1: bit 1, d - jr z, _vol_ssg_c - ld a, (state_mirrored_ssg_b+PROPS_VOL_OFFSET) - ld c, a - ld b, #1 - call ssg_mix_volume -_vol_ssg_c: + jr z, _vol_upd_post_fm2 + ld iy, #state_fm2 + set FM_BIT_LOAD_VOL, PIPELINE(iy) +_vol_upd_post_fm2: bit 2, d - jr z, _vol_post_ssg - ld a, (state_mirrored_ssg_c+PROPS_VOL_OFFSET) - ld c, a - ld b, #2 - call ssg_mix_volume -_vol_post_ssg: + jr z, _vol_upd_post_fm3 + ld iy, #state_fm3 + set FM_BIT_LOAD_VOL, PIPELINE(iy) +_vol_upd_post_fm3: + bit 3, d + jr z, _vol_upd_post_fm4 + ld iy, #state_fm4 + set FM_BIT_LOAD_VOL, PIPELINE(iy) +_vol_upd_post_fm4: + + ;; Notify the SSG pipeline + bit 4, d + jr z, _vol_upd_post_ssg_a + ld iy, #state_mirrored_ssg_a + set SSG_BIT_LOAD_VOL, PIPELINE(iy) +_vol_upd_post_ssg_a: + bit 5, d + jr z, _vol_upd_post_ssg_b + ld iy, #state_mirrored_ssg_b + set SSG_BIT_LOAD_VOL, PIPELINE(iy) +_vol_upd_post_ssg_b: + bit 6, d + jr z, _vol_upd_post_ssg_c + ld iy, #state_mirrored_ssg_c + set SSG_BIT_LOAD_VOL, PIPELINE(iy) +_vol_upd_post_ssg_c: + ;; d: ADPCM channels in use ld a, (state_ch_bits+1) @@ -332,6 +327,7 @@ _vol_adpcm_a_next: call ym2610_write_port_a _vol_post_adpcm_b: + pop iy ret @@ -372,8 +368,8 @@ update_volume_state_tracker:: call volume_level_from_ramp ld (state_adpcm_b_volume_attenuation), a - ;; update YM2610 - call volume_update_ym2610 + ;; notify the stream tracker and the YM2610 of the update + call volume_update_stream_state fade:: ;; fade progression @@ -408,7 +404,7 @@ stream_volume_down:: ld (state_volume_music_level), a _post_vol_down: call volume_update_channels_levels - call volume_update_ym2610 + call volume_update_stream_state pop bc ret @@ -425,7 +421,7 @@ stream_volume_up:: ld (state_volume_music_level), a _post_vol_up: call volume_update_channels_levels - call volume_update_ym2610 + call volume_update_stream_state pop bc ret diff --git a/tools/furtool.py b/tools/furtool.py index 7f3a173..1a38e9b 100755 --- a/tools/furtool.py +++ b/tools/furtool.py @@ -25,6 +25,9 @@ from dataclasses import dataclass, field from struct import pack, unpack, unpack_from from adpcmtool import ym2610_adpcma, ym2610_adpcmb +from copy import deepcopy +from functools import reduce +from operator import ior VERBOSE = False @@ -234,6 +237,8 @@ class ssg_macro: prog: list[int] = field(default_factory=list) keys: list[int] = field(default_factory=list) offset: list[int] = field(default_factory=list) + bits: list[int] = field(default_factory=list) + loop: int = 255 autoenv: bool = False @@ -283,6 +288,10 @@ def read_macro_data(length, bs): macros={} max_pos = bs.pos + length header_len = bs.u2() + # TODO: we only support a single loop per macro as all the data are inlined + # into a single sequence. This way, we simplify memory management at the expense + # of a incomplete macro implementation. + macro_loop = 255 while bs.pos < max_pos: header_start = bs.pos # macro code (vol, arp, pitch...) @@ -290,9 +299,14 @@ def read_macro_data(length, bs): if code == 255: break length = bs.u1() - # TODO unsupported. no loop + # loop step. 255 == no loop loop = bs.u1() - # TODO unsupported. last macro value stays + # NOTE: due to how the instruments are edited in the Furnace UI, sometimes + # the loop info stays in the module even if it's no longer in sync with + # the current data length. Double check the flag before keeping it. + if loop != 255 and loop dev213 + # pass: store envelope bit as mode for volume register + if "vol" in blocks and i < len(blocks["vol"]): + new_vol = (env<<4) | (blocks["vol"][i]) + blocks["vol"][i] = new_vol + new_wav=(noise<<3|tone)^0xff + blocks["wave"][i]=new_wav + # pass: put auto-env information aside, it requires muls and divs # and we don't want to do that at runtime on the Z80. Instead - # we will simulate that feature via a specific NSS opcode - if 1 in blocks or 2 in blocks: + # we simulate that feature via a specific NSS opcode + if "num" in blocks or "den" in blocks: # NOTE: only read a single element as we don't allow - # macros on these registers right now - num = blocks.get(1,[1])[0] - den = blocks.get(2,[1])[0] + # sequence on these registers right now + num = blocks.get("num",[1])[0] + den = blocks.get("den",[1])[0] autoenv=(num,den) - blocks.pop(1, None) - blocks.pop(2, None) + blocks.pop("num", None) + blocks.pop("den", None) + + # pass: compute load bits for all the macro steps + # macrolen = max([len(blocks[k]) for k in keys]) + maxlen = max([len(blocks[k]) for k in blocks.keys()]) + bitblocks = {} + # get the load bit for each key at every step + for k in blocks.keys(): + listbit = [code_load_bit[k] for _ in range(len(blocks[k]))] + listbit.extend([0 for _ in range(maxlen-len(blocks[k]))]) + bitblocks[k] = listbit + # add BIT_EVAL_MACRO for every step (set last step only when looping) + bitblocks['_'] = [1<<1]*maxlen + if loop == 255: + bitblocks['_'][-1] = 0 + # merge all the load bits for every step + mergedbits = [reduce(ior,l) for l in zip(*bitblocks.values())] + + # pass: convert Furnace keys to NSS offsets + tmpblocks={} + for k in blocks.keys(): + if k not in code_offset: + warning("macro element not supported yet: %02x"%code) + else: + tmpblocks[code_offset[k]]=blocks[k] + blocks=tmpblocks + # pass: build macro program - # a macro program consists of two separate parts: prog = [] - # the first parts is a sequence that initializes SSG registers - # that should not be updated at every tick (done in ssg_macro) - keys = sorted(filter(lambda x: x in [4,0],blocks.keys())) - iseq, _ = compile_macro_sequence(keys, blocks) - prog.extend(iseq) - # the second parts is a series of sequences that update SSG registers - # at every tick. Right now it only includes volume. - keys = sorted(filter(lambda x: x not in [4,0],blocks.keys())) - nseq, offset = compile_macro_sequence(keys, blocks) - prog.extend(nseq) - # add end of macro marker + realblocks=blocks + blocks=deepcopy(realblocks) + keys = sorted(list(blocks.keys())) + seq, offset = compile_macro_sequence(keys, blocks) + prog = [] + prog.extend(seq) prog.append(255) - issg = ssg_macro(prog=prog, keys=keys, offset=offset, autoenv=autoenv) + issg = ssg_macro(prog=prog, keys=keys, offset=offset, bits=mergedbits, loop=loop, autoenv=autoenv) return issg @@ -410,7 +453,7 @@ def compile_macro_sequence(keys, blocks): def read_instrument(nth, bs, smp): def asm_ident(x): return re.sub(r"\W|^(?=\d)", "_", x).lower() - + assert bs.read(4) == b"INS2" endblock = bs.pos + bs.u4() assert bs.u2() >= 127 # format version @@ -439,8 +482,8 @@ def asm_ident(x): mac = read_ssg_macro(length, bs) elif feat == b"MA" and itype == 38: # other macro types are currently not supported - mac = read_macro_data(length, bs) - elif feat == b"NE": + mac, _ = read_macro_data(length, bs) + elif feat == b"NE": # NES DPCM tag is present when the instrument # uses a PCM sample instead of ADPCM. Skip it assert bs.u1()==0, "sample map unsupported" @@ -467,6 +510,7 @@ def asm_ident(x): if itype == 6: mac.name = asm_ident("macro_%02x_%s"%(nth, name)) mac.load_name = asm_ident("macro_%02x_load_func"%nth) + mac.loop_name = asm_ident("macro_%02x_loop"%nth) return mac else: ins.name = asm_ident("instr_%02x_%s"%(nth, name)) @@ -578,37 +622,44 @@ def asm_ssg_macro(mac, fd): prev = 0 cur = mac.prog.index(255, 0) lines = [] + # split macro into list of steps while cur != prev: line = mac.prog[prev:cur+1] lines.append(", ".join(["0x%02x"%x for x in line])) prev = cur+1 cur = mac.prog.index(255,cur+1) + # there should be a load value for each line + assert len(lines) == len(mac.bits) # macro actions print("%s:" % mac.name, file=fd) - longest = max([len(x) for x in lines]) + longest = max([len(x) for x in lines]) + len(", 0x..") step = 0 print(" ;; macro load function", file=fd) print(" .dw %s" % mac.load_name, file=fd) print(" ;; macro actions", file=fd) - for l in lines: - print(" .db %s ; tick %d"%(l.ljust(longest), step), file=fd) + for l, b in zip(lines, mac.bits): + fmtstep = ("%s, 0x%02x"%(l,b)).ljust(longest) + if step==mac.loop: + print("%s:" % mac.loop_name, file=fd) + print(" .db %s ; tick %d"%(fmtstep, step), file=fd) step += 1 print(" .db %s ; end"%"0xff".ljust(longest), file=fd) + if mac.loop != 255: + print(" .dw %s ; loop"%mac.loop_name.ljust(longest), file=fd) + else: + print(" .dw %s ; no loop"%"0x0000".ljust(longest), file=fd) print("", file=fd) # load func asm_ssg_load_func(mac, fd) - + def asm_ssg_load_func(mac, fd): def asm_ssg(reg): print(" ld b, #0x%02x"%reg, file=fd) print(" ld c, (hl)", file=fd) print(" call ym2610_write_port_a", file=fd) def asm_cha(reg): - print(" ld a, (state_ssg_channel)", file=fd) - print(" ld b, a", file=fd) - print(" ld c, (hl)", file=fd) - print(" call ssg_mix_volume", file=fd) + print(" set 4, (ix)", file=fd) def offset(off): if off==1: print(" inc hl", file=fd) @@ -624,8 +675,21 @@ def offset(off): cha_map = { 3: 0x08 # REG_SSG_A_VOLUME } + + # the load function only take care of generic registers + # filter out the other registers in the macro (waveform, note) + gen_off = [] + gen_keys = [] + prev_off = 0 + for o,k in zip(mac.offset, mac.keys): + if k in [0, 4]: + prev_off += 1 + continue + gen_off.append(o+prev_off) + gen_keys.append(k) + print("%s:" % mac.load_name, file=fd) - data = zip(range(len(mac.offset)), mac.offset, mac.keys) + data = zip(range(len(mac.offset)), gen_off, gen_keys) for i, o, k in data: if i != 0: o+=1 diff --git a/tools/nsstool.py b/tools/nsstool.py index e1e1630..1252dff 100755 --- a/tools/nsstool.py +++ b/tools/nsstool.py @@ -32,6 +32,8 @@ def error(s): sys.exit("error: " + s) +def warn(s): + print("WARNING: %s"%s, file=sys.stderr) def dbg(s): if VERBOSE: @@ -68,6 +70,11 @@ def is_empty(r): return r.note==-1 and r.ins==-1 and r.vol==-1 and all([f==-1 for f,v in r.fx]) +def to_fm_note(furnace_note): + # we count octave from C-0 (furnace starts from C--5) + fm_note = furnace_note - 5*12 + return fm_note + def to_nss_note(furnace_note): octave = (furnace_note // 12) - 5 note = furnace_note % 12 @@ -86,6 +93,11 @@ def make_ssg_note(furnace_note): nss_note = (octave << 4) + note return s_note(nss_note) +def to_ssg_note(furnace_note): + # we count octave from C-0 (furnace starts from C--5) + fm_note = furnace_note - 5*12 + return fm_note + def dbg_row(r, cols): semitones = [ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" ] @@ -98,21 +110,21 @@ def dbg_row(r, cols): semitone=r.note%12 notestr = "%s%s"%(semitones[semitone].ljust(2,"-"), octave) - insstr = "%02x"%r.ins if r.ins != -1 else ".." - volstr = "%02x"%r.ins if r.vol != -1 else ".." + insstr = "%02X"%r.ins if r.ins != -1 else ".." + volstr = "%02X"%r.vol if r.vol != -1 else ".." fxstr="" for f,v in r.fx[:cols]: - sf = "%02x" % f if f!=-1 else ".." - sv = "%02x" % v if v!=-1 else ".." + sf = "%02X" % f if f!=-1 else ".." + sv = "%02X" % v if v!=-1 else ".." fxstr += " %s%s" % (sf, sv) - print("%s %s %s%s"%(notestr,insstr,volstr,fxstr)) + return "%s %s %s%s"%(notestr,insstr,volstr,fxstr) def dbg_pattern(p, m): cols = m.fxcolumns[p.channel] for r in p.rows: - dbg_row(r, cols) + print(dbg_row(r, cols)) @@ -256,11 +268,15 @@ def register_nss_ops(): ("s_slide_d", ["speed_depth"]), # 0x30 ("fm_vibrato", ["speed_depth"]), - ("fm_slide_u", ["speed_depth"]), - ("fm_slide_d", ["speed_depth"]), + ("fm_note_slide_u", ["speed_depth"]), + ("fm_note_slide_d", ["speed_depth"]), ("b_vol" , ["volume"]), ("a_vol" , ["volume"]), ("fm_pan" , ["pan_mask"]), + ("fm_vol_slide_d", ["speed"]), + ("s_vol_slide_d", ["speed"]), + # 0x40 + ("fm_pitch_slide_d", ["speed"]), # reserved opcodes ("nss_label", ["pat"]) ) @@ -328,19 +344,34 @@ def convert_fm_row(row, channel): opcodes.append(fm_pitch((fxval-0x80)//3)) elif fx == 0xe1: # slide up assert fxval != -1 - opcodes.append(fm_slide_u(fxval)) + opcodes.append(fm_note_slide_u(fxval)) elif fx == 0xe2: # slide down assert fxval != -1 - opcodes.append(fm_slide_d(fxval)) + opcodes.append(fm_note_slide_d(fxval)) + elif fx == 0x0a: # volume slide down + # fxval == -1 means disable vibrato + fxval = max(fxval, 0) + opcodes.append(fm_vol_slide_d(fxval)) + elif fx == 0x02: # pitch slide down + # fxval == -1 means disable vibrato + fxval = max(fxval, 0) + opcodes.append(fm_pitch_slide_d(fxval)) # note if row.note != -1: if row.note == 180: opcodes.append(fm_stop()) else: - opcodes.append(fm_note(to_nss_note(row.note))) + opcodes.append(fm_note(to_fm_note(row.note))) return jmp_to_order, opcodes +def s_vol_clamp(row): + # TODO report proper location + newvol = max(0, min(15, row.vol)) + if row.vol != newvol: + rowstr = dbg_row(row, 2) + warn("clamped volume to %02X for SSG row: %s"%(newvol, rowstr)) + return newvol def convert_s_row(row, channel): ctx_t = {4: s_ctx_1, 5: s_ctx_2, 6: s_ctx_3} @@ -354,6 +385,8 @@ def convert_s_row(row, channel): opcodes.append(s_macro(row.ins)) # volume if row.vol != -1: + # bound checks w.r.t SSG limit + row.vol = s_vol_clamp(row) opcodes.append(s_vol(row.vol)) # effects for fx, fxval in row.fx: @@ -377,13 +410,17 @@ def convert_s_row(row, channel): elif fx == 0xe2: # slide down assert fxval != -1 opcodes.append(s_slide_d(fxval)) + elif fx == 0x0a: # volume slide down + # fxval == -1 means disable vibrato + fxval = max(fxval, 0) + opcodes.append(s_vol_slide_d(fxval)) # note if row.note != -1: if row.note == 180: opcodes.append(s_stop()) else: - opcodes.append(make_ssg_note(row.note)) + opcodes.append(s_note(to_ssg_note(row.note))) return jmp_to_order, opcodes @@ -888,8 +925,8 @@ def autoenv_pass(op, out): elif type(op) == s_note: autoenv=s_autoenv[s_ctx] if autoenv: - o=(op.note>>4)&0xf - n=op.note&0xf + o = (op.note // 12) + 1 + n = op.note % 12 notefreq = int(freqs[o-1][n]) num, den = autoenv period = ((125000//notefreq)*den//num)//16