APU triangle emulation question

Discuss emulation of the Nintendo Entertainment System and Famicom.

Moderator: Moderators

Near
Founder of higan project
Posts: 1553
Joined: Mon Mar 27, 2006 5:23 pm

APU triangle emulation question

Post by Near »

Hi, so when playing Rockman 2 (Megaman 2), I am getting a buzzing sound all over the place. This can be fixed with band-limited resampling or sinc resampling, but those things are rather intensive.

aliaspider recently noted that by adding "if(period == 0) return 0;" to the top of my APU::Triangle::clock() function, that the buzzing went away completely, even with a simple hermite audio filter (many, many times less CPU intensive.)

Could anyone please look at my triangle emulation code and let me know if that is a proper fix (eg how the hardware really works), or if the game is really supposed to create this buzzing sound without proper audio resampling?

(Note that I use a variable-length integer class. So a uint5 type, when the value is 31, and you add one, will wrap to zero automatically. Much like your traditional uint8 type does now from 255->0. This is why there aren't as many bit-masks in the below code.)

Many thanks in advance!

Code: Select all

struct Triangle {
  unsigned length_counter;

  uint8 linear_length;
  bool halt_length_counter;

  uint11 period;
  unsigned period_counter;

  uint5 step_counter;
  uint8 linear_length_counter;
  bool reload_linear;

  void clock_length();
  void clock_linear_length();
  uint8 clock();

  void power();
  void reset();
} triangle;

Code: Select all

void APU::Triangle::clock_length() {
  if(halt_length_counter == 0) {
    if(length_counter > 0) length_counter--;
  }
}

void APU::Triangle::clock_linear_length() {
  if(reload_linear) {
    linear_length_counter = linear_length;
  } else if(linear_length_counter) {
    linear_length_counter--;
  }

  if(halt_length_counter == 0) reload_linear = false;
}

uint8 APU::Triangle::clock() {
  uint8 result = step_counter & 0x0f;
  if((step_counter & 0x10) == 0) result ^= 0x0f;
  if(length_counter == 0 || linear_length_counter == 0) return result;

  if(--period_counter == 0) {
    step_counter++;
    period_counter = period + 1;
  }

  return result;
}

void APU::Triangle::power() {
  reset();
}

void APU::Triangle::reset() {
  length_counter = 0;

  linear_length = 0;
  halt_length_counter = 0;
  period = 0;
  period_counter = 1;
  step_counter = 0;
  linear_length_counter = 0;
  reload_linear = 0;
}

Code: Select all

void APU::write(uint16 addr, uint8 data) {
  const unsigned n = (addr >> 2) & 1;  //pulse#

  switch(addr) {
  ...
  case 0x4008:
    triangle.halt_length_counter = data & 0x80;
    triangle.linear_length = data & 0x7f;
    break;

  case 0x400a:
    triangle.period = (triangle.period & 0x0700) | (data << 0);
    break;

  case 0x400b:
    triangle.period = (triangle.period & 0x00ff) | (data << 8);

    triangle.reload_linear = true;

    if(enabled_channels & (1 << 2)) {
      triangle.length_counter = length_counter_table[(data >> 3) & 0x1f];
    }
    break;

  case 0x4015:
    ...
    if((data & 0x04) == 0) triangle.length_counter = 0;
    break;
  }
}

void APU::clock_frame_counter() {
  frame.counter++;

  if(frame.counter & 1) {
    pulse[0].clock_length();
    pulse[0].sweep.clock(0);
    pulse[1].clock_length();
    pulse[1].sweep.clock(1);
    triangle.clock_length();
    noise.clock_length();
  }

  pulse[0].envelope.clock();
  pulse[1].envelope.clock();
  triangle.clock_linear_length();
  noise.envelope.clock();

  if(frame.counter == 0) {
    if(frame.mode & 2) frame.divider += FrameCounter::NtscPeriod;
    if(frame.mode == 0) {
      frame.irq_pending = true;
      set_irq_line();
    }
  }
}
User avatar
rainwarrior
Posts: 8735
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Re: APU triangle emulation question

Post by rainwarrior »

The hardware triangle is NOT halted by 0 in the period register.

It's very normal to halt it in emulation because of the reason you mentioned. It's really hard to implement a filter than can cut it out as nicely as your analog hardware does.

You can verify this on hardware by setting the period to 0 and turning the triangle on and off. You'll hear a popping where the the triangle starts and stops.

My choice for NSFPlay was to halt on period 0 by default, but I gave the option to turn it off (even though my oversampling process is not good enough to suppress it entirely, it still comes through as a high pitched aliased ringing).
tepples
Posts: 22708
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Re: APU triangle emulation question

Post by tepples »

One hack that's probably close enough is to output a constant 7 while the period value is less than 2.
lidnariq
Posts: 11432
Joined: Sun Apr 13, 2008 11:12 am

Re: APU triangle emulation question

Post by lidnariq »

As everyone else as said, you've run into the world's worst aliasing problem. In practice, you're probably safest using tepples's suggestion, possible even for periods as low as 0-3, since all of those are ultrasonic for most people.
User avatar
rainwarrior
Posts: 8735
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Re: APU triangle emulation question

Post by rainwarrior »

AFAIK the only one that matters in practice is period 0. I've seen many games that use period 0 instead of halt (e.g. Mega Man 2, Silver Surfer) but I've never seen any try to use other ultrasonic periods for anything. There's no reason for anyone to do so, really.
Near
Founder of higan project
Posts: 1553
Joined: Mon Mar 27, 2006 5:23 pm

Re: APU triangle emulation question

Post by Near »

Alright, thank you very much for the information.

Was hoping for an easy and correct fix =(

Only cheap solutions seem to be to falsely halt on period 0, or to simply decimate the audio 32x prior to resampling.

I suppose I'll keep looking for a more efficient / simplistic resampling algorithm than windowed sinc / band-limited synthesis that can remove the aliasing ...
lidnariq
Posts: 11432
Joined: Sun Apr 13, 2008 11:12 am

Re: APU triangle emulation question

Post by lidnariq »

Tepples's suggestion (just have the DAC output a constant value of 7.5 when ultrasonic) is actually what correct antialiasing would result in, so I'm not certain it's actually a hack. Especially if you keep the phase running for whenever the game brings the pitch back down to audible frequencies.
User avatar
rainwarrior
Posts: 8735
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Re: APU triangle emulation question

Post by rainwarrior »

It's what correct anti-aliasing would do when the triangle state is not changing. It's not correct during any transition, however. As I mentioned, the triangle can still be halted in its high frequency state, and this absolutely makes audible noise on the hardware. This isn't important to emulating any game I know of, however. Possibly Mega Man 2's triangle has a bit more bite if you correctly emulate this, though aesthetically it may be worse off for it.

I prefer to halt the triangle instead of jumping to 7.5 because it very slightly reduces the popping noise of a halt by freezing at current level instead of jumping to 7. It's perhaps a tradeoff, though, since the triangle position also influences the nonlinear mix. Though, the standard nonlinear mix curve we currently use isn't totally accurate to begin with, not to mention the huge variability in hardware mix between the two APU pins, so that kind of subtlety is a bit lost on the NES as a general rule. The games that do this are really just trying to silence the triangle anyway, so it's not a big deal either way. Both methods result in the expected silent triangle, and the difference is quite subtle. (I doubt I could pick them apart in a blind test.)
mic_
Posts: 922
Joined: Thu Oct 05, 2006 6:29 am

Re: APU triangle emulation question

Post by mic_ »

This can be fixed with band-limited resampling or sinc resampling, but those things are rather intensive
Is the resampling performance really that big of a deal? (unless you're targeting really old hardware). I'm using Blargg's Blip_Buffer in a chiptune player I'm writing for mobile phones. As an example, my GBS player uses a total of 8 Blip_Synths (4-channel stereo), each clocked at 1 Mhz, and this runs just fine on my Galaxy S2+.
tepples
Posts: 22708
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Re: APU triangle emulation question

Post by tepples »

As I understand it, the performance of BLEP resampling algorithms like blargg's is O(n) in the number of transitions. Doing it without hacks is fine for square and DMC because they change less often. DMC is limited to how fast writes to $4011 can occur, and square is limited to two transitions per 144 cycles because it shuts off when the period value becomes less than 8. But triangle and noise can have a transition every 1 or 2 cycles. Do GBS files routinely drive the wavetable at period $7FF (inverted vs. NES) and noise at maximum rate?
mic_
Posts: 922
Joined: Thu Oct 05, 2006 6:29 am

Re: APU triangle emulation question

Post by mic_ »

Do GBS files routinely drive the wavetable at period $7FF (inverted vs. NES) and noise at maximum rate?
Probably not. My point was that I have 8 synths (which I know is overkill) and still get performance that is good enough for a mobile phone, and you'd really only need 1 or 2 synths for NES audio (ignoring expansion audio).
User avatar
James
Posts: 431
Joined: Sat Jan 22, 2005 8:51 am
Location: Chicago, IL
Contact:

Re: APU triangle emulation question

Post by James »

byuu wrote:This can be fixed with band-limited resampling or sinc resampling, but those things are rather intensive.
Aren't you famous for your emulators' CPU requirements? Surely, a little more wouldn't hurt :)

I wrote a low CPU utilization SSE filter (512 tap FIR. IIRC, it accounts for about .3% CPU utilization on my 1.86GHz Core 2 Duo laptop). If you (or anyone else) are interested, I'll post the code.
get nemulator
http://nemulator.com
Mednafen
Posts: 60
Joined: Wed Sep 13, 2006 12:45 pm

Re: APU triangle emulation question

Post by Mednafen »

FWIW, you can get a massive performance boost in a bandlimited polyphase resampler if you don't care if the sample rate conversion ratio is slightly off(fewer impulse response phases need to be stored, and you don't have to interpolate between phases anymore).

Like so:

1789772.72 * 7 / 261 = 48001.5672
1789772.72 * 12 / 487 = 44101.1756

...and by using 16-bit samples and impulse response coefficients, and creating four copies of each phase's impulse response with zero-padding on the front(and end for SIMD multiply granularity) to account for possible alignments of the input samples read position, you can utilize MMX to make a decent-quality resampler that'll run significantly faster than realtime even on a Pentium II.
User avatar
James
Posts: 431
Joined: Sat Jan 22, 2005 8:51 am
Location: Chicago, IL
Contact:

Re: APU triangle emulation question

Post by James »

In the current release of nemulator, I decimate the APU output by 40 to get a ~44671Hz sample rate. I then adjust the buffer's playback rate accordingly and let Windows deal with resampling it to the sound card's playback rate. I wish I knew exactly what it was doing under the hood...

Anyway, decimating by 40 (using floats with appropriately sized filter and sample buffers) allows everything to be aligned on 16-byte boundaries, so the SSE implementation is really fast.

I'm currently working on an arbitrary sample rate converter (that doesn't require the OS to perform sample rate conversion). It's working well, but results in a few % additional CPU utilization. I think I'll be able to get it within a couple of points of the current implementation, though.
get nemulator
http://nemulator.com
Near
Founder of higan project
Posts: 1553
Joined: Mon Mar 27, 2006 5:23 pm

Re: APU triangle emulation question

Post by Near »

James wrote:Aren't you famous for your emulators' CPU requirements?
Probably, but I try not to entirely waste CPU cycles. They usually go to emulating fine details. The sinc filter cuts the framerate in half in NES and GB mode. When emulating the SNES+GB at the same time with that filter, it gets pretty bogged down.
Post Reply