Pitch Slide and Arpeggio Combined

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
Pitch Slide and Arpeggio Combined
by on (#58860)
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.

:)

by on (#58862)
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.

by on (#58982)
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:
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:
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.

by on (#58983)
I think Famitracker just ignores the logarithmic scale, and just adds or subtracts to and from the period directly.

by on (#58988)
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:
.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

by on (#59002)
@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 :)

by on (#59015)
I just checked Famitracker, it resets the pitch slide every time the note changes, so it doesn't work.

by on (#59020)
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:
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:
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:
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:
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.

by on (#59022)
@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?

by on (#59023)
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.

by on (#59134)
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/

by on (#59899)
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)