Page 1 of 1

Pitch Slide and Arpeggio Combined

Posted: Tue Mar 23, 2010 5:44 am
by neilbaldwin
Has anyone managed to implement this successfully?

It's something that's always plagued me so I'd be interested in theories/examples/code very welcome.

:)

Posted: Tue Mar 23, 2010 6:02 am
by neilbaldwin
I should add that I want to avoid a method that uses a fractional LUT for the pitch table. I had that in Nijuu and it was a big table and a fair old overhead for pitch calculation/manipulation.

I should also add that the issue is with adding a fixed value (i.e. the semi-tone offsets of the arpeggio) to a non-linear value (i.e. the sweeping pitch). With dramatic sweeps it's clearly obvious that as the sweep approaches the target pitch, the arpeggio is adding too much/too little to the sweeping pitch to maintain the relative offset between the base note and the arpeggiated notes. As my sweep routine will snap to the target frequency once it's reached, you hear the arpeggios snap into place.

Posted: Wed Mar 24, 2010 4:11 pm
by Drag
That logarithmic period really throws a wrench into things, doesn't it? :P

Either way, the essence of the arpeggio is that you're cycling between notes that are proportional to each other, and like you said, sliding the pitch around will wreck the proportions, since the APU's pitch scale is non-linear.

I can't really think of any clever/easy way to do it. In the end, it always comes down to either shifting each segment of the arpeggio independently from each other (meaning, you would need to store 3 or 4 periods in memory), or you'd need to constantly be recalculating the arpeggio segments from the base period (which takes CPU time).

It looks like segments are easily scalable though. Like, if I represented arpeggios like this:

Code: Select all

BASE_PERIOD * [1, 0.79, 0.67]
Then that would give me a major scale arpeggio for all BASE_PERIOD, which means you'd be able to slide BASE_PERIOD all around and as long as you keep recalculating, you'll always get a proportional arpeggio.

By the way, I calculated those values with:

Code: Select all

y = (2^(x/12))/2
C  = (x = 12)
C# = (x = 11)
D  = (x = 10)
...
B  = (x = 1)
You wouldn't need to perform *this* calculation on the 6502 though, you could just store the result in fixed point or something in the music data.

Posted: Wed Mar 24, 2010 4:15 pm
by Dwedit
I think Famitracker just ignores the logarithmic scale, and just adds or subtracts to and from the period directly.

Posted: Wed Mar 24, 2010 4:57 pm
by ReaperSMS
One way to do it is to treat your notes as 8.8 fixed point, whole choosing a semitone, fraction the distance to the next, and instead of a table, just doing the lerp math.

Yeah, the multiply sucks donkey balls as far as cycles go, but it can be done without huge tables, and IIRC you can shortcut a bit of it since you don't necessarily care about all of the bits.

I'll dig up the code when I get home later, but the general gist is of course
tone = whole(note); bend = frac(note);
diff = freq(tone+1) - freq(tone)
diff = (diff * bend)/256
freq = freq(tone) + diff;

If you store the bend seperately, this will bend over a larger range, just have to pick the right pair of semitone freqs for the diff calc.

edit: found the code. don't remember what the cycle count on this one is, guessing it's ~140 or so.

Code: Select all

.proc mul_816_16
	;; mul tmpc/tmpc+1 by A, returning the upper 16 bits in tmpb
	ldx #0
	stx tmpb
	stx tmpb+1
	eor #$FF
	sta tmpa
	ldx #8
	lda tmpc+1
	beq bytemul
l1:	lsr tmpb+1
	ror tmpb
	lsr tmpa		; inverted
	bcs next
	lda tmpb
	adc tmpc
	sta tmpb
	lda tmpb+1
	adc tmpc+1
	sta tmpb+1
next:	dex
	bne l1
	lsr tmpb+1
	ror tmpb
	rts	
bytemul:
	ldy #0
l2:	lsr tmpb+1
	ror A
	lsr tmpa
	bcs next2
	adc tmpc
	bcc next2
	inc tmpb+1
next2:	dex
	bne l2
	lsr tmpb+1
	ror A
	sta tmpb
	rts
.endproc

Posted: Thu Mar 25, 2010 1:40 am
by neilbaldwin
@Reaper

I've read your post a dozen times and while it seems intriguing, I still don't understand what it is you're proposing :)

@Dwedit

Not sure what you mean either. Are you saying Famitracker suffers the same problem that I have? Or does pitch slide + arpeggio work properly in FT? I've seen a lot of trackers that disable arpeggio when sliding (or vice versa), which is understandable :)

Posted: Thu Mar 25, 2010 8:49 am
by Dwedit
I just checked Famitracker, it resets the pitch slide every time the note changes, so it doesn't work.

Posted: Thu Mar 25, 2010 10:33 am
by ReaperSMS
In a bit more detail...

With no pitch bending, notes are pretty easily specified as a number of semitones above some base. A number of games use the MIDI note range, which is 128 tones, ranging from C-1 to somewhere around C10. The usable range on the NES is a little smaller than that, ranging from C2 to B9 for the 2a03 square channels, C1 to B8 for the triangle (it runs an octave lower for any particular frequency dropped in), and A0-B9 for the VRC6 (12 bit frequency dividers)

Here's the freq table I use(d) for the bloopageddon engine:

Code: Select all

freqtable:
   .word                   $FE3, $EED, $E1F ; 00 A0
   .word $D44, $C98, $BDF, $B32, $A97, $9F3 ; 03 C1
   .word $972, $8E0, $865, $7F1, $776, $70F
   .word $6A2, $64B, $5EF, $598, $54B, $4F9 ; 0F C2
   .word $4B8, $46F, $432, $3F8, $3BB, $387
   .word $350, $325, $2F7, $2CC, $2A5, $27C ; 1B C3
   .word $25C, $237, $218, $1FB, $1DD, $1C3
   .word $1A8, $192, $17B, $165, $152, $13E ; 27 C4
   .word $12D, $11B, $10C, $0FD, $0EE, $0E1
   .word $0D3, $0C9, $0BD, $0B2, $0A8, $09E ; 33 C5
   .word $096, $08D, $085, $07E, $076, $070
   .word $069, $064, $05E, $059, $054, $04F ; 3F C6
   .word $04B, $046, $042, $03F, $03B, $037
   .word $034, $031, $02F, $02C, $029, $027 ; 4B C7
   .word $025, $023, $021, $01F, $01D, $01B
   .word $01A, $018, $017, $015, $014, $013 ; 57 C8
   .word $012, $011, $010, $00F, $00E, $00D
   .word $00C, $00C, $00B, $00A, $00A, $009 ; 63 C9
   .word $008, $008, $007
As you can see, the differences in the divider values for each pair of notes decreases as you crawl up the octaves. Given that, the amount you add/subtract from the value above to bend between a pair of notes, or to do some tremolo effect or whatnot has to change also as you go up. Adding +/- 5 to the above would be close to an inaudible fraction of a semitone at the lower end of the scale, but up around C8, would be 2-4 semitones.

My suggestion is to represent pitch bend not as a direct amount to add/subtract, but as a fraction of the difference between a note and the following one. $1B.00 would be C3, no shift, $1B.80 would be halfway from C3 to C#3, etc. Thus, adding $0C.00 to any note would always be a 1-octave shift.

To come up with the actual register value needed for any given note $AA.BB, the value would be: table[AA] + (table[AA+1]-table[AA])*BB/256. for 1B.80, that would be:

Code: Select all

table[1B] = $350
table[1C] = $325
diff = -$3B = $FFC5
diff * $80 = -$1D80 = $FE280
last / 256 = -$1E = $FFE2
$350 - $1D = $333 (or $350 + $FFE2)
To combine this with arpeggiation, your arpeggio steps would be whole semitone values, so in the engine you'd track probably the following values per-note:

Code: Select all

a = Base tone
bb.cc = Current pitchbend amount (assuming it's 16 bits for thoroughness)
d = Current arpeggio offset
Each frame (could save some time by checking for changes) you produce the actual note N as a + b + d. The actual period to stuff into the register would be (table[N+1] - table[N]) * c >> 8.

Code: Select all

Pros:
   linear pitchbends, vibratos
   interacts smoothly with arpeggios
   no extraneous tables
   multi-semitone bends will work automatically
Cons:
   multiplication isn't fast
The freq table can get reduced in size if you go with the approach of having one octave, and shifting down appropriately, but that will be slower, and complicates the note format a bit. The multiply could be sped up greatly by only caring about the top 4 bits of the fraction, at the cost of bend precision.

Posted: Thu Mar 25, 2010 11:15 am
by neilbaldwin
@Reaper - nice :)

I probably led you down the wrong path by using the term "pitch bend" when I actually meant "portamento" or smooth sliding from one note to another over a specified time (or actually, in NTRQ, an arbitrary "speed").

Would your method still work?

Posted: Thu Mar 25, 2010 11:43 am
by ReaperSMS
Yes. You store the current portamento offset, and add speed to it every frame (or every N frames, etc). A speed of 1, added every frame, will smoothly (or as smoothly as is possible) slide up one semitone in just over 4 seconds (256 steps between semitones, 1 step per frame, 60 Hz).

If you want multi-tone slides, you'll need a 16-bit offset, and if you aren't explicitly stopping the slide in the note sequence, you'll want to add some checks to clamp it at the destination.

Posted: Sun Mar 28, 2010 6:08 am
by neilbaldwin
Not intended as a proper solution, I managed to find a simple-ish "trick" to achieve the desired effect.

As the pitch slide code detects when the pitch has reached the destination note, I used this same code to detect when the pitch of the sliding note has shifted to the next semitone above/below the original "root". From there I can use this "calculated" root as the basis for the arpeggio calculation.

It's not perfect as you can detect the steps if your arpeggio is slow but it's not bad as a quick fix.

:)

http://blog.ntrq.net/

Posted: Fri Apr 09, 2010 10:16 pm
by tomaitheous
If you have something like 3 total notes, are sliding the period after each set of notes, or on each next note played? (I would think that you would want it updated after a note set plays)