Fast LFO

Discuss technical or other issues relating to programming the Nintendo Entertainment System, Famicom, or compatible systems.

Moderator: Moderators

Post Reply
User avatar
neilbaldwin
Posts: 481
Joined: Tue Apr 28, 2009 4:12 am
Contact:

Fast LFO

Post by neilbaldwin »

Been playing around attempting to make the fastest pitch LFO possible. This is my latest version which takes about 4/5th of a scan line. I also need it to be able to handle positive and negative "depth" so that I can use it for pitch sweeping too (set sweep direction in "depth" and a LFO speed of 0).

Can anyone improve on it? It's not a pointless challenge, I'm investigating a new project that needs 36 LFOs so they need to be FAST! :)

Code: Select all

initLFO:	
	lda #$00
	sta lfoPhase	
	lda #$F0
	sta lfoDepth	
	lda #$00
	sta lfoSpeed
	sta lfoCounter
	rts
	

;
;Phase: %00 / %01 = positve, %10 / %11 = negative
;
pitchLFO:
	ldx lfoDepth
	lda lfoPhase
	and #%00000010
	bne @down
	
	txa		;if phase is negative, invert the depth
	eor #$FF
	clc
	adc #$01
	tax

@down:	txa
	bmi @a
	clc
	adc freqLo
	sta freqLo
	bcc @b
	inc freqHi
@b:	dec lfoCounter
	beq @c
	rts

@a:	clc
	adc freqLo
	sta freqLo
	bcs @b
	dec freqHi
	
	dec lfoCounter	;time to change phase?
	bne @d	
@c:	dec lfoPhase	;yes, go backwards %00, %11, %10, %01
	lda lfoSpeed	;reset counter
	sta lfoCounter
@d:	rts
User avatar
Kasumi
Posts: 1293
Joined: Wed Apr 02, 2008 2:09 pm

Re: Fast LFO

Post by Kasumi »

I know nothing about LFOs, but you can save two cycles by not loading #$00 a second time in the int phase.

Code: Select all

initLFO:	
	lda #$00
	sta lfoPhase
	sta lfoSpeed
	sta lfoCounter	
	lda #$F0
	sta lfoDepth	
	
	rts

*shrug*

And if your code that calls the functions looks like this at any time:

Code: Select all

	jsr initLFO
	jsr pitchLFO
You can save 12 cycles each time you need to init an LFO by avoiding a jsr rts pair by removing the rts. If you need to use initLFO without running pitch LFO immediately afterward, you could have the same function with an RTS duplicated someplace else in code.

Code: Select all

initLFO:	
	lda #$00
	sta lfoPhase
	sta lfoSpeed
	sta lfoCounter	
	lda #$F0
	sta lfoDepth	
	
	;rts;Removing. When you need to init and run a pitch lfo, you jsr to initLFO
;When you need to just run the pitch LFO, jsr to pitchLFO

pitchLFO:
;pitchLFO code here

Sorry, if that's not helpful. Just wanted to try.

edit: Are we allowed to use y? I'm not sure I could make it do the exact same thing, faster with y, but it'd be good to know.

Edit 2: Provided the above helps, you could further save cycles like this:

Code: Select all

initLFO:   
   lda #$00
	sta lfoPhase
	sta lfoSpeed
	sta lfoCounter	
	ldx #$F0
	stx lfoDepth
	jmp pitchLFO2;or bne;adds 3 cycles

;
;Phase: %00 / %01 = positve, %10 / %11 = negative
;
pitchLFO:
   ldx lfoDepth;But saves at least 3, depending on if these
   lda lfoPhase ;are zero page or absolute
pitchLFO2:
But I'm rambling now, sorry. Apologies also if I'm incorrect in my assumptions of the order of how these subroutines will be used. I only saved 17 cycles anyway, and probably only once, instead of 36 times, assuming my assumptions are even correct in the first place. :lol:
User avatar
blargg
Posts: 3717
Joined: Mon Sep 27, 2004 8:33 am
Location: Central Texas, USA
Contact:

Post by blargg »

Describing the basic algorithm would have made this easier. I believe it's basically this:
* Add lfoPhase to freqLo/freqHi if bit 1 of lfoPhase is set, subtract otherwise.
* Decrement lfoCounter. If zero, decrement lfoPhase and copy lfoCounter to lfoSpeed.

For your negate code, this is faster, as it avoids the ADC since you can just use INX:

Code: Select all

; Negate X
txa
eor #$FF
tax
inx
Then, since you have separate code for addition and subtraction, you might as well do that rather than negate lfoDepth. By interpreting bit 1 of lfoPhase as a subtract flag rather than add, you can get carry set properly before the add/subtract. I calculate the worst-case performance of this to be 58 cycles, assuming variables are in zero-page and no page crosses. This also doesn't use X anymore. But I'm not sure of lfoDepth; it seems the original code allowed this to be both negative and positive, which this doesn't handle. It seems the depth should be a magnitude only, so I only handle a positive depth here.

Code: Select all

pitchLFO:
        lda lfoPhase    ; carry = subtract flag
        lsr a
        lsr a
        lda freqLo      ; preload
        bcs @sub

@add:   adc lfoDepth    ; freq += lfoDepth
        bcc :+
        inc freqHi
:       jmp @final

@sub:   sbc lfoDepth    ; freq -= lfoDepth
        bcs :+
        dec freqHi
:       
@final: sta freqLo
        dec lfoCounter
        beq :+
        dec lfoPhase    ;yes, go backwards %00, %11, %10, %01
        lda lfoSpeed    ;reset counter
        sta lfoCounter
:       rts
Last edited by blargg on Sun Nov 14, 2010 7:07 pm, edited 2 times in total.
User avatar
neilbaldwin
Posts: 481
Joined: Tue Apr 28, 2009 4:12 am
Contact:

Post by neilbaldwin »

Thanks blargg.

Yes, my code allows the 'depth' variable to be positive or negative, thus I can use the LFO to do pitch sweeping by setting 'lfoSpeed' to zero and using 'lfoDepth' as a signed offset to be added each frame.
User avatar
blargg
Posts: 3717
Joined: Mon Sep 27, 2004 8:33 am
Location: Central Texas, USA
Contact:

Post by blargg »

Well, that costs you quite a few cycles. I was also wondering why you used bit 1 of lfoPhase rather than bit 0. It seems that this merely halves the speed.
User avatar
neilbaldwin
Posts: 481
Joined: Tue Apr 28, 2009 4:12 am
Contact:

Post by neilbaldwin »

blargg wrote:Well, that costs you quite a few cycles. I was also wondering why you used bit 1 of lfoPhase rather than bit 0. It seems that this merely halves the speed.
Because the phase has four states: %00 = up, %11 = down, %10 = down, %01 = up to give you the necessary cycle shape:

Code: Select all

/\
----------- center frequency
  \/
User avatar
blargg
Posts: 3717
Joined: Mon Sep 27, 2004 8:33 am
Location: Central Texas, USA
Contact:

Post by blargg »

I see; I figured you might just initialize lfoCounter to lfoSpeed/2 when starting the LFO. Anyway, to optimize this, you need to figure out the essence of what it's doing, and strip away complications like this. Most of the time, it's adding/subtracting a value from pitch. Maybe you can get it down to a counter and 16-bit value that's added to the frequency, without any branches except when the counter reaches zero.
User avatar
neilbaldwin
Posts: 481
Joined: Tue Apr 28, 2009 4:12 am
Contact:

Post by neilbaldwin »

blargg wrote:I see; I figured you might just initialize lfoCounter to lfoSpeed/2 when starting the LFO.
That's quite a nice idea actually. It only falls down if the speed is an odd number as you'll end up with the median pitch moving away from the original slightly. I guess you could look at it the other way and set the speed to *2 after the initial cycle but that's adding complication back in.
Anyway, to optimize this, you need to figure out the essence of what it's doing, and strip away complications like this. Most of the time, it's adding/subtracting a value from pitch. Maybe you can get it down to a counter and 16-bit value that's added to the frequency, without any branches except when the counter reaches zero.
Not sure I follow you there. Surely some logic is needed to figure out the direction/sign of the 16-bit value?
User avatar
blargg
Posts: 3717
Joined: Mon Sep 27, 2004 8:33 am
Location: Central Texas, USA
Contact:

Post by blargg »

Yes, when enabling the LFO, you calculate the 16-bit value, then add it to freq:

Code: Select all

pitchLFO:
        lda freqLo
        clc
        adc lfoAddLo
        sta freqLo
        
        lda freqHi
        adc lfoAddHi
        sta freqHi
        
        dec lfoCounter
        bne :+
        
        ; Negate lfoAdd
        
        lda #0
        sec
        sbc lfoAddLo
        sta lfoAddLo
        
        lda #0
        sbc lfoAddHi
        sta lfoAddHi
        
        lda lfoSpeed
        sta lfoCounter
        
:       rts
Hmmm, this might be worse, if worst-case performance is all that matters. If you can keep lfoDepth a positive value, my original code is probably fastest.
Post Reply