the math behind generating the APU waveforms?

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
the math behind generating the APU waveforms?
by on (#68151)
i'm trying to get sound going in my emulator, but i just don't quite understand what the math has to be to generate the waveforms. i have all the registers stuff working, and i even have some sort of super-crappy triangle channel playing sound but i'm not doing it right.

i've read blargg's and brad taylor's APU docs, both of which have a lot of good info - but, i'm just kind of lost when it comes to doing the math with the values calculated from all the register writes.

i know each channel gets clocked every (cpuclockrate / channelfreq) cpu ticks, and i need to take a snapshot of all the channels' values every (cpuclockrate / samplerate) cpu ticks.

the problem is i just am lost when trying to come up with the formulas to modify the values for every channel clock. i feel like am so close on this, but i can't quite figure out what's next.

here's a 1 MB WAV (44.1 KHz 8-bit mono) of what my triangle channel code is sounding like when i play megaman 1. this is from the stage select screen and then starting fire man's level.

http://rubbermallet.org/trianglechannel.wav

huge thanks to anybody who can point me in the right direction in getting this all working properly!!
Re: the math behind generating the APU waveforms?
by on (#68155)
miker00lz wrote:
i need to take a snapshot of all the channels' values every (cpuclockrate / samplerate) cpu ticks.

There is no snapshot.* The NES APU period dividers operate continuously based on the CPU clock.

I listened to the wave file, and then I looked at it in a waveform editor. The first thing I saw was an incorrect waveform. It appears to be as follows:
[ 4 5 6 7 8 7 6 5 4 3 2 1 0 1 2 3 ]
The actual NES triangle waveform is 32 units wide and 15 tall:
[ 8 9 10 11 12 13 14 15 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 0 1 2 3 4 5 6 7 ]

But what's causing the more annoying audible artifact is the sharp, temporary transition to and from an incorrect pitch once every 30 milliseconds.


* Emulators on systems with very limited CPU, such as the Game Boy Advance, might forgo accuracy for performance.

by on (#68156)
Example code to handle triangle wave. Untested. Doesn't do linear or length counters.

Code:
int tri_phase;
int tri_timer;
int r400A;
int r400B;

// Clocks triangle's timer. Should be called once per CPU cycle,
// 1789773 times per second on NTSC.
void clock_tri_timer()
{
    tri_timer--;
    if ( tri_timer <= 0 )
    {
        tri_timer = (r400B & 0x07)*0x100 + r400A + 1;
        tri_phase = (tri_phase + 1) % 32;
    }
}

// Current triangle amplitude. Call at any time.
int triangle_amp()
{
    if ( tri_phase < 16 )
        return tri_phase;
    else
        return 31 - tri_phase;
}
Re: the math behind generating the APU waveforms?
by on (#68159)
tepples wrote:
miker00lz wrote:
i need to take a snapshot of all the channels' values every (cpuclockrate / samplerate) cpu ticks.

There is no snapshot.* The NES APU period dividers operate continuously based on the CPU clock.

I listened to the wave file, and then I looked at it in a waveform editor. The first thing I saw was an incorrect waveform. It appears to be as follows:
[ 4 5 6 7 8 7 6 5 4 3 2 1 0 1 2 3 ]
The actual NES triangle waveform is 32 units wide and 15 tall:
[ 8 9 10 11 12 13 14 15 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 0 1 2 3 4 5 6 7 ]

But what's causing the more annoying audible artifact is the sharp, temporary transition to and from an incorrect pitch once every 30 milliseconds.


* Emulators on systems with very limited CPU, such as the Game Boy Advance, might forgo accuracy for performance.


i actually am using the proper waveform. i think what might be happening is that it's not hitting every byte. this is my code in the 6502 core where it generates the sound..

(the core itself is neil bradley's, which i got off of zophar.net and then made some modifications so it's more suited to a NES emu with the mappers and stuff.)

Code:
void exec6502(int timerTicks)
{
   while (timerTicks > 0)
   {
      opcode = read6502(PC++);
      instruction[opcode]();
      clockticks6502 += ticks[opcode];
      timerTicks -= clockticks6502;
      clockticks6502 = 0;
      totalticks += ticks[opcode];
      if (triangle_enable!=0 && (totalticks%triangle_freq)==0) triangle_clock();
      if (square1_enable!=0 && (totalticks%square1_freq)==0) square1_clock();
      if ((totalticks%(cpuclock/samplerate))==0) {
         cursample = (((trianglesample-8)+(square1sample-8))/2)<<1;
         putinbuf();
      }

      if ((totalticks%(cpuclock/240))==0 && doaudio==1) APU_frame_seq_tick();           
   }
}


the square1 stuff doesn't generate anything yet. i know the mixing is horribly incorrect, but i just put that together to see what it was sounding like so far with just the triangle.

anyway, it looks like it might be skipping data from my triangle enum because it's usually not going to land on exactly the right totalticks value to modulo with triangle_freq to equal zero now that i look again. i suppose i'll need to test for a range.

what i'm really confused about though is how to go about generating the waveform for the square/pulse channels. the noise channel and DMC are simple enough.

what's involved mathematically speaking for generating the envelope, etc?

by on (#68160)
blargg wrote:
Example code to handle triangle wave. Untested. Doesn't do linear or length counters.

Code:
int tri_phase;
int tri_timer;
int r400A;
int r400B;

// Clocks triangle's timer. Should be called once per CPU cycle,
// 1789773 times per second on NTSC.
void clock_tri_timer()
{
    tri_timer--;
    if ( tri_timer <= 0 )
    {
        tri_timer = (r400B & 0x07)*0x100 + r400A + 1;
        tri_phase = (tri_phase + 1) % 32;
    }
}

// Current triangle amplitude. Call at any time.
int triangle_amp()
{
    if ( tri_phase < 16 )
        return tri_phase;
    else
        return 31 - tri_phase;
}


ah, i wonder if that's more efficient than how i'm trying to do it. looks like it should be.

by on (#68162)
oh yeah, wow. i was right about the needing checking for a range condition (obviously)

listen to this new recording after fixing it. still not flawless, but damn it is kind of starting to sound like a NES! :D

http://rubbermallet.org/newtriangle.wav

but yeah, still could use some help with generating the square/pulse waveforms! you guys are awesome. :o

after i get a decent sounding APU, i'm ready to start adding more mappers other than only 0 and 2 which i have now.

by on (#68168)
Quote:
ah, i wonder if that's more efficient than how i'm trying to do it. looks like it should be.

If you want efficient, this is the most: { } I think it's best to get it correct first, then worry about efficiency. As in, don't even think about efficiency until you have something working that's easy to work with.

Your original one fails because it doesn't run the waves if the instruction beginning doesn't happen to fall on the exact clock the waves run. You need it to run them however many times they ticked since the last instruction.

I highly recommend taking a look at my blip_buf library, particularly demo_chip.c. It lets you work in terms of the waveform at the full CPU clock rate, and handles everything else to get you the samples you can play on the PC. You are still generating the waveforms, so it's not taking your work away from you, just allowing you to code the actual NES stuff, rather than the fairly involved and easy to make a mess of resampling for the PC.
Re: the math behind generating the APU waveforms?
by on (#68223)
...

by on (#68226)
Right, the Wiki covers the waveform from power on, while tepples' is just about the waveform while running, without reference to its initial phase at power.

by on (#68229)
blargg wrote:
Right, the Wiki covers the waveform from power on, while tepples' is just about the waveform while running, without reference to its initial phase at power.


- Ah, you're correct. :) He was analyzing the waveform from that file, sorry!

by on (#68239)
thanks for the info and link to your library blargg. i am trying to do all my own code though (except for obviously 98% of the CPU core) i'll get it accurate. just got it working at all last night. ;)

i've got the triangle and both squares going now, have a look at a minute and a half from megaman: http://www.youtube.com/watch?v=My0krmH9frU

far from perfect but now i'm really understanding how it works. i have to clean up the clicks and add the timer >= 8 check, as well as add noise/DMC plus sweeps for the squares but it's definitely starting to sound kinda like a NES.
Re: the math behind generating the APU waveforms?
by on (#68243)
Zepper wrote:
...

???
Re: the math behind generating the APU waveforms?
by on (#68246)
NESICIDE wrote:
Zepper wrote:
...

???

⌫⌫⌫

by on (#68248)
I never used to understand why some boards didn't allow editing of posts unless it was the last, but now I do. People here go and revise history way too often, as if there's something wrong with a record of the past. It's confusing for anyone reading the thread afterwards. I didn't see anything wrong with Zepper's post. It was correct. Now it's an enigma.

by on (#68252)
Some people are ashamed of being wrong, so they edit theirs posts to cover it up. I usually revise my posts a lot to make sure I'm not posting anything stupid, but I still do it sometimes. When someone corrects me I don't usually edit the original post, unless it said something really misleading that could could confuse other people, in which case I add something like "turns out I was wrong" without removing the original statement(s). I appreciate having an accurate history of the conversations.

by on (#68256)
I think that's one reason why I like wiki. Even though discussion using indent markup is somewhat clumsier than phpBB style discussion, at least wiki has revision history for all pages.

Back to topic: You still have an artifact every 30 milliseconds, though it's a lot more subtle than before. Perhaps the period divider's counter gets reset to 0 whenever you start refilling the sample buffer; the NES doesn't do that.

And when code turns off the triangle channel, you're setting its output to 0. The NES doesn't do that for triangle; it just holds the wave in one position.

by on (#68278)
It sounds a lot like when I was doing 0-15 for the duty cycle, instead of 0-7.

by on (#68293)
tokumaru wrote:
Some people are ashamed of being wrong, so they edit theirs posts to cover it up. I usually revise my posts a lot to make sure I'm not posting anything stupid, but I still do it sometimes.


- Just to let the things clear. There's no enigma. Tokumaru is right, I just didn't pay attention to tepples' discussion regarding the waveform from that file, and it was stupid. Since the board doesn't allow you to delete a posted message (unlike many other forums), all I can do is to delete my post and replace it with "..." for example. Plus, yes, tepples is right, it was a retraction.

- Really, I don't see anything wrong in editing/erasing a bad statement. Please, don't do any early judgment of me. :)

by on (#68295)
blargg wrote:
I never used to understand why some boards didn't allow editing of posts unless it was the last

I think phpBB allows it just in case two people post at almost the same time. It appears to happen here often; see the posting and editing times for this post and the one following it.

by on (#68300)
tepples wrote:
And when code turns off the triangle channel, you're setting its output to 0. The NES doesn't do that for triangle; it just holds the wave in one position.


So, if I read you correctly...when the channel is off [true for all channels? just the triangle?] meaning when it's length counter is zeroed [and linear counter?] the DAC value used in the amplitude calculation should be whatever it was last set to not 0?

Interesting. I'm setting my DACs to 0 when they're disabled. I'll try *not* doing that...

by on (#68302)
- I believe the triangle holds the last sample in order to avoid pops on playback.

by on (#68303)
The APU suppresses triangle pops because they'd be much more noticeable than pulse or noise pops. So I guess triangle works differently because the entire operation of pulse and noise is just one pop after another.

by on (#68308)
Zepper wrote:
I believe the triangle holds the last sample in order to avoid pops on playback.

Goos point. Since the triangle is often used for low frequency waves, pops would be even more noticeable. The wave itself lacks (strong) higher harmonics, unlike the pulse waves, as tepples noted. It's certainly noticeable on some games which set the period to 0 how clicky it sounds. It probably would have taken just about as much hardware to zero the DAC input as on the squares, so it was intentional to have it simply stop the wave in its tracks.

by on (#68407)
i've fixed the triangle resetting the position counter to 0, now it stays. there are still issues with my sound code, and i still haven't gotten around to starting the noise and DMC channel code. i decided to give the sound a rest for a while and focus on getting the main emulation bit more accurate, and fixing graphical glitches.

thanks for the help. if anybody wants to have a look and run the emulator you can get it here as-is:

http://rubbermallet.org/moarnes-0.10.8.10-dev.zip

comes with a pre-built win32 binary+sdl.dll, as well as the source code with Dev-C++ project file. i use Dev-C++ 4.9.9.2 and if you want to compile in that IDE from source you'll have to install the Dev-C++ SDL devpack found at: http://devpaks.org/details.php?devpak=200

there's also a build.sh for linux, but now after opening the SDL video window in a linux build it segfaults. i don't know why, it used to work. haven't looked much into it yet. you'll need the SDL 1.2 dev libraries obviously in linux. if in a flavor of debian, just apt-get install libsdl1.2-dev

the code is a bit ugly and the rendering code could use a rewrite to make much faster.

i'd appreciate any feedback too. it still fails a lot of blargg's tests. one thing i'd like to get going is mapper 1, i just can't quite seem to get it right.

EDIT: just want to say before reading the code makes somebody die, i know i'm missing a lot of thing still mostly in the PPU code. some pretty basic things actually like actually stopping VRAM writes when the flag is set. it will all be added soon. most games run properly though.

another EDIT: since posting this, i've made it pass blargg's sprite RAM test ROM.