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