First off, hope everyone had a good Christmas. I was away for the most of it and had very little internet access. On the upside, the time spent away from the internet made me more productive and NTRQ (my NES tracker) now has pretty much full audio. I'll put up a new video soon.
Anyway, aware that I've already posted about this in the past but I'm looking at it again to try to get a clever, low-impact solution for NTRQ.
It's to do with pitch sliding and vibrato and getting a uniform effect throughout the note range.
In Nijuu I had a massive table with 32 fractional steps in between each semi-tone for one complete octave and then multiplied/divided that value based on the octave. It worked really well but doing maths took it's toll on the CPU usage and I want to try to avoid that this time.
I was recently looking at some code for a C64 tracker and it had quite a neat little solution. In order to obtain the offset to add to the note frequency each frame, it used the current note number as an index to the period table but actually used the high-byte of the 16-bit note frequency setting as the low-byte of the per-frame pitch modification, setting the high-byte of the offset to 0 (mostly and simplified).
What this then gave you was a 16-bit offset value that was scaled depending on the note pitch. Luckily, on the C64, the frequency uses 16-bits as opposed to 11 on the NES so you have much better resolution.
I tried to copy the trick but I had to do so much bodging and massaging of the results (as you know the top 3 or 4 octaves of the NES has 0 as the high-byte so the resolution is very poor) that it became a bit silly (plus I had to have a third fraction byte as the offsets in the upper octaves were too coarse).
Another idea I had was to construct a second pitch table that was a table of 16-bit offsets to use for slide/vibrato. Currently I can't think of a clever way of calculating the values though.
Any thoughts?
If you're not worried about CPU usage (and outside of games you probably aren't), you could try linear interpolation at runtime. Subtract two consecutive semitones' periods (whose difference will always fit in 8 bits on an NES) and then multiply the difference by the fractional pitch.
Isn't anyone who makes sound engines thinking about making them usable in games? Most of the ones I've seen use too much CPU time!
I haven't done much music work on the NES, but maybe this is one of those things that have to be customized/optimized on a per-project basis (much like scrolling engines, map compression, things like that)?
Well, for my NROM projects I cut out vibrato, because too much space is taken up for me to include it. I suppose I -could- have included it, but it was just a sacrifice I was willing to make. However, for other projects, I usually have a table of 11-bit entries for each pitch in the 12-tone scale that you store directly into the sound registers. Then for arpeggio, it's a little more complicated. I have a table that has the difference between each two notes in the pitch table divided by 32. By this, I mean like the value for NTSC to store into the sound registers is $7F1 for low A, and the value for the Bb right above that is $780. I take the difference of those two values ($71) and divide that by 32. That's one entry in the arpeggio table, and I do the same for the difference between Bb and B, B and C, C and Db, etc. all the way up to the highest necessary note. As you can see, $71 is 113 in decimal, and that divided by 32 is roughly 3.52. So I take that value, and multiply it by some value between 0 and 31 depending on how much I want to bend the pitch, and I subtract it from the 11-bit value of the next note down.
If this sounds confusing, it's probably because I'm bad at explaining things. But say we were talking about bending the low A to be halfway between the low A and the Bb above it. I would take the value in the arpeggio table that is the difference between those notes divided by 32 (3.52) and I would multiply it by 16, since I want to play an A and 1/2, or A and 16/32. So you take 3.52, multiply it by 16, and you get 56.32. Then you subtract that from 2033 ($7F1), and you get 1976.68, which in hex is about $7B8. That would be the value you store in the sound registers to play the pitch between a low A and the Bb above it.
For the arpeggio table, I keep track of the difference between each pitch divided by 32 with 8 bits. The high 2 most significant bits represent the value before the decimal point. Since each value never exceeds 4.00, I only need to use 2 bits to represent values 0-3 for the numbers left of the decimal point. Then the other bits are just precision bits. All together, the arpeggio table takes up about $5D or 93 bytes. The code to deal with it takes a bit more space, and it takes up more time to process. Sorry if this doesn't make any sense
. I'd be happy to try and clarify what I'm talking about if there's any confusion.
Celius, it seems you do something similar to what tepples suggested, but you are limited to 32 units between notes. I don't know if having this table is worth it, because you need a multiplication anyway, just like in tepples' method which isn't limited to 32 units.
Well you could probably do the same but have 64 units between each pitch. But to be honest, the difference between 1/32 and 2/32 pitchbend is quite inaudible. I don't know why you'd need more precision than that. But I guess you're right about the multiplication being too slow.
I believe I might have a possible solution. You take the arpeggio table idea, and you pre-calculate the desired entry times 1, 2, 3, 4, 5, up to however much you care to alter the pitch by. By pre-calculate, I mean you calculate them with multiplication routines before doing anything with pitch bending. You keep these results stored in RAM, and you add or subtract one of these results from the current pitch to alter it one fragment at a time. Kind of like how you were saying with scrolling and map decompression. Sometimes it's not optimal to calculate the resulting value from scratch. Rather just add a little at a time. This solution only has you doing additions and subtractions rather than multiplications every frame (well, you'll have to do them at least once, which is the down side. Plus it requires more RAM).
neilbaldwin wrote:
[...] In order to obtain the offset to add to the note frequency each frame, it used the current note number as an index to the period table but actually used the high-byte of the 16-bit note frequency setting as the low-byte of the per-frame pitch modification, setting the high-byte of the offset to 0 (mostly and simplified).
[...] I tried to copy the trick but I had to do so much bodging and massaging of the results (as you know the top 3 or 4 octaves of the NES has 0 as the high-byte so the resolution is very poor) that it became a bit silly (plus I had to have a third fraction byte as the offsets in the upper octaves were too coarse).
The first thing that comes to my mind is to grab the 3 high bits of the period, then ROL some of the lower 8 bits into them. It would cost 7 CPU cycles per bit (if you use zeropage), but spending 14 cycles to achieve a 5-bit resolution instead of working with a 3-bit resolution doesn't sound so bad to me.
Better yet, it'd cost 21 cycles to get the highest 8 bits of the period, if you take the lowest byte and ROR the 3 high bits into them. I'm not entirely sure if that's what you need though.
Thanks everyone.
Celius: your difference-between-semitones-divided-by-32-table is probably close to what I was thinking. I might try a similar idea and tweak it a bit to suit. Just a general question: how does your method work if, say, you want to slide the note over several semitones or, say, an octave?
Drag: i actually tried this but I found that ROLing the 11 bits exaggerated the effect on lower pitches because of the non-linearity (obviously of course). It worked OK higher up the pitch range though. I then tried a convoluted method of ROLing just the low byte and then doing a ADC #$00 to the high byte (actually that's what I've got in the code right now). It kind of works but you don't really get a uniform(ish) effect in each octave.
Actually my current method is taking the current note's index into the pitch table, subtracting the slide setting (just an arbitrary number from $01 to $1f which denotes the speed, slow to fast, of the slide) which gives you an index that is $01 to $1f semitones lower than the current note. I then grab the pitch table values, put the low byte into the offset fraction byte and the high byte into the offset low byte.
This then gets added (or subtracted) per frame until the target pitch is reached. Actually it's slightly more complicated in the code but the idea is right.
The problem with my current method is I could only bend a tone to be + or - 4 notes. You specify pitch bend with a signed byte, where $80 specifies no pitch change, and $60 would specify bending a note a whole note down. In the case where it bends beyond one semitone, I look at the pitch table for the nearest semitone to what I'm trying to bend to, and then I find the appropriate difference between those two semitones, and multiply that value by whatever and add/subtract it from the value of the nearest semitone.
In a logarithmic pitch environment like MIDI, when you bend up or down farther than a couple semitones, you probably want to do it with a legato noteon so that the base note for the bend gets rewritten. General MIDI, for instance, appears to have a range of +/- 2 semitones for the pitch bend command.
Well I think both method of the table containing 32 entries per note (for 12 notes) and the method based on linear interpolation of a whole note table would work. I don't think the error of the linear interpolation could be noticeable by human hear. So, you'd have to figure which of both method is the fastest CPU wise and use it. I have no idea which one it will be, you'll have to try both.
I think I've got a pretty acceptable solution now.
I added a couple of fake octaves to the start of the frequency table (i.e. numbers bigger than $7FF) and then the code to get the offset is this;
Code:
;
;In : slide speed, $00 to $1F. Lower numbers = slower slide
;Requires plyrSlideFrom and plyrSlideTo to be set before calling
;
getSlideAdd:
sta plyrTemp00
lda plyrSlideFrom ;subtract end note from start
sec
sbc plyrSlideTo
sta plyrTemp01 ;temp store for later compare
lda plyrSlideFrom ;use start note as index?
asl plyrTemp01 ;shift bit 7 into carry
bcs @a ;carry set if sliding up
lda plyrSlideTo ;so use destination note for table index
@a clc
adc #$0C ;shift up by 1 octave
sec
sbc plyrTemp00 ;subtract slide setting value
bpl @b
lda #$00 ;clamp at 0 if gone negative
@b tax
lda NTSC_noteLo,x ;set fraction to low byte of freq
sta plyrSlideAddFrac
lda NTSC_noteHi,x ;set low add to high byte of freq
sta plyrSlideAddLo
lda #$00 ;clear other vars
sta plyrSlideAddHi
sta plyrSlideFrac
sta plyrSlideLo
sta plyrSlideHi
rts
I've been playing around making some music in the tracker and it's actually pretty good. I'll put up a new video soon so you can hear.
tepples wrote:
General MIDI, for instance, appears to have a range of +/- 2 semitones for the pitch bend command.
But also specified in GM are so-called RPNs that can control that range -- even if many people don't know it. My commercial synth can be told to increase the range up to +/- 24 semitones.
Ah, I think I completely missed what you were trying to do. Were you trying to get it so that the pitch slides themselves were linear, or were you trying to get it such that no matter what the source and destination pitches are, the slide itself always takes the same amount of time?
Drag wrote:
Ah, I think I completely missed what you were trying to do. Were you trying to get it so that the pitch slides themselves were linear, or were you trying to get it such that no matter what the source and destination pitches are, the slide itself always takes the same amount of time?
Probably more the latter, within reason.
Actually I was trying achieve a uniform-ish effect (slide/vibrato) despite the octave(s) you were working in.
I know it's always going to be a compromise but I wanted a compromise that was both simple(ish) ans musically nice to work with,