[PC Engine] PSG (sound) emulation

Discussion of development of software for any "obsolete" computer or video game system. See the WSdev wiki and ObscureDev wiki for more information on certain platforms.
Post Reply
Near
Founder of higan project
Posts: 1553
Joined: Mon Mar 27, 2006 5:23 pm

[PC Engine] PSG (sound) emulation

Post by Near »

Having quite a bit of trouble getting good, clean sound out of my PSG emulation.
All of the documentation I can find on the chip is extremely barebones and high level.
The best public source of info I know of is: http://www.magicengine.com/mkit/doc_hard_psg.html
pcetext.txt barely covers that the PSG even exists.

My current emulation source code is here: https://hastebin.com/raw/fagecaxano

(Note that I'm emulating sound output at the full ~3.57MHz, and then using a sixth-order biquad IIR butterworth filter to remove aliasing prior to using hermite to resample the audio to the native PC sound card rate.)

But I'm stuck on lots of points. Is anyone here knowledgable with this chip that could lend a hand?

Right now, I support waveform mode, direct D/A mode, and noise generation mode. However for noise, I'm not actually generating a proper square wave, and I don't know the PRNG algorithm used for choosing the random values. So for now, I'm just feeding in random values to it.

I'm also not sure about the noise mode's frequency counter. Magic Kit is implying it's 64*~frequency, but that results in an 11-bit period. It seems only logical that we'd want a 12-bit period. So my guess is that it's actually 12-bit, and halfway through it alternates between two randomly generated values every 32 samples, and the two values are generated every time the period hits zero.

Next up, it's not clear when the period counter is reloaded, either for the waveform or the noise mode. So for now, when enabling the channel, I reload the waveform period. And when enabling noise mode, I reload the noise period. I don't know if you need to do it when writing to the frequency registers or not.

Next, it's not clear whether the period is a decrement-and-compare, or a compare-and-decrement, and whether we reload with frequency, frequency-1, or frequency+1. There's this cryptic note in pcetext.txt:

Code: Select all

 - The PSG channel frequency is 12 bits, $001 is the highest frequency,
   $FFF is the next to lowest frequency, and $000 is the lowest frequency.
As best I can tell, he's trying to say that it's decrement-and-compare.

Whatever the case, there's periodic popping noises every few seconds. I thought it might be because this is the first system with a fractional sampling rate (~3.57MHz), but rounding the frequency to a whole number doesn't help at all, and emulator/audio should be able to handle fractional resampling rates anyway.

The popping noises could also be due to PSG writes being cycle-timed, and my HuC6280 cycle timings not being very great yet. The PSG has no kind of interrupts, so I think careful timing is the only way to do certain things, especially D/A mode.

Next up, I really don't understand the frequency modulation mode at all. It also has a frequency value that we'll need to understand how the period works and reloads. Basic idea though is the channel 1 output turns into a value to modulate channel 0's frequency by, and channel 1's output gets muted. But how does one turn that idea into code? All kinds of subtleties abound.

Next up, I don't know how the volume controls work at all. There's a master volume left+right, per-channel volume left+right, and per-channel overall volume. The documentation lists their effects in terms of decibels. I have no clue how to turn decibels into multiply-by values. Let alone how to stack THREE levels of audio volume controls >_>

Next, it looks like the output is always 5-bit unsigned per-channel, but there's also all the volume adjustments. So I don't know the final bit-depth of the final output to normalize the value into a signed floating point value between -1.0 and +1.0. So for now, half the potential speaker range (anything below zero) isn't used in the generated output.
ccovell
Posts: 1045
Joined: Sun Mar 19, 2006 9:44 pm
Location: Japan
Contact:

Re: [PC Engine] PSG (sound) emulation

Post by ccovell »

calling Tomaitheous...

Here's one link, sorry it's all in Japanese: http://www.geocities.jp/team_zero_three/PCE/psg.html

Do not reload the WF period when changing frequencies -- that's an NES bug, not PCE :) ). I'm not sure, but I believe you reload the WF period only when bit 6 of $0804 is set to 1.

The waveform data is 5-bit linear PCM, but all the volume & panning controls are logarithmic, so they all add together (in binary) rather than being multiplied, and actually saturate at some bit depth -- I don't remember the fine details, sorry.

As for the LFO, IIRC, looking at channel 1 waveform, a waveform value of $10 means no change, and $11 means increase channel 0's frequency "register" by 1 / 16 / 256 depending on the multiplier setting. A waveform value of $0f decreases by 1 / 16, etc.

For noise, apparently $00-$1E are valid values, but $1F is "undefined". In practice $1F is a higher frequency than $1E of course, but a tad quieter. Who knows how that translates to code.

I would normally recommend real hardware for testing, but Mednafen is quite accurate for most sound emulation -- the LFO seems a bit off, perhaps due to sampling/emulation rate limits.
Near
Founder of higan project
Posts: 1553
Joined: Mon Mar 27, 2006 5:23 pm

Re: [PC Engine] PSG (sound) emulation

Post by Near »

> Here's one link, sorry it's all in Japanese

Quite alright. My Japanese isn't the best, but I can manage.

Thanks a bunch for the link!

> Do not reload the WF period when changing frequencies -- that's an NES bug, not PCE

Really? Mednafen reloads on both.

Code: Select all

        case 0x02: /* Channel frequency (LSB) */
	    if(select > 5) return; // no more than 6 channels, silly game.
            ch->frequency = (ch->frequency & 0x0F00) | V;
	    RecalcFreqCache(select);
	    RecalcUOFunc(select);
            break;
        case 0x03: /* Channel frequency (MSB) */
	    if(select > 5) return; // no more than 6 channels, silly game.
            ch->frequency = (ch->frequency & 0x00FF) | ((V & 0x0F) << 8);
	    RecalcFreqCache(select);
	    RecalcUOFunc(select);
            break;
        case 0x04: /* Channel enable, DDA, volume */
	    if(select > 5) return; // no more than 6 channels, silly game.
            if((ch->control & 0x40) && !(V & 0x40)) {
	     ch->waveform_index = 0;
             ch->dda = ch->waveform[ch->waveform_index];
	     ch->counter = ch->freq_cache;
	    }
	    if(!(ch->control & 0x80) && (V & 0x80))  {
	     if(!(V & 0x40))  {
	      ch->waveform_index = (ch->waveform_index + 1) & 0x1F;
	      ch->dda = ch->waveform[ch->waveform_index];
	     }
	    }
            ch->control = V;
	    RecalcFreqCache(select);
	    RecalcUOFunc(select);
	    vol_pending = true;
            break;
If you're sure you have it right, I'll take your word for it, of course.

> I'm not sure, but I believe you reload the WF period only when bit 6 of $0804 is set to 1.

0->1 transition or any 1 write?

> so they all add together (in binary) rather than being multiplied

Wow ... I figured it just meant the volume scale wasn't linear, not that we moved to addition. That's very interesting.

> I would normally recommend real hardware for testing, but Mednafen is quite accurate for most sound emulation -- the LFO seems a bit off, perhaps due to sampling/emulation rate limits.

The difficulty with Mednafen is that it's heavily optimized for speed, tries to do bulk processing (many cycles at once), and is a fair bit beyond my understanding. I was able to glean a couple new pieces of information though, that I'll try to apply to my codebase.
ccovell
Posts: 1045
Joined: Sun Mar 19, 2006 9:44 pm
Location: Japan
Contact:

Re: [PC Engine] PSG (sound) emulation

Post by ccovell »

byuu wrote: > Do not reload the WF period when changing frequencies -- that's an NES bug, not PCE

Really? Mednafen reloads on both.

If you're sure you have it right, I'll take your word for it, of course.
Oh, I thought you meant restart the waveform read index, as in:

Code: Select all

ch->waveform_index = 0;
which is done only with writes to $0804.
byuu wrote: > I'm not sure, but I believe you reload the WF period only when bit 6 of $0804 is set to 1.

0->1 transition or any 1 write?
I do not know for certain, but it looks like any write to $0804 when bit 6 is set, according to Mednafen's code.
Near
Founder of higan project
Posts: 1553
Joined: Mon Mar 27, 2006 5:23 pm

Re: [PC Engine] PSG (sound) emulation

Post by Near »

... oh wow.

The sound emulation popping was because my FreeBSD audio latency settings were too aggressive.

I'm not sure why it became a problem only for the PC Engine core. Even though the PCE has the highest sampling rate of any system I emulate, the resampler reduces it to the standard sound card rate internally.

In that case, I have three issues left to solve:

1. how do I perform volume adjustments? I know it's logarithmic, but the formulas I've seen in other emulators are just nuts.

2. how do I perform frequency modulation? I'm not 100% sure how this is supposed to work, nor am I even aware of any good test games to compare my implementation against.

3. how do I properly emulate noise? I see a LFSR design in Mednafen, so I can probably implement that. Not sure if it's hardware accurate or not, or if there's even a way to tell.
Post Reply