How to emulate PSG and SCC in Windows applications.

Page 2/6
1 | | 3 | 4 | 5 | 6

Par ARTRAG

Enlighted (6923)

Portrait de ARTRAG

09-04-2011, 21:21

Can anyone have a look to the following code and tell me where I am wrong?
If I read your code correctly, you _add_ the noise and the tone, right? Instead you should _multiply_ both. This should happen before the volume of the channel is applied. But at that point the output of the tone square wave is either 0 or 1 and the same for the noise, so multiplication is actually the same as an AND operation.

You can also take a look at emulator source code like mame, meisei, blueMSX or openMSX. The code of mame seems simplest (may be easiest to understand). The code of openMSX is highly optimized, so likely most difficult to understand.

No! I do not add the noise!

LN is 0 or 1 and represents the level of the noise genarator
LA is -VA or VA and represents the level of tone A
Level is the analog value of the output

I do :

       if (!(R7 & 0x08) && (LN==0))
	  Level += -VA;
        else
          Level += LA;


If LN is 0 I add -VA, as 0*LA=0 ==>-VA
if LN==1 I add LA, as 1*LA =LA

is it wrong?

Par wouter_

Champion (508)

Portrait de wouter_

10-04-2011, 19:07

No! I do not add the noise!
Ok, I see it now. Tone-on with noise-on or off seems indeed correct. But tone-off/noise-on is wrong I think. In your code, with tone-off (R#7 bits 0,1,2), the output will always be zero. On real HW, setting tone-off, simply sets the output of the tone generator to '1'. So after AND'ing tone and noise, you still hear the noise part. Similarly setting noise off sets the output of the noise generator (for that channel) to '1'. This means that if both tone and noise are off, you can still generated sound by rapidly changing the volume register of that channel.

(Also you still miss an implementation for the envelope stuff, but you knew that of course).

Par Sarcasmic

Master (149)

Portrait de Sarcasmic

11-04-2011, 21:03

Virtual STudio instrument VSTi that is used in cubase,fruity loops and much more

Par ARTRAG

Enlighted (6923)

Portrait de ARTRAG

12-04-2011, 00:44

No! I do not add the noise!
Ok, I see it now. Tone-on with noise-on or off seems indeed correct. But tone-off/noise-on is wrong I think. In your code, with tone-off (R#7 bits 0,1,2), the output will always be zero. On real HW, setting tone-off, simply sets the output of the tone generator to '1'. So after AND'ing tone and noise, you still hear the noise part. Similarly setting noise off sets the output of the noise generator (for that channel) to '1'. This means that if both tone and noise are off, you can still generated sound by rapidly changing the volume register of that channel.

(Also you still miss an implementation for the envelope stuff, but you knew that of course).

I think I see the point, even if i do not see a clear way to patch the problem
btw
I'm trying to import the YM2149 emulator from here
http://leonard.oxg.free.fr/

AY8910 is compatible with YM2149 so the result should be the same

Par ARTRAG

Enlighted (6923)

Portrait de ARTRAG

12-04-2011, 14:21

What about this version, do I understand correctly the logic of the mixer now?

SHORT R01 = 0;
SHORT R23 = 0;
SHORT R45 = 0;

char R6 = 31;
char R7 = 0;

char R8 = 15;
char R9 = 15;
char R10= 15;

char R15 =0;

#define playback_frequency     44100.0
#define psg_master_frequency   (1.7897725e6/16.0)

unsigned char volume_table[] = {0,2,3,4,6,8,11,16,23,32,45,64,90,128,180,255};

void psg_generator(LPSTR data, int size)
{
    static float PhaseA,PhaseB,PhaseC;    // state counters for ch A, B and C
    static SHORT LA,LB,LC;                // output state for ch A, B and C

    static float PhaseN;                 // state counters for Noise Generator
    static SHORT LN;                     // state for Noise Generator

    SHORT VA = volume_table[R8 & 15] ;     // output volume for ch A, B and C
    SHORT VB = volume_table[R9 & 15] ;
    SHORT VC = volume_table[R10& 15] ;

    float hPeriodA = ((R01 & 0x0FFF)+0)*128.0*playback_frequency/psg_master_frequency;
    float hPeriodB = ((R23 & 0x0FFF)+0)*128.0*playback_frequency/psg_master_frequency;
    float hPeriodC = ((R45 & 0x0FFF)+0)*128.0*playback_frequency/psg_master_frequency;

    float PeriodN = ((R6 & 0x1F)+0)*256.0*playback_frequency/psg_master_frequency;

    PSHORT b = (PSHORT) data;

    for (int i=0;i<(int)size/sizeof(SHORT);i++)
    {
        SHORT Level = 0;

        PhaseN-=256;
        if (PhaseN<=0)
        {
            PhaseN += PeriodN;			// 8 bit of fractional part
//            LN = ((rand()+1)>>1) & 1;
            LN = AY8910_NoiseGenerator() & 1;   // 17 bit random generator
        }

	PhaseA-=256;					// 8 bit of fractional part
	if (PhaseA<=0)
        {
            PhaseA += hPeriodA;			// 8 bit of fractional part
            LA ^= 1;
        }
	PhaseB-=256;					// 8 bit of fractional part
	if (PhaseB<=0)
        {
            PhaseB += hPeriodB;			// 8 bit of fractional part
            LB ^= 1;
        }
	PhaseC-=256;					// 8 bit of fractional part
	if (PhaseC<=0)
        {
            PhaseC += hPeriodC;			// 8 bit of fractional part
            LC ^= 1;
        }


	// Calculate samples.
	// The 8910 has three outputs, each output is the mix of one of the
	// three tone generators and of the (single) noise generator. The two
	// are mixed BEFORE going into the DAC. The formula to mix each channel
	// is:
	//   (ToneOn | ToneDisable) & (NoiseOn | NoiseDisable),
	//   where ToneOn and NoiseOn are the current generator state
	//   and ToneDisable and NoiseDisable come from the enable reg.
	// Note that this means that if both tone and noise are disabled, the
	// output is 1, not 0, and can be modulated by changing the volume.


        Level += (((R7 & 0x01) || LA) & ((R7 & 0x08) || LN))*VA;
        Level += (((R7 & 0x02) || LB) & ((R7 & 0x10) || LN))*VB;
        Level += (((R7 & 0x04) || LC) & ((R7 & 0x20) || LN))*VC;

        b[i] = Level*16;
    }
}

Par ARTRAG

Enlighted (6923)

Portrait de ARTRAG

12-04-2011, 15:04

humm, i did it wrong, as the psg hs positive and negative outputs

this should be right:



        Level += ((((R7 & 0x01) || LA) & ((R7 & 0x08) || LN)) ? (-1): (1)) * VA;
        Level += ((((R7 & 0x02) || LB) & ((R7 & 0x10) || LN)) ? (-1): (1)) * VB;
        Level += ((((R7 & 0x04) || LC) & ((R7 & 0x20) || LN)) ? (-1): (1)) * VC;

Par wouter_

Champion (508)

Portrait de wouter_

12-04-2011, 19:51

What about this version, do I understand correctly the logic of the mixer now?
I think it's close, and maybe already good enough for the application you're building. Actually, what are you going to use this routine for? Here's a list of details that are still wrong (i think), but as i said, this only matter if you want really accurate emulation.
- On a real AY8910, period 0 is the same as period 1. In your code period 0 stops alternating the tone output.
- You have 'phaseX' variables that count down (and compare to zero), while in a real AY8910, the corresponding counters count up (and compare to the corresponding period register). This (only) matters when you're rapidly modulating the frequency registers while playing a tone.
(The envelope stuff is still missing completely, but I already mentioned that in earlier posts.)

humm, i did it wrong, as the psg hs positive and negative outputs
I'm not 100% sure about this, but I believe a real AY8910 does only output a positive or zero analog signal (based on pictures from the AY8910 data-sheet). This makes a difference when you're rapidly changing the volume registers. I believe changing the volume has no immediate effect when done while the output of a channel is zero. In your implementation it does have an immediate effect. Though I believe in most cases this difference won't be audible. So again depending on what you want to do, your current implementation may already be good enough. If you want to fix this and also still want a signal symmetrical around zero, you'll have to emulate a DC filter.

BTW: if you want better audio quality it may be better to emulate the PSG at its native frequency and 'properly' resample that to the desired host sample frequency. Otherwise you may get aliasing effects while playing higher frequency tones.

Par ARTRAG

Enlighted (6923)

Portrait de ARTRAG

12-04-2011, 21:48

Thanks for your comments! I'm doing the chip emulation for trilotracker (able to import pt3 files and to work on psg+scc)
what do you mean by:

On a real AY8910, period 0 is the same as period 1. In your code period 0 stops alternating the tone output.

Par wouter_

Champion (508)

Portrait de wouter_

12-04-2011, 22:48

what do you mean by:
On a real AY8910, period 0 is the same as period 1. In your code period 0 stops alternating the tone output.

On a real AY8910, writing 0 to register-pair 0,1 has the same effect as-if you had written 1. But in your implementation, when R01 is 0, hPeriodA is also 0 and things will go wrong ... actually it will already start going wrong when the calculated value for 'hPeriodA' is less than 256. In that case LA toggles each iteration, and 'PhaseA' will get more and more negative each iteration.
So it's wrong, but in a different way as i (tried to) explain in my previous post. Sorry for the confusion.

Par hit9918

Prophet (2927)

Portrait de hit9918

13-04-2011, 00:40

I want to remind that the counters count upwards! (this also makes clearer the period = 0 versus period = 1 issue)

see http://www.msx.org/forumtopic11564.html :

Careful studies of the chip output prove that it instead counts up from 0 until the counter becomes greater or equal to the period. This is an important difference when the program is rapidly changing the period to modulate the sound.

I also want to remind the bluemsx team of that, please change this! The typical woooaaaah sound like in Retaliot is the sliding thru all the dozens waveforms the PSG is capable of! Variable pulse length waveforms of the NES are lurking in there!

But with the wrong countdown, one cannot set phase i.e. not reliably pick one of those waveforms, and in worst case one gets silence.

Code for counting upwards to frequency register:

	PhaseA +=256; //count UP instead down. 
		      //PSG without fractional business would just add 1.
	
	if (PhaseA >= hPeriodA) {	//comparison with frequency register instead with 0
		PhaseA -= hPeriodA;	//subtract instead add
					//the subtract is needed because of fractional business, PSG would just set PhaseA to 0 here.
		if (hperiodA == 0) {
			PhaseA = 0;	//special Phase Reset.
					//PSG would not need this code, would already have set to 0 in code above
		}
		LA ^= 1;
	}

The deal: unlike comparison with 0 in the wrong version, the comparison to frequencyregister enables instant response to frequencyregister changes, which enables phase control.

p.s. with AND operations one should make sure the hPeriod value of the emu can never get too big by having garbage in upper bits. the previously posted code does this at the beginning of the function.

Page 2/6
1 | | 3 | 4 | 5 | 6