Channel 4 sounding louder than expected

Discussion of programming and development for the original Game Boy and Game Boy Color.
Post Reply
MegaBoyEXE
Posts: 12
Joined: Tue Jul 04, 2017 9:22 am

Channel 4 sounding louder than expected

Post by MegaBoyEXE »

Hello, I've got the sound emulation working and all channels sounds good. I know I'm missing a lot of the "obscure behaviors" from the guides, but it works better than I expected.

Problem is: Channel 4 is generating a "buzz" effect that sounds louder than it should be. The effect of the "ocean waves" in the Zelda: Link's Awakening intro, or almost all songs from Mega Man III are good examples.
The noise is more noticeable than the other channels on Mega Man III, which is pretty annoying. Other channels sounds good.
Also, I noticed the Zelda logo "tliiiin" hiss that plays when it appears in the intro is not reached (looks like it's tone is higher than what I can sample), so it sounds just like a normal buzz. Other sounds effects plays correctly.

I followed the GBSOUND and Sound Hardware wiki page, maybe I'm doing something really wrong.

I was sampling before at 44100 Hz, but increased to 96000 Hz and sound quality improved, but both works.
I steps cycles in multiples of 4, inside a loop until all elapsed cycles are consumed (based on the last instruction cycles count - my emu is cycle count accurate only).
The periods for sampling are:

Code: Select all

44100 Hz:  4194304 / (44100 / 2); // gives ~735 samples
96000 Hz: 4194304 / (96000 / 2); // gives ~1606 samples
I fill a buffer with that number of samples, and audio request comes from another thread.
When called, I copy the buffer to audio buffer and clear my buffer for next frame.
The emulation is not synced to audio, so there's some skip (small sound gaps, sometimes very rarely)

All channels sounds good with that configuration, but channel 4 has those issues I mentioned earlier.

Code for channel 4:

Code: Select all

public void FreqTimerStep( )
{
    // for each 4 cycles, this method is called from APU

    m_timer -= 4;

    if (m_timer <= 0)
    {
        int result = (m_linearShiftReg & 0x1) ^ ((m_linearShiftReg >> 1) & 0x1);
        m_linearShiftReg >>= 1;
        m_linearShiftReg |= result << 14;
        if (m_stepsMode == 1)
        {
            m_linearShiftReg &= ~0x40; // clear bit 6
            m_linearShiftReg |= result << 6;
        }

        // Reload Frequency
        m_timer += CalcFrequency();
    }
}


int CalcFrequency( )
{
    int freq = 8;

    switch (m_divRatio)
    {
        case 0:
            freq = 8 << m_shiftFreq;
            break;

        case 1:
            freq = 16 << m_shiftFreq;
            break;

        case 2:
            freq = 32 << m_shiftFreq;
            break;

        case 3:
            freq = 48 << m_shiftFreq;
            break;

        case 4:
            freq = 64 << m_shiftFreq;
            break;

        case 5:
            freq = 80 << m_shiftFreq;
            break;

        case 6:
            freq = 96 << m_shiftFreq;
            break;

        case 7:
            freq = 112 << m_shiftFreq;
            break;
    }

    return freq;
}
I will not post the code for volume envelope and length control, as it's the same for all channels, so it should be good (missing some APU quirks, but in normal conditions, it works).

The sampling code is

Code: Select all

// Output is bit 0 inverted

if ((m_linearShiftReg & 0x1) == 0)
{
    return 15 & m_curVolume;
}

return 0;
Can someone spot anything wrong that would cause the issues I mentioned?
Maybe I need some kind of audio filter for this channel?

Edit: if needed, full code is at https://github.com/fattard/xFF/blob/gb/ ... hannel4.cs
tepples
Posts: 22705
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Re: Channel 4 sounding louder than expected

Post by tepples »

Noise contains frequencies above 22 kHz that fold over the audio if you resample the audio naively. This makes noise sound louder than expected unless you apply a low-pass filter to remove the aliasing. The simplest such filter is a box filter, averaging together the 4194304/44100 = 95 input samples that make up one output sample. Though imperfect, this should still provide a noticeable improvement over just choosing one out of the 95 samples. Sound quality improved at 96 kHz because a lot of the aliasing folded into the 22-48 kHz band, which the speaker and your ear filter out.

When downsampling the output of a "chiptune" style synthesizer that makes discrete transitions from one level to another, you can decompose the sum of the four channels into timestamped differences between one level and the next and then resynthesize the audio using band-limited step responses (BLEPs). Blargg has published a library called Blip_Buffer to do this, using a bank of linear phase (time-symmetric) BLEPs. Others prefer minimum-phase BLEPs because forward masking is stronger than backward.
MegaBoyEXE
Posts: 12
Joined: Tue Jul 04, 2017 9:22 am

Re: Channel 4 sounding louder than expected

Post by MegaBoyEXE »

So, it really needs a filter. I was not expecting entering at this topic yet, as it was my first time doing procedural audio programming. I'm still in learn process about everything related to sound programming.

I tried applying this "filter" only to channel 4, as others sounds good already. Tried average between the last 4 to 20 samples and indeed I could hear to "tliiin" sound now on Zelda logo appearing, as well as some other effects that were only buzzes.
Most of the effects did got a lower volume level this time, but the buzz sound is looking a little more low quality.

For example, let's say I want average of 10 samples: when channel period ticks, I'm storing the sample in a queue with the last 10 samples. Each time a new sample is generated, I dispose the oldest one in the queue, keeping the last 9 plus the new one.
When the sampling time is reached, I'm getting the average of the 10 samples and sending to audio buffer, instead of getting the direct sample from the LFSR.

I'm probably doing everything wrong :roll: , but I did at least understood the problem.

I will experiment more and read the content you provided. I think I still lack the enough knowledge to understand all those concepts, but I will surely go after them.

Thank you for your time and explanations!
lidnariq
Posts: 11429
Joined: Sun Apr 13, 2008 11:12 am

Re: Channel 4 sounding louder than expected

Post by lidnariq »

tepples wrote:The simplest such filter is a box filter, averaging together the 4194304/44100 = 95 input samples that make up one output sample.
Also one of the computationally cheapest options, requiring only 94 additions and one division per output sample, while providing filtering comparable to a 1st order lowpass with a corner frequency of 1/95th of the original sample rate.

Filter design is something you can spend endless hours on.
MegaBoyEXE wrote:Tried average between the last 4 to 20 samples and indeed I could hear to "tliiin" sound now on Zelda logo appearing, as well as some other effects that were only buzzes.
Most of the effects did got a lower volume level this time, but the buzz sound is looking a little more low quality.
You're decimating before you filter. That doesn't work: that has already aliased the high frequencies down in, and by filtering after the fact you just make the channel sound muddy always.
MegaBoyEXE
Posts: 12
Joined: Tue Jul 04, 2017 9:22 am

Re: Channel 4 sounding louder than expected

Post by MegaBoyEXE »

lidnariq wrote: You're decimating before you filter. That doesn't work: that has already aliased the high frequencies down in, and by filtering after the fact you just make the channel sound muddy always.
Oh, ok maybe I did not fully understood the concept. Let me try to explain with some pseudo-code.

Here's the APU cycles step, where I tick each channel timer, and fill my samples buffer, when time to sample is reached:

Code: Select all

void CyclesStep(int aElapsedCycles) // multiple of 4, based on last instruction
{
    while (aElapsedCycles > 0)
    {
        aElapsedCycles -= 4;

        // do FrameSequencer steps (length clock check, sweep clock check, volume envelope clock check)
        .....

        // Tick all channels
        channel1.FreqTimerStep();
        channel2.FreqTimerStep();
        channel3.FreqTimerStep();
        channel4.FreqTimerStep(); // <--- channel4 Tick

        // Check if it's sampling time:  
        m_timeToGenerateSample += 4;
        if (m_timeToGenerateSample > DESIRED_TIME)  // DESIRED_TIME is: 4194304 / (96000 / 2)  ->  at each 87 cycles, generate a sample
        {
            m_timeToGenerateSample -= DESIRED_TIME; // resets counter
            
            int sampleL = channel1.SampleL() + channel2.SampleL() + channel3.SampleL() + channel4.SampleL();
            int sampleR = channel1.SampleR() + channel2.SampleR() + channel3.SampleR() + channel4.SampleR();
            
            // Samples buffer has capacity to up to 8192 entries, stores stereo interleaved
            m_samplesBuffer[m_bufferPos * 2] = sampleL;
            m_samplesBuffer[m_bufferPos * 2 + 1] = sampleR;
            
            // Circular buffer
            m_bufferPos = (m_bufferPos + 2) % (m_samplesBuffer.Length / 2);
        }
    }
}
The audio thread call comes from another thread, at a fixed rate:

Code: Select all

public void OnOutputSoundRequested(ref float[] b)
{
    // At 96000 Hz this is the number of samples (stereo) generated per frame
    // and audio lib buffer was configured to this exact size
    int numSamples = 1606;

    for (int i = 0; i < numSamples; ++i)
    {
        b[i] = Gain * (sbyte)(m_audioBuffer[i]) / 127f;
    }

    // Resets buffer pos
    m_bufferPos = 0;
}
Now, this is how I'm trying the Channel 4 with "filter":

Code: Select all

public void FreqTimerStep( )
{
    m_timer -= 4;

    if (m_timer <= 0)
    {
        int result = (m_linearShiftReg & 0x1) ^ ((m_linearShiftReg >> 1) & 0x1);
        m_linearShiftReg >>= 1;
        m_linearShiftReg |= result << 14;
        if (m_stepsMode == 1)
        {
            m_linearShiftReg &= ~0x40; // clear bit 6
            m_linearShiftReg |= result << 6;
        }

        // Stores sample in queue
        GenerateSampleToFilterL();
        GenerateSampleToFilterR();

        // Reload Frequency
        m_timer += CalcFrequency();
    }
}


void GenerateSampleToFilterR( ) // GenerateSampleToFilterL() is identical using L stuffs
{
    // Discard oldest
    m_samplesToFilterR.Dequeue();
    
    if (!IsSoundOn || !ChannelEnabled || !RightOutputEnabled || !UserEnabled)
    {
        m_samplesToFilterR.Enqueue(0);
        return;
    }
    
    // Output is bit 0 inverted

    if ((m_linearShiftReg & 0x1) == 0)
    {
        m_samplesToFilterR.Enqueue(15 & m_curVolume);
        return;
    }

    m_samplesToFilterR.Enqueue(0);
    return;
}

And, finally the code when sampling was called

Code: Select all


public int SampleR() // SampleL is identical with L stuffs
{
    return ApplyFilter(m_samplesToFilterR);
}

int ApplyFilter(CircularBuffer<int> aSamplesToFilter)
{
    int total = 0;

    // Tried sizes of 4 to 20, using 8 currently

    for (int i = 0; i < aSamplesToFilter.Count; ++i)
    {
        total += aSamplesToFilter[i];
    }

    return total / aSamplesToFilter.Count;
}
So, the final sample that goes to audio buffer is the average of the 8 stored samples.

How wrong did I get the idea? :P

I think the correct would use the filter at the APU, and getting the average of the last 87 samples, right?
This means, filtering all channels, or can I apply ONLY to channel 4?
lidnariq
Posts: 11429
Joined: Sun Apr 13, 2008 11:12 am

Re: Channel 4 sounding louder than expected

Post by lidnariq »

MegaBoyEXE wrote: if (m_timeToGenerateSample > DESIRED_TIME) // DESIRED_TIME is: 4194304 / (96000 / 2) -> at each 87 cycles, generate a sample
You're natively synthesizing audio at 96kHz here; you're generating the aliases at the same time.

The DMG noise channel actually generates audio at 524kHz.

What we're saying is:
* If you generate audio samples at 524kHz
* and you use a moving average
* then you can generate audio at 96kHz and get a result that's not too inauthentic.

But when you generate audio at 96kHz, and use a 10-sample moving average, then you're actually adding a lowpass filter at approximately 10kHz. And any higher-frequency audio components are already folded down on top of the audio band. It won't matter too much when channel 4 is in white noise mode (15-bit) because white noise is supposed to have equal energy at every frequency, but it will distinctly be audibly wrong in tonal mode (7-bit).
MegaBoyEXE
Posts: 12
Joined: Tue Jul 04, 2017 9:22 am

Re: Channel 4 sounding louder than expected

Post by MegaBoyEXE »

I understood now! Instead of sampling a specific point in the 524 KHz wave (when downsampling to 96 KHz), I need to sample a bigger interval of it, which I will get the average as a sample.

I just changed my code, and it worked pretty well! I'm sorry I did not get this earlier. It's pretty clear now what I'm actually doing!
Thank you very much for all the explanations!!! :beer:



The Channel 4 code was restored to original, as the OP, there's nothing to change there.

The actual change needs to be in the APU. So, here it is:

Code: Select all


void CyclesStep(int aElapsedCycles) // multiple of 4, based on last instruction
{
    while (aElapsedCycles > 0)
    {
        aElapsedCycles -= 4;

        // do FrameSequencer steps (length clock check, sweep clock check, volume envelope clock check)
        .....

        // Tick all channels
        channel1.FreqTimerStep();
        channel2.FreqTimerStep();
        channel3.FreqTimerStep();
        channel4.FreqTimerStep(); // <--- channel4 Tick
        

        // Generates sample for channel 4, to be filtered
        {
            m_samplesToFilterL.Dequeue(); // get rid of oldest sample
            m_samplesToFilterL.Enqueue(m_channel4.SampleL()); // add sample in APU frequency

            m_samplesToFilterR.Dequeue(); // get rid of oldest sample
            m_samplesToFilterR.Enqueue(m_channel4.SampleR()); // add sample in APU frequency
        }
        
        

        // Check if it's sampling time: 
        m_timeToGenerateSample += 4;
        if (m_timeToGenerateSample > DESIRED_TIME)  // DESIRED_TIME is: 4194304 / (96000 / 2)  ->  at each 87 cycles, generate a sample
        {
            m_timeToGenerateSample -= DESIRED_TIME; // resets counter
           
            // Add channels 1, 2 and 3 as before
            int sampleL = channel1.SampleL() + channel2.SampleL() + channel3.SampleL();
            int sampleR = channel1.SampleR() + channel2.SampleR() + channel3.SampleR();
            
            // Now include the filtered channel4 samples
            sampleL += ApplyFilter(m_samplesToFilterL);
            sampleR += ApplyFilter(m_samplesToFilterR);
           
            // Samples buffer has capacity to up to 8192 entries, stores stereo interleaved
            m_samplesBuffer[m_bufferPos * 2] = sampleL;
            m_samplesBuffer[m_bufferPos * 2 + 1] = sampleR;
           
            // Circular buffer
            m_bufferPos = (m_bufferPos + 2) % (m_samplesBuffer.Length / 2);
        }
    }
}


int ApplyFilter(CircularBuffer<int> aSamplesToFilter)
{
    int total = 0;
    
    // The size of aSamplesToFilter is: 87 cycles / 2 -> 43

    for (int i = 0; i < aSamplesToFilter.Count; ++i)
    {
        total += aSamplesToFilter[i];
    }

    return total / aSamplesToFilter.Count;
}
As I'm sampling at each 87 cycles, I got an optimal sample range of the last 43 samples ( 87/2 ) as the interval.
The effect of the filter did lowered the overall volume of noise channel to good levels, and also allowed the highest frequency samples to be included and played.

Now I have 2 questions:

1- I'm sampling the channel 4 EVERY tick of the APU. Should I been doing this only when the channel's period is terminal, as the real hardware would be outputing?
2- Would be better if I include all other channels in the this filter too, or it does not really matter?

Thank you for your help!
lidnariq
Posts: 11429
Joined: Sun Apr 13, 2008 11:12 am

Re: Channel 4 sounding louder than expected

Post by lidnariq »

MegaBoyEXE wrote:1- I'm sampling the channel 4 EVERY tick of the APU. Should I been doing this only when the channel's period is terminal, as the real hardware would be outputing?
I think that's basically the same operation, just being done in one order or the other. It doesn't much matter whether you have the noise channel audio at 524kHz, which you then average and downsample, or whether you calculate when within each output sample it would have transitioned and use a weighted average instead. (The latter puts you in a good place to graduate to using bandlimited synthesis, as Tepples was talking about, but you may not care)
2- Would be better if I include all other channels in the this filter too, or it does not really matter?
All of the channels can produce readily audible aliases: channels 1 and 2 both generate waveforms at up to 1049kHz, and channel 3's sample rate can be as high as 2097kHz (right? the DMG doesn't have the same mute functionality on short periods that the NES does, right?)

Whether the things you've found will produce audible aliases is a good question.

¹ok, ridiculous question. Has anyone ever tried using the extremely high pitches out of the DMG to transmit in the AM band?
Post Reply