From 4ebd2f85afc2b9c115b4f7491b2b11fb278449fa Mon Sep 17 00:00:00 2001 From: Damien Ciabrini Date: Tue, 10 Dec 2024 21:21:16 +0100 Subject: [PATCH] nullsound: big refactor and new pipeline processing (#2) Another round of refactoring to add the FX pipeline processing to ADPCM-A and ADPCM-B. Added support for trigger and slide effects to the ADPCM-B channels, as well as portamento and pitch/detune. nsstool is updated to support all those new opcodes. --- nullsound/Makefile.in | 2 +- nullsound/buffers.s | 23 +- nullsound/fx-slide.s | 407 +++++++++++++++++------ nullsound/{nss-adpcm.s => nss-adpcm-a.s} | 304 ----------------- nullsound/nss-fm.s | 241 ++++++++------ nullsound/nss-ssg.s | 141 +++++++- nullsound/stream.s | 13 +- nullsound/struct-fx.inc | 4 +- nullsound/volume.s | 7 +- tools/nsstool.py | 158 +++++---- 10 files changed, 694 insertions(+), 606 deletions(-) rename nullsound/{nss-adpcm.s => nss-adpcm-a.s} (66%) diff --git a/nullsound/Makefile.in b/nullsound/Makefile.in index 13e9ca6..555cd62 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 fx-vol-slide fx-trigger volume +OBJS=entrypoint bios-commands adpcm ym2610 stream timer nss-fm nss-adpcm-a nss-ssg nss-adpcm-b fx-vibrato fx-slide fx-vol-slide fx-trigger volume LIB=nullsound.lib VERSION=@version@ diff --git a/nullsound/buffers.s b/nullsound/buffers.s index 1927f96..84dd2f5 100644 --- a/nullsound/buffers.s +++ b/nullsound/buffers.s @@ -45,21 +45,20 @@ ssg_tune:: .dw 0x0077, 0x0070, 0x006a, 0x0064, 0x005e, 0x0059, 0x0054, 0x004f, 0x004b, 0x0047, 0x0043, 0x003f, 0, 0, 0, 0 .dw 0x003b, 0x0038, 0x0035, 0x0032, 0x002f, 0x002c, 0x002a, 0x0027, 0x0025, 0x0023, 0x0021, 0x001f, 0, 0, 0, 0 -;;; Vibrato - Semitone distance table +;;; SSG frequency half-distance table ;;; ------ -;;; each element in the table holds the distance to the previous semitone -;;; in the note table. The vibrato effect oscillate between one semi-tone -;;; up and down of the current note of the SSG channel. +;;; The half-distance between a semitone's SSG frequency and the next semitone's SSG frequency. +;;; This is used to compute fractional SSG frequency values from fixed-point note. ssg_semitone_distance:: ;; C-n, C#n, D-n, D#n, E-n, F-n, F#n, G-n, G#n, A-n, A#n, B-n, C-(n+1) - .db 0xe3, 0xd7, 0xca, 0xbf, 0xb5, 0xaa, 0xa1, 0x97, 0x8f, 0x88, 0x7f, 0x79, 0x71, 0, 0, 0 - .db 0xe3, 0xd7, 0xca, 0xbf, 0xb5, 0xaa, 0xa1, 0x97, 0x8f, 0x88, 0x7f, 0x79, 0x71, 0, 0, 0 - .db 0x71, 0x6c, 0x65, 0x60, 0x5a, 0x55, 0x50, 0x4c, 0x48, 0x43, 0x40, 0x3c, 0x39, 0, 0, 0 - .db 0x39, 0x36, 0x32, 0x30, 0x2d, 0x2b, 0x28, 0x26, 0x24, 0x21, 0x20, 0x1e, 0x1d, 0, 0, 0 - .db 0x1d, 0x1b, 0x19, 0x18, 0x16, 0x16, 0x14, 0x13, 0x12, 0x10, 0x10, 0x0f, 0x0f, 0, 0, 0 - .db 0x0f, 0x0d, 0x0d, 0x0c, 0x0b, 0x0b, 0x0a, 0x09, 0x09, 0x08, 0x08, 0x08, 0x07, 0, 0, 0 - .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 + .db 0x6b, 0x65, 0x5f, 0x5a, 0x55, 0x50, 0x4b, 0x47, 0x44, 0x3f, 0x3c, 0x38, 0, 0, 0, 0 + .db 0x6b, 0x65, 0x5f, 0x5a, 0x55, 0x50, 0x4b, 0x47, 0x44, 0x3f, 0x3c, 0x38, 0, 0, 0, 0 + .db 0x36, 0x32, 0x30, 0x2d, 0x2a, 0x28, 0x26, 0x24, 0x21, 0x20, 0x1e, 0x1c, 0, 0, 0, 0 + .db 0x1b, 0x19, 0x18, 0x16, 0x15, 0x14, 0x13, 0x12, 0x10, 0x10, 0x0f, 0x0e, 0, 0, 0, 0 + .db 0x0d, 0x0c, 0x0c, 0x0b, 0x0b, 0x0a, 0x09, 0x09, 0x08, 0x08, 0x07, 0x07, 0, 0, 0, 0 + .db 0x06, 0x06, 0x06, 0x05, 0x05, 0x05, 0x04, 0x04, 0x04, 0x04, 0x04, 0x03, 0, 0, 0, 0 + .db 0x03, 0x03, 0x03, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0, 0, 0, 0 + .db 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0, 0, 0, 0 ;;; Convert note flat representation to representation ;;; ------ diff --git a/nullsound/fx-slide.s b/nullsound/fx-slide.s index 7e6d2dd..c68621a 100644 --- a/nullsound/fx-slide.s +++ b/nullsound/fx-slide.s @@ -27,139 +27,128 @@ .area CODE -;;; Enable slide effect for the current channel +;;; Initialize slide effect for the current channel ;;; ------ ;;; ix : state for channel ;;; a : slide direction: 0 == up, 1 == down -;;; [ hl ]: speed (4bits) and depth (4bits) -slide_init:: - push bc - push de - +;;; c : increment size (increment = 1/2^c) +;;; h : speed (increments) +;;; l : depth (target semitone) +;;; bc, de modified +slide_init_common:: ;; b: slide direction (from a) ld b, a - ;; enable slide FX + ;; if the slide is already running, keep its internal + ;; state, otherwise initialize it. + bit BIT_FX_SLIDE, FX(ix) + jr nz, _post_enable_slide + xor a + ld SLIDE_POS16(ix), a + ld SLIDE_POS16+1(ix), a set BIT_FX_SLIDE, FX(ix) - ;; a: speed - ld a, (hl) - rra - rra - rra - rra - and #0xf +_post_enable_slide: - ;; de: inc16 = speed / 8 - ld d, a + ;; de: inc16 = speed / 2^c + ld d, h ld e, #0 +__slide_divide: srl d rr e - srl d - rr e - srl d - rr e + dec c + jr nz, __slide_divide + ;; down: negate inc16 bit 0, b - jr z, _post_inc16_negate + jr z, __post_inc16_negate ld a, #0 sub e ld e, a ld a, #0 sbc d ld d, a -_post_inc16_negate: +__post_inc16_negate: ld SLIDE_INC16(ix), e ld SLIDE_INC16+1(ix), d ;; depth - ld a, (hl) - and #0xf + ld a, l ;; 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_negate + jr z, __post_depth_negate neg dec a -_post_depth_negate: +__post_depth_negate: ld SLIDE_DEPTH(ix), a - 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 + ;; save target is the current position + new displacement ld a, SLIDE_DEPTH(ix) + add SLIDE_POS16+1(ix) ld SLIDE_END(ix), a - pop de - pop bc - ret -;;; Enable slide effect for the current channel +;;; Initialize slide increment for the current channel ;;; ------ ;;; ix : state for channel ;;; a : slide direction: 0 == up, 1 == down -;;; [ hl ]: speed (4bits) and depth (4bits) -slide_pitch_init:: - push bc - push de - +;;; c : increment size (increment = 1/2^c) +;;; d : speed (increments) +;;; bc, de modified +slide_init_setup_increment:: ;; b: slide direction (from a) ld b, a - ;; enable slide FX - set BIT_FX_SLIDE, FX(ix) - - ;; a: speed - ld a, (hl) - - ;; de: inc16 = speed / 32 - ld d, a + ;; de: inc16 = speed / 2^c ld e, #0 +_slide_divide: srl d rr e - srl d - rr e - srl d - rr e - srl d - rr e - srl d - rr e + dec c + jr nz, _slide_divide + ;; down: negate inc16 bit 0, b - jr z, _post_inc16_negate2 + jr z, _post_inc16_negate ld a, #0 sub e ld e, a ld a, #0 sbc d ld d, a -_post_inc16_negate2: +_post_inc16_negate: ld SLIDE_INC16(ix), e ld SLIDE_INC16+1(ix), d + ret + + +;;; Initialize slide depth target for the current channel +;;; ------ +;;; ix : state for channel +;;; a : slide direction: 0 == up, 1 == down +;;; c : depth (target semitone) +;;; bc modified +slide_init_depth_target:: + ;; b: slide direction (from a) + ld b, a + ;; depth - ld a, #127 + ld a, c ;; 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 + jr z, _post_depth_negate neg dec a -_post_depth_negate2: +_post_depth_negate: ld SLIDE_DEPTH(ix), a - inc hl - ;; init semitone position, fixed point representation ld a, #0 ld SLIDE_POS16(ix), a @@ -169,9 +158,6 @@ _post_depth_negate2: ld a, SLIDE_DEPTH(ix) ld SLIDE_END(ix), a - pop de - pop bc - ret @@ -179,39 +165,56 @@ _post_depth_negate2: ;;; based on the current fixed point semitone position. ;;; ---- ;;; IN: -;;; b: distance to next semitone +;;; b: fractional note position (distance to next semitone) ;;; c: result frequency increment sign (0: positive, 1: negative) -;;; e: intermediate distance to next semitone (fractional part) +;;; e: half-distance to next ym2610 note frequency ;;; OUT: -;;; de: intermediate frequency +;;; hl: intermediate frequency +;;; bc, de, hl modified slide_intermediate_freq: - ;; The minimal slide change is 1/8 of a semitone each tick. - ;; This distance `s_dist` between two semitones is encoded by - ;; bits 7,6,5 of POS16, where: - ;; s_dist = [0.0, 1.0[ - ;; For the sake of speed and simplicity, only bits 7,6 + ;; The distance between two semitones is the fraction part + ;; encoded by bits 7,6,5,... of POS16, where: + ;; distance = [0.0, 1.0[ + ;; For the sake of speed and simplicity, only bits 7,6,5 ;; are considered for computing the intermediate frequency - ;; distance `f_dist` between the current and the next semitone. - ;; so there are only 4 possible frequency distances: - ;; f_dist = {0.0, 1/4, 2/4, 3/4} * distance + ;; between the current and the next ym2610 frequency value, ;; which can be encoded as: - ;; f_dist = bit7 * 1/2 * distance + bit6 * 1/4 * distance + ;; frequency = bit7 * 1/2 * distance + + ;; bit6 * 1/4 * distance + + ;; bit5 * 1/8 * distance + + ;; ... + ;; or with out precalc distances: + ;; frequency = bit7 * 1 * half_distance + + ;; bit6 * 1/2 * half_distance + + ;; bit5 * 1/4 * half_distance + + ;; ... ;; ;; compute frequency distance w.r.t semitone distance - ld a, #0 + ld hl, #0 + ld d, h _chk_bit7: - ;; bc: scaled semitone distance - srl b - bit 7, e + bit 7, b jr z, _chk_bit6 ;; freq distance -= 1/2*(semitone distance) - add b + add hl, de _chk_bit6: - srl b - bit 6, e - jr z, _post_chk + srl e + bit 6, b + jr z, _chk_bit5 ;; freq distance -= 1/4*(semitone distance) - add b + add hl, de +_chk_bit5: + srl e + bit 5, b + jr z, _chk_bit4 + ;; freq distance -= 1/8*(semitone distance) + add hl, de +_chk_bit4: + srl e + bit 4, b + jr z, _post_chk + ;; freq distance -= 1/16*(semitone distance) + add hl, de _post_chk: ;; check whether increment must be negative or positive: @@ -221,13 +224,12 @@ _post_chk: ;; higher than the current semitone's, so `f_dist` must be positive. bit 0, c jr z, _post_sign_chk - neg + push hl + pop de + ld hl, #0 + or a + sbc hl, de _post_sign_chk: - ;; de: extend the 8bit signed distance `f_dist` to 16bit - ld e, a - add a - sbc a - ld d, a ret @@ -237,11 +239,11 @@ _post_sign_chk: ;;; ------ ;;; IN: ;;; ix : state for channel -;;; c : slide direction: 0 == up, 1 == down +;;; hl : offset of current note for channel ;;; OUT: ;;; a : whether effect is finished (0: finished, 1: still running) ;;; d : when effect is finished, target displacement -;;; de modified +;;; bc, de modified eval_slide_step: ;; c: 0 slide up, 1 slide down ld a, SLIDE_INC16+1(ix) @@ -249,9 +251,6 @@ eval_slide_step: and #1 ld c, a - ;; INC16 increment is 1/8 semitone (0x0020) * depth - ;; negative for slide down - ;; add/sub increment to the current semitone displacement POS16 ;; e: fractional part ld a, SLIDE_INC16(ix) @@ -276,10 +275,14 @@ _slide_cp_up: ld d, SLIDE_END(ix) _slide_cp: cp d - jr c, _slide_intermediate + jp m, _slide_intermediate - ;; slide is finished, stop effect + ;; slide is finished, stop effect and clear FX state res BIT_FX_SLIDE, FX(ix) + xor a + ld SLIDE_PORTAMENTO(ix), a + ld SLIDE_POS16(ix), a + ld SLIDE_POS16+1(ix), a ;; d: clamp the last slide pos to the target displacement ld d, SLIDE_END(ix) @@ -298,3 +301,201 @@ _slide_intermediate: ;; effect is still running ld a, #1 ret + + +;;; Check whether the slide NSS opcode should disable the current slide FX +;;; When disabling the FX, update the current NOTE position with the last +;;; slide displacement. +;;; ------ +;;; ix : state for channel +;;; c : offset from ix of current note for channel +;;; [ hl ]: 0 means disable FX, otherwise bail out +slide_check_disable_fx: + ld a, (hl) + cp #0 + jr z, _slide_check_disable + ;; set carry flag + scf + ret +_slide_check_disable: + inc hl + push hl + ;; hl: offset of note from current channel context + push ix + pop hl + ld b, #0 + add hl, bc + ;; update current note with slide displacement + ld a, (hl) + add SLIDE_POS16+1(ix) + ld (hl), a + pop hl + ;; stop FX + res BIT_FX_SLIDE, FX(ix) + ;; clear carry flag + or a + ret + + +;;; Enable slide effect for the current channel +;;; ------ +;;; ix : state for channel +;;; b : slide direction: 0 == up, 1 == down +;;; c : offset from ix of current note for channel +;;; [ hl ]: speed (4bits) and depth (4bits) +slide_init:: + call slide_check_disable_fx + ;; null input means 'disable FX', in that case, + ;; update current note with slide displacement and exit + jr c, _slide_init_setup + ret +_slide_init_setup: + ;; a: slide direction + ld a, b + + push de + + ;; d: slide direction + ld d, a + + ;; c: depth + ld a, (hl) + and #0xf + ld c, a + + ;; b: speed + ld a, (hl) + rra + rra + rra + rra + and #0xf + ld b, a + + inc hl + push hl + + ;; setup the slide + ;; h: speed + ld h, b + ;; l: depth + ld l, c + ;; c: increment size + ld c, #3 + ;; a: direction + ld a, d + call slide_init_common + + pop hl + pop de + + ret + + +;;; Enable pitch slide effect for the current channel +;;; ------ +;;; ix : state for channel +;;; b : slide direction: 0 == up, 1 == down +;;; c : offset from ix of current note for channel +;;; [ hl ]: speed +slide_pitch_init:: + call slide_check_disable_fx + ;; null input means 'disable FX', in that case, + ;; update current note with slide displacement and exit + jr c, _slide_pitch_init_setup + ret +_slide_pitch_init_setup: + ;; a: slide direction + ld a, b + + push de + + ;; d: slide direction + ld d, a + + ;; b: speed + ld b, (hl) + inc hl + push hl + + ;; setup the slide + ;; h: speed + ld h, b + ;; l: depth + ld l, #127 + ;; c: increment size + ld c, #5 + ;; a: direction + ld a, d + call slide_init_common + + pop hl + pop de + + ret + + +;;; Finish initializing the slide effect for the target note +;;; ------ +;;; ix : state for channel, speed increment already initialized +;;; b : current note for channel +;;; bc modified +slide_portamento_finish_init:: + ;; setup the slide + + ;; a: distance from current slide position + ld a, SLIDE_PORTAMENTO(ix) + sub b + sub SLIDE_POS16+1(ix) + jr nc, _slide_post_distance + neg +_slide_post_distance: + + push de + push hl + + ;; l: depth (distance from current slide) + ld l, a + ;; a: slide direction from current slide position + ld a, #0 + rl a + ;; h: speed + ld h, SLIDE_SPEED(ix) + ;; c: increment size + ld c, #5 + call slide_init_common + + pop hl + pop de + + ret + + +;;; Enable portamento effect for the current channel +;;; ------ +;;; ix : state for channel +;;; a : current note for channel +;;; [ hl ]: speed +slide_portamento_init:: + + ;; mark this slide as being a portamento by setting a non-null + ;; portamento target. This is not the real target yet... + set BIT_FX_SLIDE, FX(ix) + ld a, #-1 + ld SLIDE_PORTAMENTO(ix), a + + ;; ... the portamento increments can only be fully initialized when + ;; the real target note is known. This is only the case when we reach + ;; the next note NSS opcode. + ;; So mark the slide FX as `to be initialized` + ;; (end == to be initialized) + xor a + ld SLIDE_END(ix), a + + ;; a: speed + ld a, (hl) + ld SLIDE_SPEED(ix), a + inc hl + + ld a, #1 + ret diff --git a/nullsound/nss-adpcm.s b/nullsound/nss-adpcm-a.s similarity index 66% rename from nullsound/nss-adpcm.s rename to nullsound/nss-adpcm-a.s index f729a0a..ad66cdb 100644 --- a/nullsound/nss-adpcm.s +++ b/nullsound/nss-adpcm-a.s @@ -34,9 +34,6 @@ .equ NSS_ADPCM_A_INSTRUMENT_PROPS, 4 .equ NSS_ADPCM_A_NEXT_REGISTER, 8 - .equ NSS_ADPCM_B_INSTRUMENT_PROPS, 4 - .equ NSS_ADPCM_B_NEXT_REGISTER, 8 - ;; pipeline state for ADPCM-A channel .lclequ STATE_PLAYING, 0x01 @@ -93,17 +90,9 @@ state_a6_end: state_adpcm_a_channel:: .db 0 -;;; current ADPCM-B instrumment play command (with loop) -state_adpcm_b_start_cmd:: - .db 0 - -;;; current volumes for ADPCM-B channel -state_adpcm_b_vol:: .blkb 1 ;;; Global volume attenuation for all ADPCM-A channels state_adpcm_a_volume_attenuation:: .blkb 1 -;;; Global volume attenuation for ADPCM-B channel -state_adpcm_b_volume_attenuation:: .blkb 1 _state_adpcm_end: @@ -132,8 +121,6 @@ init_nss_adpcm_state_tracker:: ;; init flags ld a, #0 ld (state_adpcm_a_channel), a - ld a, #0x80 ; start flag - ld (state_adpcm_b_start_cmd), a ;; set default ld ix, #state_a1 ld d, #6 @@ -142,8 +129,6 @@ _a_init: ld VOL(ix), a dec d jr nz, _a_init - ld a, #0xff - ld (state_adpcm_b_vol), a ;; global ADPCM volumes are initialized in the volume state tracker ret @@ -544,82 +529,6 @@ _adpcm_a_clamp_level: ret -;;; adpcm_b_scale_output -;;; adjust ADPCM-B volume to match configured ADPCM-B output level -;;; output volume = [0..1] * input volume, where the scale factor -;;; is the currently configured ADPCM-B output level [0x00..0xff] -;;; ------ -;;; a: input level [0x00..0x1f] -;;; modified: bc -adpcm_b_scale_output:: - push hl - - ;; bc: note volume fraction 000000fff fffff00 - ld l, a - ld h, #0 - add hl, hl - add hl, hl - ld c, l - ld b, h - - ;; init result - ld hl, #0 - - ;; e: attenuation factor -> volume factor - ld a, (state_adpcm_b_volume_attenuation) - neg - add #64 - ld e, a - -_b_level_bit0: - bit 0, e - jr z, _b_level_bit1 - ;; add this bit's value to the result - add hl, bc -_b_level_bit1: - ;; bc: bc * 2 - sla c - rl b - bit 1, e - jr z, _b_level_bit2 - add hl, bc -_b_level_bit2: - sla c - rl b - bit 2, e - jr z, _b_level_bit3 - add hl, bc -_b_level_bit3: - sla c - rl b - bit 3, e - jr z, _b_level_bit4 - add hl, bc -_b_level_bit4: - sla c - rl b - bit 4, e - jr z, _b_level_bit5 - add hl, bc -_b_level_bit5: - sla c - rl b - bit 5, e - jr z, _b_level_bit6 - add hl, bc -_b_level_bit6: - sla c - rl b - bit 6, e - jr z, _b_level_post - add hl, bc -_b_level_post: - ;; keep the 8 MSB from hl, this is the scaled volume - ld a, h - pop hl - ret - - ;;; Compute the YM2610 output volume from the current channel ;;; ------ ;;; modified: c @@ -666,219 +575,6 @@ _a_vol_end: ret -;;; ADPCM_B_INSTRUMENT -;;; Configure the ADPCM-B channel based on an instrument's data -;;; ------ -;;; [ hl ]: instrument number -adpcm_b_instrument:: - ;; a: instrument - ld a, (hl) - inc hl - - push bc - push hl - push de - - ;; hl: instrument address in ROM - sla a - ld c, a - ld b, #0 - ld hl, (state_stream_instruments) - add hl, bc - ld e, (hl) - inc hl - ld d, (hl) - inc hl - push de - pop hl - - ;; d: all ADPCM-B properties - ld d, #4 - - ;; a: start of ADPCM-B property registers - ld a, #REG_ADPCM_B_ADDR_START_LSB - add b - -_adpcm_b_loop: - ld b, a - ld c, (hl) - call ym2610_write_port_a - add a, #1 - inc hl - dec d - jp nz, _adpcm_b_loop - - ;; play command, with/without loop bit - ld a, #0x80 - bit 0, (hl) - jr z, _adpcm_b_post_loop_chk - set 4, a -_adpcm_b_post_loop_chk: - ld (state_adpcm_b_start_cmd), a - - ;; set a default pan - ld b, #REG_ADPCM_B_PAN - ld c, #0xc0 ; default pan (L+R) - call ym2610_write_port_a - - ;; current volume - ld b, #REG_ADPCM_B_VOLUME - ld a, (state_adpcm_b_vol) - call adpcm_b_scale_output - ld c, a - call ym2610_write_port_a - - pop de - pop hl - pop bc - ld a, #1 - ret - - -;;; Semitone frequency table -;;; ------ -;;; A note in nullsound is represented as a tuple , -;;; which is translated into YM2610's register representation -;;; `Delta-N`. nullsounds decomposes Delta-N as `2^octave * base`, -;;; where base is a factor of the semitone's frequency, and the -;;; result is multiplied by a power of 2 (handy for octaves) -adpcm_b_note_base_delta_n: - .db 0x0c, 0xb7 ; 3255 - C - .db 0x0d, 0x78 ; 3448 - C# - .db 0x0e, 0x45 ; 3653 - D - .db 0x0f, 0x1f ; 3871 - D# - .db 0x10, 0x05 ; 4101 - E - .db 0x10, 0xf9 ; 4345 - F - .db 0x11, 0xfb ; 4603 - F# - .db 0x13, 0x0d ; 4877 - G - .db 0x14, 0x2f ; 5167 - G# - .db 0x15, 0x62 ; 5474 - A - .db 0x16, 0xa8 ; 5800 - A# - .db 0x18, 0x00 ; 6144 - B - - -;;; ADPCM_B_NOTE_ON -;;; Emit a specific note (sample frequency) on the ADPCM-B channel -;;; ------ -;;; [ hl ]: note (0xAB: A=octave B=semitone) -adpcm_b_note_on:: - push bc - push de - - ;; d: note (0xAB: A=octave B=semitone) - ld d, (hl) - inc hl - - push hl - - ;; stop the ADPCM-B channel - ld b, #REG_ADPCM_B_START_STOP - ld c, #1 ; reset flag (clears start and repeat in YM2610) - call ym2610_write_port_a - - ;; a: semitone - ld a, d - and #0xf - - ;; d: octave - srl d - srl d - srl d - srl d - - ;; lh: semitone -> delta_n address - ld hl, #adpcm_b_note_base_delta_n - sla a - ld b, #0 - ld c, a - add hl, bc - - ;; bc: base delta_n - ld b, (hl) - inc hl - ld c, (hl) - inc hl - - ;; hl: delta_n (base << octave) - ;; d: octave - push bc - pop hl - - ld a, d - cp #0 - jp z, _no_delta_shift -_delta_shift: - add hl, hl - dec d - jp nz, _delta_shift -_no_delta_shift: - - ;; de: delta_n - push hl - pop de - - ;; configure delta_n into the YM2610 - ld b, #REG_ADPCM_B_DELTA_N_LSB - ld c, e - call ym2610_write_port_a - ld b, #REG_ADPCM_B_DELTA_N_MSB - ld c, d - call ym2610_write_port_a - - ;; start the ADPCM-B channel - ld b, #REG_ADPCM_B_START_STOP - ;; start command (with loop when configured) - ld a, (state_adpcm_b_start_cmd) - ld c, a - call ym2610_write_port_a - - pop hl - pop de - pop bc - ld a, #1 - ret - - -;;; ADPCM_B_NOTE_OFF -;;; Stop sample playback on the ADPCM-B channel -;;; ------ -adpcm_b_note_off:: - push bc - - ;; stop the ADPCM-B channel - ld b, #REG_ADPCM_B_START_STOP - ld c, #1 ; reset flag (clears start and repeat in YM2610) - call ym2610_write_port_a - - pop bc - ld a, #1 - ret - - -;;; ADPCM_B_VOL -;;; Set playback volume of the ADPCM-B channel -;;; ------ -adpcm_b_vol:: - push bc - - ;; a: volume - ld a, (hl) - inc hl - - ;; new configured volume for ADPCM-B - ld (state_adpcm_b_vol), a - call adpcm_b_scale_output - - ;; set volume in the YM2610 - ld b, #REG_ADPCM_B_VOLUME - ld c, a - call ym2610_write_port_a - - pop bc - ld a, #1 - ret - - ;;; ADPCM_A_DELAY ;;; Enable delayed trigger for the next note and volume ;;; (note and volume and played after a number of ticks) diff --git a/nullsound/nss-fm.s b/nullsound/nss-fm.s index 0ec498b..b9c92f4 100644 --- a/nullsound/nss-fm.s +++ b/nullsound/nss-fm.s @@ -45,6 +45,7 @@ ;; getters for FM state .lclequ NOTE,(state_fm_note_semitone-state_fm) .lclequ NOTE_SEMITONE,(state_fm_note_semitone-state_fm) + .lclequ DETUNE,(state_fm_detune-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) @@ -65,12 +66,14 @@ .lclequ STATE_LOAD_REGS, 0x10 .lclequ STATE_LOAD_ALL, 0x1e .lclequ STATE_CONFIG_VOL, 0x20 + .lclequ STATE_NOTE_STARTED, 0x80 .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 + .lclequ BIT_NOTE_STARTED, 7 .area DATA @@ -105,6 +108,7 @@ state_fm_fx_vibrato: .blkb VIBRATO_SIZE state_fm_note: 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_detune: .blkb 2 ; channel's fixed-point semitone detune 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 @@ -132,14 +136,6 @@ 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 pan (and instrument's AMS PMS) per FM channel ;;; TODO move to the state struct state_pan_ams_pms:: @@ -416,6 +412,11 @@ compute_fm_fixed_point_note:: ld l, a ld h, NOTE_SEMITONE(ix) + ;; hl: detuned semitone + ld c, DETUNE(ix) + ld b, DETUNE+1(ix) + add hl, bc + ;; bc: slide offset if the slide FX is enabled bit BIT_FX_SLIDE, FX(ix) jr z, _fm_post_add_slide @@ -551,21 +552,24 @@ compute_ym2610_fm_note:: ld l, c push hl ; + f-num - ;; b: next semitone distance from current note - ;; TODO 8bit add or aligned add + ;; e: half-distance f-num to next semitone (8bit add) ld a, d and #0xf - ld hl, #fm_semitone_distance + ld hl, #fm_f_num_half_distance add l - inc a ld l, a - ld b, (hl) + adc a, h + sub l + ld h, a + ld e, (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 + ;; b: intermediate note position (fractional part) + ld b, NOTE_POS16(ix) + ;; de: current intermediate f-num call slide_intermediate_freq + push hl + pop de pop hl ; - f-num add hl, de @@ -616,6 +620,7 @@ _fm_post_fx_trigger: _fm_post_fx_vibrato: bit BIT_FX_SLIDE, FX(ix) jr z, _fm_post_fx_slide + ld hl, #NOTE_SEMITONE call eval_fm_slide_step set BIT_LOAD_NOTE, PIPELINE(ix) _fm_post_fx_slide: @@ -678,12 +683,15 @@ _post_load_fm_vol: ld c, NOTE_BLOCK(ix) call fm_set_fnum_registers - ;; start current FM channel (enable all OPs) + ;; start current FM channel (enable all OPs) if not already done + bit BIT_NOTE_STARTED, PIPELINE(ix) + jr nz, _post_load_fm_note ld a, (state_fm_ym2610_channel) or #0xf0 ld c, a ld b, #REG_FM_KEY_ON_OFF_OPS call ym2610_write_port_a + set BIT_NOTE_STARTED, PIPELINE(ix) _post_load_fm_note: _end_fm_channel_pipeline: @@ -822,6 +830,11 @@ _fm_end: set BIT_LOAD_VOL, PIPELINE(ix) + ;; setting a new instrument always trigger a note start, + ;; register it for the next pipeline run + res BIT_NOTE_STARTED, PIPELINE(ix) + set BIT_LOAD_NOTE, PIPELINE(ix) + _fm_instr_end: pop de pop hl @@ -863,26 +876,47 @@ fm_set_pan_ams_pms:: ret -;;; FM_PITCH -;;; Configure note detune for the current FM channel +;;; build the 16bit signed detune displacement ;;; ------ -;;; [ hl ]: detune -fm_pitch:: - push bc - ;; bc: address of detune for current FM channel - ld bc, #state_fm_detune - ld a, (state_fm_channel) - add a, c +;;; IN +;;; [ hl ]: detune +;;; OUT: +;;; bc : 16bits signed displacement +;;; bc, hl modified +common_pitch:: + ;; a: detune [0..255] + ld a, (hl) + inc hl + + ;; bc: detune [-128..127] + sub #0x80 ld c, a - add a, b - sub c + add a, a + sbc a ld b, a - ;; a: detune - ld a, (hl) - inc hl - ld (bc), a + ;; bc: 2*detune, semitone range in ]-1..1[ + push hl + ld hl, #0 + add hl, bc + add hl, bc + ld b, h + ld c, l + pop hl + + ld a, #1 + ret + +;;; FM_PITCH +;;; Detune up to -+1 semitone for the current FM channel +;;; ------ +;;; [ hl ]: detune +fm_pitch:: + push bc + call common_pitch + ld DETUNE(ix), c + ld DETUNE+1(ix), b pop bc ld a, #1 ret @@ -910,60 +944,13 @@ fm_note_f_num: .db 0x04, 0xd1 ; 1233 - C+1 -;;; Vibrato - semitone distance table -;;; ------ -;;; The distance between a semitone's f-num and the previous semitone's f-num. -;;; This is the same for all octaves. -;;; The vibrato effect oscillate between one semi-tone up and down of the -;;; current note of the FM channel. -fm_semitone_distance:: - ;; C , C#, D , D#, E , F , F#, G , G#, A , A#, B , C+1 - .db 0x25, 0x27, 0x29, 0x2b, 0x2f, 0x31, 0x34, 0x37, 0x3a, 0x3e, 0x41, 0x44, 0x4a - - -;;; Get the effective F-num from the current FM channel +;;; F-num half-distance table ;;; ------ -;;; hl : F-Num position in the semitone frequency table -;;; OUT: -;;; hl : detuned F-num based on current detune context -fm_get_f_num: - push bc - ld a, (hl) - inc hl - ld b, a - ld a, (hl) - ld c, a - ;; hl: 10bits f-num - ld h, b - ld l, c - - ;; bc: address of detune for current FM channel - ld bc, #state_fm_detune - ld a, (state_fm_channel) - add a, c - ld c, a - add a, b - sub c - ld b, a - - ;; a: detune - ld a, (bc) - - ;; hl += a (16bits + signed 8bits) - cp #0x80 - jr c, _detune_positive - dec h -_detune_positive: - ; Then do addition as usual - ; (to handle the "lower byte") - add a, l - ld l, a - adc a, h - sub l - ld h, a - - pop bc - ret +;;; The half-distance between a semitone's f-num and the next semitone's f-num. +;;; This is used to compute fractional f-num values from fixed-point note. +fm_f_num_half_distance:: + ;; C, C#, D, D#, E, F, F#, G, G#, A, A#, B, C+1 + .db 0x12, 0x13, 0x14, 0x15, 0x17, 0x18, 0x1a, 0x1b, 0x1d, 0x1f, 0x20, 0x22, 0x25 ;;; Update the vibrato for the current FM channel @@ -1020,21 +1007,39 @@ _end_fm_slide_load_fnum2: ;;; ------ fm_configure_note_on: push bc - - ld NOTE(ix), a - - ;; stop current FM channel (disable all OPs) - ;; CHECK: do it in the pipeline instead? - + push af ; +note + ;; if portamento is ongoing, this is treated as an update + bit BIT_FX_SLIDE, FX(ix) + jr z, _fm_cfg_note_update + ld a, SLIDE_PORTAMENTO(ix) + cp #0 + jr z, _fm_cfg_note_update + ;; update the portamento now + pop af ; -note + ld SLIDE_PORTAMENTO(ix), a + ld b, NOTE_SEMITONE(ix) + call slide_portamento_finish_init + ;; if a note is currently playing, do nothing else, the + ;; portamento will be updated at the next pipeline run... + bit BIT_NOTE_STARTED, PIPELINE(ix) + jr nz, _fm_cfg_note_end + ;; ... else a new instrument was loaded, reload this note as well + jr _fm_cfg_note_prepare_ym2610 +_fm_cfg_note_update: + ;; update the current note and prepare the ym2610 + pop af ; -note + ld NOTE_SEMITONE(ix), a +_fm_cfg_note_prepare_ym2610: + ;; stop playback on the channel, and let the pipeline restart it ld a, (state_fm_ym2610_channel) ld c, a ld b, #REG_FM_KEY_ON_OFF_OPS call ym2610_write_port_a - + res BIT_NOTE_STARTED, PIPELINE(ix) ld a, PIPELINE(ix) or #(STATE_PLAYING|STATE_EVAL_MACRO|STATE_LOAD_NOTE) ld PIPELINE(ix), a - +_fm_cfg_note_end: pop bc ret @@ -1069,7 +1074,6 @@ fm_note_on:: _fm_note_on_immediate: ;; else load note immediately - ld NOTE_SEMITONE(ix), a call fm_configure_note_on _fm_note_on_end: @@ -1095,7 +1099,7 @@ fm_note_off:: call ym2610_write_port_a pop bc - ;; disable playback in the pipeline, any note lod_note bit + ;; disable playback in the pipeline, any load_note bit ;; will get cleaned during the next pipeline run res BIT_PLAYING, PIPELINE(ix) @@ -1104,6 +1108,10 @@ fm_note_off:: inc a call fm_ctx_set_current + ;; record that playback is stopped + xor a + res BIT_NOTE_STARTED, PIPELINE(ix) + ld a, #1 ret @@ -1195,9 +1203,12 @@ _post_fm_vibrato_setup: ;;; ------ ;;; [ hl ]: speed (4bits) and depth (4bits) fm_note_slide_up:: - ld a, #0 + push bc + ld b, #0 + ld c, #NOTE_SEMITONE call slide_init ld a, #1 + pop bc ret @@ -1206,9 +1217,26 @@ fm_note_slide_up:: ;;; ------ ;;; [ hl ]: speed (4bits) and depth (4bits) fm_note_slide_down:: - ld a, #1 + push bc + ld b, #1 + ld c, #NOTE_SEMITONE call slide_init ld a, #1 + pop bc + ret + + +;;; FM_PITCH_SLIDE_UP +;;; Enable slide up effect for the current FM channel +;;; ------ +;;; [ hl ]: speed (8bits) +fm_pitch_slide_up:: + push bc + ld b, #0 + ld c, #NOTE_SEMITONE + call slide_pitch_init + ld a, #1 + pop bc ret @@ -1217,8 +1245,25 @@ fm_note_slide_down:: ;;; ------ ;;; [ hl ]: speed (8bits) fm_pitch_slide_down:: - ld a, #1 + push bc + ld b, #1 + ld c, #NOTE_SEMITONE call slide_pitch_init + ld a, #1 + pop bc + ret + + +;;; FM_PORTAMENTO +;;; Enable slide to the next note to be loaded into the pipeline +;;; ------ +;;; [ hl ]: speed +fm_portamento:: + ;; current note (start of portamento) + ld a, NOTE_POS16+1(ix) + + call slide_portamento_init + ld a, #1 ret diff --git a/nullsound/nss-ssg.s b/nullsound/nss-ssg.s index 9b0bc3d..b3c088f 100644 --- a/nullsound/nss-ssg.s +++ b/nullsound/nss-ssg.s @@ -30,6 +30,7 @@ ;; getters for SSG state .lclequ NOTE,(state_ssg_note-state_mirrored_ssg) + .lclequ DETUNE,(state_ssg_detune-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) @@ -51,6 +52,7 @@ .lclequ STATE_LOAD_VOL, 0x10 .lclequ STATE_LOAD_REGS, 0x20 .lclequ STATE_STOP_NOTE, 0x40 + .lclequ STATE_NOTE_STARTED, 0x80 .lclequ BIT_PLAYING, 0 .lclequ BIT_EVAL_MACRO, 1 .lclequ BIT_LOAD_NOTE, 2 @@ -58,7 +60,7 @@ .lclequ BIT_LOAD_VOL, 4 .lclequ BIT_LOAD_REGS, 5 .lclequ BIT_STOP_NOTE, 6 - + .lclequ BIT_NOTE_STARTED, 7 .area DATA @@ -98,6 +100,7 @@ state_ssg_fx_vibrato: .blkb VIBRATO_SIZE ;;; 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_detune: .blkb 2 ; fixed-point semitone detune state_ssg_note_fine_coarse: .blkb 2 ; YM2610 note factors (fine+coarse) state_mirrored_ssg_props: state_mirrored_ssg_envelope: .blkb 1 ; envelope shape @@ -288,10 +291,11 @@ _ssg_post_fx_trigger: set BIT_LOAD_NOTE, PIPELINE(ix) _ssg_post_fx_vibrato: bit BIT_FX_SLIDE, FX(ix) - jr z, _ssg_post_fx_side + jr z, _ssg_post_fx_slide + ld hl, #NOTE call eval_ssg_slide_step set BIT_LOAD_NOTE, PIPELINE(ix) -_ssg_post_fx_side: +_ssg_post_fx_slide: bit BIT_FX_VOL_SLIDE, FX(ix) jr z, _ssg_post_fx_vol_slide call eval_vol_slide_step @@ -381,12 +385,15 @@ _post_load_ssg_vol: call waveform_for_channel ;; start note + bit BIT_NOTE_STARTED, PIPELINE(ix) + jr nz, _post_load_waveform 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 + set BIT_NOTE_STARTED, PIPELINE(ix) _post_load_waveform: _end_ssg_channel_pipeline: @@ -418,6 +425,11 @@ compute_ssg_fixed_point_note:: ld l, a ld h, NOTE(ix) + ;; hl: detuned semitone + ld c, DETUNE(ix) + ld b, DETUNE+1(ix) + add hl, bc + ;; h: current note + arpeggio shift ld a, ARPEGGIO(ix) add h @@ -466,23 +478,24 @@ compute_ym2610_ssg_note:: ld d, (hl) push de ; +base tune - ;; b: next semitone distance from current note + ;; e: half-distance SSG tune to next semitone ld hl, #ssg_semitone_distance ld l, b - ld b, (hl) - + ld e, (hl) ;; c: SSG: intermediate frequency is negative ld c, #1 - - ;; e: current position (fractional part) - ld e, NOTE_POS16(ix) - - ;; de: current intermediate frequency f_dist + ;; e: intermediate note position (fractional part) + ld b, NOTE_POS16(ix) + ;; de: current intermediate SSG tune call slide_intermediate_freq + push hl + pop de - ;; hl: final ym2610 tune + ;; hl: ym2610 tune (coarse | fine tune) pop hl ; -base tune add hl, de + + ;; save ym2610 fine and coarse tune ld NOTE_FINE_COARSE(ix), l ld NOTE_FINE_COARSE+1(ix), h ret @@ -644,6 +657,11 @@ ssg_macro:: or #STATE_EVAL_MACRO ld PIPELINE(ix), a + ;; setting a new instrument/macro always trigger a note start, + ;; register it for the next pipeline run + res BIT_NOTE_STARTED, PIPELINE(ix) + set BIT_LOAD_NOTE, PIPELINE(ix) + pop hl pop de @@ -734,6 +752,10 @@ ssg_note_off:: inc a call fm_ctx_set_current + ;; record that playback is stopped + xor a + res BIT_NOTE_STARTED, PIPELINE(ix) + ld a, #1 ret @@ -765,11 +787,34 @@ _ssg_vol_end: ld a, #1 ret + ;;; Configure state for new note and trigger a load in the pipeline ;;; ------ ssg_configure_note_on: + push bc + push af ; +note + ;; if portamento is ongoing, this is treated as an update + bit BIT_FX_SLIDE, FX(ix) + jr z, _ssg_cfg_note_update + ld a, SLIDE_PORTAMENTO(ix) + cp #0 + jr z, _ssg_cfg_note_update + ;; update the portamento now + pop af ; -note + ld SLIDE_PORTAMENTO(ix), a + ld b, NOTE(ix) + call slide_portamento_finish_init + ;; if a note is currently playing, do nothing else, the + ;; portamento will be updated at the next pipeline run... + bit BIT_NOTE_STARTED, PIPELINE(ix) + jr nz, _ssg_cfg_note_end + ;; ... else a new instrument was loaded, reload this note as well + jr _ssg_cfg_note_prepare_ym2610 +_ssg_cfg_note_update: + ;; update the current note and prepare the ym2610 + pop af ; -note ld NOTE(ix), a - +_ssg_cfg_note_prepare_ym2610: ;; init macro position ld a, MACRO_DATA(ix) ld MACRO_POS(ix), a @@ -777,10 +822,14 @@ ssg_configure_note_on: ld MACRO_POS+1(ix), a ;; reload all registers at the next pipeline run + res BIT_NOTE_STARTED, PIPELINE(ix) ld a, PIPELINE(ix) or #(STATE_PLAYING|STATE_EVAL_MACRO|STATE_LOAD_NOTE) ld PIPELINE(ix), a +_ssg_cfg_note_end: + pop bc + ret @@ -886,9 +935,12 @@ _post_ssg_setup: ;;; ------ ;;; [ hl ]: speed (4bits) and depth (4bits) ssg_slide_up:: - ld a, #0 + push bc + ld b, #0 + ld c, #NOTE call slide_init ld a, #1 + pop bc ret @@ -897,8 +949,53 @@ ssg_slide_up:: ;;; ------ ;;; [ hl ]: speed (4bits) and depth (4bits) ssg_slide_down:: - ld a, #1 + push bc + ld b, #1 + ld c, #NOTE call slide_init + ld a, #1 + pop bc + ret + + +;;; SSG_PITCH_SLIDE_UP +;;; Enable slide up effect for the current SSG channel +;;; ------ +;;; [ hl ]: speed (8bits) +ssg_pitch_slide_up:: + push bc + ld b, #0 + ld c, #NOTE + call slide_pitch_init + ld a, #1 + pop bc + ret + + +;;; SSG_PITCH_SLIDE_DOWN +;;; Enable slide up effect for the current SSG channel +;;; ------ +;;; [ hl ]: speed (8bits) +ssg_pitch_slide_down:: + push bc + ld b, #1 + ld c, #NOTE + call slide_pitch_init + ld a, #1 + pop bc + ret + + +;;; SSG_PORTAMENTO +;;; Enable slide to the next note to be loaded into the pipeline +;;; ------ +;;; [ hl ]: speed +ssg_portamento:: + ;; current note (start of portamento) + ld a, NOTE_POS16+1(ix) + + call slide_portamento_init + ld a, #1 ret @@ -933,3 +1030,17 @@ ssg_delay:: ld a, #1 ret + + +;;; SSG_PITCH +;;; Detune up to -+1 semitone for the current channel +;;; ------ +;;; [ hl ]: detune +ssg_pitch:: + push bc + call common_pitch + ld DETUNE(ix), c + ld DETUNE+1(ix), b + pop bc + ld a, #1 + ret diff --git a/nullsound/stream.s b/nullsound/stream.s index f361f01..329db51 100644 --- a/nullsound/stream.s +++ b/nullsound/stream.s @@ -239,6 +239,7 @@ _check_update_stream_pipeline: call run_fm_pipeline call run_ssg_pipeline call run_adpcm_a_pipeline + call run_adpcm_b_pipeline res TIMER_CONSUMER_STREAM_BIT, a ld (state_timer_tick_reached), a _end_update_stream: @@ -255,6 +256,7 @@ stream_reset_state:: call init_nss_fm_state_tracker call init_nss_ssg_state_tracker call init_nss_adpcm_state_tracker + call init_nss_adpcm_b_state_tracker ld a, #1 ld (state_stream_in_use), a @@ -356,7 +358,7 @@ stream_all_ctx_switch:: .db op_id_ssg_ctx_1, op_id_ssg_ctx_2, op_id_ssg_ctx_3, op_id_nss_nop .db op_id_adpcm_a_ctx_1, op_id_adpcm_a_ctx_2, op_id_adpcm_a_ctx_3 .db op_id_adpcm_a_ctx_4, op_id_adpcm_a_ctx_5, op_id_adpcm_a_ctx_6 - .db op_id_nss_nop + .db op_id_adpcm_b_ctx ;;; Play music or sfx from a pre-compiled list of NSS opcodes, @@ -509,6 +511,15 @@ nss_opcodes: .nss_op ssg_delay .nss_op fm_delay .nss_op adpcm_a_delay + .nss_op adpcm_b_ctx + .nss_op fm_portamento + .nss_op fm_pitch_slide_up + .nss_op ssg_pitch + .nss_op b_pitch_slide_up + .nss_op ssg_pitch_slide_up + .nss_op adpcm_b_portamento + .nss_op ssg_pitch_slide_down + .nss_op ssg_portamento diff --git a/nullsound/struct-fx.inc b/nullsound/struct-fx.inc index fdd6b53..fe02920 100644 --- a/nullsound/struct-fx.inc +++ b/nullsound/struct-fx.inc @@ -29,7 +29,7 @@ .local pipeline, fx_fx, fx_trigger, fx_vol_slide, fx_slide, fx_vibrato .local _trigger_action, _trigger_note, _trigger_vol, _trigger_cut, _trigger_delay, _trigger_size .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 _slide_speed, _slide_depth, _slide_inc16, _slide_pos16, _slide_portamento, _slide_end, _slide_size .local _vibrato_speed, _vibrato_depth, _vibrato_pos, _vibrato_pos16, _vibrato_size ;;; actions to run at every tick. This must be the first field of a channel's state @@ -56,6 +56,7 @@ _slide_depth: .blkb 1 ; distance in semitones _slide_inc16: .blkw 1 ; 1/8 semitone increment * speed _slide_pos16: .blkw 1 ; slide pos _slide_end: .blkb 1 ; end note (octave/semitone) +_slide_portamento: .blkb 1 ; portamento to target note _slide_size: ;;; FX: vibrato fx_vibrato: @@ -87,6 +88,7 @@ _vibrato_size: .lclequ SLIDE_DEPTH, (_slide_depth - pipeline) .lclequ SLIDE_INC16, (_slide_inc16 - pipeline) .lclequ SLIDE_POS16, (_slide_pos16 - pipeline) + .lclequ SLIDE_PORTAMENTO, (_slide_portamento - pipeline) .lclequ SLIDE_END, (_slide_end - pipeline) .lclequ SLIDE_SIZE, (_slide_size - fx_slide) .lclequ VIBRATO, (fx_vibrato - pipeline) diff --git a/nullsound/volume.s b/nullsound/volume.s index 602c13a..2d192fc 100644 --- a/nullsound/volume.s +++ b/nullsound/volume.s @@ -30,6 +30,7 @@ .lclequ FM_BIT_LOAD_VOL, 3 .lclequ SSG_BIT_LOAD_VOL, 4 .lclequ ADPCM_A_BIT_LOAD_VOL, 3 + .lclequ ADPCM_B_BIT_LOAD_VOL, 3 ;;; @@ -318,11 +319,7 @@ _vol_adpcm_a_next: ;; update ADPCM-B if it is used in the music bit 0, d jr z, _vol_post_adpcm_b - ld a, (state_adpcm_b_vol) - call adpcm_b_scale_output - ld c, a - ld b, #REG_ADPCM_B_VOLUME - call ym2610_write_port_a + set ADPCM_B_BIT_LOAD_VOL, PIPELINE(iy) _vol_post_adpcm_b: pop iy diff --git a/tools/nsstool.py b/tools/nsstool.py index fa8cc1d..124b7dd 100755 --- a/tools/nsstool.py +++ b/tools/nsstool.py @@ -70,40 +70,17 @@ 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 - nss_note = (octave << 4) + note - return nss_note - -def to_nss_b_note(furnace_note): - octave = (furnace_note // 12) - 5 - note = furnace_note % 12 - nss_note = (octave << 4) + note - return nss_note - -def make_ssg_note(furnace_note): - octave = (furnace_note // 12) - 5 - note = furnace_note % 12 - 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 + nss_note = furnace_note - 5*12 + return nss_note # # Debugging functions # -def dbg_row(r, cols): +def row_str(r, cols): semitones = [ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" ] if r.note == -1: notestr = "..." @@ -124,13 +101,18 @@ def dbg_row(r, cols): fxstr += " %s%s" % (sf, sv) return "%s %s %s%s"%(notestr,insstr,volstr,fxstr) - + +dbg_order = 0 +dbg_row = 0 +dbg_channel = 0 +dbg_fxs = 0 + def dbg_pattern(p, m): cols = m.fxcolumns[p.channel] for r in p.rows: - print(dbg_row(r, cols)) + print(row_str(r, cols)) + - unknown_fx = {} def add_unknown_fx(channel, fx): global unknown_fx @@ -140,7 +122,7 @@ def add_unknown_fx(channel, fx): # # Furnace parsers -# +# def read_pattern(m, bs): assert bs.read(4) == b"PATN" @@ -289,7 +271,17 @@ def register_nss_ops(): ("fm_pitch_slide_d", ["speed"]), ("s_delay" , ["delay"]), ("fm_delay", ["delay"]), - ("a_delay", ["delay"]), + ("a_delay" , ["delay"]), + ("b_ctx" , ), + ("fm_porta", ["speed"]), + ("fm_pitch_slide_u", ["speed"]), + ("s_pitch" , ["pitch"]), + # 0x50 + ("b_pitch_slide_u", ["speed"]), + ("s_pitch_slide_u", ["speed"]), + ("b_porta", ["speed"]), + ("s_pitch_slide_d", ["speed"]), + ("s_porta", ["speed"]), # reserved opcodes ("nss_label", ["pat"]) ) @@ -358,7 +350,7 @@ def convert_fm_row(row, channel): elif fx == 0x15: # OP4 level opcodes.append(op4_lvl(fxval)) elif fx == 0xe5: # pitch - opcodes.append(fm_pitch((fxval-0x80)//3)) + opcodes.append(fm_pitch(fxval)) elif fx == 0xe1: # slide up assert fxval != -1 opcodes.append(fm_note_slide_u(fxval)) @@ -373,6 +365,12 @@ def convert_fm_row(row, channel): # fxval == -1 means disable vibrato fxval = max(fxval, 0) opcodes.append(fm_pitch_slide_d(fxval)) + elif fx == 0x01: # pitch slide up + # fxval == -1 means disable vibrato + fxval = max(fxval, 0) + opcodes.append(fm_pitch_slide_u(fxval)) + elif fx == 0x03: # portamento + opcodes.append(fm_porta(fxval)) else: add_unknown_fx('FM', fx) @@ -381,15 +379,21 @@ def convert_fm_row(row, channel): if row.note == 180: opcodes.append(fm_stop()) else: - opcodes.append(fm_note(to_fm_note(row.note))) + opcodes.append(fm_note(to_nss_note(row.note))) return jmp_to_order, opcodes +def row_warn(row, msg): + ch_str = ['F1','F2','F3','F4','S1','S2','S3','A1','A2','A3','A4','A5','A6','B'] + loc = "order %02X, row %3d (%s)"%(dbg_order, dbg_row,ch_str[dbg_channel]) + warn("%s: %s: %s"%(loc, msg, row_str(row, dbg_fxs))) + 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)) + rowstr = row_str(row, dbg_fxs) + # warn("clamped volume to %02X: %s"%(newvol, rowstr)) + row_warn(row, "volume clamped to %02X"%newvol) return newvol def convert_s_row(row, channel): @@ -417,6 +421,8 @@ def convert_s_row(row, channel): pass elif fx in [0xed]: # pre-instrument FX pass + elif fx == 0x08: # panning + row_warn(row, "panning FX invalid for SSG") elif fx == 0x0b: # Jump to order jmp_to_order = fxval elif fx == 0x0d: # Jump to next order @@ -435,10 +441,22 @@ def convert_s_row(row, channel): elif fx == 0xe2: # slide down assert fxval != -1 opcodes.append(s_slide_d(fxval)) + elif fx == 0xe5: # set pitch (tune) + opcodes.append(s_pitch(fxval)) elif fx == 0x0a: # volume slide down # fxval == -1 means disable vibrato fxval = max(fxval, 0) opcodes.append(s_vol_slide_d(fxval)) + elif fx == 0x01: # pitch slide up + # fxval == -1 means disable slide + fxval = max(fxval, 0) + opcodes.append(s_pitch_slide_u(fxval)) + elif fx == 0x02: # pitch slide down + # fxval == -1 means disable slide + fxval = max(fxval, 0) + opcodes.append(s_pitch_slide_d(fxval)) + elif fx == 0x03: # pitch slide down + opcodes.append(s_porta(fxval)) else: add_unknown_fx('SSG', fx) @@ -447,7 +465,7 @@ def convert_s_row(row, channel): if row.note == 180: opcodes.append(s_stop()) else: - opcodes.append(s_note(to_ssg_note(row.note))) + opcodes.append(s_note(to_nss_note(row.note))) return jmp_to_order, opcodes @@ -516,6 +534,12 @@ def convert_b_row(row, channel): jmp_to_order = 257 elif fx == 0x0f: # Speed opcodes.append(speed(fxval)) + elif fx == 0x01: # pitch slide up + # fxval == -1 means disable slide + fxval = max(fxval, 0) + opcodes.append(b_pitch_slide_u(fxval)) + elif fx == 0x03: # portamento + opcodes.append(b_porta(fxval)) else: add_unknown_fx('ADPCM-B', fx) @@ -524,11 +548,24 @@ def convert_b_row(row, channel): if row.note == 180: opcodes.append(b_stop()) else: - opcodes.append(b_note(to_nss_b_note(row.note))) + opcodes.append(b_note(to_nss_note(row.note))) return jmp_to_order, opcodes +cached_nss = {} def raw_nss(m, p, bs, channels, compact): + global dbg_order, dbg_row + + # a cache of already parsed rows data + def row_to_nss(func, pat, pos): + global cached_nss, dbg_channel, dbg_fxs + idx=(pat.channel, pat.index, pos) + if idx not in cached_nss: + dbg_channel = pat.channel + dbg_fxs = m.fxcolumns[pat.channel] + cached_nss[idx] = func(pat.rows[pos], pat.channel) + return cached_nss[idx] + # unoptimized nss opcodes generated from the Furnace song nss = [] @@ -541,7 +578,7 @@ def raw_nss(m, p, bs, channels, compact): selected_a = [x for x in a_channels if x in channels] selected_b = [x for x in b_channel if x in channels] - # initialize stream speed from module + # initialize stream speed from module tick = m.speed # -- structures @@ -560,7 +597,7 @@ def raw_nss(m, p, bs, channels, compact): # the beginning of another order, for example to avoid playing the # remaining rows of an order. any jump to a previously played order # is essentially equivalent to looping the song playback. - + seen_orders=[] seen_patterns=[] order=0 @@ -594,32 +631,38 @@ def raw_nss(m, p, bs, channels, compact): for index in range(pattern_length): # nss opcodes to add at the end of each processed Furnace row opcodes = [] + dbg_order, dbg_row = order, index # FM channels for channel in f_channels: - row = order_patterns[channel].rows[index] - j, f_opcodes = convert_fm_row(row, channel) + j, f_opcodes = row_to_nss(convert_fm_row, order_patterns[channel], index) if channel in selected_f: opcodes.extend(f_opcodes) jmp_to_order = max(jmp_to_order, j) # SSG channels for channel in s_channels: - row = order_patterns[channel].rows[index] - j, s_opcodes = convert_s_row(row, channel) + # dbg_channel = channel + # row = order_patterns[channel].rows[index] + # j, s_opcodes = convert_s_row(row, channel) + j, s_opcodes = row_to_nss(convert_s_row, order_patterns[channel], index) if channel in selected_s: opcodes.extend(s_opcodes) jmp_to_order = max(jmp_to_order, j) # ADPCM-A channels for channel in a_channels: - row = order_patterns[channel].rows[index] - j, a_opcodes = convert_a_row(row, channel) + # dbg_channel = channel + # row = order_patterns[channel].rows[index] + # j, a_opcodes = convert_a_row(row, channel) + j, a_opcodes = row_to_nss(convert_a_row, order_patterns[channel], index) if channel in selected_a: opcodes.extend(a_opcodes) jmp_to_order = max(jmp_to_order, j) # ADPCM-B channel for channel in b_channel: - row = order_patterns[channel].rows[index] - j, b_opcodes = convert_b_row(row, channel) + # dbg_channel = channel + # row = order_patterns[channel].rows[index] + # j, b_opcodes = convert_b_row(row, channel) + j, b_opcodes = row_to_nss(convert_b_row, order_patterns[channel], index) if channel in selected_b: opcodes.extend(b_opcodes) jmp_to_order = max(jmp_to_order, j) @@ -781,15 +824,6 @@ def tune_adpcm_b_notes(nss, ins): # nullsound note from current sample's frequency sample_note = c4 - def to_direct_note(note): - semitone = note & 0xf - octave = (note>>4) & 0xf - return (octave*12)+semitone - - def to_b_note(direct): - octave, semitone = direct // 12, direct % 12 - return (octave<<4) | semitone - def note_str(note): octave, semitone = note // 12, note % 12 return semitones[semitone].ljust(2, '-')+str(octave) @@ -816,16 +850,11 @@ def tune_adpcm_b_pass(op, out): out.append(op) elif type(op) == b_note: # get the semitone offset from c4 - dnote = to_direct_note(op.note) - semitone_offset = dnote - c4 + semitone_offset = op.note - c4 # the "tuned" note is the note to use in nullsound to configure the # right frequency in the YM2610 (i.e. the semitone offset from the # sample's base frequency) - tuned = to_b_note(sample_note + semitone_offset) - # fmt_off = "%s %2d"%("+" if semitone_offset>=0 else "-", abs(semitone_offset)) - # print("B NOTE: %s (= C-4 %s)"%(note_str(dnote), fmt_off), - # "-> TUNED: %s %s = %s"%(note_str(sample_note), - # fmt_off, note_str(to_direct_note(tuned)))) + tuned = sample_note + semitone_offset out.append(b_note(tuned)) else: out.append(op) @@ -1227,6 +1256,3 @@ def main(): if __name__ == "__main__": main() - - -