apu dac conversion

Discuss emulation of the Nintendo Entertainment System and Famicom.

Moderator: Moderators

Post Reply
User avatar
Anes
Posts: 702
Joined: Tue Dec 21, 2004 8:35 pm
Location: Mendoza, Argentina

apu dac conversion

Post by Anes »

im trying to emulate the apu, well only square 1 by now. And i have a few questions. i dont know how exactly works a 4 bit DAC, and i dont know how to convert 4 bit dac input to windows PCM.
ANes
User avatar
Disch
Posts: 1848
Joined: Wed Nov 10, 2004 6:47 pm

Post by Disch »

To my knowledge the DAC is what converts the digital data to an actual analog sound wave. PCs themselves have DACs that do the same thing... so you don't have to worry about this conversion.

In short... whatever gets passed to the DAC... that's what you output when outputting wavs in Windows.
User avatar
Anes
Posts: 702
Joined: Tue Dec 21, 2004 8:35 pm
Location: Mendoza, Argentina

dac

Post by Anes »

im still confused, maybe it could be that the code of my apu is not right. Im using the api "waveOutWrite" 8 bit mono and the value that send it to play is the actual value "recived" in NES dac. I run smario and it seem the music "rithm" is ok, but the tones aren't.

What could it be? :?
ANes
User avatar
Disch
Posts: 1848
Joined: Wed Nov 10, 2004 6:47 pm

Post by Disch »

waveOut will probably give you a higher latency than you'd like... trying to go low latency will result in crackling and pops. Higher latency will sound like the sound is delayed -- like for example, when mario jumps, you wouldn't hear the sound effect until some time later. But for testing purposes I suppose it's good (it's certainly easier to work with than DirectSound)

Anyway... are you downsampling properly? The NES outputs ~1789772 samples a second... whereas PCs typically only do 44100 (assuming 44KHz output). You essentially have to combine the output for several cycles into one sample.

The simplest way to do this (but very very low quality way) is to only take the output every X cycles:

X = CPU_CLOCK / SAMPLERATE

On NTSC, the CPU_CLOCK is 1789772.7272
If you're doing 44100 samples per second... SAMPLERATE is 44100
So in this situation... you'd only take output once every ~40.58 CPU (or pAPU) cycles.

A higher quality way is to combine all the output for ~40.58 cycles and output the average (aka linear interpolation). This is also easy to do and makes a much higher quality sound.

Even better sounds can come from other methods... like applying a FIR filter, or using the Band-limited synth techniques blargg covers on his page ( http://www.slack.net/~ant/bl-synth/ )
User avatar
Anes
Posts: 702
Joined: Tue Dec 21, 2004 8:35 pm
Location: Mendoza, Argentina

apu

Post by Anes »

thanks man, that helped me a lot
ANes
User avatar
Anes
Posts: 702
Joined: Tue Dec 21, 2004 8:35 pm
Location: Mendoza, Argentina

not accurate

Post by Anes »

my apu its still not accurate. maybe i missunderstad what you explain me.
I re-writed complety the code to emulate squareone, well its was very simple in the emulation loop i call "CheckAPUReg" to check if there is a write to $4000, then i AND the value with 1111 (bits), i get the evelope, and if its equal to "0" i load a global envelope variable with 080H (middle point of PCM, that means NO sound). If envelope is != 0 i load the enveloope variable with the current envelope value (i know maybe this value is not correct, since the envelope not only depends of $4000).

On the same emulate loop func. i call "PlaySquareOneDac", which plays what ever its in the global Envelope variable. So "CheckApuReg" and "PlaySquareOneDac" are continously called.

Doing this again i can hear the "rithm" ok, but not the "tones". I have been trying diferent methods, like take care of play every 40 cpu cycles, nearly the value that you calculated, buffering, etc. and still cant hear some nearly accurate sound

Well, i want to mean with all this that im complety lost.
If you can help me to "order" on how to emulate the APU, althought Square One, it will appreciate it.

Thanks in advance
ANes
User avatar
Disch
Posts: 1848
Joined: Wed Nov 10, 2004 6:47 pm

Post by Disch »

Simplest way I can explain:

Keep vars for a CPU timestamp and an APU timestamp. To keep things simple... let's say your CPU timestamp counts in CPU cycles (if the timestamp is 0, after you run LDA #$xx -- a 2 cycle instruction, your timestamp would be 2)

On writes to APU registers... Call a 'RunAPU()' function or something of the sort... which catches the APU up to the current CPU timestamp. Example: your APU timestamp is 10, your CPU timestamp is 100, and the game just wrote to $4000... so you'd run the APU for 90 cycles. Once the APU is caught up... apply the changes that the write performed.

In RunAPU, you'd emulate all your sound channels and actually produce your sound. Assuming you're using that simplest, low quality method I described earlier... RunAPU might look something like this:

Code: Select all

void RunAPU()
{
  int minticks;
  int doticks;
  int soundout;

  doticks = nCPUTimestamp - nAPUTimestamp;
  nAPUTimestamp = nCPUTimestamp;

  while(doticks > 0)
  {
    minticks = ceil( fTicksUntilNextSample );
    if(minticks > doticks)
      minticks = doticks;

    doticks -= minticks;

    ClockSquare1( minticks );

    fTicksUntilNextSample -= minticks;
    if( fTicksUntilNextSample <= 0 )
    {
      fTicksUntilNextSample += fTicksPerSample;

      soundout = 0;
      soundout += GetSquare1Output();

      //clip at 8-bits
      if(soundout < -128)  soundout = -128;
      if(soundout >  127)  soundout =  127;

      //convert to 8-bit unsigned (instead of signed)
      soundout ^= 0x80;

      OutputSample( soundout );
    }
  }
}
fTicksPerSample would be that CPU_CLOCK / SAMPLERATE value (~40.58 @ 44100 Hz). fTicksUntilNextSample is a counter which, as the name implies, tracks how many cycles need to pass before you output another sample. For this example to work decently these would probably have to be floating point to avoid roundoff (you don't want to round off to 40 or 41... since that might bend the pitch of the sound). There are ways to do things without using any floating point vars (which would provide a better performance)... but the concept is easiest to show this way.

ClockSquare1() would be the function that does your emulation for Square 1. Like... clocking the Programmable Timer and updating the Duty Cycle and all that jazz. The actual output of the channel is represented here by GetSquare1Output()... which would return the output. OutputSample() would be where you'd buffer the generated sample (and send to waveOut or whatever).

Note this method isn't really optimized but it should provide you with a concept to help you get things working. Also note that this example leaves out the APU frame thingamajig (which clocks the Sweep Units, Length Counters, Decay Units, etc)... but I wouldn't worry about that stuff until after you get the main sound working.

Also... the ~40.58 cycle thing is only if you're outputting at 44100 Hz. If you try this and the sound is still way offkey... doublecheck your samplerate and make sure it's 44100 Hz.
Post Reply