VOGONS


CMS/GameBlaster emulation thread

Topic actions

Reply 20 of 61, by superfury

User metadata
Rank l33t++
Rank
l33t++

A quick check: I'm currently switching output levels between MaxU and 0V with most outputs(cases 0-2), but shouldn't this be -MaxU and +MaxU(except case 3)?

switch (chip->channels[channel].noise_enable|(chip->channels[channel].frequency_enable<<1)) //Noise/frequency mode?
{
case 0: //Both disabled?
memset(&chip->channels[channel].ampenv,0,sizeof(chip->channels[channel].ampenv)); //No output!
break;
case 1: //Noise only?
do //Check all inputs!
{
chip->channels[channel].ampenv[input] = ((input&4)>>2)*env[input&1]; //Noise at max volume!
++input;
} while (--left); //Next input!
break;
case 2: //Frequency only?
do //Check all inputs!
{
chip->channels[channel].ampenv[input] = ((input&2)>>1)*env[input&1]; //Noise at max volume!
++input;
} while (--left); //Next input!
break;
case 3: //Noise+Frequency?
do //Check all inputs!
{
if (input&2) //Tone high state?
{
chip->channels[channel].ampenv[input] = env[input&1]; //Noise at max volume!
}
else if ((input&4)==0) //Tone low and noise is low? Low at full amplitude!
{
chip->channels[channel].ampenv[input] = -env[input&1]; //Noise at max volume!
}
else //Tone low and noise is high? 50% amplitude!
{
chip->channels[channel].ampenv[input] = env[input&1]>>1; //Noise at half volume!
}
++input;
} while (--left); //Next input!
break;
default: //Safety check
break; //Ignore!
}

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 21 of 61, by superfury

User metadata
Rank l33t++
Rank
l33t++

I've changed the 0V cases to -MaxU instead, this fixed the whole volume issue. I've also improved the formula for case 3(using a flipflop and even/odd PWM-sample modulation on freq&noise input being 0.

OPTINLINE void updateAmpEnv(SAA1099 *chip, byte channel)
{
INLINEREGISTER byte input, left;
byte env[2];
static byte flipflop[2] = {-1,1};
env[0] = (chip->channels[channel].amplitude[0]*chip->channels[channel].envelope[0]) >> 4; //Left envelope!
env[1] = (chip->channels[channel].amplitude[1]*chip->channels[channel].envelope[1]) >> 4; //Right envelope!
//bit0=right channel
//bit1=square wave output
//bit2=noise output
//bit3=PWM period
//Original algorithm from Dosbox&MAME was:
input = 0; //First input!
left = 0x10; //How many left?
#ifdef DOSBOXMAMESYNTH
INLINEREGISTER int_32 output;
INLINEREGISTER byte curenv;
do {
output = 0; //Init!
curenv = env[input&1]; //Current environment!
if ((input&4) && chip->channels[channel].noise_enable) //Noise on?
{
output -= (int_32)(curenv>>1); //Half volume substracted!
}
if ((input&2) && chip->channels[channel].frequency_enable) //Frequency on?
{
output += (int_32)curenv; //Full volume added!
}
chip->channels[channel].ampenv[input] = output; //Give the output!
++input; //Less left!
} while (--left); //Next input!

#else
switch (chip->channels[channel].noise_enable|(chip->channels[channel].frequency_enable<<1)) //Noise/frequency mode?
{
case 0: //Both disabled?
memset(&chip->channels[channel].ampenv,0,sizeof(chip->channels[channel].ampenv)); //No output!
break;
case 1: //Noise only?
do //Check all inputs!
{
chip->channels[channel].ampenv[input] = flipflop[((input&4)>>2)]*env[input&1]; //Noise at max volume!
++input;
} while (--left); //Next input!
break;
case 2: //Frequency only?
do //Check all inputs!
{
chip->channels[channel].ampenv[input] = flipflop[((input&2)>>1)]*env[input&1]; //Noise at max volume!
++input;
} while (--left); //Next input!
break;
case 3: //Noise+Frequency?
do //Check all inputs!
{
if (input&2) //Tone high state?
{
chip->channels[channel].ampenv[input] = env[input&1]; //Noise at max volume!
}
else if ((input&4)==0) //Tone low and noise is low? Low at full amplitude!
Show last 15 lines
				{
chip->channels[channel].ampenv[input] = -env[input&1]; //Noise at max volume!
}
else //Tone low? Then noise is high every other PWM period!
{
chip->channels[channel].ampenv[input] = flipflop[((input&8)>>3)]*env[input&1]; //Noise at half volume!
}
++input;
} while (--left); //Next input!
break;
default: //Safety check
break; //Ignore!
}
#endif
}

Sound recording of The Secret of Monkey Island opening songs:
https://www.dropbox.com/s/zycsfpr88gcdtfk/rec … 2_0911.zip?dl=0

That sounds a lot better already, with improved volume and Noise+Frequency case. Anyone can see what's going wrong with generating the sound? Why are the tones still off? The formulas should be correct?

The sound generation is as follows:

OPTINLINE void generateSAA1099channelsample(SAA1099 *chip, byte channel, int_32 *output_l, int_32 *output_r)
{
byte output;
channel &= 7;
chip->channels[channel].level = getSAA1099SquareWave(chip,channel); //Current flipflop output of the square wave generator!

//Tick the envelopes when needed!
if ((channel==1) && (chip->env_clock[0]==0))
tickSAAEnvelope(chip,0);
if ((channel==4) && (chip->env_clock[1]==0))
tickSAAEnvelope(chip,1);

output = chip->channels[channel].toneonnoiseonflipflop; //Tone/noise flipflop every other PWM sample!
chip->channels[channel].toneonnoiseonflipflop = (output^8); //Trigger the flipflop at PWM rate!
output |= chip->noise[chip->channels[channel].noisechannel].levelbit; //Use noise? If the noise level is high (noise 0 for channel 0-2, noise 1 for channel 3-5); Level bit 0 taken always to bit 2!
output |= chip->channels[channel].level; //Level is always 1-bit! Level to bit 2!
//Check and apply for noise! Substract to avoid overflows, half amplitude only
*output_l += chip->channels[channel].ampenv[output]; //Output left!
*output_r += chip->channels[channel].ampenv[output|1]; //Output right!
}

The rate at which these samples are generated is the PWM rate(14MHz divided by 4 to get 3.57MHz, then divided by 16 to get the PWM rate at which this function is called to generate samples for low-pass filtering. After low-pass filtering, it's resampled to 44.1kHz. After resampling it's filtered at 18.2Hz to remove DC.

Current code: https://bitbucket.org/superfury/unipcemu/src/ … ter.c?at=master

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 22 of 61, by superfury

User metadata
Rank l33t++
Rank
l33t++

I've managed to improve the sound quality and rendering speed somewhat by using two lookup tables:
- One generic lookup table(64 entries) to contain all lookup table values (in terms of left/right channel, sign and ignore sign) for each of the four rendering modes(Silence, noise only, tone(frequency) only and noise+tone(frequency)).

OPTINLINE void calcAmpEnvPrecalcs()
{
word i;
byte input;
for (i=0;i<NUMITEMS(AmpEnvPrecalcs);++i) //Process all precalcs!
{
input = (i&0xF); //Input signal we're precalculating!
//Output:
//bit0=Right channel
//bit1=Sign. 0=Negative, 1=Positive
//bit2=Ignore sign. We're silence!
//Lookup table input:
//bits0-3=Index into the lookup table to generate!
//bit4=Noise enable
//bit5=Frequency enable
switch ((i>>4)&3) //Noise/frequency mode?
{
default: //Safety check
case 0: //Both disabled?
AmpEnvPrecalcs[i] = 4; //No output, channel and positive/negative doesn't matter!
break;
case 1: //Noise only?
AmpEnvPrecalcs[i] = ((input&4)>>1)|(input&1); //Noise at max volume!
++input;
break;
case 2: //Frequency only?
AmpEnvPrecalcs[i] = (input&3); //Noise at max volume!
break;
case 3: //Noise+Frequency?
if (input&2) //Tone high state?
{
AmpEnvPrecalcs[i] = (input&1)|2; //Noise at max volume, positive!
}
else if ((input&4)==0) //Tone low and noise is low? Low at full amplitude!
{
AmpEnvPrecalcs[i] = (input&1); //Noise at max volume, negative!
}
else //Tone low? Then noise is high every other PWM period!
{
AmpEnvPrecalcs[i] = ((input&8)>>2)|(input&1); //Noise at half volume!
}
break;
}
}
}

This table is used to generate rendering lookup values when any of the Amplitude registers, Volume Envelope output(may be during rendering) or frequency/noise enable bits are changed:

byte AmpEnvPrecalcs[0x40]; //AmpEnv precalcs of all possible states!
OPTINLINE void updateAmpEnv(SAA1099 *chip, byte channel)
{
sword envdata[2];
#ifndef DOSBOXMAMESYNTH
int_32 statetranslation[8]; //All states we can use!
byte *AmpEnvPrecalc;
int_32 *output;
#endif
envdata[0] = (sword)(chip->channels[channel].amplitude[0]*chip->channels[channel].envelope[0]) >> 4; //Left envelope!
envdata[1] = (sword)(chip->channels[channel].amplitude[1]*chip->channels[channel].envelope[1]) >> 4; //Right envelope!
//bit0=right channel
//bit1=square wave output
//bit2=noise output
//bit3=PWM period
//Original algorithm from Dosbox&MAME was:
#ifdef DOSBOXMAMESYNTH
INLINEREGISTER byte input;
INLINEREGISTER byte left = 0x10; //How many left?
INLINEREGISTER int_32 output;
INLINEREGISTER byte curenv;
input = 0; //First input!
do {
output = 0; //Init!
curenv = envdata[input&1]; //Current environment!
if ((input&4) && chip->channels[channel].noise_enable) //Noise on?
{
output -= (int_32)(curenv>>1); //Half volume substracted!
}
if ((input&2) && chip->channels[channel].frequency_enable) //Frequency on?
{
output += (int_32)curenv; //Full volume added!
}
chip->channels[channel].ampenv[input] = output; //Give the output!
++input; //Less left!
} while (--left); //Next input!

#else
//Generate our translation table first!
statetranslation[0] = -envdata[0]; //Left channel, negative!
statetranslation[1] = -envdata[1]; //Right channel, negative!
statetranslation[2] = envdata[0]; //Left channel, positive!
statetranslation[3] = envdata[1]; //Right channel, positive!
memset(&statetranslation[4],0,sizeof(statetranslation)>>1); //Rest: Either channel, ignore sign: we're silence!

AmpEnvPrecalc = &AmpEnvPrecalcs[(((chip->channels[channel].frequency_enable<<1)|chip->channels[channel].noise_enable)<<4)]; //First frequency/noise lookup entry!
output = &chip->channels[channel].ampenv[0]; //Where to store the precalcs table!
//Generate all 16 state precalcs!
*output++ = statetranslation[*AmpEnvPrecalc++]; //Take the precalcs for this channel!
*output++ = statetranslation[*AmpEnvPrecalc++]; //Take the precalcs for this channel!
*output++ = statetranslation[*AmpEnvPrecalc++]; //Take the precalcs for this channel!
*output++ = statetranslation[*AmpEnvPrecalc++]; //Take the precalcs for this channel!
*output++ = statetranslation[*AmpEnvPrecalc++]; //Take the precalcs for this channel!
*output++ = statetranslation[*AmpEnvPrecalc++]; //Take the precalcs for this channel!
*output++ = statetranslation[*AmpEnvPrecalc++]; //Take the precalcs for this channel!
*output++ = statetranslation[*AmpEnvPrecalc++]; //Take the precalcs for this channel!
*output++ = statetranslation[*AmpEnvPrecalc++]; //Take the precalcs for this channel!
*output++ = statetranslation[*AmpEnvPrecalc++]; //Take the precalcs for this channel!
*output++ = statetranslation[*AmpEnvPrecalc++]; //Take the precalcs for this channel!
*output++ = statetranslation[*AmpEnvPrecalc++]; //Take the precalcs for this channel!
Show last 6 lines
	*output++ = statetranslation[*AmpEnvPrecalc++]; //Take the precalcs for this channel!
*output++ = statetranslation[*AmpEnvPrecalc++]; //Take the precalcs for this channel!
*output++ = statetranslation[*AmpEnvPrecalc++]; //Take the precalcs for this channel!
*output = statetranslation[*AmpEnvPrecalc]; //Take the precalcs for this channel!
#endif
}

The resulting 16 entries are used during rendering to create output samples, indexing based on the PWM flipflop(flips each rendered channel sample, essentially an identifier for even/odd PWM period used for noise during noise+tone rendering, according to Jepael's post), Noise output bit, Tone output bit and left/right channel bit:

OPTINLINE void generateSAA1099channelsample(SAA1099 *chip, byte channel, int_32 *output_l, int_32 *output_r)
{
byte output;
channel &= 7;
chip->channels[channel].level = getSAA1099SquareWave(chip,channel); //Current flipflop output of the square wave generator!

//Tick the envelopes when needed!
if ((channel==1) && (chip->env_clock[0]==0))
tickSAAEnvelope(chip,0);
if ((channel==4) && (chip->env_clock[1]==0))
tickSAAEnvelope(chip,1);

output = chip->channels[channel].toneonnoiseonflipflop; //Tone/noise flipflop every other PWM sample!
chip->channels[channel].toneonnoiseonflipflop = (output^8); //Trigger the flipflop at PWM rate!
output |= chip->noise[chip->channels[channel].noisechannel].levelbit; //Use noise? If the noise level is high (noise 0 for channel 0-2, noise 1 for channel 3-5); Level bit 0 taken always to bit 2!
output |= chip->channels[channel].level; //Level is always 1-bit! Level to bit 2!
//Check and apply for noise! Substract to avoid overflows, half amplitude only
*output_l += chip->channels[channel].ampenv[output]; //Output left!
*output_r += chip->channels[channel].ampenv[output|1]; //Output right!
}

Wave generator generating the normal 1-bit wave used above:

OPTINLINE byte getSAA1099SquareWave(SAA1099 *chip, byte channel)
{
byte result;
uint_32 timepoint;
result = chip->squarewave[channel].output; //Save the current output to give!
timepoint = chip->squarewave[channel].timepoint; //Next timepoint!
++timepoint; //Next timepoint!
if (timepoint>=chip->squarewave[channel].timeout) //Timeout? Flip-flop!
{
chip->squarewave[channel].output = result^2; //Flip-flop to produce a square wave! We're bit 1 of the output!
timepoint = 0; //Reset the timepoint!
}
chip->squarewave[channel].timepoint = timepoint; //Save the resulting timepoint to advance the wave!
return result; //Give the resulting square wave!
}

The frequency for a square wave is set as below:

OPTINLINE void updateSAA1099RNGfrequency(SAA1099 *chip, byte channel)
{
byte channel2=channel|8;
if (chip->noise[channel].freq!=chip->squarewave[channel2].freq) //Frequency changed?
{
chip->squarewave[channel2].timeout = (uint_32)(__GAMEBLASTER_BASERATE/(double)(2.0*chip->noise[channel].freq)); //New timeout!
chip->squarewave[channel2].timepoint = 0; //Reset the timepoint!
chip->squarewave[channel2].freq = chip->noise[channel].freq; //We're updated!
}
}

OPTINLINE void updateSAA1099frequency(SAA1099 *chip, byte channel) //on octave/frequency change!
{
channel &= 7; //Safety on channel!
chip->channels[channel].freq = (float)((double)((GAMEBLASTER.baseclock/512)<<chip->channels[channel].octave)/(double)(511.0-chip->channels[channel].frequency)); //Calculate the current frequency to use!
if (chip->channels[channel].freq!=chip->squarewave[channel].freq) //Frequency changed?
{
chip->squarewave[channel].timeout = (uint_32)(__GAMEBLASTER_BASERATE/(double)(2.0*chip->channels[channel].freq)); //New timeout!
chip->squarewave[channel].timepoint = 0; //Reset!
chip->squarewave[channel].freq = chip->channels[channel].freq; //We're updated!
}
}

The noise bit is calculated as follows:

OPTINLINE void tickSAA1099noise(SAA1099 *chip, byte channel)
{
byte noise_flipflop;

channel &= 1; //Only two channels!

//Check the current noise generators and update them!
//Noise channel output!
noise_flipflop = getSAA1099SquareWave(chip,channel|8); //Current flipflop output of the noise timer!
if (noise_flipflop & (noise_flipflop ^ chip->noise[channel].laststatus)) //High and risen?
{
if (((chip->noise[channel].level & 0x20000) == 0) == ((chip->noise[channel].level & 0x0400) == 0))
chip->noise[channel].level = (chip->noise[channel].level << 1) | 1;
else
chip->noise[channel].level <<= 1;
chip->noise[channel].levelbit = ((chip->noise[channel].level&1)<<2); //Current level bit has been updated, preshifted to bit 2 of the output!
}
chip->noise[channel].laststatus = noise_flipflop; //Save the last status!
}

Is this correct behaviour, Jepael?

Full current source code:
https://bitbucket.org/superfury/unipcemu/src/ … ter.c?at=master

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 23 of 61, by superfury

User metadata
Rank l33t++
Rank
l33t++

A quick little fix:
https://bitbucket.org/superfury/unipcemu/src/ … ter.c?at=master

Switched rendering to stereo output and modified the noise to update each time the flipflop changes output(instead of only when rising from 0 to 1).

Is this correct?

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 24 of 61, by superfury

User metadata
Rank l33t++
Rank
l33t++

I've just removed the invalid high-pass filter and disabled the low-pass filter. For some reason, there seems to be a low sweep in the tone (about 8Hz), even when not filtered?

Filename
UniPCemu_GameBlaster_MonkeyIsland_highpass18.2Hz.zip
File size
3.22 MiB
Downloads
80 downloads
File comment
Monkey Island opening, only filtered using a high-pass 18.2Hz filter.
File license
Fair use/fair dealing exception

Source code used: https://bitbucket.org/superfury/unipcemu/src/ … ter.c?at=master

Anyone can see what's going wrong here? This shouldn't be happening? Why isn't it generating a proper signal(3.58MHz/16(PWM) to render, thus about 223721Hz signal)?

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 25 of 61, by superfury

User metadata
Rank l33t++
Rank
l33t++

I've just made a little recording, filtering channel 0 only(disabling all other channel output). The output seems a bit odd. I've just modified the AmpEnv precalcs to only filter out two possibilities: case 0(silence) and 1(noise only) becoming silence, the other cases becoming frequency only:

switch ((i>>4)&2) //Noise/frequency mode?

Then I added a simply check to sound channel 0 only, before loading the toneonnoiseonflipflop and processing it:

if (channel!=0) return; //Only channel X to test?

This results in the following resampled audio:

Filename
recording_39_channel0only.zip
File size
2.03 MiB
Downloads
76 downloads
File comment
Output of Monkey Island opening playing channel 0 only, only high-pass filtered at 18.2Hz.
File license
Fair use/fair dealing exception

That audio already sounds odd. Why is the wave that's generated such an odd wave? It's supposed to be a constant output, assuming it's correct?

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 26 of 61, by superfury

User metadata
Rank l33t++
Rank
l33t++

After a bit of tweaking the variable sizes (sword envdata needing to be int_32 variables instead), that fixed the problems with basic volume. Now the square waves give the correct output as far as I can see in WavePad.

Recorded opening of Monkey Island using the latest emulation and filtering enabled again: https://www.dropbox.com/s/n4xwfkc0aaltamk/Uni … 2_1956.zip?dl=0

Done using the following source code: https://bitbucket.org/superfury/unipcemu/src/ … ter.c?at=master

I see that the square waves end up correctly in the output, but for some reason part of the output seems distorted? Maybe it's the way the noise is handled?

Is it correct that the noise signal changes every rising/lowering period? So at the same rate the square wave toggles at the specified frequency(twice the set frequency)?

Is the behaviour of the noise+tone output correct? So when the tone square wave is 1, output is positive. Else if noise bit 0 is zero, output is -1. Else output is -1 every other sample(at 3.57MHz) and 1 at the remaining case(triggered by the noiseonnoteonflipflop). Is that correct?

Also, noise only is the same as tone only, but taking the noise output bit 0 instead of the square wave bit 0?

Anyone can confirm this? I've implemented it based on that information. Jepael?

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 27 of 61, by superfury

User metadata
Rank l33t++
Rank
l33t++

I've 'improved' the sample generation, now creating actual PWM output to render. The amplitude is loaded for the left/right channel each PWM sample, then it gives high PWM output while counting, until the counter (which counts from 0-15) reaches 0(to load the next PWM sample) or 15(Toggles the RNG even/odd sample bit for the next sample to process).

I'm king of getting the 'right' output, but there seems to be a lot of noise in it? It sounds like a low-frequency carrier wave instead of the initial tone that's produced in The Secret of Monkey Island.

Filename
UniPCemu_GameBlaster_20170402_1219.zip
File size
1.67 MiB
Downloads
75 downloads
File comment
Recording of the new PWM-based output, low-pass filtered at 22.05kHz.
File license
Fair use/fair dealing exception

Current source code: https://bitbucket.org/superfury/unipcemu/src/ … ter.c?at=master

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 28 of 61, by superfury

User metadata
Rank l33t++
Rank
l33t++

This is my current code for PWM-output sample generation on the Game Blaster(with the AmpEnv precalcs only supplying basic information(positive/negative output, muted channel and left/right channel)). Although, when enabling the PWM-output instead of the basic output(Dosbox/Mame 16 level PCM method modulation samples), which simply generates +5V/-5V or silence(0V on muted channels), I get wrong output instead?

OPTINLINE int_32 getSAA1099PWM(SAA1099 *chip, byte channel, byte output)
{
#ifdef PWM_OUTPUT
static int_32 outputs[4] = {-SHRT_MAX,SHRT_MAX,0,0}; //Output, if any!
#else
static int_32 outputs[4] = {-1,1,0,0}; //Output, if any!
#endif
byte counter;
counter = chip->channels[channel].PWMOutput[output&1].PWMCounter++; //Apply the current counter!
counter &= 0xF; //Reset every 16 pulses to generate a 16-level PWM!
if (counter==0) //Timeout? Load new information and start the next PWM sample!
{
chip->channels[channel].PWMOutput[output&1].output = output; //Save the output for reference in the entire PWM output!
output &= 1; //We're only interested in the channel from now on!
chip->channels[channel].PWMOutput[output].PWMCounter = 0; //Reset the counter!
//Load the new PWM timeout from the channel!
chip->channels[channel].PWMOutput[output].Amplitude = chip->channels[channel].PWMAmplitude[output]; //Update the amplitude to use!
chip->channels[channel].PWMOutput[output].flipflopoutput = ((chip->channels[channel].PWMOutput[output].output&4)|((chip->channels[channel].PWMOutput[output].output&2))>>1); //Start output, if any! We're starting high!
}
else if ((counter==0xF) && ((output&1)==0)) //To start a new PWM pulse next sample?
{
chip->channels[channel].toneonnoiseonflipflop ^= 8; //Trigger the flipflop at PWM samplerate!
output &= 1; //We're only interested in the channel from now on!
}
else
{
output &= 1; //We're only interested in the channel from now on!
}
#ifdef PWM_OUTPUT
if ((chip->channels[channel].PWMOutput[output].output&4)==0) //Not zeroed always? We're a running channel!
{
if (counter>chip->channels[channel].PWMOutput[output].Amplitude) //Finished PWM period to use?
{
chip->channels[channel].PWMOutput[output].flipflopoutput = 2; //We're finished! Return to 0V!
}
}
return outputs[chip->channels[channel].PWMOutput[output].flipflopoutput]; //Give the proper output as a 16-bit sample!
#else
return outputs[chip->channels[channel].PWMOutput[output].flipflopoutput]*(sword)amplitudes[chip->channels[channel].PWMAmplitude[output]]; //Give the proper output as a simple pre-defined 16-bit sample!
#endif
}

When PWM_OUTPUT is defined, it uses Dosbox-style sampling. Else, it uses the PWM method instead(which doesn't work properly?). Is this correct? So with 0 output volume, it's a constant 0V. With 1/16 output volume, it's 1 sample high(positive) or low(negative) at e.g. (+/-)5V, then 15 samples 0V? Is that correct?

Edit: I've seemed to have found the problem:

if (counter>chip->channels[channel].PWMOutput[output].Amplitude) //Finished PWM period to use?

Is supposed to be:

if (counter>=chip->channels[channel].PWMOutput[output].Amplitude) //Finished PWM period to use?

Amplitude level 0 results in no output(0 samples used), amplitude level F results in one less than maximum output(15 out of 16 samples being raised(positively or negatively), one sample low(being 0V))?

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 29 of 61, by superfury

User metadata
Rank l33t++
Rank
l33t++

I've made a little 44.1kHz recording (based on UniPCemu's Game Blaster 3.57MHz PWM input, low-pass filtered at 22.05kHz, high-pass filtered at 18.2Hz).

https://www.dropbox.com/s/qvc4wzn9ahe7q7q/Uni … 4_1511.zip?dl=0

So the PWM-based rendering is working correctly now(16 positive/negative/0V(muted) raw samples for each PWM sample rendered, thus giving a 16-level output). Although I still notice that some tones seem to be off a bit(in frequency or mixing the noise into it)? Anyone can see why this is happening?

https://bitbucket.org/superfury/unipcemu/src/ … ter.c?at=master

Also, the filtering(low-pass filtering to be exact) at 3.57MHz seems to be a bit on the heavy side for the Intel i7 4790K(@4.0GHz) to handle directly. Is there a way to optimize this to perform better?
Filter code: https://bitbucket.org/superfury/unipcemu/src/ … ers.c?at=master

Just in case the high/low-pass filtering in UniPCemu might not be working fully, I've low-pass filtered the raw PWM signal that's generated (1.93GB .WAV file at 3.57MHz samplerate) at 22.05kHz, resampled it to 44.1kHz and High-pass filtered it at 20.0Hz(6dB/octave) to verify:

Filename
UniPCemu_GameBlaster_RawLog_MonkeyIsland_Audacity_LowPassHighPass.mp3
File size
2.77 MiB
Downloads
77 downloads
File comment
Audacity processed recording of UniPCemu's raw PWM output(3.57MHz).
File license
Fair use/fair dealing exception

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 30 of 61, by superfury

User metadata
Rank l33t++
Rank
l33t++

I'm wondering about one little thing though: Are the two left/right channels supposed to be mixed before low-pass and high-pass filtering, or are the filters supposed to be done before mixing?

So (case 1):
Left channel chip 0 \
-------------------------- > Sample to mix -> low pass filter -> resample -> high-pass filter -> output left channel
Left channel chip 1 /

Right channel chip 0 \
-------------------------- > Sample to mix -> low pass filter -> resample -> high-pass filter -> output right channel
Right channel chip 1 /

Or are all outputs parallel and only mixed at the output (case 2)?
Left channel chip 0 > Sample to mix -> low pass filter -> resample -> high-pass filter -> output left channel chip 0
Left channel chip 1 > Sample to mix -> low pass filter -> resample -> high-pass filter -> output left channel chip 1
Right channel chip 0 > Sample to mix -> low pass filter -> resample -> high-pass filter -> output right channel chip 0
Right channel chip 1 > Sample to mix -> low pass filter -> resample -> high-pass filter -> output right channel chip 1

I'm currently applying case 2 in my emulator. Does this matter? Also, normalizing (dividing by 6, essentially) is only done after resampling, before high-pass filtering.

Edit: Looking at the schematics from the CMS replica (source), it seems the output (first chip left channel, first chip right channel, second chip left channel, second chip right channel) is low-pass filtered before being mixed. So in this case, my emulation should be correct(case 2 being the components R11/C17; R12/C18; R13/C19 and R14/C20).

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 31 of 61, by Jepael

User metadata
Rank Oldbie
Rank
Oldbie

Filters are causal, they are linear and time invariant. So it should not matter whether you sum and filter, or filter and sum. If order does matter, then there is a problem in the implementation.

As far as I can tell, on GB and SB cards the left outputs are connected directly together, and right outputs are connected directly together.

So the replica is different, it is more hi-fi regarding the audio filtering and mixing before the speaker amp. GB or SB do not have all that mess of components there.

And the replica does not use DTACK to add wait states with IOCHRDY. I am not sure how well that works, but IIRC on some writes it could take up to 16 clocks before it is ready for next write. Maybe the solution is not to write too often.

Reply 32 of 61, by superfury

User metadata
Rank l33t++
Rank
l33t++

So I should just optimize it by first adding the left channels together and the right channels together, then low-pass filter them(using half the filters that are used now) and give the output samples to the sound module(the module that actually renders to the real (Windows/linux/Android) hardware through SDL), like Dosbox does?

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 33 of 61, by superfury

User metadata
Rank l33t++
Rank
l33t++

Jepael, I also have a little question about applying the PWM amplitude settings in combination with the output from the noise(+/-)tone generators.

Current code: https://bitbucket.org/superfury/unipcemu/src/ … ter.c?at=master

The input to generating the PWM is a byte that contains the current state of output from the noise&tone generation:
bit0=right channel instead of left
bit1=channel output high(depends on noise output, channel mode(0-3, bitwise OR of frequency on and noise on), tone output and PWM sample toggle(XOR toggles the bit after each 15th left channel sample for mode 3(tone+noise)), according to your post, to provide even/odd PWM input for the last case of your post(the 50% case))
bit2=silence(silent channel)

Bit0 is used to determine the channel to work with.
Bit1 is used to make the PWM toggle from -MaxU volts(when 0) or +MaxU volts(when 1) at t=0. When t(postincrements each sample) reaches the set amplitude(which is (the volume envelope times amplitude divided by 16)'s low 4 bits), including t=0 in the check(t wraps around 4 bits), checking is stopped and the output is set to 0V for the remaining samples(until t mod 16 reaches 0, causing the next PWM sample information and output voltage to be loaded and used).

Is that behaviour correct? So when t reaches the first sample (t=0), with a negative sample(e.g. square wave input using tone output=0), the voltage becomes -MaxU volts(-32768 in signed 16-bit PCM) for Amplitude(0-15) samples, after which output becomes 0V(0 in signed 16-bit PCM). When the tone output is 1 instead of 0, the voltage starts out at +MaxU volts(32767 in signed 16-bit PCM) instead.

Is that correct behaviour? (See the saa1099PWM function for this description's code and the 0x40 byte AmpEnv precalcs table(16 bytes per channel mode) for my implementation of your channel modes) Or did I make a mistake implementing PWM or your high/low channel states?

Edit: Btw, the channel PWM output from both chips(left channels and right channels) is now mixed(added) before taking the left and right channel output PWM stream(3 states: -32768, 0(silence&timed out PWM pulse) and +32767) and low-pass filtering the two channels seperately.

The output also doesn't sound pure like the Dosbox output. Like it's jammed by a noise signal too much or incorrect modulation. Although the plain square waves on a single channel in the Monkey Island intro show up fine in my audio editors(WavePad and Audacity).

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 34 of 61, by superfury

User metadata
Rank l33t++
Rank
l33t++

I've optimized the Game Blaster a bit by implementing the new double FIFO buffer functionality and using the PC speaker method for filtering(first storing all required samples into the doubled FIFO buffer, then rendering at the PC speaker rate, reading blocks and filtering them and eventually(after each block), render a sample to the output at the sampling rate).

I've made a little recording of the new emulation:
https://www.dropbox.com/s/mmfafnxt9jotuuj/rec … 5_1209.zip?dl=0

This speeds things up quite a bit. It's still stuttering when played during emulation, but it's already better than it used to be(probably better optimized now).
This is the current source code that was used to generate the recorded output:
https://bitbucket.org/superfury/unipcemu/src/ … ter.c?at=master

Anyone can see where the noisy part of the signal(which isn't supposed to be there?) is coming from?

Edit: Just fixed a little bug in the PC speaker and Game Blaster's rendering algorithm. It was skipping all remaining samples(instead of only the samples to be rendered), thus causing the buffer to clear too fast. I've made a new recording of it:
https://www.dropbox.com/s/qmzphi9zgfmndqn/rec … 5_1229.zip?dl=0
And the accompanying source code:
https://bitbucket.org/superfury/unipcemu/src/ … ter.c?at=master

Anyone can see what's causing the strange noise in the resulting output(it looks like it might be the triangles wave output, but this might be a property of the low-pass RC filter?)?

Edit: I've just listened to the music again with my headphones, but now it looks like the left speaker is producing correct output, but the right speaker has some noise mixed in(producing odd harmonic noise)? Could it be that the actual problem is actually something with the right channel only?

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 35 of 61, by superfury

User metadata
Rank l33t++
Rank
l33t++

My latest code, with defines describing the AmpEnv precalcs and results:
https://bitbucket.org/superfury/unipcemu/src/ … ter.c?at=master

It still gives the wrong result, though. Is there anything wrong with the AmpEnv precalcs themselves for some reason?

OPTINLINE void calcAmpEnvPrecalcs()
{
word i;
byte input;
for (i=0;i<NUMITEMS(AmpEnvPrecalcs);++i) //Process all precalcs!
{
input = (i&0xF); //Input signal we're precalculating!
//Output(AmpEnvPrecalcs):
//bit0=Right channel
//bit1=Sign. 0=Negative, 1=Positive
//bit2=Ignore sign. We're silence!
//Lookup table input(i):
//bits0-3=Index into the lookup table to generate:
//bit0=Right channel
//bit1=Channel output index
//bit2=Noise output!
//bit3=PWM period.
//Index Loaded to select synthesis method:
//bit4=Noise enable(block index)
//bit5=Frequency enable(block index)
switch ((i)&(AMPENV_INPUT_FREQUENCYENABLE|AMPENV_INPUT_NOISEENABLE)) //Noise/frequency mode?
{
default: //Safety check
case 0: //Both disabled?
AmpEnvPrecalcs[i] = AMPENV_RESULT_SILENCE; //No output, channel and positive/negative doesn't matter!
break;
case AMPENV_INPUT_NOISEENABLE: //Noise only?
AmpEnvPrecalcs[i] = ((input&AMPENV_INPUT_NOISEOUTPUT)?AMPENV_RESULT_POSITIVE:AMPENV_RESULT_NEGATIVE)|((input&AMPENV_INPUT_RIGHTCHANNEL)?AMPENV_RESULT_RIGHTCHANNEL:AMPENV_RESULT_LEFTCHANNEL); //Noise at max volume!
break;
case AMPENV_INPUT_FREQUENCYENABLE: //Frequency only?
AmpEnvPrecalcs[i] = ((input&AMPENV_INPUT_SQUAREWAVEOUTPUT)?AMPENV_RESULT_POSITIVE:AMPENV_RESULT_NEGATIVE)|((input&AMPENV_INPUT_RIGHTCHANNEL)?AMPENV_RESULT_RIGHTCHANNEL:AMPENV_RESULT_LEFTCHANNEL); //Noise at max volume!
break;
case (AMPENV_INPUT_FREQUENCYENABLE|AMPENV_INPUT_NOISEENABLE): //Noise+Frequency?
if (input&AMPENV_INPUT_SQUAREWAVEOUTPUT) //Tone high state?
{
AmpEnvPrecalcs[i] = ((input&AMPENV_INPUT_RIGHTCHANNEL)?AMPENV_RESULT_RIGHTCHANNEL:AMPENV_RESULT_LEFTCHANNEL)|AMPENV_RESULT_POSITIVE; //Noise at max volume, positive!
}
else if ((input&AMPENV_INPUT_NOISEOUTPUT)==0) //Tone low and noise is low? Low at full amplitude!
{
AmpEnvPrecalcs[i] = ((input&AMPENV_INPUT_RIGHTCHANNEL)?AMPENV_RESULT_RIGHTCHANNEL:AMPENV_RESULT_LEFTCHANNEL)|AMPENV_RESULT_NEGATIVE; //Noise at max volume, negative!
}
else //Tone low? Then noise(output) is high only every other PWM period!
{
AmpEnvPrecalcs[i] = ((input&AMPENV_INPUT_RIGHTCHANNEL)?AMPENV_RESULT_RIGHTCHANNEL:AMPENV_RESULT_LEFTCHANNEL)|((input&AMPENV_INPUT_PWMPERIOD)?AMPENV_RESULT_POSITIVE:AMPENV_RESULT_NEGATIVE); //Noise at half volume!
}
break;
}
}
}

Is this handled correctly? The AMPENV_INPUT_* defines are the states as received from the renderer. The AMPENV_RESULT_* give the state to put out on PWM.

Edit: The bits in input are the direct inputs from the different signals used to render.
The AMPENV_INPUT_RIGHTCHANNEL bit is set during right channel, cleared otherwise.
The AMPENV_INPUT_NOISEENABLE bit is set when the noise enable bit is set.
The AMPENV_INPUT_FREQUENCYENABLE bit is set when the frequency enable bit is set.
The AMPENV_INPUT_SQUAREWAVEOUTPUT bit switches state every half period of a wave.
The AMPENV_INPUT_NOISEOUTPUT is the result of the RNG(which toggles on and off as per the RNG rate).
The AMPENV_INPUT_PWMPERIOD is a bit that's toggled every other PWM sample(so it toggles after every PWM 16 samples).

The different bits that are in the value that's stored in the precalcs are the output for the new 16-sample output(16 samples that together modulate the output signal using PWM). So changes during sample #1-15 don't have any effect. It's only reloaded on the first of the 16 samples(Sample #0). The combination of AMPENV_RESULT_SILENCE and the amplitude(0-15) determine how long it takes for the result to become AMPENV_RESULT_SILENCE, thus returning to 0V(From +UMax(Positive state) or -UMax(Negative state)).

The various results of the lookup table determine how the PWM wave starts out, as well as it's resetting to 0V:
- AMPENV_RESULT_RIGHTCHANNEL is set when it's output for the right channel of the chip.
- AMPENV_RESULT_POSITIVE is set when the sample is positive(starts at +UMax). Otherwise, it's starting out negative(-UMax).
- AMPENV_RESULT_SILENCE is set when the sample is 0V entirely. When this is set, the AMPENV_RESULT_POSITIVE bit value is ignored. This state is used when the output is a constant 0V.

When the AMPENV_RESULT_SILENCE isn't set, the AMPENV_RESULT_POSITIVE determines if the channel starts out positive or negative. After Amplitude(0-15) samples, this state resets to become AMPENV_RESULT_SILENCE, essentially generating a positive or negative pulse of Amplitude samples long.

The new state for the PWM sample is reloaded every 16th sample(the values from the AmpEnv lookup in between are ignored).
So:
Sample #0: Loads the new state from the current AmpEnv precalcs table(Which contains AMPENV_RESULT_* bits). When AMPENV_RESULT_SILENCE isn't set, the voltage starts out as +UMax or -UMax, depending on AMPENV_RESULT_POSITIVE. Otherwise, the voltage is immediately set to 0V.
Sample #1-(Amplitude-1): The state is unmodfied by default.
Sample #Amplitude: The state is changed to AMPENV_RESULT_SILENCE. This has the result of clearing the output voltage to 0V.
Sample #15: Toggles(using XOR) the AMPENV_INPUT_PWMPERIOD bit that's given to the PWM generator, causing the PWM samples to get their even/odd PWM inputs for proper half-volume PWM generation(as in your post).

This way, an PWM wave is generated, which(after low-pass filtering) should result in the correct PWM wave.

Can anyone see if this is done correctly?

Edit: Switching to non-PWM generated output(half-Dosbox-style, essentially Dosbox-style, except for the way noise output is handled(even/odd PWM outputs)) results in the same incorrect outputs(although a bit more clean-sounding than the PWM method). So the PWM generation and filtering seems to do the correct job. So that means that either the tone or noise generators have problems? Since the tones on the left channel sound fine, the problem is probably with the RNG generation making some kind of strange errors? Either that, or the final case of the AmpEnv inputs(tone+noise) is having problems here? (The Noise+Frequency case)

Can you see if it has some kind of error, Jepael? The full code is here

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 36 of 61, by superfury

User metadata
Rank l33t++
Rank
l33t++
Jepael wrote:
The tone+noise mixed together on same channel is not very easy to explain. PWM of amplitude modulator is emulated as pure ampli […]
Show full quote
Lord Nightmare wrote:
Jepael: I updated the saa1099 core in MAME to use the correct lfsr polynomial: https://github.com/mamedev/mame/commit/c11c22 … e […]
Show full quote

Jepael: I updated the saa1099 core in MAME to use the correct lfsr polynomial: https://github.com/mamedev/mame/commit/c11c22 … e3be48ba31786f8
I also added a note about the fact that if the noise channels are set to be clocked by a tone channel, and the 0x1c bit 1 reset bit is set, they get clocked by the undivided octave clocks, but I did not implement this yet.
Are there any other obvious issues I should try to fix from elsewhere in this thread?

LN

The tone+noise mixed together on same channel is not very easy to explain. PWM of amplitude modulator is emulated as pure amplitude level.

Tone off, noise off : output value idles high. Easy, it's zero.

Tone on, noise off : output value toggles at tone frequency, between idle high, and active low. Low period is modulated by 16-step PWM amplitude (max vol 15 means it's 15 PWM times low and 1 PWM time high. min vol 0 means silence, output value idles high). Easy, it's tone at max volume.

Tone off, noise on: output value comes from noise bit, between idle high, and active low. Low period is modulated by 16-step PWM amplitude. Easy, it's noise at max volume.
As described in my previous post, I am positive than noise generator output bit 1 is the idle high, and bit 0 is the active low.

Tone on, noise on:
-when tone value is at one state (high?), output idles high. The noise has no effect here. (So it is not a sum of tone+noise).
-when tone value is at the other state (low?), and noise is low, the signal is low at full amplitude set by PWM.
-when tone value is at the other state (low?), and noise is high, the signal is high every other PWM period, and low every other period at amplitude set by PWM. So this averages the signal to be low at only half the amplitude.

Maybe these two recordings will help:
http://www.mediafire.com/file/mk0ccd3e1oyqa44 … oth_manual.flac
http://www.mediafire.com/file/ck45apx2faimzuw … _both_auto.flac

Both recordings have the following sequence: tone burst, noise burst, tone/both/noise, the other one has also noise/both/tone sequence at the end.

I'm a bit confused about the Tone on, noise on case. So, there's actually three inputs to the PWM: tone input(Square wave input) which you call tone value, noise input and PWM period.
- When the tone value is high(1), output is simply high(1).
- When the tone value is low(0) and noise is low(0), output is simply low(0).
- When the tone value is low(0) and noise is high(1), the signal is toggling high(1) and low(0) every PWM period?

With the signal toggling high/low every other PWM period, do you mean that the PWM output(so within the 16-stage samples that are high/low) is toggling on and off to obtain a 50% duty cycle? Or does it toggle every PWM 16-cycle sample(e.g. a bit changing state every 16 PWM(the modulating signal which together form the 16-step volume) samples) samples)?

So, if I set the volume to 16(assuming the tone value is low all the time and the noise value is low all the time), I would get sample periods of(with my current case(the second case)):

100%(volume 15)
0%(volume 15)
100%(volume 15)
0%(volume 15)
100%(volume 15)
0%(volume 15)

Or is it actually changing state within the PWM-modulated signal itself:

50%
50%
50%
50%
50%
50%

Although I would think it's the second case?

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 37 of 61, by superfury

User metadata
Rank l33t++
Rank
l33t++

I've just fixed the overflow caused by x6 multiplying, but it still results in the incorrect audio for some unknown reason?
https://bitbucket.org/superfury/unipcemu/src/ … ter.c?at=master

Can anyone see what's going wrong here?

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io

Reply 38 of 61, by Jepael

User metadata
Rank Oldbie
Rank
Oldbie

Actually I am still in the process of documenting how the chip works internally and it's not complete yet (I don't find as much time for it even if I wanted to). Therefore, I can't really say yes or no whether you are emulating it right, and most questions you have are not about the emulation itself but about random bugs in code why it does not work, and it's pretty time consuming to figure out just from code what it is supposed to do versus what it actually does. Perhaps in this thread we should talk about how the chip works or how it should be emulated here, instead of what's wrong in your implementation?

As to your noise+tone question; I am not 100% sure right now but I think goes like this: when tone bit is high, output is continuous 16-unit idle slots as noise has no effect. When tone bit is low, the amplitude alternates between 16-unit slot for tone, 16-unit slot for noise, etc, so if both tone and noise are low, you just get continuous 16-unit slots of amplitude, if tone is low and noise is high, you get 16-unit amplitude slot for tone, 16-unit idle slot for noise.

But anyway, I did try to look your code through, and would like to comment about chip emulation aspects in general.

First about the chip output. When no sound is generated, the chip output sinks no current, so the output floats at 5V set by an external resistor. For each of the six channels, there is one current sink, that either is not active and sinks no current and does not change output voltage, or it's active and sinks current thus pulling voltage down. Thus a square wave on one channel will either enable or disable the current sink, so the output toggles between two voltage levels. When expanded to six channels playing different tones, it's clear that at any moment there could be any number between 0 and 6 current sinks enabled, so for any signal out from the chip, it has only 7 analog levels.

Then about the square waves. A square wave is a binary signal, so it has only two states: 1 and 0, high and low, +1 and -1, or active and inactive. This is what is fed to the output current sink, so the output voltage toggles between two voltages when tone is enabled. But when you have a control to enable or disable the square wave generation (by means of tone enable bit or setting amplitude), it still has only two states but stays idle in one of the states when not running. So if your square wave has a peak amplitude of X and toggles between +X and -X (it's peak-to-peak amplitude being 2X), there is no third state where its amplitude is zero even if you disable it, it must be either +X or -X meaning DC offset. Every time the signal changes state, it must jump the amount of 2X in amplitude, whether the jump was due to enabling/disabling a channel or just the square wave itself toggling. Also the idle state of square wave is when the current sink is inactive, so the signal will normally float high, and the first edge on a channel is negative-going edge when the current sink gets enabled and last edge when disabling a tone is a high-going edge.

Then about the volume. The actual chip does not implement volume in PWM like most people understand PWM, it's more like PDM. It is true that a single period of chopped volume control is 16 units long, but the "active" portion of volume X is not packed to first X samples, it's somewhat cleverly distributed throughout the 16 units. So, for instance, volume of 8 (50%) is not 8 units active + 8 units inactive, but rather 4H+4L+4H+4L, and volume of 7 is not 7 units active + 9 inactive, but 1H+3L+4H+4L+4H. I believe the distribution of highs and lows are carefully selected for at least two reasons. It has more random and higher frequency content than just PWM modulating a carrier there so it is easier to filter out the pathological cases, and it appears that if you manage to turn on two square waves at identical phase, if the sum of their amplitudes is 15, it would match the waveform of single square wave with volume of 15.

I know, I should really be gathering and posting my findings.

Reply 39 of 61, by superfury

User metadata
Rank l33t++
Rank
l33t++

OK. So PDM instead of PCM. Do you have a table of outputted values(16 volume values for each positive/negative PDM sample) that I can implement, as recorded from the hardware? Does it read those 16(volumes)x2(positive/negative)x16(PDM subsample) from some kind of ROM?

Author of the UniPCemu emulator.
UniPCemu Git repository
UniPCemu for Android, Windows, PSP, Vita and Switch on itch.io