Gameboy sound wave channel

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
Gameboy sound wave channel
by on (#46334)
Hi Guys,

First of all it is great to see a gameboy dev forum here as I havent been able to find a decent one on the net. I have a fully working gameboy emulator and now just need to add sound support. I have previously posted on the nesdevemu forum asking for help with square waves because the sound chip on the gameboy and NES are very similar. I am now attempting to implement the wave channel and have come stuck. My main problem with this channel is knowing how to interpret the 32 samples in memory and how to play them. This channel has a frequency like the square wave channels so Im assuming that when enough clock cycles have passed and the frequency has decreased to 0 then you move onto the next sample in memory and reload the frequency and start counting down again (this is similar to how I handle square waves by counting down the frequency and upon 0 I increase the duty cycle and change the polarity if needed).

Does this sound correct so far?

My problem now is how to play the current sample. So if the sample Im playing has the value of 0xA what does this actually mean? You see with square waves I simply multiply the volume by the polarity (either +1 or -1) and total them all up before adding to the playback buffer, but im unsure how to do this with a wave. Also am I correct in assuming the value returned from a wave is +1 and 0 and not +1 and -1 like square waves? If so then how do I mix a wave and a squre wave before adding it to the playback buffer?

Thanks for your time and any help.

by on (#46337)
Have you read the wave section of the Game Boy Sound Hardware wiki page yet?

by on (#46338)
Thanks for the reply Blarrg.

I hadnt read that link before I posted I had only read Lord Nightmares GBSOUND.txt and also NESSOUND.txt to see if I could clarify anything in there.

I have briefly read that wiki page and it seems fantastic, exactly what I needed. Although I will probably post in this thread again when I need something else clarifying I believe that document answers my immediate questions.

Thanks again.

by on (#46339)
I've worked with the GBA sound hardware; that's close enough to that of the Game Boy. You might want to try asking on forum.gbadev.org; there are probably still some people left who haven't moved on completely to the DS and remember what "01 23 45 67 89 AB CD EF FE DC BA 98 76 54 32 10" in wave RAM means. (It's what you write to get the same waveform as the NES triangle wave channel.)

by on (#46373)
Thanks for the reply.

I've had a stab at the wave pattern channel after reading that doc. It isnt sounding right so im unsure if it is a bug in my code or a misunderstanding with how the wave channel works.

This is the algorithm I use for calculating the wave.

Code:

int CalculateWave ( int numCycles )
{
// dac power, master on off and length counter (if length counter is enabled)
if (channelIsNotEnabled)
 return 0;

bool timeToUpdate = frequency <= numCycles ;
frequency -= numCycles ;

if (timeToUpdate)
{
// reset the frequency
int value = (set5 << 8) | set4 ;
value &= 0x7FF ;
frequency = (2048 - value) * 2 ;

// move on to next wave pos
wavePos++ ;
wavePos = wavePos % 32 ;
}

int volumeControl = set3 >> 5;
volumeControl &= 3;

int shift = 0 ;
switch(volumeControl)
{
case 0: shift = 4 ; break ;
case 1: shift = 0 ; break ;
case 2: shift = 1 ; break ;
case 3: shift = 2 ; break ;
}

return GetDACOutput (wave[wavePos] >> shift)
}



This is just pretty much pseudo code of what im doing. Basically it decreases the frequency by the num of clock cycles the last opcode took. If the frequency becomes less than 0 then it reloads the frequency and moves onto the next wave pos.

The input value given to the DAC is the current sample shifted by the volume control.

This is what the GetDACOutput does

Code:
int GetDACOutput(int volume)
{
   static double analog[] = { -1, -0.8667, -0.7334, -0.6, -0.4668, -0.3335, -0.2, -0.067, 0.0664, 0.2, 0.333, 0.4668,0.6,0.7334,0.8667,1  } ;
   return static_cast<int>(8000 * analog[volume] ) ;
}   


The reason why im doing 8000 multiply the analog value is because SDL is using AUDIO_S16SYS which gives a range of approx -32000 to 32000 and as there are 4 channels 8000 will fit in range.

Also just to confirm something with the other square wave channels, if the current polarity is 0 then it always returns a value of 0, correct? However if the value is 1 then it passes its current volume into GetDACOutput which will return -1 to 1?

Thanks for any help

by on (#46379)
tepples wrote:
You might want to try asking on forum.gbadev.org; there are probably still some people left who haven't moved on completely to the DS and remember what "01 23 45 67 89 AB CD EF FE DC BA 98 76 54 32 10" in wave RAM means. (It's what you write to get the same waveform as the NES triangle wave channel.)


That makes sense. The waveform looks like:

Code:
F |               **
E |              *  *
D |             *    *
C |            *      *
B |           *        *
A |          *          *
9 |         *            *
8 |        *              *
7 |       *                *
6 |      *                  *
5 |     *                    *
4 |    *                      *
3 |   *                        *
2 |  *                          *
1 | *                            *
0 |*                              *
  *--------------------------------


And each 4 bits represents an entry. That's definitely a triangle wave. But it seems like it would be slightly off, as it repeats the wave, playing volume 0 twice.

I think this is how the GB's wave engine works, too. I know it uses 4-bit samples with 16 bytes of RAM to do it with.

by on (#46381)
It plays the entry with level 0 twice, and it also plays the entry with level 15 twice. But it's still triangular enough that it sounds like a triangle wave. And each of the 32 steps is flat, which gives the characteristic aliasing at 31f and 33f that defines part of the "NES sound".

by on (#46387)
Cloudy wrote:
This is just pretty much pseudo code of what im doing.

Well, I see problems with it, but I'm not sure it's what your real code does. Do you want to get your pseudo-code working, or the real code?

by on (#46388)
Quote:
And each 4 bits represents an entry. That's definitely a triangle wave. But it seems like it would be slightly off, as it repeats the wave, playing volume 0 twice.

I'm pretty sure the same happens on the NES : The output to the DAC is the low 4 bits each XNORED with the 5th bit of a 5 bit counter. This effectively resulting the 0, 1, ...., 14, 15, 15, 14, ...., 1, 0, 0, 1, .... sequence.

Yet, I guess it doesn't sound the exact same as triangle wave on the NES. Very probably this is due to analog filtering past the DAC, that most emulators/music players emulate (I guess).[/list]

by on (#46391)
Is it the step aliasing as it increments in 16 steps that causes the "NES Sound", or is it the doubling of the numbers at the top and bottom of the wave?

Edit:
I just generated two wave files, one using 16 step aliasing, and one using 17 step aliasing (so that the second half goes 16...1 instead of 15...0). Couldn't tell them apart very well listening to them side by side.

by on (#46394)
Thanks for the reply Blarrg.

I have edited my above code to show exactly what I am doing in my real code. The only difference is that my code belongs inside a wave channel class and the CalculateWave is a member of that class a long with the variables "frequency", "wavePos", "wave" and the sets "set3/4/5". I believe that link to the sound development you posted indexes the channel registers (sets) from 0 where I index them from 1 (this is because this is how lord nightmare indexed them and I read his document first).

The boolean "channelNotEnabled" is set to true if the DAC power is off, or the master control is off or the length counter is 0 (including if the length counter is enabled).

So if you can spot any problems im sure they are problems in my actual code.

My second question is when square waves output a polarity of 1 then i feed this to the DAC along with its volume in the same manner as I do with the wave channel to get its analog value. However if the square wave outputs a polarity of 0 then do i just return 0 instead of going though the DAC?

Thanks for any help.

Edit:

I should probably clarify that my actual wave memory is 32 bytes not 16. This is because I find it easier to treat the 2 samples encoded into one byte as two different indexes in the wave memory.

by on (#46409)
How are you spreading the 16 wave RAM bytes into your 32-entry wave table? What happens when numCycles is great enough that more than one wave step should occur? Seems you need a while loop. Otherwise, the code looks correct.

by on (#46528)
Thanks for the reply. I havent been able to reply over the weekend as I have no internet access. I managed to get some progress over the weekend and now you can clearly hear the correct melody being played in all games which is very encouraging. I feel it is still a long way off being finished though because it sounds awful. Interestingly if I disable all but one of the 4 channels and play it then im convinced I can hear the same channel being played on the real gameboy. So it is like individually they are playing correct but when combined although you can hear each channel is still playing correctly it sounds awful. This is probably because I still have the mixer to emulate. I havent done this yet as I dont know how to use SDL to play a left and right sound buffer. I'll have to look into that.

I wish there was an emulator available that allows you to turn on and off the different channels so you can hear what they individually sound like. I guess that'd really help me see where I am going wrong lol.

Thanks again.

by on (#46537)
Cloudy wrote:
I wish there was an emulator available that allows you to turn on and off the different channels so you can hear what they individually sound like.

VisualBoyAdvance.

by on (#46541)
Thanks for that. My sound channels seems fine but raspy in comparison. Im almost there I can feel it :-)

by on (#46559)
Raspy = you need to use some kind of interpolation (especially with the wave channel, as it has lots of super-sonic harmonics). Start with linear; simplest to implement, most bang for-the-buck.

by on (#46578)
Do you mean linearly interpolate the playback buffer when the sound api is requesting it?

So for example the SDL Api periodically requests a copy of the playback buffer for it to play. Is this when I perform the linear interpolation on the buffer?

When you say perform linear interpolation I assume you mean pick two indexes in the playback buffer and then interpolate those values between. I.E. if you picked every 5 indexes then the first interpolation would calculate the values in the playback buffer between indexes 0 and 4. And then between 4 and 9 etc.

Thanks again for your help mate. In a matter of days I've gone from knowing nothing about sound emulation to getting results, which is thanks to yourself and tepples.

by on (#46579)
He's talking about how you generate the square wave. Not talking about linear interpolation as in blur filters you see in image editors.

A square wave goes up and down repeatedly.

For example, let's say we have a 50% duty cycle square wave at Middle C.
440*2^(-9/12) Hz (~261.625 Hz)

We want to emit a wave at 44100 Hz.

Each stretch where it stays up or down is about 84.28 samples long. (44100 / 261.625 / 2)

There are some really bad ways to do sound:
* Round 84.28 to the nearest integer and use that as the period instead. This changes the frequency. Don't do this.
* Emit 84 samples, have .28 leftover, emit 84 more samples, have .56 leftover, emit 84 samples, have .84 leftover, emit 85 samples, have .12 leftover. This sounds really bad, the period of the wave is not consistent.

Then there's an easy way to get decent sound:
* Emit 84 samples, use the 0.28/1.0 fraction to determine what fractional amplitude to output next. Then emit 84 more samples, and use the 0.56/1.0 fraction to determine what to output next. This is the linear interpolation he was talking about. You output fraction * OLD + (1.0-fraction) * NEW every time you would need to output a fractional length sample.

by on (#46612)
Dwedit wrote:
He's talking about how you generate the square wave. Not talking about linear interpolation as in blur filters you see in image editors.

Actually, just what you see in image editors when scaling an image down a lot (though they tend to use bicubic rather than bilinear). In this case, the Game Boy sound hardware can generate sound at around a 4 MHz sampling rate, while the sound card probably only plays it at around 0.048 Mhz (48000 Hz). So if you want to graphically see what linear interpolation of 1/60 a second would be like, make a grayscale image that's 1 pixel high and 69905 pixels wide, fill it with some rectangles that are each fairly wide, then resize it horizontally to 800 pixels.

That's the ultimate goal. But as Dwedit describes, you don't have to literally do such a laborous resize, examining every input sample, as long as you generate the same final result. Since the input isn't a waveform which constantly changes every 1 four millionth of a sample (usually), other approaches are more efficient. A simple first approach is to generate samples at a lower rate that's still around 4 times that of the output rate, then do the above "rescaling" to get the final rate. You'll deal with about 22 times less data than dealing with the full rate.